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/generate.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { appendFileSync } from "node:fs";
|
|
11
11
|
import { MlxArray, gpuStream } from "./mlx/array";
|
|
12
12
|
import {
|
|
13
|
+
Dtype,
|
|
13
14
|
activeMemory,
|
|
14
15
|
clearCache,
|
|
15
16
|
maxRecommendedWorkingSetSize,
|
|
@@ -18,16 +19,18 @@ import {
|
|
|
18
19
|
synchronize,
|
|
19
20
|
} from "./mlx/ffi";
|
|
20
21
|
import * as ops from "./mlx/ops";
|
|
21
|
-
import { KVCache, RotatingKVCache,
|
|
22
|
-
import {
|
|
22
|
+
import { KVCache, RotatingKVCache, type Cache } from "./model/gemma4";
|
|
23
|
+
import { maybePageKv } from "./backends/mlx/request-state-policy";
|
|
24
|
+
export { maybePageKv } from "./backends/mlx/request-state-policy";
|
|
23
25
|
import { DiffusionGemmaModel } from "./model/diffusion-gemma";
|
|
24
26
|
import { denoiseAsync } from "./diffusion/diffusion-generate";
|
|
25
27
|
import { bindLegacyDenoisingModel, type MlxDenoisingBinding } from "./backends/mlx/diffusion";
|
|
26
28
|
import type { RuntimeModel } from "./model/factory";
|
|
27
29
|
import {
|
|
28
|
-
assertMlxAutoregressiveBinding, bindLegacyAutoregressiveModel,
|
|
30
|
+
assertMlxAutoregressiveBinding, bindLegacyAutoregressiveModel, supportsCommittedAppendCache,
|
|
29
31
|
type MlxAutoregressiveBinding, type MlxModelMemory, type MlxDecodeStep, type MlxTokenAppend,
|
|
30
32
|
} from "./backends/mlx/autoregressive";
|
|
33
|
+
import { createKvMaintenance } from "./backends/mlx/kv-maintenance";
|
|
31
34
|
import { nextPrefillStep } from "./inference/prefill";
|
|
32
35
|
import { evalCacheState, executeMlxPrefillStep } from "./backends/mlx/prefill";
|
|
33
36
|
import { appendFillHidden } from "./backends/mlx/fill-append";
|
|
@@ -40,6 +43,7 @@ import { runtimeConfig, runtimeValue, withRuntimeConfig, type RuntimeConfig } fr
|
|
|
40
43
|
import {
|
|
41
44
|
disposeStepExtras,
|
|
42
45
|
makeStepSampler,
|
|
46
|
+
readStepExtras,
|
|
43
47
|
stepExtrasArrays,
|
|
44
48
|
type LogitsProcessorOptions,
|
|
45
49
|
type SamplerOptions,
|
|
@@ -57,8 +61,8 @@ import {
|
|
|
57
61
|
} from "./fill/fill-session";
|
|
58
62
|
|
|
59
63
|
export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions {
|
|
60
|
-
/**
|
|
61
|
-
|
|
64
|
+
/** Application session affinity; independent of numerical cache identity. */
|
|
65
|
+
cacheSessionId?: string;
|
|
62
66
|
/** Resolved host policy. Direct compatibility calls resolve from their
|
|
63
67
|
* captured binding when this is absent. */
|
|
64
68
|
decodePolicy?: Readonly<Pick<import("./contracts/execution").ResolvedExecution, "compiledDecode" | "grammarJump">>;
|
|
@@ -121,14 +125,13 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
121
125
|
* Caller keeps ownership. */
|
|
122
126
|
multimodalMask?: MlxArray;
|
|
123
127
|
/** Quantize full-attention KV caches to this many bits (4 or 8).
|
|
124
|
-
*
|
|
125
|
-
* and upstream rotating-cache quantization is NYI. */
|
|
128
|
+
* Full-attention and rotating caches both convert when populated. */
|
|
126
129
|
kvBits?: number;
|
|
127
130
|
kvGroupSize?: number;
|
|
128
131
|
/** Per-layer mixed-precision KV from kv_config.json (config.kvQuant).
|
|
129
132
|
* Overrides kvBits, like optiq serve's --kv-config. layerIdx indexes
|
|
130
|
-
* the cache list (== layer index for the donor prefix)
|
|
131
|
-
* rotating/sliding caches
|
|
133
|
+
* the cache list (== layer index for the donor prefix), including
|
|
134
|
+
* rotating/sliding caches. */
|
|
132
135
|
kvConfig?: KvQuantSpec[];
|
|
133
136
|
/** Convert once a cache's offset reaches this (uniform-kvBits default
|
|
134
137
|
* 5000 = mlx-lm; kvConfig default 0 = optiq serve). */
|
|
@@ -136,14 +139,14 @@ export interface GenerateOptions extends SamplerOptions, LogitsProcessorOptions
|
|
|
136
139
|
/** TurboQuant scheme (docs/design/turboquant.md): rotation-based KV
|
|
137
140
|
* quantization, a CLI-only runtime lever in the same class as uniform
|
|
138
141
|
* kvBits (mutually exclusive with kvBits/kvConfig — maybeQuantizeKv
|
|
139
|
-
* checks
|
|
142
|
+
* checks turboQuant first, so set at most one). Full-attention
|
|
140
143
|
* KVCache layers convert via TurboQuantKVCache.fromKVCache;
|
|
141
144
|
* RotatingKVCache (sliding-window) layers stay bf16 in v1 — a one-time
|
|
142
145
|
* warning names the limitation, never a throw. */
|
|
143
146
|
turboQuant?: TurboQuantScheme;
|
|
144
147
|
/** OPTIONAL paged KV storage (docs/design/kv-cache.md): fresh
|
|
145
148
|
* full-attention KVCache layers are replaced with PagedKVCache (block
|
|
146
|
-
* pool + gather-to-contiguous) before prefill.
|
|
149
|
+
* pool + gather-to-contiguous) before prefill. Scope: B1/B>1
|
|
147
150
|
* Gemma4-family, bf16 — mutually exclusive with kvBits/kvConfig/
|
|
148
151
|
* turboQuant/draft/compiled decode (callers refuse the combos; the
|
|
149
152
|
* cache swap itself only ever touches plain empty KVCache entries).
|
|
@@ -200,17 +203,16 @@ export function shouldUseGrammarJump(
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
/** Composition gate for token fast-forwarding, enforced inside the engine
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* tier — an operator who asked for K3c must not get K3b silently.
|
|
206
|
+
* The serving planner owns request-level eligibility, including explicit
|
|
207
|
+
* seeds in serial execution and mounted draft models. Shared strict fill
|
|
208
|
+
* has its own grouped method and uses the same append-format declaration.
|
|
207
209
|
* - grammar: forced-token production is the grammar's job; two producers in
|
|
208
210
|
* one iteration is forbidden (asserted in the loop).
|
|
209
211
|
* - logprobs/top_logprobs: injected tokens are never sampled, so they have
|
|
210
212
|
* no distribution row (same rule as shouldUseGrammarJump).
|
|
211
213
|
* - promptEmbeddings: vision/audio prompts (and mRoPE) — untested shape.
|
|
212
|
-
* -
|
|
213
|
-
*
|
|
214
|
+
* - affine KV: the model's append binding declares its supported formats.
|
|
215
|
+
* Verify-policy quantized appends remain a separate gate. */
|
|
214
216
|
export function shouldUseFill(
|
|
215
217
|
options: Pick<
|
|
216
218
|
GenerateOptions,
|
|
@@ -218,6 +220,7 @@ export function shouldUseFill(
|
|
|
218
220
|
| "kvBits" | "kvConfig" | "turboQuant"
|
|
219
221
|
>,
|
|
220
222
|
runtime: RuntimeConfig = runtimeConfig(),
|
|
223
|
+
append?: Pick<MlxTokenAppend, "affineKvBits" | "turboQuantFormats"> | null,
|
|
221
224
|
): boolean {
|
|
222
225
|
if (!options.fill) return false;
|
|
223
226
|
if (resolveFillMode(runtime.value("MLX_BUN_FILL") ?? "off") === "off") return false;
|
|
@@ -225,93 +228,15 @@ export function shouldUseFill(
|
|
|
225
228
|
!options.logprobs &&
|
|
226
229
|
!(options.topLogprobs && options.topLogprobs > 0) &&
|
|
227
230
|
options.promptEmbeddings === undefined &&
|
|
228
|
-
|
|
229
|
-
!options.kvConfig?.length &&
|
|
230
|
-
options.turboQuant === undefined;
|
|
231
|
+
supportsCommittedAppendCache(append, options);
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
/**
|
|
234
|
-
*
|
|
235
|
-
* caches convert too — Phase 9):
|
|
236
|
-
* - per-layer bits/group_size selection (kvConfig overrides kvBits,
|
|
237
|
-
* matching optiq's --kv-config precedence; shipped kv_config.json
|
|
238
|
-
* files cover EVERY cache-owning layer, sliding ones included —
|
|
239
|
-
* verified 12B 48/48, 26B 30/30, e4b 24/24 distinct caches — so
|
|
240
|
-
* rotating quantization engages straight from the config; uniform
|
|
241
|
-
* kvBits — like optiq --kv-bits — reaches them too), and
|
|
242
|
-
* - STREAMING conversion (optiq streaming_kv_quant / serve.py
|
|
243
|
-
* patched_maybe_quantize): eval each layer's quantized triples and
|
|
244
|
-
* clear the buffer pool before building the next layer's conversion.
|
|
245
|
-
* Lazily batching every layer's toQuantized into one eval pins ALL
|
|
246
|
-
* layers' bf16 K/V as graph inputs alongside ALL quantized outputs —
|
|
247
|
-
* the exact transient optiq's fix kills (16.35 → 7.60 GB at 32k on a
|
|
248
|
-
* 24 GB Mac). Numerics untouched: same quantize math, only the eval
|
|
249
|
-
* ordering is forced (tests/parity/kv-quant.test.ts, tests/parity/rotating-kvq.test.ts). */
|
|
234
|
+
/** Compatibility entry point for callers that perform a single conversion.
|
|
235
|
+
* Execution sessions compose createKvMaintenance once and reuse it. */
|
|
250
236
|
export function maybeQuantizeKv(cache: Cache[], options: GenerateOptions): void {
|
|
251
|
-
|
|
252
|
-
if (turboQuant) {
|
|
253
|
-
maybeTurboQuantizeKv(cache, turboQuant, options.quantizedKvStart ?? 0);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (!kvBits && !kvConfig?.length) return;
|
|
257
|
-
const start = options.quantizedKvStart ?? (kvConfig?.length ? 0 : 5000);
|
|
258
|
-
const byLayer = kvConfig?.length
|
|
259
|
-
? new Map(kvConfig.map((e) => [e.layerIdx, e]))
|
|
260
|
-
: null;
|
|
261
|
-
for (let i = 0; i < cache.length; i++) {
|
|
262
|
-
const c = cache[i]!;
|
|
263
|
-
if (!(c instanceof KVCache || c instanceof RotatingKVCache) || c.offset < start) continue;
|
|
264
|
-
// OptiQ's mixed-KV hook skips empty caches: the first prompt prefill
|
|
265
|
-
// runs bf16, then the populated cache is quantized before decode.
|
|
266
|
-
// Converting empty caches at start=0 makes prefill itself quantized
|
|
267
|
-
// and diverges from the oracle path.
|
|
268
|
-
if (c.offset === 0) continue;
|
|
269
|
-
if (byLayer) {
|
|
270
|
-
const e = byLayer.get(i);
|
|
271
|
-
if (!e) continue;
|
|
272
|
-
cache[i] = c.toQuantized(e.groupSize, e.bits);
|
|
273
|
-
} else {
|
|
274
|
-
cache[i] = c.toQuantized(options.kvGroupSize ?? 64, kvBits!);
|
|
275
|
-
}
|
|
276
|
-
// Streaming half: materialize THIS layer's conversion now so its bf16
|
|
277
|
-
// source (already released by toQuantized) frees before the next layer
|
|
278
|
-
// converts — the transient stays ~one layer, not the whole cache.
|
|
279
|
-
ops.evalAll(cache[i]!.state());
|
|
280
|
-
clearCache();
|
|
281
|
-
}
|
|
237
|
+
createKvMaintenance(options)(cache);
|
|
282
238
|
}
|
|
283
239
|
|
|
284
|
-
/** Paged-KV conversion (docs/design/kv-cache.md): swap each FRESH
|
|
285
|
-
* plain full-attention KVCache for a PagedKVCache sized to hold
|
|
286
|
-
* `capacityTokens` (prompt + maxTokens — known exactly at generate()
|
|
287
|
-
* setup, so pool exhaustion is unreachable absent an accounting bug).
|
|
288
|
-
* Same in-place-mutation shape as maybeQuantizeKv, but runs ONCE before
|
|
289
|
-
* prefill: paging changes storage layout, not arithmetic, so there is no
|
|
290
|
-
* "convert when populated" trigger. Sliding-window (RotatingKVCache)
|
|
291
|
-
* layers keep today's scheme — mixed paged-full + rotating-sliding is
|
|
292
|
-
* the supported v1 shape. Pre-warmed caches (offset > 0) skip conversion
|
|
293
|
-
* entirely: the serve lane bypasses prompt-cache reuse for paged
|
|
294
|
-
* requests, so this only guards library callers. */
|
|
295
|
-
export function maybePageKv(
|
|
296
|
-
cache: Cache[], options: GenerateOptions, capacityTokens: number,
|
|
297
|
-
): void {
|
|
298
|
-
if (!options.pagedKv) return;
|
|
299
|
-
if (cache.some((c) => c.offset > 0)) return;
|
|
300
|
-
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
301
|
-
for (let i = 0; i < cache.length; i++) {
|
|
302
|
-
if (cache[i] instanceof KVCache) {
|
|
303
|
-
cache[i]!.dispose(); // fresh (offset 0) — nothing stored yet
|
|
304
|
-
cache[i] = new PagedKVCache(capacityTokens, blockSize);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Emitted once per process: RotatingKVCache (sliding-window) layers are a
|
|
310
|
-
* documented v1 non-goal (docs/design/turboquant.md) — they stay bf16
|
|
311
|
-
* rather than throwing, so mixed full-attention/sliding-window models (e.g.
|
|
312
|
-
* Gemma) still serve correctly under --kv-quant turbo. */
|
|
313
|
-
let warnedTurboRotating = false;
|
|
314
|
-
|
|
315
240
|
/** Emitted once per process: token fast-forwarding skips models with
|
|
316
241
|
* sliding-window layers in v1 (see the gate in generateInner). */
|
|
317
242
|
let warnedFillRotating = false;
|
|
@@ -330,39 +255,6 @@ type RewindableCache = {
|
|
|
330
255
|
};
|
|
331
256
|
const rewindable = (c: unknown): RewindableCache => c as RewindableCache;
|
|
332
257
|
|
|
333
|
-
/** TurboQuant conversion chokepoint (mirrors the uniform/config branch
|
|
334
|
-
* above): only plain full-attention KVCache instances convert, via
|
|
335
|
-
* TurboQuantKVCache.fromKVCache — RotatingKVCache stays bf16 (warn once,
|
|
336
|
-
* never throw). Same offset===0 skip-empty-cache rule as the affine path. */
|
|
337
|
-
function maybeTurboQuantizeKv(cache: Cache[], scheme: TurboQuantScheme, start: number): void {
|
|
338
|
-
for (let i = 0; i < cache.length; i++) {
|
|
339
|
-
const c = cache[i]!;
|
|
340
|
-
if (c instanceof RotatingKVCache) {
|
|
341
|
-
if (!warnedTurboRotating) {
|
|
342
|
-
warnedTurboRotating = true;
|
|
343
|
-
console.warn(
|
|
344
|
-
"[turbo-quant] sliding-window (RotatingKVCache) layers stay bf16 in v1 " +
|
|
345
|
-
"(full-attention only) — docs/design/turboquant.md.",
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (!(c instanceof KVCache) || c.offset < start || c.offset === 0) continue;
|
|
351
|
-
const tq = TurboQuantKVCache.fromKVCache(c, scheme.kBits, scheme.vBits);
|
|
352
|
-
cache[i] = tq;
|
|
353
|
-
// state() allocates fresh trimmed slice views for this cache kind
|
|
354
|
-
// (see evalCacheState) — dispose after materializing (throw included),
|
|
355
|
-
// or they leak.
|
|
356
|
-
const state = tq.state();
|
|
357
|
-
try {
|
|
358
|
-
ops.evalAll(state);
|
|
359
|
-
} finally {
|
|
360
|
-
for (const a of state) a.dispose();
|
|
361
|
-
}
|
|
362
|
-
clearCache();
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
258
|
export interface GenerateStats {
|
|
367
259
|
promptTokens: number;
|
|
368
260
|
/** An observed terminal cause takes precedence over the token-count limit. */
|
|
@@ -455,9 +347,11 @@ export class Generation implements AsyncIterable<GeneratedToken> {
|
|
|
455
347
|
// Scope semantics match the reference: set → generate → synchronize →
|
|
456
348
|
// restore; nothing stays pinned between generations. Re-entrant: only
|
|
457
349
|
// the outermost wiring scope touches the limit.
|
|
458
|
-
// macOS 26.6 reports a 24.96 GiB recommended set on
|
|
459
|
-
//
|
|
460
|
-
//
|
|
350
|
+
// macOS 26.6 reports a 26.8e9 B (24.96 GiB, 0.78 × RAM) recommended set on
|
|
351
|
+
// a 32 GB M1 Max (2026-09-08; the earlier "24 GB machine" attribution was
|
|
352
|
+
// wrong — 24.96 GiB exceeds a 24 GB box's RAM). At 0.75 × that the old
|
|
353
|
+
// fraction stopped wiring the 13-16 GiB Qwen/GLM models even though they
|
|
354
|
+
// page heavily without an explicit wired limit.
|
|
461
355
|
// Keep smaller 8-9 GiB models unwired while covering the large-model class.
|
|
462
356
|
const WIRE_THRESHOLD = 0.5;
|
|
463
357
|
let wiredScopeDepth = 0;
|
|
@@ -705,7 +599,7 @@ async function* generateInner(
|
|
|
705
599
|
const {
|
|
706
600
|
maxTokens = 512,
|
|
707
601
|
eosTokenIds = binding.eosTokenIds,
|
|
708
|
-
prefillChunkSize = 2048,
|
|
602
|
+
prefillChunkSize = binding.prefillPolicy?.chunkSize(promptTokens.length) ?? 2048,
|
|
709
603
|
} = options;
|
|
710
604
|
|
|
711
605
|
try {
|
|
@@ -719,6 +613,7 @@ async function* generateInner(
|
|
|
719
613
|
throw error;
|
|
720
614
|
}
|
|
721
615
|
const graph = binding.graph;
|
|
616
|
+
const maintainKv = createKvMaintenance(options);
|
|
722
617
|
|
|
723
618
|
// logprobs capture (mlx_lm.server semantics — see GenerateOptions.logprobs).
|
|
724
619
|
// Everything below is gated: when neither flag is set, no extra ops, evals,
|
|
@@ -746,6 +641,7 @@ async function* generateInner(
|
|
|
746
641
|
const ownsCache = !options.cache;
|
|
747
642
|
const resuming = options.initialPendingToken !== undefined;
|
|
748
643
|
let cache: Cache[] = options.cache ?? [];
|
|
644
|
+
let appender: MlxTokenAppend | null = null;
|
|
749
645
|
try {
|
|
750
646
|
if (ownsCache) cache = binding.makeCache();
|
|
751
647
|
if (!cache.length) throw new Error("AR binding returned an empty cache");
|
|
@@ -760,6 +656,9 @@ async function* generateInner(
|
|
|
760
656
|
// Replace fresh full-attention caches before any forward. The deepest
|
|
761
657
|
// write is prompt + maxTokens - 1, so capacity covers every decode step.
|
|
762
658
|
maybePageKv(cache, options, promptTokens.length + maxTokens);
|
|
659
|
+
appender = options.fill ? binding.createAppend?.({
|
|
660
|
+
hasAdapters: !!options.adapters?.length, pagedKv: !!options.pagedKv,
|
|
661
|
+
}) ?? null : null;
|
|
763
662
|
} catch (error) {
|
|
764
663
|
if (ownsCache) for (const state of cache) state.dispose();
|
|
765
664
|
stepSampler.dispose();
|
|
@@ -773,7 +672,7 @@ async function* generateInner(
|
|
|
773
672
|
// window) layers append multi-token writes through #updateConcat — O(window)
|
|
774
673
|
// per append rather than the O(L) a plain ring pays — so v1 warns and skips
|
|
775
674
|
// the whole feature for those models rather than paying it silently.
|
|
776
|
-
let fillOn = shouldUseFill(options, runtime);
|
|
675
|
+
let fillOn = shouldUseFill(options, runtime, appender);
|
|
777
676
|
if (fillOn && cache.some((c) => c instanceof RotatingKVCache)) {
|
|
778
677
|
fillOn = false;
|
|
779
678
|
if (!warnedFillRotating) {
|
|
@@ -793,29 +692,10 @@ async function* generateInner(
|
|
|
793
692
|
// restore a pre-round snapshot and bit-exactly replay the accepted prefix.
|
|
794
693
|
// A model whose caches can do neither still gets assert-policy fills; verify
|
|
795
694
|
// proposals are dropped and counted (stats.verifyUnsupported).
|
|
796
|
-
const verifyCapable = fillOn && cache.every(
|
|
695
|
+
const verifyCapable = fillOn && !options.kvBits && !options.kvConfig?.length && !options.turboQuant && cache.every(
|
|
797
696
|
(c) => c.isTrimmable() || typeof rewindable(c).specRoundRollback === "function",
|
|
798
697
|
);
|
|
799
698
|
closeBatchSetup?.();
|
|
800
|
-
/** Device-side logprob capture for one step, computed from the SAME lp the
|
|
801
|
-
* sampler saw (post-processors, pre-truncation) — read back lazily with the
|
|
802
|
-
* token so decode pipelining is preserved. */
|
|
803
|
-
/** Read extras back to JS (forces eval — they were async-dispatched with the
|
|
804
|
-
* token) without appending casts behind the next decode step. */
|
|
805
|
-
const readExtras = (e: StepExtras | null): TokenLogprobs | undefined => {
|
|
806
|
-
if (!e) return undefined;
|
|
807
|
-
const out: TokenLogprobs = {};
|
|
808
|
-
if (e.sel) out.logprob = e.sel.toFloat32Host()[0]!;
|
|
809
|
-
if (e.topIdx && e.topVals) {
|
|
810
|
-
const ids = e.topIdx.toIntTokens();
|
|
811
|
-
const vals = e.topVals.toFloat32Host();
|
|
812
|
-
out.top = Array.from(ids, (id, i) => ({ id, logprob: vals[i]! })).sort(
|
|
813
|
-
(a, b) => b.logprob - a.logprob,
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
disposeStepExtras(e);
|
|
817
|
-
return out;
|
|
818
|
-
};
|
|
819
699
|
|
|
820
700
|
// logits [1,1,V] → sampled token array [1] (+ optional logprob capture,
|
|
821
701
|
// all on-device)
|
|
@@ -844,10 +724,10 @@ async function* generateInner(
|
|
|
844
724
|
let pendingExtras: StepExtras | null = null;
|
|
845
725
|
let nextExtras: StepExtras | null = null;
|
|
846
726
|
let decoder: MlxDecodeStep | null = null;
|
|
847
|
-
let appender: MlxTokenAppend | null = null;
|
|
848
727
|
let finished = false;
|
|
849
728
|
let threw = false;
|
|
850
729
|
let executionError: unknown;
|
|
730
|
+
let earlyUnforwardedToken: number | null = null;
|
|
851
731
|
let closeTokenZero: (() => void) | undefined;
|
|
852
732
|
const makeStats = (): GenerateStats => ({
|
|
853
733
|
promptTokens: options.originalPromptTokens ?? promptTokens.length,
|
|
@@ -946,9 +826,8 @@ async function* generateInner(
|
|
|
946
826
|
nextPending = null;
|
|
947
827
|
disposeStepExtras(nextExtras);
|
|
948
828
|
nextExtras = null;
|
|
949
|
-
//
|
|
950
|
-
|
|
951
|
-
maybeQuantizeKv(cache, options);
|
|
829
|
+
// Conversion runs at the ordinary committed-token boundary as well.
|
|
830
|
+
maintainKv(cache);
|
|
952
831
|
|
|
953
832
|
let checkpointMs = 0;
|
|
954
833
|
let roundOpen = false;
|
|
@@ -963,11 +842,18 @@ async function* generateInner(
|
|
|
963
842
|
let logitsAll: MlxArray | null = null;
|
|
964
843
|
try {
|
|
965
844
|
const chunkSize = (state: readonly Cache[]) => {
|
|
966
|
-
const maxChunk = appender?.maxChunkSize(state) ?? 1
|
|
845
|
+
const maxChunk = Math.min(appender?.maxChunkSize(state) ?? 1,
|
|
846
|
+
maintainKv.maxAppendTokens?.(state) ?? Number.POSITIVE_INFINITY);
|
|
967
847
|
return fill.appendChunkSize > 0 ? Math.min(fill.appendChunkSize, maxChunk) : maxChunk;
|
|
968
848
|
};
|
|
849
|
+
const committedGraph = appender ? appender.forwardHidden.bind(appender) : graph.forwardHidden.bind(graph);
|
|
850
|
+
const forwardCommitted = maintainKv.maxAppendTokens ? async (ids: MlxArray, state: Cache[]) => {
|
|
851
|
+
const hidden = await committedGraph(ids, state);
|
|
852
|
+
try { maintainKv(state); return hidden; }
|
|
853
|
+
catch (error) { hidden.dispose(); throw error; }
|
|
854
|
+
} : committedGraph;
|
|
969
855
|
hf = await appendFillHidden(
|
|
970
|
-
|
|
856
|
+
verify ? graph.forwardHidden.bind(graph) : forwardCommitted,
|
|
971
857
|
cache, ids, verify ? () => ids.length : chunkSize,
|
|
972
858
|
);
|
|
973
859
|
const [, Lf, Hf] = hf.shape as [number, number, number];
|
|
@@ -1061,7 +947,10 @@ async function* generateInner(
|
|
|
1061
947
|
// pending token was sampled from that exact state before the snapshot,
|
|
1062
948
|
// so resume starts directly at the decode loop without another forward.
|
|
1063
949
|
if (needsTokenHistory) stepSampler.seedHistory(promptTokens);
|
|
1064
|
-
|
|
950
|
+
// Device samplers return uint32. Preserve that signature on restore so
|
|
951
|
+
// compiled decode does not trace a different signed-token graph first.
|
|
952
|
+
using restoredToken = ops.fromInt32([options.initialPendingToken!], [1]);
|
|
953
|
+
pending = restoredToken.astype(Dtype.uint32);
|
|
1065
954
|
ops.asyncEvalAll([pending]);
|
|
1066
955
|
prefillMs = 0;
|
|
1067
956
|
closePrefill?.();
|
|
@@ -1072,7 +961,7 @@ async function* generateInner(
|
|
|
1072
961
|
mechanism: diagnostics.mechanism ?? "serial",
|
|
1073
962
|
boundary: "initial",
|
|
1074
963
|
});
|
|
1075
|
-
|
|
964
|
+
maintainKv(cache);
|
|
1076
965
|
closeInitialKv?.();
|
|
1077
966
|
tPrefill = performance.now();
|
|
1078
967
|
let h0: MlxArray;
|
|
@@ -1096,7 +985,7 @@ async function* generateInner(
|
|
|
1096
985
|
: undefined;
|
|
1097
986
|
const tailSplit = runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
1098
987
|
const forward = graph.forwardHidden.bind(graph);
|
|
1099
|
-
const maintain = () =>
|
|
988
|
+
const maintain = () => maintainKv(cache);
|
|
1100
989
|
while (true) {
|
|
1101
990
|
options.signal?.throwIfAborted();
|
|
1102
991
|
const step = nextPrefillStep({ length: promptTokens.length, position: pos,
|
|
@@ -1152,9 +1041,6 @@ async function* generateInner(
|
|
|
1152
1041
|
decoder = options.decodePolicy?.compiledDecode === false ? null : binding.createDecode?.({
|
|
1153
1042
|
hasAdapters: !!options.adapters?.length, pagedKv: !!options.pagedKv,
|
|
1154
1043
|
}) ?? null;
|
|
1155
|
-
appender = fillOn ? binding.createAppend?.({
|
|
1156
|
-
hasAdapters: !!options.adapters?.length, pagedKv: !!options.pagedKv,
|
|
1157
|
-
}) ?? null : null;
|
|
1158
1044
|
let stop = false;
|
|
1159
1045
|
/** Token id read eagerly at the top of the loop for grammar (reused for
|
|
1160
1046
|
* the yield, avoiding a second readback). -1 when grammar is off — the
|
|
@@ -1170,7 +1056,7 @@ async function* generateInner(
|
|
|
1170
1056
|
const grammarJump = options.decodePolicy?.grammarJump ?? shouldUseGrammarJump(options, runtime);
|
|
1171
1057
|
// Yield token zero before its own decode forward. The captured runtime
|
|
1172
1058
|
// keeps this scheduling choice stable across consumer awaits.
|
|
1173
|
-
const earlyFirstToken = runtime.
|
|
1059
|
+
const earlyFirstToken = runtime.flag("MLX_BUN_EARLY_FIRST_TOKEN", true) &&
|
|
1174
1060
|
maxTokens > 1 && !resuming && !fillOn && !options.grammar;
|
|
1175
1061
|
while (!stop) {
|
|
1176
1062
|
options.signal?.throwIfAborted();
|
|
@@ -1186,14 +1072,16 @@ async function* generateInner(
|
|
|
1186
1072
|
closeTokenZero?.();
|
|
1187
1073
|
closeTokenZero = undefined;
|
|
1188
1074
|
tDecode = performance.now();
|
|
1189
|
-
const logprobs =
|
|
1075
|
+
const logprobs = readStepExtras(curExtras);
|
|
1190
1076
|
curExtras = null;
|
|
1191
1077
|
pendingExtras = null;
|
|
1192
1078
|
// Count before yielding so an early return reports this token. Its
|
|
1193
1079
|
// KV is still absent; forwarded describes the prompt-only cache.
|
|
1194
1080
|
generated++;
|
|
1195
1081
|
yieldedEarly = true;
|
|
1082
|
+
earlyUnforwardedToken = firstToken;
|
|
1196
1083
|
yield { token: firstToken, index: stepIndex, ...(logprobs ? { logprobs } : {}) };
|
|
1084
|
+
earlyUnforwardedToken = null;
|
|
1197
1085
|
options.signal?.throwIfAborted();
|
|
1198
1086
|
}
|
|
1199
1087
|
}
|
|
@@ -1242,7 +1130,7 @@ async function* generateInner(
|
|
|
1242
1130
|
// contract); the next sampled token, if the budget and grammar allow
|
|
1243
1131
|
// one, comes from its last position. Compiled decode resumes on the
|
|
1244
1132
|
// following iteration (supports() re-checks the grown caches).
|
|
1245
|
-
|
|
1133
|
+
maintainKv(cache);
|
|
1246
1134
|
pushHistory(cur);
|
|
1247
1135
|
stepSampler.commitNumbers(jumpEmit);
|
|
1248
1136
|
const chunk = [grammarTok, ...jumpEmit];
|
|
@@ -1267,7 +1155,7 @@ async function* generateInner(
|
|
|
1267
1155
|
h.dispose(); // burst ends the generation (max_tokens or grammar done)
|
|
1268
1156
|
}
|
|
1269
1157
|
} else if (stepIndex + 1 < maxTokens && !options.grammar?.isTerminated) {
|
|
1270
|
-
|
|
1158
|
+
maintainKv(cache);
|
|
1271
1159
|
pushHistory(cur);
|
|
1272
1160
|
let logits: MlxArray | null = null;
|
|
1273
1161
|
let evalWith: MlxArray[] = [];
|
|
@@ -1347,7 +1235,7 @@ async function* generateInner(
|
|
|
1347
1235
|
}
|
|
1348
1236
|
// readExtras before the yield: if the consumer breaks at this yield,
|
|
1349
1237
|
// the extras are already read and disposed.
|
|
1350
|
-
const logprobs =
|
|
1238
|
+
const logprobs = readStepExtras(curExtras);
|
|
1351
1239
|
pendingExtras = null;
|
|
1352
1240
|
if (!yieldedEarly) yield { token, index: generated - 1, ...(logprobs ? { logprobs } : {}) };
|
|
1353
1241
|
options.signal?.throwIfAborted();
|
|
@@ -1412,31 +1300,53 @@ async function* generateInner(
|
|
|
1412
1300
|
executionError = e;
|
|
1413
1301
|
throw e;
|
|
1414
1302
|
} finally {
|
|
1415
|
-
if (!finished) {
|
|
1416
|
-
pending?.dispose();
|
|
1417
|
-
nextPending?.dispose();
|
|
1418
|
-
disposeStepExtras(pendingExtras);
|
|
1419
|
-
disposeStepExtras(nextExtras);
|
|
1420
|
-
}
|
|
1421
1303
|
try {
|
|
1422
|
-
|
|
1423
|
-
|
|
1304
|
+
// An early consumer return leaves token zero outside the retained KV.
|
|
1305
|
+
// Finish that one M=1 forward before handing caller-owned caches back,
|
|
1306
|
+
// matching the ordinary pipeline's boundary. Otherwise a later prefix
|
|
1307
|
+
// hit folds the token into an M>1 prefill and changes native reductions.
|
|
1308
|
+
// Aborted requests and caches owned by this run need no retained state.
|
|
1309
|
+
if (!ownsCache && !threw && !options.signal?.aborted && earlyUnforwardedToken !== null) {
|
|
1310
|
+
maintainKv(cache);
|
|
1311
|
+
const ids = ops.reshape(pending!, [1, 1]);
|
|
1312
|
+
try {
|
|
1313
|
+
const hidden = await graph.forwardHidden(ids, cache);
|
|
1314
|
+
try { evalCacheState(cache); }
|
|
1315
|
+
finally { hidden.dispose(); }
|
|
1316
|
+
} finally { ids.dispose(); }
|
|
1317
|
+
forwarded.push(earlyUnforwardedToken);
|
|
1318
|
+
}
|
|
1424
1319
|
} catch (error) {
|
|
1425
|
-
|
|
1426
|
-
|
|
1320
|
+
threw = true;
|
|
1321
|
+
executionError = error;
|
|
1427
1322
|
throw error;
|
|
1428
1323
|
} finally {
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1324
|
+
if (!finished) {
|
|
1325
|
+
pending?.dispose();
|
|
1326
|
+
nextPending?.dispose();
|
|
1327
|
+
disposeStepExtras(pendingExtras);
|
|
1328
|
+
disposeStepExtras(nextExtras);
|
|
1329
|
+
}
|
|
1330
|
+
try {
|
|
1331
|
+
const closing = decoder?.close();
|
|
1332
|
+
if (closing) await closing;
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
if (threw) throw new AggregateError([executionError, error],
|
|
1335
|
+
"AR execution and decoder cleanup failed", { cause: executionError });
|
|
1336
|
+
throw error;
|
|
1337
|
+
} finally {
|
|
1338
|
+
if (ownsCache) for (const c of cache) c.dispose();
|
|
1339
|
+
// TokenizerInfo is process-cached; this request owns the matcher.
|
|
1340
|
+
options.grammar?.dispose();
|
|
1341
|
+
stepSampler.dispose();
|
|
1342
|
+
}
|
|
1343
|
+
if (!finished && !threw) {
|
|
1344
|
+
// forced early return (consumer break at a yield): still report
|
|
1345
|
+
// stats — `forwarded` only lists tokens whose KV actually entered
|
|
1346
|
+
// the cache, so cacheTokens stays exact for PromptCache.put().
|
|
1347
|
+
decodeMs = performance.now() - tDecode;
|
|
1348
|
+
return makeStats();
|
|
1349
|
+
}
|
|
1440
1350
|
}
|
|
1441
1351
|
}
|
|
1442
1352
|
}
|
package/src/grammar.ts
CHANGED
|
@@ -314,6 +314,19 @@ export class GrammarController {
|
|
|
314
314
|
return this.terminated;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/** Propose the forced string without advancing the matcher. Tokenization
|
|
318
|
+
* can differ at its first boundary; verification and the normal grammar
|
|
319
|
+
* sampler accept or reject the candidates before anything is committed. */
|
|
320
|
+
async proposeTokens(maxTokens: number): Promise<number[]> {
|
|
321
|
+
await this.ready();
|
|
322
|
+
if (this.terminated || !this.encodeRaw) return [];
|
|
323
|
+
return wasmQueue(async () => {
|
|
324
|
+
if (this.disposed) return [];
|
|
325
|
+
const text = this.matcher.findJumpForwardString();
|
|
326
|
+
return text ? this.encodeRaw!(text).slice(0, maxTokens) : [];
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
317
330
|
/** Jump-forward decoding (SGLang's technique via xgrammar's
|
|
318
331
|
* findJumpForwardString; opt-in — MLX_BUN_GRAMMAR_JUMP=1): when the grammar
|
|
319
332
|
* admits exactly one continuation string (JSON keys/punctuation, enum
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** One request's exact-token speculative acceptance walk. The backend owns
|
|
2
|
+
* logits and sampling; this state owns only the accepted prefix and stop facts.
|
|
3
|
+
* A group advances each live walk independently, regardless of batch size. */
|
|
4
|
+
export class DraftAcceptance {
|
|
5
|
+
position = 0;
|
|
6
|
+
accepted = 0;
|
|
7
|
+
correction: number | null = null;
|
|
8
|
+
readonly emitted: number[] = [];
|
|
9
|
+
sawEos = false;
|
|
10
|
+
grammarDone = false;
|
|
11
|
+
done = false;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
readonly drafts: readonly number[],
|
|
15
|
+
readonly remaining: number,
|
|
16
|
+
readonly eosTokenIds: readonly number[],
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/** Supply one target sample after its sampler has updated request history
|
|
20
|
+
* and grammar. Once done, this request needs no further target samples. */
|
|
21
|
+
accept(token: number, grammarDone = false): void {
|
|
22
|
+
const matches = this.position < this.drafts.length && token === this.drafts[this.position];
|
|
23
|
+
this.position++;
|
|
24
|
+
if (matches) {
|
|
25
|
+
this.accepted++;
|
|
26
|
+
if (this.eosTokenIds.includes(token)) {
|
|
27
|
+
this.sawEos = this.done = true;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.emitted.push(token);
|
|
31
|
+
this.grammarDone = grammarDone;
|
|
32
|
+
this.done = grammarDone || this.emitted.length >= this.remaining;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.correction = token;
|
|
36
|
+
this.sawEos = this.eosTokenIds.includes(token);
|
|
37
|
+
if (!this.sawEos) this.emitted.push(token);
|
|
38
|
+
this.grammarDone = grammarDone;
|
|
39
|
+
this.done = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Cancellation, CancelReason, GenerationOutput, TokenLogprobs } from "../contracts/generation";
|
|
2
|
+
import type { DraftAcceptance } from "./draft-acceptance";
|
|
3
|
+
|
|
4
|
+
export interface DraftOutputRow {
|
|
5
|
+
readonly acceptance: DraftAcceptance;
|
|
6
|
+
readonly logprobs?: readonly (TokenLogprobs | undefined)[];
|
|
7
|
+
readonly output: Pick<GenerationOutput, "commit">;
|
|
8
|
+
readonly cancellation?: Cancellation;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Counts apply to this round. Accepted counts describe processed proposed
|
|
12
|
+
* inputs, excluding an unprocessed correction/bonus and any unpublished EOS.
|
|
13
|
+
* Failed/cancelled requests must be discarded, never checkpointed. */
|
|
14
|
+
export type DraftOutputResult = { readonly generated: number; readonly accepted: number } & (
|
|
15
|
+
| { readonly kind: "continue"; readonly pending: number }
|
|
16
|
+
| { readonly kind: "stop" | "length" }
|
|
17
|
+
| { readonly kind: "cancelled"; readonly reason: CancelReason }
|
|
18
|
+
| { readonly kind: "failed"; readonly error: unknown }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/** Deliver verified content through the output interface before resolving
|
|
22
|
+
* retained state. A stopped or failed consumer affects only its own request.
|
|
23
|
+
* Token-sized publications preserve callbacks that stop inside a draft burst. */
|
|
24
|
+
export async function deliverDraftOutputs(rows: readonly DraftOutputRow[]): Promise<DraftOutputResult[]> {
|
|
25
|
+
const results: DraftOutputResult[] = [];
|
|
26
|
+
for (const row of rows) results.push(await deliver(row));
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function deliver(row: DraftOutputRow): Promise<DraftOutputResult> {
|
|
31
|
+
const { acceptance } = row;
|
|
32
|
+
let generated = 0;
|
|
33
|
+
const retained = () => ({ generated, accepted: Math.min(generated, acceptance.accepted) });
|
|
34
|
+
try {
|
|
35
|
+
for (const token of acceptance.emitted) {
|
|
36
|
+
const reason = row.cancellation?.reason;
|
|
37
|
+
if (reason) return { kind: "cancelled", reason, generated, accepted: 0 };
|
|
38
|
+
const metadata = row.logprobs?.[generated];
|
|
39
|
+
const more = await row.output.commit([token], metadata ? [metadata] : undefined);
|
|
40
|
+
generated++;
|
|
41
|
+
if (more === false) return { kind: "stop", ...retained() };
|
|
42
|
+
}
|
|
43
|
+
const reason = row.cancellation?.reason;
|
|
44
|
+
if (reason) return { kind: "cancelled", reason, generated, accepted: 0 };
|
|
45
|
+
if (acceptance.sawEos || acceptance.grammarDone) return { kind: "stop", ...retained() };
|
|
46
|
+
if (generated >= acceptance.remaining) return { kind: "length", ...retained() };
|
|
47
|
+
return { kind: "continue", pending: acceptance.correction!, ...retained() };
|
|
48
|
+
} catch (error) { return { kind: "failed", error, generated, accepted: 0 }; }
|
|
49
|
+
}
|