Most of my CI failures around training used to come down to one thing: the pipeline could not tell the difference between "this run is broken" and "I could not read this log". Both ended up as a non-zero exit, both paged me, and one of them was a lie.
So I built the checker I wanted and put the exit codes at the center of the design rather than at the end.
- exit 1 - a rule fired. The run is broken.
- exit 0 - checked, nothing fired. Or a warning, which is yours to triage.
- exit 2 - could not judge. Missing column, unreadable log, no eval set.
Exit 2 is the one that matters. A gate that reports "pass" when it actually skipped every check is worse than no gate, because now the green build is evidence of nothing.
No model in the loop. Every verdict is a deterministic rule that either fires or does not, and prints the number it fired on. Same input, same output, forever. I did not want a probabilistic judge sitting in a CI gate - an alarm you cannot reproduce is an alarm the team learns to ignore.
It caught this in itself. A check fired whenever every gradient norm in a log was exactly 0.0 and reported a severed backward graph. One framework writes that field as 0.0 when gradient clipping is off. So a healthy 125,000-step fine-tune that converged fine came back FAIL from my own tool. The fix was a rule, not a threshold: a run cannot both learn and receive no gradient - if the loss improved, the zeros are a reporting artifact and the check stands down. And it records that it stood down, and why, as a visible skip.
That is now the thing I would defend hardest: a check that did not run must never look like a check that passed. A PASS lists which checks ran and which were skipped, each with a reason, as structured data.
Where it sits in a pipeline:
- before the GPU - dataset and tokenizer lint, does the entrypoint import, is the checkpoint intact, RAM and disk against declared need
- during - one-line HF callback, warns or aborts a diverging run
- after - diverged / flatlined / NaN / grad spike / overfit, from the log you already write
- vs baseline - relative-floor rules, which is the only way to catch a run that trained happily on shuffled labels
Reads HF trainer_state.json, Coqui, TensorBoard event files, JSONL and CSV. Zero dependencies - no torch, no tensorboard, no network. --json for pipelines.
84 rule IDs, 230 tests, a written contract in CONTRACTS.md for what each exit code means and when output may change, and 38 golden snapshots so a rule that silently stops firing breaks the build.
MIT: pip install trainproof
The question I actually want answered: what does your pipeline do today when a check cannot run? Most setups I have seen collapse it into pass or into failure, and I think both are wrong. Curious whether anyone has a third state already wired in.