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,155 @@
1
+ # Engineering Journal
2
+
3
+ Ongoing, chronological record of AI/agent decisions, investigations, and
4
+ corrections — newest work appended under a dated heading. Purpose: bring
5
+ a fresh agent up to speed on *why* the code is the way it is. PLAN.md is
6
+ the what/when (phases, exit criteria); this is the narrative behind it.
7
+
8
+ ## 2026-06-13 — MiniCPM5 starter port (branch `codex-minicpm5-starter-port`)
9
+
10
+ - Created a dedicated branch for the MiniCPM5 starter-model port.
11
+ - Attempted `codex/minicpm5-starter-port`, but git ref creation was blocked by the sandbox/ref layout.
12
+ - Used `codex-minicpm5-starter-port`.
13
+ - Corrected the execution order per Josh's direction:
14
+ - Goldens first.
15
+ - Build the port second.
16
+ - Compare implementation against the goldens continuously.
17
+ - Downloaded `mlx-community/MiniCPM5-1B-OptiQ-4bit`.
18
+ - Snapshot: `~/.cache/huggingface/hub/models--mlx-community--MiniCPM5-1B-OptiQ-4bit/snapshots/664aabaed233c653f82716d8dc822234d0091f78`.
19
+ - Verified by the repo downloader.
20
+ - Added MiniCPM5 snapshot constants to `tests/paths.ts`.
21
+ - Added `scripts/regen-minicpm5-goldens.ts`.
22
+ - Uses `MLX_BUN_ORACLE_VENV=/Users/joshrossi/Code/mlx-lm-example/.venv`.
23
+ - Drives stock `mlx_lm.load()` against the MiniCPM5 snapshot.
24
+ - Writes `minicpm5-parity.json` and `minicpm5-logits-step*.bin`.
25
+ - Generated oracle goldens before porting.
26
+ - Output dir on this machine: `goldens/apple-m1-max/`.
27
+ - Prompt: `The capital of France is`.
28
+ - Prompt ids: `[0, 608, 4894, 304, 6918, 357]`.
29
+ - Logit steps: 4.
30
+ - Greedy tokens: 100.
31
+ - Inspected MiniCPM5 config and tensor layout.
32
+ - `model_type: "llama"`.
33
+ - 24 full-attention layers.
34
+ - Hidden size 1536.
35
+ - 16 query heads, 2 KV heads.
36
+ - Head dim 128.
37
+ - RoPE theta 5,000,000.
38
+ - RMS norm epsilon `1e-6`.
39
+ - SiLU/SwiGLU MLP.
40
+ - Untied embeddings with quantized `lm_head`.
41
+ - Weight paths are standard `model.layers.N.*`, plus `model.embed_tokens`, `model.norm`, and `lm_head`.
42
+ - Decision: first implementation is the naive Llama path only.
43
+ - No compiled decode.
44
+ - No fused custom kernels.
45
+ - No speculative decode.
46
+ - No vision path.
47
+ - Compare against goldens before optimizing.
48
+ - Added initial Llama port edits.
49
+ - `src/config.ts`: flat Llama config defaults, full-attention layer types, default RoPE params.
50
+ - `src/mlx/ffi.ts` / `src/mlx/ops.ts`: `mlx_sigmoid` binding for SwiGLU.
51
+ - `src/model/minicpm5.ts`: concrete MiniCPM5 graph mirroring `mlx_lm.models.llama`.
52
+ - `src/model/factory.ts`: dispatch this known `model_type === "llama"` artifact to `MiniCPM5Model`.
53
+ - `src/generate.ts`: widen runtime model type and gate compiled decode to Gemma-only.
54
+ - Added `tests/minicpm5-parity.test.ts`.
55
+ - Reads the MiniCPM5 goldens.
56
+ - Runs the new `MiniCPM5Model`.
57
+ - Initially required 12 greedy ids and the first 4 logit vectors; expanded to the full 100-token golden after the first green run.
58
+ - First MiniCPM5 parity run passed immediately.
59
+ - Command: `bun test tests/minicpm5-parity.test.ts`.
60
+ - Result: 12/12 greedy ids matched; first 4 full-logit vectors were bit-exact.
61
+ - Decision: expand the test to the full 100-token golden before starter wiring.
62
+ - Expanded MiniCPM5 parity to 100 greedy tokens.
63
+ - Command: `bun test tests/minicpm5-parity.test.ts`.
64
+ - Result: 100/100 greedy ids matched; first 4 full-logit vectors remained bit-exact.
65
+ - Decision: naive Llama graph is correct enough to proceed to integration wiring.
66
+ - Added shared supported-model predicate.
67
+ - `src/model/support.ts`: supports `gemma4*` and `llama`.
68
+ - CLI and server library support checks now use the same predicate.
69
+ - Swapped the first-run starter repo to `mlx-community/MiniCPM5-1B-OptiQ-4bit`.
70
+ - Recommended Gemma download still runs in the background for the machine profile.
71
+ - Widened server context model type to `RuntimeModel`.
72
+ - Updated public/library/docs surfaces.
73
+ - `src/index.ts` exports `MiniCPM5Model` and `RuntimeModel`.
74
+ - `src/lora.ts` accepts `RuntimeModel` so server construction is type-consistent.
75
+ - README quickstart and supported-model table now include MiniCPM5.
76
+ - PLAN Phase 16 and publishing gate now record MiniCPM5 Track A as chosen/ported.
77
+ - Process/testing note from Josh:
78
+ - Running the full sharded suite can start Bun test processes that load large models and in-process servers.
79
+ - Do not run the full suite again casually during this branch; use focused MiniCPM5 parity/smokes unless Josh explicitly wants the full model-loading suite.
80
+ - The suspected large Bun process was already killed before inspection; `ps` showed no remaining Bun processes afterward.
81
+ - Test-proof correction:
82
+ - Josh called out that the `bun:test` pass looked suspiciously fast and was not good enough evidence as presented.
83
+ - Verified with an explicit one-off script that config/weights/model construction occurs and first eval touches the model (`weightsBytes` about 0.907 GB, first argmax token 8181).
84
+ - Added `scripts/check-minicpm5-port.ts`: hard check, no skip guards, requires snapshot + goldens, prints open/construct/eval timings, and compares 100 oracle tokens plus first 4 logit vectors.
85
+ - Hard-check result:
86
+ - `open`: 4.1 ms.
87
+ - `construct`: 2.0 ms.
88
+ - `first eval`: 81.7 ms.
89
+ - `decode`: 100 tokens in 691.8 ms.
90
+ - `parity`: 100/100 greedy ids, 100/100 full-logit vectors bit-exact.
91
+ - Explanation: `Weights.open` and model construction are lazy/mmap-backed; the model is actually touched at eval time, not construction time.
92
+ - Oracle-strength correction:
93
+ - Josh clarified that oracle parity means bit-for-bit comparison against oracle logits, not just token agreement plus a few early logits.
94
+ - Changed MiniCPM5 golden regeneration to dump full last-position logits for all 100 decode steps.
95
+ - Changed the hard check and parity test to require/compare all 100 logit binaries.
96
+ - MiniCPM5 parity matrix:
97
+ - Standard bf16 KV mode must match stock `mlx-lm`.
98
+ - Mixed precision KV mode using the shipped `kv_config.json` must match `mlx-optiq`.
99
+ - Server default should use the shipped mixed-KV config.
100
+ - `--kv-quant off` must remain the standard-cache mode.
101
+ - Added `scripts/regen-minicpm5-kv-goldens.ts` and `tests/minicpm5-kv-parity.test.ts` for the OptiQ mixed-KV path.
102
+ - Strengthened `tests/minicpm5-parity.test.ts` after Josh's pushback.
103
+ - Removed skip guards for this MiniCPM5 proof.
104
+ - Requires the downloaded snapshot and MiniCPM5 goldens.
105
+ - Constructs the model inside the test body, asserts `weightsBytes > 900 MB`, and prints open/construct/first-eval/decode timings.
106
+ - The logit comparison is full-vector last-position logits from the same golden prompt; `lastPositionLogits()` forces evaluation via `toFloat32()`.
107
+ - Live chat failure diagnosis:
108
+ - Josh opened `/chat`, sent `hi there`, and saw an empty assistant bubble.
109
+ - Direct curl to `/v1/chat/completions` returned `QuantizedKVCache: use updateAndFetchQuantized`.
110
+ - Cause: MiniCPM5 ships `kv_config.json`; the server applied mixed KV quant by default, but `MiniCPM5Model` only supports bf16 KV so far.
111
+ - Temporary workaround was to serve MiniCPM5 bf16 KV only; this was later reverted once MiniCPM5 got QuantizedKVCache parity.
112
+ - Also fixed `/chat` to surface streamed `{ error }` SSE payloads instead of swallowing them into an empty assistant bubble.
113
+ - Verified with a temporary in-process server on an ephemeral port:
114
+ - `/stats` reported KV mode `bf16`, 24 bf16 layers.
115
+ - `/v1/chat/completions` for `hi there` returned HTTP 200 with assistant text.
116
+ - Observed default MiniCPM5 behavior includes `<think>`; non-thinking starter UX still needs template/config handling.
117
+ - Naming/scope correction from Josh:
118
+ - This branch should not become a generic Llama platform.
119
+ - Renamed the concrete model file/class to `src/model/minicpm5.ts` / `MiniCPM5Model`.
120
+ - The config still says `model_type: "llama"` because that is the upstream artifact; factory dispatch treats that as this known MiniCPM5 starter artifact for now.
121
+ - Re-ran focused parity after the rename: 100/100 greedy ids matched; first 4 logits bit-exact.
122
+ - Tightened support recognition so this is not generic Llama support.
123
+ - Registry/library support uses repo id `MiniCPM5-1B-OptiQ-4bit` plus `model_type: "llama"`.
124
+ - Factory dispatch checks the known MiniCPM5 config shape before constructing `MiniCPM5Model`.
125
+ - Other Llama configs throw unsupported instead of silently using the MiniCPM5 graph.
126
+ - Re-ran focused parity after this tightening: 100/100 greedy ids matched; first 4 logits bit-exact.
127
+ - Completed the oracle-strength MiniCPM5 parity matrix.
128
+ - Standard cache mode matches stock `mlx-lm`: 100/100 greedy ids and 100/100 full last-position logit vectors are bit-exact.
129
+ - Mixed precision cache mode matches `mlx-optiq` with the shipped `kv_config.json`: 100/100 greedy ids and 100/100 full last-position logit vectors are bit-exact.
130
+ - `MiniCPM5Model` now consumes `QuantizedKVCache` directly and routes quantized triples through `quantizedSdpa`.
131
+ - Production generation now mirrors OptiQ's quantization order: first prompt prefill runs bf16, then populated caches are quantized before decode.
132
+ - Restored MiniCPM5 server default to mixed KV from `kv_config.json`.
133
+ - Temporary in-process server `/stats` reported `mixed (kv_config.json)`, 6 kv8 layers and 18 kv4 layers.
134
+ - `kvQuant: off` reported 24 bf16 layers and still served chat correctly.
135
+ - Non-streaming and streaming `/v1/chat/completions` returned HTTP 200 without the prior `QuantizedKVCache: use updateAndFetchQuantized` error.
136
+ - Read the MiniCPM5 OptiQ manual.
137
+ - MiniCPM5's template contract is `chat_template_kwargs.enable_thinking`.
138
+ - Default starter behavior is no-think (`enable_thinking=false`), matching the manual's fast direct-answer mode.
139
+ - Explicit `chat_template_kwargs: { "enable_thinking": true }` opens the `<think>` reasoning channel.
140
+ - Server prompt rendering now forwards that flag and defaults MiniCPM5 to no-think when absent.
141
+ - Pi harness correction after Josh's pushback:
142
+ - Mistake: briefly changed branch code toward `~/.pi/agent/models.json` upsert and described the generated extension as stale.
143
+ - Correction: reverted branch code/docs/tests back to the existing extension-based live-discovery methodology; the generated extension is the path that worked for Gemma and must remain first-class.
144
+ - Note: I had already run the harness while debugging, which modified Josh's local pi config outside the repo. Do not mutate that again without explicit instruction.
145
+ - MiniCPM5 tool-call corruption fix:
146
+ - Reproduced Josh's broken output: `name="read"> name="path">...`.
147
+ - Root cause: the server applied Gemma's tool sentinel token ids globally. In MiniCPM5, token 49 decodes to literal `<`, and `<function` / `<param` are added special tokens stripped by `decode(..., true)`.
148
+ - Ported the OptiQ behavior: tool parsing is decoded-text based and active only when tools are declared. Gemma keeps its family sentinel path; MiniCPM5 tool-active decoding preserves special tokens and parses native `<function name="..."><param name="...">...` XML into OpenAI `tool_calls`.
149
+ - Added `tests/minicpm5-tools.test.ts`, covering non-streaming and streaming tool calls with the real MiniCPM5 snapshot.
150
+ - Full-branch review pass after Josh reported "a lot of bugs" vs the Gemma 4 port quality. Four real defects found and fixed:
151
+ 1. **Multi-turn tool conversations crashed at render time.** MiniCPM5's template uses `[a, b]|min` in its assistant tool-call-history branch; `@huggingface/jinja` 0.5.9 has no `min`/`max` array filter, so every second round of an agent loop returned 400 `prompt build failed: Unknown ArrayValue filter: min`. This is why pi looked broken. Fix: `ChatTemplate.load` rewrites `[a, b]|min`/`|max` into conditional expressions. Verified byte-for-byte against the oracle venv's `apply_chat_template` for the agent round-trip case (`tests/minicpm5-template.test.ts`); Gemma template parity stays bit-exact.
152
+ 2. **Streaming with tools declared buffered the entire response.** `buffered-text` mode returned "" for every token and dumped everything at flush — pi streams nothing until the end, and prose before a tool call was dropped entirely when a call parsed. Fix: ported the oracle's incremental behavior (`_QwenXMLToolCallStreamParser` buffers only from the tool marker on): content streams live with a StopMatcher-style hold-back on `<tool_call>`/`<function` prefixes; only the markup is withheld; parse-failure falls back to releasing the raw text.
153
+ 3. **Tool param values were JSON-coerced regardless of schema.** `<param name="path">2025</param>` became the number 2025. Oracle (`_decode_tool_parameter_value` + `_tool_parameter_schema`) keeps string-typed params as raw text. Ported; also made the `<param>` regex CDATA-safe (a `</param>` inside CDATA no longer truncates the value). Unit tests added.
154
+ 4. **Sampling ignored the model's `generation_config.json`.** The server hardcoded temp 0.7 / no top-p; optiq serve injects the author-recommended sampling (`optiq/runtime/gen_config.py`) as defaults. Ported: `loadContext` reads `generation_config.json` (MiniCPM5: temp 0.9, top_p 0.95; Gemma: temp 1.0, top_k 64, top_p 0.95) and `toOptions` uses it when the request omits the field. Explicit request values still win. This was the likely "quality" gap on a 1B model.
155
+ - Verification: MiniCPM5 parity (bf16 + mixed-KV) still 100/100 bit-exact; minicpm5-tools, tool-call, chat-template, tools-template, parity (Gemma 12B), server-tools, and server suites all green. Live two-turn agent round-trip (tool call → tool result → final answer) confirmed working in-process.
@@ -0,0 +1,121 @@
1
+ # Distributing mlx-bun via Homebrew (signed + notarized)
2
+
3
+ The end-to-end runbook for shipping a `brew install`-able, Developer-ID
4
+ signed and notarized mlx-bun. Companion to [embedding.md](./embedding.md)
5
+ (which covers the Tauri/Electron *sidecar* layout); this doc is the
6
+ *standalone CLI* distribution.
7
+
8
+ ## What ships
9
+
10
+ A single **self-contained** bundle — the whole `dist/` directory in one
11
+ tarball (~73 MB compressed, ~228 MB installed):
12
+
13
+ | file | what |
14
+ |---|---|
15
+ | `mlx-bun` | the CLI/server (`bun build --compile`), ~61 MB |
16
+ | `libmlxc.dylib`, `libmlx.dylib`, `libjaccl.dylib` | MLX native runtime |
17
+ | `mlx.metallib` | Metal kernels (~150 MB — the bulk) |
18
+ | pi assets | `photon_rs_bg.wasm`, `theme/`, `assets/`, `export-html/`, `native/`, `package.json`, `CHANGELOG.md` |
19
+
20
+ We ship everything in the bottle (rather than the npm/sidecar approach of
21
+ a tiny binary that downloads the native pack on first run) so that
22
+ `brew install` is the *only* network step and the entire bundle is signed
23
+ and notarized as one artifact.
24
+
25
+ ## One-time Apple setup
26
+
27
+ 1. **Developer ID Application certificate** — Xcode → Settings → Accounts →
28
+ your Apple ID → *Manage Certificates…* → **+** → **Developer ID
29
+ Application**. Verify:
30
+ ```sh
31
+ security find-identity -v -p codesigning # shows "Developer ID Application: NAME (TEAMID)"
32
+ ```
33
+ 2. **Notary credentials** — an App Store Connect **API key** (not the
34
+ Developer-portal "Keys" page; not your Apple ID password). Create at
35
+ **App Store Connect → Users and Access → Integrations → App Store
36
+ Connect API → Team Keys → +** (role: Developer). Download the
37
+ `AuthKey_<KEYID>.p8` (one-time download), note the **Key ID** and the
38
+ **Issuer ID** (UUID at the top of the page), then:
39
+ ```sh
40
+ xcrun notarytool store-credentials AC_PROFILE \
41
+ --key ~/.private_keys/AuthKey_<KEYID>.p8 \
42
+ --key-id "<KEYID>" --issuer "<ISSUER-UUID>"
43
+ ```
44
+ `store-credentials` validates against Apple before saving — success
45
+ means notarization auth works.
46
+
47
+ ## Release a build
48
+
49
+ ```sh
50
+ ./scripts/release-binary.sh # version from package.json
51
+ # or: ./scripts/release-binary.sh 0.0.4
52
+ ```
53
+
54
+ This runs [build-binary.sh](../../scripts/build-binary.sh), then:
55
+ - signs every nested Mach-O (dylibs, any `.node`) with the hardened
56
+ runtime, then the executable with the JIT
57
+ [entitlements](../../scripts/entitlements.plist) (JavaScriptCore JITs —
58
+ without `allow-jit` the binary is killed on launch);
59
+ - notarizes the zipped bundle via `AC_PROFILE` and waits;
60
+ - emits `dist-release/mlx-bun-v<ver>-arm64.tar.gz` (+ `.sha256`) and
61
+ prints the `version`/`url`/`sha256` for the formula.
62
+
63
+ > A flat CLI bundle **cannot be stapled** (`stapler` only handles
64
+ > `.app`/`.dmg`/`.pkg`); the notarization ticket lives on Apple's servers.
65
+ > Gatekeeper checks it online only when a copy is *quarantined*. brew
66
+ > formula installs are **not** quarantined (brew downloads via curl), so
67
+ > the binary runs prompt-free regardless — notarization is load-bearing
68
+ > only for a direct browser download or a Cask.
69
+
70
+ Publish the artifact as a GitHub release whose tag matches the formula url:
71
+ ```sh
72
+ gh release create v0.0.4 dist-release/mlx-bun-v0.0.4-arm64.tar.gz \
73
+ --title "mlx-bun v0.0.4" --notes "..."
74
+ ```
75
+
76
+ ## The tap
77
+
78
+ Homebrew taps must be a repo named `homebrew-<name>`:
79
+
80
+ 1. Create a public repo **`joshuarossi/homebrew-mlx-bun`**.
81
+ 2. Copy [packaging/homebrew/mlx-bun.rb](../../packaging/homebrew/mlx-bun.rb)
82
+ to `Formula/mlx-bun.rb` in it, updating `version`/`url`/`sha256` from
83
+ the release-script output. Commit and push.
84
+ 3. Install:
85
+ ```sh
86
+ brew install joshuarossi/mlx-bun/mlx-bun
87
+ mlx-bun --version
88
+ ```
89
+
90
+ `brew audit --strict --new joshuarossi/mlx-bun/mlx-bun` before pushing
91
+ catches most formula issues; `brew install --build-from-source` +
92
+ `brew test` runs the formula's `test do` locally.
93
+
94
+ ## Updating (one command)
95
+
96
+ Releases must originate on this Mac (signing + notarization need the
97
+ Developer ID cert + Apple creds), so the publish step is local too — no
98
+ cross-repo CI token to manage, and the tap can't drift out of sync.
99
+
100
+ Per release:
101
+
102
+ 1. bump `version` in `package.json` and commit;
103
+ 2. one shot:
104
+ ```sh
105
+ PUBLISH=1 ./scripts/release-binary.sh
106
+ ```
107
+ This builds → signs → notarizes → creates the `v<ver>` GitHub release →
108
+ rewrites `version`/`url`/`sha256` in the tap's `Formula/mlx-bun.rb` and
109
+ pushes it → mirrors the same fields into
110
+ [packaging/homebrew/mlx-bun.rb](../../packaging/homebrew/mlx-bun.rb)
111
+ (left staged — commit it).
112
+
113
+ Prefer two steps? Run `./scripts/release-binary.sh` to build, eyeball the
114
+ artifact, then `./scripts/publish-release.sh` to publish + sync the tap.
115
+ Both are idempotent: re-running `publish-release.sh` clobbers the release
116
+ asset and no-ops the tap push if nothing changed.
117
+
118
+ Users then upgrade with:
119
+ ```sh
120
+ brew upgrade joshuarossi/tap/mlx-bun
121
+ ```
@@ -0,0 +1,168 @@
1
+ # Embedding mlx-bun (single-binary sidecar)
2
+
3
+ Ship local inference inside a Mac app (Tauri, Electron, or anything
4
+ that can spawn a process) with zero user-visible dependencies: one
5
+ executable + three native libraries, dropped into your app's resources.
6
+
7
+ ## Build the bundle
8
+
9
+ ```sh
10
+ ./scripts/build-binary.sh dist
11
+ ```
12
+
13
+ Produces a relocatable directory:
14
+
15
+ | file | what | size (arm64) |
16
+ |---|---|---|
17
+ | `mlx-bun` | the CLI/server, compiled with `bun build --compile` | ~61 MB |
18
+ | `libmlxc.dylib` | mlx-c, rewritten to load `@loader_path/libmlx.dylib` | ~0.7 MB |
19
+ | `libmlx.dylib` | mlx core (+`@loader_path` rpath added for libjaccl) | ~15 MB |
20
+ | `libjaccl.dylib` | mlx's distributed-comm dependency | ~0.6 MB |
21
+ | `mlx.metallib` | Metal kernels — libmlx loads it from its own directory | ~150 MB |
22
+ | `photon_rs_bg.wasm` | pi image codec — only the web chat's `read`-on-image path; resolved next to the executable | ~1.8 MB |
23
+
24
+ Library resolution order (src/mlx/ffi.ts): `MLX_BUN_LIBMLXC` env var →
25
+ `libmlxc.dylib` next to the executable → homebrew
26
+ (`/opt/homebrew/lib`, `/usr/local/lib`). The whole directory can be
27
+ renamed/moved; nothing references absolute paths after the build
28
+ script's `install_name_tool` fixups.
29
+
30
+ ## Embedded pi web chat (`/ws/chat`)
31
+
32
+ The compiled binary includes the full embedded pi AgentSession
33
+ (`src/pi-web.ts`, dep `@earendil-works/pi-coding-agent`) that drives the
34
+ browser chat. The web chat runs pi **headless** — no TUI — so it needs
35
+ far less than pi's own `bun build --compile` ships.
36
+
37
+ **Bundled automatically.** All of pi's JavaScript (the SDK,
38
+ `createAgentSession`, the seven tools, the `openai-completions` provider,
39
+ the resource loader) is pulled into the executable by `bun build
40
+ --compile`. Nothing extra to do for the JS.
41
+
42
+ **Sidecar'd by `build-binary.sh`.** The only runtime asset pi resolves
43
+ *by path* on the headless web-chat path is `photon_rs_bg.wasm` (pi's
44
+ image codec). pi resolves it relative to `process.execPath`, so the build
45
+ script copies it next to `mlx-bun`. It is reached only when the `read`
46
+ tool is asked to read an image file, and pi degrades gracefully when it's
47
+ absent (the tool returns a text "[Image omitted]" note instead of
48
+ crashing), so it is best-effort, not load-bearing.
49
+
50
+ > Known limitation: `@silvia-odwyer/photon-node`'s wasm-bindgen glue
51
+ > currently fails to decode under Bun (both `bun` and `bun build
52
+ > --compile`) with *"Unreachable code should not be executed"* — a Bun
53
+ > wasm-bindgen gap, not an asset-placement issue (the bytes are valid and
54
+ > found). Net effect today: web-chat image *reads* aren't resized inline.
55
+ > The web chat itself is unaffected (its provider is text-only), and the
56
+ > wasm is shipped so it will work once Bun's support lands.
57
+
58
+ **Intentionally omitted** (TUI-only assets pi ships beside its own binary,
59
+ never reached by the headless web chat):
60
+
61
+ | omitted asset | what it's for |
62
+ |---|---|
63
+ | `theme/*.json` | TUI color themes (loaded via `initTheme`; we pass `noThemes`) |
64
+ | `assets/*.png` | TUI announcement/interactive images |
65
+ | `export-html/*` | the `/export-html` command's template + vendor JS |
66
+ | `docs/`, `examples/`, `README.md`, `CHANGELOG.md` | TUI help/doc browsing |
67
+
68
+ These are gated behind TUI code paths and the explicit `exportToHtml()`
69
+ method; `createAgentSession`'s import graph (`core/sdk.js`) pulls in no
70
+ theme/asset/export-html loaders, and `src/pi-web.ts` builds the session
71
+ with `noThemes`/`noSkills`/`noExtensions`/`noPromptTemplates`/
72
+ `noContextFiles`.
73
+
74
+ **Verifying it.** `build-binary.sh` runs `scripts/verify-binary-pi.ts` as
75
+ a sibling compiled binary inside the bundle (so `process.execPath` points
76
+ at the bundle dir, matching what the real binary sees). It builds the
77
+ exact headless session `src/pi-web.ts` builds — same provider/registry/
78
+ resource-loader config — against an unreachable provider, far enough to
79
+ prove the bundled SDK + assets resolve without a missing-asset crash. It
80
+ needs **no model and no server**; a provider/model error is the success
81
+ signal. The one thing this can't cover offline is a live token-streaming
82
+ turn over `/ws/chat` — that still requires `mlx-bun serve` running a real
83
+ model (run it yourself; the smoke covers everything up to the model call).
84
+
85
+ ## Sidecar pattern
86
+
87
+ Spawn the server on app start, point your in-app client at it:
88
+
89
+ ```ts
90
+ // Electron main / Tauri sidecar config
91
+ const child = spawn(resourcePath("mlx-bun"), [
92
+ "serve", "gemma-4-12B", "--port", "8090", "--memory-budget", "12",
93
+ ]);
94
+ // readiness: poll GET /v1/models (start→ready is ~0.4 s warm)
95
+ ```
96
+
97
+ - One generation runs at a time (single queue, one GPU) — serialize or
98
+ queue client-side too if you show progress.
99
+ - `--memory-budget <GB>` is the OOM defense: requests that can't finish
100
+ within the budget are rejected with HTTP 400 (`memory_admission`)
101
+ *before* generating. The Metal OOM it prevents is uncatchable.
102
+ - Protocols: OpenAI chat completions, Anthropic `/v1/messages`, OpenAI
103
+ Responses (`previous_response_id` works) — see
104
+ [server-api.md](./server-api.md). Use whichever your client SDK
105
+ already speaks.
106
+ - Kill the child freely (SIGKILL is fine): restart→ready is ~0.4 s and
107
+ prompt-cache misses are the only loss.
108
+
109
+ ## First-run weights
110
+
111
+ The binary includes the resumable verified downloader:
112
+
113
+ ```sh
114
+ mlx-bun get mlx-community/gemma-4-12B-it-OptiQ-4bit # → standard HF cache
115
+ mlx-bun scan # index it
116
+ mlx-bun serve gemma-4-12B --port 8090
117
+ ```
118
+
119
+ Downloads resume across interruption, every blob is checksum-verified
120
+ (sha256/sha1), and the cache layout is exactly huggingface_hub's — a
121
+ user's existing HF cache is picked up as-is. For a custom weights
122
+ location inside your app's container, set `HF_HOME` in the child's
123
+ environment.
124
+
125
+ ## Signing & notarization
126
+
127
+ The build script ad-hoc signs the dylibs (required on arm64 after
128
+ `install_name_tool`). For distribution, re-sign everything with your
129
+ Developer ID and notarize:
130
+
131
+ ```sh
132
+ IDENTITY="Developer ID Application: Your Name (TEAMID)"
133
+
134
+ # 1. entitlements — Bun executables REQUIRE allow-jit under the
135
+ # hardened runtime (JavaScriptCore JIT); without it the binary is
136
+ # killed on launch.
137
+ cat > entitlements.plist <<'EOF'
138
+ <?xml version="1.0" encoding="UTF-8"?>
139
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
140
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
141
+ <plist version="1.0"><dict>
142
+ <key>com.apple.security.cs.allow-jit</key><true/>
143
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
144
+ </dict></plist>
145
+ EOF
146
+
147
+ # 2. sign (dylibs first, then the executable with entitlements)
148
+ codesign -f --options runtime -s "$IDENTITY" \
149
+ dist/libjaccl.dylib dist/libmlx.dylib dist/libmlxc.dylib
150
+ codesign -f --options runtime --entitlements entitlements.plist \
151
+ -s "$IDENTITY" dist/mlx-bun
152
+
153
+ # 3. notarize (zip the bundle dir; metallib needs no signature)
154
+ ditto -c -k --keepParent dist mlx-bun.zip
155
+ xcrun notarytool submit mlx-bun.zip --keychain-profile "AC_PROFILE" --wait
156
+ ```
157
+
158
+ Inside an `.app`, put the bundle under
159
+ `Contents/Resources/` (Electron: `extraResources`; Tauri:
160
+ `bundle.resources` + `externalBin` for the executable) and let the
161
+ app's normal signing pass cover it — the same entitlements apply to
162
+ the sidecar binary.
163
+
164
+ ## Library API
165
+
166
+ The server is one consumer of the library — embed generation directly
167
+ in a Bun process instead of spawning, via `loadContext`/`generate`:
168
+ see [library-api.md](./library-api.md).
@@ -0,0 +1,153 @@
1
+ # Library API
2
+
3
+ The HTTP server is one consumer of a library-first API. Everything it
4
+ does — generation, sampling, KV schemes, LoRA, vision, caching, memory
5
+ admission — is importable directly into a Bun process. Not yet
6
+ published to npm; import from `src/`.
7
+
8
+ One rule everywhere: **one generation at a time per process** (one GPU,
9
+ batch=1). The server serializes through a queue; do the same.
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { loadModelConfig } from "./src/config";
15
+ import { Weights } from "./src/weights";
16
+ import { Gemma4Model } from "./src/model/gemma4";
17
+ import { loadTokenizer } from "./src/tokenizer";
18
+ import { ChatTemplate } from "./src/chat-template";
19
+ import { generate } from "./src/generate";
20
+
21
+ const dir = "/path/to/hf-snapshot"; // mlx-bun ls prints these
22
+ const model = new Gemma4Model(await Weights.open(dir), await loadModelConfig(dir));
23
+ const tok = await loadTokenizer(dir);
24
+ const template = await ChatTemplate.load(dir);
25
+
26
+ const ids = tok.encode(template.render([{ role: "user", content: "hi" }]));
27
+ const promptIds = ids[0] === ids[1] && ids[0] === tok.bosTokenId ? ids.slice(1) : ids;
28
+
29
+ const gen = generate(model, promptIds, { maxTokens: 256, temperature: 0 });
30
+ const out: number[] = [];
31
+ for await (const t of gen) out.push(t.token); // t: { token, index }
32
+ console.log(tok.decode(out, true));
33
+ console.log(gen.stats); // set once iteration ends
34
+ ```
35
+
36
+ Weight loading is lazy (mmap + mlx native loader): construction is
37
+ milliseconds; weights materialize on first forward.
38
+
39
+ ## generate(model, promptTokens, options) → Generation
40
+
41
+ `Generation` is an `AsyncIterable<GeneratedToken>` with a `stats`
42
+ property (`GenerateStats`, available after iteration completes — also
43
+ after an early `break`, which cleanly cancels the in-flight step).
44
+
45
+ `GenerateOptions` (all optional):
46
+
47
+ | option | default | notes |
48
+ |---|---|---|
49
+ | `maxTokens` | 512 | |
50
+ | `temperature` / `topP` / `topK` / `seed` | greedy when temperature 0 | on-device sampling; seeded per (seed, step) |
51
+ | `repetitionPenalty` / `repetitionContextSize` | off / 20 | |
52
+ | `eosTokenIds` | from config | includes the tool-handoff token |
53
+ | `prefillChunkSize` | 2048 | matches mlx-lm |
54
+ | `cache` | fresh | pre-warmed `Cache[]`; `cache[0].offset` tokens are treated as already prefilled. Caller keeps ownership |
55
+ | `kvBits` / `kvGroupSize` / `quantizedKvStart` | off / 64 / 5000 | uniform KV quantization (4 or 8), incl. rotating caches (Phase 9) |
56
+ | `kvConfig` | off | per-layer mixed precision from `kv_config.json` (`config.kvQuant`); overrides `kvBits`, start 0 — optiq serve semantics |
57
+ | `adapters` | none | mounted LoRA adapter ids, applied for exactly this generation |
58
+ | `promptEmbeddings` / `imageMask` | — | vision path (see `src/vision/`) |
59
+
60
+ `GenerateStats`: `promptTokens`, `cachedTokens`, `generatedTokens`,
61
+ `prefillTps` / `prefillMs`, `decodeTps` / `decodeMs`, `cacheTokens`
62
+ (the exact token sequence whose KV is in the cache — feed it to
63
+ `PromptCache.put`). Timing semantics match mlx-lm: the prompt clock
64
+ runs until the first token arrives (the prefill→decode boundary is
65
+ prompt time), the decode clock from there.
66
+
67
+ ## Serving pieces
68
+
69
+ ```ts
70
+ import { loadContext, createServer } from "./src/server";
71
+
72
+ const ctx = await loadContext(dir, "my-model", { memoryBudgetBytes: 12e9 });
73
+ const server = createServer(ctx, 8090, {
74
+ promptCacheBytes: 2e9, // byte-capped LRU (never count-capped)
75
+ kvQuant: undefined, // default: kv_config.json; "off" | bits
76
+ memoryBudgetBytes: 12e9, // admission control — the only OOM defense
77
+ });
78
+ ```
79
+
80
+ `loadContext` refuses models that cannot serve any context within the
81
+ budget (pre-GPU, mmap-only check). The server exposes OpenAI chat
82
+ completions, Anthropic `/v1/messages`, OpenAI Responses, adapters, and
83
+ `/stats` — [server-api.md](./server-api.md).
84
+
85
+ ## Prompt cache (in-process prefix reuse)
86
+
87
+ ```ts
88
+ import { PromptCache } from "./src/prompt-cache";
89
+ const cache = new PromptCache(2e9); // BYTE-capped LRU
90
+ const hit = cache.take(promptIds, /*namespace*/ ""); // longest common prefix, trimmed
91
+ const caches = hit?.caches ?? model.makeCache();
92
+ const gen = generate(model, promptIds, { cache: caches });
93
+ for await (const t of gen) { /* … */ }
94
+ cache.put(gen.stats!.cacheTokens, caches, ""); // hand ownership back
95
+ ```
96
+
97
+ Namespace entries by adapter spec when LoRA is in play (KV computed
98
+ under one adapter must not seed another's prefill).
99
+
100
+ ## KV-cache persistence (cross-process)
101
+
102
+ ```ts
103
+ import { saveKvCache, loadKvCache } from "./src/kv-store";
104
+ saveKvCache("/tmp/prefix.kv", tokens, caches); // page-aligned file
105
+ const { tokens, caches, mmap } = loadKvCache("/tmp/prefix.kv", model);
106
+ // reload is a zero-copy MAP_PRIVATE mmap straight to the GPU (~1 ms);
107
+ // keep `mmap` referenced as long as the caches live
108
+ ```
109
+
110
+ Quantized caches are not persistable yet (documented gap).
111
+
112
+ ## Registry + fit (model discovery and memory math)
113
+
114
+ ```ts
115
+ import { Registry } from "./src/registry";
116
+ import { fit, skuMatrix } from "./src/fit";
117
+
118
+ const reg = new Registry(); // bun:sqlite, scans the HF cache
119
+ await reg.scan();
120
+ const m = reg.resolve("12B"); // unique-match query, throws on ambiguity
121
+ const report = fit(await loadModelConfig(m.path), m.sizeBytes, 8192,
122
+ undefined, undefined, m.expertsBytes);
123
+ // report: { fits, maxSafeContext, predictedDecodeTps, kvBytes, … }
124
+ ```
125
+
126
+ `fit` is calibrated per chip family (MoE-aware: decode reads active
127
+ experts only). The server uses it for admission; `mlx-bun fit <query>`
128
+ is the CLI face.
129
+
130
+ ## LoRA hot-swap
131
+
132
+ ```ts
133
+ import { AdapterManager } from "./src/lora";
134
+ const adapters = new AdapterManager(model);
135
+ await adapters.mount("upper", "/path/to/adapter-dir"); // all-or-nothing validation
136
+ const gen = generate(model, promptIds, { adapters: ["upper"] });
137
+ ```
138
+
139
+ Mount N adapters on one base; selection is per-generation; an
140
+ unselected adapter costs nothing; base logits are byte-identical after
141
+ any amount of switching.
142
+
143
+ ## Memory & disposal rules
144
+
145
+ - `MlxArray` wrappers have explicit `.dispose()` with a
146
+ FinalizationRegistry backstop — dispose what you create in hot paths.
147
+ - Caches you own (from `model.makeCache()` outside the prompt cache):
148
+ dispose each via `c.dispose()` when done.
149
+ - GPU OOM is **uncatchable** (mlx throws from a Metal completion
150
+ handler → process death). Use `fit`/admission before generating, not
151
+ try/catch around it.
152
+ - Never read a typed array that native code wrote in a hot path; use
153
+ the `read.*` helpers (bun#32054 — see PLAN.md Phase 4 findings).