File size: 39,412 Bytes
201dc77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
"""G0 control plane (single process): coordinated stop, rank-reduced HALT,
skip caps, process-group timeout + flight recorder env, checkpoint durability
and completeness, RNG + pin persistence.

The collective halves of these properties -- "a signal / HALT / NaN loss on ONE
rank acts on BOTH" -- need two ranks and live in tests/test_trainer_dist.py.
What is pinned here is everything a single process can see: the decisions
themselves, the on-disk protocol, and the helpers' contracts.

Run: .venv/bin/python -m pytest tests/test_control_plane.py -q -m "not gpu and not oracle" -p no:cacheprovider
"""

from __future__ import annotations

import datetime
import logging
import os
import random
import signal
import threading

import numpy as np
import pytest
import torch
from test_model_parity import tiny_config
from test_trainer import _tiny_trainer

import align.train.trainer as trainer_mod
import align.utils.dist as udist
from align.train.probes import TripwireResult
from align.train.trainer import HaltError, LossOutput, checkpoint_problem


@pytest.fixture
def tiny(tiny_model_constants):
    return tiny_config(tiny_model_constants)


def _cfg(**train_attrs):
    """cfg_hook that sets TrainConfig attributes the Trainer reads in __init__
    (they are read with getattr, so they need not exist on the dataclass)."""

    def hook(cfg):
        for k, v in train_attrs.items():
            setattr(cfg.train, k, v)

    return hook


def _signal_at(micro: int, signum=signal.SIGTERM, also_at=()):
    """wrap_adapter that delivers a REAL signal to this process on the
    `micro`-th adapter call (one adapter call == one micro-step)."""

    def wrap(inner):
        n = {"i": 0}

        def adapter(m, batch):
            n["i"] += 1
            if n["i"] == micro or n["i"] in also_at:
                os.kill(os.getpid(), signum)
            return inner(m, batch)

        return adapter

    return wrap


def _loss_transform(fn):
    """wrap_adapter that rewrites each micro's loss: fn(micro_index, loss)."""

    def wrap(inner):
        n = {"i": 0}

        def adapter(m, batch):
            n["i"] += 1
            out = inner(m, batch)
            return LossOutput(loss=fn(n["i"], out.loss), n_tokens=out.n_tokens, metrics=out.metrics)

        return adapter

    return wrap


def _weights(trainer) -> dict:
    return {k: v.detach().clone() for k, v in trainer.model.state_dict().items()}


def _same(a: dict, b: dict) -> bool:
    return a.keys() == b.keys() and all(torch.equal(a[k], b[k]) for k in a)


# ---------------------------------------------------------------------------
# 1. coordinated stop: signal + wall clock
# ---------------------------------------------------------------------------


def test_sigterm_mid_window_stops_at_the_boundary_and_resumes_bit_exact(tiny, tmp_workdir):
    """SIGTERM-MID-SAVE / MID-WINDOW (REFERENCE_GAP_ANALYSIS gap 1).

    The signal arrives at micro 3: the first micro of the second accumulation
    window (accum 2). The run must NOT stop there -- a checkpoint holds no
    gradients, so a mid-window save resumes into a different run. It stops at
    micro 4, the boundary, with stop_reason "signal", and writes a checkpoint
    that a fresh process resumes from to weights BIT-EQUAL to the run nobody
    interrupted. save_every is 8, so the micro-4 checkpoint can only exist
    because the stop wrote it.
    """
    ref, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "ref"), save_every_micro_steps=8)
    ref.train()
    want = _weights(ref)

    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(
        tiny, run, save_every_micro_steps=8, wrap_adapter=_signal_at(3)
    )
    summary = t.train()
    assert summary["stop_reason"] == "signal", summary
    assert summary["micro_step"] == 4 and summary["optimizer_step"] == 2, summary
    assert summary["checkpoints_written"] == [4], summary
    assert os.listdir(os.path.join(run, "checkpoints")) == ["micro_00000004"]
    assert checkpoint_problem(os.path.join(run, "checkpoints", "micro_00000004")) is None

    t2, _ = _tiny_trainer(tiny, run, save_every_micro_steps=8)
    assert t2.resume_from_latest().endswith("micro_00000004")
    assert t2.micro_step == 4
    # vacuity guard: the stopped run really was behind the finished one
    assert not _same(_weights(t2), want)
    t2.train()
    assert _same(_weights(t2), want), "resume from a signal stop diverged from the uninterrupted run"


def test_signal_handlers_are_restored_after_train_and_after_an_exception(tiny, tmp_workdir):
    """Handlers live only for the duration of `train()`: a Trainer that leaked
    its SIGTERM handler would swallow the NEXT stage's signals (stage scripts
    run several trainers in one process)."""
    before = {s: signal.getsignal(s) for s in (signal.SIGTERM, signal.SIGINT)}
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    t.train()
    assert {s: signal.getsignal(s) for s in before} == before

    def boom(inner):
        def adapter(m, batch):
            raise ValueError("adapter failed")

        return adapter

    t2, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "b"), wrap_adapter=boom)
    with pytest.raises(ValueError, match="adapter failed"):
        t2.train()
    assert {s: signal.getsignal(s) for s in before} == before


def test_handler_is_installed_during_train_and_only_sets_a_flag(tiny, tmp_workdir):
    """While train() runs, SIGTERM is OURS (flag only) -- not the default
    action that kills the process wherever it is."""
    seen = {}

    def wrap(inner):
        def adapter(m, batch):
            seen.setdefault("handler", signal.getsignal(signal.SIGTERM))
            return inner(m, batch)

        return adapter

    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"), wrap_adapter=wrap)
    t.train()
    assert seen["handler"] not in (signal.SIG_DFL, signal.SIG_IGN, None)
    assert getattr(seen["handler"], "__self__", None) is t._stop_signals


def test_second_sigint_escapes_and_leaves_no_partial_checkpoint(tiny, tmp_workdir):
    """The first SIGINT is honoured at the boundary; a SECOND one raises
    KeyboardInterrupt so an operator is never locked out by a slow stop. The
    unwind must not leave a `.partial` that looks resumable."""
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(
        tiny, run, wrap_adapter=_signal_at(1, signal.SIGINT, also_at=(2,)), save_every_micro_steps=8
    )
    with pytest.raises(KeyboardInterrupt):
        t.train()
    assert not [d for d in os.listdir(os.path.join(run, "checkpoints")) if d.endswith(".partial")]
    assert signal.getsignal(signal.SIGINT) is signal.default_int_handler


def test_a_stop_request_wins_over_max_micro_steps(tiny, tmp_workdir):
    """A capped run normally writes NO end checkpoint (probes must not spend
    minutes gathering 14GB). A SIGTERM during one must still save: otherwise
    the window's work is discarded, which is the defect."""
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(
        tiny, run, save_every_micro_steps=8, max_micro_steps=4, wrap_adapter=_signal_at(3)
    )
    summary = t.train()
    assert summary["stop_reason"] == "signal" and summary["micro_step"] == 4
    assert os.listdir(os.path.join(run, "checkpoints")) == ["micro_00000004"]


def test_a_capped_run_without_a_signal_still_writes_no_end_checkpoint(tiny, tmp_workdir):
    """The other direction: the stop-save must not leak into ordinary capped
    runs (the probe-artifact rule in train())."""
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(tiny, run, save_every_micro_steps=8, max_micro_steps=4)
    assert t.train()["stop_reason"] == "max_micro_steps"
    assert os.listdir(os.path.join(run, "checkpoints")) == []


def test_wall_clock_limit_stops_at_the_first_boundary_and_saves(tiny, tmp_workdir):
    """`train.max_wall_seconds`: a 1ns limit has expired by the first boundary
    (micro 2); the stop is at that boundary, never at micro 1."""
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(
        tiny, run, save_every_micro_steps=8, cfg_hook=_cfg(max_wall_seconds=1e-9)
    )
    summary = t.train()
    assert summary["stop_reason"] == "wall_clock", summary
    assert summary["micro_step"] == 2 and summary["optimizer_step"] == 1, summary
    assert os.listdir(os.path.join(run, "checkpoints")) == ["micro_00000002"]


def test_wall_clock_generous_limit_and_default_do_not_stop_the_run(tiny, tmp_workdir):
    for name, hook in (("none", None), ("big", _cfg(max_wall_seconds=3600.0))):
        t, _ = _tiny_trainer(
            tiny, os.path.join(tmp_workdir, name), save_every_micro_steps=8, cfg_hook=hook
        )
        summary = t.train()
        assert summary["stop_reason"] == "data_exhausted" and summary["micro_step"] == 8, summary


@pytest.mark.parametrize("bad", [0, 0.0, -5])
def test_wall_clock_limit_must_be_positive(tiny, tmp_workdir, bad):
    with pytest.raises(ValueError, match="max_wall_seconds"):
        _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"), cfg_hook=_cfg(max_wall_seconds=bad))


def test_train_off_the_main_thread_runs_without_handlers(tiny, tmp_workdir, caplog):
    """signal.signal() raises ValueError off the main thread; a harness that
    runs the trainer in a worker thread must degrade (loudly), not crash."""
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    box = {}
    with caplog.at_level(logging.WARNING, logger="align.train.trainer"):
        th = threading.Thread(target=lambda: box.update(summary=t.train()))
        th.start()
        th.join(120)
    assert box["summary"]["micro_step"] == 8
    assert "cannot be installed" in caplog.text
    assert t._stop_signals.installed is False


# ---------------------------------------------------------------------------
# 2. rank-reduced HALT (the multi-rank half is in test_trainer_dist.py)
# ---------------------------------------------------------------------------


class _Halting:
    HALT = True
    INPUT_KEYS: tuple = ()

    def __init__(self, at):
        self.at, self.n = at, 0

    def update(self, _x):
        self.n += 1
        return TripwireResult(self.n >= self.at, "stop now", value=3.5)


def test_halt_tripwire_raises_haltError_with_the_original_message(tiny, tmp_workdir):
    """Single process: the HALT is raised exactly as before (RuntimeError, same
    text) -- the rank-reduction must not change what an operator reads."""
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    t.tripwires = {"grad_norm": _Halting(at=3)}
    with pytest.raises(RuntimeError, match=r"HALT tripwire 'grad_norm' fired at micro_step 3: stop now \(value=3.5\)") as ei:
        t.train()
    assert isinstance(ei.value, HaltError)
    assert t.tripwire_alarms[-1]["alarm"] == "grad_norm"


def test_agree_or_raise_peer_failure_makes_a_healthy_rank_raise(tiny, tmp_workdir, monkeypatch):
    """The reduction itself, simulated: THIS rank is healthy, rank 1 reports a
    failure. Without the reduction this rank would carry on into the next
    collective alone."""
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    monkeypatch.setattr(trainer_mod, "_dist_world_size", lambda: 2)
    monkeypatch.setattr(udist, "any_rank", lambda flag: True)  # a peer said yes
    monkeypatch.setattr(
        trainer_mod, "all_gather_objects", lambda obj: [None, ("HaltError: boom on rank 1", 80)]
    )
    with pytest.raises(HaltError, match=r"\[rank 1\] HaltError: boom on rank 1.*rank-reduced during x") as ei:
        t._agree_or_raise(None, "x")
    assert type(ei.value) is HaltError and ei.value.exit_code == 80


def test_agree_or_raise_keeps_the_original_exception_on_the_failing_rank(tiny, tmp_workdir, monkeypatch):
    """A non-HALT failure (a KeyError from a missing metric) is re-raised AS
    ITSELF where it happened -- the traceback an engineer needs -- while peers
    get the HaltError summary."""
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    monkeypatch.setattr(trainer_mod, "_dist_world_size", lambda: 2)
    monkeypatch.setattr(udist, "any_rank", lambda flag: True)
    err = KeyError("rewards/margins")
    monkeypatch.setattr(trainer_mod, "all_gather_objects", lambda obj: [obj, None])
    with pytest.raises(KeyError):
        t._agree_or_raise(err, "tripwire check")


def test_agree_or_raise_is_silent_when_no_rank_failed(tiny, tmp_workdir, monkeypatch):
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"))
    monkeypatch.setattr(trainer_mod, "_dist_world_size", lambda: 2)
    monkeypatch.setattr(udist, "any_rank", lambda flag: False)
    t._agree_or_raise(None, "x")  # must not raise


def test_a_missing_tripwire_input_still_fails_loudly(tiny, tmp_workdir):
    """The 'KeyError surfaces' contract of _check_tripwires survives the
    reduction wrapper (single process re-raises the original)."""
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, "a"), armed=("logps_chosen",))
    t.tripwires["logps_chosen"].INPUT_KEYS = ()  # let the arm-time test pass
    with pytest.raises(KeyError):
        t.train()


# ---------------------------------------------------------------------------
# 3. skip caps
# ---------------------------------------------------------------------------

NAN = float("nan")


def _nan_grads_for(micros):
    """Loss * NaN on the listed micros: NaN loss AND NaN grads (the grad gate
    skips the step)."""
    return _loss_transform(lambda i, loss: loss * NAN if i in micros else loss)


def test_consecutive_skipped_steps_beyond_the_cap_halt_the_run(tiny, tmp_workdir):
    """THE SILENT-SKIP RUN. Micros 1-8 are four consecutive NaN windows. The
    default cap is 3 CONSECUTIVE, so the fourth is the one that halts -- at its
    boundary (micro 8), with a message naming the knob."""
    t, _ = _tiny_trainer(
        tiny, os.path.join(tmp_workdir, "a"), n_batches=12, wrap_adapter=_nan_grads_for(range(1, 9))
    )
    with pytest.raises(HaltError, match=r"4 consecutive bad optimizer steps exceed train\.max_consecutive_skipped_steps=3"):
        t.train()
    assert t.micro_step == 8
    assert t.train_module.skipped_steps == 4, "vacuous: the grad gate did not skip"
    assert t.tripwire_alarms[-1]["alarm"] == "skip_cap"


def test_exactly_the_cap_is_allowed_and_a_good_step_resets_the_streak(tiny, tmp_workdir):
    """3 bad windows (== cap) then a good one then 3 more: never more than 3 in
    a row, so no halt. Guards the off-by-one in both directions."""
    bad = set(range(1, 7)) | set(range(9, 15))  # windows 1-3 and 5-7 bad, 4 good
    t, _ = _tiny_trainer(
        tiny, os.path.join(tmp_workdir, "a"), n_batches=10, epochs=2, wrap_adapter=_nan_grads_for(bad),
        save_every_micro_steps=100, max_micro_steps=16,
    )
    t.train()
    assert t.skip_stats["bad_total"] == 6 and t.skip_stats["consecutive"] == 0
    assert t.skip_stats["boundaries"] == 8


def test_non_finite_loss_counts_even_when_the_gradients_are_finite(tiny, tmp_workdir):
    """'Non-finite loss must also count, not only grads.' `loss + nan` has a
    NaN value and an UNCHANGED, finite gradient: the optimizer applies every
    step (skipped_steps stays 0), so only the loss check can see it."""
    t, _ = _tiny_trainer(
        tiny,
        os.path.join(tmp_workdir, "a"),
        n_batches=12,
        wrap_adapter=_loss_transform(lambda i, loss: loss + NAN),
    )
    with pytest.raises(HaltError, match="non-finite loss"):
        t.train()
    assert t.train_module.skipped_steps == 0, "vacuous: the grad gate fired, not the loss check"
    assert t.optimizer_step == 4 and t.micro_step == 8


def test_one_non_finite_micro_in_a_window_marks_the_whole_step_bad(tiny, tmp_workdir):
    t, _ = _tiny_trainer(
        tiny,
        os.path.join(tmp_workdir, "a"),
        wrap_adapter=_loss_transform(lambda i, loss: loss + NAN if i == 3 else loss),
        save_every_micro_steps=100,
        max_micro_steps=4,
    )
    t.train()
    assert t.skip_stats == {"consecutive": 1, "bad_total": 1, "boundaries": 2}
    assert t.train_module.skipped_steps == 0


def _health(tiny, tmp_workdir, name, **cfg_attrs):
    t, _ = _tiny_trainer(tiny, os.path.join(tmp_workdir, name), cfg_hook=_cfg(**cfg_attrs))
    return t


def test_fractional_cap_waits_for_100_steps_then_halts_above_5_percent(tiny, tmp_workdir):
    """>5% bad, evaluated only once 100 boundaries exist. Driven through
    `_record_step_health` (the decision) with a pattern that is never more than
    one bad step in a row, so the consecutive cap cannot be what fires."""
    t = _health(tiny, tmp_workdir, "frac")
    # 3 bad of the first 10 = 30%: far above 5%, but under the minimum sample
    for i in range(10):
        t._record_step_health(False, i in (1, 5, 8))
    assert t.skip_stats["bad_total"] == 3  # no halt yet
    # 90 more good steps: boundary 100, 3/100 = 3% -> still fine
    for _ in range(89):
        t._record_step_health(False, False)
    t._record_step_health(False, False)
    assert t.skip_stats["boundaries"] == 100
    # push to 6/106 > 5%
    with pytest.raises(HaltError, match=r"train\.max_skipped_fraction=0\.05"):
        for i in range(10):
            t._record_step_health(False, i % 2 == 0)


def test_fractional_cap_exactly_at_the_threshold_does_not_halt(tiny, tmp_workdir):
    t = _health(tiny, tmp_workdir, "edge")
    for i in range(100):  # 5 of 100 = 5.0%: not ABOVE the cap
        t._record_step_health(False, i % 20 == 3)
    assert t.skip_stats == {"consecutive": 0, "bad_total": 5, "boundaries": 100}


def test_fractional_cap_halts_at_the_hundredth_step_when_already_above(tiny, tmp_workdir):
    t = _health(tiny, tmp_workdir, "fire")
    with pytest.raises(HaltError, match="10 of 100 optimizer steps were bad"):
        for i in range(100):
            t._record_step_health(False, i % 10 == 0)
    assert t.skip_stats["boundaries"] == 100


def test_skip_caps_can_be_overridden_or_disabled_by_config(tiny, tmp_workdir):
    t = _health(tiny, tmp_workdir, "off", max_consecutive_skipped_steps=None, max_skipped_fraction=None)
    for _ in range(300):
        t._record_step_health(True, True)  # never halts
    t2 = _health(tiny, tmp_workdir, "tight", max_consecutive_skipped_steps=0)
    with pytest.raises(HaltError, match=r"1 consecutive bad optimizer steps exceed .*=0"):
        t2._record_step_health(False, True)
    t3 = _health(tiny, tmp_workdir, "default")
    assert (t3.max_consecutive_skipped, t3.max_skipped_fraction) == (3, 0.05)


@pytest.mark.parametrize(
    "attrs",
    [{"max_consecutive_skipped_steps": -1}, {"max_skipped_fraction": 1.5}, {"max_skipped_fraction": -0.1}],
)
def test_skip_cap_config_is_validated(tiny, tmp_workdir, attrs):
    with pytest.raises(ValueError):
        _health(tiny, tmp_workdir, "bad", **attrs)


# ---------------------------------------------------------------------------
# 4. process-group timeout + NCCL flight recorder
# ---------------------------------------------------------------------------


def test_pg_timeout_default_and_env_override():
    assert udist.pg_timeout_s({}) == 1800.0
    assert udist.pg_timeout_s({"ALIGN_PG_TIMEOUT_S": "90"}) == 90.0
    assert udist.pg_timeout_s({"ALIGN_PG_TIMEOUT_S": " "}) == 1800.0


@pytest.mark.parametrize("raw", ["30m", "0", "-1", "inf", "nan", "abc"])
def test_pg_timeout_rejects_values_that_would_silently_disarm_the_watchdog(raw):
    with pytest.raises(ValueError, match="ALIGN_PG_TIMEOUT_S"):
        udist.pg_timeout_s({"ALIGN_PG_TIMEOUT_S": raw})


def _isolate_env(monkeypatch, *names):
    """Make monkeypatch restore these env vars (to absent) at teardown, so what
    `init_distributed` exports does not leak into later tests."""
    for n in names:
        monkeypatch.setenv(n, "placeholder")
        monkeypatch.delenv(n)


FR_VARS = (
    "TORCH_FR_BUFFER_SIZE",
    "TORCH_NCCL_TRACE_BUFFER_SIZE",
    "TORCH_NCCL_DUMP_ON_TIMEOUT",
    "TORCH_FR_DUMP_TEMP_FILE",
    "TORCH_NCCL_DEBUG_INFO_TEMP_FILE",
)


def test_init_distributed_passes_an_explicit_timeout(monkeypatch):
    import torch.distributed as dist

    calls = []
    monkeypatch.setenv("WORLD_SIZE", "2")
    monkeypatch.setenv("ALIGN_PG_TIMEOUT_S", "77")
    monkeypatch.setattr(dist, "is_initialized", lambda: False)
    monkeypatch.setattr(dist, "init_process_group", lambda **kw: calls.append(kw))
    udist.init_distributed(backend="gloo")
    assert calls == [{"backend": "gloo", "timeout": datetime.timedelta(seconds=77)}]
    # explicit argument beats the environment
    calls.clear()
    udist.init_distributed(backend="gloo", timeout_s=5)
    assert calls[0]["timeout"] == datetime.timedelta(seconds=5)
    # the default (no env) is 30 minutes, not torch's backend-dependent one
    calls.clear()
    monkeypatch.delenv("ALIGN_PG_TIMEOUT_S")
    udist.init_distributed(backend="gloo")
    assert calls[0]["timeout"] == datetime.timedelta(minutes=30)


def test_init_distributed_nccl_arms_the_flight_recorder_before_init(monkeypatch, tmp_workdir):
    import torch.distributed as dist

    _isolate_env(monkeypatch, *FR_VARS)
    order = []
    monkeypatch.setenv("WORLD_SIZE", "2")
    monkeypatch.setattr(dist, "is_initialized", lambda: False)
    monkeypatch.setattr(
        dist,
        "init_process_group",
        lambda **kw: order.append(("init", os.environ.get("TORCH_NCCL_DUMP_ON_TIMEOUT"))),
    )
    monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
    udist.init_distributed(backend="nccl", run_dir=tmp_workdir)
    # the env was already set when init_process_group ran
    assert order == [("init", "1")]


def test_gloo_init_does_not_touch_the_nccl_environment(monkeypatch):
    import torch.distributed as dist

    _isolate_env(monkeypatch, *FR_VARS)
    monkeypatch.setenv("WORLD_SIZE", "2")
    monkeypatch.setattr(dist, "is_initialized", lambda: False)
    monkeypatch.setattr(dist, "init_process_group", lambda **kw: None)
    udist.init_distributed(backend="gloo")
    assert not [v for v in FR_VARS if v in os.environ]


def test_flight_recorder_env_defaults_live_under_the_run_dir_and_never_override(tmp_workdir):
    env: dict = {}
    applied = udist.configure_nccl_diagnostics(tmp_workdir, environ=env, torch_version="2.11.0+cu128")
    assert env["TORCH_FR_BUFFER_SIZE"] == "20000"
    assert "TORCH_NCCL_TRACE_BUFFER_SIZE" not in env
    assert env["TORCH_NCCL_DUMP_ON_TIMEOUT"] == "1"
    prefix = env["TORCH_FR_DUMP_TEMP_FILE"]
    assert prefix.startswith(os.path.join(tmp_workdir, "nccl_trace") + os.sep), prefix
    assert env["TORCH_NCCL_DEBUG_INFO_TEMP_FILE"] == prefix
    assert os.path.isdir(os.path.dirname(prefix)), "the dump directory must exist before a timeout"
    assert not prefix.startswith("/tmp"), "the torch default (/tmp/nccl_trace_rank_) is forbidden here"
    assert set(applied) == set(FR_VARS) - {"TORCH_NCCL_TRACE_BUFFER_SIZE"}

    keep = {"TORCH_FR_BUFFER_SIZE": "5", "TORCH_NCCL_DUMP_ON_TIMEOUT": "0"}
    env2 = dict(keep)
    udist.configure_nccl_diagnostics(tmp_workdir, environ=env2, torch_version="2.11.0")
    assert {k: env2[k] for k in keep} == keep, "an operator's explicit export must win"


def test_flight_recorder_variable_names_exist_in_the_installed_torch():
    """The names are not guesses (CLAUDE.md: verify against the installed
    torch). If a torch upgrade renames one, this fails instead of the recorder
    going silently dark."""
    import mmap

    root = os.path.dirname(torch.__file__)
    hpp = os.path.join(root, "include/torch/csrc/distributed/c10d/ProcessGroupNCCL.hpp")
    lib = os.path.join(root, "lib/libtorch_cpu.so")
    if not (os.path.isfile(hpp) and os.path.isfile(lib)):
        pytest.skip("torch headers / libtorch_cpu.so not shipped in this install")
    header = open(hpp).read()
    for name in ("TORCH_NCCL_TRACE_BUFFER_SIZE", "TORCH_NCCL_DUMP_ON_TIMEOUT"):
        assert name in header, name
    with open(lib, "rb") as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
        for name in ("TORCH_FR_DUMP_TEMP_FILE", "TORCH_NCCL_DEBUG_INFO_TEMP_FILE"):
            assert mm.find(name.encode()) != -1, name


# ---------------------------------------------------------------------------
# 5. checkpoint durability + completeness
# ---------------------------------------------------------------------------


def _two_checkpoints(tiny, run_dir, **kw):
    t, _ = _tiny_trainer(tiny, run_dir, save_every_micro_steps=4, **kw)
    t.train()
    root = os.path.join(run_dir, "checkpoints")
    assert sorted(os.listdir(root)) == ["micro_00000004", "micro_00000008"]
    return t, root


def test_every_file_and_directory_is_fsynced_before_the_rename(tiny, tmp_workdir, monkeypatch):
    """THE ATOMIC-RENAME-IS-NOT-DURABLE DEFECT. Recorded as a timeline: every
    file in the checkpoint, and the staging directory, are fsynced BEFORE
    `os.rename(.partial -> final)`; the checkpoint root is fsynced AFTER it so
    the new directory entry itself survives a power loss."""
    events = []
    real_fsync, real_rename = os.fsync, os.rename

    def spy_fsync(fd):
        events.append(("fsync", os.readlink(f"/proc/self/fd/{fd}")))
        return real_fsync(fd)

    def spy_rename(src, dst, *a, **k):
        events.append(("rename", src, dst))
        return real_rename(src, dst, *a, **k)

    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(tiny, run, save_every_micro_steps=8, max_micro_steps=8)
    monkeypatch.setattr(os, "fsync", spy_fsync)
    monkeypatch.setattr(os, "rename", spy_rename)
    t.save_checkpoint()
    monkeypatch.undo()

    root = os.path.realpath(os.path.join(run, "checkpoints"))
    staging = os.path.join(root, "micro_00000000.partial")
    renames = [i for i, e in enumerate(events) if e[0] == "rename" and e[1].endswith(".partial")]
    assert len(renames) == 1, events
    r = renames[0]
    before = {os.path.relpath(e[1], staging) for e in events[:r] if e[0] == "fsync" and e[1].startswith(staging)}
    final_files = set(os.listdir(os.path.join(root, "micro_00000000")))
    assert {"model.safetensors", "resume.pt", "config.json"} <= final_files
    assert final_files <= before, f"not fsynced before the rename: {sorted(final_files - before)}"
    assert "." in before, "the staging DIRECTORY entry must be fsynced before the rename"
    assert ("fsync", root) in events[r + 1 :], "the checkpoint root must be fsynced after the rename"


def test_a_complete_checkpoint_passes_and_the_newest_is_returned(tiny, tmp_workdir):
    t, root = _two_checkpoints(tiny, os.path.join(tmp_workdir, "run"))
    for d in os.listdir(root):
        assert checkpoint_problem(os.path.join(root, d)) is None
    assert t.latest_checkpoint() == os.path.join(root, "micro_00000008")


def _truncate_half(path):
    size = os.path.getsize(path)
    with open(path, "r+b") as f:
        f.truncate(size // 2)


MUTATIONS = {
    "weights_truncated": lambda d: _truncate_half(os.path.join(d, "model.safetensors")),
    "weights_zero_length": lambda d: open(os.path.join(d, "model.safetensors"), "wb").close(),
    "weights_missing": lambda d: os.remove(os.path.join(d, "model.safetensors")),
    "resume_missing": lambda d: os.remove(os.path.join(d, "resume.pt")),
    "resume_truncated": lambda d: _truncate_half(os.path.join(d, "resume.pt")),
    "resume_empty": lambda d: open(os.path.join(d, "resume.pt"), "wb").close(),
}


@pytest.mark.parametrize("mutation", sorted(MUTATIONS))
def test_incomplete_newest_checkpoint_is_skipped_with_a_warning_and_kept(
    tiny, tmp_workdir, mutation, caplog
):
    """A host seizure can leave a `micro_N` directory whose rename survived and
    whose data did not. `latest_checkpoint` must hand back the previous good one,
    warn, and NOT delete the evidence."""
    run = os.path.join(tmp_workdir, "run")
    t, root = _two_checkpoints(tiny, run)
    bad = os.path.join(root, "micro_00000008")
    MUTATIONS[mutation](bad)
    assert checkpoint_problem(bad) is not None

    with caplog.at_level(logging.WARNING, logger="align.train.trainer"):
        got = t.latest_checkpoint()
    assert got == os.path.join(root, "micro_00000004"), got
    assert "INCOMPLETE checkpoint micro_00000008" in caplog.text
    assert os.path.isdir(bad), "an incomplete checkpoint must be skipped, never deleted"

    # and a resume actually takes the good one
    t2, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4)
    assert t2.resume_from_latest() == got and t2.micro_step == 4


def test_all_checkpoints_incomplete_means_no_checkpoint(tiny, tmp_workdir):
    t, root = _two_checkpoints(tiny, os.path.join(tmp_workdir, "run"))
    for d in os.listdir(root):
        os.remove(os.path.join(root, d, "resume.pt"))
    assert t.latest_checkpoint() is None


def test_sharded_weights_are_checked_against_the_index(tiny, tmp_workdir):
    """A multi-shard layout (`model.safetensors.index.json`): every shard it
    names must exist and be complete."""
    import json

    _t, root = _two_checkpoints(tiny, os.path.join(tmp_workdir, "run"))
    d = os.path.join(root, "micro_00000008")
    index = os.path.join(d, "model.safetensors.index.json")
    with open(index, "w") as f:
        json.dump({"weight_map": {"a": "model.safetensors"}}, f)
    assert checkpoint_problem(d) is None
    with open(index, "w") as f:
        json.dump({"weight_map": {"a": "model.safetensors", "b": "model-00002-of-00002.safetensors"}}, f)
    assert "missing weights file model-00002-of-00002.safetensors" in checkpoint_problem(d)
    with open(index, "w") as f:
        f.write("{not json")
    assert "index.json" in checkpoint_problem(d)


def test_per_rank_optimizer_files_must_all_be_present(tiny, tmp_workdir):
    """When the manifest says per_rank_files, the newest checkpoint is complete
    only if EVERY saved rank's optimizer file is: a missing one makes a resume
    die on that rank alone, mid-load."""
    run = os.path.join(tmp_workdir, "run")
    t, root = _two_checkpoints(tiny, run, cfg_hook=_cfg(per_rank_optimizer_state=True))
    d = os.path.join(root, "micro_00000008")
    assert checkpoint_problem(d) is None
    assert os.path.isfile(os.path.join(d, "optimizer_rank0.pt"))

    # the manifest names two ranks but only rank 0's file exists
    state = torch.load(os.path.join(d, "resume.pt"), weights_only=True, map_location="cpu")
    state["optimizer_state"] = {"layout": "per_rank_files", "world_size": 2}
    torch.save(state, os.path.join(d, "resume.pt"))
    assert "optimizer_rank1.pt" in checkpoint_problem(d)

    state["optimizer_state"] = {"layout": "per_rank_files", "world_size": 1}
    torch.save(state, os.path.join(d, "resume.pt"))
    os.remove(os.path.join(d, "optimizer_rank0.pt"))
    assert "optimizer_rank0.pt" in checkpoint_problem(d)
    assert t.latest_checkpoint() == os.path.join(root, "micro_00000004")

    _truncate_half(os.path.join(root, "micro_00000004", "optimizer_rank0.pt"))
    assert t.latest_checkpoint() is None, "a cut optimizer file must disqualify its checkpoint"


def test_partial_directories_are_still_discarded_at_startup_and_never_listed(tiny, tmp_workdir):
    run = os.path.join(tmp_workdir, "run")
    os.makedirs(os.path.join(run, "checkpoints", "micro_00000100.partial"))
    t, _ = _tiny_trainer(tiny, run)
    assert os.listdir(os.path.join(run, "checkpoints")) == []
    assert t.latest_checkpoint() is None


# ---------------------------------------------------------------------------
# 6. RNG state + pinned checkpoints + skip stats persisted in resume.pt
# ---------------------------------------------------------------------------


def _draw():
    return [float(torch.rand(1)), random.random(), float(np.random.rand()), int(np.random.randint(1 << 30))]


def test_rng_state_is_restored_for_torch_python_and_numpy(tiny, tmp_workdir):
    """A resumed process re-seeds from scratch and replays the original run's
    first draws; with the state in resume.pt it CONTINUES the stream the saving
    process was on."""
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4, max_micro_steps=4)
    t.train()  # the cadence writes micro 4
    want = _draw()

    udist.seed_everything(999)  # a different stream, as a fresh process would have
    assert _draw() != want, "vacuous: the reseed reproduced the saved stream"
    t2, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4)
    t2.resume_from_latest()
    assert _draw() == want


def test_resume_payload_holds_rng_pins_and_skip_stats_and_is_weights_only_loadable(tiny, tmp_workdir):
    run = os.path.join(tmp_workdir, "run")
    t, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4, max_micro_steps=4)
    t._pin_next_save = True
    t.train()
    state = torch.load(
        os.path.join(run, "checkpoints", "micro_00000004", "resume.pt"),
        weights_only=True,
        map_location="cpu",
    )
    assert state["pinned_checkpoints"] == [4]
    assert state["skip_stats"] == {"consecutive": 0, "bad_total": 0, "boundaries": 2}
    (rng,) = state["rng_per_rank"]
    assert {"torch", "python", "numpy"} <= rng.keys()
    assert rng["torch"].dtype == torch.uint8


def test_pinned_checkpoint_survives_a_resume_and_the_following_prune(tiny, tmp_workdir):
    """R5 PERSISTENCE. Before: the pin set lived in process memory. A resumed
    run registered the resumed checkpoint as ITS OWN (so keep-N could prune it)
    and had forgotten it was pinned -- the first save after the resume deleted
    the one checkpoint it had been told to keep. keep_checkpoints=1 makes the
    prune immediate."""
    run = os.path.join(tmp_workdir, "run")
    a, _ = _tiny_trainer(
        tiny, run, n_batches=10, epochs=2, save_every_micro_steps=4, keep_checkpoints=1, max_micro_steps=4
    )
    a._pin_next_save = True
    a.train()
    root = os.path.join(run, "checkpoints")
    assert os.listdir(root) == ["micro_00000004"]

    b, _ = _tiny_trainer(
        tiny, run, n_batches=10, epochs=2, save_every_micro_steps=4, keep_checkpoints=1, max_micro_steps=16
    )
    b.resume_from_latest()
    assert b.pinned_checkpoints == {4}
    b.train()
    assert sorted(os.listdir(root)) == ["micro_00000004", "micro_00000016"], os.listdir(root)


def test_resume_adopts_older_checkpoints_so_keep_n_does_not_leak_one_per_resume(tiny, tmp_workdir):
    """TRACK M (2026-10-12, OLMoE 4-GPU SIGTERM/resume): keep_checkpoints=2 left micro_40, _44 and _80 on disk
    (83GB, disk 95% full). The interrupted attempt wrote 40 and 44; the new process registered only 44, so after
    saving 80 its list was [44, 80] and 40 was unprunable forever -- one leaked checkpoint per resume. Directories
    numbered BELOW the resumed one are this lineage's older saves and are adopted. A HIGHER-numbered directory is a
    stale-run zombie (the Arm A rerun incident that made pruning per-process) and must survive untouched.
    MUTANT (resume registers only the resumed step): micro_00000004 is still on disk at the end."""
    run = os.path.join(tmp_workdir, "run")
    a, _ = _tiny_trainer(
        tiny, run, n_batches=10, epochs=2, save_every_micro_steps=4, keep_checkpoints=3, max_micro_steps=8
    )
    a.train()
    root = os.path.join(run, "checkpoints")
    assert sorted(os.listdir(root)) == ["micro_00000004", "micro_00000008"]
    os.makedirs(os.path.join(root, "micro_00000999"))  # a zombie from an unrelated older run: incomplete, higher-numbered

    b, _ = _tiny_trainer(
        tiny, run, n_batches=10, epochs=2, save_every_micro_steps=4, keep_checkpoints=2, max_micro_steps=12
    )
    assert b.resume_from_latest().endswith("micro_00000008")
    assert b.checkpoints_written == [4, 8], "older save adopted, zombie not, and the resumed one stays LAST (= newest)"
    b.train()
    assert sorted(os.listdir(root)) == ["micro_00000008", "micro_00000012", "micro_00000999"], os.listdir(root)


def test_skip_stats_survive_a_resume(tiny, tmp_workdir):
    """A resume must not forgive a run that was already skipping."""
    run = os.path.join(tmp_workdir, "run")
    a, _ = _tiny_trainer(
        tiny, run, save_every_micro_steps=4, max_micro_steps=4, wrap_adapter=_nan_grads_for({1, 2, 3, 4})
    )
    a.train()
    assert a.skip_stats == {"consecutive": 2, "bad_total": 2, "boundaries": 2}
    b, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4)
    b.resume_from_latest()
    assert b.skip_stats == a.skip_stats


def test_a_checkpoint_from_before_g0_still_resumes(tiny, tmp_workdir, caplog):
    """Back-compat: strip the new keys from resume.pt (what every existing
    checkpoint looks like). Resume must work, warn about the RNG, and leave the
    defaults in place -- not KeyError."""
    run = os.path.join(tmp_workdir, "run")
    a, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4, max_micro_steps=4)
    a.train()
    path = os.path.join(run, "checkpoints", "micro_00000004", "resume.pt")
    state = torch.load(path, weights_only=True, map_location="cpu")
    for k in ("rng_per_rank", "pinned_checkpoints", "skip_stats"):
        del state[k]
    torch.save(state, path)
    # a pre-G0 checkpoint also predates the integrity sidecar (audit U01-3)
    os.remove(os.path.join(os.path.dirname(path), "integrity.json"))
    b, _ = _tiny_trainer(tiny, run, save_every_micro_steps=4)
    with caplog.at_level(logging.WARNING, logger="align.train.trainer"):
        b.resume_from_latest()
    assert "predates RNG persistence" in caplog.text
    assert b.micro_step == 4 and b.pinned_checkpoints == set()
    assert b.skip_stats == {"consecutive": 0, "bad_total": 0, "boundaries": 0}


def test_rng_capture_roundtrips_exactly_and_is_weights_only_clean(tmp_workdir):
    udist.seed_everything(3)
    sd = udist.capture_rng_state()
    want = _draw()
    path = os.path.join(tmp_workdir, "rng.pt")
    torch.save(sd, path)
    loaded = torch.load(path, weights_only=True, map_location="cpu")
    udist.seed_everything(4)
    udist.restore_rng_state(loaded)
    assert _draw() == want


def test_cuda_rng_is_captured_for_the_current_device_only(monkeypatch):
    """Faked CUDA (CPU-only host rules): only the CURRENT device's state is
    captured -- `get_rng_state_all` would create a context on every visible GPU
    from every rank -- and only when CUDA is already initialised."""
    calls = []
    monkeypatch.setattr(torch.cuda, "is_available", lambda: True)
    monkeypatch.setattr(torch.cuda, "is_initialized", lambda: True)
    monkeypatch.setattr(torch.cuda, "current_device", lambda: 2)
    monkeypatch.setattr(torch.cuda, "device_count", lambda: 4)
    monkeypatch.setattr(torch.cuda, "get_rng_state", lambda d: torch.tensor([7, 8, 9], dtype=torch.uint8))
    monkeypatch.setattr(torch.cuda, "set_rng_state", lambda s, device: calls.append((s.tolist(), device)))
    monkeypatch.setattr(
        torch.cuda, "get_rng_state_all", lambda: (_ for _ in ()).throw(AssertionError("all devices"))
    )
    sd = udist.capture_rng_state()
    assert list(sd["cuda"]) == ["2"] and sd["cuda"]["2"].tolist() == [7, 8, 9]
    udist.restore_rng_state(sd)
    assert calls == [([7, 8, 9], 2)]
    monkeypatch.setattr(torch.cuda, "is_initialized", lambda: False)
    assert "cuda" not in udist.capture_rng_state()


def test_all_reduce_helpers_are_identity_on_one_process():
    assert udist.all_reduce_max([0, 3, 1]) == [0, 3, 1]
    assert udist.any_rank(False) is False and udist.any_rank(True) is True
    assert udist.all_gather_objects({"a": 1}) == [{"a": 1}]