mlx-bun 0.3.0 → 0.4.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 (150) hide show
  1. package/docs/README.md +4 -1
  2. package/docs/archive/investigations/openjarvis-fit.md +198 -0
  3. package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
  4. package/docs/design/batching.md +1321 -49
  5. package/docs/design/decode-speed-program.md +331 -1163
  6. package/docs/design/generic-model-support.md +79 -3
  7. package/docs/design/kv-cache.md +198 -54
  8. package/docs/design/speculative-decoding.md +387 -24
  9. package/docs/design/turboquant.md +123 -13
  10. package/docs/design/unified-engine-frontier-plan.md +144 -142
  11. package/docs/planning/PRODUCT_ROADMAP.md +16 -0
  12. package/docs/planning/release-notes-v0.4.0.md +45 -0
  13. package/docs/reference/benchmarks.md +2457 -6
  14. package/docs/reference/cli.md +41 -2
  15. package/docs/reference/distribution.md +16 -9
  16. package/docs/reference/environment.md +55 -5
  17. package/docs/reference/models.md +13 -4
  18. package/docs/reference/server-api.md +79 -25
  19. package/docs/reference/server-config.md +178 -84
  20. package/docs/reference/troubleshooting.md +13 -21
  21. package/package.json +1 -1
  22. package/src/backends/mlx/assistant-target.ts +46 -0
  23. package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
  24. package/src/backends/mlx/batch-group.ts +368 -224
  25. package/src/backends/mlx/cache-attachment.ts +67 -0
  26. package/src/backends/mlx/cache-layout.ts +31 -0
  27. package/src/backends/mlx/checkpoint-state.ts +53 -0
  28. package/src/backends/mlx/continuation-persistence.ts +106 -0
  29. package/src/backends/mlx/continuation-request.ts +43 -0
  30. package/src/backends/mlx/continuation.ts +77 -0
  31. package/src/backends/mlx/draft-target.ts +7 -22
  32. package/src/backends/mlx/full-kv-row-append.ts +32 -0
  33. package/src/backends/mlx/full-kv-row-donor.ts +54 -0
  34. package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
  35. package/src/backends/mlx/gateway-binding.ts +77 -18
  36. package/src/backends/mlx/kv-codec.ts +9 -0
  37. package/src/backends/mlx/kv-maintenance.ts +133 -0
  38. package/src/backends/mlx/kv-tensor-rows.ts +212 -0
  39. package/src/backends/mlx/model-serving.ts +1 -1
  40. package/src/backends/mlx/prefill-cohort.ts +89 -0
  41. package/src/backends/mlx/prefill-rows.ts +200 -0
  42. package/src/backends/mlx/prefill.ts +1 -1
  43. package/src/backends/mlx/projected-context-rows.ts +72 -0
  44. package/src/backends/mlx/request-state-policy.ts +41 -0
  45. package/src/backends/mlx/rollback.ts +41 -3
  46. package/src/backends/mlx/row-sampling.ts +49 -0
  47. package/src/backends/mlx/serial-executor.ts +35 -66
  48. package/src/backends/mlx/speculative-group.ts +359 -0
  49. package/src/backends/mlx/speculative-round.ts +161 -0
  50. package/src/backends/mlx/state-rows.ts +104 -0
  51. package/src/backends/mlx/state-views.ts +21 -1
  52. package/src/backends/mlx/target-layout-capability.ts +35 -0
  53. package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
  54. package/src/cli.ts +11 -36
  55. package/src/contracts/execution.ts +11 -0
  56. package/src/contracts/prefix-cache.ts +23 -0
  57. package/src/contracts/resources.ts +7 -0
  58. package/src/contracts/sampling.ts +23 -0
  59. package/src/contracts/scheduling.ts +18 -0
  60. package/src/engine/execution-plan.ts +18 -16
  61. package/src/engine/resources.ts +12 -1
  62. package/src/engine/scheduler.ts +10 -5
  63. package/src/fit.ts +29 -15
  64. package/src/generate.ts +77 -173
  65. package/src/inference/draft-acceptance.ts +41 -0
  66. package/src/inference/draft-output.ts +49 -0
  67. package/src/inference/rollback.ts +2 -2
  68. package/src/kv-scheme.ts +70 -12
  69. package/src/kv-store.ts +70 -16
  70. package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
  71. package/src/lab/paged-kv/paged-kv.ts +25 -11
  72. package/src/mlx/array.ts +2 -1
  73. package/src/model/artifact-identity.ts +73 -0
  74. package/src/model/batched-kv.ts +235 -0
  75. package/src/model/batched-mask.ts +12 -4
  76. package/src/model/batched-quant.ts +8 -2
  77. package/src/model/batched-quantized-kv.ts +144 -0
  78. package/src/model/batched-rotating-quant.ts +76 -15
  79. package/src/model/batched-rotating-state.ts +76 -0
  80. package/src/model/batched-rotating.ts +64 -22
  81. package/src/model/batched-row-storage.ts +52 -0
  82. package/src/model/batched-ssm.ts +52 -0
  83. package/src/model/batched-turboquant-kv.ts +93 -0
  84. package/src/model/compiled-decode.ts +7 -1
  85. package/src/model/decoded-kv-donor.ts +12 -0
  86. package/src/model/delayed-quantized-kv.ts +72 -0
  87. package/src/model/delayed-rotating-quantized-kv.ts +93 -0
  88. package/src/model/delayed-turboquant-kv.ts +55 -0
  89. package/src/model/full-prefill-padding.ts +56 -0
  90. package/src/model/full-prefill-row.ts +105 -0
  91. package/src/model/gemma4-base.ts +162 -80
  92. package/src/model/gemma4.ts +34 -26
  93. package/src/model/generated/gemma4-12b.ts +4 -1
  94. package/src/model/generated/gemma4-26b.ts +4 -1
  95. package/src/model/generated/gemma4-e4b.ts +4 -1
  96. package/src/model/glm52-cache.ts +80 -4
  97. package/src/model/glm52-mla.ts +12 -6
  98. package/src/model/glm52.ts +19 -40
  99. package/src/model/kv-attention-view.ts +67 -0
  100. package/src/model/minicpm5.ts +11 -7
  101. package/src/model/qwen3-delta-state.ts +74 -0
  102. package/src/model/qwen3-delta.ts +59 -36
  103. package/src/model/qwen3_5.ts +82 -42
  104. package/src/model/rotating-kv-donor.ts +35 -0
  105. package/src/model/rotating-kv-layout.ts +262 -0
  106. package/src/model/rotating-row-transaction.ts +35 -0
  107. package/src/model/speculative-rotating-kv.ts +101 -0
  108. package/src/model/ssm-prefill-padding.ts +39 -0
  109. package/src/model/trellis-linear.ts +2 -2
  110. package/src/model/turboquant-codec.ts +83 -0
  111. package/src/model/universal/dense.ts +14 -13
  112. package/src/preflight.ts +31 -15
  113. package/src/prompt-cache.ts +42 -44
  114. package/src/sampler.ts +36 -25
  115. package/src/serve/chat-stage.ts +2 -2
  116. package/src/serve/generation-gateway.ts +39 -30
  117. package/src/serve/model-host.ts +3 -4
  118. package/src/serve/request-plan.ts +4 -3
  119. package/src/serve/request-prep.ts +2 -4
  120. package/src/serve/text-completion-stage.ts +2 -2
  121. package/src/server.ts +64 -108
  122. package/src/spec/assistant-rows.ts +92 -0
  123. package/src/spec/assistant-source.ts +28 -18
  124. package/src/spec/cache-identity.ts +22 -0
  125. package/src/spec/deepspec-rows.ts +29 -0
  126. package/src/spec/deepspec-source.ts +12 -10
  127. package/src/spec/dflash-source.ts +28 -4
  128. package/src/spec/drafter.ts +153 -142
  129. package/src/spec/dspark/deepspec-module.ts +95 -120
  130. package/src/spec/dspark/module-dflash.ts +126 -170
  131. package/src/spec/glm52-mtp-graph.ts +29 -0
  132. package/src/spec/glm52-mtp-rows.ts +132 -0
  133. package/src/spec/glm52-mtp-source.ts +53 -136
  134. package/src/spec/glm52-mtp-state.ts +33 -0
  135. package/src/spec/ngram-source.ts +92 -20
  136. package/src/spec/projected-draft-rows.ts +73 -0
  137. package/src/spec/qwen-mtp-module.ts +171 -0
  138. package/src/spec/qwen-mtp-rows.ts +219 -0
  139. package/src/spec/qwen-mtp-source.ts +81 -226
  140. package/src/spec/qwen-mtp-state.ts +30 -0
  141. package/src/spec/serve-loop.ts +70 -68
  142. package/src/spec/source.ts +80 -15
  143. package/src/spec/two-model-rows.ts +61 -0
  144. package/src/spec/two-model.ts +43 -101
  145. package/src/ssd-cache.ts +33 -21
  146. package/src/ssd-durability.ts +11 -7
  147. package/src/web/app.js +57 -57
  148. package/src/backends/mlx/memory-guard.ts +0 -40
  149. package/src/spec/prefix-state.ts +0 -45
  150. /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
@@ -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(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
  }
@@ -28,8 +28,32 @@ import { Weights } from "../weights";
28
28
  import { MlxArray } from "../mlx/array";
29
29
  import { Dtype } from "../mlx/ffi";
30
30
  import * as ops from "../mlx/ops";
31
+ import { disposeResources } from "../engine/resources";
31
32
  import type { SharedKv } from "../model/gemma4";
32
33
 
34
+ /** The graph borrows donor attention; storage owns validity and encoding. */
35
+ export interface AssistantAttention {
36
+ attend(query: MlxArray, scale: number, window: number | null): MlxArray;
37
+ }
38
+ export interface AssistantDonors { sliding: AssistantAttention; full: AssistantAttention; }
39
+
40
+ /** Legacy plain donor adapter. The common graph never reads cache geometry. */
41
+ export function plainAssistantDonors(shared: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] },
42
+ position: number): AssistantDonors {
43
+ const bind = ([keys, values]: [MlxArray, MlxArray]): AssistantAttention => ({
44
+ attend(query, scale, window) {
45
+ const length = keys.shape[2]!;
46
+ const from = window === null ? 0 : Math.max(0, Math.max(0, position - window + 1) - (position - length + 1));
47
+ using floats = from ? MlxArray.fromFloat32(Float32Array.from({ length }, (_, i) => i < from ? -1e9 : 0), [1,1,1,length]) : null;
48
+ using mask = floats?.astype(keys.dtype);
49
+ return ops.sdpa(query, keys, values, scale, mask ? "array" : "", mask ?? null);
50
+ },
51
+ });
52
+ return { sliding: bind(shared.sliding), full: bind(shared.full) };
53
+ }
54
+
55
+ export interface DrafterRowsStep { tokens: MlxArray; nextHidden: MlxArray; }
56
+
33
57
  export interface DrafterStep {
34
58
  /** Drafted token id. */
35
59
  token: number;
@@ -64,6 +88,7 @@ export class GemmaAssistantDrafter {
64
88
  readonly topK: number;
65
89
  readonly vocabPerCentroid: number;
66
90
  #w: Weights;
91
+ #owned: MlxArray[] = [];
67
92
  #blocks: Block[] = [];
68
93
  readonly useCentroids: boolean;
69
94
  #prePT: MlxArray; // pre_projection transposed [in, out]
@@ -84,73 +109,80 @@ export class GemmaAssistantDrafter {
84
109
  this.vocabPerCentroid = Math.floor(t.vocab_size / this.numCentroids);
85
110
  this.#w = w;
86
111
 
87
- const T = (name: string) => w.tensor(name);
88
- const transposed = (name: string): MlxArray => {
89
- const a = ops.transposeAxes(T(name), [1, 0]);
90
- const c = ops.contiguous(a);
91
- a.dispose();
92
- c.eval();
93
- return c;
94
- };
95
- this.#prePT = transposed("pre_projection.weight");
96
- this.#postPT = transposed("post_projection.weight");
97
- this.#embed = T("model.embed_tokens.weight");
98
- this.#norm = T("model.norm.weight");
99
- // Head variant: only the small drafters ship the centroid tensors.
100
- // The 12B/26B artifacts ship a tied embed_tokens head instead (their
101
- // config still declares num_centroids — ignore it, trust the tensors).
102
- this.useCentroids = w.has("masked_embedding.centroids.weight");
103
- if (this.useCentroids) {
104
- this.#centroidsT = transposed("masked_embedding.centroids.weight");
105
- const ord64 = T("masked_embedding.token_ordering");
106
- const ord32 = ord64.astype(Dtype.int32);
107
- this.#ordering = ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]);
108
- this.#ordering.eval();
109
- ord32.dispose();
110
- }
112
+ const own = (array: MlxArray) => { this.#owned.push(array); return array; };
113
+ try {
114
+ const T = (name: string) => w.tensor(name);
115
+ // Keep the oracle's strided weight.T: a contiguous transpose changes
116
+ // M4 matmul rounding and allocates a second copy of every linear weight.
117
+ const transposed = (name: string): MlxArray => {
118
+ const c = own(ops.transposeAxes(T(name), [1, 0]));
119
+ c.eval();
120
+ return c;
121
+ };
122
+ this.#prePT = transposed("pre_projection.weight");
123
+ this.#postPT = transposed("post_projection.weight");
124
+ this.#embed = T("model.embed_tokens.weight");
125
+ this.#norm = T("model.norm.weight");
126
+ // Head variant: only the small drafters ship the centroid tensors.
127
+ // The 12B/26B artifacts ship a tied embed_tokens head instead (their
128
+ // config still declares num_centroids — ignore it, trust the tensors).
129
+ this.useCentroids = w.has("masked_embedding.centroids.weight");
130
+ if (this.useCentroids) {
131
+ this.#centroidsT = transposed("masked_embedding.centroids.weight");
132
+ const ord64 = T("masked_embedding.token_ordering");
133
+ const ord32 = ord64.astype(Dtype.int32);
134
+ this.#ordering = own(ops.reshape(ord32, [this.numCentroids, this.vocabPerCentroid]));
135
+ this.#ordering.eval();
136
+ ord32.dispose();
137
+ }
111
138
 
112
- const ropeP = t.rope_parameters ?? {};
113
- for (let i = 0; i < t.num_hidden_layers; i++) {
114
- const layerType = t.layer_types[i];
115
- const isFull = layerType === "full_attention";
116
- const headDim = isFull ? t.global_head_dim : t.head_dim;
117
- const rp = ropeP[layerType] ?? {};
118
- let ropeBase: number | null = rp.rope_theta ?? (isFull ? 1e6 : 1e4);
119
- let ropeFreqs: MlxArray | null = null;
120
- const partial = rp.partial_rotary_factor ?? 1.0;
121
- if (isFull && partial < 1.0) {
122
- // ProportionalRoPE freqs, matching the target's donor-K rope
123
- const rotated = Math.floor(headDim * partial);
124
- const n = headDim / 2;
125
- const freqs = new Float32Array(n).fill(Infinity);
126
- for (let k = 0; k < rotated / 2; k++)
127
- freqs[k] = Math.pow(ropeBase!, (2 * k) / headDim);
128
- ropeFreqs = MlxArray.fromFloat32(freqs, [n]);
129
- ropeBase = null;
139
+ const ropeP = t.rope_parameters ?? {};
140
+ for (let i = 0; i < t.num_hidden_layers; i++) {
141
+ const layerType = t.layer_types[i];
142
+ const isFull = layerType === "full_attention";
143
+ const headDim = isFull ? t.global_head_dim : t.head_dim;
144
+ const rp = ropeP[layerType] ?? {};
145
+ let ropeBase: number | null = rp.rope_theta ?? (isFull ? 1e6 : 1e4);
146
+ let ropeFreqs: MlxArray | null = null;
147
+ const partial = rp.partial_rotary_factor ?? 1.0;
148
+ if (isFull && partial < 1.0) {
149
+ // ProportionalRoPE freqs, matching the target's donor-K rope
150
+ const rotated = Math.floor(headDim * partial);
151
+ const n = headDim / 2;
152
+ const freqs = new Float32Array(n).fill(Infinity);
153
+ for (let k = 0; k < rotated / 2; k++)
154
+ freqs[k] = Math.pow(ropeBase!, (2 * k) / headDim);
155
+ ropeFreqs = own(MlxArray.fromFloat32(freqs, [n]));
156
+ ropeBase = null;
157
+ }
158
+ const p = `model.layers.${i}`;
159
+ this.#blocks.push({
160
+ layerType, headDim, ropeBase, ropeFreqs,
161
+ qProj: transposed(`${p}.self_attn.q_proj.weight`),
162
+ oProj: transposed(`${p}.self_attn.o_proj.weight`),
163
+ qNorm: T(`${p}.self_attn.q_norm.weight`),
164
+ inputNorm: T(`${p}.input_layernorm.weight`),
165
+ postAttnNorm: T(`${p}.post_attention_layernorm.weight`),
166
+ preFfNorm: T(`${p}.pre_feedforward_layernorm.weight`),
167
+ postFfNorm: T(`${p}.post_feedforward_layernorm.weight`),
168
+ gateProj: transposed(`${p}.mlp.gate_proj.weight`),
169
+ upProj: transposed(`${p}.mlp.up_proj.weight`),
170
+ downProj: transposed(`${p}.mlp.down_proj.weight`),
171
+ layerScalar: T(`${p}.layer_scalar`),
172
+ });
130
173
  }
131
- const p = `model.layers.${i}`;
132
- this.#blocks.push({
133
- layerType, headDim, ropeBase, ropeFreqs,
134
- qProj: transposed(`${p}.self_attn.q_proj.weight`),
135
- oProj: transposed(`${p}.self_attn.o_proj.weight`),
136
- qNorm: T(`${p}.self_attn.q_norm.weight`),
137
- inputNorm: T(`${p}.input_layernorm.weight`),
138
- postAttnNorm: T(`${p}.post_attention_layernorm.weight`),
139
- preFfNorm: T(`${p}.pre_feedforward_layernorm.weight`),
140
- postFfNorm: T(`${p}.post_feedforward_layernorm.weight`),
141
- gateProj: transposed(`${p}.mlp.gate_proj.weight`),
142
- upProj: transposed(`${p}.mlp.up_proj.weight`),
143
- downProj: transposed(`${p}.mlp.down_proj.weight`),
144
- layerScalar: T(`${p}.layer_scalar`),
145
- });
146
- }
174
+ } catch (error) { disposeResources(this.#owned.splice(0)); throw error; }
147
175
  }
148
176
 
149
177
  static async load(modelDir: string): Promise<GemmaAssistantDrafter> {
150
178
  const cfg = (await Bun.file(`${modelDir}/config.json`).json()) as Record<string, any>;
151
- return new GemmaAssistantDrafter(await Weights.open(modelDir), cfg);
179
+ const weights = await Weights.open(modelDir);
180
+ try { return new GemmaAssistantDrafter(weights, cfg); }
181
+ catch (error) { weights.dispose(); throw error; }
152
182
  }
153
183
 
184
+ dispose(): void { disposeResources([...this.#owned.splice(0), this.#w]); }
185
+
154
186
  /** f32-internal RMSNorm (reference _RMSNorm casts through float32). */
155
187
  #rms(x: MlxArray, weight: MlxArray): MlxArray {
156
188
  const f = x.astype(Dtype.float32);
@@ -161,49 +193,77 @@ export class GemmaAssistantDrafter {
161
193
  return back;
162
194
  }
163
195
 
164
- /** One draft step. All inputs (1,1,·); sharedKv holds the target's
165
- * donor K/V in chronological order. position = target's last position. */
166
- forward(
167
- lastTokenEmb: MlxArray, targetHidden: MlxArray,
168
- sharedKv: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] },
169
- position: number,
170
- ): DrafterStep {
171
- let h = ops.concatAxis([lastTokenEmb, targetHidden], 2); // (1,1,2*backbone)
172
- h = disposing(h, ops.matmul(h, this.#prePT)); // (1,1,256)
196
+ /** The request adapter consumes the same graph as a batch of one.
197
+ * Donors are chronological; position is the target's last position. */
198
+ forward(lastTokenEmb: MlxArray, targetHidden: MlxArray,
199
+ sharedKv: { sliding: [MlxArray, MlxArray]; full: [MlxArray, MlxArray] }, position: number): DrafterStep {
200
+ const result = this.forwardRows(lastTokenEmb, targetHidden, plainAssistantDonors(sharedKv, position), position);
201
+ try { return { token: ops.itemUint32(result.tokens), nextHidden: result.nextHidden }; }
202
+ catch (error) { result.nextHidden.dispose(); throw error; }
203
+ finally { result.tokens.dispose(); }
204
+ }
173
205
 
174
- for (const blk of this.#blocks) {
175
- const [k, v] = blk.layerType === "sliding_attention" ? sharedKv.sliding : sharedKv.full;
176
- const kLen = k.shape[2]!;
206
+ #sampleRows(h: MlxArray, B: number): MlxArray {
207
+ if (this.useCentroids) {
208
+ using scores = ops.matmul(h, this.#centroidsT!);
209
+ using flat = ops.reshape(scores, [B, this.numCentroids]);
210
+ using neg = ops.neg(flat);
211
+ using partition = ops.argpartitionAxis(neg, this.topK - 1, -1);
212
+ using top = partition.slice([0, 0], [B, this.topK]);
213
+ using topFlat = ops.reshape(top, [B * this.topK]);
214
+ using candidates = ops.takeAxis(this.#ordering!, topFlat, 0);
215
+ const count = this.topK * this.vocabPerCentroid;
216
+ using ids = ops.reshape(candidates, [B, count]);
217
+ using flatIds = ops.reshape(ids, [B * count]);
218
+ using embeddings = ops.takeAxis(this.#embed, flatIds, 0);
219
+ using matrix = ops.reshape(embeddings, [B, count, this.hidden]);
220
+ using vector = ops.reshape(h, [B, 1, this.hidden]);
221
+ // The oracle rounds each bf16 product before reduction. Matmul's
222
+ // accumulation changes candidate rankings on M1, even at identical h.
223
+ using products = ops.mul(matrix, vector);
224
+ using candidateScores = ops.sumAxis(products, -1, false);
225
+ using maximum = ops.maxAxis(candidateScores, -1, true);
226
+ using winners = ops.equal(candidateScores, maximum);
227
+ // Sparse full-vocabulary argmax chooses the lowest token ID on a tie.
228
+ // Apply that rule directly to the candidates without a vocab-size scatter.
229
+ using sentinel = ops.fromInt32([this.#embed.shape[0]!], []);
230
+ using eligible = ops.where(winners, ids, sentinel);
231
+ using selected = ops.minAxis(eligible, -1, false);
232
+ return selected.astype(Dtype.uint32);
233
+ }
234
+ using rows = ops.reshape(h, [B, this.hidden]);
235
+ using vector = B === 1 ? ops.reshape(h, [this.hidden, 1]) : ops.transposeAxes(rows, [1, 0]);
236
+ using scores = ops.matmul(this.#embed, vector);
237
+ using flat = B === 1 ? ops.reshape(scores, [1, this.#embed.shape[0]!]) : ops.transposeAxes(scores, [1, 0]);
238
+ return ops.argmaxAxis(flat, -1);
239
+ }
177
240
 
178
- // sliding-window additive mask over cached K positions
179
- let mask: MlxArray | null = null;
180
- if (blk.layerType === "sliding_attention") {
181
- const firstCached = position - kLen + 1;
182
- const windowStart = Math.max(0, position - this.slidingWindow + 1);
183
- const allowedFrom = Math.max(0, windowStart - firstCached);
184
- if (allowedFrom > 0) {
185
- const m = new Float32Array(kLen);
186
- for (let i = 0; i < allowedFrom; i++) m[i] = -1e9;
187
- const mf = MlxArray.fromFloat32(m, [1, 1, 1, kLen]);
188
- mask = mf.astype(k.dtype);
189
- mf.dispose();
190
- }
191
- }
241
+ /** Numerical graph only. Callers supply donor validity masks for different
242
+ * row positions; token selection remains on device across the draft chain. */
243
+ forwardRows(lastTokenEmb: MlxArray, targetHidden: MlxArray,
244
+ donors: AssistantDonors, position: number | MlxArray): DrafterRowsStep {
245
+ const B = lastTokenEmb.shape[0]!;
246
+ let h = ops.concatAxis([lastTokenEmb, targetHidden], 2); // (B,1,2*backbone)
247
+ h = disposing(h, ops.matmul(h, this.#prePT)); // (B,1,256)
192
248
 
249
+ for (const blk of this.#blocks) {
250
+ const donor = blk.layerType === "sliding_attention" ? donors.sliding : donors.full;
193
251
  const residual = h;
194
252
  let x = this.#rms(h, blk.inputNorm);
195
253
  // Q-only attention
196
254
  let q = ops.matmul(x, blk.qProj);
197
255
  x.dispose();
198
- q = disposing(q, ops.reshape(q, [1, 1, this.nHeads, blk.headDim]));
256
+ q = disposing(q, ops.reshape(q, [B, 1, this.nHeads, blk.headDim]));
199
257
  q = disposing(q, this.#rms(q, blk.qNorm));
200
258
  q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
201
- q = disposing(q, ops.rope(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs));
202
- let attn = ops.sdpa(q, k, v, Math.pow(blk.headDim, -0.5), mask ? "array" : "", mask);
259
+ q = disposing(q, typeof position === "number"
260
+ ? ops.rope(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs)
261
+ : ops.ropeDynamic(q, blk.headDim, blk.ropeBase, position, blk.ropeFreqs));
262
+ let attn = donor.attend(q, Math.pow(blk.headDim, -0.5),
263
+ blk.layerType === "sliding_attention" ? this.slidingWindow : null);
203
264
  q.dispose();
204
- mask?.dispose();
205
265
  attn = disposing(attn, ops.transposeAxes(attn, [0, 2, 1, 3]));
206
- attn = disposing(attn, ops.reshape(attn, [1, 1, this.nHeads * blk.headDim]));
266
+ attn = disposing(attn, ops.reshape(attn, [B, 1, this.nHeads * blk.headDim]));
207
267
  attn = disposing(attn, ops.matmul(attn, blk.oProj));
208
268
  attn = disposing(attn, this.#rms(attn, blk.postAttnNorm));
209
269
  h = ops.add(residual, attn);
@@ -231,59 +291,10 @@ export class GemmaAssistantDrafter {
231
291
 
232
292
  h = disposing(h, this.#rms(h, this.#norm));
233
293
 
234
- let token: number;
235
- if (this.useCentroids) {
236
- // centroid decode → drafted token (argmax-equivalent shortcut)
237
- const cScores = ops.matmul(h, this.#centroidsT!); // (1,1,2048)
238
- const flat = ops.reshape(cScores, [1, this.numCentroids]);
239
- cScores.dispose();
240
- const neg = ops.neg(flat);
241
- flat.dispose();
242
- const part = ops.argpartitionAxis(neg, this.topK - 1, -1);
243
- neg.dispose();
244
- const topIdx = part.slice([0, 0], [1, this.topK]); // (1, topK)
245
- part.dispose();
246
- const topFlat = ops.reshape(topIdx, [this.topK]);
247
- topIdx.dispose();
248
- const candIds = ops.takeAxis(this.#ordering!, topFlat, 0); // (topK, vpc) int32
249
- topFlat.dispose();
250
- const candFlat = ops.reshape(candIds, [this.topK * this.vocabPerCentroid]);
251
- candIds.dispose();
252
- const candEmb = ops.takeAxis(this.#embed, candFlat, 0); // (topK*vpc, hidden)
253
- const hVec = ops.reshape(h, [this.hidden, 1]);
254
- const scores = ops.matmul(candEmb, hVec); // (topK*vpc, 1)
255
- candEmb.dispose();
256
- hVec.dispose();
257
- const scoresFlat = ops.reshape(scores, [1, this.topK * this.vocabPerCentroid]);
258
- scores.dispose();
259
- const am = ops.argmaxAxis(scoresFlat, -1);
260
- scoresFlat.dispose();
261
- const winner = ops.itemUint32(am);
262
- am.dispose();
263
- const tokArr = candFlat.slice([winner], [winner + 1]);
264
- candFlat.dispose();
265
- const tokU32 = tokArr.astype(Dtype.uint32);
266
- tokArr.dispose();
267
- token = ops.itemUint32(tokU32);
268
- tokU32.dispose();
269
- } else {
270
- // tied-embedding head: token = argmax_v (embed[v] · h) over the
271
- // full vocab — the dense form the centroid head approximates.
272
- const V = this.#embed.shape[0]!;
273
- const hVec = ops.reshape(h, [this.hidden, 1]);
274
- const scores = ops.matmul(this.#embed, hVec); // (vocab, 1)
275
- hVec.dispose();
276
- const scoresFlat = ops.reshape(scores, [1, V]);
277
- scores.dispose();
278
- const am = ops.argmaxAxis(scoresFlat, -1);
279
- scoresFlat.dispose();
280
- token = ops.itemUint32(am);
281
- am.dispose();
282
- }
283
-
284
- const nextHidden = ops.matmul(h, this.#postPT); // (1,1,backbone)
294
+ const tokens = this.#sampleRows(h, B);
295
+ const nextHidden = ops.matmul(h, this.#postPT); // (B,1,backbone)
285
296
  h.dispose();
286
- return { token, nextHidden };
297
+ return { tokens, nextHidden };
287
298
  }
288
299
  }
289
300