mlx-bun 0.0.8 → 0.0.10

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 (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,535 @@
1
+ # mlx-lm tool parity — scoping plan
2
+
3
+ Scoping for the remaining mlx-lm CLI tools not yet covered by mlx-bun, plus
4
+ serve-time speculative decoding (`--draft-model`). Written against the oracle
5
+ venv (`/Users/joshrossi/Code/mlx-lm/.venv`, **mlx-lm 0.31.3**, **mlx-optiq
6
+ 0.2.4** — note: newer than the 0.2.1 noted in CLAUDE.md), source read directly
7
+ from `site-packages/`. Sibling work in flight (not covered here): `fuse` /
8
+ `convert` / `perplexity` (another agent), `sft_scope`.
9
+
10
+ ## What actually exists in mlx-lm 0.31.3
11
+
12
+ From `mlx_lm-0.31.3.dist-info/entry_points.txt` — all six named tools are real
13
+ console scripts, plus one this plan folds in because we already cover it:
14
+
15
+ | entry point | source file |
16
+ | --- | --- |
17
+ | `mlx_lm.cache_prompt` | `mlx_lm/cache_prompt.py` |
18
+ | `mlx_lm.evaluate` | `mlx_lm/evaluate.py` |
19
+ | `mlx_lm.upload` | `mlx_lm/upload.py` |
20
+ | `mlx_lm.awq` | `mlx_lm/quant/awq.py` |
21
+ | `mlx_lm.dwq` | `mlx_lm/quant/dwq.py` |
22
+ | `mlx_lm.gptq` | `mlx_lm/quant/gptq.py` |
23
+ | `mlx_lm.dynamic_quant` | `mlx_lm/quant/dynamic_quant.py` (sensitivity-scan + 4/5-bit split — **already covered** by `src/quantize/{sensitivity,allocator}.ts`; no work item) |
24
+
25
+ `mlx_lm.server --draft-model/--num-draft-tokens` lives in `mlx_lm/server.py`
26
+ (flags at :1782–1791) driving `speculative_generate_step` in
27
+ `mlx_lm/generate.py:473`.
28
+
29
+ **optiq's quant tooling (the L2 oracle), for calibration of the awq/dwq/gptq
30
+ decision:** `optiq convert` is per-layer **KL sensitivity scan + greedy
31
+ mixed-precision knapsack** (`optiq/core/sensitivity.py`, `optimizer.py`, with
32
+ block-floor/block-run guards and a latency-aware variant), verified by
33
+ `optiq/core/verifier.py` (perplexity / KL thresholds). It does **not**
34
+ implement AWQ/DWQ/GPTQ-style learned or activation-aware quantization — final
35
+ packing is stock `mx.quantize`. So optiq covers the *bit-allocation* half of
36
+ quant quality; the *learned-refinement* half (DWQ et al.) is genuinely
37
+ uncovered by both mlx-bun and optiq today.
38
+
39
+ ---
40
+
41
+ ## 1. `cache_prompt`
42
+
43
+ **What it does.** Prefills a prompt into a KV cache and saves it to a
44
+ safetensors file for later reuse (`mlx_lm/cache_prompt.py`; save/load are
45
+ `save_prompt_cache`/`load_prompt_cache` in `mlx_lm/models/cache.py:43,62`).
46
+ Consumers: `mlx_lm.generate --prompt-cache-file` (`generate.py:180,1974-1990`,
47
+ which also validates that `--kv-bits`/`--kv-group-size` match the loaded
48
+ cache) and `mlx_lm.chat`. The chat template is applied with
49
+ `add_generation_prompt=False, continue_final_message=True` so the cache ends
50
+ mid-conversation.
51
+
52
+ **CLI surface** (`cache_prompt.py:17-80`): `--model` (default `mlx_model`),
53
+ `--adapter-path`, `--trust-remote-code`, `--eos-token`, `--max-kv-size`,
54
+ `--prompt-cache-file` (required), `--prompt` (required, `-` = stdin),
55
+ `--kv-bits` (default none), `--kv-group-size` (64), `--quantized-kv-start`
56
+ (5000). Saved metadata: model path + tokenizer config.
57
+
58
+ **mlx-bun overlap.** `src/prompt-cache.ts` — in-memory, byte-capped
59
+ (`--prompt-cache <GiB>`, default 2), LRU, longest-strict-prefix `take()`,
60
+ adapter-namespaced. It is server-only and has **no disk serialization**; cache
61
+ entries are live MlxArrays and die with the process. `mlx-bun generate` has no
62
+ `--prompt-cache-file`. The building blocks exist: safetensors write machinery
63
+ in `src/quantize/safetensors-writer.ts`, cache state (K/V tensors + offset)
64
+ owned by the `Cache` implementations in `src/model/gemma4.ts`.
65
+
66
+ **Port plan.**
67
+ - `src/prompt-cache-io.ts` (new): `savePromptCache(file, caches, meta)` /
68
+ `loadPromptCache(file)` — serialize each layer's K/V (+ scales/biases for
69
+ quantized caches, ring state for rotating caches) plus
70
+ `{modelId, tokens, kvBits, kvGroupSize, offset[]}` metadata. Mirror
71
+ mlx-lm's per-class `state`/`meta_state` split rather than inventing a format.
72
+ - `src/cli.ts`: new `cache-prompt` subcommand (model resolve via `Registry`,
73
+ prefill via the serving prompt path — reuse `promptIdsFor`, per the
74
+ eval-reuse lesson) and a `--prompt-cache-file` flag on `generate`.
75
+ - Optional follow-up: `mlx-bun serve --prompt-cache-file` to pre-warm the
76
+ server's `PromptCache` at startup.
77
+
78
+ **Oracle / tiers.** L1 bit-exact, two gates: (a) round-trip — greedy
79
+ generation from a loaded cache file must be token-identical to single-shot
80
+ generation over prompt+continuation; (b) cross-oracle — same prompt cached by
81
+ `mlx_lm.cache_prompt` and by us produce token-identical continuations from
82
+ each runtime. Byte-level file compatibility with mlx-lm's format is a stretch
83
+ goal, not the bar.
84
+
85
+ **Effort:** M. **Risky parts:** serializing `RotatingKVCache` ring state and
86
+ quantized-cache triples correctly; offset/window metadata; refusing stale
87
+ caches (model mismatch) loudly.
88
+
89
+ **Recommendation: build later.** The in-memory server cache already covers
90
+ the interactive/agent use-case; the file workflow mainly serves scripted CLI
91
+ runs. Cheap, real parity, not urgent.
92
+
93
+ ---
94
+
95
+ ## 2. `evaluate`
96
+
97
+ **What it does.** An adapter that registers mlx-lm as an
98
+ **lm-evaluation-harness** backend (`mlx_lm/evaluate.py:71` —
99
+ `@register_model("mlxlm") class MLXLM(LM)`) implementing `loglikelihood`
100
+ (prefix-grouped, batched scoring with KV reuse), `loglikelihood_rolling`, and
101
+ `generate_until` (via `batch_generate`), then calls
102
+ `lm_eval.simple_evaluate(...)` (`evaluate.py:489`). The task library, few-shot
103
+ formatting, and metrics all live in the third-party `lm_eval` package —
104
+ that's the bulk of the tool.
105
+
106
+ **CLI surface** (`evaluate.py:397-458`): `--model` (req), `--tasks` (req,
107
+ nargs+), `--output-dir`, `--batch-size` (16), `--num-shots`, `--max-tokens`,
108
+ `--limit`, `--seed` (123), `--fewshot-as-multiturn`, `--apply-chat-template`
109
+ (tri-state), `--chat-template-args` (JSON), `--confirm-run-unsafe-code`,
110
+ `--trust-remote-code`, `--temp`/`--top-p`/`--top-k`.
111
+
112
+ **mlx-bun overlap.** `src/eval/` (runner with `generateText` on the real
113
+ serving path + `greedyDecodeBitExact`, task set, KL machinery in
114
+ `src/eval/kl.ts`), results in `~/.cache/mlx-bun/evals.sqlite`
115
+ (`src/evaldb.ts`), CLI `mlx-bun benchmark` / `mlx-bun evals`. And the L2
116
+ oracle already ships a full benchmark suite: `optiq eval`
117
+ (kl/gsm8k/mmlu/ifeval/bfcl/humaneval/hashhop + Capability_Score).
118
+
119
+ **Port plan.** Do **not** port lm-eval to Bun — reimplementing the task
120
+ library is XL and permanently chasing upstream. Two-part coverage instead:
121
+ 1. **Shim (S):** document + smoke-test running the oracle venv's `lm_eval`
122
+ against a running mlx-bun server via its OpenAI `local-completions` /
123
+ `local-chat-completions` backend (mlx-bun's `/v1` already speaks the
124
+ contract; needs `logprobs`/`echo` support checked for loglikelihood tasks
125
+ — generation tasks work today). One doc page + a
126
+ `scripts/lm-eval-shim.sh`.
127
+ 2. **Native (only if a gap shows):** add a `loglikelihood` scoring mode to
128
+ `src/eval/runner.ts` (one forward, `take_along_axis` on log-softmax — the
129
+ same scoring loop as `evaluate.py:107-138`) so our own tasks can do
130
+ multiple-choice; that's an S add-on with a direct numeric oracle
131
+ (`MLXLM._score_fn` outputs on fixed inputs).
132
+
133
+ **Oracle / tiers.** Not a bit-exactness surface end-to-end; gate the scoring
134
+ primitive (loglikelihood per token vs oracle `_score_fn`, exact fp32 match on
135
+ fixed inputs = L1-style), and gate task *scores* statistically (same task,
136
+ same limit, matching accuracy).
137
+
138
+ **Effort:** S (shim) / M (native loglikelihood). **Risky parts:**
139
+ loglikelihood-via-HTTP needs prompt-logprob echo, which most local /v1 servers
140
+ (ours included) don't fully implement — that's the one real feature gap to
141
+ check first.
142
+
143
+ **Recommendation: cover-via-existing** (own eval suite + optiq eval as
144
+ quality oracle) **+ the S-sized lm-eval-over-HTTP shim**; skip a native port.
145
+
146
+ ---
147
+
148
+ ## 3. `upload`
149
+
150
+ **What it does.** 22-line wrapper (`mlx_lm/upload.py`) around
151
+ `mlx_lm.utils.upload_to_hub(path, repo)` — creates the repo and uploads a
152
+ model folder (with generated model card) via `huggingface_hub`.
153
+
154
+ **CLI surface:** `--path` (default `mlx_model`), `--upload-repo`.
155
+
156
+ **mlx-bun overlap.** Essentially done: `src/hf-push.ts` implements the whole
157
+ HF HTTP protocol natively (create-repo → preupload → LFS batch/S3 → NDJSON
158
+ commit), token management (`~/.mlx-bun/hf.json` → `$HF_TOKEN` → HF cache
159
+ token), LFS suffix + 10 MB threshold rules. It's wired to the web push routes
160
+ (`/api/{quantize,finetune,dataset}/push` in `src/server.ts`) but has **no CLI
161
+ command**.
162
+
163
+ **Port plan.** One new `case "upload"` in `src/cli.ts`: resolve `--path` (or
164
+ a registry query) → `uploadToHub(dir, repo)`; accept `--upload-repo` as the
165
+ mlx-lm-named alias for `--repo`. Optional: generate a small model card noting
166
+ mlx-bun + quant recipe, as `upload_to_hub` does.
167
+
168
+ **Oracle / tiers.** Functional, not numeric: round-trip test (upload a tiny
169
+ artifact → `mlx-bun get` it back → hash-compare files). No tier gate.
170
+
171
+ **Effort:** S. **Risky parts:** none new — the risky code already runs in
172
+ production via the Lab routes.
173
+
174
+ **Recommendation: build now.** Trivial, and the in-flight `convert`/`fuse`
175
+ work plus `quantize` all produce directories that want a publish verb.
176
+
177
+ ---
178
+
179
+ ## 4. `awq`
180
+
181
+ **What it does.** Activation-aware weight quantization
182
+ (`mlx_lm/quant/awq.py`): per transformer block, capture layer inputs with
183
+ `Catcher` modules, grid-search (`--n-grid`, default 20) per-channel **scales**
184
+ (balancing activation vs weight magnitudes) and **clip** thresholds to
185
+ minimize block-output MSE under quantization, with a per-block fallback to
186
+ plain quantization if the loss didn't improve (`awq.py:452-505`). Requires a
187
+ hand-written per-architecture `AWQConfig` (which norms feed which
188
+ projections); registry at `awq.py:143`: llama, mistral, qwen2, qwen3,
189
+ gemma3_text, gemma3, deepseek_v2. Calibration data: fixed gist download,
190
+ `quant/utils.py:load_data` (`~/.cache/mlx-lm/calibration_v5.txt`). Embeddings
191
+ and lm_head quantized separately (`--embed-bits/--embed-group-size`).
192
+
193
+ **CLI surface** (`awq.py:533-547`): `--model/-m`, `--mlx-path`, `--bits` (4),
194
+ `--group-size` (64), `--embed-bits` (4), `--embed-group-size` (32),
195
+ `--num-samples` (128), `--sequence-length` (512), `--n-grid` (20), `--seed`
196
+ (123). Distributed-aware (`mx.distributed`).
197
+
198
+ **mlx-bun overlap.** `src/quantize/` already does calibration-driven quality
199
+ work — per-layer KL sensitivity (`sensitivity.ts`) + mixed-precision knapsack
200
+ (`allocator.ts`), i.e. the optiq approach; that attacks the same quality goal
201
+ by *bit allocation* rather than *weight conditioning*. No scale/clip search
202
+ exists. optiq itself ships no AWQ (verified above), so there is **no L2
203
+ oracle path** — the only oracle is `mlx_lm.awq` itself.
204
+
205
+ **Port plan (if built).** `src/quantize/awq.ts` (scale/clip grid search —
206
+ needs per-block input capture, which means a tapped block forward like
207
+ `forwardTapped` in `src/spec/dspark/generate-dflash.ts:34`, generalized),
208
+ `src/quantize/awq-configs.ts` (per-arch registry for our ported models:
209
+ gemma3/gemma4, qwen, minicpm — note upstream has no gemma4/minicpm configs;
210
+ we'd be authoring those, unoracled), calibration loader reusing
211
+ `quantize/calibration.ts`, CLI or Lab-API entry. Oracle: run `mlx_lm.awq` in
212
+ the oracle venv on a small supported model (fixed seed + shared calibration
213
+ file) and compare final weight tensors — deterministic given identical
214
+ calibration batches, so weight-level parity is the L1-style gate; then
215
+ KL/perplexity via existing `src/eval/kl.ts` for quality claims.
216
+
217
+ **Effort:** L. **Risky parts:** per-arch config authoring for models mlx-lm
218
+ doesn't cover (no oracle); block-capture instrumentation on our hand-written
219
+ (non-module-tree) forwards; grid search runtime on 24–32 GB machines.
220
+
221
+ **Recommendation: build later (likely skip).** The L2 mixed-precision path is
222
+ our quant-quality story; only revisit if KL evals show our 4-bit uniform tier
223
+ losing to community AWQ artifacts.
224
+
225
+ ---
226
+
227
+ ## 5. `dwq`
228
+
229
+ **What it does.** Distilled weight quantization (`mlx_lm/quant/dwq.py`):
230
+ quantize the model, then **unfreeze only the quantization `scales`/`biases`**
231
+ of sub-8-bit affine layers (`dwq.py:90-100`) and train them against the bf16
232
+ teacher with a temperature-2 KL loss on top-1024 teacher logits
233
+ (`kl_div_loss`, `dwq.py:108-118`), Adam @ 1e-6, params accumulated in f32.
234
+ Teacher logits can be precomputed to disk (`--target-dir`, top-1024 +
235
+ indices as safetensors, `compute_dwq_targets`) so teacher and student never
236
+ need to be resident together. Warns if final val loss regressed. This is the
237
+ method behind most good mlx-community 4-bit artifacts.
238
+
239
+ **CLI surface** (`dwq.py:242-304`): `--model/-m` (teacher, req),
240
+ `--quantized-model` (optional pre-quantized student), `--mlx-path`, `--bits`
241
+ (4), `--group-size` (64), `--num-samples` (2048), `--max-seq-length` (1025),
242
+ `--seed`, `--learning-rate` (1e-6), `--batch-size` (4), `--data-path`
243
+ (default `allenai/tulu-3-sft-mixture`), `--grad-checkpoint`, `--target-dir`,
244
+ `--targets-only`, `--pipeline`.
245
+
246
+ **mlx-bun overlap.** Strong: full training stack (`src/train/` — trainer,
247
+ gradient checkpointing, segmented backward, flash-CCE), KL machinery
248
+ (`src/eval/kl.ts`), quantized-model load/save (`src/quantize/`), dataset
249
+ plumbing from the train CLI. Missing pieces are specific: (a) treating
250
+ quantized-layer `scales`/`biases` as trainable leaves (trainer is
251
+ LoRA-parameter-shaped today), (b) the KL-on-top-k distillation loss +
252
+ target-precompute pipeline, (c) f32 master-weights accumulation for bf16
253
+ models. Neither optiq nor mlx-bun has this today; optiq's trainer is mlx-lm's
254
+ tuner, so the oracle is direct.
255
+
256
+ **Port plan.** `src/train/dwq.ts` (loss + loop, reusing trainer infra),
257
+ `src/quantize/` hook to enumerate trainable quant params, target-precompute
258
+ script writing the same `{logits, indices}` safetensors shards, CLI
259
+ `mlx-bun train --method dwq` (or `mlx-bun dwq`). **Memory note for this
260
+ hardware:** always use the `--target-dir` two-phase flow (teacher pass, then
261
+ student-only training) — teacher+student resident together won't fit for
262
+ interesting models on 24/32 GB.
263
+
264
+ **Oracle / tiers.** Training-parity style gate (the mlx-lm-tuner-copy
265
+ standard): fixed seed/data/targets → first-N-step loss values match
266
+ `mlx_lm.dwq` bit-for-bit (or to bf16-accumulation tolerance, documented);
267
+ artifact quality gated by KL-vs-teacher and perplexity via existing eval,
268
+ plus "final val ≤ initial val" as upstream checks.
269
+
270
+ **Effort:** L. **Risky parts:** unfreezing quant params without disturbing
271
+ the LoRA-shaped trainer contract; matching `iterate_batches` order for the
272
+ step-parity gate; seq-len ceilings during the teacher pass on big models
273
+ (same wall as e4b training).
274
+
275
+ **Recommendation: build later — but first of the three quant tools.** It's
276
+ the one with real artifact-quality payoff, it composes with (rather than
277
+ duplicates) the L2 mixed-precision allocator (DWQ-refine an optiq-allocated
278
+ model), and it fits the training-first identity. Schedule after `sft_scope`
279
+ lands to avoid trainer churn.
280
+
281
+ ---
282
+
283
+ ## 6. `gptq`
284
+
285
+ **What it does.** GPTQ (`mlx_lm/quant/gptq.py`): accumulate per-layer input
286
+ Hessians `H = ΣxᵀX` via `Catcher` wrappers on every `nn.Linear`/
287
+ `SwitchLinear` (`gptq.py:40-49`), Cholesky-invert on the CPU stream
288
+ (`gptq.py:78-86`), then per-group error-propagating rounding
289
+ (`gptq.py:95-142`); non-GPTQ layers get `--fallback-bits` (default 6).
290
+ Architecture-agnostic (no per-model registry, unlike AWQ). Same calibration
291
+ loader as AWQ.
292
+
293
+ **CLI surface** (`gptq.py:162-199`): `--model/-m`, `--mlx-path`, `--bits`
294
+ (4), `--group-size` (64), `--fallback-bits` (6), `--fallback-group-size`
295
+ (64), `--num-samples` (-1 = all), `--sequence-length` (512), `--seed` (123).
296
+
297
+ **mlx-bun overlap.** Same as AWQ: `src/quantize/` covers bit-allocation
298
+ quality, nothing covers Hessian-conditioned rounding; optiq ships no GPTQ.
299
+ The mixed shape is interesting though: GPTQ's *output* is expressible in our
300
+ existing per-layer quantization config (`config-writer.ts` /
301
+ `quantization` config section) — only the solver is new.
302
+
303
+ **Port plan (if built).** `src/quantize/gptq.ts`: per-layer input capture
304
+ (shared instrumentation with the AWQ item — build once), Hessian accumulation,
305
+ Cholesky via mlx-c on the CPU stream (**verify `mlx_linalg_cholesky` /
306
+ `cholesky_inv` are bound in `src/mlx/` first** — if absent, that's new FFI
307
+ surface), the rounding loop, then reuse `safetensors-writer.ts`. Being
308
+ arch-agnostic, it needs no per-model registry — meaningfully cheaper than AWQ.
309
+
310
+ **Oracle / tiers.** `mlx_lm.gptq` on a small model with shared calibration
311
+ file + seed → weight-tensor parity (the algorithm is deterministic); then KL /
312
+ perplexity quality gate. L1-style, single oracle.
313
+
314
+ **Effort:** M–L (M if the capture instrumentation exists from DWQ/AWQ work
315
+ and cholesky is already bound; L otherwise). **Risky parts:** cholesky FFI
316
+ availability; the inner rounding loop's `mx.eval` cadence (upstream evals per
317
+ group element — naive porting is slow); MoE `SwitchLinear` equivalents.
318
+
319
+ **Recommendation: skip for now.** Dominated by DWQ on quality-per-effort and
320
+ by the L2 allocator on the positioning story; keep on the list only as a
321
+ follow-on if we build the shared capture instrumentation anyway.
322
+
323
+ ---
324
+
325
+ ## 7. `serve --draft-model` / `--num-draft-tokens` (speculative decoding at serve time)
326
+
327
+ This is the concrete, hand-to-an-implementer plan.
328
+
329
+ ### What mlx_lm.server does (read: `server.py`, `generate.py`)
330
+
331
+ - **Flags** (`server.py:1782-1791`): `--draft-model` (path/repo, default
332
+ None), `--num-draft-tokens` (default **3**; `mlx_lm.generate`'s default is
333
+ 2 — copy the server's 3).
334
+ - **Loading** (`ModelProvider._load`, `server.py:326-381`): draft loaded with
335
+ plain `load()`; **vocab-size mismatch is a warning, not an error**
336
+ (`:363-368`); draft path is part of `model_key`, and requests can override
337
+ per-request (`draft_model`, `num_draft_tokens` in the body,
338
+ `server.py:1164-1166`, validated ≥ 0 at `:1237`). No memory-budget check
339
+ (mlx-lm has none; we do — see below).
340
+ - **Lane routing:** `is_batchable = draft_model is None` (`server.py:371`) —
341
+ a draft model forces every request down `_serve_single`
342
+ (`server.py:922-988`), i.e. **spec decode is serial-lane-only upstream
343
+ too**. Distributed mode rejects draft models outright (`:327-332`).
344
+ - **Prompt cache composition** (`server.py:970-973`): one cache list =
345
+ target's caches with the draft's caches **appended**;
346
+ `speculative_generate_step` splits it back at `len(model.layers)`
347
+ (`generate.py:526-527`). The server's LRU prompt cache therefore stores
348
+ both models' KV per entry, keyed by a model_key that includes the draft
349
+ path.
350
+ - **The loop** (`speculative_generate_step`, `generate.py:473-654`): prefill
351
+ both models on the prompt; per round draft `n = min(remaining,
352
+ num_draft_tokens)` tokens autoregressively from the draft model (sampled
353
+ with the *same request sampler*), run **one target forward over
354
+ `[pending, ...drafts]`** (n+1 positions), sample the target per position,
355
+ and accept the longest prefix where target token == draft token — **exact
356
+ token-match acceptance, not distribution-level rejection sampling** — then
357
+ emit the target's token at the first mismatch (or the bonus token if all
358
+ accepted), and `trim_prompt_cache` both caches by the rejected count
359
+ (`:589-591`). Requires trimmable caches (`:529-533` raises otherwise);
360
+ kv-quant composes via `maybe_quantize_kv_cache` on both caches. When all
361
+ drafts accept, the last draft token is re-fed to the draft model next round
362
+ (`:645-648`). `logits_processors` are threaded through with a `prev_tokens`
363
+ replay (`:559-575`).
364
+
365
+ ### mlx-bun v1 scope and design
366
+
367
+ **Scope:** two-full-model speculative decoding on the **serial lane only**,
368
+ Gemma4 + the other ported archs, greedy and sampled; per-request overrides
369
+ deferred (server-level flag only in v1 — our single-user model makes
370
+ per-request draft switching pointless).
371
+
372
+ **1. Drafter abstraction — `src/spec/source.ts` (new).** One interface so the
373
+ mlx-lm two-model drafter, the optiq `GemmaAssistantDrafter`
374
+ (`src/spec/drafter.ts`), and later DSpark's `DflashDrafter`
375
+ (`src/spec/dspark/module-dflash.ts`) sit behind the same verify loop:
376
+
377
+ ```ts
378
+ interface DraftSource {
379
+ /** Process prompt tokens (two-model: prefill the draft model's cache;
380
+ * assistant/dflash: no-op — they read the target's state). */
381
+ prefill(ids: number[]): void;
382
+ /** Propose up to n tokens given the last committed token and the target's
383
+ * step context (last hidden slice + donor KV views — unused by the
384
+ * two-model source). */
385
+ draft(last: number, n: number, ctx: TargetStepCtx): number[];
386
+ /** kAccept of n drafts accepted; correction token emitted. Two-model:
387
+ * trim draft cache by max(n - kAccept - 1, 0) per mlx-lm's rewind rule
388
+ * (generate.py:589-591) and note the all-accepted re-feed. */
389
+ commit(n: number, kAccept: number, correction: number): void;
390
+ /** Resident bytes for admission accounting. */
391
+ readonly weightsBytes: number;
392
+ kvBytesAt(ctx: number): number;
393
+ dispose(): void;
394
+ }
395
+ ```
396
+
397
+ `TargetStepCtx = { lastHidden?: MlxArray; donors?: SharedKv; position: number }`
398
+ — produced by the serial generation loop; the two-model source ignores it,
399
+ which is exactly what lets the assistant/dflash drafters slot in later
400
+ (they're the ones that need it; `specGenerate` in `src/spec/generate.ts`
401
+ already computes both).
402
+
403
+ **2. Two-model source — `src/spec/two-model.ts` (new).** Owns a second
404
+ `RuntimeModel` (own `Weights.open`, own cache list, own config). `draft()` is
405
+ n sequential decode steps sampled with the request sampler (mirror
406
+ `_draft_generate`, `generate.py:593-601`, including `async_eval` pipelining).
407
+ Sampler parity matters: mlx-lm drafts with the same sampler as the target —
408
+ greedy drafting under a temperature>0 request is *not* parity.
409
+
410
+ **3. Verify loop — extend the serial lane.** The hook point is the serial
411
+ generation function the gateway calls (`serialRun` →
412
+ `runGeneration`, `src/server.ts:966-1001`; gateway in
413
+ `src/serve/generation-gateway.ts:89-183`). Add a speculative variant of the
414
+ decode loop (new `src/spec/serve-loop.ts`, structured like
415
+ `specGenerate` at `src/spec/generate.ts:157-232` but sampler-aware and
416
+ callback-streaming):
417
+
418
+ - per round: `drafts = source.draft(next, n, ctx)`; one target forward over
419
+ `[next, ...drafts]`; sample per position with the request sampler over
420
+ **batched** lm-head logits (this matches both oracles — mlx-lm and optiq
421
+ batch the verify head; see the header comment in `src/spec/generate.ts`);
422
+ - accept longest exact-match prefix; emit accepted tokens + correction
423
+ **through the existing onToken pipeline one at a time, in order** — the
424
+ stop-sequence state machine and detokenizer must see a burst of ≤ n+1
425
+ tokens per iteration (today's loop emits exactly 1; audit stop-match
426
+ rollback across a burst — this is the subtle part);
427
+ - trim the target caches by `n - kAccept` (all our serial caches implement
428
+ `isTrimmable()`/`trim()` — see `src/spec/generate.ts:213-220`); on a
429
+ rotating cache whose ring has wrapped, **fall back to non-speculative
430
+ decode for the rest of the generation** (upstream throws; a serve endpoint
431
+ shouldn't).
432
+
433
+ **4. Flags + loading.** `serverRuntimeFlags()` in `src/cli.ts` gains
434
+ `--draft-model <query>` (registry-resolved like the main model) and
435
+ `--num-draft-tokens` (default 3). Load the draft in `loadContext`
436
+ (`src/server.ts:177-231`) after the main model. Tokenizer check: compare
437
+ vocab sizes; warn like upstream, and additionally hard-fail if tokenizer
438
+ families differ (we can detect; upstream's silent garbage mode isn't worth
439
+ inheriting).
440
+
441
+ **5. Memory admission (`fit`) interaction.** mlx-bun refuses/limits at load
442
+ via `fit()` (`src/fit.ts:172-229`; server check `src/server.ts:1012-1022`).
443
+ With a draft: effective usable bytes for the target =
444
+ `memoryBudget − draftWeightsBytes − draftKvBytesAt(ctx)` (draft KV grows with
445
+ the same context; compute with the draft's own config via `kvBytesAt`). Call
446
+ `fit()` for the target with that reduced `usableBytes`; refuse startup if
447
+ `maxSafeContext < 1` and log the draft-adjusted max context. Also surface the
448
+ pair in `mlx-bun fit` later (nice-to-have: `fit <target> --draft <draft>`).
449
+
450
+ **6. Prompt cache composition.** Two options; take the mlx-lm-shaped one:
451
+ `PromptCache` entries for spec requests store `caches:
452
+ [...targetCaches, ...draftCaches]` and the entry namespace gains the draft id
453
+ (extend the existing adapter-ns mechanism, `src/server.ts:974`) so
454
+ non-spec/spec entries never cross. `take()`/`put()` byte accounting already
455
+ sums whatever arrays the entry holds — no change needed there.
456
+
457
+ **7. Composition rules (v1):**
458
+ - **Serial lane only** — with `--batch N` + `--draft-model`, route requests
459
+ like kv-quant does today (explicit spec ⇒ serial lane;
460
+ `willBatch` in `generation-gateway.ts:113-127` gains `hasDraft`). Upstream
461
+ behaves the same (`is_batchable = draft is None`).
462
+ - **kv-quant:** upstream composes (quantized caches are trimmable). v1: allow
463
+ `--kv-quant off|4|8` with spec; gate `config` mode behind a follow-up
464
+ (per-layer mixed configs make the trim/fallback matrix bigger).
465
+ - **Adapters/vision/logprobs:** out of scope v1 — reject the combination
466
+ loudly, revisit.
467
+ - **Perf levers:** compiled-decode replays a 1-token decode graph — the n+1
468
+ verify step won't hit it; run the spec loop eager v1 (flag interplay
469
+ documented, not silently wrong).
470
+
471
+ **8. Oracle / verification.**
472
+ - **L1 gate:** greedy, fixed prompts: token-for-token equality with
473
+ `mlx_lm.server --draft-model` / `stream_generate(draft_model=…)` from the
474
+ oracle venv, same target/draft pair. Compare **spec-vs-spec**, not
475
+ spec-vs-stock: both runtimes' spec paths batch the verify lm-head and
476
+ legitimately diverge from stock decode at bf16 knife-edges (documented in
477
+ `src/spec/generate.ts` header). Extend `scripts/oracle-spec.py` +
478
+ `scripts/spec-bench.ts` for the A/B.
479
+ - **Acceptance-rate telemetry:** emit `drafted/accepted/targetCalls` in the
480
+ response `usage`-adjacent stats (the `SpecStats` shape already exists) and
481
+ into the benchmark path.
482
+ - **Perf gate:** `./benchmark.sh`-quality numbers on the current machine
483
+ (label host/chip/RAM per the eval-DB rule): target/draft pair from the
484
+ local registry, decode tok/s spec vs non-spec at the same flags, recorded
485
+ in `benchmarks/RESULTS.md` only if reproduced on an idle machine.
486
+ - **Fallback correctness:** rotating-cache-wrap fallback test (long
487
+ generation past the sliding window keeps producing = the non-spec
488
+ continuation).
489
+
490
+ **9. Later: DSpark/assistant behind the same flag.** `--draft-model` value
491
+ dispatch by artifact: full model dir ⇒ `TwoModelSource` (L1); a
492
+ `gemma4_assistant` artifact (config `model_type` / tensor presence, as
493
+ `src/spec/drafter.ts:99-110` already sniffs) ⇒ an `AssistantSource` wrapping
494
+ `GemmaAssistantDrafter` (L2 — oracle is optiq's `spec_generate`); a DSpark
495
+ checkpoint ⇒ `DflashSource` wrapping `DflashDrafter` (L3 — KL/quality-gated,
496
+ per `docs/design/dspark-speculative-decoding.md`; its verify loop in
497
+ `src/spec/dspark/generate-dflash.ts` already has the greedy/sampling verdicts
498
+ to fold in). The serve loop, admission accounting, prompt-cache namespacing,
499
+ and stats do not change — only the source.
500
+
501
+ **Effort:** M (≈2 new files + flag/admission/prompt-cache plumbing; the
502
+ verify loop is ~200 lines with two in-repo references and a direct oracle).
503
+ **Risky parts:** multi-token bursts through the stop-sequence/detokenizer
504
+ pipeline; prompt-cache entry shape + namespacing; sampler parity in the draft
505
+ step; rotating-cache wrap fallback.
506
+
507
+ **Recommendation: build now** (first serving-side item after the in-flight
508
+ work): direct mlx-lm parity gap, big single-user win on this hardware class,
509
+ and it lays the interface DSpark needs to ever leave research.
510
+
511
+ ---
512
+
513
+ ## Proposed build order
514
+
515
+ Context: `fuse`/`convert`/`perplexity` are being built right now by another
516
+ agent; `sft_scope` is in flight. Order chosen to avoid touching the same
517
+ files as in-flight work first, and to sequence trainer-touching work after
518
+ `sft_scope`.
519
+
520
+ 1. **`upload` CLI** — S, zero-risk, unblocks publishing the artifacts the
521
+ in-flight `convert`/`fuse` work produces. (`src/cli.ts` +
522
+ `src/hf-push.ts`, no overlap with in-flight files beyond a cli.ts case.)
523
+ 2. **`serve --draft-model` (two-model, serial lane)** — M, flagship parity
524
+ feature; includes the `DraftSource` interface.
525
+ 3. **lm-eval shim for `evaluate`** — S, doc + script + a `/v1`
526
+ logprobs-echo check; declares the tool covered-via-existing.
527
+ 4. **`cache_prompt` + `generate --prompt-cache-file`** — M; KV serialization
528
+ also future-proofs prompt-cache persistence for serve.
529
+ 5. **`dwq`** — L, **after `sft_scope` lands** (both touch the trainer);
530
+ two-phase target-precompute flow mandatory on this hardware.
531
+ 6. **Assistant/DSpark sources behind `--draft-model`** — M, once (2) exists;
532
+ L2 oracle = optiq spec_generate, L3 = KL-gated.
533
+ 7. **`awq` / `gptq`** — L / M–L, parked: build only if KL evals show the
534
+ L2 mixed-precision + DWQ stack losing to community AWQ/GPTQ artifacts.
535
+ (`dynamic_quant`: covered today by `src/quantize/` — no work item.)
@@ -0,0 +1,90 @@
1
+ # oMLX adoption map — every feature, where it slots, status
2
+
3
+ Status: LIVING (started 2026-07-02). oMLX (github.com/jundot/omlx, Apache
4
+ 2.0) is the systematic port source: read its actual implementation before
5
+ building (full source ships locally in
6
+ `/Applications/oMLX.app/Contents/Resources/omlx/`), port the IDEA into our
7
+ architecture, and keep it only if OUR benchmarks improve (the burst-decode
8
+ refutation below is the cautionary tale). Attribution: note derived code in
9
+ THIRD_PARTY_LICENSES.md; idea ports get a source comment.
10
+
11
+ ## Where oMLX sits in the fidelity-tier model
12
+
13
+ It doesn't slot at L1 or L2 — it decomposes. The tiers are NUMERICS
14
+ contracts (parity-tier-dag.md); oMLX is a Python appliance ON mlx-lm:
15
+
16
+ - Its stock forwards are mlx-lm's numerics = our existing **L1 oracle**
17
+ (no new oracle value; mlx-lm itself stays the reference).
18
+ - Its own numeric inventions (oQ, custom kernels, DFlash, MTP) are
19
+ oracle-less = **L3-class** here, and get our L3 gating (KL/eval/envelope
20
+ + kill switch) that they don't have upstream.
21
+ - Its product surface (SSD cache, EnginePool, menu bar, MCP, grammar) is
22
+ **tier-agnostic serving layer** — keyed by the effective tier/scheme,
23
+ never part of the decode-numerics DAG.
24
+
25
+ ## Scoreboard
26
+
27
+ ### Ported, kept (benchmarked wins)
28
+ | Feature | Theirs | Ours | Result |
29
+ | --- | --- | --- | --- |
30
+ | Continuous batching parity | scheduler on mlx-lm BatchGenerator | `--batch N` lane + P5 SSM port + per-row logits processors (2026-07-02) | cpm5 345 vs 339 (win), e4b −3%, Qwen3.5 −1%, TTFT 2–3× better (batching-perf-path.md) |
31
+ | SSD KV cold tier | `cache/paged_ssd_cache.py` (content-hashed blocks) | `src/ssd-cache.ts` + kv-store v2 (whole-entry spill, zero-copy mmap) | restart TTFT 12.1s→0.24s vs their 1–3s; 0% decode overhead vs their ~20% (ssd-kv-cold-tier.md) |
32
+
33
+ ### Ported, refuted (do not re-add without new evidence)
34
+ | Feature | Why it doesn't transfer |
35
+ | --- | --- |
36
+ | Adaptive burst decode (`engine_core.py _step_burst`) | Amortizes Python GIL/asyncio ping-pong (~1ms/token). Bun has no GIL; faithful port REGRESSED cpm5 B=4 345→289 + TTFT +100ms. Reverted with breadcrumb (batching-perf-path.md P4). |
37
+
38
+ ### Queue (roughly by leverage; ★ = Josh explicitly wants)
39
+ 1. **Structured output / JSON-schema constrained decoding** (`api/grammar.py`)
40
+ — serving layer. The biggest API-surface hole vs both oMLX and agent
41
+ frameworks. We own the sampler loop; a token-mask grammar engine slots
42
+ beside the existing sampler extensions. Serial lane first, batch lane
43
+ via the per-row processor fold (already built).
44
+ 2. **★ Menu bar app** (`apps/omlx-mac/`, native SwiftUI + Sparkle) — product
45
+ layer. Their repo is the structural reference; our signed/notarized
46
+ single binary is the sidecar. Supersedes tauri-desktop-app.md's Electron
47
+ ambivalence: native SwiftUI is the received-well shape.
48
+ 3. **Multi-model serving / EnginePool** (`engine_pool.py`: LRU + pinning +
49
+ load/unload API) — serving layer. Fits our single-USER framing as model
50
+ SWITCHING (one active, LRU-evict the rest), not concurrent tenants.
51
+ 4. **oQ-style sensitivity-driven quantization** (`oq.py`,
52
+ docs/oQ_Quantization.md) — L3-class, lands in `convert` beside
53
+ `--target-bpw`: calibration-measured per-layer sensitivity, boosts on
54
+ non-expert tensors only, batched expert GPTQ. Gate: perplexity + 6-task
55
+ eval vs our knapsack at equal bpw. (arXiv-lens candidate.)
56
+ 5. **DFlash serving wiring** (`engine/dflash.py` + their published -DFlash
57
+ drafts) — L3-class; unblocks DSpark (docs/design/dspark-speculative-decoding.md).
58
+ Their integration pattern: separate engine class, ctx-length fallback
59
+ routing, tape-replay cache rollback.
60
+ 6. **Vision feature cache** (`cache/vision_feature_cache.py`) — serving
61
+ layer; encoder features for repeated-image agent turns. Natural sibling
62
+ of the SSD tier (same keying discipline).
63
+ 7. **MCP tool execution in serve** (`--mcp-config`, `mcp_routes.py`) —
64
+ serving layer. Note overlap: Pi already owns tool orchestration for our
65
+ UIs — scope this to bare-API consumers; don't duplicate Pi.
66
+ 8. **Prefill progress observability** (`prefill_progress.py`) — /stats +
67
+ web UI surfacing for long prompts. Pairs with the serial-lane hop fix.
68
+ 9. **Rerank endpoint** (`/v1/rerank`) — needs a reranker model family
69
+ first; park until a Qwen3-Reranker-class port is wanted.
70
+ 10. **Document ingestion (MarkItDown)** — product layer; ours would go
71
+ through the chat UI / Pi attachments, not the server.
72
+ 11. **Admin one-click benchmark with prefix-cache-hit testing** — fold into
73
+ bench-serving-load.ts as a `--cache-hit-ratio` mode instead of a UI.
74
+
75
+ ### Explicitly not porting
76
+ - **Audio (STT/TTS/STS)** — audio tower deferred (project scope).
77
+ - **Their SSE burst streaming** — side effect of the GIL workaround;
78
+ per-token streaming is strictly better UX at zero cost for us.
79
+ - **Python-side scheduler details** (GIL executors, collector reaping) —
80
+ runtime-specific.
81
+
82
+ ## Porting discipline (learned this week)
83
+ 1. Read their real source first (local app bundle) — the README lies less
84
+ than the code.
85
+ 2. Port the idea into OUR architecture; never transliterate Python.
86
+ 3. Benchmark before/after on THIS machine; keep only wins (burst decode
87
+ died here). Wall-clock metrics only — their own logs over-report.
88
+ 4. Numeric features get L3 gates + kill switches; serving features get
89
+ the effective-scheme keying; nothing touches L1/L2 contracts.
90
+ 5. Verify the served model id (/v1/models) before trusting any bench.