mlx-bun 0.0.11 → 0.0.13

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.
Files changed (82) hide show
  1. package/README.md +63 -27
  2. package/THIRD_PARTY_LICENSES.md +9 -0
  3. package/docs/design/colibri-glm52-port.md +1486 -0
  4. package/docs/design/pre-colibri-stabilization.md +329 -0
  5. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  6. package/docs/investigations/indexshare-performance-spike.md +357 -0
  7. package/docs/planning/release-notes-v0.0.12.md +80 -0
  8. package/docs/planning/release-notes-v0.0.13.md +77 -0
  9. package/docs/reference/cli.md +28 -2
  10. package/docs/reference/distribution.md +25 -1
  11. package/docs/reference/embedding.md +5 -2
  12. package/docs/reference/features-matrix.md +5 -2
  13. package/docs/reference/library-api.md +41 -13
  14. package/docs/reference/models.md +49 -2
  15. package/docs/reference/server-api.md +113 -20
  16. package/docs/reference/server-config.md +21 -14
  17. package/docs/reference/training.md +10 -1
  18. package/package.json +1 -1
  19. package/src/anthropic.ts +54 -44
  20. package/src/chat-template.ts +189 -11
  21. package/src/cli.ts +127 -8
  22. package/src/config.ts +81 -26
  23. package/src/diffusion/diffusion-generate.ts +14 -8
  24. package/src/download.ts +103 -12
  25. package/src/eval/ifeval.ts +63 -120
  26. package/src/eval/runner.ts +14 -5
  27. package/src/eval/tasks/ifeval.ts +167 -31
  28. package/src/expert-io.ts +391 -0
  29. package/src/expert-residency.ts +1134 -0
  30. package/src/expert-usage.ts +415 -0
  31. package/src/generate.ts +114 -17
  32. package/src/hf-push.ts +1 -1
  33. package/src/index.ts +14 -2
  34. package/src/jobs/runner.ts +37 -18
  35. package/src/kv-store.ts +147 -8
  36. package/src/lora.ts +77 -14
  37. package/src/media-fetch.ts +198 -56
  38. package/src/mlx/array.ts +43 -0
  39. package/src/mlx/autograd.ts +51 -14
  40. package/src/mlx/checkpoint.ts +46 -13
  41. package/src/mlx/custom-vjp.ts +83 -35
  42. package/src/model/factory.ts +96 -5
  43. package/src/model/gemma4-base.ts +32 -0
  44. package/src/model/glm52-atlas.ts +511 -0
  45. package/src/model/glm52-cache.ts +706 -0
  46. package/src/model/glm52-config.ts +262 -0
  47. package/src/model/glm52-container.ts +305 -0
  48. package/src/model/glm52-coupling.ts +388 -0
  49. package/src/model/glm52-dsa.ts +602 -0
  50. package/src/model/glm52-expert-layout.ts +253 -0
  51. package/src/model/glm52-layout.ts +152 -0
  52. package/src/model/glm52-memory.ts +473 -0
  53. package/src/model/glm52-mla.ts +786 -0
  54. package/src/model/glm52-moe.ts +223 -0
  55. package/src/model/glm52-pilot.ts +537 -0
  56. package/src/model/glm52-reference.ts +445 -0
  57. package/src/model/glm52-residency.ts +557 -0
  58. package/src/model/glm52-resident-weights.ts +135 -0
  59. package/src/model/glm52-streamed-experts.ts +453 -0
  60. package/src/model/glm52-streamed-metal.ts +610 -0
  61. package/src/model/glm52-weights.ts +503 -0
  62. package/src/model/glm52.ts +1153 -0
  63. package/src/model/support.ts +8 -1
  64. package/src/native/expert_io.c +523 -0
  65. package/src/native-pack.ts +6 -4
  66. package/src/responses.ts +62 -49
  67. package/src/serve/batch-scheduler.ts +95 -16
  68. package/src/serve/generation-gateway.ts +41 -16
  69. package/src/server.ts +471 -93
  70. package/src/spec/glm52-mtp-source.ts +392 -0
  71. package/src/spec/serve-loop.ts +99 -18
  72. package/src/spec/source.ts +31 -6
  73. package/src/ssd-cache.ts +6 -2
  74. package/src/tool-call.ts +30 -2
  75. package/src/train/job.ts +0 -10
  76. package/src/train/lora-params.ts +17 -6
  77. package/src/train/trainer.ts +59 -26
  78. package/src/web/app.html +13 -2
  79. package/src/web/app.js +30 -3
  80. package/src/web/src/status.ts +51 -3
  81. package/src/web-tools.ts +54 -2
  82. package/src/weights.ts +82 -19
@@ -22,7 +22,7 @@ parity). **Tiers:** L1 = bit-exact vs mlx-lm · L2 = bit-exact vs mlx-optiq
22
22
  | OpenAI Responses shim (`/v1/responses`) | on | both | — | — |
23
23
  | Continuous batching (mlx-lm B=N parity) | **on** — cap 8 (default flipped 2026-07-05; a lone request runs the exact serial engine, so the cap engages only under real concurrency; `--batch 1` pins serial) | batch | L1 | `--batch <n>` |
24
24
  | Prompt cache (prefix KV reuse) | on, 8 GB | both | — | `--prompt-cache <GB>` (0 = off) |
25
- | **SSD KV cold tier** (cache survives eviction + restarts) | off | both | — | `--ssd-cache <dir>` (+ `-max`, `-verify`, `-demote-idle <sec>` — default 300s once `--ssd-cache` is on, `0` disables) |
25
+ | **SSD KV cold tier** (cache survives eviction + restarts; v3 includes GLM compressed MLA/DSA state without full-K/V reconstruction) | off | both | — | `--ssd-cache <dir>` (+ `-max`, `-verify`, `-demote-idle <sec>` — default 300s once `--ssd-cache` is on, `0` disables) |
26
26
  | **Runtime isolation** (engine runs as a crash-isolated CHILD process behind a reverse-proxy parent; UI stays instant under GPU load and survives engine crashes — [runtime-isolation.md](../design/runtime-isolation.md)) | off | both | — | `--isolate` |
27
27
  | **Model pool** (with `--isolate`: LRU-capped RESIDENT model engines; over the cap the least-recently-used engine demotes its cache to the SSD tier and exits, respawning on the next request) | 1 resident engine | both | — | `--model-pool <n>` (requires `--isolate`) |
28
28
  | Mixed-precision KV (`kv_config.json`, optiq's scheme) | **off** — opt-in (default flipped to bf16 2026-07-05; quantized KV trades 5–20% decode for memory headroom) | serial + **batch** (per-layer configs batch on every shipped model — full-attention layers Phase 3.1, rotating layers milestone 2; uniform bits stay serial) | L2 | `--kv-quant config\|off\|4\|8`, `--l2` |
@@ -32,6 +32,7 @@ parity). **Tiers:** L1 = bit-exact vs mlx-lm · L2 = bit-exact vs mlx-optiq
32
32
  | Fused SDPA (optiq-exact quantized-KV attention) | follows `--kv-quant`: on for `config`, off for uniform/bf16 | serial | L2 | `--fused-sdpa on\|off` |
33
33
  | **Paged KV** (vLLM-style block pool: host-side block table + gather before the stock SDPA, no fused kernel in v1 — [paged-kv-cache.md](../design/paged-kv-cache.md)) | off | serial (pins `--batch 1` unless `--batch` given) | gated bit-exact vs the plain `KVCache` (`tests/paged-kv-parity.test.ts`) | `--paged-kv`, `--paged-kv-block-size <n>` (default 256) — v1 scope Gemma4-family bf16; refuses `--batch N>1`, `--kv-quant`, `--draft-model` |
34
34
  | **Speculative decoding** (auto-detected drafter: two-model = mlx-lm parity/L1, Gemma `-assistant` = optiq/L2, DSpark local = Lab, DeepSpec released = DeepSpec-reference oracle) | off | serial (forces all-serial) | per-drafter oracle | `--draft-model <path\|query>`, `--draft-kind`, `--num-draft-tokens` |
35
+ | **GLM-5.2 checkpoint-native MTP** (bounded auxiliary expert tier, exact verify) | **on** for GLM-5.2 | serial (`usage.lane: serial+spec`) | Colibri/oracle trajectory + synthetic HTTP gate | `--mtp on\|off` (`off` enables ordinary GLM batching) |
35
36
  | Speculative decoding, **model-free prompt lookup** (drafts copied from the request's own context; port of prompt-lookup decoding / vLLM `ngram`; zero weights) | off | serial (forces all-serial) | lossless by verify (gated vs non-spec greedy, tests/spec-ngram.test.ts) | `--draft-kind ngram` (no `--draft-model`), `--ngram-max`, `--ngram-min`, `--num-draft-tokens` (default 10) |
36
37
  | Memory admission (refuse what can't fit; never GPU-OOM) | on (RAM × 0.75) | both | — | `--memory-budget <GB>` |
37
38
  | Aggregate KV admission for batch rows (queue, don't OOM) | off | batch | — | `--kv-budget <GB>` |
@@ -48,7 +49,7 @@ parity). **Tiers:** L1 = bit-exact vs mlx-lm · L2 = bit-exact vs mlx-optiq
48
49
  | `guided_grammar` (EBNF) / `guided_regex`¹ / `guided_choice` / `structured_outputs` | on | both | L2 | request fields |
49
50
  | Structured output × speculative decoding | on (when both active) | serial | Lab (validity+equivalence) | — |
50
51
  | Quantized KV × speculative decoding (any axis: uniform / config / turbo) | KV scheme wins — drafted requests decode serially WITHOUT speculation (spec lane is bf16-KV-only in v1; startup warning) | serial | — | omit `--kv-quant` to speculate |
51
- | Tool calling (Gemma sentinel / CPM+Qwen XML) + `role:"tool"` loops | on | both | — | request `tools` |
52
+ | Tool calling (Gemma sentinel / CPM+Qwen XML / GLM `arg_key`+`arg_value`) + `role:"tool"` loops | on | both | — | request `tools` |
52
53
  | Vision (`image_url` parts; PNG/JPEG/HEIC/AVIF/WebP/TIFF/GIF/BMP) | on (models with a tower); remote fetches refuse private/loopback/link-local hosts by default (SSRF guard) | serial | L1/L2 | `--allow-private-media` / `MLX_BUN_ALLOW_PRIVATE_MEDIA=1` re-permits LAN hosts |
53
54
  | **Audio input** (`input_audio`/`audio`/`audio_url` parts; WAV native, mp3/m4a/flac/ogg/aiff via CoreAudio; ≤30 s / 750 tokens per clip; mixes with images) | on (models with `audio_config` + sidecar tower: e4b); remote fetches refuse private/loopback/link-local hosts by default (SSRF guard) | serial³ | L2 (greedy stream EXACT vs the optiq internal model) | `--allow-private-media` / `MLX_BUN_ALLOW_PRIVATE_MEDIA=1` re-permits LAN hosts |
54
55
  | LoRA adapters (mount at start / hot-swap at runtime) | off | serial | — | `--adapter <dir>`, `POST /v1/adapters` |
@@ -68,6 +69,8 @@ parity). **Tiers:** L1 = bit-exact vs mlx-lm · L2 = bit-exact vs mlx-optiq
68
69
  | MiniCPM5 (cpm5) | ✅ L1/L2 | ✅ | the starter model |
69
70
  | Gemma 4 (1B/e4b/12B/26B, + vision e4b/12B, + audio e4b) | ✅ L1/L2 | ✅ | sliding+full interleaved; MoE 26B |
70
71
  | Qwen3.5 (gated-DeltaNet hybrid) | ✅ L1/L2 | ✅ (SSM path) | `MLX_BUN_BATCH_SSM=0` reverts |
72
+ | Qwen3.8-27B (same qwen3_5 graph) | ✅ L1 (no L2 oracle) | ✅ (SSM path) | thinking depths + preserve_thinking; MTP/vision/video in bring-up |
73
+ | GLM-5.2 / Colibri (pre-release port gate) | ✅ chat/text, Messages, Responses, SSE, tools, grammar, logprobs, library/CLI | ✅ compressed MLA/DSA scheduler | native MTP defaults to serial+spec; `--mtp off` enables ordinary batching; artifact-aware CLI/API/status `fit` and live main/MTP expert-tier telemetry; embeddings, vision/audio, adapters, and training are explicitly unsupported; per-row batched MTP is post-release |
71
74
  | DiffusionGemma-26B (non-autoregressive) | ✅ (own engine) | — serial always | first bit-exact non-AR port |
72
75
  | Tier-0 universal (llama/qwen2/qwen3/olmo2/…, 11 archs) | ✅ L1 | ✅ plain full-attention archs² | gemma2-family/sliding universal → serial |
73
76
 
@@ -16,9 +16,7 @@ surface); direct library callers must do the same.
16
16
  ```ts
17
17
  import {
18
18
  ensureNativeRuntime,
19
- loadModelConfig,
20
- Weights,
21
- createModel, // dispatches to Gemma4Model / MiniCPM5Model / Qwen35Model
19
+ openModel, // artifact-aware dispatch, including bounded GLM-5.2
22
20
  loadTokenizer,
23
21
  ChatTemplate,
24
22
  generate,
@@ -26,13 +24,15 @@ import {
26
24
 
27
25
  await ensureNativeRuntime();
28
26
  const dir = "/path/to/hf-snapshot"; // mlx-bun ls prints these
29
- const config = await loadModelConfig(dir);
30
- const model = createModel(await Weights.open(dir), config); // returns RuntimeModel
27
+ const model = await openModel(dir, {
28
+ // Used only by the direct Colibri GLM-5.2 artifact; other models ignore it.
29
+ contextTokens: 4096, maxGenerationTokens: 256, enableMtp: false,
30
+ });
31
31
  const tok = await loadTokenizer(dir);
32
32
  const template = await ChatTemplate.load(dir);
33
33
  // Gemma4Model and MiniCPM5Model can still be imported directly from "mlx-bun"
34
34
  // if you need the concrete type. Qwen35Model is NOT exported from the public
35
- // package — use createModel/RuntimeModel (preferred dispatch), or import from
35
+ // package — use openModel/RuntimeModel (preferred artifact-aware dispatch), or import from
36
36
  // "./src/model/qwen3_5" in-repo.
37
37
 
38
38
  const ids = tok.encode(template.render([{ role: "user", content: "hi" }]));
@@ -45,13 +45,15 @@ console.log(tok.decode(out, true));
45
45
  console.log(gen.stats); // set once iteration ends
46
46
  ```
47
47
 
48
- Weight loading is lazy (mmap + mlx native loader): construction is
49
- milliseconds; weights materialize on first forward.
48
+ Ordinary safetensors loading is lazy (mmap + mlx native loader). GLM-5.2 first
49
+ runs its header-only exact process equation, then opens the bounded Colibri
50
+ resident/expert tiers; impossible plans fail before either tier is committed.
50
51
 
51
52
  > **First run:** library consumers bypass the CLI's first-run step, so
52
53
  > call `await ensureNativeRuntime()` once before constructing a model on
53
54
  > a machine that may not have the MLX native runtime yet — it downloads
54
- > the sha256-verified native pack to `~/Library/Caches/mlx-bun/` and is
55
+ > the sha256-verified native pack (MLX plus the direct-Colibri expert-I/O
56
+ > helper) to `~/Library/Caches/mlx-bun/` and is
55
57
  > a no-op when the runtime is already present (beside the executable,
56
58
  > already cached, or installed via homebrew). `nativeRuntimeDir()`
57
59
  > returns the resolved directory, or `null` on a fresh machine.
@@ -109,6 +111,24 @@ budget (pre-GPU, mmap-only check). The server exposes OpenAI chat
109
111
  completions, Anthropic `/v1/messages`, OpenAI Responses, embeddings,
110
112
  adapters, and `/stats` — [server-api.md](./server-api.md).
111
113
 
114
+ For GLM-5.2, pass its load-only resource controls under `glm`:
115
+
116
+ ```ts
117
+ const ctx = await loadContext(dir, "glm-5.2", {
118
+ glm: {
119
+ contextTokens: 4096,
120
+ maxGenerationTokens: 128,
121
+ batchSize: 8,
122
+ enableMtp: true, // default; native MTP routes requests serial+spec
123
+ },
124
+ });
125
+ ```
126
+
127
+ `openModel()` and `loadContext()` both select the streamed Colibri runtime;
128
+ callers do not use `Weights.open()` or `createModel()` for this artifact.
129
+ Direct `generate()` supports the full sampling/grammar/logprobs contract but
130
+ does not itself mount a drafter; native MTP is a serving-context feature.
131
+
112
132
  ## Text embeddings
113
133
 
114
134
  ```ts
@@ -154,16 +174,24 @@ under one adapter must not seed another's prefill).
154
174
  ```ts
155
175
  import { saveKvCache, loadKvCache } from "./src/kv-store";
156
176
  saveKvCache("/tmp/prefix.kv", tokens, caches); // page-aligned file
157
- const { tokens, caches, mmap } = loadKvCache("/tmp/prefix.kv", model);
158
- // reload is a zero-copy MAP_PRIVATE mmap straight to the GPU (~1 ms);
159
- // keep `mmap` referenced as long as the caches live
177
+ const { tokens, caches } = loadKvCache("/tmp/prefix.kv", model, {
178
+ modelId, configFingerprint, tokenizerHash, verify: true,
179
+ });
180
+ // Restore streams one tensor at a time from a read-only mmap into MLX-owned
181
+ // storage, drops each clean file-page range, and unmaps before returning.
160
182
  // Note: loadKvCache accepts anything with makeCache(): Cache[] — it validates
161
183
  // the entry count against the cache list makeCache() returns (not
162
184
  // model.layers.length, which is wrong for KV-shared models like e4b, whose
163
185
  // makeCache() returns donors only). RuntimeModel qualifies directly, no cast.
164
186
  ```
165
187
 
166
- Quantized caches are not persistable yet (documented gap).
188
+ Format v3 supports plain, rotating, quantized, TurboQuant, SSM, and GLM-5.2
189
+ checkpoint-native caches. GLM rows use `mla`, `mla-dsa`, and `mtp-mla` kinds and
190
+ store only compressed latent/RoPE/DSA state—never reconstructed full K/V.
191
+ `Glm52NativeMtpSource.clonePersistentCache()` returns an owned MTP snapshot;
192
+ `restorePersistentCache()` adopts a validated restored `mtp-mla` row. Writes
193
+ are page-aligned, hashed, atomic (`.tmp` + fsync + rename), and available in
194
+ both synchronous and event-loop-yielding `saveKvCacheAsync` forms.
167
195
 
168
196
  ## Registry + fit (model discovery and memory math)
169
197
 
@@ -35,6 +35,11 @@ mlx-bun get <substring> # no "/" = registry query, re-gets the match
35
35
  - Plain HTTPS resolve/CDN (no Xet), sequential files, `Range`-resume of
36
36
  partial blobs (`<blob>.incomplete`). Auth: `HF_TOKEN` env, then the
37
37
  token `hf auth login` writes. 401/403 answers get a "gated repo" hint.
38
+ - Before the first payload request, mlx-bun totals the bytes still absent,
39
+ credits complete shared blobs and valid `.incomplete` prefixes, and checks
40
+ the target volume for that remainder plus a fixed 1 GiB safety reserve. An
41
+ impossible acquisition fails immediately with required/available GiB and a
42
+ rerun/resume instruction rather than filling the disk halfway through.
38
43
  - A substring argument (no `/`) resolves against already-downloaded
39
44
  repos — `mlx-bun get 12B` refreshes the 12B to upstream's latest. An
40
45
  unknown substring errors with a pointer to `mlx-bun ls <q>`.
@@ -51,14 +56,56 @@ mlx-bun get <substring> # no "/" = registry query, re-gets the match
51
56
  Every blob is checksummed **while streaming** (a resume re-hashes the
52
57
  existing prefix in chunks — no whole-file allocation):
53
58
 
54
- - **LFS files** (weights): sha256 must equal the API's `lfs.oid` — which
55
- is also the blob's filename.
59
+ - **LFS files** (weights): sha256 must equal the API's current `lfs.sha256`
60
+ field (the older `lfs.oid` spelling is also accepted) — which is also the
61
+ blob's filename.
56
62
  - **Small files** (configs, tokenizer): git blob identity,
57
63
  `sha1("blob <size>\0" + content)`, must equal the API's `blobId`.
58
64
 
59
65
  A mismatch deletes the partial (never resume corrupt bytes); a short
60
66
  read keeps the `.incomplete` for resume.
61
67
 
68
+ ## GLM-5.2 on a 32 GB Mac
69
+
70
+ The validated direct-container artifact is
71
+ `mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp` at revision
72
+ `3cc8db99b1b13fc79325d987ba3c1c430766b3b8`. It is approximately 384 GB
73
+ decimal / 357 GiB on disk, so choose the Hugging Face cache volume before the
74
+ download. Fast internal NVMe is strongly preferred; an external volume also
75
+ needs to sustain the expert-streaming workload after acquisition.
76
+
77
+ ```sh
78
+ # Optional: choose a large fast volume before downloading.
79
+ export HF_HUB_CACHE=/Volumes/FastSSD/huggingface/hub
80
+
81
+ mlx-bun get mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp \
82
+ --revision 3cc8db99b1b13fc79325d987ba3c1c430766b3b8
83
+ mlx-bun scan
84
+ mlx-bun serve GLM-5.2-colibri \
85
+ --memory-budget 26.8435456 \
86
+ --context-length 4096 \
87
+ --max-tokens 128 \
88
+ --mtp on
89
+ ```
90
+
91
+ `26.8435456` decimal GB is exactly 25 GiB. The validated plan reserves about
92
+ 19.9 GiB for the process and leaves the rest of the 32 GiB machine to macOS.
93
+ Startup runs the exact header-only resource equation before opening resident
94
+ weights or expert slabs. The server reports the resulting plan at
95
+ `GET /stats` under `glm52`.
96
+
97
+ If acquisition is interrupted, rerun the identical `get` command: mlx-bun
98
+ rehashes each existing prefix in bounded chunks, sends a Range request for the
99
+ remainder, and verifies the complete blob before publishing it. If the disk
100
+ preflight refuses, free space or point `HF_HUB_CACHE` at another volume and
101
+ rerun. No Python environment and no converted copy inside the mlx-bun checkout
102
+ are involved.
103
+
104
+ The artifact is an external MIT-licensed derivative of
105
+ `zai-org/GLM-5.2-FP8`, converted with Colibri and extended with int8 MTP
106
+ weights. Model weights are not bundled with mlx-bun; review the artifact model
107
+ card before redistribution.
108
+
62
109
  ## scan + the registry
63
110
 
64
111
  `mlx-bun scan` walks `models--*/snapshots/*` reading only `config.json`
@@ -50,14 +50,25 @@ Request body (OpenAI chat schema; unknown fields ignored):
50
50
  "tools": [ /* OpenAI function tools */ ],
51
51
  "tool_choice": "auto", // "none" disables tools
52
52
  "chat_template_kwargs": { // forwarded to the chat template
53
- "enable_thinking": false // MiniCPM5 / Qwen3.5: <think> channel on/off
53
+ "enable_thinking": false, // MiniCPM5 / Qwen3.5/3.8 / GLM-5.2: <think>
54
+ // channel on/off (Qwen3.8 default: ON)
55
+ "preserve_thinking": true // Qwen3.8: keep think blocks from earlier
56
+ // assistant turns in the rendered prompt
57
+ // (template default true — better prompt-
58
+ // cache reuse in agent loops). Ignored by
59
+ // templates that don't read it.
54
60
  },
55
- "reasoning_effort": "medium", // "none"|"minimal"|"low"|"medium"|"high"
56
- // gates enable_thinking on Qwen3.5/MiniCPM5:
57
- // "none" → off, any other level → on. Only
58
- // consulted when chat_template_kwargs
59
- // .enable_thinking is not explicitly set —
60
- // an explicit enable_thinking always wins.
61
+ "reasoning_effort": "medium", // "none"|"minimal"|"low"|"medium"|"high"|"xhigh"
62
+ // gates enable_thinking on Qwen3.5/3.8/
63
+ // MiniCPM5: "none" → off, any other level →
64
+ // on. Only consulted when chat_template_
65
+ // kwargs.enable_thinking is not explicitly
66
+ // set — an explicit enable_thinking always
67
+ // wins. On templates with a reasoning-depth
68
+ // variable (Qwen3.8) the level also maps
69
+ // into the template: minimal/low → low,
70
+ // medium → medium, high/xhigh → xhigh
71
+ // (Qwen3.8's default depth is xhigh).
61
72
  "hlg": { // HLG tone-curve sampling (per request).
62
73
  "enabled": true, // merged over --hlg-sampling server defaults.
63
74
  "width": 4, // logit-width of the tone plateau
@@ -78,6 +89,11 @@ Sampling defaults follow the model author's `generation_config.json`
78
89
  when a field is omitted (optiq serve's gen_config behavior); explicit
79
90
  request values always win. MiniCPM5 defaults to the no-think direct
80
91
  answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
92
+ GLM-5.2 also defaults to no-think: its generation primer is
93
+ `<think></think>`. Enabling thinking renders `Reasoning Effort: Max` in the
94
+ system turn and leaves an open `<think>` primer for generation. The same
95
+ precedence applies through Chat Completions, Anthropic Messages, and Responses
96
+ because all three normalize into this one chat request path.
81
97
 
82
98
  `stop` sequences are matched on **decoded text**, not token ids, so a
83
99
  sequence that spans token boundaries still fires. Generation halts at
@@ -303,13 +319,16 @@ with `finish_reason: "stop"`. Works on both lanes — serial and
303
319
  `--batch N` (per-row matchers).
304
320
 
305
321
  Degrade path — a grammar that fails to compile is never rejected (no
306
- 400/500): chat prepends a system message instructing valid JSON output
307
- (schema included when one was given) and the response carries a
308
- `Warning` header (`grammar not enforced: …`); `/v1/completions` has no
309
- chat template to inject into, so it emits the `Warning` header only.
310
- `MLX_BUN_GRAMMAR=0` disables the feature entirely (the fields are
311
- ignored: no enforcement, no injection, no Warning). Design and fidelity
312
- notes: [structured-output.md](../design/structured-output.md).
322
+ 400/500): chat prepends a string-valued system instruction for the
323
+ requested JSON schema, raw grammar, regex, or choice constraint, and the
324
+ response carries a `Warning` header (`grammar not enforced: …`);
325
+ `/v1/completions` has no chat template to inject into, so it emits the
326
+ `Warning` header only.
327
+ `MLX_BUN_GRAMMAR=0` disables mask compilation but does not silently
328
+ ignore a requested constraint: it uses this same degrade path (chat
329
+ prompt injection plus `Warning`; raw completions `Warning` only).
330
+ Design and fidelity notes:
331
+ [structured-output.md](../design/structured-output.md).
313
332
 
314
333
  ### Errors
315
334
 
@@ -325,11 +344,12 @@ All errors are `{ "error": { "message": …, ... } }`.
325
344
  `top_logprobs must be of type int` / `at least 0` / `at most 11`;
326
345
  `top_logprobs: -1` is the accepted "unset" sentinel).
327
346
  - `400` with `"type": "memory_admission"`, `"code":
328
- "context_over_budget"` — `prompt + max_tokens` exceeds the memory
329
- budget's max safe context (only when serving with `--memory-budget`;
330
- the GPU OOM this prevents would kill the process, so it is refused
331
- up front). Lower `max_tokens` or shorten the prompt; the ceiling is
332
- visible at `/stats`.
347
+ "context_over_budget"` — the request cannot fit the memory budget's max
348
+ safe context. Generic budgeted serving rejects when `prompt + max_tokens`
349
+ exceeds the ceiling. GLM-5.2 instead caps a broad `max_tokens` upper bound
350
+ to the space remaining in its fixed planned context and returns this error
351
+ only when the prompt itself leaves no generation slot. The ceiling is visible
352
+ at `/stats`.
333
353
 
334
354
  ## POST /v1/completions (raw text completion)
335
355
 
@@ -486,6 +506,23 @@ The served model is FIRST, with extra capability fields:
486
506
  // lazily loadable)
487
507
  "audio": false, // input_audio content parts accepted
488
508
  // (tower loaded or lazily loadable)
509
+ "batch_mode": "batch", // loaded cache capability; per-request
510
+ // usage.lane is authoritative
511
+ "tools": true,
512
+ "structured_output": true,
513
+ "embeddings": false,
514
+ "adapters": false,
515
+ "training": false,
516
+ "dsa": true, // GLM-5.2 only; false elsewhere
517
+ "mtp": true, // checkpoint-native GLM MTP is mounted
518
+ "capabilities": { // explicit discoverable surface
519
+ "chat_completions": true, "text_completions": true,
520
+ "anthropic_messages": true, "responses": true,
521
+ "streaming": true, "tools": true, "structured_output": true,
522
+ "logprobs": true, "embeddings": false,
523
+ "vision": false, "audio": false,
524
+ "adapters": false, "training": false
525
+ },
489
526
  "gen_defaults": { // model-author sampling defaults
490
527
  "temperature": 0.7, // (generation_config.json, with any
491
528
  "top_p": 0.95, // --temperature/--top-p/--top-k server
@@ -518,6 +555,13 @@ matching `mlx_lm.server`.
518
555
  `input_audio` content parts (tower loaded or lazily loadable — see
519
556
  *Audio input* above for what audio requires), so clients can discover
520
557
  capabilities instead of probing for a `400`.
558
+ The served-model row also includes `batch_mode: "off" | "serial" | "batch"`:
559
+ the configured cap and the loaded model's cache capability together. Per-turn
560
+ `usage.lane` remains authoritative for a particular request; for example,
561
+ native-MTP requests route serial even when the served GLM model reports
562
+ `batch_mode: "batch"` for ordinary requests.
563
+ For GLM-5.2, the non-generative fields are deliberately false: embeddings,
564
+ vision/audio, adapters, and training are not emulated by the serving port.
521
565
 
522
566
  ## GET /health
523
567
 
@@ -544,9 +588,40 @@ capabilities instead of probing for a `400`.
544
588
  "usable_bytes": 0,
545
589
  "weights_bytes": 0
546
590
  },
591
+ // present only for the direct Colibri GLM-5.2 runtime:
592
+ "glm52": {
593
+ "preset": "g5-32gb-quality",
594
+ "planned_process_bytes": 0,
595
+ "process_limit_bytes": 0,
596
+ "context_tokens": 4096,
597
+ "max_generation_tokens": 128,
598
+ "batch_size": 1,
599
+ "dsa": true,
600
+ "mtp": true,
601
+ "mtp_draft_tokens": 3,
602
+ "resident_weight_bytes": 0,
603
+ "main_expert_slab_bytes": 0,
604
+ "mtp_expert_slab_bytes": 0,
605
+ "expert_runtime": {
606
+ "main_residency": { "resident": 0, "pinned": 0, "working": 0,
607
+ "hits": 0, "misses": 0, "evictions": 0 },
608
+ "mtp_residency": { "resident": 0, "pinned": 0, "working": 0,
609
+ "hits": 0, "misses": 0, "evictions": 0 },
610
+ "last_turn": {
611
+ "main": { "demand": { "hits": 0, "misses": 0,
612
+ "readBytes": 0 } },
613
+ "mtp": { "demand": { "hits": 0, "misses": 0,
614
+ "readBytes": 0 } },
615
+ "pilot": null
616
+ },
617
+ "last_repin": []
618
+ }
619
+ },
547
620
  "batch": {
548
621
  "configured": 1, // the --batch N value
549
- "batched": false, // batching enabled (N>1)
622
+ "mode": "off", // "off" | "serial" | "batch": configured
623
+ // and model-cache capability together
624
+ "batched": false, // true only when mode == "batch"
550
625
  "active_rows": 0, // rows currently decoding in the batch
551
626
  "pending_rows": 0, // queued + mid-prefill rows waiting
552
627
  "submitted_rows": 0, // cumulative rows routed to the batch lane since start
@@ -955,6 +1030,15 @@ banner and `mlx-bun fit`. When the eval DB has a real measured decode
955
1030
  rate for this model snapshot, it is included and takes precedence over
956
1031
  the prediction.
957
1032
 
1033
+ For the direct Colibri GLM-5.2 runtime this endpoint switches to the exact
1034
+ serve-time memory plan. The response separates the full streamed artifact on
1035
+ disk from resident weights, main/MTP expert slabs, KV, and other reserves.
1036
+ `measured_decode_tps` is the G5 warm quality-preserving measurement;
1037
+ `glm52.direct_oracle_warm_decode_tps` is the same-machine direct Colibri
1038
+ control, while `glm52.aspirational_decode_tps` is a target, not a release
1039
+ gate. The single-row SKU result describes the current machine because the
1040
+ generic all-resident SKU estimator is inapplicable.
1041
+
958
1042
  ```jsonc
959
1043
  {
960
1044
  "machine": { "chip": "M4 Pro", "ram_bytes": 0, "bandwidth_gbs": 0.0 },
@@ -973,6 +1057,15 @@ the prediction.
973
1057
  "max_safe_context": 8192,
974
1058
  "predicted_decode_tps": 0.0
975
1059
  },
1060
+ // GLM-5.2 only:
1061
+ "glm52": {
1062
+ "artifact_disk_bytes": 0, // exact registry size; null if unavailable
1063
+ "main_expert_slab_bytes": 0,
1064
+ "mtp_expert_slab_bytes": 0,
1065
+ "max_generation_tokens": 128,
1066
+ "direct_oracle_warm_decode_tps": 0.27,
1067
+ "aspirational_decode_tps": 2.0
1068
+ },
976
1069
  "sku_matrix_ctx": 32768,
977
1070
  "sku_matrix": [{
978
1071
  "sku": "M4 Pro 24 GB", "ram_gb": 24,
@@ -21,7 +21,7 @@ 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 | Admission ceiling. Requests whose `prompt + max_tokens` exceed the budget's max safe context are **rejected with 400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. Also caps the mlx allocator (`mlx_set_memory_limit`) as defense in depth. **Decimal GB (×10⁹).** |
24
+ | `--memory-budget` | GB | machine RAM × 0.75; GLM-5.2: min(25 GiB, physical RAM) | Admission ceiling. Generic budgeted serving rejects requests whose `prompt + max_tokens` exceed the safe context with **400** (`type: memory_admission`) instead of risking an uncatchable GPU OOM. GLM-5.2 runs its exact process equation before opening resident weights or expert slabs and caps MLX to the plan's allocator reserve; because its context is fixed up front, a broad client `max_tokens` upper bound is capped to the remaining planned context, while a prompt that leaves no generation slot is still rejected. **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 }`. |
@@ -38,11 +38,13 @@ the design rationale is in
38
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. |
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
- | `--thinking` | `true`\|`false` | model's own (false for CPM) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM and Qwen3.5 hybrid reasoning). Full precedence: an explicit request `chat_template_kwargs.enable_thinking` always wins; else `reasoning_effort` (`"none"` → off, any other level → on) if the request sent one; else this server default; else the model's own default. |
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`. |
42
+ | `--context-length` | tokens | `4096` for GLM-5.2 | Context reserved by GLM's header-only resource equation. The same value becomes the request-admission ceiling and is reported in `/stats.glm52`; an impossible plan fails before committing model memory. |
43
+ | `--thinking` | `true`\|`false` | model's own (false for CPM and GLM-5.2) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM, Qwen3.5, and GLM-5.2). Full precedence: an explicit request `chat_template_kwargs.enable_thinking` always wins; else `reasoning_effort` (`"none"` → off, any other level → on) if the request sent one; else this server default; else the model's own default. GLM renders disabled thinking as `<think></think>` and enabled thinking as an open `<think>` generation primer plus `Reasoning Effort: Max`. |
42
44
  | `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. `--temp` is accepted as an alias (mlx_lm.server compat); explicit `--temperature` wins if both are given. **Migration note:** mlx_lm.server's `--temp` *default* is `0.0` (unset-temperature requests are greedy there); mlx-bun falls back to the model's `generation_config.json`, then `0.7` — pass `--temp 0` for mlx-lm's behavior. |
43
45
  | `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
44
46
  | `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
45
- | `--max-tokens` | n | `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). |
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`. |
46
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. |
47
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). |
48
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/design/hlg-sampling.md](../design/hlg-sampling.md). |
@@ -112,7 +114,8 @@ for paired A/B harnesses).
112
114
  | `MLX_BUN_PAGED_KV` | `--paged-kv` | off (`==="1"`) | Paged KV cache (see the flag row above — v1: serial, Gemma4, bf16; refusals + prompt-cache bypass apply identically). |
113
115
  | `MLX_BUN_ALLOW_PRIVATE_MEDIA` | `--allow-private-media` | off (`==="1"`) | Permit `image_url`/`audio_url` fetches to private/loopback/link-local hosts (the SSRF guard's LAN escape hatch — see the flag row above; timeout + size cap still apply). |
114
116
  | `MLX_BUN_PREFILL_TAIL_SPLIT` | — | on (`!=="0"`) | Oracle prefill convention (mlx-lm `generate_step` and its server's batched engine): drain the prompt only to len−1, then compute step-0 logits from a separate **L=1 forward of the last prompt token**. Both lanes. **The SPEC lane (`--draft-model`) follows its own oracle's shape under the same flag** (mlx-lm `speculative_generate_step`, re-anchored 2026-07-07): both target AND draft drain to len−1 and there is **no separate step-0 at all** — the un-drained last prompt token heads the first verify window. Gated live: token-for-token vs the oracle venv incl. a knife-edge cell (tests/spec-serve.test.ts "L1 knife-edge"). (The standalone optiq-oracled assistant loop `specGenerate` deliberately keeps FULL-prompt prefill — optiq's own convention.) `=0` restores the pre-2026-07-07 full-final-chunk convention everywhere (A/B lever + kill switch) — that convention is ulp-different in bf16 at step 0 AND in the last prompt token's stored KV, which flips near-tie greedy streams vs mlx-lm (the 2026-07-07 12B completion-probe divergence). |
115
- | `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 for `logprobs` requests; 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). |
117
+ | `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
+ | `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). |
116
119
  | `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. |
117
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 — its clones free immediately and the entry becomes a future cache miss (never a wrong result). `=0` keeps only the newest + in-flight clone pinned (the soft cap never drops the item just enqueued). `/stats.ssd_cache` reports `pending_spills` / `pending_spill_bytes` / `dropped_spills`. Note: there is deliberately **no shutdown flush** — exiting under traffic loses queued write-behind entries (best-effort tier; cost = one re-prefill). |
118
121
  | `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. |
@@ -211,7 +214,8 @@ inside it per the table above).
211
214
 
212
215
  | Option | serial (`--batch 1`) | `--batch N` (N>1) |
213
216
  | --- | --- | --- |
214
- | `--kv-quant config`/`4`/`8` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
217
+ | `--kv-quant config` | ✅ applied to all requests | ✅ batches where the loaded cache capability supports the per-layer scheme; otherwise routes serial |
218
+ | `--kv-quant 4`/`8` | ✅ applied to all requests | ⚠️ uniform-threshold semantics route requests serial |
215
219
  | `--kv-quant turbo[:k<bits>v<bits>]` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
216
220
  | `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
217
221
  | *(kv-quant unset)* | bf16 (the L1 default) | **bf16** (Option B) — incl. serial-lane fallback requests |
@@ -227,6 +231,7 @@ inside it per the table above).
227
231
  | `tools` / `stop` | ✅ | ✅ (batches) |
228
232
  | structured output (`response_format`/`guided_*`) | ✅ (mask in the decode loop) | ✅ (batches; per-row matchers) |
229
233
  | `--draft-model` | ✅ spec decode (grammar composes) | ⚠️ mounts, but routes **every** request serial — spec and batching are different modes |
234
+ | GLM `--mtp on` | ✅ native MTP spec decode | ⚠️ default-on MTP routes every request serial+spec; `--mtp off` exposes ordinary GLM batching |
230
235
  | `--compiled-decode` | ✅ (serial decode route) | ✅ at **B=1 only** (Phase 3.2): a lone request's adopted serial-class caches replay the same compiled step, same kill switch; B>1 steps run the plain graph |
231
236
  | `--fused-sdpa`/`--force-wire` | ✅ (serial decode route) | **n/a — compat mode, no perf flags by design** |
232
237
 
@@ -394,9 +399,9 @@ cells in one run).
394
399
  `--expert-offload`. Inherently the serial recipe until batched
395
400
  quantized KV lands.
396
401
 
397
- **The two exclusions to remember:** batching ⊕ kv-quant (batch is bf16 by
398
- contract; the resolver is a Lab-gated follow-up) and spec ⊕ prompt
399
- cache (v1 bypass; the §7.6 composition is the fix).
402
+ **The two exclusions to remember:** batching excludes uniform/TurboQuant KV
403
+ (per-layer `kv_config.json` does compose with batching), and spec excludes
404
+ prompt-cache reuse (v1 bypass; the §7.6 composition is the fix).
400
405
 
401
406
  ## Observability — `GET /stats`
402
407
 
@@ -405,9 +410,11 @@ lives in [server-api.md](server-api.md#get-stats) (kept in lockstep with
405
410
  the code — this section only explains how to read it); highlights:
406
411
  `prompt_cache` (8 GB default cap), `kv_quant.mode` (incl. `turbo kXvY`),
407
412
  the conditional `ssd_cache` block, and `batch`
408
- (`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes`/`kv_budget_bytes`).
409
-
410
- `batch.batched` reflects only whether `--batch N` (N>1) is configured;
411
- with an explicit `--kv-quant` it can read `true` while `active_rows`
412
- never exceeds 1 (every request routes serial). `active_rows` is the
413
- honest signal of whether anything is actually batching.
413
+ (`mode`/`active_rows`/`pending_rows`/`submitted_rows`/`kv_bytes`/
414
+ `kv_budget_bytes`).
415
+
416
+ `batch.mode` is the truthful configured/capability state: `off` for
417
+ `--batch 1`, `serial` when a larger configured cap cannot batch the loaded
418
+ model's cache layout, and `batch` when the model is admitted. `batch.batched`
419
+ is the compatibility boolean for `mode == "batch"`; `active_rows` reports the
420
+ instantaneous live rows.
@@ -177,7 +177,7 @@ are `DEFAULT_TRAIN_CONFIG` (trainer.ts:89).
177
177
  | `weight_decay` | float ≥0 | `0.01` | AdamW weight decay (β = `[0.9, 0.999]`, fixed) |
178
178
  | `lora_dropout` | float [0,1) | `0.0` | LoRA-input dropout (PEFT-style), training-only regularizer for small sets. Recompute-safe (mask keyed by step+layer, so segmented/checkpointed backward reproduces it) |
179
179
  | `rs_lora` | bool | `false` | rsLoRA — scale the update by α/√rank instead of α, so `rank_scaling` changes capacity not step size. Recorded in the adapter config; inference applies the same per-layer scale |
180
- | `lora_plus_ratio` | float ≥1 | `1.0` | LoRA+ — LR multiplier for the B leaves (A stays at base LR). >1 speeds the B-driven early learning (B is zero-init). 1 = off. Wired in the ORPO loop |
180
+ | `lora_plus_ratio` | float ≥1 | `1.0` | LoRA+ — LR multiplier for the B leaves (A stays at base LR). >1 speeds the B-driven early learning (B is zero-init). 1 = off. The same optimizer policy applies to SFT, DPO, and ORPO |
181
181
  | `grad_checkpoint` | bool | `false` | Recompute layer activations in backward (memory↔compute; bit-identical) |
182
182
  | `mlp_split` | bool | `false` | Gradient-checkpoint MLP splitting (Gemma4-only, requires `grad_checkpoint`) |
183
183
  | `segment_size` | int | `0` (off) | `>0` enables segmented backward — layers per segment (see below) |
@@ -295,6 +295,15 @@ A finished run writes a PEFT-compatible adapter directory:
295
295
  - `optiq_lora_config.json` — mlx-bun/optiq adapter metadata (per-layer ranks)
296
296
  - `adapter_config.json` — PEFT-compatible config
297
297
 
298
+ The save completes only after both config files are written, including
299
+ eval-step checkpoints, so a successful process exit always leaves an
300
+ immediately mountable directory. Serving accepts both mlx-lm tensor layout
301
+ (`[in, rank]` / `[rank, out]`) and standard PEFT layout
302
+ (`[rank, in]` / `[out, rank]`, including `base_model.model.*` names).
303
+ Standard PEFT `use_rslora` metadata is honored as
304
+ `lora_alpha / sqrt(rank)`, and saved rsLoRA adapters emit that metadata so
305
+ their training and serving scales agree.
306
+
298
307
  When `save_checkpoints: true`, each eval step also writes
299
308
  `checkpoints/step-<NNNNN>-val<loss>/` and a durable `metrics.json` (config,
300
309
  wall seconds, peak GB, final/best train+val loss, full val trajectory).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mlx-bun",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
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",