mlx-bun 0.0.8 → 0.0.9

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 (133) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-v2-plan.md +515 -0
  3. package/docs/design/bucketing-stage.md +196 -0
  4. package/docs/design/compat-cli-surface-design.md +11 -11
  5. package/docs/design/docs-reorg-plan.md +1 -1
  6. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  7. package/docs/design/dspark-speculative-decoding.md +106 -0
  8. package/docs/design/generic-model-support.md +381 -0
  9. package/docs/design/memory-inference-path.md +422 -0
  10. package/docs/design/memory-synthesis.md +287 -0
  11. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  12. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  13. package/docs/design/orpo-dynamic-lambda.md +41 -0
  14. package/docs/design/orpo-training.md +35 -5
  15. package/docs/design/parallel-slots.md +12 -1
  16. package/docs/design/parity-tier-dag.md +13 -5
  17. package/docs/design/spec-decode-larger-targets.md +229 -0
  18. package/docs/design/tauri-desktop-app.md +2 -2
  19. package/docs/design/the-dreaming-handoff.md +141 -0
  20. package/docs/design/the-dreaming-master-plan.md +446 -0
  21. package/docs/design/write-pipeline-entity.md +468 -0
  22. package/docs/investigations/curve-bisector-routing.md +1 -1
  23. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  24. package/docs/investigations/dspark-handoff.md +58 -0
  25. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  27. package/docs/planning/curve-sampler-research-plan.md +331 -0
  28. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  29. package/docs/planning/pi-integration-review.md +178 -0
  30. package/docs/planning/release-notes-v0.0.9.md +122 -0
  31. package/docs/planning/web-ui-pass-plan.md +213 -0
  32. package/docs/planning/website-readme-pass-plan.md +224 -0
  33. package/docs/reference/cli.md +255 -0
  34. package/docs/reference/embedding.md +7 -2
  35. package/docs/reference/library-api.md +22 -3
  36. package/docs/reference/memory.md +5 -5
  37. package/docs/reference/models.md +137 -0
  38. package/docs/reference/orpo-quickstart.md +6 -0
  39. package/docs/reference/server-api.md +187 -9
  40. package/docs/reference/server-config.md +40 -9
  41. package/docs/reference/training.md +25 -11
  42. package/package.json +1 -1
  43. package/src/anthropic.ts +22 -0
  44. package/src/assets/curve-designer.html +1 -1
  45. package/src/chat-template.ts +25 -7
  46. package/src/cli.ts +1029 -86
  47. package/src/config.ts +4 -1
  48. package/src/download.ts +99 -8
  49. package/src/embed.ts +77 -0
  50. package/src/eval/perplexity.ts +164 -0
  51. package/src/eval/runner.ts +15 -6
  52. package/src/eval/tasks/bfcl.ts +1 -1
  53. package/src/generate.ts +141 -18
  54. package/src/harness-pi.ts +43 -17
  55. package/src/index.ts +2 -0
  56. package/src/lora.ts +2 -2
  57. package/src/memory/article.ts +558 -0
  58. package/src/memory/chunk-validation.ts +150 -0
  59. package/src/memory/chunk.ts +305 -12
  60. package/src/memory/cluster.ts +344 -17
  61. package/src/memory/coverage.ts +134 -0
  62. package/src/memory/crosslink.ts +0 -0
  63. package/src/memory/db.ts +184 -45
  64. package/src/memory/entity.ts +203 -0
  65. package/src/memory/gate.ts +187 -0
  66. package/src/memory/history.ts +152 -0
  67. package/src/memory/ingest.ts +324 -11
  68. package/src/memory/model.ts +321 -0
  69. package/src/memory/normalize.ts +671 -0
  70. package/src/memory/parse.ts +22 -0
  71. package/src/memory/pipeline.ts +242 -30
  72. package/src/memory/prompts.ts +42 -0
  73. package/src/memory/query.ts +334 -0
  74. package/src/memory/reconcile.ts +734 -0
  75. package/src/memory/redirect.ts +108 -0
  76. package/src/memory/reindex.ts +325 -0
  77. package/src/memory/resolve.ts +307 -0
  78. package/src/memory/route.ts +272 -0
  79. package/src/memory/schedule.ts +6 -4
  80. package/src/memory/stages.ts +774 -0
  81. package/src/memory/synthesize.ts +1629 -20
  82. package/src/memory/tools.ts +446 -96
  83. package/src/memory/vault.ts +462 -35
  84. package/src/memory/wikify.ts +1057 -16
  85. package/src/mlx/ffi.ts +3 -0
  86. package/src/mlx/ops.ts +46 -0
  87. package/src/model/compiled-decode.ts +43 -8
  88. package/src/model/factory.ts +29 -8
  89. package/src/model/flash-attention.ts +4 -2
  90. package/src/model/fused-decode-kernel.ts +14 -7
  91. package/src/model/fused-mlp-kernel.ts +169 -0
  92. package/src/model/fused-swiglu-kernel.ts +240 -0
  93. package/src/model/gemma4-base.ts +62 -6
  94. package/src/model/gemma4.ts +33 -2
  95. package/src/model/minicpm5.ts +106 -8
  96. package/src/model/qwen3.ts +276 -0
  97. package/src/model/steel-linear-kernel.ts +96 -0
  98. package/src/model/support.ts +38 -8
  99. package/src/model/universal/archs.ts +332 -0
  100. package/src/model/universal/dense.ts +576 -0
  101. package/src/model/universal/modules.ts +163 -0
  102. package/src/model/universal/rope.ts +366 -0
  103. package/src/pi-provider.ts +1 -1
  104. package/src/pi-terminal.ts +29 -8
  105. package/src/pi-web.ts +19 -2
  106. package/src/quantize/config-writer.ts +1 -1
  107. package/src/registry.ts +215 -7
  108. package/src/responses.ts +23 -0
  109. package/src/sampler.ts +220 -9
  110. package/src/serve/batch-scheduler.ts +272 -80
  111. package/src/serve/generation-gateway.ts +86 -18
  112. package/src/server.ts +612 -49
  113. package/src/spec/drafter.ts +81 -49
  114. package/src/spec/dspark/data-dflash.ts +156 -0
  115. package/src/spec/dspark/data.ts +201 -0
  116. package/src/spec/dspark/generate-dflash.ts +193 -0
  117. package/src/spec/dspark/generate.ts +290 -0
  118. package/src/spec/dspark/loss.ts +213 -0
  119. package/src/spec/dspark/module-dflash.ts +361 -0
  120. package/src/spec/dspark/module.ts +540 -0
  121. package/src/spec/dspark/sample.ts +112 -0
  122. package/src/spec/generate.ts +40 -19
  123. package/src/status-page.html +1 -1
  124. package/src/tokenizer.ts +7 -0
  125. package/src/train/flash-cce.ts +70 -15
  126. package/src/train/fuse.ts +243 -0
  127. package/src/train/index.ts +3 -0
  128. package/src/train/job.ts +8 -0
  129. package/src/train/loss.ts +289 -30
  130. package/src/train/prefix-shared.ts +33 -5
  131. package/src/train/segmented.ts +67 -11
  132. package/src/train/trainer.ts +92 -28
  133. package/src/web/skills/memory/SKILL.md +40 -17
@@ -4,14 +4,23 @@
4
4
  //
5
5
  // Q-only 4-layer transformer: K/V come from the TARGET's last sliding-
6
6
  // and full-attention donor caches at call time; pre/post projections
7
- // bridge target hidden space (2560) <-> drafter space (256); output via
8
- // the centroid-clustered head (2048 centroids, top-32 × 128 tokens).
7
+ // bridge target hidden space (backbone) <-> drafter space.
9
8
  //
10
- // Implementation deviation (argmax-equivalent): the reference scatters
11
- // candidate scores into full-vocab logits initialised to -1e30 and
12
- // argmaxes; we argmax over the 4096 candidate scores and map through
13
- // candidate ids directly — same winner, no 262k materialisation. The
14
- // drafter's numeric details only influence acceptance RATE, never
9
+ // Two output-head variants ship across the Gemma-4 drafter family — we
10
+ // pick by TENSOR PRESENCE, not config (the larger artifacts declare
11
+ // num_centroids in config.json but don't ship the centroid tensors —
12
+ // the field is vestigial):
13
+ // - CENTROID head (small E2B/E4B drafters, `gemma4_assistant`):
14
+ // 2048 centroids, top-32 × 128 tokens via a precomputed ordering.
15
+ // - TIED-EMBEDDING head (larger 12B/26B `gemma4_unified_assistant`):
16
+ // no centroids, no separate lm_head — logits = embed_tokens · h over
17
+ // the full vocab (the dense form the centroid head approximates).
18
+ //
19
+ // Implementation deviation (centroid path, argmax-equivalent): the
20
+ // reference scatters candidate scores into full-vocab logits initialised
21
+ // to -1e30 and argmaxes; we argmax over the 4096 candidate scores and map
22
+ // through candidate ids directly — same winner, no 262k materialisation.
23
+ // The drafter's numeric details only influence acceptance RATE, never
15
24
  // output correctness (the target's verify decides every token).
16
25
 
17
26
  import type { ModelConfig } from "../config";
@@ -56,10 +65,11 @@ export class GemmaAssistantDrafter {
56
65
  readonly vocabPerCentroid: number;
57
66
  #w: Weights;
58
67
  #blocks: Block[] = [];
68
+ readonly useCentroids: boolean;
59
69
  #prePT: MlxArray; // pre_projection transposed [in, out]
60
70
  #postPT: MlxArray;
61
- #centroidsT: MlxArray; // [hidden, numCentroids]
62
- #ordering: MlxArray; // [numCentroids, vocabPerCentroid] int32
71
+ #centroidsT: MlxArray | null = null; // [hidden, numCentroids]
72
+ #ordering: MlxArray | null = null; // [numCentroids, vocabPerCentroid] int32
63
73
  #embed: MlxArray; // drafter token embeddings [vocab, hidden]
64
74
  #norm: MlxArray;
65
75
 
@@ -84,14 +94,20 @@ export class GemmaAssistantDrafter {
84
94
  };
85
95
  this.#prePT = transposed("pre_projection.weight");
86
96
  this.#postPT = transposed("post_projection.weight");
87
- this.#centroidsT = transposed("masked_embedding.centroids.weight");
88
97
  this.#embed = T("model.embed_tokens.weight");
89
98
  this.#norm = T("model.norm.weight");
90
- const ord64 = T("masked_embedding.token_ordering");
91
- const ord32 = ord64.astype(Dtype.int32);
92
- this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
93
- this.#ordering.eval();
94
- ord32.dispose();
99
+ // Head variant: only the small drafters ship the centroid tensors.
100
+ // The 12B/26B artifacts ship a tied embed_tokens head instead (their
101
+ // config still declares num_centroids — ignore it, trust the tensors).
102
+ this.useCentroids = w.has("masked_embedding.centroids.weight");
103
+ if (this.useCentroids) {
104
+ this.#centroidsT = transposed("masked_embedding.centroids.weight");
105
+ const ord64 = T("masked_embedding.token_ordering");
106
+ const ord32 = ord64.astype(Dtype.int32);
107
+ this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
108
+ this.#ordering.eval();
109
+ ord32.dispose();
110
+ }
95
111
 
96
112
  const ropeP = t.rope_parameters ?? {};
97
113
  for (let i = 0; i < t.num_hidden_layers; i++) {
@@ -215,41 +231,57 @@ export class GemmaAssistantDrafter {
215
231
 
216
232
  h = disposing(h, this.#rms(h, this.#norm));
217
233
 
218
- // centroid decode → drafted token (argmax-equivalent shortcut)
219
- const cScores = ops.matmul(h, this.#centroidsT); // (1,1,2048)
220
- const flat = ops.reshape(cScores, [1, this.numCentroids]);
221
- cScores.dispose();
222
- const neg = ops.neg(flat);
223
- flat.dispose();
224
- const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
225
- neg.dispose();
226
- const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
227
- part.dispose();
228
- const topFlat = ops.reshape(topIdx, [this.topK]);
229
- topIdx.dispose();
230
- const candIds = ops.takeAxis(this.#ordering, topFlat, 0); // (topK, vpc) int32
231
- topFlat.dispose();
232
- const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
233
- candIds.dispose();
234
- const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
235
- const hVec = ops.reshape(h, [this.hidden, 1]);
236
- const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
237
- candEmb.dispose();
238
- hVec.dispose();
239
- const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
240
- scores.dispose();
241
- const am = ops.argmaxAxis(scoresFlat, -1);
242
- scoresFlat.dispose();
243
- const winner = ops.itemUint32(am);
244
- am.dispose();
245
- const tokArr = candFlat.slice([winner], [winner + 1]);
246
- candFlat.dispose();
247
- const tokU32 = tokArr.astype(Dtype.uint32);
248
- tokArr.dispose();
249
- const token = ops.itemUint32(tokU32);
250
- tokU32.dispose();
234
+ let token: number;
235
+ if (this.useCentroids) {
236
+ // centroid decode → drafted token (argmax-equivalent shortcut)
237
+ const cScores = ops.matmul(h, this.#centroidsT!); // (1,1,2048)
238
+ const flat = ops.reshape(cScores, [1, this.numCentroids]);
239
+ cScores.dispose();
240
+ const neg = ops.neg(flat);
241
+ flat.dispose();
242
+ const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
243
+ neg.dispose();
244
+ const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
245
+ part.dispose();
246
+ const topFlat = ops.reshape(topIdx, [this.topK]);
247
+ topIdx.dispose();
248
+ const candIds = ops.takeAxis(this.#ordering!, topFlat, 0); // (topK, vpc) int32
249
+ topFlat.dispose();
250
+ const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
251
+ candIds.dispose();
252
+ const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
253
+ const hVec = ops.reshape(h, [this.hidden, 1]);
254
+ const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
255
+ candEmb.dispose();
256
+ hVec.dispose();
257
+ const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
258
+ scores.dispose();
259
+ const am = ops.argmaxAxis(scoresFlat, -1);
260
+ scoresFlat.dispose();
261
+ const winner = ops.itemUint32(am);
262
+ am.dispose();
263
+ const tokArr = candFlat.slice([winner], [winner + 1]);
264
+ candFlat.dispose();
265
+ const tokU32 = tokArr.astype(Dtype.uint32);
266
+ tokArr.dispose();
267
+ token = ops.itemUint32(tokU32);
268
+ tokU32.dispose();
269
+ } else {
270
+ // tied-embedding head: token = argmax_v (embed[v] · h) over the
271
+ // full vocab — the dense form the centroid head approximates.
272
+ const V = this.#embed.shape[0]!;
273
+ const hVec = ops.reshape(h, [this.hidden, 1]);
274
+ const scores = ops.matmul(this.#embed, hVec); // (vocab, 1)
275
+ hVec.dispose();
276
+ const scoresFlat = ops.reshape(scores, [1, V]);
277
+ scores.dispose();
278
+ const am = ops.argmaxAxis(scoresFlat, -1);
279
+ scoresFlat.dispose();
280
+ token = ops.itemUint32(am);
281
+ am.dispose();
282
+ }
251
283
 
252
- const nextHidden = ops.matmul(h, this.#postPT); // (1,1,2560)
284
+ const nextHidden = ops.matmul(h, this.#postPT); // (1,1,backbone)
253
285
  h.dispose();
254
286
  return { token, nextHidden };
255
287
  }
@@ -0,0 +1,156 @@
1
+ // Data path for faithful DFlash (multi-layer H_ctx). Parallel to data.ts.
2
+ //
3
+ // Shard stores, per packed sequence, the target's tapped hiddens at m layers
4
+ // concatenated on the feature axis: `hidden_ml` [Ltot, m*H] bf16 (the m layers
5
+ // in tapLayers order, the LAST being the post-finalNorm hidden used for p^t),
6
+ // plus `ids` [Ltot], `seq_lens` [nSeq], `resp_starts` [nSeq].
7
+ //
8
+ // A training anchor at response position t attends the FULL prefix context
9
+ // [0..t] (paper Eq 2/3), so sampleBatch slices a per-anchor variable-length
10
+ // prefix and LEFT-PADS the batch to a fixed maxCtx with a key-padding mask.
11
+
12
+ import { MlxArray } from "../../mlx/array";
13
+ import { Dtype } from "../../mlx/ffi";
14
+ import * as ops from "../../mlx/ops";
15
+ import { loadAdapterTensors } from "../../lora";
16
+ import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
17
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ /** One re-forwarded sequence: ids, the response-region start, and the m-layer
21
+ * hiddens over the FULL sequence (row-major [L, m*H] bf16 bytes). */
22
+ export interface DflashRecord {
23
+ ids: number[];
24
+ respStart: number;
25
+ hiddenMlBf16: Uint8Array;
26
+ }
27
+
28
+ export interface DflashShardMeta {
29
+ nSeq: number; nTokens: number; hiddenSize: number; m: number; tapLayers: number[];
30
+ }
31
+
32
+ export function writeDflashShard(outDir: string, shardIdx: number, records: DflashRecord[], hiddenSize: number, m: number, tapLayers: number[]): DflashShardMeta {
33
+ const dir = join(outDir, `shard_${String(shardIdx).padStart(5, "0")}`);
34
+ mkdirSync(dir, { recursive: true });
35
+ const mH = m * hiddenSize;
36
+ const seqLens: number[] = [], respStarts: number[] = [], allIds: number[] = [], parts: Uint8Array[] = [];
37
+ for (const r of records) {
38
+ seqLens.push(r.ids.length); respStarts.push(r.respStart);
39
+ for (const id of r.ids) allIds.push(id);
40
+ parts.push(r.hiddenMlBf16);
41
+ }
42
+ const nTokens = allIds.length;
43
+ let total = 0; for (const p of parts) total += p.length;
44
+ const bytes = new Uint8Array(total); let off = 0; for (const p of parts) { bytes.set(p, off); off += p.length; }
45
+ const hidden = MlxArray.fromBytesCopy(bytes, [nTokens, mH], Dtype.bfloat16);
46
+ const ids = MlxArray.fromInt32(new Int32Array(allIds), [nTokens]);
47
+ const lens = MlxArray.fromInt32(new Int32Array(seqLens), [seqLens.length]);
48
+ const rs = MlxArray.fromInt32(new Int32Array(respStarts), [respStarts.length]);
49
+ const tensors: NamedTensor[] = [
50
+ { name: "hidden_ml", array: hidden }, { name: "ids", array: ids },
51
+ { name: "seq_lens", array: lens }, { name: "resp_starts", array: rs },
52
+ ];
53
+ writeShardedSafetensors(dir, tensors);
54
+ for (const t of tensors) t.array.dispose();
55
+ const meta: DflashShardMeta = { nSeq: records.length, nTokens, hiddenSize, m, tapLayers };
56
+ writeFileSync(join(dir, "shard.json"), JSON.stringify(meta, null, 2));
57
+ return meta;
58
+ }
59
+
60
+ export class DflashShard {
61
+ readonly hiddenMl: MlxArray; // [Ltot, m*H] bf16
62
+ readonly ids: Int32Array;
63
+ readonly seqStart: number[]; readonly seqLen: number[]; readonly respStart: number[];
64
+ readonly hiddenSize: number; readonly m: number;
65
+ constructor(hiddenMl: MlxArray, ids: Int32Array, seqLens: Int32Array, respStarts: Int32Array, hiddenSize: number, m: number) {
66
+ this.hiddenMl = hiddenMl; this.ids = ids; this.hiddenSize = hiddenSize; this.m = m;
67
+ this.seqStart = []; this.seqLen = []; this.respStart = [];
68
+ let off = 0;
69
+ for (let i = 0; i < seqLens.length; i++) { this.seqStart.push(off); this.seqLen.push(seqLens[i]!); this.respStart.push(respStarts[i]!); off += seqLens[i]!; }
70
+ }
71
+ static load(dir: string): DflashShard {
72
+ const meta = JSON.parse(readFileSync(join(dir, "shard.json"), "utf8")) as DflashShardMeta;
73
+ const t = loadAdapterTensors(join(dir, "model.safetensors"));
74
+ try {
75
+ const hidden = t.get("hidden_ml"), idsA = t.get("ids"), lensA = t.get("seq_lens"), rsA = t.get("resp_starts");
76
+ if (!hidden || !idsA || !lensA || !rsA) throw new Error(`shard ${dir} missing tensors`);
77
+ const ids = Int32Array.from(idsA.toFloat32(), (v) => Math.round(v));
78
+ const lens = Int32Array.from(lensA.toFloat32(), (v) => Math.round(v));
79
+ const rs = Int32Array.from(rsA.toFloat32(), (v) => Math.round(v));
80
+ idsA.dispose(); lensA.dispose(); rsA.dispose();
81
+ return new DflashShard(hidden, ids, lens, rs, meta.hiddenSize, meta.m);
82
+ } catch (e) { for (const [, a] of t) a.dispose(); throw e; }
83
+ }
84
+ dispose(): void { this.hiddenMl.dispose(); }
85
+ }
86
+
87
+ export function listDflashShards(root: string): string[] {
88
+ if (!existsSync(root)) return [];
89
+ return readdirSync(root).filter((n) => n.startsWith("shard_")).sort().map((n) => join(root, n));
90
+ }
91
+
92
+ export interface DflashBatch {
93
+ hCtx: MlxArray; // [A, maxCtx, m*H] bf16, LEFT-padded
94
+ ctxMask: MlxArray; // [A, maxCtx] f32 (1=real, 0=pad)
95
+ targetHidden: MlxArray; // [A, γ, H] bf16 (final-layer hiddens at block positions → p^t)
96
+ anchorToks: number[];
97
+ blockToks: number[][];
98
+ size: number;
99
+ }
100
+
101
+ /**
102
+ * Sample A anchors and gather them into a faithful DFlash batch. Each anchor at
103
+ * response position t contributes its full prefix context [0..t] (capped at
104
+ * maxCtx, keeping the most recent), left-padded. The final tapped layer (last
105
+ * in tapLayers = post-finalNorm) supplies the block's p^t hiddens.
106
+ */
107
+ export function sampleDflashBatch(shard: DflashShard, A: number, gamma: number, maxCtx: number, rng: () => number): DflashBatch | null {
108
+ const H = shard.hiddenSize, m = shard.m, mH = m * H;
109
+ const ctxIdx: number[] = []; // A*maxCtx global indices (dummy 0 for pads)
110
+ const maskRows: number[] = []; // A*maxCtx
111
+ const blockIdx: number[] = []; // A*γ global indices
112
+ const anchorToks: number[] = [];
113
+ const blockToks: number[][] = [];
114
+
115
+ let got = 0, guard = 0;
116
+ while (got < A && guard < A * 64) {
117
+ guard++;
118
+ const s = Math.floor(rng() * shard.seqLen.length);
119
+ const len = shard.seqLen[s]!, rStart = shard.respStart[s]!, start = shard.seqStart[s]!;
120
+ // anchor local t in [rStart, len-γ-1]; need γ tokens after and t in response.
121
+ const lo = rStart, hi = len - gamma - 1;
122
+ if (hi < lo) continue;
123
+ const t = lo + Math.floor(rng() * (hi - lo + 1));
124
+ const g = start + t;
125
+ // prefix context [0..t], capped to the most-recent maxCtx positions.
126
+ const La = Math.min(t + 1, maxCtx);
127
+ const ctxStart = g - La + 1;
128
+ const pad = maxCtx - La;
129
+ for (let j = 0; j < pad; j++) { ctxIdx.push(0); maskRows.push(0); }
130
+ for (let j = 0; j < La; j++) { ctxIdx.push(ctxStart + j); maskRows.push(1); }
131
+ const bt: number[] = [];
132
+ for (let k = 1; k <= gamma; k++) bt.push(shard.ids[g + k]!); // CE/markov targets x_{t+1..t+γ}
133
+ // p^t hiddens: block position k predicts x_{t+1+k}, whose target dist is
134
+ // softmax(LM_head(h_{t+k})) — the hidden that PREDICTS it. So gather
135
+ // positions [t..t+γ-1] = [g..g+γ-1], NOT [g+1..g+γ] (off-by-one fix).
136
+ for (let k = 0; k < gamma; k++) blockIdx.push(g + k);
137
+ anchorToks.push(shard.ids[g]!); blockToks.push(bt); got++;
138
+ }
139
+ if (got === 0) return null;
140
+
141
+ const cIdx = MlxArray.fromInt32(new Int32Array(ctxIdx), [got * maxCtx]);
142
+ const gathered = ops.takeAxis(shard.hiddenMl, cIdx, 0); // [A*maxCtx, m*H]
143
+ cIdx.dispose();
144
+ const hCtx = ops.reshape(gathered, [got, maxCtx, mH]); gathered.dispose();
145
+ const ctxMask = MlxArray.fromFloat32(new Float32Array(maskRows), [got, maxCtx]);
146
+
147
+ const bIdx = MlxArray.fromInt32(new Int32Array(blockIdx), [got * gamma]);
148
+ const bGathered = ops.takeAxis(shard.hiddenMl, bIdx, 0); // [A*γ, m*H]
149
+ bIdx.dispose();
150
+ // final tapped layer = last H columns → the post-finalNorm hidden for p^t
151
+ const finalSlice = bGathered.slice([0, (m - 1) * H], [got * gamma, m * H]); // [A*γ, H]
152
+ bGathered.dispose();
153
+ const targetHidden = ops.reshape(finalSlice, [got, gamma, H]); finalSlice.dispose();
154
+
155
+ return { hCtx, ctxMask, targetHidden, anchorToks, blockToks, size: got };
156
+ }
@@ -0,0 +1,201 @@
1
+ // DSpark training data — the regen target (paper §3.3 + §5.1 "hidden state
2
+ // communication"). Training the drafter requires, per block position, the
3
+ // frozen target's TRUE distribution p^t and the tapped hidden state. Rather
4
+ // than store full-vocab logits (V≈262k → enormous), we store only the target's
5
+ // per-position final hidden states (the tap, H≈2816) plus the token ids, and
6
+ // recompute p^t on the fly during training via the SHARED frozen LM head —
7
+ // O(d) storage instead of O(V). (paper §5.1.)
8
+ //
9
+ // On-disk layout: a directory of shards, each `shard_NNNNN/model.safetensors`
10
+ // holding three tensors over a pack of sequences:
11
+ // - "hidden" [Ltot, H] bf16 — concatenated per-position final hiddens
12
+ // - "ids" [Ltot] int32 — concatenated token ids
13
+ // - "seq_lens" [nSeq] int32 — per-sequence lengths (to split Ltot)
14
+ // plus a sidecar `shard.json` with counts. The regen script (Josh runs it on
15
+ // GPU) writes these; the trainer reads them and samples anchor blocks.
16
+ //
17
+ // An anchor at sequence position t yields a training example:
18
+ // hCtx = hidden[t] (the injected context, paper H_ctx)
19
+ // anchorTok = ids[t] (the bonus token x0)
20
+ // blockToks = ids[t+1 .. t+γ] (ground truth x*_1..x*_γ)
21
+ // targetHidden= hidden[t+1 .. t+γ] (→ p^t_k via the frozen head at train)
22
+
23
+ import { MlxArray } from "../../mlx/array";
24
+ import { Dtype } from "../../mlx/ffi";
25
+ import * as ops from "../../mlx/ops";
26
+ import { loadAdapterTensors } from "../../lora";
27
+ import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
28
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
29
+ import { join } from "node:path";
30
+
31
+ /** One re-forwarded sequence: ids + per-position final hidden (bf16 bytes). */
32
+ export interface DSparkRecord {
33
+ ids: number[];
34
+ /** Row-major [L, H] bf16 bytes (from MlxArray.rawBytes of the tap). */
35
+ hiddenBf16: Uint8Array;
36
+ }
37
+
38
+ export interface DSparkShardMeta {
39
+ nSeq: number;
40
+ nTokens: number;
41
+ hiddenSize: number;
42
+ }
43
+
44
+ /** Write a shard of re-forwarded sequences to `dir/shard_NNNNN/`. */
45
+ export function writeDSparkShard(
46
+ outDir: string,
47
+ shardIdx: number,
48
+ records: DSparkRecord[],
49
+ hiddenSize: number,
50
+ ): DSparkShardMeta {
51
+ const dir = join(outDir, `shard_${String(shardIdx).padStart(5, "0")}`);
52
+ mkdirSync(dir, { recursive: true });
53
+
54
+ const seqLens: number[] = [];
55
+ const allIds: number[] = [];
56
+ const hiddenParts: Uint8Array[] = [];
57
+ for (const rec of records) {
58
+ seqLens.push(rec.ids.length);
59
+ for (const id of rec.ids) allIds.push(id);
60
+ hiddenParts.push(rec.hiddenBf16);
61
+ }
62
+ const nTokens = allIds.length;
63
+
64
+ // concat hidden bytes
65
+ let total = 0;
66
+ for (const p of hiddenParts) total += p.length;
67
+ const hiddenBytes = new Uint8Array(total);
68
+ let off = 0;
69
+ for (const p of hiddenParts) { hiddenBytes.set(p, off); off += p.length; }
70
+
71
+ const hidden = MlxArray.fromBytesCopy(hiddenBytes, [nTokens, hiddenSize], Dtype.bfloat16);
72
+ const ids = MlxArray.fromInt32(new Int32Array(allIds), [nTokens]);
73
+ const lens = MlxArray.fromInt32(new Int32Array(seqLens), [seqLens.length]);
74
+ const tensors: NamedTensor[] = [
75
+ { name: "hidden", array: hidden },
76
+ { name: "ids", array: ids },
77
+ { name: "seq_lens", array: lens },
78
+ ];
79
+ writeShardedSafetensors(dir, tensors);
80
+ hidden.dispose(); ids.dispose(); lens.dispose();
81
+
82
+ const meta: DSparkShardMeta = { nSeq: records.length, nTokens, hiddenSize };
83
+ writeFileSync(join(dir, "shard.json"), JSON.stringify(meta, null, 2));
84
+ return meta;
85
+ }
86
+
87
+ /** A loaded shard, ready for anchor sampling. Owns `hidden`; call dispose(). */
88
+ export class DSparkShard {
89
+ readonly hidden: MlxArray; // [Ltot, H] bf16
90
+ readonly ids: Int32Array; // [Ltot]
91
+ readonly seqStart: number[]; // per-seq start offset into Ltot
92
+ readonly seqLen: number[]; // per-seq length
93
+ readonly hiddenSize: number;
94
+
95
+ constructor(hidden: MlxArray, ids: Int32Array, seqLens: Int32Array, hiddenSize: number) {
96
+ this.hidden = hidden;
97
+ this.ids = ids;
98
+ this.hiddenSize = hiddenSize;
99
+ this.seqStart = [];
100
+ this.seqLen = [];
101
+ let off = 0;
102
+ for (const l of seqLens) {
103
+ this.seqStart.push(off);
104
+ this.seqLen.push(l);
105
+ off += l;
106
+ }
107
+ }
108
+
109
+ static load(dir: string): DSparkShard {
110
+ const metaPath = join(dir, "shard.json");
111
+ if (!existsSync(metaPath)) throw new Error(`no shard.json in ${dir}`);
112
+ const meta = JSON.parse(readFileSync(metaPath, "utf8")) as DSparkShardMeta;
113
+ const t = loadAdapterTensors(join(dir, "model.safetensors"));
114
+ try {
115
+ const hidden = t.get("hidden");
116
+ const idsArr = t.get("ids");
117
+ const lensArr = t.get("seq_lens");
118
+ if (!hidden || !idsArr || !lensArr) throw new Error(`shard ${dir} missing tensors`);
119
+ // int32 ids round-trip exactly through f32 (vocab < 2^24).
120
+ const ids = Int32Array.from(idsArr.toFloat32(), (v) => Math.round(v));
121
+ const lens = Int32Array.from(lensArr.toFloat32(), (v) => Math.round(v));
122
+ idsArr.dispose(); lensArr.dispose();
123
+ return new DSparkShard(hidden, ids, lens, meta.hiddenSize);
124
+ } catch (e) {
125
+ for (const [, arr] of t) arr.dispose();
126
+ throw e;
127
+ }
128
+ }
129
+
130
+ dispose(): void {
131
+ this.hidden.dispose();
132
+ }
133
+ }
134
+
135
+ /** List shard directories under a data root, sorted. */
136
+ export function listShards(root: string): string[] {
137
+ if (!existsSync(root)) return [];
138
+ return readdirSync(root)
139
+ .filter((n) => n.startsWith("shard_"))
140
+ .sort()
141
+ .map((n) => join(root, n));
142
+ }
143
+
144
+ export interface DSparkBatch {
145
+ /** [A, H] bf16 — tapped target hidden at each anchor. */
146
+ hCtx: MlxArray;
147
+ /** [A, γ, H] bf16 — target final hiddens at the γ block positions. */
148
+ targetHidden: MlxArray;
149
+ /** anchor token x0 per row. */
150
+ anchorToks: number[];
151
+ /** ground-truth block tokens x*_1..x*_γ per row [A][γ]. */
152
+ blockToks: number[][];
153
+ /** number of anchors A. */
154
+ size: number;
155
+ }
156
+
157
+ /**
158
+ * Sample A anchors from a loaded shard and gather them into a training batch.
159
+ * A valid anchor at global index g (within a sequence) needs γ continuation
160
+ * tokens in the SAME sequence. `rng` returns [0,1).
161
+ */
162
+ export function sampleBatch(shard: DSparkShard, A: number, gamma: number, rng: () => number): DSparkBatch | null {
163
+ const anchorGlobal: number[] = [];
164
+ const anchorToks: number[] = [];
165
+ const blockToks: number[][] = [];
166
+ const blockGlobal: number[] = []; // flattened γ indices per anchor
167
+
168
+ let guard = 0;
169
+ while (anchorGlobal.length < A && guard < A * 64) {
170
+ guard++;
171
+ const s = Math.floor(rng() * shard.seqLen.length);
172
+ const len = shard.seqLen[s]!;
173
+ if (len < gamma + 1) continue;
174
+ const start = shard.seqStart[s]!;
175
+ // local anchor position t in [0, len-γ-1]
176
+ const t = Math.floor(rng() * (len - gamma));
177
+ const g = start + t;
178
+ anchorGlobal.push(g);
179
+ anchorToks.push(shard.ids[g]!);
180
+ const bt: number[] = [];
181
+ for (let k = 1; k <= gamma; k++) {
182
+ bt.push(shard.ids[g + k]!);
183
+ blockGlobal.push(g + k);
184
+ }
185
+ blockToks.push(bt);
186
+ }
187
+ if (anchorGlobal.length === 0) return null;
188
+ const got = anchorGlobal.length;
189
+
190
+ const anchorIdx = MlxArray.fromInt32(new Int32Array(anchorGlobal), [got]);
191
+ const hCtx = ops.takeAxis(shard.hidden, anchorIdx, 0); // [A,H]
192
+ anchorIdx.dispose();
193
+
194
+ const blockIdx = MlxArray.fromInt32(new Int32Array(blockGlobal), [got * gamma]);
195
+ const tgtFlat = ops.takeAxis(shard.hidden, blockIdx, 0); // [A*γ, H]
196
+ blockIdx.dispose();
197
+ const targetHidden = ops.reshape(tgtFlat, [got, gamma, shard.hiddenSize]);
198
+ tgtFlat.dispose();
199
+
200
+ return { hCtx, targetHidden, anchorToks, blockToks, size: got };
201
+ }