Your training run collapsed. Which exact optimizer step, data shard, and batch caused it? Trainsect answers with a causal proof, not an attribution score: it reloads optimizer state at a checkpoint, replays forward deterministically, binary-searches the checkpoint timeline, then counterfactually replays with each data shard excluded until only the guilty shard remains.
Necessity = replay the trigger interval without shard-3 and the collapse vanishes. Sufficiency = replay it with only shard-3 and the collapse reproduces. That pair is a causal proof for this trajectory โ not an attribution score.
639a024b373ce19a| Verdict | CONFIRMED (causal) |
|---|---|
| Provenance | instrumented |
| Confidence | causal by counterfactual replay (necessity + sufficiency) |
| Fidelity | BIT-EXACT |
| Change point | step 80 (threshold 3.0, baseline 0.0019, eval at change point 4.082) |
| Step range | [60, 90] |
| Trigger step | 60 |
| Offending shard | shard-3 |
| Batch fingerprint | 639a024b373ce19a |
| Necessity | True |
| Sufficiency | True |
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --exclude-shard 3 # collapse vanishes (necessity)
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --only-shard 3 # collapse reproduces (sufficiency)
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --only-batch 639a024b373ce19a # trigger batch alone
=== TRAINSECT DEMO: nanoGPT-style tiny transformer + poison shard ===
training 120 steps, poison shard=3 (steps 60-79)
step 0 shard 0 train_loss= 4.3811
step 20 shard 1 train_loss= 0.1188
step 40 shard 2 train_loss= 0.0027
step 60 shard 3 train_loss= 12.4577
step 80 shard 4 train_loss= 4.1259
step 100 shard 5 train_loss= 4.0403
eval loss log: ['0:4.298', '10:1.749', '20:0.072', '30:0.006', '40:0.002', '50:0.002', '60:0.002', '70:3.871', '80:4.082', '90:4.067', '100:4.055', '110:4.026', '119:3.997']
demo run saved to demos/demo_run (preloaded)
=== TRAINSECT DIAGNOSE (live) ===
[fidelity] replay 7622e7196398a345 vs original 7622e7196398a345 -> BIT-EXACT
[Stage A] change point: {'change_point_step': 80, 'threshold': 3.0, 'baseline': 0.001927278353832662, 'eval_loss_at_cp': 4.0823655128479}
[thresholds] train-spike=3.035 eval-health=3.000
[Stage B] checkpoint binary search -> {'ok': True, 'bracket': (60, 90)}
[Stage C] pinpoint: first train spike at step 60 (loss=12.457655906677246); first eval crossing at 70 inside bracket [60, 90)
[Stage D] offending shard = shard-3
[Stage E] necessity=True sufficiency=True
[Stage E] trigger batch: step 60, shard 3, fingerprint 639a024b373ce19a
[Stage E] offending sample: {'sample_index_in_batch': 3, 'sample_loss': 13.739205360412598, 'batch_mean_loss': 12.457656860351562}
=== TRAINSECT EVIDENCE REPORT ===
run: demos/demo_run (preloaded)
verdict: CONFIRMED (causal)
provenance: instrumented
confidence: causal by counterfactual replay (necessity + sufficiency)
fidelity: BIT-EXACT
change point: 80 (threshold=3.0)
step range: [60, 90]
trigger step: 60
offending shard: shard-3
batch fingerprint: 639a024b373ce19a
offending sample: {'sample_index_in_batch': 3, 'sample_loss': 13.739205360412598, 'batch_mean_loss': 12.457656860351562}
necessity: True
sufficiency: True
replays: 8 (540 replayed steps)
wall time: 52.2s
minimal repro:
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --exclude-shard 3 # collapse vanishes (necessity)
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --only-shard 3 # collapse reproduces (sufficiency)
trainsect replay --checkpoints demos/demo_run (preloaded) --from-step 60 --steps 30 --only-batch 639a024b373ce19a # trigger batch alone
diagnose takes the forensic path automatically when a directory lacks recorder data. It reconstructs
batch orders from the known pool under multiple seeds (3 for the bracket search, 8 for confirmation) and names a shard
only if necessity + sufficiency + specificity all hold and frozen-weight loss ranking agrees. Otherwise it reports
UNPROVEN with what evidence would settle it. Forensic mode never sets a batch fingerprint or trigger step,
and its verdict is never CONFIRMED (causal).
Measured on the two demo runs, stripped of recorder data: the P0 validation run (MLP) diagnoses FORENSIC (probable), shard-2, bracket [40, 80) โ collapse reproduces under 8/8 reconstructed orders. The transformer demo diagnoses UNPROVEN, no shard named โ its collapse is concentration-dependent and reconstructed orders cannot reproduce it, so no claim is made.
Every existing training-debugger bisects the wrong axis:
git bisect (and rldk's bisect) finds the bad commit by re-running training per commit.Trainsect bisects the training-time axis itself: it treats the optimizer trajectory as a deterministic, replayable computation, reloads optimizer state at step t, replays forward, and binary-searches the exact step-range where the collapse triggers. Then it bisects the data-shard axis with counterfactual replay (collapse disappears iff shard S is excluded) to name the exact shard, batch, and sample.
The output is not a ranked hypothesis. It is a falsifiable, re-runnable minimal repro. No existing tool combines (a) deterministic optimizer-step replay across checkpoints, (b) binary search over the checkpoint timeline, and (c) causal data-shard bisection.
A training step is deterministic given complete state: ฮธt+1, optt+1, rngt+1 = STEP(ฮธt, optt, batcht, rngt). Checkpoints capture model + optimizer + scheduler state; the one-line recorder hook captures dataloader cursor, batch sequence, RNG states, worker seeds, and determinism flags. Before trusting any bisection, Trainsect replays t โ t+k twice and compares SHA-256 parameter hashes โ if they disagree, bisection halts and reports the divergence.
Two stages. Stage A is cheap and runs on logs: change-point detection on the eval-loss curve (metric predicate + binary segmentation). Stage B is causal and runs on replay: P(c) = checkpoint c is healthy (its own eval loss below threshold) AND replaying forward c โ B reproduces the collapse. The predicate is eval-based, not train-loss-spike-based โ a healthy model can show transient train-loss spikes that a spike-based predicate mistakes for collapses. Eval loss is the collapse signal; per-step train loss pinpoints the trigger batch inside an already-established collapse bracket.
Checkpoints at steps {c0 < c1 < โฆ < cn}, known-bad point B. P(c) is monotone in c, so binary search brackets the trigger to one checkpoint interval in O(log n) replays. Then counterfactual shard bisection: replay the trigger interval with each candidate shard's batches replaced by benign substitutes from a known-clean shard. The survivor must pass necessity and sufficiency. Batch โ sample: within the trigger batch, the offending sample is the one whose per-sample loss dominates the batch. The evidence report ships minimal-repro commands for all three counterfactuals.
start_recording(...) / record(step, batch, ...) / stop_recording(...) โ record() is called
after the step's update, preserving pre-update checkpoint semantics. Measured overhead 0.7โ1.0% of step time.
A hook-recorded run replays bit-exact.
Checked against rldk (bisects code commits), culprit (bisects checkpoint releases across runs, robotics), merriv (bisects precomputed labels), NeuralDBG (in-loop hypotheses without replay), and TRAK/influence functions (approximate scores, not causal replay). Manual data bisection exists only as practitioner folklore. Nothing does deterministic optimizer-step replay + checkpoint binary search + data-shard bisect.
Full methodology: docs/P0-methodology.md in the repo.