Skip to content

Commit bb4dbed

Browse files
committed
perf(metal): x-tiled BF16 prefill matmul — 5.6x TTFT (was 4.1x)
The plain batched BF16 matmul was bound by strided token-major x reads (per 2-byte weight it streamed T×4 bytes of activation, ~16× the weight traffic). `matmul_bf16_tiled` stages the T token columns into threadgroup memory once per in_dim chunk (CHUNK=256), shared across all nsg=8 simdgroups in the threadgroup, so x global traffic drops ~8× and the matmul becomes weight-bound. All threads cooperate on the load + barriers; only o < out_dim writes. 508-token prompt (prefill-check --gpu), exact (max|Δlogit| = 0.000000): Qwen3-4B BF16 batched 4216 -> 3055 ms (4.07x -> 5.59x vs 17.1 s) forward.rs routes BF16 to the tiled kernel; the plain `matmul_bf16` remains for the ullm-metal unit test. Next: x-tile the k-quant kernels (same shared-x win) and tile weights too (full mul_mm).
1 parent 0188e84 commit bb4dbed

2 files changed

Lines changed: 54 additions & 3 deletions

File tree

crates/ullm-metal/src/forward.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ pub struct GpuForward {
179179
p_gelu_mul: ComputePipelineState,
180180
p_add: ComputePipelineState,
181181
p_matvec_mlx4: ComputePipelineState,
182-
p_matmul_bf16: ComputePipelineState,
182+
p_matmul_bf16_tiled: ComputePipelineState,
183183
p_matmul_mlx4: ComputePipelineState,
184184
p_matmul_q4k: ComputePipelineState,
185185
p_matmul_q6k: ComputePipelineState,
@@ -323,7 +323,7 @@ impl GpuForward {
323323
p_gelu_mul: pso("gelu_mul")?,
324324
p_add: pso("add_inplace")?,
325325
p_matvec_mlx4: pso("matvec_mlx4")?,
326-
p_matmul_bf16: pso("matmul_bf16")?,
326+
p_matmul_bf16_tiled: pso("matmul_bf16_tiled")?,
327327
p_matmul_mlx4: pso("matmul_mlx4")?,
328328
p_matmul_q4k: pso("matmul_q4k")?,
329329
p_matmul_q6k: pso("matmul_q6k")?,
@@ -700,7 +700,7 @@ impl GpuForward {
700700
}
701701
// BF16 and the k-quants share the (w, x, y, in, out, n_cols) signature.
702702
let pso = match w.dtype {
703-
DType::BF16 => &self.p_matmul_bf16,
703+
DType::BF16 => &self.p_matmul_bf16_tiled,
704704
DType::Q4K => &self.p_matmul_q4k,
705705
DType::Q6K => &self.p_matmul_q6k,
706706
_ => return false,

crates/ullm-metal/src/shader.metal

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,57 @@ kernel void matmul_bf16(
510510
}
511511
}
512512

513+
// Batched BF16 matmul, x-tiled. Same result as `matmul_bf16` but the T token
514+
// columns of the activation are staged into threadgroup memory ONCE per in_dim
515+
// chunk and shared by all `nsg` simdgroups (each computing a different output
516+
// row), so the strided token-major x reads — the bottleneck of the plain kernel
517+
// — drop ~nsg-fold and the matmul becomes weight-bound. All threads cooperate on
518+
// the load + barriers (no early-out), only o < out_dim writes.
519+
kernel void matmul_bf16_tiled(
520+
device const ushort* w [[buffer(0)]],
521+
device const float* x [[buffer(1)]],
522+
device float* y [[buffer(2)]],
523+
constant uint& in_dim [[buffer(3)]],
524+
constant uint& out_dim [[buffer(4)]],
525+
constant uint& n_cols [[buffer(5)]],
526+
uint2 tgpig [[threadgroup_position_in_grid]],
527+
ushort tiisg [[thread_index_in_simdgroup]],
528+
ushort sgitg [[simdgroup_index_in_threadgroup]],
529+
ushort nsg [[simdgroups_per_threadgroup]])
530+
{
531+
const uint T = 8;
532+
const uint CHUNK = 256;
533+
threadgroup float xt[T * CHUNK]; // T token columns x CHUNK in-dim
534+
uint o = (uint)tgpig.x * nsg + sgitg;
535+
uint col0 = (uint)tgpig.y * T;
536+
uint tid = (uint)sgitg * 32u + tiisg;
537+
uint nthreads = (uint)nsg * 32u;
538+
device const ushort* wrow = w + (uint)o * in_dim;
539+
float acc[T];
540+
for (uint t = 0; t < T; ++t) acc[t] = 0.f;
541+
542+
for (uint c0 = 0; c0 < in_dim; c0 += CHUNK) {
543+
threadgroup_barrier(mem_flags::mem_threadgroup); // prev chunk done reading xt
544+
for (uint idx = tid; idx < T * CHUNK; idx += nthreads) {
545+
uint t = idx / CHUNK, j = idx % CHUNK, i = c0 + j, s = col0 + t;
546+
xt[idx] = (i < in_dim && s < n_cols) ? x[s * in_dim + i] : 0.f;
547+
}
548+
threadgroup_barrier(mem_flags::mem_threadgroup); // xt filled
549+
if (o < out_dim) {
550+
uint jmax = min(CHUNK, in_dim - c0);
551+
for (uint j = tiisg; j < jmax; j += 32u) {
552+
float wv = as_type<float>((uint)wrow[c0 + j] << 16);
553+
for (uint t = 0; t < T; ++t) acc[t] += wv * xt[t * CHUNK + j];
554+
}
555+
}
556+
}
557+
for (uint t = 0; t < T; ++t) {
558+
float r = simd_sum(acc[t]);
559+
uint s = col0 + t;
560+
if (tiisg == 0 && o < out_dim && s < n_cols) y[s * out_dim + o] = r;
561+
}
562+
}
563+
513564
// Batched MLX 4-bit matmul (prompt prefill): packed-u32 weights dequantized in
514565
// the kernel (q*scale+bias, 8 nibbles/word LSB-first), W[out,in] x X[S,in] ->
515566
// Y[S,out]. One simdgroup per output row computes a tile of T columns, reading

0 commit comments

Comments
 (0)