trainproof 0.1.0__tar.gz → 0.3.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 (28) hide show
  1. trainproof-0.3.0/PKG-INFO +153 -0
  2. trainproof-0.3.0/README.md +141 -0
  3. {trainproof-0.1.0 → trainproof-0.3.0}/pyproject.toml +1 -1
  4. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/cli.py +13 -0
  5. trainproof-0.3.0/src/trainproof/compare.py +132 -0
  6. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/epoch.py +14 -1
  7. trainproof-0.3.0/src/trainproof/rules.py +90 -0
  8. trainproof-0.3.0/src/trainproof.egg-info/PKG-INFO +153 -0
  9. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof.egg-info/SOURCES.txt +2 -0
  10. trainproof-0.3.0/tests/test_compare.py +47 -0
  11. {trainproof-0.1.0 → trainproof-0.3.0}/tests/test_epoch.py +15 -0
  12. trainproof-0.1.0/PKG-INFO +0 -62
  13. trainproof-0.1.0/README.md +0 -50
  14. trainproof-0.1.0/src/trainproof/rules.py +0 -53
  15. trainproof-0.1.0/src/trainproof.egg-info/PKG-INFO +0 -62
  16. {trainproof-0.1.0 → trainproof-0.3.0}/setup.cfg +0 -0
  17. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/__init__.py +0 -0
  18. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/adapters.py +0 -0
  19. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/report.py +0 -0
  20. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/speech/__init__.py +0 -0
  21. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/speech/data.py +0 -0
  22. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof/speech/tokenizer.py +0 -0
  23. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof.egg-info/dependency_links.txt +0 -0
  24. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof.egg-info/entry_points.txt +0 -0
  25. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof.egg-info/requires.txt +0 -0
  26. {trainproof-0.1.0 → trainproof-0.3.0}/src/trainproof.egg-info/top_level.txt +0 -0
  27. {trainproof-0.1.0 → trainproof-0.3.0}/tests/test_adapters.py +0 -0
  28. {trainproof-0.1.0 → trainproof-0.3.0}/tests/test_data.py +0 -0
@@ -0,0 +1,153 @@
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.
@@ -0,0 +1,141 @@
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
+ ```bash
16
+ pip install trainproof
17
+ ```
18
+
19
+ ## See a verdict in 60 seconds
20
+
21
+ This repo ships the real logs of five QLoRA fine-tuning runs (Qwen2.5-3B,
22
+ RTX 5080 — see the gallery below). Judge one right now:
23
+
24
+ ```bash
25
+ trainproof epoch examples/gallery/lr_hot/trainer_state.json --format hf
26
+ ```
27
+
28
+ ```text
29
+ [FAIL] Critical checks failed:
30
+ [FAIL] Loss curve is diverging.
31
+ Evidence: End loss 7.492 vs Min loss 1.398
32
+ [WARN] Gradient norm spikes detected.
33
+ Evidence: Max gn 2649.75 > 10.0x median (0.55)
34
+ ```
35
+
36
+ ## Why this exists
37
+
38
+ The author lost a real 11-hour fine-tune to a failure nothing warned about.
39
+ Pointed retroactively at that run's 1MB Coqui Trainer log (2,501 logged steps),
40
+ trainproof's verdict: **FAIL — diverging**. The loss reached its minimum at 82%
41
+ of the run and ended 1.9x above it. Translation: the final two hours of GPU
42
+ time made the model measurably worse, and the checkpoint worth keeping had
43
+ already existed for hours. No tool in the stack said a word.
44
+
45
+ ## The fault-injection gallery
46
+
47
+ To validate the rules, the same QLoRA fine-tune (Qwen2.5-3B-Instruct, 4-bit,
48
+ LoRA r=16, 300 steps on Alpaca-cleaned) was run five times — once healthy, four
49
+ times with exactly one knob deliberately broken. Real runs, real logs, all
50
+ shipped in [`examples/gallery/`](examples/gallery/):
51
+
52
+ | Run | Sabotage | Verdict | Key evidence |
53
+ |---|---|---|---|
54
+ | `healthy` | none | **PASS** | loss 1.52 → 0.94, stable gradients |
55
+ | `lr_hot` | LR x100 (2e-2) | **FAIL** | diverging: end 7.49 vs min 1.40; grad spike 2650 vs median 0.55 |
56
+ | `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 |
57
+ | `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) |
58
+ | `bad_labels` | labels shuffled per-sequence | **WARN only** (single-run) — caught by `trainproof compare` (v0.3) | grad spike 23.3 vs median 1.09 |
59
+
60
+ ### The honest finding: loss curves cannot see corrupted data
61
+
62
+ The `bad_labels` run — whose shuffled labels make real learning impossible —
63
+ *reduced its loss by 62%* (18.9 → 5.75). The model was genuinely learning: not
64
+ the task, but the marginal token statistics of the garbage. From its own loss
65
+ curve, that is indistinguishable from healthy training (neural networks
66
+ famously fit random labels). **No single-run, loss-only rule can catch this
67
+ class of failure** — its real signature is *relative*: a loss floor ~6x higher
68
+ than a known-good run of the same task (5.59 vs 0.94).
69
+
70
+ That finding sets the roadmap: v0.3 is `trainproof compare <run> <baseline>` —
71
+ deterministic ratio rules against the healthy baseline you already have.
72
+ See [ROADMAP.md](ROADMAP.md). (The gallery also improved v0.2 itself: the
73
+ dead-run rule exists because `lr_zero` initially escaped with only a WARN.)
74
+
75
+ ## The three commands
76
+
77
+ ```bash
78
+ # 1. Dataset preflight (speech/TTS pack): audio integrity, transcript quality,
79
+ # duplicates, text-vs-audio duration mismatches
80
+ trainproof data /path/to/dataset_or_manifest.jsonl
81
+
82
+ # 2. Tokenizer preflight: vocabulary coverage, OOV rate, sequence blowouts,
83
+ # suspicious splits on numbers/dates
84
+ trainproof tokenizer my_tokenizer.model transcripts.txt
85
+
86
+ # 3. Training-run verdict: NaN/divergence/dead-run detection, gradient spikes,
87
+ # LR sanity, throughput — from log files, any framework
88
+ trainproof epoch logs/run.jsonl # exit code 1 on FAIL: CI-ready
89
+
90
+ # 4. Compare against a baseline
91
+ # Catch relative pathologies like the `bad_labels` run that evade single-run rules.
92
+ trainproof compare examples/gallery/bad_labels/trainer_state.json examples/gallery/healthy/trainer_state.json
93
+ ```
94
+
95
+ ```text
96
+ ========================================
97
+ TRAINPROOF VERDICT
98
+ ========================================
99
+ [FAIL] Critical checks failed:
100
+ [FAIL] loss floor ratio exceeded limit
101
+ Evidence: Run floor 5.592 vs Baseline floor 0.937 (ratio 6.0x > 2.0)
102
+ [FAIL] end loss ratio exceeded limit
103
+ Evidence: Run end 5.750 vs Baseline end 1.082 (ratio 5.3x > 2.0)
104
+ ========================================
105
+ ```
106
+
107
+ Each command prints the verdict, writes a self-contained HTML report, and sets
108
+ the process exit code — so it works as a CI gate out of the box.
109
+
110
+ ## Supported log formats
111
+
112
+ - HuggingFace Trainer (`trainer_state.json`)
113
+ - Coqui Trainer text logs (ANSI-colored `trainer_0_log.txt`)
114
+ - Generic JSONL / CSV (columns: step, loss, lr, grad_norm, time — all optional)
115
+
116
+ Auto-detected; override with `--format hf|coqui|jsonl|csv`. TensorBoard event
117
+ files are planned (Lightning console captures are TTY dumps, not logs, and
118
+ will not be supported).
119
+
120
+ ## Philosophy
121
+
122
+ 1. **Deterministic.** A rule fires or it doesn't. Thresholds live in one
123
+ module, commented, tunable.
124
+ 2. **Evidence-cited.** Every finding names the steps and values that triggered
125
+ it.
126
+ 3. **Honest about limits.** What the tool cannot detect is documented in the
127
+ README, not discovered by the user in production.
128
+
129
+ ## Family
130
+
131
+ trainproof judges training runs. Its sibling [ttsproof](https://github.com/Mormolykos/ttsproof)
132
+ judges TTS model *outputs* (structural audio checks, equivalence-aware WER/CER,
133
+ published method with DOI) — and trainproof builds on it for the speech
134
+ dataset checks.
135
+
136
+ ## Author
137
+
138
+ Panagiotis (Panos) Gkilis — [portfolio](https://tts.bedvibe.studio/portfolio/) ·
139
+ [bedvibe.studio](https://bedvibe.studio/)
140
+
141
+ MIT license.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trainproof"
3
- version = "0.1.0"
3
+ version = "0.3.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"
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
  from .speech.data import check_data
5
5
  from .speech.tokenizer import check_tokenizer
6
6
  from .epoch import check_epoch
7
+ from .compare import check_compare
7
8
  from .report import print_verdict_console, write_html_report
8
9
 
9
10
  def main():
@@ -24,6 +25,12 @@ def main():
24
25
  epoch_parser.add_argument("logfile", type=str, help="Path to JSONL or CSV log file")
25
26
  epoch_parser.add_argument("--format", choices=["auto", "hf", "coqui", "jsonl", "csv"], default="auto", help="Log format override")
26
27
 
28
+ # Compare Command
29
+ compare_parser = subparsers.add_parser("compare", help="Compare a run log against a baseline log.")
30
+ compare_parser.add_argument("run", type=str, help="Path to run log file")
31
+ compare_parser.add_argument("baseline", type=str, help="Path to baseline log file")
32
+ compare_parser.add_argument("--format", choices=["auto", "hf", "coqui", "jsonl", "csv"], default="auto", help="Log format override")
33
+
27
34
  args = parser.parse_args()
28
35
 
29
36
  report_dict = {}
@@ -38,6 +45,12 @@ def main():
38
45
  except Exception as e:
39
46
  print(f"Error checking epoch: {e}")
40
47
  sys.exit(1)
48
+ elif args.command == "compare":
49
+ try:
50
+ report_dict = check_compare(args.run, args.baseline, fmt=args.format)
51
+ except Exception as e:
52
+ print(f"Error checking compare: {e}")
53
+ sys.exit(1)
41
54
 
42
55
  print_verdict_console(report_dict.get("verdict", "FAIL"), report_dict.get("findings", []))
43
56
 
@@ -0,0 +1,132 @@
1
+ import math
2
+ from pathlib import Path
3
+ from typing import Any
4
+ from . import rules
5
+ from .adapters import parse_log_with_format
6
+ from .epoch import check_epoch
7
+
8
+ def _get_val(row, aliases):
9
+ for a in aliases:
10
+ for k in row:
11
+ if a in k: return row[k]
12
+ return None
13
+
14
+ def extract_metrics(records):
15
+ losses = []
16
+ grad_norms = []
17
+
18
+ for r in records:
19
+ loss = _get_val(r, ["loss", "train_loss"])
20
+ gn = _get_val(r, ["grad_norm", "gnorm", "grad"])
21
+
22
+ if loss is not None and not math.isnan(loss) and not math.isinf(loss):
23
+ losses.append(loss)
24
+ if gn is not None and not math.isnan(gn) and not math.isinf(gn):
25
+ grad_norms.append(gn)
26
+
27
+ if len(losses) < 10:
28
+ return None
29
+
30
+ floor = min(losses)
31
+
32
+ w = rules.LOSS_IMPROVEMENT_WINDOW
33
+ start_med = sorted(losses[:w])[w // 2]
34
+ end_med = sorted(losses[-w:])[w // 2]
35
+
36
+ improvement = 1.0 - (end_med / start_med) if start_med > 0 else 0.0
37
+
38
+ gn_median = None
39
+ if len(grad_norms) > 5:
40
+ sorted_gns = sorted(grad_norms)
41
+ gn_median = sorted_gns[len(sorted_gns) // 2]
42
+
43
+ return {
44
+ "floor": floor,
45
+ "start_med": start_med,
46
+ "end_med": end_med,
47
+ "improvement": improvement,
48
+ "gn_median": gn_median,
49
+ "losses_len": len(losses)
50
+ }
51
+
52
+ def check_compare(run_path: str | Path, base_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
53
+ findings = []
54
+ verdict = "PASS"
55
+
56
+ # Baseline sanity — a suspect baseline downgrades the whole comparison
57
+ base_epoch_res = check_epoch(base_path, fmt=fmt)
58
+ if base_epoch_res["verdict"] == "FAIL":
59
+ verdict = "WARN"
60
+ findings.append({
61
+ "level": "WARN",
62
+ "message": "baseline itself fails single-run checks - comparison may be meaningless",
63
+ "evidence": str(base_path)
64
+ })
65
+
66
+ run_records = parse_log_with_format(run_path, fmt)
67
+ base_records = parse_log_with_format(base_path, fmt)
68
+
69
+ run_metrics = extract_metrics(run_records) if run_records else None
70
+ base_metrics = extract_metrics(base_records) if base_records else None
71
+
72
+ if not run_metrics:
73
+ return {"verdict": "FAIL", "findings": [{"level": "FAIL", "message": "Run log has fewer than 10 valid loss points.", "evidence": str(run_path)}]}
74
+ if not base_metrics:
75
+ return {"verdict": "FAIL", "findings": [{"level": "FAIL", "message": "Baseline log has fewer than 10 valid loss points.", "evidence": str(base_path)}]}
76
+
77
+ # Floor ratio
78
+ if run_metrics["floor"] > base_metrics["floor"] * rules.MAX_FLOOR_RATIO:
79
+ verdict = "FAIL"
80
+ findings.append({
81
+ "level": "FAIL",
82
+ "message": "loss floor ratio exceeded limit",
83
+ "evidence": f"Run floor {run_metrics['floor']:.3f} vs Baseline floor {base_metrics['floor']:.3f} (ratio {(run_metrics['floor'] / base_metrics['floor']):.1f}x > {rules.MAX_FLOOR_RATIO})"
84
+ })
85
+
86
+ # End-loss ratio: where the run landed vs where the baseline landed.
87
+ # Robust to corrupted starts (see rules.MAX_END_RATIO comment).
88
+ if base_metrics["end_med"] > 0 and run_metrics["end_med"] > base_metrics["end_med"] * rules.MAX_END_RATIO:
89
+ verdict = "FAIL"
90
+ findings.append({
91
+ "level": "FAIL",
92
+ "message": "end loss ratio exceeded limit",
93
+ "evidence": f"Run end {run_metrics['end_med']:.3f} vs Baseline end {base_metrics['end_med']:.3f} (ratio {(run_metrics['end_med'] / base_metrics['end_med']):.1f}x > {rules.MAX_END_RATIO})"
94
+ })
95
+
96
+ # Improvement deficit
97
+ run_imp = run_metrics["improvement"]
98
+ base_imp = base_metrics["improvement"]
99
+
100
+ if run_imp < 0:
101
+ verdict = "FAIL"
102
+ findings.append({
103
+ "level": "FAIL",
104
+ "message": "negative improvement",
105
+ "evidence": f"Run improvement is {run_imp * 100:.1f}% vs Baseline {base_imp * 100:.1f}%"
106
+ })
107
+ elif base_imp > 0 and run_imp < base_imp * rules.MIN_IMPROVEMENT_FRACTION:
108
+ verdict = "FAIL"
109
+ findings.append({
110
+ "level": "FAIL",
111
+ "message": "improvement deficit",
112
+ "evidence": f"Run improvement {run_imp * 100:.1f}% vs Baseline {base_imp * 100:.1f}% (ratio {(run_imp / base_imp):.2f}x < {rules.MIN_IMPROVEMENT_FRACTION})"
113
+ })
114
+
115
+ # Gradnorm median ratio
116
+ if run_metrics["gn_median"] is not None and base_metrics["gn_median"] is not None and base_metrics["gn_median"] > 0:
117
+ if run_metrics["gn_median"] > base_metrics["gn_median"] * rules.MAX_GRADNORM_MEDIAN_RATIO:
118
+ if verdict == "PASS": verdict = "WARN"
119
+ findings.append({
120
+ "level": "WARN",
121
+ "message": "gradient norm median significantly higher than baseline",
122
+ "evidence": f"Run gn median {run_metrics['gn_median']:.2f} vs Baseline gn median {base_metrics['gn_median']:.2f} (ratio {(run_metrics['gn_median'] / base_metrics['gn_median']):.1f}x > {rules.MAX_GRADNORM_MEDIAN_RATIO})"
123
+ })
124
+
125
+ if verdict == "PASS" and not any(f["level"] == "WARN" for f in findings):
126
+ findings.append({
127
+ "level": "PASS",
128
+ "message": "Run compares favorably to baseline.",
129
+ "evidence": f"Floor ratio {(run_metrics['floor'] / base_metrics['floor']):.2f}x, Improvement {run_imp*100:.1f}% vs {base_imp*100:.1f}%"
130
+ })
131
+
132
+ return {"verdict": verdict, "findings": findings}
@@ -67,6 +67,16 @@ def check_epoch(log_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
67
67
  findings.append({"level": "FAIL", "message": "Loss curve is diverging.", "evidence": f"End loss {valid_losses[-1]:.3f} vs Min loss {min_loss:.3f}"})
68
68
  verdict = "FAIL"
69
69
 
70
+ # Check no-improvement (dead run): robust start-vs-end median comparison
71
+ if len(valid_losses) >= rules.MIN_POINTS_FOR_IMPROVEMENT_CHECK:
72
+ w = rules.LOSS_IMPROVEMENT_WINDOW
73
+ start_med = sorted(valid_losses[:w])[w // 2]
74
+ end_med = sorted(valid_losses[-w:])[w // 2]
75
+ if start_med > 0 and end_med >= start_med * (1 - rules.MIN_LOSS_IMPROVEMENT):
76
+ findings.append({"level": "FAIL", "message": "Loss never improved over the run (dead run).",
77
+ "evidence": f"median of first {w} losses {start_med:.3f} vs last {w} {end_med:.3f} (needs >={rules.MIN_LOSS_IMPROVEMENT*100:.0f}% improvement)"})
78
+ verdict = "FAIL"
79
+
70
80
  # Check Grad Norm
71
81
  valid_gns = [g for g in grad_norms if not math.isnan(g) and not math.isinf(g)]
72
82
  if valid_gns and len(valid_gns) > 5:
@@ -82,7 +92,10 @@ def check_epoch(log_path: str | Path, fmt: str = "auto") -> dict[str, Any]:
82
92
  if lrs:
83
93
  zeros = sum(1 for lr in lrs if lr <= 0)
84
94
  zero_frac = zeros / len(lrs)
85
- if zero_frac > rules.MAX_ZERO_LR_FRACTION:
95
+ if zero_frac >= rules.ZERO_LR_FAIL_FRACTION:
96
+ findings.append({"level": "FAIL", "message": "Learning rate is zero for the entire run - the optimizer never steps.", "evidence": f"{zero_frac*100:.1f}% of steps have lr=0"})
97
+ verdict = "FAIL"
98
+ elif zero_frac > rules.MAX_ZERO_LR_FRACTION:
86
99
  findings.append({"level": "WARN", "message": "Learning rate is zero for a large fraction of the run.", "evidence": f"{zero_frac*100:.1f}% of steps have lr=0"})
87
100
  if verdict == "PASS": verdict = "WARN"
88
101
 
@@ -0,0 +1,90 @@
1
+ """Deterministic thresholds for verdicts."""
2
+
3
+ # -----------------
4
+ # DATA SUBCOMMAND
5
+ # -----------------
6
+
7
+ # Maximum allowed duration for a single audio file in seconds.
8
+ MAX_AUDIO_DURATION_SEC = 25.0
9
+
10
+ # Minimum allowed duration for a single audio file in seconds.
11
+ MIN_AUDIO_DURATION_SEC = 0.5
12
+
13
+ # Peak amplitude threshold above which we consider audio to be clipping.
14
+ MAX_CLIPPING_PEAK = 0.99
15
+
16
+ # Maximum allowed continuous silence in seconds before warning.
17
+ MAX_SILENCE_SEC = 2.0
18
+
19
+ # Amplitude threshold for considering a frame as "silent" (for silence detection).
20
+ SILENCE_AMPLITUDE = 0.005
21
+
22
+ # Flag text/audio alignment outliers whose chars-per-second rate deviates from
23
+ # the corpus median by more than this ratio (in either direction).
24
+ CHARS_PER_SEC_OUTLIER_RATIO = 3.0
25
+
26
+ # -----------------
27
+ # TOKENIZER SUBCOMMAND
28
+ # -----------------
29
+
30
+ # Minimum fraction of the corpus vocabulary that must be covered by the tokenizer.
31
+ MIN_VOCAB_COVERAGE = 0.999
32
+
33
+ # Maximum acceptable Out-Of-Vocabulary (OOV) rate.
34
+ MAX_OOV_RATE = 0.001
35
+
36
+ # Maximum expected tokens per second of audio (blowout warning).
37
+ MAX_TOKENS_PER_SEC = 50.0
38
+
39
+ # -----------------
40
+ # EPOCH SUBCOMMAND
41
+ # -----------------
42
+
43
+ # If loss increases by more than this ratio from the minimum loss, flag as diverging.
44
+ MAX_LOSS_DIVERGENCE_RATIO = 1.5
45
+
46
+ # Minimum required variation (std/mean) in the loss curve to not be considered "flat/dead".
47
+ MIN_LOSS_VARIATION = 0.001
48
+
49
+ # Maximum allowable spike in gradient norm compared to the median grad norm.
50
+ MAX_GRAD_NORM_SPIKE_RATIO = 10.0
51
+
52
+ # If the learning rate is strictly zero for more than this fraction of the epoch, it's a warning.
53
+ MAX_ZERO_LR_FRACTION = 0.1
54
+
55
+ # If the learning rate is zero for essentially the ENTIRE run, the optimizer
56
+ # never steps — the run cannot train, regardless of how the loss wiggles
57
+ # (multi-seed testing showed batch-order noise can fake >5% "improvement"
58
+ # under lr=0). Total zero-LR is a fatality, not a warning.
59
+ ZERO_LR_FAIL_FRACTION = 0.99
60
+
61
+ # Dead-run detection (added in v0.2 after fault-injection testing showed that
62
+ # zero-LR and garbage-label runs — which can never learn — only earned WARNs):
63
+ # the median of the last LOSS_IMPROVEMENT_WINDOW losses must be at least
64
+ # MIN_LOSS_IMPROVEMENT (relative) below the median of the first window, else
65
+ # the run never learned. Note: this targets FIRST-EPOCH linting; a model
66
+ # resumed at its convergence floor would also trip it.
67
+ MIN_LOSS_IMPROVEMENT = 0.05
68
+ LOSS_IMPROVEMENT_WINDOW = 5
69
+ MIN_POINTS_FOR_IMPROVEMENT_CHECK = 10
70
+
71
+ # -----------------
72
+ # COMPARE SUBCOMMAND
73
+ # -----------------
74
+
75
+ # FAIL "loss floor ratio" if run_floor / baseline_floor > 2.0
76
+ MAX_FLOOR_RATIO = 2.0
77
+
78
+ # FAIL "end loss ratio" if the run's final loss (median of last window) lands
79
+ # more than this factor above the baseline's. Judges where the run LANDED —
80
+ # robust both to noisy first steps and to corruption inside the first window
81
+ # (e.g. an LR explosion at step 10 inflates the run's own start median, making
82
+ # self-relative improvement look positive; the end ratio still catches it).
83
+ MAX_END_RATIO = 2.0
84
+
85
+ # FAIL "improvement deficit" if the run's relative improvement is < 25% of the baseline's
86
+ # (only evaluated when baseline improvement > 0). A run with NEGATIVE improvement always trips this rule.
87
+ MIN_IMPROVEMENT_FRACTION = 0.25
88
+
89
+ # WARN if run's grad-norm median > 5x baseline's (skip if either lacks grad norms)
90
+ MAX_GRADNORM_MEDIAN_RATIO = 5.0
@@ -0,0 +1,153 @@
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.
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  src/trainproof/__init__.py
4
4
  src/trainproof/adapters.py
5
5
  src/trainproof/cli.py
6
+ src/trainproof/compare.py
6
7
  src/trainproof/epoch.py
7
8
  src/trainproof/report.py
8
9
  src/trainproof/rules.py
@@ -16,5 +17,6 @@ src/trainproof/speech/__init__.py
16
17
  src/trainproof/speech/data.py
17
18
  src/trainproof/speech/tokenizer.py
18
19
  tests/test_adapters.py
20
+ tests/test_compare.py
19
21
  tests/test_data.py
20
22
  tests/test_epoch.py
@@ -0,0 +1,47 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from trainproof.compare import check_compare
4
+
5
+ def get_gallery_log(name: str) -> str:
6
+ path = Path(__file__).parent.parent / "examples" / "gallery" / name / "trainer_state.json"
7
+ return str(path.resolve())
8
+
9
+ def test_compare_healthy_vs_healthy():
10
+ healthy = get_gallery_log("healthy")
11
+ res = check_compare(healthy, healthy, fmt="hf")
12
+ assert res["verdict"] == "PASS"
13
+
14
+ def test_compare_bad_labels_vs_healthy():
15
+ bad_labels = get_gallery_log("bad_labels")
16
+ healthy = get_gallery_log("healthy")
17
+ res = check_compare(bad_labels, healthy, fmt="hf")
18
+ assert res["verdict"] == "FAIL"
19
+ findings = [f for f in res["findings"] if "loss floor ratio" in f["message"]]
20
+ assert len(findings) > 0
21
+
22
+ def test_compare_lr_zero_vs_healthy():
23
+ lr_zero = get_gallery_log("lr_zero")
24
+ healthy = get_gallery_log("healthy")
25
+ res = check_compare(lr_zero, healthy, fmt="hf")
26
+ assert res["verdict"] == "FAIL"
27
+ findings = [f for f in res["findings"] if "improvement deficit" in f["message"]]
28
+ assert len(findings) > 0
29
+
30
+ def test_compare_lr_hot_vs_healthy():
31
+ # lr_hot's explosion happens INSIDE the first-5 window, inflating its own
32
+ # start median — self-relative improvement looks positive (+62%). Only the
33
+ # end-loss ratio (where the run LANDED: ~7x the baseline) catches it.
34
+ lr_hot = get_gallery_log("lr_hot")
35
+ healthy = get_gallery_log("healthy")
36
+ res = check_compare(lr_hot, healthy, fmt="hf")
37
+ assert res["verdict"] == "FAIL"
38
+ findings = [f for f in res["findings"] if "end loss ratio" in f["message"]]
39
+ assert len(findings) > 0
40
+
41
+ def test_compare_fp16_nan_vs_healthy():
42
+ fp16_nan = get_gallery_log("fp16_nan")
43
+ healthy = get_gallery_log("healthy")
44
+ res = check_compare(fp16_nan, healthy, fmt="hf")
45
+ assert res["verdict"] == "FAIL"
46
+ findings = [f for f in res["findings"] if "negative improvement" in f["message"]]
47
+ assert len(findings) > 0
@@ -34,3 +34,18 @@ def test_epoch_flat():
34
34
  report = check_epoch(FIXTURES_DIR / "flat.jsonl")
35
35
  assert report["verdict"] == "FAIL"
36
36
  assert any("completely flat" in str(f) for f in report["findings"])
37
+
38
+ def test_epoch_dead_noisy():
39
+ # noisy-but-never-improving loss with healthy lr: only the no-improvement
40
+ # rule (v0.2) can catch this dead run
41
+ report = check_epoch(FIXTURES_DIR / "dead_noisy.jsonl")
42
+ assert report["verdict"] == "FAIL"
43
+ assert any("never improved" in str(f) for f in report["findings"])
44
+
45
+ def test_epoch_total_zero_lr_is_fatal():
46
+ # lr=0 on every step: the optimizer never moves — FAIL regardless of how
47
+ # batch-order noise makes the loss wiggle (found by multi-seed testing)
48
+ gallery = Path(__file__).parent.parent / "examples" / "gallery"
49
+ report = check_epoch(gallery / "lr_zero" / "trainer_state.json", fmt="hf")
50
+ assert report["verdict"] == "FAIL"
51
+ assert any("optimizer never steps" in str(f) for f in report["findings"])
trainproof-0.1.0/PKG-INFO DELETED
@@ -1,62 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: trainproof
3
- Version: 0.1.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
- A deterministic linter for ML training runs. Run it on your dataset, tokenizer, and first-epoch logs — it gives a deterministic PASS/WARN/FAIL verdict with named findings and suggested fixes, BEFORE you burn weeks of GPU time.
16
-
17
- ## Installation
18
- ```bash
19
- pip install .
20
- ```
21
-
22
- ## Usage
23
-
24
- Exactly three subcommands:
25
-
26
- ### 1. Dataset Preflight
27
- ```bash
28
- trainproof data /path/to/dataset
29
- ```
30
- Checks audio integrity (clipping, silence, duration distribution) and transcript quality (unnormalized text, charset audit, duration correlation).
31
-
32
- ### 2. Tokenizer Preflight
33
- ```bash
34
- trainproof tokenizer my_model.model transcripts.txt
35
- ```
36
- Checks vocabulary coverage, tokens-per-second, and suspicious splits on numbers/dates.
37
-
38
- ### 3. First-Epoch Verification
39
- ```bash
40
- trainproof epoch logs/epoch1.jsonl
41
- ```
42
- Analyzes loss curves (divergence, flatlines, NaN), grad norms, learning rate response, and throughput.
43
-
44
- ## Supported log formats
45
- - **Generic JSONL / CSV**
46
- - **HuggingFace** (`trainer_state.json`)
47
- - **Coqui TTS Trainer** (plain text `trainer_0_log.txt`)
48
-
49
- *Roadmap: TensorBoard event files planned (v0.2) — Lightning console captures are TTY dumps, not logs, and will not be supported.*
50
-
51
- ## Verdict Rules
52
- All verdict rules are deterministic thresholds defined centrally in `src/trainproof/rules.py`. Examples include:
53
- - `MAX_CLIPPING_PEAK = 0.99`
54
- - `MAX_LOSS_DIVERGENCE_RATIO = 1.5`
55
- - `MIN_VOCAB_COVERAGE = 0.999`
56
-
57
- ## Explicit Non-Goals
58
- - No live learning-rate auto-adjustment.
59
- - No PyTorch/Lightning callbacks or framework hooks (log files only).
60
- - No MCP server, no LLM integration.
61
- - No dashboards/wandb-style UI.
62
- - No extra features beyond this spec.
@@ -1,50 +0,0 @@
1
- # Trainproof
2
-
3
- A deterministic linter for ML training runs. Run it on your dataset, tokenizer, and first-epoch logs — it gives a deterministic PASS/WARN/FAIL verdict with named findings and suggested fixes, BEFORE you burn weeks of GPU time.
4
-
5
- ## Installation
6
- ```bash
7
- pip install .
8
- ```
9
-
10
- ## Usage
11
-
12
- Exactly three subcommands:
13
-
14
- ### 1. Dataset Preflight
15
- ```bash
16
- trainproof data /path/to/dataset
17
- ```
18
- Checks audio integrity (clipping, silence, duration distribution) and transcript quality (unnormalized text, charset audit, duration correlation).
19
-
20
- ### 2. Tokenizer Preflight
21
- ```bash
22
- trainproof tokenizer my_model.model transcripts.txt
23
- ```
24
- Checks vocabulary coverage, tokens-per-second, and suspicious splits on numbers/dates.
25
-
26
- ### 3. First-Epoch Verification
27
- ```bash
28
- trainproof epoch logs/epoch1.jsonl
29
- ```
30
- Analyzes loss curves (divergence, flatlines, NaN), grad norms, learning rate response, and throughput.
31
-
32
- ## Supported log formats
33
- - **Generic JSONL / CSV**
34
- - **HuggingFace** (`trainer_state.json`)
35
- - **Coqui TTS Trainer** (plain text `trainer_0_log.txt`)
36
-
37
- *Roadmap: TensorBoard event files planned (v0.2) — Lightning console captures are TTY dumps, not logs, and will not be supported.*
38
-
39
- ## Verdict Rules
40
- All verdict rules are deterministic thresholds defined centrally in `src/trainproof/rules.py`. Examples include:
41
- - `MAX_CLIPPING_PEAK = 0.99`
42
- - `MAX_LOSS_DIVERGENCE_RATIO = 1.5`
43
- - `MIN_VOCAB_COVERAGE = 0.999`
44
-
45
- ## Explicit Non-Goals
46
- - No live learning-rate auto-adjustment.
47
- - No PyTorch/Lightning callbacks or framework hooks (log files only).
48
- - No MCP server, no LLM integration.
49
- - No dashboards/wandb-style UI.
50
- - No extra features beyond this spec.
@@ -1,53 +0,0 @@
1
- """Deterministic thresholds for verdicts."""
2
-
3
- # -----------------
4
- # DATA SUBCOMMAND
5
- # -----------------
6
-
7
- # Maximum allowed duration for a single audio file in seconds.
8
- MAX_AUDIO_DURATION_SEC = 25.0
9
-
10
- # Minimum allowed duration for a single audio file in seconds.
11
- MIN_AUDIO_DURATION_SEC = 0.5
12
-
13
- # Peak amplitude threshold above which we consider audio to be clipping.
14
- MAX_CLIPPING_PEAK = 0.99
15
-
16
- # Maximum allowed continuous silence in seconds before warning.
17
- MAX_SILENCE_SEC = 2.0
18
-
19
- # Amplitude threshold for considering a frame as "silent" (for silence detection).
20
- SILENCE_AMPLITUDE = 0.005
21
-
22
- # Flag text/audio alignment outliers whose chars-per-second rate deviates from
23
- # the corpus median by more than this ratio (in either direction).
24
- CHARS_PER_SEC_OUTLIER_RATIO = 3.0
25
-
26
- # -----------------
27
- # TOKENIZER SUBCOMMAND
28
- # -----------------
29
-
30
- # Minimum fraction of the corpus vocabulary that must be covered by the tokenizer.
31
- MIN_VOCAB_COVERAGE = 0.999
32
-
33
- # Maximum acceptable Out-Of-Vocabulary (OOV) rate.
34
- MAX_OOV_RATE = 0.001
35
-
36
- # Maximum expected tokens per second of audio (blowout warning).
37
- MAX_TOKENS_PER_SEC = 50.0
38
-
39
- # -----------------
40
- # EPOCH SUBCOMMAND
41
- # -----------------
42
-
43
- # If loss increases by more than this ratio from the minimum loss, flag as diverging.
44
- MAX_LOSS_DIVERGENCE_RATIO = 1.5
45
-
46
- # Minimum required variation (std/mean) in the loss curve to not be considered "flat/dead".
47
- MIN_LOSS_VARIATION = 0.001
48
-
49
- # Maximum allowable spike in gradient norm compared to the median grad norm.
50
- MAX_GRAD_NORM_SPIKE_RATIO = 10.0
51
-
52
- # If the learning rate is strictly zero for more than this fraction of the epoch, it's a warning.
53
- MAX_ZERO_LR_FRACTION = 0.1
@@ -1,62 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: trainproof
3
- Version: 0.1.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
- A deterministic linter for ML training runs. Run it on your dataset, tokenizer, and first-epoch logs — it gives a deterministic PASS/WARN/FAIL verdict with named findings and suggested fixes, BEFORE you burn weeks of GPU time.
16
-
17
- ## Installation
18
- ```bash
19
- pip install .
20
- ```
21
-
22
- ## Usage
23
-
24
- Exactly three subcommands:
25
-
26
- ### 1. Dataset Preflight
27
- ```bash
28
- trainproof data /path/to/dataset
29
- ```
30
- Checks audio integrity (clipping, silence, duration distribution) and transcript quality (unnormalized text, charset audit, duration correlation).
31
-
32
- ### 2. Tokenizer Preflight
33
- ```bash
34
- trainproof tokenizer my_model.model transcripts.txt
35
- ```
36
- Checks vocabulary coverage, tokens-per-second, and suspicious splits on numbers/dates.
37
-
38
- ### 3. First-Epoch Verification
39
- ```bash
40
- trainproof epoch logs/epoch1.jsonl
41
- ```
42
- Analyzes loss curves (divergence, flatlines, NaN), grad norms, learning rate response, and throughput.
43
-
44
- ## Supported log formats
45
- - **Generic JSONL / CSV**
46
- - **HuggingFace** (`trainer_state.json`)
47
- - **Coqui TTS Trainer** (plain text `trainer_0_log.txt`)
48
-
49
- *Roadmap: TensorBoard event files planned (v0.2) — Lightning console captures are TTY dumps, not logs, and will not be supported.*
50
-
51
- ## Verdict Rules
52
- All verdict rules are deterministic thresholds defined centrally in `src/trainproof/rules.py`. Examples include:
53
- - `MAX_CLIPPING_PEAK = 0.99`
54
- - `MAX_LOSS_DIVERGENCE_RATIO = 1.5`
55
- - `MIN_VOCAB_COVERAGE = 0.999`
56
-
57
- ## Explicit Non-Goals
58
- - No live learning-rate auto-adjustment.
59
- - No PyTorch/Lightning callbacks or framework hooks (log files only).
60
- - No MCP server, no LLM integration.
61
- - No dashboards/wandb-style UI.
62
- - No extra features beyond this spec.
File without changes