specpair 0.1.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.
Files changed (44) hide show
  1. specpair-0.1.0/.gitignore +44 -0
  2. specpair-0.1.0/CHANGELOG.md +19 -0
  3. specpair-0.1.0/LICENSE +21 -0
  4. specpair-0.1.0/PKG-INFO +349 -0
  5. specpair-0.1.0/README.ko.md +317 -0
  6. specpair-0.1.0/README.md +325 -0
  7. specpair-0.1.0/pyproject.toml +47 -0
  8. specpair-0.1.0/src/specpair/__init__.py +49 -0
  9. specpair-0.1.0/src/specpair/__main__.py +3 -0
  10. specpair-0.1.0/src/specpair/bench.py +281 -0
  11. specpair-0.1.0/src/specpair/cli.py +213 -0
  12. specpair-0.1.0/src/specpair/demo.py +76 -0
  13. specpair-0.1.0/src/specpair/demo_files/bench_replay.json +121 -0
  14. specpair-0.1.0/src/specpair/demo_files/broken.gguf +0 -0
  15. specpair-0.1.0/src/specpair/demo_files/draft-ok.gguf +0 -0
  16. specpair-0.1.0/src/specpair/demo_files/draft-other.gguf +0 -0
  17. specpair-0.1.0/src/specpair/demo_files/draft-spm.gguf +0 -0
  18. specpair-0.1.0/src/specpair/demo_files/draft-wide.gguf +0 -0
  19. specpair-0.1.0/src/specpair/demo_files/hf-draft/tokenizer.json +1035 -0
  20. specpair-0.1.0/src/specpair/demo_files/hf-draft/tokenizer_config.json +5 -0
  21. specpair-0.1.0/src/specpair/demo_files/target.gguf +0 -0
  22. specpair-0.1.0/src/specpair/errors.py +65 -0
  23. specpair-0.1.0/src/specpair/gguf_header.py +186 -0
  24. specpair-0.1.0/src/specpair/hf_tokenizer.py +112 -0
  25. specpair-0.1.0/src/specpair/i18n.py +399 -0
  26. specpair-0.1.0/src/specpair/replay.py +124 -0
  27. specpair-0.1.0/src/specpair/report.py +195 -0
  28. specpair-0.1.0/src/specpair/rules/llama.cpp-b12360.json +27 -0
  29. specpair-0.1.0/src/specpair/rules.py +206 -0
  30. specpair-0.1.0/src/specpair/ruleset.py +24 -0
  31. specpair-0.1.0/src/specpair/scan.py +92 -0
  32. specpair-0.1.0/src/specpair/vocab.py +109 -0
  33. specpair-0.1.0/tests/conftest.py +98 -0
  34. specpair-0.1.0/tests/test_bench.py +243 -0
  35. specpair-0.1.0/tests/test_cli.py +124 -0
  36. specpair-0.1.0/tests/test_demo.py +54 -0
  37. specpair-0.1.0/tests/test_errors.py +145 -0
  38. specpair-0.1.0/tests/test_fixtures.py +42 -0
  39. specpair-0.1.0/tests/test_gguf_header.py +116 -0
  40. specpair-0.1.0/tests/test_hf_tokenizer.py +112 -0
  41. specpair-0.1.0/tests/test_i18n.py +141 -0
  42. specpair-0.1.0/tests/test_rules.py +152 -0
  43. specpair-0.1.0/tests/test_scan.py +92 -0
  44. specpair-0.1.0/tools/make_fixtures.py +218 -0
@@ -0,0 +1,44 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ *.egg-info/
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+ .next/
14
+
15
+ # 실행 산출물·모델·데이터 캐시
16
+ runs/
17
+ outputs/
18
+ .cache/
19
+ *.safetensors
20
+ *.gguf
21
+ *.bin
22
+
23
+ # 환경·비밀
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+
28
+ # OS·에디터
29
+ .DS_Store
30
+ .vscode/
31
+ .idea/
32
+
33
+ # Flutter·Dart
34
+ .dart_tool/
35
+ build/
36
+ .flutter-plugins
37
+ .flutter-plugins-dependencies
38
+ coverage/
39
+
40
+ # 번들 데모 파일(합성 헤더 전용 GGUF, 수 KB)은 추적한다
41
+ !src/specpair/demo_files/*.gguf
42
+
43
+ # 연습용 파일을 README 명령으로 꺼낸 폴더
44
+ /demo-files/
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-11-13
10
+
11
+ ### Added
12
+ - `specpair check TARGET DRAFT`: pre-load verdict for the `llama.cpp@b12360` rule set from GGUF headers or
13
+ Hugging Face tokenizer folders, listing every failing rule with the first mismatching tokens.
14
+ - `specpair scan TARGET DIR`: verdicts for every GGUF in a folder; corrupt files become error rows.
15
+ - `specpair bench`: tokens/s, acceptance rate and speedup from llama-server timings, with warnings for a draft
16
+ that is not used and for settings that are slower than the baseline.
17
+ - `specpair demo` and `bench --mock`: offline practice mode with synthetic GGUF headers and replayed timings.
18
+ - English and Korean output (`--lang`, `SPECPAIR_LANG`, locale), `--json` output, stable exit codes.
19
+ - Small public Python API: `load_vocab`, `check_pair`, `recommended_command`, exception classes.
specpair-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeeGyeongPil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,349 @@
1
+ Metadata-Version: 2.5
2
+ Name: specpair
3
+ Version: 0.1.0
4
+ Summary: Check whether a draft model can speculate for a target model (GGUF headers or HF tokenizers) and measure the real speedup on llama-server.
5
+ Project-URL: Homepage, https://github.com/LeeGyeongPil/specpair
6
+ Project-URL: Issues, https://github.com/LeeGyeongPil/specpair/issues
7
+ Project-URL: Changelog, https://github.com/LeeGyeongPil/specpair/blob/main/CHANGELOG.md
8
+ Author: LeeGyeongPil
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: draft-model,gguf,llama-server,llama.cpp,speculative-decoding,tokenizer
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Testing
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+
25
+ # specpair
26
+
27
+ **English** | [한국어](https://github.com/LeeGyeongPil/specpair/blob/v0.1.0/README.ko.md)
28
+
29
+ specpair checks, before you load anything, whether a small draft model can speed up a big target model with speculative decoding in llama.cpp. It reads only the GGUF headers (or Hugging Face tokenizer files), applies the same rules llama-server applies, and lists every rule that fails. After you start the servers, `specpair bench` measures tokens per second and the draft acceptance rate, so you can see whether the draft really helped. It has no runtime dependencies and needs no model files to try (a practice mode ships with it).
30
+
31
+ ![Check result: rejected pair with all three failing rules](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/check-rejected.png)
32
+ *`specpair check` on a rejected pair: all three failing rules are shown at once, with the first mismatching token (practice files).*
33
+
34
+ ## Why
35
+
36
+ Three things surprise people who add a draft model to llama-server (checked 2026-11-13 against [`common/speculative.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/common/speculative.cpp) and the project's own probe on llama.cpp b12360):
37
+
38
+ - A rejected pair is not an error. llama-server logs `the target and draft vocabs are not compatible` and keeps serving at single-model speed, so you may not notice.
39
+ - `-md` alone does not use the draft: `--spec-type` defaults to `none`.
40
+ - A high acceptance rate does not guarantee a gain. In the recorded probe, a draft setting with 46.9% accepted draft tokens was 29.7% slower than no draft.
41
+
42
+ ## Who it is for
43
+
44
+ - You run `llama-server` and want to pick a draft GGUF from a folder of downloads without loading each one.
45
+ - `llama-server` printed `draft model vocab type must match`, or your GUI shows an empty draft list, and you want the real reason.
46
+ - You added a draft model and the server did not get faster, and you want to know whether the draft was even used.
47
+
48
+ Think of two interpreters at a meeting: the fast one can only guess ahead if both use the same dictionary. The check compares the dictionaries (token lists); the benchmark measures whether guessing ahead actually saves time.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ # uv (Python package runner): https://docs.astral.sh/uv/
54
+ curl -LsSf https://astral.sh/uv/install.sh | sh
55
+ # run without installing
56
+ uvx --from git+https://github.com/LeeGyeongPil/specpair specpair --version
57
+ # or install the command
58
+ uv tool install git+https://github.com/LeeGyeongPil/specpair
59
+ ```
60
+
61
+ Python 3.10 or newer; no other packages. On a minimal Linux image you also need `git` and `curl` for the commands above. The rest of this page assumes the `specpair` command is on your PATH.
62
+
63
+ ## Quick start (practice mock)
64
+
65
+ No model download needed. `specpair demo` runs everything on tiny synthetic GGUF headers and on timings recorded earlier, so it works offline in about a second.
66
+
67
+ ```bash
68
+ specpair demo --extract demo-files
69
+ ```
70
+
71
+ ```text
72
+ PRACTICE MOCK - replays timings recorded with llama.cpp b12360 on CPU; your hardware will differ
73
+ Wrote 9 synthetic files to demo-files. Try:
74
+ specpair check demo-files/target.gguf demo-files/draft-other.gguf
75
+ specpair check demo-files/target.gguf demo-files/draft-ok.gguf
76
+ specpair scan demo-files/target.gguf demo-files
77
+ specpair bench --mock
78
+ ```
79
+
80
+ ![Output of the demo extract command](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/demo-extract.png)
81
+ *The practice files are copied to `demo-files/`. The files are real GGUF headers, so `check` and `scan` run the real rules on them.*
82
+
83
+ Plain `specpair demo` (without `--extract`) prints a guided tour of all the steps below.
84
+
85
+ What is real and what is mock in practice mode:
86
+
87
+ | Part | Practice mode |
88
+ |---|---|
89
+ | `check`, `scan` | Real code on synthetic GGUF headers (a few KB, no weights). |
90
+ | `bench --mock` | The real HTTP client and math, talking to a local replay server. The numbers are timings recorded from llama.cpp b12360 on a CPU (Qwen2.5-1.5B and 0.5B, q4_0), not measured on your machine. The `rejected` line is derived: it reuses the baseline timings and reports no `draft_n`, which reproduces what the recording showed for a rejected pair (llama-server kept serving at the single-model speed). |
91
+
92
+ ## Usage
93
+
94
+ | Command | What it does |
95
+ |---|---|
96
+ | `specpair check TARGET DRAFT` | Pre-load verdict for one pair; GGUF files or tokenizer folders |
97
+ | `specpair scan TARGET DIR` | Verdict table for every GGUF in a folder (and its direct sub-folders) |
98
+ | `specpair bench --base URL --spec [LABEL=]URL` | Compare running llama-server instances on the same prompts |
99
+ | `specpair bench --mock` | Practice replay of recorded timings |
100
+ | `specpair demo [--extract DIR]` | Offline tour / copy practice files |
101
+ | `--json`, `--lang en\|ko` | Machine-readable output, screen language |
102
+
103
+ ### Check a pair
104
+
105
+ ```bash
106
+ specpair check demo-files/target.gguf demo-files/draft-other.gguf
107
+ ```
108
+
109
+ ![Check result for a rejected pair](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/check-rejected.png)
110
+ *Rejected: the BOS handling, the vocabulary size and the token strings all differ. llama.cpp stops at the first failure; specpair shows all of them.*
111
+
112
+ Read it like this: `verdict: rejected (3 rules failed)` means llama-server would log `the target and draft vocabs are not compatible` and keep serving without the draft. Each `x` line names the rule and the numbers. The exit code is 1 for a rejected pair.
113
+
114
+ The five rules of the `llama.cpp@b12360` rule set:
115
+
116
+ | Rule | Fails when |
117
+ |---|---|
118
+ | `vocab_type` | The tokenizer kinds differ (BPE vs SPM and so on). |
119
+ | `bos` | The "add BOS" flags differ, or, while BOS is added, the BOS ids differ. |
120
+ | `eos` | Same, for EOS. |
121
+ | `vocab_size` | The vocabulary sizes differ by more than 128. |
122
+ | `token_text` | Any token string differs from id 5 on. |
123
+
124
+ A pair that passes:
125
+
126
+ ```bash
127
+ specpair check demo-files/target.gguf demo-files/draft-ok.gguf
128
+ ```
129
+
130
+ ![Check result for a compatible pair](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/check-ok.png)
131
+ *Compatible. The size difference is exactly 128, the limit, so one more token would be rejected, and the output says so. It also prints the llama-server command to use.*
132
+
133
+ Note the line `--spec-type defaults to none, so -md alone loads the draft but never uses it`. Options such as `--draft-max` were removed in b12360; use `--spec-draft-n-max`.
134
+
135
+ The limit is easy to hit in real life: in the project's own probe, Qwen2.5-7B and Qwen2.5-0.5B GGUF files differ by exactly 128 tokens (152,064 vs 151,936). Hugging Face tokenizer files can show different sizes than GGUF files because conversion pads the vocabulary; specpair adds a note when it sees that, so confirm with the GGUF files. A tokenizer folder works as either side:
136
+
137
+ ```bash
138
+ specpair check demo-files/target.gguf demo-files/hf-draft
139
+ ```
140
+
141
+ ```text
142
+ specpair check | rule set llama.cpp@b12360 (source: https://github.com/ggml-org/llama.cpp/blob/master/common/speculative.cpp, checked 2026-11-13)
143
+ target: demo-files/target.gguf
144
+ GGUF | BPE (gpt2) | pre qwen2 | 1,000 tokens | BOS id 0, add: no (implicit default) | EOS id 2, add: no (implicit default)
145
+ draft : demo-files/hf-draft
146
+ HF | BPE | 1,000 tokens | BOS id 0, add: no (implicit default) | EOS id 2, add: no (implicit default)
147
+
148
+ verdict: compatible (token strings identical on 100.0% of ids)
149
+
150
+ notes:
151
+ - a Hugging Face tokenizer folder was used; GGUF conversion can change the vocabulary size (padding) and the add_bos default, so confirm with the GGUF files
152
+
153
+ Compatible by tokenizer files. Convert both models to GGUF and check the GGUF files before building a command.
154
+ ```
155
+
156
+ A split model (`...-00001-of-00002.gguf`) works with its first file; partial downloads are fine as long as the header is complete.
157
+
158
+ ### Scan a folder
159
+
160
+ ```bash
161
+ specpair scan demo-files/target.gguf demo-files
162
+ ```
163
+
164
+ ![Scan table of the practice folder](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/scan.png)
165
+ *One row per GGUF. The broken file becomes an `error` row with the byte position, and the scan continues.*
166
+
167
+ The exit code is 0 when at least one draft fits, 1 when none does.
168
+
169
+ ### Measure with llama-server
170
+
171
+ A compatible pair is necessary, not sufficient. Start one baseline server and one or more draft servers (file names below are placeholders for your own GGUF files; the flags are the ones `check` prints):
172
+
173
+ ```bash
174
+ llama-server -m target.gguf --port 8080
175
+ llama-server -m target.gguf -md draft.gguf --spec-type draft-simple --spec-draft-n-max 3 --port 8081
176
+ llama-server -m target.gguf -md draft.gguf --spec-type draft-simple --spec-draft-n-max 8 --port 8082
177
+ specpair bench --base http://127.0.0.1:8080 --spec n3=http://127.0.0.1:8081 --spec n8=http://127.0.0.1:8082
178
+ ```
179
+
180
+ Each server must be up (`/health` answers) before `bench` starts. Run them on the same machine under the same load. `bench` sends the same prompts (3 built-in ones, or `--prompts FILE` with one prompt per line or JSONL), greedy decoding, 128 tokens by default (`--n-predict`), one warm-up request, and `--repeat N` smooths noise. It uses the timings the servers report, not wall-clock time. Real runs can take tens of seconds per server, more when the machine is busy.
181
+
182
+ The practice version replays recorded timings through the same code:
183
+
184
+ ```bash
185
+ specpair bench --mock
186
+ ```
187
+
188
+ ![Bench table with the practice replay](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/bench-mock.png)
189
+ *Practice mock. n3 is +5.0% faster, n8 is 29.7% slower even though 46.9% of its draft tokens were accepted, and `rejected` ran at baseline speed with no draft.*
190
+
191
+ How to read the table and the warnings:
192
+
193
+ | Column or warning | Meaning |
194
+ |---|---|
195
+ | `t/s` | Generated tokens per second, from the servers' own timings |
196
+ | `drafted`, `accepted`, `acceptance` | Draft tokens proposed, accepted by the target, and their ratio |
197
+ | `vs baseline` | Speed relative to the server without a draft |
198
+ | `slower than baseline` | The setting is more than 2% slower; a high acceptance rate does not guarantee a gain, try a smaller `--spec-draft-n-max` or a cheaper draft |
199
+ | `draft not used` | The server reported no `draft_n`: the draft was rejected at load time, or `--spec-type` is `none` |
200
+ | `output differs` | With greedy decoding the text should match the baseline; a difference is worth a look (does not change the exit code) |
201
+
202
+ ## Settings
203
+
204
+ ### Language
205
+
206
+ | Setting | Meaning |
207
+ |---|---|
208
+ | `--lang en\|ko` | Screen language. Before or after the sub-command. |
209
+ | `SPECPAIR_LANG=en\|ko` | Same, via environment. |
210
+ | `LC_ALL`, `LC_MESSAGES`, `LANG` | Used when nothing above is set: a value starting with `ko` selects Korean, anything else English. |
211
+
212
+ Order: `--lang`, then `SPECPAIR_LANG`, then the locale variables. Help (`--help`), tables, warnings and runtime errors are translated. Only argparse's usage-error messages stay English, for example `the following arguments are required: draft` and `invalid choice: ...`; in Korean mode the `오류:` prefix and the usage line are still Korean.
213
+
214
+ ```bash
215
+ specpair check demo-files/target.gguf demo-files/draft-wide.gguf
216
+ ```
217
+
218
+ ![Check result for a pair that is 1 token over the size limit](https://raw.githubusercontent.com/LeeGyeongPil/specpair/v0.1.0/docs/images/check-wide.png)
219
+ *One rule fails: the vocabulary sizes differ by 129, one more than the limit. The screen language here is English (`SPECPAIR_LANG=en`).*
220
+
221
+ The same pair in Korean:
222
+
223
+ ```bash
224
+ specpair --lang ko check demo-files/target.gguf demo-files/draft-wide.gguf
225
+ ```
226
+
227
+ ```text
228
+ specpair check | 규칙 세트 llama.cpp@b12360 (출처: https://github.com/ggml-org/llama.cpp/blob/master/common/speculative.cpp, 확인일 2026-11-13)
229
+ 대상 : demo-files/target.gguf
230
+ GGUF | BPE (gpt2) | pre qwen2 | 토큰 1,000개 | BOS id 0, 추가: 아니오 (암묵 기본값) | EOS id 2, 추가: 아니오 (암묵 기본값)
231
+ 드래프트: demo-files/draft-wide.gguf
232
+ GGUF | BPE (gpt2) | pre qwen2 | 토큰 1,129개 | BOS id 0, 추가: 아니오 (암묵 기본값) | EOS id 2, 추가: 아니오 (암묵 기본값)
233
+
234
+ 판정: rejected (실패한 규칙 1개)
235
+ x vocab_size: 어휘 크기 차이가 129 입니다(상한 128): 대상 1000개 / 드래프트 1129개
236
+
237
+ 참고:
238
+ - 두 어휘가 모두 BPE 입니다. llama-server 가 'draft model vocab type must match' 를 남기더라도 실제 원인은 위에서 실패한 규칙입니다
239
+ - 드래프트 어휘에 [PAD] 슬롯이 129개 더 있습니다. GGUF 변환은 어휘를 임베딩 크기까지 채우므로 Hugging Face 파일과 크기가 다를 수 있습니다
240
+
241
+ llama-server 는 'the target and draft vocabs are not compatible' 를 남기고 드래프트 없이 계속 서비스합니다.
242
+ ```
243
+
244
+ The screenshots in this README were taken with `SPECPAIR_LANG=en`; the Korean README shows the same commands with `SPECPAIR_LANG=ko`. There are no keys and no network calls apart from `bench` talking to your own servers.
245
+
246
+ ### JSON output and exit codes
247
+
248
+ `check`, `scan` and `bench` accept `--json`. Keys are English in every language; only `message` strings follow the language.
249
+
250
+ ```bash
251
+ specpair check --json demo-files/target.gguf demo-files/draft-wide.gguf
252
+ ```
253
+
254
+ ```text
255
+ {
256
+ "schema_version": 1,
257
+ "runtime": "llama.cpp@b12360",
258
+ "verdict": "rejected",
259
+ "failures": [
260
+ {
261
+ "rule": "vocab_size",
262
+ "message": "vocabulary sizes differ by 129 (limit 128): target 1000 vs draft 1129 tokens",
263
+ "evidence": {
264
+ "target": 1000,
265
+ "draft": 1129,
266
+ "diff": 129,
267
+ "max": 128
268
+ }
269
+ }
270
+ ],
271
+ ...
272
+ ```
273
+
274
+ (The output continues with `notes`, `target`, `draft`, `same_id_ratio` and `recommended_command`, which is `null` for a rejected pair.)
275
+
276
+ Exit codes: 0 fine, 1 a verdict failure (rejected pair, no usable draft in `scan`, or a `bench` warning such as `slower than baseline` or `draft not used`), 2 input or connection errors with a defined message and no traceback (for example an empty file: `'x.gguf' is empty (0 bytes). Download it again or pass the right file.`).
277
+
278
+ ### Python API
279
+
280
+ Install it into your project environment first (the `uvx` and `uv tool install` commands above keep it in an isolated tool environment):
281
+
282
+ ```bash
283
+ uv pip install git+https://github.com/LeeGyeongPil/specpair
284
+ ```
285
+
286
+ ```python
287
+ from specpair import check_pair, load_vocab
288
+
289
+ target = load_vocab("demo-files/target.gguf")
290
+ draft = load_vocab("demo-files/draft-wide.gguf")
291
+ result = check_pair(target, draft)
292
+ print(result.verdict, [f.rule for f in result.failures])
293
+ ```
294
+
295
+ ```text
296
+ rejected ['vocab_size']
297
+ ```
298
+
299
+ `load_vocab` accepts a GGUF file or a tokenizer folder. Errors derive from `SpecpairError` (for example `InputNotFoundError`, `NotGGUFError`, `TruncatedHeaderError`).
300
+
301
+ ## How it works
302
+
303
+ ```mermaid
304
+ flowchart LR
305
+ A[GGUF header or<br/>tokenizer.json] --> B[load_vocab]
306
+ B --> C[5 rules<br/>llama.cpp@b12360]
307
+ C --> D[verdict +<br/>every failing rule]
308
+ E[llama-server<br/>/completion timings] --> F[bench]
309
+ F --> G[t/s, acceptance,<br/>vs baseline, warnings]
310
+ ```
311
+
312
+ - The GGUF reader reads only the key-value area at the start of the file and skips big arrays, so a 4 GB file costs a few megabytes of reading and a truncated download is still usable.
313
+ - The rules live in a data file (`src/specpair/rules/llama.cpp-b12360.json`) with the source and the date they were checked; every output starts with the rule set name.
314
+ - `bench` takes `predicted_n`, `predicted_ms`, `draft_n` and `draft_n_accepted` from each server's response, so there is nothing to parse in logs.
315
+
316
+ ## Rule set and versions
317
+
318
+ The rules follow llama.cpp build b12360 (`common/speculative.cpp`, checked 2026-11-13: https://github.com/ggml-org/llama.cpp/blob/master/common/speculative.cpp). Options and checks change between builds; the output header always names the rule set, and `--runtime` only accepts rule sets that exist. Not covered: vLLM, SGLang, MLX, dedicated draft heads (EAGLE-style, MTP), and drafts with a different vocabulary.
319
+
320
+ ## When to use it
321
+
322
+ - Good fit: you serve GGUF models with llama-server and want to know, before downloading or loading, whether a draft pair is accepted, and afterwards whether it is faster.
323
+ - Other tools: LM Studio filters its draft list inside the app ([LM Studio](https://lmstudio.ai/)); for engines other than llama.cpp use that engine's own documentation, for example [vLLM](https://docs.vllm.ai/).
324
+
325
+ ## Troubleshooting
326
+
327
+ | Symptom | Cause | Fix |
328
+ |---|---|---|
329
+ | Server runs but is not faster | `--spec-type` defaults to `none`, so `-md` alone does not use the draft | Add `--spec-type draft-simple --spec-draft-n-max 3` |
330
+ | `the argument has been removed` for `--draft-max` | The old option names were removed in b12360 | Use `--spec-draft-n-max`; `check` prints the current command |
331
+ | Log says `draft model vocab type must match`, but both are BPE | The message is misleading; the real reason is another rule | Run `specpair check` and read the failing rules |
332
+ | `bench`: `draft not used` | Draft rejected at load, or `--spec-type none` | Check the pair, then the flags |
333
+ | `bench`: `slower than baseline` | Too many draft tokens per step, or a draft that is too costly | Try a smaller `--spec-draft-n-max` or a cheaper draft |
334
+ | `is empty (0 bytes)` or `ends inside the fixed GGUF header` (exit 2) | The download is empty or cut before the header ends | Download it again |
335
+ | `error ... ends in the middle of key` | The GGUF file is cut inside the header | Download it again |
336
+ | `cannot reach llama-server` (exit 2) | The server is not running yet or the URL is wrong | Start it and wait for `/health` |
337
+ | The Hugging Face check says compatible, GGUF files say rejected | Conversion pads the vocabulary | Trust the GGUF check |
338
+
339
+ ## Development
340
+
341
+ ```bash
342
+ uv run pytest -q
343
+ ```
344
+
345
+ The tests use synthetic files only; no model is downloaded. `uv run python tools/make_fixtures.py [OUT_DIR]` regenerates the bundled practice files byte for byte. Report bugs and ideas as issues. Changes are listed in [CHANGELOG.md](https://github.com/LeeGyeongPil/specpair/blob/v0.1.0/CHANGELOG.md).
346
+
347
+ ## License
348
+
349
+ MIT. The practice files are synthetic, and the replayed timings are the project's own measurements with Qwen2.5-1.5B-Instruct and Qwen2.5-0.5B-Instruct (Apache-2.0) on llama.cpp b12360 (MIT); no model weights are included.