Skip to content

Commit ec7c5c4

Browse files
committed
perf improvements
1 parent 0dc2c66 commit ec7c5c4

1 file changed

Lines changed: 180 additions & 88 deletions

File tree

backends/mlx/custom_kernel_ops/gguf_linear.py

Lines changed: 180 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,14 @@ def gguf_linear_fake(
226226
IfNode,
227227
IntOrVid,
228228
MetalKernelNode,
229+
MultiplyIntNode,
229230
SubtractIntNode,
230231
)
231232

232233

233234
# Shared Metal header: the GGUF block_q6_K struct (matches llama.cpp
234-
# ggml-common.h; sizeof == 210, no padding since max align is 2) plus a
235-
# per-element dequant helper used by the mat-mat kernel.
235+
# ggml-common.h; sizeof == 210, no padding since max align is 2) plus
236+
# dequant helpers for both per-element (embedding) and vectorized (matmul).
236237
_Q6K_HEADER = """
237238
#include <metal_simdgroup>
238239
#include <metal_simdgroup_matrix>
@@ -248,7 +249,7 @@ def gguf_linear_fake(
248249
} block_q6_K;
249250
250251
// Dequantize a single element at within-block position p (0..255) of a
251-
// block_q6_K. Matches the canonical ggml dequantize_row_q6_K layout.
252+
// block_q6_K. Used by the embedding kernel.
252253
inline float dequant_q6k_elem(device const block_q6_K * blk, int p) {
253254
const int h = p >> 7; // which 128-element half (0/1)
254255
const int pp = p & 127; // position within half (0..127)
@@ -267,6 +268,43 @@ def gguf_linear_fake(
267268
const float scale = (float) sc[is + 2 * g];
268269
return (float) blk->d * scale * (float)(q - 32);
269270
}
271+
272+
// Vectorized Q6_K dequantize: decodes 16 values per call into a 4x4 half
273+
// register. Ported from llama.cpp dequantize_q6_K. `il` ranges 0..15 and
274+
// selects which 16-element slice of the 256-element block to decode.
275+
inline void dequantize_q6_K_16(device const block_q6_K * xb, short il,
276+
thread half4x4 & reg) {
277+
const half d_all = xb->d;
278+
device const uint16_t * ql = (device const uint16_t *)xb->ql;
279+
device const uint16_t * qh = (device const uint16_t *)xb->qh;
280+
device const int8_t * scales = (device const int8_t *)xb->scales;
281+
282+
ql = ql + 32*(il/8) + 16*((il/2)&1) + 8*(il&1);
283+
qh = qh + 16*(il/8) + 8*(il&1);
284+
float sc = scales[(il%2) + 2 * ((il/2))];
285+
il = (il/2) & 3;
286+
287+
const uint32_t kmask1 = il>1 ? (il>2 ? 0xC0C0C0C0 : 0x30303030) : (il>0 ? 0x0C0C0C0C : 0x03030303);
288+
const uint32_t kmask2 = il>1 ? 0xF0F0F0F0 : 0x0F0F0F0F;
289+
const float coeff = d_all * sc;
290+
const float ml = coeff * 32.f;
291+
const float dl0 = coeff;
292+
const float dl1 = dl0 / 256.f;
293+
const float dl2 = dl0 / (256.f * 256.f);
294+
const float dl3 = dl0 / (256.f * 256.f * 256.f);
295+
const uint8_t shr_h = il>2 ? 2 : 0;
296+
const uint8_t shl_h = il>1 ? 0 : (il>0 ? 2 : 4);
297+
const uint8_t shr_l = il>1 ? 4 : 0;
298+
for (int i = 0; i < 4; ++i) {
299+
const uint32_t low = (ql[2*i] | (uint32_t)(ql[2*i+1] << 16)) & kmask2;
300+
const uint32_t high = (qh[2*i] | (uint32_t)(qh[2*i+1] << 16)) & kmask1;
301+
const uint32_t q = ((high << shl_h) >> shr_h) | (low >> shr_l);
302+
reg[i][0] = (half)(dl0 * ((half)(q & 0xFF)) - ml);
303+
reg[i][1] = (half)(dl1 * ((float)(q & 0xFF00)) - ml);
304+
reg[i][2] = (half)(dl2 * ((float)(q & 0xFF0000)) - ml);
305+
reg[i][3] = (half)(dl3 * ((float)(q & 0xFF000000)) - ml);
306+
}
307+
}
270308
"""
271309

272310

@@ -344,114 +382,157 @@ def _q6k_matvec_source(has_bias: bool) -> str:
344382
"""
345383

346384

347-
# Prefill mat-mat kernel (32x32x32 tiles, 4 simdgroups / 128 threads).
348-
# Each threadgroup computes a BM x BN output tile; weights are dequantized
349-
# on the fly into threadgroup memory and reused across the BM activation rows.
385+
# Prefill mat-mat kernel, ported from llama.cpp kernel_mul_mm (Q6_K variant).
386+
# 64x32 output tiles, 4 simdgroups / 128 threads per threadgroup.
387+
# Uses vectorized dequantize_q6_K_16 to decode 16 weight values per thread
388+
# into threadgroup memory, then runs simdgroup_multiply_accumulate on 8x8
389+
# tiles. NL=16 for Q6_K (QK_K / 16 = 16 dequant steps per super-block).
350390
# C[m, n] = sum_k x[m, k] * dequant(weight)[n, k] (+ bias[n]).
351391
def _q6k_matmul_source(has_bias: bool) -> str:
352-
bias_line = " v += (float) bias[gn];\n" if has_bias else ""
392+
bias_add = "+ (float) bias[r0 + i]" if has_bias else ""
353393
return f"""
354-
constexpr short BM = 32; // activation rows per tile (M)
355-
constexpr short BN = 32; // output features per tile (N)
356-
constexpr short BK = 32; // K-chunk per iteration
357-
358-
threadgroup half As[BM * BK];
359-
threadgroup half Bs[BN * BK];
360-
threadgroup float Cs[BM * BN];
361-
362-
const ushort tid = thread_index_in_threadgroup; // 0..127
363-
const ushort sgitg = simdgroup_index_in_threadgroup; // 0..3
364-
const short sg_row = sgitg / 2; // 0/1
365-
const short sg_col = sgitg % 2; // 0/1
366-
367-
const uint tile_n_idx = thread_position_in_grid.x / 128u;
368-
const uint tile_m_idx = thread_position_in_grid.y;
369-
const int tile_m0 = (int)tile_m_idx * BM;
370-
const int tile_n0 = (int)tile_n_idx * BN;
371-
372-
// M (number of activation rows) read at runtime from the injected x_shape,
373-
// so this kernel works for both static and symbolic seqlen.
394+
constexpr short NR0 = 64; // weight/output rows per tile (N dim)
395+
constexpr short NR1 = 32; // activation rows per tile (M dim)
396+
constexpr short NK = 32; // K-chunk per iteration
397+
constexpr short NL = 16; // Q6_K: QK_K / 16
398+
constexpr short NL0 = NK / 16; // = 2 — dequant iterations per thread for weight
399+
constexpr short NL1 = NK / 8; // = 4 — load iterations per thread for activation
400+
401+
threadgroup half sa[4096]; // NR0 * NK storage (strided by 64)
402+
threadgroup half sb[4096]; // NR1 * NK storage (strided by 64)
403+
404+
const ushort tid = thread_index_in_threadgroup; // 0..127
405+
const ushort sgitg = simdgroup_index_in_threadgroup; // 0..3
406+
407+
const uint r0 = thread_position_in_grid.y * NR0; // first weight row
408+
const uint r1 = (thread_position_in_grid.x / 128u) * NR1; // first activation row
409+
410+
// M (number of activation rows) read at runtime.
374411
int M = 1;
375412
for (uint d = 0; d + 1 < x_ndim; ++d) {{ M *= (int) x_shape[d]; }}
376413
377414
const int nb = K / QK_K;
378-
device const block_q6_K * wrows = (device const block_q6_K *) weight;
379415
380-
simdgroup_float8x8 mc[2][2];
381-
for (short a = 0; a < 2; ++a) {{
382-
for (short b = 0; b < 2; ++b) {{
383-
mc[a][b] = make_filled_simdgroup_matrix<float, 8>(0.f);
384-
}}
416+
// Clamp tile edges.
417+
const short nr0 = (N - (int)r0 < NR0) ? (N - (int)r0) : NR0;
418+
const short nr1 = (M - (int)r1 < NR1) ? (M - (int)r1) : NR1;
419+
420+
// Thread → element mapping for cooperative loads.
421+
const short lr0 = ((short)(tid / NL0) < nr0) ? (short)(tid / NL0) : (nr0 - 1); // 0..63
422+
const short lr1 = ((short)(tid / NL1) < nr1) ? (short)(tid / NL1) : (nr1 - 1); // 0..31
423+
424+
short il0 = tid % NL0;
425+
short il = il0; // current dequant sub-block index within Q6_K block
426+
427+
const short offset1 = il0 / NL; // always 0 for NL=16, NL0=2
428+
429+
// Pointer to weight block for this thread's assigned row.
430+
device const block_q6_K * wblk = (device const block_q6_K *) weight
431+
+ (uint)(r0 + lr0) * nb + offset1;
432+
433+
// Pointer to activation row for this thread.
434+
const short iy = 8 * (tid % NL1);
435+
device const InT * yp = x + (uint)(r1 + lr1) * (uint)K + iy;
436+
437+
// Accumulator: 8 simdgroup 8x8 matrices (4 sgitg configs x 2 sub-tiles).
438+
simdgroup_half8x8 ma[4];
439+
simdgroup_half8x8 mb[2];
440+
simdgroup_float8x8 mc[8];
441+
for (short i = 0; i < 8; ++i) {{
442+
mc[i] = make_filled_simdgroup_matrix<float, 8>(0.f);
385443
}}
386444
387-
for (int k0 = 0; k0 < K; k0 += BK) {{
388-
// Cooperative load: activation tile (BM x BK), row-major in As.
389-
for (short i = 0; i < (BM * BK) / 128; ++i) {{
390-
const short idx = tid + i * 128;
391-
const short mm = idx / BK;
392-
const short kk = idx % BK;
393-
const int gm = tile_m0 + mm;
394-
As[mm * BK + kk] = (gm < M) ? (half) x[(uint)gm * (uint)K + (k0 + kk)] : (half)0;
445+
for (int loop_k = 0; loop_k < K; loop_k += NK) {{
446+
// --- Cooperative load: dequantized weight tile (NR0 x NK) into sa ---
447+
half4x4 temp_a;
448+
dequantize_q6_K_16(wblk, il, temp_a);
449+
450+
threadgroup_barrier(mem_flags::mem_threadgroup);
451+
452+
for (short i = 0; i < 16; ++i) {{
453+
const short sx = 2 * il0 + i / 8;
454+
const short sy = (tid / NL0) / 8;
455+
const short lx = (tid / NL0) % 8;
456+
const short ly = i % 8;
457+
const short ib = 8 * sx + sy;
458+
*(sa + 64 * ib + 8 * ly + lx) = temp_a[i / 4][i % 4];
395459
}}
396-
// Cooperative load: dequantized weight tile (BN x BK), row-major in Bs.
397-
for (short i = 0; i < (BN * BK) / 128; ++i) {{
398-
const short idx = tid + i * 128;
399-
const short nn = idx / BK;
400-
const short kk = idx % BK;
401-
const int gn = tile_n0 + nn;
402-
const int gk = k0 + kk;
403-
half val = (half)0;
404-
if (gn < N) {{
405-
device const block_q6_K * blk = wrows + (uint)gn * nb + (gk / QK_K);
406-
val = (half) dequant_q6k_elem(blk, gk % QK_K);
407-
}}
408-
Bs[nn * BK + kk] = val;
460+
461+
// --- Cooperative load: activation tile (NR1 x NK) into sb ---
462+
const short sx_b = tid % NL1;
463+
const short sy_b = (tid / NL1) / 8;
464+
const short ly_b = (tid / NL1) % 8;
465+
const short ib_b = 4 * sx_b + sy_b;
466+
467+
for (short i = 0; i < 8; ++i) {{
468+
*(sb + 64 * ib_b + 8 * ly_b + i) = (half) *(yp + i);
409469
}}
470+
471+
// Advance weight pointer through Q6_K sub-blocks.
472+
il = (il + 2 < NL) ? il + 2 : il % 2;
473+
wblk = (il < 2) ? wblk + (2 + NL - 1) / NL : wblk;
474+
475+
yp += NK;
476+
410477
threadgroup_barrier(mem_flags::mem_threadgroup);
411478
412-
for (short kk = 0; kk < BK / 8; ++kk) {{
413-
simdgroup_half8x8 a[2], b[2];
414-
for (short sr = 0; sr < 2; ++sr) {{
415-
simdgroup_load(a[sr], As + (16 * sg_row + 8 * sr) * BK + 8 * kk, BK, ulong2(0, 0), false);
479+
// --- Simdgroup matmul on loaded tiles ---
480+
threadgroup const half * lsma = sa + 4 * 64 * (sgitg % 2);
481+
threadgroup const half * lsmb = sb + 2 * 64 * (sgitg / 2);
482+
483+
for (short ik = 0; ik < NK / 8; ++ik) {{
484+
simdgroup_barrier(mem_flags::mem_none);
485+
for (short i = 0; i < 4; ++i) {{
486+
simdgroup_load(ma[i], lsma + 64 * i, 8, ulong2(0, 0), false);
416487
}}
417-
for (short sc = 0; sc < 2; ++sc) {{
418-
// transpose=true yields b[k][n] = Bs[n][k] for C = A @ B^T.
419-
simdgroup_load(b[sc], Bs + (16 * sg_col + 8 * sc) * BK + 8 * kk, BK, ulong2(0, 0), true);
488+
simdgroup_barrier(mem_flags::mem_none);
489+
for (short i = 0; i < 2; ++i) {{
490+
simdgroup_load(mb[i], lsmb + 64 * i, 8, ulong2(0, 0), false);
420491
}}
421-
for (short sr = 0; sr < 2; ++sr) {{
422-
for (short sc = 0; sc < 2; ++sc) {{
423-
simdgroup_multiply_accumulate(mc[sr][sc], a[sr], b[sc], mc[sr][sc]);
424-
}}
492+
simdgroup_barrier(mem_flags::mem_none);
493+
for (short i = 0; i < 8; ++i) {{
494+
simdgroup_multiply_accumulate(mc[i], mb[i / 4], ma[i % 4], mc[i]);
425495
}}
496+
lsma += 8 * 64;
497+
lsmb += 4 * 64;
426498
}}
427-
threadgroup_barrier(mem_flags::mem_threadgroup);
428499
}}
429500
430-
for (short sr = 0; sr < 2; ++sr) {{
431-
for (short sc = 0; sc < 2; ++sc) {{
432-
simdgroup_store(mc[sr][sc], Cs + (16 * sg_row + 8 * sr) * BN + (16 * sg_col + 8 * sc), BN, ulong2(0, 0), false);
433-
}}
434-
}}
501+
// --- Write results: always via threadgroup memory for float→InT cast ---
502+
// Barrier needed: sa was used for weight tiles during the K-loop and is now
503+
// reused as float staging for the output. Without this barrier, a fast
504+
// simdgroup could start writing mc[] into sa while a slower one is still
505+
// reading the last weight tile via simdgroup_load(ma[]).
506+
// (Matches ggml-metal.metal:9546 in llama.cpp's bounds-checked write path.)
435507
threadgroup_barrier(mem_flags::mem_threadgroup);
508+
{{
509+
threadgroup float * temp_str = ((threadgroup float *) sa)
510+
+ 32 * (sgitg & 1) + (16 * (sgitg >> 1)) * NR0;
511+
for (short i = 0; i < 8; ++i) {{
512+
simdgroup_store(mc[i], temp_str + 8 * (i % 4) + 8 * NR0 * (i / 4),
513+
NR0, ulong2(0, 0), false);
514+
}}
515+
threadgroup_barrier(mem_flags::mem_threadgroup);
436516
437-
for (short i = 0; i < (BM * BN) / 128; ++i) {{
438-
const short idx = tid + i * 128;
439-
const short mm = idx / BN;
440-
const short nn = idx % BN;
441-
const int gm = tile_m0 + mm;
442-
const int gn = tile_n0 + nn;
443-
if (gm < M && gn < N) {{
444-
float v = Cs[mm * BN + nn];
445-
{bias_line} out[(uint)gm * (uint)N + gn] = (InT) v;
517+
if (sgitg == 0) {{
518+
for (int j = tid; j < nr1; j += NR1) {{
519+
device InT * D = out + (uint)(r1 + j) * (uint)N + r0;
520+
threadgroup float * Cp = ((threadgroup float *) sa) + j * NR0;
521+
for (int i = 0; i < nr0; ++i) {{
522+
float v = Cp[i];
523+
D[i] = (InT)(v {bias_add});
524+
}}
525+
}}
446526
}}
447527
}}
448528
"""
449529

450530

451531
# Number of simdgroups per threadgroup for the mat-vec kernel.
452532
_Q6K_MV_NSG = 4
453-
# Tile size for the mat-mat kernel (BM == BN); threadgroup handles a tile.
454-
_Q6K_MM_TILE = 32
533+
# Tile sizes for the mat-mat kernel (from llama.cpp kernel_mul_mm).
534+
_Q6K_MM_NR0 = 64 # weight/output rows (N dim) per threadgroup
535+
_Q6K_MM_NR1 = 32 # activation rows (M dim) per threadgroup
455536

456537

457538
def _emit_q6k_matvec(
@@ -529,9 +610,9 @@ def _emit_q6k_matmul(
529610
leading = emit_shape(P, x_node, x_slot, end_dim=-1)
530611
out_shape_flat = leading + [IntOrVid.from_literal(N)]
531612

532-
tile = _Q6K_MM_TILE
533-
blocks_n = (N + tile - 1) // tile
534-
grid_x = blocks_n * 128 # 128 threads (4 simdgroups) per threadgroup
613+
# grid.x = ceil(M / NR1) * 128 threads (activation tiles)
614+
# grid.y = ceil(N / NR0) (weight tiles)
615+
blocks_n = (N + _Q6K_MM_NR0 - 1) // _Q6K_MM_NR0
535616

536617
has_bias = bias_slot is not None
537618
inputs = [P.slot_to_tid(x_slot), P.slot_to_tid(weight_slot)]
@@ -540,6 +621,17 @@ def _emit_q6k_matmul(
540621
inputs.append(P.slot_to_tid(bias_slot))
541622
input_names.append("bias")
542623

624+
# blocks_m_iov = ceil(M / NR1); multiply by 128 for grid.x
625+
_, grid_x_slot = P.make_tmp_value_slot()
626+
P.emit(
627+
MultiplyIntNode(
628+
a=blocks_m_iov,
629+
b=IntOrVid.from_literal(128),
630+
out=P.slot_to_vid(grid_x_slot),
631+
)
632+
)
633+
grid_x_iov = IntOrVid.from_vid(P.slot_to_vid(grid_x_slot))
634+
543635
P.emit(
544636
MetalKernelNode(
545637
name="gguf_q6k_matmul",
@@ -548,8 +640,8 @@ def _emit_q6k_matmul(
548640
inputs=inputs,
549641
outputs=[P.slot_to_tid(out)],
550642
grid=[
551-
IntOrVid.from_literal(grid_x),
552-
blocks_m_iov,
643+
grid_x_iov,
644+
IntOrVid.from_literal(blocks_n),
553645
IntOrVid.from_literal(1),
554646
],
555647
threadgroup=[
@@ -623,7 +715,7 @@ def _gguf_linear_handler(P: MLXProgramBuilder, n: Node) -> Slot:
623715
break
624716

625717
out = P.make_or_get_slot(n)
626-
tile = _Q6K_MM_TILE
718+
tile = _Q6K_MM_NR1 # M-dimension tile (activation rows per threadgroup)
627719
if M == 1:
628720
# Static decode -> mat-vec.
629721
_emit_q6k_matvec(P, n, x_node, x_slot, weight_slot, bias_slot, N, K, out)

0 commit comments

Comments
 (0)