mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
// (reproducibility ⇒ solo, matching mlx-lm's _is_batchable). Temperature /
|
|
32
32
|
// top-p / top-k DO batch (each row samples with its own seed). Full-attention
|
|
33
33
|
// AND sliding-window (Gemma) models both batch — the scheduler assembles each
|
|
34
|
-
// layer's cache by type. Prompt-cache prefix reuse
|
|
35
|
-
//
|
|
34
|
+
// layer's cache by type. Prompt-cache prefix reuse works on the batch lane
|
|
35
|
+
// too (Phase 3.2): joiners take() at admission; never-merged lone rows put()
|
|
36
|
+
// back on finish (merged rows' KV is not extracted — their entries age out).
|
|
36
37
|
|
|
37
38
|
import { MlxArray } from "../mlx/array";
|
|
38
39
|
import * as ops from "../mlx/ops";
|
|
@@ -40,23 +41,51 @@ import type { RuntimeModel } from "../model/factory";
|
|
|
40
41
|
import { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
41
42
|
import { KVCache, RotatingKVCache } from "../model/gemma4-base";
|
|
42
43
|
import { SSMCache } from "../model/qwen3-delta";
|
|
44
|
+
import { UniversalDenseModel } from "../model/universal/dense";
|
|
43
45
|
import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
|
|
46
|
+
import type { KvQuantSpec, TurboQuantScheme } from "../config";
|
|
44
47
|
import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
|
|
45
|
-
import { BatchScheduler } from "./batch-scheduler";
|
|
48
|
+
import { BatchScheduler, type RowPromptCache } from "./batch-scheduler";
|
|
46
49
|
|
|
47
50
|
/** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
|
|
48
51
|
class AsyncMutex {
|
|
49
52
|
#tail: Promise<void> = Promise.resolve();
|
|
53
|
+
/** Holders + waiters. > 0 means somebody owns or wants the engine. */
|
|
54
|
+
#pending = 0;
|
|
55
|
+
get locked(): boolean {
|
|
56
|
+
return this.#pending > 0;
|
|
57
|
+
}
|
|
50
58
|
acquire(): Promise<() => void> {
|
|
59
|
+
this.#pending++;
|
|
51
60
|
let release!: () => void;
|
|
52
61
|
const gate = new Promise<void>((r) => { release = r; });
|
|
53
62
|
const wait = this.#tail;
|
|
54
63
|
this.#tail = this.#tail.then(() => gate);
|
|
55
|
-
|
|
64
|
+
let released = false;
|
|
65
|
+
return wait.then(() => () => {
|
|
66
|
+
if (released) return; // idempotent: a double release must not skew #pending
|
|
67
|
+
released = true;
|
|
68
|
+
this.#pending--;
|
|
69
|
+
release();
|
|
70
|
+
});
|
|
56
71
|
}
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
/** Embeddings-prefill payload for media prompts (vision and/or audio — the
|
|
75
|
+
* name predates audio). Any request carrying one routes to the serial lane
|
|
76
|
+
* (shape.hasVision) and prefills through generate()'s promptEmbeddings
|
|
77
|
+
* path, bypassing the prompt cache. */
|
|
78
|
+
export type Vision = {
|
|
79
|
+
embeddings: MlxArray;
|
|
80
|
+
/** bool [L] image-token mask for the bidirectional attention overlay.
|
|
81
|
+
* Absent when the prompt carries ANY audio — audio(-containing) prompts
|
|
82
|
+
* run fully causal (audio-input-plan.md §3.3 Q1). */
|
|
83
|
+
imageMask?: MlxArray;
|
|
84
|
+
/** bool [L] union multimodal soft-token mask (image | audio) for
|
|
85
|
+
* per-layer-input id zeroing. Absent on the legacy vision-only shape,
|
|
86
|
+
* where zeroing falls back to imageMask. */
|
|
87
|
+
multimodalMask?: MlxArray;
|
|
88
|
+
};
|
|
60
89
|
|
|
61
90
|
/** Per-token sink: returning `false` halts this generation (stop sequence).
|
|
62
91
|
* `logprobs` is only populated on the serial lane when the request asked for
|
|
@@ -84,34 +113,74 @@ export interface RequestShape {
|
|
|
84
113
|
userSeed: boolean;
|
|
85
114
|
/** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
|
|
86
115
|
kvQuant: boolean;
|
|
116
|
+
/** TurboQuant is active (docs/design/turboquant-kv.md). Solo-only in v1:
|
|
117
|
+
* TurboQuantKVCache is a novel Cache implementation (not a KVCache/
|
|
118
|
+
* RotatingKVCache subclass), so #modelCachesBatchable() already excludes
|
|
119
|
+
* it automatically once a request's cache is converted — this flag is the
|
|
120
|
+
* BELT on top of that automatic BRACES, an explicit refusal at the
|
|
121
|
+
* request-shape level so a turbo request never reaches the scheduler even
|
|
122
|
+
* before any cache conversion has happened. Both layers exist on purpose. */
|
|
123
|
+
turboQuant: boolean;
|
|
87
124
|
/** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
|
|
88
|
-
* logit_bias, presence/frequency penalty.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
125
|
+
* logit_bias, presence/frequency penalty. INFORMATIONAL ONLY: since the
|
|
126
|
+
* batched lane grew per-row samplers/logits processors these all batch —
|
|
127
|
+
* willBatch() deliberately does not gate on this field (see its
|
|
128
|
+
* repetitionPenalty note). Kept so /stats and lane tracing can show what
|
|
129
|
+
* a request carries. */
|
|
93
130
|
hasLogitsExtras: boolean;
|
|
131
|
+
/** A grammar controller compiled for this request (response_format /
|
|
132
|
+
* guided_*). Degrade-path requests (compile failed → prompt injection)
|
|
133
|
+
* have NO controller and stay batchable — the injection already happened
|
|
134
|
+
* at the prompt level. B0 routes grammar to serial; B1 makes it batchable
|
|
135
|
+
* (per-row matchers), with MLX_BUN_GRAMMAR_BATCH=0 forcing the B0 serial
|
|
136
|
+
* fallback as the A/B + kill switch. */
|
|
137
|
+
hasGrammar: boolean;
|
|
94
138
|
/** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
|
|
95
139
|
* are deferred (the scheduler's per-row sampler doesn't capture or read
|
|
96
140
|
* back logprob arrays yet), so these route to the serial lane like the
|
|
97
141
|
* other mlx-lm request extensions above. */
|
|
98
142
|
wantsLogprobs: boolean;
|
|
143
|
+
/** A draft model is configured (serve --draft-model). Server-level and
|
|
144
|
+
* upstream-parity: mlx_lm.server sets is_batchable = (draft is None), so
|
|
145
|
+
* every request routes serial while a draft is mounted — speculation is a
|
|
146
|
+
* B=1 latency optimization, batching a throughput one; they are different
|
|
147
|
+
* modes by design (grammar-spec-batching-integration.md). */
|
|
148
|
+
hasDraft: boolean;
|
|
99
149
|
}
|
|
100
150
|
|
|
101
151
|
export class GenerationGateway {
|
|
102
152
|
readonly #mutex = new AsyncMutex();
|
|
103
153
|
readonly #batch: number;
|
|
104
154
|
#scheduler: BatchScheduler | null = null;
|
|
155
|
+
#rowsSubmitted = 0;
|
|
105
156
|
/** Serial-lane requests waiting for / holding the mutex. While > 0 the
|
|
106
157
|
* scheduler pauses admission (drain) so they can't be starved. */
|
|
107
158
|
#serialWaiters = 0;
|
|
108
159
|
/** Lazy, memoized: can this model's caches do the dynamic-B ops? */
|
|
109
160
|
#cacheBatchable: boolean | null = null;
|
|
161
|
+
/** Lazy, memoized: can the server's kv scheme run under batching?
|
|
162
|
+
* (Phase 3.1: kvConfig whose every configured layerIdx is a plain
|
|
163
|
+
* full-attention KVCache — uniform kvBits and rotating-layer configs
|
|
164
|
+
* stay serial. The scheme is a server-lifetime constant, so one memo.) */
|
|
165
|
+
#kvSchemeBatchable: boolean | null = null;
|
|
110
166
|
|
|
111
167
|
constructor(
|
|
112
168
|
private readonly model: RuntimeModel,
|
|
113
169
|
batch: number,
|
|
114
170
|
private readonly serialRun: SerialRun,
|
|
171
|
+
private readonly opts: {
|
|
172
|
+
kvBudgetBytes?: number;
|
|
173
|
+
/** The server-wide KV scheme (server.ts kvScheme) — threaded to the
|
|
174
|
+
* scheduler at construction when batchable (Phase 3.1). turboQuant is
|
|
175
|
+
* never threaded to the scheduler (always solo-only — see willBatch);
|
|
176
|
+
* it's here only so the gateway can see it's active. */
|
|
177
|
+
kvScheme?: { kvBits?: number; kvConfig?: KvQuantSpec[]; turboQuant?: TurboQuantScheme };
|
|
178
|
+
/** The server's prompt cache (Phase 3.2): batch-lane joiners take()
|
|
179
|
+
* the longest usable prefix at admission; never-merged rows put()
|
|
180
|
+
* back on finish. Safe to share with the serial lane — both use it
|
|
181
|
+
* only inside this gateway's mutual-exclusion domain. */
|
|
182
|
+
promptCache?: RowPromptCache;
|
|
183
|
+
} = {},
|
|
115
184
|
) {
|
|
116
185
|
this.#batch = Math.max(1, Math.floor(batch));
|
|
117
186
|
}
|
|
@@ -126,6 +195,45 @@ export class GenerationGateway {
|
|
|
126
195
|
return this.#scheduler?.activeRows ?? 0;
|
|
127
196
|
}
|
|
128
197
|
|
|
198
|
+
/** Queued + mid-prefill rows waiting behind the batch (0 when idle). */
|
|
199
|
+
get pendingRows(): number {
|
|
200
|
+
return this.#scheduler?.pendingRows ?? 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** True while ANY engine work is active or queued, on EITHER lane: the
|
|
204
|
+
* mutex is held/awaited (a serial generation, the batch scheduler's whole
|
|
205
|
+
* active period, curve endpoints, adapter mounts) or batch rows are
|
|
206
|
+
* running/pending. activeRows/pendingRows alone MISS the serial lane —
|
|
207
|
+
* a serial generation holds the mutex but shows zero rows. */
|
|
208
|
+
get busy(): boolean {
|
|
209
|
+
return this.#mutex.locked || this.activeRows > 0 || this.pendingRows > 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Resolves once the engine is idle (poll-based, ~`pollMs` granularity —
|
|
213
|
+
* cheap vs the multi-hundred-ms flushes it paces; resolves without a
|
|
214
|
+
* timer when already idle). The SSD write-behind gates each per-tensor
|
|
215
|
+
* flush step on this so durability work never steals blocking
|
|
216
|
+
* GPU-sync + writeSync slices from an active decode (the 2026-07-07
|
|
217
|
+
* decode@ctx contamination — see server.ts's write-behind block). */
|
|
218
|
+
async onIdle(pollMs = 20): Promise<void> {
|
|
219
|
+
while (this.busy) await new Promise<void>((r) => setTimeout(r, pollMs));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Cumulative rows routed to the batch lane since server start. The serial
|
|
223
|
+
* lane never advances this — /stats' race-free lane-routing observable
|
|
224
|
+
* (active/pending are instantaneous and read 0 once a request finishes). */
|
|
225
|
+
get submittedRows(): number {
|
|
226
|
+
return this.#rowsSubmitted;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Projected aggregate KV bytes of admitted rows / the --kv-budget cap. */
|
|
230
|
+
get kvBytes(): { projected: number; budget: number | null } {
|
|
231
|
+
return {
|
|
232
|
+
projected: this.#scheduler?.projectedKvBytes ?? 0,
|
|
233
|
+
budget: this.#scheduler?.kvBudgetBytes ?? this.opts.kvBudgetBytes ?? null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
129
237
|
/** Cache-capability gate (mirrors mlx-lm server.py's all-caches-have-merge
|
|
130
238
|
* check): the scheduler's dynamic-B ops (temporalView/merge/filter) exist
|
|
131
239
|
* on KVCache, RotatingKVCache, and SSMCache (hybrid gated-DeltaNet models
|
|
@@ -133,6 +241,21 @@ export class GenerationGateway {
|
|
|
133
241
|
* the kill switch back to serial routing). */
|
|
134
242
|
#modelCachesBatchable(): boolean {
|
|
135
243
|
if (this.#cacheBatchable === null) {
|
|
244
|
+
// Tier-0 universal models: batchable for PLAIN full-attention archs
|
|
245
|
+
// only. The 2026-07-03 uneven-row bug (scalar-offset RoPE decoded
|
|
246
|
+
// padded rows at wrong positions) is fixed via
|
|
247
|
+
// UniversalRope.applyDynamic + ropeOffsetArr and GATED token-exact vs
|
|
248
|
+
// mlx-lm B=2 on Llama-3.2-3B (static + dynamic join/leave,
|
|
249
|
+
// tests/batched-decode-parity.test.ts "Llama 3B Tier-0"). The
|
|
250
|
+
// maskArray archs (gemma2-family: forwardLayers builds a pad-blind
|
|
251
|
+
// causal mask) and sliding-window universal archs remain UNVALIDATED
|
|
252
|
+
// cells → serial, per the per-model-cell discipline.
|
|
253
|
+
if (this.model instanceof UniversalDenseModel) {
|
|
254
|
+
const a = this.model.args;
|
|
255
|
+
this.#cacheBatchable =
|
|
256
|
+
!a.maskArray && !a.layerTypes?.includes("sliding_attention");
|
|
257
|
+
return this.#cacheBatchable;
|
|
258
|
+
}
|
|
136
259
|
const ssmOk = process.env.MLX_BUN_BATCH_SSM !== "0";
|
|
137
260
|
const proto = this.model.makeCache(); // fresh caches hold no buffers
|
|
138
261
|
this.#cacheBatchable = proto.every(
|
|
@@ -146,6 +269,36 @@ export class GenerationGateway {
|
|
|
146
269
|
return this.#cacheBatchable;
|
|
147
270
|
}
|
|
148
271
|
|
|
272
|
+
/** Phase 3.1 + milestone 2: the server's kv scheme batches iff it is a
|
|
273
|
+
* per-layer kvConfig (the L2 mixed scheme) whose every configured layer
|
|
274
|
+
* is a full-attention KVCache (BatchedQuantDecodeMaskCache + quantized
|
|
275
|
+
* merge/extend/filter) or a rotating RotatingKVCache
|
|
276
|
+
* (BatchedRotatingQuantCache — milestone 2, gemma's kv_config). Uniform
|
|
277
|
+
* kvBits (quantizedKvStart threshold semantics via the serial no-byLayer
|
|
278
|
+
* path) and configs naming SSM layers stay serial. */
|
|
279
|
+
#kvBatchable(): boolean {
|
|
280
|
+
if (this.#kvSchemeBatchable === null) {
|
|
281
|
+
const scheme = this.opts.kvScheme;
|
|
282
|
+
if (!scheme || (!scheme.kvBits && !scheme.kvConfig?.length)) {
|
|
283
|
+
// Only consulted when shape.kvQuant is SET: a kv-quant request with
|
|
284
|
+
// no scheme threaded to the gateway cannot be applied by the
|
|
285
|
+
// scheduler — batching it would SILENTLY DROP the quantization (the
|
|
286
|
+
// exact composition bug optiq serve ships). Route serial.
|
|
287
|
+
this.#kvSchemeBatchable = false;
|
|
288
|
+
} else if (scheme.kvBits || !scheme.kvConfig?.length) {
|
|
289
|
+
this.#kvSchemeBatchable = false; // uniform bits: serial
|
|
290
|
+
} else {
|
|
291
|
+
const proto = this.model.makeCache();
|
|
292
|
+
this.#kvSchemeBatchable = scheme.kvConfig.every(
|
|
293
|
+
(e) => proto[e.layerIdx] instanceof KVCache ||
|
|
294
|
+
proto[e.layerIdx] instanceof RotatingKVCache,
|
|
295
|
+
);
|
|
296
|
+
for (const c of proto) c.dispose();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return this.#kvSchemeBatchable;
|
|
300
|
+
}
|
|
301
|
+
|
|
149
302
|
/** Decide whether a request joins the batch or runs serially. */
|
|
150
303
|
willBatch(shape: RequestShape): boolean {
|
|
151
304
|
// DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
|
|
@@ -164,7 +317,21 @@ export class GenerationGateway {
|
|
|
164
317
|
!shape.hasAdapters &&
|
|
165
318
|
!shape.wantsLogprobs &&
|
|
166
319
|
!shape.userSeed &&
|
|
167
|
-
|
|
320
|
+
// Phase 3.1: a kv-quant request batches when the server's scheme is
|
|
321
|
+
// the batchable kvConfig composition (the scheduler applies it);
|
|
322
|
+
// otherwise it routes serial exactly as before.
|
|
323
|
+
!(shape.kvQuant && !this.#kvBatchable()) &&
|
|
324
|
+
// TurboQuant: solo-only in v1, unconditionally — never partially
|
|
325
|
+
// batchable like kvConfig. The automatic instanceof exclusion in
|
|
326
|
+
// #modelCachesBatchable() already covers this once a cache converts;
|
|
327
|
+
// this is the explicit belt-and-braces refusal at request-shape time
|
|
328
|
+
// (mirrors the kvQuant pattern above — both layers exist on purpose).
|
|
329
|
+
!shape.turboQuant &&
|
|
330
|
+
!shape.hasDraft &&
|
|
331
|
+
// Grammar: B1 makes it batchable (per-row matchers) unless the kill
|
|
332
|
+
// switch forces serial. MLX_BUN_GRAMMAR_BATCH=0 = B0 behavior (serial),
|
|
333
|
+
// the A/B + kill lever for the new code, house style.
|
|
334
|
+
!(shape.hasGrammar && process.env.MLX_BUN_GRAMMAR_BATCH === "0")
|
|
168
335
|
);
|
|
169
336
|
}
|
|
170
337
|
|
|
@@ -240,6 +407,15 @@ export class GenerationGateway {
|
|
|
240
407
|
if (cur !== logits1V) cur.dispose();
|
|
241
408
|
cur = next;
|
|
242
409
|
}
|
|
410
|
+
// Grammar mask (B1): after the standard processors, before the sampler —
|
|
411
|
+
// same "grammar has the final say" ordering as serial sampleStep.
|
|
412
|
+
// Precondition: the scheduler has awaited ready() before invoking this
|
|
413
|
+
// closure (the mask for this step is materialized). applyMask is sync.
|
|
414
|
+
if (options.grammar) {
|
|
415
|
+
const masked = options.grammar.applyMask(cur);
|
|
416
|
+
if (cur !== logits1V) cur.dispose();
|
|
417
|
+
cur = masked;
|
|
418
|
+
}
|
|
243
419
|
const lp = toLogprobs(cur);
|
|
244
420
|
if (cur !== logits1V) cur.dispose();
|
|
245
421
|
const tok = sampler(lp, step);
|
|
@@ -255,17 +431,37 @@ export class GenerationGateway {
|
|
|
255
431
|
};
|
|
256
432
|
|
|
257
433
|
let st;
|
|
434
|
+
this.#rowsSubmitted++;
|
|
258
435
|
try {
|
|
259
436
|
st = await this.#ensureScheduler().submit({
|
|
260
437
|
promptIds,
|
|
261
438
|
maxTokens: options.maxTokens ?? 512,
|
|
262
439
|
eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
|
|
263
440
|
sample,
|
|
441
|
+
// Vectorized-sampling eligibility: mirrors makeSampler's greedy branch
|
|
442
|
+
// (temperature 0, no curve override) with nothing per-row in the way
|
|
443
|
+
// (no processors, no grammar mask).
|
|
444
|
+
plainGreedy:
|
|
445
|
+
(options.temperature ?? 0) === 0 &&
|
|
446
|
+
!options.curve &&
|
|
447
|
+
processors.length === 0 &&
|
|
448
|
+
!options.grammar,
|
|
264
449
|
onToken,
|
|
450
|
+
// B1: pass the per-row grammar controller through. The scheduler drives
|
|
451
|
+
// accept/ready/terminate; this gateway OWNS disposal (finally below)
|
|
452
|
+
// across resolve, reject, eviction, and the whole-batch-drop error path.
|
|
453
|
+
...(options.grammar ? { grammar: options.grammar } : {}),
|
|
454
|
+
// Stable cache boundary → the scheduler's trim-free prompt-prefix
|
|
455
|
+
// snapshot (same invariant as the serial lane's snapshotAt).
|
|
456
|
+
...(options.snapshotAt !== undefined ? { snapshotAt: options.snapshotAt } : {}),
|
|
265
457
|
});
|
|
266
458
|
} finally {
|
|
267
459
|
history?.dispose();
|
|
268
460
|
history = null;
|
|
461
|
+
// The controller's WASM matcher/compiled-grammar are per-request; free
|
|
462
|
+
// them on every exit path. (generate()'s finally does this on the serial
|
|
463
|
+
// lane; the batch lane owns it here since the scheduler never does.)
|
|
464
|
+
options.grammar?.dispose();
|
|
269
465
|
}
|
|
270
466
|
|
|
271
467
|
return {
|
|
@@ -281,6 +477,13 @@ export class GenerationGateway {
|
|
|
281
477
|
if (!this.#scheduler)
|
|
282
478
|
this.#scheduler = new BatchScheduler(this.model, {
|
|
283
479
|
maxBatch: this.#batch,
|
|
480
|
+
kvBudgetBytes: this.opts.kvBudgetBytes,
|
|
481
|
+
// Phase 3.1: the batchable kvConfig composition is applied by the
|
|
482
|
+
// scheduler (solo rows convert at serial chunk boundaries, then
|
|
483
|
+
// merge as quantized triples). Only threaded when the scheme
|
|
484
|
+
// passed #kvBatchable — otherwise those requests never reach here.
|
|
485
|
+
kvConfig: this.#kvBatchable() ? this.opts.kvScheme?.kvConfig : undefined,
|
|
486
|
+
promptCache: this.opts.promptCache,
|
|
284
487
|
lock: { acquire: () => this.#mutex.acquire() },
|
|
285
488
|
// Drain: pause admission while any serial-lane request waits; the
|
|
286
489
|
// runExclusive finally-block kick()s the loop back awake.
|