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
|
@@ -67,7 +67,9 @@ Request body (OpenAI chat schema; unknown fields ignored):
|
|
|
67
67
|
// wins. On templates with a reasoning-depth
|
|
68
68
|
// variable (Qwen3.8) the level also maps
|
|
69
69
|
// into the template: minimal/low → low,
|
|
70
|
-
// medium → medium, high/xhigh → xhigh
|
|
70
|
+
// medium → medium, high/xhigh → xhigh.
|
|
71
|
+
// Any OTHER string is a 400 — a typo used
|
|
72
|
+
// to silently force thinking ON
|
|
71
73
|
// (Qwen3.8's default depth is xhigh).
|
|
72
74
|
"hlg": { // HLG tone-curve sampling (per request).
|
|
73
75
|
"enabled": true, // merged over --hlg-sampling server defaults.
|
|
@@ -106,16 +108,37 @@ Message `content` is a string or an array of parts:
|
|
|
106
108
|
(http/https URLs also accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF,
|
|
107
109
|
BMP via native OS codecs; requires a model with the vision sidecar).
|
|
108
110
|
|
|
109
|
-
Remote (http/https) media URLs — image and
|
|
110
|
-
destination policy: **private/loopback/link-local hosts are
|
|
111
|
-
default** (the request URL is attacker-controlled input —
|
|
112
|
-
redirect hop is re-validated, and the fetch has a **10 s
|
|
113
|
-
**64 MB response cap
|
|
111
|
+
Remote (http/https) media URLs — image, audio, and video alike — go
|
|
112
|
+
through a destination policy: **private/loopback/link-local hosts are
|
|
113
|
+
refused by default** (the request URL is attacker-controlled input —
|
|
114
|
+
SSRF), every redirect hop is re-validated, and the fetch has a **10 s
|
|
115
|
+
timeout** and a **64 MB response cap** (video: **256 MB** — clips are
|
|
116
|
+
legitimately larger; frame sampling truncates long clips server-side).
|
|
117
|
+
Violations are clean `400`s
|
|
114
118
|
(`prompt build failed: image url rejected: …`). Serving media from a LAN
|
|
115
119
|
host (a NAS, another machine) is the opt-in
|
|
116
120
|
[`--allow-private-media`](server-config.md#start-flags) flag. `data:`
|
|
117
121
|
URLs are decoded locally and never policy-checked.
|
|
118
122
|
|
|
123
|
+
### Video input (Qwen3.5-family models)
|
|
124
|
+
|
|
125
|
+
Video rides the same content-part array:
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
{ "type": "video_url", "video_url": { "url": "data:video/mp4;base64,…|http(s)://…" } }
|
|
129
|
+
{ "type": "video", "data": "<base64>" } // alias
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The container decodes through macOS's own AVFoundation codecs (mp4/mov,
|
|
133
|
+
H.264/HEVC/ProRes/… — whatever this macOS plays) via the
|
|
134
|
+
`mlx-bun-frame-extract` helper that ships beside the binary and in the
|
|
135
|
+
native pack; from a source tree it compiles on first use with the Xcode
|
|
136
|
+
CLT. Frames are sampled at **2 fps, capped at 768 frames**, then run the
|
|
137
|
+
gated Qwen3-VL video pipeline (temporal frame pairs, per-frame-group
|
|
138
|
+
attention, 3D mRoPE positions). Qwen3.5-family models only; video never
|
|
139
|
+
combines with audio parts, and a non-Qwen model answers video parts with
|
|
140
|
+
a clean `400`.
|
|
141
|
+
|
|
119
142
|
### Audio input
|
|
120
143
|
|
|
121
144
|
Audio rides the same content-part array (OpenAI-canonical shape plus two
|
|
@@ -188,7 +211,7 @@ Non-streaming response:
|
|
|
188
211
|
"prompt_tokens_details": { "cached_tokens": 0 }, // prompt-cache reuse
|
|
189
212
|
"lane": "serial", // "serial" | "serial+spec" | "batched" —
|
|
190
213
|
// which execution lane served THIS
|
|
191
|
-
// request (GenerationGateway.
|
|
214
|
+
// request (GenerationGateway.place
|
|
192
215
|
// + whether a draft actually contributed
|
|
193
216
|
// accepted tokens). Chat completions
|
|
194
217
|
// only (streaming: final usage chunk).
|
|
@@ -344,12 +367,11 @@ All errors are `{ "error": { "message": …, ... } }`.
|
|
|
344
367
|
`top_logprobs must be of type int` / `at least 0` / `at most 11`;
|
|
345
368
|
`top_logprobs: -1` is the accepted "unset" sentinel).
|
|
346
369
|
- `400` with `"type": "memory_admission"`, `"code":
|
|
347
|
-
"context_over_budget"` — the
|
|
348
|
-
safe context.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
at `/stats`.
|
|
370
|
+
"context_over_budget"` — the PROMPT itself leaves no generation slot
|
|
371
|
+
within the max safe context. A prompt that fits is never rejected for a
|
|
372
|
+
broad `max_tokens`: the upper bound is capped to the remaining room
|
|
373
|
+
(`max_tokens` is a ceiling, not a promise) and generation proceeds. The
|
|
374
|
+
ceiling is visible at `/stats`.
|
|
353
375
|
|
|
354
376
|
## POST /v1/completions (raw text completion)
|
|
355
377
|
|
|
@@ -581,7 +603,9 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
581
603
|
"ssd_cache": { "dir": "...", "entries": 0, "bytes": 0, "max_bytes": 0,
|
|
582
604
|
"restores": 0, "spills": 0, "restore_ms_last": 0, "demotions": 0,
|
|
583
605
|
// bounded write-behind queue (MLX_BUN_SSD_SPILL_QUEUE_GB):
|
|
584
|
-
"
|
|
606
|
+
"pending_snapshots": 0, "pending_spills": 0,
|
|
607
|
+
"pending_spill_bytes": 0, "dropped_spills": 0,
|
|
608
|
+
"failed_spills": 0, "longest_durable_prefix_tokens": 0 },
|
|
585
609
|
"admission": {
|
|
586
610
|
"max_safe_context": 0, // tokens; requests above this 400
|
|
587
611
|
"memory_budget_bytes": null, // explicit budget, or null (machine default)
|
|
@@ -631,6 +655,37 @@ vision/audio, adapters, and training are not emulated by the serving port.
|
|
|
631
655
|
}
|
|
632
656
|
```
|
|
633
657
|
|
|
658
|
+
`pending_snapshots` counts prompt-cache entries scheduled for persistence but
|
|
659
|
+
not yet confirmed by the atomic SSD store. `longest_durable_prefix_tokens` is
|
|
660
|
+
the longest prefix already present in the SSD index, not merely queued.
|
|
661
|
+
|
|
662
|
+
## POST /admin/cache/flush
|
|
663
|
+
|
|
664
|
+
Forces every pending SSD prompt-cache snapshot through the serial write queue
|
|
665
|
+
and waits for the atomic temp-file/fsync/rename boundary. The response is `200`
|
|
666
|
+
only when no snapshot or spill remains pending and no write failed during this
|
|
667
|
+
flush; otherwise it is `503` with the counters that explain why.
|
|
668
|
+
|
|
669
|
+
```jsonc
|
|
670
|
+
{
|
|
671
|
+
"durable": true,
|
|
672
|
+
"flushedSnapshots": 1,
|
|
673
|
+
"missingSnapshots": 0,
|
|
674
|
+
"pendingSnapshots": 0,
|
|
675
|
+
"pendingSpills": 0,
|
|
676
|
+
"pendingSpillBytes": 0,
|
|
677
|
+
"droppedSpills": 0,
|
|
678
|
+
"failedSpills": 0,
|
|
679
|
+
"elapsedMs": 842,
|
|
680
|
+
"entries": 2,
|
|
681
|
+
"longest_durable_prefix_tokens": 16384
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
The standard benchmark calls this endpoint before its restart leg and records
|
|
686
|
+
the result. Normal `mlx-bun serve` shutdown on `SIGINT` or `SIGTERM` performs
|
|
687
|
+
the same flush after active requests drain, with a 120-second default timeout.
|
|
688
|
+
|
|
634
689
|
## GET /library
|
|
635
690
|
|
|
636
691
|
Returns all models found in the local HuggingFace hub cache (via the
|
|
@@ -9,8 +9,8 @@ adapters admin) see [server-api.md](./server-api.md); this doc is about
|
|
|
9
9
|
|
|
10
10
|
The flags are parsed in `serverRuntimeFlags()`
|
|
11
11
|
([src/cli.ts](../../src/cli.ts)); the runtime knobs live in
|
|
12
|
-
`ServerOptions` ([src/server.ts](../../src/server.ts)). The
|
|
13
|
-
|
|
12
|
+
`ServerOptions` ([src/server.ts](../../src/server.ts)). The serial and
|
|
13
|
+
continuous scheduling mechanisms and their declaration are in
|
|
14
14
|
[src/serve/generation-gateway.ts](../../src/serve/generation-gateway.ts);
|
|
15
15
|
the design rationale is in
|
|
16
16
|
[docs/design/parallel-slots.md](../design/parallel-slots.md).
|
|
@@ -21,21 +21,21 @@ the design rationale is in
|
|
|
21
21
|
| --- | --- | --- | --- |
|
|
22
22
|
| `--host` | addr | `127.0.0.1` | Interface to bind. Loopback-only by default (mlx_lm.server parity); pass `--host 0.0.0.0` to expose the server on your network. |
|
|
23
23
|
| `--port` | n | `8080` | Listen port. A pre-flight probe refuses to start if the port is already serving. |
|
|
24
|
-
| `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling.
|
|
24
|
+
| `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling. A request whose prompt fits is always admitted: a broad client `max_tokens` is capped to the room remaining under the safe context (`max_tokens` is a ceiling, not a promise), so agent clients with family-wide defaults never 400 over a small overshoot. Only a prompt that leaves no generation slot is rejected with **400** (`type: memory_admission`) — the alternative is an uncatchable GPU OOM. The safe-context ceiling bills the active `--kv-quant` scheme at its true bytes/element (a 4-bit cache admits the ~3.5× larger window it actually enables; `turbo` is still billed bf16, conservatively). Admission never refuses to *start*: a budget too small for any context logs a warning and serves anyway (requests then get the per-request 400). GLM-5.2 additionally runs its exact process equation before opening resident weights or expert slabs and caps MLX to the plan's allocator reserve. **Decimal GB (×10⁹).** |
|
|
25
25
|
| `--prompt-cache` | GB | `8` | RAM prefix-KV cache (byte-capped LRU; `0` disables). **PREFIX SHARING (2026-07-05): serves are NON-CONSUMING** — a hit hands out zero-copy clones and leaves the donor entry intact, so N agents (or a brand-new session) sharing a system prompt all reuse ONE prefill without destroying each other's conversation entries (the old consume-and-trim take cannibalized the donor). Extended entries supersede their prefix-ancestors (when trimmable), so a conversation stays one entry. Works on BOTH lanes and tiers over the SSD store (Layer 0). |
|
|
26
26
|
| `--isolate` | (bool) | off | Runtime isolation ([runtime-isolation.md](../design/runtime-isolation.md)): the inference engine runs as a CHILD process on a unix socket; this process stays a pure UI/API reverse proxy — instant under any GPU load, survives engine crashes (auto-respawn, in-flight requests get a clean 502). Measured cost ≈0 (−0.4% tok/s, +2 ms TTFT, per-token SSE granularity preserved). `/ws/chat` is not proxied yet (501). `/engine` reports child pid/restarts. |
|
|
27
27
|
| `--model-pool` | n | `1` | With `--isolate`: max RESIDENT model engines. Requests naming another model (**exact `/v1/models` id** — fuzzy strings keep mlx-lm's ignored-field semantics) spawn/route to that model's own engine child (SPAWN-OVERLAP: the new model loads while the old one keeps serving; nobody's stream is interrupted). Over the cap, the LRU engine drains, **demotes its prompt cache to the SSD tier**, and exits — switching back respawns it with state restored from disk. Measured (M1 Max, cpm5⇄qwen0.8b, pool 1): switch 1.5 s, switch-back 1.2 s with `cached_tokens` 103/104 — the conversation survived the eviction. The pool drives this eviction via `POST /admin/drain` — unix-socket-only (never exposed on the public TCP listener) — which quiesces the gateway and demotes the whole prompt cache to the SSD tier before eviction, responding `{ drained: true, demotions: N }`. |
|
|
28
|
-
| `--ssd-cache` | dir | off | SSD cold tier under the prompt cache ([docs/design/ssd-kv-cold-tier.md](../design/ssd-kv-cold-tier.md)): prefix KV spills to disk on RAM eviction AND idle demotion, is snapshotted after requests settle (debounced 1 s; the flush is idle-gated per tensor so it never taxes an active decode — `MLX_BUN_SSD_WRITEBEHIND=0` disables), and **survives restarts** — a long-context agent re-attach restores via a bounded streamed copy instead of re-prefilling (measured 13.7k-token prefix: 12 s → ~0.25 s TTFT; 0% decode overhead; restore transient = live entry + one tensor, no mapping outlives the load). Entries are keyed by model fingerprint **+ effective kv scheme** + tokenizer hash + adapter ns; incompatible/corrupt files self-quarantine. **Layer 0 (2026-07-05): the tiering lives inside PromptCache.take(), so BOTH lanes restore from disk — batch-scheduler joiners included — and every put (batch rows too) schedules the write-behind snapshot.** Requires the RAM cache. |
|
|
28
|
+
| `--ssd-cache` | dir | off | SSD cold tier under the prompt cache ([docs/design/ssd-kv-cold-tier.md](../design/ssd-kv-cold-tier.md)): prefix KV spills to disk on RAM eviction AND idle demotion, is snapshotted after requests settle (debounced 1 s; the flush is idle-gated per tensor so it never taxes an active decode — `MLX_BUN_SSD_WRITEBEHIND=0` disables), and **survives restarts** — a long-context agent re-attach restores via a bounded streamed copy instead of re-prefilling (measured 13.7k-token prefix: 12 s → ~0.25 s TTFT; 0% decode overhead; restore transient = live entry + one tensor, no mapping outlives the load). Dirty snapshots remain tracked until the atomic SSD store confirms them; `SIGINT`/`SIGTERM` drains active requests and flushes them before exit, and `POST /admin/cache/flush` exposes the same explicit boundary. Entries are keyed by model fingerprint **+ effective kv scheme** + tokenizer hash + adapter ns; incompatible/corrupt files self-quarantine. **Layer 0 (2026-07-05): the tiering lives inside PromptCache.take(), so BOTH lanes restore from disk — batch-scheduler joiners included — and every put (batch rows too) schedules the write-behind snapshot.** Requires the RAM cache. |
|
|
29
29
|
| `--ssd-cache-max` | GB | `32` | SSD tier byte cap (binary GiB); oldest-mtime entries evicted at the cap. |
|
|
30
30
|
| `--ssd-cache-verify` | (bool) | off | Verify every tensor hash on restore before it is copied in (roughly doubles restore reads) — integrity paranoia only; the header hash is always verified. |
|
|
31
31
|
| `--ssd-demote-idle` | sec | `300` (with `--ssd-cache`) | Idle demotion (Layer 0): prompt-cache entries unused this long spill to the SSD tier and **free their GPU memory** — unified memory drains between agent bursts while every prefix stays reachable (the next hit restores via the bounded streamed copy, ~0.25 s for a 13.7k-token entry vs a 12 s re-prefill). Swept only when the engine is fully idle (never drains a running batch). `0` disables. |
|
|
32
32
|
| `--batch` | n | `8` | Max concurrent requests decoded together. **Default flipped 1→8 (2026-07-05)** after GATE-B1-SPEED: a LONE request through the unified engine IS the serial engine — adopted serial-class caches, compiled decode, prompt cache + SSD restore, byte-identical output, 0.992–0.996 paired decode ratios — so the cap only changes behavior when concurrent requests actually arrive (the agentic sub-agent workload: 4–8 coding agents against one local server). `--batch 1` pins strict serial (arrival-independent numerics). See [Execution modes](#execution-modes-serial-vs---batch-n). `--decode-concurrency` is accepted for drop-in compatibility (mlx_lm.server's cap, default 32). |
|
|
33
|
-
| `--kv-quant` | `config`\|`off`\|`4`\|`8`\|`turbo[:k<bits>v<bits>]` | `off` (bf16) | KV-cache quantization. **Default flipped to bf16 2026-07-05** (naked = L1): quantized KV measured 5–20% slower decode than bf16 at ≤16k on every model — on mlx-lm too — so it pays only in **memory headroom** (e.g. ~1.3 GB on the 12B @16k) and is an explicit opt-in (`--kv-quant …` or `--l2`, whose preset passes `config`). `off` = bf16 (L1). `4`/`8` = **uniform** bits (group 64, start 0) — the scheme mlx-lm exposes as `--kv-bits`/`--kv-group-size`/`--quantized-kv-start`. In the L1 kernel set (`--l1 --kv-quant 8` = fused-sdpa off) our unfused quantized SDPA is **op-for-op identical** to mlx-lm's `quantized_scaled_dot_product_attention`, so uniform-quant is **bit-exact L1** (the fused-sdpa-ON path is optiq-aligned). `config` = **per-layer mixed-precision** from `kv_config.json` — optiq-only, no mlx-lm analog → **L2**. `turbo[:k<bits>v<bits>]` = **TurboQuant** ([docs/design/turboquant-kv.md](../design/turboquant-kv.md)): rotation-based KV quantization (asymmetric-affine int8/int4/int2/int5 keys, FWHT + Lloyd-Max values) — default `k8v3` (2.56× compression at head_dim 128); `kBits` ∈ {2,4,5,8}, `vBits` ∈ {2,3,4,5,8}. A separate axis from the scheme above (mutually exclusive with `config`/`4`/`8`); v1 is dequantize-on-fetch (stock `ops.sdpa`, no fused kernel) and **full-attention layers only** — sliding-window layers stay bf16 (one-time warning, never a throw). Under `--batch N`, a per-layer `config` scheme **batches** — full-attention layers since Phase 3.1 (MiniCPM5), rotating/sliding layers since milestone 2 (gemma's whole kv_config), so every shipped `kv_config.json` now batches. The scheduler applies the mixed scheme per row, gated bit-exact per row vs the serial composition (`tests/batched-kv-quant-parity.test.ts` — compositions no other stack ships). Uniform `4`/`8` still routes those requests serial (quantizedKvStart threshold semantics). **`turbo` is solo-only, unconditionally** — `TurboQuantKVCache` is a novel `Cache` implementation (no merge/filter/temporalView), so it's excluded from the batch scheduler by construction; `GenerationGateway.
|
|
33
|
+
| `--kv-quant` | `config`\|`off`\|`4`\|`8`\|`turbo[:k<bits>v<bits>]` | `off` (bf16) | KV-cache quantization. **Default flipped to bf16 2026-07-05** (naked = L1): quantized KV measured 5–20% slower decode than bf16 at ≤16k on every model — on mlx-lm too — so it pays only in **memory headroom** (e.g. ~1.3 GB on the 12B @16k) and is an explicit opt-in (`--kv-quant …` or `--l2`, whose preset passes `config`). `off` = bf16 (L1). `4`/`8` = **uniform** bits (group 64, start 0) — the scheme mlx-lm exposes as `--kv-bits`/`--kv-group-size`/`--quantized-kv-start`. In the L1 kernel set (`--l1 --kv-quant 8` = fused-sdpa off) our unfused quantized SDPA is **op-for-op identical** to mlx-lm's `quantized_scaled_dot_product_attention`, so uniform-quant is **bit-exact L1** (the fused-sdpa-ON path is optiq-aligned). `config` = **per-layer mixed-precision** from `kv_config.json` — optiq-only, no mlx-lm analog → **L2**. `turbo[:k<bits>v<bits>]` = **TurboQuant** ([docs/design/turboquant-kv.md](../design/turboquant-kv.md)): rotation-based KV quantization (asymmetric-affine int8/int4/int2/int5 keys, FWHT + Lloyd-Max values) — default `k8v3` (2.56× compression at head_dim 128); `kBits` ∈ {2,4,5,8}, `vBits` ∈ {2,3,4,5,8}. A separate axis from the scheme above (mutually exclusive with `config`/`4`/`8`); v1 is dequantize-on-fetch (stock `ops.sdpa`, no fused kernel) and **full-attention layers only** — sliding-window layers stay bf16 (one-time warning, never a throw). Under `--batch N`, a per-layer `config` scheme **batches** — full-attention layers since Phase 3.1 (MiniCPM5), rotating/sliding layers since milestone 2 (gemma's whole kv_config), so every shipped `kv_config.json` now batches. The scheduler applies the mixed scheme per row, gated bit-exact per row vs the serial composition (`tests/batched-kv-quant-parity.test.ts` — compositions no other stack ships). Uniform `4`/`8` still routes those requests serial (quantizedKvStart threshold semantics). **`turbo` is solo-only, unconditionally** — `TurboQuantKVCache` is a novel `Cache` implementation (no merge/filter/temporalView), so it's excluded from the batch scheduler by construction; `GenerationGateway.place` also refuses continuous scheduling explicitly (belt + braces — both layers exist on purpose). |
|
|
34
34
|
| `--paged-kv` | (bool) | off | **OPTIONAL vLLM-style paged KV cache** ([docs/design/paged-kv-cache.md](../design/paged-kv-cache.md)): full-attention layers store K/V in fixed-size **block pools** (host-side block table, `ops.takeAxis` gather back to contiguous before the stock SDPA — no new attention math, no fused kernel in v1). Env: `MLX_BUN_PAGED_KV=1`. **Gated bit-exact vs the plain path** (`tests/paged-kv-parity.test.ts`: identical single-forward logits + identical 48-token greedy trajectory on the 12B). **v1 scope: serial batch=1, Gemma4-family, bf16** — with no explicit `--batch` the CLI pins `--batch 1`; the server **refuses** (fails startup, never silently downgrades) `--batch N>1`, any `--kv-quant`, and `--draft-model`; sliding-window layers keep the rotating scheme (mixed paged-full + rotating-sliding is the supported shape). Media (vision/audio) and LoRA-adapter **requests** are scoped OUT per request — they run the plain cache path (v1 non-goal cells), never a 400. Paged requests **bypass the prompt cache** (fresh caches per request; `--ssd-cache` sees nothing) and run **uncompiled decode** (a data-dependent block-list length can't shapeless-replay). Expect a small decode cost at batch=1 — the per-step gather copy buys nothing until the batched/CoW follow-ups; this ships the correctness-proven abstraction, honestly labeled. Pool sized from prompt+`max_tokens` at request setup; exhaustion is a typed error (accounting tripwire), never truncation. |
|
|
35
35
|
| `--paged-kv-block-size` | n | `256` | Tokens per KV block (`--paged-kv` only). 256 = the plain cache's growth step, so v1's allocation granularity is a permutation of today's into reusable slots, not a new tuning axis. |
|
|
36
36
|
| `--adapter` | dir | none | Mount a LoRA adapter at startup (same machinery as `POST /v1/adapters`; the adapter id is the directory's basename) and make it the **default** for requests that send no `adapter` field. A request's explicit `adapter` — including `"none"` — always wins, and hot-swap via `/v1/adapters` is unchanged. `--adapter-path` is accepted as the mlx_lm.server-named alias. A bad adapter fails startup loudly rather than silently serving the base model. This is the flag `mlx-bun train`'s completion message points at. |
|
|
37
37
|
| `--draft-model` | path/query | none | **Speculative decoding**: a drafter proposes tokens the main model verifies in one forward — exact results, faster decode when drafts land. Resolves like the main model. The artifact's **kind is auto-detected**: a full same-tokenizer model (mlx_lm.server parity — L1: token-for-token vs mlx-lm's spec path; tokenizer-family mismatch fails startup, upstream silently accepts ~0%), a Gemma `-assistant` KV-borrowing drafter (L2 vs optiq spec_generate; ~1.09× γ=1 on 12B, no training), a locally-trained **DSpark** checkpoint (`dspark.json`), or one of **DeepSeek's released DSpark drafters** (DeepSpec `Gemma4DSparkModel` config stamp, e.g. `deepseek-ai/dspark_gemma4_12b_block7` for the 12B — oracle: DeepSpec's reference at temperature 0, token-for-token). All four share ONE serve loop. A confidence-calibrated DSpark checkpoint additionally self-schedules its draft length per round (threshold pruning — fewer wasted verify positions; uncalibrated checkpoints draft fixed-length). Mounting a draft routes **every** request to the serial lane (upstream `is_batchable = draft is None`). Pays on slow targets (12B+); fast small models lose to the draft overhead. Composes with structured output (the constrained verify walk). Prompt-cache reuse is bypassed on the spec path (v1). Telemetry: `usage.speculation` (`drafted`/`accepted`/`targetCalls`). |
|
|
38
|
-
| `--draft-kind` | `two-model`\|`assistant`\|`dspark`\|`deepspec`\|`ngram` | auto | Override the draft-artifact kind detection (`dspark.json` → dspark; `Gemma4DSparkModel` architecture stamp → deepspec; `*_assistant` config → assistant; else two-model). **`ngram` is never auto-detected — it has no artifact**: MODEL-FREE prompt lookup (drafts copied from the request's own prompt+generation when its trailing k-gram occurred earlier in the stream; port of prompt-lookup decoding / vLLM's `ngram` proposer). Mount it ALONE — `--draft-kind ngram` with a `--draft-model` is refused, as is any other kind without one. Zero weights/memory, lossless by the same verify (gated: serve-loop output token-identical to non-spec greedy on e4b, tests/spec-ngram.test.ts); a no-match round degrades to one plain target step. Best on agentic/RAG/code-edit traffic that re-emits context spans. |
|
|
38
|
+
| `--draft-kind` | `two-model`\|`assistant`\|`dspark`\|`deepspec`\|`mtp`\|`ngram` | auto | Override the draft-artifact kind detection (`dspark.json` → dspark; `Gemma4DSparkModel` architecture stamp → deepspec; `*_assistant` config → assistant; `*_mtp` model_type → mtp, the target-release-native multi-token-prediction head — it shares the target's embeddings/lm-head, defaults `--num-draft-tokens` to its trained `block_size - 1`, and partial rejects roll the target's recurrent DeltaNet caches back via bit-exact snapshot/replay; else two-model). `mtp` alone (no `--draft-model`) mounts the companion bundled at `<model>/mtp/` when present — single-repo artifacts — and refuses otherwise. **`ngram` is never auto-detected — it has no artifact**: MODEL-FREE prompt lookup (drafts copied from the request's own prompt+generation when its trailing k-gram occurred earlier in the stream; port of prompt-lookup decoding / vLLM's `ngram` proposer). Mount it ALONE — `--draft-kind ngram` with a `--draft-model` is refused, as is any other kind without one. Zero weights/memory, lossless by the same verify (gated: serve-loop output token-identical to non-spec greedy on e4b, tests/spec-ngram.test.ts); a no-match round degrades to one plain target step. Best on agentic/RAG/code-edit traffic that re-emits context spans. |
|
|
39
39
|
| `--num-draft-tokens` | n | `3` (`ngram`: `10`) | Drafts per verify round (mlx_lm.server's default; `mlx_lm.generate`'s is 2). A DSpark draft pins this to its trained block width (`cfg.gamma`); ngram drafting is free so its default is the reference implementation's 10. |
|
|
40
40
|
| `--ngram-max` / `--ngram-min` | k | `3` / `1` | `--draft-kind ngram` only: longest/shortest trailing k-gram searched for a match (the reference values — longest first, first occurrence wins). |
|
|
41
41
|
| `--mtp` | `on`\|`off` | on for GLM-5.2 | Mount the checkpoint-native GLM MTP row as the server's drafter. It uses the bounded auxiliary expert tier and exact serial verify loop (`usage.lane: "serial+spec"`). `off` removes the draft so ordinary GLM requests can use continuous batching; per-row batched MTP is post-release. Mutually exclusive with an explicit `--draft-model`/`--draft-kind`. |
|
|
@@ -47,7 +47,7 @@ the design rationale is in
|
|
|
47
47
|
| `--max-tokens` | n | GLM-5.2: `128`; otherwise `65536` chat / `512` raw completion | Completion cap when a request omits `max_tokens` (mlx_lm.server flag; its default there is 512 — `--max-tokens 512` reproduces mlx_lm.server exactly). For GLM this is also reserved by the pre-open resource equation and must fit inside `--context-length`. |
|
|
48
48
|
| `--allow-private-media` | (bool) | off | Let `image_url`/`audio_url` content parts fetch from **private/loopback/link-local** hosts (a NAS, another LAN box). Off by default: a request's URL is attacker-controlled input, so remote media fetches (`src/media-fetch.ts`) refuse non-http(s) schemes and destinations in private/loopback/link-local/CGNAT ranges — including hosts that *resolve* there and every **redirect hop** (each hop re-passes the whole policy) — which keeps the server from being steered at cloud metadata (`169.254.169.254`) or LAN-internal services (SSRF). Independent of the flag, every remote fetch has a **10 s wall-clock timeout** and a **64 MB response cap** (streaming-enforced, not just Content-Length); violations surface as clean `400`s (`prompt build failed: …`). `data:` URLs decode locally and are never policy-checked. |
|
|
49
49
|
| `--no-open` | (bool) | off | Skip the automatic browser open on start. By default an interactive terminal session opens `http://<host>:<port>/#/chat` once the server is ready; pass this flag to suppress it (e.g. headless or non-TTY environments already skip it). |
|
|
50
|
-
| `--hlg-sampling` | `on`\|`off` | off | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail. The overall gain folds from `--temperature`. See [docs/
|
|
50
|
+
| `--hlg-sampling` | `on`\|`off` | off | Piecewise tone-curve (HLG) sampling: rolls off the top-token region, boosts the mids, gentles the tail. The overall gain folds from `--temperature`. See [docs/archive/hlg-sampling.md](../design/hlg-sampling.md). |
|
|
51
51
|
| `--hlg-width` | nats | `4` | HLG mid-region half-width (nats). Only meaningful with `--hlg-sampling on`. |
|
|
52
52
|
| `--hlg-shoulder` | nats | `4` | HLG highlight rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
|
|
53
53
|
| `--hlg-toe` | nats | `6` | HLG shadow rolloff scale (nats). Only meaningful with `--hlg-sampling on`. |
|
|
@@ -81,6 +81,22 @@ model loads, so they apply to `mlx-bun pi` too. They affect the
|
|
|
81
81
|
3.2) — see
|
|
82
82
|
[Levers that don't reach the batched lane](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design).
|
|
83
83
|
|
|
84
|
+
## Near-ceiling models on small machines (24 GB)
|
|
85
|
+
|
|
86
|
+
A model whose weights approach the DEFAULT macOS GPU wired ceiling (~75% of
|
|
87
|
+
RAM — e.g. a 17 GB model on a 24 GB machine) loads and serves short prompts,
|
|
88
|
+
but a long prefill can exceed the ceiling and die with an **uncatchable**
|
|
89
|
+
Metal OOM (the failure is asynchronous; no server error is possible).
|
|
90
|
+
`mlx-bun serve` detects this at startup and prints the remedy:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
sudo sysctl iogpu.wired_limit_mb=<RAM_MB - ~2500> # resets on reboot
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For very long contexts on such machines, additionally prefer `--kv-quant`
|
|
97
|
+
(e.g. `turbo`) and expect the engine to use smaller prefill chunks — the
|
|
98
|
+
prefill transient scales with chunk size × context depth.
|
|
99
|
+
|
|
84
100
|
## Per-request overrides
|
|
85
101
|
|
|
86
102
|
Most quality knobs can be set per request in the chat body and override
|
|
@@ -117,7 +133,8 @@ for paired A/B harnesses).
|
|
|
117
133
|
| `MLX_BUN_GRAMMAR` | — | on (`!=="0"`) | `=0` disables structured-output mask compilation. Requested constraints still take the documented graceful-degrade route: chat prompt injection plus a `Warning` header; raw completions emit the header only. |
|
|
118
134
|
| `MLX_BUN_GRAMMAR_JUMP` | — | **off** (`==="1"`) | Jump-forward decoding for structured output (SGLang's technique via xgrammar's `findJumpForwardString`): when the grammar forces a unique continuation (JSON punctuation/keys; longest under `any_whitespace:false` fixed formatting), the SERIAL lane emits its retokenized ids with ONE multi-token forward instead of one masked forward per token. Lossless in STRING space and always grammar-valid, but the token stream (and, conditioning on it, content after a forced span) can legally differ from an unjumped run — no oracle, hence opt-in. Excluded automatically when either `logprobs` or `top_logprobs` is requested, because jumped tokens have no sampled distribution row; SentencePiece-family tokenizers whose raw encode can't reproduce a mid-stream span simply never jump (guard degrades to normal decode). The batch lane's per-row grammar (`#stepGrammar`) does not jump yet. Tests: tests/grammar-jump.test.ts (contract + Llama-3.2-1B e2e). |
|
|
119
135
|
| `MLX_BUN_SSD_WRITEBEHIND` | — | on (`!=="0"`) | `--ssd-cache`'s debounced write-behind snapshot (restart survival). `=0` disables it entirely — the paired-A/B lever + kill switch for the 2026-07-07 idle-gating fix (eviction/demotion spills still write). The flush is **idle-gated**: every per-tensor step (blocking GPU sync + `writeSync`) waits for the engine to go idle and pauses when a request arrives mid-flush, so durability work never taxes an active decode (pre-fix: a ~16k entry's flush overlapping cached ctx repeats cost e4b ~9% decode@ctx vs mlx-lm). The snapshot timer also never holds batch admission — while rows are active it re-arms instead of grabbing the exclusive. |
|
|
120
|
-
| `MLX_BUN_SSD_SPILL_QUEUE_GB` | — | 2 | Byte cap on the write-behind queue's PENDING spill clones (they pin the evicted entries' GPU memory until the idle-gated flush runs). Over cap, the oldest queued spill drops
|
|
136
|
+
| `MLX_BUN_SSD_SPILL_QUEUE_GB` | — | 2 | Byte cap on the write-behind queue's PENDING spill clones (they pin the evicted entries' GPU memory until the idle-gated flush runs). Over cap, the oldest queued spill drops and its clones free immediately. The durability coordinator keeps the source snapshot dirty so an explicit or shutdown flush can retry it. `=0` keeps only the newest + in-flight clone pinned (the soft cap never drops the item just enqueued). `/stats.ssd_cache` reports pending, dropped, failed, and longest-durable-prefix counters. |
|
|
137
|
+
| `MLX_BUN_SHUTDOWN_TIMEOUT_MS` | — | `120000` | Maximum time `mlx-bun serve` gives active requests plus the SSD durability flush after `SIGINT` or `SIGTERM`. On timeout the process logs the remaining snapshot/spill counters and exits. |
|
|
121
138
|
| `MLX_BUN_BATCH_NO_PIPELINE` | — | off (`==="1"`) | Disables the batch lane's pipelined token readback (the A/B lever for the shipped pipelining, like its peers `MLX_BUN_BATCH_EXTEND` / `MLX_BUN_BATCH_SSM` / `MLX_BUN_BATCH_VEC_SAMPLE`). Numerically equivalent, slower steps. |
|
|
122
139
|
| `MLX_BUN_DSPARK_MINCONF` | — | checkpoint-driven | Overrides the DSpark/DeepSpec draft scheduler's minimum-confidence threshold (Alg-1 draft-length pruning). Spec lane only; no CLI flag — a calibration/A/B lever (see [dspark-speculative-decoding.md](../design/dspark-speculative-decoding.md)). |
|
|
123
140
|
| `MLX_BUN_EXPERT_OFFLOAD` | `--expert-offload` | off (`==="1"`) | Env mirror of the flag for direct/library runs (scripts that never parse serve flags). |
|
|
@@ -130,12 +147,12 @@ exporting them now does nothing.)
|
|
|
130
147
|
|
|
131
148
|
## Execution modes: serial vs. `--batch N`
|
|
132
149
|
|
|
133
|
-
|
|
134
|
-
lone request
|
|
135
|
-
serial-class objects, compiled decode replays, the prompt cache and SSD
|
|
136
|
-
tier serve it
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
Scheduling is **concurrency-driven** (default cap 8, flipped 2026-07-05): a
|
|
151
|
+
lone scheduler request uses the B=1 fast path — its caches are adopted
|
|
152
|
+
serial-class objects, compiled decode replays, and the prompt cache and SSD
|
|
153
|
+
tier serve it. Only a second concurrent request causes a batch layout to
|
|
154
|
+
exist. The flag declares the concurrency cap; active rows select B=1 or B=N
|
|
155
|
+
inside the scheduler.
|
|
139
156
|
|
|
140
157
|
`--batch 1` pins the strict serialized single-queue path: one generation
|
|
141
158
|
at a time, arrival-independent numerics (a request's bits never depend on
|
|
@@ -159,23 +176,29 @@ row for bf16, and per-row oracle-gated for the quantized compositions
|
|
|
159
176
|
serial lane (quantizedKvStart threshold semantics; a startup warning
|
|
160
177
|
is printed).
|
|
161
178
|
|
|
162
|
-
###
|
|
179
|
+
### Scheduling declaration (`GenerationGateway.place`)
|
|
180
|
+
|
|
181
|
+
`--batch` declares a concurrency cap. With a cap greater than one,
|
|
182
|
+
`GenerationGateway.place()` freezes one scheduling mechanism for the exact
|
|
183
|
+
resolved execution composition. `continuous` admits it to the scheduler; that
|
|
184
|
+
scheduler chooses its B=1 fast path or B=N step from active rows. `serial`
|
|
185
|
+
preserves the strict or dedicated executor. The support check never removes,
|
|
186
|
+
downgrades, or substitutes MTP, KV schemes, TurboQuant, grammar, adapters, or
|
|
187
|
+
sampling. A composition the scheduler does not implement drains it and runs
|
|
188
|
+
unchanged through the serial mechanism.
|
|
163
189
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
mutually exclusive on the GPU (one `AsyncMutex`), so a serial-fallback
|
|
168
|
-
request never runs alongside a batched step — but batched requests run
|
|
169
|
-
concurrently with each other.
|
|
190
|
+
The two mechanisms are mutually exclusive on the GPU (one `AsyncMutex`), so a
|
|
191
|
+
serial request never runs alongside a continuous step, while requests admitted
|
|
192
|
+
to the continuous scheduler can run concurrently with each other.
|
|
170
193
|
|
|
171
|
-
| Request property |
|
|
194
|
+
| Request property | Continuous scheduler? |
|
|
172
195
|
| --- | --- |
|
|
173
196
|
| vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
|
|
174
197
|
| LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
|
|
175
198
|
| `logprobs` / `top_logprobs` | ❌ serial — the batched sampler doesn't capture logprob arrays yet |
|
|
176
199
|
| explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
|
|
177
200
|
| KV quant active (explicit `--kv-quant`) | ✅ batches for per-layer `config` schemes — full-attention layers (Phase 3.1) AND rotating layers (milestone 2: gemma's kv_config; unpadded row gated KL-0 vs serial); uniform bits → serial |
|
|
178
|
-
| `--kv-quant turbo` (TurboQuant) | ❌ serial, unconditionally — solo-only in v1 (novel `Cache` class, no batched merge/filter/temporalView; belt-and-braces refusal in
|
|
201
|
+
| `--kv-quant turbo` (TurboQuant) | ❌ serial, unconditionally — solo-only in v1 (novel `Cache` class, no batched merge/filter/temporalView; belt-and-braces refusal in placement on top of the automatic capability-gate exclusion) |
|
|
179
202
|
| `--draft-model` mounted | ❌ serial, server-wide — speculation is a B=1 latency mode (upstream `is_batchable = draft is None`) |
|
|
180
203
|
| `repetition_penalty` / `min_p` / `xtc_*` / `logit_bias` / presence+frequency penalties | ✅ batches — per-row logits processors over a per-row device-side history (since 2026-07-02; some models — Qwen3.5 — ship a *default* repetition penalty, which used to route everything serial) |
|
|
181
204
|
| structured output (`response_format` / `guided_*`) | ✅ batches — per-row grammar matchers driven by the scheduler (`MLX_BUN_GRAMMAR_BATCH=0` forces serial) |
|
|
@@ -185,7 +208,7 @@ concurrently with each other.
|
|
|
185
208
|
| `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
|
|
186
209
|
| multi-turn / long prompt | ✅ batches **with prompt-cache reuse** (Phase 3.2): a joiner restores the longest usable cached prefix and prefills only the suffix (`cached_tokens` reported); a request that finishes without ever sharing the batch puts its caches back. **Prefix sharing (2026-07-05): serves are non-consuming clones**, so concurrent agents with a shared system prompt all reuse one prefill and nobody's entry is destroyed. A row that merges with others is not re-put (its entry ages out) — heavy concurrency reduces hit rate, never correctness |
|
|
187
210
|
|
|
188
|
-
**Which models
|
|
211
|
+
**Which models use continuous scheduling:** full-attention (CPM), sliding-window (Gemma),
|
|
189
212
|
hybrid gated-DeltaNet (Qwen3.5 — the SSM batched path, token-exact vs the
|
|
190
213
|
mlx-lm B=2 oracle, landed 2026-07-02; `MLX_BUN_BATCH_SSM=0` reverts it to
|
|
191
214
|
serial routing), and **plain full-attention Tier-0 universal archs**
|
|
@@ -194,23 +217,24 @@ Still serial by the model-level capability gate: gemma2-family and
|
|
|
194
217
|
sliding-window *universal* archs (unvalidated cells) and DiffusionGemma
|
|
195
218
|
(non-autoregressive).
|
|
196
219
|
|
|
197
|
-
A
|
|
220
|
+
A request requiring the serial mechanism **drains** the scheduler: while it waits, the
|
|
198
221
|
scheduler stops admitting new rows, finishes the running ones, and
|
|
199
222
|
releases the GPU so the serial request runs (mlx-lm's `drain_batch`);
|
|
200
223
|
admission then resumes. So a steady stream of batchable traffic cannot
|
|
201
224
|
starve a serial-lane request.
|
|
202
225
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
226
|
+
Placement is not a load-dependent fallback. A request admitted to the
|
|
227
|
+
continuous scheduler stays there whether it is the only active row or one of
|
|
228
|
+
many; arrivals select B=1 versus B=N *inside* that mechanism. `--batch 1`
|
|
229
|
+
remains the arrival-independent numerics pin. This matches mlx-lm's
|
|
230
|
+
concurrency-driven serving contract while keeping the strict serial route for
|
|
231
|
+
goldens and compositions that need it.
|
|
208
232
|
|
|
209
233
|
## Compatibility matrix
|
|
210
234
|
|
|
211
|
-
How each option behaves
|
|
212
|
-
|
|
213
|
-
|
|
235
|
+
How each option behaves under each configured mechanism. "serial" =
|
|
236
|
+
`--batch 1`; `--batch N` permits continuous scheduling, although a declared
|
|
237
|
+
composition may still require the serial mechanism as shown above.
|
|
214
238
|
|
|
215
239
|
| Option | serial (`--batch 1`) | `--batch N` (N>1) |
|
|
216
240
|
| --- | --- | --- |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mlx-bun",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Native MLX inference for Bun on Apple Silicon — local LLM server (OpenAI/Anthropic-compatible) and TypeScript library. No Python.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"THIRD_PARTY_LICENSES.md"
|
|
38
38
|
],
|
|
39
39
|
"engines": {
|
|
40
|
-
"bun": ">=1.
|
|
40
|
+
"bun": ">=1.4.0"
|
|
41
41
|
},
|
|
42
42
|
"os": [
|
|
43
43
|
"darwin"
|
package/src/anthropic.ts
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
// semantics) instead of the oracle's json.dumps fallback.
|
|
24
24
|
|
|
25
25
|
import type { ChatMessage, ToolDefinition } from "./chat-template";
|
|
26
|
+
import type {
|
|
27
|
+
CompletionEvent,
|
|
28
|
+
CompletionStreamProtocol,
|
|
29
|
+
} from "./serve/completion-sink";
|
|
26
30
|
|
|
27
31
|
// ---------------------------------------------------------------------
|
|
28
32
|
// Request translation: Anthropic → OpenAI (our ChatRequest shape)
|
|
@@ -128,7 +132,15 @@ export function anthropicToChatBody(body: AnthropicRequest): Record<string, unkn
|
|
|
128
132
|
break; // prior-turn thinking is not re-fed (Anthropic semantics)
|
|
129
133
|
case "image": {
|
|
130
134
|
const part = imagePartFromBlock(block);
|
|
131
|
-
if (part)
|
|
135
|
+
if (!part)
|
|
136
|
+
// e.g. a Files-API {type:"file"} source we cannot fetch —
|
|
137
|
+
// reject loudly, never answer about an image the model can't
|
|
138
|
+
// see (2026-08-18 review; the audio-doctrine below).
|
|
139
|
+
throw new Error(
|
|
140
|
+
`unsupported image source "${block.source?.type ?? "missing"}" — ` +
|
|
141
|
+
`send base64 or url image sources`,
|
|
142
|
+
);
|
|
143
|
+
imageParts.push(part);
|
|
132
144
|
break;
|
|
133
145
|
}
|
|
134
146
|
case "tool_use":
|
|
@@ -141,15 +153,52 @@ export function anthropicToChatBody(body: AnthropicRequest): Record<string, unkn
|
|
|
141
153
|
},
|
|
142
154
|
});
|
|
143
155
|
break;
|
|
144
|
-
case "tool_result":
|
|
156
|
+
case "tool_result": {
|
|
145
157
|
// Anthropic packs tool results into the next user message;
|
|
146
158
|
// OpenAI (and the gemma template) want role:"tool" messages.
|
|
159
|
+
// Nested image blocks (screenshot-returning tools — the
|
|
160
|
+
// computer-use shape) are PRESERVED as image parts: flattening
|
|
161
|
+
// them away had the model answering about screenshots it never
|
|
162
|
+
// saw (2026-08-18 review). An unrenderable pairing downstream
|
|
163
|
+
// fails loudly on the media-count check, never silently.
|
|
164
|
+
const trImages: Record<string, unknown>[] = [];
|
|
165
|
+
if (Array.isArray(block.content)) {
|
|
166
|
+
for (const b of block.content as AnthropicContentBlock[]) {
|
|
167
|
+
if (b && typeof b === "object" && b.type === "image") {
|
|
168
|
+
const part = imagePartFromBlock(b);
|
|
169
|
+
if (!part)
|
|
170
|
+
throw new Error(
|
|
171
|
+
`unsupported image source "${b.source?.type ?? "missing"}" ` +
|
|
172
|
+
`in tool_result — send base64 or url image sources`,
|
|
173
|
+
);
|
|
174
|
+
trImages.push(part);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const txt = flattenText(block.content);
|
|
147
179
|
messages.push({
|
|
148
180
|
role: "tool",
|
|
149
181
|
tool_call_id: block.tool_use_id,
|
|
150
|
-
content:
|
|
182
|
+
content: trImages.length
|
|
183
|
+
? [
|
|
184
|
+
...(txt ? [{ type: "text", text: txt }] : []),
|
|
185
|
+
...trImages,
|
|
186
|
+
] as never
|
|
187
|
+
: txt,
|
|
151
188
|
});
|
|
152
189
|
break;
|
|
190
|
+
}
|
|
191
|
+
case "video":
|
|
192
|
+
case "video_url":
|
|
193
|
+
// Not an Anthropic Messages block type — reject loudly instead of
|
|
194
|
+
// silently answering about a video the model never saw (same
|
|
195
|
+
// doctrine as audio below; the OpenAI surface serves video on
|
|
196
|
+
// Qwen3.5-family models).
|
|
197
|
+
throw new Error(
|
|
198
|
+
`"${block.type}" content blocks are not part of the Anthropic ` +
|
|
199
|
+
`Messages protocol — send video to the OpenAI endpoint ` +
|
|
200
|
+
`/v1/chat/completions as a video_url content part`,
|
|
201
|
+
);
|
|
153
202
|
case "audio":
|
|
154
203
|
case "input_audio":
|
|
155
204
|
case "audio_url":
|
|
@@ -168,6 +217,11 @@ export function anthropicToChatBody(body: AnthropicRequest): Record<string, unkn
|
|
|
168
217
|
}
|
|
169
218
|
}
|
|
170
219
|
if (role === "assistant") {
|
|
220
|
+
if (imageParts.length)
|
|
221
|
+
// Assistants don't produce images in the Anthropic protocol; a
|
|
222
|
+
// client that puts one here would previously have it silently
|
|
223
|
+
// discarded (2026-08-18 review — loud beats silent).
|
|
224
|
+
throw new Error("image content blocks are not supported in assistant messages");
|
|
171
225
|
const msg: ChatMessage = { role, content: textParts.join("\n") || null };
|
|
172
226
|
if (toolCalls.length) msg.tool_calls = toolCalls;
|
|
173
227
|
if (msg.content || toolCalls.length) messages.push(msg);
|
|
@@ -457,6 +511,41 @@ export class AnthropicStreamTranslator {
|
|
|
457
511
|
}
|
|
458
512
|
}
|
|
459
513
|
|
|
514
|
+
/** Direct semantic-event adapter used by the server. This avoids encoding
|
|
515
|
+
* OpenAI SSE only to parse those bytes back into protocol events. */
|
|
516
|
+
export function createAnthropicStreamProtocol(model: string): CompletionStreamProtocol {
|
|
517
|
+
const translator = new AnthropicStreamTranslator(model);
|
|
518
|
+
const chunk = (delta: Record<string, unknown>) => ({ choices: [{ delta }] });
|
|
519
|
+
return {
|
|
520
|
+
start: () => translator.addChunk(chunk({ role: "assistant", content: "" })),
|
|
521
|
+
addEvents(events: CompletionEvent[]) {
|
|
522
|
+
return events.flatMap((event) => {
|
|
523
|
+
if (event.type === "reasoning") {
|
|
524
|
+
return translator.addChunk(chunk({ reasoning: event.text }));
|
|
525
|
+
}
|
|
526
|
+
if (event.type === "content") {
|
|
527
|
+
return translator.addChunk(chunk({ content: event.text }));
|
|
528
|
+
}
|
|
529
|
+
return translator.addChunk(chunk({
|
|
530
|
+
tool_calls: event.calls.map((call, index) => ({ index, ...call })),
|
|
531
|
+
}));
|
|
532
|
+
});
|
|
533
|
+
},
|
|
534
|
+
finish(reason, usage) {
|
|
535
|
+
return [
|
|
536
|
+
...translator.addChunk({ choices: [{ delta: {}, finish_reason: reason }], usage }),
|
|
537
|
+
...translator.finalize(),
|
|
538
|
+
];
|
|
539
|
+
},
|
|
540
|
+
error(message) {
|
|
541
|
+
return [sse("error", {
|
|
542
|
+
type: "error",
|
|
543
|
+
error: { type: "api_error", message },
|
|
544
|
+
})];
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
460
549
|
/** Wrap our OpenAI SSE byte stream as an Anthropic SSE byte stream.
|
|
461
550
|
* Frames translate as they arrive (per-chunk flush preserved — the
|
|
462
551
|
* upstream already rate-limits socket flushes). */
|