mlx-bun 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +175 -93
  2. package/docs/dag/training-inference-map.html +575 -0
  3. package/docs/design/adapters-end-to-end.md +50 -46
  4. package/docs/design/compat-cli-surface-design.md +10 -8
  5. package/docs/design/docs-reorg-plan.md +14 -1
  6. package/docs/design/hlg-sampling.md +12 -2
  7. package/docs/design/memory-system.md +193 -295
  8. package/docs/design/optimization_plan.md +4 -2
  9. package/docs/design/orpo-dynamic-lambda.md +179 -0
  10. package/docs/design/orpo-future-enhancements.md +340 -0
  11. package/docs/design/orpo-training.md +1212 -0
  12. package/docs/design/parallel-slots.md +34 -26
  13. package/docs/design/parity-tier-dag.md +143 -0
  14. package/docs/design/runtime-isolation.md +1 -1
  15. package/docs/design/segmented-backward-training.md +58 -39
  16. package/docs/design/tauri-desktop-app.md +127 -0
  17. package/docs/design/w4a16-compute-precision-spike.md +232 -0
  18. package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
  19. package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
  20. package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
  21. package/docs/{segmented-backward-handoff.md → investigations/segmented-backward-handoff.md} +1 -1
  22. package/docs/investigations/starter-model-port-handoff.md +32 -13
  23. package/docs/investigations/steel-flash-cce-handoff.md +489 -0
  24. package/docs/investigations/trainer-validation-experiment.md +173 -0
  25. package/docs/planning/IDEAS.md +29 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +147 -27
  27. package/docs/planning/chunk-task-roadmap.md +1 -1
  28. package/docs/planning/journal.md +161 -0
  29. package/docs/reference/distribution.md +61 -5
  30. package/docs/reference/library-api.md +29 -14
  31. package/docs/reference/memory.md +77 -0
  32. package/docs/reference/orpo-quickstart.md +130 -0
  33. package/docs/reference/server-api.md +105 -7
  34. package/docs/reference/server-config.md +12 -5
  35. package/docs/reference/training.md +296 -0
  36. package/package.json +1 -1
  37. package/src/cli.ts +651 -18
  38. package/src/eval/ifeval.ts +138 -0
  39. package/src/eval/runner.ts +76 -6
  40. package/src/eval/tasks/bfcl.ts +49 -15
  41. package/src/eval/tasks/gsm8k.ts +21 -9
  42. package/src/eval/tasks/hashhop.ts +33 -4
  43. package/src/eval/tasks/humaneval.ts +30 -8
  44. package/src/eval/tasks/ifeval.ts +52 -18
  45. package/src/eval/tasks/mmlu.ts +85 -74
  46. package/src/eval/ultrafeedback.ts +60 -0
  47. package/src/jobs/runner.ts +6 -0
  48. package/src/lora.ts +12 -7
  49. package/src/memory/chunk.ts +25 -0
  50. package/src/memory/cluster.ts +24 -0
  51. package/src/memory/db.ts +114 -0
  52. package/src/memory/ingest.ts +22 -0
  53. package/src/memory/pipeline.ts +63 -0
  54. package/src/memory/prompts.ts +23 -0
  55. package/src/memory/schedule.ts +163 -0
  56. package/src/memory/synthesize.ts +29 -0
  57. package/src/memory/tools.ts +399 -0
  58. package/src/memory/vault.ts +694 -0
  59. package/src/memory/wikify.ts +23 -0
  60. package/src/mlx/metal-kernel.ts +31 -4
  61. package/src/model/flash-attention.ts +37 -19
  62. package/src/model/fused-geglu-kernel.ts +111 -0
  63. package/src/model/gemma4-base.ts +39 -1
  64. package/src/model/gemma4.ts +196 -21
  65. package/src/model/minicpm5.ts +44 -3
  66. package/src/pi-provider.ts +29 -13
  67. package/src/pi-session.ts +81 -0
  68. package/src/pi-terminal.ts +13 -24
  69. package/src/pi-web.ts +246 -166
  70. package/src/quantize/job.ts +17 -2
  71. package/src/quantize/quantizer.ts +26 -2
  72. package/src/server.ts +216 -47
  73. package/src/tokenizer.ts +6 -2
  74. package/src/train/dataset.ts +29 -6
  75. package/src/train/flash-cce.ts +1175 -0
  76. package/src/train/job.ts +64 -3
  77. package/src/train/lora-params.ts +53 -4
  78. package/src/train/loss.ts +947 -0
  79. package/src/train/optimizer.ts +10 -3
  80. package/src/train/prefix-shared.ts +435 -0
  81. package/src/train/rank.ts +36 -0
  82. package/src/train/segmented.ts +1148 -3
  83. package/src/train/steel-qmm-header.ts +1461 -0
  84. package/src/train/trainer.ts +570 -54
  85. package/src/train/watch.ts +542 -0
  86. package/src/web/app.html +285 -19
  87. package/src/web/skills/memory/SKILL.md +97 -0
  88. package/src/web/skills/web-research/SKILL.md +3 -1
  89. package/src/web/skills.ts +46 -8
  90. /package/{docs → src/assets}/curve-designer.html +0 -0
@@ -0,0 +1,1175 @@
1
+ // Flash Cut-Cross-Entropy Metal kernel for the ORPO log-prob head — the vocab
2
+ // loop lives INSIDE the kernel, so neither the [M,V] logits nor a dequantized
3
+ // [V,hidden] head ever touch HBM (the residency bound pure MLX can't reach; see
4
+ // docs/design/orpo-training.md → "Vocab-blocked online-softmax"). Computes the
5
+ // per-token response log-prob `logp = target_logit − logsumexp_v(h·W_v)`.
6
+ //
7
+ // TRANSPILE SOURCE — this is, in effect, Apple's Cut Cross Entropy + Liger's
8
+ // FusedLinearCrossEntropy Triton kernels transpiled Triton → Metal:
9
+ // - Apple CCE (github.com/apple/ml-cross-entropy):
10
+ // cce_lse_forward.py — forward: tile (BLOCK_B tokens × BLOCK_V vocab),
11
+ // accum = tl.dot(E_tile, Cᵀ_tile) over a D-loop, per-block
12
+ // this_lse = mx + log(Σ exp(logit−mx)), merged across vocab blocks into a
13
+ // per-token LSE via locked atomic logaddexp; mask offs_v<V → −inf.
14
+ // cce_backward.py — backward: same (token×vocab) tiling, recompute logits,
15
+ // d_accum = exp(accum − lse); d_accum += where(is_target, −1, 0); then
16
+ // dE += d_accum @ C and dC += d_accumᵀ @ E via locked atomic adds;
17
+ // optional skip when |d_accum| < filter_eps.
18
+ // - Liger FusedLinearCrossEntropy — token-chunking + the in-place softmax−onehot
19
+ // gradient written back over the logit buffer.
20
+ //
21
+ // DIVERGENCES (why this isn't a line-for-line port):
22
+ // 1. Our classifier C is the QUANTIZED head (4/8-bit affine). Apple loads C
23
+ // dense (bf16); we keep the in-Metal dequant for `tl.dot` (the qdot pattern
24
+ // from src/model/fused-decode-kernel.ts). This is the one substantive change.
25
+ // 2. The head is FROZEN (not a LoRA target) → we need only dE (=dh), NOT dC.
26
+ // 3. The LSE cross-vocab-block merge is done in cheap MLX ops on per-block
27
+ // partials, not Metal atomic-logaddexp spinlocks (simpler, same result).
28
+ //
29
+ // STATUS: v1 (one threadgroup per token, whole vocab serially) is correct +
30
+ // memory-bounded (validated MiniCPM5→M=8192, e4b→M=512) but its single-token
31
+ // threadgroup runs the full 262k vocab serially → trips the GPU watchdog on e4b
32
+ // at higher M. The vocab-PARALLEL form below (grid over token × vocab blocks, à la
33
+ // Apple CCE) bounds per-threadgroup work and is the production path.
34
+
35
+ import { MetalKernel } from "../mlx/metal-kernel";
36
+ import { MlxArray } from "../mlx/array";
37
+ import { Dtype } from "../mlx/ffi";
38
+ import * as ops from "../mlx/ops";
39
+ import { STEEL_QMM_HEADER } from "./steel-qmm-header";
40
+
41
+ /** The quantized head pieces the kernel needs (packed weight + per-group affine
42
+ * scale/bias + bits/group_size). Mirrors HeadQuant in loss.ts. */
43
+ export interface FlashCceHead {
44
+ w: MlxArray; // packed quantized weight, uint32 [V, hidden*bits/32]
45
+ scales: MlxArray; // [V, hidden/groupSize]
46
+ biases: MlxArray; // [V, hidden/groupSize] (affine — required)
47
+ bits: number; // 4 or 8
48
+ groupSize: number; // 64
49
+ softcap: number | null;
50
+ }
51
+
52
+ const H_MAX = 4096; // covers MiniCPM5 (1536) and e4b (2560) hidden sizes
53
+ const TG = 128; // threads per token-threadgroup
54
+ // Vocab rows per threadgroup (Apple CCE's BLOCK_V analog). Bounds per-threadgroup
55
+ // runtime so a single dispatch never loops the whole vocab — the e4b watchdog fix.
56
+ const VOCAB_BLOCK = 8192;
57
+ // Tokens processed together per threadgroup (Apple CCE's BLOCK_B). Each thread
58
+ // dequantizes a vocab row's weights ONCE and reuses them across BLOCK_B tokens,
59
+ // amortizing the ALU-bound dequant BLOCK_B× (the dominant cost in the v1
60
+ // one-token-per-threadgroup kernel). Forward and backward use different tile
61
+ // widths: the backward holds a per-token output accumulator `dacc[BLOCK_B·DPER]`
62
+ // in registers, so it is register-bound to a smaller tile (BLOCK_B=8 spills).
63
+ const BLOCK_B = 8; // forward (online stats are only ~7·BLOCK_B floats/thread)
64
+ const BLOCK_B_BWD = 4; // backward (dacc[BLOCK_B·DPER] — 8 overflows the register file)
65
+ // simdgroup_matrix forward GEMM tiles (Apple CCE's BLOCK_B×BLOCK_V×BLOCK_D, mapped
66
+ // to Metal 8×8 simdgroup_matrix). BB tokens × BV vocab logit tile, accumulated over
67
+ // BD hidden chunks. BV/8 = 4 column-blocks → one per simdgroup (TG/32 = 4).
68
+ const SG_BB = 8; // token tile (one 8-row simdgroup block)
69
+ const SG_BV = 32; // vocab tile (4 column-blocks)
70
+ const SG_BD = 32; // hidden chunk (4 k-blocks). BD=64 (GS-aligned, ½ the barriers)
71
+ // measured SLOWER (fwd 844→1010, bwd 3311→3799): the 8KB Csh tile
72
+ // cuts occupancy more than fewer barriers help. 32 is the sweet spot.
73
+
74
+ // VOCAB-PARALLEL forward (Apple CCE cce_lse_forward, transpiled): one threadgroup
75
+ // per (token=grid.z, vocab-block=grid.y) handles a bounded V/NBLK vocab slice → no
76
+ // single threadgroup runs the whole vocab (the watchdog fix). Each writes per-block
77
+ // partials (blockMax, blockSumexp-relative-to-blockMax, target logit) to [M,NBLK];
78
+ // the cross-vocab-block LSE merge is done in cheap MLX ops (vs Apple's locked
79
+ // atomic logaddexp). 128 threads stride the slice, per-thread online softmax,
80
+ // merged across the threadgroup.
81
+ const FWD_SOURCE = String.raw`
82
+ const uint tokBlk = threadgroup_position_in_grid.z; // token-block index
83
+ const uint vb = threadgroup_position_in_grid.y; // vocab-block index
84
+ const uint tid = thread_position_in_threadgroup.x; // 0..TG-1
85
+ const uint M = shape[0];
86
+ const uint V = shape[1];
87
+ const uint H = shape[2];
88
+ const uint GS = shape[3];
89
+ const uint BITS = shape[4];
90
+ const uint WPR = shape[5]; // uint32 words per packed weight row
91
+ const uint GR = shape[6]; // groups per row (H/GS)
92
+ const uint NBLK = shape[7]; // number of vocab blocks
93
+ const float cap = capv[0];
94
+ const uint perWord = 32u / BITS;
95
+ const uint qmask = (BITS == 32u) ? 0xffffffffu : ((1u << BITS) - 1u);
96
+ const uint wordsPerGroup = GS / perWord;
97
+ const uint blockV = (V + NBLK - 1u) / NBLK;
98
+ const uint nStart = vb * blockV;
99
+ const uint nEnd = min(nStart + blockV, V);
100
+ const uint tokBase = tokBlk * BLOCK_B;
101
+
102
+ // Per-token online state (registers). Each thread keeps BLOCK_B tokens' running
103
+ // softmax stats so one vocab-row dequant feeds all BLOCK_B tokens.
104
+ float mx[BLOCK_B], sumexp[BLOCK_B], tgtLogit[BLOCK_B];
105
+ uint tgtId[BLOCK_B];
106
+ for (uint b = 0u; b < BLOCK_B; ++b) {
107
+ mx[b] = -INFINITY; sumexp[b] = 0.0f; tgtLogit[b] = 0.0f;
108
+ tgtId[b] = (tokBase + b < M) ? targets[tokBase + b] : 0xffffffffu;
109
+ }
110
+
111
+ for (uint n = nStart + tid; n < nEnd; n += TG) {
112
+ const device uint* row = wp + (ulong)n * WPR;
113
+ const uint sbBase = n * GR;
114
+ float partial[BLOCK_B];
115
+ for (uint b = 0u; b < BLOCK_B; ++b) partial[b] = 0.0f;
116
+ // qdot factoring per group; the nibble dequant (q) is computed ONCE per
117
+ // element and reused across all BLOCK_B tokens (the amortization).
118
+ for (uint grp = 0u; grp < GR; ++grp) {
119
+ const float sc = scales[sbBase + grp];
120
+ const float bi = biases[sbBase + grp];
121
+ const uint d0 = grp * GS;
122
+ float gacc[BLOCK_B], ghsum[BLOCK_B];
123
+ for (uint b = 0u; b < BLOCK_B; ++b) { gacc[b] = 0.0f; ghsum[b] = 0.0f; }
124
+ for (uint wi = 0u; wi < wordsPerGroup; ++wi) {
125
+ const uint word = row[d0 / perWord + wi];
126
+ const uint dbase = d0 + wi * perWord;
127
+ for (uint k = 0u; k < perWord; ++k) {
128
+ const float q = (float)((word >> (BITS * k)) & qmask);
129
+ const uint d = dbase + k;
130
+ for (uint b = 0u; b < BLOCK_B; ++b) {
131
+ const uint tb = min(tokBase + b, M - 1u); // clamp padding lanes (last block) — result discarded in merge
132
+ // h is passed TRANSPOSED [H, M], so the BLOCK_B inner reads hit one
133
+ // cache line (h[d, tb..tb+BLOCK_B] contiguous) instead of BLOCK_B
134
+ // separate lines — the memory-access fix for the strided pattern.
135
+ const float hd = (float)h[(ulong)d * M + tb];
136
+ gacc[b] += hd * q; ghsum[b] += hd;
137
+ }
138
+ }
139
+ }
140
+ for (uint b = 0u; b < BLOCK_B; ++b) partial[b] += sc * gacc[b] + bi * ghsum[b];
141
+ }
142
+ for (uint b = 0u; b < BLOCK_B; ++b) {
143
+ float logit = partial[b];
144
+ if (cap > 0.0f) logit = cap * tanh(logit / cap);
145
+ const float nm = max(mx[b], logit);
146
+ sumexp[b] = sumexp[b] * exp(mx[b] - nm) + exp(logit - nm);
147
+ mx[b] = nm;
148
+ if (n == tgtId[b]) tgtLogit[b] = logit;
149
+ }
150
+ }
151
+
152
+ // Merge the TG threads' partials per token → per-(token, vocab-block) output.
153
+ threadgroup float mM[TG], mS[TG], mT[TG];
154
+ for (uint b = 0u; b < BLOCK_B; ++b) {
155
+ if (tokBase + b >= M) break;
156
+ mM[tid] = mx[b]; mS[tid] = sumexp[b]; mT[tid] = tgtLogit[b];
157
+ threadgroup_barrier(mem_flags::mem_threadgroup);
158
+ if (tid == 0u) {
159
+ float gm = -INFINITY, gs = 0.0f, gt = 0.0f;
160
+ for (uint i = 0u; i < TG; ++i) {
161
+ const float im = mM[i];
162
+ const float nm = max(gm, im);
163
+ if (nm > -INFINITY) { gs = gs * exp(gm - nm) + mS[i] * exp(im - nm); gm = nm; }
164
+ gt += mT[i]; // target logit (one thread nonzero), else 0
165
+ }
166
+ blockMax_out[(tokBase + b) * NBLK + vb] = gm;
167
+ blockSum_out[(tokBase + b) * NBLK + vb] = gs;
168
+ blockTgt_out[(tokBase + b) * NBLK + vb] = gt;
169
+ }
170
+ threadgroup_barrier(mem_flags::mem_threadgroup);
171
+ }
172
+ `;
173
+
174
+ // SIMDGROUP_MATRIX forward GEMM (the speed tier): per (token-block, vocab-block)
175
+ // threadgroup, loop the vocab slice in BV-chunks; for each chunk compute the
176
+ // [BB, BV] logit tile with 8×8 simdgroup_matrix mma over BD hidden chunks, then a
177
+ // scalar online-softmax update. The classifier tile C [BV, BD] is dequantized into
178
+ // threadgroup once per (vocab-chunk, D-chunk) and reused across all BB tokens by
179
+ // the matmul — so the dequant is amortized AND the matmul runs at simdgroup-matrix
180
+ // throughput (the scalar madd was the bottleneck after BLOCK_B tiling). BV/8 = 4
181
+ // column-blocks → one per simdgroup (TG/32 = 4). logit[b,v] = Σ_d E[b,d]·C[v,d] =
182
+ // E @ Cᵀ (the C block is loaded transposed).
183
+ const FWD_SG_SOURCE = String.raw`
184
+ const uint tokBlk = threadgroup_position_in_grid.z;
185
+ const uint vb = threadgroup_position_in_grid.y;
186
+ const uint tid = thread_position_in_threadgroup.x;
187
+ const uint sg = tid / 32u; // simdgroup 0..3
188
+ const uint M=shape[0], V=shape[1], H=shape[2], GS=shape[3], BITS=shape[4], WPR=shape[5], GR=shape[6], NBLK=shape[7];
189
+ const float cap = capv[0];
190
+ const uint perWord = 32u / BITS;
191
+ const uint qmask = (BITS == 32u) ? 0xffffffffu : ((1u << BITS) - 1u);
192
+ const uint blockV = (V + NBLK - 1u) / NBLK;
193
+ const uint nStart = vb * blockV;
194
+ const uint nEnd = min(nStart + blockV, V);
195
+ const uint tokBase = tokBlk * BB;
196
+
197
+ // +4-float padded leading dims (MLX steel's BK_padded trick): a stride of 32 ==
198
+ // 32 banks → 32-way bank conflicts on every simdgroup_load; 36 breaks alignment.
199
+ threadgroup float Esh[BB * BDX];
200
+ threadgroup float Csh[BV * BDX];
201
+ threadgroup float Lsh[BB * BVX];
202
+ threadgroup float tgMax[BB], tgSum[BB], tgTgt[BB];
203
+ threadgroup uint tgTid[BB];
204
+ if (tid < BB) {
205
+ tgMax[tid] = -INFINITY; tgSum[tid] = 0.0f; tgTgt[tid] = 0.0f;
206
+ tgTid[tid] = (tokBase + tid < M) ? targets[tokBase + tid] : 0xffffffffu;
207
+ }
208
+ threadgroup_barrier(mem_flags::mem_threadgroup);
209
+
210
+ for (uint v0 = nStart; v0 < nEnd; v0 += BV) {
211
+ simdgroup_float8x8 acc = simdgroup_float8x8(0);
212
+ for (uint d0 = 0u; d0 < H; d0 += BD) {
213
+ // E tile [BB, BD] (token hiddens)
214
+ for (uint i = tid; i < BB * BD; i += TG) {
215
+ const uint b = i / BD, dd = i % BD;
216
+ const uint tb = min(tokBase + b, M - 1u);
217
+ Esh[b * BDX + dd] = (float)h[(ulong)tb * H + d0 + dd];
218
+ }
219
+ // C tile [BV, BD] (dequantized head, reused across all BB tokens).
220
+ #if CCE_SG_SKIPDQ
221
+ // PROBE: no global loads — isolates whether the staging loads are the limiter.
222
+ for (uint i = tid; i < BV * BD; i += TG) {
223
+ const uint vrow = i / BD, dd = i % BD;
224
+ Csh[vrow * BDX + dd] = (v0 + vrow < nEnd) ? ((float)((i & 7u)) * 0.01f + 0.001f) : 0.0f;
225
+ }
226
+ #else
227
+ // ONE THREAD PER PACKED WORD: each word loaded once (was perWord-redundant),
228
+ // and scale/bias once per word's group (was per-element — the whole BD chunk
229
+ // is one GS group, so this collapses ~perWord× of scale/bias traffic too).
230
+ const uint wordsPerRow = BD / perWord;
231
+ for (uint wi = tid; wi < BV * wordsPerRow; wi += TG) {
232
+ const uint vrow = wi / wordsPerRow;
233
+ const uint dloc = (wi % wordsPerRow) * perWord;
234
+ const uint n = v0 + vrow;
235
+ const uint d = d0 + dloc;
236
+ uint word = 0u; float sc = 0.0f, bi = 0.0f;
237
+ if (n < nEnd) {
238
+ word = wp[(ulong)n * WPR + d / perWord];
239
+ const uint grp = d / GS;
240
+ sc = scales[n * GR + grp]; bi = biases[n * GR + grp];
241
+ }
242
+ for (uint k = 0u; k < perWord; ++k) {
243
+ const float q = (float)((word >> (BITS * k)) & qmask);
244
+ Csh[vrow * BDX + dloc + k] = sc * q + bi;
245
+ }
246
+ }
247
+ #endif
248
+ threadgroup_barrier(mem_flags::mem_threadgroup);
249
+ // acc(this sg's 8 cols) += E @ Cᵀ over BD/8 k-blocks (padded strides BDX)
250
+ for (uint kk = 0u; kk < BD / 8u; ++kk) {
251
+ simdgroup_float8x8 me, mc;
252
+ simdgroup_load(me, Esh + kk * 8u, BDX);
253
+ simdgroup_load(mc, Csh + (ulong)sg * 8u * BDX + kk * 8u, BDX, ulong2(0, 0), true);
254
+ simdgroup_multiply_accumulate(acc, me, mc, acc);
255
+ }
256
+ threadgroup_barrier(mem_flags::mem_threadgroup);
257
+ }
258
+ simdgroup_store(acc, Lsh + sg * 8u, BVX); // [0:BB, sg*8 : sg*8+8], padded stride
259
+ threadgroup_barrier(mem_flags::mem_threadgroup);
260
+
261
+ // Online-softmax update for BB tokens over this BV chunk (thread b<BB → token b).
262
+ if (tid < BB) {
263
+ const uint b = tid;
264
+ for (uint v = 0u; v < BV; ++v) {
265
+ const uint n = v0 + v;
266
+ if (n >= nEnd) break;
267
+ float logit = Lsh[b * BVX + v];
268
+ if (cap > 0.0f) logit = cap * tanh(logit / cap);
269
+ const float nm = max(tgMax[b], logit);
270
+ tgSum[b] = tgSum[b] * exp(tgMax[b] - nm) + exp(logit - nm);
271
+ tgMax[b] = nm;
272
+ if (n == tgTid[b]) tgTgt[b] = logit;
273
+ }
274
+ }
275
+ threadgroup_barrier(mem_flags::mem_threadgroup);
276
+ }
277
+
278
+ if (tid < BB && tokBase + tid < M) {
279
+ blockMax_out[(tokBase + tid) * NBLK + vb] = tgMax[tid];
280
+ blockSum_out[(tokBase + tid) * NBLK + vb] = tgSum[tid];
281
+ blockTgt_out[(tokBase + tid) * NBLK + vb] = tgTgt[tid];
282
+ }
283
+ `;
284
+
285
+ // LANE-SLICED forward (the real memory fix): instead of one thread per vocab row
286
+ // (adjacent threads read rows WPR words apart → uncoalesced weight reads, the
287
+ // dominant ~43 GB stream at ~7% bandwidth), a SIMDGROUP of 32 lanes cooperates on
288
+ // ONE row — lane l reads words {l, l+32, ...} so the 32 lanes hit CONSECUTIVE
289
+ // memory each step (coalesced). The lane-partial dots reduce with simd_sum. The
290
+ // threadgroup's 4 simdgroups process 4 rows in parallel; BLOCK_B tokens amortize
291
+ // the dequant (each row dequantized once, reused across tokens). Same coalesced
292
+ // pattern as fused-decode-kernel.ts. h is passed TRANSPOSED [H, M].
293
+ const SGN = TG / 32; // simdgroups per threadgroup (4)
294
+ const FWD_LANE_SOURCE = String.raw`
295
+ const uint tokBlk = threadgroup_position_in_grid.z;
296
+ const uint vb = threadgroup_position_in_grid.y;
297
+ const uint lane = thread_index_in_simdgroup; // 0..31 within the simdgroup
298
+ const uint sg = thread_position_in_threadgroup.x / 32u; // 0..SGN-1
299
+ const uint M=shape[0], V=shape[1], H=shape[2], GS=shape[3], BITS=shape[4], WPR=shape[5], GR=shape[6], NBLK=shape[7];
300
+ const float cap = capv[0];
301
+ const uint perWord = 32u / BITS;
302
+ const uint qmask = (BITS == 32u) ? 0xffffffffu : ((1u << BITS) - 1u);
303
+ const uint wordsPerLane = WPR / 32u; // WPR is a multiple of 32
304
+ const uint blockV = (V + NBLK - 1u) / NBLK;
305
+ const uint nStart = vb * blockV;
306
+ const uint nEnd = min(nStart + blockV, V);
307
+ const uint tokBase = tokBlk * BLOCK_B;
308
+
309
+ float mx[BLOCK_B], sumexp[BLOCK_B], tgtLogit[BLOCK_B];
310
+ uint tgtId[BLOCK_B];
311
+ for (uint b = 0u; b < BLOCK_B; ++b) {
312
+ mx[b] = -INFINITY; sumexp[b] = 0.0f; tgtLogit[b] = 0.0f;
313
+ tgtId[b] = (tokBase + b < M) ? targets[tokBase + b] : 0xffffffffu;
314
+ }
315
+
316
+ // each simdgroup owns rows {nStart+sg, nStart+sg+SGN, ...}; its 32 lanes share the row
317
+ for (uint n = nStart + sg; n < nEnd; n += SGN) {
318
+ const device uint* row = wp + (ulong)n * WPR;
319
+ const uint sbBase = n * GR;
320
+ float partial[BLOCK_B];
321
+ for (uint b = 0u; b < BLOCK_B; ++b) partial[b] = 0.0f;
322
+ for (uint j = 0u; j < wordsPerLane; ++j) {
323
+ const uint wi = lane + 32u * j; // coalesced across the 32 lanes
324
+ const uint word = row[wi];
325
+ const uint dbase = wi * perWord;
326
+ for (uint k = 0u; k < perWord; ++k) {
327
+ const uint d = dbase + k;
328
+ const uint grp = d / GS;
329
+ const float w = scales[sbBase + grp] * (float)((word >> (BITS * k)) & qmask) + biases[sbBase + grp];
330
+ for (uint b = 0u; b < BLOCK_B; ++b) {
331
+ const uint tb = min(tokBase + b, M - 1u);
332
+ partial[b] += (float)h[(ulong)d * M + tb] * w; // h transposed [H, M]
333
+ }
334
+ }
335
+ }
336
+ // reduce each token's lane-partials across the simdgroup → full logit
337
+ for (uint b = 0u; b < BLOCK_B; ++b) {
338
+ float logit = simd_sum(partial[b]);
339
+ if (cap > 0.0f) logit = cap * tanh(logit / cap);
340
+ const float nm = max(mx[b], logit);
341
+ sumexp[b] = sumexp[b] * exp(mx[b] - nm) + exp(logit - nm);
342
+ mx[b] = nm;
343
+ if (n == tgtId[b]) tgtLogit[b] = logit;
344
+ }
345
+ }
346
+
347
+ // simd_sum broadcasts, so each simdgroup's online state is replicated across its
348
+ // lanes; lane 0 of each sg publishes, then SGN states merge per token.
349
+ threadgroup float mM[SGN * BLOCK_B], mS[SGN * BLOCK_B], mT[SGN * BLOCK_B];
350
+ if (lane == 0u) {
351
+ for (uint b = 0u; b < BLOCK_B; ++b) {
352
+ mM[sg * BLOCK_B + b] = mx[b]; mS[sg * BLOCK_B + b] = sumexp[b]; mT[sg * BLOCK_B + b] = tgtLogit[b];
353
+ }
354
+ }
355
+ threadgroup_barrier(mem_flags::mem_threadgroup);
356
+ const uint tid = thread_position_in_threadgroup.x;
357
+ if (tid < BLOCK_B && tokBase + tid < M) {
358
+ const uint b = tid;
359
+ float gm = -INFINITY, gs = 0.0f, gt = 0.0f;
360
+ for (uint s = 0u; s < SGN; ++s) {
361
+ const float im = mM[s * BLOCK_B + b];
362
+ const float nm = max(gm, im);
363
+ if (nm > -INFINITY) { gs = gs * exp(gm - nm) + mS[s * BLOCK_B + b] * exp(im - nm); gm = nm; }
364
+ gt += mT[s * BLOCK_B + b];
365
+ }
366
+ blockMax_out[(tokBase + b) * NBLK + vb] = gm;
367
+ blockSum_out[(tokBase + b) * NBLK + vb] = gs;
368
+ blockTgt_out[(tokBase + b) * NBLK + vb] = gt;
369
+ }
370
+ `;
371
+
372
+ let _fwdLaneKernel: MetalKernel | null = null;
373
+ function fwdLaneKernel(): MetalKernel {
374
+ if (!_fwdLaneKernel) {
375
+ _fwdLaneKernel = new MetalKernel({
376
+ name: "flash_cce_fwd_lane",
377
+ inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
378
+ outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
379
+ source: FWD_LANE_SOURCE,
380
+ header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B}\n#define SGN ${SGN}\n`,
381
+ ensureRowContiguous: true,
382
+ });
383
+ }
384
+ return _fwdLaneKernel;
385
+ }
386
+
387
+ let _fwdSgKernel: MetalKernel | null = null;
388
+ function fwdSgKernel(): MetalKernel {
389
+ if (!_fwdSgKernel) {
390
+ _fwdSgKernel = new MetalKernel({
391
+ name: "flash_cce_fwd_sg",
392
+ inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
393
+ outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
394
+ source: FWD_SG_SOURCE,
395
+ header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${SG_BB}\n#define BV ${SG_BV}\n#define BD ${SG_BD}\n#define BDX ${SG_BD + 4}\n#define BVX ${SG_BV + 4}\n#define CCE_SG_SKIPDQ ${process.env.MLX_BUN_CCE_SG_SKIPDQ === "1" ? 1 : 0}\n`,
396
+ ensureRowContiguous: true,
397
+ });
398
+ }
399
+ return _fwdSgKernel;
400
+ }
401
+ // simdgroup_matrix forward is now the DEFAULT (fastest): the earlier "not faster"
402
+ // verdict was confounded — the SG path was bound by the Csh dequant STAGING loads
403
+ // (packed weight + per-group scale/bias), not the matmul, so it never reached the
404
+ // compute-bound regime. Removing the staging redundancy (one thread per packed
405
+ // word; scale/bias once per group) cut it 1629→848 ms, beating the scalar
406
+ // kernel's 1097 ms. Fall back to scalar (MLX_BUN_CCE_SCALAR=1) when H % SG_BD != 0.
407
+ const USE_SIMDGROUP_FWD = process.env.MLX_BUN_CCE_SCALAR !== "1" && process.env.MLX_BUN_CCE_LANE !== "1";
408
+
409
+ // Backward: dh[t,d] = Σ_v coeff_v · dequant(W_v)[d], coeff_v = g_t·(onehot_v −
410
+ // softmax_v)·sech²_v, softmax_v = exp(softcap(logit_v) − lse_t). Two-phase per
411
+ // 128-row vocab block, NO atomics: phase 1 each thread computes coeff for one
412
+ // block row (full dot → into threadgroup coeff[]); phase 2 each thread owns
413
+ // H/TG OUTPUT dims and accumulates them over the block's rows into per-thread
414
+ // registers (dacc), so each output dim is written by exactly one thread. lse is
415
+ // reused from the forward (no recompute of the global softmax normalizer).
416
+ const DPER_MAX = H_MAX / TG; // 32
417
+ const BWD_SOURCE = String.raw`
418
+ const uint tokBlk = threadgroup_position_in_grid.z; // token-block
419
+ const uint vb = threadgroup_position_in_grid.y; // vocab-block
420
+ const uint tid = thread_position_in_threadgroup.x;
421
+ const uint M = shape[0];
422
+ const uint V = shape[1];
423
+ const uint H = shape[2];
424
+ const uint GS = shape[3];
425
+ const uint BITS = shape[4];
426
+ const uint WPR = shape[5];
427
+ const uint GR = shape[6];
428
+ const uint NBLK = shape[7];
429
+ const float cap = capv[0];
430
+ const uint perWord = 32u / BITS;
431
+ const uint qmask = (BITS == 32u) ? 0xffffffffu : ((1u << BITS) - 1u);
432
+ const uint wordsPerGroup = GS / perWord;
433
+ const uint DPER = H / TG; // output dims this thread owns
434
+ const uint dStart = tid * DPER;
435
+ const uint blockV = (V + NBLK - 1u) / NBLK;
436
+ const uint nStart = vb * blockV;
437
+ const uint nEnd = min(nStart + blockV, V);
438
+ const uint tokBase = tokBlk * BLOCK_B;
439
+
440
+ float lseB[BLOCK_B], gB[BLOCK_B]; uint tgtB[BLOCK_B];
441
+ for (uint b = 0u; b < BLOCK_B; ++b) {
442
+ const uint tk = min(tokBase + b, M - 1u);
443
+ lseB[b] = lse[tk]; gB[b] = gv[tk];
444
+ tgtB[b] = (tokBase + b < M) ? targets[tokBase + b] : 0xffffffffu;
445
+ }
446
+
447
+ threadgroup float coeffB[TG * BLOCK_B]; // [row][token]
448
+ // BLOCK_B == 4 → one float4 per output dim holds all 4 tokens, so the phase-2
449
+ // accumulation (the backward's ~98% hotspot — a scalar coeffᵀ@W GEMM) runs as
450
+ // one vector FMA per (row, dim) instead of 4 scalar ones.
451
+ float4 dacc[DACC_N]; // dacc[j] = (tok0..tok3) for dim dStart+j
452
+ for (uint i = 0u; i < DPER; ++i) dacc[i] = float4(0.0f);
453
+ threadgroup_barrier(mem_flags::mem_threadgroup);
454
+
455
+ for (uint blk = nStart; blk < nEnd; blk += TG) {
456
+ // --- phase 1: row n = blk + tid → coeff for each of BLOCK_B tokens (the
457
+ // dequant-dot is computed once per token via shared nibble q) ---
458
+ const uint n = blk + tid;
459
+ float cf[BLOCK_B];
460
+ for (uint b = 0u; b < BLOCK_B; ++b) cf[b] = 0.0f;
461
+ if (n < nEnd) {
462
+ const device uint* row = wp + (ulong)n * WPR;
463
+ const uint sbBase = n * GR;
464
+ float raw[BLOCK_B];
465
+ for (uint b = 0u; b < BLOCK_B; ++b) raw[b] = 0.0f;
466
+ for (uint grp = 0u; grp < GR; ++grp) {
467
+ const float sc = scales[sbBase + grp];
468
+ const float bi = biases[sbBase + grp];
469
+ const uint d0 = grp * GS;
470
+ float gacc[BLOCK_B], ghsum[BLOCK_B];
471
+ for (uint b = 0u; b < BLOCK_B; ++b) { gacc[b] = 0.0f; ghsum[b] = 0.0f; }
472
+ for (uint wi = 0u; wi < wordsPerGroup; ++wi) {
473
+ const uint word = row[d0 / perWord + wi];
474
+ const uint dbase = d0 + wi * perWord;
475
+ for (uint k = 0u; k < perWord; ++k) {
476
+ const float q = (float)((word >> (BITS * k)) & qmask);
477
+ const uint d = dbase + k;
478
+ for (uint b = 0u; b < BLOCK_B; ++b) {
479
+ const uint tk = min(tokBase + b, M - 1u);
480
+ const float hd = (float)h[(ulong)tk * H + d]; // phase-1 h reads aren't the bwd bottleneck (phase 2 is)
481
+ gacc[b] += hd * q; ghsum[b] += hd;
482
+ }
483
+ }
484
+ }
485
+ for (uint b = 0u; b < BLOCK_B; ++b) raw[b] += sc * gacc[b] + bi * ghsum[b];
486
+ }
487
+ for (uint b = 0u; b < BLOCK_B; ++b) {
488
+ float logit = raw[b], sech2 = 1.0f;
489
+ if (cap > 0.0f) { const float th = tanh(raw[b] / cap); logit = cap * th; sech2 = 1.0f - th * th; }
490
+ const float sm = exp(logit - lseB[b]);
491
+ float c = gB[b] * ((n == tgtB[b] ? 1.0f : 0.0f) - sm);
492
+ if (cap > 0.0f) c *= sech2;
493
+ cf[b] = c;
494
+ }
495
+ }
496
+ for (uint b = 0u; b < BLOCK_B; ++b) coeffB[tid * BLOCK_B + b] = cf[b];
497
+ threadgroup_barrier(mem_flags::mem_threadgroup);
498
+
499
+ // --- phase 2: the coeffᵀ@W accumulation — the backward's ~98% hotspot. Each
500
+ // weight dequant (wd) feeds all BLOCK_B tokens via one float4 FMA. (Loads are
501
+ // negligible — proven ~137 ms — so the per-element dequant stays; the win is
502
+ // vectorizing the arithmetic.) ---
503
+ #if !CCE_BWD_SKIP_P2
504
+ for (uint r = 0u; r < TG; ++r) {
505
+ const uint n2 = blk + r;
506
+ if (n2 >= nEnd) break;
507
+ const device uint* row = wp + (ulong)n2 * WPR;
508
+ const uint sbBase = n2 * GR;
509
+ const float4 cf = *(const threadgroup float4*)(coeffB + r * BLOCK_B); // tok0..3
510
+ #if CCE_BWD_SKIP_P2DQ
511
+ for (uint j = 0u; j < DPER; ++j) dacc[j] += cf * ((float)((dStart + j) & 7u) * 0.01f);
512
+ #else
513
+ for (uint j = 0u; j < DPER; ++j) {
514
+ const uint d = dStart + j;
515
+ const uint grp = d / GS;
516
+ const uint word = row[d / perWord];
517
+ const float q = (float)((word >> (BITS * (d % perWord))) & qmask);
518
+ const float wd = scales[sbBase + grp] * q + biases[sbBase + grp];
519
+ dacc[j] += cf * wd;
520
+ }
521
+ #endif
522
+ }
523
+ #else
524
+ // PROBE: skip phase-2 accumulation but consume coeffB so phase 1 isn't DCE'd —
525
+ // isolates phase-1 (the coeff dot) cost from phase-2 accumulation.
526
+ dacc[0] += float4(coeffB[tid * BLOCK_B]);
527
+ #endif
528
+ threadgroup_barrier(mem_flags::mem_threadgroup);
529
+ }
530
+
531
+ // Atomic-accumulate each token's partial dh across the NBLK vocab-block programs.
532
+ // dacc[j] = (tok0..tok3) for dim dStart+j.
533
+ for (uint b = 0u; b < BLOCK_B; ++b) {
534
+ if (tokBase + b >= M) break;
535
+ for (uint j = 0u; j < DPER; ++j)
536
+ atomic_fetch_add_explicit(&dh[(ulong)(tokBase + b) * H + dStart + j], dacc[j][b], memory_order_relaxed);
537
+ }
538
+ `;
539
+
540
+ // dacc is sized to the ACTUAL per-thread output dims (ceil(H/TG)) rather than the
541
+ // worst-case DPER_MAX — right-sizing the register footprint raises occupancy (the
542
+ // backward's limiter). Keyed by daccN so a model with a different H rebuilds.
543
+ const _bwdKernels = new Map<number, MetalKernel>();
544
+ function bwdKernel(daccN: number): MetalKernel {
545
+ let k = _bwdKernels.get(daccN);
546
+ if (!k) {
547
+ k = new MetalKernel({
548
+ name: "flash_cce_bwd",
549
+ inputNames: ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"],
550
+ outputNames: ["dh"],
551
+ source: BWD_SOURCE,
552
+ header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B_BWD}\n#define DACC_N ${daccN}\n#define CCE_BWD_SKIP_P2DQ ${process.env.MLX_BUN_CCE_BWD_SKIP_P2DQ === "1" ? 1 : 0}\n#define CCE_BWD_SKIP_P2 ${process.env.MLX_BUN_CCE_BWD_SKIP_P2 === "1" ? 1 : 0}\n`,
553
+ ensureRowContiguous: true,
554
+ atomicOutputs: true, // dh is summed across the NBLK vocab-block programs
555
+ });
556
+ _bwdKernels.set(daccN, k);
557
+ }
558
+ return k;
559
+ }
560
+
561
+ // SIMDGROUP_MATRIX backward (phase-2 GEMM): the phase-2 accumulation dacc = coeffᵀ@W
562
+ // is ~69% of the backward (1733 ms phase-1 + 3779 ms phase-2 at e4b M=512) and is
563
+ // compute-bound, NOT register-bound (right-sizing dacc gave 0%) — so we can afford
564
+ // simdgroup_matrix's larger accumulator. Phase 1 unchanged (one thread per row →
565
+ // coeff into threadgroup, BB=8 tokens). Phase 2: each simdgroup owns H/SGN dims as
566
+ // DIMTILES persistent 8×8 acc fragments; per 8-row chunk, load coeffᵀ (transpose)
567
+ // and the dequantized W tile, mma. dh atomic-added at the end. Needs H % (SGN*8)==0.
568
+ const BB_BWD_SG = 8;
569
+ const BWD_SG_SOURCE = String.raw`
570
+ const uint tokBlk = threadgroup_position_in_grid.z;
571
+ const uint vb = threadgroup_position_in_grid.y;
572
+ const uint tid = thread_position_in_threadgroup.x;
573
+ const uint lane = thread_index_in_simdgroup;
574
+ const uint sg = tid / 32u;
575
+ const uint M=shape[0],V=shape[1],H=shape[2],GS=shape[3],BITS=shape[4],WPR=shape[5],GR=shape[6],NBLK=shape[7];
576
+ const float cap = capv[0];
577
+ const uint perWord = 32u / BITS;
578
+ const uint qmask = (BITS == 32u) ? 0xffffffffu : ((1u << BITS) - 1u);
579
+ const uint HSG = H / SGN; // dims owned by this simdgroup
580
+ const uint d0sg = sg * HSG;
581
+ const uint blockV = (V + NBLK - 1u) / NBLK;
582
+ const uint nStart = vb * blockV;
583
+ const uint nEnd = min(nStart + blockV, V);
584
+ const uint tokBase = tokBlk * BB;
585
+
586
+ float lseB[BB], gB[BB]; uint tgtB[BB];
587
+ for (uint b = 0u; b < BB; ++b) {
588
+ const uint tk = min(tokBase + b, M - 1u);
589
+ lseB[b] = lse[tk]; gB[b] = gv[tk];
590
+ tgtB[b] = (tokBase + b < M) ? targets[tokBase + b] : 0xffffffffu;
591
+ }
592
+
593
+ threadgroup float coeffB[TG * 8]; // [row][token], 8-wide for the simdgroup tile
594
+ threadgroup float Wsh[SGN * 64]; // per-simdgroup 8×8 dequantized W tile
595
+ threadgroup float Csh[SGN * 64]; // per-simdgroup 8×8 store-out buffer
596
+ threadgroup float Esh[BB * BDP]; // phase-1: token hiddens tile (logit GEMM)
597
+ threadgroup float Cstg[BVP * BDP]; // phase-1: dequantized W tile (logit GEMM)
598
+ threadgroup float Lsh[BB * BVP]; // phase-1: logit[token, row] tile
599
+
600
+ #if CCE_BWD_BLOCKSKIP
601
+ // blockMax vocab-block early exit: this (token-block, vocab-block) program does
602
+ // ~0 work if, for EVERY token, the block's max softmax prob exp(blockMax−lse) is
603
+ // below eps AND the target isn't in this block (so no onehot term lives here).
604
+ // On peaked logits ~all but one vocab block per token is cold → skips phase 1+2.
605
+ // The early return is AFTER the threadgroup decls (uniform across the TG; declaring
606
+ // threadgroup arrays after an early return fails to compile).
607
+ bool warm = false;
608
+ for (uint b = 0u; b < BB; ++b) {
609
+ if (tokBase + b < M) {
610
+ if (exp(blockMax[(tokBase + b) * NBLK + vb] - lseB[b]) >= CCE_BWD_BLOCK_EPS) warm = true;
611
+ if (tgtB[b] >= nStart && tgtB[b] < nEnd) warm = true;
612
+ }
613
+ }
614
+ if (!warm) return;
615
+ #endif
616
+
617
+ simdgroup_float8x8 acc[DIMTILES];
618
+ for (uint t = 0u; t < DIMTILES; ++t) acc[t] = simdgroup_float8x8(0);
619
+
620
+ for (uint blk = nStart; blk < nEnd; blk += TG) {
621
+ // --- phase 1 (SG-matrix): logit[tok, row] = E @ Wᵀ over H (the SAME tile GEMM
622
+ // as the forward, with the sub-block's TG rows as the "vocab"), then coeff. ---
623
+ for (uint vc = 0u; vc < TG; vc += BVP) { // TG rows in BVP-row chunks
624
+ simdgroup_float8x8 lacc = simdgroup_float8x8(0);
625
+ for (uint d0 = 0u; d0 < H; d0 += BDP) {
626
+ for (uint i = tid; i < BB * BDP; i += TG) { // E tile [BB tokens, BDP]
627
+ const uint b = i / BDP, dd = i % BDP;
628
+ const uint tk = min(tokBase + b, M - 1u);
629
+ Esh[i] = (float)h[(ulong)tk * H + d0 + dd];
630
+ }
631
+ const uint wordsPerRow = BDP / perWord; // C tile [BVP rows, BDP] (one thread/word)
632
+ for (uint wi = tid; wi < BVP * wordsPerRow; wi += TG) {
633
+ const uint vrow = wi / wordsPerRow;
634
+ const uint dloc = (wi % wordsPerRow) * perWord;
635
+ const uint n2 = blk + vc + vrow;
636
+ const uint d = d0 + dloc;
637
+ uint word = 0u; float scl = 0.0f, bia = 0.0f;
638
+ if (n2 < nEnd) {
639
+ word = wp[(ulong)n2 * WPR + d / perWord];
640
+ const uint grp = d / GS; scl = scales[n2 * GR + grp]; bia = biases[n2 * GR + grp];
641
+ }
642
+ for (uint k = 0u; k < perWord; ++k) Cstg[vrow * BDP + dloc + k] = scl * (float)((word >> (BITS * k)) & qmask) + bia;
643
+ }
644
+ threadgroup_barrier(mem_flags::mem_threadgroup);
645
+ for (uint kk = 0u; kk < BDP / 8u; ++kk) { // lacc(sg's 8 rows) += E @ Cᵀ
646
+ simdgroup_float8x8 me, mc;
647
+ simdgroup_load(me, Esh + kk * 8u, BDP);
648
+ simdgroup_load(mc, Cstg + (ulong)sg * 8u * BDP + kk * 8u, BDP, ulong2(0, 0), true);
649
+ simdgroup_multiply_accumulate(lacc, me, mc, lacc);
650
+ }
651
+ threadgroup_barrier(mem_flags::mem_threadgroup);
652
+ }
653
+ simdgroup_store(lacc, Lsh + sg * 8u, BVP); // Lsh[tok*BVP + row] = logit[tok][row]
654
+ threadgroup_barrier(mem_flags::mem_threadgroup);
655
+ for (uint i = tid; i < BB * BVP; i += TG) { // derive coeff[row][tok] → coeffB
656
+ const uint tok = i / BVP, row = i % BVP;
657
+ const uint n2 = blk + vc + row;
658
+ float c = 0.0f;
659
+ if (n2 < nEnd && tokBase + tok < M) {
660
+ float logit = Lsh[tok * BVP + row], sech2 = 1.0f;
661
+ if (cap > 0.0f) { const float th = tanh(logit / cap); logit = cap * th; sech2 = 1.0f - th * th; }
662
+ const float sm = exp(logit - lseB[tok]);
663
+ c = gB[tok] * ((n2 == tgtB[tok] ? 1.0f : 0.0f) - sm);
664
+ if (cap > 0.0f) c *= sech2;
665
+ }
666
+ coeffB[(vc + row) * 8u + tok] = c;
667
+ }
668
+ threadgroup_barrier(mem_flags::mem_threadgroup);
669
+ }
670
+
671
+ // --- phase 2: acc[t][tok][dim] += Σ_row coeff[row][tok] · W[row][dim] ---
672
+ for (uint rc = 0u; rc < TG; rc += 8u) {
673
+ // Apple-CCE near-zero coeff filter: softmax mass concentrates on a few
674
+ // tokens, so coeff ≈ 0 for almost every vocab row → most 8-row chunks
675
+ // contribute nothing. Skip the chunk's dim-tile dequant+mma when its whole
676
+ // [8 rows × 8 tokens] coeff block is below eps. Safe because the SKIPPED
677
+ // softmax mass (not each element) is what's tiny. Uniform across the
678
+ // simdgroup (all lanes/sgs see the same coeffB[rc]).
679
+ #if CCE_BWD_FILTER
680
+ float cmax = 0.0f;
681
+ for (uint i = lane; i < 64u; i += 32u) cmax = max(cmax, fabs(coeffB[rc * 8u + i]));
682
+ if (simd_max(cmax) < CCE_BWD_FILTER_EPS) continue;
683
+ #endif
684
+ simdgroup_float8x8 mA;
685
+ simdgroup_load(mA, coeffB + rc * 8u, 8, ulong2(0, 0), true); // mA[tok][row]
686
+ threadgroup float* ws = Wsh + sg * 64u;
687
+ for (uint t = 0u; t < DIMTILES; ++t) {
688
+ const uint d0 = d0sg + t * 8u;
689
+ for (uint i = lane; i < 64u; i += 32u) { // dequant W[8 rows, 8 dims]
690
+ const uint rr = i / 8u, dd = i % 8u;
691
+ const uint n2 = blk + rc + rr;
692
+ float w = 0.0f;
693
+ if (n2 < nEnd) {
694
+ const uint d = d0 + dd;
695
+ const uint word = wp[(ulong)n2 * WPR + d / perWord];
696
+ const float q = (float)((word >> (BITS * (d % perWord))) & qmask);
697
+ w = scales[n2 * GR + d / GS] * q + biases[n2 * GR + d / GS];
698
+ }
699
+ ws[i] = w;
700
+ }
701
+ simdgroup_barrier(mem_flags::mem_threadgroup);
702
+ simdgroup_float8x8 mB;
703
+ simdgroup_load(mB, ws, 8); // mB[row][dim]
704
+ simdgroup_multiply_accumulate(acc[t], mA, mB, acc[t]);
705
+ simdgroup_barrier(mem_flags::mem_threadgroup);
706
+ }
707
+ }
708
+ threadgroup_barrier(mem_flags::mem_threadgroup);
709
+ }
710
+
711
+ // write each dim-tile's acc → dh (atomic across the NBLK vocab-block programs)
712
+ threadgroup float* cs = Csh + sg * 64u;
713
+ for (uint t = 0u; t < DIMTILES; ++t) {
714
+ simdgroup_store(acc[t], cs, 8);
715
+ simdgroup_barrier(mem_flags::mem_threadgroup);
716
+ const uint d0 = d0sg + t * 8u;
717
+ for (uint i = lane; i < 64u; i += 32u) {
718
+ const uint tok = i / 8u, dd = i % 8u;
719
+ if (tokBase + tok < M)
720
+ atomic_fetch_add_explicit(&dh[(ulong)(tokBase + tok) * H + d0 + dd], cs[i], memory_order_relaxed);
721
+ }
722
+ simdgroup_barrier(mem_flags::mem_threadgroup);
723
+ }
724
+ `;
725
+
726
+ // Apple-CCE coeff filter epsilon (phase-2 per-tile skip). DEFAULT OFF ("0") — the
727
+ // Apple-production shortcut skips vocab tiles whose whole coeff block < eps, which gave
728
+ // the OLD (slow, phase-2-dominated) SG kernel ~3.5×. On the steel kernel — whose phase-2
729
+ // is now QuantizedBlockLoader-fast — it is a BAD trade: measured e4b M=512 REAL it cut dh
730
+ // accuracy 0.66%→2.7% for only ~7% speedup (the win was already captured by the fast
731
+ // dequant). Opt in with MLX_BUN_CCE_BWD_FILTER_EPS=1e-5 only on genuinely peaked data.
732
+ const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "0";
733
+ // blockMax vocab-block early-exit eps (skip whole cold (token-block,vocab-block) programs
734
+ // — attacks BOTH phases). DEFAULT OFF ("0"). It is LOSSLESS when it fires (only skips
735
+ // blocks whose max prob < eps AND which lack the target → ~0 contribution), so it is the
736
+ // SAFE skip; but it only pays off on genuinely peaked real text where whole 8192-vocab
737
+ // blocks go cold (the M=512 synthetic probe found nothing cold → pure overhead). Opt in
738
+ // with MLX_BUN_CCE_BWD_BLOCK_EPS=1e-5 for long real-text training; needs blockMax supplied.
739
+ const BWD_BLOCK_EPS = process.env.MLX_BUN_CCE_BWD_BLOCK_EPS ?? "0";
740
+ const _bwdSgKernels = new Map<string, MetalKernel>();
741
+ function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): MetalKernel {
742
+ const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
743
+ const blockOn = Number.parseFloat(blockEps) > 0 ? 1 : 0;
744
+ const key = `${dimTiles}:${filterEps}:${blockEps}`;
745
+ let k = _bwdSgKernels.get(key);
746
+ if (!k) {
747
+ const inputNames = ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"];
748
+ if (blockOn) inputNames.push("blockMax"); // [M, NBLK] forward partial, for the skip
749
+ k = new MetalKernel({
750
+ name: "flash_cce_bwd_sg",
751
+ inputNames,
752
+ outputNames: ["dh"],
753
+ source: BWD_SG_SOURCE,
754
+ header: `#include <metal_simdgroup_matrix>\n#define TG ${TG}\n#define BB ${BB_BWD_SG}\n#define SGN ${SGN}\n#define DIMTILES ${dimTiles}\n#define BVP ${SG_BV}\n#define BDP ${SG_BD}\n#define CCE_BWD_FILTER ${filterOn}\n#define CCE_BWD_FILTER_EPS ${filterEps}f\n#define CCE_BWD_BLOCKSKIP ${blockOn}\n#define CCE_BWD_BLOCK_EPS ${blockEps}f\n`,
755
+ ensureRowContiguous: true,
756
+ atomicOutputs: true,
757
+ });
758
+ _bwdSgKernels.set(key, k);
759
+ }
760
+ return k;
761
+ }
762
+
763
+ // STEEL backward (the production path): MLX's verbatim BlockMMA GEMM (steel-qmm-
764
+ // header.ts) for BOTH phases of dh = Σ_v coeff_v·dequant(W_v), at MLX speed AND
765
+ // [M,V]-free. Per (token-block z = BM=8 tokens, vocab-block y) threadgroup:
766
+ // phase 1 — logit[8,32] = X[8,H] @ Wᵀ[H,32] via the SAME steel logit GEMM as the
767
+ // forward (mma_t + QuantizedBlockLoader, K-pipelined over H) → coeff epilogue.
768
+ // phase 2 — dh[8,H] += coeff[8,32] @ W[32,H], accumulated over the vocab tiles in a
769
+ // persistent register accumulator D = MMATile<1, H/32>. To keep threadgroup
770
+ // bounded (the full Wd[32,H] is 320 KB at e4b), H is tiled into HTw=128 columns:
771
+ // per H-tile a TEMP BlockMMA over a bounded Wd[32,HTw] dequant, whose Ctile is
772
+ // accumulated into D's frags LANE-LOCALLY (frag += frag — no get_coord math, the
773
+ // per-lane element mapping is identical between temp and D). dh atomic-added
774
+ // across the NBLK vocab-block programs. Validated dh ~1e-4 rel vs autograd.
775
+ // (Design + parity: scripts/experiments/steel-bwd-htile-test.ts.)
776
+ // PHASE-2 DEQUANT: the Wd[32,HTw] staging uses MLX's vectorized+fused
777
+ // QuantizedBlockLoader (the SAME loader the forward uses), NOT a manual scalar
778
+ // dequant. The wrinkle: phase-2's GEMM contracts over vocab (K=32) and outputs H
779
+ // (the dim the quant groups run along), so the loader must run with reduction_dim=0
780
+ // and one group per H-tile → HTw is pinned to group_size (so each H-tile is exactly
781
+ // one quant group, satisfying BCOLS<=group_size && group_size%BCOLS==0). The loader
782
+ // is built fresh per H-tile (correct src/scales/biases group offsets) + a single
783
+ // load_unsafe (no next()). TN = HTw/32 = group_size/32 frags per H-tile. The earlier
784
+ // manual scalar dequant (recomputing grp/scale/bias per element) was the bottleneck;
785
+ // this matches the forward's qmm staging. (Validated: steel-bwd-htile-test.ts.)
786
+ // HTw (the phase-2 H-tile width) is pinned to the quant group_size in-kernel (HTw=GS_T),
787
+ // so it is not a standalone constant here — the JS side derives NHT_T/NHT4_T from gs.
788
+ const BWD_STEEL_SOURCE = String.raw`
789
+ const uint M=shape[0], V=shape[1], H=shape[2], NBLK=shape[7], WPR=shape[5];
790
+ const uint tokBlk=threadgroup_position_in_grid.z, vb=threadgroup_position_in_grid.y;
791
+ // HTw is pinned to the quant group_size so each phase-2 H-tile is exactly one group
792
+ // (lets QuantizedBlockLoader stage Wd with reduction_dim=0). TNv = HTw/32 frags/tile;
793
+ // NHTc = H/HTw tiles; total accumulator frags = NHTc*TNv = H/32 (= NHT4_T from JS).
794
+ constexpr int BM=8,BNv=32,BK=32,WM=1,WN=4,gs=GS_T,bts=BITS_T,HTw=GS_T,NHTc=NHT_T,TNv=GS_T/32;
795
+ constexpr int pf=get_pack_factor<bts,8>(), bpp=get_bytes_per_pack<bts>(), BKp=BK+16/sizeof(float);
796
+ threadgroup float Xs[BM*BKp]; threadgroup float Ws[BNv*BKp]; threadgroup float Ls[BM*BNv];
797
+ threadgroup float coeffS[BM*BNv]; threadgroup float Wd[BNv*HTw];
798
+ threadgroup float redMax[4]; threadgroup float tileMaxSh; // coeff-filter reduction scratch
799
+ using lmma_t=mlx::steel::BlockMMA<float,float,BM,BNv,BK,WM,WN,false,true,BKp,BKp>;
800
+ using dmma_t=mlx::steel::BlockMMA<float,float,BM,HTw,BNv,WM,WN,false,false,BNv,HTw>;
801
+ using lx_t=mlx::steel::BlockLoader<float,BM,BK,BKp,1,WM*WN*SIMD_SIZE>;
802
+ using lw_t=QuantizedBlockLoader<float,BNv,BK,BKp,1,WM*WN*SIMD_SIZE,gs,bts>;
803
+ // phase-2 W loader: BROWS=32 vocab (GEMM K), BCOLS=HTw=group_size (one group/tile),
804
+ // reduction_dim=0, dst_ld=HTw — built fresh per H-tile, single load_unsafe.
805
+ using lw2_t=QuantizedBlockLoader<float,BNv,HTw,HTw,0,WM*WN*SIMD_SIZE,gs,bts>;
806
+ const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup, lid=thread_position_in_threadgroup.x;
807
+ const float cap=capv[0];
808
+ const int Hh=(int)H, K_w=Hh*bpp/pf, K_g=Hh/gs, tokBase=(int)tokBlk*BM;
809
+ const int blockV=((int)V+(int)NBLK-1)/(int)NBLK, nStart=(int)vb*blockV, nEnd=min(nStart+blockV,(int)V);
810
+ const short num_els=(short)min(BM,(int)M-tokBase);
811
+ short2 sc2=mlx::steel::BaseMMAFrag<float,8,8>::get_coord(sl); const short fm=sc2.y, fn=sc2.x;
812
+ const uint perWord=32u/bts, qmask=(bts==32u)?0xffffffffu:((1u<<bts)-1u);
813
+
814
+ #if CCE_BWD_BLOCKSKIP
815
+ // Apple-CCE vocab-block early exit: this (token-block, vocab-block) program does ~0
816
+ // work if, for EVERY token, the block's max softmax prob exp(blockMax−lse) is below
817
+ // eps AND the target isn't in this block (no onehot term lives here). On peaked
818
+ // logits ~all but a couple of vocab blocks per token are cold → skips BOTH phases.
819
+ // Uniform across the threadgroup (all threads read the same blockMax/lse/targets), so
820
+ // the early return can't desync barriers; it is AFTER the threadgroup decls (required).
821
+ bool warm = false;
822
+ for (int b=0; b<BM; ++b){ int tk=tokBase+b; if (tk<(int)M){
823
+ if (exp(blockMax[tk*(int)NBLK + (int)vb] - lse[tk]) >= CCE_BWD_BLOCK_EPS) warm=true;
824
+ int tg2=(int)targets[tk]; if (tg2>=nStart && tg2<nEnd) warm=true; } }
825
+ if (!warm) return;
826
+ #endif
827
+
828
+ mlx::steel::MMATile<float,1,NHT4_T> D; // persistent dh accumulator (H/32 frags/sg)
829
+
830
+ for (int v0=nStart; v0<nEnd; v0+=BNv){
831
+ // phase 1: logit tile → coeff
832
+ lmma_t lmma(sg,sl);
833
+ lx_t loader_x(h+(int64_t)tokBase*Hh, Hh, Xs, sg, sl);
834
+ lw_t loader_w((const device uint8_t*)wp+v0*K_w, scales+v0*K_g, biases+v0*K_g, Hh, Ws, sg, sl);
835
+ for(int k=0;k<Hh;k+=BK){ threadgroup_barrier(mem_flags::mem_threadgroup);
836
+ if(num_els<BM) loader_x.load_safe(short2(BK,num_els)); else loader_x.load_unsafe();
837
+ loader_w.load_unsafe();
838
+ threadgroup_barrier(mem_flags::mem_threadgroup); lmma.mma(Xs,Ws); loader_x.next(); loader_w.next(); }
839
+ threadgroup_barrier(mem_flags::mem_threadgroup);
840
+ lmma.Ctile.store<float,WM,WN,BNv,1>(Ls + lmma.sm*BNv + lmma.sn);
841
+ threadgroup_barrier(mem_flags::mem_threadgroup);
842
+ if (lid<(uint)BM){ uint b=lid;
843
+ if(tokBase+(int)b<(int)M){ const float lseb=lse[tokBase+b], gb=gv[tokBase+b]; const uint tgt=targets[tokBase+b];
844
+ for(int v=0;v<BNv;++v){ int n=v0+v; float logit=Ls[b*BNv+v],sech2=1.0f; if(cap>0.0f){float th=tanh(logit/cap);logit=cap*th;sech2=1.0f-th*th;} float sm=exp(logit-lseb); float c=(n<nEnd)?gb*(((uint)n==tgt?1.0f:0.0f)-sm):0.0f; if(cap>0.0f)c*=sech2; coeffS[b*BNv+v]=c; } }
845
+ else { for(int v=0;v<BNv;++v) coeffS[b*BNv+v]=0.0f; } }
846
+ threadgroup_barrier(mem_flags::mem_threadgroup);
847
+
848
+ #if CCE_BWD_FILTER
849
+ // Apple-CCE coeff filter: softmax mass concentrates on a few tokens, so coeff ≈ 0 for
850
+ // almost every vocab tile even within a warm block → skip the whole phase-2 (Wd staging
851
+ // + temp BlockMMA × NHT) when the entire coeff[8,32] tile is below eps. Parallel max
852
+ // over the 256 coeff elems (each thread 2, simd_max per simdgroup, combine 4). Uniform
853
+ // continue (all threads read the same tileMaxSh) so there is no barrier desync.
854
+ { float lm=max(fabs(coeffS[lid]), fabs(coeffS[lid+128u])); lm=simd_max(lm);
855
+ if (sl==0u) redMax[sg]=lm; }
856
+ threadgroup_barrier(mem_flags::mem_threadgroup);
857
+ if (lid==0u) tileMaxSh=max(max(redMax[0],redMax[1]),max(redMax[2],redMax[3]));
858
+ threadgroup_barrier(mem_flags::mem_threadgroup);
859
+ if (tileMaxSh < CCE_BWD_FILTER_EPS) continue;
860
+ #endif
861
+
862
+ // phase 2: per H-tile stage Wd[32,HTw] via QuantizedBlockLoader, temp BlockMMA, accum into D
863
+ for (int ht=0; ht<NHTc; ++ht){
864
+ const int hbase=ht*HTw;
865
+ // src at byte addr of W[v0, hbase]; scales/biases at group ht of vocab row v0.
866
+ lw2_t loader_w2((const device uint8_t*)wp + (int64_t)v0*K_w + (int64_t)hbase*bpp/pf,
867
+ scales + (int64_t)v0*K_g + ht, biases + (int64_t)v0*K_g + ht, Hh, Wd, sg, sl);
868
+ loader_w2.load_unsafe();
869
+ threadgroup_barrier(mem_flags::mem_threadgroup);
870
+ dmma_t tmp(sg,sl); tmp.mma(coeffS, Wd);
871
+ for(int j=0;j<TNv;++j) D.frag_at(0, ht*TNv+j) += tmp.Ctile.frag_at(0,j);
872
+ threadgroup_barrier(mem_flags::mem_threadgroup);
873
+ }
874
+ }
875
+
876
+ // store D → dh (atomic-add across the NBLK vocab-block programs)
877
+ for(int gj=0; gj<NHT4_T; ++gj){
878
+ int ht=gj/TNv, j=gj%TNv; int row=tokBase+(int)fm; int col=ht*HTw + 8*(int)sg + (int)fn + j*32;
879
+ if(row<(int)M && col+1<=Hh){ thread auto& fr=D.frag_at(0,gj);
880
+ atomic_fetch_add_explicit(&dh[(int64_t)row*Hh + col], fr[0], memory_order_relaxed);
881
+ atomic_fetch_add_explicit(&dh[(int64_t)row*Hh + col+1], fr[1], memory_order_relaxed); }
882
+ }
883
+ `;
884
+ const BWD_STEEL_BM = 8;
885
+ const _bwdSteelKernels = new Map<string, MetalKernel>();
886
+ function bwdSteelKernel(gs: number, bits: number, H: number, filterEps: string, blockEps: string): MetalKernel {
887
+ // HTw == group_size (one quant group per phase-2 H-tile → QuantizedBlockLoader).
888
+ const nht = H / gs; // # H-tiles
889
+ const nFrags = nht * (gs / 32); // total accumulator frags/sg = H/32 (NHT4_T name kept)
890
+ // Apple-CCE skips: coeff filter (phase-2 per-tile) + blockMax vocab-block early exit
891
+ // (whole-program). Both compile out when eps=0 → zero overhead, exact dh (the parity
892
+ // gate). blockMax skip needs the forward's [M,NBLK] blockMax as an extra input.
893
+ const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
894
+ const blockOn = Number.parseFloat(blockEps) > 0 ? 1 : 0;
895
+ const key = `${gs}:${bits}:${H}:${filterEps}:${blockEps}`;
896
+ let k = _bwdSteelKernels.get(key);
897
+ if (!k) {
898
+ const inputNames = ["h", "wp", "scales", "biases", "targets", "lse", "gv", "shape", "capv"];
899
+ if (blockOn) inputNames.push("blockMax"); // [M, NBLK] forward partial, for the vocab-block skip
900
+ k = new MetalKernel({
901
+ name: "flash_cce_bwd_steel",
902
+ inputNames,
903
+ outputNames: ["dh"],
904
+ source: BWD_STEEL_SOURCE,
905
+ header: `${STEEL_QMM_HEADER}\n#define GS_T ${gs}\n#define BITS_T ${bits}\n#define NHT_T ${nht}\n#define NHT4_T ${nFrags}\n#define CCE_BWD_FILTER ${filterOn}\n#define CCE_BWD_FILTER_EPS ${filterEps}f\n#define CCE_BWD_BLOCKSKIP ${blockOn}\n#define CCE_BWD_BLOCK_EPS ${blockEps}f\n`,
906
+ ensureRowContiguous: true,
907
+ atomicOutputs: true,
908
+ });
909
+ _bwdSteelKernels.set(key, k);
910
+ }
911
+ return k;
912
+ }
913
+
914
+ let _fwdKernel: MetalKernel | null = null;
915
+ function fwdKernel(): MetalKernel {
916
+ if (!_fwdKernel) {
917
+ _fwdKernel = new MetalKernel({
918
+ name: "flash_cce_fwd",
919
+ inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
920
+ outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
921
+ source: FWD_SOURCE,
922
+ header: `#define TG ${TG}\n#define BLOCK_B ${BLOCK_B}\n`,
923
+ ensureRowContiguous: true,
924
+ });
925
+ }
926
+ return _fwdKernel;
927
+ }
928
+
929
+ function u32(values: number[]): MlxArray {
930
+ // COPY into an mlx-owned array (fromBytesCopy = mlx_array_new_data), NOT the
931
+ // pinning fromView. These tiny kernel-arg arrays (shape, targets) feed a LAZY
932
+ // kernel and are disposed before it evaluates; pinning leaks one host buffer
933
+ // per call, never released (unbounded `pinned` map + a deferred native
934
+ // use-after-free under memory pressure — the seg/accum crash). A few-int copy
935
+ // is free; correctness is not.
936
+ return MlxArray.fromBytesCopy(new Uint8Array(new Uint32Array(values).buffer), [values.length], Dtype.uint32);
937
+ }
938
+
939
+ /** Flash-CCE forward: per-token response log-prob [M] (f32) + lse [M] (f32),
940
+ * computing logits in-kernel from the quantized head — no [M,V] materialized.
941
+ * `hResp` is [M, hidden] (any float dtype; cast to f32 for the kernel). `targets`
942
+ * are the response target token ids [M]. Caller owns the returned arrays. */
943
+ // STEEL forward: MLX's verbatim quantized BlockMMA GEMM (steel-qmm-header.ts) for the
944
+ // logit tile (BM=BN=BK=32, WM=WN=2 → 4 simdgroups × 16×16 register tiles, BK_padded),
945
+ // with ONLY the ORPO epilogue diverging — softcap + online-softmax over each BN vocab
946
+ // tile (no [M,V]). Per (token-block z, vocab-block y) threadgroup; partials [M,NBLK]
947
+ // merged in MLX. Same logit math as quantizedMatmul (validated 1e-5) at MLX speed
948
+ // (e4b logits 101 ms vs MLX 106 ms; 8.4× our old hand-rolled forward).
949
+ const FWD_STEEL_SOURCE = String.raw`
950
+ const uint M=shape[0], V=shape[1], H=shape[2], NBLK=shape[7];
951
+ const uint tokBlk=threadgroup_position_in_grid.z, vb=threadgroup_position_in_grid.y;
952
+ constexpr int BM=32,BK=32,BN=32,WM=2,WN=2,gs=GS_T,bts=BITS_T;
953
+ constexpr int pf=get_pack_factor<bts,8>(), bpp=get_bytes_per_pack<bts>(), BKp=BK+16/sizeof(float);
954
+ threadgroup float Xs[BM*BKp]; threadgroup float Ws[BN*BKp]; threadgroup float Ls[BM*BN];
955
+ threadgroup float tgMax[BM], tgSum[BM], tgTgt[BM]; threadgroup uint tgTid[BM];
956
+ using mma_t=mlx::steel::BlockMMA<float,float,BM,BN,BK,WM,WN,false,true,BKp,BKp>;
957
+ using lx_t=mlx::steel::BlockLoader<float,BM,BK,BKp,1,WM*WN*SIMD_SIZE>;
958
+ using lw_t=QuantizedBlockLoader<float,BN,BK,BKp,1,WM*WN*SIMD_SIZE,gs,bts>;
959
+ const uint sg=simdgroup_index_in_threadgroup, sl=thread_index_in_simdgroup, lid=thread_position_in_threadgroup.x;
960
+ const float cap=capv[0];
961
+ const int Hh=(int)H, K_w=Hh*bpp/pf, K_g=Hh/gs;
962
+ const int blockV=((int)V+(int)NBLK-1)/(int)NBLK;
963
+ const int nStart=(int)vb*blockV, nEnd=min(nStart+blockV,(int)V), tokBase=(int)tokBlk*BM;
964
+ const short num_els=(short)min(BM,(int)M-tokBase);
965
+ if (lid<(uint)BM){ tgMax[lid]=-INFINITY; tgSum[lid]=0.0f; tgTgt[lid]=0.0f; tgTid[lid]=(tokBase+(int)lid<(int)M)?targets[tokBase+lid]:0xffffffffu; }
966
+ threadgroup_barrier(mem_flags::mem_threadgroup);
967
+ for (int v0=nStart; v0<nEnd; v0+=BN){
968
+ mma_t mma_op(sg,sl);
969
+ lx_t loader_x(h+(int64_t)tokBase*Hh, Hh, Xs, sg, sl);
970
+ lw_t loader_w((const device uint8_t*)wp+v0*K_w, scales+v0*K_g, biases+v0*K_g, Hh, Ws, sg, sl);
971
+ for(int k=0;k<Hh;k+=BK){ threadgroup_barrier(mem_flags::mem_threadgroup);
972
+ if(num_els<BM) loader_x.load_safe(short2(BK,num_els)); else loader_x.load_unsafe();
973
+ loader_w.load_unsafe();
974
+ threadgroup_barrier(mem_flags::mem_threadgroup); mma_op.mma(Xs,Ws); loader_x.next(); loader_w.next(); }
975
+ threadgroup_barrier(mem_flags::mem_threadgroup);
976
+ mma_op.Ctile.store<float,WM,WN,BN,1>(Ls + mma_op.sm*BN + mma_op.sn);
977
+ threadgroup_barrier(mem_flags::mem_threadgroup);
978
+ if (lid<(uint)BM && tokBase+(int)lid<(int)M){ uint b=lid;
979
+ for (int v=0; v<BN; ++v){ int n=v0+v; if(n>=nEnd) break;
980
+ float logit=Ls[b*BN+v]; if(cap>0.0f) logit=cap*tanh(logit/cap);
981
+ float nm=max(tgMax[b],logit); tgSum[b]=tgSum[b]*exp(tgMax[b]-nm)+exp(logit-nm); tgMax[b]=nm;
982
+ if((uint)n==tgTid[b]) tgTgt[b]=logit; }
983
+ }
984
+ threadgroup_barrier(mem_flags::mem_threadgroup);
985
+ }
986
+ if (lid<(uint)BM && tokBase+(int)lid<(int)M){ uint t=tokBase+lid;
987
+ blockMax_out[t*NBLK+vb]=tgMax[lid]; blockSum_out[t*NBLK+vb]=tgSum[lid]; blockTgt_out[t*NBLK+vb]=tgTgt[lid]; }
988
+ `;
989
+ const STEEL_BM = 32;
990
+ const _fwdSteelKernels = new Map<string, MetalKernel>();
991
+ function fwdSteelKernel(gs: number, bits: number): MetalKernel {
992
+ const key = `${gs}:${bits}`;
993
+ let k = _fwdSteelKernels.get(key);
994
+ if (!k) {
995
+ k = new MetalKernel({
996
+ name: "flash_cce_fwd_steel",
997
+ inputNames: ["h", "wp", "scales", "biases", "targets", "shape", "capv"],
998
+ outputNames: ["blockMax_out", "blockSum_out", "blockTgt_out"],
999
+ source: FWD_STEEL_SOURCE,
1000
+ header: `${STEEL_QMM_HEADER}\n#define GS_T ${gs}\n#define BITS_T ${bits}\n`,
1001
+ ensureRowContiguous: true,
1002
+ });
1003
+ _fwdSteelKernels.set(key, k);
1004
+ }
1005
+ return k;
1006
+ }
1007
+
1008
+ export function flashCceForward(
1009
+ hResp: MlxArray, head: FlashCceHead, targets: number[],
1010
+ ): { logp: MlxArray; lse: MlxArray; blockMax: MlxArray } {
1011
+ const M = hResp.shape[0]!;
1012
+ const H = hResp.shape[1]!;
1013
+ const V = head.scales.shape[0]!;
1014
+ const GR = head.scales.shape[1]!;
1015
+ const GS = head.groupSize;
1016
+ const WPR = head.w.shape[1]!;
1017
+ if (H > H_MAX) throw new Error(`flashCceForward: hidden ${H} > H_MAX ${H_MAX}`);
1018
+ if (GR * GS !== H) throw new Error(`flashCceForward: GR*GS ${GR * GS} != H ${H}`);
1019
+
1020
+ // Vocab blocks sized so each threadgroup handles a bounded slice (≈ VOCAB_BLOCK
1021
+ // rows) — independent of V, so a single threadgroup never runs the whole vocab
1022
+ // (the e4b watchdog fix). NBLK = ceil(V / VOCAB_BLOCK).
1023
+ const NBLK = Math.max(1, Math.ceil(V / VOCAB_BLOCK));
1024
+ const hF = hResp.dtype === Dtype.float32 ? hResp : hResp.astype(Dtype.float32);
1025
+ const scF = head.scales.dtype === Dtype.float32 ? head.scales : head.scales.astype(Dtype.float32);
1026
+ const biF = head.biases.dtype === Dtype.float32 ? head.biases : head.biases.astype(Dtype.float32);
1027
+ const tgtArr = u32(targets);
1028
+ const shape = u32([M, V, H, GS, head.bits, WPR, GR, NBLK]);
1029
+ const capv = MlxArray.fromFloat32(new Float32Array([head.softcap ?? 0]), [1]);
1030
+
1031
+ // DEFAULT = simdgroup_matrix kernel with efficient dequant staging (848 ms, the
1032
+ // fastest measured — matmul throughput once the staging loads stopped bounding
1033
+ // it). MLX_BUN_CCE_SCALAR=1 → one-thread-per-row scalar+coalesced-h (1097 ms;
1034
+ // also the H % SG_BD != 0 fallback). MLX_BUN_CCE_LANE=1 → lane-sliced (2.6×
1035
+ // slower; scatters the h broadcast — recorded dead end).
1036
+ // DEFAULT = STEEL (verbatim MLX BlockMMA GEMM, 8.4× the old SG forward) when the
1037
+ // tiling is clean (H,V,blockV all % 32). MLX_BUN_CCE_NOSTEEL=1 falls back to the
1038
+ // old kernels (simdgroup / scalar / lane).
1039
+ const blockV0 = Math.ceil(V / NBLK);
1040
+ const useSteel = process.env.MLX_BUN_CCE_NOSTEEL !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
1041
+ const useSimd = !useSteel && USE_SIMDGROUP_FWD && H % SG_BD === 0;
1042
+ const useLane = !useSteel && !useSimd && process.env.MLX_BUN_CCE_LANE === "1" && WPR % 32 === 0;
1043
+ const kernel = useSteel ? fwdSteelKernel(GS, head.bits) : useSimd ? fwdSgKernel() : useLane ? fwdLaneKernel() : fwdKernel();
1044
+ const tokTile = useSteel ? STEEL_BM : useSimd ? SG_BB : BLOCK_B;
1045
+ // steel + simdgroup read h as [M, H] (M on axis 0); lane + scalar read h TRANSPOSED
1046
+ // [H, M] (M on axis 1) — they stage E into threadgroup themselves.
1047
+ const transposedH = !useSteel && !useSimd;
1048
+ const hIn = transposedH ? ops.transposeAxes(hF, [1, 0]) : hF;
1049
+ let bMax: MlxArray | undefined, bSum: MlxArray | undefined, bTgt: MlxArray | undefined;
1050
+ try {
1051
+ const outs = kernel.apply([hIn, head.w, scF, biF, tgtArr, shape, capv], {
1052
+ // Derive outputs + grid from the input shapes (hResp = inputs[0]) so the
1053
+ // kernel composes inside an mx.compile'd closure: [M,NBLK] f32 partials, one
1054
+ // threadgroup per (token-block, vocab-block). NBLK is fixed (from V).
1055
+ outputShapeFn: (ins) => {
1056
+ const m = transposedH ? ins[0]!.shape[1]! : ins[0]!.shape[0]!;
1057
+ return [
1058
+ { shape: [m, NBLK], dtype: Dtype.float32 },
1059
+ { shape: [m, NBLK], dtype: Dtype.float32 },
1060
+ { shape: [m, NBLK], dtype: Dtype.float32 },
1061
+ ];
1062
+ },
1063
+ grid: (ins) => [TG, NBLK, Math.ceil((transposedH ? ins[0]!.shape[1]! : ins[0]!.shape[0]!) / tokTile)],
1064
+ threadGroup: [TG, 1, 1],
1065
+ });
1066
+ [bMax, bSum, bTgt] = outs as [MlxArray, MlxArray, MlxArray];
1067
+ // Cross-vocab-block merge in MLX: gMax = max_b; gSum = Σ_b sum_b·exp(max_b−gMax);
1068
+ // lse = gMax + log(gSum); logp = target − lse. (Apple CCE does this with a
1069
+ // locked atomic logaddexp in-kernel; the partials are tiny [M,NBLK] so an MLX
1070
+ // reduction is simpler and just as exact.)
1071
+ const gMax = ops.maxAxis(bMax, 1, false); // [M]
1072
+ const gMaxCol = ops.reshape(gMax, [M, 1]);
1073
+ // Bind every nested intermediate so its MlxArray wrapper gets disposed — a
1074
+ // bare `ops.exp(ops.sub(...))` leaks the inner `sub` (stays live in `active`
1075
+ // until the GC finalizer backstop), and this runs TWICE per step (chosen +
1076
+ // rejected). gMax was also missing from the dispose list below.
1077
+ const sub = ops.sub(bMax, gMaxCol);
1078
+ const shifted = ops.exp(sub); // [M, NBLK]
1079
+ const prod = ops.mul(bSum, shifted);
1080
+ const gSum = ops.sumAxis(prod, 1, false); // [M]
1081
+ const logG = ops.log(gSum);
1082
+ const lse = ops.add(gMax, logG); // [M]
1083
+ const tgt = ops.sumAxis(bTgt, 1, false); // [M] (owning block only is nonzero)
1084
+ const logp = ops.sub(tgt, lse);
1085
+ for (const a of [gMax, gMaxCol, sub, shifted, prod, gSum, logG, tgt]) a.dispose();
1086
+ const blockMax = bMax!; bMax = undefined; // hand off to caller (backward block-skip)
1087
+ return { logp, lse, blockMax };
1088
+ } finally {
1089
+ bMax?.dispose(); bSum?.dispose(); bTgt?.dispose();
1090
+ if (hIn !== hF) hIn.dispose();
1091
+ if (hF !== hResp) hF.dispose();
1092
+ if (scF !== head.scales) scF.dispose();
1093
+ if (biF !== head.biases) biF.dispose();
1094
+ tgtArr.dispose();
1095
+ shape.dispose();
1096
+ capv.dispose();
1097
+ }
1098
+ }
1099
+
1100
+ /** Flash-CCE backward: dh [M, hidden] (f32) = ∂(Σ_t cotangent_t · logp_t)/∂h.
1101
+ * `lse` is the forward's saved log-sum-exp [M]; `cot` is the per-token cotangent
1102
+ * ∂loss/∂logp_t [M]. Recomputes logits in-kernel (no [M,V]); writes dh directly
1103
+ * (no atomics). Caller owns the result. */
1104
+ export function flashCceBackward(
1105
+ hResp: MlxArray, head: FlashCceHead, targets: number[], lse: MlxArray, cot: number[],
1106
+ // Apple-CCE coeff filter eps. Default ON (BWD_FILTER_EPS) for the ORPO training
1107
+ // path — a pretrained model's softmax is peaked, so skipping near-zero-coeff rows
1108
+ // is near-lossless and ~3.5× faster. Pass "0" for exact gradients (the parity gate
1109
+ // does this, since synthetic-flat softmax has no negligible rows to skip).
1110
+ filterEps: string = BWD_FILTER_EPS,
1111
+ // The forward's `blockMax [M, NBLK]` enables the vocab-block early exit (skips
1112
+ // whole cold programs — attacks phase 1). Omit (or set blockEps "0") for exact.
1113
+ blockMax?: MlxArray, blockEps: string = BWD_BLOCK_EPS,
1114
+ ): MlxArray {
1115
+ const M = hResp.shape[0]!;
1116
+ const H = hResp.shape[1]!;
1117
+ const V = head.scales.shape[0]!;
1118
+ const GR = head.scales.shape[1]!;
1119
+ const GS = head.groupSize;
1120
+ const WPR = head.w.shape[1]!;
1121
+ if (H > H_MAX) throw new Error(`flashCceBackward: hidden ${H} > H_MAX ${H_MAX}`);
1122
+ if (H % TG !== 0) throw new Error(`flashCceBackward: hidden ${H} not divisible by TG ${TG}`);
1123
+
1124
+ const hF = hResp.dtype === Dtype.float32 ? hResp : hResp.astype(Dtype.float32);
1125
+ const scF = head.scales.dtype === Dtype.float32 ? head.scales : head.scales.astype(Dtype.float32);
1126
+ const biF = head.biases.dtype === Dtype.float32 ? head.biases : head.biases.astype(Dtype.float32);
1127
+ const lseF = lse.dtype === Dtype.float32 ? lse : lse.astype(Dtype.float32);
1128
+ const NBLK = Math.max(1, Math.ceil(V / VOCAB_BLOCK));
1129
+ const tgtArr = u32(targets);
1130
+ const gv = MlxArray.fromFloat32(new Float32Array(cot), [M]);
1131
+ const shape = u32([M, V, H, GS, head.bits, WPR, GR, NBLK]);
1132
+ const capv = MlxArray.fromFloat32(new Float32Array([head.softcap ?? 0]), [1]);
1133
+
1134
+ // STEEL is the DEFAULT (verbatim MLX BlockMMA GEMM for both phases, [M,V]-free) when
1135
+ // the tiling is clean (H,V,blockV all %32 AND H % HTw for the H-tiled accumulator).
1136
+ // MLX_BUN_CCE_BWD_NOSTEEL=1 falls back to the old kernels. The steel backward now
1137
+ // carries the SAME Apple-CCE skips as the SG kernel — coeff filter (phase-2 per-tile)
1138
+ // + blockMax vocab-block early exit (whole-program) — both compiled out at eps=0
1139
+ // (exact, the parity gate) and default-on for training (peaked softmax → ~all vocab cold).
1140
+ const blockV0 = Math.ceil(V / NBLK);
1141
+ // HTw == group_size for the phase-2 QuantizedBlockLoader (one group per H-tile); needs
1142
+ // H % GS == 0 (always, GR*GS=H) and GS % 32 == 0 (TNv = GS/32 frags per H-tile integral).
1143
+ const useSteel = process.env.MLX_BUN_CCE_BWD_NOSTEEL !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0 && H % GS === 0 && GS % 32 === 0;
1144
+ // simdgroup_matrix phase-2 GEMM is the next tier (5512→3913 ms, 1.41×) when the H
1145
+ // tiling is clean; MLX_BUN_CCE_BWD_SCALAR=1 forces the float4 scalar kernel.
1146
+ const useSg = !useSteel && process.env.MLX_BUN_CCE_BWD_SCALAR !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
1147
+ // Block-skip is on (either kernel) only when blockMax is supplied AND blockEps>0 (the
1148
+ // kernel reads it as an extra input). The coeff filter needs no extra input (just eps).
1149
+ const useBlockSkip = (useSteel || useSg) && blockMax != null && Number.parseFloat(blockEps) > 0;
1150
+ const bmF = useBlockSkip ? (blockMax!.dtype === Dtype.float32 ? blockMax! : blockMax!.astype(Dtype.float32)) : null;
1151
+ const kernel = useSteel
1152
+ ? bwdSteelKernel(GS, head.bits, H, filterEps, useBlockSkip ? blockEps : "0")
1153
+ : useSg ? bwdSgKernel(H / SGN / 8, filterEps, useBlockSkip ? blockEps : "0") : bwdKernel(Math.ceil(H / TG));
1154
+ const tokTile = useSteel ? BWD_STEEL_BM : useSg ? BB_BWD_SG : BLOCK_B_BWD;
1155
+ const baseInputs = [hF, head.w, scF, biF, tgtArr, lseF, gv, shape, capv];
1156
+ try {
1157
+ const outs = kernel.apply(useBlockSkip ? [...baseInputs, bmF!] : baseInputs, {
1158
+ outputShapeFn: (ins) => [{ shape: [ins[0]!.shape[0]!, H], dtype: Dtype.float32 }], // dh [M, hidden]
1159
+ grid: (ins) => [TG, NBLK, Math.ceil(ins[0]!.shape[0]! / tokTile)],
1160
+ threadGroup: [TG, 1, 1],
1161
+ initValue: 0, // dh starts at 0; vocab-block programs atomic-add into it
1162
+ });
1163
+ return outs[0]!;
1164
+ } finally {
1165
+ if (hF !== hResp) hF.dispose();
1166
+ if (bmF && bmF !== blockMax) bmF.dispose();
1167
+ if (scF !== head.scales) scF.dispose();
1168
+ if (biF !== head.biases) biF.dispose();
1169
+ if (lseF !== lse) lseF.dispose();
1170
+ tgtArr.dispose();
1171
+ gv.dispose();
1172
+ shape.dispose();
1173
+ capv.dispose();
1174
+ }
1175
+ }