trainproof 0.3.0__tar.gz → 0.5.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.
Files changed (31) hide show
  1. {trainproof-0.3.0/src/trainproof.egg-info → trainproof-0.5.0}/PKG-INFO +95 -6
  2. trainproof-0.3.0/PKG-INFO → trainproof-0.5.0/README.md +230 -153
  3. {trainproof-0.3.0 → trainproof-0.5.0}/pyproject.toml +1 -1
  4. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/cli.py +35 -0
  5. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/epoch.py +8 -3
  6. trainproof-0.5.0/src/trainproof/integrations/__init__.py +3 -0
  7. trainproof-0.5.0/src/trainproof/integrations/hf.py +89 -0
  8. trainproof-0.5.0/src/trainproof/preflight.py +149 -0
  9. trainproof-0.5.0/src/trainproof/watch.py +57 -0
  10. trainproof-0.3.0/README.md → trainproof-0.5.0/src/trainproof.egg-info/PKG-INFO +242 -141
  11. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof.egg-info/SOURCES.txt +8 -1
  12. trainproof-0.5.0/tests/test_hf_callback.py +66 -0
  13. trainproof-0.5.0/tests/test_preflight.py +102 -0
  14. trainproof-0.5.0/tests/test_watch.py +42 -0
  15. {trainproof-0.3.0 → trainproof-0.5.0}/setup.cfg +0 -0
  16. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/__init__.py +0 -0
  17. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/adapters.py +0 -0
  18. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/compare.py +0 -0
  19. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/report.py +0 -0
  20. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/rules.py +0 -0
  21. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/speech/__init__.py +0 -0
  22. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/speech/data.py +0 -0
  23. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof/speech/tokenizer.py +0 -0
  24. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof.egg-info/dependency_links.txt +0 -0
  25. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof.egg-info/entry_points.txt +0 -0
  26. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof.egg-info/requires.txt +0 -0
  27. {trainproof-0.3.0 → trainproof-0.5.0}/src/trainproof.egg-info/top_level.txt +0 -0
  28. {trainproof-0.3.0 → trainproof-0.5.0}/tests/test_adapters.py +0 -0
  29. {trainproof-0.3.0 → trainproof-0.5.0}/tests/test_compare.py +0 -0
  30. {trainproof-0.3.0 → trainproof-0.5.0}/tests/test_data.py +0 -0
  31. {trainproof-0.3.0 → trainproof-0.5.0}/tests/test_epoch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trainproof
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: A deterministic linter for ML training runs: dataset, tokenizer, and epoch logs.
5
5
  Author-email: "Panagiotis (Panos) Gkilis" <bedvibe@bedvibe.studio>
6
6
  License: MIT
@@ -24,6 +24,16 @@ No ML judging ML. No invented "confidence 97%". Every rule is a deterministic
24
24
  threshold in [one auditable module](src/trainproof/rules.py), and every finding
25
25
  cites the numbers that triggered it.
26
26
 
27
+ **What it is:** the reliability layer between your training code and the GPU
28
+ bill — checks that run *before* training (is this run safe to start?), *during*
29
+ training (should it keep going?), and *after* (is it reproducible; did it match
30
+ a known-good baseline?).
31
+
32
+ **The one rule it never breaks:** trainproof does not infer causes, does not
33
+ invent confidence scores, and does not guess. It reports deterministic findings
34
+ backed by evidence, or it stays silent. Every feature earns its place by
35
+ answering a single question — *if it were gone, would someone lose GPU hours?*
36
+
27
37
  ```bash
28
38
  pip install trainproof
29
39
  ```
@@ -79,12 +89,18 @@ famously fit random labels). **No single-run, loss-only rule can catch this
79
89
  class of failure** — its real signature is *relative*: a loss floor ~6x higher
80
90
  than a known-good run of the same task (5.59 vs 0.94).
81
91
 
82
- That finding sets the roadmap: v0.3 is `trainproof compare <run> <baseline>` —
83
- deterministic ratio rules against the healthy baseline you already have.
84
- See [ROADMAP.md](ROADMAP.md). (The gallery also improved v0.2 itself: the
85
- dead-run rule exists because `lr_zero` initially escaped with only a WARN.)
92
+ That finding produced v0.3: `trainproof compare <run> <baseline>` —
93
+ deterministic ratio rules against the healthy baseline you already have
94
+ which catches `bad_labels` at a 6x loss-floor ratio, in 3 seeds out of 3.
95
+ The full study was repeated with three random seeds (15 runs):
96
+ see [EVIDENCE_MATRIX.md](EVIDENCE_MATRIX.md) for every verdict, including the
97
+ honest miss (compare alone overlooks one lr_zero seed — the single-run
98
+ zero-LR fatality rule owns that case; the two commands cover each other's
99
+ blind spots). The gallery also improved the tool itself twice: the dead-run
100
+ rule and the total-zero-LR fatality rule both exist because runs escaped
101
+ earlier rule versions. See [ROADMAP.md](ROADMAP.md).
86
102
 
87
- ## The three commands
103
+ ## The commands
88
104
 
89
105
  ```bash
90
106
  # 1. Dataset preflight (speech/TTS pack): audio integrity, transcript quality,
@@ -119,6 +135,79 @@ TRAINPROOF VERDICT
119
135
  Each command prints the verdict, writes a self-contained HTML report, and sets
120
136
  the process exit code — so it works as a CI gate out of the box.
121
137
 
138
+ ## Live guardian (v0.4)
139
+
140
+ Don't wait for the post-mortem — catch a doomed run *while it is still burning
141
+ GPU*. Add one line to a HuggingFace `Trainer`:
142
+
143
+ ```python
144
+ from transformers import Trainer
145
+ from trainproof.integrations.hf import TrainproofCallback
146
+
147
+ trainer = Trainer(
148
+ ...,
149
+ callbacks=[TrainproofCallback(policy="stop_on_fail")], # or policy="warn"
150
+ )
151
+ ```
152
+
153
+ Run against a real diverging QLoRA fine-tune (learning rate 100x too high), the
154
+ guardian aborts it 20 steps into a 300-step schedule — on its own:
155
+
156
+ ```text
157
+ {'loss': '1.784', 'grad_norm': '9.634', 'learning_rate': '0.007'}
158
+ {'loss': '4.282', 'grad_norm': '53.76', 'learning_rate': '0.009'}
159
+ {'loss': '10.6', 'grad_norm': '13.34', 'learning_rate': '0.011'}
160
+ {'loss': '31.67', 'grad_norm': '76.67', 'learning_rate': '0.013'}
161
+ ...
162
+ TRAINPROOF ABORT - stopping training at step 20. Findings:
163
+ [FAIL] Loss curve is diverging.
164
+ Evidence: End loss 22.952 vs Min loss 1.358
165
+ [FAIL] Loss never improved over the run (dead run).
166
+ Evidence: median of first 5 losses 1.502 vs last 5 22.952
167
+
168
+ scheduled steps : 300
169
+ stopped at step : 20
170
+ run saved : 93% of the scheduled steps never ran
171
+ ```
172
+
173
+ On a two-day pre-training run, that fraction is days of GPU time. Or watch a
174
+ growing log file from outside the process (CI-friendly, exits non-zero on FAIL):
175
+
176
+ ```bash
177
+ trainproof watch logs/run.jsonl --interval 10 --until-fail
178
+ # [21:37:44] warming up (5 records)
179
+ # [21:37:44] n_records=15 verdict=PASS findings=1
180
+ ```
181
+
182
+ **The default is safe.** `policy="warn"` (the default) only observes and reports
183
+ — it never interrupts your run, so you can leave it on even for experiments you
184
+ expect to fail. Aborting is strictly opt-in via `policy="stop_on_fail"`, the one
185
+ mode that takes an irreversible action. trainproof does not make that decision
186
+ for you unless you ask.
187
+
188
+ The guardian applies the same deterministic rules as `trainproof epoch`, so it
189
+ inherits their documented single-run limitations.
190
+
191
+ ## Pre-flight (v0.5): stop a run before it starts
192
+
193
+ The guardian saves most of a doomed run; preflight saves 100% because it never starts. Catch broken datasets and tokenizer misconfigurations instantly.
194
+
195
+ ```bash
196
+ trainproof preflight data/dataset.jsonl --tokenizer mistralai/Mistral-7B-v0.1 --max-len 4096
197
+ ```
198
+
199
+ ```text
200
+ ========================================
201
+ TRAINPROOF VERDICT
202
+ ========================================
203
+ [FAIL] Critical checks failed:
204
+ [FAIL] Empty or whitespace-only text found.
205
+ Evidence: 1 records (indices [1]...)
206
+ ========================================
207
+ ```
208
+
209
+ *Checks: malformed JSONL, empty text, exact duplicate text, tokenizer structural checks (EOS/PAD/BOS), and context length overflows.*
210
+
122
211
  ## Supported log formats
123
212
 
124
213
  - HuggingFace Trainer (`trainer_state.json`)
@@ -1,153 +1,230 @@
1
- Metadata-Version: 2.4
2
- Name: trainproof
3
- Version: 0.3.0
4
- Summary: A deterministic linter for ML training runs: dataset, tokenizer, and epoch logs.
5
- Author-email: "Panagiotis (Panos) Gkilis" <bedvibe@bedvibe.studio>
6
- License: MIT
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: numpy
10
- Requires-Dist: soundfile
11
- Requires-Dist: ttsproof
12
-
13
- # trainproof
14
-
15
- [![PyPI](https://img.shields.io/pypi/v/trainproof)](https://pypi.org/project/trainproof/)
16
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
17
-
18
- **A deterministic linter for ML training runs.** Point it at your dataset, your
19
- tokenizer, or your first-epoch logs — it returns a PASS / WARN / FAIL verdict
20
- with named findings and cited evidence, before you burn days of GPU time on a
21
- run that was doomed at step 50.
22
-
23
- No ML judging ML. No invented "confidence 97%". Every rule is a deterministic
24
- threshold in [one auditable module](src/trainproof/rules.py), and every finding
25
- cites the numbers that triggered it.
26
-
27
- ```bash
28
- pip install trainproof
29
- ```
30
-
31
- ## See a verdict in 60 seconds
32
-
33
- This repo ships the real logs of five QLoRA fine-tuning runs (Qwen2.5-3B,
34
- RTX 5080 — see the gallery below). Judge one right now:
35
-
36
- ```bash
37
- trainproof epoch examples/gallery/lr_hot/trainer_state.json --format hf
38
- ```
39
-
40
- ```text
41
- [FAIL] Critical checks failed:
42
- [FAIL] Loss curve is diverging.
43
- Evidence: End loss 7.492 vs Min loss 1.398
44
- [WARN] Gradient norm spikes detected.
45
- Evidence: Max gn 2649.75 > 10.0x median (0.55)
46
- ```
47
-
48
- ## Why this exists
49
-
50
- The author lost a real 11-hour fine-tune to a failure nothing warned about.
51
- Pointed retroactively at that run's 1MB Coqui Trainer log (2,501 logged steps),
52
- trainproof's verdict: **FAIL diverging**. The loss reached its minimum at 82%
53
- of the run and ended 1.9x above it. Translation: the final two hours of GPU
54
- time made the model measurably worse, and the checkpoint worth keeping had
55
- already existed for hours. No tool in the stack said a word.
56
-
57
- ## The fault-injection gallery
58
-
59
- To validate the rules, the same QLoRA fine-tune (Qwen2.5-3B-Instruct, 4-bit,
60
- LoRA r=16, 300 steps on Alpaca-cleaned) was run five times — once healthy, four
61
- times with exactly one knob deliberately broken. Real runs, real logs, all
62
- shipped in [`examples/gallery/`](examples/gallery/):
63
-
64
- | Run | Sabotage | Verdict | Key evidence |
65
- |---|---|---|---|
66
- | `healthy` | none | **PASS** | loss 1.52 0.94, stable gradients |
67
- | `lr_hot` | LR x100 (2e-2) | **FAIL** | diverging: end 7.49 vs min 1.40; grad spike 2650 vs median 0.55 |
68
- | `lr_zero` | LR = 0 | **FAIL** | dead run: first-5 median 1.52 vs last-5 1.49 (<5% improvement); lr=0 on 100% of steps |
69
- | `fp16_nan` | fp16 + hot LR, no clipping | **FAIL** | diverging: end 7.21 vs min 1.09 (grad scaling absorbed the intended NaN — the run diverged instead; reported as observed) |
70
- | `bad_labels` | labels shuffled per-sequence | **WARN only** (single-run) — caught by `trainproof compare` (v0.3) | grad spike 23.3 vs median 1.09 |
71
-
72
- ### The honest finding: loss curves cannot see corrupted data
73
-
74
- The `bad_labels` run whose shuffled labels make real learning impossible
75
- *reduced its loss by 62%* (18.9 5.75). The model was genuinely learning: not
76
- the task, but the marginal token statistics of the garbage. From its own loss
77
- curve, that is indistinguishable from healthy training (neural networks
78
- famously fit random labels). **No single-run, loss-only rule can catch this
79
- class of failure** — its real signature is *relative*: a loss floor ~6x higher
80
- than a known-good run of the same task (5.59 vs 0.94).
81
-
82
- That finding sets the roadmap: v0.3 is `trainproof compare <run> <baseline>`
83
- deterministic ratio rules against the healthy baseline you already have.
84
- See [ROADMAP.md](ROADMAP.md). (The gallery also improved v0.2 itself: the
85
- dead-run rule exists because `lr_zero` initially escaped with only a WARN.)
86
-
87
- ## The three commands
88
-
89
- ```bash
90
- # 1. Dataset preflight (speech/TTS pack): audio integrity, transcript quality,
91
- # duplicates, text-vs-audio duration mismatches
92
- trainproof data /path/to/dataset_or_manifest.jsonl
93
-
94
- # 2. Tokenizer preflight: vocabulary coverage, OOV rate, sequence blowouts,
95
- # suspicious splits on numbers/dates
96
- trainproof tokenizer my_tokenizer.model transcripts.txt
97
-
98
- # 3. Training-run verdict: NaN/divergence/dead-run detection, gradient spikes,
99
- # LR sanity, throughput — from log files, any framework
100
- trainproof epoch logs/run.jsonl # exit code 1 on FAIL: CI-ready
101
-
102
- # 4. Compare against a baseline
103
- # Catch relative pathologies like the `bad_labels` run that evade single-run rules.
104
- trainproof compare examples/gallery/bad_labels/trainer_state.json examples/gallery/healthy/trainer_state.json
105
- ```
106
-
107
- ```text
108
- ========================================
109
- TRAINPROOF VERDICT
110
- ========================================
111
- [FAIL] Critical checks failed:
112
- [FAIL] loss floor ratio exceeded limit
113
- Evidence: Run floor 5.592 vs Baseline floor 0.937 (ratio 6.0x > 2.0)
114
- [FAIL] end loss ratio exceeded limit
115
- Evidence: Run end 5.750 vs Baseline end 1.082 (ratio 5.3x > 2.0)
116
- ========================================
117
- ```
118
-
119
- Each command prints the verdict, writes a self-contained HTML report, and sets
120
- the process exit code — so it works as a CI gate out of the box.
121
-
122
- ## Supported log formats
123
-
124
- - HuggingFace Trainer (`trainer_state.json`)
125
- - Coqui Trainer text logs (ANSI-colored `trainer_0_log.txt`)
126
- - Generic JSONL / CSV (columns: step, loss, lr, grad_norm, time — all optional)
127
-
128
- Auto-detected; override with `--format hf|coqui|jsonl|csv`. TensorBoard event
129
- files are planned (Lightning console captures are TTY dumps, not logs, and
130
- will not be supported).
131
-
132
- ## Philosophy
133
-
134
- 1. **Deterministic.** A rule fires or it doesn't. Thresholds live in one
135
- module, commented, tunable.
136
- 2. **Evidence-cited.** Every finding names the steps and values that triggered
137
- it.
138
- 3. **Honest about limits.** What the tool cannot detect is documented in the
139
- README, not discovered by the user in production.
140
-
141
- ## Family
142
-
143
- trainproof judges training runs. Its sibling [ttsproof](https://github.com/Mormolykos/ttsproof)
144
- judges TTS model *outputs* (structural audio checks, equivalence-aware WER/CER,
145
- published method with DOI) — and trainproof builds on it for the speech
146
- dataset checks.
147
-
148
- ## Author
149
-
150
- Panagiotis (Panos) Gkilis [portfolio](https://tts.bedvibe.studio/portfolio/) ·
151
- [bedvibe.studio](https://bedvibe.studio/)
152
-
153
- MIT license.
1
+ # trainproof
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/trainproof)](https://pypi.org/project/trainproof/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+
6
+ **A deterministic linter for ML training runs.** Point it at your dataset, your
7
+ tokenizer, or your first-epoch logs — it returns a PASS / WARN / FAIL verdict
8
+ with named findings and cited evidence, before you burn days of GPU time on a
9
+ run that was doomed at step 50.
10
+
11
+ No ML judging ML. No invented "confidence 97%". Every rule is a deterministic
12
+ threshold in [one auditable module](src/trainproof/rules.py), and every finding
13
+ cites the numbers that triggered it.
14
+
15
+ **What it is:** the reliability layer between your training code and the GPU
16
+ bill — checks that run *before* training (is this run safe to start?), *during*
17
+ training (should it keep going?), and *after* (is it reproducible; did it match
18
+ a known-good baseline?).
19
+
20
+ **The one rule it never breaks:** trainproof does not infer causes, does not
21
+ invent confidence scores, and does not guess. It reports deterministic findings
22
+ backed by evidence, or it stays silent. Every feature earns its place by
23
+ answering a single question *if it were gone, would someone lose GPU hours?*
24
+
25
+ ```bash
26
+ pip install trainproof
27
+ ```
28
+
29
+ ## See a verdict in 60 seconds
30
+
31
+ This repo ships the real logs of five QLoRA fine-tuning runs (Qwen2.5-3B,
32
+ RTX 5080 — see the gallery below). Judge one right now:
33
+
34
+ ```bash
35
+ trainproof epoch examples/gallery/lr_hot/trainer_state.json --format hf
36
+ ```
37
+
38
+ ```text
39
+ [FAIL] Critical checks failed:
40
+ [FAIL] Loss curve is diverging.
41
+ Evidence: End loss 7.492 vs Min loss 1.398
42
+ [WARN] Gradient norm spikes detected.
43
+ Evidence: Max gn 2649.75 > 10.0x median (0.55)
44
+ ```
45
+
46
+ ## Why this exists
47
+
48
+ The author lost a real 11-hour fine-tune to a failure nothing warned about.
49
+ Pointed retroactively at that run's 1MB Coqui Trainer log (2,501 logged steps),
50
+ trainproof's verdict: **FAIL diverging**. The loss reached its minimum at 82%
51
+ of the run and ended 1.9x above it. Translation: the final two hours of GPU
52
+ time made the model measurably worse, and the checkpoint worth keeping had
53
+ already existed for hours. No tool in the stack said a word.
54
+
55
+ ## The fault-injection gallery
56
+
57
+ To validate the rules, the same QLoRA fine-tune (Qwen2.5-3B-Instruct, 4-bit,
58
+ LoRA r=16, 300 steps on Alpaca-cleaned) was run five times — once healthy, four
59
+ times with exactly one knob deliberately broken. Real runs, real logs, all
60
+ shipped in [`examples/gallery/`](examples/gallery/):
61
+
62
+ | Run | Sabotage | Verdict | Key evidence |
63
+ |---|---|---|---|
64
+ | `healthy` | none | **PASS** | loss 1.52 → 0.94, stable gradients |
65
+ | `lr_hot` | LR x100 (2e-2) | **FAIL** | diverging: end 7.49 vs min 1.40; grad spike 2650 vs median 0.55 |
66
+ | `lr_zero` | LR = 0 | **FAIL** | dead run: first-5 median 1.52 vs last-5 1.49 (<5% improvement); lr=0 on 100% of steps |
67
+ | `fp16_nan` | fp16 + hot LR, no clipping | **FAIL** | diverging: end 7.21 vs min 1.09 (grad scaling absorbed the intended NaN — the run diverged instead; reported as observed) |
68
+ | `bad_labels` | labels shuffled per-sequence | **WARN only** (single-run) caught by `trainproof compare` (v0.3) | grad spike 23.3 vs median 1.09 |
69
+
70
+ ### The honest finding: loss curves cannot see corrupted data
71
+
72
+ The `bad_labels` run whose shuffled labels make real learning impossible —
73
+ *reduced its loss by 62%* (18.9 → 5.75). The model was genuinely learning: not
74
+ the task, but the marginal token statistics of the garbage. From its own loss
75
+ curve, that is indistinguishable from healthy training (neural networks
76
+ famously fit random labels). **No single-run, loss-only rule can catch this
77
+ class of failure** — its real signature is *relative*: a loss floor ~6x higher
78
+ than a known-good run of the same task (5.59 vs 0.94).
79
+
80
+ That finding produced v0.3: `trainproof compare <run> <baseline>`
81
+ deterministic ratio rules against the healthy baseline you already have —
82
+ which catches `bad_labels` at a 6x loss-floor ratio, in 3 seeds out of 3.
83
+ The full study was repeated with three random seeds (15 runs):
84
+ see [EVIDENCE_MATRIX.md](EVIDENCE_MATRIX.md) for every verdict, including the
85
+ honest miss (compare alone overlooks one lr_zero seed the single-run
86
+ zero-LR fatality rule owns that case; the two commands cover each other's
87
+ blind spots). The gallery also improved the tool itself twice: the dead-run
88
+ rule and the total-zero-LR fatality rule both exist because runs escaped
89
+ earlier rule versions. See [ROADMAP.md](ROADMAP.md).
90
+
91
+ ## The commands
92
+
93
+ ```bash
94
+ # 1. Dataset preflight (speech/TTS pack): audio integrity, transcript quality,
95
+ # duplicates, text-vs-audio duration mismatches
96
+ trainproof data /path/to/dataset_or_manifest.jsonl
97
+
98
+ # 2. Tokenizer preflight: vocabulary coverage, OOV rate, sequence blowouts,
99
+ # suspicious splits on numbers/dates
100
+ trainproof tokenizer my_tokenizer.model transcripts.txt
101
+
102
+ # 3. Training-run verdict: NaN/divergence/dead-run detection, gradient spikes,
103
+ # LR sanity, throughput from log files, any framework
104
+ trainproof epoch logs/run.jsonl # exit code 1 on FAIL: CI-ready
105
+
106
+ # 4. Compare against a baseline
107
+ # Catch relative pathologies like the `bad_labels` run that evade single-run rules.
108
+ trainproof compare examples/gallery/bad_labels/trainer_state.json examples/gallery/healthy/trainer_state.json
109
+ ```
110
+
111
+ ```text
112
+ ========================================
113
+ TRAINPROOF VERDICT
114
+ ========================================
115
+ [FAIL] Critical checks failed:
116
+ [FAIL] loss floor ratio exceeded limit
117
+ Evidence: Run floor 5.592 vs Baseline floor 0.937 (ratio 6.0x > 2.0)
118
+ [FAIL] end loss ratio exceeded limit
119
+ Evidence: Run end 5.750 vs Baseline end 1.082 (ratio 5.3x > 2.0)
120
+ ========================================
121
+ ```
122
+
123
+ Each command prints the verdict, writes a self-contained HTML report, and sets
124
+ the process exit code — so it works as a CI gate out of the box.
125
+
126
+ ## Live guardian (v0.4)
127
+
128
+ Don't wait for the post-mortem catch a doomed run *while it is still burning
129
+ GPU*. Add one line to a HuggingFace `Trainer`:
130
+
131
+ ```python
132
+ from transformers import Trainer
133
+ from trainproof.integrations.hf import TrainproofCallback
134
+
135
+ trainer = Trainer(
136
+ ...,
137
+ callbacks=[TrainproofCallback(policy="stop_on_fail")], # or policy="warn"
138
+ )
139
+ ```
140
+
141
+ Run against a real diverging QLoRA fine-tune (learning rate 100x too high), the
142
+ guardian aborts it 20 steps into a 300-step schedule — on its own:
143
+
144
+ ```text
145
+ {'loss': '1.784', 'grad_norm': '9.634', 'learning_rate': '0.007'}
146
+ {'loss': '4.282', 'grad_norm': '53.76', 'learning_rate': '0.009'}
147
+ {'loss': '10.6', 'grad_norm': '13.34', 'learning_rate': '0.011'}
148
+ {'loss': '31.67', 'grad_norm': '76.67', 'learning_rate': '0.013'}
149
+ ...
150
+ TRAINPROOF ABORT - stopping training at step 20. Findings:
151
+ [FAIL] Loss curve is diverging.
152
+ Evidence: End loss 22.952 vs Min loss 1.358
153
+ [FAIL] Loss never improved over the run (dead run).
154
+ Evidence: median of first 5 losses 1.502 vs last 5 22.952
155
+
156
+ scheduled steps : 300
157
+ stopped at step : 20
158
+ run saved : 93% of the scheduled steps never ran
159
+ ```
160
+
161
+ On a two-day pre-training run, that fraction is days of GPU time. Or watch a
162
+ growing log file from outside the process (CI-friendly, exits non-zero on FAIL):
163
+
164
+ ```bash
165
+ trainproof watch logs/run.jsonl --interval 10 --until-fail
166
+ # [21:37:44] warming up (5 records)
167
+ # [21:37:44] n_records=15 verdict=PASS findings=1
168
+ ```
169
+
170
+ **The default is safe.** `policy="warn"` (the default) only observes and reports
171
+ — it never interrupts your run, so you can leave it on even for experiments you
172
+ expect to fail. Aborting is strictly opt-in via `policy="stop_on_fail"`, the one
173
+ mode that takes an irreversible action. trainproof does not make that decision
174
+ for you unless you ask.
175
+
176
+ The guardian applies the same deterministic rules as `trainproof epoch`, so it
177
+ inherits their documented single-run limitations.
178
+
179
+ ## Pre-flight (v0.5): stop a run before it starts
180
+
181
+ The guardian saves most of a doomed run; preflight saves 100% because it never starts. Catch broken datasets and tokenizer misconfigurations instantly.
182
+
183
+ ```bash
184
+ trainproof preflight data/dataset.jsonl --tokenizer mistralai/Mistral-7B-v0.1 --max-len 4096
185
+ ```
186
+
187
+ ```text
188
+ ========================================
189
+ TRAINPROOF VERDICT
190
+ ========================================
191
+ [FAIL] Critical checks failed:
192
+ [FAIL] Empty or whitespace-only text found.
193
+ Evidence: 1 records (indices [1]...)
194
+ ========================================
195
+ ```
196
+
197
+ *Checks: malformed JSONL, empty text, exact duplicate text, tokenizer structural checks (EOS/PAD/BOS), and context length overflows.*
198
+
199
+ ## Supported log formats
200
+
201
+ - HuggingFace Trainer (`trainer_state.json`)
202
+ - Coqui Trainer text logs (ANSI-colored `trainer_0_log.txt`)
203
+ - Generic JSONL / CSV (columns: step, loss, lr, grad_norm, time — all optional)
204
+
205
+ Auto-detected; override with `--format hf|coqui|jsonl|csv`. TensorBoard event
206
+ files are planned (Lightning console captures are TTY dumps, not logs, and
207
+ will not be supported).
208
+
209
+ ## Philosophy
210
+
211
+ 1. **Deterministic.** A rule fires or it doesn't. Thresholds live in one
212
+ module, commented, tunable.
213
+ 2. **Evidence-cited.** Every finding names the steps and values that triggered
214
+ it.
215
+ 3. **Honest about limits.** What the tool cannot detect is documented in the
216
+ README, not discovered by the user in production.
217
+
218
+ ## Family
219
+
220
+ trainproof judges training runs. Its sibling [ttsproof](https://github.com/Mormolykos/ttsproof)
221
+ judges TTS model *outputs* (structural audio checks, equivalence-aware WER/CER,
222
+ published method with DOI) — and trainproof builds on it for the speech
223
+ dataset checks.
224
+
225
+ ## Author
226
+
227
+ Panagiotis (Panos) Gkilis — [portfolio](https://tts.bedvibe.studio/portfolio/) ·
228
+ [bedvibe.studio](https://bedvibe.studio/)
229
+
230
+ MIT license.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainproof"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "A deterministic linter for ML training runs: dataset, tokenizer, and epoch logs."
5
5
  authors = [{name = "Panagiotis (Panos) Gkilis", email = "bedvibe@bedvibe.studio"}]
6
6
  readme = "README.md"
@@ -5,6 +5,7 @@ from .speech.data import check_data
5
5
  from .speech.tokenizer import check_tokenizer
6
6
  from .epoch import check_epoch
7
7
  from .compare import check_compare
8
+ from .watch import watch_loop
8
9
  from .report import print_verdict_console, write_html_report
9
10
 
10
11
  def main():
@@ -31,6 +32,20 @@ def main():
31
32
  compare_parser.add_argument("baseline", type=str, help="Path to baseline log file")
32
33
  compare_parser.add_argument("--format", choices=["auto", "hf", "coqui", "jsonl", "csv"], default="auto", help="Log format override")
33
34
 
35
+ # Watch Command
36
+ watch_parser = subparsers.add_parser("watch", help="Live guardian: poll a training log.")
37
+ watch_parser.add_argument("logfile", type=str, help="Path to run log file")
38
+ watch_parser.add_argument("--interval", type=int, default=10, help="Polling interval in seconds")
39
+ watch_parser.add_argument("--format", choices=["auto", "hf", "coqui", "jsonl", "csv"], default="auto", help="Log format override")
40
+ watch_parser.add_argument("--until-fail", action="store_true", help="Exit with code 1 as soon as verdict becomes FAIL")
41
+
42
+ # Preflight Command
43
+ preflight_parser = subparsers.add_parser("preflight", help="Pre-flight linter for LLM fine-tuning datasets.")
44
+ preflight_parser.add_argument("dataset", type=str, help="Path to JSONL dataset")
45
+ preflight_parser.add_argument("--tokenizer", type=str, help="Tokenizer name or path")
46
+ preflight_parser.add_argument("--max-len", type=int, help="Max context length")
47
+ preflight_parser.add_argument("--text-field", type=str, help="Text field to extract")
48
+
34
49
  args = parser.parse_args()
35
50
 
36
51
  report_dict = {}
@@ -51,6 +66,26 @@ def main():
51
66
  except Exception as e:
52
67
  print(f"Error checking compare: {e}")
53
68
  sys.exit(1)
69
+ elif args.command == "watch":
70
+ watch_loop(args.logfile, interval=args.interval, fmt=args.format, until_fail=args.until_fail)
71
+ sys.exit(0)
72
+ elif args.command == "preflight":
73
+ from .preflight import preflight
74
+ tokenizer = None
75
+ if args.tokenizer:
76
+ try:
77
+ from transformers import AutoTokenizer
78
+ tokenizer = AutoTokenizer.from_pretrained(args.tokenizer)
79
+ except ImportError:
80
+ report_dict = {
81
+ "verdict": "FAIL",
82
+ "findings": [{"id": "transformers_missing", "level": "FAIL", "message": "pip install transformers is required to load tokenizer", "evidence": ""}]
83
+ }
84
+ print_verdict_console(report_dict["verdict"], report_dict["findings"])
85
+ sys.exit(1)
86
+
87
+ result = preflight(args.dataset, tokenizer=tokenizer, max_len=args.max_len, text_field=args.text_field)
88
+ report_dict = {"verdict": result.verdict, "findings": result.findings}
54
89
 
55
90
  print_verdict_console(report_dict.get("verdict", "FAIL"), report_dict.get("findings", []))
56
91
 
@@ -4,13 +4,12 @@ from typing import Any
4
4
  from . import rules
5
5
  from .adapters import parse_log_with_format
6
6
 
7
- def check_epoch(log_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
8
- records = parse_log_with_format(log_path, fmt)
7
+ def check_records(records: list[dict]) -> dict[str, Any]:
9
8
  findings = []
10
9
  verdict = "PASS"
11
10
 
12
11
  if not records:
13
- return {"verdict": "FAIL", "findings": [{"level": "FAIL", "message": "No valid log records found.", "evidence": str(log_path)}]}
12
+ return {"verdict": "FAIL", "findings": [{"level": "FAIL", "message": "No valid log records found.", "evidence": ""}]}
14
13
 
15
14
  # Find relevant keys
16
15
  def get_val(row, aliases):
@@ -112,3 +111,9 @@ def check_epoch(log_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
112
111
  findings.append({"level": "PASS", "message": "Loss curve shows healthy shape, grad norms are stable.", "evidence": f"{len(valid_losses)} steps analyzed."})
113
112
 
114
113
  return {"verdict": verdict, "findings": findings}
114
+
115
+ def check_epoch(log_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
116
+ records = parse_log_with_format(log_path, fmt)
117
+ if not records:
118
+ return {"verdict": "FAIL", "findings": [{"level": "FAIL", "message": "No valid log records found.", "evidence": str(log_path)}]}
119
+ return check_records(records)
@@ -0,0 +1,3 @@
1
+ from .hf import TrainproofCallback
2
+
3
+ __all__ = ["TrainproofCallback"]