mlx-bun 0.0.7 → 0.0.9

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 (138) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-v2-plan.md +515 -0
  3. package/docs/design/bucketing-stage.md +196 -0
  4. package/docs/design/compat-cli-surface-design.md +11 -11
  5. package/docs/design/diffusion-gemma-port.md +602 -0
  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/orpo-dynamic-lambda.md +41 -0
  15. package/docs/design/orpo-training.md +35 -5
  16. package/docs/design/parallel-slots.md +12 -1
  17. package/docs/design/parity-tier-dag.md +13 -5
  18. package/docs/design/spec-decode-larger-targets.md +229 -0
  19. package/docs/design/tauri-desktop-app.md +2 -2
  20. package/docs/design/the-dreaming-handoff.md +141 -0
  21. package/docs/design/the-dreaming-master-plan.md +446 -0
  22. package/docs/design/write-pipeline-entity.md +468 -0
  23. package/docs/investigations/curve-bisector-routing.md +1 -1
  24. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  25. package/docs/investigations/dspark-handoff.md +58 -0
  26. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  27. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  28. package/docs/planning/curve-sampler-research-plan.md +331 -0
  29. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  30. package/docs/planning/pi-integration-review.md +178 -0
  31. package/docs/planning/release-notes-v0.0.9.md +122 -0
  32. package/docs/planning/web-ui-pass-plan.md +213 -0
  33. package/docs/planning/website-readme-pass-plan.md +224 -0
  34. package/docs/reference/cli.md +255 -0
  35. package/docs/reference/embedding.md +7 -2
  36. package/docs/reference/library-api.md +22 -3
  37. package/docs/reference/memory.md +5 -5
  38. package/docs/reference/models.md +137 -0
  39. package/docs/reference/orpo-quickstart.md +6 -0
  40. package/docs/reference/server-api.md +187 -9
  41. package/docs/reference/server-config.md +40 -9
  42. package/docs/reference/training.md +25 -11
  43. package/package.json +1 -1
  44. package/src/anthropic.ts +22 -0
  45. package/src/assets/curve-designer.html +1 -1
  46. package/src/chat-template.ts +25 -7
  47. package/src/cli.ts +1029 -86
  48. package/src/config.ts +54 -3
  49. package/src/diffusion/diffusion-generate.ts +436 -0
  50. package/src/download.ts +99 -8
  51. package/src/embed.ts +77 -0
  52. package/src/eval/perplexity.ts +164 -0
  53. package/src/eval/runner.ts +19 -7
  54. package/src/eval/tasks/bfcl.ts +1 -1
  55. package/src/generate.ts +196 -19
  56. package/src/harness-pi.ts +43 -17
  57. package/src/index.ts +2 -0
  58. package/src/lora.ts +2 -2
  59. package/src/memory/article.ts +558 -0
  60. package/src/memory/chunk-validation.ts +150 -0
  61. package/src/memory/chunk.ts +305 -12
  62. package/src/memory/cluster.ts +344 -17
  63. package/src/memory/coverage.ts +134 -0
  64. package/src/memory/crosslink.ts +0 -0
  65. package/src/memory/db.ts +184 -45
  66. package/src/memory/entity.ts +203 -0
  67. package/src/memory/gate.ts +187 -0
  68. package/src/memory/history.ts +152 -0
  69. package/src/memory/ingest.ts +324 -11
  70. package/src/memory/model.ts +321 -0
  71. package/src/memory/normalize.ts +671 -0
  72. package/src/memory/parse.ts +22 -0
  73. package/src/memory/pipeline.ts +242 -30
  74. package/src/memory/prompts.ts +42 -0
  75. package/src/memory/query.ts +334 -0
  76. package/src/memory/reconcile.ts +734 -0
  77. package/src/memory/redirect.ts +108 -0
  78. package/src/memory/reindex.ts +325 -0
  79. package/src/memory/resolve.ts +307 -0
  80. package/src/memory/route.ts +272 -0
  81. package/src/memory/schedule.ts +6 -4
  82. package/src/memory/stages.ts +774 -0
  83. package/src/memory/synthesize.ts +1629 -20
  84. package/src/memory/tools.ts +446 -96
  85. package/src/memory/vault.ts +462 -35
  86. package/src/memory/wikify.ts +1057 -16
  87. package/src/mlx/ffi.ts +11 -0
  88. package/src/mlx/ops.ts +113 -0
  89. package/src/model/compiled-decode.ts +43 -8
  90. package/src/model/diffusion-gemma.ts +1026 -0
  91. package/src/model/factory.ts +35 -8
  92. package/src/model/flash-attention.ts +4 -2
  93. package/src/model/fused-decode-kernel.ts +14 -7
  94. package/src/model/fused-mlp-kernel.ts +169 -0
  95. package/src/model/fused-swiglu-kernel.ts +240 -0
  96. package/src/model/gemma4-base.ts +62 -6
  97. package/src/model/gemma4.ts +33 -2
  98. package/src/model/minicpm5.ts +106 -8
  99. package/src/model/qwen3.ts +276 -0
  100. package/src/model/steel-linear-kernel.ts +96 -0
  101. package/src/model/support.ts +45 -7
  102. package/src/model/universal/archs.ts +332 -0
  103. package/src/model/universal/dense.ts +576 -0
  104. package/src/model/universal/modules.ts +163 -0
  105. package/src/model/universal/rope.ts +366 -0
  106. package/src/pi-provider.ts +1 -1
  107. package/src/pi-terminal.ts +29 -8
  108. package/src/pi-web.ts +19 -2
  109. package/src/quantize/config-writer.ts +1 -1
  110. package/src/registry.ts +215 -7
  111. package/src/responses.ts +23 -0
  112. package/src/sampler.ts +220 -9
  113. package/src/serve/batch-scheduler.ts +272 -80
  114. package/src/serve/generation-gateway.ts +90 -18
  115. package/src/server.ts +643 -50
  116. package/src/spec/drafter.ts +81 -49
  117. package/src/spec/dspark/data-dflash.ts +156 -0
  118. package/src/spec/dspark/data.ts +201 -0
  119. package/src/spec/dspark/generate-dflash.ts +193 -0
  120. package/src/spec/dspark/generate.ts +290 -0
  121. package/src/spec/dspark/loss.ts +213 -0
  122. package/src/spec/dspark/module-dflash.ts +361 -0
  123. package/src/spec/dspark/module.ts +540 -0
  124. package/src/spec/dspark/sample.ts +112 -0
  125. package/src/spec/generate.ts +40 -19
  126. package/src/status-page.html +1 -1
  127. package/src/tokenizer.ts +7 -0
  128. package/src/train/diffusion-lora.ts +216 -0
  129. package/src/train/flash-cce.ts +70 -15
  130. package/src/train/fuse.ts +243 -0
  131. package/src/train/index.ts +3 -0
  132. package/src/train/job.ts +8 -0
  133. package/src/train/loss.ts +289 -30
  134. package/src/train/prefix-shared.ts +33 -5
  135. package/src/train/segmented.ts +67 -11
  136. package/src/train/trainer.ts +92 -28
  137. package/src/vision/diffusion-vision.ts +620 -0
  138. 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.)
@@ -1,9 +1,50 @@
1
1
  # Dynamic λ controller for ORPO (adaptive preference pressure)
2
2
 
3
3
  **Status:** proposed (design only — not built). **Opened:** 2026-06-20.
4
+ **Reaffirmed + reframed by Josh 2026-07-01** (see "The band framing" below).
4
5
  **Surface:** `src/train/loss.ts` (orpoMetrics/orpoLoss), `src/train/trainer.ts`
5
6
  (orpoLoop, the λ config), the segmented ORPO classes in `src/train/segmented.ts`.
6
7
 
8
+ ## The band framing (Josh, 2026-07-01 — the product statement)
9
+
10
+ > "Vary the lambda term based on the loss or another signal — maintain the
11
+ > interest inside a band: learn preference HARDER while you are still able to
12
+ > stay coherent. A self-stabilizing AND self-discovering hyperparameter, which
13
+ > optimizes the training to get as much preference as possible while not
14
+ > resulting in degeneracy."
15
+
16
+ That is the controller's contract in one sentence: **λ is not a hyperparameter
17
+ to tune, it is a process variable to control** — push preference pressure to
18
+ the maximum the model can currently bear (self-discovering), retreat on
19
+ confirmed coherence damage (self-stabilizing), and hold the system inside the
20
+ healthy band rather than at a fixed point. Everything below (phases, AIMD/PID,
21
+ displacement decomposition) is the mechanism for that contract.
22
+
23
+ **Interplay with `sft_scope` (landed 2026-07-01):** the default chosen-NLL is
24
+ now full prompt+response. The controller's displacement signal must key on the
25
+ **response-only `lw`** (which `orpoMetrics` computes for the odds ratio in both
26
+ modes) — full-scope NLL mixes prompt modeling into the level and would mask a
27
+ crushed `logp(chosen-response)`. Instrument both, control on response-only.
28
+
29
+ ## Paper lens (2026-07-01): is this citable?
30
+
31
+ Closest prior: **β-DPO (arXiv 2407.08639)** — dynamic β for DPO, calibrated
32
+ per-batch from the reward gap. Ours differs on three axes worth claiming:
33
+ (1) **closed-loop trajectory control** (a controller tracking a non-stationary
34
+ optimum across the run) vs per-batch heuristic calibration; (2) the control
35
+ signal is **displacement-aware** — the (dLw, dLr) decomposition targets the
36
+ known ORPO/DPO failure mode directly (cf. likelihood displacement,
37
+ arXiv 2410.08847 "Unintentional Unalignment"); (3) **objective-agnostic**
38
+ actuator (ORPO λ, SimPO β/γ). The incremental P→PD→PID build IS the ablation
39
+ section. The claim to stake: *"the optimal preference pressure is
40
+ non-stationary, so a controller strictly dominates any fixed λ a sweep can
41
+ find — and a displacement-aware control law finds the coherence cliff without
42
+ a priori knowledge of it."* Referees will demand: ≥2 datasets, ≥2 model sizes,
43
+ the static-λ sweep as the baseline family (the controller must beat the BEST
44
+ constant, not λ=0.1), seeds, and the positive-control sanity run this doc
45
+ already specifies. Workshop/arXiv-preprint tier as a single-model study;
46
+ main-track needs the multi-model sweep.
47
+
7
48
  ## Motivation
8
49
 
9
50
  ORPO's loss is `L = NLL(chosen) + λ·OR`. The two terms learn on different
@@ -260,9 +260,38 @@ intra-layer MLP split, shared prompt-prefix.
260
260
 
261
261
  $$\mathcal{L}_{ORPO} = \mathcal{L}_{NLL}(y_w) + \lambda \cdot \mathcal{L}_{OR}$$
262
262
 
263
- - $\mathcal{L}_{NLL}(y_w)$ — standard SFT cross-entropy on the **chosen**
264
- response (prompt positions masked out), i.e. the negative mean response
265
- log-prob $-\ell_w$.
263
+ - $\mathcal{L}_{NLL}(y_w)$ — the SFT cross-entropy on the **chosen** sequence.
264
+ Scope is selected by **`sft_scope`** (TrainConfig `sftScope`, default
265
+ **`full`**):
266
+ - **`full`** (default; paper/TRL-faithful): the token-mean cross-entropy over
267
+ **all non-pad supervised positions — prompt AND response** (label-shifted;
268
+ `nn.CrossEntropyLoss reduction="mean"`), exactly TRL ORPOTrainer's
269
+ `chosen_nll_loss` (labels = input ids with only padding masked to −100) and
270
+ xfactlab/orpo's `outputs_pos.loss`. Computed from the **same chosen
271
+ forward** (no extra forward): the head additionally runs over the prompt
272
+ span and the two span means combine as
273
+ $\text{NLL}_{full} = -\frac{(P{-}1)\,\text{pm} + M\,\ell_w}{(P{-}1)+M}$
274
+ (`combineFullNll`, `src/train/loss.ts`).
275
+ - **`response`**: $-\ell_w$, the negative mean response log-prob — the
276
+ pre-2026-07 mlx-bun behavior (the house convention of masking the prompt),
277
+ kept **bit-exact** for reproducing older runs.
278
+
279
+ > **Resolved (2026-07-01).** The review found the then-hardcoded
280
+ > response-only scope diverged from the reference implementations (TRL /
281
+ > xfactlab/orpo supervise the full prompt+response, only padding masked).
282
+ > Decision (Josh): `sft_scope: full | response`, **default `full`** — loss
283
+ > curves are apples-to-apples with TRL/paper ORPO by default; `response`
284
+ > reproduces old runs. The odds-ratio $\ell_w/\ell_r$ terms stay
285
+ > **response-only length-normalized means in BOTH modes** — that also matches
286
+ > TRL (its `get_batch_logps` uses prompt-masked labels for the odds ratio;
287
+ > only the NLL term is full-sequence). Implemented across every ORPO path
288
+ > (naive / chunked / fused / flash-CCE / prefix-shared / all four segmented
289
+ > classes): the span-parameterized heads run once over the prompt span and
290
+ > once over the response span (disjoint — no token headed twice), keeping
291
+ > $\ell_w$ bit-identical to the `response` path. Cross-path full-scope parity
292
+ > + an independent oracle (full NLL ≙ `sftLoss` with `promptLen=1`, bf16
293
+ > class) are wired in `tests/train-orpo-fused-ce.test.ts`; loss-math units +
294
+ > exact response-scope regression pins in `tests/train-orpo.test.ts`.
266
295
  - $\mathcal{L}_{OR} = -\log \sigma(\text{log\_odds})$, the odds-ratio term:
267
296
 
268
297
  $$\text{log\_odds} = (\ell_w - \ell_r) - \big(\text{log1mexp}(\ell_w) - \text{log1mexp}(\ell_r)\big)$$
@@ -983,8 +1012,9 @@ Metal path**. Two openings stand out:
983
1012
 
984
1013
  - **Correctness:** the current MLX SOTA, `mlx-lm-lora`, ships a **non-canonical
985
1014
  ORPO** — it uses `log_sigmoid(chosen_logp − rejected_logp)` and **omits the
986
- additive SFT-NLL term** ORPO requires. Our paper-faithful loss (true log-odds
987
- + λ·NLL, length-normalized) is *already* more correct than anything shipping.
1015
+ additive SFT-NLL term** ORPO requires. Our loss (true log-odds + λ·NLL,
1016
+ length-normalized; one known scope divergence — see the note under "The
1017
+ objective") is *already* closer to the paper than anything shipping.
988
1018
  **Do not copy their loss.**
989
1019
  - **Efficiency:** a **Metal fused/chunked CE + fused LoRA** would be the first
990
1020
  fused preference loss in the MLX world, on a quantized base, with grad
@@ -18,6 +18,16 @@ scheduler that runs multiple sequences through one forward pass.
18
18
  This is a server-path feature — serving speed is the user metric.
19
19
  Direct/embedding mode stays batch=1.
20
20
 
21
+ > **v2 execution plan:** this document remains the PRIMITIVE-design record
22
+ > (masks, caches, lanes). The living gap/debt plan and sequencing for the
23
+ > batching engine is [batching-v2-plan.md](batching-v2-plan.md). Landed from
24
+ > it (2026-07-01, steps 1–3): cache-capability gate (hybrid/SSM models route
25
+ > serial), slice-before-head admit, ONE lock domain (curve `/generate`,
26
+ > `/signal`, adapter mount/unmount via `gateway.runExclusive`), per-row
27
+ > onToken failure containment, drain-on-serial-waiter (no starvation), and
28
+ > the engine-hygiene transplant (pipelined decode via asyncEval, clearCache
29
+ > 1/256 steps, chunked admit prefill interleaved with decode steps).
30
+
21
31
  ## STATUS (2026-06-14) — LIVE for full-attention models
22
32
 
23
33
  **`--batch N` now serves B>1** for full-attention models (CPM). The numerics
@@ -266,7 +276,8 @@ Harness: `tests/batched-decode-parity.test.ts` has two paths — (a)
266
276
  NOT the gate); (b) **`realBatchedGreedy` = the REAL gate**: left-pad → one
267
277
  `BatchedDecodeMaskCache` (handles prefill at offset 0 AND per-step decode) →
268
278
  batch-prefill → greedy batch-decode, trajectory compared bit-for-bit to the
269
- mlx-lm B=N oracle fixture (`tests/fixtures/batched-golden-*.json`, generated by
279
+ mlx-lm B=N oracle golden (`goldens/batched-golden-*.json`, machine-layered via
280
+ `tests/goldens.ts` — per-GPU overrides in `goldens/<machine>/` — generated by
270
281
  `scripts/gen-batched-golden.py` in the oracle venv).
271
282
 
272
283
  **CPM L1 oracle-verified 2026-06-14d:** mlx-bun's real batched greedy trajectory