mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# Structured output (grammar-constrained decoding) — design
|
|
2
|
+
|
|
3
|
+
Status: LANDED (2026-07-02). Adoption-map item #1
|
|
4
|
+
(`docs/design/omlx-adoption-map.md`). L2-class (oMLX is the oracle).
|
|
5
|
+
|
|
6
|
+
## What it is
|
|
7
|
+
|
|
8
|
+
OpenAI/oMLX/vLLM-compatible `response_format` + `guided_*` fields on
|
|
9
|
+
`/v1/chat/completions` and `/v1/completions`, enforced at the sampler via
|
|
10
|
+
**grammar-constrained decoding**: a per-step token bitmask masks invalid
|
|
11
|
+
logits to `-inf` before sampling, guaranteeing the generated text is valid
|
|
12
|
+
JSON (or matches a JSON schema, regex, EBNF grammar, or enum choice).
|
|
13
|
+
|
|
14
|
+
| Field | Compiles to | Example |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| `response_format: {type:"json_object"}` | xgrammar builtin JSON grammar | any valid JSON |
|
|
17
|
+
| `response_format: {type:"json_schema", json_schema:{name,schema,strict}}` | `compileJSONSchema(schema)` | `{"name":"…","age":N}` |
|
|
18
|
+
| `guided_grammar` (EBNF/LARK) | `Grammar.fromEBNF` | `root ::= "\"" [a-z]+ "\""` |
|
|
19
|
+
| `guided_regex` | EBNF-wrapped regex | `[A-Z]{3}\d+` |
|
|
20
|
+
| `guided_choice` (enum) | `root ::= ("a"\|"b"\|"c")` | `["yes","no","maybe"]` |
|
|
21
|
+
| `structured_outputs` | bare schema (alias for json_schema.schema) | — |
|
|
22
|
+
|
|
23
|
+
From our side it's **one shape**: compile something → `GrammarMatcher` →
|
|
24
|
+
per-step bitmask → mask logits. The only difference between modes is which
|
|
25
|
+
xgrammar compile method we call. Precedence mirrors oMLX's
|
|
26
|
+
`_effective_guided_grammar`: `guided_grammar > json_schema > json_object >
|
|
27
|
+
structured_outputs > guided_regex > guided_choice`.
|
|
28
|
+
|
|
29
|
+
## Engine choice: `@mlc-ai/web-xgrammar` (WASM)
|
|
30
|
+
|
|
31
|
+
The **official MLC-ai xgrammar WASM/JS build** (Apache-2.0, same authors as
|
|
32
|
+
xgrammar). Same engine oMLX uses, just WASM-packaged. Chosen over a C/ABI
|
|
33
|
+
FFI shim because:
|
|
34
|
+
|
|
35
|
+
- **No native build.** No `libxgrammar.a` linking, no `extern "C"` shim, no
|
|
36
|
+
`build-binary.sh` changes, no signing/notarization concerns. It's an npm
|
|
37
|
+
dep that ships WASM, same as any JS dep.
|
|
38
|
+
- **Behavioral parity by construction.** Same engine → same bitmask
|
|
39
|
+
semantics, same schema→grammar compilation, same structural-tag handling.
|
|
40
|
+
- **Fast enough.** Measured per-step `getNextTokenBitmask` on a real JSON
|
|
41
|
+
walk (nanosecond timing, state-changing): 0.004–0.19 ms across Gemma 256k
|
|
42
|
+
byte_fallback / Llama-3 128k byte_level / Qwen2.5 151k byte_level. <1% of
|
|
43
|
+
a GPU decode step. The bitmask is CPU work either way (WASM vs C++ native
|
|
44
|
+
would only differ in WASM overhead, which is negligible on M-series), so
|
|
45
|
+
the C/ABI fallback was not needed. Evidence: `scripts/experiments/xgrammar-spike.ts`.
|
|
46
|
+
|
|
47
|
+
## Vocab extraction + type detection
|
|
48
|
+
|
|
49
|
+
`TokenizerInfo` is fed from data we already have. `loadVocab(tokenizer.json)`:
|
|
50
|
+
- `encoded_vocab` = the per-id decoded strings (the `model.vocab` object,
|
|
51
|
+
inverted to an ordered array).
|
|
52
|
+
- `vocabType`: `decoder.type=="ByteLevel"` → `byte_level` (Llama-3, Qwen);
|
|
53
|
+
`decoder` is a `Sequence` containing `ByteFallback` → `byte_fallback`
|
|
54
|
+
(Gemma/SentencePiece); else `raw`.
|
|
55
|
+
- `prependSpace = (vocabType == "byte_level")` (ByteLevel pretokenizers add
|
|
56
|
+
a leading space).
|
|
57
|
+
|
|
58
|
+
`TokenizerInfo` is **cached per `tokenizer.json` path** (it decodes every
|
|
59
|
+
vocab id once — expensive, but vocab-structural, not per-request).
|
|
60
|
+
`LoadedTokenizer.tokenizerJsonPath` threads the path from `loadTokenizer`.
|
|
61
|
+
|
|
62
|
+
## Integration: the pipelining tension
|
|
63
|
+
|
|
64
|
+
`GrammarMatcher.acceptToken()` needs the token id as a **JS number**,
|
|
65
|
+
which requires the sync GPU readback (`ops.itemUint32`). The decode loop's
|
|
66
|
+
pipelining defers that readback to overlap the forward; existing
|
|
67
|
+
logits-processors (repetition penalty etc.) operate on device-side token
|
|
68
|
+
arrays with no readback, so they ride the sync `LogitsProcessor` array
|
|
69
|
+
cleanly. Grammar can't.
|
|
70
|
+
|
|
71
|
+
Resolution: grammar requests take a slightly different loop shape in
|
|
72
|
+
`generate()` (`src/generate.ts`):
|
|
73
|
+
|
|
74
|
+
1. Eager-read `cur` (the just-sampled token) at the **top** of the loop.
|
|
75
|
+
2. `grammar.accept(tokenId)` — sync matcher advance + fires async
|
|
76
|
+
`getNextTokenBitmask()` (overlaps the GPU forward dispatched just below).
|
|
77
|
+
3. `await grammar.ready()` — the mask for the next step is materialized.
|
|
78
|
+
4. `sampleStep` applies `grammar.applyMask(logits)` after the standard
|
|
79
|
+
processors, before the sampler.
|
|
80
|
+
|
|
81
|
+
Non-grammar requests keep the fast pipelined loop untouched. The cost is
|
|
82
|
+
~0.1 ms/step of GPU idle during the eager readback (grammar is opt-in;
|
|
83
|
+
outputs are usually short). The mask fill (CPU WASM) overlaps the forward.
|
|
84
|
+
|
|
85
|
+
`terminateWithoutStopToken=true` on the matcher: it terminates when the
|
|
86
|
+
**grammar is satisfied** (e.g. the closing `}` of a complete JSON), not
|
|
87
|
+
only on an EOS stop token. Without this, the sampler faced an all-`-inf`
|
|
88
|
+
mask after the JSON closed (greedy argmax → garbage token 0, matcher
|
|
89
|
+
rejected, looped to max_tokens). The loop skips building the next step
|
|
90
|
+
once `isTerminated`.
|
|
91
|
+
|
|
92
|
+
## Degrade path (oMLX parity)
|
|
93
|
+
|
|
94
|
+
If grammar compile fails (malformed schema/EBNF), the request **degrades**,
|
|
95
|
+
never 500:
|
|
96
|
+
|
|
97
|
+
- `/v1/chat/completions`: inject a system message — *"You must respond with
|
|
98
|
+
valid JSON matching the '<name>' schema. JSON Schema: ```json…```"*
|
|
99
|
+
(ported from oMLX `api.tool_calling.build_json_system_prompt`) — and emit
|
|
100
|
+
a `Warning` header.
|
|
101
|
+
- `/v1/completions`: no chat template to inject into, so emit the `Warning`
|
|
102
|
+
header only (documented gap vs oMLX's text-completions behavior).
|
|
103
|
+
|
|
104
|
+
xgrammar's EBNF parser calls `abort()` on a parse error, but the WASM trap
|
|
105
|
+
surfaces as a catchable `RuntimeError: Aborted()` — the process survives,
|
|
106
|
+
WASM state is intact, and a later good grammar still compiles (verified in
|
|
107
|
+
`tests/grammar.test.ts`). The `[FATAL]/Aborted()` stderr line is xgrammar's
|
|
108
|
+
`LOG(FATAL)` noise, harmless.
|
|
109
|
+
|
|
110
|
+
## Kill switch
|
|
111
|
+
|
|
112
|
+
`MLX_BUN_GRAMMAR=0` disables grammar compilation entirely (requests degrade
|
|
113
|
+
to prompt injection). Matches the project's envelope-gated kill-switch
|
|
114
|
+
discipline (`MLX_BUN_COMPILED_DECODE`, `MLX_BUN_NO_FUSED_SDPA`, …).
|
|
115
|
+
|
|
116
|
+
## Tests
|
|
117
|
+
|
|
118
|
+
- `tests/grammar.test.ts` (8 tests, model-free — needs only a tokenizer.json):
|
|
119
|
+
step0 mask admits the expected tight set across all five request kinds;
|
|
120
|
+
accept/ready advances state; choice + EBNF + json_object + json_schema all
|
|
121
|
+
mask correctly; degrade returns null on malformed grammar (abort caught);
|
|
122
|
+
text/unset → null; applyMask keeps valid / -infs invalid.
|
|
123
|
+
- `scripts/experiments/xgrammar-spike.ts`: phase-0 perf + correctness across
|
|
124
|
+
three vocab types.
|
|
125
|
+
- `scripts/experiments/xgrammar-parity.ts`: L2 parity vs oMLX through the
|
|
126
|
+
real chat template — byte-identical content, both valid JSON.
|
|
127
|
+
|
|
128
|
+
## Known gaps / follow-ups
|
|
129
|
+
|
|
130
|
+
- **Whitespace formatting differs from oMLX.** With `anyWhitespace=true`
|
|
131
|
+
(the default both use) the grammar allows any whitespace; the model freely
|
|
132
|
+
picks indented (ours) vs compact (oMLX). Content is byte-identical; both
|
|
133
|
+
are valid JSON. A subtle version-skew (WASM 0.1.27 vs native 0.2.0) /
|
|
134
|
+
logit detail at whitespace steps. Not a correctness issue.
|
|
135
|
+
- **Whitespace STALL mode** (observed 2026-07-03, B2 bring-up): unlimited
|
|
136
|
+
whitespace means a whitespace-degenerate model can greedily emit
|
|
137
|
+
whitespace until max_tokens (CPM5-1B base + raw prompt + a boolean
|
|
138
|
+
value → tab loop after the colon, 96/96 tokens, no value emitted). Same
|
|
139
|
+
exposure as oMLX (same default); chat-tuned models rarely hit it.
|
|
140
|
+
The knob LANDED 2026-07-07 (it bit tests/batch-grammar.test.ts on the
|
|
141
|
+
M1 Max — 96/96 tabs after a key): `json_schema.any_whitespace: false`
|
|
142
|
+
compiles compact separators (no whitespace choice points). Default-off,
|
|
143
|
+
oMLX parity stays the default; server-api.md documents the field.
|
|
144
|
+
- **B2 batch gates: LANDED 2026-07-03** (`tests/batch-grammar.test.ts`,
|
|
145
|
+
`MLX_BUN_TEST_BATCH_DECODE=1`, CPM): all-grammar B=4 four-schemas
|
|
146
|
+
cross-bleed gate, mixed-batch sibling byte-match (pinned schedule),
|
|
147
|
+
early-termination + churn + joiner, mid-JSON max_tokens truncation,
|
|
148
|
+
prefill-terminated row never merges. Bring-up also hardened the F4 cache
|
|
149
|
+
(single-flight rebuild on tokenizer switch — concurrent compiles were
|
|
150
|
+
double-disposing the previous compiler). Still open from B2: the bench
|
|
151
|
+
(all-grammar vs all-free at `--batch 4`) — now part of the Phase E
|
|
152
|
+
feature-matrix benchmark (grammar-spec-batching-integration.md).
|
|
153
|
+
- **Batched lane.** LANDED (B1, 2026-07-02). The WASM build has no
|
|
154
|
+
`BatchGrammarMatcher`; the batched lane (`--batch N`) uses N individual
|
|
155
|
+
per-row matchers driven by the scheduler's `#stepGrammar` (read-before-build:
|
|
156
|
+
read pending [B] tokens, accept per row, build forward, await ready, sample,
|
|
157
|
+
emit). Terminated rows finish + evict before their slot is sampled again
|
|
158
|
+
(the all-`-inf` guarantee, per row). `MLX_BUN_GRAMMAR_BATCH=0` forces serial
|
|
159
|
+
fallback. A module-level `wasmQueue` serializes ALL xgrammar WASM calls — the
|
|
160
|
+
single-threaded WASM instance corrupts under concurrent fills/compiles
|
|
161
|
+
(BindingError, caught by the B2 test).
|
|
162
|
+
- **Structural tags for thinking models.** `compileStructuralTag` (permissive
|
|
163
|
+
during `<think>`, constraining only the output) is supported by xgrammar
|
|
164
|
+
but NOT YET WIRED. oMLX wraps the grammar in a structural tag when
|
|
165
|
+
`reasoning_parser` is set; the equivalent here needs the thinking-tag
|
|
166
|
+
format from `ctx.template`.
|
|
167
|
+
- **`/v1/responses` logprobs-style gap.** `response_format` is accepted on
|
|
168
|
+
chat + completions; the Responses API shim routes through chat so it
|
|
169
|
+
inherits support, but untested there.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
# Serial review + batched-lane plan (2026-07-02)
|
|
174
|
+
|
|
175
|
+
A full read of the serial implementation (`src/grammar.ts`, the grammar
|
|
176
|
+
branch in `src/generate.ts`, the server wiring, the spike/parity scripts,
|
|
177
|
+
and the batch engine `src/serve/{generation-gateway,batch-scheduler}.ts`)
|
|
178
|
+
before wiring grammar into `--batch N`. Findings first, then the plan.
|
|
179
|
+
|
|
180
|
+
## What's solid (verified, keep as-is)
|
|
181
|
+
|
|
182
|
+
- **Terminal-state handling.** After the grammar's final character (e.g.
|
|
183
|
+
the closing `}`) NO token is valid — the next mask is all-`-inf` and
|
|
184
|
+
greedy argmax would return garbage. The serial loop handles this
|
|
185
|
+
correctly at both ends: `terminateWithoutStopToken=true` makes the
|
|
186
|
+
matcher terminate the moment the grammar is satisfied, and the decode
|
|
187
|
+
loop skips building step n+1 when `isTerminated` (generate.ts). The
|
|
188
|
+
batched lane must replicate this **per row**: a terminated row must
|
|
189
|
+
never be sampled again (plan B1).
|
|
190
|
+
- **Perf is a measured non-issue.** Per-step `getNextTokenBitmask` on a
|
|
191
|
+
real JSON walk: 0.004–0.19 ms across 128k–256k vocabs (nanosecond
|
|
192
|
+
timing, `xgrammar-spike.ts`), ~constant regardless of state tightness;
|
|
193
|
+
the CPU-side `applyMask` host loop is inside the <1%-of-decode-step
|
|
194
|
+
envelope. At B=4 the aggregate worst case is ~0.8 ms of CPU fill work
|
|
195
|
+
that overlaps GPU compute — no device-side masking needed for v1.
|
|
196
|
+
- **The pipelining resolution.** Eager-read the sampled token, `accept()`
|
|
197
|
+
(fires the async fill), `await ready()` overlapping the next forward —
|
|
198
|
+
the right shape; the batch lane reuses it (B1).
|
|
199
|
+
- **Degrade path** (never 500, prompt injection + Warning header, WASM
|
|
200
|
+
abort is catchable) and **disposal discipline** (generate()'s finally
|
|
201
|
+
owns the controller on every exit path).
|
|
202
|
+
- **Accept-failure policy**: a matcher reject after masking can only mean
|
|
203
|
+
vocab/mask drift — warn + continue, don't kill the generation.
|
|
204
|
+
|
|
205
|
+
## Findings
|
|
206
|
+
|
|
207
|
+
**F1 — BUG (serial): stale/garbage final token at the max_tokens
|
|
208
|
+
boundary.** In generate.ts, `grammarTok` is only refreshed when
|
|
209
|
+
`generated + 1 < maxTokens`, but the token emitted is
|
|
210
|
+
`options.grammar ? grammarTok : ops.itemUint32(cur)` unconditionally. When
|
|
211
|
+
a grammar generation reaches the max_tokens cap, the LAST iteration skips
|
|
212
|
+
the refresh: the emitted token duplicates the previous step's token (or is
|
|
213
|
+
`-1` when max_tokens=1). Truncated-JSON responses end on a corrupted
|
|
214
|
+
token; `forwarded`/cacheTokens record the wrong id. Fix: always eager-read
|
|
215
|
+
`cur` when grammar is on; gate only `accept()`/`ready()` (which exist to
|
|
216
|
+
prepare the NEXT step) on "a next step will be built".
|
|
217
|
+
|
|
218
|
+
**F2 — CRITICAL (batch): grammar is silently dropped under `--batch N`.**
|
|
219
|
+
`RequestShape` has no grammar field, `GenerationGateway.willBatch()` never
|
|
220
|
+
checks it, and the batch branch of `gateway.run()` ignores
|
|
221
|
+
`options.grammar` entirely. An otherwise-batchable request with
|
|
222
|
+
`response_format` is admitted to the batch and generates UNCONSTRAINED —
|
|
223
|
+
HTTP 200, no Warning header, schema silently not enforced — and the
|
|
224
|
+
compiled `GrammarController` (WASM matcher + compiled grammar + compiler)
|
|
225
|
+
is never disposed (generate() never runs), leaking WASM state per request.
|
|
226
|
+
The design doc's "serial-lane-only under `--batch`" was intent, not code.
|
|
227
|
+
Plan B0 fixes routing immediately; B1 makes batching real.
|
|
228
|
+
|
|
229
|
+
**F3 — robustness: mask width vs padded config vocab.** `applyMask`
|
|
230
|
+
iterates the logits width V = `config.vocab_size` (padded, can exceed the
|
|
231
|
+
tokenizer vocab) while `readyMask` has the MATCHER's width. For padded ids
|
|
232
|
+
`this.readyMask[word]!` is `undefined`, and `(undefined >>> bit) & 1`
|
|
233
|
+
evaluates to 0 → `-inf`. That is the correct behavior (padding ids are
|
|
234
|
+
never valid) but it works by accident under a lying non-null assertion.
|
|
235
|
+
Make it explicit (treat out-of-range words as 0) + a comment.
|
|
236
|
+
|
|
237
|
+
**F4 — efficiency: fresh `GrammarCompiler` per request.**
|
|
238
|
+
`compileGrammarRequest` builds a new compiler every request, discarding
|
|
239
|
+
xgrammar's internal compilation cache (`createGrammarCompiler(info,
|
|
240
|
+
cacheEnabled?)` — the WASM API exposes it). Agentic clients replay the
|
|
241
|
+
same schema hundreds of times; each pays full schema→grammar compile.
|
|
242
|
+
Cache ONE compiler per TokenizerInfo (both are vocab-structural); the
|
|
243
|
+
`ownsCompiler=false` path in GrammarController already exists for this.
|
|
244
|
+
|
|
245
|
+
**F5 — gap: `guided_regex` supports only the regex∩EBNF intersection.**
|
|
246
|
+
The WASM build has NO `Grammar.fromRegex` (verified in
|
|
247
|
+
`lib/xgrammar.d.ts`: fromEBNF / fromJSONSchema / builtinJSONGrammar /
|
|
248
|
+
fromStructuralTag only). Our `root ::= ${regex}` hack accepts regexes that
|
|
249
|
+
happen to parse as xgrammar EBNF (char classes, `{m,n}`); real-world
|
|
250
|
+
regexes (`\d`, `.`, anchors, groups) hit the degrade path. Follow-up: port
|
|
251
|
+
a regex→EBNF translation (native xgrammar has `_regex_to_ebnf`) or
|
|
252
|
+
document the subset. Not a batching blocker.
|
|
253
|
+
|
|
254
|
+
**F6 — minor: `guided_choice` escaping** covers `\` and `"` only; a choice
|
|
255
|
+
containing a newline/control char breaks the EBNF string literal. Escape
|
|
256
|
+
`\n\r\t` or 400 on control chars.
|
|
257
|
+
|
|
258
|
+
**F7 — known, hotter under batch: thinking models.** The mask constrains
|
|
259
|
+
from token 0, so it forbids the `<think>` preamble. Qwen3.5 — a headline
|
|
260
|
+
`--batch` model, thinking default-on — makes this visible. The fix is
|
|
261
|
+
structural tags (`fromStructuralTag` IS in the WASM API): permissive
|
|
262
|
+
during the think span, constrained output after. Separate work item; not
|
|
263
|
+
part of the batching plan.
|
|
264
|
+
|
|
265
|
+
## The plan
|
|
266
|
+
|
|
267
|
+
### B0 — correctness stopgaps (land first, small)
|
|
268
|
+
|
|
269
|
+
1. **F1 fix** in generate.ts: under grammar, read `grammarTok =
|
|
270
|
+
ops.itemUint32(cur)` unconditionally at the top of the loop; keep
|
|
271
|
+
`accept()`/`ready()` gated on `generated + 1 < maxTokens`. Regression
|
|
272
|
+
test: grammar + max_tokens smaller than the JSON → truncated output's
|
|
273
|
+
final token is the actually-sampled token (and max_tokens=1 emits a
|
|
274
|
+
real token, not -1).
|
|
275
|
+
2. **F2 routing fix**: add `hasGrammar` to `RequestShape` (true when a
|
|
276
|
+
controller compiled — degrade-path requests have NO controller and stay
|
|
277
|
+
batchable, the injection already happened at the prompt level);
|
|
278
|
+
`willBatch()` returns false for it. This makes the design doc's claim
|
|
279
|
+
true and stops the silent non-enforcement + controller leak TODAY,
|
|
280
|
+
independent of B1's schedule.
|
|
281
|
+
3. Lane-debug: `MLX_BUN_LANE_DEBUG=1` line includes `hasGrammar`.
|
|
282
|
+
|
|
283
|
+
Exit: `--batch 4` server + `response_format` request → enforced output
|
|
284
|
+
(valid schema-conformant JSON), routed serial, controller disposed.
|
|
285
|
+
|
|
286
|
+
### B1 — per-row grammar in the batch scheduler
|
|
287
|
+
|
|
288
|
+
No `BatchGrammarMatcher` in the WASM build → N independent per-row
|
|
289
|
+
matchers, driven by the scheduler. The design maps the serial loop's three
|
|
290
|
+
grammar duties (mask-before-sample, accept-after-read,
|
|
291
|
+
terminate-without-stop) onto the batch engine's existing structures:
|
|
292
|
+
|
|
293
|
+
- **`BatchRequest.grammar?: GrammarController`** (new field). The gateway
|
|
294
|
+
passes `options.grammar` through on the batch lane and **owns disposal**
|
|
295
|
+
in a finally around `submit()` (covers resolve, reject, eviction, and
|
|
296
|
+
the whole-batch-drop error path — the scheduler uses but never owns).
|
|
297
|
+
- **Mask application** goes inside the gateway's existing per-row `sample`
|
|
298
|
+
closure: after the logits processors, before `toLogprobs` — same "grammar
|
|
299
|
+
has the final say" ordering as serial `sampleStep`. `applyMask` is sync
|
|
300
|
+
and reused as-is; the documented precondition is that the scheduler has
|
|
301
|
+
awaited `ready()` before invoking `sample` for that row.
|
|
302
|
+
- **Prefill (admission)**: token 0 is already sampled through the row's
|
|
303
|
+
closure, and the controller is primed at compile time, so the step-0
|
|
304
|
+
mask applies with zero scheduler changes. After the existing
|
|
305
|
+
`#readToken`: `accept(tok0)`; if the matcher terminated (a 1-token
|
|
306
|
+
grammar), emit + finish("stop") WITHOUT merging into the batch;
|
|
307
|
+
otherwise the fill is in flight and `ready()` is awaited before that
|
|
308
|
+
row's next sample.
|
|
309
|
+
- **`#step()` with ≥1 live grammar row** takes the read-before-build shape
|
|
310
|
+
(the serial resolution, transplanted):
|
|
311
|
+
1. Read the pending `[B]` token array NOW (one sync readback, before
|
|
312
|
+
building the next step's graph).
|
|
313
|
+
2. For each grammar row: `accept(tok_b)` — fires that row's async fill.
|
|
314
|
+
Rows whose matcher terminated are marked done: they keep their `[B]`
|
|
315
|
+
slot through the forward (one harmless KV write, exactly like today's
|
|
316
|
+
length-doomed placeholder rows) but are NOT sampled.
|
|
317
|
+
3. Build the batched forward graph (host-side; the fills overlap it).
|
|
318
|
+
4. `await ready()` on every live grammar row.
|
|
319
|
+
5. Sample per live row (closure applies the mask), asyncEval.
|
|
320
|
+
6. Emit using the values read in (1) — no second readback — via the
|
|
321
|
+
existing `#emitRows`; grammar-terminated rows emit their final token
|
|
322
|
+
and finish with `finish_reason: "stop"`, then the existing filter
|
|
323
|
+
evicts them. **This is the all-`-inf` guarantee, per row: a
|
|
324
|
+
terminated row is finished/evicted before its slot is ever sampled
|
|
325
|
+
again.**
|
|
326
|
+
Batches with NO grammar rows keep the current pipelined path
|
|
327
|
+
byte-identical (zero cost when the feature is unused).
|
|
328
|
+
- **The trade, stated:** while any grammar row is live the batch runs
|
|
329
|
+
effectively NO_PIPELINE (the readback no longer overlaps the next step's
|
|
330
|
+
GPU compute) — same math, scheduling only; bounded by the readback
|
|
331
|
+
(~0.1 ms) + fills (0.004–0.19 ms/row, overlapped with graph build).
|
|
332
|
+
Serial grammar pays the identical trade today. Rejected alternative:
|
|
333
|
+
speculative pipelining via the matcher's `rollBack(1)` (sample with a
|
|
334
|
+
stale mask, roll back on mismatch) — complexity without evidence; the
|
|
335
|
+
WASM API exposes rollBack if a measured need ever appears.
|
|
336
|
+
- **EOS / stop sequences / accept-failure**: unchanged semantics. Grammar
|
|
337
|
+
termination is an additional per-row stop source; accept-failure warns
|
|
338
|
+
and continues (serial parity).
|
|
339
|
+
- **Levers**: `MLX_BUN_GRAMMAR=0` still kills the feature (degrade
|
|
340
|
+
everywhere). New `MLX_BUN_GRAMMAR_BATCH=0` forces grammar rows back to
|
|
341
|
+
the serial lane (B0 behavior) — the A/B + kill switch for the new code,
|
|
342
|
+
house style.
|
|
343
|
+
- Seeded requests already route serial (`userSeed`), so grammar+seed
|
|
344
|
+
reproducibility is untouched.
|
|
345
|
+
|
|
346
|
+
### B2 — gates (tests + numbers)
|
|
347
|
+
|
|
348
|
+
- **Scheduler tests** (model-gated, alongside tests/batch-scheduler.test.ts):
|
|
349
|
+
- Mixed batch: 1 grammar row + free-running siblings — grammar row emits
|
|
350
|
+
valid schema JSON; siblings byte-match a no-grammar-sibling run (mask
|
|
351
|
+
isolation).
|
|
352
|
+
- All-grammar B=4 with FOUR DIFFERENT schemas — each row conforms to ITS
|
|
353
|
+
schema. This is THE bug-class gate: row↔matcher misalignment after
|
|
354
|
+
eviction/join would cross-bleed masks.
|
|
355
|
+
- Early termination + churn: a grammar row terminates mid-batch,
|
|
356
|
+
siblings continue, a joiner admits afterward; controller disposed.
|
|
357
|
+
- max_tokens truncation mid-JSON under batch (F1's regression, batch
|
|
358
|
+
flavor).
|
|
359
|
+
- Prefill-terminated row (grammar satisfied at token 0) never merges.
|
|
360
|
+
- **Model-free unit** (tests/grammar.test.ts): N matchers advanced
|
|
361
|
+
interleaved with overlapping async fills — the one novel runtime
|
|
362
|
+
assumption (concurrent per-row fills on the single WASM instance don't
|
|
363
|
+
cross-contaminate) gets cheap insurance.
|
|
364
|
+
- **Lane parity**: same greedy request through serial vs batch lane → both
|
|
365
|
+
valid + schema-conformant. Byte-equality is NOT the gate (batched
|
|
366
|
+
forward is mlx-lm-B=N-parity, not B=1-parity; the scheduler's existing
|
|
367
|
+
gate is teacher-forced/KL) — the grammar gate is validity + matcher
|
|
368
|
+
never rejects + correct finish_reasons.
|
|
369
|
+
- **Bench**: `--batch 4` all-grammar vs all-free, aggregate tok/s + TTFT
|
|
370
|
+
(cpm5 + e4b). Every perf claim gets a number on this machine; expected
|
|
371
|
+
cost <2% from the pipeline trade.
|
|
372
|
+
|
|
373
|
+
## Addendum (2026-07-02): XGrammar-2 / TVM-FFI / the WASM version gap
|
|
374
|
+
|
|
375
|
+
Investigated per Josh: [XGrammar-2](https://blog.mlc.ai/2026/05/04/xgrammar-2-fast-customizable-structured-generation)
|
|
376
|
+
(native 0.2.x, May 2026) vs our `@mlc-ai/web-xgrammar` 0.1.27 WASM.
|
|
377
|
+
|
|
378
|
+
**What XGrammar-2 adds (native):** Structural Tag v2 (composable JSON DSL:
|
|
379
|
+
sequence/tag/any_text/triggered_tags/**regex**/json_schema — uniformly
|
|
380
|
+
expresses harmony format, tool calling, reasoning channels), cross-grammar
|
|
381
|
+
caching + repetition-state compression (up to 80× compile speedup),
|
|
382
|
+
**`BatchGrammarMatcher`** (`batch_fill_next_token_bitmask` /
|
|
383
|
+
`batch_accept_token` / `batch_rollback` — a C++ **thread pool** over N
|
|
384
|
+
matchers, one call per step), `traverse_draft_tree` for speculative
|
|
385
|
+
decoding, and [TVM-FFI](https://github.com/apache/tvm-ffi)-based bindings
|
|
386
|
+
(Python/C++/Rust/JS) with **MLX support** (`pip install "xgrammar[metal]"`).
|
|
387
|
+
|
|
388
|
+
**The npm package IS behind, but less than the version number implies.**
|
|
389
|
+
[npm 0.1.27](https://www.npmjs.com/package/@mlc-ai/web-xgrammar) was
|
|
390
|
+
published 2025-12-25 (native 0.2.0 landed 2026-05-01 — that's the
|
|
391
|
+
whitespace-parity version skew). BUT the `web/` tree on current main still
|
|
392
|
+
says 0.1.27 while its `xgrammar_binding.cc` compiles against the CURRENT
|
|
393
|
+
`cpp/` core — a source rebuild today produces an XGrammar-2-core WASM.
|
|
394
|
+
The npm version number is just stale packaging.
|
|
395
|
+
|
|
396
|
+
**BatchGrammarMatcher is NOT in the WASM binding — and wouldn't help at
|
|
397
|
+
our B anyway.** Verified in `web/src/xgrammar_binding.cc` on main: no
|
|
398
|
+
batch class is bound. Its win is the C++ thread pool, and the emscripten
|
|
399
|
+
build isn't compiled with pthreads — a WASM batch call would be a serial
|
|
400
|
+
C++ loop, saving only per-call JS↔WASM overhead on fills already measured
|
|
401
|
+
at 0.004–0.19 ms. At B≤4 (≤0.8 ms/step aggregate, overlapped) there is no
|
|
402
|
+
prize. **B1's N-per-row-matchers design stands regardless of engine
|
|
403
|
+
version.**
|
|
404
|
+
|
|
405
|
+
**Two upgrade paths, decoupled from B1:**
|
|
406
|
+
|
|
407
|
+
- **U1 — rebuild web-xgrammar from current main** (emsdk + `web/build.sh`,
|
|
408
|
+
vendor the artifact). Gets the XGrammar-2 core as WASM: cross-grammar
|
|
409
|
+
cache + repetition compression (compile-time wins — F4's pain),
|
|
410
|
+
Structural Tag v2 incl. `RegexFormat` (a real F5 fix: compile
|
|
411
|
+
`guided_regex` via `fromStructuralTag({format:{type:"regex",pattern}})`
|
|
412
|
+
instead of the `root ::= regex` EBNF hack) and the triggered-tags
|
|
413
|
+
machinery F7 needs for thinking models. Also re-tests the
|
|
414
|
+
whitespace-parity gap (it was 0.1.27-vs-0.2.0 skew). Same API surface,
|
|
415
|
+
no native-build/signing implications. NOTE: the INSTALLED 0.1.27
|
|
416
|
+
typedefs already declare `RegexFormat`/`fromStructuralTag` — try F5/F7
|
|
417
|
+
against the current package first; upgrade only if the Dec-2025 core
|
|
418
|
+
predates the semantics we need.
|
|
419
|
+
- **U2 — native XGrammar-2 over the TVM-FFI stable C ABI via bun:ffi.**
|
|
420
|
+
The original "no native build" rationale is weakened by TVM-FFI (a
|
|
421
|
+
stable C ABI is exactly what we bind mlx-c through today), and it would
|
|
422
|
+
buy: real threaded batch fill, MLX-side mask application, and
|
|
423
|
+
`traverse_draft_tree` — which composes with the DSpark speculative-
|
|
424
|
+
decoding path (grammar-masking a draft tree is otherwise awkward).
|
|
425
|
+
Costs: a dylib in distribution (build-binary.sh, signing/notarization),
|
|
426
|
+
a new FFI surface. Trigger conditions, not calendar: B grows past ~8,
|
|
427
|
+
spec decode + grammar needs to compose, or agentic multi-tool schemas
|
|
428
|
+
make compile time visible despite the F4 cache. Until one fires, WASM
|
|
429
|
+
stays the engine.
|
|
430
|
+
|
|
431
|
+
### B3 — follow-ups (explicitly out of scope for the batch landing)
|
|
432
|
+
|
|
433
|
+
F4 compiler cache (biggest real-world win — agentic schema replay), F5
|
|
434
|
+
regex via structural tag `RegexFormat` (see addendum — try the installed
|
|
435
|
+
package first, else U1), F6 choice escaping, F3 explicit padded-vocab
|
|
436
|
+
handling, F7 structural tags for thinking models (batch-relevant:
|
|
437
|
+
Qwen3.5; the Structural Tag v2 machinery in the addendum is the tool),
|
|
438
|
+
U1/U2 engine upgrades (addendum), and device-side bitmask expansion
|
|
439
|
+
(upload the packed `ceil(V/32)` int32 mask ≈ 8 KB instead of host-building
|
|
440
|
+
a V-float mask ≈ 1 MB/step/row, expand with `right_shift`+`bitwise_and`
|
|
441
|
+
on device) — ONLY if the B2 bench shows the O(B·V) host loop, which the
|
|
442
|
+
measured serial numbers say it won't at B≤4.
|
|
443
|
+
|
|
444
|
+
## Addendum (2026-07-07): jump-forward decoding LANDED (opt-in, serial lane)
|
|
445
|
+
|
|
446
|
+
`MLX_BUN_GRAMMAR_JUMP=1` enables SGLang's jump-forward technique through
|
|
447
|
+
the WASM API we already ship (`GrammarMatcher.findJumpForwardString`,
|
|
448
|
+
exposed since 0.1.27 and previously uncalled): after each matcher
|
|
449
|
+
advance, if the grammar admits exactly ONE continuation string, the
|
|
450
|
+
serial decode loop retokenizes it (`tokenizer.encode(str, false)` — no
|
|
451
|
+
specials), advances the matcher per id (partial-accept keeps the
|
|
452
|
+
accepted prefix; matcher/emitted lockstep holds by construction, no
|
|
453
|
+
rollback machinery), and carries the whole burst into the KV with ONE
|
|
454
|
+
`[1, 1+m]` forward instead of m masked single-token forwards.
|
|
455
|
+
`GrammarController.jumpForward` owns the contract (src/grammar.ts);
|
|
456
|
+
generate()'s jump iteration owns the KV/history/`forwarded`/emission
|
|
457
|
+
bookkeeping.
|
|
458
|
+
|
|
459
|
+
Why opt-in (Lab, no oracle): the emitted STRING is identical and
|
|
460
|
+
validity is preserved, but encode(jumpStr) standalone may tokenize a
|
|
461
|
+
forced span differently than the model would have sampled it — the
|
|
462
|
+
token stream, and content generated after the span (conditioning), can
|
|
463
|
+
legally differ from an unjumped run. oMLX does not jump, so there is
|
|
464
|
+
nothing to be bit-exact against. Gates: string-losslessness invariants +
|
|
465
|
+
schema validity + jump-fired telemetry (`controller.jumpedTokens`),
|
|
466
|
+
tests/grammar-jump.test.ts (contract suite + Llama-3.2-1B e2e, flag
|
|
467
|
+
on/off).
|
|
468
|
+
|
|
469
|
+
Notes for the next pass:
|
|
470
|
+
- `any_whitespace:false` compiles to a FIXED pretty-printed layout
|
|
471
|
+
(measured: step-0 jump = `{\n "name": "` on the person schema) —
|
|
472
|
+
fixed formatting makes forced spans longer, so compact/fixed schemas
|
|
473
|
+
are where the flag pays.
|
|
474
|
+
- Excluded for `logprobs` requests (jumped tokens are never sampled).
|
|
475
|
+
- SP-family (byte_fallback) tokenizers whose raw encode prepends space
|
|
476
|
+
never jump — first id gets matcher-rejected, guard degrades to normal
|
|
477
|
+
decode. Structurally safe, documented in server-config.md.
|
|
478
|
+
- Batch lane (`#stepGrammar`) does NOT jump yet: per-row bursts make the
|
|
479
|
+
batched step ragged — follow-up only if serial measurements justify it.
|
|
480
|
+
- Landed alongside: a disposed-guard on the async bitmask fill
|
|
481
|
+
(`fireFill`) — disposing a controller with a queued fill used to call
|
|
482
|
+
into the deleted WASM matcher and poison the module-wide wasmChain for
|
|
483
|
+
every later grammar request (BindingError; found by the jump contract
|
|
484
|
+
tests, latent for any exception between accept() and ready()).
|
|
@@ -77,7 +77,7 @@ Every "port Lucien X" target lives under **`/Users/joshrossi/Code/lucien/scripts
|
|
|
77
77
|
```
|
|
78
78
|
- **The model seam.** `loadTaskModel(query, adapterDir)` from `src/eval/runner.ts` takes a **model selector as its FIRST arg** (a dir or registry id → `resolveModelDir`), NOT a prompt. The prompt goes to **`generateText(taskModel, body, opts)`**. So `callLocal` calls `loadTaskModel(MODEL_ID, adapterDir)` once (cached), then `generateText(tm, prompt, {maxTokens, useChat:true})`. `generateText` already does the bit-exact render→encode→trim — **never re-implement prompt→ids**. Adapter activation: `loadTaskModel` mounts AND activates the adapter (`model.loraState.active`); `generateText` re-passes `activeAdapters` each call.
|
|
79
79
|
- **The SEGMENT (chunking) adapter — real on-disk path.** The validated Gemma-4-e4b chunk SFT adapter is **`~/.cache/mlx-bun/adapters/e4b-chunk-300`** (the sweet-spot checkpoint). `orpo-e4b-chunk` is the parent run and `e4b-chunk-400` over-merged — use neither. P5-T2 symlinks `e4b-chunk-300` to `~/.cache/mlx-bun/adapters/memory-chunk` so `callLocal("chunk", …)` finds it by the `memory-<stage>` convention. The `cpm-chunk-*` / `orpo-cpm5-chunk` dirs are the abandoned MiniCPM5 runs — ignore them.
|
|
80
|
-
- **LoRA recipe (all stages).** `scripts/chunk-finetune.ts` (sft, rank 16, lr 1e-5, seq 8192, `
|
|
80
|
+
- **LoRA recipe (all stages).** `scripts/chunk-finetune.ts` (sft, rank 16, lr 1e-5, seq 8192, `ops.sdpa` not flash; no env-flag sanitization needed — the old no-vjp kernels were deleted 2026-07-05). Checkpoints picked by the task judge, **NOT val loss**.
|
|
81
81
|
- **`callLocal` stage→adapter rule.** `callLocal(stage, prompt)` selects `~/.cache/mlx-bun/adapters/memory-<stage>` **iff that dir exists**, else falls back to base `MODEL_ID` (no adapter). So a stage runs base+policy until its LoRA is trained and symlinked in — the whole plan is executable base-only through M4.
|
|
82
82
|
|
|
83
83
|
### MODEL/DATA PRECHECK (USER-ACTION fallbacks — agent must NOT download multi-GB or start GPU jobs)
|
|
@@ -396,7 +396,7 @@ Loss is masked to the assistant turn (`promptLen`). Per-stage field mapping:
|
|
|
396
396
|
## P10 — LoRA TRAINING (GATED, LATE, only on measured miss)
|
|
397
397
|
**Goal:** Train per-stage adapters ONLY where P5–P8 base+policy missed the F1/quality bar. Priority: `memory-synthesis` (PATCH/CREATE) > `memory-section` > `memory-entity` > `memory-editor`. Checkpoints by judge F1, not val loss. **All training is USER-ACTION (Josh launches; the agent never starts GPU work).**
|
|
398
398
|
|
|
399
|
-
**e4b training-recipe caveat (applies to EVERY P10 stage — Gemma-4-e4b is the base).** Beyond
|
|
399
|
+
**e4b training-recipe caveat (applies to EVERY P10 stage — Gemma-4-e4b is the base).** Beyond using `ops.sdpa` (not flash), e4b long-context training needs the **segmented-backward path** — the validated chunk adapter (`e4b-chunk-300`) was trained that way. Plain `chunk-finetune.ts` SFT at `SEQ=8192` may OOM e4b WITHOUT it. So every P10 command sets `SEG=2` (`chunk-finetune.ts` `SEG` env → `segment_size` = layers per segment; **2** is the value the validated `e4b-chunk-300` adapter was trained with — `reports/orpo-chunk-paper/research-journal.md` records `seg2`; background in `docs/design/segmented-backward-training.md`). `SEG=0` (default) disables segmented backward and is the OOM footgun for e4b.
|
|
400
400
|
|
|
401
401
|
**P10-T0 · Generalize `chunk-finetune.ts` with a `TASK` env selector** — TAG: NOW · DEPENDS-ON: none *(B3 — the trainer is env-driven and single-task today; this is the ONLY trainer change needed)*
|
|
402
402
|
- DELIVERABLE: Two changes to `scripts/chunk-finetune.ts`. **(a) Re-default the base MODEL off the ABANDONED MiniCPM5.** `resolveModel()` (`chunk-finetune.ts:19-23`) currently defaults `MODEL` (when unset) to `models--mlx-community--MiniCPM5-1B-OptiQ-4bit` — and that snapshot is on disk, so it loads SILENTLY rather than erroring. Change the unset-`MODEL` branch to resolve the Gemma-4-e4b snapshot via the PREAMBLE `config.json`-iterating rule (iterate `…models--mlx-community--gemma-4-e4b-it-OptiQ-4bit/snapshots/*`, pick the one containing `config.json`), and update the stale header comment `// MODEL defaults to MiniCPM5-1B-OptiQ-4bit` (`chunk-finetune.ts:11`) to name the e4b default. Also re-point the stale default `ADAPTER` (`chunk-finetune.ts:31`, currently `…/mlx-bun-finetunes/minicpm5-chunk-seq${SEQ}`) off the MiniCPM5 name — fold it into the `TASK`-driven default below (when `TASK` is unset, default to a neutral `…/mlx-bun-finetunes/e4b-chunk-seq${SEQ}`) so no path string still advertises MiniCPM5. **(b) Add a `TASK` env var** that ONLY changes the DEFAULT `DATA` and `ADAPTER` (the SFT path is identical across stages). Mapping: `TASK=synthesis|section|entity|editor` → `DATA` defaults to `~/.cache/mlx-bun/memory-train/<TASK>` and `ADAPTER` defaults to `~/.cache/mlx-bun/adapters/memory-<TASK>`. Explicit `MODEL`/`DATA`/`ADAPTER` env still override. No task-routing inside the trainer, no new CLI flags (Bun ignores unknown argv — the existing interface is env-only). After this, NO P10 run can silently load MiniCPM5.
|
|
@@ -409,7 +409,6 @@ Loss is masked to the assistant turn (`promptLen`). Per-stage field mapping:
|
|
|
409
409
|
TASK=synthesis DATA=~/.cache/mlx-bun/memory-train/synthesis \
|
|
410
410
|
ADAPTER=~/.cache/mlx-bun/adapters/memory-synthesis \
|
|
411
411
|
SEQ=8192 ITERS=300 RANK=16 LR=1e-5 SEG=2 \
|
|
412
|
-
MLX_BUN_PERF_KERNEL=0 MLX_BUN_FUSED_GELU=0 \
|
|
413
412
|
bun scripts/chunk-finetune.ts
|
|
414
413
|
```
|
|
415
414
|
Then `callLocal("synthesis", …)` auto-loads it. (Josh launches; the agent must not start GPU work.)
|