Repository navigation
[ckpt, hardware] fix: make checkpoint CPU offload device-agnostic - #8232
Merged
wuxibin89 merged 1 commit intoOct 15, 2026
Merged
Conversation
FSDPCheckpointManager gates ShardedStateDictConfig and ShardedOptimStateDictConfig `offload_to_cpu` on `is_cuda_available`, so every non-CUDA accelerator keeps the gathered model and optimizer shards resident in device memory for the whole save/load window. Switch the four sites to `is_device_available()`, the platform-abstraction check the same class already uses for accelerator RNG state.
kahlun
marked this pull request as ready for review
October 14, 2026 07:51
Contributor
wuxibin89
approved these changes
Oct 15, 2026
38 tasks
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.
What does this PR do?
FSDPCheckpointManagergates FSDP's sharded-state-dict CPU offload onis_cuda_available:is_cuda_availableistorch.cuda.is_available()evaluated once at import(
verl/utils/device.py). On any non-CUDA accelerator it is permanentlyFalse, so the gatheredmodel and optimizer shards stay resident in device memory for the whole
torch.savewindowinstead of being staged through host memory. CUDA gets the memory headroom; every other backend
pays for it, with no way to opt in.
This switches the four sites to
is_device_available()— the platform-abstraction check(
get_platform().is_available()) that this same class already uses to decide whether acceleratorRNG state is reachable in
BaseCheckpointManager.get_rng_state.Why this is a per-device-class oversight rather than a deliberate CUDA-only choice: the
offload_to_cpulines predate the platform abstraction layer (present at least as of2026-04-20;
is_device_availablearrived 2026-06-04 in #6086), and the migration to it has beenincremental — #7871 applied the same
is_device_available()treatment to the Megatron engine'sseed path in September.
offload_to_cpuis a plain PyTorch FSDP feature with no CUDA-specificcode behind it, so there was never a technical reason to exclude other backends. The measurement
below confirms it works on a non-CUDA backend today.
Checklist Before Starting
Search for similar PRs:
I enumerated every open PR in the repo and checked its changed-file list: [ckpt] fix: load FSDP checkpoint shards onto CPU instead of the saved device #7286 is the only
open PR that touches
verl/utils/checkpoint/fsdp_checkpoint_manager.py.Not a duplicate of [ckpt] fix: load FSDP checkpoint shards onto CPU instead of the saved device #7286 (
[ckpt] fix: load FSDP checkpoint shards onto CPU instead of the saved device, open). That PR is the closest neighbour — same file, same "treat sharded statedicts as CPU-resident" theme — but a disjoint change: it adds
map_location="cpu"to the threetorch.loadcalls inload_checkpoint, which is wrong on CUDA too and fixes where tensors arerevived from disk. This PR changes the
ShardedStateDictConfig/ShardedOptimStateDictConfiggather configs, which is CUDA-privileged by construction and governs where tensors land during
the FSDP gather, on both the save and load paths. The two touch different lines and neither
subsumes the other; they merge independently in either order. Worth noting for whoever lands the
second one: together they make the manager's CPU-residency story consistent end to end, which is
the stated motivation of [ckpt] fix: load FSDP checkpoint shards onto CPU instead of the saved device #7286.
Related precedent, not overlapping: [hardware] fix: add device_available check for devices other than GPU/NPU #7871 (merged) added an
is_device_available()check fornon-GPU/NPU devices in
verl/workers/engine/megatron/utils.py. Same class of fix, differentfile; it did not touch the checkpoint managers.
PR title formatted as
[{modules}] {type}: {description}Test
Measured on real hardware: 2× Intel Arc Pro B60 (XPU),
torch 2.11.0,xcclbackend.The standalone repro below wraps an 8-layer 4096-wide model in FSDP across 2 ranks, runs one
optimizer.step()so optimizer state exists, then performs the same sharded gather twice —once with
offload_to_cpu=False(today's behaviour on XPU) and once withTrue(this patch) —reporting the resulting tensor device and the peak device memory during each gather:
Two things this establishes:
offload_to_cpu=Trueworks correctly on a non-CUDA backend. The gathered shard lands oncpuand the optimizer state dict is intact (16 entries, matching the 16 parameters). Nothingabout this FSDP feature is CUDA-specific, so the current gate is excluding a working code path.
Falsedefault is real and scales with model size. 256 MiB of extrapeak device memory held through the gather, on the order of the per-rank sharded state-dict size
for this model — i.e. proportional to model/optimizer size, not a fixed overhead. On a card
already near capacity this is the difference between a checkpoint save that completes and one
that OOMs, which is the failure mode [ckpt] fix: load FSDP checkpoint shards onto CPU instead of the saved device #7286 describes from the load side.
On CUDA this patch is a no-op by construction, which is why no CUDA regression run is included:
PlatformCUDA.is_available()isreturn torch.cuda.is_available()— the identical expression theold module-level
is_cuda_availablecaptured. The only semantic difference is import-time vs.call-time evaluation, which cannot differ in practice since CUDA availability is fixed for a
process. On a CPU-only host both spellings yield
False, so the*_on_cpucheckpoint tests areunaffected.
Lint:
ruff==0.12.2 checkandruff format --checkclean (pinned to the repo's.pre-commit-config.yamlrev; noteruff formatreflows two now-shorter ternaries, included inthe diff).
Not covered:
tests/special_distributed/test_fsdp_ckpt.py— the repo's own end-to-endFSDPCheckpointManagersave/load round trip — has not been run with this patch. It needs 2accelerators and a Qwen2.5 tokenizer, and on XPU additionally a verl image with
flash_attention_2available. What is established here is the mechanism and its memory cost onreal XPU silicon, not a full verl checkpoint round trip through the patched manager. That test is
the natural place to settle it if you would like it settled before merging, and I am happy to run
it on the B60s on request.
Known trade-off, stated explicitly: this makes non-CUDA backends stage shards through host
memory, so it trades device-memory pressure for host-memory pressure — exactly the trade CUDA has
always made here. On nodes where host RAM is the scarcer resource this is not free. I considered
exposing it as a
checkpoint.offload_to_cpuconfig override instead, and deliberately did not:that adds a user-facing knob to work around what is currently a one-platform exclusion from an
existing default, and the right first step is to make the default consistent across backends. If
maintainers would prefer the knob (either instead of or on top of this), say so and I will follow up
separately.
Repro script (2 accelerators, torchrun, no verl import needed)
API and Usage Example
No API change. No config change. Behaviour change is limited to non-CUDA accelerators, which now
get the same CPU-offload default CUDA already had.
Design & Code Changes
verl/utils/checkpoint/fsdp_checkpoint_manager.py:is_device_availableinstead ofis_cuda_available.load_checkpoint:ShardedStateDictConfig/ShardedOptimStateDictConfignow takeoffload_to_cpu=is_device_available().save_checkpoint: same for both configs.True if … else Falsewrapper;ruff formatthen reflows the twosurrounding ternaries onto single lines.
Checklist Before Submitting
ruff/ruff-formatclean at the pinnedv0.12.2ci-requestSlack messagerecipesubmodule