mlx-bun 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/docs/README.md +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- 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 +107 -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/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- 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 +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- 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 +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- 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 +94 -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 +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -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 +183 -82
- package/src/model/gemma4.ts +116 -28
- 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 +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- 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/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -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 +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- 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 +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +217 -57
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- 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,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,29 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
3
|
+
|
|
4
|
+
/** Prepared native input with an indivisible prefill. The preparation owner
|
|
5
|
+
* retains its tensors; the model binding returns an owned final hidden row.
|
|
6
|
+
* Decode, sampling and row retirement use the ordinary execution group. */
|
|
7
|
+
export interface MlxPromptInput {
|
|
8
|
+
forward(ids: MlxArray, caches: Cache[], start?: number): MlxArray;
|
|
9
|
+
readonly decodeState?: MlxDecodeState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Additional model-owned forward state. Equal keys declare compatible row
|
|
13
|
+
* state; the backend supplies the current row order after joins/retirement. */
|
|
14
|
+
export interface MlxDecodeState {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
forward(ids: MlxArray, caches: Cache[], rows: readonly MlxDecodeState[]): MlxArray;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Preserve the ordinary generator's projection geometry for a prepared
|
|
20
|
+
* embedding sequence: process the whole prompt, then project only its tip. */
|
|
21
|
+
export function bindEmbeddingsInput(
|
|
22
|
+
forward: (ids: MlxArray, caches: Cache[], start: number) => MlxArray,
|
|
23
|
+
): MlxPromptInput {
|
|
24
|
+
return { forward(ids, caches, start = 0) {
|
|
25
|
+
using hidden = forward(ids, caches, start);
|
|
26
|
+
const [batch, length, width] = hidden.shape;
|
|
27
|
+
return hidden.slice([0, length! - 1, 0], [batch!, length!, width!]);
|
|
28
|
+
} };
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Qwen35Model } from "../../model/qwen3_5";
|
|
2
|
+
import { mropePositionIds, type MropeRequestState } from "../../model/qwen3-mrope";
|
|
3
|
+
import type { MlxArray } from "../../mlx/array";
|
|
4
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
5
|
+
import * as ops from "../../mlx/ops";
|
|
6
|
+
import { Dtype } from "../../mlx/ffi";
|
|
7
|
+
import { bindEmbeddingsInput, type MlxDecodeState, type MlxPromptInput } from "./prompt-input";
|
|
8
|
+
|
|
9
|
+
class QwenMediaDecodeState implements MlxDecodeState {
|
|
10
|
+
readonly key = "qwen-interleaved-mrope";
|
|
11
|
+
constructor(readonly model: Qwen35Model, readonly delta: number) {}
|
|
12
|
+
|
|
13
|
+
forward(ids: MlxArray, caches: Cache[], rows: readonly MlxDecodeState[]): MlxArray {
|
|
14
|
+
const B = rows.length, L = ids.shape[1]!;
|
|
15
|
+
const cache = caches[this.model.faIdx]!;
|
|
16
|
+
// The cache supplies logical positions, including left padding, on device.
|
|
17
|
+
// No offset readback or request-global position mutation is needed.
|
|
18
|
+
const offsets = (cache as Cache & { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
19
|
+
using deltas = ops.fromInt32(rows.map(row => (row as QwenMediaDecodeState).delta), [B]);
|
|
20
|
+
using scalar = offsets ? null : ops.fromInt32([cache.offset], []);
|
|
21
|
+
using starts = ops.add(offsets ?? scalar!, deltas);
|
|
22
|
+
using startRows = ops.reshape(starts, [B, 1]);
|
|
23
|
+
using indices = ops.arange(0, L, 1, Dtype.int32);
|
|
24
|
+
using positions = ops.add(startRows, indices);
|
|
25
|
+
using axis = ops.reshape(positions, [1, B, L]);
|
|
26
|
+
using grid = ops.concatAxis([axis, axis, axis], 0);
|
|
27
|
+
return this.model.forwardHiddenAtPositions(ids, caches, grid);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The prompt owns its grid; only its continuation delta survives into decode. */
|
|
32
|
+
export function bindQwenMediaInput(model: Qwen35Model, embeddings: MlxArray,
|
|
33
|
+
state: MropeRequestState): MlxPromptInput {
|
|
34
|
+
const input = bindEmbeddingsInput((ids, caches, start) => {
|
|
35
|
+
using positions = mropePositionIds(state, start, ids.shape[1]!);
|
|
36
|
+
return start > 0 ? model.forwardHiddenAtPositions(ids, caches, positions)
|
|
37
|
+
: model.forwardEmbeddingsAtPositions(embeddings, caches, positions);
|
|
38
|
+
});
|
|
39
|
+
return { ...input, decodeState: new QwenMediaDecodeState(model, state.delta) };
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { namespacedCache } from "../../engine/namespaced-cache";
|
|
2
|
+
import { runtimeConfig, runtimeFlag, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
3
|
+
import type { GenerateOptions } from "../../generate";
|
|
4
|
+
import type { RuntimeModel } from "../../model/factory";
|
|
5
|
+
import { KVCache, type Cache } from "../../model/gemma4-base";
|
|
6
|
+
import { PagedKVCache } from "../../lab/paged-kv/paged-kv";
|
|
7
|
+
import type { RowPromptCache } from "./batch-group";
|
|
8
|
+
import { disposeResources } from "../../engine/resources";
|
|
9
|
+
|
|
10
|
+
/** Request state construction and reusable-prefix policy. Equal keys declare
|
|
11
|
+
* compatible state layouts; the executor need not inspect their settings. */
|
|
12
|
+
export interface MlxRequestStatePolicy {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly promptCache?: RowPromptCache;
|
|
15
|
+
create(): Cache[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Replace fresh full-attention storage before prefill. Sliding layers retain
|
|
19
|
+
* their layout; pre-warmed library caches retain their existing storage. */
|
|
20
|
+
export function maybePageKv(
|
|
21
|
+
cache: Cache[], options: GenerateOptions, capacityTokens: number,
|
|
22
|
+
): void {
|
|
23
|
+
if (!options.pagedKv) return;
|
|
24
|
+
if (cache.some((c) => c.offset > 0)) return;
|
|
25
|
+
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
26
|
+
for (let i = 0; i < cache.length; i++) {
|
|
27
|
+
if (cache[i] instanceof KVCache) {
|
|
28
|
+
cache[i]!.dispose(); // fresh (offset 0) — nothing stored yet
|
|
29
|
+
cache[i] = new PagedKVCache(capacityTokens, blockSize, undefined,
|
|
30
|
+
options.kvBits === 4 || options.kvBits === 8 ? { bits: options.kvBits, groupSize: options.kvGroupSize ?? 64 } : undefined);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pagedPrefixNamespace(options: GenerateOptions, base: string,
|
|
36
|
+
direct = runtimeFlag("MLX_BUN_PAGED_ATTN", false)): string {
|
|
37
|
+
return JSON.stringify(["paged-v1", options.pagedKv?.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE,
|
|
38
|
+
options.kvBits ?? 0, options.kvGroupSize ?? 64, direct, base]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function bindPagedRequestState(model: RuntimeModel, options: GenerateOptions,
|
|
42
|
+
capacityTokens: number, cache?: RowPromptCache, runtime: RuntimeConfig = runtimeConfig()): MlxRequestStatePolicy | undefined {
|
|
43
|
+
if (!options.pagedKv) return undefined;
|
|
44
|
+
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
45
|
+
const stateOptions = { pagedKv: { blockSize }, kvBits: options.kvBits, kvGroupSize: options.kvGroupSize };
|
|
46
|
+
const direct = runtime.flag("MLX_BUN_PAGED_ATTN", false);
|
|
47
|
+
const namespace = (base: string) => pagedPrefixNamespace(stateOptions, base, direct);
|
|
48
|
+
return { key: namespace(""),
|
|
49
|
+
promptCache: cache ? namespacedCache(cache, namespace) : undefined, create: () => withRuntimeConfig(runtime, () => {
|
|
50
|
+
const caches = model.makeCache();
|
|
51
|
+
try { maybePageKv(caches, stateOptions, capacityTokens); return caches; }
|
|
52
|
+
catch (error) { disposeResources(caches); throw error; }
|
|
53
|
+
}) };
|
|
54
|
+
}
|
|
@@ -1,7 +1,39 @@
|
|
|
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(drafts: number): 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
|
+
// A zero-candidate round consumes only the committed pending token.
|
|
24
|
+
// It cannot reject any input, so retaining rollback state is unnecessary.
|
|
25
|
+
begin: drafts => { if (drafts > 0) cache.specRoundBegin(); },
|
|
26
|
+
resolve(accepted: readonly number[], drafts: number) {
|
|
27
|
+
if (drafts === 0) return;
|
|
28
|
+
if (accepted.every(count => count === drafts)) cache.specRoundCommit();
|
|
29
|
+
else cache.specRoundRollback(accepted.every(count => count === accepted[0])
|
|
30
|
+
? accepted[0]! + 1 : accepted.map(count => count + 1));
|
|
31
|
+
},
|
|
32
|
+
})), (accepted, drafts) => accepted.length === rows &&
|
|
33
|
+
accepted.every(count => Number.isSafeInteger(count) && count >= 0 && count <= drafts));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bind the recurrent transaction or the trim implementation once.
|
|
5
37
|
* Ring-capacity checks still happen at each safe boundary before any writes. */
|
|
6
38
|
export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransaction {
|
|
7
39
|
const layers = caches.map((cache) => {
|
|
@@ -24,6 +56,15 @@ export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransact
|
|
|
24
56
|
},
|
|
25
57
|
};
|
|
26
58
|
});
|
|
59
|
+
return bindTransaction(layers, (accepted, drafts) =>
|
|
60
|
+
Number.isSafeInteger(accepted) && accepted >= 0 && accepted <= drafts);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One transaction lifecycle for scalar and row-wise state bindings. */
|
|
64
|
+
function bindTransaction<Acceptance>(
|
|
65
|
+
layers: readonly TransactionLayer<Acceptance>[],
|
|
66
|
+
accepts: (accepted: Acceptance, drafts: number) => boolean,
|
|
67
|
+
): SpeculativeTransaction<Acceptance> {
|
|
27
68
|
let active: number | undefined;
|
|
28
69
|
let invalid = false;
|
|
29
70
|
const validate = (drafts: number) => {
|
|
@@ -37,12 +78,12 @@ export function bindCacheRollback(caches: readonly Cache[]): SpeculativeTransact
|
|
|
37
78
|
if (active !== undefined || !layers.every((layer) => layer.fits(drafts)))
|
|
38
79
|
throw new Error("speculative state cannot begin this round");
|
|
39
80
|
active = drafts;
|
|
40
|
-
try { for (const layer of layers) layer.begin(); }
|
|
81
|
+
try { for (const layer of layers) layer.begin(drafts); }
|
|
41
82
|
catch (error) { invalid = true; throw error; }
|
|
42
83
|
},
|
|
43
84
|
resolve(accepted) {
|
|
44
|
-
|
|
45
|
-
if (active === undefined || accepted
|
|
85
|
+
if (invalid) throw new Error("speculative state is invalid and must be discarded");
|
|
86
|
+
if (active === undefined || !accepts(accepted, active)) throw new Error("invalid speculative acceptance count");
|
|
46
87
|
try { for (const layer of layers) layer.resolve(accepted, active); }
|
|
47
88
|
catch (error) { invalid = true; throw error; }
|
|
48
89
|
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,31 @@
|
|
|
1
|
+
import { pagedPrefixNamespace } from "./request-state-policy";
|
|
2
|
+
import type { SpecRunServices } from "../../spec/serve-loop";
|
|
1
3
|
import { cleanupFailure, disposeResources, ownResource } from "../../engine/resources";
|
|
2
4
|
import { bindGeneration } from "../../generate";
|
|
3
5
|
import type { RuntimeModel } from "../../model/factory";
|
|
4
6
|
import { Qwen35Model } from "../../model/qwen3_5";
|
|
5
7
|
import type { Cache } from "../../model/gemma4";
|
|
6
|
-
import type {
|
|
8
|
+
import type { MlxPrefixCache } from "./checkpoint-state";
|
|
7
9
|
import type { SsdCacheStore } from "../../ssd-cache";
|
|
8
10
|
import type { DraftProvider } from "../../spec/source";
|
|
9
11
|
import type { SerialRun, Vision } from "../../serve/generation-gateway";
|
|
10
|
-
import {
|
|
12
|
+
import { bindContinuationPolicy } from "./continuation";
|
|
11
13
|
import { runtimeConfig, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
12
14
|
import { bindLegacySpeculativeModel } from "./speculative";
|
|
13
|
-
import { enterMlxMemoryGuard, type MlxMemoryBudget } from "./memory-guard";
|
|
14
15
|
|
|
15
16
|
/** Native serial execution depends on this bound port, never a model union.
|
|
16
17
|
* Weights remain borrowed. The gateway supplies the exclusive runtime lease. */
|
|
17
18
|
export interface MlxSerialBinding {
|
|
18
19
|
readonly runtime: RuntimeConfig;
|
|
19
20
|
readonly generate: ReturnType<typeof bindGeneration>;
|
|
20
|
-
readonly speculate?:
|
|
21
|
+
readonly speculate?: (
|
|
22
|
+
promptIds: Parameters<SerialRun>[0], options: Parameters<SerialRun>[1],
|
|
23
|
+
onToken: Parameters<SerialRun>[2], services?: SpecRunServices,
|
|
24
|
+
) => ReturnType<SerialRun>;
|
|
21
25
|
makeCache(): Cache[];
|
|
22
26
|
enterMedia?(vision?: Vision): () => void;
|
|
23
|
-
enterMemoryGuard?(budget: SerialMemoryBudget, promptTokens: number,
|
|
24
|
-
prefillChunkSize?: number): { check(): void; close(): void };
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
/** Admission's ceiling and the cache that can release memory under the GPU lease. */
|
|
28
|
-
export type SerialMemoryBudget = MlxMemoryBudget;
|
|
29
|
-
|
|
30
29
|
/** Family-specific context is bound once at the compatibility boundary. */
|
|
31
30
|
export function bindLegacySerialModel(
|
|
32
31
|
model: RuntimeModel,
|
|
@@ -37,13 +36,10 @@ export function bindLegacySerialModel(
|
|
|
37
36
|
runtime: runtimeConfig(),
|
|
38
37
|
generate: bindGeneration(model),
|
|
39
38
|
makeCache: model.makeCache.bind(model),
|
|
40
|
-
|
|
41
|
-
return enterMlxMemoryGuard(model, budget, promptTokens, prefillChunkSize);
|
|
42
|
-
},
|
|
43
|
-
...(speculative && draft ? { speculate: (async (prompt, options, onToken) => {
|
|
39
|
+
...(speculative && draft ? { speculate: (async (prompt, options, onToken, services) => {
|
|
44
40
|
const { specRun } = await import("../../spec/serve-loop");
|
|
45
|
-
return specRun(speculative, draft.numDraftTokens, prompt, options, onToken);
|
|
46
|
-
}) satisfies
|
|
41
|
+
return specRun(speculative, draft.numDraftTokens, prompt, options, onToken, services);
|
|
42
|
+
}) satisfies NonNullable<MlxSerialBinding["speculate"]> } : {}),
|
|
47
43
|
...(model instanceof Qwen35Model ? { enterMedia(vision?: Vision) {
|
|
48
44
|
const previous = model.mrope;
|
|
49
45
|
if (vision?.mrope) model.mrope = vision.mrope;
|
|
@@ -53,11 +49,11 @@ export function bindLegacySerialModel(
|
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
export interface MlxSerialServices {
|
|
56
|
-
readonly promptCache:
|
|
52
|
+
readonly promptCache: MlxPrefixCache & { readonly maxBytes?: number };
|
|
57
53
|
readonly checkpoints: Pick<SsdCacheStore, "findGenerationCheckpoint" | "restore" |
|
|
58
54
|
"storeGenerationCheckpoint" | "removeGenerationCheckpoints"> | null;
|
|
59
55
|
readonly checkpointEveryTokens?: number;
|
|
60
|
-
readonly
|
|
56
|
+
readonly checkpointPersistence?: import("./continuation-persistence").ContinuationPersistence;
|
|
61
57
|
/** Artifact, implementation, state ABI and codec identity captured at load. */
|
|
62
58
|
readonly identity: unknown;
|
|
63
59
|
adapterNamespace(adapters: string[]): string;
|
|
@@ -71,17 +67,25 @@ export interface MlxSerialServices {
|
|
|
71
67
|
export function createMlxSerialExecutor(binding: MlxSerialBinding, services: MlxSerialServices): SerialRun {
|
|
72
68
|
const { promptCache, checkpoints: ssdStore } = services;
|
|
73
69
|
return (promptIds, options, onToken, vision, trace, execution) => withRuntimeConfig(binding.runtime, async () => {
|
|
70
|
+
if (promptCache.reclaim) {
|
|
71
|
+
const publish = onToken;
|
|
72
|
+
let steps = 0;
|
|
73
|
+
onToken = (token, logprobs) => {
|
|
74
|
+
if (steps++ % 256 === 0) promptCache.reclaim!();
|
|
75
|
+
return publish(token, logprobs);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let releaseContinuation: (() => void) | undefined;
|
|
74
79
|
let caches: Cache[] = [];
|
|
75
80
|
let retain: (() => void) | undefined;
|
|
76
81
|
let closeMedia: (() => void) | undefined;
|
|
77
|
-
let memoryGuard: ReturnType<NonNullable<MlxSerialBinding["enterMemoryGuard"]>> | undefined;
|
|
78
82
|
const cleanup = ownResource(null, () => disposeResources([
|
|
79
83
|
{ dispose() {
|
|
80
84
|
disposeResources(caches);
|
|
81
85
|
retain?.(); // backing release requires successful cache disposal
|
|
82
86
|
} },
|
|
83
87
|
{ dispose: () => closeMedia?.() },
|
|
84
|
-
{ dispose: () =>
|
|
88
|
+
{ dispose: () => releaseContinuation?.() },
|
|
85
89
|
...[options.grammar, vision?.embeddings, vision?.imageMask,
|
|
86
90
|
vision?.multimodalMask, options.visionPixels].filter((value) => value != null),
|
|
87
91
|
]));
|
|
@@ -90,53 +94,48 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
90
94
|
if (!execution) throw new Error("serial execution requires a resolved plan");
|
|
91
95
|
if (execution.method === "speculative") {
|
|
92
96
|
if (!binding.speculate) throw new Error("resolved speculation requires a bound verifier");
|
|
93
|
-
return await binding.speculate(promptIds, {
|
|
94
|
-
...
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
return await binding.speculate(promptIds, options, onToken, {
|
|
98
|
+
...(binding.runtime.flag("MLX_BUN_MTP_PROMPT_CACHE", true) && services.promptCache.maxBytes !== 0
|
|
99
|
+
? { prefixCache: services.promptCache } : {}),
|
|
100
|
+
cloneState: services.cloneState,
|
|
101
|
+
cacheNamespace: options.adapters?.length ? services.adapterNamespace(options.adapters) : "",
|
|
102
|
+
});
|
|
98
103
|
}
|
|
99
104
|
// Cache entries are adapter-specific: KV computed under one adapter
|
|
100
105
|
// must never seed another's (or the base's) prefill.
|
|
101
|
-
const
|
|
106
|
+
const baseNs = options.adapters?.length ? services.adapterNamespace(options.adapters) : "";
|
|
107
|
+
const cacheNs = execution.pagedKv ? pagedPrefixNamespace(options, baseNs) : baseNs;
|
|
102
108
|
// Paged-KV request scope (docs/design/kv-cache.md): media
|
|
103
109
|
// prompts (bidir overlay) and LoRA-adapter requests are v1 non-goals —
|
|
104
110
|
// they run the PLAIN cache path even under --paged-kv (scope the flag
|
|
105
111
|
// per request, never 400). Effective value computed ONCE so the
|
|
106
112
|
// prompt-cache bypass below and the generate() options can't disagree.
|
|
107
113
|
const pagedKv = execution.pagedKv ? options.pagedKv : undefined;
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// precedent). Fresh caches per request, disposed on completion.
|
|
114
|
+
// The resolved policy controls reuse; paged layouts have their own
|
|
115
|
+
// numerical namespace and use the same RAM/SSD cache interface.
|
|
111
116
|
const skipPromptCache = !execution.promptCache;
|
|
112
117
|
const checkpointEvery = services.checkpointEveryTokens;
|
|
113
118
|
const checkpointEligible = execution.checkpoint;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
: null;
|
|
119
|
+
const continuation = checkpointEligible ? bindContinuationPolicy({ store: ssdStore!,
|
|
120
|
+
restore: entry => ssdStore!.restore(entry, binding), prompt: promptIds,
|
|
121
|
+
options, execution, identity: services.identity, namespace: cacheNs, persistence: services.checkpointPersistence }) : null;
|
|
122
|
+
releaseContinuation = continuation?.release;
|
|
123
|
+
const checkpointKey = continuation?.key;
|
|
117
124
|
// Both tiers in one call (Layer 0): take() prefers a strictly-longer
|
|
118
125
|
// SSD prefix, restores it zero-copy, and trims — see PromptCache.take.
|
|
119
126
|
const closeCacheLookup = trace?.begin("cache.lookup_restore", {
|
|
120
127
|
mechanism: "serial",
|
|
121
128
|
bypassed: skipPromptCache,
|
|
122
129
|
});
|
|
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;
|
|
130
|
+
const checkpoint = continuation?.restore();
|
|
131
|
+
caches = checkpoint?.caches ?? [];
|
|
132
|
+
const resuming = Boolean(checkpoint);
|
|
133
|
+
const generationPromptIds = checkpoint?.cacheTokens ?? promptIds;
|
|
135
134
|
const entry = skipPromptCache || resuming
|
|
136
135
|
? null
|
|
137
|
-
: promptCache.take(promptIds, cacheNs);
|
|
136
|
+
: promptCache.take(promptIds, cacheNs, options.cacheSessionId);
|
|
138
137
|
if (entry) { caches = entry.caches; retain = entry.retain; }
|
|
139
|
-
if (!
|
|
138
|
+
if (!checkpoint && !entry) caches = binding.makeCache();
|
|
140
139
|
closeCacheLookup?.();
|
|
141
140
|
// Prompt-boundary snapshot (the multi-turn agent fix, 2026-07-04): the
|
|
142
141
|
// prompt+gen entry put() below is UNTRIMMABLE at context > sliding
|
|
@@ -160,12 +159,6 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
160
159
|
const snapshotBoundary =
|
|
161
160
|
!skipPromptCache && !resuming && boundary >= 256 &&
|
|
162
161
|
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
162
|
closeMedia = binding.enterMedia?.(vision);
|
|
170
163
|
if (resuming) {
|
|
171
164
|
const replay = generationPromptIds.slice(promptIds.length);
|
|
@@ -190,7 +183,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
190
183
|
? {
|
|
191
184
|
initialPendingToken: checkpoint!.pendingToken,
|
|
192
185
|
initialGeneratedTokens: checkpoint!.generatedTokens,
|
|
193
|
-
originalPromptTokens:
|
|
186
|
+
originalPromptTokens: promptIds.length,
|
|
194
187
|
}
|
|
195
188
|
: {}),
|
|
196
189
|
...(checkpointEligible
|
|
@@ -202,23 +195,9 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
202
195
|
generatedTokens: number;
|
|
203
196
|
pendingToken: number;
|
|
204
197
|
}) => {
|
|
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
|
-
);
|
|
198
|
+
if (services.checkpointPersistence)
|
|
199
|
+
continuation!.captureOwned({ ...state, caches: services.cloneState(state.caches) });
|
|
200
|
+
else await continuation!.capture(state);
|
|
222
201
|
},
|
|
223
202
|
}
|
|
224
203
|
: {}),
|
|
@@ -234,7 +213,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
234
213
|
try {
|
|
235
214
|
const snapshot = ownResource(services.cloneState(caches), disposeResources);
|
|
236
215
|
try {
|
|
237
|
-
promptCache.put(promptIds.slice(0, boundary), snapshot.borrow(), cacheNs);
|
|
216
|
+
promptCache.put(promptIds.slice(0, boundary), snapshot.borrow(), cacheNs, undefined, undefined, options.cacheSessionId);
|
|
238
217
|
snapshot.transfer();
|
|
239
218
|
} finally { snapshot.close(); }
|
|
240
219
|
} catch (e) {
|
|
@@ -252,15 +231,14 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
252
231
|
: {}),
|
|
253
232
|
}, { trace, mechanism: "serial" });
|
|
254
233
|
for await (const t of gen) {
|
|
255
|
-
if (t.index % 256 === 0) memoryGuard?.check();
|
|
256
234
|
if ((await onToken(t.token, t.logprobs)) === false) break;
|
|
257
235
|
}
|
|
258
236
|
const s = gen.stats!; // set on completion AND on early break
|
|
259
|
-
|
|
237
|
+
continuation?.complete();
|
|
260
238
|
if (!skipPromptCache) {
|
|
261
239
|
// put() fires onPut → the debounced write-behind SSD snapshot
|
|
262
240
|
// (wired below), covering the batch lane's puts too.
|
|
263
|
-
promptCache.put(s.cacheTokens, caches, cacheNs, retain);
|
|
241
|
+
promptCache.put(s.cacheTokens, caches, cacheNs, retain, undefined, options.cacheSessionId);
|
|
264
242
|
caches = []; retain = undefined; // ownership returned to the prefix store
|
|
265
243
|
}
|
|
266
244
|
return s;
|