mlx-bun 0.0.10 → 0.0.12

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 (216) hide show
  1. package/README.md +107 -32
  2. package/THIRD_PARTY_LICENSES.md +10 -0
  3. package/docs/archive/mlx-bun-lab-report.html +275 -0
  4. package/docs/archive/wwdc-mlx-bun.html +848 -0
  5. package/docs/design/audio-input-plan.md +356 -0
  6. package/docs/design/batching-perf-path.md +58 -35
  7. package/docs/design/batching-v2-plan.md +23 -9
  8. package/docs/design/bucketing-stage.md +2 -1
  9. package/docs/design/colibri-glm52-port.md +1479 -0
  10. package/docs/design/compat-cli-surface-design.md +4 -3
  11. package/docs/design/decode-speed-program.md +172 -0
  12. package/docs/design/docs-reorg-plan.md +1 -1
  13. package/docs/design/dspark-serving-program.md +371 -0
  14. package/docs/design/dspark-speculative-decoding.md +167 -5
  15. package/docs/design/faithful-l1-consolidation.md +310 -0
  16. package/docs/design/generic-model-support.md +8 -5
  17. package/docs/design/grammar-spec-batching-integration.md +301 -0
  18. package/docs/design/minicpm5-decode-megakernel.md +16 -11
  19. package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
  20. package/docs/design/omlx-adoption-map.md +14 -6
  21. package/docs/design/orpo-training.md +34 -30
  22. package/docs/design/paged-kv-cache.md +214 -0
  23. package/docs/design/parallel-slots.md +82 -49
  24. package/docs/design/parity-tier-dag.md +43 -33
  25. package/docs/design/pre-colibri-stabilization.md +329 -0
  26. package/docs/design/repo-cleanup-plan.md +189 -0
  27. package/docs/design/runtime-isolation.md +19 -3
  28. package/docs/design/segmented-backward-training.md +3 -2
  29. package/docs/design/ssd-kv-cold-tier.md +130 -0
  30. package/docs/design/structured-output.md +484 -0
  31. package/docs/design/the-dreaming-master-plan.md +2 -3
  32. package/docs/design/turboquant-kv.md +235 -0
  33. package/docs/design/unified-engine-frontier-plan.md +774 -0
  34. package/docs/design/web-chat-beat-matrix.md +315 -0
  35. package/docs/design/web-chat-redesign.md +1112 -0
  36. package/docs/investigations/colibri-oracle-pin.md +1284 -0
  37. package/docs/investigations/dspark-handoff.md +182 -16
  38. package/docs/investigations/indexshare-performance-spike.md +357 -0
  39. package/docs/investigations/lab-build-journal.md +2 -2
  40. package/docs/investigations/openwiki-evaluation.md +249 -0
  41. package/docs/planning/IDEAS.md +14 -0
  42. package/docs/planning/PRODUCT_ROADMAP.md +2 -3
  43. package/docs/planning/chat-ui-vision-memory-first.md +199 -0
  44. package/docs/planning/chat-ui-vision.md +50 -0
  45. package/docs/planning/memory-docs-and-dag-plan.md +15 -6
  46. package/docs/planning/release-notes-v0.0.11.md +239 -0
  47. package/docs/planning/release-notes-v0.0.12.md +80 -0
  48. package/docs/reference/cli.md +63 -18
  49. package/docs/reference/distribution.md +37 -6
  50. package/docs/reference/embedding.md +40 -16
  51. package/docs/reference/features-matrix.md +99 -0
  52. package/docs/reference/library-api.md +69 -20
  53. package/docs/reference/memory.md +26 -6
  54. package/docs/reference/models.md +60 -3
  55. package/docs/reference/orpo-quickstart.md +3 -4
  56. package/docs/reference/server-api.md +717 -24
  57. package/docs/reference/server-config.md +265 -129
  58. package/docs/reference/training.md +24 -16
  59. package/package.json +5 -3
  60. package/src/anthropic.ts +67 -44
  61. package/src/audio/conformer.ts +647 -0
  62. package/src/audio/decode.ts +133 -0
  63. package/src/audio/features.ts +202 -0
  64. package/src/audio/transcode.ts +67 -0
  65. package/src/chat-template.ts +155 -10
  66. package/src/cli.ts +483 -113
  67. package/src/config.ts +116 -24
  68. package/src/diffusion/diffusion-generate.ts +14 -8
  69. package/src/download.ts +143 -12
  70. package/src/eval/README.md +7 -6
  71. package/src/eval/ifeval.ts +63 -120
  72. package/src/eval/kl.ts +59 -3
  73. package/src/eval/quality-db.ts +2 -2
  74. package/src/eval/runner.ts +17 -8
  75. package/src/eval/tasks/ifeval.ts +167 -31
  76. package/src/expert-io.ts +391 -0
  77. package/src/expert-offload.ts +15 -2
  78. package/src/expert-residency.ts +1134 -0
  79. package/src/expert-usage.ts +415 -0
  80. package/src/flags.ts +13 -0
  81. package/src/generate.ts +438 -35
  82. package/src/grammar.ts +585 -0
  83. package/src/hf-push.ts +1 -1
  84. package/src/hub-rest.ts +242 -0
  85. package/src/index.ts +14 -2
  86. package/src/jobs/runner.ts +37 -18
  87. package/src/kv-store.ts +586 -93
  88. package/src/lora.ts +85 -14
  89. package/src/md-modules.d.ts +42 -0
  90. package/src/media-fetch.ts +426 -0
  91. package/src/memory/rest.ts +245 -0
  92. package/src/memory/vault.ts +74 -0
  93. package/src/mlx/array.ts +149 -19
  94. package/src/mlx/autograd.ts +53 -16
  95. package/src/mlx/checkpoint.ts +46 -13
  96. package/src/mlx/custom-vjp.ts +83 -35
  97. package/src/mlx/ffi.ts +27 -3
  98. package/src/mlx/metal-kernel.ts +7 -4
  99. package/src/mlx/ops.ts +153 -23
  100. package/src/mlx/turboquant-ops.ts +831 -0
  101. package/src/mlx/turboquant-tables.ts +237 -0
  102. package/src/model/batched-mask.ts +62 -1
  103. package/src/model/batched-quant.ts +206 -0
  104. package/src/model/batched-rotating-quant.ts +346 -0
  105. package/src/model/batched-rotating.ts +46 -1
  106. package/src/model/compiled-decode.ts +4 -15
  107. package/src/model/factory.ts +103 -6
  108. package/src/model/gemma4-base.ts +395 -39
  109. package/src/model/gemma4.ts +122 -33
  110. package/src/model/generated/gemma4-12b.ts +9 -13
  111. package/src/model/generated/gemma4-26b.ts +10 -5
  112. package/src/model/generated/gemma4-e4b.ts +14 -9
  113. package/src/model/glm52-atlas.ts +511 -0
  114. package/src/model/glm52-cache.ts +706 -0
  115. package/src/model/glm52-config.ts +262 -0
  116. package/src/model/glm52-container.ts +305 -0
  117. package/src/model/glm52-coupling.ts +388 -0
  118. package/src/model/glm52-dsa.ts +602 -0
  119. package/src/model/glm52-expert-layout.ts +253 -0
  120. package/src/model/glm52-layout.ts +152 -0
  121. package/src/model/glm52-memory.ts +473 -0
  122. package/src/model/glm52-mla.ts +786 -0
  123. package/src/model/glm52-moe.ts +223 -0
  124. package/src/model/glm52-pilot.ts +537 -0
  125. package/src/model/glm52-reference.ts +445 -0
  126. package/src/model/glm52-residency.ts +557 -0
  127. package/src/model/glm52-resident-weights.ts +135 -0
  128. package/src/model/glm52-streamed-experts.ts +453 -0
  129. package/src/model/glm52-streamed-metal.ts +610 -0
  130. package/src/model/glm52-weights.ts +503 -0
  131. package/src/model/glm52.ts +1153 -0
  132. package/src/model/minicpm5.ts +58 -102
  133. package/src/model/paged-kv.ts +253 -0
  134. package/src/model/qwen3-delta.ts +76 -15
  135. package/src/model/qwen3-moe.ts +404 -0
  136. package/src/model/qwen3.ts +9 -8
  137. package/src/model/qwen3_5.ts +88 -23
  138. package/src/model/support.ts +19 -1
  139. package/src/model/universal/dense.ts +40 -5
  140. package/src/model/universal/rope.ts +36 -21
  141. package/src/native/expert_io.c +523 -0
  142. package/src/native-pack.ts +6 -4
  143. package/src/pi-web.ts +1299 -82
  144. package/src/preflight.ts +27 -8
  145. package/src/prompt-cache.ts +293 -33
  146. package/src/quantize/quantizer.ts +16 -1
  147. package/src/registry.ts +57 -2
  148. package/src/responses.ts +62 -49
  149. package/src/serve/batch-scheduler.ts +1050 -78
  150. package/src/serve/generation-gateway.ts +252 -24
  151. package/src/serve/isolate.ts +417 -0
  152. package/src/serve/lane-registry.ts +44 -0
  153. package/src/serve/session-search.ts +326 -0
  154. package/src/server.ts +1701 -175
  155. package/src/spec/assistant-source.ts +150 -0
  156. package/src/spec/deepspec-source.ts +143 -0
  157. package/src/spec/dflash-source.ts +124 -0
  158. package/src/spec/dspark/ab-stats.ts +136 -0
  159. package/src/spec/dspark/calibration.ts +112 -0
  160. package/src/spec/dspark/deepspec-module.ts +801 -0
  161. package/src/spec/dspark/generate-dflash.ts +72 -20
  162. package/src/spec/dspark/loader.ts +23 -0
  163. package/src/spec/dspark/module-dflash.ts +278 -36
  164. package/src/spec/dspark/quantize-drafter.ts +91 -0
  165. package/src/spec/generate.ts +59 -25
  166. package/src/spec/glm52-mtp-source.ts +392 -0
  167. package/src/spec/ngram-source.ts +132 -0
  168. package/src/spec/serve-loop.ts +617 -0
  169. package/src/spec/source.ts +118 -0
  170. package/src/spec/two-model.ts +171 -0
  171. package/src/ssd-cache.ts +111 -35
  172. package/src/tokenizer.ts +288 -2
  173. package/src/tool-approvals.ts +131 -0
  174. package/src/tool-call.ts +438 -22
  175. package/src/train/flash-cce.ts +2 -2
  176. package/src/train/job.ts +0 -12
  177. package/src/train/lora-params.ts +17 -6
  178. package/src/train/trainer.ts +60 -30
  179. package/src/vision/prompt.ts +217 -48
  180. package/src/web/app.html +1197 -1791
  181. package/src/web/app.js +5694 -0
  182. package/src/web/icon.svg +13 -0
  183. package/src/web/manifest.webmanifest +24 -0
  184. package/src/web/src/adapters-panel.ts +288 -0
  185. package/src/web/src/api.ts +57 -0
  186. package/src/web/src/assistant.ts +485 -0
  187. package/src/web/src/chat.ts +1050 -0
  188. package/src/web/src/composer.ts +959 -0
  189. package/src/web/src/dataset.ts +132 -0
  190. package/src/web/src/finetune.ts +267 -0
  191. package/src/web/src/hub.ts +428 -0
  192. package/src/web/src/main.ts +98 -0
  193. package/src/web/src/markdown.ts +0 -0
  194. package/src/web/src/memory-panel.ts +572 -0
  195. package/src/web/src/model-picker.ts +159 -0
  196. package/src/web/src/palette.ts +346 -0
  197. package/src/web/src/protocol.ts +62 -0
  198. package/src/web/src/quantize.ts +167 -0
  199. package/src/web/src/rag.ts +274 -0
  200. package/src/web/src/sessions.ts +469 -0
  201. package/src/web/src/shell.ts +804 -0
  202. package/src/web/src/status.ts +300 -0
  203. package/src/web/src/ui-catalog.ts +92 -0
  204. package/src/web/sw.js +71 -0
  205. package/src/web/vendor/README.md +61 -0
  206. package/src/web/vendor/hljs-LICENSE +29 -0
  207. package/src/web/vendor/hljs-theme.css +59 -0
  208. package/src/web/vendor/hljs.js +26 -0
  209. package/src/web-tools.ts +54 -2
  210. package/src/weights.ts +82 -19
  211. package/src/model/fused-decode-kernel.ts +0 -312
  212. package/src/model/fused-geglu-kernel.ts +0 -198
  213. package/src/model/fused-mlp-kernel.ts +0 -169
  214. package/src/model/fused-swiglu-kernel.ts +0 -240
  215. package/src/model/steel-linear-kernel.ts +0 -96
  216. package/src/status-page.html +0 -497
package/README.md CHANGED
@@ -65,7 +65,8 @@ mlx-bun
65
65
 
66
66
  Self-contained (binary + MLX runtime) and notarized, so it runs without a
67
67
  Gatekeeper prompt. Installs to `~/.mlx-bun` by default — override with
68
- `MLX_BUN_INSTALL_DIR`, or pin a release with `MLX_BUN_VERSION=v0.0.8`.
68
+ `MLX_BUN_INSTALL_DIR`, or pin a release with `MLX_BUN_VERSION=<tag>`
69
+ (see [releases](https://github.com/joshuarossi/mlx-bun/releases)).
69
70
 
70
71
  ### Homebrew
71
72
 
@@ -89,8 +90,9 @@ nothing to install:
89
90
  bunx mlx-bun
90
91
  ```
91
92
 
92
- First run fetches the MLX native runtime (~52 MB) and the model into your
93
- caches, then serves. (Bun only — `npx mlx-bun` under Node exits with a
93
+ First run fetches the MLX native runtime plus the streamed-expert helper
94
+ (~52 MB) and the model into your caches, then serves. (Bun only — `npx
95
+ mlx-bun` under Node exits with a
94
96
  "requires Bun" notice by design.)
95
97
 
96
98
  ### From source
@@ -115,7 +117,8 @@ mlx-bun
115
117
  Bare `mlx-bun` is an alias for `mlx-bun serve`. On its first run, with no
116
118
  model named, it does everything for you:
117
119
 
118
- 1. pulls the MLX native runtime (~52 MB) into `~/Library/Caches/mlx-bun/`
120
+ 1. pulls the MLX native runtime plus streamed-expert helper (~52 MB) into
121
+ `~/Library/Caches/mlx-bun/`
119
122
  (skipped on the Homebrew install — the runtime ships in the bottle);
120
123
  2. downloads the sub-GB `MiniCPM5-1B` starter and serves it, so you're
121
124
  chatting in well under a minute;
@@ -144,7 +147,11 @@ curl http://localhost:8080/v1/chat/completions \
144
147
 
145
148
  That's it. The server serves the one model it was started with (the
146
149
  request's `model` field is ignored and the loaded model id is echoed
147
- back in responses).
150
+ back in responses). Run `serve --isolate --model-pool N` instead and
151
+ the parent proxy pools per-model engine children, routing each request
152
+ by its `model` field and spawning on demand — see
153
+ [docs/reference/server-config.md](./docs/reference/server-config.md)
154
+ and [docs/design/runtime-isolation.md](./docs/design/runtime-isolation.md).
148
155
 
149
156
  Longer walkthrough on the site:
150
157
  [Installation](https://mlx-bun.dev/getting-started/installation/) and
@@ -175,9 +182,11 @@ need to know those knobs exist.
175
182
 
176
183
  `mlx-bun memory init` creates a git-tracked Markdown wiki at
177
184
  `~/.mlx-bun/wiki`; `Reference/` is seeded with read-only symlinks to
178
- mlx-bun's own docs, and the built-in pi agents read both those docs and your
179
- articles quietly (`memory_search`, `memory_section`, etc.) and can open it in
180
- Obsidian for you (`mlx-bun memory open [article]`). Chat-time memory is
185
+ mlx-bun's own docs, and the built-in pi agents read both quietly through two
186
+ separate tool families — `reference_search`/`reference_read` for the seeded
187
+ Reference docs, `memory_search`/`memory_section`/`memory_read` for your own
188
+ articles — and can open it in Obsidian for you (`mlx-bun memory open
189
+ [article]`). Chat-time memory is
181
190
  read-only; synthesis is the separate writer path: `mlx-bun memory synthesize`
182
191
  runs the built-in local pipeline (segment → extract → route → create/patch →
183
192
  reconcile → link → wikify) that turns your conversations into cross-linked
@@ -214,9 +223,9 @@ On the site: [The lab](https://mlx-bun.dev/about/lab/).
214
223
 
215
224
  ## Supported models
216
225
 
217
- Scope is deliberate: a few model families held to **bit-exact** logit
218
- parity with the Python reference, rather than dozens held to none.
219
- Currently MiniCPM5, the Gemma-4 OptiQ quants, and Qwen3.5-4B:
226
+ Scope is deliberate: a few model families held to explicit oracle contracts,
227
+ rather than dozens held to none. Currently MiniCPM5, the Gemma-4 OptiQ quants,
228
+ Qwen3.5-4B, and the direct-container GLM-5.2 path:
220
229
 
221
230
  | Model | Download | Fits on | Vision | Notes |
222
231
  |---|---|---|---|---|
@@ -225,6 +234,7 @@ Currently MiniCPM5, the Gemma-4 OptiQ quants, and Qwen3.5-4B:
225
234
  | [`mlx-community/gemma-4-e4b-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-e4b-it-OptiQ-4bit) | 7.0 GB | 16 GB | ✓ | **Recommended starter** (16 GB+); ~56 tok/s |
226
235
  | [`mlx-community/gemma-4-12B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-12B-it-OptiQ-4bit) | 8.4 GB | 16 GB | ✓ | Vision + tool calling, both verified end-to-end |
227
236
  | [`mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit`](https://huggingface.co/mlx-community/gemma-4-26B-A4B-it-OptiQ-4bit) | 18 GB | 24 GB | — | MoE (top-8 of 128 experts); ~54 tok/s |
237
+ | [`mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp`](https://huggingface.co/mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp) | ~384 GB / 357 GiB | 32 GB + fast disk | — | Direct streamed Colibri container; true top-8, native MTP, bounded 25 GiB process plan; warm single-stream performance is disk-bound and below the aspirational 2 tok/s target |
228
238
 
229
239
  Not sure what fits your machine? `bun src/cli.ts fit <model> --ctx 8192`
230
240
  gives a deterministic answer (see below). The larger
@@ -233,6 +243,26 @@ remain (see [PLAN.md](./PLAN.md)). Downloading, cache layout, and reclaiming
233
243
  disk: [docs/reference/models.md](./docs/reference/models.md); on the site:
234
244
  [Choosing a model](https://mlx-bun.dev/getting-started/models/).
235
245
 
246
+ ### GLM-5.2: 32 GB quickstart
247
+
248
+ Choose a volume with at least the artifact remainder plus 1 GiB free before
249
+ starting; mlx-bun checks this before the first payload and credits resumable
250
+ partials. The validated artifact revision and exact 25 GiB serving command are
251
+ documented in the [model-management guide](./docs/reference/models.md#glm-52-on-a-32-gb-mac):
252
+
253
+ ```sh
254
+ export HF_HUB_CACHE=/Volumes/FastSSD/huggingface/hub # optional large fast volume
255
+ mlx-bun get mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp \
256
+ --revision 3cc8db99b1b13fc79325d987ba3c1c430766b3b8
257
+ mlx-bun scan
258
+ mlx-bun serve GLM-5.2-colibri --memory-budget 26.8435456 \
259
+ --context-length 4096 --max-tokens 128 --mtp on
260
+ ```
261
+
262
+ Rerun `get` after interruption; partial files resume and every completed blob
263
+ is checksum-verified. The model remains in the standard Hugging Face cache—no
264
+ Python environment and no second converted copy inside this repository.
265
+
236
266
  ## Why
237
267
 
238
268
  MLX is Apple's ML framework: hand-tuned Metal kernels for Apple Silicon,
@@ -271,6 +301,7 @@ bun src/cli.ts ls # list models (size, params, quant, capabili
271
301
  bun src/cli.ts ls --vision --max-size 10GB
272
302
  bun src/cli.ts fit gemma --ctx 32768 # memory contract: fits? max context? predicted tok/s
273
303
  bun src/cli.ts fit gemma --ctx 8192 --skus # ...same, across the Apple Silicon lineup
304
+ bun src/cli.ts fit GLM-5.2 # streamed plan: disk vs resident slabs + measured speed
274
305
  bun src/cli.ts serve gemma --port 8080 # OpenAI-compatible server
275
306
  bun src/cli.ts serve gemma --memory-budget 18 # ...with admission control (GB)
276
307
  bun src/cli.ts generate gemma "a haiku about metal shaders" # raw one-shot generation
@@ -292,7 +323,10 @@ listing the candidates — just make it more specific.
292
323
  headers, KV bytes/token from the config (sliding-window layers capped,
293
324
  MoE active-expert bytes for decode), calibrated prefill transient, and
294
325
  the machine's wired-memory ceiling. Predictions are recorded next to
295
- measured peaks in the eval DB.
326
+ measured peaks in the eval DB. The direct Colibri GLM-5.2 path uses its exact
327
+ streamed plan instead: full artifact-on-disk size is separated from bounded
328
+ resident weights, main/MTP expert slabs, KV, and reserves, and measured warm
329
+ decode is separated from the direct oracle and the 2 tok/s aspiration.
296
330
 
297
331
  Every verb, with flags: [docs/reference/cli.md](./docs/reference/cli.md);
298
332
  on the site: [CLI reference](https://mlx-bun.dev/reference/cli/).
@@ -321,9 +355,28 @@ agent CLIs like pi/OpenClaw via their provider config.
321
355
  (Gemma 4 `<|tool_call>` sentinel tokens; MiniCPM5 / Qwen3.5
322
356
  `<function name=…>` XML with schema-aware argument decoding);
323
357
  `role: "tool"` round-trips, including multi-turn agent loops.
358
+ The built-in `web_fetch` tool accepts public HTTP(S) destinations only,
359
+ revalidates DNS and every redirect, and streams into a 1 MiB response cap.
360
+ - **Structured output** — `response_format` (JSON mode / JSON schema),
361
+ plus the vLLM/oMLX aliases `guided_grammar`, `guided_regex`,
362
+ `guided_choice`, and `structured_outputs`, enforced by
363
+ grammar-constrained decoding (xgrammar) on chat and raw completions,
364
+ serial and batched lanes alike; a grammar that fails to compile
365
+ degrades to a string-valued constraint-specific prompt injection
366
+ (oMLX parity) instead of an error
367
+ (`MLX_BUN_GRAMMAR=0` disables mask compilation server-wide and routes
368
+ requested constraints through that same warning-bearing degrade path).
324
369
  - **Vision** — `image_url` content parts (data: URLs or http/s), on
325
370
  models with the vision sidecar. PNG, JPEG, HEIC, AVIF, WebP, TIFF,
326
- GIF, BMP via native OS codecs.
371
+ GIF, BMP via native OS codecs. Remote fetches are SSRF-guarded by
372
+ default (private/loopback/LAN destinations refused); opt out with
373
+ `--allow-private-media` / `MLX_BUN_ALLOW_PRIVATE_MEDIA=1`.
374
+ - **Audio input** — `input_audio` content parts (base64 WAV, plus
375
+ `audio` / `audio_url` aliases), on models whose sidecar ships the
376
+ audio tower (gemma-4 e4b). WAV decoded natively; mp3, m4a, FLAC, ogg,
377
+ AIFF via CoreAudio. Up to 30 s per clip; images and audio mix in one
378
+ message — a capability neither mlx-lm nor optiq serves. Same SSRF
379
+ guard and opt-out as `image_url` applies to `audio_url`.
327
380
  - **Prompt caching** — a byte-capped LRU KV cache reuses the longest
328
381
  common token prefix across requests (multi-turn conversations
329
382
  re-prefill only the new turn) — automatic, no client changes.
@@ -333,17 +386,34 @@ agent CLIs like pi/OpenClaw via their provider config.
333
386
  request with the `adapter` body field (`"id"`, stacked `"a+b"`, or
334
387
  `"none"`). The base model never reloads; an unselected adapter costs
335
388
  nothing.
336
- - **Mixed-precision KV** — when the model repo ships `kv_config.json`
337
- (every Gemma-4 OptiQ repo does), per-layer KV quantization applies
338
- automatically; the config sets bits per layer for both full-attention
339
- and sliding-window (rotating) layers (Phase 9). Measured decode-neutral
340
- at 8k on the 12B with KV bytes ÷4 on the quantized layers. `--kv-quant off` forces bf16; `--kv-quant 4|8`
341
- forces uniform bits. Long prefills over quantized caches run a fused
342
- FlashAttention-2 tiling that never materializes the full scores
343
- matrix (bounded transient; `MLX_BUN_NO_FUSED_SDPA=1` to disable).
344
- `MLX_BUN_FUSED_DECODE=1` extends the tiling to single-token decode —
345
- experimental, default off (measured ~4% slower on Gemma @8k kv8;
346
- kept for model families where it may win).
389
+ - **Mixed-precision KV** — opt in with `--kv-quant config` when the model
390
+ repo ships `kv_config.json` (every Gemma-4 OptiQ repo does): per-layer
391
+ KV quantization with bits set per layer for both full-attention and
392
+ sliding-window (rotating) layers (Phase 9). It trades decode speed
393
+ (measured 5–20% at ≤16k, on mlx-lm's uniform scheme too) for KV bytes
394
+ ÷4 on the quantized layers — reach for it when context, not speed, is
395
+ the constraint. The default KV cache is bf16 (the mlx-lm-parity L1
396
+ route); `--kv-quant 4|8` selects uniform bits instead, and
397
+ `--kv-quant turbo[:kXvY]` opts into TurboQuant rotation-based KV
398
+ (affine keys + FWHT/Lloyd-Max values, full-attention layers, serial
399
+ lane — [design](./docs/design/turboquant-kv.md)). Long prefills
400
+ over quantized caches run a fused FlashAttention-2 tiling that never
401
+ materializes the full scores matrix (bounded transient;
402
+ `MLX_BUN_NO_FUSED_SDPA=1` to disable).
403
+ - **Speculative decoding** — `serve --draft-model <path|query>`
404
+ (mlx_lm.server parity; `--num-draft-tokens`): the drafter KIND is
405
+ auto-detected from the artifact (`--draft-kind` overrides) — a full
406
+ smaller same-tokenizer model (mlx-lm parity), a Gemma `-assistant`
407
+ KV-borrowing drafter (optiq parity), a DeepSpec/DSpark
408
+ hidden-tapping drafter (e.g. DeepSeek's released
409
+ `dspark_gemma4_12b_block7`), or **model-free prompt-lookup drafting**
410
+ (`--draft-kind ngram`, no draft artifact at all — drafts are copied
411
+ from the request's own prompt/generation; free wins on echo-heavy
412
+ workloads like extraction and code edits, lossless at any
413
+ temperature). The main model verifies every draft —
414
+ exact results, faster decode when drafts land; pays on 12B-class
415
+ targets, not small fast models. Serial lane only — with `--batch N` a
416
+ mounted draft routes every request serial, like mlx_lm.server.
347
417
  - **Memory admission control** — `--memory-budget <GB>` refuses to load
348
418
  a model that can't serve within the budget and rejects requests whose
349
419
  `prompt + max_tokens` exceed the budget's max safe context with a 400
@@ -375,7 +445,7 @@ Guide with examples on the site:
375
445
  ## Library
376
446
 
377
447
  The server is one consumer of a library-first API. Published to npm as
378
- `mlx-bun` (current: 0.0.8) — import from the package (`from "mlx-bun"`)
448
+ `mlx-bun` — import from the package (`from "mlx-bun"`)
379
449
  or `./src/index` in a clone; `bunx mlx-bun` runs the CLI without
380
450
  installing. Full reference:
381
451
  [docs/reference/library-api.md](./docs/reference/library-api.md). For shipping inside a
@@ -385,13 +455,12 @@ in [docs/reference/embedding.md](./docs/reference/embedding.md).
385
455
 
386
456
  ```ts
387
457
  import {
388
- createModel, // dispatches to Gemma4 / MiniCPM5 / Qwen3.5
389
- Weights, loadModelConfig, loadTokenizer, ChatTemplate, generate,
458
+ openModel, // artifact-aware, incl. bounded streamed GLM-5.2
459
+ loadTokenizer, ChatTemplate, generate,
390
460
  } from "mlx-bun"; // or "./src/index" in a clone
391
461
 
392
462
  const dir = "/path/to/hf/snapshot";
393
- const config = await loadModelConfig(dir);
394
- const model = createModel(await Weights.open(dir), config); // RuntimeModel
463
+ const model = await openModel(dir); // RuntimeModel
395
464
  const tok = await loadTokenizer(dir);
396
465
  const template = await ChatTemplate.load(dir);
397
466
 
@@ -524,9 +593,15 @@ remain. MTP speculation and Qwen3-VL vision deferred.
524
593
  **Experimental** — opt-in, default-off, still being hardened: transparent
525
594
  expert offload for MoE models (`serve --expert-offload`, Phase 20:
526
595
  page-aligned mmap-backed experts, bit-exact — 26B-A4B 17.1→4.2 GB
527
- resident, decode unregressed); batched serving (`--batch N`, Phase 18:
528
- continuous-batching bf16 B=N decode, B=2 bit-parity verified for
529
- MiniCPM5/12B/e4b/26B; throughput polish remains).
596
+ resident, decode unregressed); batched serving (`--batch N`, default 8
597
+ since 2026-07-05 — a lone request runs the exact serial engine, so the cap
598
+ engages only under real concurrency, e.g. coding sub-agents; `--batch 1`
599
+ pins strict serial; Phase 18: continuous-batching bf16 decode at mlx-lm B=N parity,
600
+ B=2 bit-parity verified for MiniCPM5/12B/e4b/26B; sampler extras,
601
+ repetition penalty, and grammar-constrained requests batch; Qwen3.5's
602
+ SSM caches batch, as do plain full-attention Tier-0 archs (e.g. Llama) —
603
+ gemma2-family / sliding-window universal archs and DiffusionGemma route
604
+ serial).
530
605
 
531
606
  **Open** — e4b's ~5% per-step host-overhead decode residual (Phase 7);
532
607
  SigLIP vision for 26B.
@@ -21,6 +21,7 @@ the upstream algorithm and is a derivative work of the original.
21
21
  | [mlx-vlm](https://github.com/Blaizzy/mlx-vlm) (BSD-3-Clause) — Gemma-4 unified `VisionEmbedder`, `MultimodalEmbedder`, image preprocessing (as vendored in mlx-optiq) | BSD-3-Clause | `src/vision/embedder.ts`, `src/vision/preprocess.ts` |
22
22
  | mlx-optiq (MIT) — vision sidecar wiring, fused quantized SDPA orchestration | MIT | `src/vision/`, quantized-KV paths in `src/model/gemma4.ts` |
23
23
  | [Pillow](https://github.com/python-pillow/Pillow) (MIT-CMU / HPND) — `ImagingResample` bicubic convolution algorithm | HPND | `resizeBicubic` in `src/vision/preprocess.ts` (algorithm port) |
24
+ | [Colibri](https://github.com/JustVugg/colibri) (pinned development oracle `44e489b196c9b7876b3d37a0570ebf1c6f90f54c`) — direct-container layout, expert-residency/I/O policy, GLM-5.2 execution and Metal-kernel reference | Apache-2.0 | Native GLM-5.2 implementation under `src/model/glm52-*`, `src/expert-*`, and `src/native/expert_io.c`; rewritten for Bun + MLX, with direct Colibri retained only as an external oracle |
24
25
 
25
26
  ## Runtime npm dependencies
26
27
 
@@ -29,6 +30,7 @@ the upstream algorithm and is a derivative work of the original.
29
30
  | `@huggingface/tokenizers` | Apache-2.0 | Pure-JS tokenizer (tokenizer.json) |
30
31
  | `@huggingface/jinja` | MIT | Chat-template rendering |
31
32
  | `fast-png` | MIT | PNG decode (raw pixels for the vision path) |
33
+ | `@mlc-ai/web-xgrammar` | Apache-2.0 | Grammar-constrained decoding (WASM build of [xgrammar](https://github.com/mlc-ai/xgrammar) by the MLC-ai team). `response_format` / `guided_grammar` / `guided_regex` / `guided_choice` compile to a GrammarMatcher whose per-step token bitmask masks invalid logits (`src/grammar.ts`). Idea ported from oMLX (`api/grammar.py`), same engine, WASM-packaged. |
32
34
 
33
35
  Run `bun pm licenses` (or inspect `node_modules/*/LICENSE`) for the full
34
36
  text of each npm dependency's license, including transitive ones.
@@ -39,3 +41,11 @@ Model weights (e.g. `mlx-community/gemma-4-12B-it-OptiQ-4bit`) are not
39
41
  part of this repository and are governed by their own licenses and use
40
42
  policies (e.g. Google's Gemma Terms of Use). Check the model card on
41
43
  Hugging Face before redistribution.
44
+
45
+ The validated GLM-5.2 direct-container artifact,
46
+ [`mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp`](https://huggingface.co/mateogrgic/GLM-5.2-colibri-int4-with-int8-mtp),
47
+ declares MIT and identifies itself as a Colibri conversion/derivative of
48
+ [`zai-org/GLM-5.2-FP8`](https://huggingface.co/zai-org/GLM-5.2-FP8), with
49
+ lineage through `jlnsrk/GLM-5.2-colibri-int4` and added int8 MTP weights. The
50
+ artifact is downloaded separately into the Hugging Face cache and is not part
51
+ of the mlx-bun distribution.
@@ -0,0 +1,275 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>mlx-bun Lab — build report</title>
7
+ <style>
8
+ :root{
9
+ --bg:#000; --ink:#f5f5f7; --dim:#a1a1a6; --dimmer:#6e6e73;
10
+ --hairline:rgba(255,255,255,.13); --card:rgba(255,255,255,.045);
11
+ --card2:rgba(255,255,255,.07);
12
+ --blue:#0a84ff; --cyan:#64d2ff; --indigo:#5e5ce6; --purple:#bf5af2;
13
+ --pink:#ff375f; --orange:#ff9f0a; --yellow:#ffd60a; --green:#30d158; --red:#ff453a;
14
+ --ease:cubic-bezier(.22,.61,.21,1);
15
+ --mono:ui-monospace,"SF Mono",Menlo,monospace;
16
+ }
17
+ *{box-sizing:border-box;margin:0;padding:0}
18
+ html{scroll-behavior:smooth}
19
+ body{background:var(--bg);color:var(--ink);
20
+ font:16px/1.65 -apple-system,"SF Pro Text",system-ui,sans-serif;
21
+ -webkit-font-smoothing:antialiased;overflow-x:hidden}
22
+ .bloom{position:fixed;border-radius:50%;filter:blur(120px);opacity:.5;z-index:-1;pointer-events:none}
23
+ .b1{width:560px;height:560px;background:radial-gradient(circle,rgba(94,92,230,.5),transparent 70%);top:-160px;left:-120px}
24
+ .b2{width:620px;height:620px;background:radial-gradient(circle,rgba(255,55,95,.32),transparent 70%);top:380px;right:-200px}
25
+ .b3{width:680px;height:680px;background:radial-gradient(circle,rgba(48,209,88,.22),transparent 70%);top:1500px;left:-220px}
26
+ .wrap{max-width:1000px;margin:0 auto;padding:0 24px}
27
+ .grad{background-clip:text;-webkit-background-clip:text;color:transparent}
28
+ .g-hero{background-image:linear-gradient(96deg,var(--orange),var(--pink) 34%,var(--purple) 64%,var(--blue))}
29
+ .g-green{background-image:linear-gradient(96deg,var(--green),var(--cyan))}
30
+ .g-pink{background-image:linear-gradient(96deg,var(--pink),var(--purple))}
31
+ .g-blue{background-image:linear-gradient(96deg,var(--cyan),var(--blue))}
32
+
33
+ header.hero{padding:120px 0 70px;text-align:center}
34
+ .eyebrow{font:600 13px var(--mono);letter-spacing:.18em;text-transform:uppercase;color:var(--dim)}
35
+ h1{font:800 clamp(40px,7vw,76px)/1.04 "SF Pro Display",-apple-system,sans-serif;letter-spacing:-.03em;margin:18px 0}
36
+ .lede{font-size:clamp(17px,2.2vw,21px);color:var(--dim);max-width:720px;margin:0 auto;line-height:1.5}
37
+ .meta{margin-top:26px;font:13px var(--mono);color:var(--dimmer)}
38
+
39
+ section{padding:54px 0;border-top:1px solid var(--hairline)}
40
+ h2{font:700 clamp(26px,3.6vw,38px)/1.1 "SF Pro Display",sans-serif;letter-spacing:-.02em;margin-bottom:8px}
41
+ h2 + .sub{color:var(--dim);margin-bottom:30px;font-size:16px;max-width:760px}
42
+ h3{font:650 19px "SF Pro Display",sans-serif;letter-spacing:-.01em;margin-bottom:6px}
43
+
44
+ .stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:38px}
45
+ .stat{background:var(--card);border:1px solid var(--hairline);border-radius:16px;padding:20px}
46
+ .stat .n{font:800 30px "SF Pro Display",sans-serif;letter-spacing:-.02em}
47
+ .stat .l{font-size:12.5px;color:var(--dim);margin-top:3px;line-height:1.35}
48
+
49
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px}
50
+ .card{background:var(--card);border:1px solid var(--hairline);border-radius:18px;padding:24px;transition:border-color .3s var(--ease),transform .3s var(--ease)}
51
+ .card:hover{border-color:rgba(255,255,255,.28);transform:translateY(-2px)}
52
+ .card .ic{font-size:22px;margin-bottom:12px}
53
+ .card p{color:var(--dim);font-size:14.5px;margin-top:6px}
54
+ .tag{display:inline-block;font:600 11px var(--mono);color:var(--green);background:rgba(48,209,88,.12);
55
+ border:1px solid rgba(48,209,88,.3);border-radius:999px;padding:2px 9px;margin-top:12px}
56
+ .tag.warn{color:var(--orange);background:rgba(255,159,10,.12);border-color:rgba(255,159,10,.3)}
57
+
58
+ .flow{font:13px var(--mono);color:var(--dim);background:#0b0b0d;border:1px solid var(--hairline);
59
+ border-radius:16px;padding:22px;white-space:pre;overflow-x:auto;line-height:1.6}
60
+ .flow b{color:var(--ink)} .flow .k{color:var(--cyan)} .flow .g{color:var(--green)} .flow .p{color:var(--purple)}
61
+
62
+ table{width:100%;border-collapse:collapse;font-size:14px;margin-top:6px}
63
+ th,td{text-align:left;padding:11px 12px;border-bottom:1px solid var(--hairline);vertical-align:top}
64
+ th{font:600 12px var(--mono);letter-spacing:.04em;text-transform:uppercase;color:var(--dim)}
65
+ td code,.mono{font:12.5px var(--mono);color:var(--cyan)}
66
+ td .ok{color:var(--green);font-weight:600}
67
+ tr:last-child td{border-bottom:none}
68
+
69
+ .showcase{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:20px}
70
+ .term{background:#0b0b0d;border:1px solid var(--hairline);border-radius:16px;overflow:hidden}
71
+ .term .bar{display:flex;gap:7px;padding:12px 16px;border-bottom:1px solid var(--hairline);align-items:center}
72
+ .term .bar i{width:11px;height:11px;border-radius:50%;display:inline-block}
73
+ .term .bar .t{margin-left:8px;font:12px var(--mono);color:var(--dimmer)}
74
+ .term .body{padding:18px;font:13.5px/1.6 var(--mono);color:var(--ink);white-space:pre-wrap;word-break:break-word;min-height:96px}
75
+ .lbl{font:600 12px var(--mono);letter-spacing:.05em;text-transform:uppercase;margin-bottom:8px}
76
+
77
+ .loss{background:var(--card);border:1px solid var(--hairline);border-radius:16px;padding:22px;margin-top:18px}
78
+ ul.j{list-style:none;margin-top:8px}
79
+ ul.j li{padding:14px 0;border-bottom:1px solid var(--hairline);display:flex;gap:14px}
80
+ ul.j li:last-child{border-bottom:none}
81
+ ul.j .b{flex:0 0 auto;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;font-size:15px;
82
+ background:var(--card2);border:1px solid var(--hairline)}
83
+ ul.j .c h4{font:650 15.5px "SF Pro Display",sans-serif;margin-bottom:3px}
84
+ ul.j .c p{color:var(--dim);font-size:14px}
85
+ ul.j .c code{font:12px var(--mono);color:var(--cyan)}
86
+ .note{color:var(--dimmer);font-size:13px;margin-top:18px;line-height:1.55}
87
+ code.cmd{display:block;background:#0b0b0d;border:1px solid var(--hairline);border-radius:12px;
88
+ padding:14px 16px;font:13px var(--mono);color:var(--green);margin-top:10px;overflow-x:auto;white-space:pre}
89
+ footer{padding:60px 0 90px;text-align:center;color:var(--dimmer);font-size:13px;border-top:1px solid var(--hairline)}
90
+ @media(max-width:760px){.stats{grid-template-columns:repeat(2,1fr)}.showcase{grid-template-columns:1fr}}
91
+ </style>
92
+ </head>
93
+ <body>
94
+ <div class="bloom b1"></div><div class="bloom b2"></div><div class="bloom b3"></div>
95
+
96
+ <header class="hero"><div class="wrap">
97
+ <div class="eyebrow">build report · branch web-ui-and-native-lab</div>
98
+ <h1>mlx-bun <span class="grad g-hero">Lab</span></h1>
99
+ <p class="lede">A complete, beautiful web workbench for local AI on Apple silicon —
100
+ a full pi agent in the browser, plus <b>native</b> Quantize, Fine-tune, and
101
+ Build-Dataset. No Python anywhere. Every engine is mlx-c through bun:ffi.</p>
102
+ <p class="meta">M4 Pro · 24 GB · MiniCPM5-1B-OptiQ-4bit · 2026-06-14</p>
103
+ <div class="stats">
104
+ <div class="stat"><div class="n grad g-blue">5</div><div class="l">unified sections: Chat · Quantize · Fine-tune · Dataset · Status</div></div>
105
+ <div class="stat"><div class="n grad g-green">0</div><div class="l">lines of Python — native quantize &amp; LoRA training via mlx-c</div></div>
106
+ <div class="stat"><div class="n grad g-pink">150+</div><div class="l">new tests, all green (incl. real-model e2e)</div></div>
107
+ <div class="stat"><div class="n grad g-hero">50+</div><div class="l">new source files across 11 subsystems</div></div>
108
+ </div>
109
+ </div></header>
110
+
111
+ <section><div class="wrap">
112
+ <h2>What shipped</h2>
113
+ <p class="sub">One self-contained, keynote-styled SPA served from the same binary, backed by four new engines and a shared job system. The status page is kept and folded in.</p>
114
+ <div class="cards">
115
+ <div class="card"><div class="ic">💬</div><h3>Chat</h3><p>The full pi coding-agent embedded server-side, streamed to the browser over WebSocket: live tokens, tool calls (read/bash/edit/write/grep/find/ls), steering, and a real pre-execution approval gate for the dangerous tools.</p><span class="tag">pi SDK · verified streaming + tools</span></div>
116
+ <div class="card"><div class="ic">🧮</div><h3>Quantize</h3><p>Reads a model, quantizes every eligible Linear + embedding via <span class="mono">mlx_quantize</span>, writes a real HF-layout 4/8-bit model dir with <span class="mono">mlx_save_safetensors</span> the existing loader serves unchanged.</p><span class="tag">native · 170 modules → 4.50 bpw</span></div>
117
+ <div class="card"><div class="ic">🎛️</div><h3>Fine-tune</h3><p>Native LoRA + DPO. <span class="mono">mlx_value_and_grad</span> driven by a bun:ffi closure, hand-rolled AdamW, masked cross-entropy. Saves an adapter the server hot-mounts.</p><span class="tag">native · loss 4.48 → 0.68</span></div>
118
+ <div class="card"><div class="ic">📦</div><h3>Build Dataset</h3><p>All 13 OptIQ-Lab templates ported to TypeScript — pure-data ones run locally, LLM-driven ones call the server's own loopback. Splits to train/valid JSONL.</p><span class="tag">13 templates · 90/10 split</span></div>
119
+ <div class="card"><div class="ic">📊</div><h3>Status</h3><p>The existing keynote dashboard, kept and integrated: live memory meters, prompt-cache hit rate, KV-quant split, the fit solver + Apple-silicon SKU matrix, downloads.</p><span class="tag">kept &amp; unified</span></div>
120
+ <div class="card"><div class="ic">⚙️</div><h3>Job system</h3><p>Shared SQLite-backed jobs with NDJSON-log SSE streaming. GPU jobs run as crash-isolated subprocesses (uncatchable Metal OOM can't take the server down); dataset jobs run in-process.</p><span class="tag">SSE · crash-isolated</span></div>
121
+ </div>
122
+ </div></section>
123
+
124
+ <section><div class="wrap">
125
+ <h2>Architecture</h2>
126
+ <p class="sub">Five sections over one <span class="mono">Bun.serve</span>. The chat WebSocket and the dataset LLM templates both ride the server's own loopback <span class="mono">/v1</span>, so everything serializes through the single generation queue — one GPU, no second path.</p>
127
+ <div class="flow"><b>src/web/app.html</b> <span class="k">── unified SPA (hash routes #/chat #/quantize #/finetune #/dataset #/status)</span>
128
+ │
129
+ ▼
130
+ <b>src/server.ts</b> <span class="k">── Bun.serve({ fetch, websocket })</span>
131
+ ├─ <span class="g">/ws/chat</span> .............. src/pi-web.ts <span class="k">→ embedded pi AgentSession → loopback /v1</span>
132
+ ├─ <span class="g">/api/dataset/*</span> ........ src/dataset/* <span class="k">→ jobs (in-process)</span>
133
+ ├─ <span class="g">/api/quantize/*</span> ....... src/quantize/* <span class="k">→ jobs (GPU subprocess)</span>
134
+ ├─ <span class="g">/api/finetune/*</span> ....... src/train/* <span class="k">→ jobs (GPU subprocess)</span>
135
+ ├─ <span class="g">/api/jobs/:id[/stream]</span> .. src/jobs/* <span class="k">→ SQLite rows + NDJSON-log SSE</span>
136
+ └─ <span class="p">/v1/* · /stats · /fit · /library</span> <span class="k">(unchanged)</span>
137
+ │
138
+ ▼
139
+ <b>src/mlx/{ffi,ops,autograd}.ts</b> <span class="k">→ mlx-c via bun:ffi</span>
140
+ <span class="k">mlx_quantize · mlx_save_safetensors · mlx_value_and_grad</span></div>
141
+ </div></section>
142
+
143
+ <section><div class="wrap">
144
+ <h2>The headline: <span class="grad g-green">native, no Python</span></h2>
145
+ <p class="sub">mlx-bun's thesis is that the buggy layer (loading, tokenizing, the training loop, serving) doesn't need Python. The two hardest pieces — weight quantization and gradient training — are now native, proven on a real model.</p>
146
+
147
+ <h3 style="margin-top:8px">It actually trains</h3>
148
+ <p style="color:var(--dim);font-size:14.5px;max-width:780px">A 20-iteration LoRA on a 3-row "always answer in UPPERCASE" fixture, run entirely in Bun/MLX. The base model and the freshly-trained adapter, on a held-out prompt — <i>"Tell me about your day."</i>:</p>
149
+ <div class="showcase">
150
+ <div><div class="lbl" style="color:var(--dimmer)">base model (adapter off)</div>
151
+ <div class="term"><div class="bar"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i><span class="t">before</span></div>
152
+ <div class="body" id="before"></div></div></div>
153
+ <div><div class="lbl grad g-green">after 20 native LoRA steps</div>
154
+ <div class="term" style="border-color:rgba(48,209,88,.4)"><div class="bar"><i style="background:#ff5f57"></i><i style="background:#febc2e"></i><i style="background:#28c840"></i><span class="t">after</span></div>
155
+ <div class="body grad g-green" id="after" style="font-weight:600"></div></div></div>
156
+ </div>
157
+
158
+ <div class="loss">
159
+ <div style="display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:10px">
160
+ <div class="lbl grad g-blue">training loss · 20 iters</div>
161
+ <div class="mono" style="color:var(--dim)">4.48 → 0.68 · 4.6 s · 168 adapters mounted</div>
162
+ </div>
163
+ <svg id="spark" viewBox="0 0 900 160" preserveAspectRatio="none" style="width:100%;height:160px;margin-top:12px"></svg>
164
+ </div>
165
+
166
+ <div class="cards" style="margin-top:22px">
167
+ <div class="card"><div class="ic">🧮</div><h3>Quantize, measured</h3>
168
+ <p>Re-quantized MiniCPM5-1B to uniform 4-bit g64: <b>170 modules</b> → <b>4.50 bits/weight</b>, a <b>0.62 GB</b> model dir written in <b>7.0 s</b>, reopened bit-correctly through the existing loader. Stages: resolving → quantizing → writing.</p>
169
+ <span class="tag">mlx_quantize + mlx_save_safetensors</span></div>
170
+ <div class="card"><div class="ic">🔬</div><h3>The autograd spike</h3>
171
+ <p>Before writing any trainer, a feasibility gate proved <span class="mono">mlx_value_and_grad</span> driven by a <span class="mono">bun:ffi</span> JSCallback closure returns correct gradients (finite-difference &lt; 1e-2) for the LoRA A/B leaves, with the quantized base frozen.</p>
172
+ <span class="tag">spikes/phase-train-vag.ts · PASS</span></div>
173
+ </div>
174
+ </div></section>
175
+
176
+ <section><div class="wrap">
177
+ <h2>Round 2 — the leftover seams, <span class="grad g-pink">ported 1:1</span></h2>
178
+ <p class="sub">The five "designed-but-not-built" items, finished as faithful ports of the OptIQ / mlx-lm source (open book) — built in parallel on non-overlapping files, then integrated.</p>
179
+ <div class="cards">
180
+ <div class="card"><div class="ic">🎚️</div><h3>Mixed-precision quantization</h3><p>OptIQ's sensitivity + greedy-knapsack allocator ported <b>1:1</b> — per-layer bit choices are <b>bit-identical to OptIQ</b> on shared inputs (checked against the Python in the venv). Per-layer KL sensitivity runs on our own graph.</p><span class="tag">170 modules → 80×4-bit + 90×8-bit · 5.19 bpw · 32 s</span></div>
181
+ <div class="card"><div class="ic">📚</div><h3>Batched (B&gt;1) training</h3><p>Ragged padding + a per-row attention mask through the forward; B=1 stays bit-identical (separate path). Verified: a batched row's loss equals its single-row forward (shape-held) within bf16 noise.</p><span class="tag">B=2 loss↓ + masking parity ✓</span></div>
182
+ <div class="card"><div class="ic">🤗</div><h3>hf_dataset_import</h3><p>The 13th template — pulls any HF dataset via the datasets-server REST API (config / split / column / label-filter / min-chars / cap), no Python <span class="mono">datasets</span> lib.</p><span class="tag">13/13 templates real</span></div>
183
+ <div class="card"><div class="ic">⬆️</div><h3>HF push-to-hub</h3><p>Native Hub upload — create repo → LFS preupload → S3 PUT → NDJSON commit — for model, adapter, and dataset repos. Wired into all three wizards + an HF-token settings gear.</p><span class="tag">11 protocol tests ✓ (mock; no live push)</span></div>
184
+ <div class="card"><div class="ic">📦</div><h3>Single-binary web chat</h3><p>pi's JS is already bundled by <span class="mono">bun build --compile</span>; only the photon wasm is sidecarred. The compiled binary's headless pi session was verified to construct under the binary.</p><span class="tag">+1.8 MB sidecar</span></div>
185
+ </div>
186
+ <p class="note">The web UI now exposes all of it: a Uniform/Mixed quantize toggle with a target-bpw slider, live merge/export/push on the fine-tune done step, push on quantize &amp; dataset, and a Hugging-Face token settings gear in the nav.</p>
187
+ </div></section>
188
+
189
+ <section><div class="wrap">
190
+ <h2>Verification</h2>
191
+ <p class="sub">No browser screenshots (per request) — proof is the test suite. Integration tests spin a server up <i>inside the test process</i> and exit; the two real-model runs were explicitly approved.</p>
192
+ <table>
193
+ <thead><tr><th>area</th><th>evidence</th><th>result</th></tr></thead>
194
+ <tbody>
195
+ <tr><td>Web UI + routes</td><td><code>tests/lab-routes.test.ts</code> — SPA served, dataset build → JSONL, jobs SSE, quantize inspect, <b>real pi chat turn over WebSocket</b></td><td class="ok">7/7 ✓</td></tr>
196
+ <tr><td>Chat (pi embed)</td><td>WS turn streamed 25+ text deltas, executed a tool, completed; <code>tests/pi-web.test.ts</code> event mapping</td><td class="ok">9/9 ✓</td></tr>
197
+ <tr><td>Native quantize</td><td><code>MLX_BUN_TEST_QUANTIZE=1</code> e2e — real 4-bit model written + reopened; + 5 unit</td><td class="ok">6/6 ✓</td></tr>
198
+ <tr><td>Native train</td><td><code>MLX_BUN_TEST_TRAIN=1</code> e2e — loss↓, adapter mounts, output changes; + 19 fast (autograd FD, AdamW, dataset)</td><td class="ok">20/20 ✓</td></tr>
199
+ <tr><td>Dataset engine</td><td><code>tests/dataset.test.ts</code> — generators, CSV parse, 90/10 split, 13 templates</td><td class="ok">32/32 ✓</td></tr>
200
+ <tr><td>Job system</td><td><code>tests/jobs.test.ts</code> — lifecycle, GPU lease, <b>subprocess crash isolation</b>, zombie recovery, SSE</td><td class="ok">12/12 ✓</td></tr>
201
+ <tr><td>No regressions</td><td><code>tests/server.test.ts</code> — existing OpenAI/Anthropic/admission surface after the integration</td><td class="ok">17/17 ✓</td></tr>
202
+ <tr><td>Types</td><td><code>bunx tsc --noEmit</code> across the whole project</td><td class="ok">clean ✓</td></tr>
203
+ </tbody>
204
+ </table>
205
+ </div></section>
206
+
207
+ <section><div class="wrap">
208
+ <h2>Open-book ports</h2>
209
+ <p class="sub">Every engine follows a readable source — the OptIQ-Lab Python in the installed venv and mlx-lm — rather than being invented.</p>
210
+ <table>
211
+ <thead><tr><th>our module</th><th>ported from</th></tr></thead>
212
+ <tbody>
213
+ <tr><td><code>src/dataset/*</code></td><td><code>optiq/lab/dataset_templates.py</code> — 13 templates, field-for-field</td></tr>
214
+ <tr><td><code>src/train/{trainer,loss,dataset}.ts</code></td><td><code>optiq/lora/{trainer,dpo,config}.py</code> + mlx-lm <code>default_loss</code> / <code>iterate_batches</code></td></tr>
215
+ <tr><td><code>src/quantize/*</code></td><td>mlx-lm <code>convert.py</code> rules + the on-disk OptIQ model format (config block, sharding, eligibility)</td></tr>
216
+ <tr><td><code>src/jobs/*</code></td><td><code>optiq/lab/{jobs,db}.py</code> — job lifecycle + SSE event grammar</td></tr>
217
+ <tr><td><code>src/pi-web.ts</code></td><td>pi SDK (<code>@earendil-works/pi-coding-agent</code>) <code>examples/sdk/12-full-control.ts</code></td></tr>
218
+ <tr><td><code>src/web/app.html</code></td><td>OptIQ-Lab wizard UX + mlx-bun's own <code>status-page.html</code> design system</td></tr>
219
+ </tbody>
220
+ </table>
221
+ </div></section>
222
+
223
+ <section><div class="wrap">
224
+ <h2>Build journal</h2>
225
+ <p class="sub">How it was built, the decisions, and the bugs found along the way.</p>
226
+ <ul class="j">
227
+ <li><div class="b">🔭</div><div class="c"><h4>Plan first, fork the big decision to the user</h4><p>Surfaced the one decision that changed everything — native compute vs. orchestrating the installed Python vs. proxying — and you chose <b>native, no Python</b>. Full pi agent for chat; real tiny runs for verification.</p></div></li>
228
+ <li><div class="b">🔬</div><div class="c"><h4>Gated the riskiest unknown before building</h4><p>Native training needs autograd through <span class="mono">bun:ffi</span>. A feasibility spike proved <code>mlx_value_and_grad</code> + a JSCallback closure returns correct grads (finite-diff &lt; 1e-2) before a line of trainer code — de-risking the whole subsystem.</p></div></li>
229
+ <li><div class="b">⚡</div><div class="c"><h4>Parallel sub-agents on non-overlapping files</h4><p>After laying a shared job contract (<code>src/jobs/types.ts</code>), fanned out six subsystems concurrently — jobs, dataset, quantizer, trainer, web SPA, pi embed — each owning its own directory, then integrated <code>server.ts</code>/<code>cli.ts</code> centrally to avoid conflicts.</p></div></li>
230
+ <li><div class="b">🧱</div><div class="c"><h4>Found: <code>mlx_save_safetensors</code> exists</h4><p>The quantizer needed a safetensors writer; reading the mlx-c header (open book) showed a native writer already exists — no hand-rolled byte layout, eliminating a whole class of dtype/alignment bugs.</p></div></li>
231
+ <li><div class="b">🛡️</div><div class="c"><h4>Crash isolation by design</h4><p>Async Metal OOM is uncatchable (it std::terminates the process). So GPU jobs run as child <code>mlx-bun</code> subprocesses with a kernel fault boundary; a job that OOMs dies alone and the server survives. Verified with a deliberate-crash test.</p></div></li>
232
+ <li><div class="b">🐛</div><div class="c"><h4>Bug: LoRA init key shape</h4><p>The first real training run failed — <code>random_normal</code> got a <code>[1,2]</code> key but mlx wants <code>[2]</code>. Reshaped the per-module subkey after slicing the split-keys array. Loss curve immediately worked.</p></div></li>
233
+ <li><div class="b">🐛</div><div class="c"><h4>Bug: flaky chat test</h4><p>The pi agent sometimes takes a tool-call turn <i>before</i> producing text; the test resolved on the first <code>turn_end</code> and saw no tokens. Fixed to settle on the first turn that actually streams text — the embed was always correct.</p></div></li>
234
+ <li><div class="b">🔁</div><div class="c"><h4>Test, fix, re-run</h4><p>Each real-model e2e surfaced a small integration gap (a path not resolved, a key shape) — fixed at the source and re-run green, rather than worked around in the test.</p></div></li>
235
+ </ul>
236
+ </div></section>
237
+
238
+ <section><div class="wrap">
239
+ <h2>Run it live</h2>
240
+ <p class="sub">Screenshots were skipped by request; here's how to see it yourself. The unified app is served from the same binary.</p>
241
+ <code class="cmd">bun src/cli.ts serve cpm <span style="color:var(--dimmer)"># serve MiniCPM5-1B; app at http://localhost:8090/</span>
242
+ <span style="color:var(--dimmer)"># then open http://localhost:8090/ → Chat · Quantize · Fine-tune · Dataset · Status</span></code>
243
+ <p class="note">Re-run the native engines headless (no server):</p>
244
+ <code class="cmd">bun scripts/verify-lab.ts <span style="color:var(--dimmer)"># quantize + train + dataset, prints evidence</span>
245
+ MLX_BUN_TEST_QUANTIZE=1 bun test tests/quantize.test.ts
246
+ MLX_BUN_TEST_TRAIN=1 bun test tests/train-e2e.test.ts</code>
247
+ <p class="note">Everything lives on branch <span class="mono">web-ui-and-native-lab</span>. The OptIQ sensitivity+knapsack mixed-precision path, DPO at scale, batched (B&gt;1) training, and HF push-to-hub are designed seams left for follow-ups.</p>
248
+ </div></section>
249
+
250
+ <footer><div class="wrap">mlx-bun Lab · native MLX inference, quantization &amp; training for Bun on Apple silicon · no Python</div></footer>
251
+
252
+ <script>
253
+ const before = "<think>\nOkay, the user is asking about my day. Hmm, I need to clarify my nature first. I";
254
+ const after = "MY DAY AHEAD.\nAPPROVE AND PROPERLY END THE DAY.\nAPPROVE AND";
255
+ document.getElementById("before").textContent = before;
256
+ document.getElementById("after").textContent = after;
257
+
258
+ // loss sparkline
259
+ const loss = [4.4836,2.7138,2.5195,1.8625,0.8894,4.877,2.2799,1.1992,1.1923,1.4514,1.9102,0.6184,0.9046,0.6076,0.601,0.4917,0.3558,2.4688,0.25,0.6766];
260
+ const W=900,H=160,pad=14, max=Math.max(...loss), min=Math.min(...loss);
261
+ const x=i=>pad+i*(W-2*pad)/(loss.length-1);
262
+ const y=v=>pad+(1-(v-min)/(max-min))*(H-2*pad);
263
+ const pts=loss.map((v,i)=>`${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
264
+ const area=`M ${x(0)},${H-pad} L ${pts.split(" ").join(" L ")} L ${x(loss.length-1)},${H-pad} Z`;
265
+ const svg=document.getElementById("spark");
266
+ svg.innerHTML=`
267
+ <defs><linearGradient id="lg" x1="0" y1="0" x2="0" y2="1">
268
+ <stop offset="0" stop-color="rgba(100,210,255,.35)"/><stop offset="1" stop-color="rgba(100,210,255,0)"/>
269
+ </linearGradient></defs>
270
+ <path d="${area}" fill="url(#lg)"/>
271
+ <polyline points="${pts}" fill="none" stroke="#64d2ff" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
272
+ ${loss.map((v,i)=>`<circle cx="${x(i).toFixed(1)}" cy="${y(v).toFixed(1)}" r="2.6" fill="${i===loss.length-1?'#30d158':'#64d2ff'}"/>`).join("")}`;
273
+ </script>
274
+ </body>
275
+ </html>