mlx-bun 0.0.7 → 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 (138) 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/diffusion-gemma-port.md +602 -0
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/orpo-dynamic-lambda.md +41 -0
  15. package/docs/design/orpo-training.md +35 -5
  16. package/docs/design/parallel-slots.md +12 -1
  17. package/docs/design/parity-tier-dag.md +13 -5
  18. package/docs/design/spec-decode-larger-targets.md +229 -0
  19. package/docs/design/tauri-desktop-app.md +2 -2
  20. package/docs/design/the-dreaming-handoff.md +141 -0
  21. package/docs/design/the-dreaming-master-plan.md +446 -0
  22. package/docs/design/write-pipeline-entity.md +468 -0
  23. package/docs/investigations/curve-bisector-routing.md +1 -1
  24. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  25. package/docs/investigations/dspark-handoff.md +58 -0
  26. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  27. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  28. package/docs/planning/curve-sampler-research-plan.md +331 -0
  29. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  30. package/docs/planning/pi-integration-review.md +178 -0
  31. package/docs/planning/release-notes-v0.0.9.md +122 -0
  32. package/docs/planning/web-ui-pass-plan.md +213 -0
  33. package/docs/planning/website-readme-pass-plan.md +224 -0
  34. package/docs/reference/cli.md +255 -0
  35. package/docs/reference/embedding.md +7 -2
  36. package/docs/reference/library-api.md +22 -3
  37. package/docs/reference/memory.md +5 -5
  38. package/docs/reference/models.md +137 -0
  39. package/docs/reference/orpo-quickstart.md +6 -0
  40. package/docs/reference/server-api.md +187 -9
  41. package/docs/reference/server-config.md +40 -9
  42. package/docs/reference/training.md +25 -11
  43. package/package.json +1 -1
  44. package/src/anthropic.ts +22 -0
  45. package/src/assets/curve-designer.html +1 -1
  46. package/src/chat-template.ts +25 -7
  47. package/src/cli.ts +1029 -86
  48. package/src/config.ts +54 -3
  49. package/src/diffusion/diffusion-generate.ts +436 -0
  50. package/src/download.ts +99 -8
  51. package/src/embed.ts +77 -0
  52. package/src/eval/perplexity.ts +164 -0
  53. package/src/eval/runner.ts +19 -7
  54. package/src/eval/tasks/bfcl.ts +1 -1
  55. package/src/generate.ts +196 -19
  56. package/src/harness-pi.ts +43 -17
  57. package/src/index.ts +2 -0
  58. package/src/lora.ts +2 -2
  59. package/src/memory/article.ts +558 -0
  60. package/src/memory/chunk-validation.ts +150 -0
  61. package/src/memory/chunk.ts +305 -12
  62. package/src/memory/cluster.ts +344 -17
  63. package/src/memory/coverage.ts +134 -0
  64. package/src/memory/crosslink.ts +0 -0
  65. package/src/memory/db.ts +184 -45
  66. package/src/memory/entity.ts +203 -0
  67. package/src/memory/gate.ts +187 -0
  68. package/src/memory/history.ts +152 -0
  69. package/src/memory/ingest.ts +324 -11
  70. package/src/memory/model.ts +321 -0
  71. package/src/memory/normalize.ts +671 -0
  72. package/src/memory/parse.ts +22 -0
  73. package/src/memory/pipeline.ts +242 -30
  74. package/src/memory/prompts.ts +42 -0
  75. package/src/memory/query.ts +334 -0
  76. package/src/memory/reconcile.ts +734 -0
  77. package/src/memory/redirect.ts +108 -0
  78. package/src/memory/reindex.ts +325 -0
  79. package/src/memory/resolve.ts +307 -0
  80. package/src/memory/route.ts +272 -0
  81. package/src/memory/schedule.ts +6 -4
  82. package/src/memory/stages.ts +774 -0
  83. package/src/memory/synthesize.ts +1629 -20
  84. package/src/memory/tools.ts +446 -96
  85. package/src/memory/vault.ts +462 -35
  86. package/src/memory/wikify.ts +1057 -16
  87. package/src/mlx/ffi.ts +11 -0
  88. package/src/mlx/ops.ts +113 -0
  89. package/src/model/compiled-decode.ts +43 -8
  90. package/src/model/diffusion-gemma.ts +1026 -0
  91. package/src/model/factory.ts +35 -8
  92. package/src/model/flash-attention.ts +4 -2
  93. package/src/model/fused-decode-kernel.ts +14 -7
  94. package/src/model/fused-mlp-kernel.ts +169 -0
  95. package/src/model/fused-swiglu-kernel.ts +240 -0
  96. package/src/model/gemma4-base.ts +62 -6
  97. package/src/model/gemma4.ts +33 -2
  98. package/src/model/minicpm5.ts +106 -8
  99. package/src/model/qwen3.ts +276 -0
  100. package/src/model/steel-linear-kernel.ts +96 -0
  101. package/src/model/support.ts +45 -7
  102. package/src/model/universal/archs.ts +332 -0
  103. package/src/model/universal/dense.ts +576 -0
  104. package/src/model/universal/modules.ts +163 -0
  105. package/src/model/universal/rope.ts +366 -0
  106. package/src/pi-provider.ts +1 -1
  107. package/src/pi-terminal.ts +29 -8
  108. package/src/pi-web.ts +19 -2
  109. package/src/quantize/config-writer.ts +1 -1
  110. package/src/registry.ts +215 -7
  111. package/src/responses.ts +23 -0
  112. package/src/sampler.ts +220 -9
  113. package/src/serve/batch-scheduler.ts +272 -80
  114. package/src/serve/generation-gateway.ts +90 -18
  115. package/src/server.ts +643 -50
  116. package/src/spec/drafter.ts +81 -49
  117. package/src/spec/dspark/data-dflash.ts +156 -0
  118. package/src/spec/dspark/data.ts +201 -0
  119. package/src/spec/dspark/generate-dflash.ts +193 -0
  120. package/src/spec/dspark/generate.ts +290 -0
  121. package/src/spec/dspark/loss.ts +213 -0
  122. package/src/spec/dspark/module-dflash.ts +361 -0
  123. package/src/spec/dspark/module.ts +540 -0
  124. package/src/spec/dspark/sample.ts +112 -0
  125. package/src/spec/generate.ts +40 -19
  126. package/src/status-page.html +1 -1
  127. package/src/tokenizer.ts +7 -0
  128. package/src/train/diffusion-lora.ts +216 -0
  129. package/src/train/flash-cce.ts +70 -15
  130. package/src/train/fuse.ts +243 -0
  131. package/src/train/index.ts +3 -0
  132. package/src/train/job.ts +8 -0
  133. package/src/train/loss.ts +289 -30
  134. package/src/train/prefix-shared.ts +33 -5
  135. package/src/train/segmented.ts +67 -11
  136. package/src/train/trainer.ts +92 -28
  137. package/src/vision/diffusion-vision.ts +620 -0
  138. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,540 @@
1
+ // DSpark draft module — a trainable, semi-autoregressive speculative drafter
2
+ // for frozen Gemma-4 e4b. Port of the architecture in DeepSeek's DSpark paper
3
+ // ("Confidence-Scheduled Speculative Decoding with Semi-Autoregressive
4
+ // Generation"), instantiated against this repo's e4b spine.
5
+ //
6
+ // Three sub-parts (paper §3.1–3.2), all reading e4b's tapped hidden state:
7
+ // 1. DFlash backbone — a lightweight bidirectional transformer over a
8
+ // γ-token block (anchor + γ−1 learned mask tokens) plus one injected
9
+ // context token (the tapped target hidden, H_ctx). One forward pass
10
+ // predicts all γ draft positions in parallel. (paper Eq 2–3, m=1
11
+ // single-vector context for v1.)
12
+ // 2. Markov head — low-rank first-order transition bias added to the base
13
+ // logits, B(x_{k-1},·) = W1[x_{k-1}]·W2, r=256. W2 INIT 0 so the module
14
+ // starts as the pure-parallel DFlash baseline and τ climbs as the head
15
+ // learns intra-block dependency. (paper Eq 5)
16
+ // 3. Confidence head — c_k = σ(wᵀ[h_k; W1[x_{k-1}]]) ∈ (0,1), per-position
17
+ // predicted acceptance probability. (paper Eq 7)
18
+ //
19
+ // It SHARES e4b's frozen embedding + LM head (logits = the same head the
20
+ // target verifies with, so draft tokens live in the same vocab/logit space).
21
+ // Only the parts here are trainable; the target stays frozen.
22
+ //
23
+ // Tap point (v1, by design decision): the final post-norm hidden from
24
+ // model.forwardHidden — the m=1 degenerate of DFlash's multi-layer H_ctx,
25
+ // and exactly what the existing GemmaAssistantDrafter consumes. Zero changes
26
+ // to the parity-gated model files. Richer context injection (intermediate
27
+ // post-PLE residual, multi-layer concat, or borrowing the target's prefix
28
+ // K/V à la GemmaAssistantDrafter) is the main τ lever after v1.
29
+
30
+ import { MlxArray } from "../../mlx/array";
31
+ import { Dtype } from "../../mlx/ffi";
32
+ import * as ops from "../../mlx/ops";
33
+ import type { Gemma4Model } from "../../model/gemma4";
34
+ import { loadAdapterTensors } from "../../lora";
35
+ import { writeShardedSafetensors, type NamedTensor } from "../../quantize/safetensors-writer";
36
+ import { processLogits, sampleToken, KeyStream, type DSparkSampleConfig } from "./sample";
37
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
38
+ import { join } from "node:path";
39
+
40
+ export interface DSparkConfig {
41
+ /** Max draft-block size γ (paper default 5). */
42
+ gamma: number;
43
+ /** Backbone hidden width. */
44
+ dDraft: number;
45
+ /** Backbone depth (paper default 5; 2 already beats 5-layer DFlash). */
46
+ nLayers: number;
47
+ /** Backbone attention heads (dDraft must be divisible). */
48
+ nHeads: number;
49
+ /** Markov low-rank dim r (paper default 256). */
50
+ markovRank: number;
51
+ /** FFN expansion multiple (dInter = ffMult·dDraft). */
52
+ ffMult: number;
53
+ }
54
+
55
+ export const DEFAULT_DSPARK_CONFIG: DSparkConfig = {
56
+ gamma: 5,
57
+ dDraft: 1024,
58
+ nLayers: 2,
59
+ nHeads: 8,
60
+ markovRank: 256,
61
+ ffMult: 2,
62
+ };
63
+
64
+ /** Dims read from the live target model — never hardcoded. */
65
+ export interface TargetDims {
66
+ hiddenSize: number;
67
+ vocabSize: number;
68
+ eps: number;
69
+ }
70
+
71
+ /** Output of the parallel training forward over a batch of A anchors. */
72
+ export interface DSparkTrainOut {
73
+ /** Draft logits U_k + B_k, pre-softmax. [A, γ, V] (float32). */
74
+ draftLogits: MlxArray;
75
+ /** Per-position predicted acceptance probability c_k ∈ (0,1). [A, γ]. */
76
+ conf: MlxArray;
77
+ }
78
+
79
+ /** Output of one inference draft-block generation. */
80
+ export interface DSparkDraftBlock {
81
+ /** Drafted token ids d_1..d_γ. */
82
+ tokens: number[];
83
+ /** Per-position confidence c_1..c_γ. */
84
+ conf: number[];
85
+ /** Raw per-position draft logits U_k+B_k [1, γ, V] (given the sampled prefix).
86
+ * The verifier reprocesses these with the same sampling config to recover the
87
+ * draft distribution q_k for the accept/residual rule. Caller disposes. */
88
+ draftLogits: MlxArray;
89
+ }
90
+
91
+ /** Options for one draft-block generation. */
92
+ export interface DSparkDraftOpts {
93
+ /** Sampling config; omit (or temperature 0) ⇒ greedy argmax. */
94
+ sample?: DSparkSampleConfig;
95
+ /** Shared key stream (so draft + verify draws are reproducible together). */
96
+ keys?: KeyStream;
97
+ }
98
+
99
+ const CDT = Dtype.float32; // compute & param dtype for the small draft module
100
+
101
+ function disposing(old: MlxArray, next: MlxArray): MlxArray {
102
+ old.dispose();
103
+ return next;
104
+ }
105
+
106
+ // --- seeded init --------------------------------------------------------
107
+ // xorshift32 + Box–Muller. Deterministic given a seed so checkpoints and
108
+ // smoke tests reproduce. (Date/Math.random are unavailable in some harness
109
+ // contexts; this is self-contained anyway.)
110
+ function makeRng(seed: number): () => number {
111
+ let s = seed >>> 0 || 0x9e3779b9;
112
+ return () => {
113
+ s ^= s << 13; s >>>= 0;
114
+ s ^= s >> 17;
115
+ s ^= s << 5; s >>>= 0;
116
+ return s / 0x100000000;
117
+ };
118
+ }
119
+
120
+ function normalArray(rng: () => number, shape: number[], std: number): MlxArray {
121
+ const n = shape.reduce((a, b) => a * b, 1);
122
+ const out = new Float32Array(n);
123
+ for (let i = 0; i < n; i += 2) {
124
+ const u1 = Math.max(rng(), 1e-12);
125
+ const u2 = rng();
126
+ const r = Math.sqrt(-2 * Math.log(u1));
127
+ out[i] = r * Math.cos(2 * Math.PI * u2) * std;
128
+ if (i + 1 < n) out[i + 1] = r * Math.sin(2 * Math.PI * u2) * std;
129
+ }
130
+ return MlxArray.fromFloat32(out, shape);
131
+ }
132
+
133
+ function zerosArray(shape: number[]): MlxArray {
134
+ const n = shape.reduce((a, b) => a * b, 1);
135
+ return MlxArray.fromFloat32(new Float32Array(n), shape);
136
+ }
137
+
138
+ function constArray(shape: number[], value: number): MlxArray {
139
+ const n = shape.reduce((a, b) => a * b, 1);
140
+ return MlxArray.fromFloat32(new Float32Array(n).fill(value), shape);
141
+ }
142
+
143
+ export class DSparkDrafter {
144
+ readonly cfg: DSparkConfig;
145
+ readonly dims: TargetDims;
146
+ /** Stamped onto the checkpoint so the registry can key it to its target. */
147
+ readonly targetId: string;
148
+ /** Trainable params keyed by stable name. */
149
+ #p = new Map<string, MlxArray>();
150
+ /** Canonical flat order — the only source of truth for param ordering. */
151
+ readonly names: string[];
152
+
153
+ private constructor(cfg: DSparkConfig, dims: TargetDims, targetId: string) {
154
+ this.cfg = cfg;
155
+ this.dims = dims;
156
+ this.targetId = targetId;
157
+ if (cfg.dDraft % cfg.nHeads !== 0)
158
+ throw new Error(`dDraft ${cfg.dDraft} not divisible by nHeads ${cfg.nHeads}`);
159
+ this.names = buildNames(cfg);
160
+ }
161
+
162
+ static dimsOf(model: Gemma4Model): TargetDims {
163
+ const t = model.config.text;
164
+ return { hiddenSize: t.hiddenSize, vocabSize: t.vocabSize, eps: t.rmsNormEps };
165
+ }
166
+
167
+ /** Fresh, randomly initialized module sized to `model`. W2=0 (pure DFlash
168
+ * at init), confidence head zero (c≈0.5 at init). */
169
+ static init(model: Gemma4Model, cfg: DSparkConfig, targetId: string, seed = 0): DSparkDrafter {
170
+ return DSparkDrafter.initFromDims(DSparkDrafter.dimsOf(model), cfg, targetId, seed);
171
+ }
172
+
173
+ /** As `init` but sized from explicit dims (no model needed — used by tests
174
+ * and the offline checkpoint path). */
175
+ static initFromDims(dims: TargetDims, cfg: DSparkConfig, targetId: string, seed = 0): DSparkDrafter {
176
+ const d = new DSparkDrafter(cfg, dims, targetId);
177
+ const rng = makeRng(seed);
178
+ const { dDraft, nLayers, markovRank: r, ffMult } = cfg;
179
+ const dInter = ffMult * dDraft;
180
+ const { hiddenSize: H, vocabSize: V } = d.dims;
181
+ const set = (n: string, a: MlxArray) => d.#p.set(n, a.eval());
182
+
183
+ for (let i = 0; i < nLayers; i++) {
184
+ set(`bb.${i}.attn_norm`, constArray([dDraft], 1.0));
185
+ for (const w of ["q", "k", "v", "o"]) set(`bb.${i}.${w}`, normalArray(rng, [dDraft, dDraft], 0.02));
186
+ set(`bb.${i}.ff_norm`, constArray([dDraft], 1.0));
187
+ set(`bb.${i}.gate`, normalArray(rng, [dDraft, dInter], 0.02));
188
+ set(`bb.${i}.up`, normalArray(rng, [dDraft, dInter], 0.02));
189
+ set(`bb.${i}.down`, normalArray(rng, [dInter, dDraft], 0.02));
190
+ }
191
+ set("ctx_proj", normalArray(rng, [H, dDraft], 0.02));
192
+ set("tok_proj", normalArray(rng, [H, dDraft], 0.02));
193
+ set("mask_emb", normalArray(rng, [cfg.gamma - 1, dDraft], 0.02));
194
+ set("pos_emb", normalArray(rng, [cfg.gamma + 1, dDraft], 0.02));
195
+ set("out_norm", constArray([dDraft], 1.0));
196
+ set("out_proj", normalArray(rng, [dDraft, H], 0.02));
197
+ set("markov.w1", normalArray(rng, [V, r], 0.02));
198
+ set("markov.w2", zerosArray([r, V])); // ← starts as pure DFlash
199
+ set("conf.w", zerosArray([dDraft + r, 1])); // ← c≈0.5 at init
200
+ set("conf.b", zerosArray([1]));
201
+ return d;
202
+ }
203
+
204
+ get(name: string): MlxArray {
205
+ const a = this.#p.get(name);
206
+ if (!a) throw new Error(`DSpark param not found: ${name}`);
207
+ return a;
208
+ }
209
+
210
+ /** Params in canonical flat order — the leaves ValueAndGrad differentiates. */
211
+ flatParams(): MlxArray[] {
212
+ return this.names.map((n) => this.get(n));
213
+ }
214
+
215
+ /** Run `fn` with `primals` temporarily installed as the params (then restore
216
+ * the originals). Mirrors the LoRA swapPrimals discipline — lets a
217
+ * ValueAndGrad closure differentiate w.r.t. the flat param list while the
218
+ * forward reads params by name. */
219
+ useParams<T>(primals: MlxArray[], fn: () => T): T {
220
+ if (primals.length !== this.names.length)
221
+ throw new Error(`useParams: ${primals.length} primals vs ${this.names.length} params`);
222
+ const saved = this.names.map((n) => this.get(n));
223
+ this.names.forEach((n, i) => this.#p.set(n, primals[i]!));
224
+ try {
225
+ return fn();
226
+ } finally {
227
+ this.names.forEach((n, i) => this.#p.set(n, saved[i]!));
228
+ }
229
+ }
230
+
231
+ /** Install the optimizer's updated leaf as the i-th param. Does NOT dispose
232
+ * the previous one — AdamW.step already freed it. Use as AdamW's write
233
+ * callback: `new AdamW(d.flatParams(), opts, (i, p) => d.installParam(i, p))`. */
234
+ installParam(i: number, p: MlxArray): void {
235
+ this.#p.set(this.names[i]!, p);
236
+ }
237
+
238
+ // --- backbone -----------------------------------------------------------
239
+
240
+ /** One pre-norm bidirectional transformer layer over [A, S, dDraft]. */
241
+ #layer(i: number, x: MlxArray, A: number, S: number): MlxArray {
242
+ const { dDraft, nHeads } = this.cfg;
243
+ const hd = dDraft / nHeads;
244
+ const eps = this.dims.eps;
245
+
246
+ // attention (full / bidirectional — no mask)
247
+ const residual = x;
248
+ let n = ops.rmsNorm(x, this.get(`bb.${i}.attn_norm`), eps);
249
+ const proj = (w: string) => {
250
+ const m = ops.matmul(n, this.get(`bb.${i}.${w}`)); // [A,S,dDraft]
251
+ const r = ops.reshape(m, [A, S, nHeads, hd]);
252
+ m.dispose();
253
+ const t = ops.transposeAxes(r, [0, 2, 1, 3]); // [A,nHeads,S,hd]
254
+ r.dispose();
255
+ return t;
256
+ };
257
+ const q = proj("q");
258
+ const k = proj("k");
259
+ const v = proj("v");
260
+ n.dispose();
261
+ let attn = ops.sdpa(q, k, v, Math.pow(hd, -0.5), "", null);
262
+ q.dispose(); k.dispose(); v.dispose();
263
+ attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3])); // [A,S,nHeads,hd]
264
+ attn = disposing(attn, ops.reshape(attn, [A, S, dDraft]));
265
+ attn = disposing(attn, ops.matmul(attn, this.get(`bb.${i}.o`)));
266
+ x = ops.add(residual, attn);
267
+ attn.dispose();
268
+
269
+ // gated-GELU FFN
270
+ const res2 = x;
271
+ n = ops.rmsNorm(x, this.get(`bb.${i}.ff_norm`), eps);
272
+ const g = ops.matmul(n, this.get(`bb.${i}.gate`));
273
+ const u = ops.matmul(n, this.get(`bb.${i}.up`));
274
+ n.dispose();
275
+ const act = ops.geluApprox(g);
276
+ g.dispose();
277
+ let mlp = ops.mul(act, u);
278
+ act.dispose(); u.dispose();
279
+ mlp = disposing(mlp, ops.matmul(mlp, this.get(`bb.${i}.down`)));
280
+ const out = ops.add(res2, mlp);
281
+ res2.dispose(); mlp.dispose();
282
+ return out;
283
+ }
284
+
285
+ /** Build the [A, γ+1, dDraft] input sequence: [ctxToken, anchor, mask₁..].
286
+ * hCtx [A,H], anchorEmb [A,H] (raw embedding, no scale). */
287
+ #buildSeq(hCtx: MlxArray, anchorEmb: MlxArray, A: number): MlxArray {
288
+ const { gamma, dDraft } = this.cfg;
289
+ const posEmb = this.get("pos_emb"); // [γ+1, dDraft]
290
+
291
+ // backbone math is float32; cast the (possibly bf16) target tensors in.
292
+ const hCtxF = hCtx.dtype === CDT ? hCtx : hCtx.astype(CDT);
293
+ const anchorF = anchorEmb.dtype === CDT ? anchorEmb : anchorEmb.astype(CDT);
294
+
295
+ // ctx token = ctx_proj(hCtx) + pos[0]
296
+ let ctx = ops.matmul(hCtxF, this.get("ctx_proj")); // [A,dDraft]
297
+ if (hCtxF !== hCtx) hCtxF.dispose();
298
+ const pos0 = posEmb.slice([0, 0], [1, dDraft]);
299
+ ctx = disposing(ctx, ops.add(ctx, pos0));
300
+ pos0.dispose();
301
+ ctx = disposing(ctx, ops.reshape(ctx, [A, 1, dDraft]));
302
+
303
+ // anchor token (block position 0) = tok_proj(anchorEmb) + pos[1]
304
+ let anchor = ops.matmul(anchorF, this.get("tok_proj")); // [A,dDraft]
305
+ if (anchorF !== anchorEmb) anchorF.dispose();
306
+ const pos1 = posEmb.slice([1, 0], [2, dDraft]);
307
+ anchor = disposing(anchor, ops.add(anchor, pos1));
308
+ pos1.dispose();
309
+ anchor = disposing(anchor, ops.reshape(anchor, [A, 1, dDraft]));
310
+
311
+ const parts = [ctx, anchor];
312
+ if (gamma > 1) {
313
+ // mask tokens (block positions 1..γ-1) = mask_emb[j-1] + pos[j+1], built
314
+ // once at batch 1 then broadcast to A in a single add.
315
+ const maskEmb = this.get("mask_emb"); // [γ-1, dDraft]
316
+ const posMask = posEmb.slice([2, 0], [gamma + 1, dDraft]); // pos[2..γ] → [γ-1,dDraft]
317
+ let masks = ops.add(maskEmb, posMask); // [γ-1, dDraft]
318
+ posMask.dispose();
319
+ masks = disposing(masks, ops.reshape(masks, [1, gamma - 1, dDraft]));
320
+ const zerosA = MlxArray.fromFloat32(new Float32Array(A * dDraft), [A, 1, dDraft]);
321
+ const masksA = ops.add(masks, zerosA); // broadcast → [A, γ-1, dDraft]
322
+ masks.dispose(); zerosA.dispose();
323
+ parts.push(masksA);
324
+ }
325
+ const seq = ops.concatAxis(parts, 1); // [A, γ+1, dDraft]
326
+ for (const p of parts) p.dispose();
327
+ return seq;
328
+ }
329
+
330
+ /** Run the backbone and return the per-block-position hidden h_1..h_γ
331
+ * [A, γ, dDraft]. */
332
+ #backbone(hCtx: MlxArray, anchorEmb: MlxArray, A: number): MlxArray {
333
+ const { gamma, dDraft } = this.cfg;
334
+ const S = gamma + 1;
335
+ let h = this.#buildSeq(hCtx, anchorEmb, A);
336
+ for (let i = 0; i < this.cfg.nLayers; i++) h = disposing(h, this.#layer(i, h, A, S));
337
+ h = disposing(h, ops.rmsNorm(h, this.get("out_norm"), this.dims.eps));
338
+ // drop the ctx position (index 0); keep the γ block outputs
339
+ const block = h.slice([0, 1, 0], [A, S, dDraft]); // [A,γ,dDraft]
340
+ h.dispose();
341
+ return block;
342
+ }
343
+
344
+ /** Base logits U from the SHARED frozen LM head over the backbone block
345
+ * hidden. [A,γ,V] (model dtype). */
346
+ #baseLogits(model: Gemma4Model, block: MlxArray): MlxArray {
347
+ const hOut = ops.matmul(block, this.get("out_proj")); // [A,γ,H] (f32)
348
+ const hBf = hOut.astype(model.embed.scales.dtype); // head's activation dtype
349
+ hOut.dispose();
350
+ const logits = model.logitsFromHidden(hBf); // shared head (+softcap) [A,γ,V]
351
+ hBf.dispose();
352
+ return logits;
353
+ }
354
+
355
+ /** Markov transition bias B [A,γ,V] from teacher/prev tokens [A,γ] and the
356
+ * W1-embedding emb1 [A,γ,r] (also returned for the confidence head). */
357
+ #markov(prevToks: MlxArray): { B: MlxArray; emb1: MlxArray } {
358
+ const w1 = this.get("markov.w1"); // [V,r]
359
+ const w2 = this.get("markov.w2"); // [r,V]
360
+ const emb1 = ops.takeAxis(w1, prevToks, 0); // [A,γ,r]
361
+ const B = ops.matmul(emb1, w2); // [A,γ,V]
362
+ return { B, emb1 };
363
+ }
364
+
365
+ /** Confidence c_k = σ(wᵀ[h_k; emb1_k]). block [A,γ,dDraft], emb1 [A,γ,r]. */
366
+ #confidence(block: MlxArray, emb1: MlxArray): MlxArray {
367
+ const inp = ops.concatAxis([block, emb1], 2); // [A,γ,dDraft+r]
368
+ let z = ops.matmul(inp, this.get("conf.w")); // [A,γ,1]
369
+ inp.dispose();
370
+ z = disposing(z, ops.add(z, this.get("conf.b")));
371
+ z = disposing(z, ops.sigmoid(z));
372
+ const A = z.shape[0]!, G = z.shape[1]!;
373
+ const c = ops.reshape(z, [A, G]); // [A,γ]
374
+ z.dispose();
375
+ return c;
376
+ }
377
+
378
+ /**
379
+ * Parallel TRAINING forward over A anchors (teacher-forced Markov).
380
+ * - hCtx [A, H] target final hidden at each anchor (the tap, const)
381
+ * - anchorEmb [A, H] raw embedding of the anchor token x0 (const)
382
+ * - prevToks [A, γ] int32: [x0, x*_1, …, x*_{γ-1}] (teacher tokens)
383
+ * Returns draftLogits = U + B [A,γ,V] (float32) and conf [A,γ].
384
+ */
385
+ forwardTrain(model: Gemma4Model, hCtx: MlxArray, anchorEmb: MlxArray, prevToks: MlxArray): DSparkTrainOut {
386
+ const A = hCtx.shape[0]!;
387
+ const block = this.#backbone(hCtx, anchorEmb, A); // [A,γ,dDraft]
388
+ const U = this.#baseLogits(model, block); // [A,γ,V]
389
+ const Uf = U.astype(CDT); U.dispose();
390
+ const { B, emb1 } = this.#markov(prevToks);
391
+ let draftLogits = ops.add(Uf, B); // [A,γ,V] f32
392
+ Uf.dispose(); B.dispose();
393
+ const conf = this.#confidence(block, emb1);
394
+ block.dispose(); emb1.dispose();
395
+ return { draftLogits, conf };
396
+ }
397
+
398
+ /**
399
+ * INFERENCE: generate one γ-block by the semi-autoregressive rule — the
400
+ * backbone runs ONCE (parallel U_1..U_γ), then the Markov head walks the block
401
+ * left-to-right (each position conditions on the previously emitted draft
402
+ * token). Greedy argmax when no sampling config (or temperature 0); otherwise
403
+ * samples each token from the SAME processed distribution the verifier
404
+ * reconstructs (so q is consistent). hCtx [1,H], anchorTok the bonus token.
405
+ * Returns the raw per-position logits so the verifier can recover q.
406
+ */
407
+ forwardInfer(model: Gemma4Model, hCtx: MlxArray, anchorTok: number, gamma: number, opts: DSparkDraftOpts = {}): DSparkDraftBlock {
408
+ const { dDraft, markovRank: r } = this.cfg;
409
+ const V = this.dims.vocabSize;
410
+ const A = 1;
411
+ const sample = opts.sample && opts.sample.temperature > 0 ? opts.sample : null;
412
+ const keys = opts.keys ?? new KeyStream(opts.sample?.seed ?? 0);
413
+
414
+ // anchor embedding (raw)
415
+ const ids = ops.fromInt32([anchorTok], [1, 1]);
416
+ const anchorEmb3 = model.embed.encode(ids); // [1,1,H]
417
+ ids.dispose();
418
+ const anchorEmb = ops.reshape(anchorEmb3, [1, this.dims.hiddenSize]);
419
+ anchorEmb3.dispose();
420
+
421
+ const block = this.#backbone(hCtx, anchorEmb, A); // [1,γ,dDraft]
422
+ anchorEmb.dispose();
423
+ const U = this.#baseLogits(model, block); // [1,γ,V]
424
+ const Uf = U.astype(CDT); U.dispose();
425
+
426
+ const w1 = this.get("markov.w1"); // [V,r]
427
+ const w2 = this.get("markov.w2"); // [r,V]
428
+ const tokens: number[] = [];
429
+ const conf: number[] = [];
430
+ const perPosLogits: MlxArray[] = []; // each [1,1,V], stacked into draftLogits
431
+ let prevTok = anchorTok;
432
+ for (let k = 0; k < gamma; k++) {
433
+ // U_k [1,V]
434
+ const Uk = Uf.slice([0, k, 0], [1, k + 1, V]);
435
+ const UkFlat = ops.reshape(Uk, [1, V]);
436
+ Uk.dispose();
437
+ // B_k = W1[prevTok] · W2 → [1,V]
438
+ const prevIdx = ops.fromInt32([prevTok], [1]);
439
+ const e1 = ops.takeAxis(w1, prevIdx, 0); // [1,r]
440
+ prevIdx.dispose();
441
+ const Bk = ops.matmul(e1, w2); // [1,V]
442
+ const logitsK = ops.add(UkFlat, Bk); // raw U_k + B_k [1,V]
443
+ UkFlat.dispose(); Bk.dispose();
444
+
445
+ let tok: number;
446
+ if (sample) {
447
+ const scaled = processLogits(logitsK, sample); // top-p/top-k/temp
448
+ tok = sampleToken(scaled, keys.next());
449
+ scaled.dispose();
450
+ } else {
451
+ const am = ops.argmaxAxis(logitsK, -1);
452
+ tok = ops.itemUint32(am);
453
+ am.dispose();
454
+ }
455
+ perPosLogits.push(ops.reshape(logitsK, [1, 1, V]));
456
+ logitsK.dispose();
457
+
458
+ // confidence c_k = σ(conf.w·[h_k; e1] + b)
459
+ const hk = block.slice([0, k, 0], [1, k + 1, dDraft]);
460
+ const hkFlat = ops.reshape(hk, [1, dDraft]);
461
+ hk.dispose();
462
+ const ci = ops.concatAxis([hkFlat, e1], 1); // [1,dDraft+r]
463
+ hkFlat.dispose(); e1.dispose();
464
+ let cz = ops.matmul(ci, this.get("conf.w"));
465
+ ci.dispose();
466
+ cz = disposing(cz, ops.add(cz, this.get("conf.b")));
467
+ cz = disposing(cz, ops.sigmoid(cz));
468
+ const cv = cz.toFloat32()[0]!;
469
+ cz.dispose();
470
+ tokens.push(tok);
471
+ conf.push(cv);
472
+ prevTok = tok;
473
+ }
474
+ Uf.dispose();
475
+ block.dispose();
476
+ const draftLogits = ops.concatAxis(perPosLogits, 1); // [1,γ,V]
477
+ for (const a of perPosLogits) a.dispose();
478
+ return { tokens, conf, draftLogits };
479
+ }
480
+
481
+ // --- persistence --------------------------------------------------------
482
+
483
+ /** Save params + a sidecar dspark.json (config + target identity). */
484
+ save(dir: string): void {
485
+ mkdirSync(dir, { recursive: true });
486
+ const tensors: NamedTensor[] = this.names.map((name) => ({ name, array: this.get(name) }));
487
+ writeShardedSafetensors(dir, tensors);
488
+ const meta = {
489
+ kind: "dspark-drafter",
490
+ version: 1,
491
+ target_id: this.targetId,
492
+ config: this.cfg,
493
+ dims: this.dims,
494
+ param_names: this.names,
495
+ };
496
+ writeFileSync(join(dir, "dspark.json"), JSON.stringify(meta, null, 2));
497
+ }
498
+
499
+ /** Load a saved module. Validates only that the tensor set matches the
500
+ * declared names (structural staleness lives in the registry association,
501
+ * not a runtime check). */
502
+ static load(dir: string): DSparkDrafter {
503
+ const metaPath = join(dir, "dspark.json");
504
+ if (!existsSync(metaPath)) throw new Error(`no dspark.json in ${dir}`);
505
+ const meta = JSON.parse(readFileSync(metaPath, "utf8")) as {
506
+ target_id: string; config: DSparkConfig; dims: TargetDims; param_names: string[];
507
+ };
508
+ const d = new DSparkDrafter(meta.config, meta.dims, meta.target_id);
509
+ const tensors = loadAdapterTensors(join(dir, "model.safetensors"));
510
+ try {
511
+ for (const name of d.names) {
512
+ const a = tensors.get(name);
513
+ if (!a) throw new Error(`checkpoint missing tensor: ${name}`);
514
+ d.#p.set(name, a);
515
+ }
516
+ } finally {
517
+ // dispose anything not adopted
518
+ for (const [name, arr] of tensors) if (!d.names.includes(name)) arr.dispose();
519
+ }
520
+ return d;
521
+ }
522
+
523
+ dispose(): void {
524
+ for (const a of this.#p.values()) a.dispose();
525
+ this.#p.clear();
526
+ }
527
+ }
528
+
529
+ /** Canonical flat param-name order (single source of truth). */
530
+ function buildNames(cfg: DSparkConfig): string[] {
531
+ const names: string[] = [];
532
+ for (let i = 0; i < cfg.nLayers; i++) {
533
+ names.push(`bb.${i}.attn_norm`, `bb.${i}.q`, `bb.${i}.k`, `bb.${i}.v`, `bb.${i}.o`,
534
+ `bb.${i}.ff_norm`, `bb.${i}.gate`, `bb.${i}.up`, `bb.${i}.down`);
535
+ }
536
+ names.push("ctx_proj", "tok_proj", "mask_emb", "pos_emb", "out_norm", "out_proj",
537
+ "markov.w1", "markov.w2", "conf.w", "conf.b");
538
+ return names;
539
+ }
540
+
@@ -0,0 +1,112 @@
1
+ // Sampling support for DSpark speculative decoding at temperature > 0.
2
+ //
3
+ // Lossless speculative SAMPLING (Leviathan/Chen): a drafted token x sampled
4
+ // from draft dist q is accepted w.p. min(1, p(x)/q(x)) where p is the target
5
+ // dist; on rejection x is resampled from the residual norm(relu(p − q)); if the
6
+ // whole block is accepted a bonus token is sampled from p. The emitted stream is
7
+ // then distributed EXACTLY as if the target sampled token-by-token from p —
8
+ // independent of q (q only moves the acceptance rate τ). This is the
9
+ // temperature analog of the greedy longest-prefix gate.
10
+ //
11
+ // `p` and `q` must be the SAME processing applied to target/draft logits — so we
12
+ // reuse the server sampler's exact top-p/top-k/temperature path (sampler.ts).
13
+ // HLG/curve samplers are out of scope here; the caller rejects them.
14
+
15
+ import { MlxArray } from "../../mlx/array";
16
+ import { Dtype } from "../../mlx/ffi";
17
+ import * as ops from "../../mlx/ops";
18
+ import { applyTopP, applyTopK, toLogprobs } from "../../sampler";
19
+
20
+ export interface DSparkSampleConfig {
21
+ /** temperature > 0 enables sampling; 0 ⇒ greedy (handled by the caller). */
22
+ temperature: number;
23
+ topP?: number;
24
+ topK?: number;
25
+ seed?: number;
26
+ }
27
+
28
+ const GOLDEN = 0x9e3779b97f4a7c15n;
29
+ const U64 = 0xffffffffffffffffn;
30
+
31
+ /** Seeded stream of mlx random keys — reproducible draws without sharing global
32
+ * RNG state (same derivation as sampler.ts stepKey). */
33
+ export class KeyStream {
34
+ #seed: bigint;
35
+ #ctr = 0;
36
+ constructor(seed = 0) { this.#seed = BigInt(seed >>> 0); }
37
+ next(): MlxArray {
38
+ const mixed = (this.#seed ^ ((BigInt(this.#ctr++) + 1n) * GOLDEN)) & U64;
39
+ return ops.randomKey(mixed);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Processed categorical logits for one position: logits [1,V] → masked +
45
+ * temperature-scaled logits [1,V] (the input to randomCategorical;
46
+ * softmax(·) is the sampling distribution). Mirrors makeSampler's top-p/top-k/
47
+ * temperature path exactly. temperature must be > 0.
48
+ */
49
+ export function processLogits(logits: MlxArray, cfg: DSparkSampleConfig): MlxArray {
50
+ if (cfg.temperature <= 0) throw new Error("processLogits requires temperature > 0");
51
+ const lp = toLogprobs(logits); // [1,V]
52
+ let cur = lp;
53
+ const owned: MlxArray[] = [lp];
54
+ if (cfg.topP && cfg.topP > 0 && cfg.topP < 1) { cur = applyTopP(cur, cfg.topP); owned.push(cur); }
55
+ if (cfg.topK && cfg.topK > 0) { cur = applyTopK(cur, cfg.topK); owned.push(cur); }
56
+ const scaled = ops.mulScalar(cur, 1 / cfg.temperature);
57
+ for (const a of owned) a.dispose();
58
+ return scaled;
59
+ }
60
+
61
+ /** softmax of processed logits → probability vector [1,V]. */
62
+ export function probsOf(scaled: MlxArray): MlxArray {
63
+ return ops.softmaxAxis(scaled, 1, true);
64
+ }
65
+
66
+ /** Sample one token id from processed logits [1,V] (∝ softmax). */
67
+ export function sampleToken(scaled: MlxArray, key: MlxArray): number {
68
+ const t = ops.randomCategorical(scaled, key); // [1] uint32
69
+ const id = ops.itemUint32(t);
70
+ t.dispose();
71
+ return id;
72
+ }
73
+
74
+ /** Probability mass a [1,V] distribution assigns to token id (host scalar). */
75
+ export function probAtToken(probs: MlxArray, id: number): number {
76
+ const sl = probs.slice([0, id], [1, id + 1]); // [1,1]
77
+ const v = sl.toFloat32()[0]!;
78
+ sl.dispose();
79
+ return v;
80
+ }
81
+
82
+ /** Sample one token from the residual norm(relu(p − q)) over [1,V]. Falls back
83
+ * to sampling from p if the residual is degenerate (mass ≈ 0). */
84
+ export function sampleResidual(p: MlxArray, q: MlxArray, key: MlxArray): number {
85
+ const diff = ops.sub(p, q);
86
+ const zero = MlxArray.fromFloat32(new Float32Array([0]), []);
87
+ const relu = ops.maximum(diff, zero);
88
+ diff.dispose(); zero.dispose();
89
+ const mass = ops.sumAxis(relu, 1, true); // [1,1]
90
+ const massV = mass.toFloat32()[0]!;
91
+ if (!(massV > 1e-12)) {
92
+ // degenerate (p ⪯ q everywhere) — sample from p directly.
93
+ relu.dispose(); mass.dispose();
94
+ const lp = ops.log(p);
95
+ const id = sampleToken(lp, key);
96
+ lp.dispose();
97
+ return id;
98
+ }
99
+ const resid = ops.div(relu, mass); // normalized [1,V]
100
+ relu.dispose(); mass.dispose();
101
+ // randomCategorical samples ∝ exp(logits) → feed log(resid).
102
+ const eps = MlxArray.fromFloat32(new Float32Array([1e-30]), []);
103
+ const safe = ops.add(resid, eps);
104
+ resid.dispose(); eps.dispose();
105
+ const logits = ops.log(safe);
106
+ safe.dispose();
107
+ const id = sampleToken(logits, key);
108
+ logits.dispose();
109
+ return id;
110
+ }
111
+
112
+ export { Dtype };