The parity test is 2,098 lines; about half of them check something

Size review of NVIDIA-NeMo/RL #4507, stacked on #4506. Line counts are from git diff 834241af deb0e146. All permalinks pin the PR head deb0e146. Review comments are in a pending review; the links work once it is published.

Before
The base PR's prefix-recovery test killed one run and checked that the restored run trained. It never compared against a run that was not interrupted.
This PR
Adds that comparison: an uninterrupted run, then a killed-and-restored run, and a 1,037-line script that compares what they trained. Test files only.
Status
Proposed โ€” not implemented. The test is the right idea. About 1,040 of its lines can go without dropping a check; each cut is a review comment, none applied yet.

Three pages on this PR โ€” this one (round 1, an older head): which lines do the checking, and which repeat code that already exists.

The test runs the same small Workplace workload twice and requires every trained row to match token for token. The checks that can fail the run are a short list: rows match, each restored prefix trained exactly once with the right length, no old prefix keys left behind, rewards are not all the same, and generation log-probs agree. Most of the remaining code prints reports, re-checks something already checked, or rebuilds a training-data dump that NeMo-RL already writes.

Where the lines go

The prefix-recovery stack, in stack order: ~21,100 added lines #4500 ยท 8,171 (turn recovery) #4501 #4506 ยท 3,752 #4507 #4508 #4583 #4612 #4655 #4507: 2,098 lines (1,436 test-script + 662 unit-test) keep: script ~740 tests ~320 A โˆ’153 B โˆ’342 C โˆ’195 D โˆ’185 E โˆ’165 kept: checks that can fail the run (light: their unit tests) can go without dropping a check: ~1,040 lines (Aโ€“E, table below) A smaller test with the same checks: ~330 script lines + ~260 unit-test lines.
What the PR writesWhy it can golines
AA wrapper around the trainer that saves every trained row to JSONL (turn_recovery_hook.py:57, parity.py:65-141)

review comment โ€” turn_recovery_hook.py:57

NeMo-RL already writes this file with async_rl.log_full_train_data. A ~20-line probe for staging keys stays. Diff below.~125 + 28 test
BRow order, per-rollout timelines, Kendall tau (parity.py:691-884)

review comment โ€” _gym_prefix_recovery_parity.py:691

Reports only. Its one check, group order inside a step, cannot change what trains: each step trains as one chunk of 4 groups.~215 + 127 test
CHand-written Spearman, Pearson, percentiles (parity.py:548-661)

review comment โ€” _gym_prefix_recovery_parity.py:548

The check reads 3 numbers. With โ‰ฅ99% of log-probs identical, Spearman is ~1 whatever happens. Two numbers in ~15 lines of torch do the job.~125 + 70 test
DA 5-way sorting of token differences that excuses some rows (parity.py:455-523)

review comment โ€” _gym_prefix_recovery_parity.py:516

It also lets real restore bugs pass โ€” see the other page. Every other row already has to match exactly.~70 + 115 test
ESmaller items: a check that can never fail (:346), two checks that repeat others (:285, :367), a fallback for a file format that cannot appear (:304), copied readers and log parsers, report printing

review comment โ€” parity.py:350review comment โ€” parity.py:367review comment โ€” parity.py:285review comment โ€” parity.py:306review comment โ€” parity.py:54

Each repeats a check or a helper that already exists in the same stack.~165

The biggest reuse, as a diff โ€” proposed, not in the PR

Nothing below is implemented, and it has not been run.

Where the fix lands: RL test files only. prefix_recovery.sh, the hook, and the parity script. No product change: the dump already exists. It is written at the advantage stage with token ids, loss mask, advantages, generation log-probs, rewards and sample mask, and a retried step replaces its own file (train_data_dump.py:95). The author's earlier version of this harness (#4320) read these same files.

@@ grpo_async_gym_single_controller_prefix_recovery.sh L162 ยท COMMON_OVERRIDES
 COMMON_OVERRIDES=(
     policy.model_name="$MODEL_NAME"
+    # Built-in per-step dump of every trained row: what the parity check compares.
+    ++async_rl.log_full_train_data=true

@@ _single_controller_turn_recovery_hook.py L57-L139 ยท _TrainingPayloadRecorder
-class _TrainingPayloadRecorder:
-    """Test-only TQPolicy wrapper that records the rows sent to training."""
   โ€ฆ 81 lines: read rows back from the data plane, serialize, append โ€ฆ
+class _StagingKeyProbe:
+    """Record the live staging keys per train step; the dump has everything else."""
   โ€ฆ ~20 lines: wrap begin_train_step, append list_sample_ids(staging) โ€ฆ

@@ _gym_prefix_recovery_parity.py L65-L141 ยท serialize / append / _tensor / _generation_identity
-def serialize_training_batch(
   โ€ฆ 76 more deleted lines โ€ฆ
+def _load_dump(log_dir: Path) -> list[dict[str, Any]]:
+    # One file per optimizer step, written only after the step finished.
+    return [row for path in sorted(log_dir.rglob("train_data_step*.jsonl"))
+            for row in _read_jsonl(path)]

Each run (reference, phase 1, phase 2) gets its own log folder, so phase 1's files never mix with phase 2's. Today phase 1 and phase 2 share one (:234, :285).

review comment โ€” turn_recovery_hook.py:57

So what. The check this PR adds is worth having, and earlier reviews asked for it. But the prefix-recovery stack is already ~21,100 lines, and the size asks on #4506 are still open. Cutting this PR to its checks (about 1,060 lines instead of 2,098) keeps a test that is easy to read when it fails. Separately, top_p=0.000001 means rewards never reach the weights, so steps 2 and 3 cost GPU time without testing learning. That is a separate review comment, not on this page.