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.
- package/README.md +112 -20
- package/docs/design/batching-perf-path.md +232 -0
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +43 -9
- package/docs/reference/training.md +31 -12
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1077 -90
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- 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.
|