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.
- package/docs/README.md +4 -1
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/design/batching.md +1321 -49
- package/docs/design/decode-speed-program.md +331 -1163
- package/docs/design/generic-model-support.md +79 -3
- package/docs/design/kv-cache.md +198 -54
- package/docs/design/speculative-decoding.md +387 -24
- package/docs/design/turboquant.md +123 -13
- package/docs/design/unified-engine-frontier-plan.md +144 -142
- package/docs/planning/PRODUCT_ROADMAP.md +16 -0
- package/docs/planning/release-notes-v0.4.0.md +45 -0
- package/docs/reference/benchmarks.md +2457 -6
- package/docs/reference/cli.md +41 -2
- package/docs/reference/distribution.md +16 -9
- package/docs/reference/environment.md +55 -5
- package/docs/reference/models.md +13 -4
- package/docs/reference/server-api.md +79 -25
- package/docs/reference/server-config.md +178 -84
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +1 -1
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/batch-group.ts +368 -224
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +106 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- package/src/backends/mlx/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +77 -18
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +133 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/model-serving.ts +1 -1
- package/src/backends/mlx/prefill-cohort.ts +89 -0
- package/src/backends/mlx/prefill-rows.ts +200 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/request-state-policy.ts +41 -0
- package/src/backends/mlx/rollback.ts +41 -3
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +35 -66
- package/src/backends/mlx/speculative-group.ts +359 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +165 -0
- package/src/cli.ts +11 -36
- package/src/contracts/execution.ts +11 -0
- package/src/contracts/prefix-cache.ts +23 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +18 -0
- package/src/engine/execution-plan.ts +18 -16
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +10 -5
- package/src/fit.ts +29 -15
- package/src/generate.ts +77 -173
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +70 -12
- package/src/kv-store.ts +70 -16
- package/src/lab/paged-kv/paged-kv-rows.ts +83 -0
- package/src/lab/paged-kv/paged-kv.ts +25 -11
- package/src/mlx/array.ts +2 -1
- package/src/model/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +93 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +55 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +162 -80
- package/src/model/gemma4.ts +34 -26
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +67 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3_5.ts +82 -42
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +83 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +42 -44
- package/src/sampler.ts +36 -25
- package/src/serve/chat-stage.ts +2 -2
- package/src/serve/generation-gateway.ts +39 -30
- package/src/serve/model-host.ts +3 -4
- package/src/serve/request-plan.ts +4 -3
- package/src/serve/request-prep.ts +2 -4
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/server.ts +64 -108
- package/src/spec/assistant-rows.ts +92 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +53 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +92 -20
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +219 -0
- package/src/spec/qwen-mtp-source.ts +81 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +70 -68
- package/src/spec/source.ts +80 -15
- package/src/spec/two-model-rows.ts +61 -0
- package/src/spec/two-model.ts +43 -101
- package/src/ssd-cache.ts +33 -21
- package/src/ssd-durability.ts +11 -7
- package/src/web/app.js +57 -57
- package/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import * as ops from "../mlx/ops";
|
|
3
|
+
import { materializeCopy } from "../mlx/materialize";
|
|
4
|
+
import { toLogprobs } from "../sampler";
|
|
5
|
+
import { BatchedKVCache } from "../model/batched-kv";
|
|
6
|
+
import { KVCache } from "../model/gemma4-base";
|
|
7
|
+
import type { DraftRowSampling, QwenMtpTarget } from "./source";
|
|
8
|
+
import type { MtpModule } from "./qwen-mtp-module";
|
|
9
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
10
|
+
import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
|
|
11
|
+
|
|
12
|
+
/** A committed request's draft KV and the preceding true target hidden row.
|
|
13
|
+
* Construction borrows these arrays; extraction returns owned state. */
|
|
14
|
+
export interface MtpRowState {
|
|
15
|
+
readonly cache: KVCache;
|
|
16
|
+
readonly hidden: MlxArray;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Sampling owns each request's RNG policy. Scores are [B,V] log-probabilities;
|
|
20
|
+
* the returned owned [B] token array stays on device through the draft chain. */
|
|
21
|
+
export type MtpRowSampling = DraftRowSampling;
|
|
22
|
+
|
|
23
|
+
/** The same draft graph and state lifecycle for one or several requests.
|
|
24
|
+
* Admission, target verification, output publication and persistence belong to
|
|
25
|
+
* the caller. Rows remain fixed within a draft/commit round. */
|
|
26
|
+
export class QwenMtpRows {
|
|
27
|
+
#cache = new BatchedKVCache();
|
|
28
|
+
#hidden: MlxArray | null = null;
|
|
29
|
+
#prefilled: boolean[] = [];
|
|
30
|
+
#drafts: number[][] = [];
|
|
31
|
+
#depth = 0;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
readonly target: QwenMtpTarget,
|
|
35
|
+
readonly module: MtpModule,
|
|
36
|
+
readonly sampling: MtpRowSampling | null,
|
|
37
|
+
states: readonly (MtpRowState | null)[],
|
|
38
|
+
) {
|
|
39
|
+
this.append(states);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get rowCount(): number { return this.#cache.rowOffsets.length; }
|
|
43
|
+
|
|
44
|
+
/** Add committed state at a method boundary. Inputs remain caller-owned. */
|
|
45
|
+
append(states: readonly (MtpRowState | null)[]): void {
|
|
46
|
+
if (!states.length) return;
|
|
47
|
+
applyStateChanges([() => this.prepareAppend(states)]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
prepareAppend(states: readonly (MtpRowState | null)[]): PreparedStateChange {
|
|
51
|
+
if (!states.length) return { commit() {}, dispose() {} };
|
|
52
|
+
const prefilled = [...this.#prefilled, ...states.map(state => state !== null)];
|
|
53
|
+
const example = this.#hidden ?? states.find(state => state !== null)?.hidden;
|
|
54
|
+
let hidden: MlxArray | null = null;
|
|
55
|
+
let cache: BatchedKVCache | null = new BatchedKVCache();
|
|
56
|
+
const held: { dispose(): void }[] = [];
|
|
57
|
+
try {
|
|
58
|
+
if (example) {
|
|
59
|
+
const zero = (rows: number) => {
|
|
60
|
+
const array = ops.zeros([rows, 1, this.target.hiddenSize], example.dtype);
|
|
61
|
+
held.push(array); return array;
|
|
62
|
+
};
|
|
63
|
+
hidden = ops.concatAxis([
|
|
64
|
+
...(this.rowCount ? [this.#hidden ?? zero(this.rowCount)] : []),
|
|
65
|
+
...states.map(state => state?.hidden ?? zero(1)),
|
|
66
|
+
], 0);
|
|
67
|
+
}
|
|
68
|
+
cache.mergeRows([...(this.rowCount ? [this.#cache] : []), ...states.map(state => {
|
|
69
|
+
if (state) return state.cache;
|
|
70
|
+
const empty = new KVCache(); held.push(empty); return empty;
|
|
71
|
+
})]);
|
|
72
|
+
} catch (error) { return cleanupFailure(error, () => disposeResources([cache!, ...(hidden ? [hidden] : [])])); }
|
|
73
|
+
finally { disposeResources(held); }
|
|
74
|
+
return {
|
|
75
|
+
commit: () => {
|
|
76
|
+
const previousCache = this.#cache, previousHidden = this.#hidden;
|
|
77
|
+
this.#cache = cache!; this.#hidden = hidden;
|
|
78
|
+
this.#prefilled = prefilled;
|
|
79
|
+
cache = previousCache; hidden = previousHidden;
|
|
80
|
+
},
|
|
81
|
+
dispose: () => {
|
|
82
|
+
const held = [cache, hidden].filter(value => value !== null);
|
|
83
|
+
cache = null; hidden = null;
|
|
84
|
+
disposeResources(held);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Retire/reorder committed requests without involving their sampler policy. */
|
|
90
|
+
filterRows(keep: readonly number[]): void {
|
|
91
|
+
if (!keep.length) { this.dispose(); return; }
|
|
92
|
+
using indices = ops.fromInt32([...keep], [keep.length]);
|
|
93
|
+
const hidden = this.#hidden ? ops.takeAxis(this.#hidden, indices, 0) : null;
|
|
94
|
+
try { this.#cache.filterRows(keep); }
|
|
95
|
+
catch (error) { hidden?.dispose(); throw error; }
|
|
96
|
+
this.#hidden?.dispose(); this.#hidden = hidden;
|
|
97
|
+
this.#prefilled = keep.map(row => this.#prefilled[row]!);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Build companion KV from borrowed new target tokens/taps at the same B.
|
|
101
|
+
* The first token bridges to each restored row's preceding true hidden.
|
|
102
|
+
* Cold rows discard that bridge. All later rows pair token[p+1] with h[p].
|
|
103
|
+
* Only KV and the final true hidden are evaluated; unused attention/MLP
|
|
104
|
+
* outputs remain outside the evaluated prefill graph. */
|
|
105
|
+
prefill(tokens: MlxArray, context: MlxArray): void {
|
|
106
|
+
const [B, length] = tokens.shape as [number, number];
|
|
107
|
+
const H = context.shape[2]!;
|
|
108
|
+
if (this.#prefilled.some(Boolean)) {
|
|
109
|
+
using first = tokens.slice([0, 0], [B, 1]);
|
|
110
|
+
using embeds = this.target.embed(first);
|
|
111
|
+
const mixed = this.#prefilled.some(value => !value);
|
|
112
|
+
if (mixed) this.#cache.specRoundBegin();
|
|
113
|
+
this.module.forward(embeds, this.#hidden!, this.#cache).dispose();
|
|
114
|
+
if (mixed) this.#cache.specRoundRollback(this.#prefilled.map(Number));
|
|
115
|
+
}
|
|
116
|
+
for (let pos = 0; pos + 1 < length; pos += 2048) {
|
|
117
|
+
const end = Math.min(pos + 2048, length - 1);
|
|
118
|
+
using ids = tokens.slice([0, pos + 1], [B, end + 1]);
|
|
119
|
+
using embeds = this.target.embed(ids);
|
|
120
|
+
using hidden = context.slice([0, pos, 0], [B, end, H]);
|
|
121
|
+
this.module.forward(embeds, hidden, this.#cache).dispose();
|
|
122
|
+
ops.evalAll(this.#cache.state());
|
|
123
|
+
}
|
|
124
|
+
using tail = context.slice([0, length - 1, 0], [B, length, H]);
|
|
125
|
+
const hidden = materializeCopy(tail);
|
|
126
|
+
this.#hidden?.dispose(); this.#hidden = hidden;
|
|
127
|
+
ops.evalAll([hidden, ...this.#cache.state()]);
|
|
128
|
+
this.#prefilled.fill(true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
materialize(): void {
|
|
132
|
+
const state = [...this.#cache.state(), ...(this.#hidden ? [this.#hidden] : [])];
|
|
133
|
+
if (state.length) ops.evalAll(state);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Nonnegative depth, with every request's first pending token already known.
|
|
137
|
+
* Build the complete device dependency chain before reading proposal IDs. */
|
|
138
|
+
draft(pending: readonly number[], depth: number, steps: readonly number[]): number[][] {
|
|
139
|
+
const B = pending.length;
|
|
140
|
+
const tokens: MlxArray[] = [];
|
|
141
|
+
let ids: MlxArray | null = ops.fromInt32([...pending], [B, 1]);
|
|
142
|
+
let chained: MlxArray | null = null;
|
|
143
|
+
this.#cache.specRoundBegin();
|
|
144
|
+
try {
|
|
145
|
+
// A zero-proposal round still processes the pending target token. Keep
|
|
146
|
+
// its companion KV aligned so a later round can resume speculation.
|
|
147
|
+
if (depth === 0) {
|
|
148
|
+
using embeds = this.target.embed(ids!);
|
|
149
|
+
this.module.forward(embeds, this.#hidden!, this.#cache).dispose();
|
|
150
|
+
this.#drafts = Array.from({ length: B }, () => []);
|
|
151
|
+
this.#depth = 0;
|
|
152
|
+
this.#hidden!.dispose(); this.#hidden = null;
|
|
153
|
+
return this.#drafts;
|
|
154
|
+
}
|
|
155
|
+
for (let position = 0; position < depth; position++) {
|
|
156
|
+
using embeds = this.target.embed(ids!);
|
|
157
|
+
const output = this.module.forward(embeds, chained ?? this.#hidden!, this.#cache);
|
|
158
|
+
chained?.dispose(); chained = output;
|
|
159
|
+
using logits = this.target.logitsFromHidden(output);
|
|
160
|
+
using flat = ops.reshape(logits, [B, logits.shape.at(-1)!]);
|
|
161
|
+
using logprobs = toLogprobs(flat);
|
|
162
|
+
const token = this.sampling!.sample(logprobs, steps.map(step => step + position));
|
|
163
|
+
tokens.push(token);
|
|
164
|
+
ids!.dispose(); ids = ops.reshape(token, [B, 1]);
|
|
165
|
+
}
|
|
166
|
+
using packed = ops.concatAxis(tokens, 0);
|
|
167
|
+
const read = packed.toIntTokens();
|
|
168
|
+
this.#drafts = Array.from({ length: B }, (_, row) =>
|
|
169
|
+
Array.from({ length: depth }, (_, position) => read[position * B + row]!));
|
|
170
|
+
this.#depth = depth;
|
|
171
|
+
this.#hidden!.dispose(); this.#hidden = null;
|
|
172
|
+
return this.#drafts;
|
|
173
|
+
} finally {
|
|
174
|
+
ids?.dispose(); chained?.dispose();
|
|
175
|
+
for (const token of tokens) token.dispose();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Resolve draft storage using each request's accepted prefix. The verify
|
|
180
|
+
* context is borrowed [B,depth+1,H], before the target's final norm. */
|
|
181
|
+
commit(accepted: readonly number[], context: MlxArray): void {
|
|
182
|
+
const B = accepted.length, depth = this.#depth;
|
|
183
|
+
const appended = Math.max(1, depth);
|
|
184
|
+
const kept = accepted.map(count => Math.min(count + 1, appended));
|
|
185
|
+
if (kept.every(count => count === appended)) this.#cache.specRoundCommit();
|
|
186
|
+
else this.#cache.specRoundRollback(kept);
|
|
187
|
+
|
|
188
|
+
// Full acceptance needs the last accepted token's missing draft row,
|
|
189
|
+
// using the TRUE target hidden at its preceding position. Other requests
|
|
190
|
+
// keep their current coverage; their temporary append is never retained.
|
|
191
|
+
const extra = accepted.map(count => Number(depth > 0 && count === depth));
|
|
192
|
+
if (extra.some(Boolean)) {
|
|
193
|
+
using ids = ops.fromInt32(this.#drafts.map(row => row[depth - 1]!), [B, 1]);
|
|
194
|
+
using embeds = this.target.embed(ids);
|
|
195
|
+
using hidden = context.slice([0, depth - 1, 0], [B, depth, context.shape[2]!]);
|
|
196
|
+
this.#cache.specRoundBegin();
|
|
197
|
+
this.module.forward(embeds, hidden, this.#cache).dispose();
|
|
198
|
+
this.#cache.specRoundRollback(extra);
|
|
199
|
+
}
|
|
200
|
+
using positions = ops.fromInt32([...accepted], [B, 1, 1]);
|
|
201
|
+
this.#hidden = ops.takeAlongAxis(context, positions, 1);
|
|
202
|
+
this.#drafts = []; this.#depth = 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Only between committed rounds. Returned state is independent of the
|
|
206
|
+
* group's storage and uses the existing single-request cache representation. */
|
|
207
|
+
extractRow(row: number): MtpRowState {
|
|
208
|
+
const cache = this.#cache.extractRow(row);
|
|
209
|
+
try {
|
|
210
|
+
using view = this.#hidden!.slice([row, 0, 0], [row + 1, 1, this.#hidden!.shape[2]!]);
|
|
211
|
+
return { cache, hidden: materializeCopy(view) };
|
|
212
|
+
} catch (error) { cache.dispose(); throw error; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
dispose(): void {
|
|
216
|
+
this.#cache.dispose(); this.#hidden?.dispose(); this.#hidden = null;
|
|
217
|
+
this.#drafts = []; this.#depth = 0; this.#prefilled = [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -30,206 +30,40 @@ import { materializeCopy } from "../mlx/materialize";
|
|
|
30
30
|
import { toLogprobs } from "../sampler";
|
|
31
31
|
import { loadModelConfig, type ModelConfig } from "../config";
|
|
32
32
|
import { Weights } from "../weights";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import type { DraftProvider, DraftSource, QwenMtpTarget } from "./source";
|
|
33
|
+
import { KVCache } from "../model/gemma4-base";
|
|
34
|
+
import { MtpModule } from "./qwen-mtp-module";
|
|
35
|
+
import type { DraftProvider, DraftSource, DraftRowCheckpoint, DraftRowGroup, DraftPrefillGroup, DraftRowSampling, GroupedDraftProvider, QwenMtpTarget, TargetView } from "./source";
|
|
36
|
+
import { QwenMtpRows, type MtpRowState } from "./qwen-mtp-rows";
|
|
37
|
+
import { captureQwenMtpState, restoreQwenMtpState } from "./qwen-mtp-state";
|
|
36
38
|
import type { Cache } from "../model/gemma4";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
|
|
40
|
+
import { artifactIdentity } from "../model/artifact-identity";
|
|
41
|
+
import { configFingerprint } from "../model/fingerprint";
|
|
42
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
41
43
|
|
|
42
44
|
type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
|
|
43
45
|
|
|
44
46
|
const DRAFT_PREFILL_CHUNK = 2048;
|
|
45
47
|
|
|
46
|
-
type MtpLinear = DenseLinear | QuantizedLinear;
|
|
47
|
-
|
|
48
|
-
/** Projection tensors belong to Weights; dense transpose views belong to
|
|
49
|
-
* the provider's resource stack. Quantized heads keep no dense copy. */
|
|
50
|
-
function loadMtpLinear(
|
|
51
|
-
weights: Weights, path: string, config: ModelConfig, resources: DisposableStack,
|
|
52
|
-
): MtpLinear {
|
|
53
|
-
if (weights.has(`${path}.scales`)) return QuantizedLinear.load(weights, path, config);
|
|
54
|
-
const layer = new DenseLinear(weights.tensor(`${path}.weight`), null);
|
|
55
|
-
resources.use(layer.wT);
|
|
56
|
-
return layer;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Qwen3Attention.forward with the companion's dense or quantized projections.
|
|
60
|
-
* Attention operations follow src/model/qwen3_5.ts verbatim. */
|
|
61
|
-
class MtpAttention {
|
|
62
|
-
readonly qProj: MtpLinear;
|
|
63
|
-
readonly kProj: MtpLinear;
|
|
64
|
-
readonly vProj: MtpLinear;
|
|
65
|
-
readonly oProj: MtpLinear;
|
|
66
|
-
readonly qNorm: RMSNorm;
|
|
67
|
-
readonly kNorm: RMSNorm;
|
|
68
|
-
readonly nHeads: number;
|
|
69
|
-
readonly nKvHeads: number;
|
|
70
|
-
readonly headDim: number;
|
|
71
|
-
readonly scale: number;
|
|
72
|
-
readonly ropeDims: number;
|
|
73
|
-
readonly ropeBase: number;
|
|
74
|
-
|
|
75
|
-
constructor(weights: Weights, config: ModelConfig, prefix: string, resources: DisposableStack) {
|
|
76
|
-
const t = config.text;
|
|
77
|
-
this.nHeads = t.numAttentionHeads;
|
|
78
|
-
this.nKvHeads = t.numKeyValueHeads;
|
|
79
|
-
this.headDim = t.headDim;
|
|
80
|
-
this.scale = Math.pow(this.headDim, -0.5);
|
|
81
|
-
this.ropeDims = Math.trunc(this.headDim * t.partialRotaryFactor);
|
|
82
|
-
this.ropeBase = t.ropeParameters.full_attention?.ropeTheta ?? 10000;
|
|
83
|
-
this.qProj = loadMtpLinear(weights, `${prefix}.q_proj`, config, resources);
|
|
84
|
-
this.kProj = loadMtpLinear(weights, `${prefix}.k_proj`, config, resources);
|
|
85
|
-
this.vProj = loadMtpLinear(weights, `${prefix}.v_proj`, config, resources);
|
|
86
|
-
this.oProj = loadMtpLinear(weights, `${prefix}.o_proj`, config, resources);
|
|
87
|
-
this.qNorm = new RMSNorm(weights.tensor(`${prefix}.q_norm.weight`), t.rmsNormEps);
|
|
88
|
-
this.kNorm = new RMSNorm(weights.tensor(`${prefix}.k_norm.weight`), t.rmsNormEps);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
forward(x: MlxArray, mask: Mask, cache: KVCache): MlxArray {
|
|
92
|
-
const [B, L] = x.shape as [number, number, number];
|
|
93
|
-
const qp = this.qProj.forward(x);
|
|
94
|
-
const qpr = disposing(qp, ops.reshape(qp, [B, L, this.nHeads, this.headDim * 2]));
|
|
95
|
-
const [qHeads, gateHeads] = ops.split(qpr, [this.headDim], -1) as [MlxArray, MlxArray];
|
|
96
|
-
qpr.dispose();
|
|
97
|
-
const gate = disposing(gateHeads, ops.reshape(gateHeads, [B, L, this.nHeads * this.headDim]));
|
|
98
|
-
|
|
99
|
-
let k = this.kProj.forward(x);
|
|
100
|
-
let v = this.vProj.forward(x);
|
|
101
|
-
|
|
102
|
-
let q = this.qNorm.forward(qHeads);
|
|
103
|
-
qHeads.dispose();
|
|
104
|
-
q = disposing(q, ops.transposeAxes(q, [0, 2, 1, 3]));
|
|
105
|
-
k = disposing(k, ops.reshape(k, [B, L, this.nKvHeads, this.headDim]));
|
|
106
|
-
k = disposing(k, this.kNorm.forward(k));
|
|
107
|
-
k = disposing(k, ops.transposeAxes(k, [0, 2, 1, 3]));
|
|
108
|
-
v = disposing(v, ops.reshape(v, [B, L, this.nKvHeads, this.headDim]));
|
|
109
|
-
v = disposing(v, ops.transposeAxes(v, [0, 2, 1, 3]));
|
|
110
|
-
|
|
111
|
-
q = disposing(q, ops.rope(q, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
112
|
-
k = disposing(k, ops.rope(k, this.ropeDims, this.ropeBase, cache.offset, null));
|
|
113
|
-
|
|
114
|
-
const [keys, values] = cache.updateAndFetch(k, v);
|
|
115
|
-
k.dispose();
|
|
116
|
-
v.dispose();
|
|
117
|
-
const attn = ops.sdpa(q, keys, values, this.scale, mask.mode, mask.arr);
|
|
118
|
-
keys.dispose();
|
|
119
|
-
values.dispose();
|
|
120
|
-
q.dispose();
|
|
121
|
-
|
|
122
|
-
const attnT = ops.transposeAxes(attn, [0, 2, 1, 3]);
|
|
123
|
-
attn.dispose();
|
|
124
|
-
const merged = ops.reshape(attnT, [B, L, -1]);
|
|
125
|
-
attnT.dispose();
|
|
126
|
-
const sig = ops.sigmoid(gate);
|
|
127
|
-
gate.dispose();
|
|
128
|
-
const gated = ops.mul(merged, sig);
|
|
129
|
-
merged.dispose();
|
|
130
|
-
sig.dispose();
|
|
131
|
-
const out = this.oProj.forward(gated);
|
|
132
|
-
gated.dispose();
|
|
133
|
-
return out;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** The one MTP decoder block: fc-merge → attention → swiglu MLP → norm. */
|
|
138
|
-
class MtpModule {
|
|
139
|
-
readonly fc: MtpLinear;
|
|
140
|
-
readonly preFcNormEmbedding: RMSNorm;
|
|
141
|
-
readonly preFcNormHidden: RMSNorm;
|
|
142
|
-
readonly attn: MtpAttention;
|
|
143
|
-
readonly mlpGate: MtpLinear;
|
|
144
|
-
readonly mlpUp: MtpLinear;
|
|
145
|
-
readonly mlpDown: MtpLinear;
|
|
146
|
-
readonly inputNorm: RMSNorm;
|
|
147
|
-
readonly postAttnNorm: RMSNorm;
|
|
148
|
-
readonly finalNorm: RMSNorm;
|
|
149
|
-
|
|
150
|
-
constructor(weights: Weights, config: ModelConfig, resources: DisposableStack) {
|
|
151
|
-
const eps = config.text.rmsNormEps;
|
|
152
|
-
this.fc = loadMtpLinear(weights, "fc", config, resources);
|
|
153
|
-
this.preFcNormEmbedding = new RMSNorm(weights.tensor("pre_fc_norm_embedding.weight"), eps);
|
|
154
|
-
this.preFcNormHidden = new RMSNorm(weights.tensor("pre_fc_norm_hidden.weight"), eps);
|
|
155
|
-
this.attn = new MtpAttention(weights, config, "layers.0.self_attn", resources);
|
|
156
|
-
this.mlpGate = loadMtpLinear(weights, "layers.0.mlp.gate_proj", config, resources);
|
|
157
|
-
this.mlpUp = loadMtpLinear(weights, "layers.0.mlp.up_proj", config, resources);
|
|
158
|
-
this.mlpDown = loadMtpLinear(weights, "layers.0.mlp.down_proj", config, resources);
|
|
159
|
-
this.inputNorm = new RMSNorm(weights.tensor("layers.0.input_layernorm.weight"), eps);
|
|
160
|
-
this.postAttnNorm = new RMSNorm(weights.tensor("layers.0.post_attention_layernorm.weight"), eps);
|
|
161
|
-
this.finalNorm = new RMSNorm(weights.tensor("norm.weight"), eps);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** One block forward over [1,S,·]: token embeddings ([1,S,H], target
|
|
165
|
-
* embed_tokens output) paired with hiddens ([1,S,H], target pre-final-norm
|
|
166
|
-
* or the module's own chained output). Appends S rows to `cache`; returns
|
|
167
|
-
* the module output [1,S,H] (post final norm — what the target lm_head
|
|
168
|
-
* consumes AND what chains into the next step's `hidden`). */
|
|
169
|
-
forward(tokenEmbeds: MlxArray, hiddens: MlxArray, cache: KVCache): MlxArray {
|
|
170
|
-
const embNorm = this.preFcNormEmbedding.forward(tokenEmbeds);
|
|
171
|
-
const hidNorm = this.preFcNormHidden.forward(hiddens);
|
|
172
|
-
const joined = ops.concatAxis([embNorm, hidNorm], 2);
|
|
173
|
-
embNorm.dispose();
|
|
174
|
-
hidNorm.dispose();
|
|
175
|
-
const x = this.fc.forward(joined);
|
|
176
|
-
joined.dispose();
|
|
177
|
-
|
|
178
|
-
// Decoder layer (Qwen3Layer.forward shape).
|
|
179
|
-
const L = x.shape[1]!;
|
|
180
|
-
const mask = cache.makeMask(L, null);
|
|
181
|
-
const xn = this.inputNorm.forward(x);
|
|
182
|
-
const r = this.attn.forward(xn, mask, cache);
|
|
183
|
-
xn.dispose();
|
|
184
|
-
mask.arr?.dispose();
|
|
185
|
-
const h = ops.add(x, r);
|
|
186
|
-
x.dispose();
|
|
187
|
-
r.dispose();
|
|
188
|
-
const hn = this.postAttnNorm.forward(h);
|
|
189
|
-
const g = this.mlpGate.forward(hn);
|
|
190
|
-
const u = this.mlpUp.forward(hn);
|
|
191
|
-
hn.dispose();
|
|
192
|
-
const silu = ops.silu(g);
|
|
193
|
-
g.dispose();
|
|
194
|
-
const act = ops.mul(silu, u);
|
|
195
|
-
silu.dispose();
|
|
196
|
-
u.dispose();
|
|
197
|
-
const m = this.mlpDown.forward(act);
|
|
198
|
-
act.dispose();
|
|
199
|
-
const out = ops.add(h, m);
|
|
200
|
-
h.dispose();
|
|
201
|
-
m.dispose();
|
|
202
|
-
return disposing(out, this.finalNorm.forward(out));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
class QwenMtpPrefixState implements SpeculativePrefixState {
|
|
207
|
-
constructor(readonly tokens: number[], readonly targetIdentity: object,
|
|
208
|
-
readonly namespace: string, readonly bytes: number, readonly target: Cache[],
|
|
209
|
-
public draft: KVCache | null, public hidden: MlxArray | null) {}
|
|
210
|
-
|
|
211
|
-
dispose(): void {
|
|
212
|
-
const resources = [...this.target.splice(0), this.draft, this.hidden];
|
|
213
|
-
this.draft = null;
|
|
214
|
-
this.hidden = null;
|
|
215
|
-
disposeResources(resources.filter((r) => r != null));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
48
|
export class QwenMtpProvider implements DraftProvider {
|
|
49
|
+
readonly grouped: GroupedDraftProvider = {
|
|
50
|
+
open: options => this.#openRows(options.target, options.sampling, options.checkpoints),
|
|
51
|
+
openPrefill: options => this.#openRows(options.target, null, options.checkpoints),
|
|
52
|
+
};
|
|
220
53
|
readonly id: string;
|
|
221
54
|
readonly weightsBytes: number;
|
|
222
55
|
readonly #module: MtpModule;
|
|
223
56
|
readonly #config: ModelConfig;
|
|
224
57
|
readonly #resources: DisposableStack;
|
|
225
|
-
readonly #
|
|
58
|
+
readonly #checkpointNamespace: string;
|
|
226
59
|
|
|
227
|
-
private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack) {
|
|
60
|
+
private constructor(id: string, config: ModelConfig, weightsBytes: number, module: MtpModule, resources: DisposableStack, checkpointNamespace: string) {
|
|
228
61
|
this.id = id;
|
|
229
62
|
this.#config = config;
|
|
230
63
|
this.#module = module;
|
|
231
64
|
this.weightsBytes = weightsBytes;
|
|
232
65
|
this.#resources = resources;
|
|
66
|
+
this.#checkpointNamespace = checkpointNamespace;
|
|
233
67
|
}
|
|
234
68
|
|
|
235
69
|
static async load(dir: string): Promise<QwenMtpProvider> {
|
|
@@ -242,16 +76,24 @@ export class QwenMtpProvider implements DraftProvider {
|
|
|
242
76
|
const weightsBytes = [...weights.shards.files.values()]
|
|
243
77
|
.reduce((a, f) => a + f.mmap.size, 0);
|
|
244
78
|
const module = new MtpModule(weights, config, resources);
|
|
79
|
+
// Stable across restarts and distinct for differently folded/quantized
|
|
80
|
+
// companions. Hash once at provider load, outside inference execution.
|
|
81
|
+
const identity = await artifactIdentity(configFingerprint(config),
|
|
82
|
+
[...weights.shards.files].map(([name, shard]) => ({ name, path: shard.path })));
|
|
245
83
|
return new QwenMtpProvider(
|
|
246
84
|
dir.split("/").filter(Boolean).at(-1) ?? "qwen-mtp",
|
|
247
|
-
config, weightsBytes, module, resources.move(),
|
|
85
|
+
config, weightsBytes, module, resources.move(), `qwen-mtp-v1:${identity}`,
|
|
248
86
|
);
|
|
249
87
|
}
|
|
250
88
|
|
|
251
89
|
open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
90
|
+
return new QwenMtpSource(this.#target(opts.target), this.#module, opts.sampler, this.#checkpointNamespace);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#target(view: TargetView): QwenMtpTarget {
|
|
252
94
|
if (this.#resources.disposed)
|
|
253
95
|
throw new Error("qwen MTP provider is disposed");
|
|
254
|
-
const target =
|
|
96
|
+
const target = view.qwenMtp;
|
|
255
97
|
if (!target)
|
|
256
98
|
throw new Error("qwen MTP drafting requires a qwen3_5-family target");
|
|
257
99
|
if (target.hiddenSize !== this.#config.text.hiddenSize) {
|
|
@@ -260,14 +102,50 @@ export class QwenMtpProvider implements DraftProvider {
|
|
|
260
102
|
`${target.hiddenSize} — split from a different checkpoint?`,
|
|
261
103
|
);
|
|
262
104
|
}
|
|
263
|
-
return
|
|
264
|
-
|
|
105
|
+
return target;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#openRows(view: TargetView, sampling: DraftRowSampling | null,
|
|
109
|
+
checkpoints: readonly (DraftRowCheckpoint | null)[]): DraftRowGroup & DraftPrefillGroup {
|
|
110
|
+
const target = this.#target(view);
|
|
111
|
+
const rows = new QwenMtpRows(target, this.#module, sampling, []);
|
|
112
|
+
const prepareAppend = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => {
|
|
113
|
+
const states: Array<MtpRowState | null> = [];
|
|
114
|
+
let change: PreparedStateChange | undefined;
|
|
115
|
+
try {
|
|
116
|
+
for (const checkpoint of checkpoints) states.push(checkpoint ? restoreQwenMtpState(checkpoint) : null);
|
|
117
|
+
change = rows.prepareAppend(states);
|
|
118
|
+
disposeResources(states.splice(0).flatMap(state => state ? [state.cache, state.hidden] : []));
|
|
119
|
+
return change;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return cleanupFailure(error, () => disposeResources([
|
|
122
|
+
...states.flatMap(state => state ? [state.cache, state.hidden] : []), ...(change ? [change] : []),
|
|
123
|
+
]));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const append = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => applyStateChanges([() => prepareAppend(checkpoints)]);
|
|
127
|
+
try { append(checkpoints); }
|
|
128
|
+
catch (error) { return cleanupFailure(error, () => rows.dispose()); }
|
|
129
|
+
return {
|
|
130
|
+
namespace: this.#checkpointNamespace, prefillMode: "full", tapLayers: [target.layerCount - 1],
|
|
131
|
+
get rowCount() { return rows.rowCount; },
|
|
132
|
+
append, prepareAppend, prefill: (tokens, context) => rows.prefill(tokens, context!), materialize: rows.materialize.bind(rows),
|
|
133
|
+
filterRows: rows.filterRows.bind(rows),
|
|
134
|
+
draft: rows.draft.bind(rows), commit: rows.commit.bind(rows),
|
|
135
|
+
capture(row) {
|
|
136
|
+
const state = rows.extractRow(row);
|
|
137
|
+
try { return captureQwenMtpState(state); }
|
|
138
|
+
finally { disposeResources([state.cache, state.hidden]); }
|
|
139
|
+
},
|
|
140
|
+
dispose: rows.dispose.bind(rows),
|
|
141
|
+
};
|
|
265
142
|
}
|
|
266
143
|
|
|
144
|
+
|
|
267
145
|
dispose(): void {
|
|
268
146
|
// Release cached transpose views before their native weight maps. MLX
|
|
269
147
|
// retains buffers needed by outstanding GPU commands until completion.
|
|
270
|
-
|
|
148
|
+
this.#resources.dispose();
|
|
271
149
|
}
|
|
272
150
|
}
|
|
273
151
|
|
|
@@ -284,7 +162,7 @@ export class QwenMtpSource implements DraftSource {
|
|
|
284
162
|
readonly #module: MtpModule;
|
|
285
163
|
readonly #sampler: Sampler;
|
|
286
164
|
#cache = new KVCache();
|
|
287
|
-
readonly
|
|
165
|
+
readonly checkpoint: NonNullable<DraftSource["checkpoint"]>;
|
|
288
166
|
#prefilledTokens = 0;
|
|
289
167
|
#hasDrafted = false;
|
|
290
168
|
/** Target pre-norm hidden at the position preceding the next pending
|
|
@@ -295,52 +173,29 @@ export class QwenMtpSource implements DraftSource {
|
|
|
295
173
|
#closed = false;
|
|
296
174
|
|
|
297
175
|
constructor(target: QwenMtpTarget, module: MtpModule, sampler: Sampler,
|
|
298
|
-
|
|
176
|
+
namespace = "qwen-mtp-v1") {
|
|
299
177
|
this.#target = target;
|
|
300
178
|
this.#module = module;
|
|
301
179
|
this.#sampler = sampler;
|
|
302
180
|
this.tapLayers = [target.layerCount - 1];
|
|
303
|
-
|
|
304
|
-
|
|
181
|
+
this.checkpoint = {
|
|
182
|
+
namespace,
|
|
183
|
+
restore: (tokens, attachment) => {
|
|
305
184
|
this.#checkOpen();
|
|
306
185
|
if (this.#prefilledTokens !== 0 || this.#hasDrafted)
|
|
307
|
-
throw new Error("Qwen MTP
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
disposeResources(caches);
|
|
315
|
-
caches.splice(0, caches.length, ...entry.target.splice(0));
|
|
316
|
-
this.#cache.dispose();
|
|
317
|
-
this.#cache = entry.draft;
|
|
318
|
-
entry.draft = null;
|
|
319
|
-
this.#pendingTrueHidden?.dispose();
|
|
320
|
-
this.#pendingTrueHidden = entry.hidden;
|
|
321
|
-
entry.hidden = null;
|
|
322
|
-
return this.#prefilledTokens = entry.tokens.length;
|
|
323
|
-
} finally { entry.dispose(); }
|
|
186
|
+
throw new Error("Qwen MTP checkpoint restore requires a fresh source");
|
|
187
|
+
const state = restoreQwenMtpState({ processedTokens: tokens, attachment });
|
|
188
|
+
try { disposeResources([this.#cache, ...(this.#pendingTrueHidden ? [this.#pendingTrueHidden] : [])]); }
|
|
189
|
+
catch (error) { return cleanupFailure(error, () => disposeResources([state.cache, state.hidden])); }
|
|
190
|
+
this.#cache = state.cache;
|
|
191
|
+
this.#pendingTrueHidden = state.hidden;
|
|
192
|
+
this.#prefilledTokens = tokens;
|
|
324
193
|
},
|
|
325
|
-
capture: (tokens
|
|
194
|
+
capture: (tokens) => {
|
|
326
195
|
this.#checkOpen();
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const bytes = cacheBytes([...caches, this.#cache]) + this.#pendingTrueHidden.nbytes;
|
|
331
|
-
if (bytes > maxBytes) { prefix.store.dispose(); return; }
|
|
332
|
-
const retained: Cache[] = [];
|
|
333
|
-
let draft: KVCache | null = null, hidden: MlxArray | null = null;
|
|
334
|
-
try {
|
|
335
|
-
retained.push(...cloneKvCaches(caches));
|
|
336
|
-
draft = cloneKvCaches([this.#cache])[0] as KVCache;
|
|
337
|
-
hidden = this.#pendingTrueHidden.slice([0, 0, 0], this.#pendingTrueHidden.shape);
|
|
338
|
-
const entry = new QwenMtpPrefixState([...tokens], prefix.identity, namespace,
|
|
339
|
-
bytes, retained.splice(0), draft, hidden);
|
|
340
|
-
draft = null;
|
|
341
|
-
hidden = null;
|
|
342
|
-
prefix.store.put(entry, maxBytes);
|
|
343
|
-
} finally { disposeResources([...retained, draft, hidden].filter((r) => r != null)); }
|
|
196
|
+
if (!this.#pendingTrueHidden || this.#cache.offset !== tokens - 1 || this.#roundAppended !== 0)
|
|
197
|
+
throw new Error("Qwen MTP snapshot requires an aligned committed boundary");
|
|
198
|
+
return captureQwenMtpState({ cache: this.#cache, hidden: this.#pendingTrueHidden }).attachment;
|
|
344
199
|
},
|
|
345
200
|
};
|
|
346
201
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MlxArray } from "../mlx/array";
|
|
2
|
+
import { KVCache } from "../model/gemma4-base";
|
|
3
|
+
import { cloneAttachments } from "../backends/mlx/checkpoint-state";
|
|
4
|
+
import { disposeResources } from "../engine/resources";
|
|
5
|
+
import type { MtpRowState } from "./qwen-mtp-rows";
|
|
6
|
+
import type { DraftRowCheckpoint } from "./source";
|
|
7
|
+
|
|
8
|
+
/** The existing Qwen companion attachment is also the state interchange
|
|
9
|
+
* between prepared requests and a group. Storage-tier policy stays outside. */
|
|
10
|
+
export function restoreQwenMtpState(checkpoint: DraftRowCheckpoint): MtpRowState {
|
|
11
|
+
const { processedTokens: tokens, attachment } = checkpoint;
|
|
12
|
+
if (attachment.schema !== "qwen-mtp-v1" || attachment.metadata.draftOffset !== tokens - 1)
|
|
13
|
+
throw new Error("invalid paired Qwen MTP checkpoint alignment");
|
|
14
|
+
const held = cloneAttachments([attachment])[0]!.tensors;
|
|
15
|
+
let cache: KVCache | null = new KVCache();
|
|
16
|
+
try {
|
|
17
|
+
if (tokens > 1) { cache.restoreState(held[0]!, held[1]!, tokens - 1); held.splice(0, 2); }
|
|
18
|
+
const state = { cache, hidden: held.pop()! }; cache = null; return state;
|
|
19
|
+
} finally { disposeResources([...held, ...(cache ? [cache] : [])]); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function captureQwenMtpState(state: MtpRowState): DraftRowCheckpoint {
|
|
23
|
+
const held: MlxArray[] = [];
|
|
24
|
+
try {
|
|
25
|
+
if (state.cache.offset > 0) held.push(...state.cache.temporalView());
|
|
26
|
+
held.push(state.hidden.slice([0, 0, 0], [...state.hidden.shape]));
|
|
27
|
+
return { processedTokens: state.cache.offset + 1,
|
|
28
|
+
attachment: { schema: "qwen-mtp-v1", metadata: { draftOffset: state.cache.offset }, tensors: held.splice(0) } };
|
|
29
|
+
} finally { disposeResources(held); }
|
|
30
|
+
}
|