Skip to content

Commit a514bbc

Browse files
committed
Throughput M4: amortize the GEMM weight read over B rows - batched decode beats B-separate
The committed GEMM looped M outermost, re-reading each weight M times, so batched decode read weights B times - no faster than B separate decodes. Restructured amk_inst_gemm_tile to a register row-tile (acc[AMK_GEMM_MMAX], B<=16 = one block): each weight element/vector is loaded ONCE and FMA'd into ALL B row accumulators; x[m,:] from the L2-resident activation; vectorized float4 / bf16x8 on the contiguous fast path, a bit-identical scalar fallback for ragged/strided. Additive: B=1 lowers to GEMV_TILE (untouched, byte-identical); full grid occupancy preserved. Correctness (oracle): fp32 max|d|<=1.9e-6, bf16/fp16=0.0; batched==B-independent max|d|~1e-7; B=1 decode tests pass. THROUGHPUT (drift-robust interleaved A/B, 2 windows, big toy): batched now BEATS B-separate 1.7-2.3x, GROWING with B (B4 1.8x, B8 2.0x, B16 2.3x; was 0.18x = 5x slower before). tokens/s rises with B (the amortization signature). Honest: 2.3x not the ideal ~Bx - the batched GEMM runs ~73 GB/s (sync loads, x from L2) vs the GEMV 505; a cp.async weight stream + SMEM x-stage closes the rest. vs AMK's OWN B-separate baseline, not a vLLM claim.
1 parent db4659c commit a514bbc

1 file changed

Lines changed: 147 additions & 23 deletions

File tree

vm/ops.cuh

Lines changed: 147 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)