Skip to content

Commit 14eb661

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 14eb661

7 files changed

Lines changed: 229 additions & 2 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: 83 additions & 2 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)
@@ -1207,6 +1276,7 @@ def ulysses_custom_kernel(q, k, v, context):
12071276
use_custom_kernel=True,
12081277
use_base2_exp=context.get("use_base2_exp", True),
12091278
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1279+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12101280
)
12111281

12121282

@@ -1271,6 +1341,7 @@ def ulysses_kernel(q, k, v, context):
12711341
mask_padding_tokens=context["mask_padding_tokens"],
12721342
residual_checkpoint_name=context["residual_checkpoint_name"],
12731343
attention_mask=context["attention_mask"],
1344+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12741345
)
12751346

12761347

@@ -1292,6 +1363,7 @@ def ulysses_ring_kernel(q, k, v, context):
12921363
use_base2_exp=context["use_base2_exp"],
12931364
use_experimental_scheduler=context["use_experimental_scheduler"],
12941365
ulysses_shards=context["ulysses_shards"],
1366+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12951367
)
12961368

12971369

@@ -1404,6 +1476,7 @@ def _apply_attention(
14041476
use_base2_exp: bool = False,
14051477
use_experimental_scheduler: bool = False,
14061478
ulysses_shards: int = -1,
1479+
ulysses_attention_chunks: int = 1,
14071480
):
14081481
"""Routes to different attention kernels using a module-level registry."""
14091482

@@ -1435,6 +1508,7 @@ def _apply_attention(
14351508
"use_base2_exp": use_base2_exp,
14361509
"use_experimental_scheduler": use_experimental_scheduler,
14371510
"ulysses_shards": ulysses_shards,
1511+
"ulysses_attention_chunks": ulysses_attention_chunks,
14381512
"dim_head": dim_head,
14391513
"split_head_dim": split_head_dim,
14401514
"float32_qk_product": float32_qk_product,
@@ -1648,11 +1722,13 @@ def __init__(
16481722
use_base2_exp: bool = False,
16491723
use_experimental_scheduler: bool = False,
16501724
ulysses_shards: int = -1,
1725+
ulysses_attention_chunks: int = 1,
16511726
):
16521727
self.dpa_layer = None
16531728
self.use_base2_exp = use_base2_exp
16541729
self.use_experimental_scheduler = use_experimental_scheduler
16551730
self.ulysses_shards = ulysses_shards
1731+
self.ulysses_attention_chunks = ulysses_attention_chunks
16561732
if attention_kernel == "cudnn_flash_te":
16571733
from transformer_engine.jax.flax.transformer import DotProductAttention # pytype: disable=import-error
16581734

@@ -1716,6 +1792,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17161792
use_base2_exp=self.use_base2_exp if hasattr(self, "use_base2_exp") else False,
17171793
use_experimental_scheduler=self.use_experimental_scheduler if hasattr(self, "use_experimental_scheduler") else False,
17181794
ulysses_shards=(self.ulysses_shards if hasattr(self, "ulysses_shards") else -1),
1795+
ulysses_attention_chunks=(self.ulysses_attention_chunks if hasattr(self, "ulysses_attention_chunks") else 1),
17191796
)
17201797

17211798

@@ -1737,6 +1814,7 @@ class AttentionOp(nn.Module):
17371814
use_base2_exp: bool = False
17381815
use_experimental_scheduler: bool = False
17391816
ulysses_shards: int = -1
1817+
ulysses_attention_chunks: int = 1
17401818

17411819
def setup(self):
17421820
self.dpa_layer = None
@@ -1785,6 +1863,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17851863
use_base2_exp=self.use_base2_exp,
17861864
use_experimental_scheduler=self.use_experimental_scheduler,
17871865
ulysses_shards=self.ulysses_shards,
1866+
ulysses_attention_chunks=self.ulysses_attention_chunks,
17881867
)
17891868

17901869

@@ -1827,6 +1906,7 @@ def __init__(
18271906
"use_base2_exp": False,
18281907
"use_experimental_scheduler": False,
18291908
"ulysses_shards": -1,
1909+
"ulysses_attention_chunks": 1,
18301910
**(attention_config or {}),
18311911
}
18321912

@@ -1878,6 +1958,7 @@ def __init__(
18781958
use_base2_exp=attention_config["use_base2_exp"],
18791959
use_experimental_scheduler=attention_config["use_experimental_scheduler"],
18801960
ulysses_shards=attention_config["ulysses_shards"],
1961+
ulysses_attention_chunks=attention_config["ulysses_attention_chunks"],
18811962
)
18821963
# None axes corresponds to the stacked weights across all blocks
18831964
# 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_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_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)