inferlast 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.
- inferlast-0.1.0/LICENSE +21 -0
- inferlast-0.1.0/PKG-INFO +211 -0
- inferlast-0.1.0/README.md +186 -0
- inferlast-0.1.0/pyproject.toml +49 -0
- inferlast-0.1.0/setup.cfg +4 -0
- inferlast-0.1.0/src/auto_optimizer.py +159 -0
- inferlast-0.1.0/src/batcher.py +99 -0
- inferlast-0.1.0/src/cli.py +47 -0
- inferlast-0.1.0/src/gpucheck.py +210 -0
- inferlast-0.1.0/src/inferlast.egg-info/PKG-INFO +211 -0
- inferlast-0.1.0/src/inferlast.egg-info/SOURCES.txt +22 -0
- inferlast-0.1.0/src/inferlast.egg-info/dependency_links.txt +1 -0
- inferlast-0.1.0/src/inferlast.egg-info/entry_points.txt +2 -0
- inferlast-0.1.0/src/inferlast.egg-info/requires.txt +3 -0
- inferlast-0.1.0/src/inferlast.egg-info/top_level.txt +7 -0
- inferlast-0.1.0/src/profiler.py +280 -0
- inferlast-0.1.0/src/quantize.py +193 -0
- inferlast-0.1.0/src/trustcheck.py +283 -0
- inferlast-0.1.0/tests/test_batcher.py +60 -0
- inferlast-0.1.0/tests/test_gpucheck.py +81 -0
- inferlast-0.1.0/tests/test_profiler.py +83 -0
- inferlast-0.1.0/tests/test_quantize.py +94 -0
- inferlast-0.1.0/tests/test_report_schema.py +42 -0
- inferlast-0.1.0/tests/test_trustcheck.py +135 -0
inferlast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuvraj Singh Bhadoria
|
|
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.
|
inferlast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inferlast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CPU-first LLM inference optimizer: prove a real win on your laptop CPU before you pay to rent a GPU. Trustcheck catches any benchmark that is really just noise.
|
|
5
|
+
Author-email: Yuvraj Singh Bhadoria <yuvrajsinghbhado2030@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YuvrajSinghBhadoria2/inferlast
|
|
8
|
+
Project-URL: Repository, https://github.com/YuvrajSinghBhadoria2/inferlast
|
|
9
|
+
Project-URL: Releases, https://github.com/YuvrajSinghBhadoria2/inferlast/releases
|
|
10
|
+
Keywords: llm,inference,optimization,cpu,quantization,benchmark,trustcheck
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: torch<2.5,>=2.2
|
|
22
|
+
Requires-Dist: transformers<4.47,>=4.40
|
|
23
|
+
Requires-Dist: numpy<2,>=1.26
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<br/>
|
|
28
|
+
<img src="https://img.shields.io/badge/status-Phase%201%20(CPU)-informational" alt="Phase 1 CPU"/>
|
|
29
|
+
<img src="https://img.shields.io/badge/tests-55%20passing-brightgreen" alt="tests"/>
|
|
30
|
+
<img src="https://img.shields.io/badge/license-MIT-blue" alt="license"/>
|
|
31
|
+
<img src="https://img.shields.io/badge/python-3.10%2F3.11%2F3.12-blue" alt="python"/>
|
|
32
|
+
<a href="https://github.com/YuvrajSinghBhadoria2/inferlast/releases"><img src="https://img.shields.io/github/v/release/YuvrajSinghBhadoria2/inferlast" alt="release"/></a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<h1 align="center">inferlast</h1>
|
|
36
|
+
<p align="center"><b>Fine-tune-free, GPU-free inference:</b> prove a real optimization win on a plain laptop CPU <i>before</i> you ever pay to rent one.<br/>
|
|
37
|
+
Most demos tell you a change is <b>3x faster</b> — inferlast catches when that number is just machine noise.</p>
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="#why">Why</a> · <a href="#30-second-try">30-second try</a> · <a href="#what-it-caught">What it caught on my machine</a> · <a href="#how-it-works">How it works</a> · <a href="#roadmap">Roadmap</a>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
Most LLM-inference guides tell you what to do: *quantize to INT8, batch bigger, grab a GPU.* They don't tell you **whether it helps *your* model on *your* hardware** — and they quietly assume you can rent a GPU to find out.
|
|
48
|
+
|
|
49
|
+
inferlast is the opposite. It is **CPU-first by design**: it profiles and optimizes entirely on the CPU you already have, so **anyone can run it — no GPU, no cloud GPU bill, no CUDA install.** And when you *do* move to a GPU later, inferlast tells you honestly whether it was even worth it.
|
|
50
|
+
|
|
51
|
+
> **Profile → pick → apply → prove.** And if the measurement says an "obvious" optimization doesn't help, inferlast says so — instead of making you guess wrong.
|
|
52
|
+
|
|
53
|
+
This is the core of what inference engineers actually do: not "apply the standard thing," but **find where the time really goes and only ship changes that provably pay off** — cheaply enough that you don't need a GPU to do it.
|
|
54
|
+
|
|
55
|
+
> **Hardware scope — Phase 1 is CPU-only.** Built and measured on a 2019 Intel MacBook Pro 16" (i7-9750H, 6C/12T, 16 GB, no GPU). Findings are CPU-specific and stated as such; on GPU the same model is typically weight-bandwidth-bound, not overhead-bound, so results would differ.
|
|
56
|
+
|
|
57
|
+
The project is organized around one falsifiable claim — **a decision rule for when GPU spend is actually warranted, estimable from CPU-only measurement.** That thesis, its boundaries, and its frozen success test live in [`docs/RESEARCH-SPEC.md`](docs/RESEARCH-SPEC.md).
|
|
58
|
+
|
|
59
|
+
## The honest insight it encodes
|
|
60
|
+
|
|
61
|
+
For tiny models on CPU, inferlast measures that **~98–99% of decode wall time is framework overhead, not model math**. So **blindly quantizing the weights will not speed up an overhead-bound model** — and inferlast *measures* that rather than pretending otherwise. That refusal-to-guess behavior is the whole point.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install inferlast # Python 3.10-3.12; CPU-first, no GPU/CUDA needed
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This installs the core (`import trustcheck`, `import gpucheck`, ...) and the
|
|
70
|
+
`inferlast` CLI. Or run straight from the repo:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/YuvrajSinghBhadoria2/inferlast.git
|
|
74
|
+
pip install -r requirements.txt
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 30-second try
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install -r requirements.txt # torch CPU, transformers, pytest
|
|
81
|
+
python scripts/run_all.py --model Qwen/Qwen2.5-0.5B-Instruct
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
One command. One report that tells you:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
Bottleneck: mlp 47% · attention 41% · norm 11% (~99% overhead on CPU)
|
|
88
|
+
Decode: ~1220 ms/token (0.8 tok/s)
|
|
89
|
+
Quantization: INT8 = 1.5x but shifts the answer → KEEP fp32 (not a win)
|
|
90
|
+
Batching: B=16 → ~48 tok/s total Pick B for your goal.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
And the part that catches the false wins — `trustcheck` on this repo's own
|
|
94
|
+
recorded evidence (the same config that reported 3.0x faster):
|
|
95
|
+
|
|
96
|
+
<p align="center">
|
|
97
|
+
<img src="assets/trustcheck-proof.png" alt="trustcheck catches a false win" width="640"/>
|
|
98
|
+
</p>
|
|
99
|
+
|
|
100
|
+
Run the individual stages to look closer:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python scripts/bench.py --model <m> # prefill profile
|
|
104
|
+
python scripts/bench.py --model <m> --decode # decode / per-token latency
|
|
105
|
+
python scripts/bench.py --model <m> --quant # auto-quantization verdict
|
|
106
|
+
python scripts/bench.py --model <m> --batch # latency vs throughput sweep
|
|
107
|
+
python scripts/bench.py --model <m> --trustcheck # is that win real? (see below)
|
|
108
|
+
python scripts/bench.py --model <m> --gpucheck # do you even need a GPU? (see below)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## What it caught on my machine
|
|
112
|
+
|
|
113
|
+
A quiet, annoying truth that most optimization tutorials skip: **on an overhead-bound CPU model, INT8 quantization is not a free win.** inferlast measured it three ways and told the truth:
|
|
114
|
+
|
|
115
|
+
| | fp32 | INT8 | verdict |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| speed (Qwen 0.5B) | 1.31 s/tok | 0.87 s/tok | 1.5x — *but* |
|
|
118
|
+
| quality (logit-cosine) | — | 0.68 | preserved-ish |
|
|
119
|
+
| quality (top-5 overlap) | — | **0.15** | **token ranking shifted** |
|
|
120
|
+
|
|
121
|
+
INT8 was faster but disturbed what the model would actually say. inferlast's call: **KEEP fp32.** That's not a bug — it's the tool doing its job: *refuse to recommend a change that isn't a real win.*
|
|
122
|
+
|
|
123
|
+
The full measured records are in `benchmarks/` (JSON + markdown), published as measured — the failures included.
|
|
124
|
+
|
|
125
|
+
## How it works
|
|
126
|
+
|
|
127
|
+
Six small modules, one job each:
|
|
128
|
+
|
|
129
|
+
| Module | Job |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `src/profiler.py` | Per-category (attention / mlp / norm / embed / head) wall-clock profile, and the overhead-vs-weight split. |
|
|
132
|
+
| `src/quantize.py` | Auto-quantization (INT8 dynamic). Measures fp32 vs INT8 averaged over repeats, with a **robust quality metric** (per-token logit cosine + top-5 overlap) — not brittle greedy-token identity. |
|
|
133
|
+
| `src/batcher.py` | Latency-vs-throughput sweep over batch size, with a best-batch picker. |
|
|
134
|
+
| `src/trustcheck.py` | **Is that 'win' worth trusting?** Audits any before/after benchmark for the three ways it lies: single-run noise, a brittle/wrong metric, and a "validated, documented, read by nothing" knob. Returns a REAL / MARGINAL / FALSE verdict. |
|
|
135
|
+
| `src/gpucheck.py` | **Do you even need a GPU?** Estimates, from CPU-only measurements, whether GPU spend would actually beat the best-scheduled CPU config. Returns GPU-warranted / CPU-suffices / insufficient-data — and refuses to guess when it can't tell. |
|
|
136
|
+
| `src/auto_optimizer.py` | Orchestrator: runs all four, emits a combined proof report + JSON. |
|
|
137
|
+
| `scripts/` | `run_all.py` (one command) + `bench.py` (per stage). |
|
|
138
|
+
|
|
139
|
+
A key design decision: `run_all.py` **always persists** a canonical report, so the evidence on disk always matches the latest run — it can't go stale.
|
|
140
|
+
|
|
141
|
+
## gpucheck — the part that stops you overspending on hardware
|
|
142
|
+
|
|
143
|
+
The whole project is built around one falsifiable claim (in
|
|
144
|
+
[`docs/RESEARCH-SPEC.md`](docs/RESEARCH-SPEC.md)): **for overhead-bound small models on CPU, you
|
|
145
|
+
usually don't need a GPU at all.** `gpucheck` puts that to the test from local CPU measurement:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python scripts/bench.py --model <m> \
|
|
149
|
+
--gpucheck --num-params 0.5e9 --overhead-fraction 0.98 --latency-target-ms 2000
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It labels the decision — **GPU-warranted / CPU-suffices / insufficient-data** — with the exact inputs
|
|
153
|
+
and reasoning, and it **refuses to guess** (returns `insufficient-data`) whenever it genuinely cannot
|
|
154
|
+
tell from the data you gave it. That refusal is a feature: it never sells you a GPU rental it can't
|
|
155
|
+
defend.
|
|
156
|
+
|
|
157
|
+
## trustcheck — the part that tells you your benchmark lied
|
|
158
|
+
|
|
159
|
+
Most tools *produce* a number. `trustcheck` tells you whether to believe it. It caught all three lies live on this repo's own evidence:
|
|
160
|
+
|
|
161
|
+
- **Single-run noise.** The same INT8-vs-fp32 config, measured twice, gave `3.0x` faster in one session and `0.65x` **slower** in another. `trustcheck` computes the confidence interval and says: *"CI [-1.8x, 5.5x] straddles 1.0x → not a reliable win."* A naive dashboard would have reported `3.0x`.
|
|
162
|
+
- **Brittle metric.** A quality comparison at greedy-token level, with no logits, is flagged as "can lie" and, when logits are available, re-measured with logit cosine + top-5 overlap.
|
|
163
|
+
- **Read-by-nothing knob.** A flag that is documented/validated but never read by any code is a silent bug (the class of bug Soup and vLLM chased); `trustcheck`'s static pass flags it.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
python scripts/bench.py --trustcheck \
|
|
167
|
+
--controls 1180 1316 --treatments 390 2031 # audit two real sessions
|
|
168
|
+
python scripts/bench.py --model <m> --trustcheck \
|
|
169
|
+
--collect-repeats 2 --config-key stream_layers # measure the noise band live
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Tests
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install pytest
|
|
176
|
+
python -m pytest # 55 fast tests, no model downloads
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The suite guards the things that would sink a tool like this: profiler categorisation & no-double-counting, the robust INT8 quality metric + the honest decision rule, batcher best-batch selection, the `trustcheck` noise/brittle-metric/read-by-nothing logic, the `gpucheck` GPU-necessity decision rule (including its refusal to guess when data is missing), and a **regression test that the report always emits the new metrics — never a stale one.**
|
|
180
|
+
|
|
181
|
+
## Roadmap
|
|
182
|
+
|
|
183
|
+
Phase 1 is done and runs on a plain laptop CPU — no GPU needed.
|
|
184
|
+
|
|
185
|
+
**Shipped:**
|
|
186
|
+
|
|
187
|
+
- **`trustcheck` — the false-win catcher.** Tells you the benchmark you were
|
|
188
|
+
about to publish is machine noise, not a win. It caught *this repo's own*
|
|
189
|
+
3.0x-vs-0.65x as noise.
|
|
190
|
+
- **`gpucheck` — "do you even need a GPU?"** A CPU-only decision rule:
|
|
191
|
+
GPU-warranted / CPU-suffices / insufficient-data. It refuses to guess when it
|
|
192
|
+
can't tell.
|
|
193
|
+
- Bottleneck / decode / quant / batch selection.
|
|
194
|
+
|
|
195
|
+
**Where we want help next** (strongest help first):
|
|
196
|
+
|
|
197
|
+
- **Validate the `gpucheck` boundary on hardware we can't reach.** Your numbers
|
|
198
|
+
ship behind an honest "requires <hardware>" gate — a 4 GB card, an Apple
|
|
199
|
+
Silicon box, a desktop with more RAM. Proof on hardware we lack turns a claim
|
|
200
|
+
into a finding. *This is the most valuable way to contribute right now.*
|
|
201
|
+
- Quantization beyond INT8 (FP4 / INT4), latency percentiles (p50/p99), memory /
|
|
202
|
+
KV-cache footprint, and serving-engine integration (vLLM / llama.cpp) as an
|
|
203
|
+
enrichment layer.
|
|
204
|
+
|
|
205
|
+
PRs welcome. Nothing here is a live claim — it's the plan.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT — free, stays free, built in the open. If inferlast saved you a guessing session, a star helps others find it.
|
|
210
|
+
|
|
211
|
+
<sub>Not a replacement for vLLM / llama.cpp — a *decision layer* that runs on CPU and tells you which setting is right for your model and hardware, with proof, before you spend on a GPU.</sub>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<br/>
|
|
3
|
+
<img src="https://img.shields.io/badge/status-Phase%201%20(CPU)-informational" alt="Phase 1 CPU"/>
|
|
4
|
+
<img src="https://img.shields.io/badge/tests-55%20passing-brightgreen" alt="tests"/>
|
|
5
|
+
<img src="https://img.shields.io/badge/license-MIT-blue" alt="license"/>
|
|
6
|
+
<img src="https://img.shields.io/badge/python-3.10%2F3.11%2F3.12-blue" alt="python"/>
|
|
7
|
+
<a href="https://github.com/YuvrajSinghBhadoria2/inferlast/releases"><img src="https://img.shields.io/github/v/release/YuvrajSinghBhadoria2/inferlast" alt="release"/></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<h1 align="center">inferlast</h1>
|
|
11
|
+
<p align="center"><b>Fine-tune-free, GPU-free inference:</b> prove a real optimization win on a plain laptop CPU <i>before</i> you ever pay to rent one.<br/>
|
|
12
|
+
Most demos tell you a change is <b>3x faster</b> — inferlast catches when that number is just machine noise.</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="#why">Why</a> · <a href="#30-second-try">30-second try</a> · <a href="#what-it-caught">What it caught on my machine</a> · <a href="#how-it-works">How it works</a> · <a href="#roadmap">Roadmap</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why
|
|
21
|
+
|
|
22
|
+
Most LLM-inference guides tell you what to do: *quantize to INT8, batch bigger, grab a GPU.* They don't tell you **whether it helps *your* model on *your* hardware** — and they quietly assume you can rent a GPU to find out.
|
|
23
|
+
|
|
24
|
+
inferlast is the opposite. It is **CPU-first by design**: it profiles and optimizes entirely on the CPU you already have, so **anyone can run it — no GPU, no cloud GPU bill, no CUDA install.** And when you *do* move to a GPU later, inferlast tells you honestly whether it was even worth it.
|
|
25
|
+
|
|
26
|
+
> **Profile → pick → apply → prove.** And if the measurement says an "obvious" optimization doesn't help, inferlast says so — instead of making you guess wrong.
|
|
27
|
+
|
|
28
|
+
This is the core of what inference engineers actually do: not "apply the standard thing," but **find where the time really goes and only ship changes that provably pay off** — cheaply enough that you don't need a GPU to do it.
|
|
29
|
+
|
|
30
|
+
> **Hardware scope — Phase 1 is CPU-only.** Built and measured on a 2019 Intel MacBook Pro 16" (i7-9750H, 6C/12T, 16 GB, no GPU). Findings are CPU-specific and stated as such; on GPU the same model is typically weight-bandwidth-bound, not overhead-bound, so results would differ.
|
|
31
|
+
|
|
32
|
+
The project is organized around one falsifiable claim — **a decision rule for when GPU spend is actually warranted, estimable from CPU-only measurement.** That thesis, its boundaries, and its frozen success test live in [`docs/RESEARCH-SPEC.md`](docs/RESEARCH-SPEC.md).
|
|
33
|
+
|
|
34
|
+
## The honest insight it encodes
|
|
35
|
+
|
|
36
|
+
For tiny models on CPU, inferlast measures that **~98–99% of decode wall time is framework overhead, not model math**. So **blindly quantizing the weights will not speed up an overhead-bound model** — and inferlast *measures* that rather than pretending otherwise. That refusal-to-guess behavior is the whole point.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install inferlast # Python 3.10-3.12; CPU-first, no GPU/CUDA needed
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This installs the core (`import trustcheck`, `import gpucheck`, ...) and the
|
|
45
|
+
`inferlast` CLI. Or run straight from the repo:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/YuvrajSinghBhadoria2/inferlast.git
|
|
49
|
+
pip install -r requirements.txt
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 30-second try
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -r requirements.txt # torch CPU, transformers, pytest
|
|
56
|
+
python scripts/run_all.py --model Qwen/Qwen2.5-0.5B-Instruct
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
One command. One report that tells you:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Bottleneck: mlp 47% · attention 41% · norm 11% (~99% overhead on CPU)
|
|
63
|
+
Decode: ~1220 ms/token (0.8 tok/s)
|
|
64
|
+
Quantization: INT8 = 1.5x but shifts the answer → KEEP fp32 (not a win)
|
|
65
|
+
Batching: B=16 → ~48 tok/s total Pick B for your goal.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And the part that catches the false wins — `trustcheck` on this repo's own
|
|
69
|
+
recorded evidence (the same config that reported 3.0x faster):
|
|
70
|
+
|
|
71
|
+
<p align="center">
|
|
72
|
+
<img src="assets/trustcheck-proof.png" alt="trustcheck catches a false win" width="640"/>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
Run the individual stages to look closer:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python scripts/bench.py --model <m> # prefill profile
|
|
79
|
+
python scripts/bench.py --model <m> --decode # decode / per-token latency
|
|
80
|
+
python scripts/bench.py --model <m> --quant # auto-quantization verdict
|
|
81
|
+
python scripts/bench.py --model <m> --batch # latency vs throughput sweep
|
|
82
|
+
python scripts/bench.py --model <m> --trustcheck # is that win real? (see below)
|
|
83
|
+
python scripts/bench.py --model <m> --gpucheck # do you even need a GPU? (see below)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## What it caught on my machine
|
|
87
|
+
|
|
88
|
+
A quiet, annoying truth that most optimization tutorials skip: **on an overhead-bound CPU model, INT8 quantization is not a free win.** inferlast measured it three ways and told the truth:
|
|
89
|
+
|
|
90
|
+
| | fp32 | INT8 | verdict |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| speed (Qwen 0.5B) | 1.31 s/tok | 0.87 s/tok | 1.5x — *but* |
|
|
93
|
+
| quality (logit-cosine) | — | 0.68 | preserved-ish |
|
|
94
|
+
| quality (top-5 overlap) | — | **0.15** | **token ranking shifted** |
|
|
95
|
+
|
|
96
|
+
INT8 was faster but disturbed what the model would actually say. inferlast's call: **KEEP fp32.** That's not a bug — it's the tool doing its job: *refuse to recommend a change that isn't a real win.*
|
|
97
|
+
|
|
98
|
+
The full measured records are in `benchmarks/` (JSON + markdown), published as measured — the failures included.
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
Six small modules, one job each:
|
|
103
|
+
|
|
104
|
+
| Module | Job |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `src/profiler.py` | Per-category (attention / mlp / norm / embed / head) wall-clock profile, and the overhead-vs-weight split. |
|
|
107
|
+
| `src/quantize.py` | Auto-quantization (INT8 dynamic). Measures fp32 vs INT8 averaged over repeats, with a **robust quality metric** (per-token logit cosine + top-5 overlap) — not brittle greedy-token identity. |
|
|
108
|
+
| `src/batcher.py` | Latency-vs-throughput sweep over batch size, with a best-batch picker. |
|
|
109
|
+
| `src/trustcheck.py` | **Is that 'win' worth trusting?** Audits any before/after benchmark for the three ways it lies: single-run noise, a brittle/wrong metric, and a "validated, documented, read by nothing" knob. Returns a REAL / MARGINAL / FALSE verdict. |
|
|
110
|
+
| `src/gpucheck.py` | **Do you even need a GPU?** Estimates, from CPU-only measurements, whether GPU spend would actually beat the best-scheduled CPU config. Returns GPU-warranted / CPU-suffices / insufficient-data — and refuses to guess when it can't tell. |
|
|
111
|
+
| `src/auto_optimizer.py` | Orchestrator: runs all four, emits a combined proof report + JSON. |
|
|
112
|
+
| `scripts/` | `run_all.py` (one command) + `bench.py` (per stage). |
|
|
113
|
+
|
|
114
|
+
A key design decision: `run_all.py` **always persists** a canonical report, so the evidence on disk always matches the latest run — it can't go stale.
|
|
115
|
+
|
|
116
|
+
## gpucheck — the part that stops you overspending on hardware
|
|
117
|
+
|
|
118
|
+
The whole project is built around one falsifiable claim (in
|
|
119
|
+
[`docs/RESEARCH-SPEC.md`](docs/RESEARCH-SPEC.md)): **for overhead-bound small models on CPU, you
|
|
120
|
+
usually don't need a GPU at all.** `gpucheck` puts that to the test from local CPU measurement:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python scripts/bench.py --model <m> \
|
|
124
|
+
--gpucheck --num-params 0.5e9 --overhead-fraction 0.98 --latency-target-ms 2000
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It labels the decision — **GPU-warranted / CPU-suffices / insufficient-data** — with the exact inputs
|
|
128
|
+
and reasoning, and it **refuses to guess** (returns `insufficient-data`) whenever it genuinely cannot
|
|
129
|
+
tell from the data you gave it. That refusal is a feature: it never sells you a GPU rental it can't
|
|
130
|
+
defend.
|
|
131
|
+
|
|
132
|
+
## trustcheck — the part that tells you your benchmark lied
|
|
133
|
+
|
|
134
|
+
Most tools *produce* a number. `trustcheck` tells you whether to believe it. It caught all three lies live on this repo's own evidence:
|
|
135
|
+
|
|
136
|
+
- **Single-run noise.** The same INT8-vs-fp32 config, measured twice, gave `3.0x` faster in one session and `0.65x` **slower** in another. `trustcheck` computes the confidence interval and says: *"CI [-1.8x, 5.5x] straddles 1.0x → not a reliable win."* A naive dashboard would have reported `3.0x`.
|
|
137
|
+
- **Brittle metric.** A quality comparison at greedy-token level, with no logits, is flagged as "can lie" and, when logits are available, re-measured with logit cosine + top-5 overlap.
|
|
138
|
+
- **Read-by-nothing knob.** A flag that is documented/validated but never read by any code is a silent bug (the class of bug Soup and vLLM chased); `trustcheck`'s static pass flags it.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python scripts/bench.py --trustcheck \
|
|
142
|
+
--controls 1180 1316 --treatments 390 2031 # audit two real sessions
|
|
143
|
+
python scripts/bench.py --model <m> --trustcheck \
|
|
144
|
+
--collect-repeats 2 --config-key stream_layers # measure the noise band live
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Tests
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install pytest
|
|
151
|
+
python -m pytest # 55 fast tests, no model downloads
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The suite guards the things that would sink a tool like this: profiler categorisation & no-double-counting, the robust INT8 quality metric + the honest decision rule, batcher best-batch selection, the `trustcheck` noise/brittle-metric/read-by-nothing logic, the `gpucheck` GPU-necessity decision rule (including its refusal to guess when data is missing), and a **regression test that the report always emits the new metrics — never a stale one.**
|
|
155
|
+
|
|
156
|
+
## Roadmap
|
|
157
|
+
|
|
158
|
+
Phase 1 is done and runs on a plain laptop CPU — no GPU needed.
|
|
159
|
+
|
|
160
|
+
**Shipped:**
|
|
161
|
+
|
|
162
|
+
- **`trustcheck` — the false-win catcher.** Tells you the benchmark you were
|
|
163
|
+
about to publish is machine noise, not a win. It caught *this repo's own*
|
|
164
|
+
3.0x-vs-0.65x as noise.
|
|
165
|
+
- **`gpucheck` — "do you even need a GPU?"** A CPU-only decision rule:
|
|
166
|
+
GPU-warranted / CPU-suffices / insufficient-data. It refuses to guess when it
|
|
167
|
+
can't tell.
|
|
168
|
+
- Bottleneck / decode / quant / batch selection.
|
|
169
|
+
|
|
170
|
+
**Where we want help next** (strongest help first):
|
|
171
|
+
|
|
172
|
+
- **Validate the `gpucheck` boundary on hardware we can't reach.** Your numbers
|
|
173
|
+
ship behind an honest "requires <hardware>" gate — a 4 GB card, an Apple
|
|
174
|
+
Silicon box, a desktop with more RAM. Proof on hardware we lack turns a claim
|
|
175
|
+
into a finding. *This is the most valuable way to contribute right now.*
|
|
176
|
+
- Quantization beyond INT8 (FP4 / INT4), latency percentiles (p50/p99), memory /
|
|
177
|
+
KV-cache footprint, and serving-engine integration (vLLM / llama.cpp) as an
|
|
178
|
+
enrichment layer.
|
|
179
|
+
|
|
180
|
+
PRs welcome. Nothing here is a live claim — it's the plan.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT — free, stays free, built in the open. If inferlast saved you a guessing session, a star helps others find it.
|
|
185
|
+
|
|
186
|
+
<sub>Not a replacement for vLLM / llama.cpp — a *decision layer* that runs on CPU and tells you which setting is right for your model and hardware, with proof, before you spend on a GPU.</sub>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inferlast"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CPU-first LLM inference optimizer: prove a real win on your laptop CPU before you pay to rent a GPU. Trustcheck catches any benchmark that is really just noise."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Yuvraj Singh Bhadoria", email = "yuvrajsinghbhado2030@gmail.com" }]
|
|
13
|
+
keywords = ["llm", "inference", "optimization", "cpu", "quantization", "benchmark", "trustcheck"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"torch>=2.2,<2.5",
|
|
25
|
+
"transformers>=4.40,<4.47",
|
|
26
|
+
"numpy>=1.26,<2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/YuvrajSinghBhadoria2/inferlast"
|
|
31
|
+
Repository = "https://github.com/YuvrajSinghBhadoria2/inferlast"
|
|
32
|
+
Releases = "https://github.com/YuvrajSinghBhadoria2/inferlast/releases"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
inferlast = "cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
# The src/ modules are intentionally top-level importables (import trustcheck,
|
|
39
|
+
# import gpucheck, ...) — identical to how the scripts and tests already use them.
|
|
40
|
+
package-dir = { "" = "src" }
|
|
41
|
+
py-modules = [
|
|
42
|
+
"auto_optimizer",
|
|
43
|
+
"batcher",
|
|
44
|
+
"cli",
|
|
45
|
+
"gpucheck",
|
|
46
|
+
"profiler",
|
|
47
|
+
"quantize",
|
|
48
|
+
"trustcheck",
|
|
49
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Auto-Optimizer orchestrator: one command -> optimized config + proof report.
|
|
2
|
+
|
|
3
|
+
Runs all four measurements on a given model and emits:
|
|
4
|
+
* profile (prefill per-category bottleneck + overhead-vs-weight fraction)
|
|
5
|
+
* decode (ms/token, tok/s)
|
|
6
|
+
* auto-quantization (fp32 vs int8 BEFORE/AFTER + honest verdict)
|
|
7
|
+
* auto-batching (latency vs throughput sweep + best batch for a goal)
|
|
8
|
+
|
|
9
|
+
Returns one dict with every result plus a human-readable proof report string.
|
|
10
|
+
Everything is measured on the caller's hardware (CPU here). The whole point:
|
|
11
|
+
refuse to recommend a change that doesn't provably help.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
from profiler import profile_forward, profile_decode
|
|
20
|
+
from quantize import auto_quantize
|
|
21
|
+
from batcher import bench_batch, best_batch
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _overhead_frac(pr) -> float:
|
|
25
|
+
"""fraction of wall time NOT accounted for by leaf matmul spans."""
|
|
26
|
+
leaf = sum(pr.cat_times.values())
|
|
27
|
+
return 1.0 - (leaf / max(pr.total_time, 1e-9))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_auto_optimizer(
|
|
31
|
+
model,
|
|
32
|
+
tok,
|
|
33
|
+
prompt: str = "The capital of France is the city of",
|
|
34
|
+
n_new: int = 12,
|
|
35
|
+
batches=(1, 2, 4, 8, 16),
|
|
36
|
+
batch_goal: str = "throughput",
|
|
37
|
+
quant_speedup_floor: float = 1.05,
|
|
38
|
+
quant_quality_floor: float = 0.65,
|
|
39
|
+
quant_top5_floor: float = 0.60,
|
|
40
|
+
) -> tuple[dict, str]:
|
|
41
|
+
out: dict = {
|
|
42
|
+
"prompt": prompt,
|
|
43
|
+
"hardware": "CPU (2019 MacBook Pro 16in i7-9750H, 16GB)",
|
|
44
|
+
"model_untouched": True,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# 1 & 2: bottleneck profile (prefill) + overhead fraction
|
|
48
|
+
t0 = time.time()
|
|
49
|
+
ids = tok([prompt], return_tensors="pt").input_ids
|
|
50
|
+
pr_pre = profile_forward(model, ids, run_repeats=2, warmup=1)
|
|
51
|
+
out["prefill"] = {
|
|
52
|
+
"wall_total_s": pr_pre.total_time,
|
|
53
|
+
"tokens": pr_pre.tokens,
|
|
54
|
+
"tok_per_s": pr_pre.tokens / max(pr_pre.total_time, 1e-9),
|
|
55
|
+
"leaf_time_s": sum(pr_pre.cat_times.values()),
|
|
56
|
+
"pct_of_leaf": {
|
|
57
|
+
k: round(v / max(sum(pr_pre.cat_times.values()), 1e-9) * 100, 1)
|
|
58
|
+
for k, v in pr_pre.cat_times.items()
|
|
59
|
+
},
|
|
60
|
+
"overhead_fraction": round(_overhead_frac(pr_pre), 3),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# decode
|
|
64
|
+
pr_dec = profile_decode(model, ids, n_new_tokens=n_new,
|
|
65
|
+
run_repeats=1, warmup=1)
|
|
66
|
+
out["decode"] = {
|
|
67
|
+
"ms_per_token": pr_dec.total_time / n_new * 1000.0,
|
|
68
|
+
"tok_per_s": n_new / max(pr_dec.total_time, 1e-9),
|
|
69
|
+
"overhead_fraction": round(_overhead_frac(pr_dec), 3),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# quantization
|
|
73
|
+
qv = auto_quantize(model, tok, prompt=prompt, n_new=n_new,
|
|
74
|
+
speedup_floor=quant_speedup_floor,
|
|
75
|
+
quality_floor=quant_quality_floor,
|
|
76
|
+
top5_floor=quant_top5_floor)
|
|
77
|
+
out["quantization"] = {
|
|
78
|
+
"fp32_ms_per_token": qv.before["ms_per_token"],
|
|
79
|
+
"int8_ms_per_token": qv.after["ms_per_token"],
|
|
80
|
+
"speedup_int8_over_fp32": round(qv.speedup, 3),
|
|
81
|
+
"speedup_worst_repeat": round(qv.speedup_min, 3),
|
|
82
|
+
"logit_cosine": round(qv.logit_cosine, 3),
|
|
83
|
+
"top5_overlap": round(qv.top5_overlap, 3),
|
|
84
|
+
"recommended_int8": qv.recommended,
|
|
85
|
+
"reason": qv.reason,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# batching
|
|
89
|
+
rows = bench_batch(model, tok, prompt=prompt, batches=batches,
|
|
90
|
+
n_steps=1, warmup=1)
|
|
91
|
+
best = best_batch(rows, goal=batch_goal)
|
|
92
|
+
out["batching"] = {
|
|
93
|
+
"goal": batch_goal,
|
|
94
|
+
"rows": [
|
|
95
|
+
{"batch": r.batch, "wall_ms": r.wall_ms, "tok_per_s": r.tok_per_s,
|
|
96
|
+
"ms_per_req": r.ms_per_req, "tok_per_s_per_req": r.eff_tok_per_s_per_req}
|
|
97
|
+
for r in rows
|
|
98
|
+
],
|
|
99
|
+
"best_batch": best.batch,
|
|
100
|
+
"best_tok_per_s": best.tok_per_s,
|
|
101
|
+
}
|
|
102
|
+
out["elapsed_s"] = round(time.time() - t0, 1)
|
|
103
|
+
|
|
104
|
+
report = _report(out)
|
|
105
|
+
return out, report
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _report(o: dict) -> str:
|
|
109
|
+
p = o["prefill"]
|
|
110
|
+
d = o["decode"]
|
|
111
|
+
q = o["quantization"]
|
|
112
|
+
b = o["batching"]
|
|
113
|
+
overhead_note = (
|
|
114
|
+
f"{p['overhead_fraction']*100:.0f}% of prefill wall time and "
|
|
115
|
+
f"{d['overhead_fraction']*100:.0f}% of decode wall time is framework "
|
|
116
|
+
"overhead on CPU. NOTE: this is CPU-specific; on a GPU the same model "
|
|
117
|
+
"(with fused kernels) is typically weight/compute-bound, so the picture "
|
|
118
|
+
"differs. Here it means: on CPU, tiny models are overhead-bound."
|
|
119
|
+
)
|
|
120
|
+
lines = [
|
|
121
|
+
"# Auto-Optimizer proof report",
|
|
122
|
+
f"hardware: {o['hardware']} prompt: {o['prompt']}",
|
|
123
|
+
"",
|
|
124
|
+
"## 1. Bottleneck (from profile)",
|
|
125
|
+
f"prefill throughput ~{p['tok_per_s']:.1f} tok/s; wall {p['wall_total_s']:.2f}s "
|
|
126
|
+
f"for {p['tokens']} tokens.",
|
|
127
|
+
"per-category (% of measured leaf work): " + ", ".join(
|
|
128
|
+
f"{k} {v}%" for k, v in sorted(p['pct_of_leaf'].items(),
|
|
129
|
+
key=lambda x: -x[1])
|
|
130
|
+
),
|
|
131
|
+
f"overhead-vs-weight: {overhead_note}",
|
|
132
|
+
"",
|
|
133
|
+
"## 2. Decode",
|
|
134
|
+
f"{d['ms_per_token']:.0f} ms/token (~{d['tok_per_s']:.2f} tok/s). "
|
|
135
|
+
"Single-token serial, latency-sensitive.",
|
|
136
|
+
"",
|
|
137
|
+
"## 3. Quantization (fp32 vs int8, measured BEFORE/AFTER, averaged)",
|
|
138
|
+
f"fp32 {q['fp32_ms_per_token']:.0f} ms/tok -> int8 {q['int8_ms_per_token']:.0f} ms/tok "
|
|
139
|
+
f"= {q['speedup_int8_over_fp32']:.2f}x (worst repeat {q['speedup_worst_repeat']:.2f}x), "
|
|
140
|
+
f"quality logit-cos {q['logit_cosine']:.2f}, top-5 {q['top5_overlap']:.2f}.",
|
|
141
|
+
f"suggested: {'USE int8' if q['recommended_int8'] else 'KEEP fp32'} — {q['reason']}",
|
|
142
|
+
"",
|
|
143
|
+
"## 4. Batching (latency vs throughput)",
|
|
144
|
+
"B | wall(ms) | tok/s | ms/req | tok/s/req",
|
|
145
|
+
]
|
|
146
|
+
for r in b["rows"]:
|
|
147
|
+
lines.append(
|
|
148
|
+
f"{r['batch']} | {r['wall_ms']:.0f} | {r['tok_per_s']:.1f} | "
|
|
149
|
+
f"{r['ms_per_req']:.0f} | {r['tok_per_s_per_req']:.2f}"
|
|
150
|
+
)
|
|
151
|
+
lines += [
|
|
152
|
+
f"best batch for goal='{b['goal']}': B={b['best_batch']} "
|
|
153
|
+
f"-> {b['best_tok_per_s']:.1f} tok/s total.",
|
|
154
|
+
"",
|
|
155
|
+
"## Honest takeaway",
|
|
156
|
+
"The model was never permanently modified; any optimization is advised "
|
|
157
|
+
"ONLY if it provably helps.",
|
|
158
|
+
]
|
|
159
|
+
return "\n".join(lines)
|