Skip to content

Commit e09195e

Browse files
committed
feat: configure chunked Ulysses all-to-all
Add a ulysses_attention_chunks attention config to split the Ulysses all-to-all into head-group passes. The chunked path lets XLA overlap all-to-all collectives with head-parallel local attention compute while preserving the existing single-shot path by default. Apply the same chunking to plain Ulysses and Ulysses+Ring, and allow the final chunk to carry the remainder when the requested chunk count does not divide the Ulysses head groups evenly. Add mocked attention tests for numerical and layout equivalence across chunk counts.
1 parent 0984457 commit e09195e

9 files changed

Lines changed: 265 additions & 7 deletions

File tree

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ use_base2_exp: True
9494
use_experimental_scheduler: True
9595
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9696
ulysses_shards: -1
97+
# Splits Ulysses all-to-all into head-group chunks. The last chunk carries any remainder.
98+
ulysses_attention_chunks: 1
9799
flash_min_seq_length: 4096
98100
dropout: 0.0
99101

src/maxdiffusion/models/attention_flax.py

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,44 @@ def _build_padding_segment_ids(
413413
return segment_ids_cls(q=q_segment_ids, kv=kv_segment_ids)
414414

415415

416+
def _ulysses_head_chunk_ranges(num_heads: int, ulysses_shards: int, num_chunks: int):
417+
"""Build head-axis ranges for chunked Ulysses all-to-all.
418+
419+
The Ulysses all-to-all splits each local chunk's head axis over
420+
`ulysses_shards`, so every returned range length is a multiple of
421+
`ulysses_shards`. When `num_chunks` does not evenly divide the number of
422+
Ulysses-sized head groups, earlier chunks get the floor-sized range and the
423+
final chunk carries the remainder.
424+
425+
Returns:
426+
A list of `(start, end)` half-open ranges over the head axis. Concatenating
427+
tensors sliced with these ranges along the head axis restores the original
428+
head layout. For `num_chunks <= 1`, returns `[(0, num_heads)]`, which is the
429+
unchunked all-to-all path.
430+
"""
431+
if num_chunks <= 1:
432+
return [(0, num_heads)]
433+
if num_heads % ulysses_shards != 0:
434+
raise ValueError(
435+
"Ulysses attention requires the number of heads to be divisible by the Ulysses shard count, "
436+
f"got heads={num_heads} and ulysses_shards={ulysses_shards}."
437+
)
438+
439+
head_groups = num_heads // ulysses_shards
440+
num_chunks = min(num_chunks, head_groups)
441+
regular_groups_per_chunk = max(1, head_groups // num_chunks)
442+
443+
ranges = []
444+
start_group = 0
445+
for chunk_idx in range(num_chunks):
446+
end_group = head_groups if chunk_idx == num_chunks - 1 else min(start_group + regular_groups_per_chunk, head_groups)
447+
if start_group >= end_group:
448+
break
449+
ranges.append((start_group * ulysses_shards, end_group * ulysses_shards))
450+
start_group = end_group
451+
return ranges
452+
453+
416454
def _tpu_flash_attention(
417455
query: jax.Array,
418456
key: jax.Array,
@@ -640,6 +678,7 @@ def _ulysses_attention(
640678
use_custom_kernel: bool = False,
641679
use_base2_exp: bool = True,
642680
use_experimental_scheduler: bool = False,
681+
ulysses_attention_chunks: int = 1,
643682
) -> jax.Array:
644683
"""Ulysses sequence-parallel attention.
645684
@@ -662,6 +701,7 @@ def _ulysses_attention(
662701
"Ulysses attention requires the number of heads to be divisible by the context shard count, "
663702
f"got heads={num_heads} and context_shards={num_shards}."
664703
)
704+
665705
if not use_custom_kernel:
666706
block_sizes = _select_flash_block_sizes(query, key, flash_block_sizes, dtype, "flash")
667707

@@ -762,7 +802,21 @@ def wrap_ulysses_attention(query, key, value):
762802
"Warning, batch dimension should be shardable among the devices in data and fsdp"
763803
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
764804
)
765-
x = wrap_ulysses_attention(query, key, value)
805+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_shards, ulysses_attention_chunks)
806+
if len(head_chunk_ranges) > 1:
807+
# Run Ulysses all-to-all per head group so XLA can overlap one group's
808+
# collective with another group's head-parallel local attention compute.
809+
chunk_outputs = [
810+
wrap_ulysses_attention(
811+
query[:, start:end],
812+
key[:, start:end],
813+
value[:, start:end],
814+
)
815+
for start, end in head_chunk_ranges
816+
]
817+
x = jnp.concatenate(chunk_outputs, axis=1)
818+
else:
819+
x = wrap_ulysses_attention(query, key, value)
766820
x = x[:, :, :orig_q_seq_len, :]
767821
x = _reshape_heads_to_head_dim(x)
768822

@@ -787,6 +841,7 @@ def _ulysses_ring_attention(
787841
use_base2_exp: bool = False,
788842
use_experimental_scheduler: bool = False,
789843
ulysses_shards: int = -1,
844+
ulysses_attention_chunks: int = 1,
790845
) -> jax.Array:
791846
"""2D context-parallel attention using a private Ulysses x ring mesh.
792847
@@ -916,7 +971,21 @@ def wrap_ulysses_ring_attention(query, key, value):
916971
"Warning, batch dimension should be shardable among the devices in data and fsdp"
917972
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
918973
)
919-
x = wrap_ulysses_ring_attention(query, key, value)
974+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
975+
if len(head_chunk_ranges) > 1:
976+
# Run the Ulysses phase per head group before the ring phase so XLA can
977+
# overlap all-to-all collectives with head-parallel local attention compute.
978+
chunk_outputs = [
979+
wrap_ulysses_ring_attention(
980+
query[:, start:end],
981+
key[:, start:end],
982+
value[:, start:end],
983+
)
984+
for start, end in head_chunk_ranges
985+
]
986+
x = jnp.concatenate(chunk_outputs, axis=1)
987+
else:
988+
x = wrap_ulysses_ring_attention(query, key, value)
920989
x = jax.lax.with_sharding_constraint(x, q_axis_names)
921990
x = x[:, :, :orig_q_seq_len, :]
922991
x = _reshape_heads_to_head_dim(x)
@@ -941,6 +1010,7 @@ def _ulysses_ring_custom_attention(
9411010
use_base2_exp: bool = True,
9421011
use_experimental_scheduler: bool = False,
9431012
bidirectional: bool = False,
1013+
ulysses_attention_chunks: int = 1,
9441014
) -> jax.Array:
9451015
"""Hybrid Ulysses + Ring (USP) with the CUSTOM splash kernel on main's mesh.
9461016
@@ -1057,7 +1127,21 @@ def wrap_ulysses_ring_attention(query, key, value):
10571127
attention_output = a2a(attention_output, split_axis=2, concat_axis=1)
10581128
return attention_output
10591129

1060-
x = wrap_ulysses_ring_attention(query, key, value)
1130+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
1131+
if len(head_chunk_ranges) > 1:
1132+
# Run the custom Ulysses phase per head group before the custom ring phase so
1133+
# XLA can overlap all-to-all collectives with head-parallel attention compute.
1134+
chunk_outputs = [
1135+
wrap_ulysses_ring_attention(
1136+
query[:, start:end],
1137+
key[:, start:end],
1138+
value[:, start:end],
1139+
)
1140+
for start, end in head_chunk_ranges
1141+
]
1142+
x = jnp.concatenate(chunk_outputs, axis=1)
1143+
else:
1144+
x = wrap_ulysses_ring_attention(query, key, value)
10611145
x = jax.lax.with_sharding_constraint(x, q_axis_names)
10621146
x = x[:, :, :orig_q_seq_len, :]
10631147
x = _reshape_heads_to_head_dim(x)
@@ -1207,6 +1291,7 @@ def ulysses_custom_kernel(q, k, v, context):
12071291
use_custom_kernel=True,
12081292
use_base2_exp=context.get("use_base2_exp", True),
12091293
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1294+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12101295
)
12111296

12121297

@@ -1228,6 +1313,7 @@ def ulysses_ring_custom_kernel(q, k, v, context):
12281313
ulysses_shards=context["ulysses_shards"],
12291314
use_base2_exp=context.get("use_base2_exp", True),
12301315
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1316+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12311317
)
12321318

12331319

@@ -1253,6 +1339,7 @@ def ulysses_ring_custom_bidir_kernel(q, k, v, context):
12531339
use_base2_exp=context.get("use_base2_exp", True),
12541340
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
12551341
bidirectional=True,
1342+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12561343
)
12571344

12581345

@@ -1271,6 +1358,7 @@ def ulysses_kernel(q, k, v, context):
12711358
mask_padding_tokens=context["mask_padding_tokens"],
12721359
residual_checkpoint_name=context["residual_checkpoint_name"],
12731360
attention_mask=context["attention_mask"],
1361+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12741362
)
12751363

12761364

@@ -1292,6 +1380,7 @@ def ulysses_ring_kernel(q, k, v, context):
12921380
use_base2_exp=context["use_base2_exp"],
12931381
use_experimental_scheduler=context["use_experimental_scheduler"],
12941382
ulysses_shards=context["ulysses_shards"],
1383+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12951384
)
12961385

12971386

@@ -1404,6 +1493,7 @@ def _apply_attention(
14041493
use_base2_exp: bool = False,
14051494
use_experimental_scheduler: bool = False,
14061495
ulysses_shards: int = -1,
1496+
ulysses_attention_chunks: int = 1,
14071497
):
14081498
"""Routes to different attention kernels using a module-level registry."""
14091499

@@ -1435,6 +1525,7 @@ def _apply_attention(
14351525
"use_base2_exp": use_base2_exp,
14361526
"use_experimental_scheduler": use_experimental_scheduler,
14371527
"ulysses_shards": ulysses_shards,
1528+
"ulysses_attention_chunks": ulysses_attention_chunks,
14381529
"dim_head": dim_head,
14391530
"split_head_dim": split_head_dim,
14401531
"float32_qk_product": float32_qk_product,
@@ -1648,11 +1739,13 @@ def __init__(
16481739
use_base2_exp: bool = False,
16491740
use_experimental_scheduler: bool = False,
16501741
ulysses_shards: int = -1,
1742+
ulysses_attention_chunks: int = 1,
16511743
):
16521744
self.dpa_layer = None
16531745
self.use_base2_exp = use_base2_exp
16541746
self.use_experimental_scheduler = use_experimental_scheduler
16551747
self.ulysses_shards = ulysses_shards
1748+
self.ulysses_attention_chunks = ulysses_attention_chunks
16561749
if attention_kernel == "cudnn_flash_te":
16571750
from transformer_engine.jax.flax.transformer import DotProductAttention # pytype: disable=import-error
16581751

@@ -1716,6 +1809,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17161809
use_base2_exp=self.use_base2_exp if hasattr(self, "use_base2_exp") else False,
17171810
use_experimental_scheduler=self.use_experimental_scheduler if hasattr(self, "use_experimental_scheduler") else False,
17181811
ulysses_shards=(self.ulysses_shards if hasattr(self, "ulysses_shards") else -1),
1812+
ulysses_attention_chunks=(self.ulysses_attention_chunks if hasattr(self, "ulysses_attention_chunks") else 1),
17191813
)
17201814

17211815

@@ -1737,6 +1831,7 @@ class AttentionOp(nn.Module):
17371831
use_base2_exp: bool = False
17381832
use_experimental_scheduler: bool = False
17391833
ulysses_shards: int = -1
1834+
ulysses_attention_chunks: int = 1
17401835

17411836
def setup(self):
17421837
self.dpa_layer = None
@@ -1785,6 +1880,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17851880
use_base2_exp=self.use_base2_exp,
17861881
use_experimental_scheduler=self.use_experimental_scheduler,
17871882
ulysses_shards=self.ulysses_shards,
1883+
ulysses_attention_chunks=self.ulysses_attention_chunks,
17881884
)
17891885

17901886

@@ -1827,6 +1923,7 @@ def __init__(
18271923
"use_base2_exp": False,
18281924
"use_experimental_scheduler": False,
18291925
"ulysses_shards": -1,
1926+
"ulysses_attention_chunks": 1,
18301927
**(attention_config or {}),
18311928
}
18321929

@@ -1878,6 +1975,7 @@ def __init__(
18781975
use_base2_exp=attention_config["use_base2_exp"],
18791976
use_experimental_scheduler=attention_config["use_experimental_scheduler"],
18801977
ulysses_shards=attention_config["ulysses_shards"],
1978+
ulysses_attention_chunks=attention_config["ulysses_attention_chunks"],
18811979
)
18821980
# None axes corresponds to the stacked weights across all blocks
18831981
# because of the use of nnx.vmap and nnx.scan.

src/maxdiffusion/models/wan/transformers/transformer_wan.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ def __init__(
360360
"use_base2_exp": False,
361361
"use_experimental_scheduler": False,
362362
"ulysses_shards": -1,
363+
"ulysses_attention_chunks": 1,
363364
**(attention_config or {}),
364365
}
365366

@@ -584,6 +585,7 @@ def __init__(
584585
"use_base2_exp": False,
585586
"use_experimental_scheduler": False,
586587
"ulysses_shards": -1,
588+
"ulysses_attention_chunks": 1,
587589
**(attention_config or {}),
588590
}
589591

src/maxdiffusion/models/wan/transformers/transformer_wan_animate.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ def __init__(
788788
enable_jax_named_scopes: bool = False,
789789
use_base2_exp: bool = False,
790790
use_experimental_scheduler: bool = False,
791+
attention_config: Optional[dict] = None,
791792
face_flash_min_seq_length: int = 0,
792793
motion_encoder_channel_sizes: Optional[Dict[str, int]] = None,
793794
motion_encoder_size: int = 512,
@@ -812,6 +813,13 @@ def __init__(
812813
self.gradient_checkpoint = GradientCheckpointType.from_str(remat_policy)
813814
self.names_which_can_be_saved = names_which_can_be_saved or []
814815
self.names_which_can_be_offloaded = names_which_can_be_offloaded or []
816+
attention_config = {
817+
"use_base2_exp": use_base2_exp,
818+
"use_experimental_scheduler": use_experimental_scheduler,
819+
"ulysses_shards": -1,
820+
"ulysses_attention_chunks": 1,
821+
**(attention_config or {}),
822+
}
815823

816824
self.rope = WanRotaryPosEmbed(attention_head_dim, patch_size, rope_max_seq_len)
817825

@@ -903,8 +911,7 @@ def init_block(rngs):
903911
dropout=dropout,
904912
mask_padding_tokens=mask_padding_tokens,
905913
enable_jax_named_scopes=enable_jax_named_scopes,
906-
use_base2_exp=use_base2_exp,
907-
use_experimental_scheduler=use_experimental_scheduler,
914+
attention_config=attention_config,
908915
)
909916

910917
if scan_layers:
@@ -932,8 +939,7 @@ def init_block(rngs):
932939
dropout=dropout,
933940
mask_padding_tokens=mask_padding_tokens,
934941
enable_jax_named_scopes=enable_jax_named_scopes,
935-
use_base2_exp=use_base2_exp,
936-
use_experimental_scheduler=use_experimental_scheduler,
942+
attention_config=attention_config,
937943
)
938944
blocks.append(block)
939945
self.blocks = nnx.List(blocks)

src/maxdiffusion/models/wan/transformers/transformer_wan_vace.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def __init__(
100100
"use_base2_exp": False,
101101
"use_experimental_scheduler": False,
102102
"ulysses_shards": -1,
103+
"ulysses_attention_chunks": 1,
103104
**(attention_config or {}),
104105
}
105106

@@ -348,6 +349,7 @@ def __init__(
348349
"use_base2_exp": False,
349350
"use_experimental_scheduler": False,
350351
"ulysses_shards": -1,
352+
"ulysses_attention_chunks": 1,
351353
**(attention_config or {}),
352354
}
353355

src/maxdiffusion/pipelines/wan/wan_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def create_model(rngs: nnx.Rngs, wan_config: dict):
174174
"use_base2_exp": config.use_base2_exp,
175175
"use_experimental_scheduler": config.use_experimental_scheduler,
176176
"ulysses_shards": getattr(config, "ulysses_shards", -1),
177+
"ulysses_attention_chunks": getattr(config, "ulysses_attention_chunks", 1),
177178
}
178179

179180
# 2. eval_shape - will not use flops or create weights on device

src/maxdiffusion/pipelines/wan/wan_pipeline_animate.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def _create_model(rngs: nnx.Rngs, wan_config: dict):
9292
wan_config["enable_jax_named_scopes"] = config.enable_jax_named_scopes
9393
wan_config["use_base2_exp"] = config.use_base2_exp
9494
wan_config["use_experimental_scheduler"] = config.use_experimental_scheduler
95+
wan_config["attention_config"] = {
96+
"use_base2_exp": config.use_base2_exp,
97+
"use_experimental_scheduler": config.use_experimental_scheduler,
98+
"ulysses_shards": getattr(config, "ulysses_shards", -1),
99+
"ulysses_attention_chunks": getattr(config, "ulysses_attention_chunks", 1),
100+
}
95101

96102
# 2. eval_shape – creates the model structure without allocating HBM.
97103
p_model_factory = partial(_create_model, wan_config=wan_config)

src/maxdiffusion/pipelines/wan/wan_vace_pipeline_2_1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def create_model(rngs: nnx.Rngs, wan_config: dict):
9090
"use_base2_exp": config.use_base2_exp,
9191
"use_experimental_scheduler": config.use_experimental_scheduler,
9292
"ulysses_shards": getattr(config, "ulysses_shards", -1),
93+
"ulysses_attention_chunks": getattr(config, "ulysses_attention_chunks", 1),
9394
}
9495

9596
wan_config["scan_layers"] = False

0 commit comments

Comments
 (0)