mlx-bun 0.0.10 → 0.0.11

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 (173) hide show
  1. package/README.md +66 -21
  2. package/THIRD_PARTY_LICENSES.md +1 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/compat-cli-surface-design.md +4 -3
  10. package/docs/design/decode-speed-program.md +172 -0
  11. package/docs/design/docs-reorg-plan.md +1 -1
  12. package/docs/design/dspark-serving-program.md +371 -0
  13. package/docs/design/dspark-speculative-decoding.md +167 -5
  14. package/docs/design/faithful-l1-consolidation.md +310 -0
  15. package/docs/design/generic-model-support.md +8 -5
  16. package/docs/design/grammar-spec-batching-integration.md +301 -0
  17. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  18. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  19. package/docs/design/omlx-adoption-map.md +14 -6
  20. package/docs/design/orpo-training.md +34 -30
  21. package/docs/design/paged-kv-cache.md +214 -0
  22. package/docs/design/parallel-slots.md +82 -49
  23. package/docs/design/parity-tier-dag.md +43 -33
  24. package/docs/design/repo-cleanup-plan.md +189 -0
  25. package/docs/design/runtime-isolation.md +19 -3
  26. package/docs/design/segmented-backward-training.md +3 -2
  27. package/docs/design/ssd-kv-cold-tier.md +130 -0
  28. package/docs/design/structured-output.md +484 -0
  29. package/docs/design/the-dreaming-master-plan.md +2 -3
  30. package/docs/design/turboquant-kv.md +235 -0
  31. package/docs/design/unified-engine-frontier-plan.md +774 -0
  32. package/docs/design/web-chat-beat-matrix.md +315 -0
  33. package/docs/design/web-chat-redesign.md +1112 -0
  34. package/docs/investigations/dspark-handoff.md +182 -16
  35. package/docs/investigations/lab-build-journal.md +2 -2
  36. package/docs/investigations/openwiki-evaluation.md +249 -0
  37. package/docs/planning/IDEAS.md +14 -0
  38. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  39. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  40. package/docs/planning/chat-ui-vision.md +50 -0
  41. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  42. package/docs/planning/release-notes-v0.0.11.md +239 -0
  43. package/docs/reference/cli.md +35 -16
  44. package/docs/reference/distribution.md +13 -6
  45. package/docs/reference/embedding.md +35 -14
  46. package/docs/reference/features-matrix.md +97 -0
  47. package/docs/reference/library-api.md +30 -9
  48. package/docs/reference/memory.md +26 -6
  49. package/docs/reference/models.md +11 -1
  50. package/docs/reference/orpo-quickstart.md +3 -4
  51. package/docs/reference/server-api.md +634 -22
  52. package/docs/reference/server-config.md +250 -121
  53. package/docs/reference/training.md +14 -15
  54. package/package.json +5 -3
  55. package/src/anthropic.ts +13 -0
  56. package/src/audio/conformer.ts +647 -0
  57. package/src/audio/decode.ts +133 -0
  58. package/src/audio/features.ts +202 -0
  59. package/src/audio/transcode.ts +67 -0
  60. package/src/cli.ts +356 -105
  61. package/src/config.ts +49 -2
  62. package/src/download.ts +40 -0
  63. package/src/eval/README.md +7 -6
  64. package/src/eval/kl.ts +59 -3
  65. package/src/eval/quality-db.ts +2 -2
  66. package/src/eval/runner.ts +3 -3
  67. package/src/expert-offload.ts +15 -2
  68. package/src/flags.ts +13 -0
  69. package/src/generate.ts +330 -24
  70. package/src/grammar.ts +585 -0
  71. package/src/hub-rest.ts +242 -0
  72. package/src/kv-store.ts +442 -88
  73. package/src/lora.ts +8 -0
  74. package/src/md-modules.d.ts +42 -0
  75. package/src/media-fetch.ts +284 -0
  76. package/src/memory/rest.ts +245 -0
  77. package/src/memory/vault.ts +74 -0
  78. package/src/mlx/array.ts +106 -19
  79. package/src/mlx/autograd.ts +2 -2
  80. package/src/mlx/ffi.ts +27 -3
  81. package/src/mlx/metal-kernel.ts +7 -4
  82. package/src/mlx/ops.ts +153 -23
  83. package/src/mlx/turboquant-ops.ts +831 -0
  84. package/src/mlx/turboquant-tables.ts +237 -0
  85. package/src/model/batched-mask.ts +62 -1
  86. package/src/model/batched-quant.ts +206 -0
  87. package/src/model/batched-rotating-quant.ts +346 -0
  88. package/src/model/batched-rotating.ts +46 -1
  89. package/src/model/compiled-decode.ts +4 -15
  90. package/src/model/factory.ts +10 -4
  91. package/src/model/gemma4-base.ts +363 -39
  92. package/src/model/gemma4.ts +122 -33
  93. package/src/model/generated/gemma4-12b.ts +9 -13
  94. package/src/model/generated/gemma4-26b.ts +10 -5
  95. package/src/model/generated/gemma4-e4b.ts +14 -9
  96. package/src/model/minicpm5.ts +58 -102
  97. package/src/model/paged-kv.ts +253 -0
  98. package/src/model/qwen3-delta.ts +76 -15
  99. package/src/model/qwen3-moe.ts +404 -0
  100. package/src/model/qwen3.ts +9 -8
  101. package/src/model/qwen3_5.ts +88 -23
  102. package/src/model/support.ts +12 -1
  103. package/src/model/universal/dense.ts +40 -5
  104. package/src/model/universal/rope.ts +36 -21
  105. package/src/pi-web.ts +1299 -82
  106. package/src/preflight.ts +27 -8
  107. package/src/prompt-cache.ts +293 -33
  108. package/src/quantize/quantizer.ts +16 -1
  109. package/src/registry.ts +57 -2
  110. package/src/serve/batch-scheduler.ts +965 -72
  111. package/src/serve/generation-gateway.ts +214 -11
  112. package/src/serve/isolate.ts +417 -0
  113. package/src/serve/lane-registry.ts +44 -0
  114. package/src/serve/session-search.ts +326 -0
  115. package/src/server.ts +1380 -135
  116. package/src/spec/assistant-source.ts +150 -0
  117. package/src/spec/deepspec-source.ts +143 -0
  118. package/src/spec/dflash-source.ts +124 -0
  119. package/src/spec/dspark/ab-stats.ts +136 -0
  120. package/src/spec/dspark/calibration.ts +112 -0
  121. package/src/spec/dspark/deepspec-module.ts +801 -0
  122. package/src/spec/dspark/generate-dflash.ts +72 -20
  123. package/src/spec/dspark/loader.ts +23 -0
  124. package/src/spec/dspark/module-dflash.ts +278 -36
  125. package/src/spec/dspark/quantize-drafter.ts +91 -0
  126. package/src/spec/generate.ts +59 -25
  127. package/src/spec/ngram-source.ts +132 -0
  128. package/src/spec/serve-loop.ts +536 -0
  129. package/src/spec/source.ts +93 -0
  130. package/src/spec/two-model.ts +171 -0
  131. package/src/ssd-cache.ts +107 -35
  132. package/src/tokenizer.ts +288 -2
  133. package/src/tool-approvals.ts +131 -0
  134. package/src/tool-call.ts +408 -20
  135. package/src/train/flash-cce.ts +2 -2
  136. package/src/train/job.ts +0 -2
  137. package/src/train/trainer.ts +1 -4
  138. package/src/vision/prompt.ts +217 -48
  139. package/src/web/app.html +1184 -1789
  140. package/src/web/app.js +5667 -0
  141. package/src/web/icon.svg +13 -0
  142. package/src/web/manifest.webmanifest +24 -0
  143. package/src/web/src/adapters-panel.ts +288 -0
  144. package/src/web/src/api.ts +57 -0
  145. package/src/web/src/assistant.ts +485 -0
  146. package/src/web/src/chat.ts +1050 -0
  147. package/src/web/src/composer.ts +959 -0
  148. package/src/web/src/dataset.ts +132 -0
  149. package/src/web/src/finetune.ts +267 -0
  150. package/src/web/src/hub.ts +428 -0
  151. package/src/web/src/main.ts +98 -0
  152. package/src/web/src/markdown.ts +0 -0
  153. package/src/web/src/memory-panel.ts +572 -0
  154. package/src/web/src/model-picker.ts +159 -0
  155. package/src/web/src/palette.ts +346 -0
  156. package/src/web/src/protocol.ts +62 -0
  157. package/src/web/src/quantize.ts +167 -0
  158. package/src/web/src/rag.ts +274 -0
  159. package/src/web/src/sessions.ts +469 -0
  160. package/src/web/src/shell.ts +804 -0
  161. package/src/web/src/status.ts +252 -0
  162. package/src/web/src/ui-catalog.ts +92 -0
  163. package/src/web/sw.js +71 -0
  164. package/src/web/vendor/README.md +61 -0
  165. package/src/web/vendor/hljs-LICENSE +29 -0
  166. package/src/web/vendor/hljs-theme.css +59 -0
  167. package/src/web/vendor/hljs.js +26 -0
  168. package/src/model/fused-decode-kernel.ts +0 -312
  169. package/src/model/fused-geglu-kernel.ts +0 -198
  170. package/src/model/fused-mlp-kernel.ts +0 -169
  171. package/src/model/fused-swiglu-kernel.ts +0 -240
  172. package/src/model/steel-linear-kernel.ts +0 -96
  173. package/src/status-page.html +0 -497
@@ -0,0 +1,91 @@
1
+ // Quantize a DeepSpec-format drafter checkpoint (Phase 1a of the DSpark
2
+ // serving program, docs/design/dspark-serving-program.md).
3
+ //
4
+ // Thin policy layer over the house quantizer (src/quantize): the generic
5
+ // shape-eligibility rule already captures every 2-D matmul weight — the
6
+ // layer projections, fc, lm_head, markov_w2, and the two gather tables
7
+ // (embed_tokens, markov_w1: quantized-embedding gather-dequant at load,
8
+ // mlx QuantizedEmbedding semantics). The drafter-specific overlay:
9
+ //
10
+ // KEEP bf16: confidence_head.* — threshold-sensitive (the truncation
11
+ // comparison runs on its sigmoid; borderline flips change proposal
12
+ // lengths) and tiny (~4k params), so quantizing it buys nothing.
13
+ // Norms / layer_scalar / biases are 1-D and never shape-eligible.
14
+ //
15
+ // Drafter numerics only move ACCEPTANCE, never correctness (the target
16
+ // verifies every draft) — the quality gate for any scheme swept through
17
+ // here is the Phase-1c acceptance A/B, not a KL battery.
18
+
19
+ import { quantizeModelDir, type QuantizeResult } from "../../quantize";
20
+ import { DeepspecDrafter } from "./deepspec-module";
21
+ import { MlxArray } from "../../mlx/array";
22
+ import { Dtype } from "../../mlx/ffi";
23
+
24
+ /** Module bases kept full-precision regardless of shape eligibility. */
25
+ const KEEP_BF16 = [/^confidence_head\./];
26
+
27
+ /** The drafter per-tensor policy: quantize everything shape-eligible
28
+ * except the KEEP_BF16 set. */
29
+ export function drafterQuantizePredicate(base: string): boolean {
30
+ return !KEEP_BF16.some((re) => re.test(base));
31
+ }
32
+
33
+ export interface QuantizeDrafterOptions {
34
+ bits: 4 | 8;
35
+ groupSize: 32 | 64;
36
+ /** Skip the post-write load smoke (tests exercise loading separately). */
37
+ skipLoadSmoke?: boolean;
38
+ onProgress?: (stage: string, message: string, progress: number) => void;
39
+ }
40
+
41
+ /**
42
+ * Quantize the DeepSpec drafter at `srcDir` into `outDir` and smoke the
43
+ * result: reload through DeepspecDrafter (quantized detection + graph
44
+ * build) and eval one projectContext row through the quantized fc path.
45
+ */
46
+ export async function quantizeDrafterDir(
47
+ srcDir: string,
48
+ outDir: string,
49
+ opts: QuantizeDrafterOptions,
50
+ ): Promise<QuantizeResult> {
51
+ const raw = (await Bun.file(`${srcDir}/config.json`).json()) as Record<string, any>;
52
+ if (raw.architectures?.[0] !== "Gemma4DSparkModel")
53
+ throw new Error(
54
+ `quantizeDrafterDir: expected a DeepSpec checkpoint (architectures[0]==="Gemma4DSparkModel"), got ${JSON.stringify(raw.architectures)} — for regular models use \`mlx-bun quantize\``,
55
+ );
56
+ if (raw.quantization ?? raw.quantization_config)
57
+ throw new Error("quantizeDrafterDir: source is already quantized — start from the bf16 checkpoint");
58
+
59
+ const result = await quantizeModelDir(
60
+ srcDir,
61
+ outDir,
62
+ {
63
+ bits: opts.bits,
64
+ groupSize: opts.groupSize,
65
+ quantizePredicate: drafterQuantizePredicate,
66
+ },
67
+ opts.onProgress ? (e) => opts.onProgress!(e.stage, e.message, e.progress) : undefined,
68
+ );
69
+
70
+ if (!opts.skipLoadSmoke) {
71
+ // Load smoke: quantized detection + one real quantized matmul (fc) so a
72
+ // wrong spec/layout fails HERE, not at serve time.
73
+ const d = await DeepspecDrafter.load(outDir);
74
+ try {
75
+ const width = d.tapLayers.length * d.hidden;
76
+ const zeros = MlxArray.fromFloat32(new Float32Array(width), [1, 1, width]);
77
+ const x = zeros.astype(Dtype.bfloat16);
78
+ zeros.dispose();
79
+ const proj = d.projectContext(x);
80
+ x.dispose();
81
+ const got = proj.shape;
82
+ proj.dispose();
83
+ if (got.length !== 3 || got[2] !== d.hidden)
84
+ throw new Error(`load smoke: projectContext shape ${JSON.stringify(got)}, expected [1,1,${d.hidden}]`);
85
+ } finally {
86
+ d.dispose();
87
+ }
88
+ }
89
+
90
+ return result;
91
+ }
@@ -128,18 +128,36 @@ export function specGenerate(
128
128
  full: (caches[donors.full] as never as { temporalView(): [MlxArray, MlxArray] }).temporalView(),
129
129
  });
130
130
 
131
+ // Round/prefill scratch tensors, hoisted so the finally frees them on a
132
+ // mid-round throw (the 2f not-trimmable throw, or any mlx op) — try-body
133
+ // locals are invisible to the finally. Each is nulled at its normal
134
+ // disposal, so the finally never double-frees. Same discipline as
135
+ // src/spec/serve-loop.ts / generate-dflash.ts (2026-07-06 review fix);
136
+ // see memory [[mlx-inline-slice-leak-pattern]].
137
+ let prefillH: MlxArray | null = null;
138
+ let lastHidden: MlxArray | null = null;
139
+ let vHidden: MlxArray | null = null;
140
+
131
141
  try {
132
- // 1. prefill
142
+ // 1. prefill — FULL-prompt single forward, deliberately. Per-oracle
143
+ // convention (2026-07-07 re-anchor): mlx-lm's speculative path drains to
144
+ // len-1 with no step-0 (the SERVE loop, whose oracle it is, now mirrors
145
+ // that — src/spec/serve-loop.ts); THIS loop's oracle is optiq
146
+ // spec_generate, which prefills the whole prompt in one forward and
147
+ // samples token0 from it (runtime/spec/runtime.py:162-170, read from the
148
+ // installed oracle venv) — the 2026-06-14 bit-exact gate was established
149
+ // against exactly that shape. Do not tail-split here.
133
150
  const t0 = performance.now();
134
151
  const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
135
- const hidden = model.forwardHidden(ids, caches);
152
+ prefillH = model.forwardHidden(ids, caches);
136
153
  ids.dispose();
137
154
  stats.targetCalls++;
138
- const H = hidden.shape[2]!;
139
- const Lp = hidden.shape[1]!;
140
- let next = pickFromHidden(model, hidden, Lp - 1);
141
- let lastHidden = hidden.slice([0, Lp - 1, 0], [1, Lp, H]);
142
- hidden.dispose();
155
+ const H = prefillH.shape[2]!;
156
+ const Lp = prefillH.shape[1]!;
157
+ let next = pickFromHidden(model, prefillH, Lp - 1);
158
+ lastHidden = prefillH.slice([0, Lp - 1, 0], [1, Lp, H]);
159
+ prefillH.dispose();
160
+ prefillH = null;
143
161
  stats.prefillMs = performance.now() - t0;
144
162
 
145
163
  // EOS convention: an EOS id stops generation and counts toward
@@ -158,30 +176,38 @@ export function specGenerate(
158
176
  const position = caches[0]!.offset - 1;
159
177
  const shared = readDonors();
160
178
 
161
- // 2a. draft γ tokens against the frozen donor views
179
+ // 2a. draft γ tokens against the frozen donor views. try/finally so a
180
+ // mid-draft throw still frees the chained hiddens + donor views + the
181
+ // in-flight emb (assistant-source.ts draft() discipline).
162
182
  const drafts: number[] = [];
163
183
  let dTok = next;
164
- let dHid = lastHidden; // borrowed for k=0
184
+ let dHid = lastHidden!; // borrowed for k=0
165
185
  const ownedHiddens: MlxArray[] = [];
166
- for (let k = 0; k < gamma; k++) {
167
- const emb = embedScaled(dTok);
168
- const step = drafter.forward(emb, dHid, shared, position + k);
169
- emb.dispose();
170
- drafts.push(step.token);
171
- stats.drafted++;
172
- ownedHiddens.push(step.nextHidden);
173
- dTok = step.token;
174
- dHid = step.nextHidden;
175
- }
176
- for (const a of ownedHiddens) a.dispose();
177
- for (const [k, v] of [shared.sliding, shared.full]) {
178
- k.dispose();
179
- v.dispose();
186
+ let emb: MlxArray | null = null;
187
+ try {
188
+ for (let k = 0; k < gamma; k++) {
189
+ emb = embedScaled(dTok);
190
+ const step = drafter.forward(emb, dHid, shared, position + k);
191
+ emb.dispose();
192
+ emb = null;
193
+ drafts.push(step.token);
194
+ stats.drafted++;
195
+ ownedHiddens.push(step.nextHidden);
196
+ dTok = step.token;
197
+ dHid = step.nextHidden;
198
+ }
199
+ } finally {
200
+ emb?.dispose();
201
+ for (const a of ownedHiddens) a.dispose();
202
+ for (const [k, v] of [shared.sliding, shared.full]) {
203
+ k.dispose();
204
+ v.dispose();
205
+ }
180
206
  }
181
207
 
182
208
  // 2b. verify all γ drafts (+ the pending token) in one forward
183
209
  const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
184
- const vHidden = model.forwardHidden(vIds, caches);
210
+ vHidden = model.forwardHidden(vIds, caches);
185
211
  vIds.dispose();
186
212
  stats.targetCalls++;
187
213
 
@@ -199,6 +225,7 @@ export function specGenerate(
199
225
  stats.emitted++;
200
226
  if (isEos || stats.emitted >= maxTokens) {
201
227
  vHidden.dispose();
228
+ vHidden = null;
202
229
  break outer;
203
230
  }
204
231
  }
@@ -221,18 +248,25 @@ export function specGenerate(
221
248
 
222
249
  if (emitIsEos || stats.emitted >= maxTokens) {
223
250
  vHidden.dispose();
251
+ vHidden = null;
224
252
  break;
225
253
  }
226
254
 
227
255
  // 2g. chain state
228
256
  next = emit;
229
- lastHidden.dispose();
257
+ lastHidden!.dispose();
230
258
  lastHidden = vHidden.slice([0, kAccept, 0], [1, kAccept + 1, H]);
231
259
  vHidden.dispose();
260
+ vHidden = null;
232
261
  }
233
262
  stats.decodeMs = performance.now() - tDecode;
234
263
  lastHidden.dispose();
264
+ lastHidden = null;
235
265
  } finally {
266
+ // Survivors of a mid-round throw (each nulled at its normal disposal).
267
+ prefillH?.dispose();
268
+ lastHidden?.dispose();
269
+ vHidden?.dispose();
236
270
  for (const c of caches) c.dispose();
237
271
  }
238
272
 
@@ -0,0 +1,132 @@
1
+ // NgramSource — MODEL-FREE prompt-lookup speculative drafting behind the
2
+ // DraftSource seam (src/spec/source.ts). No weights, no caches, no tokenizer:
3
+ // drafts are copied from the request's own token stream. When the trailing
4
+ // k-gram of (prompt + emitted-so-far) has occurred earlier in that stream,
5
+ // the tokens that followed the earlier occurrence are proposed as the draft;
6
+ // the shared verify/accept executor (src/spec/serve-loop.ts) makes the result
7
+ // LOSSLESS by construction (drafts are only proposals — the target's own
8
+ // samples decide every emitted token, at any temperature; only the acceptance
9
+ // rate moves). Best case: agentic/RAG/code-edit traffic that re-emits spans
10
+ // already present in context.
11
+ //
12
+ // Prior art (ported, not invented): "Prompt Lookup Decoding" (Apoorv Saxena,
13
+ // github.com/apoorvumang/prompt-lookup-decoding) and vLLM's `ngram` proposer
14
+ // (vllm/v1 spec_decode ngram_proposer) — both match the LONGEST k-gram first
15
+ // (k = max..min) and take the FIRST occurrence scanning left-to-right; we
16
+ // follow that exactly. Defaults mirror Saxena (max_ngram_size=3,
17
+ // num_pred_tokens=10 → our --num-draft-tokens default for this kind).
18
+ //
19
+ // Token-history reconstruction (the one subtle part): the seam hands sources
20
+ // the prompt at prefill() and only the FEED tail each round — never the full
21
+ // emitted stream — so this source rebuilds it from the feed/commit discipline
22
+ // (same information two-model keeps in its draft KV):
23
+ // - prefill: history = prompt to len-1 under the oracle tail-split shape
24
+ // (the last prompt token arrives as the first round's feed = [pending]);
25
+ // full prompt under the legacy shape (token0 arrives as its feed).
26
+ // - draft(feed): history += feed. feed is [correction] after a rejected
27
+ // round, or [lastDraft, bonus] after an all-accept round (mlx-lm's
28
+ // re-feed rule, generate.py:645-648) — so the all-accept round's LAST
29
+ // draft is deliberately NOT pushed at commit (it arrives here instead).
30
+ // - commit(d, kAccept): history += drafts[0 .. min(kAccept, d-1)) — the
31
+ // accepted drafts except that re-fed last one. Rejected tips never enter
32
+ // history; the correction token arrives via the next feed.
33
+ // Invariant at matching time (just after the feed push): history ==
34
+ // prompt + every emitted token, ending at the pending/anchor token.
35
+ //
36
+ // A round with no match returns [] — the serve loop's d=0 semantics degrade
37
+ // it to one plain target step (bit-equivalent to non-spec decode), so ngram
38
+ // speculation is structurally never worse than plain decode by more than the
39
+ // JS scan (~µs against 30k-token histories).
40
+
41
+ import { flagOn } from "../flags";
42
+ import type { DraftProvider, DraftSource } from "./source";
43
+
44
+ export interface NgramOptions {
45
+ /** Longest suffix k-gram tried first (Saxena max_ngram_size). */
46
+ max?: number;
47
+ /** Shortest k-gram tried before giving up (vLLM prompt_lookup_min). */
48
+ min?: number;
49
+ }
50
+
51
+ export class NgramProvider implements DraftProvider {
52
+ readonly id = "ngram";
53
+ readonly weightsBytes = 0;
54
+ readonly max: number;
55
+ readonly min: number;
56
+
57
+ constructor(opts: NgramOptions = {}) {
58
+ this.max = Math.max(1, opts.max ?? 3);
59
+ this.min = Math.max(1, Math.min(opts.min ?? 1, this.max));
60
+ }
61
+
62
+ open(_opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
63
+ // Sampler and target both ignored: proposals come from lookup, not a
64
+ // model, so drafting is sampler-independent (like the assistant source —
65
+ // correctness holds at any temperature, only acceptance moves).
66
+ return new NgramSource(this.max, this.min);
67
+ }
68
+
69
+ dispose(): void {}
70
+ }
71
+
72
+ class NgramSource implements DraftSource {
73
+ readonly weightsBytes = 0;
74
+ #hist: number[] = [];
75
+ #lastDrafts: number[] = [];
76
+
77
+ constructor(
78
+ private readonly max: number,
79
+ private readonly min: number,
80
+ ) {}
81
+
82
+ /** Test hook — the reconstructed prompt+emitted stream (see header). */
83
+ get history(): readonly number[] {
84
+ return this.#hist;
85
+ }
86
+
87
+ prefill(promptIds: number[]): void {
88
+ // Mirror the serve loop's prefill shape (two-model.ts does the same): under
89
+ // the oracle tail split the last prompt token is never prefilled — it IS
90
+ // the first feed. Legacy shape (kill switch / 1-token prompt): full prompt,
91
+ // and the sampled token0 arrives as the first feed.
92
+ const tailSplit = flagOn("MLX_BUN_PREFILL_TAIL_SPLIT", true);
93
+ const upTo = tailSplit && promptIds.length > 1 ? promptIds.length - 1 : promptIds.length;
94
+ this.#hist = promptIds.slice(0, upTo);
95
+ }
96
+
97
+ draft(feed: number[], n: number, _stepBase: number): number[] {
98
+ this.#hist.push(...feed);
99
+ this.#lastDrafts = this.#propose(n);
100
+ return this.#lastDrafts;
101
+ }
102
+
103
+ commit(d: number, kAccept: number): void {
104
+ // Accepted drafts join history, EXCEPT an all-accept round's last draft —
105
+ // the serve loop re-feeds it ([lastDraft, bonus]) and the next draft()'s
106
+ // feed push would double it (see header). d=0 rounds push nothing.
107
+ const upTo = Math.max(0, Math.min(kAccept, d - 1));
108
+ for (let i = 0; i < upTo; i++) this.#hist.push(this.#lastDrafts[i]!);
109
+ }
110
+
111
+ /** Longest-k-first, first-occurrence prompt lookup (Saxena/vLLM order):
112
+ * find the trailing k-gram earlier in history, propose what followed it. */
113
+ #propose(n: number): number[] {
114
+ const h = this.#hist;
115
+ const L = h.length;
116
+ const kTop = Math.min(this.max, L - 1);
117
+ for (let k = kTop; k >= this.min; k--) {
118
+ const tailAt = L - k;
119
+ // First occurrence, left to right; i + k < L both excludes the trailing
120
+ // self-match and guarantees at least one continuation token.
121
+ search: for (let i = 0; i + k < L; i++) {
122
+ for (let j = 0; j < k; j++) {
123
+ if (h[i + j] !== h[tailAt + j]) continue search;
124
+ }
125
+ return h.slice(i + k, i + k + n);
126
+ }
127
+ }
128
+ return [];
129
+ }
130
+
131
+ dispose(): void {}
132
+ }