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,200 @@
|
|
|
1
|
+
import type { BatchableCache, Cache } from "../../model/gemma4-base";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { clearCache } from "../../mlx/ffi";
|
|
5
|
+
import type { PrefillStep } from "../../inference/prefill";
|
|
6
|
+
import { cloneKvCaches, type CacheCodecProvider } from "../../kv-store";
|
|
7
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
8
|
+
import type { MlxGroupPreparation, Row } from "./batch-group";
|
|
9
|
+
import { evalCacheState } from "./prefill";
|
|
10
|
+
import { prefillCacheLayout } from "./cache-layout";
|
|
11
|
+
import { MlxStateRows } from "./state-rows";
|
|
12
|
+
import type { KvMaintenance } from "./kv-maintenance";
|
|
13
|
+
|
|
14
|
+
export interface MlxPrefillStep extends PrefillStep {
|
|
15
|
+
/** Some methods maintain their final target chunk before sampling. */
|
|
16
|
+
readonly maintain?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MlxPrefillState {
|
|
20
|
+
row: Row;
|
|
21
|
+
solo: Cache[];
|
|
22
|
+
pos: number;
|
|
23
|
+
retain?: () => void;
|
|
24
|
+
planned?: MlxPrefillStep;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The method owns initialization, planning, companion state, snapshots and
|
|
28
|
+
* completion. This driver owns common target batching and work boundaries.
|
|
29
|
+
* Checkpoint factories return owned cache state; close/dispose are idempotent. */
|
|
30
|
+
export interface MlxPrefillRowsHost<State extends MlxPrefillState> {
|
|
31
|
+
stateCodecs?: CacheCodecProvider;
|
|
32
|
+
maintain?: KvMaintenance;
|
|
33
|
+
open(row: Row): State;
|
|
34
|
+
plan(state: State): MlxPrefillStep;
|
|
35
|
+
/** Already prepared state can complete without any target forward. */
|
|
36
|
+
ready?(state: State): boolean;
|
|
37
|
+
forward(ids: MlxArray, caches: Cache[], states: readonly State[]): Promise<MlxArray>;
|
|
38
|
+
project(hidden: MlxArray, caches: Cache[], completed: readonly State[]): MlxArray | null;
|
|
39
|
+
/** Consume method context after target maintenance, before snapshots. */
|
|
40
|
+
afterForward?(ids: MlxArray, caches: Cache[], states: readonly State[], hidden: MlxArray): void | Promise<void>;
|
|
41
|
+
checkpoint(state: State, capture: () => Cache[], row: number): void;
|
|
42
|
+
complete(state: State, logits: MlxArray | null, row: number): Promise<void>;
|
|
43
|
+
reject(row: Row, error: unknown): void;
|
|
44
|
+
close(state: State): void;
|
|
45
|
+
filterRows?(keep: readonly number[]): void;
|
|
46
|
+
dispose?(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One target preparation lifecycle at B1/B>1. Late joins retain each row's
|
|
50
|
+
* planned chunk endpoint, precision transition and checkpoint boundary. */
|
|
51
|
+
export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPreparation {
|
|
52
|
+
#states: State[] = [];
|
|
53
|
+
#rows: Row[] = [];
|
|
54
|
+
#stateRows?: MlxStateRows;
|
|
55
|
+
constructor(readonly operations: MlxPrefillRowsHost<State>) {}
|
|
56
|
+
get rows(): readonly Row[] { return this.#rows; }
|
|
57
|
+
get canAdmit(): boolean { return this.#rows.length > 0; }
|
|
58
|
+
/** Keep a request's prefill weight until retirement so a nearly completed
|
|
59
|
+
* request does not repeatedly attract another long prefill into its tail. */
|
|
60
|
+
get tokenWeight(): number { return this.#states.reduce((sum, state) => sum + state.row.promptTokens - state.row.cachedTokens, 0); }
|
|
61
|
+
|
|
62
|
+
admit(row: Row): void {
|
|
63
|
+
this.#states.push(this.operations.open(row));
|
|
64
|
+
this.#rows.push(row);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#release(state: State): void {
|
|
68
|
+
const caches = state.solo; state.solo = [];
|
|
69
|
+
const retain = state.retain; state.retain = undefined;
|
|
70
|
+
disposeResources([...caches, { dispose: () => retain?.() }, { dispose: () => this.operations.close(state) }]);
|
|
71
|
+
}
|
|
72
|
+
#filter(keep: number[]): void {
|
|
73
|
+
if (keep.length) this.#stateRows?.filterRows(keep);
|
|
74
|
+
else { const rows = this.#stateRows; this.#stateRows = undefined; rows?.dispose(); }
|
|
75
|
+
this.operations.filterRows?.(keep);
|
|
76
|
+
this.#states = keep.map(row => this.#states[row]!);
|
|
77
|
+
this.#rows = keep.map(row => this.#rows[row]!);
|
|
78
|
+
}
|
|
79
|
+
/** Admission changes membership only between forwards. Existing planned
|
|
80
|
+
* endpoints survive a join, including partially consumed drain chunks. */
|
|
81
|
+
#synchronizeRows(): void {
|
|
82
|
+
if (!this.#stateRows && this.#states.length === 1) return;
|
|
83
|
+
if (!this.#stateRows) {
|
|
84
|
+
for (const state of this.#states) this.operations.maintain?.preparePrefill?.(state.solo);
|
|
85
|
+
const layouts: BatchableCache[] = [];
|
|
86
|
+
try { for (const cache of this.#states[0]!.solo) layouts.push(prefillCacheLayout(cache)); }
|
|
87
|
+
catch (error) { return cleanupFailure(error, () => disposeResources(layouts)); }
|
|
88
|
+
this.#stateRows = new MlxStateRows(layouts);
|
|
89
|
+
this.#stateRows.mergeRows(this.#states.map(state => state.solo));
|
|
90
|
+
} else {
|
|
91
|
+
for (const state of this.#states.slice(this.#stateRows.rowCount)) {
|
|
92
|
+
this.operations.maintain?.preparePrefill?.(state.solo);
|
|
93
|
+
this.#stateRows.append(state.solo);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Resolve input-backed copies before releasing SSD restore leases.
|
|
97
|
+
if (this.#states.some(state => state.solo.length)) {
|
|
98
|
+
evalCacheState(this.#stateRows.caches);
|
|
99
|
+
for (const state of this.#states) {
|
|
100
|
+
const caches = state.solo; state.solo = [];
|
|
101
|
+
const retain = state.retain; state.retain = undefined;
|
|
102
|
+
disposeResources([...caches, { dispose: () => retain?.() }]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const cache of this.#stateRows.caches) cache.prefillMaintenance?.beginPrefill();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async advance(): Promise<boolean> {
|
|
109
|
+
// Complete restored inputs before merging: their cache coverage must not
|
|
110
|
+
// introduce padding or a zero-width forward into unrelated cold rows.
|
|
111
|
+
if (this.operations.ready && this.#states.some(state => this.operations.ready!(state))) {
|
|
112
|
+
const remaining: State[] = [];
|
|
113
|
+
for (const state of this.#states) {
|
|
114
|
+
if (!this.operations.ready(state)) { remaining.push(state); continue; }
|
|
115
|
+
try {
|
|
116
|
+
state.row.req.signal?.throwIfAborted();
|
|
117
|
+
await this.operations.complete(state, null, -1);
|
|
118
|
+
state.solo = []; state.retain = undefined;
|
|
119
|
+
} catch (error) { this.operations.reject(state.row, error); this.#release(state); }
|
|
120
|
+
finally { this.operations.close(state); }
|
|
121
|
+
}
|
|
122
|
+
this.#states = remaining; this.#rows = remaining.map(state => state.row);
|
|
123
|
+
if (!remaining.length) { this.dispose(); return true; }
|
|
124
|
+
}
|
|
125
|
+
this.#synchronizeRows();
|
|
126
|
+
const keep: number[] = [];
|
|
127
|
+
for (const [index, state] of this.#states.entries()) {
|
|
128
|
+
if (!state.row.req.signal?.aborted) { keep.push(index); continue; }
|
|
129
|
+
this.#release(state); this.operations.reject(state.row, state.row.req.signal.reason);
|
|
130
|
+
}
|
|
131
|
+
if (keep.length !== this.#states.length) this.#filter(keep);
|
|
132
|
+
if (!this.#states.length) { this.dispose(); return true; }
|
|
133
|
+
while (this.#states.length) {
|
|
134
|
+
for (const state of this.#states) state.planned ??= this.operations.plan(state);
|
|
135
|
+
const count = Math.min(...this.#states.map(state => state.planned!.end - state.pos));
|
|
136
|
+
const closes = this.#states.map(state => state.row.req.trace?.begin("prefill.chunk", {
|
|
137
|
+
mechanism: "continuous", startToken: state.pos, batchSize: this.#states.length,
|
|
138
|
+
}));
|
|
139
|
+
const caches = this.#stateRows?.caches ?? this.#states[0]!.solo;
|
|
140
|
+
try {
|
|
141
|
+
using ids = count ? ops.fromInt32(this.#states.flatMap(state => state.row.req.promptIds.slice(state.pos, state.pos + count)),
|
|
142
|
+
[this.#states.length, count]) : null;
|
|
143
|
+
using hidden = ids ? await this.operations.forward(ids, caches, this.#states) : null;
|
|
144
|
+
const drains: number[] = [], finals: number[] = [];
|
|
145
|
+
let batchYield = false;
|
|
146
|
+
for (const [row, state] of this.#states.entries()) {
|
|
147
|
+
state.pos += count;
|
|
148
|
+
if (state.pos !== state.planned!.end) continue;
|
|
149
|
+
if (state.planned!.kind === "final") finals.push(row);
|
|
150
|
+
else drains.push(row);
|
|
151
|
+
batchYield ||= state.planned!.batchYield;
|
|
152
|
+
}
|
|
153
|
+
const maintained = [...drains, ...finals.filter(row => this.#states[row]!.planned!.maintain)];
|
|
154
|
+
if (count && (maintained.length || !finals.length)) {
|
|
155
|
+
evalCacheState(caches);
|
|
156
|
+
if (this.#stateRows) {
|
|
157
|
+
for (const cache of caches) cache.prefillMaintenance?.commitPrefill(maintained);
|
|
158
|
+
} else if (maintained.length) this.operations.maintain?.(this.#states[0]!.solo);
|
|
159
|
+
clearCache();
|
|
160
|
+
}
|
|
161
|
+
if (ids) {
|
|
162
|
+
const companion = this.operations.afterForward?.(ids, caches, this.#states, hidden!);
|
|
163
|
+
if (companion) await companion;
|
|
164
|
+
}
|
|
165
|
+
for (const row of [...drains, ...finals]) {
|
|
166
|
+
const state = this.#states[row]!;
|
|
167
|
+
if (state.planned!.snapshot) this.operations.checkpoint(state, () =>
|
|
168
|
+
this.#stateRows ? this.#stateRows.extractRow(row) : cloneKvCaches(state.solo, this.operations.stateCodecs), row);
|
|
169
|
+
if (!finals.includes(row)) state.planned = undefined;
|
|
170
|
+
}
|
|
171
|
+
using logits = hidden && finals.length ? this.operations.project(hidden, caches, finals.map(row => this.#states[row]!)) : null;
|
|
172
|
+
for (const row of finals) {
|
|
173
|
+
const state = this.#states[row]!;
|
|
174
|
+
if (this.#stateRows) state.solo = this.#stateRows.extractRow(row);
|
|
175
|
+
using part = logits ? logits.slice([row, logits.shape[1]! - 1, 0], [row + 1, logits.shape[1]!, logits.shape[2]!]) : null;
|
|
176
|
+
try {
|
|
177
|
+
state.row.req.signal?.throwIfAborted();
|
|
178
|
+
await this.operations.complete(state, part, row);
|
|
179
|
+
state.solo = []; state.retain = undefined;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.operations.reject(state.row, error);
|
|
182
|
+
this.#release(state);
|
|
183
|
+
} finally { this.operations.close(state); }
|
|
184
|
+
}
|
|
185
|
+
for (const cache of caches) (cache as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
186
|
+
if (finals.length) this.#filter(this.#states.flatMap((_, row) => finals.includes(row) ? [] : [row]));
|
|
187
|
+
if (!this.#states.length) { this.dispose(); return true; }
|
|
188
|
+
if (batchYield || finals.length) return false;
|
|
189
|
+
} finally { for (const close of closes) close?.(); }
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
dispose(): void {
|
|
195
|
+
const states = this.#states; this.#states = []; this.#rows = [];
|
|
196
|
+
const rows = this.#stateRows; this.#stateRows = undefined;
|
|
197
|
+
disposeResources([...(rows ? [rows] : []), ...states.map(state => ({ dispose: () => this.#release(state) })),
|
|
198
|
+
{ dispose: () => this.operations.dispose?.() }]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -9,7 +9,7 @@ import { withResource } from "../../engine/resources";
|
|
|
9
9
|
/** State views with explicit ownership are released even on deferred errors.
|
|
10
10
|
* Owned views precede borrowed state, preserving the serial eval ordering. */
|
|
11
11
|
export function evalCacheState(cache: Cache[]): void {
|
|
12
|
-
withResource(leaseCacheStates(cache), (state) => ops.evalAll([...state]));
|
|
12
|
+
withResource(leaseCacheStates(cache), (state) => { if (state.length) ops.evalAll([...state]); });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Forward once. Drains evaluate state, apply KV maintenance, then clear the
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import { Dtype } from "../../mlx/ffi";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { BatchedKVCache } from "../../model/batched-kv";
|
|
5
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
6
|
+
import { MlxStateRows } from "./state-rows";
|
|
7
|
+
import { disposeResources } from "../../engine/resources";
|
|
8
|
+
|
|
9
|
+
export interface ProjectedContext { k: MlxArray; v: MlxArray; }
|
|
10
|
+
export interface ContextProjection {
|
|
11
|
+
project(hidden: MlxArray, positions: number | MlxArray): ProjectedContext[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Incremental projected context reuses ordinary full-attention row storage.
|
|
15
|
+
* The projection graph owns numerics; this object owns positions and validity. */
|
|
16
|
+
export class MlxProjectedContextRows {
|
|
17
|
+
readonly state: MlxStateRows<BatchedKVCache>;
|
|
18
|
+
constructor(layers: number, readonly graph: ContextProjection) {
|
|
19
|
+
this.state = new MlxStateRows(Array.from({ length: layers }, () => new BatchedKVCache()));
|
|
20
|
+
}
|
|
21
|
+
get positions(): readonly number[] { return this.state.caches[0]!.rowOffsets; }
|
|
22
|
+
prepareAppend(rows: readonly (readonly Cache[])[]) { return this.state.prepareAppendMany(rows); }
|
|
23
|
+
append(hidden: MlxArray, lengths: readonly number[]): void {
|
|
24
|
+
const width = Math.max(...lengths), B = lengths.length, positions = this.positions;
|
|
25
|
+
using context = hidden.slice([0, 0, 0], [B, width, hidden.shape[2]!]);
|
|
26
|
+
using offsets = positions.every(position => position === positions[0]) ? null : ops.fromInt32([...positions], [B]);
|
|
27
|
+
const projected = this.graph.project(context, offsets ?? positions[0]!);
|
|
28
|
+
try {
|
|
29
|
+
for (const [layer, cache] of this.state.caches.entries()) {
|
|
30
|
+
cache.specRoundBegin();
|
|
31
|
+
for (const array of cache.updateAndFetch(projected[layer]!.k, projected[layer]!.v)) array.dispose();
|
|
32
|
+
if (lengths.every(length => length === width)) cache.specRoundCommit();
|
|
33
|
+
else cache.specRoundRollback(lengths);
|
|
34
|
+
}
|
|
35
|
+
} finally { disposeResources(projected.flatMap(pair => [pair.k, pair.v])); }
|
|
36
|
+
}
|
|
37
|
+
materialize(): void { ops.evalAll(this.state.caches.flatMap(cache => cache.state())); }
|
|
38
|
+
readAttention() {
|
|
39
|
+
const views: import("../../model/gemma4-base").KvDonorRows[] = [];
|
|
40
|
+
try { for (const cache of this.state.caches) views.push(cache.captureDonorRows()); }
|
|
41
|
+
catch (error) { disposeResources(views.flatMap(view => [view.keys, view.values])); throw error; }
|
|
42
|
+
const masks = new Map<string, MlxArray | null>();
|
|
43
|
+
return {
|
|
44
|
+
attend(layer: number, query: MlxArray, blockKeys: MlxArray, blockValues: MlxArray, scale = 1): MlxArray {
|
|
45
|
+
const view = views[layer]!, width = view.keys.shape[2]!, block = blockKeys.shape[2]!, B = view.offsets.length;
|
|
46
|
+
// Membership and coverage advance together in every context layer.
|
|
47
|
+
const key = `${width}:${block}:${view.keys.dtype}`;
|
|
48
|
+
if (!masks.has(key)) {
|
|
49
|
+
let mask: MlxArray | null = null;
|
|
50
|
+
if (view.starts.some(start => start !== 0) || view.ends.some(end => end !== width)) {
|
|
51
|
+
using columns = ops.arange(0, width, 1, Dtype.int32);
|
|
52
|
+
using lower = ops.fromInt32([...view.starts], [B, 1, 1, 1]);
|
|
53
|
+
using upper = ops.fromInt32([...view.ends], [B, 1, 1, 1]);
|
|
54
|
+
using before = ops.less(columns, lower), after = ops.greaterEqual(columns, upper);
|
|
55
|
+
using invalid = ops.logicalOr(before, after);
|
|
56
|
+
using floats = invalid.astype(view.keys.dtype);
|
|
57
|
+
using prefix = ops.mulScalar(floats, -1e9);
|
|
58
|
+
using suffix = ops.zeros([B, 1, 1, block], view.keys.dtype);
|
|
59
|
+
mask = ops.concatAxis([prefix, suffix], 3);
|
|
60
|
+
}
|
|
61
|
+
masks.set(key, mask);
|
|
62
|
+
}
|
|
63
|
+
using keys = ops.concatAxis([view.keys, blockKeys], 2);
|
|
64
|
+
using values = ops.concatAxis([view.values, blockValues], 2);
|
|
65
|
+
const mask = masks.get(key)!;
|
|
66
|
+
return ops.sdpa(query, keys, values, scale, mask ? "array" : "", mask);
|
|
67
|
+
},
|
|
68
|
+
dispose() { disposeResources([...views.flatMap(view => [view.keys, view.values]), ...[...masks.values()].filter(value => value !== null)]); },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
dispose(): void { this.state.dispose(); }
|
|
72
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GenerateOptions } from "../../generate";
|
|
2
|
+
import type { RuntimeModel } from "../../model/factory";
|
|
3
|
+
import { KVCache, type Cache } from "../../model/gemma4-base";
|
|
4
|
+
import { PagedKVCache } from "../../lab/paged-kv/paged-kv";
|
|
5
|
+
import type { RowPromptCache } from "./batch-group";
|
|
6
|
+
import { disposeResources } from "../../engine/resources";
|
|
7
|
+
|
|
8
|
+
/** Request state construction and reusable-prefix policy. Equal keys declare
|
|
9
|
+
* compatible state layouts; the executor need not inspect their settings. */
|
|
10
|
+
export interface MlxRequestStatePolicy {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly promptCache?: RowPromptCache;
|
|
13
|
+
create(): Cache[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Replace fresh full-attention storage before prefill. Sliding layers retain
|
|
17
|
+
* their layout; pre-warmed library caches retain their existing storage. */
|
|
18
|
+
export function maybePageKv(
|
|
19
|
+
cache: Cache[], options: GenerateOptions, capacityTokens: number,
|
|
20
|
+
): void {
|
|
21
|
+
if (!options.pagedKv) return;
|
|
22
|
+
if (cache.some((c) => c.offset > 0)) return;
|
|
23
|
+
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
24
|
+
for (let i = 0; i < cache.length; i++) {
|
|
25
|
+
if (cache[i] instanceof KVCache) {
|
|
26
|
+
cache[i]!.dispose(); // fresh (offset 0) — nothing stored yet
|
|
27
|
+
cache[i] = new PagedKVCache(capacityTokens, blockSize);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function bindPagedRequestState(model: RuntimeModel, options: GenerateOptions,
|
|
33
|
+
capacityTokens: number): MlxRequestStatePolicy | undefined {
|
|
34
|
+
if (!options.pagedKv) return undefined;
|
|
35
|
+
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
36
|
+
return { key: `paged:${blockSize}`, create() {
|
|
37
|
+
const caches = model.makeCache();
|
|
38
|
+
try { maybePageKv(caches, { pagedKv: { blockSize } }, capacityTokens); return caches; }
|
|
39
|
+
catch (error) { disposeResources(caches); throw error; }
|
|
40
|
+
} };
|
|
41
|
+
}
|
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
import type { SpeculativeTransaction } from "../../inference/rollback";
|
|
2
2
|
import type { Cache } from "../../model/gemma4-base";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface TransactionLayer<Acceptance> {
|
|
5
|
+
fits(drafts: number): boolean;
|
|
6
|
+
begin(): void;
|
|
7
|
+
resolve(accepted: Acceptance, drafts: number): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** State storage that can retain a different verified prefix for each row.
|
|
11
|
+
* Both attention and recurrent layouts implement this same transaction. */
|
|
12
|
+
export interface RowSpeculativeState {
|
|
13
|
+
specRoundBegin(): void;
|
|
14
|
+
specRoundCommit(): void;
|
|
15
|
+
specRoundRollback(keep: number | readonly number[]): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function bindRowCacheRollback(
|
|
19
|
+
caches: readonly RowSpeculativeState[], rows: number,
|
|
20
|
+
): SpeculativeTransaction<readonly number[]> {
|
|
21
|
+
return bindTransaction(caches.map(cache => ({
|
|
22
|
+
fits: () => true,
|
|
23
|
+
begin: () => cache.specRoundBegin(),
|
|
24
|
+
resolve(accepted: readonly number[], drafts: number) {
|
|
25
|
+
if (accepted.every(count => count === drafts)) cache.specRoundCommit();
|
|
26
|
+
else cache.specRoundRollback(accepted.every(count => count === accepted[0])
|
|
27
|
+
? accepted[0]! + 1 : accepted.map(count => count + 1));
|
|
28
|
+
},
|
|
29
|
+
})), (accepted, drafts) => accepted.length === rows &&
|
|
30
|
+
accepted.every(count => Number.isSafeInteger(count) && count >= 0 && count <= drafts));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Bind the recurrent transaction or the trim implementation once.
|
|
5
34
|
* Ring-capacity checks still happen at each safe boundary before any writes. */
|
|
6
35
|
export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransaction {
|
|
7
36
|
const layers = caches.map((cache) => {
|
|
@@ -24,6 +53,15 @@ export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransact
|
|
|
24
53
|
},
|
|
25
54
|
};
|
|
26
55
|
});
|
|
56
|
+
return bindTransaction(layers, (accepted, drafts) =>
|
|
57
|
+
Number.isSafeInteger(accepted) && accepted >= 0 && accepted <= drafts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One transaction lifecycle for scalar and row-wise state bindings. */
|
|
61
|
+
function bindTransaction<Acceptance>(
|
|
62
|
+
layers: readonly TransactionLayer<Acceptance>[],
|
|
63
|
+
accepts: (accepted: Acceptance, drafts: number) => boolean,
|
|
64
|
+
): SpeculativeTransaction<Acceptance> {
|
|
27
65
|
let active: number | undefined;
|
|
28
66
|
let invalid = false;
|
|
29
67
|
const validate = (drafts: number) => {
|
|
@@ -41,8 +79,8 @@ export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransact
|
|
|
41
79
|
catch (error) { invalid = true; throw error; }
|
|
42
80
|
},
|
|
43
81
|
resolve(accepted) {
|
|
44
|
-
|
|
45
|
-
if (active === undefined || accepted
|
|
82
|
+
if (invalid) throw new Error("speculative state is invalid and must be discarded");
|
|
83
|
+
if (active === undefined || !accepts(accepted, active)) throw new Error("invalid speculative acceptance count");
|
|
46
84
|
try { for (const layer of layers) layer.resolve(accepted, active); }
|
|
47
85
|
catch (error) { invalid = true; throw error; }
|
|
48
86
|
active = undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import * as ops from "../../mlx/ops";
|
|
3
|
+
import type { TokenLogprobs } from "../../contracts/generation";
|
|
4
|
+
import { readStepExtras, stepExtrasArrays, disposeStepExtras,
|
|
5
|
+
type DeviceStepSampler, type StepExtras } from "../../sampler";
|
|
6
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
7
|
+
|
|
8
|
+
type TokenSink = (token: number, logprobs?: TokenLogprobs) => void | boolean | Promise<void | boolean>;
|
|
9
|
+
|
|
10
|
+
/** Request-owned sampling and output metadata. Execution groups receive token
|
|
11
|
+
* tensors and an output callback; they need no logprob or retention policy. */
|
|
12
|
+
export function createRowSampling(
|
|
13
|
+
sampler: DeviceStepSampler, onToken: TokenSink,
|
|
14
|
+
) {
|
|
15
|
+
if (!sampler.capturesLogprobs) return {
|
|
16
|
+
sample: (logits: MlxArray, step: number) => sampler.sample(logits, step).token,
|
|
17
|
+
plainGreedy: sampler.isPlainGreedy, onToken, dispose: () => sampler.dispose(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// A pipelined group can sample the next token before emitting the previous
|
|
21
|
+
// one. Keep captures by request-local step, independently of row position.
|
|
22
|
+
const pending = new Map<number, StepExtras>();
|
|
23
|
+
let emitted = 0;
|
|
24
|
+
return {
|
|
25
|
+
sample(logits: MlxArray, step: number): MlxArray {
|
|
26
|
+
const result = sampler.sample(logits, step);
|
|
27
|
+
try {
|
|
28
|
+
ops.asyncEvalAll([result.token, ...stepExtrasArrays(result.extras)]);
|
|
29
|
+
if (result.extras) pending.set(step, result.extras);
|
|
30
|
+
return result.token;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return cleanupFailure(error, () => disposeResources([result.token,
|
|
33
|
+
{ dispose: () => disposeStepExtras(result.extras) }]));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
// The vectorized token-only shortcut bypasses the per-request sampler.
|
|
37
|
+
plainGreedy: false,
|
|
38
|
+
onToken(token: number) {
|
|
39
|
+
const extras = pending.get(emitted) ?? null;
|
|
40
|
+
pending.delete(emitted++);
|
|
41
|
+
return onToken(token, readStepExtras(extras));
|
|
42
|
+
},
|
|
43
|
+
dispose() {
|
|
44
|
+
const extras = [...pending.values()];
|
|
45
|
+
pending.clear();
|
|
46
|
+
disposeResources([sampler, ...extras.map(value => ({ dispose: () => disposeStepExtras(value) }))]);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,32 +1,30 @@
|
|
|
1
|
+
import type { SpecRunServices } from "../../spec/serve-loop";
|
|
1
2
|
import { cleanupFailure, disposeResources, ownResource } from "../../engine/resources";
|
|
2
3
|
import { bindGeneration } from "../../generate";
|
|
3
4
|
import type { RuntimeModel } from "../../model/factory";
|
|
4
5
|
import { Qwen35Model } from "../../model/qwen3_5";
|
|
5
6
|
import type { Cache } from "../../model/gemma4";
|
|
6
|
-
import type {
|
|
7
|
+
import type { MlxPrefixCache } from "./checkpoint-state";
|
|
7
8
|
import type { SsdCacheStore } from "../../ssd-cache";
|
|
8
9
|
import type { DraftProvider } from "../../spec/source";
|
|
9
10
|
import type { SerialRun, Vision } from "../../serve/generation-gateway";
|
|
10
|
-
import {
|
|
11
|
+
import { bindContinuationPolicy } from "./continuation";
|
|
11
12
|
import { runtimeConfig, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
12
13
|
import { bindLegacySpeculativeModel } from "./speculative";
|
|
13
|
-
import { enterMlxMemoryGuard, type MlxMemoryBudget } from "./memory-guard";
|
|
14
14
|
|
|
15
15
|
/** Native serial execution depends on this bound port, never a model union.
|
|
16
16
|
* Weights remain borrowed. The gateway supplies the exclusive runtime lease. */
|
|
17
17
|
export interface MlxSerialBinding {
|
|
18
18
|
readonly runtime: RuntimeConfig;
|
|
19
19
|
readonly generate: ReturnType<typeof bindGeneration>;
|
|
20
|
-
readonly speculate?:
|
|
20
|
+
readonly speculate?: (
|
|
21
|
+
promptIds: Parameters<SerialRun>[0], options: Parameters<SerialRun>[1],
|
|
22
|
+
onToken: Parameters<SerialRun>[2], services?: SpecRunServices,
|
|
23
|
+
) => ReturnType<SerialRun>;
|
|
21
24
|
makeCache(): Cache[];
|
|
22
25
|
enterMedia?(vision?: Vision): () => void;
|
|
23
|
-
enterMemoryGuard?(budget: SerialMemoryBudget, promptTokens: number,
|
|
24
|
-
prefillChunkSize?: number): { check(): void; close(): void };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
/** Admission's ceiling and the cache that can release memory under the GPU lease. */
|
|
28
|
-
export type SerialMemoryBudget = MlxMemoryBudget;
|
|
29
|
-
|
|
30
28
|
/** Family-specific context is bound once at the compatibility boundary. */
|
|
31
29
|
export function bindLegacySerialModel(
|
|
32
30
|
model: RuntimeModel,
|
|
@@ -37,13 +35,10 @@ export function bindLegacySerialModel(
|
|
|
37
35
|
runtime: runtimeConfig(),
|
|
38
36
|
generate: bindGeneration(model),
|
|
39
37
|
makeCache: model.makeCache.bind(model),
|
|
40
|
-
|
|
41
|
-
return enterMlxMemoryGuard(model, budget, promptTokens, prefillChunkSize);
|
|
42
|
-
},
|
|
43
|
-
...(speculative && draft ? { speculate: (async (prompt, options, onToken) => {
|
|
38
|
+
...(speculative && draft ? { speculate: (async (prompt, options, onToken, services) => {
|
|
44
39
|
const { specRun } = await import("../../spec/serve-loop");
|
|
45
|
-
return specRun(speculative, draft.numDraftTokens, prompt, options, onToken);
|
|
46
|
-
}) satisfies
|
|
40
|
+
return specRun(speculative, draft.numDraftTokens, prompt, options, onToken, services);
|
|
41
|
+
}) satisfies NonNullable<MlxSerialBinding["speculate"]> } : {}),
|
|
47
42
|
...(model instanceof Qwen35Model ? { enterMedia(vision?: Vision) {
|
|
48
43
|
const previous = model.mrope;
|
|
49
44
|
if (vision?.mrope) model.mrope = vision.mrope;
|
|
@@ -53,11 +48,11 @@ export function bindLegacySerialModel(
|
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
export interface MlxSerialServices {
|
|
56
|
-
readonly promptCache:
|
|
51
|
+
readonly promptCache: MlxPrefixCache & { readonly maxBytes?: number };
|
|
57
52
|
readonly checkpoints: Pick<SsdCacheStore, "findGenerationCheckpoint" | "restore" |
|
|
58
53
|
"storeGenerationCheckpoint" | "removeGenerationCheckpoints"> | null;
|
|
59
54
|
readonly checkpointEveryTokens?: number;
|
|
60
|
-
readonly
|
|
55
|
+
readonly checkpointPersistence?: import("./continuation-persistence").ContinuationPersistence;
|
|
61
56
|
/** Artifact, implementation, state ABI and codec identity captured at load. */
|
|
62
57
|
readonly identity: unknown;
|
|
63
58
|
adapterNamespace(adapters: string[]): string;
|
|
@@ -71,17 +66,17 @@ export interface MlxSerialServices {
|
|
|
71
66
|
export function createMlxSerialExecutor(binding: MlxSerialBinding, services: MlxSerialServices): SerialRun {
|
|
72
67
|
const { promptCache, checkpoints: ssdStore } = services;
|
|
73
68
|
return (promptIds, options, onToken, vision, trace, execution) => withRuntimeConfig(binding.runtime, async () => {
|
|
69
|
+
let releaseContinuation: (() => void) | undefined;
|
|
74
70
|
let caches: Cache[] = [];
|
|
75
71
|
let retain: (() => void) | undefined;
|
|
76
72
|
let closeMedia: (() => void) | undefined;
|
|
77
|
-
let memoryGuard: ReturnType<NonNullable<MlxSerialBinding["enterMemoryGuard"]>> | undefined;
|
|
78
73
|
const cleanup = ownResource(null, () => disposeResources([
|
|
79
74
|
{ dispose() {
|
|
80
75
|
disposeResources(caches);
|
|
81
76
|
retain?.(); // backing release requires successful cache disposal
|
|
82
77
|
} },
|
|
83
78
|
{ dispose: () => closeMedia?.() },
|
|
84
|
-
{ dispose: () =>
|
|
79
|
+
{ dispose: () => releaseContinuation?.() },
|
|
85
80
|
...[options.grammar, vision?.embeddings, vision?.imageMask,
|
|
86
81
|
vision?.multimodalMask, options.visionPixels].filter((value) => value != null),
|
|
87
82
|
]));
|
|
@@ -90,11 +85,12 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
90
85
|
if (!execution) throw new Error("serial execution requires a resolved plan");
|
|
91
86
|
if (execution.method === "speculative") {
|
|
92
87
|
if (!binding.speculate) throw new Error("resolved speculation requires a bound verifier");
|
|
93
|
-
return await binding.speculate(promptIds, {
|
|
94
|
-
...
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
return await binding.speculate(promptIds, options, onToken, {
|
|
89
|
+
...(binding.runtime.flag("MLX_BUN_MTP_PROMPT_CACHE", true) && services.promptCache.maxBytes !== 0
|
|
90
|
+
? { prefixCache: services.promptCache } : {}),
|
|
91
|
+
cloneState: services.cloneState,
|
|
92
|
+
cacheNamespace: options.adapters?.length ? services.adapterNamespace(options.adapters) : "",
|
|
93
|
+
});
|
|
98
94
|
}
|
|
99
95
|
// Cache entries are adapter-specific: KV computed under one adapter
|
|
100
96
|
// must never seed another's (or the base's) prefill.
|
|
@@ -111,32 +107,26 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
111
107
|
const skipPromptCache = !execution.promptCache;
|
|
112
108
|
const checkpointEvery = services.checkpointEveryTokens;
|
|
113
109
|
const checkpointEligible = execution.checkpoint;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
: null;
|
|
110
|
+
const continuation = checkpointEligible ? bindContinuationPolicy({ store: ssdStore!,
|
|
111
|
+
restore: entry => ssdStore!.restore(entry, binding), prompt: promptIds,
|
|
112
|
+
options, execution, identity: services.identity, namespace: cacheNs, persistence: services.checkpointPersistence }) : null;
|
|
113
|
+
releaseContinuation = continuation?.release;
|
|
114
|
+
const checkpointKey = continuation?.key;
|
|
117
115
|
// Both tiers in one call (Layer 0): take() prefers a strictly-longer
|
|
118
116
|
// SSD prefix, restores it zero-copy, and trims — see PromptCache.take.
|
|
119
117
|
const closeCacheLookup = trace?.begin("cache.lookup_restore", {
|
|
120
118
|
mechanism: "serial",
|
|
121
119
|
bypassed: skipPromptCache,
|
|
122
120
|
});
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
? ssdStore!.restore(checkpointEntry, binding)
|
|
128
|
-
: null;
|
|
129
|
-
caches = restoredCheckpoint?.caches ?? [];
|
|
130
|
-
const checkpoint = restoredCheckpoint?.header.generationCheckpoint;
|
|
131
|
-
if (restoredCheckpoint && !checkpoint)
|
|
132
|
-
throw new Error("restored generation checkpoint has no continuation metadata");
|
|
133
|
-
const resuming = Boolean(restoredCheckpoint && checkpoint);
|
|
134
|
-
const generationPromptIds = resuming ? restoredCheckpoint!.tokens : promptIds;
|
|
121
|
+
const checkpoint = continuation?.restore();
|
|
122
|
+
caches = checkpoint?.caches ?? [];
|
|
123
|
+
const resuming = Boolean(checkpoint);
|
|
124
|
+
const generationPromptIds = checkpoint?.cacheTokens ?? promptIds;
|
|
135
125
|
const entry = skipPromptCache || resuming
|
|
136
126
|
? null
|
|
137
127
|
: promptCache.take(promptIds, cacheNs);
|
|
138
128
|
if (entry) { caches = entry.caches; retain = entry.retain; }
|
|
139
|
-
if (!
|
|
129
|
+
if (!checkpoint && !entry) caches = binding.makeCache();
|
|
140
130
|
closeCacheLookup?.();
|
|
141
131
|
// Prompt-boundary snapshot (the multi-turn agent fix, 2026-07-04): the
|
|
142
132
|
// prompt+gen entry put() below is UNTRIMMABLE at context > sliding
|
|
@@ -160,12 +150,6 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
160
150
|
const snapshotBoundary =
|
|
161
151
|
!skipPromptCache && !resuming && boundary >= 256 &&
|
|
162
152
|
boundary > (entry?.tokens.length ?? 0);
|
|
163
|
-
if (services.memoryBudget) {
|
|
164
|
-
memoryGuard = binding.enterMemoryGuard?.(
|
|
165
|
-
services.memoryBudget, generationPromptIds.length, options.prefillChunkSize,
|
|
166
|
-
);
|
|
167
|
-
memoryGuard?.check();
|
|
168
|
-
}
|
|
169
153
|
closeMedia = binding.enterMedia?.(vision);
|
|
170
154
|
if (resuming) {
|
|
171
155
|
const replay = generationPromptIds.slice(promptIds.length);
|
|
@@ -190,7 +174,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
190
174
|
? {
|
|
191
175
|
initialPendingToken: checkpoint!.pendingToken,
|
|
192
176
|
initialGeneratedTokens: checkpoint!.generatedTokens,
|
|
193
|
-
originalPromptTokens:
|
|
177
|
+
originalPromptTokens: promptIds.length,
|
|
194
178
|
}
|
|
195
179
|
: {}),
|
|
196
180
|
...(checkpointEligible
|
|
@@ -202,23 +186,9 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
202
186
|
generatedTokens: number;
|
|
203
187
|
pendingToken: number;
|
|
204
188
|
}) => {
|
|
205
|
-
|
|
206
|
-
state.
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
key: checkpointKey!,
|
|
210
|
-
cacheNs,
|
|
211
|
-
originalPromptTokens: promptIds.length,
|
|
212
|
-
generatedTokens: state.generatedTokens,
|
|
213
|
-
pendingToken: state.pendingToken,
|
|
214
|
-
seed: options.seed ?? 0,
|
|
215
|
-
seedWasExplicit: options.seedWasExplicit === true,
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
if (stored)
|
|
219
|
-
console.log(
|
|
220
|
-
`[generation-checkpoint] saved ${state.generatedTokens} emitted tokens`,
|
|
221
|
-
);
|
|
189
|
+
if (services.checkpointPersistence)
|
|
190
|
+
continuation!.captureOwned({ ...state, caches: services.cloneState(state.caches) });
|
|
191
|
+
else await continuation!.capture(state);
|
|
222
192
|
},
|
|
223
193
|
}
|
|
224
194
|
: {}),
|
|
@@ -252,11 +222,10 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
252
222
|
: {}),
|
|
253
223
|
}, { trace, mechanism: "serial" });
|
|
254
224
|
for await (const t of gen) {
|
|
255
|
-
if (t.index % 256 === 0) memoryGuard?.check();
|
|
256
225
|
if ((await onToken(t.token, t.logprobs)) === false) break;
|
|
257
226
|
}
|
|
258
227
|
const s = gen.stats!; // set on completion AND on early break
|
|
259
|
-
|
|
228
|
+
continuation?.complete();
|
|
260
229
|
if (!skipPromptCache) {
|
|
261
230
|
// put() fires onPut → the debounced write-behind SSD snapshot
|
|
262
231
|
// (wired below), covering the batch lane's puts too.
|