Feat: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled#21
Open
zhihengy wants to merge 24 commits into
Open
Feat: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled#21zhihengy wants to merge 24 commits into
zhihengy wants to merge 24 commits into
Conversation
FSDP shards on the dp mesh dim only; SP (ulysses x ring) gets its own process groups plus sglang's _SP coordinator so USPAttention resolves them. context_parallel_size stays as a backward-compatible alias. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-attention runs sglang's USPAttention; the sequence is sharded before the first block and gathered after proj_out, so model outputs stay full-sequence and loss/log_prob code is unchanged. Partial grads are summed across the sp group after scaler.unscale_. Applied per component, covering dual-DiT. Recipe gains SP_SIZE/ULYSSES_DEGREE/RING_DEGREE. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
connect_rollout_engines grouped by raw rollout_num_gpus_per_engine while rollout.init_rollout_engines uses min(per_engine, num_gpus_per_node); a mismatch orphans trailing ranks that fail later with an opaque error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rt guard) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…_plan The shard/gather placement (rope outputs, first-block input, proj_out output) was hard-coded to Wan's module topology; diffusers 0.37 declares the same contract per model as _cp_plan, so consume that instead. The attention processor remains Wan-specific and is now guarded explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-data band comparison Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ning dependency RL needs a differentiable attention in the trainer, not in the rollout engine; importing sglang's USPAttention leaked that requirement into a fork the engine had to carry. sp_ops.py holds the Ulysses all-to-all, ring attention (torch ring templates), and SDPA local attention with the same layout, so training numerics are unchanged and sglang needs no patches. The trainer's only remaining sglang import is the weight-sync checksum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- drop context_parallel_size and its CP->SP aliasing: the old assert==1 meant no config with cp>1 could ever have existed - drop the num_heads param from validate_sp_config and the always-None getattr feeding it: the real guard lives in apply_sequence_parallel - degree-1 ulysses/ring dimensions get no process groups (None = local), and the unused per-rank singleton tp_group is gone - move the sequence shard/gather collectives into sp_ops so all differentiable SP collectives live in one module - remove the dead --fwd-only flag (ring backward works) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mesh Aligns with torchtitan/VeOmni/DeepSpeed-Ulysses: FSDP shards over the flattened dp x sp mesh (default), halving/quartering per-rank parameter and master-state memory under SP (measured 54.4 -> 27.2 GB/rank at dp2xsp2). Gradient semantics live in the sequence gather's backward (sum over the sp group), so the explicit cross-sp grad all-reduce is skipped; fsdp_shard_mode=dp keeps the replicated placement as a validation anchor. Guardrail preamble no longer kills by process name; it refuses to start when this run's GPUs are occupied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
clip_grad_norm_ returns a lazily-reduced partial-norm DTensor; logging it without full_tensor() leaks the local shard's norm, under-reporting grad_norm by sqrt(n_shards) (2x on the dp4 recipe). Clipping itself was always correct — the coefficient is computed in DTensor arithmetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…delBackend A model family opts into SP by declaring a SequenceParallelPlan (boundaries, attention installer, head count) instead of the engine hard-coding Wan: - ModelBackend.sequence_parallel_plan assembles the plan; the diffusers default reads the model's _cp_plan and the family config's apply_sp_attention hook. Native backends override the method wholesale. - WanUSPAttnProcessor moves to configs/wan2_2.py; the engine layer (sp_attention/sp_ops/sp_mesh/parallel) no longer references any model class. - The isinstance whitelist becomes capability checks: no _cp_plan or no apply_sp_attention raises with the missing declaration named. Validated: CPU suites (88 passed) + full SP GPU suite (21 bands, 66 checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First full pre-commit pass on this branch (the job never ran while the PR was based on feat/wan): isort import merges, black line wraps, ruff Callable-import modernization, unused-import removal. No code changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ard and key-resolution guards Adversarial review of the plan seam surfaced four holes no numerics test covers; all four now fail loudly at the earliest point the facts exist: - validate_sp_support (driver arg validation, zero weight loads): rejects sp>1 with an explicit --fsdp-attention-backend (the SP installer replaces every processor, so backend selection and the deterministic-flash patch can never take effect), a model backend without sequence_parallel_plan, or a diffusers family without apply_sp_attention — previously these surfaced only after full load + FSDP sharding of every component. - plan construction rejects wildcard _cp_plan boundaries (e.g. QwenImage's transformer_blocks.*) the hook installer cannot expand yet, instead of a bare AttributeError deep in the getattr walk. - boundary key resolution reads the forward signature from the get_base_model()-unwrapped module (PeftModel exposes (*args, **kwargs)) and raises at install time for unmappable keys instead of silently skipping the split. Validated: 95 CPU tests (7 new), full SP GPU suite (21 bands, 66 checks), grad-sync --lora bands for both shard modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inistic mode Validates that deterministic_mode's guarantees survive USP: forward+backward twice on identical inputs must produce bitwise-equal outputs and grads for both the ulysses (SDPA local) and ring (aten flash fwd/bwd) paths, with use_deterministic_algorithms(True, warn_only=False) engaged — warn_only=False also asserts no op on the path is registered nondeterministic, confirming the aten deterministic gate covers the ring backward torch templates call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… demoted to a test anchor FSDP always shards over the flattened dp x sp mesh; SP is invisible to it (same parameter account as sp=1). Placement is not a user decision, so the config surface shrinks to --sequence-parallel-size + --ulysses-degree (ring = sp / ulysses, derived): - fsdp_shard_mode and ring_degree leave arguments; the mesh selection in parallel.py is branch-free (comment notes the HSDP caveat: flatten the shard axes, which today happen to be the whole world). - _all_reduce_sp_grads leaves the actor; the sequence gather's sum_grad backward is the only gradient mechanism in production. - The sp-replicated Option B placement survives verbatim in tests/sp as a validation anchor: grad-sync/weight-sync build it from the exported dp_mesh and apply_sequence_parallel(..., sum_grad=False), so the cross-placement parity bands still exercise production machinery. Validated: 95 CPU tests, full SP GPU suite (66 checks), grad-sync --lora for both placements, determinism smokes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y input surface ring = sp / ulysses is fully determined, so taking it as input carried zero information; its one historical value (cross-checking contradictory u,r pairs) died with the config-surface removal. sp_mesh's pure functions now take (sp, ulysses) only and return the derived ring; the u*r==sp product check collapses to a plain sp % u divisibility check. Test CLIs express ring topologies as (sp, ulysses): u2r2 = --sp 4 --ulysses 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 2026
…ention_fn A model family no longer writes an attention processor: the default apply_sp_attention wraps the modeling module's dispatch_attention_fn (found through the MRO — fully_shard swizzles the class into torch's fsdp module) and reroutes self-attention call sites to sp_ops.usp_attention. Models flag self- vs cross-attention themselves (upstream passes parallel_config only when encoder_hidden_states is None), so the model's own processors run untouched and WanUSPAttnProcessor (the 80-line copy) is deleted. The wrapper fails loudly outside the validated envelope (mask / dropout / is_causal / custom scale); models whose modules never imported dispatch_attention_fn are rejected at install with instructions to provide a family override. Kernel choice stays pinned inside usp_attention. Validated: parity vs the stock diffusers processors is bitwise for forward and input grads (fp32 band exact); full GPU suite 77 checks; 10-step real-RL runs at sp=1 / u4 / u2r2 / u1r4 — per-step train-vs-rollout canary flat across all topologies (no rounding accumulation), rewards and clipfrac in the sp=1 band. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ll coverage" This reverts commit bc44519.
Partial revert of 0420f3b: that commit also carried the dp_mesh -> fsdp_mesh line (folded in during a rebase), which belongs to the SP placement and stays. Only the DTensor import and the full_tensor materialization leave; they land independently as #35, which this PR's cross-placement grad_norm comparisons depend on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full validation suite (mesh invariants, attention/grad/weight-sync parity, determinism smokes, RL guardrail runner) stays out of this PR by maintainer decision; every band was run locally against this exact tree (results recorded in the PR description). The files remain in the working tree for local use and will be contributed with CI registration in a follow-up PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tch-only SequenceParallelPlan, the boundary-hook interpreter and apply_sequence_parallel move to sp_plan.py; sp_attention.py keeps only the dispatch-level attention installer. The two are siblings over sp_ops — the plan holds attention as an opaque callable, so neither imports the other, and the file layout now matches the dependency structure (model_backend imports the contract, the family-config default imports the installer). Pure code motion, no behavior change. 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.
Summary
USP (Ulysses × Ring) sequence parallelism for DiT GRPO training: the latent sequence of a single sample is sharded across GPUs, so activation memory scales ÷sp and trainable video length ×sp, while training stays numerically equivalent to sp=1 up to bounded floating-point rounding. The engine is model-agnostic — a family opts in by declaring a
SequenceParallelPlan; Wan is enabled with zero family-specific attention code. Default--sequence-parallel-size 1is a no-op.Design
Coordinates.
global rank = dp_rank · sp + sp_rank, withsp = ulysses × ring. Ulysses ranks are contiguous (its all-to-all is bandwidth-hungry and unoverlappable → keep it on NVLink); ring ranks are strided (its p2p overlaps with compute → fit for cross-node). Layout is pure functions (sp_mesh.py), no distributed init required.Data. Dispatch is by
dp_rank, so sp peers automatically share samples — the foundation for every "identical loss across the sp group" argument below, and it makes dual-DiT expert routing consistent for free.Sequence lifecycle. The full latent enters the model; boundary hooks — declared by the model's own diffusers
_cp_plan— slice hidden_states after patchify (and rope to the local chunk), and gather (a differentiable all-gather) afterproj_out. Model outputs are therefore full-sequence and bitwise identical across sp ranks: loss / log_prob / metrics code is untouched.Attention. The default installer intercepts diffusers'
dispatch_attention_fn— the single choke point every modern diffusers processor routes attention through, where the model itself already flags self- vs cross-attention per call site. Self-attention is rerouted tosp_ops.usp_attention(ulysses a2a → local SDPA, or ring via torch's_templated_ring_attention→ a2a back); cross-attention and the model's own processors run untouched. No per-family processor copies to maintain.Parameter placement. FSDP always shards over the flattened dp×sp mesh — its memory account is identical to sp=1; SP is invisible to FSDP. Gradient correctness is one exact identity: the sequence gather's backward all-reduces(SUM) over the sp group (×sp on every partial grad), and FSDP's mean over dp·sp divides it back —
(1/(dp·sp)) · Σ sp·g = (1/dp) · Σ_dp g. No per-param cross-sp sync, no optimizer divide factor. Measured at dp2×sp2: 54.4 → 27.2 GB/rank.Model seam.
ModelBackend.sequence_parallel_plan(model)assembles{boundaries, attention installer, head count}; the diffusers default reads the model's_cp_plan. Native-modeling backends override the method wholesale. The engine references no model class.Config surface:
--sequence-parallel-size+--ulysses-degree(ring = sp / ulysses, derived).Why a 2D mesh + hand-built subgroups (not a 3D mesh)
The device mesh is 2D
(dp, sp); ulysses/ring groups are built withdist.new_groupfrom the pure-function layout rather than as mesh dimensions:all_to_all_single, ring send/recv), which take ProcessGroup handles. A 3D mesh would create dims with zero consumers._flatteninstead of two overlapping ones. FSDP needs flatten(dp, sp); a 3D mesh would additionally need flatten(ring, ulysses) for the sp group, and the two flattens share dims — fragile territory on a private API.None→ the dimension doesn't exist", which composes badly with size-1 mesh dims.The 2D-mesh row-major order and the pure-function layout are the same implicit contract; it is pinned by tests (NCCL group members asserted equal to the pure-function lists).
Limitations — everything outside the validated envelope fails loudly
world % sp != 0validate_sp_config, driver-side at startupsp % ulysses != 0resolve_sp_degreesnum_heads % ulysses != 0apply_sequence_parallel(the plan carries the real head count)S % sp != 0(padding unimplemented)shard_sequence, first forward--fsdp-attention-backendwith sp>1validate_sp_support, driver-side, before any weight loadsequence_parallel_planvalidate_sp_support_cp_plan, or wildcard plan keys (e.g.transformer_blocks.*)dispatch_attention_fnKnown non-guarded boundaries: sp>1 weight grads differ from sp=1 by bf16 summation rounding (inherent, see below); multi-node is untested; checkpoint resume across topologies is untested.
Validation (selected)
--deterministic-mode: one tree, same inputs, run twiceNumerics: deterministic vs random deviation, and what
--deterministic-modebuysUlysses — deterministic deviation, in exactly one place. Its all-to-all is a pure permutation and each head's attention runs whole on one rank with the unchanged kernel, so forward and input grads are bitwise equal to sp=1. Only weight grads differ:
dW = xᵀ·dyreduces over the token axis — precisely the axis SP splits — so the summation tree changes (one long GEMM accumulation vs per-rank partials + NCCL sum). This is deterministic reordering, not noise: 3–8e-3 rel in bf16 (1–2 ulp of bf16's 2⁻⁸ epsilon, the floor for any reordered sum), collapsing to 3–8e-7 in fp32 — the error scales with mantissa width, the fingerprint of rounding rather than a logic error.Ring — adds deterministic deviation to forward too, plus one genuinely random source. Ring replaces the single-kernel softmax with r online-softmax chunk merges (the same algebra flash attention uses across SRAM tiles, moved to rank boundaries): forward deviates from sp=1 by ~3.6e-3 — frozen, reproduced identically across reruns and grad-ckpt variants. The merge chain accumulates in fp32 inside torch's template, so this does not grow with ring degree. The random component lives in the aten flash backward it calls: gradient accumulation via
atomicAdd, whose thread arrival order varies run to run.--deterministic-modeeliminates the random component — verified.use_deterministic_algorithms(True, warn_only=False)engages the aten deterministic gate, switching the flash backward to ordered accumulation; the determinism smoke shows double-runs bitwise identical (outputs and every gradient) on the pure-ring path, andwarn_only=Falsedoubles as a detector — any op on the path lacking a deterministic implementation would raise, and none does. Note the precise claim: deterministic mode buys repeatability (run-to-run bitwise), not sp=1-equality — the rounding deviation above remains, frozen and certified as rounding by the fp32 scaling law. The 10-step topology study confirms it does not accumulate in any topology.