Skip to content

Feat: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled#21

Merged
Rockdu merged 40 commits into
mainfrom
feat/wan_usp
Jul 25, 2026
Merged

Feat: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled#21
Rockdu merged 40 commits into
mainfrom
feat/wan_usp

Conversation

@zhihengy

@zhihengy zhihengy commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

USP (Ulysses x 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 x sp, while training stays numerically equivalent to sp=1 up to bounded floating-point rounding. Default --sequence-parallel-size 1 is a no-op.

The Wan call chain under SP:

actor (sp > 1)
├─ boundary hooks, declared by the model's diffusers _cp_plan:
│    shard hidden_states after patchify → gather (differentiable all-gather) after proj_out.
│    The gather's backward all-reduces(SUM) over the sp group; FSDP's 1/(dp·sp) mean then
│    restores (1/dp)·Σ_dp exactly. Model outputs stay full-sequence and bitwise identical
│    across sp ranks, so loss/log_prob code is untouched.
└─ wrapped dispatch_attention_fn (the modeling module's, found through the MRO under
   FSDP's class swizzle), marker-gated by _USPDispatchConfig set as the processors'
   _parallel_config:
     self-attention        → usp_attention: Ulysses all-to-all (heads sharded, sequence
                             gathered), then Ring via torch's ring templates with an aten
                             kernel selected from RING_KERNELS (ring_backend = the call
                             site's backend kwarg, i.e. --fsdp-attention-backend)
     cross-attention / I2V → original dispatch_attention_fn, stock path (text K/V are
                             replicated, local attention is exact; the configured
                             backend stays live)

Calls without the marker pass through the original dispatch untouched; the model's own processors are never replaced.

Design

Coordinates. global rank = dp_rank · sp + sp_rank, with sp = ulysses × ring. Ulysses ranks are contiguous (its all-to-all is bandwidth-hungry → keep it on NVLink); ring ranks are strided (its transfers overlap with compute → fit for cross-node). Layout is pure functions (sequence_parallel/topology.py).

Data. Dispatch is by dp_rank, so sp peers automatically share samples — the foundation for every "identical loss across the sp group" argument, and it makes dual-DiT expert routing consistent for free.

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: (1/(dp·sp)) · Σ sp·g = (1/dp) · Σ_dp g. Measured at dp2×sp2: 54.4 → 27.2 GB/rank.

Attention install. A family declares a SequenceParallelPlan{boundaries, attention_installer, num_attention_heads}; DiffusersModelBackend.sequence_parallel_plan returns the model's pre-attached _miles_sp_plan if present, else assembles the default from the model's _cp_plan boundaries with apply_dispatch_sp_attention as installer. Native backends override sequence_parallel_plan wholesale. set_attention_backend runs independently of SP install; the backend reaches the wrapped dispatch as the ordinary call-site kwarg.

Ring kernel selection. --fsdp-attention-backend also picks the ring kernel via RING_KERNELS (_native_flash → aten flash, _native_cudnn → aten cudnn; both return LSE and pair with a real backward kernel). Everything else is rejected for ring at argument validation. The cudnn kernel measured 1.8x faster than aten flash for Wan shapes on H200 (fwd+bwd, B1 H40 S32768 D128 bf16: 129ms vs 235ms).

Why not diffusers' own context parallelism. Its training backward is broken on 0.37.0 in four distinct places (native op recomputes SDPA without enable_grad, fixed upstream in 0.38; cudnn/flash backward ops re-transpose already-transposed saved tensors; cudnn forward computes LSE only when return_lse; TemplatedRingAttention.backward feeds every rotation step from chunk-0 saved tensors — the last three still present on upstream main). The wrapped dispatch passes no diffusers parallel_config downstream, so that branch is unreachable.

Config surface: --sequence-parallel-size + --ulysses-degree (ring = sp / ulysses, derived) + --fsdp-attention-backend.

Limitations — everything outside the validated envelope fails loudly

Condition Where it raises
sp < 1, ulysses < 0, world % sp, sp % ulysses topology validation, driver-side at startup
num_attention_heads % ulysses != 0 apply_sequence_parallel
S % sp != 0 (padding unimplemented) shard_sequence, first forward
ring > 1 on torch < 2.11 (torch's private ring templates; the CI image pins torch==2.11; 2.9's template merge is additionally flash-only) ModuleNotFoundError at first ring call
ring > 1 with a backend outside RING_KERNELS validate_sp_args, driver-side
modeling module without dispatch_attention_fn and no family _miles_sp_plan apply_dispatch_sp_attention, at install
model backend without sequence_parallel_plan validate_sp_args, driver-side
model without _cp_plan, or wildcard plan keys plan construction
attention mask / dropout / is_causal / explicit scale on self-attention under SP wrapped dispatch, at call time

Known non-guarded boundaries: sp>1 weight grads differ from sp=1 by bf16 summation rounding (inherent, see numerics); multi-node is untested; checkpoint resume across topologies is untested.

Validation

Offline suite (4x H200; ulysses bands on torch 2.9.1, ring bands on torch 2.11):

Test What it locks Result
CPU fast tests (108) validation rules, plan ownership, ring kernel rejection PASS
Op-level parity, 5 bands (u4r1 / u2r2 / u1r4 flash; u2r2 / u1r4 cudnn) usp_attention vs full-sequence SDPA reference PASS — u4r1 forward AND dQ/dK/dV bitwise; ring forward 5.6e-3, grads 6.1–7.2e-3 (rtol 8e-3); cudnn deviation identical to flash to 3 digits
Deterministic double-run (u4r1, u2r2, u1r4) --deterministic-mode under SP incl. ring PASS — forward and all grads bitwise in every band
Grad-sync parity under FSDP2 (3 bands incl. u2r2) the dp×sp gradient identity on the production path PASS (07-21 revalidation at f76da39; code path unchanged)
Weight-sync parity (3 bands) train→rollout weight reconstruction PASS (07-21 revalidation; checksums bitwise, dtype/shape-sensitive)

E2E (Wan2.2 PickScore GRPO, 4 GPUs colocate, 5 rollouts = 10 optimizer steps per band; ring imports re-homed to torch 2.9 for the box's conda env, one-line delta):

Band Result
sp1 / u4r1 / u2r2 / u1r4 / u2r2+_native_cudnn sp1 / u4r1 / u2r2 / u1r4 PASS (10 optimizer steps each); u2r2+cudnn: rollout + reward identical, train step unrunnable on the box's torch-2.9 ring-template re-home (2.9's merge is flash-only, cudnn LSE layout mismatches) — the cudnn ring kernel is locked by the op-level band on torch 2.11
rollout-0 reward stats across all 5 bands identical to every printed digit across all 5 bands (mean=0.7977 std=0.0511 min=0.6587 max=0.8907)
train-vs-rollout canary (log_prob_mean_abs_diff), 10 steps per band flat in every completed band, tails ~ heads (sp1 8.3e-6->1.0e-5; u4r1 8.9e-6->1.3e-5; u2r2 8.9e-6->9.1e-6; u1r4 8.5e-6->1.2e-5); no accumulation

Per-band mean optimizer-step wall time (same recipe, 8 prompts x 8 samples, 5 frames 480x480):

Band dp x sp opt-step wall
sp1 dp4 6.6s
u4r1 dp1 x sp4 14.2s
u2r2 dp1 x sp4 15.8s
u1r4 dp1 x sp4 15.2s

SP at dp1 pays ~2.2-2.4x per optimizer step at this scale (serialized grad accumulation + per-layer collectives); the same ratio reproduces at the production recipe (48x16, 480x832).

Numerics

Ulysses — deterministic deviation, in exactly one place. The 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ᵀ·dy reduces over the token axis — precisely the axis SP splits — so the summation tree changes. 3–8e-3 rel in bf16, collapsing to 3–8e-7 in fp32: the error scales with mantissa width, the fingerprint of rounding.

Ring — adds deterministic deviation to forward, plus one random source. Ring replaces the single-kernel softmax with r online-softmax chunk merges; forward deviates from sp=1 by ~5.6e-3, frozen across reruns. The random component is the fused kernel's backward atomicAdd ordering. The cudnn ring kernel shows the same deviation class as flash (op-level bands: normalized deltas equal to 3 digits, ≤7.25e-3 vs the 8e-3 band).

--deterministic-mode buys repeatability, not sp=1-equality. use_deterministic_algorithms(True, warn_only=False) switches the aten backward to ordered accumulation; double-runs are bitwise identical (outputs and every gradient) in all three topologies, and warn_only=False doubles as a detector for any uncovered op on the path. The rounding deviation above remains, frozen. The 10-step topology study confirms it does not accumulate.

@zhihengy
zhihengy changed the base branch from feat/wan to main July 10, 2026 00:42
@zhihengy zhihengy changed the title diffusion: USP sequence parallelism (Ulysses x Ring) for Wan training diffusion: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled Jul 10, 2026
@zhihengy zhihengy added the run-ci-all Run every GPU test regardless of domain labels label Jul 12, 2026
@zhihengy zhihengy changed the title diffusion: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled Feat: USP sequence parallelism (Ulysses x Ring) via SequenceParallelPlan; Wan enabled Jul 13, 2026
@zhihengy
zhihengy marked this pull request as ready for review July 14, 2026 00:01
Comment thread miles/backends/fsdp_utils/parallel.py Outdated
Comment thread miles/backends/fsdp_utils/sequence_parallel/attention.py
Comment thread miles/backends/fsdp_utils/configs/train_pipeline_config.py Outdated
@zhihengy
zhihengy force-pushed the feat/wan_usp branch 6 times, most recently from 90b60e0 to 1f235f0 Compare July 24, 2026 02:17
zhihengy and others added 12 commits July 24, 2026 02:35
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>
zhihengy and others added 22 commits July 24, 2026 02:36
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>
…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>
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>
… axis

cp_rank/cp_size/cp_group/dp_cp_group had no consumers outside the
sp-alias wiring; the dp x sp composites rename to dp_sp_* to match the
flattened FSDP mesh dim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erimental dependency

The CI image (sglang v0.5.12) pins torch==2.11.0, where torch moved the
private ring-attention templates to _context_parallel/_attention.py and
the back-compat stub re-exports only the forward one. Import the 2.11
home directly; older torch fails loudly at first ring use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ows the attention backend"

This reverts commit c957a13.
@Rockdu

Rockdu commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Made a few edits, others are good for now

@Rockdu
Rockdu merged commit 90b6451 into main Jul 25, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci-all Run every GPU test regardless of domain labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants