effbelt 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.
- effbelt-0.1.0/LICENSE +21 -0
- effbelt-0.1.0/PKG-INFO +168 -0
- effbelt-0.1.0/README.md +144 -0
- effbelt-0.1.0/effbelt/__init__.py +21 -0
- effbelt-0.1.0/effbelt/cheapest.py +57 -0
- effbelt-0.1.0/effbelt/cli.py +101 -0
- effbelt-0.1.0/effbelt/evalsanity.py +115 -0
- effbelt-0.1.0/effbelt/memory.py +129 -0
- effbelt-0.1.0/effbelt/modelcard.py +92 -0
- effbelt-0.1.0/effbelt/models.py +57 -0
- effbelt-0.1.0/effbelt/quantcheck.py +98 -0
- effbelt-0.1.0/effbelt/tokenwaste.py +165 -0
- effbelt-0.1.0/effbelt.egg-info/PKG-INFO +168 -0
- effbelt-0.1.0/effbelt.egg-info/SOURCES.txt +19 -0
- effbelt-0.1.0/effbelt.egg-info/dependency_links.txt +1 -0
- effbelt-0.1.0/effbelt.egg-info/entry_points.txt +2 -0
- effbelt-0.1.0/effbelt.egg-info/requires.txt +8 -0
- effbelt-0.1.0/effbelt.egg-info/top_level.txt +1 -0
- effbelt-0.1.0/pyproject.toml +35 -0
- effbelt-0.1.0/setup.cfg +4 -0
- effbelt-0.1.0/tests/test_effbelt.py +222 -0
effbelt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sardor Razikov
|
|
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.
|
effbelt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: effbelt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM efficiency toolbelt: fit, tokenwaste, evalsanity, quantcheck, cheapest, modelcard — save money and catch silent bugs, on a laptop.
|
|
5
|
+
Author: Sardor Razikov
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SRKRZ23/effbelt
|
|
8
|
+
Project-URL: Source, https://github.com/SRKRZ23/effbelt
|
|
9
|
+
Project-URL: Sponsor, https://github.com/sponsors/SRKRZ23
|
|
10
|
+
Keywords: llm,gpu,memory,kv-cache,quantization,cost,efficiency,benchmark
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: torch
|
|
18
|
+
Requires-Dist: torch>=2.1; extra == "torch"
|
|
19
|
+
Provides-Extra: hf
|
|
20
|
+
Requires-Dist: torch>=2.1; extra == "hf"
|
|
21
|
+
Requires-Dist: transformers>=5; extra == "hf"
|
|
22
|
+
Requires-Dist: kvquant-hf; extra == "hf"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# effbelt — the LLM efficiency toolbelt
|
|
26
|
+
|
|
27
|
+
Six small, honest tools that **save money and catch silent bugs** — all runnable
|
|
28
|
+
on a laptop with **no GPU and no API key**. Four of them need nothing but Python;
|
|
29
|
+
two can optionally measure real numbers on a tiny local model.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install effbelt # the four pure tools
|
|
33
|
+
pip install "effbelt[hf]" # + quantcheck tensors and live modelcard
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| tool | one line | needs a model? |
|
|
37
|
+
|------|----------|:---:|
|
|
38
|
+
| `fit` | will this model fit on my card? (weights + KV cache + overhead) | no |
|
|
39
|
+
| `tokenwaste` | where your LLM **API bill leaks** (duplicate prompts, retries, oversized model) | no |
|
|
40
|
+
| `evalsanity` | **is your benchmark lying?** (contamination, degenerate data/preds) | no |
|
|
41
|
+
| `quantcheck` | **did your quantization actually do anything?** (silent no-op guard) | tensors only |
|
|
42
|
+
| `cheapest` | the cheapest config that still clears **your** quality bar | your eval |
|
|
43
|
+
| `modelcard` | one-page quant tradeoff card (exact memory + *measured* ppl) | optional |
|
|
44
|
+
|
|
45
|
+
Every number here was produced on an Apple-silicon MacBook, offline. Nothing is invented.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Why trust the memory math? It matches a real cache to the byte.
|
|
50
|
+
|
|
51
|
+
`fit`'s KV-cache formula is cross-checked in the test suite against the **real
|
|
52
|
+
measured bytes** of a live Hugging Face cache (via the sister project
|
|
53
|
+
[kvquant](https://github.com/SRKRZ23/kvquant)):
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
KV formula check: analytic 180.0KB vs measured 180.0KB at seq=10 (0.00% off)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## `fit` — stop guessing at OOM
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ effbelt fit llama-3.1-70b --gpu rtx-4090 --seq 8192
|
|
63
|
+
llama-3.1-70b @ seq=8192 batch=1 weights=fp16 kv=fp16
|
|
64
|
+
weights 131.50 GB
|
|
65
|
+
kv cache 2.50 GB (8 kv-heads GQA)
|
|
66
|
+
overhead 1.00 GB
|
|
67
|
+
-------- 135.00 GB total
|
|
68
|
+
DOES NOT FIT on a 24 GB card (headroom -111.00 GB)
|
|
69
|
+
```
|
|
70
|
+
```
|
|
71
|
+
$ effbelt fit llama-3.1-8b --gpu rtx-4090 --seq 32768 --kv int8
|
|
72
|
+
... FITS on a 24 GB card (headroom +6.04 GB)
|
|
73
|
+
```
|
|
74
|
+
Use `--hf <repo>` to pull authoritative dimensions straight from a model's config.
|
|
75
|
+
|
|
76
|
+
## `tokenwaste` — audit an API request log (JSONL)
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
$ effbelt tokenwaste requests.jsonl
|
|
80
|
+
22 requests, ~$0.0780 total (prices as of 2026-10)
|
|
81
|
+
recoverable up to ~$0.0780 (<= 100% of bill; levers below can overlap):
|
|
82
|
+
- oversized-model ~$0.0718 large model, <=20 output tok -> try a small model
|
|
83
|
+
- over-long-context ~$0.0300 context over 8000 tok
|
|
84
|
+
- no-prefix-cache ~$0.0183 22x identical system prompt (~290 tok)
|
|
85
|
+
- retries ~$0.0013 requests marked retry=true
|
|
86
|
+
```
|
|
87
|
+
The levers overlap on the same request, so the headline is an honest **upper
|
|
88
|
+
bound** capped at 100% of the bill — never a made-up "save 300%".
|
|
89
|
+
|
|
90
|
+
## `evalsanity` — before you believe a leaderboard number
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
$ effbelt evalsanity eval.json
|
|
94
|
+
41 test items: 2 flag(s)
|
|
95
|
+
[HIGH] 1/41 test items appear VERBATIM in train -> contamination
|
|
96
|
+
[MED ] labels 93% one class ('A') -> imbalanced
|
|
97
|
+
-> at least one HIGH flag: do NOT trust this number yet.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## `quantcheck` — the silent no-op killer
|
|
101
|
+
|
|
102
|
+
The lesson behind kvquant, generalized to **any** tensor transform you think
|
|
103
|
+
quantizes. A no-op (e.g. you mutated a non-persisting view) passes every smoke
|
|
104
|
+
test and moves no metric — quantcheck proves it *bit*, estimates effective
|
|
105
|
+
bits, and flags the two classic bugs:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from effbelt import quantcheck
|
|
109
|
+
r = quantcheck.check(my_quant_fn, some_tensor, claimed_bits=8)
|
|
110
|
+
print(r.report())
|
|
111
|
+
# quantcheck: SILENT NO-OP <- if your "quantizer" changed nothing
|
|
112
|
+
# quantcheck: SUSPICIOUS <- not idempotent / coarser than claimed
|
|
113
|
+
# quantcheck: OK
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## `modelcard` — know the tradeoff before you quantize
|
|
117
|
+
|
|
118
|
+
Exact memory arithmetic, and *measured* quality when you point it at a model:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
$ effbelt modelcard gemma-3-270m --seq 8192 --live google/gemma-3-270m-it
|
|
122
|
+
kv dtype kv cache vs fp16 quality
|
|
123
|
+
fp16 0.14GB 1.00x +0.00% ppl
|
|
124
|
+
int8 0.07GB 2.00x -0.33% ppl <- free: 2x smaller, lossless
|
|
125
|
+
int4 0.04GB 4.00x +68.41% ppl <- breaks on this small model
|
|
126
|
+
quality measured on google/gemma-3-270m-it (160 tok, MPS/CPU)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## `cheapest` — smallest model that still passes
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from effbelt import cheapest
|
|
133
|
+
res = cheapest.sweep([
|
|
134
|
+
cheapest.Candidate("7b-int4", cost=1, quality_fn=lambda: my_eval("7b-int4")),
|
|
135
|
+
cheapest.Candidate("7b-fp16", cost=2, quality_fn=lambda: my_eval("7b-fp16")),
|
|
136
|
+
cheapest.Candidate("70b-int4", cost=8, quality_fn=lambda: my_eval("70b-int4")),
|
|
137
|
+
], bar=0.80)
|
|
138
|
+
print(res.report()) # -> use the cheapest one that clears 0.80
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Honest scope
|
|
144
|
+
- Prices and the small model table are **approximate and editable** (see
|
|
145
|
+
`effbelt/models.py`); for authoritative model dims use `fit --hf`.
|
|
146
|
+
- `quantcheck`'s effective-bit estimate is advisory at small group sizes — the
|
|
147
|
+
*silent-no-op* and *idempotency* checks are the reliable ones.
|
|
148
|
+
- `modelcard` quality numbers are measured only when you pass `--live`; otherwise
|
|
149
|
+
the quality column is left blank. We don't fill it with guesses.
|
|
150
|
+
|
|
151
|
+
## Tests
|
|
152
|
+
All validation runs on a laptop, offline:
|
|
153
|
+
```bash
|
|
154
|
+
python -m pytest tests/ -v # 18 tests, incl. the real-cache cross-check
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Support
|
|
158
|
+
Free and MIT. If it saved you a GPU or a bill, consider sponsoring:
|
|
159
|
+
**https://github.com/sponsors/SRKRZ23**
|
|
160
|
+
|
|
161
|
+
Crypto (receive-only):
|
|
162
|
+
- BTC `bc1q9hm4upksmvkzhl8z3zvxgz94da358qfe3tred0`
|
|
163
|
+
- ETH / Polygon / USDT-ERC20 `0xa9e3b63799f5E5cc6258042D45869a5a1d39F021`
|
|
164
|
+
- USDT-TRC20 `TUyNkMXme1CYAc1wZ8SZ64X9upBc9nwTZ5`
|
|
165
|
+
- SOL `j8zZCsWwM8WmUZwMSDHikNuz6E49Vn8nwo9FMQnHSoW`
|
|
166
|
+
|
|
167
|
+
Sister project: [kvquant](https://github.com/SRKRZ23/kvquant) — the engine behind
|
|
168
|
+
`quantcheck` and `modelcard`.
|
effbelt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# effbelt — the LLM efficiency toolbelt
|
|
2
|
+
|
|
3
|
+
Six small, honest tools that **save money and catch silent bugs** — all runnable
|
|
4
|
+
on a laptop with **no GPU and no API key**. Four of them need nothing but Python;
|
|
5
|
+
two can optionally measure real numbers on a tiny local model.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install effbelt # the four pure tools
|
|
9
|
+
pip install "effbelt[hf]" # + quantcheck tensors and live modelcard
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| tool | one line | needs a model? |
|
|
13
|
+
|------|----------|:---:|
|
|
14
|
+
| `fit` | will this model fit on my card? (weights + KV cache + overhead) | no |
|
|
15
|
+
| `tokenwaste` | where your LLM **API bill leaks** (duplicate prompts, retries, oversized model) | no |
|
|
16
|
+
| `evalsanity` | **is your benchmark lying?** (contamination, degenerate data/preds) | no |
|
|
17
|
+
| `quantcheck` | **did your quantization actually do anything?** (silent no-op guard) | tensors only |
|
|
18
|
+
| `cheapest` | the cheapest config that still clears **your** quality bar | your eval |
|
|
19
|
+
| `modelcard` | one-page quant tradeoff card (exact memory + *measured* ppl) | optional |
|
|
20
|
+
|
|
21
|
+
Every number here was produced on an Apple-silicon MacBook, offline. Nothing is invented.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Why trust the memory math? It matches a real cache to the byte.
|
|
26
|
+
|
|
27
|
+
`fit`'s KV-cache formula is cross-checked in the test suite against the **real
|
|
28
|
+
measured bytes** of a live Hugging Face cache (via the sister project
|
|
29
|
+
[kvquant](https://github.com/SRKRZ23/kvquant)):
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
KV formula check: analytic 180.0KB vs measured 180.0KB at seq=10 (0.00% off)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## `fit` — stop guessing at OOM
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
$ effbelt fit llama-3.1-70b --gpu rtx-4090 --seq 8192
|
|
39
|
+
llama-3.1-70b @ seq=8192 batch=1 weights=fp16 kv=fp16
|
|
40
|
+
weights 131.50 GB
|
|
41
|
+
kv cache 2.50 GB (8 kv-heads GQA)
|
|
42
|
+
overhead 1.00 GB
|
|
43
|
+
-------- 135.00 GB total
|
|
44
|
+
DOES NOT FIT on a 24 GB card (headroom -111.00 GB)
|
|
45
|
+
```
|
|
46
|
+
```
|
|
47
|
+
$ effbelt fit llama-3.1-8b --gpu rtx-4090 --seq 32768 --kv int8
|
|
48
|
+
... FITS on a 24 GB card (headroom +6.04 GB)
|
|
49
|
+
```
|
|
50
|
+
Use `--hf <repo>` to pull authoritative dimensions straight from a model's config.
|
|
51
|
+
|
|
52
|
+
## `tokenwaste` — audit an API request log (JSONL)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
$ effbelt tokenwaste requests.jsonl
|
|
56
|
+
22 requests, ~$0.0780 total (prices as of 2026-10)
|
|
57
|
+
recoverable up to ~$0.0780 (<= 100% of bill; levers below can overlap):
|
|
58
|
+
- oversized-model ~$0.0718 large model, <=20 output tok -> try a small model
|
|
59
|
+
- over-long-context ~$0.0300 context over 8000 tok
|
|
60
|
+
- no-prefix-cache ~$0.0183 22x identical system prompt (~290 tok)
|
|
61
|
+
- retries ~$0.0013 requests marked retry=true
|
|
62
|
+
```
|
|
63
|
+
The levers overlap on the same request, so the headline is an honest **upper
|
|
64
|
+
bound** capped at 100% of the bill — never a made-up "save 300%".
|
|
65
|
+
|
|
66
|
+
## `evalsanity` — before you believe a leaderboard number
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
$ effbelt evalsanity eval.json
|
|
70
|
+
41 test items: 2 flag(s)
|
|
71
|
+
[HIGH] 1/41 test items appear VERBATIM in train -> contamination
|
|
72
|
+
[MED ] labels 93% one class ('A') -> imbalanced
|
|
73
|
+
-> at least one HIGH flag: do NOT trust this number yet.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## `quantcheck` — the silent no-op killer
|
|
77
|
+
|
|
78
|
+
The lesson behind kvquant, generalized to **any** tensor transform you think
|
|
79
|
+
quantizes. A no-op (e.g. you mutated a non-persisting view) passes every smoke
|
|
80
|
+
test and moves no metric — quantcheck proves it *bit*, estimates effective
|
|
81
|
+
bits, and flags the two classic bugs:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from effbelt import quantcheck
|
|
85
|
+
r = quantcheck.check(my_quant_fn, some_tensor, claimed_bits=8)
|
|
86
|
+
print(r.report())
|
|
87
|
+
# quantcheck: SILENT NO-OP <- if your "quantizer" changed nothing
|
|
88
|
+
# quantcheck: SUSPICIOUS <- not idempotent / coarser than claimed
|
|
89
|
+
# quantcheck: OK
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## `modelcard` — know the tradeoff before you quantize
|
|
93
|
+
|
|
94
|
+
Exact memory arithmetic, and *measured* quality when you point it at a model:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
$ effbelt modelcard gemma-3-270m --seq 8192 --live google/gemma-3-270m-it
|
|
98
|
+
kv dtype kv cache vs fp16 quality
|
|
99
|
+
fp16 0.14GB 1.00x +0.00% ppl
|
|
100
|
+
int8 0.07GB 2.00x -0.33% ppl <- free: 2x smaller, lossless
|
|
101
|
+
int4 0.04GB 4.00x +68.41% ppl <- breaks on this small model
|
|
102
|
+
quality measured on google/gemma-3-270m-it (160 tok, MPS/CPU)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## `cheapest` — smallest model that still passes
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from effbelt import cheapest
|
|
109
|
+
res = cheapest.sweep([
|
|
110
|
+
cheapest.Candidate("7b-int4", cost=1, quality_fn=lambda: my_eval("7b-int4")),
|
|
111
|
+
cheapest.Candidate("7b-fp16", cost=2, quality_fn=lambda: my_eval("7b-fp16")),
|
|
112
|
+
cheapest.Candidate("70b-int4", cost=8, quality_fn=lambda: my_eval("70b-int4")),
|
|
113
|
+
], bar=0.80)
|
|
114
|
+
print(res.report()) # -> use the cheapest one that clears 0.80
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Honest scope
|
|
120
|
+
- Prices and the small model table are **approximate and editable** (see
|
|
121
|
+
`effbelt/models.py`); for authoritative model dims use `fit --hf`.
|
|
122
|
+
- `quantcheck`'s effective-bit estimate is advisory at small group sizes — the
|
|
123
|
+
*silent-no-op* and *idempotency* checks are the reliable ones.
|
|
124
|
+
- `modelcard` quality numbers are measured only when you pass `--live`; otherwise
|
|
125
|
+
the quality column is left blank. We don't fill it with guesses.
|
|
126
|
+
|
|
127
|
+
## Tests
|
|
128
|
+
All validation runs on a laptop, offline:
|
|
129
|
+
```bash
|
|
130
|
+
python -m pytest tests/ -v # 18 tests, incl. the real-cache cross-check
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Support
|
|
134
|
+
Free and MIT. If it saved you a GPU or a bill, consider sponsoring:
|
|
135
|
+
**https://github.com/sponsors/SRKRZ23**
|
|
136
|
+
|
|
137
|
+
Crypto (receive-only):
|
|
138
|
+
- BTC `bc1q9hm4upksmvkzhl8z3zvxgz94da358qfe3tred0`
|
|
139
|
+
- ETH / Polygon / USDT-ERC20 `0xa9e3b63799f5E5cc6258042D45869a5a1d39F021`
|
|
140
|
+
- USDT-TRC20 `TUyNkMXme1CYAc1wZ8SZ64X9upBc9nwTZ5`
|
|
141
|
+
- SOL `j8zZCsWwM8WmUZwMSDHikNuz6E49Vn8nwo9FMQnHSoW`
|
|
142
|
+
|
|
143
|
+
Sister project: [kvquant](https://github.com/SRKRZ23/kvquant) — the engine behind
|
|
144
|
+
`quantcheck` and `modelcard`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""effbelt — the LLM efficiency toolbelt.
|
|
2
|
+
|
|
3
|
+
Six small, honest tools that save money and catch silent bugs, all runnable
|
|
4
|
+
on a laptop with no GPU and no API key:
|
|
5
|
+
|
|
6
|
+
fit will this model fit on my card? (weights + KV cache + overhead)
|
|
7
|
+
tokenwaste where your LLM API bill leaks (duplicate prompts, retries, ...)
|
|
8
|
+
evalsanity is your benchmark lying? (contamination, degenerate data/preds)
|
|
9
|
+
quantcheck did your quantization actually do anything? (silent no-op guard)
|
|
10
|
+
cheapest the cheapest config that still clears your quality bar
|
|
11
|
+
modelcard one-page quant tradeoff card (arithmetic memory + measured ppl)
|
|
12
|
+
|
|
13
|
+
Sister project: kvquant (the engine behind quantcheck/modelcard).
|
|
14
|
+
MIT. If it saved you a GPU, consider sponsoring.
|
|
15
|
+
"""
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
from . import memory, tokenwaste, evalsanity, quantcheck, cheapest, modelcard
|
|
19
|
+
|
|
20
|
+
__all__ = ["memory", "tokenwaste", "evalsanity", "quantcheck", "cheapest",
|
|
21
|
+
"modelcard", "__version__"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""`cheapest` — the smallest/cheapest config that still passes YOUR bar.
|
|
2
|
+
|
|
3
|
+
You give candidate configs (each with a relative cost and a way to score
|
|
4
|
+
quality on your own task) and a quality bar. cheapest runs the sweep and
|
|
5
|
+
returns the lowest-cost config that clears the bar. The quality function is
|
|
6
|
+
yours (an eval on your data); the logic here is just the honest sweep +
|
|
7
|
+
pick, so the recommendation is only as good as your bar and your eval.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Candidate:
|
|
16
|
+
name: str
|
|
17
|
+
cost: float # relative $ or GB or latency -- your unit
|
|
18
|
+
quality_fn: Callable[[], float] # returns a score; higher is better
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SweepRow:
|
|
23
|
+
name: str
|
|
24
|
+
cost: float
|
|
25
|
+
quality: float
|
|
26
|
+
passes: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CheapestResult:
|
|
31
|
+
bar: float
|
|
32
|
+
rows: list[SweepRow]
|
|
33
|
+
pick: SweepRow | None
|
|
34
|
+
|
|
35
|
+
def report(self) -> str:
|
|
36
|
+
lines = [f"quality bar = {self.bar:g}"]
|
|
37
|
+
for r in sorted(self.rows, key=lambda x: x.cost):
|
|
38
|
+
mark = "PASS" if r.passes else "fail"
|
|
39
|
+
star = " <= cheapest passing" if self.pick and r.name == self.pick.name else ""
|
|
40
|
+
lines.append(f" cost {r.cost:8.3f} q={r.quality:.4f} [{mark}] "
|
|
41
|
+
f"{r.name}{star}")
|
|
42
|
+
if self.pick:
|
|
43
|
+
lines.append(f"-> use {self.pick.name!r} (cost {self.pick.cost:g}, "
|
|
44
|
+
f"q={self.pick.quality:.4f})")
|
|
45
|
+
else:
|
|
46
|
+
lines.append("-> NOTHING clears the bar; raise budget or lower the bar.")
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sweep(candidates: list[Candidate], bar: float) -> CheapestResult:
|
|
51
|
+
rows = []
|
|
52
|
+
for c in candidates:
|
|
53
|
+
q = float(c.quality_fn())
|
|
54
|
+
rows.append(SweepRow(c.name, c.cost, q, q >= bar))
|
|
55
|
+
passing = [r for r in rows if r.passes]
|
|
56
|
+
pick = min(passing, key=lambda r: r.cost) if passing else None
|
|
57
|
+
return CheapestResult(bar, rows, pick)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""effbelt command-line interface: `effbelt <tool> ...`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _cmd_fit(a):
|
|
8
|
+
from .memory import fit, shape_from_hf
|
|
9
|
+
from .models import GPUS
|
|
10
|
+
gpu = a.gpu
|
|
11
|
+
if isinstance(gpu, str):
|
|
12
|
+
if gpu.lower() not in GPUS:
|
|
13
|
+
print(f"unknown gpu {gpu!r}; known: {sorted(GPUS)}", file=sys.stderr)
|
|
14
|
+
return 2
|
|
15
|
+
gpu = GPUS[gpu.lower()]
|
|
16
|
+
shape = shape_from_hf(a.hf) if a.hf else a.model
|
|
17
|
+
r = fit(shape, gpu_gb=float(gpu), seq_len=a.seq, batch=a.batch,
|
|
18
|
+
weight_dtype=a.wdtype, kv_dtype=a.kv, overhead_gb=a.overhead)
|
|
19
|
+
print(r.report())
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cmd_tokenwaste(a):
|
|
24
|
+
from .tokenwaste import analyze, load_jsonl
|
|
25
|
+
recs = load_jsonl(a.log)
|
|
26
|
+
print(analyze(recs, long_context_tokens=a.long_context).report())
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_evalsanity(a):
|
|
31
|
+
from .evalsanity import check
|
|
32
|
+
import json
|
|
33
|
+
with open(a.data, encoding="utf-8") as f:
|
|
34
|
+
d = json.load(f)
|
|
35
|
+
rep = check(d["test"], d.get("train"), d.get("labels"), d.get("predictions"))
|
|
36
|
+
print(rep.report())
|
|
37
|
+
return 0 if rep.clean else 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_modelcard(a):
|
|
41
|
+
from .modelcard import build, measure_kv_quality
|
|
42
|
+
measured, on = None, None
|
|
43
|
+
if a.live:
|
|
44
|
+
measured = measure_kv_quality(a.live, max_tokens=a.max_tokens)
|
|
45
|
+
on = f"{a.live} ({a.max_tokens} tok, MPS/CPU)"
|
|
46
|
+
print(build(a.model, seq_len=a.seq, batch=a.batch,
|
|
47
|
+
weight_dtype=a.wdtype, measured_ppl=measured,
|
|
48
|
+
measured_on=on).report())
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
53
|
+
p = argparse.ArgumentParser(prog="effbelt",
|
|
54
|
+
description="LLM efficiency toolbelt")
|
|
55
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
56
|
+
|
|
57
|
+
f = sub.add_parser("fit", help="will this model fit on my card?")
|
|
58
|
+
f.add_argument("model", help="known model name, or use --hf")
|
|
59
|
+
f.add_argument("--hf", help="authoritative dims from a HF repo id")
|
|
60
|
+
f.add_argument("--gpu", default="rtx-4090", help="gpu name or GB number")
|
|
61
|
+
f.add_argument("--seq", type=int, default=4096)
|
|
62
|
+
f.add_argument("--batch", type=int, default=1)
|
|
63
|
+
f.add_argument("--wdtype", default="fp16")
|
|
64
|
+
f.add_argument("--kv", default="fp16")
|
|
65
|
+
f.add_argument("--overhead", type=float, default=1.0)
|
|
66
|
+
f.set_defaults(func=_cmd_fit)
|
|
67
|
+
|
|
68
|
+
t = sub.add_parser("tokenwaste", help="audit an API request log (jsonl)")
|
|
69
|
+
t.add_argument("log")
|
|
70
|
+
t.add_argument("--long-context", type=int, default=8000)
|
|
71
|
+
t.set_defaults(func=_cmd_tokenwaste)
|
|
72
|
+
|
|
73
|
+
e = sub.add_parser("evalsanity", help="is your benchmark lying?")
|
|
74
|
+
e.add_argument("data", help="json with keys: test[, train, labels, predictions]")
|
|
75
|
+
e.set_defaults(func=_cmd_evalsanity)
|
|
76
|
+
|
|
77
|
+
m = sub.add_parser("modelcard", help="quant tradeoff card")
|
|
78
|
+
m.add_argument("model")
|
|
79
|
+
m.add_argument("--seq", type=int, default=4096)
|
|
80
|
+
m.add_argument("--batch", type=int, default=1)
|
|
81
|
+
m.add_argument("--wdtype", default="fp16")
|
|
82
|
+
m.add_argument("--live", help="HF repo id to MEASURE real ppl deltas")
|
|
83
|
+
m.add_argument("--max-tokens", type=int, default=200)
|
|
84
|
+
m.set_defaults(func=_cmd_modelcard)
|
|
85
|
+
|
|
86
|
+
return p
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main(argv=None) -> int:
|
|
90
|
+
args = build_parser().parse_args(argv)
|
|
91
|
+
try:
|
|
92
|
+
gpu = getattr(args, "gpu", None)
|
|
93
|
+
if gpu is not None and gpu.replace(".", "", 1).isdigit():
|
|
94
|
+
args.gpu = float(gpu)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return args.func(args)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""`evalsanity` — is your benchmark lying to you?
|
|
2
|
+
|
|
3
|
+
Cheap, deterministic checks that catch the usual ways an eval number is
|
|
4
|
+
inflated or meaningless BEFORE you trust it:
|
|
5
|
+
- train/test contamination (exact + near-dup via shingled Jaccard)
|
|
6
|
+
- degenerate data (duplicate rows, near-constant labels)
|
|
7
|
+
- degenerate predictions (model emits one answer, or copies the input)
|
|
8
|
+
- too-good-to-be-true accuracy on a tiny / imbalanced set
|
|
9
|
+
No model required. Works on lists of strings.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _shingles(text: str, k: int = 5) -> set[str]:
|
|
17
|
+
toks = re.findall(r"\w+", text.lower())
|
|
18
|
+
if len(toks) < k:
|
|
19
|
+
return {" ".join(toks)} if toks else set()
|
|
20
|
+
return {" ".join(toks[i:i + k]) for i in range(len(toks) - k + 1)}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _jaccard(a: set[str], b: set[str]) -> float:
|
|
24
|
+
if not a or not b:
|
|
25
|
+
return 0.0
|
|
26
|
+
return len(a & b) / len(a | b)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class SanityReport:
|
|
31
|
+
n_test: int
|
|
32
|
+
flags: list[tuple[str, str]] = field(default_factory=list) # (severity, msg)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def clean(self) -> bool:
|
|
36
|
+
return not any(s == "HIGH" for s, _ in self.flags)
|
|
37
|
+
|
|
38
|
+
def report(self) -> str:
|
|
39
|
+
if not self.flags:
|
|
40
|
+
return f"{self.n_test} test items: no sanity problems found."
|
|
41
|
+
lines = [f"{self.n_test} test items: {len(self.flags)} flag(s)"]
|
|
42
|
+
for sev, msg in sorted(self.flags, key=lambda x: {"HIGH": 0, "MED": 1,
|
|
43
|
+
"LOW": 2}[x[0]]):
|
|
44
|
+
lines.append(f" [{sev:4s}] {msg}")
|
|
45
|
+
if not self.clean:
|
|
46
|
+
lines.append(" -> at least one HIGH flag: do NOT trust this number yet.")
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check(test_texts: list[str],
|
|
51
|
+
train_texts: list[str] | None = None,
|
|
52
|
+
labels: list | None = None,
|
|
53
|
+
predictions: list | None = None,
|
|
54
|
+
near_dup_threshold: float = 0.8) -> SanityReport:
|
|
55
|
+
flags: list[tuple[str, str]] = []
|
|
56
|
+
n = len(test_texts)
|
|
57
|
+
|
|
58
|
+
# ---- contamination ----
|
|
59
|
+
if train_texts:
|
|
60
|
+
train_set = set(t.strip() for t in train_texts)
|
|
61
|
+
exact = sum(1 for t in test_texts if t.strip() in train_set)
|
|
62
|
+
if exact:
|
|
63
|
+
flags.append(("HIGH", f"{exact}/{n} test items appear VERBATIM in "
|
|
64
|
+
f"train -> contamination"))
|
|
65
|
+
# near-dup (sampled to stay cheap)
|
|
66
|
+
tr_sh = [_shingles(t) for t in train_texts[:2000]]
|
|
67
|
+
near = 0
|
|
68
|
+
for t in test_texts[:2000]:
|
|
69
|
+
sh = _shingles(t)
|
|
70
|
+
if any(_jaccard(sh, trs) >= near_dup_threshold for trs in tr_sh):
|
|
71
|
+
near += 1
|
|
72
|
+
if near > exact:
|
|
73
|
+
flags.append(("HIGH", f"{near}/{min(n,2000)} test items are NEAR "
|
|
74
|
+
f"-duplicates of train (Jaccard>="
|
|
75
|
+
f"{near_dup_threshold})"))
|
|
76
|
+
|
|
77
|
+
# ---- degenerate test data ----
|
|
78
|
+
uniq = len(set(test_texts))
|
|
79
|
+
if n and uniq < n:
|
|
80
|
+
flags.append(("MED", f"{n-uniq}/{n} duplicate test items"))
|
|
81
|
+
if labels is not None:
|
|
82
|
+
from collections import Counter
|
|
83
|
+
c = Counter(labels)
|
|
84
|
+
top, topn = c.most_common(1)[0]
|
|
85
|
+
frac = topn / len(labels)
|
|
86
|
+
if frac >= 0.95:
|
|
87
|
+
flags.append(("HIGH", f"labels {frac*100:.0f}% one class ({top!r}) "
|
|
88
|
+
f"-> accuracy is meaningless; use balanced metric"))
|
|
89
|
+
elif frac >= 0.8:
|
|
90
|
+
flags.append(("MED", f"labels {frac*100:.0f}% one class ({top!r}) "
|
|
91
|
+
f"-> imbalanced"))
|
|
92
|
+
|
|
93
|
+
# ---- degenerate predictions ----
|
|
94
|
+
if predictions is not None:
|
|
95
|
+
from collections import Counter
|
|
96
|
+
pc = Counter(predictions)
|
|
97
|
+
ptop, ptopn = pc.most_common(1)[0]
|
|
98
|
+
if ptopn / len(predictions) >= 0.95 and len(pc) > 1:
|
|
99
|
+
flags.append(("HIGH", f"predictions {100*ptopn/len(predictions):.0f}% "
|
|
100
|
+
f"a single value ({ptop!r}) -> model collapsed"))
|
|
101
|
+
elif len(pc) == 1:
|
|
102
|
+
flags.append(("HIGH", f"predictions are ALL the same value ({ptop!r})"))
|
|
103
|
+
# copying the input?
|
|
104
|
+
if len(predictions) == len(test_texts):
|
|
105
|
+
copied = sum(1 for p, t in zip(predictions, test_texts)
|
|
106
|
+
if isinstance(p, str) and p.strip() == t.strip())
|
|
107
|
+
if copied / len(predictions) >= 0.3:
|
|
108
|
+
flags.append(("MED", f"{copied}/{len(predictions)} predictions "
|
|
109
|
+
f"equal the input text -> possible copying"))
|
|
110
|
+
|
|
111
|
+
# ---- tiny set ----
|
|
112
|
+
if 0 < n < 30:
|
|
113
|
+
flags.append(("LOW", f"only {n} test items -> wide confidence interval; "
|
|
114
|
+
f"a few flips swing the score"))
|
|
115
|
+
return SanityReport(n, flags)
|