mlx-bun 0.0.4

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,520 @@
1
+ // Segmented backward: long-context LoRA SFT that streams the backward pass
2
+ // segment-by-segment so only ONE segment's activations are ever live, instead
3
+ // of holding every layer's recompute activations at once (the spike that makes
4
+ // naive gradient checkpointing crash at long context). Design + proof:
5
+ // docs/design/segmented-backward-training.md (mechanism validated on a toy in
6
+ // scripts/ckpt-mem-test.ts, SEG mode: 0.000% grad error, peak below per-layer
7
+ // checkpointing; bit-exact on MiniCPM5 under flash attention).
8
+ //
9
+ // Mechanism (B=1 SFT only — the responseOnlyCe path):
10
+ // 1. Forward, saving boundaries. Run the layer stack; at each segment edge
11
+ // materialize (eval) and DETACH (copy to a graph-free leaf) the residual
12
+ // stream hidden into a boundary. Intra-segment interiors are discarded; the
13
+ // boundaries are small ([1, T, hidden] per edge).
14
+ // 2. Loss head. finalNorm + LM head + response-only CE over the LAST boundary,
15
+ // differentiated w.r.t. that boundary -> the loss value and dh_out, the
16
+ // gradient of the loss w.r.t. the final residual stream.
17
+ // 3. Backward, reverse over segments. For each segment k (last -> first) an
18
+ // mlx_vjp of `segment_forward(boundary_k, ...LoRA)` with the cotangent dh_out
19
+ // returns [dh_in, ...dLoRA_segment]: dh_in seeds segment k-1, the LoRA grads
20
+ // scatter into the global flat grad vector. Only segment k's activations are
21
+ // live. (A vjp is exactly reverse-mode AD: vjps = (df/dx)^T (.) dh.)
22
+ //
23
+ // Why mlx_vjp and not a surrogate-loss value_and_grad: the equivalent
24
+ // `value_and_grad(sum(stop_grad(dh) (.) segment_forward(...)))` is numerically
25
+ // identical but LEAKS ~one activation buffer per segment per step at the mlx
26
+ // level (measured; not GC/cache/synchronize/reuse reclaimable — see
27
+ // docs/design/segmented-backward-training.md §9.4). mlx_vjp takes the cotangent
28
+ // directly, needs no surrogate, and does not leak.
29
+ //
30
+ // IMPORTANT lifetime fact: mlx `eval` does NOT detach — an eval'd array retains
31
+ // its upstream graph, so each forward boundary must be copied into a fresh leaf
32
+ // (detachLeaf) or it keeps a whole layer-stack's activations alive (~0.1 GB per
33
+ // 4-layer segment of within-step retention otherwise). The vjp objects are built
34
+ // ONCE and reused across steps; their closures read the per-step batch from a
35
+ // mutable holder.
36
+
37
+ import { MlxArray } from "../mlx/array";
38
+ import * as ops from "../mlx/ops";
39
+ import { Vjp } from "../mlx/autograd";
40
+ import type { MiniCPM5Model } from "../model/minicpm5";
41
+ import type { Gemma4Model } from "../model/gemma4";
42
+ import type { Cache, Mask, SharedKv } from "../model/gemma4-base";
43
+ import { TrainingCache } from "./forward";
44
+ import { responseOnlyCe } from "./loss";
45
+ import type { TrainableLora } from "./lora-params";
46
+ import { type SftBatch } from "./dataset";
47
+
48
+ /** Split `nLayers` into contiguous `[lo, hi)` ranges of at most `segSize`
49
+ * layers each (the last range may be shorter). `segSize` is the memory knob:
50
+ * fewer layers per segment -> lower peak, ~fixed total recompute time. */
51
+ export function planSegmentsBySize(nLayers: number, segSize: number): [number, number][] {
52
+ if (segSize < 1) throw new Error(`planSegmentsBySize: segSize must be >= 1 (got ${segSize})`);
53
+ const ranges: [number, number][] = [];
54
+ for (let lo = 0; lo < nLayers; lo += segSize) ranges.push([lo, Math.min(lo + segSize, nLayers)]);
55
+ return ranges;
56
+ }
57
+
58
+ /** Detach `a` into a graph-free LEAF holding its exact (evaluated) bytes, then
59
+ * dispose `a`. mlx retains the upstream graph after `eval`, so a segment
60
+ * boundary (or a value_and_grad output) would otherwise keep its whole layer
61
+ * stack's activations alive. Copying the bytes into a fresh leaf frees that
62
+ * graph. The leaf is a valid differentiable input for the per-segment vag. */
63
+ function detachLeaf(a: MlxArray): MlxArray {
64
+ // Force a row-major copy first: `rawBytes` reads the underlying buffer
65
+ // linearly, so a NON-contiguous input (e.g. a transposed donor K/V view) would
66
+ // be byte-scrambled. `ops.contiguous` is a no-op for already-contiguous arrays
67
+ // (hidden boundaries) and materializes row-major for strided views.
68
+ const c = ops.contiguous(a);
69
+ c.eval();
70
+ const leaf = MlxArray.fromBytesCopy(c.rawBytes(), c.shape, c.dtype);
71
+ c.dispose();
72
+ a.dispose();
73
+ return leaf;
74
+ }
75
+
76
+ /** Layer index a target's modulePath belongs to, or -1 (e.g. embedding/head). */
77
+ function targetLayer(modulePath: string): number {
78
+ const m = modulePath.match(/\.layers\.(\d+)\./);
79
+ return m ? Number(m[1]) : -1;
80
+ }
81
+
82
+ /** Reusable segmented-backward driver for one (model, lora, segmentation). Build
83
+ * ONCE per training run and call `.step(batch)` each iteration — the per-segment
84
+ * value_and_grads are created up front and reused (rebuilding them every step
85
+ * leaks at the mlx level). Returns the loss scalar and the LoRA gradients in
86
+ * `flatParams(lora)` order ([...A, ...B] over targets), drop-in for
87
+ * `ValueAndGrad.apply(flatParams(lora))`. Caller owns the returned arrays.
88
+ *
89
+ * The active adapter must be attached (attachForTraining) so the layer forward
90
+ * picks up the swapped LoRA leaves via loraState. B=1 SFT only. */
91
+ export class SegmentedBackward {
92
+ private readonly nLayers: number;
93
+ private readonly n: number; // number of LoRA targets
94
+ private readonly segIdxs: number[][]; // target indices per segment
95
+ private readonly caches: Cache[];
96
+ private readonly headVjp: Vjp;
97
+ private readonly segVjps: Vjp[];
98
+ private readonly one: MlxArray; // scalar cotangent 1.0 for the (scalar) head loss
99
+ // Per-step context the (reused) closures read.
100
+ private curBatch: SftBatch | null = null;
101
+ private disposed = false;
102
+
103
+ constructor(
104
+ private readonly model: MiniCPM5Model,
105
+ private readonly lora: TrainableLora,
106
+ private readonly ranges: [number, number][],
107
+ ) {
108
+ this.nLayers = model.layers.length;
109
+ const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
110
+ if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
111
+ throw new Error(`SegmentedBackward: ranges must cover [0, ${this.nLayers}) (got ${JSON.stringify(ranges)})`);
112
+
113
+ this.n = lora.targets.length;
114
+ const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
115
+ this.segIdxs = ranges.map(([lo, hi]) =>
116
+ layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
117
+ );
118
+ this.caches = Array.from({ length: this.nLayers }, () => new TrainingCache());
119
+ this.one = MlxArray.fromFloat32(new Float32Array([1]), []); // d(loss)/d(loss) = 1
120
+
121
+ // Loss head: vjp of finalNorm + responseOnlyCe over the last boundary leaf.
122
+ // outputs[0] = loss scalar; vjps[0] = dLoss/d(last residual stream) = dh.
123
+ this.headVjp = new Vjp((p) => {
124
+ const hn = model.finalNorm.forward(p[0]!);
125
+ const loss = responseOnlyCe(model, hn, this.curBatch!);
126
+ hn.dispose();
127
+ return [loss];
128
+ }, 1);
129
+
130
+ // One reused vjp per segment over [boundary, ...segment LoRA] -> [output].
131
+ // apply(..., [dh]) returns vjps [dh_in, dA_0, dB_0, ...].
132
+ this.segVjps = ranges.map(([lo, hi], k) => {
133
+ const idxs = this.segIdxs[k]!;
134
+ return new Vjp((p) => {
135
+ // Swap this segment's LoRA primals into the live LoraWeights so the
136
+ // forward graph differentiates them; restore the originals after (the
137
+ // primal wrappers are disposed by the vjp closure).
138
+ const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
139
+ idxs.forEach((j, s) => {
140
+ lora.targets[j]!.lw.a = p[1 + 2 * s]!;
141
+ lora.targets[j]!.lw.b = p[1 + 2 * s + 1]!;
142
+ });
143
+ try {
144
+ return [model.runLayerRange(p[0]!, lo, hi, this.caches)];
145
+ } finally {
146
+ idxs.forEach((j, s) => {
147
+ lora.targets[j]!.lw.a = saved[s]![0];
148
+ lora.targets[j]!.lw.b = saved[s]![1];
149
+ });
150
+ }
151
+ }, 1);
152
+ });
153
+ }
154
+
155
+ step(batch: SftBatch): { value: MlxArray; grads: MlxArray[] } {
156
+ if (this.disposed) throw new Error("SegmentedBackward used after dispose");
157
+ const { model, lora, ranges, caches, segIdxs, n } = this;
158
+ const B = batch.ids.length;
159
+ if (B !== 1) throw new Error("SegmentedBackward: only B=1 is supported (responseOnlyCe path)");
160
+ const L = batch.ids[0]!.length;
161
+ if (L < 2) throw new Error("SegmentedBackward: sequence too short (need >= 2 tokens)");
162
+ this.curBatch = batch;
163
+
164
+ const nSeg = ranges.length;
165
+ const T = L - 1;
166
+ const inputHost = new Int32Array(T);
167
+ for (let t = 0; t < T; t++) inputHost[t] = batch.ids[0]![t]!;
168
+ const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
169
+
170
+ const boundaries: (MlxArray | null)[] = [];
171
+ let value: MlxArray | null = null;
172
+ let dhOut: MlxArray | null = null;
173
+ let transferred = false;
174
+ const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
175
+
176
+ try {
177
+ // --- 1. Forward, saving detached boundaries at segment edges. --------
178
+ boundaries.push(detachLeaf(model.embed.encode(inputIds))); // [1, T, hidden]
179
+ for (let k = 0; k < nSeg; k++) {
180
+ const [lo, hi] = ranges[k]!;
181
+ boundaries.push(detachLeaf(model.runLayerRange(boundaries[k]!, lo, hi, caches)));
182
+ }
183
+
184
+ // --- 2. Loss head over the last boundary (vjp with cotangent 1.0). ---
185
+ const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
186
+ value = detachLeaf(head.outputs[0]!); // scalar loss, detached from the head graph (frees logits)
187
+ dhOut = detachLeaf(head.vjps[0]!); // dLoss / d(last residual stream), graph-free
188
+ boundaries[nSeg]!.dispose();
189
+ boundaries[nSeg] = null;
190
+
191
+ // --- 3. Backward, reverse over segments (vjp, cotangent = dh). --------
192
+ for (let k = nSeg - 1; k >= 0; k--) {
193
+ const idxs = segIdxs[k]!;
194
+ const params: MlxArray[] = [boundaries[k]!];
195
+ for (const j of idxs) params.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
196
+
197
+ const res = this.segVjps[k]!.apply(params, [dhOut!]);
198
+ res.outputs[0]!.dispose(); // segment output — unused (only its vjp matters)
199
+ // vjps[0] = dh_in (grad w.r.t. this segment's input boundary) -> dh_out
200
+ // for segment k-1; the rest scatter into the global flat vector. Detach
201
+ // each to a graph-free leaf (eval doesn't detach; keeps peak bounded).
202
+ idxs.forEach((j, s) => {
203
+ grads[j] = detachLeaf(res.vjps[1 + 2 * s]!); // dA
204
+ grads[n + j] = detachLeaf(res.vjps[1 + 2 * s + 1]!); // dB
205
+ });
206
+ dhOut!.dispose(); // consumed as this segment's cotangent
207
+ dhOut = detachLeaf(res.vjps[0]!);
208
+ boundaries[k]!.dispose();
209
+ boundaries[k] = null;
210
+ }
211
+
212
+ // Segment-0 dh_in is the gradient w.r.t. the embedding output — MiniCPM5's
213
+ // embedding carries no LoRA, so it is discarded (disposed in finally).
214
+ transferred = true;
215
+ return { value: value!, grads: grads as MlxArray[] };
216
+ } finally {
217
+ inputIds.dispose();
218
+ this.curBatch = null;
219
+ dhOut?.dispose();
220
+ for (const b of boundaries) b?.dispose();
221
+ if (!transferred) {
222
+ value?.dispose();
223
+ for (const g of grads) g?.dispose();
224
+ }
225
+ }
226
+ }
227
+
228
+ dispose(): void {
229
+ if (this.disposed) return;
230
+ this.disposed = true;
231
+ this.headVjp.dispose();
232
+ for (const v of this.segVjps) v.dispose();
233
+ for (const c of this.caches) c.dispose();
234
+ this.one.dispose();
235
+ }
236
+ }
237
+
238
+ // ===========================================================================
239
+ // e4b (Gemma4Model): segmented backward with KV-shared donor threading.
240
+ // ===========================================================================
241
+ //
242
+ // Same skeleton as SegmentedBackward (forward boundaries -> head vjp -> reverse
243
+ // per-segment vjp), with the two e4b-specific pieces (docs §10):
244
+ //
245
+ // - per-layer-input is a PURE CONSTANT boundary: the per_layer_input_gate /
246
+ // per_layer_projection LoRA live inside the layer (differentiated normally
247
+ // within each segment); the [B,L,nLayers,width] tensor feeds in as a
248
+ // constant (its grad would flow to the embed / per_layer_model_projection,
249
+ // neither a LoRA target) — detached once, sliced per segment, cotangent
250
+ // discarded.
251
+ //
252
+ // - the KV-shared donor K/V is a SECOND boundary stream WITH its own cotangent.
253
+ // A reused donor d (e4b: 22 sliding, 23 full) is PRODUCED by the segment
254
+ // containing d (saved as a detached boundary) and CONSUMED by later segments
255
+ // whose sharers reuse it. In the backward, a consumer segment's vjp yields a
256
+ // cotangent on the donor K/V (accumulated into dKV[d]); the producer segment
257
+ // has a MULTI-OUTPUT forward [h, donorK, donorV] whose vjp takes cotangents
258
+ // [dh, dKV[d].k, dKV[d].v] — folding the sharers' gradient into the donor.
259
+
260
+ type KvPair = { keys: MlxArray; values: MlxArray };
261
+
262
+ export class SegmentedBackwardGemma4 {
263
+ private readonly nLayers: number;
264
+ private readonly n: number;
265
+ private readonly segIdxs: number[][];
266
+ private readonly consumes: number[][]; // reused donors consumed as input, per segment
267
+ private readonly produces: number[][]; // reused donors output for later segments, per segment
268
+ private readonly caches: Cache[];
269
+ private readonly headVjp: Vjp;
270
+ private readonly segVjps: Vjp[];
271
+ private readonly one: MlxArray;
272
+ // Per-step constants the (reused) closures read.
273
+ private curBatch: SftBatch | null = null;
274
+ private curMasks: Map<string, Mask> | null = null;
275
+ private curPerLayer: MlxArray | null = null;
276
+ private disposed = false;
277
+
278
+ constructor(
279
+ private readonly model: Gemma4Model,
280
+ private readonly lora: TrainableLora,
281
+ private readonly ranges: [number, number][],
282
+ ) {
283
+ this.nLayers = model.layers.length;
284
+ const lastHi = ranges[ranges.length - 1]?.[1] ?? 0;
285
+ if (ranges[0]?.[0] !== 0 || lastHi !== this.nLayers)
286
+ throw new Error(`SegmentedBackwardGemma4: ranges must cover [0, ${this.nLayers})`);
287
+ this.n = lora.targets.length;
288
+
289
+ const layerOf = lora.targets.map((t) => targetLayer(t.modulePath));
290
+ this.segIdxs = ranges.map(([lo, hi]) =>
291
+ layerOf.map((li, j) => ({ li, j })).filter((x) => x.li >= lo && x.li < hi).map((x) => x.j),
292
+ );
293
+
294
+ // Donor/sharer KV threading: for each reused donor d, the segment that holds
295
+ // d PRODUCES its K/V; a later segment with a sharer of d CONSUMES it.
296
+ const segOfLayer = new Map<number, number>();
297
+ ranges.forEach(([lo, hi], k) => { for (let i = lo; i < hi; i++) segOfLayer.set(i, k); });
298
+ const reused = model.reusedDonors;
299
+ const producerSeg = new Map<number, number>();
300
+ for (const d of reused) producerSeg.set(d, segOfLayer.get(d)!);
301
+ this.consumes = ranges.map(() => []);
302
+ this.produces = ranges.map(() => []);
303
+ for (let i = 0; i < this.nLayers; i++) {
304
+ if (model.cacheIndex[i] !== -1) continue; // donors don't consume
305
+ const d = model.previousKvs[i]!; // this sharer's donor (a reused donor)
306
+ const sSeg = segOfLayer.get(i)!;
307
+ const pSeg = producerSeg.get(d)!;
308
+ if (sSeg > pSeg) { // cross-segment: thread d's K/V from pSeg to sSeg
309
+ if (!this.consumes[sSeg]!.includes(d)) this.consumes[sSeg]!.push(d);
310
+ if (!this.produces[pSeg]!.includes(d)) this.produces[pSeg]!.push(d);
311
+ }
312
+ // sSeg === pSeg: runLayerRange threads d internally; no boundary needed.
313
+ }
314
+
315
+ this.caches = Array.from({ length: model.numDonors }, () => new TrainingCache());
316
+ this.one = MlxArray.fromFloat32(new Float32Array([1]), []);
317
+
318
+ // Loss head (identical to MiniCPM5): finalNorm + responseOnlyCe over the last
319
+ // boundary, vjp with cotangent 1.0 -> [loss], [dh].
320
+ this.headVjp = new Vjp((p) => {
321
+ const hn = model.finalNorm.forward(p[0]!);
322
+ const loss = responseOnlyCe(model, hn, this.curBatch!);
323
+ hn.dispose();
324
+ return [loss];
325
+ }, 1);
326
+
327
+ // One reused vjp per segment. Inputs: [boundary, (k,v)*consumes, (a,b)*lora].
328
+ // Outputs: [h, (k,v)*produces]. The closures read curMasks/curPerLayer.
329
+ this.segVjps = ranges.map(([lo, hi], k) => {
330
+ const idxs = this.segIdxs[k]!;
331
+ const cons = this.consumes[k]!;
332
+ const prod = this.produces[k]!;
333
+ const base = 1 + 2 * cons.length; // first LoRA primal index
334
+ const nOut = 1 + 2 * prod.length;
335
+ const vjp = new Vjp((p) => {
336
+ const boundary = p[0]!;
337
+ const kvIn = new Map<number, SharedKv>();
338
+ cons.forEach((d, ci) => kvIn.set(d, { kind: "plain", keys: p[1 + 2 * ci]!, values: p[1 + 2 * ci + 1]!, offset: 0 }));
339
+ const saved: [MlxArray, MlxArray][] = idxs.map((j) => [lora.targets[j]!.lw.a, lora.targets[j]!.lw.b]);
340
+ idxs.forEach((j, s) => {
341
+ lora.targets[j]!.lw.a = p[base + 2 * s]!;
342
+ lora.targets[j]!.lw.b = p[base + 2 * s + 1]!;
343
+ });
344
+ try {
345
+ const { h, donorKvOut } = model.runLayerRange(
346
+ boundary, lo, hi, this.caches, this.curMasks!, this.curPerLayer, kvIn,
347
+ );
348
+ const outs: MlxArray[] = [h];
349
+ for (const d of prod) {
350
+ const kv = donorKvOut.get(d)!;
351
+ if (kv.kind !== "plain") throw new Error("segmented e4b: quantized donor KV unsupported in training");
352
+ // Output the CONTIGUOUS donor K/V: the consumer segments received a
353
+ // contiguous (detachLeaf'd) copy, so their accumulated cotangent dKV
354
+ // is row-major. The producer's raw K/V are transposed views; matching
355
+ // layouts keeps the vjp's cotangent indexing consistent.
356
+ const ck = ops.contiguous(kv.keys), cv = ops.contiguous(kv.values);
357
+ kv.keys.dispose(); kv.values.dispose();
358
+ outs.push(ck, cv);
359
+ }
360
+ // in-segment-only reused donors (produced but not threaded forward) — free.
361
+ for (const [d, kv] of donorKvOut)
362
+ if (!prod.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
363
+ return outs;
364
+ } finally {
365
+ idxs.forEach((j, s) => { lora.targets[j]!.lw.a = saved[s]![0]; lora.targets[j]!.lw.b = saved[s]![1]; });
366
+ }
367
+ }, nOut);
368
+ return vjp;
369
+ });
370
+ }
371
+
372
+ step(batch: SftBatch): { value: MlxArray; grads: MlxArray[] } {
373
+ if (this.disposed) throw new Error("SegmentedBackwardGemma4 used after dispose");
374
+ const { model, lora, ranges, caches, segIdxs, consumes, produces, n } = this;
375
+ const B = batch.ids.length;
376
+ if (B !== 1) throw new Error("SegmentedBackwardGemma4: only B=1 is supported");
377
+ const L = batch.ids[0]!.length;
378
+ if (L < 2) throw new Error("SegmentedBackwardGemma4: sequence too short");
379
+ this.curBatch = batch;
380
+
381
+ const nSeg = ranges.length;
382
+ const T = L - 1;
383
+ const inputHost = new Int32Array(T);
384
+ for (let t = 0; t < T; t++) inputHost[t] = batch.ids[0]![t]!;
385
+ const inputIds = MlxArray.fromInt32(inputHost, [1, T]);
386
+
387
+ const boundaries: (MlxArray | null)[] = [];
388
+ const donorBnd = new Map<number, KvPair>(); // detached reused-donor K/V boundaries
389
+ const dKV = new Map<number, KvPair>(); // accumulated reused-donor cotangents
390
+ let value: MlxArray | null = null;
391
+ let dhOut: MlxArray | null = null;
392
+ let transferred = false;
393
+ const grads: (MlxArray | null)[] = new Array(2 * n).fill(null);
394
+
395
+ try {
396
+ // --- 1. Forward, saving detached boundaries (hidden + reused donor K/V). ---
397
+ const { hScaled, perLayer } = model.embedForSegmented(inputIds);
398
+ boundaries.push(detachLeaf(hScaled));
399
+ this.curPerLayer = perLayer ? detachLeaf(perLayer) : null;
400
+ this.curMasks = model.makeTrainingMasks(caches, T);
401
+
402
+ for (let k = 0; k < nSeg; k++) {
403
+ const [lo, hi] = ranges[k]!;
404
+ const kvIn = new Map<number, SharedKv>();
405
+ for (const d of consumes[k]!) {
406
+ const b = donorBnd.get(d)!;
407
+ kvIn.set(d, { kind: "plain", keys: b.keys, values: b.values, offset: 0 });
408
+ }
409
+ const { h, donorKvOut } = model.runLayerRange(boundaries[k]!, lo, hi, caches, this.curMasks, this.curPerLayer, kvIn);
410
+ for (const d of produces[k]!) {
411
+ const kv = donorKvOut.get(d)!;
412
+ if (kv.kind !== "plain") throw new Error("segmented e4b: quantized donor KV unsupported in training");
413
+ donorBnd.set(d, { keys: detachLeaf(kv.keys), values: detachLeaf(kv.values) });
414
+ }
415
+ for (const [d, kv] of donorKvOut)
416
+ if (!produces[k]!.includes(d) && kv.kind === "plain") { kv.keys.dispose(); kv.values.dispose(); }
417
+ boundaries.push(detachLeaf(h));
418
+ }
419
+
420
+ // --- 2. Loss head (vjp, cotangent 1.0). ---
421
+ const head = this.headVjp.apply([boundaries[nSeg]!], [this.one]);
422
+ value = detachLeaf(head.outputs[0]!);
423
+ dhOut = detachLeaf(head.vjps[0]!);
424
+ boundaries[nSeg]!.dispose();
425
+ boundaries[nSeg] = null;
426
+
427
+ // --- 3. Backward, reverse over segments (vjp, cotangents dh + dKV). ---
428
+ for (let k = nSeg - 1; k >= 0; k--) {
429
+ const idxs = segIdxs[k]!;
430
+ const cons = consumes[k]!;
431
+ const prod = produces[k]!;
432
+ const dh = dhOut!;
433
+
434
+ const primals: MlxArray[] = [boundaries[k]!];
435
+ for (const d of cons) { const b = donorBnd.get(d)!; primals.push(b.keys, b.values); }
436
+ for (const j of idxs) primals.push(lora.targets[j]!.lw.a, lora.targets[j]!.lw.b);
437
+
438
+ const cots: MlxArray[] = [dh];
439
+ for (const d of prod) {
440
+ const acc = dKV.get(d)!; // set by the consumer segments already processed (reverse order)
441
+ cots.push(acc.keys, acc.values);
442
+ }
443
+
444
+ const res = this.segVjps[k]!.apply(primals, cots);
445
+ for (const o of res.outputs) o.dispose();
446
+
447
+ // vjps[0] = dh_in; then (dk,dv) per consumed donor; then (da,db) per LoRA.
448
+ const newDh = detachLeaf(res.vjps[0]!);
449
+ cons.forEach((d, ci) => {
450
+ const dk = detachLeaf(res.vjps[1 + 2 * ci]!);
451
+ const dv = detachLeaf(res.vjps[1 + 2 * ci + 1]!);
452
+ this.accumulateDKV(dKV, d, dk, dv);
453
+ });
454
+ const base = 1 + 2 * cons.length;
455
+ idxs.forEach((j, s) => {
456
+ grads[j] = detachLeaf(res.vjps[base + 2 * s]!);
457
+ grads[n + j] = detachLeaf(res.vjps[base + 2 * s + 1]!);
458
+ });
459
+
460
+ dh.dispose();
461
+ dhOut = newDh;
462
+ // produced donors' cotangents are now consumed -> free them + their boundary.
463
+ for (const d of prod) {
464
+ const acc = dKV.get(d)!; acc.keys.dispose(); acc.values.dispose(); dKV.delete(d);
465
+ const b = donorBnd.get(d)!; b.keys.dispose(); b.values.dispose(); donorBnd.delete(d);
466
+ }
467
+ boundaries[k]!.dispose();
468
+ boundaries[k] = null;
469
+ }
470
+
471
+ transferred = true;
472
+ return { value: value!, grads: grads as MlxArray[] };
473
+ } finally {
474
+ inputIds.dispose();
475
+ this.curBatch = null;
476
+ this.curPerLayer?.dispose();
477
+ this.curPerLayer = null;
478
+ if (this.curMasks) { for (const m of this.curMasks.values()) m.arr?.dispose(); this.curMasks = null; }
479
+ dhOut?.dispose();
480
+ for (const b of boundaries) b?.dispose();
481
+ for (const kv of donorBnd.values()) { kv.keys.dispose(); kv.values.dispose(); }
482
+ for (const kv of dKV.values()) { kv.keys.dispose(); kv.values.dispose(); }
483
+ if (!transferred) {
484
+ value?.dispose();
485
+ for (const g of grads) g?.dispose();
486
+ }
487
+ }
488
+ }
489
+
490
+ /** Accumulate a reused-donor K/V cotangent (sum across consuming segments) in
491
+ * bf16. The donor K/V grad ends up ~0.5-1% (relNorm) off the full backward —
492
+ * bf16 NON-ASSOCIATIVITY, established (not a logic bug), via:
493
+ * - single-consumer reuse is BIT-EXACT (no sum to reorder);
494
+ * - the error is flat in consumer-SEGMENT count and tracks SUMMAND count
495
+ * (donor 22's 15 sharers ≈0.46%, donor 23's 3 sharers ≈0.000%);
496
+ * - it is grouping-DEPENDENT (donor-22 grad moves ~0.5% from a 2- vs
497
+ * 9-consumer split) for BOTH bf16 AND fp32 accumulation.
498
+ * fp32 cross-segment accumulation does NOT fix it (it's identical at the
499
+ * natural cut and marginally worse at fine cuts): the dominant term is mlx's
500
+ * WITHIN-vjp bf16 cotangent sum per consumer, which regrouping the sharers
501
+ * changes — unfixable without an fp32 forward. bf16-class, fine for training.
502
+ * `dk`/`dv` are detached bf16 leaves; the sum is re-detached. */
503
+ private accumulateDKV(dKV: Map<number, KvPair>, d: number, dk: MlxArray, dv: MlxArray): void {
504
+ const prev = dKV.get(d);
505
+ if (!prev) { dKV.set(d, { keys: dk, values: dv }); return; }
506
+ const sk = detachLeaf(ops.add(prev.keys, dk));
507
+ const sv = detachLeaf(ops.add(prev.values, dv));
508
+ prev.keys.dispose(); prev.values.dispose(); dk.dispose(); dv.dispose();
509
+ dKV.set(d, { keys: sk, values: sv });
510
+ }
511
+
512
+ dispose(): void {
513
+ if (this.disposed) return;
514
+ this.disposed = true;
515
+ this.headVjp.dispose();
516
+ for (const v of this.segVjps) v.dispose();
517
+ for (const c of this.caches) c.dispose();
518
+ this.one.dispose();
519
+ }
520
+ }