Skip to content

refactor(configs)!: one model.offload field for CPU offloading - #3988

Merged
samsja merged 3 commits into
mainfrom
refactor/model-offload-field
Oct 10, 2026
Merged

samsja merged 3 commits into
mainfrom
refactor/model-offload-field

Conversation

@faresobeid

@faresobeid faresobeid commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the three switches model.fsdp_cpu_offload, model.optim_cpu_offload and model.full_offload with one field:

[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:

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".

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.

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

@faresobeid
faresobeid marked this pull request as ready for review October 10, 2026 17:42
mikasenghaas
mikasenghaas previously approved these changes Oct 10, 2026

@mikasenghaas mikasenghaas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice ya always found our naming confusing before. one thing: couldn't we compose fsdp+optim offload before, which would not be possible anymore no?

`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
samsja merged commit 0936378 into main Oct 10, 2026
20 checks passed
@samsja
samsja deleted the refactor/model-offload-field branch October 10, 2026 22:39
artzhuravel added a commit that referenced this pull request Oct 12, 2026
…ding

- The objective validator refuses `model.offload = "full"` (the field
  that replaced `full_offload`, #3988).
- Tests pass `pad_to_multiple_of` to `CatDataset` (#3948): packs are now
  padded to a multiple of cp, not to seq_len, so the padding test uses
  an explicit multiple.
- `render()` carries upstream's `message_loss_mask` column (#3950),
  which the rebase placed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
artzhuravel added a commit that referenced this pull request Oct 14, 2026
…ding

- The objective validator refuses `model.offload = "full"` (the field
  that replaced `full_offload`, #3988).
- Tests pass `pad_to_multiple_of` to `CatDataset` (#3948): packs are now
  padded to a multiple of cp, not to seq_len, so the padding test uses
  an explicit multiple.
- `render()` carries upstream's `message_loss_mask` column (#3950),
  which the rebase placed there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.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.

3 participants