mlx-bun 0.0.13 → 0.2.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 +31 -21
- package/bin/mlx-bun.mjs +1 -1
- package/docs/{design → archive}/hlg-sampling.md +3 -3
- package/docs/{investigations → archive/investigations}/hlg-sampling-investigation.md +1 -1
- package/docs/archive/investigations/pareto-specialized-runtime-findings.md +92 -0
- package/docs/{planning → archive/planning}/chat-ui-vision.md +1 -1
- package/docs/{planning → archive/planning}/release-notes-v0.0.9.md +1 -1
- package/docs/{planning → archive/planning}/website-readme-pass-plan.md +1 -1
- package/docs/design/colibri-glm52-port.md +1 -1
- package/docs/design/compat-cli-surface-design.md +1 -1
- package/docs/design/decode-speed-program.md +1 -1
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +1 -1
- package/docs/design/dspark-speculative-decoding.md +53 -3
- package/docs/design/grammar-spec-batching-integration.md +1 -1
- package/docs/design/mlx-lm-serving-execution-seam.md +81 -0
- package/docs/design/orpo-dynamic-lambda.md +1 -1
- package/docs/design/orpo-future-enhancements.md +1 -1
- package/docs/design/orpo-training.md +2 -2
- package/docs/design/parity-tier-dag.md +1 -1
- package/docs/design/repo-cleanup-plan.md +46 -4
- package/docs/design/segmented-backward-training.md +2 -2
- package/docs/design/ssd-kv-cold-tier.md +127 -0
- package/docs/design/turboquant-weights.md +467 -0
- package/docs/design/w4a16-compute-precision-spike.md +1 -1
- package/docs/design/web-chat-redesign.md +5 -5
- package/docs/planning/chunk-task-roadmap.md +1 -1
- package/docs/planning/curve-sampler-research-plan.md +4 -4
- package/docs/planning/memory-docs-and-dag-plan.md +1 -1
- package/docs/planning/release-notes-v0.2.0.md +49 -0
- package/docs/planning/video-series.md +421 -0
- package/docs/reference/cli.md +15 -4
- package/docs/reference/distribution.md +1 -0
- package/docs/reference/features-matrix.md +3 -2
- package/docs/reference/library-api.md +41 -1
- package/docs/reference/models.md +33 -0
- package/docs/reference/server-api.md +69 -14
- package/docs/reference/server-config.md +57 -33
- package/package.json +2 -2
- package/src/anthropic.ts +92 -3
- package/src/cli.ts +131 -36
- package/src/eval/kl.ts +27 -20
- package/src/eval/runner.ts +23 -4
- package/src/eval/tasks/bfcl.ts +2 -1
- package/src/eval/tasks/gsm8k.ts +2 -1
- package/src/eval/tasks/hashhop.ts +2 -1
- package/src/eval/tasks/humaneval.ts +5 -4
- package/src/eval/tasks/ifeval.ts +2 -1
- package/src/eval/tasks/mmlu.ts +2 -1
- package/src/expert-io.ts +3 -1
- package/src/expert-offload.ts +3 -1
- package/src/expert-trace.ts +3 -1
- package/src/fit.ts +24 -43
- package/src/flags.ts +7 -9
- package/src/generate.ts +107 -92
- package/src/grammar.ts +2 -1
- package/src/harness-pi.ts +1 -1
- package/src/index.ts +20 -0
- package/src/jobs/job-entry.ts +3 -2
- package/src/kv-scheme.ts +199 -0
- package/src/kv-store.ts +370 -205
- package/src/media-fetch.ts +19 -3
- package/src/memory/model.ts +2 -1
- package/src/memory/vault.ts +2 -1
- package/src/memory-plan.ts +18 -0
- package/src/mlx/ffi.ts +8 -11
- package/src/mlx/ops.ts +31 -6
- package/src/model/batched-mask.ts +1 -1
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +43 -97
- package/src/model/batched-rotating-state.ts +73 -0
- package/src/model/batched-rotating.ts +56 -105
- package/src/model/batched-row-storage.ts +127 -0
- package/src/model/compiled-decode.ts +3 -2
- package/src/model/factory.ts +44 -49
- package/src/model/gemma4-base.ts +163 -10
- package/src/model/generated/gemma4-12b.ts +50 -146
- package/src/model/generated/gemma4-26b.ts +32 -92
- package/src/model/generated/gemma4-e4b.ts +26 -74
- package/src/model/glm52-cache.ts +19 -9
- package/src/model/glm52-memory.ts +28 -1
- package/src/model/minicpm5.ts +2 -1
- package/src/model/profile.ts +433 -0
- package/src/model/qwen3-delta.ts +98 -2
- package/src/model/qwen3-mrope.ts +252 -0
- package/src/model/qwen3_5.ts +217 -25
- package/src/native/frame_extract.swift +140 -0
- package/src/native-pack.ts +13 -6
- package/src/pi-web.ts +6 -5
- package/src/prompt-cache.ts +2 -2
- package/src/quantize/atomic-output.ts +38 -0
- package/src/quantize/calibration.ts +2 -1
- package/src/quantize/config-writer.ts +6 -0
- package/src/quantize/index.ts +28 -1
- package/src/quantize/job.ts +11 -0
- package/src/quantize/quantizer.ts +265 -100
- package/src/quantize/rotate.ts +603 -0
- package/src/quantize/safetensors-writer.ts +79 -1
- package/src/quantize/sensitivity.ts +41 -59
- package/src/quantize/weight-transform.ts +292 -0
- package/src/responses.ts +102 -2
- package/src/runtime-config.ts +67 -0
- package/src/sampler.ts +272 -0
- package/src/serve/admin-routes.ts +184 -0
- package/src/serve/aux-routes.ts +132 -0
- package/src/serve/batch-scheduler.ts +200 -87
- package/src/serve/completion-executor.ts +346 -0
- package/src/serve/completion-sink.ts +157 -0
- package/src/serve/discovery-routes.ts +205 -0
- package/src/serve/generation-gateway.ts +116 -109
- package/src/serve/kv-budget.ts +14 -0
- package/src/serve/lab-routes.ts +338 -0
- package/src/serve/model-admin-routes.ts +153 -0
- package/src/serve/prompt-response-trace.ts +147 -0
- package/src/serve/request-plan.ts +158 -0
- package/src/serve/static-routes.ts +101 -0
- package/src/server.ts +704 -1400
- package/src/spec/dflash-source.ts +2 -1
- package/src/spec/dspark/module-dflash.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +8 -0
- package/src/spec/qwen-mtp-source.ts +431 -0
- package/src/spec/serve-loop.ts +36 -68
- package/src/ssd-cache.ts +17 -14
- package/src/ssd-durability.ts +195 -0
- package/src/train/flash-cce.ts +12 -11
- package/src/train/forward.ts +32 -1
- package/src/train/loss.ts +2 -1
- package/src/train/segmented.ts +4 -3
- package/src/train/trainer.ts +3 -2
- package/src/vision/preprocess.ts +1 -1
- package/src/vision/prompt.ts +47 -1
- package/src/vision/qwen3vl-preprocess.ts +409 -0
- package/src/vision/qwen3vl-prompt.ts +196 -0
- package/src/vision/qwen3vl-tower.ts +513 -0
- package/src/vision/video-frames.ts +148 -0
- package/src/web/app.js +10 -3
- package/src/web/src/chat.ts +12 -5
- package/src/weights.ts +81 -26
- /package/docs/{investigations → archive/investigations}/chunk-finetune-experiment.md +0 -0
- /package/docs/{investigations → archive/investigations}/colibri-oracle-pin.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-bisector-routing.md +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-route.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/bisector-trace.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.json +0 -0
- /package/docs/{investigations → archive/investigations}/curve-runs/curve-terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/curve-terrain.html +0 -0
- /package/docs/{investigations → archive/investigations}/decode-roofline-lookagain.md +0 -0
- /package/docs/{investigations → archive/investigations}/dspark-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/expert-offload-single-user-moe.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-ab.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-explorer.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cliff.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/cross.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/edges.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-grids.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/k20-peaks.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/lw-curve.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-figs/terrain.svg +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-report.html +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-ab.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind-prompts.md +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-blind.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cliff.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-cross.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-edge.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-eval1.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-frontier.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-grid2.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-lw-coarse.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/hlg-map.json +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/run0-original.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runA-recommended.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAA-cliff-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAB-grid2-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAC-peak-e4b-partial.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAD-ab-samples.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAE-edge-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAF-logit-calibration.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runAG-eval-config2.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runB-gain-variants.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runC-pivots.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runD-oetf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runE-eotf.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runF-shaper-working.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runG-shaper-sweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runH-wsweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runI-asweep.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runJ-knobmap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runK-frontier.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runL-widemap.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runM-frontier-emb.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runN-verify.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runO-frontier-seed5000.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runP-grid-AxSm.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runQ-grid-tgW.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runR-blind-transcript.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runS-cross-coherence.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runT-cross-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runU-cross-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runV-lw-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runW-lw-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runX-lw-cpm5.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runY-cliff-e4b.log +0 -0
- /package/docs/{investigations → archive/investigations}/hlg-runs/runZ-cliff-12b.log +0 -0
- /package/docs/{investigations → archive/investigations}/indexshare-performance-spike.md +0 -0
- /package/docs/{investigations → archive/investigations}/kernel-perf-review-2026-07.md +0 -0
- /package/docs/{investigations → archive/investigations}/lab-build-journal.md +0 -0
- /package/docs/{investigations → archive/investigations}/openwiki-evaluation.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-base-uf-experiment-and-directions.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-flash-cce-pin-leak.md +0 -0
- /package/docs/{investigations → archive/investigations}/orpo-uf-testing-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/pi-builtin-investigation.md +0 -0
- /package/docs/{investigations → archive/investigations}/segmented-backward-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/starter-model-port-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/steel-flash-cce-handoff.md +0 -0
- /package/docs/{investigations → archive/investigations}/trainer-validation-experiment.md +0 -0
- /package/docs/{planning → archive/planning}/chat-ui-vision-memory-first.md +0 -0
- /package/docs/{planning → archive/planning}/journal.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.10.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.11.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.12.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.0.13.md +0 -0
- /package/docs/{planning → archive/planning}/web-ui-pass-plan.md +0 -0
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// temporal axis and no padding (rows solo-prefill unpadded, decode feeds one
|
|
27
27
|
// real token per row), so merge/filter are B-axis concat/take and the cache
|
|
28
28
|
// passes through the step unwrapped (no mask, no per-row RoPE — full layers
|
|
29
|
-
// carry positions). Gate: GenerationGateway.
|
|
29
|
+
// carry positions). Gate: GenerationGateway.place on cache capability.
|
|
30
30
|
//
|
|
31
31
|
// Engine mechanics (the serial decode loop's hygiene, transplanted —
|
|
32
32
|
// batching-v2-plan step 3):
|
|
@@ -56,13 +56,22 @@
|
|
|
56
56
|
|
|
57
57
|
import { MlxArray } from "../mlx/array";
|
|
58
58
|
import * as ops from "../mlx/ops";
|
|
59
|
-
import { clearCache, Dtype } from "../mlx/ffi";
|
|
59
|
+
import { activeMemory, cacheMemory, clearCache, Dtype, peakMemory } from "../mlx/ffi";
|
|
60
60
|
import { flagOn } from "../flags";
|
|
61
|
+
import { runtimeValue } from "../runtime-config";
|
|
61
62
|
import { CompiledDecode } from "../model/compiled-decode";
|
|
62
63
|
import type { Gemma4Model } from "../model/gemma4";
|
|
63
64
|
import {
|
|
64
65
|
KVCache, QuantizedKVCache, RotatingKVCache, RotatingQuantizedKVCache,
|
|
65
|
-
|
|
66
|
+
cacheSignature,
|
|
67
|
+
isBatchableCache,
|
|
68
|
+
isPlainKvCache,
|
|
69
|
+
isQuantizedKvCache,
|
|
70
|
+
isRotatingPlainCache,
|
|
71
|
+
isRotatingQuantizedCache,
|
|
72
|
+
isRowBatchCache,
|
|
73
|
+
type BatchableCache,
|
|
74
|
+
type Cache,
|
|
66
75
|
} from "../model/gemma4-base";
|
|
67
76
|
import { BatchedDecodeMaskCache, mergeKVRows, extendKVRows, extractKVRow, filterKVRows } from "../model/batched-mask";
|
|
68
77
|
import {
|
|
@@ -71,24 +80,27 @@ import {
|
|
|
71
80
|
} from "../model/batched-quant";
|
|
72
81
|
import { BatchedRotatingQuantCache } from "../model/batched-rotating-quant";
|
|
73
82
|
import type { KvQuantSpec } from "../config";
|
|
83
|
+
import type { KvScheme } from "../kv-scheme";
|
|
74
84
|
import { BatchedRotatingCache } from "../model/batched-rotating";
|
|
75
85
|
import { cloneKvCaches } from "../kv-store";
|
|
76
86
|
import { SSMCache } from "../model/qwen3-delta";
|
|
77
87
|
import type { RuntimeModel } from "../model/factory";
|
|
78
88
|
import type { GrammarController } from "../grammar";
|
|
79
|
-
import { kvBytesAt } from "../fit";
|
|
80
89
|
import { toLogprobs } from "../sampler";
|
|
90
|
+
import { acquireModelWiredLimit } from "../generate";
|
|
91
|
+
import { batchRowKvBytes } from "./kv-budget";
|
|
92
|
+
import type { PromptResponseTrace } from "./prompt-response-trace";
|
|
81
93
|
|
|
82
94
|
/** Decode-pipeline kill switch (read once at load, like the serial loop's
|
|
83
95
|
* MLX_BUN_COMPILED_DECODE): 1 ⇒ read each step's tokens synchronously. */
|
|
84
|
-
const NO_PIPELINE =
|
|
96
|
+
const NO_PIPELINE = runtimeValue("MLX_BUN_BATCH_NO_PIPELINE") === "1";
|
|
85
97
|
|
|
86
98
|
/** Per-step phase timing (MLX_BUN_BATCH_STEP_TRACE=1, debug-only): where a
|
|
87
99
|
* decode step's wall time goes — graph BUILD+dispatch (host), the pipelined
|
|
88
100
|
* READ of the previous step's tokens (GPU wait), row EMIT (onToken/SSE), and
|
|
89
101
|
* the GAP between consecutive steps (drive-loop + everything else). Sums
|
|
90
102
|
* print via `stepTraceReport()` (the b1 profile experiment calls it). */
|
|
91
|
-
const STEP_TRACE =
|
|
103
|
+
const STEP_TRACE = runtimeValue("MLX_BUN_BATCH_STEP_TRACE") === "1";
|
|
92
104
|
const STEP_T = { t0: 0, lastEnd: 0, build: 0, read: 0, emit: 0, gap: 0, n: 0 };
|
|
93
105
|
export function stepTraceReport(): string {
|
|
94
106
|
const per = (x: number) => (STEP_T.n ? (x / STEP_T.n).toFixed(3) : "0");
|
|
@@ -125,6 +137,10 @@ export interface BatchRequest {
|
|
|
125
137
|
* THIS row only (its submit promise rejects; siblings continue). May be
|
|
126
138
|
* async; keep it cheap — it runs inline in the step loop. */
|
|
127
139
|
onToken: (token: number) => void | boolean | Promise<void | boolean>;
|
|
140
|
+
/** Diagnostic-only request-local trace. */
|
|
141
|
+
trace?: PromptResponseTrace;
|
|
142
|
+
/** Closes the gateway's queue/admission span when this row leaves pending. */
|
|
143
|
+
onAdmitted?: () => void;
|
|
128
144
|
/** Client/request lifetime. An aborted pending row is removed before
|
|
129
145
|
* admission; a row in prefill or decode is evicted at the next safe
|
|
130
146
|
* scheduler boundary. */
|
|
@@ -223,6 +239,7 @@ interface PrefillState {
|
|
|
223
239
|
* exactly here, clones + put()s the trim-free prefix entry, then nulls
|
|
224
240
|
* it. null = no snapshot for this row (short prompt / cached past it). */
|
|
225
241
|
snapAt: number | null;
|
|
242
|
+
closePrefill?: () => void;
|
|
226
243
|
}
|
|
227
244
|
|
|
228
245
|
/** Held while the batch is active; the serial fallback acquires the same lock. */
|
|
@@ -248,16 +265,10 @@ export interface BatchSchedulerOptions {
|
|
|
248
265
|
* evict. A request that can NEVER fit (over budget alone, empty batch)
|
|
249
266
|
* is rejected instead of deadlocking. Unset = unlimited (v1 behavior). */
|
|
250
267
|
kvBudgetBytes?: number;
|
|
251
|
-
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
* convert; the GATEWAY admits batching only when every configured
|
|
256
|
-
* layerIdx is such a layer (rotating/uniform stay serial). Each joiner's
|
|
257
|
-
* solo prefill converts at the SAME chunk boundaries as the serial
|
|
258
|
-
* maybeQuantizeKv, so a row's quantized bytes are bit-exact vs serial
|
|
259
|
-
* `--kv-quant config` by construction (the L2-oracle composition rule). */
|
|
260
|
-
kvConfig?: KvQuantSpec[];
|
|
268
|
+
/** Authoritative server-wide scheme. Each joiner's solo prefill converts at
|
|
269
|
+
* the same chunk boundaries as the serial path, so a row's quantized bytes
|
|
270
|
+
* preserve the L2 composition. Unsupported schemes fail at construction. */
|
|
271
|
+
kvScheme?: KvScheme;
|
|
261
272
|
/** Prompt-cache hook (Phase 3.2): admission take()s the longest usable
|
|
262
273
|
* prefix into the joiner's solo caches (suffix-only prefill — the
|
|
263
274
|
* multi-turn chat TTFT path); rows that finish never-merged put() their
|
|
@@ -310,6 +321,7 @@ export class BatchScheduler {
|
|
|
310
321
|
readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
|
|
311
322
|
readonly #compressedProjectors: Array<(tokens: number) => number> | null;
|
|
312
323
|
readonly #batchCacheMaxTokens: number | null;
|
|
324
|
+
readonly #kvScheme: KvScheme | undefined;
|
|
313
325
|
/** layerIdx → mixed-precision spec (Phase 3.1); null = bf16 batch (v1). */
|
|
314
326
|
readonly #kvByLayer: Map<number, KvQuantSpec> | null;
|
|
315
327
|
/** Compiled decode runner for the B=1 serial-class case (Phase 3.2) —
|
|
@@ -326,13 +338,22 @@ export class BatchScheduler {
|
|
|
326
338
|
this.#prefillChunkSize = Math.max(1, Math.floor(opts.prefillChunkSize ?? 2048));
|
|
327
339
|
this.#kvBudgetBytes = opts.kvBudgetBytes;
|
|
328
340
|
this.#promptCache = opts.promptCache;
|
|
341
|
+
this.#kvScheme = opts.kvScheme;
|
|
329
342
|
const proto = model.makeCache(); // fresh caches hold no buffers
|
|
343
|
+
if (this.#kvScheme && !this.#kvScheme.batchable(
|
|
344
|
+
model.config,
|
|
345
|
+
(layerIdx) =>
|
|
346
|
+
isPlainKvCache(proto[layerIdx]) || isRotatingPlainCache(proto[layerIdx]),
|
|
347
|
+
)) {
|
|
348
|
+
for (const cache of proto) cache.dispose();
|
|
349
|
+
throw new Error(`unsupported KV scheme for batch scheduler: ${this.#kvScheme.kind}`);
|
|
350
|
+
}
|
|
330
351
|
this.#kinds = proto.map((c) =>
|
|
331
352
|
isBatchableCache(c)
|
|
332
353
|
? "owned-batch"
|
|
333
|
-
: c
|
|
354
|
+
: isRotatingPlainCache(c)
|
|
334
355
|
? "rot"
|
|
335
|
-
: c
|
|
356
|
+
: cacheSignature(c) === "ssm"
|
|
336
357
|
? "ssm"
|
|
337
358
|
: "full",
|
|
338
359
|
);
|
|
@@ -342,10 +363,11 @@ export class BatchScheduler {
|
|
|
342
363
|
this.#batchCacheMaxTokens = proto.every(isBatchableCache)
|
|
343
364
|
? Math.min(...proto.map((cache) => cache.maxTokens ?? Number.MAX_SAFE_INTEGER))
|
|
344
365
|
: null;
|
|
345
|
-
|
|
346
|
-
|
|
366
|
+
const kvConfig = this.#kvScheme?.options.kvConfig;
|
|
367
|
+
this.#kvByLayer = kvConfig?.length
|
|
368
|
+
? new Map(kvConfig.map((entry) => [entry.layerIdx, entry]))
|
|
347
369
|
: null;
|
|
348
|
-
this.#rotMaxSize = proto.map((c) => (c
|
|
370
|
+
this.#rotMaxSize = proto.map((c) => (isRotatingPlainCache(c) ? c.maxSize : 0));
|
|
349
371
|
for (const c of proto) c.dispose();
|
|
350
372
|
this.#compiled =
|
|
351
373
|
flagOn("MLX_BUN_COMPILED_DECODE", true) &&
|
|
@@ -366,10 +388,17 @@ export class BatchScheduler {
|
|
|
366
388
|
/** Projected KV bytes of one row at its worst case (full prompt + full
|
|
367
389
|
* completion; the sliding-window term is window-capped by kvBytesAt). */
|
|
368
390
|
#rowKvBytes(row: Row): number {
|
|
369
|
-
const tokens = row.promptTokens + row.req.maxTokens;
|
|
370
391
|
return this.#compressedProjectors
|
|
371
|
-
? this.#compressedProjectors.reduce(
|
|
372
|
-
|
|
392
|
+
? this.#compressedProjectors.reduce(
|
|
393
|
+
(sum, project) => sum + project(row.promptTokens + row.req.maxTokens),
|
|
394
|
+
0,
|
|
395
|
+
)
|
|
396
|
+
: batchRowKvBytes(
|
|
397
|
+
this.model.config,
|
|
398
|
+
row.promptTokens,
|
|
399
|
+
row.req.maxTokens,
|
|
400
|
+
this.#kvScheme,
|
|
401
|
+
);
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
async #forwardHidden(ids: MlxArray, cache: Cache[]): Promise<MlxArray> {
|
|
@@ -463,6 +492,7 @@ export class BatchScheduler {
|
|
|
463
492
|
|
|
464
493
|
async #drive(): Promise<void> {
|
|
465
494
|
let release: (() => void) | null = null;
|
|
495
|
+
let releaseWired: (() => void) | null = null;
|
|
466
496
|
try {
|
|
467
497
|
while (true) {
|
|
468
498
|
for (let i = this.#pending.length - 1; i >= 0; i--) {
|
|
@@ -480,10 +510,12 @@ export class BatchScheduler {
|
|
|
480
510
|
(!held && this.#pending.length > 0);
|
|
481
511
|
if (!hasWork) {
|
|
482
512
|
if (release) { release(); release = null; } // let the serial lane in
|
|
513
|
+
if (releaseWired) { releaseWired(); releaseWired = null; }
|
|
483
514
|
await new Promise<void>((r) => { this.#wake = r; });
|
|
484
515
|
this.#wake = null;
|
|
485
516
|
continue;
|
|
486
517
|
}
|
|
518
|
+
releaseWired ??= acquireModelWiredLimit(this.model);
|
|
487
519
|
if (!release && this.#lock) release = await this.#lock.acquire();
|
|
488
520
|
|
|
489
521
|
// Start at most one joiner prefill (drain-aware); it advances one
|
|
@@ -492,10 +524,15 @@ export class BatchScheduler {
|
|
|
492
524
|
this.#pending.length > 0 && this.#running.length < this.#maxBatch &&
|
|
493
525
|
this.#kvAdmits(this.#pending[0]!)) {
|
|
494
526
|
const row = this.#pending.shift()!;
|
|
527
|
+
row.req.onAdmitted?.();
|
|
495
528
|
// Prompt-cache reuse (Phase 3.2): start the solo prefill from the
|
|
496
529
|
// longest usable cached prefix — the serial lane's take(), same
|
|
497
530
|
// semantics (entry ownership transfers; put back or dispose).
|
|
531
|
+
const closeCache = row.req.trace?.begin("cache.lookup_restore", {
|
|
532
|
+
mechanism: "continuous",
|
|
533
|
+
});
|
|
498
534
|
const hit = this.#promptCache?.take(row.req.promptIds) ?? null;
|
|
535
|
+
closeCache?.();
|
|
499
536
|
if (hit) row.cachedTokens = hit.tokens.length;
|
|
500
537
|
// Boundary snapshot (the serial lane's A1, ported): a trim-free
|
|
501
538
|
// strict-prefix entry at min(stableLen, len-1) for every
|
|
@@ -510,19 +547,35 @@ export class BatchScheduler {
|
|
|
510
547
|
this.#promptCache && boundary >= 256 && boundary > (hit?.tokens.length ?? 0)
|
|
511
548
|
? boundary
|
|
512
549
|
: null;
|
|
550
|
+
const closePrefill = row.req.trace?.begin("prefill.total", {
|
|
551
|
+
mechanism: "continuous",
|
|
552
|
+
promptTokens: row.promptTokens,
|
|
553
|
+
cachedTokens: row.cachedTokens,
|
|
554
|
+
});
|
|
555
|
+
const closeBatchSetup = row.req.trace?.begin("prefill.batch_setup", {
|
|
556
|
+
mechanism: "continuous",
|
|
557
|
+
});
|
|
513
558
|
this.#prefill = hit
|
|
514
|
-
? {
|
|
515
|
-
|
|
559
|
+
? {
|
|
560
|
+
row, solo: hit.caches, pos: hit.tokens.length,
|
|
561
|
+
retain: hit.retain, snapAt, closePrefill,
|
|
562
|
+
}
|
|
563
|
+
: { row, solo: this.model.makeCache(), pos: 0, snapAt, closePrefill };
|
|
564
|
+
closeBatchSetup?.();
|
|
516
565
|
}
|
|
517
566
|
|
|
518
567
|
if (this.#prefill) {
|
|
519
568
|
const p = this.#prefill;
|
|
520
569
|
try {
|
|
521
|
-
if (await this.#prefillChunk(p))
|
|
570
|
+
if (await this.#prefillChunk(p)) {
|
|
571
|
+
p.closePrefill?.();
|
|
572
|
+
this.#prefill = null;
|
|
573
|
+
}
|
|
522
574
|
} catch (e) {
|
|
523
575
|
// Prefill/admission failure is per-row: reject the joiner, keep
|
|
524
576
|
// the running batch.
|
|
525
577
|
this.#prefill = null;
|
|
578
|
+
p.closePrefill?.();
|
|
526
579
|
for (const c of p.solo) c.dispose();
|
|
527
580
|
p.retain?.();
|
|
528
581
|
p.row.reject(e);
|
|
@@ -569,6 +622,7 @@ export class BatchScheduler {
|
|
|
569
622
|
}
|
|
570
623
|
} finally {
|
|
571
624
|
if (release) release();
|
|
625
|
+
releaseWired?.();
|
|
572
626
|
this.#looping = false;
|
|
573
627
|
}
|
|
574
628
|
}
|
|
@@ -580,23 +634,30 @@ export class BatchScheduler {
|
|
|
580
634
|
* source frees before the next layer converts). Called at every prefill
|
|
581
635
|
* chunk boundary AND once before merge, exactly where the serial loop
|
|
582
636
|
* calls maybeQuantizeKv — that placement is what makes a row's quantized
|
|
583
|
-
* bytes bit-exact vs serial `--kv-quant config`.
|
|
584
|
-
*
|
|
585
|
-
#quantizeSolo(solo: Cache[]): void {
|
|
637
|
+
* bytes bit-exact vs serial `--kv-quant config`. Gateway placement and the
|
|
638
|
+
* constructor both guarantee every named cache can convert. */
|
|
639
|
+
#quantizeSolo(solo: Cache[], trace?: PromptResponseTrace): void {
|
|
586
640
|
if (!this.#kvByLayer) return;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
641
|
+
const close = trace?.begin("prefill.kv_maintenance", {
|
|
642
|
+
mechanism: "continuous",
|
|
643
|
+
});
|
|
644
|
+
try {
|
|
645
|
+
for (let i = 0; i < solo.length; i++) {
|
|
646
|
+
const e = this.#kvByLayer.get(i);
|
|
647
|
+
const c = solo[i]!;
|
|
648
|
+
if (!e || c.offset === 0) continue;
|
|
649
|
+
// Milestone 2: rotating layers convert too — RotatingKVCache.
|
|
650
|
+
// toQuantized → RotatingQuantizedKVCache, exactly maybeQuantizeKv's
|
|
651
|
+
// dispatch (generate.ts). Converted caches don't match either class
|
|
652
|
+
// (the four cache classes are siblings), so re-conversion never fires.
|
|
653
|
+
if (!(isPlainKvCache(c) || isRotatingPlainCache(c))) continue;
|
|
654
|
+
const q = c.toQuantized(e.groupSize, e.bits);
|
|
655
|
+
solo[i] = q;
|
|
656
|
+
ops.evalAll(q.state());
|
|
657
|
+
clearCache();
|
|
658
|
+
}
|
|
659
|
+
} finally {
|
|
660
|
+
close?.();
|
|
600
661
|
}
|
|
601
662
|
}
|
|
602
663
|
|
|
@@ -607,6 +668,12 @@ export class BatchScheduler {
|
|
|
607
668
|
async #prefillChunk(p: PrefillState): Promise<boolean> {
|
|
608
669
|
p.row.req.signal?.throwIfAborted();
|
|
609
670
|
const prompt = p.row.req.promptIds;
|
|
671
|
+
const chunkStart = p.pos;
|
|
672
|
+
const closeChunk = p.row.req.trace?.begin("prefill.chunk", {
|
|
673
|
+
mechanism: "continuous",
|
|
674
|
+
startToken: chunkStart,
|
|
675
|
+
});
|
|
676
|
+
try {
|
|
610
677
|
// Boundary-snapshot chunking (mirrors generate.ts's snapshotAt split):
|
|
611
678
|
// chunk edges land EXACTLY on snapAt so the clone is taken while the
|
|
612
679
|
// caches hold precisely that prefix — slicing tokens against caches at
|
|
@@ -621,7 +688,7 @@ export class BatchScheduler {
|
|
|
621
688
|
ids.dispose();
|
|
622
689
|
h.dispose();
|
|
623
690
|
ops.evalAll(p.solo.flatMap((c) => c.state()));
|
|
624
|
-
this.#quantizeSolo(p.solo); // serial converts at every chunk boundary
|
|
691
|
+
this.#quantizeSolo(p.solo, p.row.req.trace); // serial converts at every chunk boundary
|
|
625
692
|
clearCache();
|
|
626
693
|
p.pos = end;
|
|
627
694
|
if (p.pos === p.snapAt) {
|
|
@@ -651,7 +718,7 @@ export class BatchScheduler {
|
|
|
651
718
|
ids.dispose();
|
|
652
719
|
h.dispose(); // logits never computed for non-final chunks
|
|
653
720
|
ops.evalAll(p.solo.flatMap((c) => c.state()));
|
|
654
|
-
this.#quantizeSolo(p.solo); // serial converts at every chunk boundary
|
|
721
|
+
this.#quantizeSolo(p.solo, p.row.req.trace); // serial converts at every chunk boundary
|
|
655
722
|
clearCache(); // serial prefill's per-chunk clear (generate.ts)
|
|
656
723
|
p.pos += this.#prefillChunkSize;
|
|
657
724
|
return false;
|
|
@@ -669,7 +736,7 @@ export class BatchScheduler {
|
|
|
669
736
|
ids.dispose();
|
|
670
737
|
h.dispose();
|
|
671
738
|
ops.evalAll(p.solo.flatMap((c) => c.state()));
|
|
672
|
-
this.#quantizeSolo(p.solo);
|
|
739
|
+
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
673
740
|
clearCache();
|
|
674
741
|
p.pos = prompt.length - 1;
|
|
675
742
|
}
|
|
@@ -683,15 +750,51 @@ export class BatchScheduler {
|
|
|
683
750
|
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
684
751
|
const h = await this.#forwardHidden(ids, p.solo);
|
|
685
752
|
ids.dispose();
|
|
753
|
+
closeChunk?.();
|
|
754
|
+
p.closePrefill?.();
|
|
755
|
+
p.closePrefill = undefined;
|
|
756
|
+
const closeTokenZero = p.row.req.trace?.begin("token_zero.total", {
|
|
757
|
+
mechanism: "continuous",
|
|
758
|
+
});
|
|
759
|
+
// MLX is lazy: synchronize(stream) only waits for already-submitted work;
|
|
760
|
+
// it does not submit `h`'s graph. Attribution mode must evaluate the final
|
|
761
|
+
// L=1 forward explicitly or its work is silently charged to the head/read.
|
|
762
|
+
const forceAttribution = !!p.row.req.trace &&
|
|
763
|
+
runtimeValue("MLX_BUN_P2R_SYNC") === "1";
|
|
764
|
+
if (forceAttribution) {
|
|
765
|
+
const closeForward = p.row.req.trace!.begin("token_zero.forward", {
|
|
766
|
+
mechanism: "continuous",
|
|
767
|
+
activeBytes: activeMemory(),
|
|
768
|
+
cacheBytes: cacheMemory(),
|
|
769
|
+
peakBytes: peakMemory(),
|
|
770
|
+
});
|
|
771
|
+
ops.evalAll([h, ...p.solo.flatMap((c) => c.state())]);
|
|
772
|
+
closeForward();
|
|
773
|
+
}
|
|
686
774
|
const [, Lc, H] = h.shape as [number, number, number];
|
|
687
775
|
const hLast = h.slice([0, Lc - 1, 0], [1, Lc, H]);
|
|
688
776
|
h.dispose();
|
|
689
777
|
const lg = this.model.logitsFromHidden(hLast); // [1,1,V]
|
|
690
778
|
hLast.dispose();
|
|
779
|
+
if (forceAttribution) {
|
|
780
|
+
const closeHead = p.row.req.trace!.begin("token_zero.head", {
|
|
781
|
+
mechanism: "continuous",
|
|
782
|
+
activeBytes: activeMemory(),
|
|
783
|
+
cacheBytes: cacheMemory(),
|
|
784
|
+
peakBytes: peakMemory(),
|
|
785
|
+
});
|
|
786
|
+
ops.evalAll([lg]);
|
|
787
|
+
closeHead();
|
|
788
|
+
}
|
|
691
789
|
const V = lg.shape[2]!;
|
|
692
790
|
const last2 = ops.reshape(lg, [1, V]);
|
|
693
791
|
lg.dispose();
|
|
792
|
+
const closeSample = forceAttribution
|
|
793
|
+
? p.row.req.trace!.begin("token_zero.sample", { mechanism: "continuous" })
|
|
794
|
+
: undefined;
|
|
694
795
|
const tok = this.#readToken(p.row.req.sample(last2, 0));
|
|
796
|
+
closeSample?.();
|
|
797
|
+
closeTokenZero?.();
|
|
695
798
|
last2.dispose();
|
|
696
799
|
p.row.sampled = 1;
|
|
697
800
|
p.row.generated = 1;
|
|
@@ -728,9 +831,12 @@ export class BatchScheduler {
|
|
|
728
831
|
// MLX_BUN_PREFILL_TAIL_SPLIT=0 — the old serial order: token 0 sampled
|
|
729
832
|
// from the unconverted final-chunk logits, THEN the caches convert
|
|
730
833
|
// (before decode step 1 == before the merge).
|
|
731
|
-
this.#quantizeSolo(p.solo);
|
|
834
|
+
this.#quantizeSolo(p.solo, p.row.req.trace);
|
|
732
835
|
await this.#mergeJoiner(p);
|
|
733
836
|
return true;
|
|
837
|
+
} finally {
|
|
838
|
+
closeChunk?.();
|
|
839
|
+
}
|
|
734
840
|
}
|
|
735
841
|
|
|
736
842
|
/** Merge a fully-prefilled joiner with the running batch, layer by layer
|
|
@@ -784,7 +890,7 @@ export class BatchScheduler {
|
|
|
784
890
|
));
|
|
785
891
|
continue;
|
|
786
892
|
}
|
|
787
|
-
if (p.solo[layer]
|
|
893
|
+
if (isQuantizedKvCache(p.solo[layer])) {
|
|
788
894
|
// Phase 3.1 — quantized full layer: same merge/extend shapes as the
|
|
789
895
|
// bf16 branch below, over (packed, scales, biases) triples. The solo
|
|
790
896
|
// row was converted by #quantizeSolo with the serial ops, so its
|
|
@@ -797,7 +903,7 @@ export class BatchScheduler {
|
|
|
797
903
|
t.packed.dispose(); t.scales.dispose(); t.biases.dispose();
|
|
798
904
|
};
|
|
799
905
|
const prevQ = prev?.[layer] as QuantizedKVCache | undefined;
|
|
800
|
-
if (prevQ &&
|
|
906
|
+
if (prevQ && runtimeValue("MLX_BUN_BATCH_EXTEND") !== "0") {
|
|
801
907
|
const [k0, v0] = prevQ.temporalView();
|
|
802
908
|
const ext = extendQuantRows(k0, v0, prevPad, qRow);
|
|
803
909
|
dispose3(k0); dispose3(v0);
|
|
@@ -830,7 +936,7 @@ export class BatchScheduler {
|
|
|
830
936
|
for (const r of rows) { dispose3(r.keys); dispose3(r.values); }
|
|
831
937
|
}
|
|
832
938
|
// qRow views are disposed via the rows loop above or here for extend
|
|
833
|
-
if (prevQ &&
|
|
939
|
+
if (prevQ && runtimeValue("MLX_BUN_BATCH_EXTEND") !== "0") { dispose3(qRow.keys); dispose3(qRow.values); }
|
|
834
940
|
else if (!prevQ) { /* disposed in the rows loop */ }
|
|
835
941
|
continue;
|
|
836
942
|
}
|
|
@@ -840,7 +946,7 @@ export class BatchScheduler {
|
|
|
840
946
|
// introduced in 859572d; 2026-07-07 review fix). The view is taken
|
|
841
947
|
// AFTER the branch, on the paths that actually consume it.
|
|
842
948
|
const soloC = p.solo[layer] as KVCache | RotatingKVCache;
|
|
843
|
-
if (this.#kinds[layer] === "rot" && p.solo[layer]
|
|
949
|
+
if (this.#kinds[layer] === "rot" && isRotatingQuantizedCache(p.solo[layer])) {
|
|
844
950
|
// Milestone 2 — QUANTIZED rotating layer: the solo row converted at
|
|
845
951
|
// the serial boundaries (#quantizeSolo), so its ring bytes are the
|
|
846
952
|
// serial oracle's; this branch re-arranges temporal triples across
|
|
@@ -854,21 +960,22 @@ export class BatchScheduler {
|
|
|
854
960
|
t.values.packed.dispose(); t.values.scales.dispose(); t.values.biases.dispose();
|
|
855
961
|
};
|
|
856
962
|
const prevC = prev?.[layer];
|
|
857
|
-
if (prevC
|
|
858
|
-
const
|
|
963
|
+
if (prevC && isRowBatchCache(prevC) && isRotatingQuantizedCache(prevC)) {
|
|
964
|
+
const batched = prevC as BatchedRotatingQuantCache;
|
|
965
|
+
const [k0, v0] = batched.temporalView(); // [B,H,valid,*] triples, temporal
|
|
859
966
|
const valid = k0.packed.shape[2]!;
|
|
860
967
|
for (let b = 0; b < B; b++) {
|
|
861
|
-
const pad = Math.max(0,
|
|
968
|
+
const pad = Math.max(0, batched.leftPad[b]!);
|
|
862
969
|
const cutRow = (t: typeof k0): typeof k0 => ({
|
|
863
970
|
packed: t.packed.slice([b, 0, pad, 0], [b + 1, t.packed.shape[1]!, valid, t.packed.shape[3]!]),
|
|
864
971
|
scales: t.scales.slice([b, 0, pad, 0], [b + 1, t.scales.shape[1]!, valid, t.scales.shape[3]!]),
|
|
865
972
|
biases: t.biases.slice([b, 0, pad, 0], [b + 1, t.biases.shape[1]!, valid, t.biases.shape[3]!]),
|
|
866
973
|
});
|
|
867
974
|
rows.push({ keys: cutRow(k0), values: cutRow(v0) });
|
|
868
|
-
offsets.push(
|
|
975
|
+
offsets.push(batched.offsetArr[b]!);
|
|
869
976
|
}
|
|
870
977
|
for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
|
|
871
|
-
} else if (prevC
|
|
978
|
+
} else if (isRotatingQuantizedCache(prevC)) {
|
|
872
979
|
// Adopted lone row (Phase 3.2 adopt): its chronological triples
|
|
873
980
|
// are the merge's first row, pad 0 by definition.
|
|
874
981
|
const [k0, v0] = prevC.temporalView();
|
|
@@ -889,7 +996,7 @@ export class BatchScheduler {
|
|
|
889
996
|
const rows: Row1[] = [];
|
|
890
997
|
const offsets: number[] = [];
|
|
891
998
|
const prevC = prev?.[layer];
|
|
892
|
-
if (prevC
|
|
999
|
+
if (isRotatingPlainCache(prevC) && !isRowBatchCache(prevC)) {
|
|
893
1000
|
// Adopted lone row (Phase 3.2): a plain serial rotating cache —
|
|
894
1001
|
// its chronological view is the merge's first row, same as a
|
|
895
1002
|
// fresh solo (pad 0 by definition).
|
|
@@ -897,7 +1004,16 @@ export class BatchScheduler {
|
|
|
897
1004
|
rows.push({ keys: k0, values: v0 });
|
|
898
1005
|
offsets.push(prevC.offset);
|
|
899
1006
|
}
|
|
900
|
-
|
|
1007
|
+
// Capability-only: BatchedRotatingCache has NO signature() override
|
|
1008
|
+
// (cacheSignature → "unknown"), so any signature-based conjunct here
|
|
1009
|
+
// silently drops the running batch's cache from the merge — joiners
|
|
1010
|
+
// then build a B<max batched ring while forwards arrive at full B
|
|
1011
|
+
// (grow-path concatenate crash, whole-batch drop; 2026-08-22 agg×4
|
|
1012
|
+
// regression from 443f333). The quant family is routed by its own
|
|
1013
|
+
// branch above (its subclass inherits a real signature).
|
|
1014
|
+
const prevRot = prevC && isRowBatchCache(prevC)
|
|
1015
|
+
? prevC as unknown as BatchedRotatingCache
|
|
1016
|
+
: undefined;
|
|
901
1017
|
if (prevRot) {
|
|
902
1018
|
const [k0, v0] = prevRot.temporalView(); // [B,H,valid,D]
|
|
903
1019
|
const [, H, valid, D] = k0.shape as [number, number, number, number];
|
|
@@ -918,7 +1034,7 @@ export class BatchScheduler {
|
|
|
918
1034
|
for (const r of rows) { r.keys.dispose(); r.values.dispose(); }
|
|
919
1035
|
} else {
|
|
920
1036
|
const prevFull = prev?.[layer] as KVCache | undefined;
|
|
921
|
-
if (prevFull &&
|
|
1037
|
+
if (prevFull && runtimeValue("MLX_BUN_BATCH_EXTEND") !== "0") {
|
|
922
1038
|
// extend-join (mlx-lm BatchKVCache.extend semantics, P0): append
|
|
923
1039
|
// the new right-justified row to the running buffer in ONE pad +
|
|
924
1040
|
// ONE concat — no per-row extraction. Existing pads grow, never
|
|
@@ -1050,7 +1166,7 @@ export class BatchScheduler {
|
|
|
1050
1166
|
// A filtered-to-one BATCHED rot-quant cache subclasses the serial
|
|
1051
1167
|
// class (so supports() passes) but carries batched ring state —
|
|
1052
1168
|
// exclude it; only truly-adopted serial caches replay compiled.
|
|
1053
|
-
!inners.some((c) => c
|
|
1169
|
+
!inners.some((c) => isRowBatchCache(c) && isRotatingQuantizedCache(c)) &&
|
|
1054
1170
|
CompiledDecode.supports(inners as Cache[])
|
|
1055
1171
|
) {
|
|
1056
1172
|
let cur = this.#pendingToks;
|
|
@@ -1078,10 +1194,9 @@ export class BatchScheduler {
|
|
|
1078
1194
|
try {
|
|
1079
1195
|
if (!lg) {
|
|
1080
1196
|
fwd = inners.map((c) =>
|
|
1081
|
-
c
|
|
1082
|
-
c instanceof SSMCache || isBatchableCache(c) || unpadded
|
|
1197
|
+
isRowBatchCache(c) || isBatchableCache(c) || unpadded
|
|
1083
1198
|
? c
|
|
1084
|
-
: c
|
|
1199
|
+
: isQuantizedKvCache(c)
|
|
1085
1200
|
? new BatchedQuantDecodeMaskCache(c, B, this.#fullLeftPad)
|
|
1086
1201
|
: new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
|
|
1087
1202
|
);
|
|
@@ -1103,7 +1218,7 @@ export class BatchScheduler {
|
|
|
1103
1218
|
// harmless (the slot is filtered before it is ever emitted; its only
|
|
1104
1219
|
// use is one KV write on the row's own, about-to-evict row).
|
|
1105
1220
|
const vecOk =
|
|
1106
|
-
|
|
1221
|
+
runtimeValue("MLX_BUN_BATCH_VEC_SAMPLE") !== "0" &&
|
|
1107
1222
|
rows.every((r) => r.sampled >= r.req.maxTokens || r.req.plainGreedy);
|
|
1108
1223
|
// Doomed slots hold placeholders (vec path: a real argmax value,
|
|
1109
1224
|
// equally not part of the row's stream) — flag them so the fed
|
|
@@ -1228,7 +1343,7 @@ export class BatchScheduler {
|
|
|
1228
1343
|
const prev = this.#pendingToks;
|
|
1229
1344
|
const prevVals: number[] =
|
|
1230
1345
|
prev ? prev.toIntTokens() : [];
|
|
1231
|
-
if (
|
|
1346
|
+
if (runtimeValue("MLX_BUN_GRAMMAR_DEBUG") === "1")
|
|
1232
1347
|
console.log(`[sg] B=${B} prevVals=${JSON.stringify(prevVals)} current=${JSON.stringify(rows.map((r) => r.current))} sampled=${JSON.stringify(rows.map((r) => r.sampled))}`);
|
|
1233
1348
|
|
|
1234
1349
|
// (2) accept() per live grammar row — fires that row's async bitmask fill.
|
|
@@ -1253,10 +1368,9 @@ export class BatchScheduler {
|
|
|
1253
1368
|
// Unpadded fast path — same rule as #step (bare caches == serial graph).
|
|
1254
1369
|
const unpadded = this.#fullLeftPad.every((p) => p === 0);
|
|
1255
1370
|
const fwd: Cache[] = inners.map((c) =>
|
|
1256
|
-
c
|
|
1257
|
-
c instanceof SSMCache || isBatchableCache(c) || unpadded
|
|
1371
|
+
isRowBatchCache(c) || isBatchableCache(c) || unpadded
|
|
1258
1372
|
? c
|
|
1259
|
-
: c
|
|
1373
|
+
: isQuantizedKvCache(c)
|
|
1260
1374
|
? new BatchedQuantDecodeMaskCache(c, B, this.#fullLeftPad)
|
|
1261
1375
|
: new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
|
|
1262
1376
|
);
|
|
@@ -1442,22 +1556,25 @@ export class BatchScheduler {
|
|
|
1442
1556
|
for (const inner of this.#inners!) {
|
|
1443
1557
|
let c: Cache | null;
|
|
1444
1558
|
if (isBatchableCache(inner)) c = inner.extractRow(b);
|
|
1445
|
-
else if (inner
|
|
1446
|
-
else if (inner instanceof BatchedRotatingCache) c = inner.extractRow(b);
|
|
1447
|
-
else if (inner instanceof SSMCache)
|
|
1559
|
+
else if (isRowBatchCache(inner) && cacheSignature(inner) === "ssm")
|
|
1448
1560
|
// Coverage gate: recurrent state is UNTRIMMABLE, so an entry is only
|
|
1449
1561
|
// valid when the row's own advance count equals the [promptIds+fed]
|
|
1450
1562
|
// key EXACTLY (a mismatched entry would silently corrupt every future
|
|
1451
1563
|
// exact-hit). Defensive — a miss here means the fed accounting broke.
|
|
1452
|
-
c = inner.conv && inner.rowOffset(b) === expectTokens
|
|
1564
|
+
c = (inner as SSMCache).conv && (inner as SSMCache).rowOffset(b) === expectTokens
|
|
1565
|
+
? inner.extractRow(b)
|
|
1566
|
+
: null;
|
|
1567
|
+
else if (isRowBatchCache(inner)) c = inner.extractRow(b);
|
|
1453
1568
|
else if (
|
|
1454
|
-
inner
|
|
1455
|
-
inner
|
|
1569
|
+
isRotatingQuantizedCache(inner) || // adopted serial state never
|
|
1570
|
+
isRotatingPlainCache(inner) // coexists with a merged row (defensive)
|
|
1456
1571
|
)
|
|
1457
1572
|
c = null;
|
|
1458
|
-
else if (inner
|
|
1573
|
+
else if (isQuantizedKvCache(inner))
|
|
1459
1574
|
c = inner.keys ? extractQuantRow(inner, this.#fullLeftPad[b]!, b) : null;
|
|
1460
|
-
else
|
|
1575
|
+
else if (isPlainKvCache(inner))
|
|
1576
|
+
c = inner.keys ? extractKVRow(inner, this.#fullLeftPad[b]!, b) : null;
|
|
1577
|
+
else c = null;
|
|
1461
1578
|
if (!c) {
|
|
1462
1579
|
for (const d of out) d.dispose();
|
|
1463
1580
|
return null;
|
|
@@ -1506,20 +1623,14 @@ export class BatchScheduler {
|
|
|
1506
1623
|
if (isBatchableCache(inner)) {
|
|
1507
1624
|
inner.filterRows(keep);
|
|
1508
1625
|
out.push(inner);
|
|
1509
|
-
} else if (inner
|
|
1510
|
-
inner.
|
|
1511
|
-
out.push(inner);
|
|
1512
|
-
} else if (inner instanceof BatchedRotatingQuantCache) {
|
|
1513
|
-
inner.filter(keep); // in-place B-axis take on all six components
|
|
1626
|
+
} else if (isRowBatchCache(inner)) {
|
|
1627
|
+
inner.filterRows(keep);
|
|
1514
1628
|
out.push(inner);
|
|
1515
|
-
} else if (inner
|
|
1629
|
+
} else if (isRotatingQuantizedCache(inner) || isRotatingPlainCache(inner)) {
|
|
1516
1630
|
// Adopted lone-row state exists only at B=1, where the only filter
|
|
1517
1631
|
// is the keep=[] dispose-all handled above — unreachable.
|
|
1518
1632
|
throw new Error("applyFilter: adopted serial rotating cache cannot be row-filtered");
|
|
1519
|
-
} else if (inner
|
|
1520
|
-
inner.filter(keep); // in-place B-axis take on both state slots
|
|
1521
|
-
out.push(inner);
|
|
1522
|
-
} else if (inner instanceof QuantizedKVCache) {
|
|
1633
|
+
} else if (isQuantizedKvCache(inner)) {
|
|
1523
1634
|
const [k0, v0] = inner.temporalView();
|
|
1524
1635
|
const f = filterQuantRows(k0, v0, keep);
|
|
1525
1636
|
for (const t of [k0, v0]) { t.packed.dispose(); t.scales.dispose(); t.biases.dispose(); }
|
|
@@ -1527,7 +1638,7 @@ export class BatchScheduler {
|
|
|
1527
1638
|
c.restoreState(f.keys, f.values, inner.offset);
|
|
1528
1639
|
out.push(c);
|
|
1529
1640
|
inner.dispose();
|
|
1530
|
-
} else {
|
|
1641
|
+
} else if (isPlainKvCache(inner)) {
|
|
1531
1642
|
const [k0, v0] = inner.temporalView();
|
|
1532
1643
|
const f = filterKVRows(k0, v0, keep);
|
|
1533
1644
|
k0.dispose(); v0.dispose();
|
|
@@ -1535,6 +1646,8 @@ export class BatchScheduler {
|
|
|
1535
1646
|
c.restoreState(f.keys, f.values, inner.offset);
|
|
1536
1647
|
out.push(c);
|
|
1537
1648
|
inner.dispose();
|
|
1649
|
+
} else {
|
|
1650
|
+
throw new Error(`applyFilter: unsupported cache signature ${cacheSignature(inner)}`);
|
|
1538
1651
|
}
|
|
1539
1652
|
}
|
|
1540
1653
|
this.#inners = out;
|