mlx-bun 0.3.0 → 0.5.0

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 (231) hide show
  1. package/README.md +57 -32
  2. package/docs/README.md +58 -42
  3. package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
  4. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  5. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  6. package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
  7. package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
  8. package/docs/design/batching.md +1535 -50
  9. package/docs/design/decode-speed-program.md +394 -1171
  10. package/docs/design/generic-model-support.md +195 -3
  11. package/docs/design/kv-cache.md +538 -225
  12. package/docs/design/speculative-decoding.md +552 -50
  13. package/docs/design/turboquant.md +140 -17
  14. package/docs/design/unified-engine-frontier-plan.md +259 -150
  15. package/docs/planning/PRODUCT_ROADMAP.md +21 -6
  16. package/docs/planning/public-readiness-handoff.md +244 -0
  17. package/docs/planning/public-readiness.md +149 -0
  18. package/docs/reference/benchmarks.md +3537 -1112
  19. package/docs/reference/cli.md +102 -2
  20. package/docs/reference/distribution.md +45 -13
  21. package/docs/reference/environment.md +159 -13
  22. package/docs/reference/library-api.md +18 -4
  23. package/docs/reference/memory.md +1 -1
  24. package/docs/reference/models.md +28 -7
  25. package/docs/reference/server-api.md +257 -33
  26. package/docs/reference/server-config.md +372 -151
  27. package/docs/reference/troubleshooting.md +13 -21
  28. package/package.json +2 -2
  29. package/src/audio/audiotoolbox.ts +112 -0
  30. package/src/audio/conformer.ts +8 -0
  31. package/src/audio/mic-capture.ts +98 -0
  32. package/src/audio/silero-vad.ts +379 -0
  33. package/src/audio/whisper-decode.ts +774 -0
  34. package/src/audio/whisper-fast.ts +732 -0
  35. package/src/audio/whisper-format.ts +73 -0
  36. package/src/audio/whisper-mel.ts +185 -0
  37. package/src/audio/whisper-timing.ts +274 -0
  38. package/src/audio/whisper-tokenizer.ts +239 -0
  39. package/src/audio/whisper-transcribe.ts +410 -0
  40. package/src/backends/mlx/assistant-target.ts +46 -0
  41. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  42. package/src/backends/mlx/autoregressive.ts +22 -1
  43. package/src/backends/mlx/batch-group.ts +464 -236
  44. package/src/backends/mlx/cache-attachment.ts +67 -0
  45. package/src/backends/mlx/cache-layout.ts +31 -0
  46. package/src/backends/mlx/checkpoint-state.ts +53 -0
  47. package/src/backends/mlx/continuation-persistence.ts +107 -0
  48. package/src/backends/mlx/continuation-request.ts +43 -0
  49. package/src/backends/mlx/continuation.ts +77 -0
  50. package/src/backends/mlx/draft-target.ts +7 -22
  51. package/src/backends/mlx/encoder-cache.ts +25 -0
  52. package/src/backends/mlx/fill-append.ts +21 -8
  53. package/src/backends/mlx/fill-group.ts +497 -0
  54. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  55. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  56. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  57. package/src/backends/mlx/gateway-binding.ts +122 -19
  58. package/src/backends/mlx/kv-codec.ts +9 -0
  59. package/src/backends/mlx/kv-maintenance.ts +161 -0
  60. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  61. package/src/backends/mlx/mixed-iteration.ts +62 -0
  62. package/src/backends/mlx/model-prompt.ts +46 -32
  63. package/src/backends/mlx/model-serving.ts +5 -4
  64. package/src/backends/mlx/paged-cache-codec.ts +27 -0
  65. package/src/backends/mlx/prefill-cohort.ts +100 -0
  66. package/src/backends/mlx/prefill-policy.ts +26 -0
  67. package/src/backends/mlx/prefill-rows.ts +244 -0
  68. package/src/backends/mlx/prefill.ts +1 -1
  69. package/src/backends/mlx/projected-context-rows.ts +72 -0
  70. package/src/backends/mlx/prompt-input.ts +29 -0
  71. package/src/backends/mlx/qwen-prompt-input.ts +40 -0
  72. package/src/backends/mlx/request-state-policy.ts +54 -0
  73. package/src/backends/mlx/rollback.ts +45 -4
  74. package/src/backends/mlx/row-sampling.ts +49 -0
  75. package/src/backends/mlx/serial-executor.ts +51 -73
  76. package/src/backends/mlx/speculative-group.ts +438 -0
  77. package/src/backends/mlx/speculative-round.ts +161 -0
  78. package/src/backends/mlx/speculative.ts +24 -7
  79. package/src/backends/mlx/state-rows.ts +104 -0
  80. package/src/backends/mlx/state-views.ts +21 -1
  81. package/src/backends/mlx/target-layout-capability.ts +35 -0
  82. package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
  83. package/src/cli.ts +333 -36
  84. package/src/contracts/execution.ts +21 -0
  85. package/src/contracts/object-cache.ts +8 -0
  86. package/src/contracts/pi-provider.ts +1 -1
  87. package/src/contracts/pi.ts +3 -1
  88. package/src/contracts/prefix-cache.ts +33 -0
  89. package/src/contracts/resources.ts +7 -0
  90. package/src/contracts/sampling.ts +23 -0
  91. package/src/contracts/scheduling.ts +27 -0
  92. package/src/engine/execution-plan.ts +38 -25
  93. package/src/engine/execution-tasks.ts +44 -0
  94. package/src/engine/namespaced-cache.ts +13 -0
  95. package/src/engine/resources.ts +12 -1
  96. package/src/engine/scheduler.ts +32 -7
  97. package/src/fill/fill-session.ts +14 -3
  98. package/src/fit.ts +29 -15
  99. package/src/generate.ts +105 -195
  100. package/src/grammar.ts +13 -0
  101. package/src/inference/draft-acceptance.ts +41 -0
  102. package/src/inference/draft-output.ts +49 -0
  103. package/src/inference/prefill.ts +6 -0
  104. package/src/inference/rollback.ts +2 -2
  105. package/src/kv-scheme.ts +92 -12
  106. package/src/kv-store.ts +183 -93
  107. package/src/lab/README.md +5 -1
  108. package/src/lab/paged-kv/paged-attention.ts +117 -0
  109. package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
  110. package/src/lab/paged-kv/paged-kv.ts +126 -46
  111. package/src/md-modules.d.ts +5 -0
  112. package/src/mlx/array.ts +39 -1
  113. package/src/mlx/ffi.ts +19 -0
  114. package/src/mlx/normalized-argmax.ts +116 -0
  115. package/src/mlx/ops.ts +97 -10
  116. package/src/model/artifact-identity.ts +73 -0
  117. package/src/model/batched-kv.ts +235 -0
  118. package/src/model/batched-mask.ts +12 -4
  119. package/src/model/batched-quant.ts +8 -2
  120. package/src/model/batched-quantized-kv.ts +144 -0
  121. package/src/model/batched-rotating-quant.ts +76 -15
  122. package/src/model/batched-rotating-state.ts +76 -0
  123. package/src/model/batched-rotating.ts +64 -22
  124. package/src/model/batched-row-storage.ts +52 -0
  125. package/src/model/batched-ssm.ts +52 -0
  126. package/src/model/batched-turboquant-kv.ts +94 -0
  127. package/src/model/compiled-decode.ts +7 -1
  128. package/src/model/decoded-kv-donor.ts +12 -0
  129. package/src/model/delayed-quantized-kv.ts +72 -0
  130. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  131. package/src/model/delayed-turboquant-kv.ts +57 -0
  132. package/src/model/factory.ts +26 -0
  133. package/src/model/fingerprint.ts +12 -0
  134. package/src/model/full-prefill-padding.ts +56 -0
  135. package/src/model/full-prefill-row.ts +105 -0
  136. package/src/model/gemma4-base.ts +183 -82
  137. package/src/model/gemma4.ts +116 -28
  138. package/src/model/generated/gemma4-12b.ts +4 -1
  139. package/src/model/generated/gemma4-26b.ts +4 -1
  140. package/src/model/generated/gemma4-e4b.ts +4 -1
  141. package/src/model/glm52-cache.ts +80 -4
  142. package/src/model/glm52-mla.ts +12 -6
  143. package/src/model/glm52.ts +19 -40
  144. package/src/model/kv-attention-view.ts +70 -0
  145. package/src/model/minicpm5.ts +11 -7
  146. package/src/model/profile.ts +29 -4
  147. package/src/model/quantized-append-attention.ts +31 -0
  148. package/src/model/qwen3-delta-state.ts +74 -0
  149. package/src/model/qwen3-delta.ts +59 -36
  150. package/src/model/qwen3-mrope.ts +7 -9
  151. package/src/model/qwen3_5.ts +177 -75
  152. package/src/model/rotating-kv-donor.ts +35 -0
  153. package/src/model/rotating-kv-layout.ts +262 -0
  154. package/src/model/rotating-row-transaction.ts +35 -0
  155. package/src/model/speculative-rotating-kv.ts +101 -0
  156. package/src/model/ssm-prefill-padding.ts +39 -0
  157. package/src/model/support.ts +8 -0
  158. package/src/model/token-groups.ts +66 -0
  159. package/src/model/trellis-linear.ts +4 -4
  160. package/src/model/turboquant-codec.ts +89 -0
  161. package/src/model/universal/dense.ts +14 -13
  162. package/src/model/whisper.ts +435 -0
  163. package/src/native/mic_capture.swift +92 -0
  164. package/src/native-pack.ts +6 -5
  165. package/src/pi-web.ts +9 -1
  166. package/src/preflight.ts +31 -15
  167. package/src/prompt-cache.ts +306 -86
  168. package/src/runtime-config.ts +1 -5
  169. package/src/safetensors.ts +9 -2
  170. package/src/sampler.ts +37 -25
  171. package/src/serve/audio-routes.ts +323 -0
  172. package/src/serve/chat-request.ts +2 -0
  173. package/src/serve/chat-stage.ts +19 -14
  174. package/src/serve/completion-executor.ts +9 -1
  175. package/src/serve/discovery-routes.ts +17 -1
  176. package/src/serve/generated-token-history.ts +57 -0
  177. package/src/serve/generation-gateway.ts +88 -38
  178. package/src/serve/model-binding.ts +7 -1
  179. package/src/serve/model-host.ts +3 -4
  180. package/src/serve/preparation.ts +3 -3
  181. package/src/serve/prompt-response-trace.ts +14 -0
  182. package/src/serve/request-plan.ts +6 -3
  183. package/src/serve/request-prep.ts +40 -25
  184. package/src/serve/text-completion-stage.ts +2 -2
  185. package/src/serve/transcription-server.ts +64 -0
  186. package/src/serve/transcription-service.ts +502 -0
  187. package/src/server.ts +185 -261
  188. package/src/spec/assistant-rows.ts +93 -0
  189. package/src/spec/assistant-source.ts +28 -18
  190. package/src/spec/cache-identity.ts +22 -0
  191. package/src/spec/deepspec-rows.ts +29 -0
  192. package/src/spec/deepspec-source.ts +12 -10
  193. package/src/spec/dflash-source.ts +28 -4
  194. package/src/spec/drafter.ts +153 -142
  195. package/src/spec/dspark/deepspec-module.ts +95 -120
  196. package/src/spec/dspark/module-dflash.ts +126 -170
  197. package/src/spec/glm52-mtp-graph.ts +29 -0
  198. package/src/spec/glm52-mtp-rows.ts +132 -0
  199. package/src/spec/glm52-mtp-source.ts +54 -136
  200. package/src/spec/glm52-mtp-state.ts +33 -0
  201. package/src/spec/ngram-source.ts +115 -21
  202. package/src/spec/projected-draft-rows.ts +73 -0
  203. package/src/spec/qwen-mtp-module.ts +171 -0
  204. package/src/spec/qwen-mtp-rows.ts +230 -0
  205. package/src/spec/qwen-mtp-source.ts +84 -226
  206. package/src/spec/qwen-mtp-state.ts +30 -0
  207. package/src/spec/serve-loop.ts +73 -69
  208. package/src/spec/source.ts +93 -15
  209. package/src/spec/two-model-rows.ts +62 -0
  210. package/src/spec/two-model.ts +45 -103
  211. package/src/ssd-cache.ts +107 -36
  212. package/src/ssd-durability.ts +29 -27
  213. package/src/storage/host-buffer.ts +24 -0
  214. package/src/storage/kv-writer.ts +83 -0
  215. package/src/storage/kv-writer.worker.js +264 -0
  216. package/src/storage/retention-policy.ts +37 -0
  217. package/src/tiered-prompt-cache.ts +45 -0
  218. package/src/vision/embedder.ts +8 -0
  219. package/src/vision/prompt.ts +101 -65
  220. package/src/vision/qwen3vl-prompt.ts +101 -64
  221. package/src/vision/qwen3vl-tower.ts +11 -0
  222. package/src/vision/siglip.ts +8 -0
  223. package/src/web/app.html +12 -0
  224. package/src/web/app.js +217 -57
  225. package/src/web/src/chat.ts +4 -1
  226. package/src/web/src/composer.ts +3 -0
  227. package/src/web/src/voice.ts +141 -0
  228. package/src/web/sw.js +1 -1
  229. package/src/backends/mlx/memory-guard.ts +0 -40
  230. package/src/spec/prefix-state.ts +0 -45
  231. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -0,0 +1,93 @@
1
+ import type { MlxArray } from "../mlx/array";
2
+ import * as ops from "../mlx/ops";
3
+ import { materializeCopy } from "../mlx/materialize";
4
+ import { disposeResources, applyStateChanges } from "../engine/resources";
5
+ import type { PreparedStateChange } from "../contracts/resources";
6
+ import type { AssistantRowsTarget, DraftPrefillGroup, DraftRowGroup, DraftRowCheckpoint, GroupedDraftProvider } from "./source";
7
+ import type { GemmaAssistantDrafter } from "./drafter";
8
+
9
+ /** The assistant owns only its last true target hidden and coverage. Donor
10
+ * attention stays with target storage; checkpoints contain no duplicate KV. */
11
+ class AssistantRows implements DraftPrefillGroup, DraftRowGroup {
12
+ readonly prefillMode = "tail-split" as const;
13
+ readonly tapLayers: readonly number[] = [];
14
+ #hidden: MlxArray | null = null;
15
+ #processed: number[] = [];
16
+ constructor(readonly target: AssistantRowsTarget, readonly drafter: GemmaAssistantDrafter,
17
+ readonly namespace: string, checkpoints: readonly (DraftRowCheckpoint | null)[]) { this.append(checkpoints); }
18
+ get rowCount(): number { return this.#processed.length; }
19
+ append(checkpoints: readonly (DraftRowCheckpoint | null)[]): void {
20
+ if (checkpoints.length) applyStateChanges([() => this.prepareAppend(checkpoints)]);
21
+ }
22
+ prepareAppend(checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange {
23
+ const processed = [...this.#processed, ...checkpoints.map(checkpoint => checkpoint?.processedTokens ?? 0)];
24
+ const example = this.#hidden ?? checkpoints.find(checkpoint => checkpoint !== null)?.attachment.tensors[0];
25
+ const held: MlxArray[] = []; let hidden: MlxArray | null = null;
26
+ try {
27
+ for (const checkpoint of checkpoints) if (checkpoint && (checkpoint.attachment.schema !== "gemma-assistant-v1" ||
28
+ checkpoint.attachment.metadata.processedTokens !== checkpoint.processedTokens))
29
+ throw new Error("invalid assistant checkpoint alignment");
30
+ if (example) {
31
+ const zero = (rows: number) => { const value = ops.zeros([rows,1,this.target.hiddenSize],example.dtype); held.push(value); return value; };
32
+ hidden = ops.concatAxis([...(this.rowCount ? [this.#hidden ?? zero(this.rowCount)] : []),
33
+ ...checkpoints.map(checkpoint => checkpoint?.attachment.tensors[0] ?? zero(1))],0);
34
+ }
35
+ } finally { disposeResources(held); }
36
+ return { commit: () => { const old = this.#hidden; this.#hidden=hidden; hidden=old; this.#processed=processed; },
37
+ dispose: () => { hidden?.dispose(); hidden=null; } };
38
+ }
39
+ filterRows(keep: readonly number[]): void {
40
+ if (!keep.length) { this.dispose(); return; }
41
+ using indices = ops.fromInt32([...keep],[keep.length]);
42
+ const next = this.#hidden && keep.length ? ops.takeAxis(this.#hidden,indices,0) : null;
43
+ this.#hidden?.dispose(); this.#hidden=next; this.#processed=keep.map(row=>this.#processed[row]!);
44
+ }
45
+ prefill(tokens: MlxArray, context?: MlxArray): void {
46
+ const [B,N] = tokens.shape as [number,number];
47
+ using tail = context!.slice([0,N-1,0],[B,N,context!.shape[2]!]);
48
+ const hidden = materializeCopy(tail);
49
+ this.#hidden?.dispose(); this.#hidden=hidden; this.#processed=this.#processed.map(count=>count+N);
50
+ }
51
+ materialize(): void { this.#hidden?.eval(); }
52
+ capture(row: number): DraftRowCheckpoint {
53
+ const processedTokens=this.#processed[row]!;
54
+ const hidden=this.#hidden!.slice([row,0,0],[row+1,1,this.target.hiddenSize]);
55
+ return { processedTokens, attachment: { schema:"gemma-assistant-v1",metadata:{processedTokens},tensors:[hidden] } };
56
+ }
57
+ draft(pending: readonly number[], depth: number, _steps: readonly number[]): number[][] {
58
+ if (!depth) return pending.map(()=>[]);
59
+ const B=pending.length, owned: MlxArray[]=[];
60
+ const donors=this.target.readDonors();
61
+ let ids=ops.fromInt32([...pending],[B,1]), hidden=this.#hidden!;
62
+ const tokens: MlxArray[]=[];
63
+ try {
64
+ for(let step=0;step<depth;step++) {
65
+ using emb=this.target.embed(ids);
66
+ const positions=donors.positions.map(position=>position+step);
67
+ const common=positions.every(position=>position===positions[0]);
68
+ using offsets=common ? null : ops.fromInt32(positions,[B]);
69
+ const next=this.drafter.forwardRows(emb,hidden,donors,offsets ?? positions[0]!);
70
+ owned.push(next.nextHidden); tokens.push(next.tokens); hidden=next.nextHidden;
71
+ ids.dispose(); ids=ops.reshape(next.tokens,[B,1]);
72
+ }
73
+ using packed=ops.concatAxis(tokens,0);
74
+ const values=packed.toIntTokens();
75
+ return pending.map((_,row)=>Array.from({length:depth},(_,step)=>values[step*B+row]!));
76
+ } finally { ids.dispose(); donors.dispose(); disposeResources([...owned,...tokens]); }
77
+ }
78
+ commit(accepted: readonly number[], context: MlxArray): void {
79
+ using indices=ops.fromInt32([...accepted],[accepted.length,1,1]);
80
+ const hidden=ops.takeAlongAxis(context,indices,1);
81
+ this.#hidden?.dispose(); this.#hidden=hidden;
82
+ this.#processed=this.#processed.map((count,row)=>count+accepted[row]!+1);
83
+ }
84
+ dispose(): void { this.#hidden?.dispose(); this.#hidden=null; this.#processed=[]; }
85
+ }
86
+
87
+ export function assistantGroups(drafter: GemmaAssistantDrafter, namespace: string): GroupedDraftProvider {
88
+ return {
89
+ checkpointNamespace: () => namespace,
90
+ openPrefill: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
91
+ open: ({target,checkpoints}) => new AssistantRows(target.assistantRows!,drafter,namespace,checkpoints),
92
+ };
93
+ }
@@ -1,3 +1,6 @@
1
+ import * as ops from "../mlx/ops";
2
+ import { artifactIdentity } from "../model/artifact-identity";
3
+ import { assistantGroups } from "./assistant-rows";
1
4
  // AssistantSource — the optiq KV-borrowing Gemma "-assistant" drafter
2
5
  // (src/spec/drafter.ts) behind the serve-time DraftSource seam. L2 oracle:
3
6
  // optiq spec_generate. This is the SAME drafter the standalone specGenerate
@@ -22,7 +25,7 @@ import { readdirSync, statSync } from "node:fs";
22
25
  import { join } from "node:path";
23
26
  import type { MlxArray } from "../mlx/array";
24
27
  import { GemmaAssistantDrafter } from "./drafter";
25
- import type { DraftProvider, DraftSource, TargetView, AssistantTarget } from "./source";
28
+ import type { DraftProvider, DraftSource, TargetView, AssistantRowsTarget } from "./source";
26
29
 
27
30
  /** Sum of the drafter's on-disk safetensors — admission accounting only. */
28
31
  function safetensorsBytes(dir: string): number {
@@ -35,20 +38,27 @@ function safetensorsBytes(dir: string): number {
35
38
  export class AssistantProvider implements DraftProvider {
36
39
  readonly id: string;
37
40
  readonly weightsBytes: number;
41
+ readonly grouped: import("./source").GroupedDraftProvider;
38
42
 
39
43
  private constructor(
40
44
  private readonly drafter: GemmaAssistantDrafter,
41
45
  id: string,
42
- weightsBytes: number,
46
+ weightsBytes: number, namespace: string,
43
47
  ) {
44
48
  this.id = id;
45
49
  this.weightsBytes = weightsBytes;
50
+ this.grouped = assistantGroups(drafter,namespace);
46
51
  }
47
52
 
48
53
  static async load(modelDir: string): Promise<AssistantProvider> {
49
54
  const drafter = await GemmaAssistantDrafter.load(modelDir);
50
55
  const id = modelDir.split("/").filter(Boolean).at(-1)!;
51
- return new AssistantProvider(drafter, id, safetensorsBytes(modelDir));
56
+ try {
57
+ const identity = await artifactIdentity(await Bun.file(`${modelDir}/config.json`).text(),
58
+ readdirSync(modelDir).filter(file => file.endsWith(".safetensors"))
59
+ .map(name => ({ name, path: join(modelDir, name) })));
60
+ return new AssistantProvider(drafter, id, safetensorsBytes(modelDir), `gemma-assistant-v1:${identity}`);
61
+ } catch (error) { drafter.dispose(); throw error; }
52
62
  }
53
63
 
54
64
  open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
@@ -56,18 +66,18 @@ export class AssistantProvider implements DraftProvider {
56
66
  }
57
67
 
58
68
  dispose(): void {
59
- // The drafter's Weights are process-lifetime (pinned mmaps); nothing to free.
69
+ this.drafter.dispose();
60
70
  }
61
71
  }
62
72
 
63
73
  export class AssistantSource implements DraftSource {
64
74
  readonly weightsBytes = 0; // provider-owned weights; per-request adds nothing
65
- private readonly target: AssistantTarget;
75
+ private readonly target: AssistantRowsTarget;
66
76
 
67
- constructor(private readonly drafter: Pick<GemmaAssistantDrafter, "forward">, target: TargetView) {
68
- if (!target.assistant)
77
+ constructor(private readonly drafter: Pick<GemmaAssistantDrafter, "forwardRows">, target: TargetView) {
78
+ if (!target.assistantRows)
69
79
  throw new Error("assistant drafter requires a Gemma4 target with donor views");
70
- this.target = target.assistant;
80
+ this.target = target.assistantRows;
71
81
  }
72
82
 
73
83
  /** No own cache to prime — the drafter reads the target's live state. */
@@ -79,8 +89,8 @@ export class AssistantSource implements DraftSource {
79
89
  * specGenerate's 2a block. anchorHidden is borrowed; never disposed here. */
80
90
  draft(feed: number[], n: number, _stepBase: number, anchorHidden?: MlxArray): number[] {
81
91
  if (!anchorHidden) throw new Error("assistant drafter needs the target anchor hidden");
82
- const position = this.target.position();
83
92
  const shared = this.target.readDonors();
93
+ const position = shared.positions[0]!;
84
94
  const drafts: number[] = [];
85
95
  const ownedHiddens: MlxArray[] = [];
86
96
  let dTok = feed[feed.length - 1]!; // the pending token (anchorHidden's token)
@@ -88,22 +98,22 @@ export class AssistantSource implements DraftSource {
88
98
  let emb: MlxArray | null = null; // hoisted so a mid-loop throw still frees it
89
99
  try {
90
100
  for (let k = 0; k < n; k++) {
91
- emb = this.target.embedScaled(dTok);
92
- const step = this.drafter.forward(emb, dHid, shared, position + k);
101
+ using ids = ops.fromInt32([dTok], [1, 1]);
102
+ emb = this.target.embed(ids);
103
+ const step = this.drafter.forwardRows(emb, dHid, shared, position + k);
104
+ ownedHiddens.push(step.nextHidden);
105
+ let token: number;
106
+ try { token = ops.itemUint32(step.tokens); } finally { step.tokens.dispose(); }
93
107
  emb.dispose();
94
108
  emb = null;
95
- drafts.push(step.token);
96
- ownedHiddens.push(step.nextHidden);
97
- dTok = step.token;
109
+ drafts.push(token);
110
+ dTok = token;
98
111
  dHid = step.nextHidden;
99
112
  }
100
113
  } finally {
101
114
  emb?.dispose();
102
115
  for (const a of ownedHiddens) a.dispose();
103
- for (const [k, v] of [shared.sliding, shared.full]) {
104
- k.dispose();
105
- v.dispose();
106
- }
116
+ shared.dispose();
107
117
  }
108
118
  return drafts;
109
119
  }
@@ -0,0 +1,22 @@
1
+ import type { GenerateOptions } from "../generate";
2
+
3
+ /** Snapshot numerical configuration when the request is bound. Caller-owned
4
+ * arrays must not change the graph or persistence identity after grouping. */
5
+ export function captureSpeculativeOptions(options: GenerateOptions): GenerateOptions {
6
+ return { ...options,
7
+ ...(options.kvConfig ? { kvConfig: options.kvConfig.map(({ layerIdx, bits, groupSize }) =>
8
+ ({ layerIdx, bits, groupSize })).sort((a, b) => a.layerIdx - b.layerIdx) } : {}),
9
+ ...(options.turboQuant ? { turboQuant: { ...options.turboQuant } } : {}),
10
+ };
11
+ }
12
+
13
+ /** Existing namespaces remain byte-identical when no per-layer scheme is
14
+ * supplied. Execution and persistence consume the same captured settings. */
15
+ export function speculativePrefixNamespace(method: string | undefined, adapters: string | string[],
16
+ options: GenerateOptions): string {
17
+ return JSON.stringify({ method, adapters, kvBits: options.kvBits ?? null,
18
+ kvGroupSize: options.kvGroupSize ?? 64, quantizedKvStart: options.quantizedKvStart ?? null,
19
+ turboQuant: options.turboQuant ?? null,
20
+ ...(options.kvConfig?.length ? { kvConfig: options.kvConfig } : {}),
21
+ });
22
+ }
@@ -0,0 +1,29 @@
1
+ import { projectedDraftGroups } from "./projected-draft-rows";
2
+ import type { GroupedDraftProvider, TargetView } from "./source";
3
+ import type { DeepspecDrafter } from "./dspark/deepspec-module";
4
+
5
+ /** Existing pairing requirements are checked once when the provider opens. */
6
+ export function bindDeepspecTarget(target: TargetView, drafter: Pick<DeepspecDrafter, "cfg">): void {
7
+ if (!target.gemmaTaps) throw new Error("DeepSpec drafter requires a Gemma4 target");
8
+ const layers = drafter.cfg.num_target_layers;
9
+ if (target.gemmaTaps.layerCount !== layers) throw new Error(
10
+ `DeepSpec drafter was trained for a ${layers}-layer target; ` +
11
+ `this model has ${target.gemmaTaps.layerCount} layers — wrong (target, drafter) pairing`);
12
+ }
13
+
14
+ export function deepspecGroups(drafter: DeepspecDrafter, namespace: string): GroupedDraftProvider {
15
+ return projectedDraftGroups(namespace, target => {
16
+ bindDeepspecTarget(target, drafter);
17
+ return {
18
+ namespace, schema: "deepspec-context-v1",
19
+ layers: drafter.cfg.num_hidden_layers, tapLayers: drafter.tapLayers,
20
+ project(hidden, positions) {
21
+ using projected = drafter.projectContext(hidden);
22
+ return drafter.projectContextKVRows(projected, positions);
23
+ },
24
+ draft(context, pending, positions, depth) {
25
+ return drafter.draftRows(context, pending, positions).tokens.map(row => row.slice(0, depth));
26
+ },
27
+ };
28
+ });
29
+ }
@@ -22,6 +22,8 @@
22
22
  // Context rows are cached post-norm-post-rope — bit-equivalent to their
23
23
  // in-round concat (per-row RMSNorm/RoPE; argued in projectContextKV's doc).
24
24
 
25
+ import { artifactIdentity } from "../model/artifact-identity";
26
+ import { deepspecGroups, bindDeepspecTarget } from "./deepspec-rows";
25
27
  import { readdirSync, statSync } from "node:fs";
26
28
  import { join } from "node:path";
27
29
  import type { MlxArray } from "../mlx/array";
@@ -39,20 +41,27 @@ function safetensorsBytes(dir: string): number {
39
41
  export class DeepspecProvider implements DraftProvider {
40
42
  readonly id: string;
41
43
  readonly weightsBytes: number;
44
+ readonly grouped: import("./source").GroupedDraftProvider;
42
45
  /** Trained block width (config block_size, e.g. 7) — the server pins
43
46
  * numDraftTokens to this. */
44
47
  readonly gamma: number;
45
48
 
46
- private constructor(private readonly drafter: DeepspecDrafter, id: string, weightsBytes: number) {
49
+ private constructor(private readonly drafter: DeepspecDrafter, id: string, weightsBytes: number, namespace: string) {
47
50
  this.id = id;
48
51
  this.weightsBytes = weightsBytes;
49
52
  this.gamma = drafter.gamma;
53
+ this.grouped = deepspecGroups(drafter, namespace);
50
54
  }
51
55
 
52
56
  static async load(modelDir: string): Promise<DeepspecProvider> {
53
57
  const drafter = await DeepspecDrafter.load(modelDir);
54
58
  const id = modelDir.split("/").filter(Boolean).at(-1)!;
55
- return new DeepspecProvider(drafter, id, safetensorsBytes(modelDir));
59
+ try {
60
+ const identity = await artifactIdentity(await Bun.file(`${modelDir}/config.json`).text(),
61
+ readdirSync(modelDir).filter(file => file.endsWith(".safetensors"))
62
+ .map(name => ({ name, path: join(modelDir, name) })));
63
+ return new DeepspecProvider(drafter, id, safetensorsBytes(modelDir), `deepspec-context-v1:${identity}`);
64
+ } catch (error) { drafter.dispose(); throw error; }
56
65
  }
57
66
 
58
67
  open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
@@ -72,14 +81,7 @@ export class DeepspecSource implements DraftSource {
72
81
 
73
82
  constructor(private readonly drafter: Pick<DeepspecDrafter,
74
83
  "cfg" | "tapLayers" | "projectContext" | "projectContextKV" | "draftBlock">, target: TargetView) {
75
- if (!target.gemmaTaps)
76
- throw new Error("DeepSpec drafter requires a Gemma4 target");
77
- const nLayers = target.gemmaTaps.layerCount;
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
- );
84
+ bindDeepspecTarget(target, drafter);
83
85
  this.tapLayers = drafter.tapLayers;
84
86
  }
85
87
 
@@ -18,6 +18,8 @@
18
18
  // target verify decides). The richer rejection-sampling verify stays in the
19
19
  // standalone dflashGenerate for the measure script.
20
20
 
21
+ import { artifactIdentity } from "../model/artifact-identity";
22
+ import { projectedDraftGroups } from "./projected-draft-rows";
21
23
  import { readdirSync, statSync } from "node:fs";
22
24
  import { join } from "node:path";
23
25
  import type { MlxArray } from "../mlx/array";
@@ -37,20 +39,43 @@ function safetensorsBytes(dir: string): number {
37
39
  export class DflashProvider implements DraftProvider {
38
40
  readonly id: string;
39
41
  readonly weightsBytes: number;
42
+ readonly grouped: import("./source").GroupedDraftProvider;
40
43
  /** The trained block width — the server pins numDraftTokens to this so the
41
44
  * serve loop never asks for more positions than the block was trained for. */
42
45
  readonly gamma: number;
43
46
 
44
- private constructor(private readonly drafter: DflashDrafter, id: string, weightsBytes: number) {
47
+ private constructor(private readonly drafter: DflashDrafter, id: string, weightsBytes: number, namespace: string) {
45
48
  this.id = id;
46
49
  this.weightsBytes = weightsBytes;
47
50
  this.gamma = drafter.cfg.gamma;
51
+ this.grouped = projectedDraftGroups(namespace, target => {
52
+ if (!target.gemmaTaps) throw new Error("DSpark drafter requires a Gemma4 target");
53
+ const projection = target.gemmaTaps.projection;
54
+ const minConf = runtimeValue("MLX_BUN_DSPARK_MINCONF");
55
+ return {
56
+ namespace, schema: "dflash-context-v1",
57
+ layers: drafter.cfg.nLayers, tapLayers: drafter.cfg.tapLayers,
58
+ project: hidden => drafter.projectContextRows(hidden),
59
+ draft(context, pending, _positions, depth) {
60
+ return drafter.forwardRows(projection, null, pending, depth, {
61
+ collectLogits: false, collectConfidence: false,
62
+ thresholds: drafter.cfg.sts?.thresholds,
63
+ ...(minConf ? { minConf: Number(minConf) } : {}),
64
+ }, context).tokens;
65
+ },
66
+ };
67
+ });
48
68
  }
49
69
 
50
70
  static async load(modelDir: string): Promise<DflashProvider> {
51
71
  const drafter = loadDsparkDrafter(modelDir); // variant dispatch (dspark|legacy dflash)
52
72
  const id = modelDir.split("/").filter(Boolean).at(-1)!;
53
- return new DflashProvider(drafter, id, safetensorsBytes(modelDir));
73
+ try {
74
+ const identity = await artifactIdentity(await Bun.file(`${modelDir}/dspark.json`).text(),
75
+ readdirSync(modelDir).filter(file => file.endsWith(".safetensors"))
76
+ .map(name => ({ name, path: join(modelDir, name) })));
77
+ return new DflashProvider(drafter, id, safetensorsBytes(modelDir), `dflash-context-v1:${identity}`);
78
+ } catch (error) { drafter.dispose(); throw error; }
54
79
  }
55
80
 
56
81
  open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
@@ -58,8 +83,7 @@ export class DflashProvider implements DraftProvider {
58
83
  }
59
84
 
60
85
  dispose(): void {
61
- // Unlike the assistant drafter (process-pinned mmaps), DflashDrafter
62
- // materializes owned MlxArrays — free them on teardown.
86
+ // Provider-owned weights are released when the mounted draft is closed.
63
87
  this.drafter.dispose();
64
88
  }
65
89
  }