feat(fsdp): meta-init + rank0-streamed weight loading (--fsdp-load-mode stream)#39
Draft
zhihengy wants to merge 1 commit into
Draft
feat(fsdp): meta-init + rank0-streamed weight loading (--fsdp-load-mode stream)#39zhihengy wants to merge 1 commit into
zhihengy wants to merge 1 commit into
Conversation
…de stream)
Every rank used to materialize the full pipeline on CPU
(DiffusionPipeline.from_pretrained), move the whole unsharded model to GPU,
and only then fully_shard — peak GPU = full model (54 GB for one Wan2.2-A14B
expert in fp32 master dtype), N×full disk reads, and no way to honor a meta
init context (diffusers' pipeline loader crashes under one).
New default path (--fsdp-load-mode stream), per component:
build on meta via init_empty_weights(include_buffers=False) # buffers real
-> fully_shard on meta (costs nothing)
-> to_empty: allocate only this rank's shards; carry non-persistent
buffers (Wan rope tables) across the transition
-> rank0 streams safetensors file-by-file, set_model_state_dict
broadcast_from_rank0 scatters into the shards
-> LoRA adapters build on meta too (low_cpu_mem_usage) and initialize
after materialization; data-aware inits (pissa/olora) are rejected
-> family postprocess hook runs the before-fsdp hook after
materialization, where qwen-image's rope parity rebuild has real
CUDA tensors instead of silently no-oping on meta
--fsdp-load-mode legacy keeps the old path for custom pipelines.
Wan2.2-A14B (both experts, fp32, 2×H200): init 285s -> 71s, peak GPU
80.5 -> 54.0 GB (= the sharded params themselves); only rank0 reads the
checkpoint. Streamed weights verified bitwise-equal to from_pretrained
(params + buffers) at 1.3B scale and in the new 2-GPU CI test.
Co-Authored-By: Claude Fable 5 <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.
Problem
The FSDP init path materializes the full pipeline on every rank (
DiffusionPipeline.from_pretrained), moves the whole unsharded model to GPU, and only then callsfully_shard:NotImplementedError: Cannot copy out of meta tensor)Fix:
--fsdp-load-mode stream(new default)Per component, invert "load-then-shard" into "shard-then-load":
model_index.json+ componentfrom_configunderaccelerate.init_empty_weights(include_buffers=False)— params on meta (zero memory), buffers/plain tensor attrs computed for real by__init__(same context diffusers' ownfrom_pretraineduses, so values match bit-exactly).fully_shardwhile still on meta — sharding plan needs only shapes, costs nothing.to_emptyallocates only this rank's shards. Non-persistent buffers (Wan'srope.freqs_cos/sin— not in any checkpoint) are snapshotted and restored across the transition.set_model_state_dict(..., broadcast_from_rank0=True, strict=False)scatters each tensor into the shards over NVLink. A coverage check makesstrict=Falsesafe: any parameter not covered by the checkpoint raises.get_peft_model(..., low_cpu_mem_usage=True)), base weights land on the peft-wrapped FQNs via a key map (exact inverse of the stripping indiffusion_update_weight_utils), and A/B initialize after materialization viareset_lora_parameterson the sharded DTensors. Data-aware inits (pissa/olora/...) need real base weights at wrap time and are rejected with a pointer to legacy mode.postprocess_model_after_materialize(default: run the existing before-fsdp hook) so qwen-image's rope-parity rebuild sees real CUDA tensors — under stream itsdevice.type != "cuda"guard would otherwise silently no-op on meta and lose train/rollout bit-parity.--fsdp-load-mode legacykeeps the previous behavior (custom/remote-code pipelines, data-aware LoRA inits).Measurements (2×H200, GPFS)
Real code path, Wan2.2-T2V-A14B, both experts, fp32 master:
Standalone cold-cache single-expert comparison (fair A/B on untouched file sets): 94.9 s → 65.6 s, peak 53.9 → 27.3 GB. With more ranks the gap widens: legacy peak stays ≈ full-component size while stream peak shrinks with world size.
Correctness
from_pretrainedon Wan2.1-T2V-1.3B via the real backend path: 825/825 params (gathered viafull_tensor()) + both non-persistent rope buffers.tests/fast-gpu/test_fsdp_stream_load_equivalence.py): self-contained tiny sharded Wan checkpoint; base + LoRA variants; asserts param/buffer bitwise equality and LoRA A-init/B-zero.tests/fast/.../test_stream_load.py): meta build contract, non-diffusers component rejection, buffer survival acrossto_empty, key-map inversion, data-aware-init rejection.tests/fast/backends/fsdp_utils: 26 passed.lora_only, strict=False) actually requires base weights materialized fromhf_checkpoint— satisfied by both modes.Known limitations
qwen-image + --fsdp-cpu-offload + stream: the rope rebuild's CUDA-device guard no-ops when params materialize on CPU (same class of issue existed pre-PR only if the model never hit CUDA; flagging for a follow-up).trust_remote_code) can't be class-resolved frommodel_index.json→ clear error pointing at legacy mode.set_model_state_dictbroadcast is per-tensor; further wins available (pinned staging, batched broadcast) but init is already 4× faster.🤖 Generated with Claude Code