Skip to content

Commit 3d7416f

Browse files
ai-singhalclaude
andcommitted
v0.3.4: ship the real minGPT measurement (3.11×) + two correctness/dotenv fixes
Headline: 3.11× faster (-67.8% step time), -68.7% peak memory on Karpathy's minGPT (single A100, BF16+TF32+torch.compile-max-autotune+SDPA+Fused AdamW). Correctness curves match within BF16-widened tolerance. Fixes: - load_dotenv now uses find_dotenv(usecwd=True). Default usecwd=False walks up from the script's directory (e.g. ~/anaconda3/bin/), which never has a .env, so the binary silently failed to pick up OPENAI_API_KEY from the project root. - _strip_warmup no longer chops loss_values per-payload. Auto-detected stabilization gave different warmup counts for baseline vs torch.compile'd candidate (10 vs 15), so correctness compared misaligned training steps and reported spurious FAILs with ~0.18 loss divergence. - SUMMARY.md architecture rendering: evidence-wrapped {value,confidence,evidence} fields and compound fields now render as "k=v, k=v" instead of raw JSON soup. Artifacts: - examples/minGPT/profine_output/ — the actual JSON + MD outputs from the reproducible run, checked in so the README's headline number is verifiable. - README Results table updated with measured numbers (was 3.1×/66.4% placeholder). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9d9e879 commit 3d7416f

93 files changed

Lines changed: 9426 additions & 16 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,13 @@ uv.lock
4242
.profine_tmp/
4343
.profine_optimized_*.py
4444
profine_output*/
45+
# Exception: the canonical demo artifacts in examples/minGPT/ ARE checked in
46+
# as evidence the headline numbers in the README are reproducible.
47+
!examples/minGPT/profine_output/
48+
!examples/minGPT/profine_output/**
4549
quickstart_out/
4650
dist/
51+
52+
# profine-output _debug dumps (per-run, not for the repo)
53+
examples/**/profine_output/_debug/
54+
examples/**/run.log

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to `profine` are documented here. This project follows [Semantic Versioning](https://semver.org/).
44

5+
## [0.3.4] — 2026-05-12
6+
7+
### Fixed
8+
- **`.env` loading from installed binary.** `load_dotenv()` defaults to searching upward from the entry-script directory (e.g. `~/anaconda3/bin/`), which never has a `.env`. Now passes `find_dotenv(usecwd=True)` explicitly, so `OPENAI_API_KEY` (and friends) in your project's `.env` are picked up when you run `profine` from your project root.
9+
- **Spurious "correctness: FAIL" verdicts.** `_strip_warmup` used to chop `loss_values` per-payload using auto-detected stabilization points, which yielded different warmup counts for baseline vs. optimized (e.g. 10 vs. 15 with `torch.compile`). The correctness check then compared baseline-step-10 against optimized-step-15 — five training steps apart — and flagged the natural divergence as a failure. Now `_strip_warmup` only touches `step_times_ms`; losses stay aligned by original training-step index.
10+
- **Cleaner `SUMMARY.md` architecture section.** Evidence-wrapped and compound architecture fields now render as `key=value, key=value` instead of dumping raw nested JSON.
11+
512
## [0.3.3] — 2026-05-12
613

714
### Added

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,23 @@ Profile your PyTorch code on real GPUs. Get a transparent rewrite. Ship measured
1515

1616
## Results
1717

18-
On [Karpathy's minGPT](https://github.com/karpathy/minGPT), single-A100:
18+
On [Karpathy's minGPT](https://github.com/karpathy/minGPT) (single A100, measured end-to-end by profine):
1919

2020
| Metric | Baseline | profine | Δ |
2121
|---|---|---|---|
22-
| Step time | 1.00× | **3.1× faster** | −67.7% ms/step |
23-
| Peak memory | 1.00× | **−66.4%** | substantial headroom for larger batch |
22+
| Step time (ms) | 25.22 | 8.11 | **−67.8%** (**3.11× faster**) |
23+
| Peak memory (GB) | 1.43 | 0.45 | **−68.7%** |
24+
| Correctness (loss curves match) ||| within BF16-widened tolerance |
2425

25-
Reproducible with some variation of (as shown in the [demo](https://youtu.be/CY9aW1Dcrn0)):
26+
Stack applied: **BF16 Mixed Precision + TF32 matmul + torch.compile (max-autotune) + SDPA + Fused AdamW.** Reproducible with:
2627

2728
```bash
28-
profine run-all examples/minGPT/projects/chargpt/chargpt.py --hardware 1x_a100 --steps 25 --warmup 10
29+
profine run-all examples/minGPT/projects/chargpt/chargpt.py \
30+
--hardware 1x_a100 --steps 25 --warmup 10 --seed 42
2931
```
3032

33+
Full artifacts (JSON + Markdown reports for every pipeline step) live in [`examples/minGPT/profine_output/`](examples/minGPT/profine_output/) — read [`SUMMARY.md`](examples/minGPT/profine_output/SUMMARY.md) first. As shown in the [demo](https://youtu.be/CY9aW1Dcrn0).
34+
3135
## Install
3236

3337
```bash
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# profine run-all — examples/minGPT/projects/chargpt/chargpt.py
2+
3+
**Hardware:** `1x_a100`
4+
5+
## ✅ 67.8% faster (3.11× speedup), correctness preserved.
6+
7+
## Architecture (what we found in your code)
8+
9+
- **Model:** GPT
10+
- **Framework:** PyTorch
11+
- **Precision:** training_dtype=float32
12+
- **Optimizer:** name=AdamW, learning_rate=0.0005
13+
- **Dataloader:** dataset_class=CharDataset
14+
15+
## Bottleneck (what's slowing it down)
16+
17+
- **matmul**
18+
- **elementwise**
19+
- **optimizer**
20+
21+
## Optimizations
22+
23+
**Ranked by LLM ROI:**
24+
25+
1. `bf16_mixed_precision` — This can materially accelerate the matmul-heavy workload on A100 by moving compute to tensor cores and reducing activation bandwidth, but it is more intrusive t
26+
2. `fp16_mixed_precision` — Similar upside to bf16 for compute throughput, but slightly riskier because it needs GradScaler and is more prone to underflow
27+
3. `tf32_matmul` — Best ROI: the run is dominated by fp32 GEMMs (46
28+
4. `torch_compile_max_autotune` — Strong fit because the profile shows substantial elementwise work (15
29+
5. `torch_compile` — Useful for fusing the observed elementwise work and reducing Python/kernel-launch overhead, but likely lower ROI than max-autotune
30+
31+
**Skipped (4):** `fp16_mixed_precision` (exclusive group 2 — conflicts with already-applied optimization), `torch_compile` (exclusive group 3 — conflicts with already-applied optimization), `dataloader_workers` (The input pipeline is already lightweight and the optimization record states that increasing DataLoader workers would add CPU/memory overhead with essentially no expected return for this CharDataset, so it should be skipped.), `foreach_adamw` (exclusive group 4 — conflicts with already-applied optimization)
32+
33+
## Benchmark (measured on-GPU)
34+
35+
| Metric | Δ |
36+
|---|---|
37+
| Step time | **+67.8%** (3.11× faster) |
38+
| Peak memory | -68.7% |
39+
| GPU utilization | -11.4% |
40+
| Verdict | **PASS** |
41+
| Correctness | ✓ pass |
42+
43+
## Artifacts
44+
45+
- [`read/architecture_record.json`](read/architecture_record.json) — Parsed architecture (JSON)
46+
- [`read/architecture_brief.md`](read/architecture_brief.md) — Architecture brief (MD)
47+
- [`profile/profile_record.json`](profile/profile_record.json) — Profile data (JSON)
48+
- [`profile/profile_report.md`](profile/profile_report.md) — Profile report (MD)
49+
- [`interpret/bottleneck_report.json`](interpret/bottleneck_report.json) — Bottleneck diagnosis (JSON)
50+
- [`interpret/bottleneck_brief.md`](interpret/bottleneck_brief.md) — Bottleneck brief (MD)
51+
- [`suggest/suggestion_report.json`](suggest/suggestion_report.json) — Ranked optimizations (JSON)
52+
- [`suggest/suggestion_brief.md`](suggest/suggestion_brief.md) — Suggestion brief (MD)
53+
- [`edit/edited_train.py`](edit/edited_train.py) — Optimized training script
54+
- [`edit/change_manifest.json`](edit/change_manifest.json) — What was changed and why
55+
- [`benchmark/benchmark_comparison.json`](benchmark/benchmark_comparison.json) — Benchmark comparison (JSON)
56+
- [`benchmark/benchmark_report.md`](benchmark/benchmark_report.md) — Benchmark report (MD)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"metrics": [
3+
{
4+
"name": "step_time_median_ms",
5+
"baseline": 25.2165,
6+
"candidate": 8.111,
7+
"delta": -17.1056,
8+
"delta_pct": -67.83,
9+
"improved": true
10+
},
11+
{
12+
"name": "throughput_steps_per_sec",
13+
"baseline": 39.6565,
14+
"candidate": 123.2901,
15+
"delta": 83.6336,
16+
"delta_pct": 210.9,
17+
"improved": true
18+
},
19+
{
20+
"name": "memory_peak_gb",
21+
"baseline": 1.4278,
22+
"candidate": 0.4472,
23+
"delta": -0.9806,
24+
"delta_pct": -68.68,
25+
"improved": true
26+
},
27+
{
28+
"name": "gpu_util_mean_pct",
29+
"baseline": 15.5833,
30+
"candidate": 4.155,
31+
"delta": -11.4283,
32+
"delta_pct": -73.34,
33+
"improved": false
34+
}
35+
],
36+
"correctness": {
37+
"passed": true,
38+
"loss_match": true,
39+
"max_loss_diff": 0.03388619422912598,
40+
"rtol": 0.05,
41+
"atol": 0.01,
42+
"notes": "Loss curves match across 15 steps (max diff: 0.033886)"
43+
},
44+
"speedup_pct": 67.83,
45+
"memory_delta_pct": -68.68,
46+
"util_delta_pct": -11.428341606913037,
47+
"verdict": "PASS",
48+
"summary": "PASS | 67.8% faster | 68.7% less memory | GPU util -11pp | correctness: PASS"
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Benchmark Report
2+
3+
## ✅ 67.8% faster (3.11× speedup), correctness preserved.
4+
5+
**Optimization applied:** BF16 Mixed Precision + TF32 matmul precision + torch.compile mode='max-autotune' + Scaled Dot Product Attention (SDPA) + Fused AdamW
6+
**Hardware:** 1x_a100 ($2.50/hr)
7+
**Verdict:** PASS
8+
**Notes:** PASS | 67.8% faster | 68.7% less memory | GPU util -11pp | correctness: PASS
9+
10+
---
11+
12+
## Metrics
13+
14+
| Metric | Baseline | Optimized | Δ | |
15+
|---|---|---|---|---|
16+
| Step time (ms) | 25.22 | 8.11 | -67.8% | ↑ improved |
17+
| Throughput (steps/s) | 39.66 | 123.29 | +210.9% | ↑ improved |
18+
| Peak memory (GB) | 1.43 | 0.45 | -68.7% | ↑ improved |
19+
| GPU utilization (%) | 15.6 | 4.2 | -73.3% | ↓ regressed |
20+
21+
## Projected Savings
22+
23+
For every **100 hours** of training time saved at the optimized step time, you'd have spent **311 hours** on the baseline.
24+
25+
| Baseline run length | Time saved | Cost saved |
26+
|---|---|---|
27+
| 1 hr | 0.68 hr (41 min) | $1.70 |
28+
| 10 hr | 6.78 hr (407 min) | $16.96 |
29+
| 100 hr | 67.83 hr (4070 min) | $169.57 |
30+
| 1000 hr | 678.30 hr (40698 min) | $1695.75 |
31+
32+
## Correctness
33+
34+
- **Loss curves match:** Yes ✓
35+
- **Max loss diff:** 0.033886
36+
- **Tolerance:** rtol=0.05, atol=0.01
37+
- **Notes:** Loss curves match across 15 steps (max diff: 0.033886)
38+
39+
## Recommendation
40+
41+
**Ship it.** Speedup exceeds the 3% threshold and correctness passed.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"applied": true,
3+
"optimization_id": "bf16_mixed_precision",
4+
"explanation": "Applied BF16 mixed precision by autocasting the model forward pass during training. This keeps the model, loss, optimizer, and training loop behavior intact while allowing CUDA matmuls/activations to run in bfloat16 on supported hardware.",
5+
"changes": [
6+
{
7+
"path": "mingpt/trainer.py",
8+
"line_start": 92,
9+
"line_end": 94,
10+
"description": "Wrapped the training forward pass in CUDA bfloat16 autocast to enable BF16 mixed precision.",
11+
"original_snippet": " # forward the model\n logits, self.loss = model(x, y)\n",
12+
"new_snippet": " # forward the model\n with torch.autocast(device_type='cuda', dtype=torch.bfloat16): # profine: bf16_mixed_precision\n logits, self.loss = model(x, y)\n"
13+
}
14+
],
15+
"new_imports": [],
16+
"warnings": [
17+
"This requires bf16-capable CUDA hardware; on unsupported setups, torch.autocast with device_type='cuda' may not provide the intended benefit.",
18+
"The autocast scope is limited to the forward pass only, so backward and optimizer steps remain in full precision as before."
19+
],
20+
"not_applicable_reason": null,
21+
"extra_file_edits": [
22+
{
23+
"path": "mingpt/trainer.py",
24+
"diff": "--- mingpt/trainer.py+++ mingpt/trainer.py (optimized)@@ -90,7 +90,8 @@ x, y = batch\n \n # forward the model\n- logits, self.loss = model(x, y)\n+ with torch.autocast(device_type='cuda', dtype=torch.bfloat16): # profine: bf16_mixed_precision\n+ logits, self.loss = model(x, y)\n \n # backprop and update the parameters\n model.zero_grad(set_to_none=True)\n"
25+
}
26+
]
27+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Trains a character-level language model.
3+
"""
4+
5+
import os
6+
import sys
7+
8+
import torch
9+
from torch.utils.data import Dataset
10+
from torch.utils.data.dataloader import DataLoader
11+
12+
from mingpt.model import GPT
13+
from mingpt.trainer import Trainer
14+
from mingpt.utils import set_seed, setup_logging, CfgNode as CN
15+
16+
# -----------------------------------------------------------------------------
17+
18+
def get_config():
19+
20+
C = CN()
21+
22+
# system
23+
C.system = CN()
24+
C.system.seed = 3407
25+
C.system.work_dir = './out/chargpt'
26+
27+
# data
28+
C.data = CharDataset.get_default_config()
29+
30+
# model
31+
C.model = GPT.get_default_config()
32+
C.model.model_type = 'gpt-mini'
33+
34+
# trainer
35+
C.trainer = Trainer.get_default_config()
36+
C.trainer.learning_rate = 5e-4 # the model we're using is so small that we can go a bit faster
37+
38+
return C
39+
40+
# -----------------------------------------------------------------------------
41+
42+
class CharDataset(Dataset):
43+
"""
44+
Emits batches of characters
45+
"""
46+
47+
@staticmethod
48+
def get_default_config():
49+
C = CN()
50+
C.block_size = 128
51+
return C
52+
53+
def __init__(self, config, data):
54+
self.config = config
55+
56+
chars = sorted(list(set(data)))
57+
data_size, vocab_size = len(data), len(chars)
58+
print('data has %d characters, %d unique.' % (data_size, vocab_size))
59+
60+
self.stoi = { ch:i for i,ch in enumerate(chars) }
61+
self.itos = { i:ch for i,ch in enumerate(chars) }
62+
self.vocab_size = vocab_size
63+
self.data = data
64+
65+
def get_vocab_size(self):
66+
return self.vocab_size
67+
68+
def get_block_size(self):
69+
return self.config.block_size
70+
71+
def __len__(self):
72+
return len(self.data) - self.config.block_size
73+
74+
def __getitem__(self, idx):
75+
# grab a chunk of (block_size + 1) characters from the data
76+
chunk = self.data[idx:idx + self.config.block_size + 1]
77+
# encode every character to an integer
78+
dix = [self.stoi[s] for s in chunk]
79+
# return as tensors
80+
x = torch.tensor(dix[:-1], dtype=torch.long)
81+
y = torch.tensor(dix[1:], dtype=torch.long)
82+
return x, y
83+
84+
# -----------------------------------------------------------------------------
85+
86+
if __name__ == '__main__':
87+
88+
# get default config and overrides from the command line, if any
89+
config = get_config()
90+
config.merge_from_args(sys.argv[1:])
91+
print(config)
92+
setup_logging(config)
93+
set_seed(config.system.seed)
94+
95+
# construct the training dataset
96+
text = open('input.txt', 'r').read() # don't worry we won't run out of file handles
97+
train_dataset = CharDataset(config.data, text)
98+
99+
# construct the model
100+
config.model.vocab_size = train_dataset.get_vocab_size()
101+
config.model.block_size = train_dataset.get_block_size()
102+
model = GPT(config.model)
103+
104+
# construct the trainer object
105+
trainer = Trainer(config.trainer, model, train_dataset)
106+
107+
# iteration callback
108+
def batch_end_callback(trainer):
109+
110+
if trainer.iter_num % 10 == 0:
111+
print(f"iter_dt {trainer.iter_dt * 1000:.2f}ms; iter {trainer.iter_num}: train loss {trainer.loss.item():.5f}")
112+
113+
if trainer.iter_num % 500 == 0:
114+
# evaluate both the train and test score
115+
model.eval()
116+
with torch.no_grad():
117+
# sample from the model...
118+
context = "O God, O God!"
119+
x = torch.tensor([train_dataset.stoi[s] for s in context], dtype=torch.long)[None,...].to(trainer.device)
120+
y = model.generate(x, 500, temperature=1.0, do_sample=True, top_k=10)[0]
121+
completion = ''.join([train_dataset.itos[int(i)] for i in y])
122+
print(completion)
123+
# save the latest model
124+
print("saving model")
125+
ckpt_path = os.path.join(config.system.work_dir, "model.pt")
126+
torch.save(model.state_dict(), ckpt_path)
127+
# revert model to training mode
128+
model.train()
129+
130+
trainer.set_callback('on_batch_end', batch_end_callback)
131+
132+
# run the optimization
133+
trainer.run()

examples/minGPT/profine_output/edit/01_bf16_mixed_precision/edited_train.py.diff

Whitespace-only changes.

0 commit comments

Comments
 (0)