""" python repro.py # A: stock _cp_plan -> crash python repro.py --mask # B: corrected plan -> attn_mask head-axis bug python repro.py --fixed # C: corrected plan + fix -> matches single-rank"""importargparse, os, tracebackimporttorchimporttorch.distributedasdistimporttorch.multiprocessingasmpimportdiffusers.models.attention_dispatchasADfromdiffusers.models._modeling_parallelimport (
ContextParallelConfig, ContextParallelInput, ContextParallelOutput,
)
fromdiffusers.models.transformers.transformer_ltx2importLTX2VideoTransformer3DModelWORLD=2CONFIG=dict(
in_channels=8, out_channels=8, patch_size=1, patch_size_t=1,
num_attention_heads=4, attention_head_dim=16, cross_attention_dim=64,
vae_scale_factors=(8, 32, 32), pos_embed_max_pos=20,
base_height=2048, base_width=2048, gated_attn=True, cross_attn_mod=True,
audio_in_channels=8, audio_out_channels=8, audio_patch_size=1,
audio_patch_size_t=1, audio_num_attention_heads=4, audio_attention_head_dim=8,
audio_cross_attention_dim=32, audio_scale_factor=4, audio_pos_embed_max_pos=20,
audio_gated_attn=True, audio_cross_attn_mod=True,
num_layers=2, caption_channels=32, rope_type="interleaved",
use_prompt_embeddings=True,
)
B, N_VIDEO, N_AUDIO, N_TEXT=1, 32, 8, 16# all divisible by WORLDdefmake_inputs():
g=torch.Generator().manual_seed(0)
returndict(
hidden_states=torch.randn(B, N_VIDEO, 8, generator=g),
audio_hidden_states=torch.randn(B, N_AUDIO, 8, generator=g),
encoder_hidden_states=torch.randn(B, N_TEXT, 32, generator=g),
audio_encoder_hidden_states=torch.randn(B, N_TEXT, 32, generator=g),
timestep=torch.full((B, N_VIDEO), 500.0), # PER-TOKEN, as the pipelines pass itaudio_timestep=torch.full((B, N_AUDIO), 500.0),
sigma=torch.full((B,), 0.5), # per-batch, must NOT be splitaudio_sigma=torch.full((B,), 0.5),
encoder_attention_mask=torch.ones(B, N_TEXT),
audio_encoder_attention_mask=torch.ones(B, N_TEXT),
num_frames=2, height=4, width=4, # 2 * 4 * 4 = 32 video tokensaudio_num_frames=N_AUDIO,
fps=24.0, use_cross_timestep=True, return_dict=False,
)
def_in(dim=1, nd=3, out=False):
returnContextParallelInput(split_dim=dim, expected_dims=nd, split_output=out)
_ROPE= {0: _in(out=True), 1: _in(out=True)}
LTX2_CP_PLAN= {
"": {
"hidden_states": _in(),
"timestep": _in(nd=2),
"audio_hidden_states": _in(),
"audio_timestep": _in(nd=2),
"encoder_hidden_states": _in(),
"encoder_attention_mask": _in(nd=2),
"audio_encoder_hidden_states": _in(),
"audio_encoder_attention_mask": _in(nd=2),
},
"rope": _ROPE,
"audio_rope": _ROPE,
"cross_attn_rope": _ROPE,
"cross_attn_audio_rope": _ROPE,
"proj_out": ContextParallelOutput(gather_dim=1, expected_dims=3),
"audio_proj_out": ContextParallelOutput(gather_dim=1, expected_dims=3),
}
_ORIG=AD.TemplatedUlyssesAttention.forwarddef_patched(ctx, query, key, value, attn_mask, *args, **kwargs):
parallel_config=kwargs.get("_parallel_config") orargs[-1]
ifattn_maskisnotNoneandattn_mask.ndim==4:
cp=parallel_config.context_parallel_configworld_size, local_rank=cp.ulysses_degree, cp._ulysses_local_ranknum_heads=query.shape[2]
ifattn_mask.shape[1] ==num_headsandnum_heads%world_size==0:
attn_mask=attn_mask.chunk(world_size, dim=1)[local_rank].contiguous()
return_ORIG(ctx, query, key, value, attn_mask, *args, **kwargs)
defworker(rank, mode):
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29591")
dist.init_process_group("gloo", rank=rank, world_size=WORLD)
torch.manual_seed(0)
model=LTX2VideoTransformer3DModel(**CONFIG).eval()
withtorch.no_grad():
ref_video, ref_audio=model(**make_inputs())
ifmode=="fixed":
AD.TemplatedUlyssesAttention.forward=staticmethod(_patched)
kwargs= {} ifmode=="stock"else {"cp_plan": LTX2_CP_PLAN}
model.enable_parallelism(config=ContextParallelConfig(ulysses_degree=WORLD), **kwargs)
try:
withtorch.no_grad():
video, audio=model(**make_inputs())
ifrank==0:
print(f"[{mode}] ran. max|delta| vs single-rank: "f"video={(video-ref_video).abs().max():.3e} "f"audio={(audio-ref_audio).abs().max():.3e}")
exceptExceptionasexc:
ifrank==0:
frames= [ln.strip() forlnintraceback.format_exc().splitlines()
if"transformer_ltx2.py"inlnor"attention_dispatch.py"inln]
print(f"[{mode}] FAILED: {type(exc).__name__}: {exc}")
print(f"[{mode}] at {frames[-1] ifframeselse'?'}")
dist.destroy_process_group()
if__name__=="__main__":
ap=argparse.ArgumentParser()
ap.add_argument("--fixed", action="store_true")
ap.add_argument("--mask", action="store_true")
a=ap.parse_args()
mp.spawn(worker, args=("fixed"ifa.fixedelse"mask"ifa.maskelse"stock",),
nprocs=WORLD, join=True)
Describe the bug
LTX2VideoTransformer3DModel._cp_plan(transformer_ltx2.py:1094) is textually identical toLTXVideoTransformer3DModel._cp_plan(transformer_ltx.py:417), but LTX-2 is a dual-stream audio+video model with a different forward. The plan doesn't cover:timestep— per-token on LTX-2,(B, num_video_tokens), so it must be shardedaudio_hidden_states,audio_timestep,audio_encoder_hidden_states,audio_encoder_attention_mask,audio_proj_outaudio_rope,cross_attn_rope,cross_attn_audio_ropeenable_parallelism()accepts it, then the forward fails attransformer_ltx2.py:634withsize of tensor a (16) must match tensor b (32)— sharded video tokens against the unsharded per-token timestep.The part I'd flag as more important than the crash: the obvious minimal repair — mirror Wan and shard video only — is silently wrong on LTX-2. Its blocks run bidirectional AV cross-attention;
v2ahas Q=audio, KV=video, so with video sharded and no collective on that call each rank attends over1/Pof the video keys. Shapes line up, nothing raises, output is wrong. Both streams have to be sharded.Verified against
main@60092255.Reproduction
CPU-only, gloo, 2 ranks, no GPU — spawns its own ranks, so
python repro.pyis all it takes.repro.py—--maskand--fixedselect the armsThe
--fixedarm is the 12-entry plan in the repro, which matches single-rank to fp32 accumulation error through the a2v/v2a path.Secondary:
TemplatedUlyssesAttentiondoesn't shardattn_mask's head axisIndependent of LTX-2, but LTX-2 is the first model to reach it (it applies CP to masked cross-attention; Wan guards that at
transformer_wan.py:152).attention_dispatch.py:2393all-gathers a sharded mask on the KV axis, but q/k/v come back from the all-to-all withH // world_sizeheads while the mask keeps allH→ broadcast failure in SDPA. Distinct from #13696/#13756, which fixed the sequence axis. Fix is achunk(world_size, dim=1)[ulysses_local_rank]alongside the KV gather; same gap in theulysses_anythingbranch at:2609.Two smaller notes:
_prepare_cp_inputwarns and skips on a dims mismatch rather than raising, soexpected_dims=3for"rope"silently degrades to unsharded atrope_type="split"(4-D output); andvideo_self_attention_maskis(B, T_v, T_v), a two-axis shardContextParallelInputcan't express, so the IC-LoRA path can't be fixed by a plan alone.System Info
diffusersmain@60092255(also on0.39.0) · torch 2.13.0 · Python 3.12 · Linux · CPU-only, gloo, 2 ranksWho can help?
@sayakpaul@DN6@yiyixuxu · cc @zhtmike re the Ulysses mask handling