@@ -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 .
252253inline 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]).
351391def _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
457538def _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