mlx-bun 0.0.4

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
@@ -0,0 +1,224 @@
1
+ # Server API
2
+
3
+ `mlx-bun serve` (or `bun scripts/serve.ts`) exposes an OpenAI-compatible
4
+ HTTP API on one model. The request's `model` field is ignored; the
5
+ loaded model's id is echoed back. By default generation is serialized
6
+ through a single queue (one GPU, batch = 1); `--batch N` switches the
7
+ server into bf16 continuous batching.
8
+
9
+ For the full set of start flags (`--port`, `--memory-budget`,
10
+ `--prompt-cache`, `--batch`, `--kv-quant`, `--thinking`, sampling
11
+ defaults, and the perf levers) and a compatibility matrix of which
12
+ combinations compose, see [server-config.md](./server-config.md). This
13
+ doc covers the request/response wire format. LoRA adapters are mounted at
14
+ runtime via `POST /v1/adapters` (below), not a start flag.
15
+
16
+ ## POST /v1/chat/completions
17
+
18
+ Request body (OpenAI chat schema; unknown fields ignored):
19
+
20
+ ```jsonc
21
+ {
22
+ "messages": [ /* role: system | user | assistant | tool */ ],
23
+ "stream": false,
24
+ "max_tokens": 1024, // or max_completion_tokens (wins)
25
+ "temperature": 0.7, // 0 = greedy
26
+ "top_p": 0, "top_k": 0, // 0 = off
27
+ "seed": 1234, // omit for time-derived
28
+ "repetition_penalty": 1.1, // optional
29
+ "stop": "\n\n", // or ["###", "\n\n"] (spec: up to 4)
30
+ "tools": [ /* OpenAI function tools */ ],
31
+ "tool_choice": "auto", // "none" disables tools
32
+ "chat_template_kwargs": { // forwarded to the chat template
33
+ "enable_thinking": false // MiniCPM5: <think> channel on/off
34
+ },
35
+ "adapter": "id" // LoRA: "id", stacked "a+b", or "none"
36
+ }
37
+ ```
38
+
39
+ Sampling defaults follow the model author's `generation_config.json`
40
+ when a field is omitted (optiq serve's gen_config behavior); explicit
41
+ request values always win. MiniCPM5 defaults to the no-think direct
42
+ answer mode unless `chat_template_kwargs.enable_thinking` is `true`.
43
+
44
+ `stop` sequences are matched on **decoded text**, not token ids, so a
45
+ sequence that spans token boundaries still fires. Generation halts at
46
+ the first match; the stop sequence itself is excluded from the content
47
+ and `finish_reason` is `"stop"`.
48
+
49
+ Message `content` is a string or an array of parts:
50
+ `{ "type": "text", "text": ... }` and
51
+ `{ "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }`
52
+ (http/https URLs also accepted; PNG, JPEG, HEIC, AVIF, WebP, TIFF, GIF,
53
+ BMP via native OS codecs; requires a model with the vision sidecar).
54
+
55
+ Non-streaming response:
56
+
57
+ ```jsonc
58
+ {
59
+ "id": "chatcmpl-…", "object": "chat.completion", "created": 1760000000,
60
+ "model": "<loaded model id>",
61
+ "choices": [{
62
+ "index": 0,
63
+ "message": {
64
+ "role": "assistant",
65
+ "content": "…", // "" when only tool calls
66
+ "tool_calls": [{ // present when the model called tools
67
+ "id": "call_…", "type": "function",
68
+ "function": { "name": "…", "arguments": "{…json…}" }
69
+ }]
70
+ },
71
+ "finish_reason": "stop" | "length" | "tool_calls"
72
+ }],
73
+ "usage": {
74
+ "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
75
+ "prompt_tokens_details": { "cached_tokens": 0 } // prompt-cache reuse
76
+ }
77
+ }
78
+ ```
79
+
80
+ Streaming (`"stream": true`) is SSE: `data: <chunk>\n\n` per event,
81
+ terminated by `data: "[DONE]"`. Chunks are `chat.completion.chunk`
82
+ objects whose `choices[0].delta` carries `{role}`, then `{content}`
83
+ increments (multi-byte sequences are held back until decodable; text
84
+ that could begin a `stop` sequence is held back until disambiguated,
85
+ so no part of a stop sequence is ever streamed), then
86
+ for tool calls a final `{tool_calls: [{index, id, type, function}]}`
87
+ delta; the last chunk carries `finish_reason` and `usage`.
88
+
89
+ Tool round-trip: send the assistant message with its `tool_calls` back,
90
+ followed by `{ "role": "tool", "tool_call_id": …, "content": … }`
91
+ messages; multi-turn prompt prefixes reuse the KV prompt cache
92
+ automatically.
93
+
94
+ Tool-call parsing is per model family. Gemma 4 uses its native
95
+ `<|tool_call>`…`<tool_call|>` sentinel tokens. MiniCPM5 emits XML in
96
+ decoded text (`<function name="…"><param name="…">…`, CDATA-wrapped
97
+ values supported); content before the tool markup still streams live,
98
+ only the markup is withheld and converted to `tool_calls`. Argument
99
+ values are decoded against the tool's JSON schema (string-typed params
100
+ stay strings); markup that fails to parse falls back to plain content.
101
+
102
+ ### Errors
103
+
104
+ All errors are `{ "error": { "message": …, ... } }`.
105
+
106
+ - `400` — malformed JSON, empty `messages`, unknown adapter id, vision
107
+ request on a model without a sidecar, prompt build failures.
108
+ - `400` with `"type": "memory_admission"`, `"code":
109
+ "context_over_budget"` — `prompt + max_tokens` exceeds the memory
110
+ budget's max safe context (only when serving with `--memory-budget`;
111
+ the GPU OOM this prevents would kill the process, so it is refused
112
+ up front). Lower `max_tokens` or shorten the prompt; the ceiling is
113
+ visible at `/stats`.
114
+
115
+ ## POST /v1/messages (Anthropic Messages API)
116
+
117
+ Anthropic-protocol surface over the same engine — on by default, like
118
+ `optiq serve`. Point any Anthropic-SDK tool at the server
119
+ (`ANTHROPIC_BASE_URL=http://localhost:8090`, any `x-api-key`) — Claude
120
+ Code works as a client this way.
121
+
122
+ - `system` (string or text blocks), `messages` with string or
123
+ content-block arrays; `tool_use` / `tool_result` blocks map to the
124
+ native gemma tool-calling path (better than the optiq shim, which
125
+ inlines them as text); `image` blocks (base64 or url source) hit the
126
+ vision path on sidecar models.
127
+ - `tools` (`{name, description, input_schema}`) map to function tools;
128
+ server-tool types (web_search, …) are dropped silently.
129
+ - `max_tokens`, `temperature`, `top_p`, `top_k`, `stop_sequences`,
130
+ `stream` as in the Anthropic spec.
131
+ - Response: `{id: "msg_…", type: "message", content: [{type: "text"} |
132
+ {type: "tool_use"}…], stop_reason, usage: {input_tokens,
133
+ output_tokens, cache_read_input_tokens}}` —
134
+ `cache_read_input_tokens` comes from the prompt cache.
135
+ - Streaming follows the Anthropic event grammar exactly:
136
+ `message_start → content_block_start/delta/stop (text_delta,
137
+ input_json_delta) → message_delta (stop_reason + usage) →
138
+ message_stop`. Errors are `event: error` frames.
139
+ - Errors: `{type: "error", error: {type: "invalid_request_error" |
140
+ "api_error", message}}`.
141
+
142
+ ## POST /v1/responses (OpenAI Responses API)
143
+
144
+ Responses-protocol surface (Codex, Cursor, Continue, Cline, and the
145
+ OpenAI SDK speak this now). Oracle: optiq responses shim.
146
+
147
+ - `input` (string or item array: `message`, `function_call`,
148
+ `function_call_output`), `instructions` (merged with any
149
+ system/developer items into one leading system message),
150
+ `max_output_tokens`, `temperature`, `top_p`, `top_k`, flat
151
+ `tools`/`tool_choice` (built-in tool types dropped), `stream`.
152
+ - **`previous_response_id` resumption**: pass a prior response id
153
+ instead of resending the conversation; the server splices the stored
154
+ input + output back in (instructions carry forward when omitted).
155
+ Store is per-process, 1 h TTL, 32 MiB byte-capped LRU — observable
156
+ at `GET /stats` (`response_store`). Unknown/expired id → 404.
157
+ - Response: `{id: "resp_…", object: "response", status: "completed" |
158
+ "incomplete", output: [{type: "message"|"function_call"…}], usage}`.
159
+ - Streaming event chain: `response.created → response.in_progress →
160
+ response.output_item.added → response.content_part.added →
161
+ response.output_text.delta… → response.output_text.done →
162
+ response.content_part.done → response.output_item.done →
163
+ response.completed` (+ `response.function_call_arguments.delta/.done`
164
+ for tool calls).
165
+
166
+ ## GET /v1/models
167
+
168
+ `{ "object": "list", "data": [{ "id": "<model id>", "object": "model", … }] }`
169
+
170
+ ## GET /stats
171
+
172
+ ```jsonc
173
+ {
174
+ "server": { "owner": "serve" | "pi-session" | "embedded", "model": "...", "started_at": 0 },
175
+ "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 0, "hits": 0, "misses": 0 },
176
+ "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
177
+ "kv_quant": { "mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
178
+ "layers": { "kv4": 8, "bf16": 40 },
179
+ "attention": { "global": 10, "sliding_window": 38 } },
180
+ "admission": {
181
+ "max_safe_context": 0, // tokens; requests above this 400
182
+ "memory_budget_bytes": null, // explicit budget, or null (machine default)
183
+ "usable_bytes": 0,
184
+ "weights_bytes": 0
185
+ },
186
+ "batch": {
187
+ "configured": 1, // the --batch N value
188
+ "batched": false, // batching enabled (N>1)
189
+ "active_rows": 0 // rows currently decoding in the batch
190
+ }
191
+ }
192
+ ```
193
+
194
+ ## Adapters (LoRA hot-swap)
195
+
196
+ - `GET /v1/adapters` — `{ adapters: [{ id, path, rank, scale, size_bytes, mounted_layers }] }`
197
+ - `POST /v1/adapters` — `{ "id": "...", "path": "/dir" }`; mounts
198
+ through the generation queue (never races a forward pass). 400 on
199
+ shape/compat mismatch — validation is all-or-nothing.
200
+ - `DELETE /v1/adapters/<id>` — unmount; 404 if not mounted.
201
+
202
+ Select per request with the `adapter` body field. Prompt-cache entries
203
+ are namespaced per adapter spec, so switching adapters never reuses
204
+ another adapter's KV.
205
+
206
+ ## Client setup: pi
207
+
208
+ `~/.pi/agent/models.json`:
209
+
210
+ ```jsonc
211
+ {
212
+ "providers": {
213
+ "mlx-bun": {
214
+ "baseUrl": "http://localhost:8090/v1",
215
+ "api": "openai-completions",
216
+ "apiKey": "sk-anything-nonempty",
217
+ "models": [{ "id": "<model id from /v1/models>" }]
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ Any OpenAI SDK works the same way: `baseURL: "http://localhost:8090/v1"`,
224
+ any non-empty `apiKey`.
@@ -0,0 +1,243 @@
1
+ # Server configuration
2
+
3
+ Every start flag for `mlx-bun serve` (and `mlx-bun pi`, which shares the
4
+ same flag set), what each does, its default, and — the part that bites —
5
+ **which combinations compose and which don't**. For the request/response
6
+ wire format (chat/messages/responses bodies, SSE grammar, tool calling,
7
+ adapters admin) see [server-api.md](./server-api.md); this doc is about
8
+ *starting* the server.
9
+
10
+ The flags are parsed in `serverRuntimeFlags()`
11
+ ([src/cli.ts](../../src/cli.ts)); the runtime knobs live in
12
+ `ServerOptions` ([src/server.ts](../../src/server.ts)). The two execution
13
+ lanes (serial vs. `--batch N`) and the lane picker are in
14
+ [src/serve/generation-gateway.ts](../../src/serve/generation-gateway.ts);
15
+ the design rationale is in
16
+ [docs/design/parallel-slots.md](../design/parallel-slots.md).
17
+
18
+ ## Start flags
19
+
20
+ | Flag | Arg | Default | What it does |
21
+ | --- | --- | --- | --- |
22
+ | `--host` | addr | all interfaces | Interface to bind. `127.0.0.1` = loopback only. |
23
+ | `--port` | n | `8090` | 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⁹).** |
25
+ | `--prompt-cache` | GB | `2` (≈2e9 bytes) | Byte cap for the prompt (prefix-reuse KV) cache. **Binary GiB (×2³⁰)** on input. `0`/unset ⇒ default; there is no flag to disable the cache outright. |
26
+ | `--batch` | n | `1` (serial) | Max concurrent requests batched through the mlx-lm-parity engine. `>1` switches the **whole server** into bf16 continuous batching — a *mode*, not a load fallback. See [Execution modes](#execution-modes-serial-vs---batch-n). Alias: `--decode-concurrency` (mlx_lm.server drop-in). |
27
+ | `--kv-quant` | `config`\|`off`\|`4`\|`8` | `config` serial / `off`(bf16) under `--batch N` | KV-cache quantization. `config` = per-layer `kv_config.json` (optiq parity); `off` = bf16; `4`/`8` = uniform bits (group 64, start 0). Under `--batch N`, an explicit value routes those requests to the **serial** lane (batched is bf16-only). |
28
+ | `--thinking` | `true`\|`false` | model's own (false for CPM) | Server-wide default for the chat template's `enable_thinking` (MiniCPM5/CPM hybrid reasoning). A request's `chat_template_kwargs.enable_thinking` overrides it. |
29
+ | `--temperature` | n ∈ [0,5] | `generation_config.json` | Server-wide sampling default. Per-request `temperature` still wins; the browser chat (sends none) inherits this. |
30
+ | `--top-p` | n ∈ [0,1] | `generation_config.json` | Server-wide top-p default (per-request `top_p` wins). |
31
+ | `--top-k` | n ∈ [0,1e6] | `generation_config.json` | Server-wide top-k default (per-request `top_k` wins). |
32
+ | `--compiled-decode` | on\|off | on | Replay the per-step decode graph in C++ (`MLX_BUN_COMPILED_DECODE`). Bit-exact A/B lever. **Serial lane only** (see note below). |
33
+ | `--perf-kernel` | on\|off | **on** | Fused quantized-KV decode-SDPA Metal kernel (`MLX_BUN_PERF_KERNEL`), the perf side of the compat A/B. Engages on quantized caches at decode. **Serial lane only.** |
34
+ | `--fused-decode` | on\|off | off | Experimental: tile the quantized decode SDPA (`MLX_BUN_FUSED_DECODE`). **Serial lane only.** |
35
+ | `--fused-sdpa` | on\|off | on | Fused SDPA path for quantized prefill/continuation (inverted env `MLX_BUN_NO_FUSED_SDPA`). **Serial lane only.** |
36
+ | `--force-wire` | (bool) | off | Wire weights into memory for the whole generation (`MLX_BUN_FORCE_WIRE`). Near-ceiling models (e.g. 26B) need it. **Serial lane only.** |
37
+
38
+ The five performance levers are A/B knobs whose defaults are the measured
39
+ winners; flip them to compare. They are set as `MLX_BUN_*` env vars
40
+ before the model loads, so they apply to `mlx-bun pi` too. They affect
41
+ the **serial** decode path (`generate()`); the batched scheduler calls
42
+ `model.forwardHidden` directly and so is unaffected by all of them — see
43
+ [Levers that don't reach the batched lane](#levers-that-dont-reach-the-batched-lane).
44
+
45
+ > **Note — `--perf-kernel` default.** The code defaults it **on**
46
+ > (`perfKernelEnabled()` returns true unless `MLX_BUN_PERF_KERNEL=0`, and
47
+ > the ready card / `/stats` report it on). `STATUS.md` still lists a
48
+ > "default flip" as pending; treat that as stale — the engaged default
49
+ > today is on. (If the intent is genuinely off-until-clean-machine-pass,
50
+ > the *code* is what needs changing, not this doc.)
51
+
52
+ ## Per-request overrides
53
+
54
+ Most quality knobs can be set per request in the chat body and override
55
+ the server-wide default. Precedence, highest first:
56
+
57
+ 1. explicit request field (`temperature`, `top_p`, `top_k`, `seed`,
58
+ `repetition_penalty`, `max_tokens`/`max_completion_tokens`, `stop`,
59
+ `chat_template_kwargs.enable_thinking`, `adapter`, `tools`)
60
+ 2. the matching `--temperature`/`--top-p`/`--top-k`/`--thinking` server default
61
+ 3. the model's `generation_config.json`
62
+ 4. the built-in fallback (temp 0.7, top-p/top-k 0 = off)
63
+
64
+ These ride all three protocol surfaces (`/v1/chat/completions`,
65
+ `/v1/messages`, `/v1/responses`) because each funnels through one
66
+ `handleChat`. Full field list: [server-api.md](./server-api.md).
67
+
68
+ ## Environment levers
69
+
70
+ The CLI flags above set these; you can also export them directly (e.g.
71
+ for `bun scripts/serve.ts` or paired A/B harnesses). One has no CLI flag.
72
+
73
+ | Env var | CLI flag | Default | Effect |
74
+ | --- | --- | --- | --- |
75
+ | `MLX_BUN_COMPILED_DECODE` | `--compiled-decode` | on (`!=="0"`) | Compiled decode graph replay. |
76
+ | `MLX_BUN_PERF_KERNEL` | `--perf-kernel` | on (`!=="0"`) | Fused quantized-KV decode kernel (not bit-exact; perf A/B). |
77
+ | `MLX_BUN_FUSED_DECODE` | `--fused-decode` | off (`==="1"`) | Tile quantized decode SDPA. |
78
+ | `MLX_BUN_NO_FUSED_SDPA` | `--fused-sdpa` (inverted) | fused on | `=1` forces the stock unfused SDPA everywhere. |
79
+ | `MLX_BUN_FUSED_GELU` | *(none)* | on (`!=="0"`) | Fused GeGLU MLP kernel. Bit-exact, so it stays on both lanes; env-only opt-out. |
80
+ | `MLX_BUN_FORCE_WIRE` | `--force-wire` | off (`==="1"`) | Wire weights for the generation. |
81
+
82
+ ## Execution modes: serial vs. `--batch N`
83
+
84
+ `--batch 1` (default) is the serialized single-queue path: one GPU, one
85
+ generation at a time, prompt-cache prefix reuse, mixed-precision KV by
86
+ default. Untouched and unconditional.
87
+
88
+ `--batch N` (N>1) opts the **whole server** into a continuous-batching
89
+ engine that is **bit-parity with `mlx_lm.server` at `B=N`**. It is a
90
+ *mode switch*, not a load-dependent fallback (results must not depend on
91
+ concurrency).
92
+
93
+ Parity is the guarantee, so the batched lane runs in **compat mode**: it
94
+ exposes **none** of the serial lane's perf flags and runs the plain
95
+ bit-exact forward — the same path mlx-lm runs, never the optional
96
+ parity-breaking kernels. This is the mechanism of the guarantee, not a
97
+ missing feature (see
98
+ [compat mode](#--batch-n-is-compat-mode--perf-flags-dont-apply-by-design)).
99
+ Because mlx-lm's batched path is bf16 (its quantized batching is NYI),
100
+ bf16 continuous batching **is** the drop-in:
101
+
102
+ - **KV quant unset ⇒ bf16** so the batch path engages out of the box
103
+ ("Option B"). The serial default stays mixed-precision (optiq parity).
104
+ - **Explicit `--kv-quant config|4|8` ⇒** those requests route to the
105
+ serial lane (batched is bf16-only; a startup warning is printed). With
106
+ an explicit `--kv-quant`, *every* request carries a quant scheme, so
107
+ **nothing batches** — `--batch N --kv-quant config` is effectively
108
+ serial-with-quant. Omit `--kv-quant` to actually batch.
109
+
110
+ ### The lane picker (`GenerationGateway.willBatch`)
111
+
112
+ Under `--batch N`, each request is routed per-request. It joins the
113
+ batch only if **all** of these hold; otherwise it drains the batch and
114
+ runs solo (mlx-lm's `_is_batchable` behavior). The two lanes are
115
+ mutually exclusive on the GPU (one `AsyncMutex`), so a serial-fallback
116
+ request never runs alongside a batched step — but batched requests run
117
+ concurrently with each other.
118
+
119
+ | Request property | Batches? |
120
+ | --- | --- |
121
+ | vision (image parts) | ❌ serial — needs offset-0 single-seq prefill + bidirectional image mask |
122
+ | LoRA `adapter` (resolves to ≥1) | ❌ serial — `loraState.active` is one per-generation field; per-row adapters unsupported |
123
+ | `repetition_penalty` | ❌ serial — per-row logits processors are a later refinement |
124
+ | explicit `seed` | ❌ serial — reproducibility ⇒ solo (matches mlx-lm) |
125
+ | KV quant active (explicit `--kv-quant`) | ❌ serial — batched is bf16-only in v1 |
126
+ | `temperature` / `top_p` / `top_k` | ✅ batches (each row samples with its own seed) |
127
+ | `stop` sequences | ✅ batches (per-row `StopMatcher` in the onToken closure) |
128
+ | `tools` / `tool_choice` | ✅ batches (per-row tool router; decode-layer parse) |
129
+ | `--thinking` / `enable_thinking` | ✅ batches (template-render concern, lane-independent) |
130
+ | multi-turn / long prompt | ✅ batches, but **no prompt-cache reuse** (`cached_tokens=0`) |
131
+
132
+ Both full-attention (CPM) and sliding-window (Gemma) models batch; the
133
+ scheduler assembles each layer's cache by attention type.
134
+
135
+ ## Compatibility matrix
136
+
137
+ How each option behaves in each mode. "serial" = `--batch 1`; "`--batch N`"
138
+ = the batched mode (a given request may still take the serial *lane*
139
+ inside it per the table above).
140
+
141
+ | Option | serial (`--batch 1`) | `--batch N` (N>1) |
142
+ | --- | --- | --- |
143
+ | `--kv-quant config`/`4`/`8` | ✅ applied to all requests | ⚠️ applied, but forces **all** requests to the serial lane (no batching) |
144
+ | `--kv-quant off` | ✅ bf16 | ✅ bf16 (same as the implicit batch default) |
145
+ | *(kv-quant unset)* | mixed-precision `config` | **bf16** (Option B) — incl. serial-lane fallback requests |
146
+ | `--memory-budget` | ✅ per-request admission | ✅ per-request admission — but **not aggregate** across rows (see limitations) |
147
+ | `--prompt-cache` | ✅ prefix reuse | ⚠️ bypassed for batched requests (`cached_tokens=0`); serial-lane requests still reuse |
148
+ | `--temperature`/`--top-p`/`--top-k` | ✅ | ✅ (per-row) |
149
+ | `--thinking` | ✅ | ✅ |
150
+ | vision request | ✅ | ✅ via serial lane (in bf16 under Option B) |
151
+ | LoRA `adapter` | ✅ | ✅ via serial lane |
152
+ | `repetition_penalty` | ✅ | ✅ via serial lane |
153
+ | `seed` | ✅ | ✅ via serial lane |
154
+ | `tools` / `stop` | ✅ | ✅ (batches) |
155
+ | `--compiled-decode`/`--perf-kernel`/`--fused-*`/`--force-wire` | ✅ (serial perf tree) | **n/a — compat mode, no perf flags by design** |
156
+
157
+ ### `--batch N` is compat mode — perf flags don't apply by design
158
+
159
+ The bit-parity guarantee (mlx-lm B=N) is the *whole point* of `--batch N`,
160
+ and it requires running the plain forward path. So the batched lane
161
+ deliberately exposes **no** perf knobs: the scheduler
162
+ ([batch-scheduler.ts](../../src/serve/batch-scheduler.ts)) drives the
163
+ model through `forwardHidden`/`logitsFromHidden` directly (not
164
+ `generate()`), running the same bit-exact kernels mlx-lm runs — never the
165
+ optional, parity-breaking ones. Flagging the batched lane would defeat
166
+ the guarantee, so it's intentionally not wired.
167
+
168
+ - **`--perf-kernel` / `--fused-decode` / `--compiled-decode`** — never
169
+ engage in the batched lane. They diverge from the -O0 reference (or, for
170
+ the quantized-KV kernels, are moot since batched is bf16). They're the
171
+ L3 perf row, a separate KL-gated path — see the validation matrix in
172
+ [parallel-slots.md](../design/parallel-slots.md).
173
+ - **`--force-wire`** — doesn't wire (the scheduler bypasses `generate()`'s
174
+ wired scope). A model that needs wiring for speed (e.g. 26B: 8.6 → 32.3
175
+ tok/s wired) runs at mlx-lm-equivalent unwired speed under `--batch N`;
176
+ in practice such a model has no headroom for B>1 KV anyway.
177
+ - **Always-on bit-exact kernels still run.** Fused GeGLU is bit-exact with
178
+ the spelled-out MLP, so it stays on in both lanes without breaking
179
+ parity — "compat mode" means *no parity-breaking optionality*, not
180
+ -O0.
181
+
182
+ The serial lane is where the perf/optimization tree lives: `--perf-kernel`
183
+ / `--fused-*` / `--compiled-decode` engage there, and the
184
+ mixed-precision-KV (optiq) default applies there.
185
+
186
+ ## Known limitations under `--batch N`
187
+
188
+ These are deliberate v1 scope, not bugs — but they change behavior, so
189
+ know them:
190
+
191
+ 1. **Prompt cache bypassed.** Batched requests solo-prefill every row;
192
+ `cached_tokens=0`. Wiring `PromptCache` into the scheduler is a
193
+ follow-up.
194
+ 2. **Admission is per-request, not aggregate.** `--memory-budget` checks
195
+ each request against single-sequence max-safe-context, but N
196
+ concurrent rows can collectively exceed the budget (the `B×S_max`
197
+ KV-budget admission is TODO). With a tight budget and several
198
+ large-context requests, the allocator cap is the only backstop — and
199
+ a true GPU OOM here is uncatchable. Size the budget with headroom for
200
+ N rows, or keep N small.
201
+ 3. **Short-context only.** Verified pre-ring-wrap (rows < the 1024
202
+ sliding window). Long-context (context > window) batched decode is a
203
+ separate validation.
204
+ 4. **bf16 only — by contract.** mlx-lm's batched path *is* bf16, so
205
+ bf16-only batching is exactly what mlx-lm-parity means (not a
206
+ shortcoming). Going further — batched + mixed-precision KV quant — is
207
+ novel territory with no mlx-lm/optiq oracle, so it's a deferred,
208
+ KL-gated extension.
209
+ 5. **`extend` join not yet used.** A joining request re-merges the whole
210
+ batch (O(B·S)); mlx-lm's keep-the-running-batch `extend` is a later
211
+ optimization (numerically equivalent).
212
+
213
+ ## Observability — `GET /stats`
214
+
215
+ The live config and batch state:
216
+
217
+ ```jsonc
218
+ {
219
+ "server": { "owner": "serve", "model": "...", "started_at": 0 },
220
+ "prompt_cache": { "entries": 0, "bytes": 0, "max_bytes": 2000000000, "hits": 0, "misses": 0 },
221
+ "response_store": { "entries": 0, "bytes": 0, "max_bytes": 33554432, "ttl_ms": 3600000 },
222
+ "kv_quant": {
223
+ "mode": "mixed (kv_config.json)" | "uniform-kv8" | "bf16",
224
+ "layers": { "kv4": 8, "bf16": 40 },
225
+ "attention": { "global": 10, "sliding_window": 38 }
226
+ },
227
+ "admission": {
228
+ "max_safe_context": 0, // requests above this 400
229
+ "memory_budget_bytes": null, // explicit budget, or null = machine default
230
+ "usable_bytes": 0, "weights_bytes": 0
231
+ },
232
+ "batch": {
233
+ "configured": 1, // the --batch N value
234
+ "batched": false, // batching enabled (N>1) for this server
235
+ "active_rows": 0 // rows currently decoding in the batch
236
+ }
237
+ }
238
+ ```
239
+
240
+ `batch.batched` reflects only whether `--batch N` (N>1) is configured;
241
+ with an explicit `--kv-quant` it can read `true` while `active_rows`
242
+ never exceeds 1 (every request routes serial). `active_rows` is the
243
+ honest signal of whether anything is actually batching.
@@ -0,0 +1,148 @@
1
+ # Handoff — segmented backward training (2026-06-16)
2
+
3
+ Current-state report for the segmented-backward LoRA training work. Deep dossier
4
+ (design, proofs, every measurement): [`docs/design/segmented-backward-training.md`](design/segmented-backward-training.md)
5
+ §9 (MiniCPM5) and §10 (e4b). Branch: `segmented-backward-training`,
6
+ PR [#9](https://github.com/joshuarossi/mlx-bun/pull/9).
7
+
8
+ ## TL;DR
9
+
10
+ Long-context LoRA SFT that **streams the backward segment-by-segment** so only one
11
+ segment's activations are live at a time — training fits at context lengths where
12
+ the full backward spikes. Two phases, both working:
13
+
14
+ - **MiniCPM5 (Phase A): done, production-ready.** Bit-exact mechanism, no memory
15
+ leak, trains end-to-end. Real 300-iter chunk fine-tune → **chunk-eval 95.10/100**
16
+ (beats the 91.70 non-segmented baseline; pre-tune 11.89).
17
+ - **e4b / gemma-3n (Phase B): built + validated.** Forward bit-exact; grads
18
+ bf16-class; trains **all 42 layers at 8K context** where `mlx_lm.lora` OOMs.
19
+
20
+ **Parity:** our **non-segmented** training is **bit-exact with mlx-lm** at 2k
21
+ (forward, loss, AND gradient — verified by running mlx-lm's own `default_loss` /
22
+ `nn.value_and_grad`). The **segmented** path is **bf16-class (~2%)** off, a
23
+ grouping-controllable bf16 non-associativity (not a logic bug).
24
+
25
+ ## What was built
26
+
27
+ | file | what |
28
+ |---|---|
29
+ | `src/train/segmented.ts` | `SegmentedBackward` (MiniCPM5) + `SegmentedBackwardGemma4` (e4b) + `planSegmentsBySize`; the per-segment vjp driver, `detachLeaf` |
30
+ | `src/mlx/autograd.ts` | `Vjp` — `mlx_vjp` binding (the backward primitive; see "leak" below) |
31
+ | `src/mlx/ffi.ts` | `mlx_vjp` FFI decl |
32
+ | `src/mlx/array.ts` | `MlxArray.fromBytesCopy` — graph-free leaf copy (row-major) |
33
+ | `src/model/minicpm5.ts` | `runLayerRange` (forwardLayers delegates) |
34
+ | `src/model/gemma4.ts` | `runLayerRange` + `makeTrainingMasks` + `embedForSegmented` + `reusedDonors` (additive; forwardLayers untouched) |
35
+ | `src/train/trainer.ts` | `TrainConfig.segmentSize`; gated segmented path (MiniCPM5 + Gemma4) |
36
+ | `src/train/loss.ts` | exported `responseOnlyCe`, `maskedCe` |
37
+
38
+ **Enable:** set `segmentSize` (layers per segment) > 0 in the train config, or
39
+ `SEG=n` in `scripts/chunk-finetune.ts`. 0 = the normal full backward.
40
+
41
+ How it works (B=1 SFT): forward saves a DETACHED boundary (graph-free leaf) at each
42
+ segment edge → a loss-head `mlx_vjp` gives `dh` for the last boundary → reverse
43
+ per-segment `mlx_vjp`s thread `dh` and the segment's LoRA params, accumulating
44
+ grads. Only one segment's activations are live. e4b adds two boundary streams: the
45
+ per-layer-input tensor (a pure constant) and the **KV-shared donor K/V** (a second
46
+ boundary stream WITH cotangent accumulation — the intricate part; docs §10).
47
+
48
+ ## What was validated (honest numbers)
49
+
50
+ **Correctness**
51
+ - MiniCPM5 segmented grads vs the full value_and_grad: **bit-exact under flash**
52
+ (relNorm 0.0000%). Under `ops.sdpa`, ~6% — a *pre-existing* `ops.sdpa`
53
+ eager-vs-autograd forward divergence, not segmentation.
54
+ - e4b: forward **bit-exact** vs full; single-consumer donor reuse **bit-exact**;
55
+ natural-cut donor-KV grads **~1% (bf16 non-associativity)** — single-consumer
56
+ exact, flat in consumer count, tracks summand count (donor 22's 15 sharers
57
+ ≈0.46% vs donor 23's 3 ≈0%), grouping-dependent for both bf16 AND fp32. fp32
58
+ accumulation does NOT help (the term is mlx's within-vjp bf16 sum). **Controllable
59
+ by the segment cut** (coarser sharer grouping → tighter, down to bit-exact at
60
+ 1 consumer), not the dtype.
61
+ - **Parity vs mlx-lm (ran their code):** forward/loss — denominator (`M` vs
62
+ `ntoks`) bit-exact across 6 mask spans incl. edges; response-only CE == our
63
+ full-masked CE (a pure memory optimization); loss bit-exact on 4/6 spans, 0.5
64
+ float32-ulp on 2/6 (a logsumexp/reduction f32 effect, ~5e-5%, harmless).
65
+ Gradient — our FULL (non-segmented) `dB` over all 42 q_proj **bit-exact** with
66
+ mlx-lm; segmented `dB` ~2.3% (the bf16 donor-KV effect).
67
+
68
+ **Memory leak (found + fixed).** The first cut used a surrogate-loss
69
+ `value_and_grad`; it leaked ~32 MB/segment/step (mlx-level, not GC/cache/sync
70
+ reclaimable). **Fixed by using `mlx_vjp`** (the natural primitive — no surrogate).
71
+ End-to-end now flat (active constant across a 300-iter run). Repro:
72
+ `scripts/vag-leak-test.ts` (MODE=layers leaks via value_and_grad; MODE=vjp flat).
73
+
74
+ **Memory vs the real reference** (`mlx_lm.lora --grad-checkpoint`, the way a user
75
+ trains — it loads the OptiQ e4b directly). mlx-lm checkpoints **per-layer**; ours
76
+ groups `segmentSize` layers. Matched grouping (both per-layer = segSize 1, all 42
77
+ layers, batch 1):
78
+
79
+ | L | mlx-lm (per-layer) | ours (segSize=1) |
80
+ |---|---|---|
81
+ | 2048 | 12.84 GB | 8.76 GB |
82
+ | 4096 | 20.87 GB | 10.93 GB |
83
+ | 8192 | **OOM** (25.7 GB @16 layers) | 15.29 GB |
84
+
85
+ Ours lower at every length AND trains all 42 layers at 8K where mlx-lm OOMs. **Two
86
+ confounds, not isolated:** (1) mlx-lm materializes the full `[1,L,262k]` logits
87
+ (~4 GB @4K) while we use response-only CE; (2) our full backward streaming vs
88
+ mlx-lm's per-layer checkpoint. So the gap is part CE-optimization, part mechanism —
89
+ not purely "segmentation beats checkpointing." (Earlier "reference crashes at 4K /
90
+ ~70 GB" was WRONG — it used mlx-bun's *own* ineffective checkpoint as the baseline.)
91
+
92
+ ## How to run an adapter
93
+
94
+ The fine-tune saves a hot-swap adapter dir (`adapters.safetensors` +
95
+ `optiq_lora_config.json` + `adapter_config.json`).
96
+
97
+ **One-off generate** (verified working) — `scripts/run-adapter.ts`:
98
+ ```sh
99
+ ADAPTER=~/.cache/mlx-bun-finetunes/minicpm5-chunk-segmented \
100
+ PROMPT="Split into chunks and return JSON: ..." \
101
+ MAXTOK=256 bun scripts/run-adapter.ts
102
+ # omit ADAPTER for the base model; MODEL=<dir> to change base; SYSTEM=... TEMP=...
103
+ ```
104
+
105
+ **Score it on the chunk task:**
106
+ ```sh
107
+ ADAPTER=~/.cache/mlx-bun-finetunes/minicpm5-chunk-segmented bun scripts/chunk-eval.ts
108
+ ```
109
+
110
+ **Serve it** (you run the server; mount via the API):
111
+ ```sh
112
+ # 1) start: mlx-bun serve <model-dir>
113
+ # 2) mount: curl -X POST localhost:PORT/v1/adapters -d '{"id":"chunk","path":"<adapter-dir>"}'
114
+ # 3) list: curl localhost:PORT/v1/adapters # unmount: DELETE /v1/adapters/chunk
115
+ ```
116
+ The adapter activates per request through `loraState`; in code the pattern is
117
+ `new AdapterManager(model).mount(id, dir)` then `model.loraState.active = [id]`.
118
+
119
+ **Train a new adapter** (segmented):
120
+ ```sh
121
+ # MiniCPM5, seq 4096, segments of 4 layers:
122
+ MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0 \
123
+ SEQ=4096 ITERS=300 SEG=4 DATA=<data-dir> ADAPTER=<out-dir> \
124
+ bun scripts/chunk-finetune.ts
125
+ # e4b: add MODEL=<e4b-dir> and use SEG=2 (or 1) for ≥8K to keep peak down.
126
+ ```
127
+ Training REQUIRES `MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0` (those fused kernels
128
+ have no vjp). Use `MLX_BUN_TRAIN_ATTN=flash` for exact grads (slower).
129
+
130
+ ## What's next / open
131
+
132
+ 1. **e4b @8K toward ~10 GB** (currently 15.3 GB segSize=1): the §5 full-attention
133
+ ISOLATION `planSegments` — put each O(L²) full-attn layer (5,11,17,23,29,35,41)
134
+ in its own short segment. Mechanism + tuning knob are proven; this is peak-shaving.
135
+ 2. **Isolate the memory confound** (optional): run ours at segSize=1 with full-logits
136
+ CE to separate the response-only-CE saving from the streaming mechanism.
137
+ 3. **mlx-bun's own gradient checkpoint is ineffective** (23 GB @2048 vs mlx-lm's
138
+ 12.8) — orthogonal bug; segmented sidesteps it.
139
+ 4. Scale knobs: `segmentSize` is the peak↔grad-fidelity knob (finer = lower peak,
140
+ more donor-KV bf16 deviation).
141
+
142
+ ## Diagnostic scripts (kept)
143
+
144
+ `scripts/segmented-grad-test.ts` (MiniCPM5 grads + peak + leak loop),
145
+ `segmented-grad-test-e4b.ts` (e4b grads + peak; RANGES/SEG_ONLY/SEG_DUMP),
146
+ `parity-vs-mlxlm.ts` + `grad-parity-vs-mlxlm.ts` (mlx-lm parity; Python sides in
147
+ `/tmp/mlxlm-*.py`), `vag-leak-test.ts`, `seg-debug.ts`, `gemma4-runlayerrange-test.ts`,
148
+ `run-adapter.ts`, `chunk-{finetune,eval}.ts`.