carmen-kernels 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.
- carmen_kernels-0.1.0/LICENSE +21 -0
- carmen_kernels-0.1.0/PKG-INFO +144 -0
- carmen_kernels-0.1.0/README.md +113 -0
- carmen_kernels-0.1.0/carmen/__init__.py +7 -0
- carmen_kernels-0.1.0/carmen/backends/__init__.py +11 -0
- carmen_kernels-0.1.0/carmen/backends/base.py +102 -0
- carmen_kernels-0.1.0/carmen/backends/metal.py +129 -0
- carmen_kernels-0.1.0/carmen/bench.py +102 -0
- carmen_kernels-0.1.0/carmen/broken.py +213 -0
- carmen_kernels-0.1.0/carmen/carmy.py +254 -0
- carmen_kernels-0.1.0/carmen/champions/__init__.py +49 -0
- carmen_kernels-0.1.0/carmen/champions/published.json +11 -0
- carmen_kernels-0.1.0/carmen/champions/qwen2.5-0.5b-4bit/mlp_down.json +15 -0
- carmen_kernels-0.1.0/carmen/champions/qwen2.5-0.5b-4bit/mlp_up.json +15 -0
- carmen_kernels-0.1.0/carmen/champions/qwen2.5-0.5b-4bit/result.json +14 -0
- carmen_kernels-0.1.0/carmen/cli.py +607 -0
- carmen_kernels-0.1.0/carmen/dotenv.py +36 -0
- carmen_kernels-0.1.0/carmen/e2e.py +665 -0
- carmen_kernels-0.1.0/carmen/events.py +30 -0
- carmen_kernels-0.1.0/carmen/golden/add_rmsnorm.metal +30 -0
- carmen_kernels-0.1.0/carmen/golden/attention.header.metal +8 -0
- carmen_kernels-0.1.0/carmen/golden/attention.metal +58 -0
- carmen_kernels-0.1.0/carmen/golden/layernorm.metal +45 -0
- carmen_kernels-0.1.0/carmen/golden/masked_softmax.metal +46 -0
- carmen_kernels-0.1.0/carmen/golden/matmul.metal +19 -0
- carmen_kernels-0.1.0/carmen/golden/mlp_down.metal +30 -0
- carmen_kernels-0.1.0/carmen/golden/mlp_up.metal +54 -0
- carmen_kernels-0.1.0/carmen/golden/residual_rmsnorm.metal +33 -0
- carmen_kernels-0.1.0/carmen/golden/rmsnorm.metal +29 -0
- carmen_kernels-0.1.0/carmen/golden/softmax.metal +44 -0
- carmen_kernels-0.1.0/carmen/judge/__init__.py +136 -0
- carmen_kernels-0.1.0/carmen/judge/check.py +136 -0
- carmen_kernels-0.1.0/carmen/judge/stats.py +38 -0
- carmen_kernels-0.1.0/carmen/judge/worker.py +294 -0
- carmen_kernels-0.1.0/carmen/loop.py +289 -0
- carmen_kernels-0.1.0/carmen/memory.py +138 -0
- carmen_kernels-0.1.0/carmen/ops/__init__.py +28 -0
- carmen_kernels-0.1.0/carmen/ops/add_rmsnorm.py +121 -0
- carmen_kernels-0.1.0/carmen/ops/attention.py +174 -0
- carmen_kernels-0.1.0/carmen/ops/base.py +208 -0
- carmen_kernels-0.1.0/carmen/ops/layernorm.py +125 -0
- carmen_kernels-0.1.0/carmen/ops/masked_softmax.py +133 -0
- carmen_kernels-0.1.0/carmen/ops/matmul.py +132 -0
- carmen_kernels-0.1.0/carmen/ops/mlp.py +322 -0
- carmen_kernels-0.1.0/carmen/ops/residual_rmsnorm.py +108 -0
- carmen_kernels-0.1.0/carmen/ops/rmsnorm.py +114 -0
- carmen_kernels-0.1.0/carmen/ops/softmax.py +140 -0
- carmen_kernels-0.1.0/carmen/profile.py +256 -0
- carmen_kernels-0.1.0/carmen/target.py +73 -0
- carmen_kernels-0.1.0/carmen/tui.py +1080 -0
- carmen_kernels-0.1.0/carmen/ui.py +64 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/PKG-INFO +144 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/SOURCES.txt +63 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/dependency_links.txt +1 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/entry_points.txt +2 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/requires.txt +13 -0
- carmen_kernels-0.1.0/carmen_kernels.egg-info/top_level.txt +1 -0
- carmen_kernels-0.1.0/pyproject.toml +45 -0
- carmen_kernels-0.1.0/setup.cfg +4 -0
- carmen_kernels-0.1.0/tests/test_e2e.py +151 -0
- carmen_kernels-0.1.0/tests/test_judge.py +237 -0
- carmen_kernels-0.1.0/tests/test_loop.py +159 -0
- carmen_kernels-0.1.0/tests/test_profile.py +37 -0
- carmen_kernels-0.1.0/tests/test_target.py +117 -0
- carmen_kernels-0.1.0/tests/test_tui.py +55 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Utsav Khairnar
|
|
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,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: carmen-kernels
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An AI writes Apple Metal GPU kernels, a judge it can't fool proves them, and the winners run inside a real model.
|
|
5
|
+
Author: Utsav Khairnar
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/utsav1033/carmen
|
|
8
|
+
Project-URL: Repository, https://github.com/utsav1033/carmen
|
|
9
|
+
Project-URL: How it works, https://github.com/utsav1033/carmen/blob/main/docs/how-it-works.md
|
|
10
|
+
Keywords: mlx,metal,apple-silicon,gpu-kernels,llm-inference,ai-agents,benchmarking
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: anthropic>=1.0
|
|
22
|
+
Requires-Dist: textual>=0.80
|
|
23
|
+
Provides-Extra: metal
|
|
24
|
+
Requires-Dist: mlx>=0.25; extra == "metal"
|
|
25
|
+
Provides-Extra: models
|
|
26
|
+
Requires-Dist: mlx>=0.25; extra == "models"
|
|
27
|
+
Requires-Dist: mlx-lm>=0.20; extra == "models"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="https://raw.githubusercontent.com/utsav1033/carmen/main/assets/banner.svg" alt="carmen: let the model cook. trust nothing it can't prove." width="100%">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<b>An AI writes GPU kernels for Apple Silicon. A judge it can't fool decides what's real.<br>
|
|
38
|
+
The winners go into a real model.</b>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<a href="https://github.com/utsav1033/carmen/actions/workflows/tests.yml"><img src="https://github.com/utsav1033/carmen/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
AI can write GPU kernels now, and the papers are full of "3× faster". A lot of it doesn't survive a second look:
|
|
48
|
+
|
|
49
|
+
- A one-shape `allclose` check, the KernelBench standard, **misses 78.6% of precision bugs** ([Measuring the Checker](https://arxiv.org/abs/2609.22220)).
|
|
50
|
+
- A speedup reported as **1.43× was 0.88×** on inputs the model hadn't seen ([KernelBench-Verified](https://github.com/facebookresearch/kernel_bench_verified)).
|
|
51
|
+
- On Apple Metal, **30% of LLM "wins" broke** on a config nobody tested ([Gaming Without an Attacker](https://arxiv.org/abs/2608.08722)).
|
|
52
|
+
|
|
53
|
+
Writing kernels is cheap now. **Trusting them is the hard part.** I built carmen to learn how kernels work and to answer one question
|
|
54
|
+
honestly: can an AI make a model on my Mac faster, with proof?
|
|
55
|
+
|
|
56
|
+
## What happened
|
|
57
|
+
|
|
58
|
+
**On a real model, yes, by a little.** carmen's AI wrote two fused 4-bit kernels that replace 8 MLX kernels with 2 in every layer
|
|
59
|
+
of Qwen 2.5 0.5B. Decode got **3% faster, with identical output.**
|
|
60
|
+
|
|
61
|
+
| Qwen 2.5 0.5B (4-bit), M4, decode | vs stock MLX | answers |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `mx.compile` alone | 1.00× [1.00–1.01] | identical |
|
|
64
|
+
| **carmen's kernels** | **1.03× [1.02–1.05]** | **identical 256 tokens** |
|
|
65
|
+
|
|
66
|
+
<sub>30 paired turns, 95% bootstrap range. `carmen e2e --modes stock,compile,mlp+compile --repeats 30 --gen 256`</sub>
|
|
67
|
+
|
|
68
|
+
**On single kernels, it wins where MLX runs several kernels and ties where Apple wrote one.**
|
|
69
|
+
|
|
70
|
+
| kernel | vs MLX | vs `mx.compile` |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| masked_softmax (MLX: 3 kernels) | **1.82×** | **1.38×** |
|
|
73
|
+
| add_rmsnorm (MLX: 2 kernels) | **1.41×** | **1.43×** |
|
|
74
|
+
| mlp_down, 4-bit, Qwen's shape (MLX: 2 kernels) | **1.02×** | **1.06×** |
|
|
75
|
+
| softmax · rmsnorm · layernorm (MLX: 1 tuned kernel) | 0.95–0.99× | |
|
|
76
|
+
| matmul · attention (MLX: heavily tuned) | 0.93× · 0.54× | |
|
|
77
|
+
|
|
78
|
+
**The judge caught all 75 bugs planted in seven kernels.** A KernelBench-style check let 35 of them through.
|
|
79
|
+
[How the judge works →](https://github.com/utsav1033/carmen/blob/main/docs/judge.md)
|
|
80
|
+
|
|
81
|
+
## What I learned
|
|
82
|
+
|
|
83
|
+
- **AI kernels don't beat hand-tuned code. They beat code nobody fused.**
|
|
84
|
+
- **A kernel that wins alone can lose inside a model.** One scored 1.15× in the judge and made Qwen 18% slower: it was timed
|
|
85
|
+
at the wrong shapes, against a weak baseline, one launch at a time. Now `carmen run <op> --for qwen0.5b` judges at the model's
|
|
86
|
+
real shapes and number format, against `mx.compile`, and checks the winner inside the model at the end
|
|
87
|
+
(ideas from [SoL-Pi](https://arxiv.org/abs/2609.20519)).
|
|
88
|
+
- **Measuring honestly took more work than writing kernels.** Running versions one after another, instead of taking turns,
|
|
89
|
+
produced two "wins" (+40% prefill, 1.09× decode) that weren't real. A browser in the background swung speed by ±27%.
|
|
90
|
+
- **Small models on a Mac are launch-bound.** Qwen 0.5B reads 0.28 GB of weights per word, so the M4 could do ~346 tok/s.
|
|
91
|
+
Stock MLX reaches 51%. The rest is hundreds of tiny kernel launches per word. Bigger fused kernels are the way forward.
|
|
92
|
+
|
|
93
|
+
New to kernels? [How it all works](https://github.com/utsav1033/carmen/blob/main/docs/how-it-works.md) explains LLMs, GPUs and kernels from scratch.
|
|
94
|
+
|
|
95
|
+
## How it works
|
|
96
|
+
|
|
97
|
+
- **Carmy** (the model) writes the kernel. It's trusted to be smart and **never trusted to grade**. It can only return text.
|
|
98
|
+
- **The judge** is plain Python. It checks every kernel against a float64 answer key on hundreds of inputs, including secret ones
|
|
99
|
+
drawn fresh every round, races it against MLX, and says **where** a kernel is wrong, not just that it is.
|
|
100
|
+
- **The loop:** three drafts in parallel, the best becomes the champion, the next round attacks one bottleneck. Lessons are kept
|
|
101
|
+
only when the judge proved them.
|
|
102
|
+
|
|
103
|
+
```mermaid
|
|
104
|
+
flowchart LR
|
|
105
|
+
C["Carmy × 3<br/>parallel drafts"] -->|kernel text| J{{"judge<br/>compile · float64 · invariants<br/>fresh fuzz · hidden draw · timing"}}
|
|
106
|
+
J -->|"located error<br/>+ speed profile"| C
|
|
107
|
+
J -->|verified & faster| K(["champion"])
|
|
108
|
+
K -->|"one structural change"| C
|
|
109
|
+
J -->|"proven lessons"| P[("playbook")]
|
|
110
|
+
P --> C
|
|
111
|
+
J -.->|"hidden results<br/>(never shown to Carmy)"| R["report"]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
<p align="center"><img src="https://raw.githubusercontent.com/utsav1033/carmen/main/assets/cook.webp" alt="a cook in progress: three drafts, the judge's verdicts, the champion" width="90%"></p>
|
|
115
|
+
|
|
116
|
+
## Try it
|
|
117
|
+
|
|
118
|
+
On an Apple Silicon Mac, with [uv](https://docs.astral.sh/uv/):
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv tool install "carmen-kernels[metal,models]" # or: pip install "carmen-kernels[metal,models]"
|
|
122
|
+
export ANTHROPIC_API_KEY=... # or put it in a .env file where you run carmen
|
|
123
|
+
carmen # the app
|
|
124
|
+
carmen speedup # Qwen 2.5 0.5B, stock vs carmen's kernels, on your Mac (no API key)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| command | what it does |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `carmen` | the terminal app: what's proven, speed up a model, pick a kernel, watch it cook |
|
|
130
|
+
| `carmen speedup [model]` | stock MLX vs `mx.compile` vs carmen's bundled kernels on a real model, paired runs; no API key (`--quick` for ~2 min) |
|
|
131
|
+
| `carmen run <op> [--for qwen0.5b]` | let Carmy cook one kernel; `--for` judges it at a real model's shapes |
|
|
132
|
+
| `carmen broken <op>` | plant bugs and check the judge catches every one, next to a KernelBench-style check |
|
|
133
|
+
| `carmen judge <op> <file>` | judge a kernel you wrote |
|
|
134
|
+
| `carmen e2e [model]` | run a real model stock vs with carmen's kernels: tok/s, noise range, same answers? (`pip install 'carmen-kernels[models]'`) |
|
|
135
|
+
| `carmen profile [model]` | where a model spends its time, step by step |
|
|
136
|
+
| `carmen bench` | the feedback loop vs plain best-of-N, same budget |
|
|
137
|
+
|
|
138
|
+
From source: `git clone https://github.com/utsav1033/carmen`, then `pip install -e '.[metal,models,dev]'`, and put your key in `.env` (see `.env.example`).
|
|
139
|
+
|
|
140
|
+
## Built on the shoulders of
|
|
141
|
+
|
|
142
|
+
[Measuring the Checker](https://arxiv.org/abs/2609.22220) · [Gaming Without an Attacker](https://arxiv.org/abs/2608.08722) · [Metal-Sci](https://arxiv.org/abs/2605.09708) · [Hacker-Fixer Loops](https://arxiv.org/abs/2606.08960) · [Test-Input Generation for Tensor Programs](https://arxiv.org/abs/2606.27396) · [Reward Hacking in Self-Improving Code Agents](https://openreview.net/forum?id=ikrQWGgxYg) · [Meta-Harness](https://arxiv.org/abs/2603.28052) · [Auditing Harness Tampering](https://arxiv.org/abs/2609.00069) · [SAGE](https://arxiv.org/abs/2609.35568) · [Prime Agent](https://arxiv.org/abs/2608.23552) · [KernelBench-Verified](https://github.com/facebookresearch/kernel_bench_verified) · [Contract-Grade Verifier](https://github.com/RishiShah99/lethe) · [Building a C compiler with parallel Claudes](https://www.anthropic.com/engineering/building-c-compiler)
|
|
143
|
+
|
|
144
|
+
<sub>Named after Carmen "Carmy" Berzatto. Yes, chef. Banner set in Space Grotesk and JetBrains Mono (SIL Open Font License).</sub>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/utsav1033/carmen/main/assets/banner.svg" alt="carmen: let the model cook. trust nothing it can't prove." width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>An AI writes GPU kernels for Apple Silicon. A judge it can't fool decides what's real.<br>
|
|
7
|
+
The winners go into a real model.</b>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://github.com/utsav1033/carmen/actions/workflows/tests.yml"><img src="https://github.com/utsav1033/carmen/actions/workflows/tests.yml/badge.svg" alt="tests"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
AI can write GPU kernels now, and the papers are full of "3× faster". A lot of it doesn't survive a second look:
|
|
17
|
+
|
|
18
|
+
- A one-shape `allclose` check, the KernelBench standard, **misses 78.6% of precision bugs** ([Measuring the Checker](https://arxiv.org/abs/2609.22220)).
|
|
19
|
+
- A speedup reported as **1.43× was 0.88×** on inputs the model hadn't seen ([KernelBench-Verified](https://github.com/facebookresearch/kernel_bench_verified)).
|
|
20
|
+
- On Apple Metal, **30% of LLM "wins" broke** on a config nobody tested ([Gaming Without an Attacker](https://arxiv.org/abs/2608.08722)).
|
|
21
|
+
|
|
22
|
+
Writing kernels is cheap now. **Trusting them is the hard part.** I built carmen to learn how kernels work and to answer one question
|
|
23
|
+
honestly: can an AI make a model on my Mac faster, with proof?
|
|
24
|
+
|
|
25
|
+
## What happened
|
|
26
|
+
|
|
27
|
+
**On a real model, yes, by a little.** carmen's AI wrote two fused 4-bit kernels that replace 8 MLX kernels with 2 in every layer
|
|
28
|
+
of Qwen 2.5 0.5B. Decode got **3% faster, with identical output.**
|
|
29
|
+
|
|
30
|
+
| Qwen 2.5 0.5B (4-bit), M4, decode | vs stock MLX | answers |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `mx.compile` alone | 1.00× [1.00–1.01] | identical |
|
|
33
|
+
| **carmen's kernels** | **1.03× [1.02–1.05]** | **identical 256 tokens** |
|
|
34
|
+
|
|
35
|
+
<sub>30 paired turns, 95% bootstrap range. `carmen e2e --modes stock,compile,mlp+compile --repeats 30 --gen 256`</sub>
|
|
36
|
+
|
|
37
|
+
**On single kernels, it wins where MLX runs several kernels and ties where Apple wrote one.**
|
|
38
|
+
|
|
39
|
+
| kernel | vs MLX | vs `mx.compile` |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| masked_softmax (MLX: 3 kernels) | **1.82×** | **1.38×** |
|
|
42
|
+
| add_rmsnorm (MLX: 2 kernels) | **1.41×** | **1.43×** |
|
|
43
|
+
| mlp_down, 4-bit, Qwen's shape (MLX: 2 kernels) | **1.02×** | **1.06×** |
|
|
44
|
+
| softmax · rmsnorm · layernorm (MLX: 1 tuned kernel) | 0.95–0.99× | |
|
|
45
|
+
| matmul · attention (MLX: heavily tuned) | 0.93× · 0.54× | |
|
|
46
|
+
|
|
47
|
+
**The judge caught all 75 bugs planted in seven kernels.** A KernelBench-style check let 35 of them through.
|
|
48
|
+
[How the judge works →](https://github.com/utsav1033/carmen/blob/main/docs/judge.md)
|
|
49
|
+
|
|
50
|
+
## What I learned
|
|
51
|
+
|
|
52
|
+
- **AI kernels don't beat hand-tuned code. They beat code nobody fused.**
|
|
53
|
+
- **A kernel that wins alone can lose inside a model.** One scored 1.15× in the judge and made Qwen 18% slower: it was timed
|
|
54
|
+
at the wrong shapes, against a weak baseline, one launch at a time. Now `carmen run <op> --for qwen0.5b` judges at the model's
|
|
55
|
+
real shapes and number format, against `mx.compile`, and checks the winner inside the model at the end
|
|
56
|
+
(ideas from [SoL-Pi](https://arxiv.org/abs/2609.20519)).
|
|
57
|
+
- **Measuring honestly took more work than writing kernels.** Running versions one after another, instead of taking turns,
|
|
58
|
+
produced two "wins" (+40% prefill, 1.09× decode) that weren't real. A browser in the background swung speed by ±27%.
|
|
59
|
+
- **Small models on a Mac are launch-bound.** Qwen 0.5B reads 0.28 GB of weights per word, so the M4 could do ~346 tok/s.
|
|
60
|
+
Stock MLX reaches 51%. The rest is hundreds of tiny kernel launches per word. Bigger fused kernels are the way forward.
|
|
61
|
+
|
|
62
|
+
New to kernels? [How it all works](https://github.com/utsav1033/carmen/blob/main/docs/how-it-works.md) explains LLMs, GPUs and kernels from scratch.
|
|
63
|
+
|
|
64
|
+
## How it works
|
|
65
|
+
|
|
66
|
+
- **Carmy** (the model) writes the kernel. It's trusted to be smart and **never trusted to grade**. It can only return text.
|
|
67
|
+
- **The judge** is plain Python. It checks every kernel against a float64 answer key on hundreds of inputs, including secret ones
|
|
68
|
+
drawn fresh every round, races it against MLX, and says **where** a kernel is wrong, not just that it is.
|
|
69
|
+
- **The loop:** three drafts in parallel, the best becomes the champion, the next round attacks one bottleneck. Lessons are kept
|
|
70
|
+
only when the judge proved them.
|
|
71
|
+
|
|
72
|
+
```mermaid
|
|
73
|
+
flowchart LR
|
|
74
|
+
C["Carmy × 3<br/>parallel drafts"] -->|kernel text| J{{"judge<br/>compile · float64 · invariants<br/>fresh fuzz · hidden draw · timing"}}
|
|
75
|
+
J -->|"located error<br/>+ speed profile"| C
|
|
76
|
+
J -->|verified & faster| K(["champion"])
|
|
77
|
+
K -->|"one structural change"| C
|
|
78
|
+
J -->|"proven lessons"| P[("playbook")]
|
|
79
|
+
P --> C
|
|
80
|
+
J -.->|"hidden results<br/>(never shown to Carmy)"| R["report"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
<p align="center"><img src="https://raw.githubusercontent.com/utsav1033/carmen/main/assets/cook.webp" alt="a cook in progress: three drafts, the judge's verdicts, the champion" width="90%"></p>
|
|
84
|
+
|
|
85
|
+
## Try it
|
|
86
|
+
|
|
87
|
+
On an Apple Silicon Mac, with [uv](https://docs.astral.sh/uv/):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv tool install "carmen-kernels[metal,models]" # or: pip install "carmen-kernels[metal,models]"
|
|
91
|
+
export ANTHROPIC_API_KEY=... # or put it in a .env file where you run carmen
|
|
92
|
+
carmen # the app
|
|
93
|
+
carmen speedup # Qwen 2.5 0.5B, stock vs carmen's kernels, on your Mac (no API key)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| command | what it does |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `carmen` | the terminal app: what's proven, speed up a model, pick a kernel, watch it cook |
|
|
99
|
+
| `carmen speedup [model]` | stock MLX vs `mx.compile` vs carmen's bundled kernels on a real model, paired runs; no API key (`--quick` for ~2 min) |
|
|
100
|
+
| `carmen run <op> [--for qwen0.5b]` | let Carmy cook one kernel; `--for` judges it at a real model's shapes |
|
|
101
|
+
| `carmen broken <op>` | plant bugs and check the judge catches every one, next to a KernelBench-style check |
|
|
102
|
+
| `carmen judge <op> <file>` | judge a kernel you wrote |
|
|
103
|
+
| `carmen e2e [model]` | run a real model stock vs with carmen's kernels: tok/s, noise range, same answers? (`pip install 'carmen-kernels[models]'`) |
|
|
104
|
+
| `carmen profile [model]` | where a model spends its time, step by step |
|
|
105
|
+
| `carmen bench` | the feedback loop vs plain best-of-N, same budget |
|
|
106
|
+
|
|
107
|
+
From source: `git clone https://github.com/utsav1033/carmen`, then `pip install -e '.[metal,models,dev]'`, and put your key in `.env` (see `.env.example`).
|
|
108
|
+
|
|
109
|
+
## Built on the shoulders of
|
|
110
|
+
|
|
111
|
+
[Measuring the Checker](https://arxiv.org/abs/2609.22220) · [Gaming Without an Attacker](https://arxiv.org/abs/2608.08722) · [Metal-Sci](https://arxiv.org/abs/2605.09708) · [Hacker-Fixer Loops](https://arxiv.org/abs/2606.08960) · [Test-Input Generation for Tensor Programs](https://arxiv.org/abs/2606.27396) · [Reward Hacking in Self-Improving Code Agents](https://openreview.net/forum?id=ikrQWGgxYg) · [Meta-Harness](https://arxiv.org/abs/2603.28052) · [Auditing Harness Tampering](https://arxiv.org/abs/2609.00069) · [SAGE](https://arxiv.org/abs/2609.35568) · [Prime Agent](https://arxiv.org/abs/2608.23552) · [KernelBench-Verified](https://github.com/facebookresearch/kernel_bench_verified) · [Contract-Grade Verifier](https://github.com/RishiShah99/lethe) · [Building a C compiler with parallel Claudes](https://www.anthropic.com/engineering/building-c-compiler)
|
|
112
|
+
|
|
113
|
+
<sub>Named after Carmen "Carmy" Berzatto. Yes, chef. Banner set in Space Grotesk and JetBrains Mono (SIL Open Font License).</sub>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""carmen: a self-correcting harness that gets LLMs to write fast GPU kernels you can trust.
|
|
2
|
+
|
|
3
|
+
The model (Carmy) is the intelligence; the judge is the truth; correction is the loop
|
|
4
|
+
between them, and memory is that loop's verified residue.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .base import PAD, Adapter, Kernel, static_check
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get(name: str):
|
|
5
|
+
if name == "metal":
|
|
6
|
+
from .metal import MetalAdapter
|
|
7
|
+
return MetalAdapter()
|
|
8
|
+
raise KeyError(f"unknown backend {name!r}; available: metal")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = ["PAD", "Adapter", "Kernel", "static_check", "get"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""What a kernel is, and what a chip adapter must provide.
|
|
2
|
+
|
|
3
|
+
A kernel is Metal (or CUDA, later) *source text* plus launch configs. It is never
|
|
4
|
+
Python: Carmy cannot run code inside the judge, only submit a kernel body.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Callable, Protocol
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
# Extra elements allocated after the real output. They start as NaN and must stay NaN:
|
|
17
|
+
# a write here means the kernel wrote past the end of its output.
|
|
18
|
+
PAD = 64
|
|
19
|
+
|
|
20
|
+
MAX_SOURCE_CHARS = 30_000
|
|
21
|
+
MAX_CONFIGS = 6
|
|
22
|
+
BANNED_TOKENS = ("#include", "asm(", "__asm")
|
|
23
|
+
# Loop-unrolling hints are harmless and often faster; any other pragma is refused.
|
|
24
|
+
ALLOWED_PRAGMAS = ("#pragma unroll", "#pragma clang loop")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Kernel:
|
|
29
|
+
source: str
|
|
30
|
+
header: str = ""
|
|
31
|
+
# Each config maps template constant -> int. "TG" (threads per threadgroup) is required.
|
|
32
|
+
configs: list[dict[str, int]] = field(default_factory=lambda: [{"TG": 256}])
|
|
33
|
+
plan: str = ""
|
|
34
|
+
|
|
35
|
+
def digest(self) -> str:
|
|
36
|
+
blob = json.dumps([self.source, self.header], sort_keys=True).encode()
|
|
37
|
+
return hashlib.sha256(blob).hexdigest()[:12]
|
|
38
|
+
|
|
39
|
+
def to_json(self) -> dict:
|
|
40
|
+
return {"source": self.source, "header": self.header, "configs": self.configs, "plan": self.plan}
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_json(d: dict) -> "Kernel":
|
|
44
|
+
return Kernel(d["source"], d.get("header", ""), d.get("configs") or [{"TG": 256}], d.get("plan", ""))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def static_check(kernel: Kernel) -> str | None:
|
|
48
|
+
"""Cheap checks before touching the GPU. Returns an error message or None."""
|
|
49
|
+
text = kernel.source + "\n" + kernel.header
|
|
50
|
+
if not kernel.source.strip():
|
|
51
|
+
return "empty kernel source"
|
|
52
|
+
if len(text) > MAX_SOURCE_CHARS:
|
|
53
|
+
return f"kernel is {len(text)} chars; limit is {MAX_SOURCE_CHARS}"
|
|
54
|
+
for tok in BANNED_TOKENS:
|
|
55
|
+
if tok in text:
|
|
56
|
+
return f"banned token {tok!r} (MLX already includes the Metal standard library)"
|
|
57
|
+
for line in text.splitlines():
|
|
58
|
+
if line.strip().startswith("#pragma") and not line.strip().startswith(ALLOWED_PRAGMAS):
|
|
59
|
+
return f"only loop-unrolling pragmas are allowed, got {line.strip()!r}"
|
|
60
|
+
if not 1 <= len(kernel.configs) <= MAX_CONFIGS:
|
|
61
|
+
return f"give between 1 and {MAX_CONFIGS} configs"
|
|
62
|
+
for cfg in kernel.configs:
|
|
63
|
+
tg = cfg.get("TG")
|
|
64
|
+
if not isinstance(tg, int) or tg < 32 or tg > 1024 or tg % 32:
|
|
65
|
+
return f"config {cfg}: TG must be a multiple of 32 between 32 and 1024"
|
|
66
|
+
for k, v in cfg.items():
|
|
67
|
+
if not k.isidentifier() or not isinstance(v, int) or not 0 <= v <= 65536:
|
|
68
|
+
return f"config {cfg}: constants must be identifiers mapped to ints in [0, 65536]"
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Adapter(Protocol):
|
|
73
|
+
"""The only chip-specific surface. Adding a new chip means writing one of these."""
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
|
|
77
|
+
def chip(self) -> str: ...
|
|
78
|
+
|
|
79
|
+
def build(self, op, kernel: Kernel):
|
|
80
|
+
"""Compile the kernel. Raise on compile errors."""
|
|
81
|
+
|
|
82
|
+
def upload(self, inputs: dict[str, np.ndarray], dtype: str = "float32"):
|
|
83
|
+
"""Move numpy inputs to the device once, so timing excludes transfers. For bfloat16,
|
|
84
|
+
inputs arrive as float32 arrays of exact bf16 values and are cast on the device."""
|
|
85
|
+
|
|
86
|
+
def launch(self, built, op, config: dict, dev_inputs, rows: int, n: int, dtype: str,
|
|
87
|
+
inner: int = 1) -> Callable[[], None]:
|
|
88
|
+
"""Return a function that runs the kernel `inner` times back to back and blocks until done.
|
|
89
|
+
For timing only: must not add work the stock op doesn't do (e.g. no NaN pre-fill)."""
|
|
90
|
+
|
|
91
|
+
def run(self, built, op, config: dict, dev_inputs, rows: int, n: int, dtype: str) -> np.ndarray:
|
|
92
|
+
"""Run once; return the flat output (rows * n + PAD elements), NaN-poisoned before launch."""
|
|
93
|
+
|
|
94
|
+
def baseline(self, op, dev_inputs) -> Callable[[], np.ndarray]:
|
|
95
|
+
"""Return a function that runs the stock library op, blocks, and returns its output."""
|
|
96
|
+
|
|
97
|
+
def baseline_launch(self, op, dev_inputs, inner: int = 1, compiled: bool = False) -> Callable[[], None]:
|
|
98
|
+
"""Return a function that runs the stock library op `inner` times and blocks (for timing).
|
|
99
|
+
`compiled=True` wraps it in the library's graph compiler (mx.compile), which fuses element-wise ops."""
|
|
100
|
+
|
|
101
|
+
def measure_peak_gbps(self) -> float:
|
|
102
|
+
"""Practical memory bandwidth from a copy kernel."""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Apple Metal via MLX's mx.fast.metal_kernel. The only file that knows about Metal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..ops.base import SCALAR_INPUTS
|
|
10
|
+
from .base import PAD, Kernel
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import mlx.core as mx
|
|
14
|
+
except ImportError as e: # pragma: no cover - only on machines without MLX
|
|
15
|
+
raise ImportError("the metal backend needs MLX on Apple Silicon: pip install 'carmen-kernels[metal]'") from e
|
|
16
|
+
|
|
17
|
+
_DTYPES = {"float32": mx.float32, "float16": mx.float16, "bfloat16": mx.bfloat16}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _host(a) -> np.ndarray:
|
|
21
|
+
"""numpy can't hold bfloat16: read it back as float32 (exact)."""
|
|
22
|
+
return np.array(a.astype(mx.float32) if a.dtype == mx.bfloat16 else a)
|
|
23
|
+
|
|
24
|
+
COPY_KERNEL = """
|
|
25
|
+
uint i = thread_position_in_grid.x;
|
|
26
|
+
if (i < inp_shape[0]) { out[i] = inp[i]; }
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MetalAdapter:
|
|
31
|
+
name = "metal"
|
|
32
|
+
|
|
33
|
+
def chip(self) -> str:
|
|
34
|
+
info = mx.metal.device_info() if hasattr(mx, "metal") and hasattr(mx.metal, "device_info") else mx.device_info()
|
|
35
|
+
return str(info.get("device_name", "apple-gpu"))
|
|
36
|
+
|
|
37
|
+
def build(self, op, kernel: Kernel):
|
|
38
|
+
return mx.fast.metal_kernel(
|
|
39
|
+
name=f"carmen_{op.name}_{kernel.digest()}",
|
|
40
|
+
input_names=list(op.input_names),
|
|
41
|
+
output_names=["out"],
|
|
42
|
+
source=kernel.source,
|
|
43
|
+
header=kernel.header,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def upload(self, inputs, dtype: str = "float32"):
|
|
47
|
+
def put(k, v):
|
|
48
|
+
a = mx.array(v)
|
|
49
|
+
cast = dtype == "bfloat16" and k not in SCALAR_INPUTS and np.asarray(v).dtype.kind == "f"
|
|
50
|
+
return a.astype(mx.bfloat16) if cast else a # integer inputs (packed weights) stay as they are
|
|
51
|
+
return {k: put(k, v) for k, v in inputs.items()}
|
|
52
|
+
|
|
53
|
+
def _call(self, built, op, config, dev_inputs, rows, n, dtype, poison: bool = True):
|
|
54
|
+
grid, threadgroup = op.grid(rows, n, config)
|
|
55
|
+
template = [("T", _DTYPES[dtype])] + [(k, v) for k, v in config.items()]
|
|
56
|
+
(out,) = built(
|
|
57
|
+
inputs=[dev_inputs[name] for name in op.input_names],
|
|
58
|
+
template=template,
|
|
59
|
+
# The harness sets the launch shape (one threadgroup per row, or per output tile).
|
|
60
|
+
# Carmy never picks the grid, which rules out MLX's silent dispatch truncation.
|
|
61
|
+
grid=grid,
|
|
62
|
+
threadgroup=threadgroup,
|
|
63
|
+
output_shapes=[(op.outputs * rows * n + PAD,)],
|
|
64
|
+
output_dtypes=[_DTYPES[dtype]],
|
|
65
|
+
# Correctness runs pre-fill the output with NaN so unwritten elements show up.
|
|
66
|
+
# Timing runs must not: the fill is an extra full write the stock op never pays.
|
|
67
|
+
**({"init_value": float("nan")} if poison else {}),
|
|
68
|
+
)
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
def launch(self, built, op, config, dev_inputs, rows, n, dtype, inner: int = 1):
|
|
72
|
+
# `inner` launches share one eval, so per-call Python/dispatch overhead is amortized
|
|
73
|
+
# and the timer sees GPU time, not host time.
|
|
74
|
+
def go():
|
|
75
|
+
mx.eval(*[self._call(built, op, config, dev_inputs, rows, n, dtype, poison=False) for _ in range(inner)])
|
|
76
|
+
return go
|
|
77
|
+
|
|
78
|
+
def run(self, built, op, config, dev_inputs, rows, n, dtype):
|
|
79
|
+
out = self._call(built, op, config, dev_inputs, rows, n, dtype)
|
|
80
|
+
mx.eval(out)
|
|
81
|
+
return _host(out)
|
|
82
|
+
|
|
83
|
+
def baseline(self, op, dev_inputs):
|
|
84
|
+
def go():
|
|
85
|
+
y = op.mlx_baseline(mx, dev_inputs)
|
|
86
|
+
mx.eval(y)
|
|
87
|
+
if isinstance(y, (tuple, list)): # several outputs: stacked, as the reference is
|
|
88
|
+
return np.concatenate([_host(a).reshape(-1, a.shape[-1]) for a in y])
|
|
89
|
+
return _host(y)
|
|
90
|
+
return go
|
|
91
|
+
|
|
92
|
+
def baseline_launch(self, op, dev_inputs, inner: int = 1, compiled: bool = False):
|
|
93
|
+
fn = lambda d: op.mlx_baseline(mx, d) # noqa: E731
|
|
94
|
+
if compiled:
|
|
95
|
+
# mx.compile fuses chains of element-wise ops (e.g. x * scale + mask) into one kernel:
|
|
96
|
+
# the strongest stock baseline a user gets without writing Metal.
|
|
97
|
+
# eps is a Python number to the stock op; read it before tracing (no reads inside compile).
|
|
98
|
+
consts = {"eps": float(dev_inputs["eps"].item())} if "eps" in dev_inputs else {}
|
|
99
|
+
names = [k for k in dev_inputs if k not in consts]
|
|
100
|
+
fused = mx.compile(lambda *arrs: op.mlx_baseline(mx, {**dict(zip(names, arrs)), **consts}))
|
|
101
|
+
fn = lambda d: fused(*[d[k] for k in names]) # noqa: E731
|
|
102
|
+
|
|
103
|
+
def go():
|
|
104
|
+
mx.eval(*[fn(dev_inputs) for _ in range(inner)])
|
|
105
|
+
return go
|
|
106
|
+
|
|
107
|
+
def release(self) -> None:
|
|
108
|
+
"""Hand MLX's cached buffers back to the OS between timing runs."""
|
|
109
|
+
(mx.clear_cache if hasattr(mx, "clear_cache") else mx.metal.clear_cache)()
|
|
110
|
+
|
|
111
|
+
def measure_peak_gbps(self) -> float:
|
|
112
|
+
n = 64 * 1024 * 1024 # 256 MB of float32
|
|
113
|
+
kern = mx.fast.metal_kernel(name="carmen_copy", input_names=["inp"], output_names=["out"], source=COPY_KERNEL)
|
|
114
|
+
inp = mx.random.uniform(shape=(n,))
|
|
115
|
+
mx.eval(inp)
|
|
116
|
+
|
|
117
|
+
def go():
|
|
118
|
+
(o,) = kern(inputs=[inp], grid=(n, 1, 1), threadgroup=(256, 1, 1),
|
|
119
|
+
output_shapes=[(n,)], output_dtypes=[mx.float32])
|
|
120
|
+
mx.eval(o)
|
|
121
|
+
|
|
122
|
+
for _ in range(3):
|
|
123
|
+
go()
|
|
124
|
+
best = float("inf")
|
|
125
|
+
for _ in range(20):
|
|
126
|
+
t = time.perf_counter()
|
|
127
|
+
go()
|
|
128
|
+
best = min(best, time.perf_counter() - t)
|
|
129
|
+
return 2 * n * 4 / best / 1e9
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`carmen bench`: does the feedback loop beat blind sampling?
|
|
2
|
+
|
|
3
|
+
For each op, run the loop and the best-of-N control arm the same number of times with the
|
|
4
|
+
same budget (rounds x k Carmy calls, no early stop), each from a fresh memory, and compare:
|
|
5
|
+
|
|
6
|
+
- final champion speed (vs MLX, and vs mx.compile(MLX))
|
|
7
|
+
- round-1 best vs final best: did later rounds add anything?
|
|
8
|
+
- how many kernels the judge verified
|
|
9
|
+
|
|
10
|
+
Writes bench.json and a markdown table (bench.md) ready to paste into a README.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import statistics
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Callable
|
|
20
|
+
|
|
21
|
+
from .events import read_events
|
|
22
|
+
|
|
23
|
+
MODES = ("loop", "bon")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _curve(run_dir: Path) -> list[float]:
|
|
27
|
+
"""Best verified speedup found in each round (0 when a round verified nothing)."""
|
|
28
|
+
best: dict[int, float] = {}
|
|
29
|
+
for e in read_events(run_dir):
|
|
30
|
+
if e["type"] == "judge_result":
|
|
31
|
+
best.setdefault(e["round"], 0.0)
|
|
32
|
+
if e.get("correct") and e.get("speedup"):
|
|
33
|
+
best[e["round"]] = max(best[e["round"]], e["speedup"])
|
|
34
|
+
return [best[r] for r in sorted(best)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(ops: list[str], *, repeats: int, rounds: int, k: int, root: Path, run_fn: Callable,
|
|
38
|
+
on_progress: Callable[[str], None] = print, **run_kw) -> dict:
|
|
39
|
+
root = Path(root)
|
|
40
|
+
results = []
|
|
41
|
+
for op in ops:
|
|
42
|
+
for i in range(repeats):
|
|
43
|
+
for mode in MODES:
|
|
44
|
+
tag = f"{op}-{mode}-{i}"
|
|
45
|
+
on_progress(f"{tag}: cooking ({rounds} rounds x {k} drafts)")
|
|
46
|
+
sm = run_fn(op, rounds=rounds, k=k, mode=mode, patience=rounds + 1,
|
|
47
|
+
runs_dir=root / tag, memory_dir=root / tag / "memory", **run_kw)
|
|
48
|
+
curve = _curve(Path(sm["run_dir"]))
|
|
49
|
+
results.append({"op": op, "mode": mode, "repeat": i, "speedup": sm.get("speedup"),
|
|
50
|
+
"speedup_compiled": sm.get("speedup_compiled"),
|
|
51
|
+
"hidden_speedup": sm.get("hidden_speedup"), "verified": sm["verified"],
|
|
52
|
+
"attempts": sm["attempts"], "curve": curve, "run_dir": sm["run_dir"]})
|
|
53
|
+
on_progress(f"{tag}: best {_x(sm.get('speedup'))}, per round {' '.join(_x(c) for c in curve)}")
|
|
54
|
+
out = {"when": time.strftime("%Y-%m-%d %H:%M"), "rounds": rounds, "k": k, "repeats": repeats,
|
|
55
|
+
"results": results, "table": table(results)}
|
|
56
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
(root / "bench.json").write_text(json.dumps(out, indent=2, default=float))
|
|
58
|
+
(root / "bench.md").write_text(markdown(out["table"], rounds, k, repeats))
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _mean(xs):
|
|
63
|
+
xs = [x for x in xs if x]
|
|
64
|
+
return statistics.mean(xs) if xs else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def table(results: list[dict]) -> list[dict]:
|
|
68
|
+
rows = []
|
|
69
|
+
for op in dict.fromkeys(r["op"] for r in results):
|
|
70
|
+
for mode in MODES:
|
|
71
|
+
rs = [r for r in results if r["op"] == op and r["mode"] == mode]
|
|
72
|
+
if not rs:
|
|
73
|
+
continue
|
|
74
|
+
first = [r["curve"][0] for r in rs if r["curve"]]
|
|
75
|
+
rows.append({
|
|
76
|
+
"op": op, "mode": mode, "runs": len(rs),
|
|
77
|
+
"speedup": _mean(r["speedup"] for r in rs),
|
|
78
|
+
"speedup_min": min((r["speedup"] or 0 for r in rs), default=0),
|
|
79
|
+
"speedup_max": max((r["speedup"] or 0 for r in rs), default=0),
|
|
80
|
+
"speedup_compiled": _mean(r["speedup_compiled"] for r in rs),
|
|
81
|
+
"hidden_speedup": _mean(r["hidden_speedup"] for r in rs),
|
|
82
|
+
"round1": _mean(first),
|
|
83
|
+
"verified": sum(r["verified"] for r in rs), "attempts": sum(r["attempts"] for r in rs),
|
|
84
|
+
})
|
|
85
|
+
return rows
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _x(v) -> str:
|
|
89
|
+
return f"{v:.2f}x" if v else "-"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def markdown(rows: list[dict], rounds: int, k: int, repeats: int) -> str:
|
|
93
|
+
lines = [f"Each row: {repeats} run(s) of {rounds} rounds x {k} drafts, fresh memory, no early stop.",
|
|
94
|
+
"`loop` feeds the judge's results back; `best-of-N` makes the same number of calls blind.", "",
|
|
95
|
+
"| kernel | mode | speedup vs MLX (mean, range) | vs mx.compile | unseen sizes | round 1 → final | verified |",
|
|
96
|
+
"|---|---|---|---|---|---|---|"]
|
|
97
|
+
for r in rows:
|
|
98
|
+
mode = "loop" if r["mode"] == "loop" else "best-of-N"
|
|
99
|
+
lines.append(f"| {r['op']} | {mode} | {_x(r['speedup'])} ({_x(r['speedup_min'])}–{_x(r['speedup_max'])}) "
|
|
100
|
+
f"| {_x(r['speedup_compiled'])} | {_x(r['hidden_speedup'])} | {_x(r['round1'])} → {_x(r['speedup'])} "
|
|
101
|
+
f"| {r['verified']}/{r['attempts']} |")
|
|
102
|
+
return "\n".join(lines) + "\n"
|