Skip to content

feat(fsdp2): load model on meta device for non-rank0 ranks (0 CPU RAM per worker) - #9982

Open
cben484 wants to merge 2 commits into
modelscope:mainfrom
cben484:feat/fsdp2-meta-loading
Open

feat(fsdp2): load model on meta device for non-rank0 ranks (0 CPU RAM per worker)#9982
cben484 wants to merge 2 commits into
modelscope:mainfrom
cben484:feat/fsdp2-meta-loading

Conversation

@cben484

@cben484 cben484 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Problem

With --fsdp fsdp2, transformers 5.x has no rank0-only zero-memory loading path. In from_pretrained, the FSDP non-rank0 branch (transformers/modeling_utils.py, _move_missing_keys) materializes every parameter with torch.zeros_like(param, device="cpu") — i.e. each rank allocates the full model in CPU RAM (a 35B bf16 model ≈ 67G per rank, ~136G peak including prepare-time copies). On hosts that budget CPU RAM per device (e.g. 125G per accelerator), N x 136G inevitably OOM-kills workers during loading/prepare, regardless of world size.

Measured on 16x Ascend 910B (2T host RAM, Qwen3.5-35B-A3B): 16 workers x ~136G anon RSS ≈ 2.2T → kernel global OOM kill during prepare (dmesg evidence captured).

Solution

Only rank 0 needs real weights; accelerate's cpu_ram_efficient_loading path (accelerate/utils/fsdp_utils.py: "If cpu_ram_efficient_loading is enabled, only rank 0 loads the weights") already distributes rank-0 weights to all ranks during prepare. This PR makes non-rank0 ranks load on the meta device and keeps the model there until accelerate takes over:

  • module-level helper _fsdp2_use_meta_loading() — enabled when FSDP2 + world_size>1, disable via SWIFT_FSDP2_META_LOADING=0
  • in get_model_processor(): non-rank0 loads under init_empty_weights(), with ACCELERATE_USE_FSDP temporarily masked during the loading window

Two details that matter:

  1. Masking ACCELERATE_USE_FSDP inside the loading window is essential: transformers' non-rank0 branch would otherwise turn meta params back into CPU zeros (full model in host RAM per rank), silently defeating the optimization. device_map is resolved earlier in the same function, so masking at this point is safe.
  2. Do NOT add a custom weight-sync hook after prepare: we prototyped a per-parameter broadcast(p.to_local(), src=0) hook on Trainer._prepare_for_training and it deadlocks HCCL collective ordering (HcclAllGather dispatch timeout), because accelerate's own sync already runs inside prepare. The official path handles everything — this PR deliberately contains no sync code.

Verification (16x Ascend 910B 64G, ms-swift 4.4.2, Qwen3.5-35B-A3B LoRA)

Metric Before (full load per rank) After (meta loading)
Host CPU RAM peak (loading+prepare) ~2.2T for 16 ranks → kernel OOM kill 187G total, independent of world size
Per-worker host RAM (non-rank0) ~136G ~0 (meta device)
NPU HBM per card 28.3G @ 8 cards 19.5G (DPO) / 20.9G (KTO) @ 16 cards
Correctness (DPO) loss start 0.6914 (=ln2); logps/chosen -128.8 loss start 0.6914; logps/chosen -129.0 (matches full-load baseline)

DPO and KTO smoke tests both pass end-to-end at full 16-card world size.

Requires #9980 (ACCELERATE_USE_FSDP fix) as prerequisite — without it, device_map='npu:{rank}' forces full per-device NPU loading before this code path is even reached.

transformers 5.x has no rank0-only zero-memory loading path: its FSDP
non-rank0 branch materializes the full model as CPU zeros per rank
(~model-size host RAM each). On hosts that budget CPU RAM per device,
N x model-size inevitably OOM-kills workers during loading/prepare.

Let non-rank0 ranks load under init_empty_weights() with
ACCELERATE_USE_FSDP temporarily masked (to bypass the CPU-zeros branch),
and let accelerate's cpu_ram_efficient_loading distribute rank0 weights
during prepare.

Validated on 16x Ascend 910B (Qwen3.5-35B-A3B, LoRA DPO/KTO):
- host RAM peak: 187G total (rank0 only), independent of world size
  (was ~2.2T with 16 ranks full-loading -> global OOM kill)
- NPU HBM: 19.5G/card (DPO) / 20.9G/card (KTO) at 16 cards
- identical training metrics vs full loading (DPO loss start 0.6914=ln2,
  logps/chosen -129.0 vs -128.8)

Requires modelscope#9980 as prerequisite. Disable with SWIFT_FSDP2_META_LOADING=0.
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