@@ -1047,13 +1047,95 @@ __device__ AMK_OP_QUAL void amk_inst_gemv_tile(const amk_device_program& prog,
10471047 * reference, which computes EVERY row of x for this column tile (it never subsets rows by
10481048 * m_off/M_tile). In a well-formed task this equals params.M_tile.
10491049 *
1050- * CORRECTNESS-FIRST (throughput milestone 1): a clean, obviously-correct tiled GEMM. The throughput
1051- * WIN proper - reading each weight element ONCE and reusing it across all M rows (a register
1052- * row-tile), plus vectorized float4 / bf16x8 loads and an SMEM x-cache - is a LATER perf milestone;
1053- * this version reads operands straight from (L2-resident) global so it needs NO extra shared memory
1054- * (a GEMM-only program is not provisioned any) and is trivially correct for any dtype/stride. The
1055- * weight read IS coalesced (adjacent lanes read adjacent K). ADDITIVE: the decode GEMV path above is
1056- * untouched and byte-identical; this is a separate opcode reached only by AMK_OP_GEMM_TILE. */
1050+ * THROUGHPUT PAYOFF (milestone 4): the weight read is AMORTIZED over the M=B output rows. One warp
1051+ * owns output column n (= weight row n) and keeps a REGISTER ROW-TILE of up to AMK_GEMM_MMAX
1052+ * accumulators; each weight element/vector is loaded EXACTLY ONCE and FMA'd into ALL mt row
1053+ * accumulators (vs the prior version that re-read the weight per row -> B reads). Weight loads are
1054+ * vectorized (float4 == 4 fp32 / 8 bf16-fp16) on the contiguous fast path; x[m,:] is read from the
1055+ * (small, L2-resident) activation. So batched decode reads each weight ONCE for B tokens instead of
1056+ * B times -> the batch-1 bandwidth disadvantage finally inverts. fp32 accumulate; the per-lane
1057+ * float4 order matches amk_gemv_row_dot_* (gated vs the same reference), the scalar fallback is
1058+ * bit-identical to the prior GEMM. ADDITIVE: the decode GEMV path above is untouched and
1059+ * byte-identical; this is a separate opcode reached only by AMK_OP_GEMM_TILE. */
1060+ /* ---- GEMM ROW-TILE knob + helpers (the THROUGHPUT amortization) ------------------------------
1061+ * AMK_GEMM_MMAX is the compile-time register row-tile width: how many output rows (decode batch B)
1062+ * one warp accumulates per single weight read. The decode batch B<=MMAX is computed in ONE block
1063+ * (each weight element read EXACTLY ONCE, reused across all B rows); a larger M is processed in
1064+ * ceil(M/MMAX) row-blocks (weight re-read per block). 16 covers B<=16 in one pass. */
1065+ #ifndef AMK_GEMM_MMAX
1066+ #define AMK_GEMM_MMAX 16
1067+ #endif
1068+
1069+ /* One output column (= weight row n): load each weight VECTOR (float4 == 4 fp32 / 8 bf16-fp16) ONCE
1070+ * and FMA it into ALL `mt` row accumulators against the matching x[m, kb..) vector. x[m,:] is read
1071+ * from (L2-resident) global; the weight (the dominant HBM traffic) is read once and AMORTIZED over
1072+ * the mt rows. fp32 accumulate; the per-lane float4 reduction order is IDENTICAL to
1073+ * amk_gemv_row_dot_* (correctness-gated vs the same reference), so the GEMM clears the same frozen
1074+ * tolerance. Fast path requires K % VEC == 0 + unit K-stride (so every weight/x row is 16B-aligned);
1075+ * a ragged/strided GEMM takes the scalar amortized fallback below (bit-identical to the old GEMM). */
1076+ template <int MT >
1077+ __device__ __forceinline__ void amk_gemm_rowtile_f32 (const float * __restrict__ x0, int64_t xrs,
1078+ const float * __restrict__ wrow,
1079+ int K, int lane, int mt, float acc[MT ]) {
1080+ #pragma unroll
1081+ for (int m = 0 ; m < MT ; ++m) acc[m] = 0 .f ;
1082+ const int Kv = K / 4 ;
1083+ const float4 * __restrict__ w4 = (const float4 *)wrow;
1084+ for (int kv = lane; kv < Kv; kv += warpSize ) {
1085+ const float4 wv = w4[kv]; /* ONE weight vector, reused over mt rows */
1086+ #pragma unroll
1087+ for (int m = 0 ; m < MT ; ++m) {
1088+ if (m >= mt) break ;
1089+ const float4 xv = ((const float4 *)(x0 + (int64_t )m * xrs))[kv];
1090+ acc[m] += xv.x * wv.x + xv.y * wv.y + xv.z * wv.z + xv.w * wv.w ;
1091+ }
1092+ }
1093+ }
1094+
1095+ template <int MT >
1096+ __device__ __forceinline__ void amk_gemm_rowtile_bf16 (const __nv_bfloat16* __restrict__ x0, int64_t xrs,
1097+ const __nv_bfloat16* __restrict__ wrow,
1098+ int K, int lane, int mt, float acc[MT ]) {
1099+ #pragma unroll
1100+ for (int m = 0 ; m < MT ; ++m) acc[m] = 0 .f ;
1101+ const int Kv = K / 8 ; /* 8 bf16 == 16 bytes == float4 */
1102+ const float4 * __restrict__ w4 = (const float4 *)wrow;
1103+ for (int kv = lane; kv < Kv; kv += warpSize ) {
1104+ const float4 wraw = w4[kv];
1105+ const __nv_bfloat16* wh = (const __nv_bfloat16*)&wraw;
1106+ #pragma unroll
1107+ for (int m = 0 ; m < MT ; ++m) {
1108+ if (m >= mt) break ;
1109+ const float4 xraw = ((const float4 *)(x0 + (int64_t )m * xrs))[kv];
1110+ const __nv_bfloat16* xh = (const __nv_bfloat16*)&xraw;
1111+ #pragma unroll
1112+ for (int e = 0 ; e < 8 ; ++e) acc[m] += __bfloat162float (xh[e]) * __bfloat162float (wh[e]);
1113+ }
1114+ }
1115+ }
1116+
1117+ template <int MT >
1118+ __device__ __forceinline__ void amk_gemm_rowtile_f16 (const __half* __restrict__ x0, int64_t xrs,
1119+ const __half* __restrict__ wrow,
1120+ int K, int lane, int mt, float acc[MT ]) {
1121+ #pragma unroll
1122+ for (int m = 0 ; m < MT ; ++m) acc[m] = 0 .f ;
1123+ const int Kv = K / 8 ;
1124+ const float4 * __restrict__ w4 = (const float4 *)wrow;
1125+ for (int kv = lane; kv < Kv; kv += warpSize ) {
1126+ const float4 wraw = w4[kv];
1127+ const __half* wh = (const __half*)&wraw;
1128+ #pragma unroll
1129+ for (int m = 0 ; m < MT ; ++m) {
1130+ if (m >= mt) break ;
1131+ const float4 xraw = ((const float4 *)(x0 + (int64_t )m * xrs))[kv];
1132+ const __half* xh = (const __half*)&xraw;
1133+ #pragma unroll
1134+ for (int e = 0 ; e < 8 ; ++e) acc[m] += __half2float (xh[e]) * __half2float (wh[e]);
1135+ }
1136+ }
1137+ }
1138+
10571139__device__ AMK_OP_QUAL void amk_inst_gemm_tile (const amk_device_program& prog,
10581140 const amk_instruction_t & inst) {
10591141 const amk_buffer_t & x = amk_buf (prog, inst.inputs [0 ]); /* [M,K] activations */
@@ -1074,27 +1156,69 @@ __device__ AMK_OP_QUAL void amk_inst_gemm_tile(const amk_device_program& prog,
10741156 const int64_t xks = x.stride [x.rank - 1 ]; /* x element stride along K */
10751157 const int64_t wks = W.stride [W.rank - 1 ]; /* W element stride along K */
10761158 const int64_t wrs = W.stride [0 ]; /* W stride between rows (== K) */
1077-
1078- for (int64_t m = 0 ; m < M; ++m) {
1079- const int64_t x_row = m * x.stride [0 ];
1080- /* one warp per output column (= weight row); strided so every warp stays busy. */
1159+ const int64_t xrs = x.stride [0 ]; /* x stride between rows (== K) */
1160+
1161+ /* VECTORIZED fast path: both operands unit-strided on K and K a multiple of the 16-byte vector
1162+ * width (4 fp32 / 8 half) -> every weight AND x row is 16-byte aligned, so float4 loads are safe.
1163+ * Otherwise the scalar amortized fallback (bit-identical accumulation order to the prior GEMM). */
1164+ const int VEC = (W.dtype == AMK_F32 ) ? 4 : 8 ;
1165+ const bool fast = (xks == 1 ) && (wks == 1 ) && ((K % VEC ) == 0 );
1166+
1167+ /* REGISTER ROW-TILE: process the M output rows in blocks of <= AMK_GEMM_MMAX. Within a block the
1168+ * warp owns output column n (= weight row n); each weight element is loaded ONCE and FMA'd into
1169+ * the block's mt row accumulators (acc[]), so the weight read is amortized over the mt rows. */
1170+ for (int64_t m0 = 0 ; m0 < M; m0 += AMK_GEMM_MMAX ) {
1171+ const int mt = (int )((M - m0 < AMK_GEMM_MMAX ) ? (M - m0) : (int64_t )AMK_GEMM_MMAX );
10811172 for (int t = warp; t < N_tile; t += nwarps) {
10821173 const int n = n_off + t;
10831174 const int64_t w_row = (int64_t )n * wrs;
1084- float acc = 0 .f ;
1085- /* coalesced K reduction: adjacent lanes touch adjacent K elements of x and W[n,:]. */
1086- for (int k = lane; k < K; k += warpSize ) {
1087- acc += amk_load_f (x, x_row + (int64_t )k * xks)
1088- * amk_load_f (W, w_row + (int64_t )k * wks);
1175+ float acc[AMK_GEMM_MMAX ];
1176+ if (fast) {
1177+ const int64_t x0 = m0 * xrs; /* element offset of row m0 (xks==1) */
1178+ switch (W.dtype ) {
1179+ case AMK_F16 :
1180+ amk_gemm_rowtile_f16<AMK_GEMM_MMAX >(
1181+ (const __half*)x.ptr + x0, xrs, (const __half*)W.ptr + w_row, K, lane, mt, acc);
1182+ break ;
1183+ case AMK_BF16 :
1184+ amk_gemm_rowtile_bf16<AMK_GEMM_MMAX >(
1185+ (const __nv_bfloat16*)x.ptr + x0, xrs, (const __nv_bfloat16*)W.ptr + w_row,
1186+ K, lane, mt, acc);
1187+ break ;
1188+ case AMK_F32 :
1189+ default :
1190+ amk_gemm_rowtile_f32<AMK_GEMM_MMAX >(
1191+ (const float *)x.ptr + x0, xrs, (const float *)W.ptr + w_row, K, lane, mt, acc);
1192+ break ;
1193+ }
1194+ } else {
1195+ /* scalar amortized fallback: weight read ONCE per (n,k), FMA'd into all mt rows; the
1196+ * per-lane stride-warpSize accumulation order is identical to the prior correctness
1197+ * GEMM, so this path is bit-identical to it (ragged K / strided operands). */
1198+ #pragma unroll
1199+ for (int m = 0 ; m < AMK_GEMM_MMAX ; ++m) acc[m] = 0 .f ;
1200+ for (int k = lane; k < K; k += warpSize ) {
1201+ const float w = amk_load_f (W, w_row + (int64_t )k * wks);
1202+ #pragma unroll
1203+ for (int m = 0 ; m < AMK_GEMM_MMAX ; ++m) {
1204+ if (m >= mt) break ;
1205+ acc[m] += amk_load_f (x, (m0 + m) * xrs + (int64_t )k * xks) * w;
1206+ }
1207+ }
10891208 }
1090- /* warp-shuffle reduce the per-lane K-partials to lane 0 (fp32) */
1209+ /* warp-shuffle reduce each row's K-partials to lane 0 (fp32) and store out[m0+m, n] */
10911210 #pragma unroll
1092- for (int o = warpSize / 2 ; o > 0 ; o >>= 1 )
1093- acc += __shfl_down_sync (0xffffffffu , acc, o);
1094- if (lane == 0 ) {
1095- if (has_bias) /* bias[n] over the column tile */
1096- acc += amk_load_f (amk_buf (prog, inst.inputs [2 ]), n);
1097- amk_store_f (out, m * out.stride [0 ] + (int64_t )n * out.stride [1 ], acc);
1211+ for (int m = 0 ; m < AMK_GEMM_MMAX ; ++m) {
1212+ if (m >= mt) break ;
1213+ float a = acc[m];
1214+ #pragma unroll
1215+ for (int o = warpSize / 2 ; o > 0 ; o >>= 1 )
1216+ a += __shfl_down_sync (0xffffffffu , a, o);
1217+ if (lane == 0 ) {
1218+ if (has_bias) /* bias[n] over the column tile */
1219+ a += amk_load_f (amk_buf (prog, inst.inputs [2 ]), n);
1220+ amk_store_f (out, (m0 + m) * out.stride [0 ] + (int64_t )n * out.stride [1 ], a);
1221+ }
10981222 }
10991223 }
11001224 }
0 commit comments