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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- 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).
|