ml-systems-lab 0.1.1__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.
- ml_systems_lab-0.1.1/LICENSE +21 -0
- ml_systems_lab-0.1.1/PKG-INFO +258 -0
- ml_systems_lab-0.1.1/README.md +221 -0
- ml_systems_lab-0.1.1/pyproject.toml +56 -0
- ml_systems_lab-0.1.1/setup.cfg +4 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/PKG-INFO +258 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/SOURCES.txt +45 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/dependency_links.txt +1 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/entry_points.txt +2 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/requires.txt +9 -0
- ml_systems_lab-0.1.1/src/ml_systems_lab.egg-info/top_level.txt +1 -0
- ml_systems_lab-0.1.1/src/mlsyslab/__init__.py +19 -0
- ml_systems_lab-0.1.1/src/mlsyslab/__main__.py +4 -0
- ml_systems_lab-0.1.1/src/mlsyslab/agent.py +388 -0
- ml_systems_lab-0.1.1/src/mlsyslab/analysis/__init__.py +66 -0
- ml_systems_lab-0.1.1/src/mlsyslab/analysis/dataset.py +279 -0
- ml_systems_lab-0.1.1/src/mlsyslab/analysis/plots.py +392 -0
- ml_systems_lab-0.1.1/src/mlsyslab/analysis/tables.py +320 -0
- ml_systems_lab-0.1.1/src/mlsyslab/backends/__init__.py +44 -0
- ml_systems_lab-0.1.1/src/mlsyslab/backends/base.py +105 -0
- ml_systems_lab-0.1.1/src/mlsyslab/backends/llamacpp.py +396 -0
- ml_systems_lab-0.1.1/src/mlsyslab/backends/onnxrt.py +208 -0
- ml_systems_lab-0.1.1/src/mlsyslab/bench_onnx.py +177 -0
- ml_systems_lab-0.1.1/src/mlsyslab/cli.py +396 -0
- ml_systems_lab-0.1.1/src/mlsyslab/config.py +259 -0
- ml_systems_lab-0.1.1/src/mlsyslab/devices/__init__.py +37 -0
- ml_systems_lab-0.1.1/src/mlsyslab/devices/base.py +139 -0
- ml_systems_lab-0.1.1/src/mlsyslab/devices/local.py +83 -0
- ml_systems_lab-0.1.1/src/mlsyslab/devices/ssh.py +179 -0
- ml_systems_lab-0.1.1/src/mlsyslab/membw.py +182 -0
- ml_systems_lab-0.1.1/src/mlsyslab/runner.py +252 -0
- ml_systems_lab-0.1.1/src/mlsyslab/schema.py +288 -0
- ml_systems_lab-0.1.1/src/mlsyslab/sysinfo.py +272 -0
- ml_systems_lab-0.1.1/src/mlsyslab/telemetry/__init__.py +183 -0
- ml_systems_lab-0.1.1/src/mlsyslab/telemetry/cpu.py +126 -0
- ml_systems_lab-0.1.1/src/mlsyslab/telemetry/dvfs.py +91 -0
- ml_systems_lab-0.1.1/src/mlsyslab/telemetry/power.py +162 -0
- ml_systems_lab-0.1.1/src/mlsyslab/telemetry/thermal.py +98 -0
- ml_systems_lab-0.1.1/tests/test_agent_and_devices.py +88 -0
- ml_systems_lab-0.1.1/tests/test_analysis.py +113 -0
- ml_systems_lab-0.1.1/tests/test_config.py +94 -0
- ml_systems_lab-0.1.1/tests/test_llamacpp_backend.py +124 -0
- ml_systems_lab-0.1.1/tests/test_membw_and_cli.py +74 -0
- ml_systems_lab-0.1.1/tests/test_onnx_backend.py +88 -0
- ml_systems_lab-0.1.1/tests/test_runner.py +117 -0
- ml_systems_lab-0.1.1/tests/test_schema.py +68 -0
- ml_systems_lab-0.1.1/tests/test_telemetry.py +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manu Nicholas Jacob
|
|
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,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ml-systems-lab
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A reproducible benchmarking framework for ML inference across laptops, single-board computers and servers.
|
|
5
|
+
Author: Manu Nicholas Jacob
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/manunicholasjacob/ml-systems-lab
|
|
8
|
+
Project-URL: Issues, https://github.com/manunicholasjacob/ml-systems-lab/issues
|
|
9
|
+
Keywords: benchmark,inference,llama.cpp,onnxruntime,edge-ai,raspberry-pi,energy,latency,ttft,quantization,reproducibility
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering
|
|
25
|
+
Classifier: Topic :: System :: Benchmark
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.20
|
|
30
|
+
Requires-Dist: matplotlib>=3.4
|
|
31
|
+
Requires-Dist: PyYAML>=5.4
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
34
|
+
Provides-Extra: onnx
|
|
35
|
+
Requires-Dist: onnxruntime>=1.15; extra == "onnx"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# ML Systems Lab
|
|
39
|
+
|
|
40
|
+
[](https://github.com/manunicholasjacob/ml-systems-lab/actions/workflows/ci.yml)
|
|
41
|
+
[](https://doi.org/10.5281/zenodo.21867055)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](pyproject.toml)
|
|
44
|
+
|
|
45
|
+
A reproducible benchmarking framework for ML inference across heterogeneous hardware:
|
|
46
|
+
laptops, single-board computers, and servers, from one config file and one command.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
mlsys run configs/example-smoke.yaml
|
|
50
|
+
mlsys report runs/smoke-test --full
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every run produces a self-describing JSON record carrying the hardware, OS, kernel,
|
|
54
|
+
compiler, backend version, model, quantization, the measured metrics, and the physical
|
|
55
|
+
state of the machine while they were measured (power, temperature, clocks, throttle
|
|
56
|
+
flags, CPU utilization). The analysis layer turns a directory of records into
|
|
57
|
+
publication-quality tables (text, Markdown, LaTeX/booktabs) and figures.
|
|
58
|
+
|
|
59
|
+
Built and used for a real research program: the records under `results/paper12/` are the
|
|
60
|
+
measurements behind an IEEE Transactions on Computers submission, and the framework
|
|
61
|
+
reproduces that paper's published roofline fits exactly (Pi 5: 10.7 GB/s effective,
|
|
62
|
+
R^2 = 0.980; i7-12700H: 35.7 GB/s, R^2 = 0.980). Two campaigns run natively by this
|
|
63
|
+
framework then re-measured the same quantities independently and agreed within 1.6%.
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
*Every point is a different model or quantization; every line is one device's effective
|
|
68
|
+
memory bandwidth. Two independent Pi 5 campaigns (weeks apart, different harnesses) and
|
|
69
|
+
two laptop campaigns land on top of each other: decode throughput is model bytes divided
|
|
70
|
+
by one number per device.*
|
|
71
|
+
|
|
72
|
+
## What it measures
|
|
73
|
+
|
|
74
|
+
| Metric | How |
|
|
75
|
+
|---|---|
|
|
76
|
+
| TTFT (time to first token) | streaming request against `llama-server`, first-chunk timing |
|
|
77
|
+
| Prefill / decode throughput | `llama-bench`, parsed from its JSON output |
|
|
78
|
+
| End-to-end request latency | same streaming path, submit to last token |
|
|
79
|
+
| Single-inference latency (mean, p50/p95/p99) | ONNX Runtime, timed in-process on the device |
|
|
80
|
+
| Memory | peak RSS, plus free-memory and swap state around every run |
|
|
81
|
+
| CPU utilization | `/proc/stat` deltas (Linux), `GetSystemTimes` (Windows), per-core where available |
|
|
82
|
+
| Temperature and throttling | sysfs thermal zones, Pi throttle bitmask, in the record not a side file |
|
|
83
|
+
| Power and energy | Raspberry Pi PMIC per-rail (core vs DRAM split), Intel RAPL where present |
|
|
84
|
+
| Derived | decode bandwidth, roofline utilization %, energy per token |
|
|
85
|
+
|
|
86
|
+
Anything a platform cannot measure is reported as absent, never as zero.
|
|
87
|
+
|
|
88
|
+
## Devices exercised
|
|
89
|
+
|
|
90
|
+
| Device | Route | Notes |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| Raspberry Pi 5 (2 GB, Cortex-A76) | SSH agent | PMIC per-rail power, throttle bits, DVFS control |
|
|
93
|
+
| i7-12700H laptop (Windows) | local agent | 20-thread sweeps, up to 7B models |
|
|
94
|
+
| RTX 3050 (same laptop) | ONNX Runtime DirectML | modeled as its own device; at batch 1 the GPU loses to the CPU (12.3 ms vs 3.0 ms, dispatch overhead), at batch 64 it wins 29x (9,885 vs 338 inf/s), and both facts come out of the same config file |
|
|
95
|
+
|
|
96
|
+
A new machine is a config block, not code: `host`, an SSH key, and the paths to its
|
|
97
|
+
models. A new accelerator is a device entry pointing at an interpreter whose ONNX
|
|
98
|
+
Runtime carries the right execution provider.
|
|
99
|
+
|
|
100
|
+
## Design
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
config.yaml ──> RunSpecs ──> Device ──> agent (on the device) ──> RunRecord ──> analysis
|
|
104
|
+
│
|
|
105
|
+
├── LocalDevice (this machine, agent as subprocess)
|
|
106
|
+
└── SSHDevice (agent pushed over SSH, runs remotely)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
* **The agent runs on the device under test**, so the benchmark and the telemetry
|
|
110
|
+
sampler are colocated; nothing crosses the network inside a measurement window.
|
|
111
|
+
It is pure standard library and is copied, not installed.
|
|
112
|
+
* **Backends** (`llamacpp`, `onnxruntime`) turn one spec into one task and parse one
|
|
113
|
+
result. The agent returns raw output; parsing happens on the host, so a parser bug is
|
|
114
|
+
fixed by re-parsing stored output rather than re-running a campaign.
|
|
115
|
+
* **Run ids are deterministic** over the spec, so an interrupted campaign resumes by
|
|
116
|
+
skipping what is already on disk (`--no-resume` to override). Failures are records
|
|
117
|
+
too, with the error and the raw output preserved.
|
|
118
|
+
* **Capability model**: each device reports what it can measure (`mlsys probe`), and
|
|
119
|
+
sweeps degrade gracefully rather than failing on a machine without, say, a PMIC.
|
|
120
|
+
|
|
121
|
+
## Install
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
pip install -e ".[dev]" # numpy, matplotlib, PyYAML; pytest for the test suite
|
|
125
|
+
pip install -e ".[onnx]" # optional: onnxruntime for the ORT backend on this host
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The measurement core (agent, devices, backends, config, schema) is standard library
|
|
129
|
+
only, verified by a dedicated no-dependencies CI job. Devices under test need Python 3.9+
|
|
130
|
+
and their inference backend (a llama.cpp build and/or onnxruntime), nothing else.
|
|
131
|
+
|
|
132
|
+
## Quick start
|
|
133
|
+
|
|
134
|
+
A ready-to-edit template lives at `configs/example-smoke.yaml`. Copy it, fill in
|
|
135
|
+
your paths, and run:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
mlsys doctor --config configs/example-smoke.yaml # check everything is wired up
|
|
139
|
+
mlsys run configs/example-smoke.yaml # run the experiment
|
|
140
|
+
mlsys report runs/smoke-test # see the results
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For a multi-device setup, describe your machines and models once:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
# configs/lab.yaml
|
|
147
|
+
experiment: my-sweep
|
|
148
|
+
devices:
|
|
149
|
+
laptop:
|
|
150
|
+
kind: local
|
|
151
|
+
dram_peak_GBs: 53.9 # your measured read ceiling; drives utilization %
|
|
152
|
+
llamacpp: { bin_dir: C:/llmpc/bin }
|
|
153
|
+
pi5:
|
|
154
|
+
host: 100.98.217.64 # any SSH-reachable box; Tailscale IPs work fine
|
|
155
|
+
user: manu
|
|
156
|
+
identity_file: ~/.ssh/raspberry_pi_key
|
|
157
|
+
dram_peak_GBs: 13.98
|
|
158
|
+
llamacpp: { bin_dir: ~/llm/llama.cpp/build/bin }
|
|
159
|
+
models:
|
|
160
|
+
qwen0.5b-q4km:
|
|
161
|
+
quantization: Q4_K_M
|
|
162
|
+
paths: { laptop: C:/llmpc/models/qwen0.5b-q4km.gguf, pi5: ~/llm/models/qwen0.5b-q4km.gguf }
|
|
163
|
+
defaults: { backend: llamacpp, repetitions: 3 }
|
|
164
|
+
matrix:
|
|
165
|
+
- devices: [laptop, pi5]
|
|
166
|
+
models: [qwen0.5b-q4km]
|
|
167
|
+
modes: [throughput]
|
|
168
|
+
threads: [1, 2, 4]
|
|
169
|
+
prompt_tokens: [128]
|
|
170
|
+
output_tokens: [64]
|
|
171
|
+
- devices: [laptop, pi5]
|
|
172
|
+
models: [qwen0.5b-q4km]
|
|
173
|
+
modes: [latency] # TTFT via llama-server streaming
|
|
174
|
+
threads: [4]
|
|
175
|
+
prompt_tokens: [128, 512]
|
|
176
|
+
output_tokens: [64]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
2. Check the machines are reachable and see what they can measure:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
mlsys probe --config configs/lab.yaml
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
3. Preview, then run:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
mlsys run configs/lab.yaml --dry-run
|
|
189
|
+
mlsys run configs/lab.yaml
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
4. Tables and figures:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
mlsys report runs/my-sweep # tables to the terminal
|
|
196
|
+
mlsys report runs/my-sweep --format latex # booktabs, ready to paste
|
|
197
|
+
mlsys report runs/my-sweep --full # REPORT.md + PNG/PDF figures
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
5. More tools:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
mlsys doctor --config configs/lab.yaml
|
|
204
|
+
# checks Python, dependencies, llama.cpp binaries, model paths,
|
|
205
|
+
# and device reachability; run this first if something is not working
|
|
206
|
+
|
|
207
|
+
mlsys membw --device pi5 --config configs/lab.yaml
|
|
208
|
+
# measures the device's achievable DRAM read ceiling and prints the
|
|
209
|
+
# dram_peak_GBs line to put in the config, with a stability check that
|
|
210
|
+
# flags a machine that was not idle
|
|
211
|
+
|
|
212
|
+
mlsys compare runs/before runs/after --metric decode_tps
|
|
213
|
+
# same workloads in two result sets, side by side with the ratio
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Measurement methodology
|
|
217
|
+
|
|
218
|
+
The rules encoded in this framework, and why, are documented in
|
|
219
|
+
[docs/METHOD.md](docs/METHOD.md). The short version:
|
|
220
|
+
|
|
221
|
+
* benchmark on an idle machine; the framework flags high run-to-run spread,
|
|
222
|
+
* never sample power in the run you take throughput from (the sampler perturbs decode),
|
|
223
|
+
* temperature and throttle state live inside the record so a hot run cannot be
|
|
224
|
+
silently compared with a cool one,
|
|
225
|
+
* `llama-bench` for throughput, `llama-server` streaming for TTFT, and `llama-cli`
|
|
226
|
+
never (it hangs when scripted),
|
|
227
|
+
* every failure is written to disk with its raw output.
|
|
228
|
+
|
|
229
|
+
## Repository layout
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
src/mlsyslab/
|
|
233
|
+
schema.py the RunRecord and its loader
|
|
234
|
+
sysinfo.py automatic hardware/OS description
|
|
235
|
+
config.py YAML/JSON sweep expansion
|
|
236
|
+
runner.py resumable execution, atomic writes
|
|
237
|
+
agent.py the on-device payload (stdlib only)
|
|
238
|
+
bench_onnx.py the on-device ONNX Runtime benchmark
|
|
239
|
+
devices/ local and SSH devices, capability model
|
|
240
|
+
backends/ llamacpp (bench + server) and onnxruntime
|
|
241
|
+
telemetry/ power (PMIC/RAPL), thermal, CPU, DVFS
|
|
242
|
+
analysis/ dataset, tables, figures, REPORT.md
|
|
243
|
+
configs/ experiment definitions
|
|
244
|
+
tools/ result backfill converters
|
|
245
|
+
results/paper12/ real measurements from the IEEE TC submission
|
|
246
|
+
tests/ 70 hardware-free tests (recorded fixtures)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Citing
|
|
250
|
+
|
|
251
|
+
Archived on Zenodo; the concept DOI [10.5281/zenodo.21867055](https://doi.org/10.5281/zenodo.21867055)
|
|
252
|
+
always resolves to the latest version. `CITATION.cff` carries the full citation metadata,
|
|
253
|
+
and GitHub's "Cite this repository" button renders it.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT. llama.cpp and ONNX Runtime are invoked as external tools and are licensed by their
|
|
258
|
+
respective projects.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ML Systems Lab
|
|
2
|
+
|
|
3
|
+
[](https://github.com/manunicholasjacob/ml-systems-lab/actions/workflows/ci.yml)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.21867055)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](pyproject.toml)
|
|
7
|
+
|
|
8
|
+
A reproducible benchmarking framework for ML inference across heterogeneous hardware:
|
|
9
|
+
laptops, single-board computers, and servers, from one config file and one command.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
mlsys run configs/example-smoke.yaml
|
|
13
|
+
mlsys report runs/smoke-test --full
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Every run produces a self-describing JSON record carrying the hardware, OS, kernel,
|
|
17
|
+
compiler, backend version, model, quantization, the measured metrics, and the physical
|
|
18
|
+
state of the machine while they were measured (power, temperature, clocks, throttle
|
|
19
|
+
flags, CPU utilization). The analysis layer turns a directory of records into
|
|
20
|
+
publication-quality tables (text, Markdown, LaTeX/booktabs) and figures.
|
|
21
|
+
|
|
22
|
+
Built and used for a real research program: the records under `results/paper12/` are the
|
|
23
|
+
measurements behind an IEEE Transactions on Computers submission, and the framework
|
|
24
|
+
reproduces that paper's published roofline fits exactly (Pi 5: 10.7 GB/s effective,
|
|
25
|
+
R^2 = 0.980; i7-12700H: 35.7 GB/s, R^2 = 0.980). Two campaigns run natively by this
|
|
26
|
+
framework then re-measured the same quantities independently and agreed within 1.6%.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
*Every point is a different model or quantization; every line is one device's effective
|
|
31
|
+
memory bandwidth. Two independent Pi 5 campaigns (weeks apart, different harnesses) and
|
|
32
|
+
two laptop campaigns land on top of each other: decode throughput is model bytes divided
|
|
33
|
+
by one number per device.*
|
|
34
|
+
|
|
35
|
+
## What it measures
|
|
36
|
+
|
|
37
|
+
| Metric | How |
|
|
38
|
+
|---|---|
|
|
39
|
+
| TTFT (time to first token) | streaming request against `llama-server`, first-chunk timing |
|
|
40
|
+
| Prefill / decode throughput | `llama-bench`, parsed from its JSON output |
|
|
41
|
+
| End-to-end request latency | same streaming path, submit to last token |
|
|
42
|
+
| Single-inference latency (mean, p50/p95/p99) | ONNX Runtime, timed in-process on the device |
|
|
43
|
+
| Memory | peak RSS, plus free-memory and swap state around every run |
|
|
44
|
+
| CPU utilization | `/proc/stat` deltas (Linux), `GetSystemTimes` (Windows), per-core where available |
|
|
45
|
+
| Temperature and throttling | sysfs thermal zones, Pi throttle bitmask, in the record not a side file |
|
|
46
|
+
| Power and energy | Raspberry Pi PMIC per-rail (core vs DRAM split), Intel RAPL where present |
|
|
47
|
+
| Derived | decode bandwidth, roofline utilization %, energy per token |
|
|
48
|
+
|
|
49
|
+
Anything a platform cannot measure is reported as absent, never as zero.
|
|
50
|
+
|
|
51
|
+
## Devices exercised
|
|
52
|
+
|
|
53
|
+
| Device | Route | Notes |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Raspberry Pi 5 (2 GB, Cortex-A76) | SSH agent | PMIC per-rail power, throttle bits, DVFS control |
|
|
56
|
+
| i7-12700H laptop (Windows) | local agent | 20-thread sweeps, up to 7B models |
|
|
57
|
+
| RTX 3050 (same laptop) | ONNX Runtime DirectML | modeled as its own device; at batch 1 the GPU loses to the CPU (12.3 ms vs 3.0 ms, dispatch overhead), at batch 64 it wins 29x (9,885 vs 338 inf/s), and both facts come out of the same config file |
|
|
58
|
+
|
|
59
|
+
A new machine is a config block, not code: `host`, an SSH key, and the paths to its
|
|
60
|
+
models. A new accelerator is a device entry pointing at an interpreter whose ONNX
|
|
61
|
+
Runtime carries the right execution provider.
|
|
62
|
+
|
|
63
|
+
## Design
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
config.yaml ──> RunSpecs ──> Device ──> agent (on the device) ──> RunRecord ──> analysis
|
|
67
|
+
│
|
|
68
|
+
├── LocalDevice (this machine, agent as subprocess)
|
|
69
|
+
└── SSHDevice (agent pushed over SSH, runs remotely)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
* **The agent runs on the device under test**, so the benchmark and the telemetry
|
|
73
|
+
sampler are colocated; nothing crosses the network inside a measurement window.
|
|
74
|
+
It is pure standard library and is copied, not installed.
|
|
75
|
+
* **Backends** (`llamacpp`, `onnxruntime`) turn one spec into one task and parse one
|
|
76
|
+
result. The agent returns raw output; parsing happens on the host, so a parser bug is
|
|
77
|
+
fixed by re-parsing stored output rather than re-running a campaign.
|
|
78
|
+
* **Run ids are deterministic** over the spec, so an interrupted campaign resumes by
|
|
79
|
+
skipping what is already on disk (`--no-resume` to override). Failures are records
|
|
80
|
+
too, with the error and the raw output preserved.
|
|
81
|
+
* **Capability model**: each device reports what it can measure (`mlsys probe`), and
|
|
82
|
+
sweeps degrade gracefully rather than failing on a machine without, say, a PMIC.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
pip install -e ".[dev]" # numpy, matplotlib, PyYAML; pytest for the test suite
|
|
88
|
+
pip install -e ".[onnx]" # optional: onnxruntime for the ORT backend on this host
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The measurement core (agent, devices, backends, config, schema) is standard library
|
|
92
|
+
only, verified by a dedicated no-dependencies CI job. Devices under test need Python 3.9+
|
|
93
|
+
and their inference backend (a llama.cpp build and/or onnxruntime), nothing else.
|
|
94
|
+
|
|
95
|
+
## Quick start
|
|
96
|
+
|
|
97
|
+
A ready-to-edit template lives at `configs/example-smoke.yaml`. Copy it, fill in
|
|
98
|
+
your paths, and run:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
mlsys doctor --config configs/example-smoke.yaml # check everything is wired up
|
|
102
|
+
mlsys run configs/example-smoke.yaml # run the experiment
|
|
103
|
+
mlsys report runs/smoke-test # see the results
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For a multi-device setup, describe your machines and models once:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# configs/lab.yaml
|
|
110
|
+
experiment: my-sweep
|
|
111
|
+
devices:
|
|
112
|
+
laptop:
|
|
113
|
+
kind: local
|
|
114
|
+
dram_peak_GBs: 53.9 # your measured read ceiling; drives utilization %
|
|
115
|
+
llamacpp: { bin_dir: C:/llmpc/bin }
|
|
116
|
+
pi5:
|
|
117
|
+
host: 100.98.217.64 # any SSH-reachable box; Tailscale IPs work fine
|
|
118
|
+
user: manu
|
|
119
|
+
identity_file: ~/.ssh/raspberry_pi_key
|
|
120
|
+
dram_peak_GBs: 13.98
|
|
121
|
+
llamacpp: { bin_dir: ~/llm/llama.cpp/build/bin }
|
|
122
|
+
models:
|
|
123
|
+
qwen0.5b-q4km:
|
|
124
|
+
quantization: Q4_K_M
|
|
125
|
+
paths: { laptop: C:/llmpc/models/qwen0.5b-q4km.gguf, pi5: ~/llm/models/qwen0.5b-q4km.gguf }
|
|
126
|
+
defaults: { backend: llamacpp, repetitions: 3 }
|
|
127
|
+
matrix:
|
|
128
|
+
- devices: [laptop, pi5]
|
|
129
|
+
models: [qwen0.5b-q4km]
|
|
130
|
+
modes: [throughput]
|
|
131
|
+
threads: [1, 2, 4]
|
|
132
|
+
prompt_tokens: [128]
|
|
133
|
+
output_tokens: [64]
|
|
134
|
+
- devices: [laptop, pi5]
|
|
135
|
+
models: [qwen0.5b-q4km]
|
|
136
|
+
modes: [latency] # TTFT via llama-server streaming
|
|
137
|
+
threads: [4]
|
|
138
|
+
prompt_tokens: [128, 512]
|
|
139
|
+
output_tokens: [64]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
2. Check the machines are reachable and see what they can measure:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
mlsys probe --config configs/lab.yaml
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
3. Preview, then run:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
mlsys run configs/lab.yaml --dry-run
|
|
152
|
+
mlsys run configs/lab.yaml
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
4. Tables and figures:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
mlsys report runs/my-sweep # tables to the terminal
|
|
159
|
+
mlsys report runs/my-sweep --format latex # booktabs, ready to paste
|
|
160
|
+
mlsys report runs/my-sweep --full # REPORT.md + PNG/PDF figures
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
5. More tools:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
mlsys doctor --config configs/lab.yaml
|
|
167
|
+
# checks Python, dependencies, llama.cpp binaries, model paths,
|
|
168
|
+
# and device reachability; run this first if something is not working
|
|
169
|
+
|
|
170
|
+
mlsys membw --device pi5 --config configs/lab.yaml
|
|
171
|
+
# measures the device's achievable DRAM read ceiling and prints the
|
|
172
|
+
# dram_peak_GBs line to put in the config, with a stability check that
|
|
173
|
+
# flags a machine that was not idle
|
|
174
|
+
|
|
175
|
+
mlsys compare runs/before runs/after --metric decode_tps
|
|
176
|
+
# same workloads in two result sets, side by side with the ratio
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Measurement methodology
|
|
180
|
+
|
|
181
|
+
The rules encoded in this framework, and why, are documented in
|
|
182
|
+
[docs/METHOD.md](docs/METHOD.md). The short version:
|
|
183
|
+
|
|
184
|
+
* benchmark on an idle machine; the framework flags high run-to-run spread,
|
|
185
|
+
* never sample power in the run you take throughput from (the sampler perturbs decode),
|
|
186
|
+
* temperature and throttle state live inside the record so a hot run cannot be
|
|
187
|
+
silently compared with a cool one,
|
|
188
|
+
* `llama-bench` for throughput, `llama-server` streaming for TTFT, and `llama-cli`
|
|
189
|
+
never (it hangs when scripted),
|
|
190
|
+
* every failure is written to disk with its raw output.
|
|
191
|
+
|
|
192
|
+
## Repository layout
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
src/mlsyslab/
|
|
196
|
+
schema.py the RunRecord and its loader
|
|
197
|
+
sysinfo.py automatic hardware/OS description
|
|
198
|
+
config.py YAML/JSON sweep expansion
|
|
199
|
+
runner.py resumable execution, atomic writes
|
|
200
|
+
agent.py the on-device payload (stdlib only)
|
|
201
|
+
bench_onnx.py the on-device ONNX Runtime benchmark
|
|
202
|
+
devices/ local and SSH devices, capability model
|
|
203
|
+
backends/ llamacpp (bench + server) and onnxruntime
|
|
204
|
+
telemetry/ power (PMIC/RAPL), thermal, CPU, DVFS
|
|
205
|
+
analysis/ dataset, tables, figures, REPORT.md
|
|
206
|
+
configs/ experiment definitions
|
|
207
|
+
tools/ result backfill converters
|
|
208
|
+
results/paper12/ real measurements from the IEEE TC submission
|
|
209
|
+
tests/ 70 hardware-free tests (recorded fixtures)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Citing
|
|
213
|
+
|
|
214
|
+
Archived on Zenodo; the concept DOI [10.5281/zenodo.21867055](https://doi.org/10.5281/zenodo.21867055)
|
|
215
|
+
always resolves to the latest version. `CITATION.cff` carries the full citation metadata,
|
|
216
|
+
and GitHub's "Cite this repository" button renders it.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT. llama.cpp and ONNX Runtime are invoked as external tools and are licensed by their
|
|
221
|
+
respective projects.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ml-systems-lab"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "A reproducible benchmarking framework for ML inference across laptops, single-board computers and servers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Manu Nicholas Jacob" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"benchmark", "inference", "llama.cpp", "onnxruntime", "edge-ai",
|
|
15
|
+
"raspberry-pi", "energy", "latency", "ttft", "quantization", "reproducibility",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Operating System :: POSIX :: Linux",
|
|
24
|
+
"Operating System :: MacOS",
|
|
25
|
+
"Operating System :: Microsoft :: Windows",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Topic :: Scientific/Engineering",
|
|
33
|
+
"Topic :: System :: Benchmark",
|
|
34
|
+
]
|
|
35
|
+
# The measurement core is pure standard library on purpose: it has to run unmodified on
|
|
36
|
+
# a 2 GB Raspberry Pi over SSH, where installing a scientific stack is neither cheap nor
|
|
37
|
+
# always possible. numpy and matplotlib are ordinary dependencies so that analysis and
|
|
38
|
+
# plotting work on first install, and a CI job proves the core still imports without them.
|
|
39
|
+
dependencies = ["numpy>=1.20", "matplotlib>=3.4", "PyYAML>=5.4"]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest>=7"]
|
|
43
|
+
onnx = ["onnxruntime>=1.15"]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/manunicholasjacob/ml-systems-lab"
|
|
47
|
+
Issues = "https://github.com/manunicholasjacob/ml-systems-lab/issues"
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
mlsys = "mlsyslab.cli:main"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|