Skip to content

Revert "chore: remove fsdp_cpu_offload" - #3052

Merged
mikasenghaas merged 1 commit into
mainfrom
chore/restore-fsdp-cpu-offload
Jul 16, 2026
Merged

mikasenghaas merged 1 commit into
mainfrom
chore/restore-fsdp-cpu-offload

Conversation

@mikasenghaas

@mikasenghaas mikasenghaas commented Jul 16, 2026 •

Copy link
Copy Markdown
Member

Reverts #3049 — decision reversed after measuring the 2-node ceilings for Nemotron Super at cp=8 without it:

config fsdp_cpu_offload verdict
SFT 131k on 79.8/139.8 GiB (57%) — comfortable
SFT 196k on 114.8 GiB (82%), 37.5% MFU
SFT 131k off OOM (demand ~142+ GiB; fp32 param+grad baseline stays on GPU)
RL 131k off OOM by 15–20 MiB (with expandable_segments)

Without FSDP param offload, fp32 master weights+grads (~60 GiB/GPU on 2 nodes) cap trainable context at ~96–120k; with it, 131k–196k fit. Restoring the knob; validation runs at 131k with it re-enabled are in flight (results to follow).

Note: combining fsdp_cpu_offload with NCCL weight broadcast still needs the staging fix from #3045 (closed; can be reopened) — trainer-only / filesystem-broadcast paths are unaffected.

🤖 Generated with Claude Code


Note

Medium Risk
Touches core FSDP sharding, weight loading, and distributed backends; misconfiguration is guarded but throughput and NCCL weight broadcast interactions remain sensitive.

Overview
Restores the trainer.model.fsdp_cpu_offload option (reverting its removal) so FSDP can offload parameters, gradients, and optimizer state to CPU with pinned memory—needed for long-context SFT/RL on tight GPU memory.

Trainer wiring: setup_fsdp applies CPUOffloadPolicy across FSDP units; weight load / to_empty uses CPU when offload is on, with _move_buffers_to_cuda because FSDP does not offload buffers. RL and SFT call setup_torch_distributed(..., enable_gloo=True) when offload is enabled (cpu:gloo,cuda:nccl).

Config/docs: fsdp_cpu_offload is mutually exclusive with optim_cpu_offload; Muon rejects FSDP CPU offload. docs/scaling.md documents the knob and tradeoffs.

Reviewed by Cursor Bugbot for commit b1cecf5. Bugbot is set up for automated code reviews on this repo. Configure here.

@mikasenghaas

Copy link
Copy Markdown
Member Author

Validation complete: with fsdp_cpu_offload restored, Nemotron Super @131k/cp=8 on 2×H200 runs at 79.8/139.8 GiB (57%) peak — SFT 22.8% MFU, RL trainer 30.8% MFU, 4/4 steps each (research-prod PR #155 debug configs). Without it the same configs OOM (15–336 MiB short at 131k; ~140+ GiB demand at 96–120k).

@mikasenghaas
mikasenghaas merged commit 013a7f8 into main Jul 16, 2026
21 of 22 checks passed
samsja added a commit that referenced this pull request Oct 10, 2026
`model.offload` is now `none` | `optimizer` | `full`. The `fsdp` mode was the
stock torch FSDP2 `CPUOffloadPolicy` with no in-tree config, example, or test
user; Muon rejected it and it broke NCCL weight broadcast. It survived only
for long-context runs whose fp32 masters/moments no longer fit on GPU (#3052);
the `full` mode added in #3234 keeps FP32 masters, moments, and gradients in
host RAM, covering that memory pressure instead.

Removed with it: the `CPUOffloadPolicy` selection in `setup_fsdp`, CPU-side
model init in `setup_model`/`load_dcp_from_hf`, `_move_buffers_to_cuda`, the
Muon/FSDP validators in `TrainerConfig` and `SFTConfig`, and the fsdp leg of
the Gloo special case.
samsja added a commit that referenced this pull request Oct 10, 2026
Replaces the three switches `model.fsdp_cpu_offload`,
`model.optim_cpu_offload` and `model.full_offload` with one field:

```toml
[model]
offload = "optimizer"   # default; also "full", "none"
```

This applies to both the RL trainer and SFT. The `fsdp` switch is not
migrated: the mode is removed outright (rationale below). The
`[model.full_offload] numa_bind` knob is dropped: no recipe, deployment,
template or doc sets it. Full offload now always pins each rank's CPUs
to its GPU's NUMA node. That was already the default. Ranks whose GPU
reports no NUMA node still skip binding with a warning. On hosts without
GPU sysfs topology (`/sys/bus/pci/devices/<bus>/numa_node`), binding is
no longer optional: full offload fails there instead of being switched
off.

Why: the three switches could only be used one at a time, enforced by
`cpu_offload_mutual_exclusion`. Because `optim_cpu_offload` defaulted to
`true`, enabling full offload also meant writing `optim_cpu_offload =
false`, otherwise validation failed (see the old
`configs/advanced/deepseek-v4-flash/sft.toml` comment). With one value
the conflicting combinations cannot be expressed, so that validator is
deleted. The default behaviour does not change: optimizer-state offload
stays on.

Why drop `fsdp`: the mode was the stock torch FSDP2 `CPUOffloadPolicy`
with no in-tree config, example, or test user; Muon rejected it and it
broke NCCL weight broadcast. It was kept only for long-context runs
whose fp32 masters/moments no longer fit on GPU (the reason it was
restored in #3052); the `full` mode added in #3234 keeps FP32 masters,
moments, and gradients in host RAM, covering that memory pressure
instead. Gone with it: the `CPUOffloadPolicy` selection in `setup_fsdp`,
CPU-side model init in `setup_model`/`load_dcp_from_hf`,
`_move_buffers_to_cuda`, and the fsdp leg of the Gloo special case.

Changes:
- `ModelConfig.offload: Literal["none", "optimizer", "full"] =
"optimizer"` replaces the three fields.
`OptimizerInBackwardOffloadConfig` (and with it `numa_bind`) and its
bool-normalizing `BeforeValidator` are gone.
- Validators in `TrainerConfig` and `SFTConfig`:
- full offload accepts only AdamW or SignSGD and disables clipping, now
reading `offload == "full"`;
- the `Muon optimizer does not support FSDP CPU offload` validators are
deleted with the mode.
- Runtime:
- `setup_optimizer(..., offload=...)` replaces `cpu_offload` +
`full_offload_config`, and its "both enabled" check is gone.
- `FullCPUOffloadOptimizer` drops its unused `offload_config` argument.
- `setup_full_cpu_optimizer_offload()` takes no arguments and always
binds.
- `model.py`: FSDP always uses the default `OffloadPolicy()`; CPU-side
model init in `setup_model`/`load_dcp_from_hf` and
`_move_buffers_to_cuda` are gone.
- Tomls: `glm-4.5-air/swe-budget.toml` and
`ci/integration/reverse-text-sft/full-offload-resume.toml` → `offload =
"full"`; `glm-5.3/sft/h200/16k-12-node.toml` → `offload = "none"`;
`glm-5.3/sft/h200/base.toml` → `offload = "optimizer"`. Also updated
`docs/scaling.md`, the `deepseek-v4-flash/sft.toml` comment, and
`skills/training/start-run/SKILL.md`, which documented the removed
fields.

Breaking: the old keys now fail with `extra_forbidden`, and `offload =
"fsdp"` fails the `Literal`. There is no shim.

Interaction with #3849 (one trainer-common config base): #3849 moves the
full-offload and Muon/FSDP validators from `TrainerConfig`/`SFTConfig`
onto `BaseTrainerConfig`. This PR edits the full-offload validator's
condition and deletes the Muon/FSDP one, so the conflict is textual
only: keep #3849's single copy of the full-offload validators with
`self.model.offload == "full"` and drop its Muon/FSDP copy. Whichever
lands second takes a small rebase.

Other open PRs that set or read the old keys need a rebase:
- tomls: #3883, #3879, #3766, #3765, #3729, #3629, #3627, #3616, #3544
- code: #3885 (`optim_cpu_offload`), #3739 (`full_offload` validators),
#3724, #3719, #3699, #3610, #3487 (`fsdp_cpu_offload` / `enable_gloo`)

Test plan
- `pytest tests/unit/orchestrator tests/unit/test_configs.py
tests/unit/train/test_cpu_adam.py tests/unit/train/test_state_offload.py
-m "not gpu"`: 336 passed, 1 skipped (Qwen3-VL snapshot not cached on
this host), 4 deselected (GPU; this host has no GPU). The existing
offload config tests are updated to the new field. `test_load_configs`
covers the 4 edited tomls.
- `cli(TrainerConfig|SFTConfig, ...)`: `--model.offload full|none` and
the default (`optimizer`) resolve as expected; `optim_cpu_offload` and
`offload = "fsdp"` are rejected.
- `import prime_rl.trainer.rl.train, prime_rl.trainer.sft.train` works.
- `ruff check` and `ruff format --check` on the changed Python files:
clean.
- Not run: GPU or multi-node training with `offload = "full"`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Breaking TOML/API surface touches distributed training memory paths
(FSDP offload, optimizer-in-backward, Gloo); behavior should match prior
modes when mapped correctly, but mis-migrated configs will fail at parse
time.
> 
> **Overview**
> **Breaking config change:** `model.fsdp_cpu_offload`,
`model.optim_cpu_offload`, and `model.full_offload` are replaced by a
single **`model.offload`** setting (`"optimizer"` default, or `"full"`,
`"fsdp"`, `"none"`). RL and SFT share the same field; example TOMLs and
scaling docs are updated accordingly.
> 
> The mutual-exclusion validator between the old flags is removed
because only one mode can be expressed. **`[model.full_offload]` /
`numa_bind` is dropped** — `"full"` always runs NUMA binding and CPU
optimizer thread setup via parameterless
`setup_full_cpu_optimizer_offload()`. Runtime paths (`setup_optimizer`,
FSDP CPU offload in `model.py`, Gloo when offload is `"full"` or
`"fsdp"`) key off the new enum; `FullCPUOffloadOptimizer` no longer
takes an offload config object.
> 
> Default training behavior is unchanged: optimizer-state CPU offload
remains the default (`"optimizer"`). Old TOML keys are not aliased and
should fail validation.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
35da73a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: sami jaghouar <sami.jaghouar@gmail.com>
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.

2 participants