Skip to content

Give every process group one collective timeout that survives rank-0-only work - #444

Merged
luciaquirke merged 5 commits into
mainfrom
fix/dist-timeout-rank0-hessian-save
Sep 1, 2026
Merged

luciaquirke merged 5 commits into
mainfrom
fix/dist-timeout-rank0-hessian-save

Conversation

@luciaquirke

Copy link
Copy Markdown
Collaborator

Give every process group one collective timeout that survives rank-0-only work

EK-FAC runs abort at larger dataset sizes with

Watchdog caught collective operation timeout
checkTimeout at ProcessGroupNCCL.cpp:733

always immediately after "Collecting gradients: 100%", and never during the
gradient loop itself. The loop is not the problem -- allocate_batches pads the
batch count to a multiple of the world size and asserts every rank gets an equal
number, so ranks leave the loop together.

What follows the loop is not symmetric:

run_with_collector_hooks()                    collector/collector.py
    ... gradient loop (symmetric) ...
    self.collector.teardown()                 hessians/autocorrelation.py
        process_autocorrelation_matrices(...)   `if rank == 0:` ...
        if self.rank == 0: processor.save(...)
    dist.all_reduce(total_processed)          <- other ranks block HERE

Rank 0 reduces and eigendecomposes the Grams and writes them to storage while
every other rank is already waiting in the all_reduce. That is not a deadlock --
rank 0 does arrive -- but the rank-0 work grows with the dataset and the write
lands on shared storage, so past the timeout the watchdog kills a run that was
making normal progress. We see it at 64k documents where 32k with the same
config and the same hardware completes.

The timeouts in place were inconsistent and, in the build path, too short:

build.py         30 minutes   <- the index/EK-FAC path, where this fires
distributed.py   1 hour  (init_dist) / none (parent_barrier)
score/score.py   1 hour
magic/cli.py     none -> NCCL default, 10 minutes
magic/metasmoothness.py  none -> 10 minutes

This replaces all five with a single DIST_TIMEOUT in bergson/distributed.py,
defaulting to 1 hour and overridable per-run with BERGSON_DIST_TIMEOUT_MIN.

Worth stating explicitly: TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC does NOT help here.
It governs the watchdog monitor thread, not the collective timeout, and the
collective timeout has no environment override at all -- it is an
init_process_group argument. That is why this has to be a code change.

This raises the ceiling rather than removing the asymmetry. Doing the all_reduce
before teardown, or sharding the Hessian save across ranks, would shrink the
window itself; both are larger changes and neither is needed to stop healthy
runs being killed.

luciaquirke and others added 2 commits August 26, 2026 12:12
…only work

EK-FAC runs abort at larger dataset sizes with

    Watchdog caught collective operation timeout
    checkTimeout at ProcessGroupNCCL.cpp:733

always immediately after "Collecting gradients: 100%", and never during the
gradient loop itself. The loop is not the problem -- allocate_batches pads the
batch count to a multiple of the world size and asserts every rank gets an equal
number, so ranks leave the loop together.

What follows the loop is not symmetric:

    run_with_collector_hooks()                    collector/collector.py
        ... gradient loop (symmetric) ...
        self.collector.teardown()                 hessians/autocorrelation.py
            process_autocorrelation_matrices(...)   `if rank == 0:` ...
            if self.rank == 0: processor.save(...)
        dist.all_reduce(total_processed)          <- other ranks block HERE

Rank 0 reduces and eigendecomposes the Grams and writes them to storage while
every other rank is already waiting in the all_reduce. That is not a deadlock --
rank 0 does arrive -- but the rank-0 work grows with the dataset and the write
lands on shared storage, so past the timeout the watchdog kills a run that was
making normal progress. We see it at 64k documents where 32k with the same
config and the same hardware completes.

The timeouts in place were inconsistent and, in the build path, too short:

    build.py         30 minutes   <- the index/EK-FAC path, where this fires
    distributed.py   1 hour  (init_dist) / none (parent_barrier)
    score/score.py   1 hour
    magic/cli.py     none -> NCCL default, 10 minutes
    magic/metasmoothness.py  none -> 10 minutes

This replaces all five with a single DIST_TIMEOUT in bergson/distributed.py,
defaulting to 1 hour and overridable per-run with BERGSON_DIST_TIMEOUT_MIN.

Worth stating explicitly: TORCH_NCCL_HEARTBEAT_TIMEOUT_SEC does NOT help here.
It governs the watchdog monitor thread, not the collective timeout, and the
collective timeout has no environment override at all -- it is an
init_process_group argument. That is why this has to be a code change.

This raises the ceiling rather than removing the asymmetry. Doing the all_reduce
before teardown, or sharding the Hessian save across ranks, would shrink the
window itself; both are larger changes and neither is needed to stop healthy
runs being killed.
luciaquirke added a commit to EleutherAI/metasmoothness that referenced this pull request Aug 26, 2026
Lucia ruling 2026-08-26. Three process decisions, an error-analysis log that
treats each failure as an open task, and scripts/hung_check.py.

The checker earns its place immediately: it found two hung muon 128k tuning runs
nobody knew about, quiet for 644 and 323 minutes, holding four GPUs. Neither had
a claim, so no claim-based check could have seen them. Both show wchan
pipe_write, PPid 1, and fd 1/2 pointing at a DELETED train.log -- orphaned
children blocked writing into a pipe their dead launcher was supposed to drain.
A closed read end would have delivered EPIPE and killed them; a half-open
undrained one hangs them instead.

That is a different failure from the london 32k/64k hangs (wait_woken, 39
threads in futex_wait, intact logs), and the log keeps them separate.

Also backports EleutherAI/bergson#444 into the pinned -429 checkout: one
DIST_TIMEOUT across all five init_process_group sites. The 64k EK-FAC abort is
rank 0 processing and saving the Hessians past the collective timeout while the
other rank waits in all_reduce -- build.py allowed 30 minutes and rank 0 needs
more at 64k.
luciaquirke added a commit to EleutherAI/metasmoothness that referenced this pull request Aug 26, 2026
plan_adam_eps1e17_64k_bs32 EK-FAC aborted with "Watchdog" on iris-0. It launched
before the DIST_TIMEOUT backport, so it still had build.py's 30 minutes.

The useful number came from its muon twin, still ALIVE in the same rank-0 section
at 114 minutes, GPU 3 at 100% and GPU 2 at 0%. Rank-0 Hessian processing and save
on a 64k row therefore takes over 110 minutes.

So the 1-hour default I proposed in EleutherAI/bergson#444 would abort exactly
these runs. I picked that ceiling before measuring what it had to clear. The
measurement now says 2 h minimum at 64k and more above that. Relaunched with
BERGSON_DIST_TIMEOUT_MIN=360; both GPUs at ~100%.

A timeout raises a ceiling, it does not remove the asymmetry. Doing the
all_reduce before teardown, or sharding the Hessian save across ranks, is the
actual fix.

Also: the muon twin at 114 minutes reads STALE to check_runs.py and would read
hung to hung_check.py. One rank at 100% with the rest at 0% is healthy rank-0
work -- check per-GPU utilisation before acting on either script.

Fleet: eight london tuning runs now training across 32k and 64k, adamw and muon,
after the stale-cache fix. shared-ord-0 fully saturated.
@luciaquirke

Copy link
Copy Markdown
Collaborator Author

Measurement update: raised the default from 1 h to 3 h.

The original hour was a guess made before I had a number for the rank-0 section. On a 64k-document row I have now observed it still healthy at 114 minutes — rank 0 pinned at 100% GPU running process_autocorrelation_matrices and processor.save(), rank 1 at 0% blocked in dist.all_reduce(total_processed). An hour would abort exactly the runs this PR is meant to protect.

The two-GPU utilisation split is also the cleanest way to tell this state from a real hang: one rank saturated and the rest idle is healthy rank-0 work.

Still worth saying that this raises a ceiling rather than removing the asymmetry — doing the all_reduce before teardown, or sharding the Hessian save across ranks, would shrink the window itself.

The 1 h default was a guess made before measuring what it had to clear. A 64k
row was observed still healthy in the rank-0 section at 114 minutes -- rank 0 at
100% GPU, rank 1 at 0% blocked in all_reduce -- so an hour would have aborted
exactly the runs this is meant to protect.

Raised to 3 h and recorded the measurement in the comment, so the next person
changing it knows what the number is based on.
luciaquirke added a commit to EleutherAI/metasmoothness that referenced this pull request Aug 26, 2026
…ult is far short

Both 4000-step rows aborted again, and this time the log says exactly why:

    Timeout(ms)=21600000

That is 360 minutes -- the BERGSON_DIST_TIMEOUT_MIN=360 I set actually applied.
So the run had six hours and still exceeded it. The patch works; the number was
wrong.

That revises the measurement twice over. I first proposed 1 hour in
EleutherAI/bergson#444, raised it to 3 after seeing a 64k bs256 row still healthy
in that section at 114 minutes, and now a 64k bs32 row at 4000 steps has blown
past six. The rank-0 section scales with the trajectory, and 4000 steps is 8x the
500-step row that took under two hours.

Ruled out I/O, which was my first suspicion since these write to ssd-1 and that
volume hit its quota twice today. It has recovered -- 2.7T free, and it now
writes at 3.0 GB/s against ssd-2 at 538 MB/s. So the six hours is compute in
process_autocorrelation_matrices plus the save, not a stalled filesystem.

Relaunched both with BERGSON_DIST_TIMEOUT_MIN=1440. Twenty-four hours is a guess
in the same family as the last two, and I want to be plain that raising the
ceiling again is not a fix. The asymmetry is the fix: rank 0 works while every
other rank sits in dist.all_reduce(total_processed), and no timeout makes that
efficient -- it just stops it being fatal. Doing the all_reduce before teardown,
or sharding the Hessian save across ranks, is what removes it.

Practical consequence for the correlation Lucia keeps asking about: the 4000-step
rows are not a reliable route to a high-step LDS point. The 500-step row is --
plan_adam_eps1e17_64k_bs256 is at 83/100 and I have just sharded its last unowned
gap (22-24) onto a free A100 pair. That is the next new (LDS, delta) pair and it
is close.
luciaquirke and others added 2 commits September 1, 2026 03:25
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@luciaquirke
luciaquirke merged commit c563fbd into main Sep 1, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant