mlx-bun 0.2.0 → 0.3.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 +71 -588
- package/docs/README.md +91 -0
- package/docs/{design → archive/investigations}/audio-input-plan.md +4 -4
- package/docs/{design → archive/investigations}/diffusion-gemma-port.md +3 -3
- package/docs/{design → archive/investigations}/faithful-l1-consolidation.md +4 -4
- package/docs/{design → archive/investigations}/minicpm5-decode-megakernel.md +11 -11
- package/docs/{design → archive/investigations}/optimization_plan.md +1 -1
- package/docs/{design → archive/investigations}/pre-colibri-stabilization.md +5 -5
- package/docs/{design → archive/investigations}/the-dreaming-handoff.md +2 -2
- package/docs/design/batching.md +569 -0
- package/docs/design/decode-speed-program.md +3178 -169
- package/docs/design/dreaming-nightly-pipeline.md +675 -282
- package/docs/design/generic-model-support.md +579 -334
- package/docs/design/kv-cache.md +680 -0
- package/docs/design/orpo-training.md +673 -1239
- package/docs/design/speculative-decoding.md +1627 -0
- package/docs/design/turboquant.md +2102 -0
- package/docs/design/unified-engine-frontier-plan.md +1831 -727
- package/docs/design/web-chat-redesign.md +666 -1083
- package/docs/planning/PRODUCT_ROADMAP.md +34 -1
- package/docs/planning/release-notes-v0.3.0.md +35 -0
- package/docs/planning/video-series.md +2 -2
- package/docs/reference/benchmarks.md +1162 -0
- package/docs/reference/cli.md +394 -159
- package/docs/reference/distribution.md +321 -182
- package/docs/reference/environment.md +348 -0
- package/docs/reference/glossary.md +39 -0
- package/docs/reference/library-api.md +566 -168
- package/docs/reference/memory.md +227 -64
- package/docs/reference/models.md +104 -6
- package/docs/reference/server-api.md +886 -425
- package/docs/reference/server-config.md +638 -330
- package/docs/reference/training.md +393 -254
- package/docs/reference/troubleshooting.md +166 -0
- package/package.json +9 -4
- package/src/audio/decode.ts +2 -2
- package/src/audio/features.ts +2 -2
- package/src/backends/mlx/autoregressive.ts +108 -0
- package/src/backends/mlx/batch-group.ts +1646 -0
- package/src/backends/mlx/diffusion.ts +39 -0
- package/src/backends/mlx/draft-target.ts +47 -0
- package/src/backends/mlx/evaluation.ts +105 -0
- package/src/backends/mlx/fill-append.ts +45 -0
- package/src/backends/mlx/gateway-binding.ts +71 -0
- package/src/backends/mlx/graph.ts +38 -0
- package/src/backends/mlx/memory-client.ts +189 -0
- package/src/backends/mlx/memory-guard.ts +40 -0
- package/src/backends/mlx/methods.ts +90 -0
- package/src/backends/mlx/model-prompt.ts +164 -0
- package/src/backends/mlx/model-serving.ts +79 -0
- package/src/backends/mlx/prefill.ts +33 -0
- package/src/backends/mlx/request-policy.ts +20 -0
- package/src/backends/mlx/rollback.ts +51 -0
- package/src/backends/mlx/serial-executor.ts +273 -0
- package/src/backends/mlx/speculative.ts +114 -0
- package/src/backends/mlx/state-views.ts +26 -0
- package/src/backends/mlx/text-engine.ts +73 -0
- package/src/cli.ts +58 -25
- package/src/client.ts +66 -0
- package/src/config.ts +21 -4
- package/src/contracts/completion.ts +16 -0
- package/src/contracts/execution.ts +49 -0
- package/src/contracts/generation.ts +103 -0
- package/src/contracts/host.ts +7 -0
- package/src/contracts/jobs.ts +66 -0
- package/src/contracts/pi-provider.ts +30 -0
- package/src/contracts/pi.ts +259 -0
- package/src/contracts/resources.ts +18 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/contracts/tsconfig.json +14 -0
- package/src/diffusion/diffusion-generate.ts +218 -153
- package/src/embed.ts +1 -1
- package/src/engine/admission.ts +59 -0
- package/src/engine/cancellation.ts +38 -0
- package/src/engine/engine.ts +56 -0
- package/src/engine/execution-coordinator.ts +70 -0
- package/src/engine/execution-plan.ts +51 -0
- package/src/engine/resources.ts +42 -0
- package/src/engine/scheduler.ts +67 -0
- package/src/engine/session.ts +247 -0
- package/src/eval/README.md +44 -1
- package/src/eval/kl.ts +236 -3
- package/src/eval/reference-logits.ts +207 -0
- package/src/eval/runner.ts +8 -96
- package/src/fill/echo-index.ts +188 -0
- package/src/fill/fill-session.ts +452 -0
- package/src/fill/proposal.ts +68 -0
- package/src/fill/schema-rows.ts +506 -0
- package/src/fill/tool-boundary.ts +103 -0
- package/src/fit.ts +1 -1
- package/src/generate.ts +656 -268
- package/src/grammar.ts +12 -7
- package/src/harness-pi.ts +14 -33
- package/src/hub-rest.ts +4 -4
- package/src/index.ts +7 -0
- package/src/inference/denoising.ts +17 -0
- package/src/inference/graph.ts +33 -0
- package/src/inference/prefill.ts +39 -0
- package/src/inference/rollback.ts +7 -0
- package/src/jobs/db.ts +4 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-entry.ts +12 -12
- package/src/jobs/runner.ts +98 -13
- package/src/jobs/task-client.ts +19 -0
- package/src/jobs/types.ts +2 -58
- package/src/kv-store.ts +109 -49
- package/src/lab/README.md +18 -0
- package/src/{assets → lab/curve}/curve-designer.html +2 -2
- package/src/{curve-sampler.ts → lab/curve/curve-sampler.ts} +2 -2
- package/src/{expert-trace.ts → lab/expert-trace/expert-trace.ts} +5 -5
- package/src/{model → lab/paged-kv}/paged-kv.ts +9 -6
- package/src/library.ts +40 -0
- package/src/lora.ts +18 -0
- package/src/media-fetch.ts +1 -1
- package/src/memory/article.ts +1 -1
- package/src/memory/chunk.ts +1 -1
- package/src/memory/model.ts +30 -169
- package/src/memory/prompts.ts +1 -1
- package/src/memory/rest.ts +1 -1
- package/src/memory/schedule.ts +1 -1
- package/src/memory/stages.ts +4 -1
- package/src/memory/vault.ts +1 -3
- package/src/mlx/array.ts +9 -10
- package/src/mlx/compile.ts +12 -2
- package/src/mlx/ffi.ts +58 -11
- package/src/mlx/materialize.ts +23 -0
- package/src/mlx/metal-kernel.ts +10 -11
- package/src/mlx/ops.ts +33 -13
- package/src/mlx/quantized-rows.ts +36 -0
- package/src/mlx/token-bitmask.ts +52 -0
- package/src/mlx/turboquant-kv-decode.ts +82 -0
- package/src/mlx/turboquant-kv-inverse.ts +122 -0
- package/src/mlx/turboquant-ops.ts +33 -3
- package/src/mlx/turboquant-tables.ts +2 -2
- package/src/model/batched-mask.ts +8 -2
- package/src/model/batched-quant.ts +1 -1
- package/src/model/batched-rotating-quant.ts +2 -2
- package/src/model/batched-rotating.ts +1 -1
- package/src/model/compiled-decode.ts +1 -1
- package/src/model/diffusion-gemma.ts +8 -8
- package/src/model/factory.ts +76 -33
- package/src/model/fingerprint.ts +1 -1
- package/src/model/flash-attention.ts +3 -3
- package/src/model/gemma4-base.ts +26 -9
- package/src/model/gemma4.ts +6 -6
- package/src/model/generated/gemma4-12b.ts +2 -2
- package/src/model/generated/gemma4-26b.ts +2 -2
- package/src/model/generated/gemma4-e4b.ts +2 -2
- package/src/model/glm52-dsa.ts +13 -9
- package/src/model/glm52-memory.ts +1 -1
- package/src/model/implementation.ts +69 -0
- package/src/model/minicpm5.ts +6 -8
- package/src/model/profile.ts +12 -2
- package/src/model/qwen-append.ts +12 -0
- package/src/model/qwen-conv.ts +52 -0
- package/src/model/qwen3_5-checkpoint.ts +164 -0
- package/src/model/qwen3_5.ts +140 -56
- package/src/model/support.ts +1 -1
- package/src/model/trellis-balanced-scatter.ts +51 -0
- package/src/model/trellis-linear.ts +672 -0
- package/src/model/trellis-shared-m.ts +151 -0
- package/src/model/trellis-shared-scatter.ts +144 -0
- package/src/model/trellis-splitk-prefill.ts +158 -0
- package/src/model/trellis-tiled-prefill.ts +136 -0
- package/src/model/trellis-vector-expand.ts +62 -0
- package/src/model/trellis-wide-prefill.ts +94 -0
- package/src/model/universal/dense.ts +1 -1
- package/src/model/universal/modules.ts +8 -1
- package/src/native-pack.ts +6 -5
- package/src/pi-provider.ts +6 -64
- package/src/pi-web.ts +8 -315
- package/src/prompt-cache.ts +96 -53
- package/src/quantize/allocator.ts +1 -1
- package/src/quantize/config-writer.ts +10 -3
- package/src/quantize/rotate.ts +4 -4
- package/src/quantize/trellis.ts +586 -0
- package/src/responses.ts +28 -1
- package/src/runtime-config.ts +18 -4
- package/src/sampler.ts +4 -4
- package/src/serve/batch-scheduler.ts +8 -1671
- package/src/serve/chat-request.ts +391 -0
- package/src/serve/chat-stage.ts +195 -0
- package/src/serve/checkpoint-identity.ts +64 -0
- package/src/serve/completion-executor.ts +15 -7
- package/src/serve/discovery-routes.ts +12 -13
- package/src/serve/generation-gateway.ts +154 -193
- package/src/serve/http.ts +159 -0
- package/src/serve/inference-request.ts +79 -0
- package/src/serve/isolate.ts +297 -105
- package/src/serve/lab-routes.ts +3 -0
- package/src/serve/lane-registry.ts +2 -1
- package/src/serve/model-admin-routes.ts +7 -5
- package/src/serve/model-binding.ts +43 -0
- package/src/serve/model-host.ts +512 -0
- package/src/serve/openai-wire.ts +103 -0
- package/src/serve/parent-application.ts +56 -0
- package/src/serve/pipeline.ts +33 -0
- package/src/serve/preparation.ts +33 -0
- package/src/serve/request-plan.ts +19 -15
- package/src/serve/request-prep.ts +346 -0
- package/src/serve/response-lease.ts +24 -0
- package/src/serve/responses-client.ts +69 -0
- package/src/serve/session-completion-engine.ts +110 -0
- package/src/serve/session-search.ts +1 -1
- package/src/serve/static-routes.ts +5 -5
- package/src/serve/text-completion-stage.ts +104 -0
- package/src/serve/token-streams.ts +519 -0
- package/src/server.ts +317 -2649
- package/src/spec/assistant-source.ts +10 -44
- package/src/spec/deepspec-source.ts +6 -6
- package/src/spec/dflash-source.ts +8 -8
- package/src/spec/dspark/ab-stats.ts +3 -3
- package/src/spec/dspark/calibration.ts +2 -2
- package/src/spec/dspark/deepspec-module.ts +2 -2
- package/src/spec/dspark/generate-dflash.ts +2 -2
- package/src/spec/dspark/generate.ts +2 -2
- package/src/spec/dspark/loader.ts +1 -1
- package/src/spec/dspark/module-dflash.ts +4 -3
- package/src/spec/dspark/module.ts +3 -3
- package/src/spec/dspark/quantize-drafter.ts +1 -1
- package/src/spec/generate.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -1
- package/src/spec/ngram-source.ts +1 -1
- package/src/spec/prefix-state.ts +45 -0
- package/src/spec/qwen-mtp-source.ts +166 -69
- package/src/spec/serve-loop.ts +154 -156
- package/src/spec/source.ts +42 -10
- package/src/spec/two-model.ts +1 -1
- package/src/ssd-cache.ts +80 -12
- package/src/ssd-durability.ts +25 -13
- package/src/tool-call.ts +51 -14
- package/src/train/flash-cce.ts +2 -2
- package/src/train/forward.ts +6 -0
- package/src/train/loss.ts +1 -1
- package/src/train/prefix-shared.ts +5 -1
- package/src/train/segmented.ts +5 -3
- package/src/train/trainer.ts +6 -6
- package/src/vision/prompt.ts +1 -1
- package/src/vision/qwen3vl-tower.ts +52 -10
- package/src/web/app.html +1 -1
- package/src/web/app.js +5 -3
- package/src/web/src/adapters-panel.ts +3 -3
- package/src/web/src/assistant.ts +2 -36
- package/src/web/src/chat.ts +4 -4
- package/src/web/src/composer.ts +4 -4
- package/src/web/src/hub.ts +1 -1
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +1 -1
- package/src/web/src/model-picker.ts +1 -1
- package/src/web/src/palette.ts +1 -1
- package/src/web/src/protocol.ts +3 -39
- package/src/web/src/sessions.ts +1 -1
- package/src/web/src/ui-catalog.ts +4 -18
- package/src/web-assets.ts +47 -0
- package/src/weights.ts +55 -9
- package/docs/archive/investigations/curve-runs/bisector-route.json +0 -98
- package/docs/archive/investigations/curve-runs/bisector-trace.json +0 -1393
- package/docs/archive/investigations/curve-runs/curve-terrain.json +0 -1179
- package/docs/archive/investigations/curve-runs/curve-terrain.svg +0 -42
- package/docs/archive/investigations/hlg-ab.html +0 -34
- package/docs/archive/investigations/hlg-explorer.html +0 -238
- package/docs/archive/investigations/hlg-figs/cliff.svg +0 -1
- package/docs/archive/investigations/hlg-figs/cross.svg +0 -1
- package/docs/archive/investigations/hlg-figs/edges.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-grids.svg +0 -1
- package/docs/archive/investigations/hlg-figs/k20-peaks.svg +0 -1
- package/docs/archive/investigations/hlg-figs/lw-curve.svg +0 -1
- package/docs/archive/investigations/hlg-figs/terrain.svg +0 -1
- package/docs/archive/investigations/hlg-report.html +0 -262
- package/docs/archive/investigations/hlg-runs/hlg-ab.json +0 -149
- package/docs/archive/investigations/hlg-runs/hlg-blind-prompts.md +0 -87
- package/docs/archive/investigations/hlg-runs/hlg-blind.json +0 -107
- package/docs/archive/investigations/hlg-runs/hlg-cliff.json +0 -197
- package/docs/archive/investigations/hlg-runs/hlg-cross.json +0 -505
- package/docs/archive/investigations/hlg-runs/hlg-edge.json +0 -388
- package/docs/archive/investigations/hlg-runs/hlg-eval1.json +0 -25
- package/docs/archive/investigations/hlg-runs/hlg-frontier.json +0 -86
- package/docs/archive/investigations/hlg-runs/hlg-grid.json +0 -242
- package/docs/archive/investigations/hlg-runs/hlg-grid2.json +0 -343
- package/docs/archive/investigations/hlg-runs/hlg-lw-coarse.json +0 -110
- package/docs/archive/investigations/hlg-runs/hlg-map.json +0 -383
- package/docs/archive/investigations/hlg-runs/run0-original.log +0 -159
- package/docs/archive/investigations/hlg-runs/runA-recommended.log +0 -131
- package/docs/archive/investigations/hlg-runs/runAA-cliff-cpm5.log +0 -26
- package/docs/archive/investigations/hlg-runs/runAB-grid2-e4b.log +0 -18
- package/docs/archive/investigations/hlg-runs/runAC-peak-e4b-partial.log +0 -25
- package/docs/archive/investigations/hlg-runs/runAD-ab-samples.log +0 -8
- package/docs/archive/investigations/hlg-runs/runAE-edge-e4b.log +0 -51
- package/docs/archive/investigations/hlg-runs/runAF-logit-calibration.log +0 -10
- package/docs/archive/investigations/hlg-runs/runAG-eval-config2.log +0 -16
- package/docs/archive/investigations/hlg-runs/runB-gain-variants.log +0 -192
- package/docs/archive/investigations/hlg-runs/runC-pivots.log +0 -162
- package/docs/archive/investigations/hlg-runs/runD-oetf.log +0 -132
- package/docs/archive/investigations/hlg-runs/runE-eotf.log +0 -162
- package/docs/archive/investigations/hlg-runs/runF-shaper-working.log +0 -86
- package/docs/archive/investigations/hlg-runs/runG-shaper-sweep.log +0 -101
- package/docs/archive/investigations/hlg-runs/runH-wsweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runI-asweep.log +0 -176
- package/docs/archive/investigations/hlg-runs/runJ-knobmap.log +0 -49
- package/docs/archive/investigations/hlg-runs/runK-frontier.log +0 -19
- package/docs/archive/investigations/hlg-runs/runL-widemap.log +0 -65
- package/docs/archive/investigations/hlg-runs/runM-frontier-emb.log +0 -15
- package/docs/archive/investigations/hlg-runs/runN-verify.log +0 -116
- package/docs/archive/investigations/hlg-runs/runO-frontier-seed5000.log +0 -15
- package/docs/archive/investigations/hlg-runs/runP-grid-AxSm.log +0 -15
- package/docs/archive/investigations/hlg-runs/runQ-grid-tgW.log +0 -15
- package/docs/archive/investigations/hlg-runs/runR-blind-transcript.log +0 -79
- package/docs/archive/investigations/hlg-runs/runS-cross-coherence.log +0 -33
- package/docs/archive/investigations/hlg-runs/runT-cross-cpm5.log +0 -33
- package/docs/archive/investigations/hlg-runs/runU-cross-12b.log +0 -33
- package/docs/archive/investigations/hlg-runs/runV-lw-e4b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runW-lw-12b.log +0 -22
- package/docs/archive/investigations/hlg-runs/runX-lw-cpm5.log +0 -22
- package/docs/archive/investigations/hlg-runs/runY-cliff-e4b.log +0 -26
- package/docs/archive/investigations/hlg-runs/runZ-cliff-12b.log +0 -26
- package/docs/archive/mlx-bun-lab-report.html +0 -275
- package/docs/archive/wwdc-mlx-bun.html +0 -848
- package/docs/design/adapters-end-to-end.md +0 -205
- package/docs/design/batching-perf-path.md +0 -255
- package/docs/design/batching-v2-plan.md +0 -529
- package/docs/design/bucketing-stage.md +0 -197
- package/docs/design/compat-cli-surface-design.md +0 -230
- package/docs/design/docs-reorg-plan.md +0 -165
- package/docs/design/dspark-serving-program.md +0 -371
- package/docs/design/dspark-speculative-decoding.md +0 -318
- package/docs/design/grammar-spec-batching-integration.md +0 -301
- package/docs/design/memory-inference-path.md +0 -422
- package/docs/design/memory-synthesis.md +0 -287
- package/docs/design/memory-system.md +0 -237
- package/docs/design/mlx-lm-serving-execution-seam.md +0 -81
- package/docs/design/mlx-lm-tool-parity-plan.md +0 -545
- package/docs/design/omlx-adoption-map.md +0 -98
- package/docs/design/orpo-dynamic-lambda.md +0 -220
- package/docs/design/orpo-future-enhancements.md +0 -340
- package/docs/design/paged-kv-cache.md +0 -214
- package/docs/design/parallel-slots.md +0 -567
- package/docs/design/parity-tier-dag.md +0 -161
- package/docs/design/repo-cleanup-plan.md +0 -231
- package/docs/design/runtime-isolation.md +0 -263
- package/docs/design/segmented-backward-training.md +0 -543
- package/docs/design/spec-decode-larger-targets.md +0 -229
- package/docs/design/ssd-kv-cold-tier.md +0 -415
- package/docs/design/structured-output.md +0 -484
- package/docs/design/tauri-desktop-app.md +0 -127
- package/docs/design/the-dreaming-master-plan.md +0 -445
- package/docs/design/turboquant-kv.md +0 -235
- package/docs/design/turboquant-weights.md +0 -467
- package/docs/design/w4a16-compute-precision-spike.md +0 -232
- package/docs/design/web-chat-beat-matrix.md +0 -315
- package/docs/design/write-pipeline-entity.md +0 -468
- package/docs/planning/IDEAS.md +0 -69
- package/docs/reference/embedding.md +0 -197
- package/docs/reference/features-matrix.md +0 -101
- package/docs/reference/orpo-quickstart.md +0 -135
- package/src/flags.ts +0 -11
- /package/docs/{design → archive/investigations}/colibri-glm52-port.md +0 -0
- /package/docs/{planning → archive/planning}/chunk-task-roadmap.md +0 -0
- /package/docs/{planning → archive/planning}/curve-sampler-research-plan.md +0 -0
- /package/docs/{planning → archive/planning}/memory-docs-and-dag-plan.md +0 -0
- /package/docs/{planning → archive/planning}/pi-integration-review.md +0 -0
- /package/docs/{planning → archive/planning}/release-notes-v0.2.0.md +0 -0
- /package/src/{memory-plan.ts → admission-plan.ts} +0 -0
- /package/{docs/archive/investigations → src/serve/assets}/curve-terrain.html +0 -0
- /package/{docs/dag → src/serve/assets}/training-inference-map.html +0 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: active
|
|
3
|
+
axis: ON
|
|
4
|
+
canonical-for: batching
|
|
5
|
+
plan-anchor: "Phase 18 — Concurrent / batched serving (slots) + parallel load benchmark `[~]` (2026-06-13)"
|
|
6
|
+
last-verified: 2026-08-23
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Batching — continuous scheduling for `--batch N`
|
|
10
|
+
|
|
11
|
+
The canonical design for concurrent serving: the scheduling seam
|
|
12
|
+
(`src/serve/generation-gateway.ts`), the continuous-batching engine
|
|
13
|
+
(`src/serve/batch-scheduler.ts`), the dynamic-B cache primitives
|
|
14
|
+
(`src/model/batched-*.ts`), admission, and the parity discipline that
|
|
15
|
+
gates all of it. Consolidates docs/design/batching.md, docs/design/batching.md,
|
|
16
|
+
docs/design/batching.md, the batching items of
|
|
17
|
+
docs/design/batching.md, and the scheduler tie-in from
|
|
18
|
+
docs/design/kv-cache.md. Status and dated progress live in PLAN.md (see the
|
|
19
|
+
plan anchor above); this file describes the mechanism as it exists and
|
|
20
|
+
what is still open.
|
|
21
|
+
|
|
22
|
+
User-facing mirror: docs/reference/server-config.md ("Execution modes",
|
|
23
|
+
compatibility matrix, known limitations) and docs/reference/cli.md.
|
|
24
|
+
|
|
25
|
+
## 1. Why batch, and why continuous
|
|
26
|
+
|
|
27
|
+
Decode on Apple Silicon is bandwidth-bound: every token streams the whole
|
|
28
|
+
weight set through the GPU to produce one token. Batch B sequences and
|
|
29
|
+
the weights stream once for B tokens — aggregate tok/s scales roughly
|
|
30
|
+
linearly until the compute roofline.
|
|
31
|
+
|
|
32
|
+
The motivating workload is one person's agent harness: a coding agent
|
|
33
|
+
fans out sub-agents against one local server. Serially, the Nth agent's
|
|
34
|
+
first token waits for N−1 full generations. Batched, 4–8 agents decode
|
|
35
|
+
together — each stream somewhat slower, nobody queued. "Single-user" in
|
|
36
|
+
this project means single user, many agents. Consequences that shape the
|
|
37
|
+
design: arrivals are uncoordinated (so scheduling is concurrency-driven),
|
|
38
|
+
sub-agents share long system prompts (so prefix reuse on the batch lane
|
|
39
|
+
matters), and agent fleets want long contexts (so quantized KV must
|
|
40
|
+
compose with batching).
|
|
41
|
+
|
|
42
|
+
The scheduler is **continuous** (iteration-level), never static: it
|
|
43
|
+
works per decode step, admitting a newly arrived request after a short
|
|
44
|
+
prefill and retiring finished rows between steps. A late request joins at
|
|
45
|
+
the next step instead of waiting for the in-flight batch to finish. By
|
|
46
|
+
Little's law requests overlap even at modest rates because generations
|
|
47
|
+
last seconds. When traffic never overlaps there is only ever one live row
|
|
48
|
+
and the engine runs its B=1 fast path (§4.4).
|
|
49
|
+
|
|
50
|
+
## 2. Decisions and rationale
|
|
51
|
+
|
|
52
|
+
- **Concurrency IS the batch size.** `--batch N` is a concurrency cap
|
|
53
|
+
(mlx-lm's `--decode-concurrency` twin, accepted as an alias), not a
|
|
54
|
+
mode switch. Placement declares a *mechanism* per request; the
|
|
55
|
+
scheduler picks B=1 or B=N from its live row count. Default cap is 8
|
|
56
|
+
(`server.ts`: `serverOptions.batch ?? 8`), flipped from 1 on
|
|
57
|
+
2026-07-05 once a lone scheduler request measured within 0.992–0.996
|
|
58
|
+
of the strict serial decode (paired A/B, byte-identical output). The
|
|
59
|
+
earlier "mode switch, auto-batching rejected for determinism" decision
|
|
60
|
+
was reversed with an answer to its objection: mlx_lm.server itself
|
|
61
|
+
batches by arrival, so drop-in parity *requires* it, and anyone who
|
|
62
|
+
needs arrival-independent numerics pins `--batch 1`.
|
|
63
|
+
- **`--batch 1` is the determinism pin.** It runs the strict serialized
|
|
64
|
+
single-queue path; a request's bits never depend on what else was in
|
|
65
|
+
flight. Use it for golden regeneration.
|
|
66
|
+
- **bf16 batching IS the mlx-lm-parity contract.** mlx-lm's batched
|
|
67
|
+
path is bf16 (`BatchRotatingKVCache.to_quantized` raises), so the
|
|
68
|
+
oracle for a batched row is **mlx-lm's own batch mode at the same B**,
|
|
69
|
+
bit-exact — never our own B=1 (batching changes the attention
|
|
70
|
+
kernel's reduction order, so B=N vs B=1 measures the wrong thing).
|
|
71
|
+
KV-quant unset ⇒ bf16 on both lanes.
|
|
72
|
+
- **Compositions inherit the scheme's oracle.** Batched per-layer
|
|
73
|
+
quantized KV (`--kv-quant config`) has no mlx-lm oracle; each row is
|
|
74
|
+
gated against the *serial* composition of the same scheme (unpadded
|
|
75
|
+
rows bit-exact / KL 0; padded rows within a calibrated bf16
|
|
76
|
+
reduction-order envelope). Lab-style KL/eval gates are for no-oracle
|
|
77
|
+
schemes only, never a substitute for an oracle that exists.
|
|
78
|
+
- **Compat mode: no parity-breaking levers on the batched step.** The
|
|
79
|
+
scheduler drives `forwardHidden`/`logitsFromHidden` directly. Only
|
|
80
|
+
bit-exact machinery engages: compiled decode at B=1 on adopted
|
|
81
|
+
serial-class caches (same kill switch as serial), the compiled
|
|
82
|
+
activations, the pipelined readback. `--fused-sdpa` and `--force-wire`
|
|
83
|
+
never reach the batched lane.
|
|
84
|
+
- **Never downgrade a composition to make it batchable.** Placement is a
|
|
85
|
+
support check only: it does not strip MTP/drafting, KV schemes,
|
|
86
|
+
TurboQuant, grammar, adapters, or sampling. A composition the scheduler
|
|
87
|
+
does not implement runs unchanged on the serial mechanism.
|
|
88
|
+
- **Drain, never starve.** A serial-lane waiter pauses admission; running
|
|
89
|
+
rows finish; the lock hands over FIFO; admission resumes (mlx-lm
|
|
90
|
+
`drain_batch`). One `AsyncMutex` is the single exclusion domain for
|
|
91
|
+
the GPU and shared model state (`loraState`): serial generation, the
|
|
92
|
+
scheduler's whole active period, curve `/generate` + `/signal`, and
|
|
93
|
+
adapter mount/unmount all go through it.
|
|
94
|
+
- **Byte-budget admission is ours, not mlx-lm's.** mlx-lm caps counts
|
|
95
|
+
only; we project bytes (`--kv-budget`) because GPU OOM is uncatchable.
|
|
96
|
+
Budget accounting is total bytes, mirroring the byte-capped
|
|
97
|
+
`PromptCache` precedent. Queue-don't-OOM; never evict a running row.
|
|
98
|
+
- **Solo prefill, then join.** Joiners prefill alone (chunked,
|
|
99
|
+
interleaved with decode steps) and merge/extend into the running
|
|
100
|
+
batch. This is why SSM state needs no `ssm_mask` under left-padding:
|
|
101
|
+
state never sees a pad token, and the token-exact match vs mlx-lm's
|
|
102
|
+
masked-pad prefill proves the equivalence.
|
|
103
|
+
- **oMLX-style burst decode is refuted here.** Their `_step_burst` runs
|
|
104
|
+
K steps per event-loop hand-off to dodge Python's per-token GIL
|
|
105
|
+
ping-pong (~1 ms/token). Ported faithfully and measured 2026-07-02
|
|
106
|
+
(M1 Max 32 GB): cpm5 B=4 aggregate 345→289 tok/s, batch-lane B=1
|
|
107
|
+
149→121, TTFT +~100 ms (SSE flushes wait out the budget). Bun's
|
|
108
|
+
`setImmediate` hop costs microseconds; there is nothing to dodge.
|
|
109
|
+
Reverted; the per-yield step is the measured optimum. Don't re-add
|
|
110
|
+
without new evidence.
|
|
111
|
+
- **Spec decode and batching are different mechanisms.** A mounted
|
|
112
|
+
`--draft-model` routes every request serial (upstream
|
|
113
|
+
`is_batchable = draft is None`); per-row speculation inside the batch
|
|
114
|
+
is a research question, not a feature gap.
|
|
115
|
+
|
|
116
|
+
## 3. Scheduling seam — `GenerationGateway`
|
|
117
|
+
|
|
118
|
+
`place(shape)` freezes one `GenerationPlacement { shape, mechanism }`
|
|
119
|
+
with `mechanism ∈ {"serial", "continuous"}`; `run()` rejects a placement
|
|
120
|
+
made for another shape. The predicate, exactly as implemented
|
|
121
|
+
(`#supportsContinuous`):
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
continuous iff
|
|
125
|
+
model is not DiffusionGemmaModel (non-autoregressive)
|
|
126
|
+
AND batch > 1
|
|
127
|
+
AND #modelCachesBatchable() (cache-capability gate, below)
|
|
128
|
+
AND !hasVision (offset-0 prefill + bidirectional mask)
|
|
129
|
+
AND !hasAdapters (loraState is one global field)
|
|
130
|
+
AND !wantsLogprobs (batch sampler does not capture logprob arrays)
|
|
131
|
+
AND !userSeed (reproducibility ⇒ solo, matches mlx-lm)
|
|
132
|
+
AND !(kvQuant AND !#kvBatchable()) (scheme must be a batchable per-layer config)
|
|
133
|
+
AND !turboQuant (solo-only v1; belt on top of the capability braces)
|
|
134
|
+
AND !hasDraft (spec is a B=1 mode)
|
|
135
|
+
AND !(hasGrammar AND MLX_BUN_GRAMMAR_BATCH=0)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`hasRepetitionPenalty` and `hasLogitsExtras` (min_p, XTC, logit_bias,
|
|
139
|
+
presence/frequency) are carried on `RequestShape` for stats only — they
|
|
140
|
+
do **not** gate. The load-bearing reason: Qwen3.5 ships a default
|
|
141
|
+
repetition penalty in `generation_config.json`, which once routed every
|
|
142
|
+
request serial. Per-row `StepSampler`s fold the processors over a per-row
|
|
143
|
+
device-side token history, so all of these batch.
|
|
144
|
+
|
|
145
|
+
**Cache-capability gate** (`#modelCachesBatchable`, mirrors mlx-lm
|
|
146
|
+
server.py's all-caches-have-`merge` check), memoized from a fresh
|
|
147
|
+
`makeCache()`:
|
|
148
|
+
- `UniversalDenseModel`: batchable iff `!maskArray` and no
|
|
149
|
+
`sliding_attention` layer type — plain full-attention Tier-0 archs
|
|
150
|
+
(Llama) only; gemma2-family (pad-blind causal mask in `forwardLayers`)
|
|
151
|
+
and sliding-window universal archs are unvalidated cells → serial.
|
|
152
|
+
- Otherwise every cache must be `KVCache`, `RotatingKVCache`, a
|
|
153
|
+
`BatchableCache` implementer (GLM-5.2's `MLACache`, which owns its own
|
|
154
|
+
merge/extract/filter and byte projection), or `SSMCache` (Qwen3.5's
|
|
155
|
+
gated-DeltaNet state; `MLX_BUN_BATCH_SSM=0` removes it → serial).
|
|
156
|
+
|
|
157
|
+
**KV-scheme gate** (`#kvBatchable`): the server's resolved `KvScheme`
|
|
158
|
+
batches iff `kind === "affine-config"` and `scheme.batchable()` holds
|
|
159
|
+
with every configured layer a plain `KVCache` or plain `RotatingKVCache`.
|
|
160
|
+
Uniform `--kv-quant 4|8` (quantizedKvStart threshold semantics) and
|
|
161
|
+
`turbo` route serial with a startup warning. The scheme is threaded to
|
|
162
|
+
the scheduler only when this passes; the scheduler re-checks at
|
|
163
|
+
construction and throws on an unsupported scheme (never bf16 service
|
|
164
|
+
with quantized accounting).
|
|
165
|
+
|
|
166
|
+
Startup refusals (server.ts): `--paged-kv` with `--batch N>1` throws
|
|
167
|
+
(paged caches can't merge); GLM `--mtp on` (its default) mounts the MTP
|
|
168
|
+
drafter and therefore routes serial+spec — `--mtp off` exposes ordinary
|
|
169
|
+
GLM batching.
|
|
170
|
+
|
|
171
|
+
**Lanes and the lock.** Serial requests run under `runExclusive`, which
|
|
172
|
+
counts itself as a serial waiter; the scheduler's `admissionHeld()` reads
|
|
173
|
+
that count. The serial branch also wraps `onToken` in a ≥25 ms
|
|
174
|
+
rate-limited macrotask hop: serial decode is an unbroken microtask chain
|
|
175
|
+
and without the hop `/stats`, `/health`, and accepts stall for the whole
|
|
176
|
+
generation (measured 2.5 s on a 512-token cpm5 run, 2026-07-02; after:
|
|
177
|
+
10–44 ms). The batch lane needs none — its drive loop yields per step.
|
|
178
|
+
|
|
179
|
+
`/stats.batch` exposes `{configured, mode, batched, active_rows,
|
|
180
|
+
pending_rows, submitted_rows, kv_bytes, kv_budget_bytes}`; `mode` is
|
|
181
|
+
`off | serial | batch` (cap ≤1 / model not batchable / batchable).
|
|
182
|
+
`MLX_BUN_LANE_DEBUG=1` logs each placement with its shape.
|
|
183
|
+
|
|
184
|
+
## 4. The engine — `BatchScheduler`
|
|
185
|
+
|
|
186
|
+
One detached Bun-async driver loop (no threads) owns one running batch.
|
|
187
|
+
Per iteration: purge aborted pending rows → decide `hasWork` (running
|
|
188
|
+
rows, an in-flight prefill — finished even under drain — or admissible
|
|
189
|
+
pending) → sleep on `#wake` when idle, releasing the lock and the wired
|
|
190
|
+
limit → otherwise acquire both, start/advance at most one joiner prefill,
|
|
191
|
+
burst-admit further queued rows that fit before the next step, run one
|
|
192
|
+
decode step, yield.
|
|
193
|
+
|
|
194
|
+
### 4.1 Admission
|
|
195
|
+
|
|
196
|
+
FIFO from `#pending`, head-of-line, never reordered. A joiner starts iff
|
|
197
|
+
no prefill is in flight, admission isn't held, `running < maxBatch`, and
|
|
198
|
+
`#kvAdmits` passes:
|
|
199
|
+
- context limit: `prompt + max_tokens > batchCacheMaxTokens` (GLM's
|
|
200
|
+
compressed caches declare one) rejects with `RangeError`;
|
|
201
|
+
- no `--kv-budget` ⇒ admit;
|
|
202
|
+
- a row whose projected bytes exceed the budget *alone* (empty batch, no
|
|
203
|
+
prefill) rejects with an actionable error — never deadlocks the queue;
|
|
204
|
+
- otherwise admit iff `projectedKvBytes + need ≤ budget`, else wait.
|
|
205
|
+
|
|
206
|
+
Projection is worst-case: `batchRowKvBytes(config, prompt, max_tokens,
|
|
207
|
+
scheme)` = the scheme's `bytesAt` (window-capped for rotating layers,
|
|
208
|
+
quantized bytes under a config scheme), or per-cache `projectedBytes()`
|
|
209
|
+
when every cache is a `BatchableCache`. `/stats.batch.kv_bytes` is the
|
|
210
|
+
running + mid-prefill sum.
|
|
211
|
+
|
|
212
|
+
### 4.2 Joiner prefill (chunked, interleaved, cache-aware)
|
|
213
|
+
|
|
214
|
+
- **Prompt-cache take at admission** (`RowPromptCache.take`): the solo
|
|
215
|
+
prefill starts from the longest usable cached prefix (namespace `""`
|
|
216
|
+
— adapter requests never reach this lane); `cachedTokens` is real.
|
|
217
|
+
Serves are non-consuming zero-copy clones, so N agents sharing a
|
|
218
|
+
system prompt reuse one prefill. The SSD tier lives inside `take()`,
|
|
219
|
+
so restores reach batch joiners too.
|
|
220
|
+
- **Boundary snapshot**: for a cold-ish prefill ≥256 tokens, chunking
|
|
221
|
+
splits exactly at `min(snapshotAt ?? len, len−1)` and a trim-free
|
|
222
|
+
strict-prefix entry is cloned + put there — the same invariant as the
|
|
223
|
+
serial lane's `snapshotAt`. Without it the lane's only entry was the
|
|
224
|
+
untrimmable `[prompt+gen]` finish-time put, a total miss once rings
|
|
225
|
+
wrap (12B batched ctx-repeat 84 s vs serial 0.4 s, 2026-07-06).
|
|
226
|
+
- **One chunk per loop iteration** (`prefillChunkSize`, default 2048 =
|
|
227
|
+
the serial constant) with `evalAll(state)` + `#quantizeSolo` +
|
|
228
|
+
`clearCache()` at every chunk boundary, one decode step between
|
|
229
|
+
chunks. Running rows stall at most one chunk per joiner; the prefill
|
|
230
|
+
transient stays bounded.
|
|
231
|
+
- **Oracle tail convention** (`MLX_BUN_PREFILL_TAIL_SPLIT`, default on):
|
|
232
|
+
drain to `len−1`, then an L=1 forward of the last prompt token yields
|
|
233
|
+
step-0 logits — mlx-lm's `insert_segments` + `GenerationBatch._step`
|
|
234
|
+
shape, and what keeps `--batch 1 == unified == oracle` bit-exact.
|
|
235
|
+
- **Slice before the head**: the last hidden position is sliced before
|
|
236
|
+
`logitsFromHidden` (a whole-prompt `[1,Lp,V]` would be ~4.3 GB bf16 at
|
|
237
|
+
Gemma's V=262k and an 8k prompt).
|
|
238
|
+
- **Token 0 is sampled on the solo caches before merge**, so per-row
|
|
239
|
+
serial equivalence holds by construction. A row that stops at token 0
|
|
240
|
+
(EOS, stop sequence, 1-token grammar) never joins `#running`; its
|
|
241
|
+
prompt-only caches go back to the prompt cache.
|
|
242
|
+
- **Mixed-precision conversion** (`#quantizeSolo`): the scheduler-side
|
|
243
|
+
mirror of serial `maybeQuantizeKv` — same per-layer map, same skip
|
|
244
|
+
rules, same chunk-boundary placement (`KVCache.toQuantized` /
|
|
245
|
+
`RotatingKVCache.toQuantized`). That placement is what makes a row's
|
|
246
|
+
quantized bytes bit-exact vs serial `--kv-quant config`.
|
|
247
|
+
|
|
248
|
+
### 4.3 Join: adopt, extend, or merge — by layer kind
|
|
249
|
+
|
|
250
|
+
`#kinds[layer] ∈ {full, rot, ssm, owned-batch}` from the prototype
|
|
251
|
+
caches. Before any merge the decode pipeline is flushed (read + emit) so
|
|
252
|
+
the row set is settled and the next step starts cold.
|
|
253
|
+
|
|
254
|
+
**Adopt-don't-copy.** A row joining an *empty* batch keeps its solo
|
|
255
|
+
caches as the inners (pointer handoff, zero bytes moved). The lone row's
|
|
256
|
+
caches stay serial-class (`KVCache` / `RotatingKVCache` /
|
|
257
|
+
`QuantizedKVCache` / `RotatingQuantizedKVCache`), which is what makes the
|
|
258
|
+
B=1 step literally the serial graph and lets compiled decode and
|
|
259
|
+
prompt-cache put/take apply to it.
|
|
260
|
+
|
|
261
|
+
When a second row joins, per layer:
|
|
262
|
+
|
|
263
|
+
| kind | running batch cache | join op | file |
|
|
264
|
+
| --- | --- | --- | --- |
|
|
265
|
+
| full (bf16) | `KVCache` `[B,H,S,D]` + shared `#fullLeftPad` | `extendKVRows` — one pad + one B-axis concat, running rows untouched, pads grow (mlx-lm `BatchKVCache.extend`); `MLX_BUN_BATCH_EXTEND=0` = `mergeKVRows` re-merge (per-row slices, pads re-normalized; O(B·S)) | `src/model/batched-mask.ts` |
|
|
266
|
+
| full (quantized) | `QuantizedKVCache` over (packed, scales, biases) | `extendQuantRows` / `mergeQuantRows` — the same surgery on all three triples | `src/model/batched-quant.ts` |
|
|
267
|
+
| rot (bf16) | `BatchedRotatingCache` (persistent; self-tracks per-row leftPad/offsetArr through ring wrap) | always re-merge: extract each running row's temporal view + the new row → `BatchedRotatingCache.merge(rows, offsets, maxSize)`; an adopted lone `RotatingKVCache` is the first row (pad 0) | `src/model/batched-rotating.ts` |
|
|
268
|
+
| rot (quantized) | `BatchedRotatingQuantCache` | same recipe over triples, `merge(rows, offsets, maxSize, groupSize, bits)` | `src/model/batched-rotating-quant.ts` |
|
|
269
|
+
| ssm | `SSMCache` `[B,…]` state, no temporal axis, no padding | `SSMCache.mergeRows(prev, solo)` — B-axis concat | `src/model/qwen3-delta.ts` |
|
|
270
|
+
| owned-batch | `BatchableCache` (GLM `MLACache`) | `solo.makeEmptyBatch().mergeRows([prev, solo])`; the cache owns leftPad/rowOffsets | `src/model/glm52-cache.ts` |
|
|
271
|
+
|
|
272
|
+
Routing among these branches is **by capability only** (`isRowBatchCache`,
|
|
273
|
+
`isBatchableCache`, `isQuantizedKvCache`, …). `BatchedRotatingCache` has
|
|
274
|
+
no `signature()` override, so any signature-based conjunct silently drops
|
|
275
|
+
the running batch from the merge — the 2026-08-22 agg×4 outage from
|
|
276
|
+
443f333 (joiners built a B<max ring; the next full-B step crashed in the
|
|
277
|
+
grow-path concatenate; whole-batch drop). `tests/batch-rotating-join.test.ts`
|
|
278
|
+
pins this.
|
|
279
|
+
|
|
280
|
+
After a real merge every row is marked `merged`: its KV is interleaved in
|
|
281
|
+
batched buffers and must be *extracted*, not adopted, at finish.
|
|
282
|
+
|
|
283
|
+
### 4.4 The decode step
|
|
284
|
+
|
|
285
|
+
Pipelined, mirroring mlx-lm `GenerationBatch._step`: (1) forward all rows
|
|
286
|
+
from the still-unread `[B]` token register (or, cold, from each row's
|
|
287
|
+
`current`), sample, `asyncEvalAll` the new register; (2) then read the
|
|
288
|
+
*previous* register with `toIntTokens()` (an eval + raw int read — never
|
|
289
|
+
`toFloat32()`, whose cast kernel queued behind the whole next step and
|
|
290
|
+
stalled every token), emit, evict. Rows that finish get one extra
|
|
291
|
+
harmless KV write from the already-built step; `filter` drops the row
|
|
292
|
+
(mlx-lm behaves identically). `MLX_BUN_BATCH_NO_PIPELINE=1` reads
|
|
293
|
+
synchronously (same math, slower).
|
|
294
|
+
|
|
295
|
+
**Unpadded fast path.** When every `#fullLeftPad` is 0 the full layers'
|
|
296
|
+
per-step `BatchedDecodeMaskCache` / `BatchedQuantDecodeMaskCache`
|
|
297
|
+
wrapper is skipped: `KVCache.makeMask(1)` is already the empty mask and
|
|
298
|
+
every row sits at the shared scalar offset, so the bare cache dispatches
|
|
299
|
+
the same per-step graph serial builds. The wrapper otherwise costs a
|
|
300
|
+
host mask build + ~8 device nodes per full layer per token. Rot,
|
|
301
|
+
owned-batch, and ssm caches always pass through unwrapped.
|
|
302
|
+
|
|
303
|
+
**Compiled decode at B=1.** With exactly one row, unpadded, a uint32
|
|
304
|
+
register, serial-class inners (`CompiledDecode.supports`; a
|
|
305
|
+
filtered-to-one batched rot-quant cache is excluded), and a Gemma4
|
|
306
|
+
dense model, the scheduler replays the serial engine's compiled step
|
|
307
|
+
(`MLX_BUN_COMPILED_DECODE` is the same kill switch). A failed step is
|
|
308
|
+
transactional: the scheduler permanently falls back to the graph path.
|
|
309
|
+
|
|
310
|
+
**Vectorized homogeneous sampling.** When every live row is plain greedy
|
|
311
|
+
(`plainGreedy` from the gateway: temperature 0, no processors, no
|
|
312
|
+
grammar), one `toLogprobs` + `argmaxAxis` over `[B,V]` replaces B
|
|
313
|
+
slice/sample/concat graphs — per-row identical math, tie behavior
|
|
314
|
+
included. `MLX_BUN_BATCH_VEC_SAMPLE=0` forces the per-row closure path.
|
|
315
|
+
Length-doomed rows get a placeholder slot (never emitted; one KV write
|
|
316
|
+
on their own about-to-evict row).
|
|
317
|
+
|
|
318
|
+
**Grammar rows** (`#stepGrammar`): read the previous register *before*
|
|
319
|
+
building the graph (the matcher's `accept` needs JS numbers), `accept()`
|
|
320
|
+
per live grammar row (fires async bitmask fills that overlap the graph
|
|
321
|
+
build), `await ready()`, sample per row with the mask applied in the
|
|
322
|
+
closure, emit the values already read. While any grammar row is live the
|
|
323
|
+
batch effectively runs unpipelined — bounded by the ~0.1 ms readback +
|
|
324
|
+
0.004–0.19 ms/row fills. Flushes must `accept()` the flushed tokens too,
|
|
325
|
+
or matchers run one token behind on every mid-decode join (found by the
|
|
326
|
+
conformance gate, 2026-07-03).
|
|
327
|
+
|
|
328
|
+
**Housekeeping.** `clearCache()` every 256 steps (serial's cadence;
|
|
329
|
+
mlx-lm batched uses 512), not per step. The per-step `setImmediate` hop
|
|
330
|
+
is rate-limited to 25 ms when exactly one row is running with an empty
|
|
331
|
+
queue and no drain — the 0.4–0.8% B=1 drive-loop tax, paired-measured
|
|
332
|
+
2026-07-05. `MLX_BUN_BATCH_STEP_TRACE=1` prints per-step build/read/
|
|
333
|
+
emit/gap timings.
|
|
334
|
+
|
|
335
|
+
### 4.5 Emit, evict, extract
|
|
336
|
+
|
|
337
|
+
`#emit` mirrors `generate()`: EOS terminates **without** an `onToken`
|
|
338
|
+
call; `onToken` returning `false` → `stop`; grammar terminated → `stop`
|
|
339
|
+
(the final token was delivered; never sample into an all-`-inf` mask);
|
|
340
|
+
`generated ≥ max_tokens` → `length`. Per-row `StopMatcher`, tool router,
|
|
341
|
+
and SSE fan-out live in each request's `onToken` closure.
|
|
342
|
+
|
|
343
|
+
**Failure containment.** A row's `onToken` throwing rejects and evicts
|
|
344
|
+
*that* row (mlx-lm `remove`); siblings continue. A forward/sampling error
|
|
345
|
+
cannot be attributed to a row: every row rejects and the batch KV is
|
|
346
|
+
dropped (`dropOnly`, never put). A prefill error rejects only the joiner.
|
|
347
|
+
|
|
348
|
+
**Eviction** (`#applyFilter(keep)`): `filterRows` on batched/owned
|
|
349
|
+
caches; `filterKVRows` / `filterQuantRows` (B-axis `takeAxis`) on full
|
|
350
|
+
layers; the pending register and `#pendingReal` flags are filtered in
|
|
351
|
+
lockstep. Full-layer filter keeps stale left padding (no `min_left_pad`
|
|
352
|
+
shift-left) — width waste, not correctness (open, §8).
|
|
353
|
+
|
|
354
|
+
**Finish-time prompt-cache put.** A lone never-merged row hands its
|
|
355
|
+
adopted serial-class caches back zero-copy, keyed by `promptIds + fed`
|
|
356
|
+
(`fed` = generated tokens whose KV actually entered the cache, per-row
|
|
357
|
+
exact via the register's real/placeholder flags; a placeholder that ever
|
|
358
|
+
fed taints the row and extraction refuses). A merged row with ≥256
|
|
359
|
+
prompt tokens is **extracted** per layer into fresh owned serial caches
|
|
360
|
+
before the filter mutates the batch (`#extractRowCaches`: `extractRow`,
|
|
361
|
+
`extractKVRow`, `extractQuantRow`; SSM only when the row's advance count
|
|
362
|
+
equals the key exactly — recurrent state is untrimmable), evaluated
|
|
363
|
+
asynchronously so the batched source buffers free once the copies land,
|
|
364
|
+
then put. Extract is a pure slice+copy of byte-preserved rows
|
|
365
|
+
(`tests/batched-extract.test.ts`), so an extracted row's bytes equal the
|
|
366
|
+
solo run's. All take/put happens inside the gateway's exclusion domain —
|
|
367
|
+
the scheduler is the sole owner of cache mutations on its lane.
|
|
368
|
+
|
|
369
|
+
## 5. The one hard numerics problem: per-sequence position
|
|
370
|
+
|
|
371
|
+
Rows are different lengths at different positions, but a cache has one
|
|
372
|
+
`offset`, RoPE takes one offset, and the mask is built from it. mlx-lm's
|
|
373
|
+
solution, and ours: **left-padding + per-row mask + per-row RoPE**.
|
|
374
|
+
|
|
375
|
+
- Right-align every row so "the current token" is one column → one
|
|
376
|
+
advancing offset still works. Full layers share `#fullLeftPad`; rot
|
|
377
|
+
caches self-track per-row `leftPad`/`offsetArr` across the ring wrap.
|
|
378
|
+
- Mask padding with a real `[B,1,1,S]` boolean mask (`j >= leftPad[b]`,
|
|
379
|
+
`buildBatchedDecodeMask`) even at the N=1 decode step, and use the same
|
|
380
|
+
fused bool-mask `scaled_dot_product_attention` mlx-lm uses. An additive
|
|
381
|
+
mask would deviate.
|
|
382
|
+
- RoPE positions are a per-row array (`ropeOffsetArr`,
|
|
383
|
+
`ops.ropeDynamic`; `UniversalRope.applyDynamic` for Tier-0 archs).
|
|
384
|
+
Two traps every attention implementation must respect: capture the
|
|
385
|
+
offset array **once** per forward (reading it before and after
|
|
386
|
+
`updateAndFetch` gives K and Q different positions — the 2026-06-14
|
|
387
|
+
Gemma fix; the generator `scripts/gen-model.ts` carries it for
|
|
388
|
+
generated handlers), and a batched cache's `ropeOffsetArr` must be
|
|
389
|
+
**stable within a step** and refreshed only in `releaseRopeArr` (the
|
|
390
|
+
rope-array step-stability contract from milestone 2). MiniCPM5
|
|
391
|
+
(2026-06-14) and universal dense (2026-07-03, latent since v0.0.9)
|
|
392
|
+
each shipped a scalar-offset bug that only the batched oracle caught:
|
|
393
|
+
every model family wires the per-row path separately.
|
|
394
|
+
- Batched prefill numerics differ from B=1 prefill (GEMV vs GEMM
|
|
395
|
+
reduction order), and a padded row's attention sum accumulates in a
|
|
396
|
+
different order than its solo run. The unpadded row is bit-exact vs
|
|
397
|
+
solo; the padded row carries bounded bf16 noise (≤0.23 logit diff over
|
|
398
|
+
8 steps, CPM, 2026-06-14). Calibrate the envelope per model; don't
|
|
399
|
+
chase it — mlx-lm B=2 diverges from its own B=1 the same way.
|
|
400
|
+
|
|
401
|
+
## 6. KV memory model
|
|
402
|
+
|
|
403
|
+
Three rungs, in increasing fidelity of "who needs it most":
|
|
404
|
+
|
|
405
|
+
1. **Static per-slot partition** — rejected, never built (wastes memory,
|
|
406
|
+
arbitrary per-slot context cap).
|
|
407
|
+
2. **Dynamic shared budget, contiguous padded caches** — what ships.
|
|
408
|
+
Each layer holds one `[B,H,S,D]` buffer (growing in `KVCache.STEP`
|
|
409
|
+
= 256 granules) whose width tracks the *longest* live row; short rows
|
|
410
|
+
pay padding to that width. Admission is by projected total bytes
|
|
411
|
+
(§4.1). Quantized KV multiplies the budget (~4× at 4-bit), and
|
|
412
|
+
batching × quantized KV compounds.
|
|
413
|
+
3. **Paged KV** — the density upgrade. `PagedKVCache`/`BlockPool`
|
|
414
|
+
(`src/lab/paged-kv/paged-kv.ts`, `--paged-kv`) shipped as a serial-only,
|
|
415
|
+
bf16, Gemma4 full-attention block manager, bit-exact vs the plain
|
|
416
|
+
path. The batching payoff is **not built**: paged caches into the
|
|
417
|
+
scheduler's `LayerInner` union, block-count admission replacing byte
|
|
418
|
+
projection, `#mergeJoiner`/`#applyFilter`/`#extractAndPut` allocating
|
|
419
|
+
and freeing blocks instead of pad+concat, then block-level CoW prefix
|
|
420
|
+
sharing and a fused paged-attention kernel. That is where the
|
|
421
|
+
padded-batch-waste win lives (a 50-token row cohabiting with a
|
|
422
|
+
4000-token row pays ~4000 tokens of KV today).
|
|
423
|
+
|
|
424
|
+
The `--prompt-cache` budget is the *prefix* cache; live decode KV is a
|
|
425
|
+
separate pool. mlx-lm trims its prompt cache to `total − live batch
|
|
426
|
+
bytes`; sharing one ceiling between the LRU and the live batch is still
|
|
427
|
+
open (§8).
|
|
428
|
+
|
|
429
|
+
## 7. Verification
|
|
430
|
+
|
|
431
|
+
Oracle discipline: every batched mechanism lands with a token-for-token
|
|
432
|
+
gate against whoever ships the same protocol, generated from the oracle
|
|
433
|
+
venv (`scripts/oracle/gen-batched-golden.py` static B=N;
|
|
434
|
+
`gen-batched-dynamic-golden.py` merge/extract/filter join+leave;
|
|
435
|
+
`gen-batched-extend-golden.py` — extend's pad layout differs from
|
|
436
|
+
re-merge, so it has its own golden; `gen-rotating-golden.py`
|
|
437
|
+
`BatchRotatingKVCache` model-free incl. ring wrap). Goldens are
|
|
438
|
+
machine-layered (`goldens/*.json`, per-GPU overrides under
|
|
439
|
+
`goldens/<machine>/`). The B=N-vs-our-B=1 KL harness is an internal
|
|
440
|
+
consistency check only, never the gate.
|
|
441
|
+
|
|
442
|
+
| Behavior | Test | Tier |
|
|
443
|
+
| --- | --- | --- |
|
|
444
|
+
| rotating merge/decode/mask/filter incl. ring wrap vs mlx-lm; generic row-storage state | `tests/batched-rotating.test.ts`, `batched-rotating-state.test.ts` | fast, oracle |
|
|
445
|
+
| static B=2 + dynamic join/leave + extend-join vs mlx-lm (CPM, Gemma 12B/e4b/26B, Llama 3B Tier-0) | `tests/batched-decode-parity.test.ts` (`MLX_BUN_TEST_BATCH_DECODE=1`) | gated, oracle |
|
|
446
|
+
| Qwen3.5 SSM batched vs mlx-lm B=2 | `tests/qwen35-batched-parity.test.ts` | gated, oracle |
|
|
447
|
+
| scheduler orchestration (admit/step/evict/join) | `tests/batch-scheduler.test.ts` | gated (teacher-forced KL + 12B golden) |
|
|
448
|
+
| per-row containment, drain, full-attention join routing | `tests/batch-containment.test.ts` | gated |
|
|
449
|
+
| rotating-layer join mid-decode (2026-08-22 regression) | `tests/batch-rotating-join.test.ts` | gated |
|
|
450
|
+
| KV-budget queueing + projection math | `tests/batch-kv-budget.test.ts`, `kv-budget-projection.test.ts` | gated / fast |
|
|
451
|
+
| vectorized sampling bit-equal A/B | `tests/batch-vec-sample.test.ts` | gated |
|
|
452
|
+
| grammar under batching (mixed masks, 4 schemas at B=4, churn, joiner, truncation) | `tests/batch-grammar.test.ts` | gated |
|
|
453
|
+
| quantized dynamic-B ops; batched mixed-KV per row vs serial composition | `tests/batched-quant.test.ts`, `batched-rotating-quant.test.ts`, `batched-kv-quant-parity.test.ts` | fast / gated |
|
|
454
|
+
| per-row extract == solo bytes | `tests/batched-extract.test.ts` | fast |
|
|
455
|
+
| live server: engage, `/stats`, SSE fan-out, serial coexistence | `tests/batch-serving.test.ts` | gated smoke |
|
|
456
|
+
|
|
457
|
+
Benchmark harness: `scripts/bench-matrix.ts features` (six-cell
|
|
458
|
+
composition matrix over live SSE: TTFT p50/p95 at the first SSE token,
|
|
459
|
+
wall-clock aggregate tok/s, schema conformance as a hard gate);
|
|
460
|
+
`scripts/bench-serving-load.ts` is the client-only stack-vs-stack tool.
|
|
461
|
+
Quotable numbers come from a clean machine only; SSE bursts inflate
|
|
462
|
+
naive tok/s — use wall-clock.
|
|
463
|
+
|
|
464
|
+
## 8. Open items
|
|
465
|
+
|
|
466
|
+
- **`--prompt-concurrency`**: not accepted by the CLI (mlx-lm's cap on
|
|
467
|
+
joiners per iteration; the drop-in alias slot). Today at most one
|
|
468
|
+
joiner prefills at a time.
|
|
469
|
+
- **Rotating-layer `extend`**: Gemma joins extend their full layers but
|
|
470
|
+
re-merge their sliding layers (`BatchedRotatingCache.merge` only).
|
|
471
|
+
- **Full-layer filter `min_left_pad` shift** after eviction (mlx-lm
|
|
472
|
+
cache.py does it); width waste only.
|
|
473
|
+
- **Logprobs on the batch lane**: `wantsLogprobs` routes serial; the
|
|
474
|
+
per-row sampler already computes `toLogprobs` — extend `RowSampler` to
|
|
475
|
+
return extras and pass them through `onToken`.
|
|
476
|
+
- **Batched timing stats**: the gateway returns `prefillMs/decodeMs/
|
|
477
|
+
*Tps = 0` for continuous rows; `usage` timing is missing there.
|
|
478
|
+
`maxTokens ?? 512` fallback in the gateway differs from the server
|
|
479
|
+
default (dead in practice, a trap for library callers).
|
|
480
|
+
- **LoRA-group batching**: adapter requests route serial. Design
|
|
481
|
+
decided (adapter-set as the batch compatibility key; join if equal,
|
|
482
|
+
drain if not; `loraState.active` set once per epoch); end state is
|
|
483
|
+
per-slot adapter state in the batched step (vLLM/Punica is the oracle).
|
|
484
|
+
- **Prompt-cache byte interplay**: mirror mlx-lm's
|
|
485
|
+
`trim_to(total − activeBatchBytes)` so the LRU and the live batch
|
|
486
|
+
share one ceiling. Derived default for `--kv-budget` (usable − weights
|
|
487
|
+
− prompt-cache cap − headroom) instead of unlimited-when-unset.
|
|
488
|
+
- **Long-context batched golden**: bf16 batched decode past the sliding
|
|
489
|
+
window is oracle-gated model-free (`batched-rotating`), not yet as an
|
|
490
|
+
end-to-end forward golden on a real Gemma.
|
|
491
|
+
- **Unvalidated model cells** stay serial by the capability gate:
|
|
492
|
+
gemma2-family (`maskArray`) and sliding-window universal archs.
|
|
493
|
+
- **Paged KV batched integration** (§6 rung 3): block allocation in the
|
|
494
|
+
scheduler, block-count admission, block CoW, fused paged kernel.
|
|
495
|
+
- **GLM per-row MTP**: `--mtp on` routes serial+spec; batched MTP is
|
|
496
|
+
post-release.
|
|
497
|
+
- **Grammar jump-forward** (`MLX_BUN_GRAMMAR_JUMP`) is serial-only;
|
|
498
|
+
`#stepGrammar` does not jump. Native XGrammar-2 via TVM-FFI has a
|
|
499
|
+
B≫8 trigger.
|
|
500
|
+
- **Device-side step chaining (depth-k pipelining)**: serial-decode
|
|
501
|
+
work, orthogonal to the scheduler; the matrix's serial baselines show
|
|
502
|
+
its headroom.
|
|
503
|
+
- **Batch-invariant kernels** (identical numerics regardless of B): Lab
|
|
504
|
+
research item; would restore determinism under load for free.
|
|
505
|
+
- **Per-row speculation inside the batch**: non-goal (variable accept
|
|
506
|
+
lengths break uniform B); research question, not scheduled.
|
|
507
|
+
- **Stale source comments**: `batch-scheduler.ts` header still says
|
|
508
|
+
joins re-merge (extend landed 2026-07-04); `generation-gateway.ts`
|
|
509
|
+
header and server-config.md say merged rows are never re-put
|
|
510
|
+
(`#extractAndPut` now extracts merged rows ≥256 prompt tokens).
|
|
511
|
+
|
|
512
|
+
## 9. Measured numbers (labels required)
|
|
513
|
+
|
|
514
|
+
- oMLX head-to-head, **M1 Max 32 GB, loaded machine, 2026-07-02**, 4
|
|
515
|
+
concurrent × 128 tok, median of 3, wall-clock aggregate tok/s, oMLX
|
|
516
|
+
0.4.5.dev1 on the same OptiQ snapshots: MiniCPM5-1B 339.0 vs mlx-bun
|
|
517
|
+
`--batch 4` 345.4 (mean TTFT 397 vs 119 ms); gemma-4-e4b 89.8 vs 87.1;
|
|
518
|
+
Qwen3.5-4B 100.9 vs 99.5 (TTFT 848 vs 368 ms). Before P5/logits
|
|
519
|
+
processors landed the same day, oMLX led 1.46–1.89× and our TTFT
|
|
520
|
+
queue-inflated to 1.1–3.8 s.
|
|
521
|
+
- Batch-lane B=1 gap before Phase 2, **cpm5, M1 Max, 2026-07-05**: ~149
|
|
522
|
+
tok/s vs 267 serial (1.8×). mlx-lm's own `BatchGenerator` at B=1:
|
|
523
|
+
256.5 vs 264.6 for `stream_generate` (3.3% tax) — the unified design
|
|
524
|
+
was proven achievable before the gap was closed.
|
|
525
|
+
- After Phase 3.2, **2026-07-05 paired A/B**: unified B=1 decode ratios
|
|
526
|
+
0.992–0.996 vs strict serial, byte-identical output (the default-flip
|
|
527
|
+
evidence).
|
|
528
|
+
- Batched mixed-KV, **cpm5, 2026-07-05**: `--batch 2 --kv-quant config`
|
|
529
|
+
400 tok @ 240 tok/s aggregate; B=1 bit-exact vs the optiq golden;
|
|
530
|
+
padded-row KL within the 5e-2 envelope (bf16 same-harness ~9e-3).
|
|
531
|
+
Gemma 12B rotating-quant B=2 join: unpadded row KL 0 every step, padded
|
|
532
|
+
≤4e-3.
|
|
533
|
+
|
|
534
|
+
## 10. History
|
|
535
|
+
|
|
536
|
+
- 2026-06-13 — Phase 18 opened; batched padded prefill found already
|
|
537
|
+
proven in the training path (`buildBatchedPadMask`, `BatchedMaskCache`).
|
|
538
|
+
- 2026-06-14 — `--batch N` flag (designed as `--slots`; `--decode-concurrency`
|
|
539
|
+
alias); `BatchedDecodeMaskCache` + decode-parity harness; dynamic-B
|
|
540
|
+
`mergeKVRows`/`filterKVRows` vs mlx-lm dynamic golden;
|
|
541
|
+
`BatchedRotatingCache`; `BatchScheduler` + `GenerationGateway` wired;
|
|
542
|
+
L1 row green — CPM, Gemma 12B, e4b, 26B bit-exact vs mlx-lm B=2.
|
|
543
|
+
- 2026-07-01 — v2 hotfix bundle: slice-before-head, cache-capability
|
|
544
|
+
gate, one lock domain (`runExclusive`); per-row containment +
|
|
545
|
+
drain-on-serial-waiter; engine hygiene (pipelined decode, clearCache/256,
|
|
546
|
+
chunked interleaved admit).
|
|
547
|
+
- 2026-07-02 — SSM batched path (Qwen3.5, token-exact vs mlx-lm B=2);
|
|
548
|
+
per-row logits processors; `MLX_BUN_LANE_DEBUG`; serial-lane macrotask
|
|
549
|
+
hop; oMLX burst port refuted; `--model` override honored by serve/bench.
|
|
550
|
+
- 2026-07-03 — grammar B1 per-row matchers + churn gates; `--kv-budget`
|
|
551
|
+
admission + `/stats.batch` fields; Tier-0 universal per-row RoPE fix
|
|
552
|
+
(Llama token-exact vs mlx-lm B=2); `bench-matrix.ts features`.
|
|
553
|
+
- 2026-07-04 — extend-join for full-attention layers (own oracle);
|
|
554
|
+
vectorized homogeneous greedy sampling (bit-equal A/B).
|
|
555
|
+
- 2026-07-05 — Phase 2: B=1 host tax closed (`toIntTokens`, unpadded
|
|
556
|
+
fast path); Phase 3.1: batched per-layer quantized KV (full layers);
|
|
557
|
+
Phase 3.2: adopt-don't-copy, compiled decode at B=1, prompt-cache
|
|
558
|
+
take/put on the batch lane; milestone 2: batched rotating-quant (every
|
|
559
|
+
shipped `kv_config` batches); `--batch` default 1→8; concurrency-is-the-
|
|
560
|
+
batch-size decision.
|
|
561
|
+
- 2026-07-06 — boundary-snapshot put on the batch lane (12B ctx repeat
|
|
562
|
+
84 s → serial-class).
|
|
563
|
+
- 2026-07-07 — rot-quant join `temporalView` leak fixed.
|
|
564
|
+
- 2026-08-21 — Serving consolidation: immutable `GenerationPlacement`
|
|
565
|
+
(`serial` | `continuous`); resolved `KvScheme` authoritative through
|
|
566
|
+
placement, conversion, and projection; GLM `MLACache` as a
|
|
567
|
+
`BatchableCache`.
|
|
568
|
+
- 2026-08-22 — rotating-join regression from 443f333 fixed (capability-only
|
|
569
|
+
merge routing); `tests/batch-rotating-join.test.ts`.
|