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,150 @@
1
+ // AssistantSource — the optiq KV-borrowing Gemma "-assistant" drafter
2
+ // (src/spec/drafter.ts) behind the serve-time DraftSource seam. L2 oracle:
3
+ // optiq spec_generate. This is the SAME drafter the standalone specGenerate
4
+ // loop (src/spec/generate.ts) drives; here it plugs into the shared
5
+ // verify/accept executor (src/spec/serve-loop.ts) so it composes with grammar,
6
+ // logits processors, admission accounting, and stats like any other source.
7
+ //
8
+ // KV-borrowing (why the seam carries a TargetView): the drafter has NO cache
9
+ // of its own — each draft step reads the target's donor K/V (last sliding +
10
+ // last full layer, in chronological order) and, for the block's first step,
11
+ // the target's hidden at the anchor position. Both arrive through the seam:
12
+ // donor views from `target.caches`, the anchor hidden from draft()'s
13
+ // `anchorHidden` argument (borrowed — never disposed here). See
14
+ // [[dspark-seam-kv-borrowing]].
15
+ //
16
+ // Drafting is GREEDY (the drafter's own argmax head), independent of the
17
+ // request sampler — drafts are only proposals; the target's verify (which does
18
+ // honor the sampler) decides every emitted token, so correctness holds at any
19
+ // temperature and only the acceptance RATE moves. Matches specGenerate.
20
+
21
+ import { readdirSync, statSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import type { MlxArray } from "../mlx/array";
24
+ import * as ops from "../mlx/ops";
25
+ import { Gemma4Model, type Cache } from "../model/gemma4";
26
+ import { GemmaAssistantDrafter } from "./drafter";
27
+ import type { DraftProvider, DraftSource, TargetView } from "./source";
28
+
29
+ /** Sum of the drafter's on-disk safetensors — admission accounting only. */
30
+ function safetensorsBytes(dir: string): number {
31
+ let total = 0;
32
+ for (const f of readdirSync(dir))
33
+ if (f.endsWith(".safetensors")) total += statSync(join(dir, f)).size;
34
+ return total;
35
+ }
36
+
37
+ /** The target's LAST sliding and LAST full cache owners (port of optiq
38
+ * kv_view.find_donor_layers; mirrors donorIndices in generate.ts). */
39
+ function donorIndices(model: Gemma4Model): { sliding: number; full: number } {
40
+ let sliding = -1;
41
+ let full = -1;
42
+ for (let i = 0; i < model.numDonors; i++) {
43
+ if (model.layers[i]!.layerType === "sliding_attention") sliding = i;
44
+ else full = i;
45
+ }
46
+ if (sliding < 0 || full < 0) throw new Error("assistant drafter: missing donor layer type");
47
+ return { sliding, full };
48
+ }
49
+
50
+ export class AssistantProvider implements DraftProvider {
51
+ readonly id: string;
52
+ readonly weightsBytes: number;
53
+
54
+ private constructor(
55
+ private readonly drafter: GemmaAssistantDrafter,
56
+ id: string,
57
+ weightsBytes: number,
58
+ ) {
59
+ this.id = id;
60
+ this.weightsBytes = weightsBytes;
61
+ }
62
+
63
+ static async load(modelDir: string): Promise<AssistantProvider> {
64
+ const drafter = await GemmaAssistantDrafter.load(modelDir);
65
+ const id = modelDir.split("/").filter(Boolean).at(-1)!;
66
+ return new AssistantProvider(drafter, id, safetensorsBytes(modelDir));
67
+ }
68
+
69
+ open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
70
+ return new AssistantSource(this.drafter, opts.target);
71
+ }
72
+
73
+ dispose(): void {
74
+ // The drafter's Weights are process-lifetime (pinned mmaps); nothing to free.
75
+ }
76
+ }
77
+
78
+ class AssistantSource implements DraftSource {
79
+ readonly weightsBytes = 0; // provider-owned weights; per-request adds nothing
80
+ private readonly model: Gemma4Model;
81
+ private readonly caches: Cache[];
82
+ private readonly donors: { sliding: number; full: number };
83
+
84
+ constructor(private readonly drafter: GemmaAssistantDrafter, target: TargetView) {
85
+ if (!(target.model instanceof Gemma4Model))
86
+ throw new Error("assistant drafter requires a Gemma4 target");
87
+ this.model = target.model;
88
+ this.caches = target.caches;
89
+ this.donors = donorIndices(this.model);
90
+ }
91
+
92
+ /** No own cache to prime — the drafter reads the target's live state. */
93
+ prefill(_promptIds: number[], _ctxML?: MlxArray): void {}
94
+
95
+ private embedScaled(token: number): MlxArray {
96
+ const ids = ops.fromInt32([token], [1, 1]);
97
+ const e = this.model.embed.encode(ids);
98
+ ids.dispose();
99
+ const s = ops.mulScalar(e, this.model.embedScale);
100
+ e.dispose();
101
+ return s;
102
+ }
103
+
104
+ private readDonors(): { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] } {
105
+ const view = (i: number) =>
106
+ (this.caches[i] as unknown as { temporalView(): [MlxArray, MlxArray] }).temporalView();
107
+ return { sliding: view(this.donors.sliding), full: view(this.donors.full) };
108
+ }
109
+
110
+ /** Draft n tokens against the target's donor K/V, conditioning the first
111
+ * step on the anchor (the last emitted token + its target hidden) and
112
+ * chaining the drafter's own post-projected hiddens after that — exactly
113
+ * specGenerate's 2a block. anchorHidden is borrowed; never disposed here. */
114
+ draft(feed: number[], n: number, _stepBase: number, anchorHidden?: MlxArray): number[] {
115
+ if (!anchorHidden) throw new Error("assistant drafter needs the target anchor hidden");
116
+ const position = this.caches[0]!.offset - 1;
117
+ const shared = this.readDonors();
118
+ const drafts: number[] = [];
119
+ const ownedHiddens: MlxArray[] = [];
120
+ let dTok = feed[feed.length - 1]!; // the pending token (anchorHidden's token)
121
+ let dHid = anchorHidden; // borrowed for k=0
122
+ let emb: MlxArray | null = null; // hoisted so a mid-loop throw still frees it
123
+ try {
124
+ for (let k = 0; k < n; k++) {
125
+ emb = this.embedScaled(dTok);
126
+ const step = this.drafter.forward(emb, dHid, shared, position + k);
127
+ emb.dispose();
128
+ emb = null;
129
+ drafts.push(step.token);
130
+ ownedHiddens.push(step.nextHidden);
131
+ dTok = step.token;
132
+ dHid = step.nextHidden;
133
+ }
134
+ } finally {
135
+ emb?.dispose();
136
+ for (const a of ownedHiddens) a.dispose();
137
+ for (const [k, v] of [shared.sliding, shared.full]) {
138
+ k.dispose();
139
+ v.dispose();
140
+ }
141
+ }
142
+ return drafts;
143
+ }
144
+
145
+ /** The serve loop already trims the target caches on rejection; the drafter
146
+ * borrows them, so it has nothing of its own to roll back. */
147
+ commit(_n: number, _kAccept: number, _vCtxML?: MlxArray): void {}
148
+
149
+ dispose(): void {}
150
+ }
@@ -0,0 +1,143 @@
1
+ // DeepspecSource — DeepSeek's released DSpark drafter checkpoints
2
+ // (Gemma4DSparkModel, e.g. deepseek-ai/dspark_gemma4_12b_block7) behind the
3
+ // serve-time DraftSource seam. The reference implementation is DeepSpec
4
+ // (github.com/deepseek-ai/DeepSpec, MIT); the module port with transcription
5
+ // notes lives in src/spec/dspark/deepspec-module.ts. Oracle: DeepSpec's eval
6
+ // at temperature 0 (RNG-free — verify degenerates to argmax token-match),
7
+ // scripts/oracle-dspark-deepspec.py → scripts/dspark-deepspec-compare.ts.
8
+ //
9
+ // Seam mapping (why this fits without serve-loop changes): the reference
10
+ // recycles target hidden states from each verify pass into the drafter's
11
+ // growing context — exactly the tapped ctxML/vCtxML flow the seam carries:
12
+ // prefill(ids, ctxML) → project the prompt's tapped hiddens into the
13
+ // per-layer context K/V cache (their DynamicCache
14
+ // of PROJECTED context rows, positions 0..Lp-1);
15
+ // draft(feed, n) → one block forward vs the cache (anchor = feed
16
+ // tail at position ctxLen); confidence truncation
17
+ // may return 0..n tokens (ℓ=0 = plain step);
18
+ // commit(d, kAccept, vCtxML)→ project + append ONLY the accepted rows
19
+ // [anchor + kAccept drafts] (their crop()
20
+ // semantics: rejected rows never enter the
21
+ // cache), positions ctxLen..ctxLen+kAccept.
22
+ // Context rows are cached post-norm-post-rope — bit-equivalent to their
23
+ // in-round concat (per-row RMSNorm/RoPE; argued in projectContextKV's doc).
24
+
25
+ import { readdirSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import type { MlxArray } from "../mlx/array";
28
+ import * as ops from "../mlx/ops";
29
+ import { Gemma4Model } from "../model/gemma4";
30
+ import { DeepspecDrafter, type ContextKV } from "./dspark/deepspec-module";
31
+ import type { DraftProvider, DraftSource, TargetView } from "./source";
32
+
33
+ function safetensorsBytes(dir: string): number {
34
+ let total = 0;
35
+ for (const f of readdirSync(dir))
36
+ if (f.endsWith(".safetensors")) total += statSync(join(dir, f)).size;
37
+ return total;
38
+ }
39
+
40
+ export class DeepspecProvider implements DraftProvider {
41
+ readonly id: string;
42
+ readonly weightsBytes: number;
43
+ /** Trained block width (config block_size, e.g. 7) — the server pins
44
+ * numDraftTokens to this. */
45
+ readonly gamma: number;
46
+
47
+ private constructor(private readonly drafter: DeepspecDrafter, id: string, weightsBytes: number) {
48
+ this.id = id;
49
+ this.weightsBytes = weightsBytes;
50
+ this.gamma = drafter.gamma;
51
+ }
52
+
53
+ static async load(modelDir: string): Promise<DeepspecProvider> {
54
+ const drafter = await DeepspecDrafter.load(modelDir);
55
+ const id = modelDir.split("/").filter(Boolean).at(-1)!;
56
+ return new DeepspecProvider(drafter, id, safetensorsBytes(modelDir));
57
+ }
58
+
59
+ open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
60
+ return new DeepspecSource(this.drafter, opts.target);
61
+ }
62
+
63
+ dispose(): void {
64
+ this.drafter.dispose(); // owned MlxArrays (transpose views + mmap'd weights)
65
+ }
66
+ }
67
+
68
+ class DeepspecSource implements DraftSource {
69
+ readonly weightsBytes = 0; // provider-owned weights
70
+ readonly tapLayers: number[];
71
+ private ctxKV: ContextKV[] | null = null; // per-layer projected context rows
72
+ private ctxLen = 0; // context rows cached == the anchor's absolute position
73
+
74
+ constructor(private readonly drafter: DeepspecDrafter, target: TargetView) {
75
+ if (!(target.model instanceof Gemma4Model))
76
+ throw new Error("DeepSpec drafter requires a Gemma4 target");
77
+ const nLayers = target.model.layers.length;
78
+ if (nLayers !== drafter.cfg.num_target_layers)
79
+ throw new Error(
80
+ `DeepSpec drafter was trained for a ${drafter.cfg.num_target_layers}-layer target; ` +
81
+ `this model has ${nLayers} layers — wrong (target, drafter) pairing`,
82
+ );
83
+ this.tapLayers = drafter.tapLayers;
84
+ }
85
+
86
+ /** Project the prompt's tapped hiddens into the context cache (ownership of
87
+ * ctxML transfers here). */
88
+ prefill(promptIds: number[], ctxML?: MlxArray): void {
89
+ if (!ctxML) throw new Error("DeepSpec drafter: prefill missing tapped context");
90
+ const Lp = ctxML.shape[1]!;
91
+ const projected = this.drafter.projectContext(ctxML);
92
+ ctxML.dispose();
93
+ const positions = Array.from({ length: Lp }, (_, i) => i);
94
+ this.ctxKV = this.drafter.projectContextKV(projected, positions);
95
+ projected.dispose();
96
+ this.ctxLen = Lp;
97
+ }
98
+
99
+ /** One block forward against the cached context. Confidence truncation may
100
+ * return 0..n tokens (0 = skip drafting; the serve loop runs a plain
101
+ * tapped step and commit still grows the context by the anchor row). */
102
+ draft(feed: number[], n: number, _stepBase: number, _anchorHidden?: MlxArray): number[] {
103
+ if (!this.ctxKV) throw new Error("DeepSpec drafter: draft before prefill");
104
+ const anchorTok = feed[feed.length - 1]!;
105
+ const block = this.drafter.draftBlock(this.ctxKV, anchorTok, this.ctxLen);
106
+ block.baseLogits.dispose(); // serve loop runs its own verify lm-head
107
+ // n < gamma near maxTokens: the block always drafts full γ; hand back
108
+ // only what the round may verify.
109
+ return block.tokens.length > n ? block.tokens.slice(0, n) : block.tokens;
110
+ }
111
+
112
+ /** Grow the context cache by the ACCEPTED rows of the verify window —
113
+ * [anchor + kAccept drafts] from vCtxML [1,d+1,m*H] (ownership transfers
114
+ * here); rejected rows never enter (reference crop() semantics). */
115
+ commit(_d: number, kAccept: number, vCtxML?: MlxArray): void {
116
+ if (!vCtxML) throw new Error("DeepSpec drafter: commit missing tapped verify context");
117
+ if (!this.ctxKV) throw new Error("DeepSpec drafter: commit before prefill");
118
+ const mH = vCtxML.shape[2]!;
119
+ const keep = kAccept + 1; // anchor + accepted drafts
120
+ const rows = vCtxML.slice([0, 0, 0], [1, keep, mH]);
121
+ vCtxML.dispose();
122
+ const projected = this.drafter.projectContext(rows);
123
+ rows.dispose();
124
+ const positions = Array.from({ length: keep }, (_, i) => this.ctxLen + i);
125
+ const newKV = this.drafter.projectContextKV(projected, positions);
126
+ projected.dispose();
127
+ for (let i = 0; i < this.ctxKV.length; i++) {
128
+ const old = this.ctxKV[i]!;
129
+ const add = newKV[i]!;
130
+ const k = ops.concatAxis([old.k, add.k], 2);
131
+ const v = ops.concatAxis([old.v, add.v], 2);
132
+ old.k.dispose(); old.v.dispose();
133
+ add.k.dispose(); add.v.dispose();
134
+ this.ctxKV[i] = { k, v };
135
+ }
136
+ this.ctxLen += keep;
137
+ }
138
+
139
+ dispose(): void {
140
+ if (this.ctxKV) for (const { k, v } of this.ctxKV) { k.dispose(); v.dispose(); }
141
+ this.ctxKV = null;
142
+ }
143
+ }
@@ -0,0 +1,124 @@
1
+ // DflashSource — DSpark (faithful DFlash + Markov + confidence,
2
+ // src/spec/dspark/module-dflash.ts) behind the serve-time DraftSource seam.
3
+ // L3 (KL/quality-gated). The DRAFT half of the standalone dflashGenerate loop
4
+ // (src/spec/dspark/generate-dflash.ts) plugged into the shared verify/accept
5
+ // executor (src/spec/serve-loop.ts).
6
+ //
7
+ // H_ctx (why the seam taps): DSpark drafts by attending to a GROWING
8
+ // multi-layer context — the target's tapped hiddens over the accepted stream
9
+ // (paper Eq 2-3). The seam declares `tapLayers`, so the serve loop taps the
10
+ // target's prefill (→ seeds H_ctx here) and every verify forward (→ grown in
11
+ // commit by the accepted window; rejected tips are never appended, mirroring
12
+ // the target-cache trim). See [[dspark-seam-kv-borrowing]].
13
+ //
14
+ // v1 acceptance: the serve loop verifies with TOKEN-MATCH acceptance
15
+ // (mlx-lm/optiq style — lossless at any temperature), NOT the paper's
16
+ // distribution-level rejection-sampling verify. Greedy is identical; temp>0 is
17
+ // still lossless but lower-acceptance. Drafting here is greedy (proposals; the
18
+ // target verify decides). The richer rejection-sampling verify stays in the
19
+ // standalone dflashGenerate for the measure script.
20
+
21
+ import { readdirSync, statSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import type { MlxArray } from "../mlx/array";
24
+ import * as ops from "../mlx/ops";
25
+ import { Gemma4Model } from "../model/gemma4";
26
+ import type { DflashDrafter } from "./dspark/module-dflash";
27
+ import { loadDsparkDrafter } from "./dspark/loader";
28
+ import type { DraftProvider, DraftSource, TargetView } from "./source";
29
+
30
+ function safetensorsBytes(dir: string): number {
31
+ let total = 0;
32
+ for (const f of readdirSync(dir))
33
+ if (f.endsWith(".safetensors")) total += statSync(join(dir, f)).size;
34
+ return total;
35
+ }
36
+
37
+ export class DflashProvider implements DraftProvider {
38
+ readonly id: string;
39
+ readonly weightsBytes: number;
40
+ /** The trained block width — the server pins numDraftTokens to this so the
41
+ * serve loop never asks for more positions than the block was trained for. */
42
+ readonly gamma: number;
43
+
44
+ private constructor(private readonly drafter: DflashDrafter, id: string, weightsBytes: number) {
45
+ this.id = id;
46
+ this.weightsBytes = weightsBytes;
47
+ this.gamma = drafter.cfg.gamma;
48
+ }
49
+
50
+ static async load(modelDir: string): Promise<DflashProvider> {
51
+ const drafter = loadDsparkDrafter(modelDir); // variant dispatch (dspark|legacy dflash)
52
+ const id = modelDir.split("/").filter(Boolean).at(-1)!;
53
+ return new DflashProvider(drafter, id, safetensorsBytes(modelDir));
54
+ }
55
+
56
+ open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
57
+ return new DflashSource(this.drafter, opts.target);
58
+ }
59
+
60
+ dispose(): void {
61
+ // Unlike the assistant drafter (process-pinned mmaps), DflashDrafter
62
+ // materializes owned MlxArrays — free them on teardown.
63
+ this.drafter.dispose();
64
+ }
65
+ }
66
+
67
+ class DflashSource implements DraftSource {
68
+ readonly weightsBytes = 0; // provider-owned weights
69
+ readonly tapLayers: number[];
70
+ private readonly model: Gemma4Model;
71
+ private hCtx: MlxArray | null = null; // [1, L, m*H] — grows with the accepted stream
72
+
73
+ constructor(private readonly drafter: DflashDrafter, target: TargetView) {
74
+ if (!(target.model instanceof Gemma4Model))
75
+ throw new Error("DSpark drafter requires a Gemma4 target");
76
+ this.model = target.model;
77
+ this.tapLayers = drafter.cfg.tapLayers;
78
+ }
79
+
80
+ /** Seed H_ctx from the tapped prompt context (ownership transfers here). */
81
+ prefill(_promptIds: number[], ctxML?: MlxArray): void {
82
+ if (!ctxML) throw new Error("DSpark drafter: prefill missing tapped context");
83
+ this.hCtx = ctxML;
84
+ }
85
+
86
+ /** Draft up to n tokens against the current H_ctx, conditioned on the anchor
87
+ * token (the last emitted token = feed's tail). Greedy — the target verify
88
+ * decides. anchorHidden is unused (DSpark reads H_ctx, not the target
89
+ * hidden). Confidence-scheduled pruning (Alg 1) may return FEWER than n:
90
+ * active when the checkpoint carries STS thresholds (cfg.sts, §3.2.1) or
91
+ * via the MLX_BUN_DSPARK_MINCONF env override; uncalibrated checkpoints
92
+ * draft fixed-length exactly as before. The server pins n ≤ cfg.gamma. */
93
+ draft(feed: number[], n: number, _stepBase: number, _anchorHidden?: MlxArray): number[] {
94
+ if (!this.hCtx) throw new Error("DSpark drafter: draft before prefill");
95
+ const anchorTok = feed[feed.length - 1]!;
96
+ const envMin = process.env.MLX_BUN_DSPARK_MINCONF;
97
+ const block = this.drafter.forwardInfer(this.model, this.hCtx, anchorTok, n, {
98
+ thresholds: this.drafter.cfg.sts?.thresholds,
99
+ collectLogits: false, // serve loop runs its own verify lm-head, never reads this
100
+ ...(envMin ? { minConf: Number(envMin) } : {}),
101
+ });
102
+ return block.tokens;
103
+ }
104
+
105
+ /** Grow H_ctx by the accepted window [anchor + kAccept drafts] from the
106
+ * verified positions' tapped context [1,d+1,m*H]; drop the rejected tips
107
+ * (ownership of vCtxML transfers here). */
108
+ commit(_d: number, kAccept: number, vCtxML?: MlxArray): void {
109
+ if (!vCtxML) throw new Error("DSpark drafter: commit missing tapped verify context");
110
+ if (!this.hCtx) throw new Error("DSpark drafter: commit before prefill");
111
+ const mH = vCtxML.shape[2]!;
112
+ const add = vCtxML.slice([0, 0, 0], [1, kAccept + 1, mH]); // anchor + accepted
113
+ const grown = ops.concatAxis([this.hCtx, add], 1);
114
+ this.hCtx.dispose();
115
+ add.dispose();
116
+ vCtxML.dispose();
117
+ this.hCtx = grown;
118
+ }
119
+
120
+ dispose(): void {
121
+ this.hCtx?.dispose();
122
+ this.hCtx = null;
123
+ }
124
+ }
@@ -0,0 +1,136 @@
1
+ // Model-free math for the Phase-1c drafter acceptance A/B
2
+ // (docs/design/dspark-serving-program.md; runner:
3
+ // scripts/dspark-drafter-ab.ts). THE gate for every drafter-quantization
4
+ // experiment: same target, same prompts, temp 0 — drafter A vs drafter B.
5
+ //
6
+ // Paired design: prompts are the pairing unit, so per-prompt deltas cancel
7
+ // prompt difficulty AND machine-load drift between arms (the
8
+ // dirty-machine-numbers rule: paired deltas survive memory pressure,
9
+ // absolutes don't — final pre/post pairs still belong on a clean machine).
10
+
11
+ /** One prompt × one drafter arm. */
12
+ export interface AbPromptResult {
13
+ prompt: string;
14
+ generatedTokens: number;
15
+ drafted: number;
16
+ accepted: number;
17
+ targetCalls: number;
18
+ wallMs: number;
19
+ draftedByPos: number[];
20
+ acceptedByPos: number[];
21
+ }
22
+
23
+ /** Aggregates over one arm's prompt set. */
24
+ export interface AbArmSummary {
25
+ nPrompts: number;
26
+ generatedTokens: number;
27
+ /** accepted / drafted over the whole set (per-token acceptance). */
28
+ acceptance: number;
29
+ /** committed tokens per target forward (τ). */
30
+ tau: number;
31
+ /** generated tokens per wall second over the whole set. */
32
+ tokPerSec: number;
33
+ /** acceptance at each draft position (0..γ-1). */
34
+ acceptanceByPos: number[];
35
+ }
36
+
37
+ export interface AbVerdict {
38
+ a: AbArmSummary;
39
+ b: AbArmSummary;
40
+ /** B − A, absolute points (e.g. -2.1 = B accepts 2.1 points less). */
41
+ acceptanceDeltaPts: number;
42
+ /** B / A wall-clock throughput ratio (>1 = B faster). */
43
+ speedRatio: number;
44
+ /** Prompts where B's per-prompt acceptance ≥ A's − tolerance. */
45
+ pairedAcceptanceHolds: number;
46
+ /** Phase-1d exit criteria: acceptance drop ≤ `maxDropPts` AND
47
+ * wall-clock strictly improves. */
48
+ pass: boolean;
49
+ verdict: string;
50
+ }
51
+
52
+ export function summarizeArm(results: AbPromptResult[]): AbArmSummary {
53
+ const total = (f: (r: AbPromptResult) => number) => results.reduce((s, r) => s + f(r), 0);
54
+ const generatedTokens = total((r) => r.generatedTokens);
55
+ const drafted = total((r) => r.drafted);
56
+ const accepted = total((r) => r.accepted);
57
+ const targetCalls = total((r) => r.targetCalls);
58
+ const wallMs = total((r) => r.wallMs);
59
+
60
+ const gamma = Math.max(0, ...results.map((r) => r.draftedByPos.length));
61
+ const acceptanceByPos: number[] = [];
62
+ for (let i = 0; i < gamma; i++) {
63
+ const d = total((r) => r.draftedByPos[i] ?? 0);
64
+ const a = total((r) => r.acceptedByPos[i] ?? 0);
65
+ acceptanceByPos.push(d > 0 ? a / d : 0);
66
+ }
67
+
68
+ return {
69
+ nPrompts: results.length,
70
+ generatedTokens,
71
+ acceptance: drafted > 0 ? accepted / drafted : 0,
72
+ tau: targetCalls > 0 ? generatedTokens / targetCalls : 0,
73
+ tokPerSec: wallMs > 0 ? (generatedTokens / wallMs) * 1000 : 0,
74
+ acceptanceByPos,
75
+ };
76
+ }
77
+
78
+ /** Phase-1d exit: acceptance drop ≤ maxDropPts absolute AND wall-clock
79
+ * strictly improves (docs/design/dspark-serving-program.md 1d). */
80
+ export function pairedVerdict(
81
+ aResults: AbPromptResult[],
82
+ bResults: AbPromptResult[],
83
+ opts: { maxDropPts?: number } = {},
84
+ ): AbVerdict {
85
+ if (aResults.length !== bResults.length)
86
+ throw new Error(`pairedVerdict: arm sizes differ (${aResults.length} vs ${bResults.length})`);
87
+ const maxDropPts = opts.maxDropPts ?? 3;
88
+ const a = summarizeArm(aResults);
89
+ const b = summarizeArm(bResults);
90
+
91
+ const acceptanceDeltaPts = (b.acceptance - a.acceptance) * 100;
92
+ const speedRatio = a.tokPerSec > 0 ? b.tokPerSec / a.tokPerSec : 0;
93
+
94
+ let pairedAcceptanceHolds = 0;
95
+ for (let i = 0; i < aResults.length; i++) {
96
+ const ra = aResults[i]!;
97
+ const rb = bResults[i]!;
98
+ const accA = ra.drafted > 0 ? ra.accepted / ra.drafted : 0;
99
+ const accB = rb.drafted > 0 ? rb.accepted / rb.drafted : 0;
100
+ if (accB >= accA - maxDropPts / 100) pairedAcceptanceHolds++;
101
+ }
102
+
103
+ const acceptanceOk = acceptanceDeltaPts >= -maxDropPts;
104
+ const speedOk = speedRatio > 1;
105
+ const pass = acceptanceOk && speedOk;
106
+ const verdict = pass
107
+ ? `PASS — acceptance ${fmtDelta(acceptanceDeltaPts)} pts (within ${maxDropPts}), wall-clock ×${speedRatio.toFixed(2)}`
108
+ : !acceptanceOk
109
+ ? `FAIL — acceptance dropped ${fmtDelta(acceptanceDeltaPts)} pts (limit ${maxDropPts}); B does not preserve A's acceptance`
110
+ : `FAIL — acceptance held (${fmtDelta(acceptanceDeltaPts)} pts) but wall-clock did not improve (×${speedRatio.toFixed(2)})`;
111
+
112
+ return { a, b, acceptanceDeltaPts, speedRatio, pairedAcceptanceHolds, pass, verdict };
113
+ }
114
+
115
+ function fmtDelta(x: number): string {
116
+ return `${x >= 0 ? "+" : ""}${x.toFixed(1)}`;
117
+ }
118
+
119
+ /** Render the two-arm report (pure string — unit-testable, printed by the
120
+ * runner). */
121
+ export function renderReport(labelA: string, labelB: string, v: AbVerdict): string {
122
+ const pct = (x: number) => `${(x * 100).toFixed(1)}%`;
123
+ const row = (label: string, s: AbArmSummary) =>
124
+ ` ${label.padEnd(28)} acceptance ${pct(s.acceptance).padStart(6)} τ ${s.tau.toFixed(2)} ` +
125
+ `${s.tokPerSec.toFixed(1)} tok/s (${s.generatedTokens} tok over ${s.nPrompts} prompts)`;
126
+ const posRow = (label: string, s: AbArmSummary) =>
127
+ ` ${label.padEnd(28)} by-pos [${s.acceptanceByPos.map(pct).join(" ")}]`;
128
+ return [
129
+ row(`A: ${labelA}`, v.a),
130
+ row(`B: ${labelB}`, v.b),
131
+ posRow(`A: ${labelA}`, v.a),
132
+ posRow(`B: ${labelB}`, v.b),
133
+ ` paired: B holds A's acceptance on ${v.pairedAcceptanceHolds}/${v.a.nPrompts} prompts`,
134
+ ` ${v.verdict}`,
135
+ ].join("\n");
136
+ }
@@ -0,0 +1,112 @@
1
+ // Threshold calibration for the confidence scheduler — fits per-position
2
+ // confidence thresholds from real (confidence, accepted) verify outcomes, so
3
+ // the scheduler in module-dflash.ts#forwardInfer prunes positions the
4
+ // confidence head can't actually call. Pure math, no MLX/GPU here: the
5
+ // caller (scripts/dspark-calibrate.ts) collects samples off a live model via
6
+ // dflashGenerate's onRound hook, this module just fits.
7
+ //
8
+ // VERIFIED vs the paper + reference (2026-07-06 audit, arXiv:2607.05147 +
9
+ // github.com/deepseek-ai/DeepSpec): the RELEASED reference schedules exactly
10
+ // this way — per-position threshold truncation on sigmoid confidence
11
+ // (default 0 = off) — so this calibrator fits the reference-shaped consumer.
12
+ // The PAPER's §3.2.1 "STS" is a different mechanism (Sequential Temperature
13
+ // Scaling: per-position temperatures by ECE grid search on cumulative
14
+ // survival products, feeding Alg-1's throughput-maximizing scheduler with a
15
+ // profiled SPS(B) cost table) — that machinery lives in DeepSeek's UNRELEASED
16
+ // production serving layer, not in DeepSpec. Paper-faithful STS/Alg-1 is a
17
+ // future Lab item; details in docs/design/dspark-speculative-decoding.md
18
+ // "DeepSpec ground truth". The isotonic-style "smallest τ meeting a target
19
+ // precision, Laplace-smoothed" fit below is ours (the reference ships no
20
+ // calibrator at all — raw sigmoid vs a hand-set threshold).
21
+
22
+ import type { StsCalibration } from "./module-dflash";
23
+
24
+ /** One verify-time observation: the drafter's predicted confidence at draft
25
+ * position `pos`, and whether the target ACTUALLY accepted that position
26
+ * (k < kAccept in the round that produced it). */
27
+ export interface ConfSample {
28
+ pos: number;
29
+ conf: number;
30
+ accepted: boolean;
31
+ }
32
+
33
+ /**
34
+ * Fit per-position STS thresholds from observed (conf, accepted) pairs.
35
+ *
36
+ * For each position k, we want the smallest τ_k such that pruning on
37
+ * `conf_k < τ_k` still keeps empirical acceptance precision at or above
38
+ * `target` among the samples that WOULD be kept (conf ≥ τ_k). Smaller τ_k
39
+ * keeps more of the block (better speedup); we want the smallest one that
40
+ * still meets the target, i.e. we scan candidate thresholds from low to
41
+ * high (in the order of observed conf values) and stop at the first that
42
+ * clears the bar.
43
+ *
44
+ * Candidate thresholds are exactly the position's observed conf values
45
+ * (a threshold only ever needs to sit at an observed value — moving it
46
+ * between two observed values changes no sample's keep/drop outcome, so a
47
+ * finer sweep can't do better). Sorted ascending; a candidate τ's precision
48
+ * is P(accepted | conf ≥ τ) over the position's samples, with Laplace
49
+ * (+1 accept, +2 total) smoothing so single-sample / early candidates near
50
+ * the tail can't spike to a false 100%.
51
+ *
52
+ * Edge rules (in priority order):
53
+ * 1. Position 0 → threshold 0. Alg-1 never prunes position 0 anyway (the
54
+ * scheduler in forwardInfer special-cases `k > 0`), so any fit would be
55
+ * dead code; 0 documents that explicitly and short-circuits the fit.
56
+ * 2. Fewer than `minSamples` observations at position k → threshold 0
57
+ * (insufficient evidence: don't prune on a guess).
58
+ * 3. No candidate τ clears `target` (including τ = 1, i.e. "prune always")
59
+ * → threshold 1.0 (the head can't call this position at all; prune it
60
+ * unconditionally rather than risk emitting a token the head has no
61
+ * signal on).
62
+ * 4. Otherwise → the smallest candidate τ meeting the target.
63
+ *
64
+ * Pure and deterministic: same `samples` array (any order) → same output,
65
+ * since scoring only depends on the multiset of (conf, accepted) pairs per
66
+ * position, and ties in conf sort stably by value (order-independent).
67
+ */
68
+ export function fitStsThresholds(
69
+ samples: ConfSample[],
70
+ gamma: number,
71
+ target = 0.5,
72
+ minSamples = 50,
73
+ ): StsCalibration {
74
+ const byPos: ConfSample[][] = Array.from({ length: gamma }, () => []);
75
+ for (const s of samples) {
76
+ // Non-finite conf would poison the fit: a NaN candidate τ makes every
77
+ // `conf >= τ` comparison false, and the empty keep-set's Laplace
78
+ // precision (0+1)/(0+2)=0.5 clears the default target — serializing NaN
79
+ // (→ null in JSON) into the thresholds. Drop such samples at intake; a
80
+ // NaN-emitting confidence head surfaces via the sample counts instead.
81
+ if (Number.isFinite(s.conf) && s.pos >= 0 && s.pos < gamma) byPos[s.pos]!.push(s);
82
+ }
83
+
84
+ const thresholds: number[] = new Array(gamma).fill(0);
85
+
86
+ for (let k = 1; k < gamma; k++) {
87
+ const pos = byPos[k]!;
88
+ if (pos.length < minSamples) {
89
+ thresholds[k] = 0; // rule 2 — insufficient evidence, don't prune
90
+ continue;
91
+ }
92
+
93
+ // Candidates = observed conf values, ascending (smallest-τ-first scan).
94
+ const cands = pos.map((s) => s.conf).slice().sort((a, b) => a - b);
95
+
96
+ let chosen: number | null = null;
97
+ for (const tau of cands) {
98
+ let accept = 0, total = 0;
99
+ for (const s of pos) {
100
+ if (s.conf >= tau) { total++; if (s.accepted) accept++; }
101
+ }
102
+ // Laplace (+1/+2) smoothing: precision = (accept+1)/(total+2).
103
+ const precision = (accept + 1) / (total + 2);
104
+ if (precision >= target) { chosen = tau; break; }
105
+ }
106
+
107
+ thresholds[k] = chosen ?? 1.0; // rule 3 if nothing clears the bar
108
+ }
109
+ thresholds[0] = 0; // rule 1 — always, regardless of samples
110
+
111
+ return { thresholds, target, samples: samples.length };
112
+ }