train-doctor 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.
- train_doctor-0.1.0/.gitignore +11 -0
- train_doctor-0.1.0/CHANGELOG.md +13 -0
- train_doctor-0.1.0/LICENSE +21 -0
- train_doctor-0.1.0/PKG-INFO +163 -0
- train_doctor-0.1.0/README.md +144 -0
- train_doctor-0.1.0/examples/README.md +57 -0
- train_doctor-0.1.0/examples/cnn_images/train.py +165 -0
- train_doctor-0.1.0/examples/run_validation.sh +30 -0
- train_doctor-0.1.0/examples/tabular_mlp/train.py +195 -0
- train_doctor-0.1.0/pyproject.toml +57 -0
- train_doctor-0.1.0/skills/train-doctor/SKILL.md +82 -0
- train_doctor-0.1.0/src/train_doctor/__init__.py +36 -0
- train_doctor-0.1.0/src/train_doctor/__main__.py +3 -0
- train_doctor-0.1.0/src/train_doctor/_hook/sitecustomize.py +47 -0
- train_doctor-0.1.0/src/train_doctor/_hook/td_hook.py +922 -0
- train_doctor-0.1.0/src/train_doctor/accel.py +107 -0
- train_doctor-0.1.0/src/train_doctor/api.py +335 -0
- train_doctor-0.1.0/src/train_doctor/cli.py +247 -0
- train_doctor-0.1.0/src/train_doctor/evidence.py +421 -0
- train_doctor-0.1.0/src/train_doctor/lossdiff.py +106 -0
- train_doctor-0.1.0/src/train_doctor/machine.py +91 -0
- train_doctor-0.1.0/src/train_doctor/mcp_server.py +159 -0
- train_doctor-0.1.0/src/train_doctor/redact.py +22 -0
- train_doctor-0.1.0/src/train_doctor/report.py +319 -0
- train_doctor-0.1.0/src/train_doctor/rules.py +496 -0
- train_doctor-0.1.0/src/train_doctor/runner.py +293 -0
- train_doctor-0.1.0/src/train_doctor/setup_agents.py +173 -0
- train_doctor-0.1.0/src/train_doctor/stats.py +165 -0
- train_doctor-0.1.0/tests/conftest.py +43 -0
- train_doctor-0.1.0/tests/test_accel.py +27 -0
- train_doctor-0.1.0/tests/test_evidence.py +160 -0
- train_doctor-0.1.0/tests/test_hook_fake_cuda.py +96 -0
- train_doctor-0.1.0/tests/test_lossdiff.py +38 -0
- train_doctor-0.1.0/tests/test_mcp.py +36 -0
- train_doctor-0.1.0/tests/test_rules.py +195 -0
- train_doctor-0.1.0/tests/test_runs.py +181 -0
- train_doctor-0.1.0/tests/test_setup.py +80 -0
- train_doctor-0.1.0/tests/test_stats.py +58 -0
- train_doctor-0.1.0/tests/test_torch_hook.py +79 -0
- train_doctor-0.1.0/tests/test_writing.py +19 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
First version.
|
|
6
|
+
|
|
7
|
+
- `profile`: runs a training command for a bounded window (warmup plus at least N steps and T seconds) with an in-process recorder injected through `sitecustomize`. Records step times, DataLoader wait, host/device sync calls, `torch.save` and log-write time, samples per step, loss, DataLoader and precision config, process CPU and memory, and GPU telemetry (nvidia-smi, or ioreg on Apple silicon). Optional torch.profiler steps after the window and py-spy sampling.
|
|
8
|
+
- `diagnose`: deterministic rules for DataLoader-bound runs, expensive per-sample preprocessing, pin_memory on CUDA, host syncs in the loop, small batches, missing mixed precision, missing torch.compile, checkpoints and logging in the hot loop, gradient accumulation (including DDP without no_sync), cuDNN autotuning, and CPU training with an accelerator available.
|
|
9
|
+
- `compare`: discarded warmup runs, K repeats per arm in randomized pair order, same seed, median and spread, bootstrap interval on the ratio of medians, a verdict that refuses to call changes inside the noise, and a loss-trajectory check.
|
|
10
|
+
- `report`: Markdown and JSON bundles with machine, versions, commands, per-repeat numbers, findings, verdict and limits.
|
|
11
|
+
- `setup`: registers the MCP server with Claude Code, Codex and Cursor and installs the skill, with a dry run, backups and idempotent re-runs.
|
|
12
|
+
- `--label` names run directories; unstartable commands give a clear error instead of a traceback.
|
|
13
|
+
- MCP server over stdio with `profile`, `diagnose`, `compare`, `report` and `version` tools.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abel Yagubyan
|
|
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,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: train-doctor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Give your agent a way to find out why a training run is slow, fix it, and prove the speedup.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Abelo9996/train-doctor
|
|
6
|
+
Author: Abel Yagubyan
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,benchmark,mcp,profiling,pytorch,training
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: mcp>=1.2
|
|
17
|
+
Requires-Dist: psutil>=5.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# train-doctor
|
|
21
|
+
|
|
22
|
+
Give your agent a way to find out why your training run is slow, fix it, and prove the speedup.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uvx train-doctor profile -- python train.py # where does step time go?
|
|
26
|
+
uvx train-doctor compare --baseline "python train.py" --candidate "python train.py --num-workers 4"
|
|
27
|
+
uvx train-doctor setup # give Claude Code, Codex and Cursor the MCP tools
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> Not on PyPI yet. Until the first release, run it straight from GitHub by replacing `uvx train-doctor` with
|
|
31
|
+
> `uvx --from git+https://github.com/Abelo9996/train-doctor train-doctor`. `setup` registers `uvx train-doctor mcp`, so it works once the
|
|
32
|
+
> package is on PyPI.
|
|
33
|
+
|
|
34
|
+
## Example output
|
|
35
|
+
|
|
36
|
+
From a real run in this repo: [examples/cnn_images](examples/cnn_images), a small CNN written the way many
|
|
37
|
+
first scripts are (per-sample augmentation in `__getitem__`, `num_workers=0`, `.item()` and a print on every
|
|
38
|
+
step). Apple M4 (10 cores, 16 GB), torch 2.14.1 on MPS, with other jobs running on the machine (1-minute
|
|
39
|
+
load average between 4.6 and 10.4 across all runs).
|
|
40
|
+
|
|
41
|
+
Profile output (trimmed where marked `...`):
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
$ train-doctor profile -- python examples/cnn_images/train.py
|
|
45
|
+
source: hook, device: mps, steps measured: 87
|
|
46
|
+
step time: median 20.88 ms (p10 14.32, p90 32.84)
|
|
47
|
+
throughput: 2766.8 samples/s
|
|
48
|
+
split: data_wait 39.1%, host_sync 26.4%, checkpoint_save 0.0%, log_write 0.1%, other 34.4%
|
|
49
|
+
findings:
|
|
50
|
+
1. [dataloader_bound] Training waits on the DataLoader (num_workers=0) (score 0.391, risk low)
|
|
51
|
+
2. [cpu_bound_preprocessing] Per-sample preprocessing in SyntheticImages is expensive (score 0.319, risk low)
|
|
52
|
+
evidence: 39% of step time is data wait and 91% of that is inside SyntheticImages.__getitem__ (0.13 ms per sample).
|
|
53
|
+
3. [host_sync_in_loop] Host waits on the device inside every step (score 0.132, risk none)
|
|
54
|
+
evidence: 2.0 blocking reads of mps tensors per step (...), 6.1 ms per step blocked in them (26% of step time).
|
|
55
|
+
4. [no_mixed_precision] ... 5. [logging_in_hot_loop] ... 6. [no_torch_compile] ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then one change at a time, each checked with `train-doctor compare` (5 measured repeats per side after one
|
|
59
|
+
discarded warmup run, randomized order, 95% bootstrap interval on the ratio of median samples/s):
|
|
60
|
+
|
|
61
|
+
| Change | Median samples/s, before to after | Ratio (95% interval) | Loss check | Decision |
|
|
62
|
+
|---|---|---|---|---|
|
|
63
|
+
| `num_workers=5` | 2,249 to 2,750 | 1.22x (0.66 to 1.68) | within tolerance | inconclusive, dropped |
|
|
64
|
+
| crop/flip/normalize per batch on the device | 2,232 to 2,855 | 1.28x (1.10 to 1.35) | within tolerance | keep |
|
|
65
|
+
| read metrics every 50 steps instead of every step | 2,420 to 3,029 | 1.25x (1.11 to 1.35) | identical | keep |
|
|
66
|
+
| autocast float16 | 2,648 to 2,842 | 1.07x (0.81 to 1.28) | within tolerance | inconclusive, dropped |
|
|
67
|
+
| original vs the two kept changes | 2,091 to 3,381 | 1.62x (1.37 to 3.48) | within tolerance | keep |
|
|
68
|
+
|
|
69
|
+
Two of the four candidates looked faster by their medians and were still not kept, because their intervals
|
|
70
|
+
included 1.0. In the second example ([examples/tabular_mlp](examples/tabular_mlp)), checkpointing less often
|
|
71
|
+
was inconclusive with 5 repeats (1.54x, interval 0.62 to 2.67) and clearly faster with 9 (2.01x, 1.54 to 2.52);
|
|
72
|
+
original vs final there is 3.05x (2.15 to 3.40) with an identical loss trajectory. Every report, per-repeat
|
|
73
|
+
number and raw trace is committed under `examples/*/runs/`, and
|
|
74
|
+
[examples/run_validation.sh](examples/run_validation.sh) is the exact command sequence.
|
|
75
|
+
|
|
76
|
+
## How it works
|
|
77
|
+
|
|
78
|
+
A "2x faster" claim is only useful if you can trust it. train-doctor excludes warmup, brackets the
|
|
79
|
+
measured window with a device synchronize, runs baseline and candidate several times in randomized
|
|
80
|
+
interleaved order with the same seed, reports medians, spread and a bootstrap interval, and calls a
|
|
81
|
+
change "faster" only when the whole interval clears a threshold. It also checks that the loss
|
|
82
|
+
trajectory didn't move, so a speedup that broke training is rejected.
|
|
83
|
+
|
|
84
|
+
train-doctor runs your real training command as a child process and stops it after a bounded window
|
|
85
|
+
(warmup steps, then at least N steps and T seconds; defaults 5, 50 and 2 s). No code changes are needed
|
|
86
|
+
for PyTorch:
|
|
87
|
+
|
|
88
|
+
- **Step recorder.** A `sitecustomize.py` on the child's PYTHONPATH loads a small standard-library
|
|
89
|
+
recorder that patches torch right after it is imported. It marks a step at every `optimizer.step()`
|
|
90
|
+
(a global post-hook), times `DataLoader.__next__`, times blocking reads of GPU/MPS tensors
|
|
91
|
+
(`.item()`, `.cpu()`, `.tolist()`, `float()`, `bool()`, `synchronize()`), times `torch.save` and
|
|
92
|
+
stdout/stderr writes, counts `backward()` calls, and keeps the loss tensor passed to `backward()`
|
|
93
|
+
(converted to a number only at the end, so recording it adds no sync). It also notes DataLoader
|
|
94
|
+
settings, autocast, GradScaler, `torch.compile`, DDP, parameter count and dtype. Any other
|
|
95
|
+
`sitecustomize` you have still runs. Loops without an optimizer (or other frameworks) can call
|
|
96
|
+
`train_doctor.step(samples=..., loss=...)`, or just print a line containing `loss` each step.
|
|
97
|
+
- **torch.profiler** traces a few extra steps after the measured window (so it doesn't distort the
|
|
98
|
+
timings) and saves a Chrome trace you can open in Perfetto.
|
|
99
|
+
- **py-spy** (optional, `--py-spy`) samples Python stacks for any framework when it is installed.
|
|
100
|
+
- **System telemetry**: psutil for CPU and memory of the whole process tree (including DataLoader
|
|
101
|
+
workers), `nvidia-smi` for NVIDIA GPUs, and `ioreg` for Apple GPU utilization (no sudo needed).
|
|
102
|
+
- **Diagnosis** is a fixed set of rules over that evidence: DataLoader-bound, expensive per-sample
|
|
103
|
+
preprocessing, pin_memory off on CUDA, host syncs in the loop, small batch on an idle device, no
|
|
104
|
+
mixed precision on capable hardware, no torch.compile, checkpoints or logging in the hot loop,
|
|
105
|
+
gradient accumulation (including DDP without `no_sync()`), cuDNN autotuning off, and training on CPU
|
|
106
|
+
while an accelerator is available. Each finding carries the numbers it fired on, a suggested change,
|
|
107
|
+
the expected effect (an upper bound where one can be computed) and the risk to results.
|
|
108
|
+
- **Compare** runs one discarded warmup run per command, then K repeats per command (default 5) in
|
|
109
|
+
randomized pair order. Throughput is samples/s over the synchronized window. The verdict uses a 95%
|
|
110
|
+
percentile bootstrap interval on the ratio of medians: "faster" only if the whole interval is above
|
|
111
|
+
1 + min-effect (default 2%), "slower" if it's below 1 - min-effect, otherwise "no clear difference".
|
|
112
|
+
The loss check aligns both loss curves on samples seen, smooths them, and requires the mean and final
|
|
113
|
+
relative difference to stay within a tolerance (default 5%). The decision is `keep` only when both pass.
|
|
114
|
+
- **Reports**: every run directory gets `report.md` and `report.json` with the machine, versions,
|
|
115
|
+
commands, file hashes, git commit, per-repeat numbers, findings, verdict and limits, plus the raw
|
|
116
|
+
files (per-step events, resource samples, stdout and stderr, profiler output).
|
|
117
|
+
|
|
118
|
+
The tool is deterministic and never calls an LLM. Your coding agent reads the findings, makes one
|
|
119
|
+
change, and uses `compare` to decide whether to keep it.
|
|
120
|
+
|
|
121
|
+
## Setup for agents
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uvx train-doctor setup # shows what it would change
|
|
125
|
+
uvx train-doctor setup --yes # applies it
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`setup` detects Claude Code, Codex and Cursor. It runs `claude mcp add --scope user train-doctor -- uvx train-doctor mcp`,
|
|
129
|
+
adds `[mcp_servers.train-doctor]` to `~/.codex/config.toml`, adds the server to `~/.cursor/mcp.json`, and copies the
|
|
130
|
+
skill to `~/.claude/skills/train-doctor/` and `~/.codex/skills/train-doctor/`. It backs up every file it edits and
|
|
131
|
+
does nothing when an entry is already there. `--project DIR` also writes a project `.mcp.json`.
|
|
132
|
+
|
|
133
|
+
MCP tools: `profile`, `diagnose`, `compare`, `report`, `version`. The skill
|
|
134
|
+
([skills/train-doctor/SKILL.md](skills/train-doctor/SKILL.md)) tells the agent the workflow: profile, read
|
|
135
|
+
the findings, change one thing, compare, keep only clear wins that don't move the loss, report.
|
|
136
|
+
|
|
137
|
+
CLI: `train-doctor profile|diagnose|compare|report|setup|mcp`, each with `--help` and `--json`.
|
|
138
|
+
|
|
139
|
+
## What it can't do
|
|
140
|
+
|
|
141
|
+
- It doesn't edit your code. The agent (or you) makes the change; train-doctor measures it.
|
|
142
|
+
- Timings come from the host side. On CUDA and MPS, time inside a sync call includes waiting for device
|
|
143
|
+
work that has to happen anyway, so the "host sync" share is an upper bound on what removing syncs saves.
|
|
144
|
+
- Apple GPU utilization from `ioreg` is system-wide, not per process. There is no per-kernel timing on MPS.
|
|
145
|
+
- `__getitem__` time is measured only with `num_workers=0`; with workers it runs in other processes.
|
|
146
|
+
- py-spy needs root on macOS, so `--py-spy` usually fails there. It was only tested with a fake py-spy.
|
|
147
|
+
- CUDA support (nvidia-smi parsing, CUDA sync and memory paths, CUDA-specific rules) is implemented and
|
|
148
|
+
tested with fakes. It has not been run on an NVIDIA GPU yet. The real runs in this repo are on Apple MPS.
|
|
149
|
+
- Multi-GPU: with DDP it reads the process that recorded the most steps (usually rank 0); it doesn't merge ranks.
|
|
150
|
+
- The loss check covers the first steps only. It catches changes that break or shift training early; it
|
|
151
|
+
can't tell you about final accuracy. Batch size and learning-rate changes are flagged as high risk for that reason.
|
|
152
|
+
- Short windows on a busy machine are noisy. Reports record the load average and system CPU per run; when
|
|
153
|
+
the spread is high, compare says so and is more likely to answer "no clear difference".
|
|
154
|
+
|
|
155
|
+
## Privacy and safety
|
|
156
|
+
|
|
157
|
+
Everything runs locally. train-doctor makes no network requests and sends nothing anywhere. It runs the
|
|
158
|
+
command you give it with your permissions, and writes to `.train-doctor/runs/` (or `--out`). Reports
|
|
159
|
+
replace your home directory with `~`. `setup` changes only the files it lists, and only after `--yes`.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# train-doctor
|
|
2
|
+
|
|
3
|
+
Give your agent a way to find out why your training run is slow, fix it, and prove the speedup.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uvx train-doctor profile -- python train.py # where does step time go?
|
|
7
|
+
uvx train-doctor compare --baseline "python train.py" --candidate "python train.py --num-workers 4"
|
|
8
|
+
uvx train-doctor setup # give Claude Code, Codex and Cursor the MCP tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> Not on PyPI yet. Until the first release, run it straight from GitHub by replacing `uvx train-doctor` with
|
|
12
|
+
> `uvx --from git+https://github.com/Abelo9996/train-doctor train-doctor`. `setup` registers `uvx train-doctor mcp`, so it works once the
|
|
13
|
+
> package is on PyPI.
|
|
14
|
+
|
|
15
|
+
## Example output
|
|
16
|
+
|
|
17
|
+
From a real run in this repo: [examples/cnn_images](examples/cnn_images), a small CNN written the way many
|
|
18
|
+
first scripts are (per-sample augmentation in `__getitem__`, `num_workers=0`, `.item()` and a print on every
|
|
19
|
+
step). Apple M4 (10 cores, 16 GB), torch 2.14.1 on MPS, with other jobs running on the machine (1-minute
|
|
20
|
+
load average between 4.6 and 10.4 across all runs).
|
|
21
|
+
|
|
22
|
+
Profile output (trimmed where marked `...`):
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
$ train-doctor profile -- python examples/cnn_images/train.py
|
|
26
|
+
source: hook, device: mps, steps measured: 87
|
|
27
|
+
step time: median 20.88 ms (p10 14.32, p90 32.84)
|
|
28
|
+
throughput: 2766.8 samples/s
|
|
29
|
+
split: data_wait 39.1%, host_sync 26.4%, checkpoint_save 0.0%, log_write 0.1%, other 34.4%
|
|
30
|
+
findings:
|
|
31
|
+
1. [dataloader_bound] Training waits on the DataLoader (num_workers=0) (score 0.391, risk low)
|
|
32
|
+
2. [cpu_bound_preprocessing] Per-sample preprocessing in SyntheticImages is expensive (score 0.319, risk low)
|
|
33
|
+
evidence: 39% of step time is data wait and 91% of that is inside SyntheticImages.__getitem__ (0.13 ms per sample).
|
|
34
|
+
3. [host_sync_in_loop] Host waits on the device inside every step (score 0.132, risk none)
|
|
35
|
+
evidence: 2.0 blocking reads of mps tensors per step (...), 6.1 ms per step blocked in them (26% of step time).
|
|
36
|
+
4. [no_mixed_precision] ... 5. [logging_in_hot_loop] ... 6. [no_torch_compile] ...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then one change at a time, each checked with `train-doctor compare` (5 measured repeats per side after one
|
|
40
|
+
discarded warmup run, randomized order, 95% bootstrap interval on the ratio of median samples/s):
|
|
41
|
+
|
|
42
|
+
| Change | Median samples/s, before to after | Ratio (95% interval) | Loss check | Decision |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| `num_workers=5` | 2,249 to 2,750 | 1.22x (0.66 to 1.68) | within tolerance | inconclusive, dropped |
|
|
45
|
+
| crop/flip/normalize per batch on the device | 2,232 to 2,855 | 1.28x (1.10 to 1.35) | within tolerance | keep |
|
|
46
|
+
| read metrics every 50 steps instead of every step | 2,420 to 3,029 | 1.25x (1.11 to 1.35) | identical | keep |
|
|
47
|
+
| autocast float16 | 2,648 to 2,842 | 1.07x (0.81 to 1.28) | within tolerance | inconclusive, dropped |
|
|
48
|
+
| original vs the two kept changes | 2,091 to 3,381 | 1.62x (1.37 to 3.48) | within tolerance | keep |
|
|
49
|
+
|
|
50
|
+
Two of the four candidates looked faster by their medians and were still not kept, because their intervals
|
|
51
|
+
included 1.0. In the second example ([examples/tabular_mlp](examples/tabular_mlp)), checkpointing less often
|
|
52
|
+
was inconclusive with 5 repeats (1.54x, interval 0.62 to 2.67) and clearly faster with 9 (2.01x, 1.54 to 2.52);
|
|
53
|
+
original vs final there is 3.05x (2.15 to 3.40) with an identical loss trajectory. Every report, per-repeat
|
|
54
|
+
number and raw trace is committed under `examples/*/runs/`, and
|
|
55
|
+
[examples/run_validation.sh](examples/run_validation.sh) is the exact command sequence.
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
58
|
+
|
|
59
|
+
A "2x faster" claim is only useful if you can trust it. train-doctor excludes warmup, brackets the
|
|
60
|
+
measured window with a device synchronize, runs baseline and candidate several times in randomized
|
|
61
|
+
interleaved order with the same seed, reports medians, spread and a bootstrap interval, and calls a
|
|
62
|
+
change "faster" only when the whole interval clears a threshold. It also checks that the loss
|
|
63
|
+
trajectory didn't move, so a speedup that broke training is rejected.
|
|
64
|
+
|
|
65
|
+
train-doctor runs your real training command as a child process and stops it after a bounded window
|
|
66
|
+
(warmup steps, then at least N steps and T seconds; defaults 5, 50 and 2 s). No code changes are needed
|
|
67
|
+
for PyTorch:
|
|
68
|
+
|
|
69
|
+
- **Step recorder.** A `sitecustomize.py` on the child's PYTHONPATH loads a small standard-library
|
|
70
|
+
recorder that patches torch right after it is imported. It marks a step at every `optimizer.step()`
|
|
71
|
+
(a global post-hook), times `DataLoader.__next__`, times blocking reads of GPU/MPS tensors
|
|
72
|
+
(`.item()`, `.cpu()`, `.tolist()`, `float()`, `bool()`, `synchronize()`), times `torch.save` and
|
|
73
|
+
stdout/stderr writes, counts `backward()` calls, and keeps the loss tensor passed to `backward()`
|
|
74
|
+
(converted to a number only at the end, so recording it adds no sync). It also notes DataLoader
|
|
75
|
+
settings, autocast, GradScaler, `torch.compile`, DDP, parameter count and dtype. Any other
|
|
76
|
+
`sitecustomize` you have still runs. Loops without an optimizer (or other frameworks) can call
|
|
77
|
+
`train_doctor.step(samples=..., loss=...)`, or just print a line containing `loss` each step.
|
|
78
|
+
- **torch.profiler** traces a few extra steps after the measured window (so it doesn't distort the
|
|
79
|
+
timings) and saves a Chrome trace you can open in Perfetto.
|
|
80
|
+
- **py-spy** (optional, `--py-spy`) samples Python stacks for any framework when it is installed.
|
|
81
|
+
- **System telemetry**: psutil for CPU and memory of the whole process tree (including DataLoader
|
|
82
|
+
workers), `nvidia-smi` for NVIDIA GPUs, and `ioreg` for Apple GPU utilization (no sudo needed).
|
|
83
|
+
- **Diagnosis** is a fixed set of rules over that evidence: DataLoader-bound, expensive per-sample
|
|
84
|
+
preprocessing, pin_memory off on CUDA, host syncs in the loop, small batch on an idle device, no
|
|
85
|
+
mixed precision on capable hardware, no torch.compile, checkpoints or logging in the hot loop,
|
|
86
|
+
gradient accumulation (including DDP without `no_sync()`), cuDNN autotuning off, and training on CPU
|
|
87
|
+
while an accelerator is available. Each finding carries the numbers it fired on, a suggested change,
|
|
88
|
+
the expected effect (an upper bound where one can be computed) and the risk to results.
|
|
89
|
+
- **Compare** runs one discarded warmup run per command, then K repeats per command (default 5) in
|
|
90
|
+
randomized pair order. Throughput is samples/s over the synchronized window. The verdict uses a 95%
|
|
91
|
+
percentile bootstrap interval on the ratio of medians: "faster" only if the whole interval is above
|
|
92
|
+
1 + min-effect (default 2%), "slower" if it's below 1 - min-effect, otherwise "no clear difference".
|
|
93
|
+
The loss check aligns both loss curves on samples seen, smooths them, and requires the mean and final
|
|
94
|
+
relative difference to stay within a tolerance (default 5%). The decision is `keep` only when both pass.
|
|
95
|
+
- **Reports**: every run directory gets `report.md` and `report.json` with the machine, versions,
|
|
96
|
+
commands, file hashes, git commit, per-repeat numbers, findings, verdict and limits, plus the raw
|
|
97
|
+
files (per-step events, resource samples, stdout and stderr, profiler output).
|
|
98
|
+
|
|
99
|
+
The tool is deterministic and never calls an LLM. Your coding agent reads the findings, makes one
|
|
100
|
+
change, and uses `compare` to decide whether to keep it.
|
|
101
|
+
|
|
102
|
+
## Setup for agents
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uvx train-doctor setup # shows what it would change
|
|
106
|
+
uvx train-doctor setup --yes # applies it
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`setup` detects Claude Code, Codex and Cursor. It runs `claude mcp add --scope user train-doctor -- uvx train-doctor mcp`,
|
|
110
|
+
adds `[mcp_servers.train-doctor]` to `~/.codex/config.toml`, adds the server to `~/.cursor/mcp.json`, and copies the
|
|
111
|
+
skill to `~/.claude/skills/train-doctor/` and `~/.codex/skills/train-doctor/`. It backs up every file it edits and
|
|
112
|
+
does nothing when an entry is already there. `--project DIR` also writes a project `.mcp.json`.
|
|
113
|
+
|
|
114
|
+
MCP tools: `profile`, `diagnose`, `compare`, `report`, `version`. The skill
|
|
115
|
+
([skills/train-doctor/SKILL.md](skills/train-doctor/SKILL.md)) tells the agent the workflow: profile, read
|
|
116
|
+
the findings, change one thing, compare, keep only clear wins that don't move the loss, report.
|
|
117
|
+
|
|
118
|
+
CLI: `train-doctor profile|diagnose|compare|report|setup|mcp`, each with `--help` and `--json`.
|
|
119
|
+
|
|
120
|
+
## What it can't do
|
|
121
|
+
|
|
122
|
+
- It doesn't edit your code. The agent (or you) makes the change; train-doctor measures it.
|
|
123
|
+
- Timings come from the host side. On CUDA and MPS, time inside a sync call includes waiting for device
|
|
124
|
+
work that has to happen anyway, so the "host sync" share is an upper bound on what removing syncs saves.
|
|
125
|
+
- Apple GPU utilization from `ioreg` is system-wide, not per process. There is no per-kernel timing on MPS.
|
|
126
|
+
- `__getitem__` time is measured only with `num_workers=0`; with workers it runs in other processes.
|
|
127
|
+
- py-spy needs root on macOS, so `--py-spy` usually fails there. It was only tested with a fake py-spy.
|
|
128
|
+
- CUDA support (nvidia-smi parsing, CUDA sync and memory paths, CUDA-specific rules) is implemented and
|
|
129
|
+
tested with fakes. It has not been run on an NVIDIA GPU yet. The real runs in this repo are on Apple MPS.
|
|
130
|
+
- Multi-GPU: with DDP it reads the process that recorded the most steps (usually rank 0); it doesn't merge ranks.
|
|
131
|
+
- The loss check covers the first steps only. It catches changes that break or shift training early; it
|
|
132
|
+
can't tell you about final accuracy. Batch size and learning-rate changes are flagged as high risk for that reason.
|
|
133
|
+
- Short windows on a busy machine are noisy. Reports record the load average and system CPU per run; when
|
|
134
|
+
the spread is high, compare says so and is more likely to answer "no clear difference".
|
|
135
|
+
|
|
136
|
+
## Privacy and safety
|
|
137
|
+
|
|
138
|
+
Everything runs locally. train-doctor makes no network requests and sends nothing anywhere. It runs the
|
|
139
|
+
command you give it with your permissions, and writes to `.train-doctor/runs/` (or `--out`). Reports
|
|
140
|
+
replace your home directory with `~`. `setup` changes only the files it lists, and only after `--yes`.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Two small PyTorch training scripts with realistic slow spots, and the real train-doctor runs on them.
|
|
4
|
+
Every directory under `*/runs/` is untouched tool output: `report.md` for reading, `report.json` and
|
|
5
|
+
`compare.json` for the numbers, and the raw per-step events, resource samples, logs and profiler output.
|
|
6
|
+
|
|
7
|
+
Machine: Apple M4 MacBook (10 cores, 16 GB), macOS 26 (Darwin 25.2), Python 3.12.13, torch 2.14.1, device MPS.
|
|
8
|
+
Other jobs were running on the machine during these runs (1-minute load average 4.6 to 10.4 at the start
|
|
9
|
+
of each run); the per-repeat tables in the compare reports list the load and system CPU of every run.
|
|
10
|
+
|
|
11
|
+
Each command-line flag on the scripts stands in for one code edit, so a single change can be compared
|
|
12
|
+
on its own. [run_validation.sh](run_validation.sh) is the exact sequence of commands.
|
|
13
|
+
|
|
14
|
+
## cnn_images
|
|
15
|
+
|
|
16
|
+
A 3-layer CNN on 20,000 synthetic 32x32 images: per-sample crop, flip and normalize in `__getitem__`,
|
|
17
|
+
`num_workers=0`, and loss and accuracy read with `.item()` and printed every step.
|
|
18
|
+
|
|
19
|
+
| Run | What happened |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `profile-baseline` | 39.1% of step time waiting on the DataLoader (91% of that in `__getitem__`), 26.4% blocked in 2 `.item()` calls per step. Top finding: `dataloader_bound`. |
|
|
22
|
+
| `compare-workers-5` | `--num-workers 5`: 1.22x by medians but the interval is 0.66 to 1.68, spread up to 33%. Inconclusive, dropped. |
|
|
23
|
+
| `compare-batch-augment` | Next finding, `cpu_bound_preprocessing`: augmentation moved to one vectorized call per batch on the device. 1.28x (1.10 to 1.35), loss within tolerance (1.7% mean difference). Kept. |
|
|
24
|
+
| `profile-batch-augment` | Data wait down to 6.0%; host sync now 37.1% of step time. Top finding: `host_sync_in_loop`. |
|
|
25
|
+
| `compare-log-every-50` | Metrics kept on the device and read every 50 steps. 1.25x (1.11 to 1.35), loss identical. Kept. |
|
|
26
|
+
| `profile-log-every-50` | Host sync 0.2%, compute and Python 95.8%. Top finding: `no_mixed_precision`. |
|
|
27
|
+
| `compare-amp` | `--amp` (autocast float16 on MPS): 1.07x (0.81 to 1.28). Inconclusive, dropped. |
|
|
28
|
+
| `compare-original-vs-final` | Original vs `--batch-augment --log-every 50`: 1.62x (1.37 to 3.48), loss within tolerance. |
|
|
29
|
+
|
|
30
|
+
## tabular_mlp
|
|
31
|
+
|
|
32
|
+
A 4-layer MLP on 60,000 synthetic CSV-like rows, featurized in plain Python inside `__getitem__`
|
|
33
|
+
(parse, normalize, one-hot, hashed text buckets), with a `torch.save` checkpoint every 5 steps.
|
|
34
|
+
|
|
35
|
+
| Run | What happened |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `profile-baseline` | 52.8% of step time in `torch.save`, 28.2% waiting on the DataLoader. Top finding: `checkpoint_in_hot_loop`. |
|
|
38
|
+
| `compare-ckpt-every-200` | Checkpoint every 200 steps: 1.54x by medians, interval 0.62 to 2.67 (spread up to 35%). Inconclusive. |
|
|
39
|
+
| `compare-ckpt-every-200-9-repeats` | Same change with 9 repeats: 2.01x (1.54 to 2.52), loss identical. Kept. |
|
|
40
|
+
| `profile-ckpt-every-200` | Data wait 34.3% with `num_workers=0`. Top finding: `dataloader_bound`. |
|
|
41
|
+
| `compare-workers-5` | `--num-workers 5`: 1.93x (1.54 to 2.86), loss identical. Kept. The candidate needs 6.1 s more to reach its first step (spawned workers import torch); the report states that separately. |
|
|
42
|
+
| `profile-workers-5` | Data wait 7.3%; the window caught one checkpoint (8.8%). Next findings: mixed precision, torch.compile. Stopped here. |
|
|
43
|
+
| `compare-original-vs-final` | Original vs `--ckpt-every 200 --num-workers 5`: 3.05x (2.15 to 3.40), loss identical. |
|
|
44
|
+
|
|
45
|
+
Throughput medians differ between compares of the same command (for example the CNN baseline at 2,091
|
|
46
|
+
to 2,249 samples/s) because each compare ran at a different time on a shared machine. That is why every
|
|
47
|
+
decision comes from baseline and candidate runs interleaved in the same compare, never from numbers
|
|
48
|
+
taken at different times.
|
|
49
|
+
|
|
50
|
+
## Reproduce
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv venv --python 3.12 && uv pip install -e . torch numpy
|
|
54
|
+
PATH="$PWD/.venv/bin:$PATH" sh examples/run_validation.sh
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Your numbers will differ on a different machine or under different load, and so can the decisions.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Small CNN on a synthetic 10-class image dataset.
|
|
2
|
+
|
|
3
|
+
Written the way a lot of first training scripts are: per-sample augmentation
|
|
4
|
+
in __getitem__, num_workers=0, and the loss and accuracy read back with
|
|
5
|
+
.item() and printed on every step.
|
|
6
|
+
|
|
7
|
+
Each command-line flag stands in for one code edit, so train-doctor can
|
|
8
|
+
compare a single change at a time:
|
|
9
|
+
|
|
10
|
+
--num-workers N DataLoader workers (default 0)
|
|
11
|
+
--log-every N read metrics and print every N steps (default 1);
|
|
12
|
+
between logs, metrics stay on the device
|
|
13
|
+
--amp autocast to float16 on mps/cuda
|
|
14
|
+
--batch-augment do the crop, flip and normalize once per batch on the
|
|
15
|
+
device instead of per sample in __getitem__
|
|
16
|
+
--batch-size N default 64
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
import torch.nn as nn
|
|
24
|
+
import torch.nn.functional as F
|
|
25
|
+
from torch.utils.data import DataLoader, Dataset
|
|
26
|
+
|
|
27
|
+
MEAN = torch.tensor([0.5, 0.5, 0.5]).view(3, 1, 1)
|
|
28
|
+
STD = torch.tensor([0.25, 0.25, 0.25]).view(3, 1, 1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SyntheticImages(Dataset):
|
|
32
|
+
"""10 classes told apart by a faint colored square in noise. Stored as uint8 like decoded JPEGs."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, n: int = 20000, seed: int = 0, augment: bool = True):
|
|
35
|
+
self.augment = augment
|
|
36
|
+
g = torch.Generator().manual_seed(seed)
|
|
37
|
+
self.labels = torch.randint(0, 10, (n,), generator=g)
|
|
38
|
+
base = torch.randint(0, 256, (n, 3, 32, 32), generator=g, dtype=torch.uint8)
|
|
39
|
+
# a faint class-colored square blended into the noise, plus 10% label noise
|
|
40
|
+
colors = torch.linspace(0, 255, 10)
|
|
41
|
+
for c in range(10):
|
|
42
|
+
idx = (self.labels == c).nonzero().squeeze(1)
|
|
43
|
+
patch = base[idx, c % 3, 8:24, 8:24].float()
|
|
44
|
+
base[idx, c % 3, 8:24, 8:24] = (0.75 * patch + 0.25 * colors[c]).to(torch.uint8)
|
|
45
|
+
flip = torch.rand(n, generator=g) < 0.1
|
|
46
|
+
self.labels[flip] = torch.randint(0, 10, (int(flip.sum()),), generator=g)
|
|
47
|
+
self.images = base
|
|
48
|
+
|
|
49
|
+
def __len__(self):
|
|
50
|
+
return len(self.labels)
|
|
51
|
+
|
|
52
|
+
def __getitem__(self, i):
|
|
53
|
+
if not self.augment:
|
|
54
|
+
return self.images[i], self.labels[i]
|
|
55
|
+
img = self.images[i].float() / 255.0
|
|
56
|
+
# random crop with 4 px padding
|
|
57
|
+
img = F.pad(img, (4, 4, 4, 4), mode="reflect")
|
|
58
|
+
x, y = torch.randint(0, 9, (2,)).tolist()
|
|
59
|
+
img = img[:, y : y + 32, x : x + 32]
|
|
60
|
+
if torch.rand(1).item() < 0.5:
|
|
61
|
+
img = torch.flip(img, dims=[2])
|
|
62
|
+
img = (img - MEAN) / STD
|
|
63
|
+
return img, self.labels[i]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def augment_batch(x: torch.Tensor) -> torch.Tensor:
|
|
67
|
+
"""Same augmentation as SyntheticImages.__getitem__, vectorized over a uint8 batch on the device."""
|
|
68
|
+
b = x.shape[0]
|
|
69
|
+
x = F.pad(x.float() / 255.0, (4, 4, 4, 4), mode="reflect")
|
|
70
|
+
oy = torch.randint(0, 9, (b,), device=x.device)
|
|
71
|
+
ox = torch.randint(0, 9, (b,), device=x.device)
|
|
72
|
+
ar = torch.arange(32, device=x.device)
|
|
73
|
+
rows = (oy[:, None] + ar)[:, None, :, None].expand(b, 3, 32, 40)
|
|
74
|
+
x = torch.gather(x, 2, rows)
|
|
75
|
+
cols = (ox[:, None] + ar)[:, None, None, :].expand(b, 3, 32, 32)
|
|
76
|
+
x = torch.gather(x, 3, cols)
|
|
77
|
+
flip = torch.rand(b, device=x.device) < 0.5
|
|
78
|
+
x = torch.where(flip[:, None, None, None], x.flip(3), x)
|
|
79
|
+
return (x - MEAN.to(x.device)) / STD.to(x.device)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Net(nn.Module):
|
|
83
|
+
def __init__(self):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.c1 = nn.Conv2d(3, 32, 3, padding=1)
|
|
86
|
+
self.c2 = nn.Conv2d(32, 64, 3, padding=1)
|
|
87
|
+
self.c3 = nn.Conv2d(64, 128, 3, padding=1)
|
|
88
|
+
self.bn1 = nn.BatchNorm2d(32)
|
|
89
|
+
self.bn2 = nn.BatchNorm2d(64)
|
|
90
|
+
self.bn3 = nn.BatchNorm2d(128)
|
|
91
|
+
self.fc = nn.Linear(128 * 4 * 4, 10)
|
|
92
|
+
|
|
93
|
+
def forward(self, x):
|
|
94
|
+
x = F.max_pool2d(F.relu(self.bn1(self.c1(x))), 2)
|
|
95
|
+
x = F.max_pool2d(F.relu(self.bn2(self.c2(x))), 2)
|
|
96
|
+
x = F.max_pool2d(F.relu(self.bn3(self.c3(x))), 2)
|
|
97
|
+
return self.fc(x.flatten(1))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def pick_device(name: str) -> torch.device:
|
|
101
|
+
if name != "auto":
|
|
102
|
+
return torch.device(name)
|
|
103
|
+
if torch.cuda.is_available():
|
|
104
|
+
return torch.device("cuda")
|
|
105
|
+
if torch.backends.mps.is_available():
|
|
106
|
+
return torch.device("mps")
|
|
107
|
+
return torch.device("cpu")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main():
|
|
111
|
+
ap = argparse.ArgumentParser()
|
|
112
|
+
ap.add_argument("--device", default="auto")
|
|
113
|
+
ap.add_argument("--batch-size", type=int, default=64)
|
|
114
|
+
ap.add_argument("--num-workers", type=int, default=0)
|
|
115
|
+
ap.add_argument("--log-every", type=int, default=1)
|
|
116
|
+
ap.add_argument("--amp", action="store_true")
|
|
117
|
+
ap.add_argument("--batch-augment", action="store_true")
|
|
118
|
+
ap.add_argument("--max-steps", type=int, default=300)
|
|
119
|
+
ap.add_argument("--lr", type=float, default=0.002)
|
|
120
|
+
args = ap.parse_args()
|
|
121
|
+
|
|
122
|
+
torch.manual_seed(0)
|
|
123
|
+
device = pick_device(args.device)
|
|
124
|
+
ds = SyntheticImages(augment=not args.batch_augment)
|
|
125
|
+
loader = DataLoader(
|
|
126
|
+
ds,
|
|
127
|
+
batch_size=args.batch_size,
|
|
128
|
+
shuffle=True,
|
|
129
|
+
num_workers=args.num_workers,
|
|
130
|
+
persistent_workers=args.num_workers > 0,
|
|
131
|
+
drop_last=True,
|
|
132
|
+
)
|
|
133
|
+
model = Net().to(device)
|
|
134
|
+
opt = torch.optim.SGD(model.parameters(), lr=args.lr, momentum=0.9)
|
|
135
|
+
|
|
136
|
+
step = 0
|
|
137
|
+
run_loss = torch.zeros((), device=device)
|
|
138
|
+
run_correct = torch.zeros((), device=device)
|
|
139
|
+
t0 = time.time()
|
|
140
|
+
while step < args.max_steps:
|
|
141
|
+
for xb, yb in loader:
|
|
142
|
+
xb, yb = xb.to(device), yb.to(device)
|
|
143
|
+
if args.batch_augment:
|
|
144
|
+
xb = augment_batch(xb)
|
|
145
|
+
with torch.autocast(device.type, dtype=torch.float16, enabled=args.amp):
|
|
146
|
+
out = model(xb)
|
|
147
|
+
loss = F.cross_entropy(out, yb)
|
|
148
|
+
opt.zero_grad(set_to_none=True)
|
|
149
|
+
loss.backward()
|
|
150
|
+
opt.step()
|
|
151
|
+
step += 1
|
|
152
|
+
run_loss += loss.detach()
|
|
153
|
+
run_correct += (out.argmax(1) == yb).sum()
|
|
154
|
+
if step % args.log_every == 0:
|
|
155
|
+
n = args.log_every * args.batch_size
|
|
156
|
+
print(f"step {step} loss {run_loss.item() / args.log_every:.4f} acc {run_correct.item() / n:.3f}")
|
|
157
|
+
run_loss.zero_()
|
|
158
|
+
run_correct.zero_()
|
|
159
|
+
if step >= args.max_steps:
|
|
160
|
+
break
|
|
161
|
+
print(f"done: {step} steps in {time.time() - t0:.1f} s")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The exact sequence that produced examples/*/runs, in order.
|
|
3
|
+
# Run from the repo root with a venv that has torch and train-doctor installed:
|
|
4
|
+
# uv venv --python 3.12 && uv pip install -e . torch numpy
|
|
5
|
+
# PATH="$PWD/.venv/bin:$PATH" sh examples/run_validation.sh
|
|
6
|
+
# Each flag on the training scripts stands in for one code edit. Which edit
|
|
7
|
+
# came next was decided from the previous profile's top finding and the
|
|
8
|
+
# previous compare's decision (see examples/README.md).
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
CNN="python examples/cnn_images/train.py"
|
|
12
|
+
OUT=examples/cnn_images/runs
|
|
13
|
+
train-doctor profile --out $OUT --label baseline -- $CNN
|
|
14
|
+
train-doctor compare --out $OUT --label workers-5 --baseline "$CNN" --candidate "$CNN --num-workers 5"
|
|
15
|
+
train-doctor compare --seconds 4 --out $OUT --label batch-augment --baseline "$CNN" --candidate "$CNN --batch-augment"
|
|
16
|
+
train-doctor profile --out $OUT --label batch-augment -- $CNN --batch-augment
|
|
17
|
+
train-doctor compare --seconds 4 --out $OUT --label log-every-50 --baseline "$CNN --batch-augment" --candidate "$CNN --batch-augment --log-every 50"
|
|
18
|
+
train-doctor profile --out $OUT --label log-every-50 -- $CNN --batch-augment --log-every 50
|
|
19
|
+
train-doctor compare --seconds 4 --out $OUT --label amp --baseline "$CNN --batch-augment --log-every 50" --candidate "$CNN --batch-augment --log-every 50 --amp"
|
|
20
|
+
train-doctor compare --seconds 4 --out $OUT --label original-vs-final --baseline "$CNN" --candidate "$CNN --batch-augment --log-every 50"
|
|
21
|
+
|
|
22
|
+
MLP="python examples/tabular_mlp/train.py"
|
|
23
|
+
OUT=examples/tabular_mlp/runs
|
|
24
|
+
train-doctor profile --out $OUT --label baseline -- $MLP
|
|
25
|
+
train-doctor compare --seconds 4 --out $OUT --label ckpt-every-200 --baseline "$MLP" --candidate "$MLP --ckpt-every 200"
|
|
26
|
+
train-doctor compare --seconds 4 --repeats 9 --out $OUT --label ckpt-every-200-9-repeats --baseline "$MLP" --candidate "$MLP --ckpt-every 200"
|
|
27
|
+
train-doctor profile --out $OUT --label ckpt-every-200 -- $MLP --ckpt-every 200
|
|
28
|
+
train-doctor compare --seconds 4 --out $OUT --label workers-5 --baseline "$MLP --ckpt-every 200" --candidate "$MLP --ckpt-every 200 --num-workers 5"
|
|
29
|
+
train-doctor profile --out $OUT --label workers-5 -- $MLP --ckpt-every 200 --num-workers 5
|
|
30
|
+
train-doctor compare --seconds 4 --out $OUT --label original-vs-final --baseline "$MLP" --candidate "$MLP --ckpt-every 200 --num-workers 5"
|