chad-code 2.1.0__tar.gz → 2.2.0__tar.gz
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.
- {chad_code-2.1.0 → chad_code-2.2.0}/PKG-INFO +27 -17
- {chad_code-2.1.0 → chad_code-2.2.0}/README.md +26 -16
- {chad_code-2.1.0 → chad_code-2.2.0}/pyproject.toml +1 -1
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/__init__.py +1 -1
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/agent.py +16 -7
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/base_engine.py +3 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/cli.py +18 -22
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/completion_engine.py +1 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/engine.py +34 -4
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mlx_dflash.py +98 -2
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mlx_fastpath.py +370 -10
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mlx_qmm_mma.py +150 -54
- chad_code-2.2.0/src/chad/prism_pack.py +253 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/PKG-INFO +27 -17
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/SOURCES.txt +2 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_agent_e2e.py +33 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_cli.py +2 -2
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_engine_dflash.py +140 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_mlx_qmm_mma.py +75 -14
- chad_code-2.2.0/tests/test_prism_pack.py +374 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_warm_prefix_tiers.py +23 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/LICENSE +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/setup.cfg +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/ambient.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/atif.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/bench.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/checkpoint.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/compaction.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/config.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/diag.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/guardrails.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/ignore.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/levers.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mcp.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mcp_oauth.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/LICENSE +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/__init__.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/alignment.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/attention.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/audio.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/cache.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/conformer.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/ctc.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/parakeet.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/rnnt.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/tokenizer.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/parakeet/utils.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/prompt.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/prove.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/render.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/repomap.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/seatbelt.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/session.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/skills.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/speech.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/spill.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/syntaxgate.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/toolcall_parse.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/tools.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/tui.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad/validate.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_agent.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_agent_guards.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_ambient.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_atif.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_bench.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_checkpoint.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_cli_modes.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_compaction.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_completion_engine.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_config.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_confirm_preview.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_edit.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_engine.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_engine_kvquant.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_engine_pld_hybrid.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_engine_pld_wide.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_feel_pack.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_gate.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_ignore.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_intent.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_levers.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_log_redaction.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_matrix_bench.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_mcp.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_mcp_oauth.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_prove.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_render.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_repomap.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_repomap_polyglot.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_seatbelt.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_session.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_skills.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_speech.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_speech_tui.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_spill.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_syntaxgate.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_toolcall_dialect.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_toolcall_parse.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_tools.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_tui.py +0 -0
- {chad_code-2.1.0 → chad_code-2.2.0}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chad-code
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/nathansutton/chad
|
|
@@ -59,13 +59,13 @@ uvx chad-code # runs anywhere; the command is still `chad`
|
|
|
59
59
|
uvx chad-code prove # offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The first run asks, then downloads the model once (~
|
|
62
|
+
The first run asks, then downloads the model once (~8 GB) into the shared Hugging
|
|
63
63
|
Face cache. While it downloads, `cd` into a project and think of a scoped first ask:
|
|
64
64
|
*"fix the failing test in `tests/test_x.py`"* lands, *"improve my codebase"* flails.
|
|
65
65
|
|
|
66
66
|
chad targets 24 GB and nothing smaller. It runs below that and tells you it is doing so,
|
|
67
|
-
but
|
|
68
|
-
window too small to work in.
|
|
67
|
+
but 8 GB of weights and drafter plus a ~4 GB prefill transient sit resident before a single
|
|
68
|
+
token of context, so a 16 GB Mac gets a window too small to work in.
|
|
69
69
|
|
|
70
70
|
The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
|
|
71
71
|
|
|
@@ -79,9 +79,9 @@ The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
|
|
|
79
79
|
## Same model, same Mac, stock engine
|
|
80
80
|
|
|
81
81
|
What do you gain over pointing a generic local-model tool at the same weights?
|
|
82
|
-
Qwen3.8-27B at the
|
|
83
|
-
|
|
84
|
-
resident at a time, each measured with its own benchmark on a 512-token prompt and a
|
|
82
|
+
Qwen3.8-27B at the `UD-Q3_K_XL` recipe (Unsloth's GGUF for llama.cpp, chad's MLX
|
|
83
|
+
conversion of the same bit map; chad runs it with `--model`), the same M4 Pro (24 GB), one
|
|
84
|
+
engine resident at a time, each measured with its own benchmark on a 512-token prompt and a
|
|
85
85
|
128-token generation.
|
|
86
86
|
|
|
87
87
|
| Engine | Prefill (512-tok prompt) | Decode (128 tok) | Speculative decoding |
|
|
@@ -90,11 +90,17 @@ resident at a time, each measured with its own benchmark on a 512-token prompt a
|
|
|
90
90
|
| llama.cpp `llama-server` (build 10917), serial | 97 tok/s | 11.3 tok/s | off |
|
|
91
91
|
| llama.cpp `llama-server` (build 10917) | 95 tok/s | 11.1 tok/s² | DFlash2 drafter (Q4_K_M GGUF) |
|
|
92
92
|
| **chad**, serial (`CHAD_NO_DFLASH=1`) | 100 tok/s | 17.9 tok/s | off |
|
|
93
|
-
| **chad
|
|
93
|
+
| **chad** | 101 tok/s | **62.9 tok/s**¹ | DFlash2 block drafter |
|
|
94
94
|
|
|
95
95
|
A 200-token function body takes roughly 18 seconds at 10.9 tok/s and 3 at 63. You wait for
|
|
96
96
|
the first one and you talk to the second.
|
|
97
97
|
|
|
98
|
+
The weights chad ships by default are the same model in Prism ML's **ternary** build
|
|
99
|
+
(every projection Hadamard-rotated and stored at 2 bits): 7.2 GB resident instead of 12.3,
|
|
100
|
+
the same 64 tok/s drafted and 21 serial on this Mac, and the 5 GB it gives back is
|
|
101
|
+
context, roughly a **150k-token window** where the 3-bit landed near 56k. The like-for-like
|
|
102
|
+
table above stays on the 3-bit recipe because that is the quant llama.cpp has a GGUF of.
|
|
103
|
+
|
|
98
104
|
Ollama does not get its own row: it is llama.cpp underneath, measured without speculative
|
|
99
105
|
decoding, and on the same GGUF (0.32.15, Modelfile `FROM` only) it measures 96 tok/s
|
|
100
106
|
prefill and the same **10.9** decode.
|
|
@@ -202,20 +208,24 @@ chad ships exactly one, downloaded once into the shared Hugging Face cache
|
|
|
202
208
|
|
|
203
209
|
| Model | Quant | Footprint |
|
|
204
210
|
|---|---|---|
|
|
205
|
-
| [Qwen3.8-27B `
|
|
211
|
+
| [Qwen3.8-27B `Ternary-Bonsai-2-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-Ternary-Bonsai-2-DFlash2-MLX) (default) | Prism ML's ternary build: Hadamard-rotated, 2-bit group-128, levels {−s, 0, +s}; bundled 4-bit DFlash2 drafter | ~8 GB resident, 262k native context, ~150k usable on 24 GB |
|
|
212
|
+
| [Qwen3.8-27B `UD-Q3_K_XL-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-UD-Q3_K_XL-DFlash2-MLX) via `--model` | 3-bit group-64 body, 5-bit `lm_head`, the same bundled drafter | ~13 GB resident, ~56k usable on 24 GB |
|
|
206
213
|
|
|
207
214
|
Qwen3.8-27B is **dense** (64 layers: 48 GatedDeltaNet + 16 full attention), so every
|
|
208
|
-
parameter is on the critical path for every token and the quant is where decode speed
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
parameter is on the critical path for every token and the quant is where both decode speed
|
|
216
|
+
and context come from: the governor prices a gigabyte of weights at about 29k tokens of
|
|
217
|
+
window. The ternary build is Prism ML's [Bonsai](https://huggingface.co/prism-ml/Ternary-Bonsai-2-27B-mlx-2bit)
|
|
218
|
+
pack of this checkpoint, repacked text-only with the base tokenizer and the drafter
|
|
219
|
+
bundled (Apache-2.0; created using Bonsai by Prism ML). Its projections are stored in a
|
|
220
|
+
rotated basis that an ordinary MLX loader would silently get wrong, so chad carries its own
|
|
221
|
+
loader for it and fits the decode fast-path, the verify kernel and the drafter to it. What it
|
|
222
|
+
costs: on code, teacher-forced perplexity is 4.49 against the 3-bit's 3.99 (+12%), while
|
|
223
|
+
the private eval tiers tie at 56/56. The 3-bit stays one flag away.
|
|
214
224
|
|
|
215
225
|
`--model <repo or local dir>` runs different weights through the same engine and stays a
|
|
216
226
|
first-class escape hatch. The drafter, the fused-attention coverage, the decode fastpath and
|
|
217
|
-
the context governor are all fitted to the
|
|
218
|
-
they do not break.
|
|
227
|
+
the context governor are all fitted to the two checkpoints above, so other weights run
|
|
228
|
+
slower; they do not break.
|
|
219
229
|
|
|
220
230
|
## Installing & upgrading
|
|
221
231
|
|
|
@@ -23,13 +23,13 @@ uvx chad-code # runs anywhere; the command is still `chad`
|
|
|
23
23
|
uvx chad-code prove # offline smoke test: 4 tiny fix-it tasks, verified, timed 🗿
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
The first run asks, then downloads the model once (~
|
|
26
|
+
The first run asks, then downloads the model once (~8 GB) into the shared Hugging
|
|
27
27
|
Face cache. While it downloads, `cd` into a project and think of a scoped first ask:
|
|
28
28
|
*"fix the failing test in `tests/test_x.py`"* lands, *"improve my codebase"* flails.
|
|
29
29
|
|
|
30
30
|
chad targets 24 GB and nothing smaller. It runs below that and tells you it is doing so,
|
|
31
|
-
but
|
|
32
|
-
window too small to work in.
|
|
31
|
+
but 8 GB of weights and drafter plus a ~4 GB prefill transient sit resident before a single
|
|
32
|
+
token of context, so a 16 GB Mac gets a window too small to work in.
|
|
33
33
|
|
|
34
34
|
The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
|
|
35
35
|
|
|
@@ -43,9 +43,9 @@ The PyPI package is `chad-code`. Bare `chad` is an unrelated squatted package.
|
|
|
43
43
|
## Same model, same Mac, stock engine
|
|
44
44
|
|
|
45
45
|
What do you gain over pointing a generic local-model tool at the same weights?
|
|
46
|
-
Qwen3.8-27B at the
|
|
47
|
-
|
|
48
|
-
resident at a time, each measured with its own benchmark on a 512-token prompt and a
|
|
46
|
+
Qwen3.8-27B at the `UD-Q3_K_XL` recipe (Unsloth's GGUF for llama.cpp, chad's MLX
|
|
47
|
+
conversion of the same bit map; chad runs it with `--model`), the same M4 Pro (24 GB), one
|
|
48
|
+
engine resident at a time, each measured with its own benchmark on a 512-token prompt and a
|
|
49
49
|
128-token generation.
|
|
50
50
|
|
|
51
51
|
| Engine | Prefill (512-tok prompt) | Decode (128 tok) | Speculative decoding |
|
|
@@ -54,11 +54,17 @@ resident at a time, each measured with its own benchmark on a 512-token prompt a
|
|
|
54
54
|
| llama.cpp `llama-server` (build 10917), serial | 97 tok/s | 11.3 tok/s | off |
|
|
55
55
|
| llama.cpp `llama-server` (build 10917) | 95 tok/s | 11.1 tok/s² | DFlash2 drafter (Q4_K_M GGUF) |
|
|
56
56
|
| **chad**, serial (`CHAD_NO_DFLASH=1`) | 100 tok/s | 17.9 tok/s | off |
|
|
57
|
-
| **chad
|
|
57
|
+
| **chad** | 101 tok/s | **62.9 tok/s**¹ | DFlash2 block drafter |
|
|
58
58
|
|
|
59
59
|
A 200-token function body takes roughly 18 seconds at 10.9 tok/s and 3 at 63. You wait for
|
|
60
60
|
the first one and you talk to the second.
|
|
61
61
|
|
|
62
|
+
The weights chad ships by default are the same model in Prism ML's **ternary** build
|
|
63
|
+
(every projection Hadamard-rotated and stored at 2 bits): 7.2 GB resident instead of 12.3,
|
|
64
|
+
the same 64 tok/s drafted and 21 serial on this Mac, and the 5 GB it gives back is
|
|
65
|
+
context, roughly a **150k-token window** where the 3-bit landed near 56k. The like-for-like
|
|
66
|
+
table above stays on the 3-bit recipe because that is the quant llama.cpp has a GGUF of.
|
|
67
|
+
|
|
62
68
|
Ollama does not get its own row: it is llama.cpp underneath, measured without speculative
|
|
63
69
|
decoding, and on the same GGUF (0.32.15, Modelfile `FROM` only) it measures 96 tok/s
|
|
64
70
|
prefill and the same **10.9** decode.
|
|
@@ -166,20 +172,24 @@ chad ships exactly one, downloaded once into the shared Hugging Face cache
|
|
|
166
172
|
|
|
167
173
|
| Model | Quant | Footprint |
|
|
168
174
|
|---|---|---|
|
|
169
|
-
| [Qwen3.8-27B `
|
|
175
|
+
| [Qwen3.8-27B `Ternary-Bonsai-2-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-Ternary-Bonsai-2-DFlash2-MLX) (default) | Prism ML's ternary build: Hadamard-rotated, 2-bit group-128, levels {−s, 0, +s}; bundled 4-bit DFlash2 drafter | ~8 GB resident, 262k native context, ~150k usable on 24 GB |
|
|
176
|
+
| [Qwen3.8-27B `UD-Q3_K_XL-DFlash2`](https://huggingface.co/nathansutton/Qwen3.8-27B-UD-Q3_K_XL-DFlash2-MLX) via `--model` | 3-bit group-64 body, 5-bit `lm_head`, the same bundled drafter | ~13 GB resident, ~56k usable on 24 GB |
|
|
170
177
|
|
|
171
178
|
Qwen3.8-27B is **dense** (64 layers: 48 GatedDeltaNet + 16 full attention), so every
|
|
172
|
-
parameter is on the critical path for every token and the quant is where decode speed
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
parameter is on the critical path for every token and the quant is where both decode speed
|
|
180
|
+
and context come from: the governor prices a gigabyte of weights at about 29k tokens of
|
|
181
|
+
window. The ternary build is Prism ML's [Bonsai](https://huggingface.co/prism-ml/Ternary-Bonsai-2-27B-mlx-2bit)
|
|
182
|
+
pack of this checkpoint, repacked text-only with the base tokenizer and the drafter
|
|
183
|
+
bundled (Apache-2.0; created using Bonsai by Prism ML). Its projections are stored in a
|
|
184
|
+
rotated basis that an ordinary MLX loader would silently get wrong, so chad carries its own
|
|
185
|
+
loader for it and fits the decode fast-path, the verify kernel and the drafter to it. What it
|
|
186
|
+
costs: on code, teacher-forced perplexity is 4.49 against the 3-bit's 3.99 (+12%), while
|
|
187
|
+
the private eval tiers tie at 56/56. The 3-bit stays one flag away.
|
|
178
188
|
|
|
179
189
|
`--model <repo or local dir>` runs different weights through the same engine and stays a
|
|
180
190
|
first-class escape hatch. The drafter, the fused-attention coverage, the decode fastpath and
|
|
181
|
-
the context governor are all fitted to the
|
|
182
|
-
they do not break.
|
|
191
|
+
the context governor are all fitted to the two checkpoints above, so other weights run
|
|
192
|
+
slower; they do not break.
|
|
183
193
|
|
|
184
194
|
## Installing & upgrading
|
|
185
195
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# import name, and command name are independent. `uvx chad-code` runs the alias
|
|
5
5
|
# script added under [project.scripts].
|
|
6
6
|
name = "chad-code"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Claude-Code-style coding agent that runs entirely on a 24 GB Apple Silicon Mac (MLX, Qwen3.8-27B)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -4,7 +4,7 @@ A flat collection of cooperating modules behind one console script (``chad``):
|
|
|
4
4
|
the inference engine, the tool layer, the agent loop, and the terminal UI.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "2.
|
|
7
|
+
__version__ = "2.2.0"
|
|
8
8
|
|
|
9
9
|
# chad sets no MLX_* runtime vars. MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER
|
|
10
10
|
# and MLX_MAX_MB_PER_BUFFER were each measured end-to-end on the 35B and every
|
|
@@ -753,6 +753,12 @@ class Agent:
|
|
|
753
753
|
self._split_ok = ok
|
|
754
754
|
return ok
|
|
755
755
|
|
|
756
|
+
def _template_extra(self) -> dict:
|
|
757
|
+
"""The template kwargs beyond messages/tools/thinking: the reasoning effort,
|
|
758
|
+
when one is set (the user's, else the engine's loader-carried default)."""
|
|
759
|
+
effort = self.reasoning_effort or self.engine.reasoning_effort_default
|
|
760
|
+
return {"reasoning_effort": effort} if effort else {}
|
|
761
|
+
|
|
756
762
|
def _render(self, thinking: bool = None):
|
|
757
763
|
# `thinking` overrides self.thinking for THIS render only (no-think
|
|
758
764
|
# escalation renders one step with <think> off, then restores). None => self.thinking.
|
|
@@ -772,13 +778,14 @@ class Agent:
|
|
|
772
778
|
# malformed TypeScript it cannot repair. `low` keeps the reasoning block and
|
|
773
779
|
# shrinks it. Passed only when set, and via **kwargs, so templates that do not
|
|
774
780
|
# accept the argument (q3_s6's, which has no reasoning_effort at all) render
|
|
775
|
-
# byte-identically to before.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
781
|
+
# byte-identically to before. A loader may carry a default of its own
|
|
782
|
+
# (engine.reasoning_effort_default) for a template whose default it disagrees
|
|
783
|
+
# with; the user's setting still wins. `_prefix_ids` renders with the same
|
|
784
|
+
# kwargs: the effort lands in the system block, so a prefix priced without
|
|
785
|
+
# it would not be a prefix of the prompt.
|
|
779
786
|
ids = self._template_ids(self.engine.tok.apply_chat_template(
|
|
780
787
|
messages, tools=self._active_schemas(), add_generation_prompt=True,
|
|
781
|
-
enable_thinking=thinking, **
|
|
788
|
+
enable_thinking=thinking, **self._template_extra(),
|
|
782
789
|
))
|
|
783
790
|
# Debug hook (env-gated, off by default): dump the first decoded render so a
|
|
784
791
|
# rendered-prompt difference across environments can be diffed. Best-effort.
|
|
@@ -808,7 +815,8 @@ class Agent:
|
|
|
808
815
|
def render1(u):
|
|
809
816
|
return self._template_ids(self.engine.tok.apply_chat_template(
|
|
810
817
|
[sysm, {"role": "user", "content": u}], tools=schemas,
|
|
811
|
-
add_generation_prompt=True, enable_thinking=self.thinking
|
|
818
|
+
add_generation_prompt=True, enable_thinking=self.thinking,
|
|
819
|
+
**self._template_extra()))
|
|
812
820
|
a, b = render1("a"), render1("the quick brown fox jumps")
|
|
813
821
|
n = 0
|
|
814
822
|
for x, y in zip(a, b):
|
|
@@ -835,7 +843,8 @@ class Agent:
|
|
|
835
843
|
def render1(m):
|
|
836
844
|
return self._template_ids(self.engine.tok.apply_chat_template(
|
|
837
845
|
[m, {"role": "user", "content": "a"}], tools=schemas,
|
|
838
|
-
add_generation_prompt=True, enable_thinking=self.thinking
|
|
846
|
+
add_generation_prompt=True, enable_thinking=self.thinking,
|
|
847
|
+
**self._template_extra()))
|
|
839
848
|
a, b = render1({"role": "system", "content": static}), render1(sysm)
|
|
840
849
|
n = 0
|
|
841
850
|
for x, y in zip(a, b):
|
|
@@ -168,6 +168,9 @@ class BaseEngine(Protocol):
|
|
|
168
168
|
effective_ctx: int # usable context window (tokens)
|
|
169
169
|
cache_dir: Optional[str] # on-disk KV checkpoint dir; None disables warm-start
|
|
170
170
|
_cached_ids: list # tokens currently resident in the prefix cache
|
|
171
|
+
# A chat-template `reasoning_effort` the loaded weights want passed when the user
|
|
172
|
+
# set none (a loader that disagrees with its template's default); None = template's.
|
|
173
|
+
reasoning_effort_default: Optional[str]
|
|
171
174
|
|
|
172
175
|
# --- generation ------------------------------------------------------
|
|
173
176
|
def generate(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""chad — a local, MLX-backed, Claude-Code-style coding agent.
|
|
3
3
|
|
|
4
|
-
One model (Qwen3.8-27B,
|
|
4
|
+
One model (Qwen3.8-27B, ternary, with its DFlash2 drafter), one entrypoint, run with uv:
|
|
5
5
|
|
|
6
6
|
uv run chad # interactive full-screen TUI
|
|
7
7
|
uv run chad "fix the bug in greet.py" # one-shot, headless
|
|
@@ -38,31 +38,27 @@ if TYPE_CHECKING:
|
|
|
38
38
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
39
39
|
_PROJECT_ROOT = os.path.dirname(os.path.dirname(_HERE))
|
|
40
40
|
|
|
41
|
-
# The shipped model, on Hugging Face.
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
# discoverability. The quant itself is MLX group-64 affine, not llama.cpp Q3_K
|
|
45
|
-
# k-quants — the model card says so; the tag is for recognition, not bit-for-bit
|
|
46
|
-
# equivalence. The repo also carries the DFlash2 block drafter, pre-quantized, in
|
|
47
|
-
# `dflash/` (~1.1 GB): one download gets the model and its speculative decoder,
|
|
48
|
-
# with nothing built on first run (mlx_dflash.py).
|
|
41
|
+
# The shipped model, on Hugging Face. The repo carries the weights AND the DFlash2
|
|
42
|
+
# block drafter, pre-quantized, in `dflash/` (~1.1 GB): one download gets the model
|
|
43
|
+
# and its speculative decoder, with nothing built on first run (mlx_dflash.py).
|
|
49
44
|
#
|
|
50
45
|
# Qwen3.8-27B is `qwen3_5` — DENSE (64 layers: 48 GatedDeltaNet + 16 full attention),
|
|
51
46
|
# so every parameter is on the critical path for every token and shrinking the model
|
|
52
|
-
# is the only decode lever there is. The
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
_HF_MODEL = "nathansutton/Qwen3.8-27B-
|
|
47
|
+
# is the only decode lever there is. The shipped weights are Prism ML's ternary build:
|
|
48
|
+
# every projection Hadamard-rotated offline and stored as 2-bit affine group-128 whose
|
|
49
|
+
# three levels are {-s, 0, +s}, ~7.2 GB resident against the 3-bit recipe's 12.1 — and
|
|
50
|
+
# on a dense model those 5 GB are context, at the governor's measured 34,816 B/token.
|
|
51
|
+
# The rotation has to be undone on the activations at runtime (prism_pack.py); the
|
|
52
|
+
# repo is our repack of the pack with the base tokenizer and the drafter bundled.
|
|
53
|
+
_HF_MODEL = "nathansutton/Qwen3.8-27B-Ternary-Bonsai-2-DFlash2-MLX"
|
|
59
54
|
# A dev clone that already built the weights locally should use them rather than
|
|
60
55
|
# re-download — prefer this dir when present.
|
|
61
|
-
_LOCAL_MODEL = os.path.join(_PROJECT_ROOT, "models", "Qwen3.8-27B-
|
|
56
|
+
_LOCAL_MODEL = os.path.join(_PROJECT_ROOT, "models", "Qwen3.8-27B-Ternary-Bonsai-2")
|
|
62
57
|
# chad targets 24 GB Apple Silicon and nothing smaller. Below this the model still
|
|
63
|
-
# loads, but the context governor has
|
|
64
|
-
#
|
|
65
|
-
# We warn and proceed rather than refuse: the harness advises, the caller
|
|
58
|
+
# loads, but the context governor has little left to spend after ~7.2 GB of weights,
|
|
59
|
+
# the ~1.1 GB drafter and the ~4.3 GB prefill transient, so the window shrinks toward
|
|
60
|
+
# its floor. We warn and proceed rather than refuse: the harness advises, the caller
|
|
61
|
+
# decides.
|
|
66
62
|
_MIN_RAM_GB = 23.5
|
|
67
63
|
|
|
68
64
|
|
|
@@ -487,10 +483,10 @@ def _pick_model(spec=None, *, host: Host = HOST, local_model: str = _LOCAL_MODEL
|
|
|
487
483
|
|
|
488
484
|
def _model_download_gb(model_id):
|
|
489
485
|
"""Approximate download size in GiB for the shipped model (for the disk preflight
|
|
490
|
-
and the confirm prompt — display honesty, not accounting): ~
|
|
486
|
+
and the confirm prompt — display honesty, not accounting): ~7.2 GB of weights
|
|
491
487
|
plus the ~1.1 GB bundled DFlash2 drafter. An arbitrary `--model` is unknowable
|
|
492
488
|
ahead of the resolve, so it gets the same figure."""
|
|
493
|
-
return
|
|
489
|
+
return 8.3
|
|
494
490
|
|
|
495
491
|
|
|
496
492
|
def _cached_weights_complete(
|
|
@@ -142,6 +142,7 @@ class CompletionEngine:
|
|
|
142
142
|
self.timeout = timeout
|
|
143
143
|
# 0 = auto: load() asks the server (/props n_ctx) so chad's window matches
|
|
144
144
|
# the wall the server actually enforces; falls back to 32768 offline.
|
|
145
|
+
self.reasoning_effort_default = None
|
|
145
146
|
self.effective_ctx = effective_ctx
|
|
146
147
|
# tokenizer_id points at the HF repo whose tokenizer matches the served GGUF's
|
|
147
148
|
# vocab (the served model_id is usually a GGUF repo with no tokenizer files).
|
|
@@ -518,6 +518,9 @@ class Engine:
|
|
|
518
518
|
model: Any = field(init=False, default=None)
|
|
519
519
|
tok: Any = field(init=False, default=None)
|
|
520
520
|
effective_ctx: int = field(init=False, default=32768)
|
|
521
|
+
# A chat-template default the loaded weights need overridden (the Prism pack's
|
|
522
|
+
# template says xhigh where the shipped model's says medium); None = the template's.
|
|
523
|
+
reasoning_effort_default: Optional[str] = field(init=False, default=None)
|
|
521
524
|
_cache: Any = field(init=False, default=None)
|
|
522
525
|
_cached_ids: list = field(init=False, default_factory=list)
|
|
523
526
|
# Token ids generated in the CURRENT turn, for the presence penalty. Reset at
|
|
@@ -541,6 +544,8 @@ class Engine:
|
|
|
541
544
|
# dense — the MoE arm is here for `--model`.
|
|
542
545
|
_is_moe: bool = field(init=False, default=False)
|
|
543
546
|
_dflash: Any = field(init=False, default=None)
|
|
547
|
+
fastpath: bool = field(init=False, default=False) # mlx_fastpath installed
|
|
548
|
+
_dflash_ladder: Any = field(init=False, default=None) # learned round-cost seed
|
|
544
549
|
_n_attn_heads: int = field(init=False, default=16)
|
|
545
550
|
_n_kv_heads: int = field(init=False, default=0)
|
|
546
551
|
_head_dim: int = field(init=False, default=0)
|
|
@@ -624,7 +629,11 @@ class Engine:
|
|
|
624
629
|
# dense qwen3_5 hybrid; silent no-op on any other model or on failure.
|
|
625
630
|
from . import mlx_fastpath
|
|
626
631
|
_log_mlx_provenance()
|
|
627
|
-
mlx_fastpath.install(self.model, model_path=path)
|
|
632
|
+
self.fastpath = mlx_fastpath.install(self.model, model_path=path)
|
|
633
|
+
if not self.fastpath and not config.flag("CHAD_NO_FASTPATH"):
|
|
634
|
+
# install() says why when it declines a Prism pack or fails outright;
|
|
635
|
+
# this is the line that makes a stock-graph run visible at all.
|
|
636
|
+
log.info("FASTPATH not installed: decoding on the stock op graph")
|
|
628
637
|
# Fused quantized-KV decode attention: makes kv_bits=8 a speed win
|
|
629
638
|
# instead of a loss. Patches mlx_lm's quantized SDPA branch
|
|
630
639
|
# only; inert unless a QuantizedKVCache is actually in play.
|
|
@@ -662,9 +671,26 @@ class Engine:
|
|
|
662
671
|
does, including the tokenizer's stop ids from the model config (which folds in
|
|
663
672
|
generation_config.json; the override never touches them)."""
|
|
664
673
|
model_path = _download(path)
|
|
665
|
-
|
|
666
|
-
|
|
674
|
+
cfg = load_config(model_path)
|
|
675
|
+
eos = cfg.get("eos_token_id")
|
|
676
|
+
# Prism's Hadamard-folded ternary packs carry weights in a rotated basis and
|
|
677
|
+
# declare their own model_type. mlx-lm's affine loader would find the right
|
|
678
|
+
# shapes, skip the activation transform and return garbage without erroring,
|
|
679
|
+
# so they route to chad's own loader on the declared type.
|
|
680
|
+
from . import prism_pack
|
|
681
|
+
prism = prism_pack.is_prism_pack(cfg)
|
|
682
|
+
with prism_pack.quiet_tokenizer_load() if prism else contextlib.nullcontext():
|
|
683
|
+
self.tok = load_tokenizer(model_path, eos_token_ids=eos)
|
|
667
684
|
override, self.effective_ctx = self._ctx_override(path)
|
|
685
|
+
if prism:
|
|
686
|
+
if override:
|
|
687
|
+
cfg = {**cfg, "text_config": {**cfg["text_config"], **override}}
|
|
688
|
+
self.model, _ = prism_pack.load(str(model_path), cfg)
|
|
689
|
+
self.reasoning_effort_default = prism_pack.REASONING_EFFORT_DEFAULT
|
|
690
|
+
# The speculative schedule's round-cost seed is per weight width.
|
|
691
|
+
from . import mlx_dflash
|
|
692
|
+
self._dflash_ladder = list(mlx_dflash.round_costs(prism_pack.BITS))
|
|
693
|
+
return
|
|
668
694
|
self.model, _ = load_model(model_path, model_config=override)
|
|
669
695
|
|
|
670
696
|
def _read_model_shape(self, path: str) -> None:
|
|
@@ -2552,6 +2578,8 @@ class Engine:
|
|
|
2552
2578
|
stats.generated_tokens = len(out_ids)
|
|
2553
2579
|
stats.gen_ids = list(out_ids)
|
|
2554
2580
|
self._cached_ids = fed_ids
|
|
2581
|
+
if policy is not None:
|
|
2582
|
+
self._dflash_ladder = policy.learned_seed()
|
|
2555
2583
|
mx.clear_cache()
|
|
2556
2584
|
return detok.text, stats
|
|
2557
2585
|
except BaseException:
|
|
@@ -2597,7 +2625,9 @@ class _DFlashDrafter:
|
|
|
2597
2625
|
cfg.block_size - 1))
|
|
2598
2626
|
# Fresh per-turn schedule state: acceptance statistics are a property
|
|
2599
2627
|
# of the current prompt/content, not of the session.
|
|
2600
|
-
|
|
2628
|
+
# The round-cost ladder is the exception: it belongs to the machine and the
|
|
2629
|
+
# checkpoint, so each turn starts from what the last one measured.
|
|
2630
|
+
self.policy = (mlx_dflash.block_policy(self.cap, eng._dflash_ladder)
|
|
2601
2631
|
if eng.dflash_adaptive else None)
|
|
2602
2632
|
self._ids = list(cfg.target_layer_ids)
|
|
2603
2633
|
self._mask = int(cfg.mask_token_id)
|
|
@@ -450,6 +450,24 @@ def build(config: DFlashConfig):
|
|
|
450
450
|
# first extra row too cheaply and collapsed the schedule on medium acceptance).
|
|
451
451
|
BLOCK_ROUND_COSTS = (1.0, 1.76, 1.93, 2.30, 2.18, 2.20, 2.19, 2.20)
|
|
452
452
|
|
|
453
|
+
# The same ladder on the 2-bit ternary pack (benchmarks/spec_decode.py, a fixed-width
|
|
454
|
+
# arm per depth, round wall over the serial step of the same prompt). It is NOT the
|
|
455
|
+
# 3-bit ladder rescaled: the serial step is shorter (46-51 ms against 57), so the
|
|
456
|
+
# drafter forward, the rollback and the host sync are a larger share of every round,
|
|
457
|
+
# and rows 2..4 ride mlx's 2-bit matmul, which pays more per row than its 3-bit one.
|
|
458
|
+
# Measured twice, six prompt x preset cells each agreeing to +-0.05: on a cool machine
|
|
459
|
+
# (21.5 tok/s serial) 1.68 / 2.21 / 2.60 / 2.56 / 2.57 / 2.57 / 2.60, on a warm one
|
|
460
|
+
# (19.7) 1.58 / 2.09 / 2.44 / 2.39 / 2.44 / 2.44 / 2.46 — the ratio moves with the
|
|
461
|
+
# serial step, which is the dispatch-bound half. The seed is the midpoint; either way
|
|
462
|
+
# the 3-bit seed priced every width past 1 some 9-18% too cheap here, and
|
|
463
|
+
# WidthPolicy.learned_seed carries what a turn measures into the next.
|
|
464
|
+
BLOCK_ROUND_COSTS_2BIT = (1.0, 1.63, 2.15, 2.52, 2.48, 2.50, 2.50, 2.53)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def round_costs(weight_bits: Optional[int]) -> tuple:
|
|
468
|
+
"""The measured seed ladder for a target quantized at `weight_bits`."""
|
|
469
|
+
return BLOCK_ROUND_COSTS_2BIT if weight_bits == 2 else BLOCK_ROUND_COSTS
|
|
470
|
+
|
|
453
471
|
|
|
454
472
|
class WidthPolicy:
|
|
455
473
|
"""Cost-model width schedule: choose the verified width that maximizes
|
|
@@ -537,6 +555,29 @@ class WidthPolicy:
|
|
|
537
555
|
return ob[0]
|
|
538
556
|
return self._seed[depth] * self._unit()
|
|
539
557
|
|
|
558
|
+
LEARN_MIN = 4 # rounds at a depth before its wall may re-shape the seed
|
|
559
|
+
|
|
560
|
+
def learned_seed(self) -> list:
|
|
561
|
+
"""The seed ladder for the NEXT turn: this one's, with every depth the turn
|
|
562
|
+
measured enough moved halfway to what it measured, in seed units (so the
|
|
563
|
+
result is scale-free like the seed itself — the next turn's context is a
|
|
564
|
+
different length and its walls a different scale).
|
|
565
|
+
|
|
566
|
+
The policy is rebuilt per turn because acceptance is a property of the
|
|
567
|
+
content, but the cost ladder is a property of the machine and the
|
|
568
|
+
checkpoint, and a turn that re-learns it from a wrong seed spends its first
|
|
569
|
+
rounds at the wrong width. Halfway, and only past LEARN_MIN rounds: a depth
|
|
570
|
+
priced out by one inflated sample (a kernel build, a rollback) is never
|
|
571
|
+
visited again to correct it."""
|
|
572
|
+
if not self._obs:
|
|
573
|
+
return list(self._seed)
|
|
574
|
+
unit = self._unit()
|
|
575
|
+
out = list(self._seed)
|
|
576
|
+
for d, (wall, n) in self._obs.items():
|
|
577
|
+
if n >= self.LEARN_MIN and unit > 0:
|
|
578
|
+
out[d] = 0.5 * out[d] + 0.5 * wall / unit
|
|
579
|
+
return out
|
|
580
|
+
|
|
540
581
|
def _tail_p(self) -> float:
|
|
541
582
|
"""Acceptance estimate for positions >= TAIL_SPLIT. Real pooled
|
|
542
583
|
observations once any wide round has run; before that, INFERENCE
|
|
@@ -806,6 +847,57 @@ def bundle_dir(model_dir: str) -> Optional[str]:
|
|
|
806
847
|
return d if os.path.isfile(os.path.join(d, "config.json")) else None
|
|
807
848
|
|
|
808
849
|
|
|
850
|
+
# Drafters transfer across quantizations of one base model: a drafter reads the
|
|
851
|
+
# target's residual stream at its tapped layers, which a different weight quantization
|
|
852
|
+
# only perturbs (measured on the Prism ternary pack: 93% acceptance with the 3-bit
|
|
853
|
+
# model's sidecar). A checkpoint that bundles no drafter borrows the one a sibling
|
|
854
|
+
# repo bundles, keyed on the (hidden, layers, vocab) shape the tap needs anyway.
|
|
855
|
+
DONORS: dict = {(5120, 64, 248320): "nathansutton/Qwen3.8-27B-Ternary-Bonsai-2-DFlash2-MLX"}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _donor_file(repo_id: str, filename: str, cached: bool = True) -> str:
|
|
859
|
+
"""The local path of one file of a hub repo: the cache when it holds it (and
|
|
860
|
+
`cached`), a download — into the repo's CURRENT snapshot — otherwise."""
|
|
861
|
+
from huggingface_hub import hf_hub_download, try_to_load_from_cache
|
|
862
|
+
hit = try_to_load_from_cache(repo_id, filename) if cached else None
|
|
863
|
+
return hit if isinstance(hit, str) else hf_hub_download(repo_id, filename)
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def donor_bundle_dir(key, repo_id: Optional[str], donors: Optional[dict] = None,
|
|
867
|
+
fetch: Optional[Callable[[str, str], str]] = None) -> Optional[str]:
|
|
868
|
+
"""The sidecar dir a `donors` (default DONORS) sibling bundles for a target of
|
|
869
|
+
shape `key`, or None (no donor for the shape, the model IS the donor, or the
|
|
870
|
+
fetch failed). `fetch(repo_id, filename)` resolves one file; None = the hub."""
|
|
871
|
+
donor = (DONORS if donors is None else donors).get(key)
|
|
872
|
+
if not donor or donor == repo_id:
|
|
873
|
+
return None
|
|
874
|
+
resolve = fetch or _donor_file
|
|
875
|
+
try:
|
|
876
|
+
cfg = resolve(donor, f"{_BUNDLE}/config.json")
|
|
877
|
+
wts = resolve(donor, f"{_BUNDLE}/model.safetensors")
|
|
878
|
+
d = os.path.dirname(wts)
|
|
879
|
+
if os.path.dirname(cfg) != d and not _complete(d):
|
|
880
|
+
# The two files resolve independently, and a cache hit on the config
|
|
881
|
+
# from an older revision's snapshot next to weights downloaded into the
|
|
882
|
+
# current one is two directories, neither a bundle. The weights' snapshot
|
|
883
|
+
# is the live one: fetch the config into it.
|
|
884
|
+
if fetch is not None:
|
|
885
|
+
fetch(donor, f"{_BUNDLE}/config.json")
|
|
886
|
+
else:
|
|
887
|
+
_donor_file(donor, f"{_BUNDLE}/config.json", cached=False)
|
|
888
|
+
except Exception as e: # noqa: BLE001 — offline/gated: decode without the drafter
|
|
889
|
+
log.warning("DFlash drafter: donor bundle %s unavailable (%s); decoding "
|
|
890
|
+
"without it", donor, e)
|
|
891
|
+
return None
|
|
892
|
+
if not _complete(d):
|
|
893
|
+
log.warning("DFlash drafter: donor bundle %s resolved to a config and weights "
|
|
894
|
+
"in different snapshots (%s, %s); decoding without it", donor,
|
|
895
|
+
os.path.dirname(cfg), d)
|
|
896
|
+
return None
|
|
897
|
+
log.info("DFlash drafter: no bundle with these weights; borrowing %s's", donor)
|
|
898
|
+
return d
|
|
899
|
+
|
|
900
|
+
|
|
809
901
|
def _complete(d: str) -> bool:
|
|
810
902
|
return (os.path.isfile(os.path.join(d, "config.json"))
|
|
811
903
|
and os.path.isfile(os.path.join(d, "model.safetensors")))
|
|
@@ -849,18 +941,22 @@ def ensure_bundle(model_dir: str, repo_id: Optional[str],
|
|
|
849
941
|
|
|
850
942
|
|
|
851
943
|
def load_drafter(model: "nn.Module", model_dir: str, repo_id: Optional[str] = None,
|
|
852
|
-
bits: int = 4, gs: int = 64
|
|
944
|
+
bits: int = 4, gs: int = 64, donors: Optional[dict] = None,
|
|
945
|
+
fetch: Optional[Callable[[str, str], str]] = None) -> Optional["nn.Module"]:
|
|
853
946
|
"""Load the DFlash drafter bundled with the target's weights (or the dir
|
|
854
947
|
CHAD_DFLASH_PATH names), bound to the target's embedding/lm_head, with the
|
|
855
948
|
target tap installed. None when no drafter ships for this model or on any
|
|
856
949
|
failure — DFlash is a pure speed feature, never load-bearing.
|
|
857
950
|
|
|
858
951
|
`repo_id` is the model's HF repo when it came from the hub, so a bundle whose
|
|
859
|
-
weights the base download filtered out can be completed (see ensure_bundle).
|
|
952
|
+
weights the base download filtered out can be completed (see ensure_bundle).
|
|
953
|
+
`donors`/`fetch` are donor_bundle_dir's (tests inject them)."""
|
|
860
954
|
try:
|
|
861
955
|
key = _target_key(model)
|
|
862
956
|
ensure_bundle(model_dir, repo_id)
|
|
863
957
|
sdir = bundle_dir(model_dir)
|
|
958
|
+
if sdir is None and key is not None:
|
|
959
|
+
sdir = donor_bundle_dir(key, repo_id, donors, fetch)
|
|
864
960
|
if key is None or sdir is None:
|
|
865
961
|
return None
|
|
866
962
|
if not _is_sidecar(sdir):
|