Repository navigation
refactor(configs)!: one model.offload field for CPU offloading - #3988
Merged
Merged
Conversation
…, optim_cpu_offload and full_offload
faresobeid
marked this pull request as ready for review
October 10, 2026 17:42
mikasenghaas
previously approved these changes
Oct 10, 2026
mikasenghaas
left a comment
Member
There was a problem hiding this comment.
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
approved these changes
Oct 10, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the three switches
model.fsdp_cpu_offload,model.optim_cpu_offloadandmodel.full_offloadwith one field:This applies to both the RL trainer and SFT. The
fsdpswitch is not migrated: the mode is removed outright (rationale below). The[model.full_offload] numa_bindknob 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. Becauseoptim_cpu_offloaddefaulted totrue, enabling full offload also meant writingoptim_cpu_offload = false, otherwise validation failed (see the oldconfigs/advanced/deepseek-v4-flash/sft.tomlcomment). 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 FSDP2CPUOffloadPolicywith 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); thefullmode added in #3234 keeps FP32 masters, moments, and gradients in host RAM, covering that memory pressure instead. Gone with it: theCPUOffloadPolicyselection insetup_fsdp, CPU-side model init insetup_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 itnuma_bind) and its bool-normalizingBeforeValidatorare gone.TrainerConfigandSFTConfig:offload == "full";Muon optimizer does not support FSDP CPU offloadvalidators are deleted with the mode.setup_optimizer(..., offload=...)replacescpu_offload+full_offload_config, and its "both enabled" check is gone.FullCPUOffloadOptimizerdrops its unusedoffload_configargument.setup_full_cpu_optimizer_offload()takes no arguments and always binds.model.py: FSDP always uses the defaultOffloadPolicy(); CPU-side model init insetup_model/load_dcp_from_hfand_move_buffers_to_cudaare gone.glm-4.5-air/swe-budget.tomlandci/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 updateddocs/scaling.md, thedeepseek-v4-flash/sft.tomlcomment, andskills/training/start-run/SKILL.md, which documented the removed fields.Breaking: the old keys now fail with
extra_forbidden, andoffload = "fsdp"fails theLiteral. There is no shim.Interaction with #3849 (one trainer-common config base): #3849 moves the full-offload and Muon/FSDP validators from
TrainerConfig/SFTConfigontoBaseTrainerConfig. 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 withself.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:
optim_cpu_offload), feat(sft): packed multiple-choice SFT with a pluggable loss and choice evals #3739 (full_offloadvalidators), feat(trainer): remove CP token padding and duplicated RL microbatches #3724, fix(optim): stream Muon gradients and state for 8-node GLM-5.3 SFT #3719, Add PPO and SAO value-model training #3699, feat(multimodal): enable Nemotron-H Omni VLM training #3610, feat: add ModelExpress weight updates #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_configscovers the 4 edited tomls.cli(TrainerConfig|SFTConfig, ...):--model.offload full|noneand the default (optimizer) resolve as expected;optim_cpu_offloadandoffload = "fsdp"are rejected.import prime_rl.trainer.rl.train, prime_rl.trainer.sft.trainworks.ruff checkandruff format --checkon the changed Python files: clean.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, andmodel.full_offloadare replaced by a singlemodel.offloadsetting ("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_bindis dropped —"full"always runs NUMA binding and CPU optimizer thread setup via parameterlesssetup_full_cpu_optimizer_offload(). Runtime paths (setup_optimizer, FSDP CPU offload inmodel.py, Gloo when offload is"full"or"fsdp") key off the new enum;FullCPUOffloadOptimizerno 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.