Skip to content

fix(trainer): stage CPU-offloaded weights through CUDA in NCCL broadcast#3045

Draft
mikasenghaas wants to merge 4 commits into
mainfrom
fix/nccl-broadcast-cpu-offload
Draft

fix(trainer): stage CPU-offloaded weights through CUDA in NCCL broadcast#3045
mikasenghaas wants to merge 4 commits into
mainfrom
fix/nccl-broadcast-cpu-offload

Conversation

@mikasenghaas

Copy link
Copy Markdown
Member

Follow-up to #3042 (found during bring-up of the first run to combine the two settings; the commit just missed the merge).

With fsdp_cpu_offload the gathered state-dict tensors live on CPU, and broadcast_state_dict passed them to the NCCL communicator directly:

AssertionError: this nccl communicator is created to work on cuda:0, but the input tensor is on cpu

Pre-existing: RL never combined fsdp_cpu_offload (params on CPU) with NCCL weight broadcast before — muon (the usual RL optimizer) rejects FSDP CPU offload, and it's the memory profile that makes 196k-context NemotronH runs fit on 2 nodes (adamw). Stage the per-layer, per-dtype concatenated buffer through CUDA before broadcasting (+4 lines; a transient per-layer GPU buffer, ~3 GB max for a 120B/88-layer model).

Verified: the 196k cp=8 Nemotron Super RL run that hit this assert proceeds past weight broadcast with the fix (run in flight — will confirm rollouts/step 1 here).

🤖 Generated with Claude Code

@mikasenghaas

Copy link
Copy Markdown
Member Author

Superseded by the removal PR — team decided to remove fsdp_cpu_offload entirely rather than fix it.

@mikasenghaas

Copy link
Copy Markdown
Member Author

Reopening: #3049 (fsdp_cpu_offload removal) is being reverted in #3052 after 2-node long-context measurements, so this staging fix is needed again — it's the only fast weight-broadcast path with FSDP CPU offload (filesystem broadcast full-gathers CPU-resident fp32 weights over Gloo and effectively hangs; measured on a Nemotron Super 131k run).

@mikasenghaas mikasenghaas reopened this Jul 16, 2026
@mikasenghaas

Copy link
Copy Markdown
Member Author

Second fix pushed (a6bdfe8): the original staging happened after _resolve_dtensors, but full_tensor() on CPU DTensors already gathers over Gloo — measured ~1h+ stall at the v0 broadcast for Super's 480 GB fp32 until the NCCL watchdog killed the job. Now each shard moves to CUDA before full_tensor() so the gather runs over NCCL per tensor. Validating e2e on the Super 131k run.

@mikasenghaas

Copy link
Copy Markdown
Member Author

Status: the per-tensor CUDA staging (a6bdfe8) is insufficient — DTensor.to("cuda") moves the local shard but the DeviceMesh stays CPU/Gloo, so full_tensor() still gathers over Gloo (observed: ranks stuck 4.5h in the gather, no exception). A correct fix needs the gather itself to run on a CUDA mesh (or explicit NCCL collectives on staged shards) — leaving this PR as WIP; the immediate runs proceed without fsdp_cpu_offload instead.

mikasenghaas and others added 4 commits July 16, 2026 21:56
With fsdp_cpu_offload the gathered state-dict tensors live on CPU, and
broadcast_state_dict passed them to the NCCL communicator directly:
"AssertionError: this nccl communicator is created to work on cuda:0, but the
input tensor is on cpu". Move the per-layer, per-dtype concatenated buffer to
CUDA before broadcasting. Needed to combine fsdp_cpu_offload (the memory
profile that fits 196k on 2 nodes) with NCCL weight broadcast in RL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_resolve_dtensors called full_tensor() on CPU DTensors, so the gather ran
over Gloo — hours for 100B+ fp32 params (observed: Nemotron Super 131k run
stalled ~1h at the v0 broadcast until the NCCL watchdog killed it). Move each
local shard to CUDA before full_tensor() so the gather runs over NCCL, one
tensor at a time; the downstream concatenated-buffer staging then no-ops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DTensor.to("cuda") moves the local shard but collectives still route by the
original backend selection, so full_tensor() on CPU-offloaded params gathered
over Gloo regardless (observed: ranks stuck hours in the v0 broadcast).
Reconstruct the DTensor via from_local with the CUDA-staged shard on the same
(CUDA) device mesh so the gather dispatches to NCCL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-tensor synchronous H2D with CPU-side bf16 casts made the v0 broadcast
latency-dominated (minutes). FSDP CPU offload pins the shards, so stage them
with non_blocking H2D and cast to bf16 on GPU after the move; the NCCL gather
serializes correctly behind the async copies on the same stream. Rebased onto
main (includes the uint16 routed-experts wire format).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikasenghaas
mikasenghaas force-pushed the fix/nccl-broadcast-cpu-offload branch from 92c891c to bb8a777 Compare July 16, 2026 21:56
@mikasenghaas

Copy link
Copy Markdown
Member Author

Deprioritized on our side: even with async pinned staging (bb8a777, unvalidated — test run cancelled), FSDP CPU offload implies re-staging the full CPU-resident weights every step's broadcast, a recurring per-step tax the team judged not worth it for RL-with-inference. The branch is rebased onto main with the mesh-correct NCCL gather + async staging if anyone wants to pick it up; trainer-only/SFT users of fsdp_cpu_offload are unaffected (no broadcast).

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