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
package/src/spec/serve-loop.ts
CHANGED
|
@@ -17,11 +17,9 @@
|
|
|
17
17
|
// trimmable mid-generation; a serve endpoint must not 500 mid-stream, so
|
|
18
18
|
// we STOP SPECULATING and finish the generation with plain single-token
|
|
19
19
|
// decode (bit-equivalent continuation — the target's own samples).
|
|
20
|
-
// -
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// SSD tier is a tracked follow-up in the integration plan, not silently
|
|
24
|
-
// absent.
|
|
20
|
+
// - Sources with checkpoint support use the injected prefix-cache service.
|
|
21
|
+
// Target and companion state share the existing RAM/SSD lifecycle. Other
|
|
22
|
+
// sources begin with fresh state until they supply a checkpoint codec.
|
|
25
23
|
//
|
|
26
24
|
// Grammar × spec (Phase C, the constrained verify walk — novel: NO runtime
|
|
27
25
|
// serves both; mlx-lm has no grammar, oMLX no spec): the drafter runs FREE
|
|
@@ -40,14 +38,18 @@
|
|
|
40
38
|
// false halts the generation mid-burst.
|
|
41
39
|
|
|
42
40
|
import { MlxArray } from "../mlx/array";
|
|
41
|
+
import { sampleSpeculativeRows } from "../backends/mlx/speculative-round";
|
|
43
42
|
import * as ops from "../mlx/ops";
|
|
44
43
|
import { clearCache } from "../mlx/ffi";
|
|
45
44
|
import { cleanupFailure, disposeResources } from "../engine/resources";
|
|
46
45
|
import type { DisposableResource } from "../contracts/resources";
|
|
47
46
|
import { assertMlxSpeculativeBinding, bindLegacySpeculativeModel, type MlxSpeculativeBinding } from "../backends/mlx/speculative";
|
|
48
47
|
import type { RuntimeModel } from "../model/factory";
|
|
48
|
+
import { cloneKvCaches } from "../kv-store";
|
|
49
|
+
import { disposeAttachments, type CheckpointAttachment } from "../backends/mlx/checkpoint-state";
|
|
49
50
|
import type { Cache } from "../model/gemma4";
|
|
50
|
-
import {
|
|
51
|
+
import { withModelUsageFlush, withModelWiredLimit, type GenerateOptions, type GenerateStats } from "../generate";
|
|
52
|
+
import { createKvMaintenance } from "../backends/mlx/kv-maintenance";
|
|
51
53
|
import { runtimeConfig, withRuntimeConfig } from "../runtime-config";
|
|
52
54
|
import { makeSampler, makeStepSampler } from "../sampler";
|
|
53
55
|
import type { OnToken } from "../serve/generation-gateway";
|
|
@@ -72,6 +74,13 @@ export interface SpecServeExtras {
|
|
|
72
74
|
acceptedByPos: number[];
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export interface SpecRunServices {
|
|
78
|
+
readonly prefixCache?: import("../backends/mlx/checkpoint-state").MlxPrefixCache;
|
|
79
|
+
/** Mounted adapter revision, resolved while the execution lease is held. */
|
|
80
|
+
readonly cacheNamespace?: string;
|
|
81
|
+
readonly cloneState?: (caches: Cache[]) => Cache[];
|
|
82
|
+
}
|
|
83
|
+
|
|
75
84
|
export async function specServeRun(
|
|
76
85
|
model: RuntimeModel,
|
|
77
86
|
provider: DraftProvider,
|
|
@@ -79,8 +88,9 @@ export async function specServeRun(
|
|
|
79
88
|
promptIds: number[],
|
|
80
89
|
options: GenerateOptions & { stopSequences?: string[] },
|
|
81
90
|
onToken: OnToken,
|
|
91
|
+
services: SpecRunServices = {},
|
|
82
92
|
): Promise<GenerateStats> {
|
|
83
|
-
return specRun(bindLegacySpeculativeModel(model, provider), numDraftTokens, promptIds, options, onToken);
|
|
93
|
+
return specRun(bindLegacySpeculativeModel(model, provider), numDraftTokens, promptIds, options, onToken, services);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/** The verifier consumes a bound target and source factory, independent of
|
|
@@ -91,9 +101,10 @@ export async function specRun(
|
|
|
91
101
|
promptIds: number[],
|
|
92
102
|
options: GenerateOptions & { stopSequences?: string[] },
|
|
93
103
|
onToken: OnToken,
|
|
104
|
+
services: SpecRunServices = {},
|
|
94
105
|
): Promise<GenerateStats> {
|
|
95
106
|
return withRuntimeConfig(binding.runtime ?? runtimeConfig(), () => {
|
|
96
|
-
const run = () => specRunInner(binding, numDraftTokens, promptIds, options, onToken);
|
|
107
|
+
const run = () => specRunInner(binding, numDraftTokens, promptIds, options, onToken, services);
|
|
97
108
|
return binding.memory
|
|
98
109
|
? withModelWiredLimit(binding.memory, () => withModelUsageFlush(binding.memory!, run))
|
|
99
110
|
: run();
|
|
@@ -106,8 +117,10 @@ async function specRunInner(
|
|
|
106
117
|
promptIds: number[],
|
|
107
118
|
options: GenerateOptions & { stopSequences?: string[] },
|
|
108
119
|
onToken: OnToken,
|
|
120
|
+
services: SpecRunServices = {},
|
|
109
121
|
): Promise<GenerateStats> {
|
|
110
122
|
const maxTokens = options.maxTokens ?? 512;
|
|
123
|
+
const maintainKv = createKvMaintenance(options);
|
|
111
124
|
const eos = options.eosTokenIds ?? [...binding.eosTokenIds];
|
|
112
125
|
const sampler = makeSampler(options);
|
|
113
126
|
const stepSampler = makeStepSampler(options, {
|
|
@@ -120,7 +133,9 @@ async function specRunInner(
|
|
|
120
133
|
sampler,
|
|
121
134
|
});
|
|
122
135
|
const gamma = Math.max(1, numDraftTokens);
|
|
136
|
+
const trace = runtimeConfig().value("MLX_BUN_SPEC_TRACE") === "1";
|
|
123
137
|
const prefillChunk = options.prefillChunkSize ??
|
|
138
|
+
binding.prefillPolicy?.chunkSize(promptIds.length) ??
|
|
124
139
|
runtimeConfig().number("MLX_BUN_RD_PREFILL_CHUNK", PREFILL_CHUNK);
|
|
125
140
|
|
|
126
141
|
// Allocated INSIDE the try below (2026-07-07 review): provider.open()
|
|
@@ -129,6 +144,7 @@ async function specRunInner(
|
|
|
129
144
|
// and turned a config error into a per-request 500. loadContext now
|
|
130
145
|
// probe-opens the pairing at startup, so a throw here is belt+suspenders.
|
|
131
146
|
let caches: Cache[] = [];
|
|
147
|
+
let prefixRetain: (() => void) | undefined;
|
|
132
148
|
let source: import("./source").DraftSource | null = null;
|
|
133
149
|
let tapLayers: number[] | undefined;
|
|
134
150
|
// Target final hidden [1,1,H] at the anchor position — the assistant source
|
|
@@ -175,16 +191,6 @@ async function specRunInner(
|
|
|
175
191
|
const samplePos = async (logits1V: MlxArray, step: number): Promise<number> =>
|
|
176
192
|
(await stepSampler.sample(logits1V, step)).token;
|
|
177
193
|
|
|
178
|
-
/** The [1,V] logits row at position `pos` of a hidden window — batched
|
|
179
|
-
* lm-head is applied by the caller ONCE; this slices its output. */
|
|
180
|
-
const logitsRow = (logitsWindow: MlxArray, pos: number): MlxArray => {
|
|
181
|
-
const V = logitsWindow.shape[logitsWindow.shape.length - 1]!;
|
|
182
|
-
const sl = logitsWindow.slice([0, pos, 0], [1, pos + 1, V]);
|
|
183
|
-
const flat = ops.reshape(sl, [1, V]);
|
|
184
|
-
sl.dispose();
|
|
185
|
-
return flat;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
194
|
const forwardTokens = async (tokens: number[], taps?: number[]) => {
|
|
189
195
|
const ids = ops.fromInt32(tokens, [1, tokens.length]);
|
|
190
196
|
try { return await binding.forward(ids, caches, taps); }
|
|
@@ -197,25 +203,50 @@ async function specRunInner(
|
|
|
197
203
|
if (!Number.isSafeInteger(prefillChunk) || prefillChunk < 1)
|
|
198
204
|
throw new Error("speculative prefill chunk must be a positive integer");
|
|
199
205
|
assertMlxSpeculativeBinding(binding);
|
|
200
|
-
if (options.
|
|
201
|
-
(options.
|
|
202
|
-
|
|
206
|
+
if (options.kvConfig?.length ||
|
|
207
|
+
(options.turboQuant && (options.quantizedKvStart ?? 0) !== 0) ||
|
|
208
|
+
(options.kvBits && ((options.kvBits !== 4 && options.kvBits !== 8) || options.quantizedKvStart !== 0)))
|
|
209
|
+
throw new Error("speculative KV requires uniform KV4/KV8 or TurboQuant start=0");
|
|
203
210
|
caches = binding.makeCache();
|
|
204
211
|
const src = binding.openDraft(sampler, caches);
|
|
205
212
|
source = src;
|
|
206
213
|
tapLayers = src.tapLayers;
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const prefixNamespace = JSON.stringify({
|
|
210
|
-
|
|
211
|
-
quantizedKvStart: options.quantizedKvStart ?? null
|
|
212
|
-
|
|
214
|
+
const checkpoint = src.prefillMode === "full" ? src.checkpoint : undefined;
|
|
215
|
+
const prefixCache = checkpoint ? services.prefixCache : undefined;
|
|
216
|
+
const prefixNamespace = JSON.stringify({ method: checkpoint?.namespace,
|
|
217
|
+
adapters: services.cacheNamespace ?? options.adapters ?? [], kvBits: options.kvBits ?? null,
|
|
218
|
+
kvGroupSize: options.kvGroupSize ?? 64, quantizedKvStart: options.quantizedKvStart ?? null,
|
|
219
|
+
turboQuant: options.turboQuant ?? null });
|
|
220
|
+
let cached = 0;
|
|
221
|
+
const hit = prefixCache?.take(promptIds, prefixNamespace, options.cacheSessionId);
|
|
222
|
+
if (hit) {
|
|
223
|
+
try {
|
|
224
|
+
disposeResources(caches);
|
|
225
|
+
caches.splice(0, caches.length, ...hit.caches.splice(0));
|
|
226
|
+
prefixRetain = hit.retain;
|
|
227
|
+
hit.retain = undefined;
|
|
228
|
+
checkpoint!.restore(hit.tokens.length, hit.attachments![0]!);
|
|
229
|
+
cached = hit.tokens.length;
|
|
230
|
+
} finally {
|
|
231
|
+
disposeResources([...hit.caches, { dispose: () => disposeAttachments(hit.attachments) },
|
|
232
|
+
{ dispose: () => hit.retain?.() }]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
213
235
|
stats.cachedTokens = cached;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
:
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
const capture = (tokens: number[]) => {
|
|
237
|
+
let state: Cache[] = [];
|
|
238
|
+
let attachments: CheckpointAttachment[] = [];
|
|
239
|
+
try {
|
|
240
|
+
state = (services.cloneState ?? cloneKvCaches)(caches);
|
|
241
|
+
attachments.push(checkpoint!.capture(tokens.length));
|
|
242
|
+
prefixCache!.put(tokens, state, prefixNamespace, undefined, attachments, options.cacheSessionId);
|
|
243
|
+
state = []; attachments = [];
|
|
244
|
+
} finally {
|
|
245
|
+
disposeResources([...state, { dispose: () => disposeAttachments(attachments) }]);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const snapshotAt = prefixCache
|
|
249
|
+
? Math.min(options.snapshotAt ?? promptIds.length, promptIds.length - 1) : -1;
|
|
219
250
|
const seedSource = async (end: number) => {
|
|
220
251
|
if (ctxParts.length === 1) prefillCtx = ctxParts[0]!;
|
|
221
252
|
else if (ctxParts.length > 1) {
|
|
@@ -275,7 +306,7 @@ async function specRunInner(
|
|
|
275
306
|
anchorHidden = h.slice([0, L - 1, 0], [1, L, H]); // position len-2
|
|
276
307
|
}
|
|
277
308
|
} finally { h.dispose(); } // no logits during the drain
|
|
278
|
-
|
|
309
|
+
maintainKv(caches);
|
|
279
310
|
clearCache();
|
|
280
311
|
pos += n;
|
|
281
312
|
if (pos < end) await new Promise<void>((resolve) => setImmediate(resolve));
|
|
@@ -300,10 +331,10 @@ async function specRunInner(
|
|
|
300
331
|
lastLogits = ops.reshape(lg, [1, V]);
|
|
301
332
|
} finally { lg.dispose(); }
|
|
302
333
|
} } finally { h.dispose(); }
|
|
303
|
-
|
|
334
|
+
maintainKv(caches);
|
|
304
335
|
if (end === snapshotAt && snapshotAt > cached) {
|
|
305
336
|
await seedSource(end);
|
|
306
|
-
|
|
337
|
+
capture(promptIds.slice(0, end));
|
|
307
338
|
}
|
|
308
339
|
clearCache();
|
|
309
340
|
off = end;
|
|
@@ -416,7 +447,7 @@ async function specRunInner(
|
|
|
416
447
|
);
|
|
417
448
|
const d = drafts.length;
|
|
418
449
|
if (d < 0 || d > n) throw new Error(`DraftSource returned ${d} drafts (contract: 0..${n})`);
|
|
419
|
-
if (
|
|
450
|
+
if (trace) {
|
|
420
451
|
console.error(
|
|
421
452
|
`[SPEC_TRACE] round=${extras.rounds + 1} ` +
|
|
422
453
|
`feed=${feed.join(",")} drafts=${drafts.join(",")}`,
|
|
@@ -460,39 +491,12 @@ async function specRunInner(
|
|
|
460
491
|
// accept while it reproduces the draft. Sampling is sequential because
|
|
461
492
|
// the processor history (and, in Phase C, the grammar mask) at position
|
|
462
493
|
// i depends on the tokens accepted at positions < i.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
494
|
+
const [result] = await sampleSpeculativeRows([{
|
|
495
|
+
pending, step: stats.generatedTokens, remaining: maxTokens - stats.generatedTokens,
|
|
496
|
+
eosTokenIds: eos, sampling: stepSampler, grammarDone: () => grammar?.isTerminated ?? false,
|
|
497
|
+
}], [drafts], vLogits!);
|
|
498
|
+
const { accepted: kAccept, correction, sawEos, emitted, grammarDone } = result!.acceptance;
|
|
466
499
|
let halted = false;
|
|
467
|
-
const emitted: number[] = [];
|
|
468
|
-
let grammarDone = false;
|
|
469
|
-
for (let i = 0; i <= d; i++) {
|
|
470
|
-
roundRow = logitsRow(vLogits!, i);
|
|
471
|
-
const tok = await samplePos(roundRow, stats.generatedTokens + emitted.length);
|
|
472
|
-
roundRow.dispose();
|
|
473
|
-
roundRow = null;
|
|
474
|
-
if (i < d && tok === drafts[i]) {
|
|
475
|
-
kAccept++;
|
|
476
|
-
// EOS is never content — even when it arrives as an ACCEPTED DRAFT
|
|
477
|
-
// (the correction/bonus branch below always excluded it; this
|
|
478
|
-
// branch pushed-then-broke, leaking the EOS through onToken —
|
|
479
|
-
// caught by the 2026-07-07 live oracle gate: streams were
|
|
480
|
-
// bit-identical, ours emitted one extra "content" token: <|eot_id|>).
|
|
481
|
-
if (eos.includes(tok)) { sawEos = true; break; }
|
|
482
|
-
emitted.push(tok);
|
|
483
|
-
// grammar termination mid-burst truncates the round — nothing may
|
|
484
|
-
// be sampled past a satisfied grammar (the all--inf guarantee).
|
|
485
|
-
if (grammar?.isTerminated) { grammarDone = true; break; }
|
|
486
|
-
if (stats.generatedTokens + emitted.length >= maxTokens) break;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
// mismatch (target replaces the draft) or bonus (i === n)
|
|
490
|
-
correction = tok;
|
|
491
|
-
if (!eos.includes(tok)) emitted.push(tok);
|
|
492
|
-
else sawEos = true;
|
|
493
|
-
if (grammar?.isTerminated) grammarDone = true;
|
|
494
|
-
break;
|
|
495
|
-
}
|
|
496
500
|
vLogits!.dispose();
|
|
497
501
|
vLogits = null;
|
|
498
502
|
extras.accepted += kAccept;
|
|
@@ -575,7 +579,7 @@ async function specRunInner(
|
|
|
575
579
|
} finally {
|
|
576
580
|
const resources = [anchorHidden, lastLogits, prefillCtx, ...ctxParts,
|
|
577
581
|
vLogits, roundRow, vHidden, vCtxML, ...caches, source, stepSampler,
|
|
578
|
-
options.grammar, { dispose: clearCache }]
|
|
582
|
+
options.grammar, { dispose: () => prefixRetain?.() }, { dispose: clearCache }]
|
|
579
583
|
.filter((resource): resource is DisposableResource => resource != null);
|
|
580
584
|
if (failure) cleanupFailure(failure.error, () => disposeResources(resources));
|
|
581
585
|
else disposeResources(resources);
|
package/src/spec/source.ts
CHANGED
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
import type { MlxArray } from "../mlx/array";
|
|
24
24
|
import type { Dtype } from "../mlx/ffi";
|
|
25
25
|
import type { Cache } from "../model/gemma4";
|
|
26
|
+
import type { CheckpointAttachment } from "../backends/mlx/checkpoint-state";
|
|
27
|
+
import type { MlxDraftRows } from "../backends/mlx/speculative-round";
|
|
28
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
26
29
|
|
|
27
30
|
/** Numerical ports are backend-specific; returned arrays are caller-owned. */
|
|
28
31
|
export interface DraftProjection {
|
|
@@ -33,11 +36,14 @@ export interface DraftProjection {
|
|
|
33
36
|
logitsFromHidden(hidden: MlxArray): MlxArray;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
readDonors():
|
|
39
|
+
/** Read-only target state and embedding, independent of draft scheduling. */
|
|
40
|
+
export interface AssistantRowsTarget {
|
|
41
|
+
readonly hiddenSize: number;
|
|
42
|
+
embed(ids: MlxArray): MlxArray;
|
|
43
|
+
readDonors(): import("./drafter").AssistantDonors & {
|
|
44
|
+
readonly positions: readonly number[];
|
|
45
|
+
dispose(): void;
|
|
46
|
+
};
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
export interface QwenMtpTarget {
|
|
@@ -53,7 +59,7 @@ export interface QwenMtpTarget {
|
|
|
53
59
|
export interface TargetView {
|
|
54
60
|
/** Opaque identity for providers borrowing weights from one exact target. */
|
|
55
61
|
readonly identity: object;
|
|
56
|
-
readonly
|
|
62
|
+
readonly assistantRows?: AssistantRowsTarget;
|
|
57
63
|
readonly gemmaTaps?: { readonly layerCount: number; readonly projection: DraftProjection };
|
|
58
64
|
readonly qwenMtp?: QwenMtpTarget;
|
|
59
65
|
}
|
|
@@ -61,12 +67,13 @@ export interface TargetView {
|
|
|
61
67
|
/** A per-request draft-token producer. Created per generation (owns its own
|
|
62
68
|
* draft-side state), disposed by the serve loop's finally. */
|
|
63
69
|
export interface DraftSource {
|
|
64
|
-
/**
|
|
65
|
-
*
|
|
66
|
-
* the source
|
|
67
|
-
readonly
|
|
68
|
-
|
|
69
|
-
capture(
|
|
70
|
+
/** Method-owned companion state, independent of retention and storage.
|
|
71
|
+
* Capture returns owned immutable views. Restore borrows its attachment;
|
|
72
|
+
* the source retains the views it needs before returning. */
|
|
73
|
+
readonly checkpoint?: {
|
|
74
|
+
readonly namespace: string;
|
|
75
|
+
capture(processedTokens: number): import("../backends/mlx/checkpoint-state").CheckpointAttachment;
|
|
76
|
+
restore(processedTokens: number, attachment: import("../backends/mlx/checkpoint-state").CheckpointAttachment): void;
|
|
70
77
|
};
|
|
71
78
|
/** Target prefill shape required by this source's oracle. Most mlx-lm
|
|
72
79
|
* sources leave the final prompt token pending; native Colibri MTP starts
|
|
@@ -86,8 +93,9 @@ export interface DraftSource {
|
|
|
86
93
|
/** Process the prompt (two-model: prefill the draft model's cache;
|
|
87
94
|
* assistant/dflash: read the target's state — mostly a no-op, but DSpark
|
|
88
95
|
* seeds H_ctx from `ctxML`, the tapped prefill context [1,Lp,m*H], present
|
|
89
|
-
* iff tapLayers is set).
|
|
90
|
-
|
|
96
|
+
* iff tapLayers is set). A composed prefill may supply its exact processed
|
|
97
|
+
* token count, so a history source need not infer the target's boundary. */
|
|
98
|
+
prefill(promptIds: number[], ctxML?: MlxArray, processedTokens?: number): void | Promise<void>;
|
|
91
99
|
|
|
92
100
|
/** Propose 0..n tokens (RETURN LENGTH IS AUTHORITATIVE — a source may
|
|
93
101
|
* return fewer than n, e.g. DSpark's confidence-scheduled draft-length
|
|
@@ -132,12 +140,82 @@ export interface DraftSource {
|
|
|
132
140
|
dispose(): void;
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
export interface DraftRowSampling {
|
|
144
|
+
/** Borrow [B,V] log-probabilities; return owned [B] IDs on device. */
|
|
145
|
+
sample(logprobs: MlxArray, steps: readonly number[]): MlxArray;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Request-owned constraints can propose known continuations without changing
|
|
149
|
+
* their committed state. The target verifier still decides every output. */
|
|
150
|
+
export interface DraftRowConstraints {
|
|
151
|
+
propose(row: number, maxTokens: number): Promise<number[]>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface DraftRowCheckpoint {
|
|
155
|
+
readonly processedTokens: number;
|
|
156
|
+
readonly attachment: CheckpointAttachment;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Method-owned state membership, independent of scheduling and cache tiers.
|
|
160
|
+
* Open/append borrow checkpoints; capture returns owned immutable state.
|
|
161
|
+
* Membership changes and capture occur only between committed rounds.
|
|
162
|
+
* The caller retains the provider's residency lease for the group's lifetime. */
|
|
163
|
+
export interface DraftRowGroup extends MlxDraftRows {
|
|
164
|
+
/** Consume externally chosen target inputs at a committed round boundary.
|
|
165
|
+
* Context and tokens are borrowed rectangular windows; lengths retain each
|
|
166
|
+
* row's consumed prefix, excluding right padding and the pending output. */
|
|
167
|
+
consume?(tokens: MlxArray, context: MlxArray, lengths: readonly number[]): void | Promise<void>;
|
|
168
|
+
readonly namespace: string;
|
|
169
|
+
readonly tapLayers: readonly number[];
|
|
170
|
+
readonly rowCount: number;
|
|
171
|
+
append(checkpoints: readonly DraftRowCheckpoint[]): void;
|
|
172
|
+
prepareAppend(checkpoints: readonly DraftRowCheckpoint[]): PreparedStateChange;
|
|
173
|
+
filterRows(keep: readonly number[]): void;
|
|
174
|
+
capture(row: number): DraftRowCheckpoint;
|
|
175
|
+
dispose(): void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Draft preparation consumes each target chunk at the same batch geometry.
|
|
179
|
+
* Inputs are borrowed and contain only new tokens/context, with equal chunk
|
|
180
|
+
* lengths across rows. A null checkpoint appends a cold request. The provider
|
|
181
|
+
* owns alignment and retained hidden state; scheduling and persistence do not.
|
|
182
|
+
* Membership and capture occur between prefill calls. Capture returns owned
|
|
183
|
+
* state after at least one target token has been consumed for that row. */
|
|
184
|
+
export interface DraftPrefillGroup {
|
|
185
|
+
readonly namespace: string;
|
|
186
|
+
readonly prefillMode: NonNullable<DraftSource["prefillMode"]>;
|
|
187
|
+
readonly tapLayers: readonly number[];
|
|
188
|
+
readonly rowCount: number;
|
|
189
|
+
append(checkpoints: readonly (DraftRowCheckpoint | null)[]): void;
|
|
190
|
+
prepareAppend(checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange;
|
|
191
|
+
prefill(tokens: MlxArray, context?: MlxArray): void | Promise<void>;
|
|
192
|
+
/** Resolve borrowed backing before its external restore lease is released. */
|
|
193
|
+
materialize(): void;
|
|
194
|
+
filterRows(keep: readonly number[]): void;
|
|
195
|
+
capture(row: number): DraftRowCheckpoint;
|
|
196
|
+
dispose(): void;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface GroupedDraftProvider {
|
|
200
|
+
readonly supportsExternalTokens?: boolean;
|
|
201
|
+
/** Resolve persistence identity without allocating a draft row. */
|
|
202
|
+
checkpointNamespace?(): string;
|
|
203
|
+
/** Draft state remains valid when target forwards run under a mounted
|
|
204
|
+
* adapter context. Unqualified learned draft graphs leave this absent. */
|
|
205
|
+
readonly supportsTargetAdapters?: boolean;
|
|
206
|
+
openPrefill(options: { target: TargetView;
|
|
207
|
+
checkpoints: readonly (DraftRowCheckpoint | null)[] }): DraftPrefillGroup;
|
|
208
|
+
open(options: { target: TargetView; sampling: DraftRowSampling; constraints?: DraftRowConstraints;
|
|
209
|
+
checkpoints: readonly DraftRowCheckpoint[] }): DraftRowGroup;
|
|
210
|
+
}
|
|
211
|
+
|
|
135
212
|
/** Server-lifetime owner of the draft machinery (the loaded draft model);
|
|
136
|
-
*
|
|
213
|
+
* open() mints a per-request DraftSource; grouped opens shared row state. */
|
|
137
214
|
export interface DraftProvider {
|
|
138
215
|
/** Human-readable id (registry id / path tail) for logs + cache namespacing. */
|
|
139
216
|
readonly id: string;
|
|
140
217
|
readonly weightsBytes: number;
|
|
218
|
+
readonly grouped?: GroupedDraftProvider;
|
|
141
219
|
open(opts: {
|
|
142
220
|
/** The request's sampler over logprobs [1,V] → token array [1] (the SAME
|
|
143
221
|
* sampler as the target — mlx-lm parity; greedy drafting under a
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MlxAutoregressiveBinding } from "../backends/mlx/autoregressive";
|
|
2
|
+
import { MlxAutoregressiveDraftRows, type MlxDraftLayout } from "../backends/mlx/autoregressive-draft-rows";
|
|
3
|
+
import { prefillCacheLayout } from "../backends/mlx/cache-layout";
|
|
4
|
+
import { MlxStateRows } from "../backends/mlx/state-rows";
|
|
5
|
+
import { captureCacheAttachment, restoreCacheAttachment } from "../backends/mlx/cache-attachment";
|
|
6
|
+
import { applyStateChanges, cleanupFailure, disposeResources } from "../engine/resources";
|
|
7
|
+
import type { Cache } from "../model/gemma4-base";
|
|
8
|
+
import type { DraftPrefillGroup, DraftRowCheckpoint, DraftRowGroup, DraftRowSampling, GroupedDraftProvider } from "./source";
|
|
9
|
+
import type { PreparedStateChange } from "../contracts/resources";
|
|
10
|
+
import type { MlxArray } from "../mlx/array";
|
|
11
|
+
|
|
12
|
+
export interface StandaloneDraftRows extends DraftPrefillGroup {
|
|
13
|
+
draft: DraftRowGroup["draft"];
|
|
14
|
+
commit(accepted: readonly number[], context?: MlxArray): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The standalone model supplies graph/state capabilities once. Target
|
|
18
|
+
* verification, scheduling and cache tiers use the existing provider ports. */
|
|
19
|
+
export function openStandaloneDraftRows(binding: MlxAutoregressiveBinding, namespace: string,
|
|
20
|
+
sampling: DraftRowSampling | null, checkpoints: readonly (DraftRowCheckpoint | null)[]): StandaloneDraftRows {
|
|
21
|
+
const empty = binding.makeCache(), layouts: MlxDraftLayout[] = [];
|
|
22
|
+
try { for (const cache of empty) layouts.push(prefillCacheLayout(cache) as MlxDraftLayout); }
|
|
23
|
+
catch (error) { disposeResources(layouts); throw error; }
|
|
24
|
+
finally { disposeResources(empty); }
|
|
25
|
+
const state = new MlxStateRows(layouts), rows = new MlxAutoregressiveDraftRows(binding.graph, state, sampling);
|
|
26
|
+
const prepareAppend = (checkpoints: readonly (DraftRowCheckpoint | null)[]): PreparedStateChange => {
|
|
27
|
+
const restored: Cache[][] = [], pending: Array<number | null> = [];
|
|
28
|
+
try {
|
|
29
|
+
for (const checkpoint of checkpoints) {
|
|
30
|
+
restored.push(checkpoint ? restoreCacheAttachment(checkpoint.attachment, binding.makeCache) : binding.makeCache());
|
|
31
|
+
pending.push(checkpoint && checkpoint.attachment.metadata.pendingLast !== false ? Number(checkpoint.attachment.metadata.pendingLast) : null);
|
|
32
|
+
}
|
|
33
|
+
return rows.prepareAppend(restored, pending);
|
|
34
|
+
} finally { disposeResources(restored.flat()); }
|
|
35
|
+
};
|
|
36
|
+
const append = (checkpoints: readonly (DraftRowCheckpoint | null)[]) => applyStateChanges([() => prepareAppend(checkpoints)]);
|
|
37
|
+
try { append(checkpoints); }
|
|
38
|
+
catch (error) { return cleanupFailure(error, () => rows.dispose()); }
|
|
39
|
+
return {
|
|
40
|
+
namespace, prefillMode: "tail-split", tapLayers: [],
|
|
41
|
+
get rowCount() { return state.rowCount; },
|
|
42
|
+
append, prepareAppend, prefill: tokens => rows.prefill(tokens), materialize: () => rows.materialize(),
|
|
43
|
+
filterRows: keep => rows.filterRows(keep), draft: rows.draft.bind(rows), commit: accepted => rows.commit(accepted),
|
|
44
|
+
capture(row): DraftRowCheckpoint {
|
|
45
|
+
const caches = state.extractRow(row), pendingLast = rows.pendingLast[row] ?? null;
|
|
46
|
+
try {
|
|
47
|
+
const processedTokens = caches[0]!.offset + Number(pendingLast !== null);
|
|
48
|
+
return { processedTokens, attachment: captureCacheAttachment("standalone-draft-v1", caches,
|
|
49
|
+
{ processedTokens, pendingLast: pendingLast ?? false }) };
|
|
50
|
+
} finally { disposeResources(caches); }
|
|
51
|
+
},
|
|
52
|
+
dispose: () => rows.dispose(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function standaloneDraftGroups(binding: MlxAutoregressiveBinding, namespace: string): GroupedDraftProvider {
|
|
57
|
+
return {
|
|
58
|
+
checkpointNamespace: () => namespace,
|
|
59
|
+
open: options => openStandaloneDraftRows(binding, namespace, options.sampling, options.checkpoints),
|
|
60
|
+
openPrefill: options => openStandaloneDraftRows(binding, namespace, null, options.checkpoints),
|
|
61
|
+
};
|
|
62
|
+
}
|