mlx-bun 0.0.10 → 0.0.12
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 +107 -32
- package/THIRD_PARTY_LICENSES.md +10 -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/colibri-glm52-port.md +1479 -0
- 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/pre-colibri-stabilization.md +329 -0
- 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/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- 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/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +63 -18
- package/docs/reference/distribution.md +37 -6
- package/docs/reference/embedding.md +40 -16
- package/docs/reference/features-matrix.md +99 -0
- package/docs/reference/library-api.md +69 -20
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +60 -3
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +717 -24
- package/docs/reference/server-config.md +265 -129
- package/docs/reference/training.md +24 -16
- package/package.json +5 -3
- package/src/anthropic.ts +67 -44
- 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/chat-template.ts +155 -10
- package/src/cli.ts +483 -113
- package/src/config.ts +116 -24
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +143 -12
- package/src/eval/README.md +7 -6
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +17 -8
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-offload.ts +15 -2
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/flags.ts +13 -0
- package/src/generate.ts +438 -35
- package/src/grammar.ts +585 -0
- package/src/hf-push.ts +1 -1
- package/src/hub-rest.ts +242 -0
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +586 -93
- package/src/lora.ts +85 -14
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +426 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +149 -19
- package/src/mlx/autograd.ts +53 -16
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- 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 +103 -6
- package/src/model/gemma4-base.ts +395 -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/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- 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 +19 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- 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/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +1050 -78
- package/src/serve/generation-gateway.ts +252 -24
- 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 +1701 -175
- 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/glm52-mtp-source.ts +392 -0
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +617 -0
- package/src/spec/source.ts +118 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +111 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +438 -22
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -12
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +60 -30
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1197 -1791
- package/src/web/app.js +5694 -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 +300 -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/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
- 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
|
@@ -20,43 +20,70 @@
|
|
|
20
20
|
// curve endpoints, adapter mount/unmount) goes through runExclusive() so there
|
|
21
21
|
// is exactly ONE mutual-exclusion domain.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
// dynamic-B-capable type
|
|
25
|
-
// server.py's all-caches-have-merge check
|
|
26
|
-
// merge/filter
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// follow-up) AND no vision AND no LoRA adapters AND no repetition penalty
|
|
30
|
-
// (per-row logits processors are a later refinement) AND no user-fixed seed
|
|
23
|
+
// Batchable gate (the rest → serial): batch>1 AND every model cache is a
|
|
24
|
+
// dynamic-B-capable built-in type or implements BatchableCache (mirrors mlx-lm
|
|
25
|
+
// server.py's all-caches-have-merge check). GLM's capability owns compressed
|
|
26
|
+
// MLA/DSA merge/extract/filter and byte projection; Qwen3.5's SSM cache owns
|
|
27
|
+
// its row state. The remaining request-shape exclusions include vision, LoRA,
|
|
28
|
+
// serial-only KV modes, and user-fixed seed
|
|
31
29
|
// (reproducibility ⇒ solo, matching mlx-lm's _is_batchable). Temperature /
|
|
32
30
|
// top-p / top-k DO batch (each row samples with its own seed). Full-attention
|
|
33
31
|
// AND sliding-window (Gemma) models both batch — the scheduler assembles each
|
|
34
|
-
// layer's cache by type. Prompt-cache prefix reuse
|
|
35
|
-
//
|
|
32
|
+
// layer's cache by type. Prompt-cache prefix reuse works on the batch lane
|
|
33
|
+
// too (Phase 3.2): joiners take() at admission; never-merged lone rows put()
|
|
34
|
+
// back on finish (merged rows' KV is not extracted — their entries age out).
|
|
36
35
|
|
|
37
36
|
import { MlxArray } from "../mlx/array";
|
|
38
37
|
import * as ops from "../mlx/ops";
|
|
39
38
|
import type { RuntimeModel } from "../model/factory";
|
|
40
39
|
import { DiffusionGemmaModel } from "../model/diffusion-gemma";
|
|
41
|
-
import { KVCache, RotatingKVCache } from "../model/gemma4-base";
|
|
40
|
+
import { KVCache, RotatingKVCache, isBatchableCache } from "../model/gemma4-base";
|
|
42
41
|
import { SSMCache } from "../model/qwen3-delta";
|
|
42
|
+
import { UniversalDenseModel } from "../model/universal/dense";
|
|
43
43
|
import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
|
|
44
|
+
import type { KvQuantSpec, TurboQuantScheme } from "../config";
|
|
44
45
|
import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
|
|
45
|
-
import { BatchScheduler } from "./batch-scheduler";
|
|
46
|
+
import { BatchScheduler, type RowPromptCache } from "./batch-scheduler";
|
|
46
47
|
|
|
47
48
|
/** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
|
|
48
49
|
class AsyncMutex {
|
|
49
50
|
#tail: Promise<void> = Promise.resolve();
|
|
51
|
+
/** Holders + waiters. > 0 means somebody owns or wants the engine. */
|
|
52
|
+
#pending = 0;
|
|
53
|
+
get locked(): boolean {
|
|
54
|
+
return this.#pending > 0;
|
|
55
|
+
}
|
|
50
56
|
acquire(): Promise<() => void> {
|
|
57
|
+
this.#pending++;
|
|
51
58
|
let release!: () => void;
|
|
52
59
|
const gate = new Promise<void>((r) => { release = r; });
|
|
53
60
|
const wait = this.#tail;
|
|
54
61
|
this.#tail = this.#tail.then(() => gate);
|
|
55
|
-
|
|
62
|
+
let released = false;
|
|
63
|
+
return wait.then(() => () => {
|
|
64
|
+
if (released) return; // idempotent: a double release must not skew #pending
|
|
65
|
+
released = true;
|
|
66
|
+
this.#pending--;
|
|
67
|
+
release();
|
|
68
|
+
});
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
59
|
-
|
|
72
|
+
/** Embeddings-prefill payload for media prompts (vision and/or audio — the
|
|
73
|
+
* name predates audio). Any request carrying one routes to the serial lane
|
|
74
|
+
* (shape.hasVision) and prefills through generate()'s promptEmbeddings
|
|
75
|
+
* path, bypassing the prompt cache. */
|
|
76
|
+
export type Vision = {
|
|
77
|
+
embeddings: MlxArray;
|
|
78
|
+
/** bool [L] image-token mask for the bidirectional attention overlay.
|
|
79
|
+
* Absent when the prompt carries ANY audio — audio(-containing) prompts
|
|
80
|
+
* run fully causal (audio-input-plan.md §3.3 Q1). */
|
|
81
|
+
imageMask?: MlxArray;
|
|
82
|
+
/** bool [L] union multimodal soft-token mask (image | audio) for
|
|
83
|
+
* per-layer-input id zeroing. Absent on the legacy vision-only shape,
|
|
84
|
+
* where zeroing falls back to imageMask. */
|
|
85
|
+
multimodalMask?: MlxArray;
|
|
86
|
+
};
|
|
60
87
|
|
|
61
88
|
/** Per-token sink: returning `false` halts this generation (stop sequence).
|
|
62
89
|
* `logprobs` is only populated on the serial lane when the request asked for
|
|
@@ -84,41 +111,87 @@ export interface RequestShape {
|
|
|
84
111
|
userSeed: boolean;
|
|
85
112
|
/** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
|
|
86
113
|
kvQuant: boolean;
|
|
114
|
+
/** TurboQuant is active (docs/design/turboquant-kv.md). Solo-only in v1:
|
|
115
|
+
* TurboQuantKVCache is a novel Cache implementation (not a KVCache/
|
|
116
|
+
* RotatingKVCache subclass), so #modelCachesBatchable() already excludes
|
|
117
|
+
* it automatically once a request's cache is converted — this flag is the
|
|
118
|
+
* BELT on top of that automatic BRACES, an explicit refusal at the
|
|
119
|
+
* request-shape level so a turbo request never reaches the scheduler even
|
|
120
|
+
* before any cache conversion has happened. Both layers exist on purpose. */
|
|
121
|
+
turboQuant: boolean;
|
|
87
122
|
/** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
|
|
88
|
-
* logit_bias, presence/frequency penalty.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
123
|
+
* logit_bias, presence/frequency penalty. INFORMATIONAL ONLY: since the
|
|
124
|
+
* batched lane grew per-row samplers/logits processors these all batch —
|
|
125
|
+
* willBatch() deliberately does not gate on this field (see its
|
|
126
|
+
* repetitionPenalty note). Kept so /stats and lane tracing can show what
|
|
127
|
+
* a request carries. */
|
|
93
128
|
hasLogitsExtras: boolean;
|
|
129
|
+
/** A grammar controller compiled for this request (response_format /
|
|
130
|
+
* guided_*). Degrade-path requests (compile failed → prompt injection)
|
|
131
|
+
* have NO controller and stay batchable — the injection already happened
|
|
132
|
+
* at the prompt level. B0 routes grammar to serial; B1 makes it batchable
|
|
133
|
+
* (per-row matchers), with MLX_BUN_GRAMMAR_BATCH=0 forcing the B0 serial
|
|
134
|
+
* fallback as the A/B + kill switch. */
|
|
135
|
+
hasGrammar: boolean;
|
|
94
136
|
/** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
|
|
95
137
|
* are deferred (the scheduler's per-row sampler doesn't capture or read
|
|
96
138
|
* back logprob arrays yet), so these route to the serial lane like the
|
|
97
139
|
* other mlx-lm request extensions above. */
|
|
98
140
|
wantsLogprobs: boolean;
|
|
141
|
+
/** A draft model is configured (serve --draft-model). Server-level and
|
|
142
|
+
* upstream-parity: mlx_lm.server sets is_batchable = (draft is None), so
|
|
143
|
+
* every request routes serial while a draft is mounted — speculation is a
|
|
144
|
+
* B=1 latency optimization, batching a throughput one; they are different
|
|
145
|
+
* modes by design (grammar-spec-batching-integration.md). */
|
|
146
|
+
hasDraft: boolean;
|
|
99
147
|
}
|
|
100
148
|
|
|
101
149
|
export class GenerationGateway {
|
|
102
150
|
readonly #mutex = new AsyncMutex();
|
|
103
151
|
readonly #batch: number;
|
|
104
152
|
#scheduler: BatchScheduler | null = null;
|
|
153
|
+
#rowsSubmitted = 0;
|
|
105
154
|
/** Serial-lane requests waiting for / holding the mutex. While > 0 the
|
|
106
155
|
* scheduler pauses admission (drain) so they can't be starved. */
|
|
107
156
|
#serialWaiters = 0;
|
|
108
157
|
/** Lazy, memoized: can this model's caches do the dynamic-B ops? */
|
|
109
158
|
#cacheBatchable: boolean | null = null;
|
|
159
|
+
/** Lazy, memoized: can the server's kv scheme run under batching?
|
|
160
|
+
* (Phase 3.1: kvConfig whose every configured layerIdx is a plain
|
|
161
|
+
* full-attention KVCache — uniform kvBits and rotating-layer configs
|
|
162
|
+
* stay serial. The scheme is a server-lifetime constant, so one memo.) */
|
|
163
|
+
#kvSchemeBatchable: boolean | null = null;
|
|
110
164
|
|
|
111
165
|
constructor(
|
|
112
166
|
private readonly model: RuntimeModel,
|
|
113
167
|
batch: number,
|
|
114
168
|
private readonly serialRun: SerialRun,
|
|
169
|
+
private readonly opts: {
|
|
170
|
+
kvBudgetBytes?: number;
|
|
171
|
+
/** The server-wide KV scheme (server.ts kvScheme) — threaded to the
|
|
172
|
+
* scheduler at construction when batchable (Phase 3.1). turboQuant is
|
|
173
|
+
* never threaded to the scheduler (always solo-only — see willBatch);
|
|
174
|
+
* it's here only so the gateway can see it's active. */
|
|
175
|
+
kvScheme?: { kvBits?: number; kvConfig?: KvQuantSpec[]; turboQuant?: TurboQuantScheme };
|
|
176
|
+
/** The server's prompt cache (Phase 3.2): batch-lane joiners take()
|
|
177
|
+
* the longest usable prefix at admission; never-merged rows put()
|
|
178
|
+
* back on finish. Safe to share with the serial lane — both use it
|
|
179
|
+
* only inside this gateway's mutual-exclusion domain. */
|
|
180
|
+
promptCache?: RowPromptCache;
|
|
181
|
+
} = {},
|
|
115
182
|
) {
|
|
116
183
|
this.#batch = Math.max(1, Math.floor(batch));
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
/** True if `--batch N` (N>1) is on (batchability is then per-request). */
|
|
120
187
|
get batchingEnabled(): boolean {
|
|
121
|
-
return this
|
|
188
|
+
return this.batchMode === "batch";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Truthful configured/model-capability mode for stats and discovery. */
|
|
192
|
+
get batchMode(): "off" | "serial" | "batch" {
|
|
193
|
+
if (this.#batch <= 1) return "off";
|
|
194
|
+
return this.#modelCachesBatchable() ? "batch" : "serial";
|
|
122
195
|
}
|
|
123
196
|
|
|
124
197
|
/** Rows currently decoding in the batch (0 if no scheduler / idle). */
|
|
@@ -126,6 +199,45 @@ export class GenerationGateway {
|
|
|
126
199
|
return this.#scheduler?.activeRows ?? 0;
|
|
127
200
|
}
|
|
128
201
|
|
|
202
|
+
/** Queued + mid-prefill rows waiting behind the batch (0 when idle). */
|
|
203
|
+
get pendingRows(): number {
|
|
204
|
+
return this.#scheduler?.pendingRows ?? 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** True while ANY engine work is active or queued, on EITHER lane: the
|
|
208
|
+
* mutex is held/awaited (a serial generation, the batch scheduler's whole
|
|
209
|
+
* active period, curve endpoints, adapter mounts) or batch rows are
|
|
210
|
+
* running/pending. activeRows/pendingRows alone MISS the serial lane —
|
|
211
|
+
* a serial generation holds the mutex but shows zero rows. */
|
|
212
|
+
get busy(): boolean {
|
|
213
|
+
return this.#mutex.locked || this.activeRows > 0 || this.pendingRows > 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Resolves once the engine is idle (poll-based, ~`pollMs` granularity —
|
|
217
|
+
* cheap vs the multi-hundred-ms flushes it paces; resolves without a
|
|
218
|
+
* timer when already idle). The SSD write-behind gates each per-tensor
|
|
219
|
+
* flush step on this so durability work never steals blocking
|
|
220
|
+
* GPU-sync + writeSync slices from an active decode (the 2026-07-07
|
|
221
|
+
* decode@ctx contamination — see server.ts's write-behind block). */
|
|
222
|
+
async onIdle(pollMs = 20): Promise<void> {
|
|
223
|
+
while (this.busy) await new Promise<void>((r) => setTimeout(r, pollMs));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Cumulative rows routed to the batch lane since server start. The serial
|
|
227
|
+
* lane never advances this — /stats' race-free lane-routing observable
|
|
228
|
+
* (active/pending are instantaneous and read 0 once a request finishes). */
|
|
229
|
+
get submittedRows(): number {
|
|
230
|
+
return this.#rowsSubmitted;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Projected aggregate KV bytes of admitted rows / the --kv-budget cap. */
|
|
234
|
+
get kvBytes(): { projected: number; budget: number | null } {
|
|
235
|
+
return {
|
|
236
|
+
projected: this.#scheduler?.projectedKvBytes ?? 0,
|
|
237
|
+
budget: this.#scheduler?.kvBudgetBytes ?? this.opts.kvBudgetBytes ?? null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
129
241
|
/** Cache-capability gate (mirrors mlx-lm server.py's all-caches-have-merge
|
|
130
242
|
* check): the scheduler's dynamic-B ops (temporalView/merge/filter) exist
|
|
131
243
|
* on KVCache, RotatingKVCache, and SSMCache (hybrid gated-DeltaNet models
|
|
@@ -133,12 +245,28 @@ export class GenerationGateway {
|
|
|
133
245
|
* the kill switch back to serial routing). */
|
|
134
246
|
#modelCachesBatchable(): boolean {
|
|
135
247
|
if (this.#cacheBatchable === null) {
|
|
248
|
+
// Tier-0 universal models: batchable for PLAIN full-attention archs
|
|
249
|
+
// only. The 2026-07-03 uneven-row bug (scalar-offset RoPE decoded
|
|
250
|
+
// padded rows at wrong positions) is fixed via
|
|
251
|
+
// UniversalRope.applyDynamic + ropeOffsetArr and GATED token-exact vs
|
|
252
|
+
// mlx-lm B=2 on Llama-3.2-3B (static + dynamic join/leave,
|
|
253
|
+
// tests/batched-decode-parity.test.ts "Llama 3B Tier-0"). The
|
|
254
|
+
// maskArray archs (gemma2-family: forwardLayers builds a pad-blind
|
|
255
|
+
// causal mask) and sliding-window universal archs remain UNVALIDATED
|
|
256
|
+
// cells → serial, per the per-model-cell discipline.
|
|
257
|
+
if (this.model instanceof UniversalDenseModel) {
|
|
258
|
+
const a = this.model.args;
|
|
259
|
+
this.#cacheBatchable =
|
|
260
|
+
!a.maskArray && !a.layerTypes?.includes("sliding_attention");
|
|
261
|
+
return this.#cacheBatchable;
|
|
262
|
+
}
|
|
136
263
|
const ssmOk = process.env.MLX_BUN_BATCH_SSM !== "0";
|
|
137
264
|
const proto = this.model.makeCache(); // fresh caches hold no buffers
|
|
138
265
|
this.#cacheBatchable = proto.every(
|
|
139
266
|
(c) =>
|
|
140
267
|
c instanceof KVCache ||
|
|
141
268
|
c instanceof RotatingKVCache ||
|
|
269
|
+
isBatchableCache(c) ||
|
|
142
270
|
(ssmOk && c instanceof SSMCache),
|
|
143
271
|
);
|
|
144
272
|
for (const c of proto) c.dispose();
|
|
@@ -146,6 +274,36 @@ export class GenerationGateway {
|
|
|
146
274
|
return this.#cacheBatchable;
|
|
147
275
|
}
|
|
148
276
|
|
|
277
|
+
/** Phase 3.1 + milestone 2: the server's kv scheme batches iff it is a
|
|
278
|
+
* per-layer kvConfig (the L2 mixed scheme) whose every configured layer
|
|
279
|
+
* is a full-attention KVCache (BatchedQuantDecodeMaskCache + quantized
|
|
280
|
+
* merge/extend/filter) or a rotating RotatingKVCache
|
|
281
|
+
* (BatchedRotatingQuantCache — milestone 2, gemma's kv_config). Uniform
|
|
282
|
+
* kvBits (quantizedKvStart threshold semantics via the serial no-byLayer
|
|
283
|
+
* path) and configs naming SSM layers stay serial. */
|
|
284
|
+
#kvBatchable(): boolean {
|
|
285
|
+
if (this.#kvSchemeBatchable === null) {
|
|
286
|
+
const scheme = this.opts.kvScheme;
|
|
287
|
+
if (!scheme || (!scheme.kvBits && !scheme.kvConfig?.length)) {
|
|
288
|
+
// Only consulted when shape.kvQuant is SET: a kv-quant request with
|
|
289
|
+
// no scheme threaded to the gateway cannot be applied by the
|
|
290
|
+
// scheduler — batching it would SILENTLY DROP the quantization (the
|
|
291
|
+
// exact composition bug optiq serve ships). Route serial.
|
|
292
|
+
this.#kvSchemeBatchable = false;
|
|
293
|
+
} else if (scheme.kvBits || !scheme.kvConfig?.length) {
|
|
294
|
+
this.#kvSchemeBatchable = false; // uniform bits: serial
|
|
295
|
+
} else {
|
|
296
|
+
const proto = this.model.makeCache();
|
|
297
|
+
this.#kvSchemeBatchable = scheme.kvConfig.every(
|
|
298
|
+
(e) => proto[e.layerIdx] instanceof KVCache ||
|
|
299
|
+
proto[e.layerIdx] instanceof RotatingKVCache,
|
|
300
|
+
);
|
|
301
|
+
for (const c of proto) c.dispose();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return this.#kvSchemeBatchable;
|
|
305
|
+
}
|
|
306
|
+
|
|
149
307
|
/** Decide whether a request joins the batch or runs serially. */
|
|
150
308
|
willBatch(shape: RequestShape): boolean {
|
|
151
309
|
// DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
|
|
@@ -164,7 +322,21 @@ export class GenerationGateway {
|
|
|
164
322
|
!shape.hasAdapters &&
|
|
165
323
|
!shape.wantsLogprobs &&
|
|
166
324
|
!shape.userSeed &&
|
|
167
|
-
|
|
325
|
+
// Phase 3.1: a kv-quant request batches when the server's scheme is
|
|
326
|
+
// the batchable kvConfig composition (the scheduler applies it);
|
|
327
|
+
// otherwise it routes serial exactly as before.
|
|
328
|
+
!(shape.kvQuant && !this.#kvBatchable()) &&
|
|
329
|
+
// TurboQuant: solo-only in v1, unconditionally — never partially
|
|
330
|
+
// batchable like kvConfig. The automatic instanceof exclusion in
|
|
331
|
+
// #modelCachesBatchable() already covers this once a cache converts;
|
|
332
|
+
// this is the explicit belt-and-braces refusal at request-shape time
|
|
333
|
+
// (mirrors the kvQuant pattern above — both layers exist on purpose).
|
|
334
|
+
!shape.turboQuant &&
|
|
335
|
+
!shape.hasDraft &&
|
|
336
|
+
// Grammar: B1 makes it batchable (per-row matchers) unless the kill
|
|
337
|
+
// switch forces serial. MLX_BUN_GRAMMAR_BATCH=0 = B0 behavior (serial),
|
|
338
|
+
// the A/B + kill lever for the new code, house style.
|
|
339
|
+
!(shape.hasGrammar && process.env.MLX_BUN_GRAMMAR_BATCH === "0")
|
|
168
340
|
);
|
|
169
341
|
}
|
|
170
342
|
|
|
@@ -197,7 +369,16 @@ export class GenerationGateway {
|
|
|
197
369
|
onToken: OnToken,
|
|
198
370
|
vision: Vision | undefined,
|
|
199
371
|
shape: RequestShape,
|
|
372
|
+
signal?: AbortSignal,
|
|
200
373
|
): Promise<GenerateStats> {
|
|
374
|
+
try {
|
|
375
|
+
signal?.throwIfAborted();
|
|
376
|
+
} catch (e) {
|
|
377
|
+
// The gateway is the first component that accepts ownership of a
|
|
378
|
+
// compiled controller. An already-aborted request reaches neither lane.
|
|
379
|
+
options.grammar?.dispose();
|
|
380
|
+
throw e;
|
|
381
|
+
}
|
|
201
382
|
if (!this.willBatch(shape)) {
|
|
202
383
|
// Serial decode is an unbroken microtask chain (FFI + generator
|
|
203
384
|
// resumes): without a periodic macrotask hop the event loop starves for
|
|
@@ -210,14 +391,26 @@ export class GenerationGateway {
|
|
|
210
391
|
let lastHop = performance.now();
|
|
211
392
|
const hop = (): Promise<void> => new Promise<void>((r) => setImmediate(r));
|
|
212
393
|
const hoppingOnToken: OnToken = (token, lp) => {
|
|
394
|
+
if (signal?.aborted) return false;
|
|
213
395
|
const r = onToken(token, lp);
|
|
214
396
|
if (r === false) return false;
|
|
215
397
|
if (performance.now() - lastHop < 25) return r;
|
|
216
398
|
lastHop = performance.now();
|
|
217
|
-
if (r instanceof Promise)
|
|
218
|
-
|
|
399
|
+
if (r instanceof Promise)
|
|
400
|
+
return r.then((v) =>
|
|
401
|
+
v === false || signal?.aborted ? false : hop().then(() => signal?.aborted ? false : v),
|
|
402
|
+
);
|
|
403
|
+
return hop().then(() => signal?.aborted ? false : r);
|
|
219
404
|
};
|
|
220
|
-
return this.runExclusive(() =>
|
|
405
|
+
return this.runExclusive(async () => {
|
|
406
|
+
signal?.throwIfAborted();
|
|
407
|
+
const stats = await this.serialRun(promptIds, options, hoppingOnToken, vision);
|
|
408
|
+
signal?.throwIfAborted();
|
|
409
|
+
return stats;
|
|
410
|
+
})
|
|
411
|
+
// Covers a serial waiter aborted before serialRun takes ownership.
|
|
412
|
+
// generate() also disposes defensively; the operation is idempotent.
|
|
413
|
+
.finally(() => options.grammar?.dispose());
|
|
221
414
|
}
|
|
222
415
|
|
|
223
416
|
// Per-row sampler, mirroring generate()'s sampleStep: logits processors
|
|
@@ -240,6 +433,15 @@ export class GenerationGateway {
|
|
|
240
433
|
if (cur !== logits1V) cur.dispose();
|
|
241
434
|
cur = next;
|
|
242
435
|
}
|
|
436
|
+
// Grammar mask (B1): after the standard processors, before the sampler —
|
|
437
|
+
// same "grammar has the final say" ordering as serial sampleStep.
|
|
438
|
+
// Precondition: the scheduler has awaited ready() before invoking this
|
|
439
|
+
// closure (the mask for this step is materialized). applyMask is sync.
|
|
440
|
+
if (options.grammar) {
|
|
441
|
+
const masked = options.grammar.applyMask(cur);
|
|
442
|
+
if (cur !== logits1V) cur.dispose();
|
|
443
|
+
cur = masked;
|
|
444
|
+
}
|
|
243
445
|
const lp = toLogprobs(cur);
|
|
244
446
|
if (cur !== logits1V) cur.dispose();
|
|
245
447
|
const tok = sampler(lp, step);
|
|
@@ -255,17 +457,36 @@ export class GenerationGateway {
|
|
|
255
457
|
};
|
|
256
458
|
|
|
257
459
|
let st;
|
|
460
|
+
this.#rowsSubmitted++;
|
|
258
461
|
try {
|
|
259
462
|
st = await this.#ensureScheduler().submit({
|
|
260
463
|
promptIds,
|
|
261
464
|
maxTokens: options.maxTokens ?? 512,
|
|
262
465
|
eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
|
|
263
466
|
sample,
|
|
467
|
+
// Vectorized-sampling eligibility: mirrors makeSampler's greedy branch
|
|
468
|
+
// (temperature 0, no curve override) with nothing per-row in the way
|
|
469
|
+
// (no processors, no grammar mask).
|
|
470
|
+
plainGreedy:
|
|
471
|
+
(options.temperature ?? 0) === 0 &&
|
|
472
|
+
!options.curve &&
|
|
473
|
+
processors.length === 0 &&
|
|
474
|
+
!options.grammar,
|
|
264
475
|
onToken,
|
|
476
|
+
...(signal ? { signal } : {}),
|
|
477
|
+
// B1: pass the per-row grammar controller through. The scheduler drives
|
|
478
|
+
// accept/ready/terminate; this gateway OWNS disposal (finally below)
|
|
479
|
+
// across resolve, reject, eviction, and the whole-batch-drop error path.
|
|
480
|
+
...(options.grammar ? { grammar: options.grammar } : {}),
|
|
481
|
+
// Stable cache boundary → the scheduler's trim-free prompt-prefix
|
|
482
|
+
// snapshot (same invariant as the serial lane's snapshotAt).
|
|
483
|
+
...(options.snapshotAt !== undefined ? { snapshotAt: options.snapshotAt } : {}),
|
|
265
484
|
});
|
|
266
485
|
} finally {
|
|
267
486
|
history?.dispose();
|
|
268
487
|
history = null;
|
|
488
|
+
// The scheduler never owns per-row grammar state.
|
|
489
|
+
options.grammar?.dispose();
|
|
269
490
|
}
|
|
270
491
|
|
|
271
492
|
return {
|
|
@@ -281,6 +502,13 @@ export class GenerationGateway {
|
|
|
281
502
|
if (!this.#scheduler)
|
|
282
503
|
this.#scheduler = new BatchScheduler(this.model, {
|
|
283
504
|
maxBatch: this.#batch,
|
|
505
|
+
kvBudgetBytes: this.opts.kvBudgetBytes,
|
|
506
|
+
// Phase 3.1: the batchable kvConfig composition is applied by the
|
|
507
|
+
// scheduler (solo rows convert at serial chunk boundaries, then
|
|
508
|
+
// merge as quantized triples). Only threaded when the scheme
|
|
509
|
+
// passed #kvBatchable — otherwise those requests never reach here.
|
|
510
|
+
kvConfig: this.#kvBatchable() ? this.opts.kvScheme?.kvConfig : undefined,
|
|
511
|
+
promptCache: this.opts.promptCache,
|
|
284
512
|
lock: { acquire: () => this.#mutex.acquire() },
|
|
285
513
|
// Drain: pause admission while any serial-lane request waits; the
|
|
286
514
|
// runExclusive finally-block kick()s the loop back awake.
|