Skip to content

Commit 8287cb6

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 5fe8bcf commit 8287cb6

15 files changed

Lines changed: 285 additions & 12 deletions

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,19 @@ packages = ["src/maxdiffusion", "src/install_maxdiffusion_extra_deps"]
9292

9393
[tool.ruff]
9494
# Never enforce `E501` (line length violations).
95+
line-length = 119
96+
97+
[tool.ruff.lint]
9598
ignore = ["C901", "E501", "E741", "F402", "F823", "E402", "I001"]
9699
select = ["C", "E", "F", "I", "W"]
97-
line-length = 119
98100

99101
# Ignore import violations in all `__init__.py` files.
100-
[tool.ruff.per-file-ignores]
102+
[tool.ruff.lint.per-file-ignores]
101103
"__init__.py" = ["E402", "F401", "F403", "F811"]
102104
"src/maxdiffusion/utils/dummy_*.py" = ["F401"]
103105
"src/maxdiffusion/pyconfig.py" = ["E721"]
104106

105-
[tool.ruff.isort]
107+
[tool.ruff.lint.isort]
106108
lines-after-imports = 2
107109
known-first-party = ["maxdiffusion"]
108110

src/maxdiffusion/configs/base_wan_14b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
92+
ulysses_attention_chunks: 1
9193
flash_min_seq_length: 4096
9294
dropout: 0.0
9395

src/maxdiffusion/configs/base_wan_1_3b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ use_base2_exp: True
8585
use_experimental_scheduler: True
8686
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
8787
ulysses_shards: -1
88+
89+
ulysses_attention_chunks: 1
8890
flash_min_seq_length: 0
8991

9092
# If mask_padding_tokens is True, we pass in segment ids to splash attention to avoid attending to padding tokens.

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/configs/base_wan_animate.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ use_base2_exp: True
8686
use_experimental_scheduler: True
8787
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
8888
ulysses_shards: -1
89+
90+
ulysses_attention_chunks: 1
8991
flash_min_seq_length: 4096
9092
# If mask_padding_tokens is True, we pass in segment ids to splash attention to avoid attending to padding tokens.
9193
# Else we do not pass in segment ids and on vpu bound hardware like trillium this is faster.

src/maxdiffusion/configs/base_wan_i2v_14b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
92+
ulysses_attention_chunks: 1
9193
flash_min_seq_length: 4096
9294
dropout: 0.0
9395

src/maxdiffusion/configs/base_wan_i2v_27b.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ use_base2_exp: True
8888
use_experimental_scheduler: True
8989
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.
9090
ulysses_shards: -1
91+
92+
ulysses_attention_chunks: 1
9193
flash_min_seq_length: 4096
9294
dropout: 0.0
9395

src/maxdiffusion/models/attention_flax.py

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,44 @@ def _build_padding_segment_ids(
419419
return segment_ids_cls(q=q_segment_ids, kv=kv_segment_ids)
420420

421421

422+
def _ulysses_head_chunk_ranges(num_heads: int, ulysses_shards: int, num_chunks: int):
423+
"""Build head-axis ranges for chunked Ulysses all-to-all.
424+
425+
The Ulysses all-to-all splits each local chunk's head axis over
426+
`ulysses_shards`, so every returned range length is a multiple of
427+
`ulysses_shards`. When `num_chunks` does not evenly divide the number of
428+
Ulysses-sized head groups, earlier chunks get the floor-sized range and the
429+
final chunk carries the remainder.
430+
431+
Returns:
432+
A list of `(start, end)` half-open ranges over the head axis. Concatenating
433+
tensors sliced with these ranges along the head axis restores the original
434+
head layout. For `num_chunks <= 1`, returns `[(0, num_heads)]`, which is the
435+
unchunked all-to-all path.
436+
"""
437+
if num_chunks <= 1:
438+
return [(0, num_heads)]
439+
if num_heads % ulysses_shards != 0:
440+
raise ValueError(
441+
"Ulysses attention requires the number of heads to be divisible by the Ulysses shard count, "
442+
f"got heads={num_heads} and ulysses_shards={ulysses_shards}."
443+
)
444+
445+
head_groups = num_heads // ulysses_shards
446+
num_chunks = min(num_chunks, head_groups)
447+
regular_groups_per_chunk = max(1, head_groups // num_chunks)
448+
449+
ranges = []
450+
start_group = 0
451+
for chunk_idx in range(num_chunks):
452+
end_group = head_groups if chunk_idx == num_chunks - 1 else min(start_group + regular_groups_per_chunk, head_groups)
453+
if start_group >= end_group:
454+
break
455+
ranges.append((start_group * ulysses_shards, end_group * ulysses_shards))
456+
start_group = end_group
457+
return ranges
458+
459+
422460
def _tpu_flash_attention(
423461
query: jax.Array,
424462
key: jax.Array,
@@ -647,6 +685,7 @@ def _ulysses_attention(
647685
use_base2_exp: bool = True,
648686
use_experimental_scheduler: bool = False,
649687
use_fixed_m: bool = False,
688+
ulysses_attention_chunks: int = 1,
650689
) -> jax.Array:
651690
"""Ulysses sequence-parallel attention.
652691
@@ -669,6 +708,7 @@ def _ulysses_attention(
669708
"Ulysses attention requires the number of heads to be divisible by the context shard count, "
670709
f"got heads={num_heads} and context_shards={num_shards}."
671710
)
711+
672712
if not use_custom_kernel:
673713
block_sizes = _select_flash_block_sizes(query, key, flash_block_sizes, dtype, "flash")
674714

@@ -792,7 +832,6 @@ def wrap_ulysses_attention(query, key, value):
792832
"Warning, batch dimension should be shardable among the devices in data and fsdp"
793833
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
794834
)
795-
796835
# Fold the (CFG) batch into the heads axis around the Ulysses exchange.
797836
# Each (batch, head) pair is an independent attention problem, so
798837
# [B, H, S, D] -> [1, B*H, S, D] is mathematically identity — but it makes
@@ -807,8 +846,25 @@ def wrap_ulysses_attention(query, key, value):
807846
query = query.reshape(1, batch * num_heads, *query.shape[2:])
808847
key = key.reshape(1, batch * num_heads, *key.shape[2:])
809848
value = value.reshape(1, batch * num_heads, *value.shape[2:])
810-
811-
x = wrap_ulysses_attention(query, key, value)
849+
effective_num_heads = batch * num_heads
850+
else:
851+
effective_num_heads = num_heads
852+
853+
head_chunk_ranges = _ulysses_head_chunk_ranges(effective_num_heads, num_shards, ulysses_attention_chunks)
854+
if len(head_chunk_ranges) > 1:
855+
# Run Ulysses all-to-all per head group so XLA can overlap one group's
856+
# collective with another group's head-parallel local attention compute.
857+
chunk_outputs = [
858+
wrap_ulysses_attention(
859+
query[:, start:end],
860+
key[:, start:end],
861+
value[:, start:end],
862+
)
863+
for start, end in head_chunk_ranges
864+
]
865+
x = jnp.concatenate(chunk_outputs, axis=1)
866+
else:
867+
x = wrap_ulysses_attention(query, key, value)
812868

813869
if fold_batch:
814870
x = x.reshape(batch, num_heads, *x.shape[2:])
@@ -836,6 +892,7 @@ def _ulysses_ring_attention(
836892
use_base2_exp: bool = False,
837893
use_experimental_scheduler: bool = False,
838894
ulysses_shards: int = -1,
895+
ulysses_attention_chunks: int = 1,
839896
) -> jax.Array:
840897
"""2D context-parallel attention using a private Ulysses x ring mesh.
841898
@@ -877,6 +934,7 @@ def _ulysses_ring_attention(
877934
query, orig_q_seq_len = _reshape_data_for_flash(query, heads, num_sequence_shards)
878935
key, _ = _reshape_data_for_flash(key, heads, num_sequence_shards)
879936
value, _ = _reshape_data_for_flash(value, heads, num_sequence_shards)
937+
num_heads = query.shape[1]
880938

881939
block_sizes = _select_flash_block_sizes(query, key, flash_block_sizes, dtype, "tokamax_ring")
882940

@@ -965,7 +1023,21 @@ def wrap_ulysses_ring_attention(query, key, value):
9651023
"Warning, batch dimension should be shardable among the devices in data and fsdp"
9661024
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
9671025
)
968-
x = wrap_ulysses_ring_attention(query, key, value)
1026+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
1027+
if len(head_chunk_ranges) > 1:
1028+
# Run the Ulysses phase per head group before the ring phase so XLA can
1029+
# overlap all-to-all collectives with head-parallel local attention compute.
1030+
chunk_outputs = [
1031+
wrap_ulysses_ring_attention(
1032+
query[:, start:end],
1033+
key[:, start:end],
1034+
value[:, start:end],
1035+
)
1036+
for start, end in head_chunk_ranges
1037+
]
1038+
x = jnp.concatenate(chunk_outputs, axis=1)
1039+
else:
1040+
x = wrap_ulysses_ring_attention(query, key, value)
9691041
x = jax.lax.with_sharding_constraint(x, q_axis_names)
9701042
x = x[:, :, :orig_q_seq_len, :]
9711043
x = _reshape_heads_to_head_dim(x)
@@ -991,6 +1063,7 @@ def _ulysses_ring_custom_attention(
9911063
use_experimental_scheduler: bool = False,
9921064
bidirectional: bool = False,
9931065
use_fixed_m: bool = False,
1066+
ulysses_attention_chunks: int = 1,
9941067
) -> jax.Array:
9951068
"""Hybrid Ulysses + Ring (USP) with the CUSTOM splash kernel on main's mesh.
9961069
@@ -1141,7 +1214,21 @@ def wrap_ulysses_ring_attention(query, key, value):
11411214
attention_output = a2a(attention_output, split_axis=2, concat_axis=1)
11421215
return attention_output
11431216

1144-
x = wrap_ulysses_ring_attention(query, key, value)
1217+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
1218+
if len(head_chunk_ranges) > 1:
1219+
# Run the custom Ulysses phase per head group before the custom ring phase so
1220+
# XLA can overlap all-to-all collectives with head-parallel attention compute.
1221+
chunk_outputs = [
1222+
wrap_ulysses_ring_attention(
1223+
query[:, start:end],
1224+
key[:, start:end],
1225+
value[:, start:end],
1226+
)
1227+
for start, end in head_chunk_ranges
1228+
]
1229+
x = jnp.concatenate(chunk_outputs, axis=1)
1230+
else:
1231+
x = wrap_ulysses_ring_attention(query, key, value)
11451232
x = jax.lax.with_sharding_constraint(x, q_axis_names)
11461233
x = x[:, :, :orig_q_seq_len, :]
11471234
x = _reshape_heads_to_head_dim(x)
@@ -1291,6 +1378,7 @@ def ulysses_custom_kernel(q, k, v, context):
12911378
use_custom_kernel=True,
12921379
use_base2_exp=context.get("use_base2_exp", True),
12931380
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1381+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12941382
)
12951383

12961384

@@ -1312,6 +1400,7 @@ def ulysses_ring_custom_kernel(q, k, v, context):
13121400
ulysses_shards=context["ulysses_shards"],
13131401
use_base2_exp=context.get("use_base2_exp", True),
13141402
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1403+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
13151404
)
13161405

13171406

@@ -1364,6 +1453,7 @@ def ulysses_ring_custom_bidir_kernel(q, k, v, context):
13641453
use_base2_exp=context.get("use_base2_exp", True),
13651454
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
13661455
bidirectional=True,
1456+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
13671457
)
13681458

13691459

@@ -1404,6 +1494,7 @@ def ulysses_kernel(q, k, v, context):
14041494
mask_padding_tokens=context["mask_padding_tokens"],
14051495
residual_checkpoint_name=context["residual_checkpoint_name"],
14061496
attention_mask=context["attention_mask"],
1497+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
14071498
)
14081499

14091500

@@ -1425,6 +1516,7 @@ def ulysses_ring_kernel(q, k, v, context):
14251516
use_base2_exp=context["use_base2_exp"],
14261517
use_experimental_scheduler=context["use_experimental_scheduler"],
14271518
ulysses_shards=context["ulysses_shards"],
1519+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
14281520
)
14291521

14301522

@@ -1537,6 +1629,7 @@ def _apply_attention(
15371629
use_base2_exp: bool = False,
15381630
use_experimental_scheduler: bool = False,
15391631
ulysses_shards: int = -1,
1632+
ulysses_attention_chunks: int = 1,
15401633
):
15411634
"""Routes to different attention kernels using a module-level registry."""
15421635

@@ -1568,6 +1661,7 @@ def _apply_attention(
15681661
"use_base2_exp": use_base2_exp,
15691662
"use_experimental_scheduler": use_experimental_scheduler,
15701663
"ulysses_shards": ulysses_shards,
1664+
"ulysses_attention_chunks": ulysses_attention_chunks,
15711665
"dim_head": dim_head,
15721666
"split_head_dim": split_head_dim,
15731667
"float32_qk_product": float32_qk_product,
@@ -1781,11 +1875,13 @@ def __init__(
17811875
use_base2_exp: bool = False,
17821876
use_experimental_scheduler: bool = False,
17831877
ulysses_shards: int = -1,
1878+
ulysses_attention_chunks: int = 1,
17841879
):
17851880
self.dpa_layer = None
17861881
self.use_base2_exp = use_base2_exp
17871882
self.use_experimental_scheduler = use_experimental_scheduler
17881883
self.ulysses_shards = ulysses_shards
1884+
self.ulysses_attention_chunks = ulysses_attention_chunks
17891885
if attention_kernel == "cudnn_flash_te":
17901886
from transformer_engine.jax.flax.transformer import DotProductAttention # pytype: disable=import-error
17911887

@@ -1849,6 +1945,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
18491945
use_base2_exp=self.use_base2_exp if hasattr(self, "use_base2_exp") else False,
18501946
use_experimental_scheduler=self.use_experimental_scheduler if hasattr(self, "use_experimental_scheduler") else False,
18511947
ulysses_shards=(self.ulysses_shards if hasattr(self, "ulysses_shards") else -1),
1948+
ulysses_attention_chunks=(self.ulysses_attention_chunks if hasattr(self, "ulysses_attention_chunks") else 1),
18521949
)
18531950

18541951

@@ -1870,6 +1967,7 @@ class AttentionOp(nn.Module):
18701967
use_base2_exp: bool = False
18711968
use_experimental_scheduler: bool = False
18721969
ulysses_shards: int = -1
1970+
ulysses_attention_chunks: int = 1
18731971

18741972
def setup(self):
18751973
self.dpa_layer = None
@@ -1918,6 +2016,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
19182016
use_base2_exp=self.use_base2_exp,
19192017
use_experimental_scheduler=self.use_experimental_scheduler,
19202018
ulysses_shards=self.ulysses_shards,
2019+
ulysses_attention_chunks=self.ulysses_attention_chunks,
19212020
)
19222021

19232022

@@ -1960,6 +2059,7 @@ def __init__(
19602059
"use_base2_exp": False,
19612060
"use_experimental_scheduler": False,
19622061
"ulysses_shards": -1,
2062+
"ulysses_attention_chunks": 1,
19632063
**(attention_config or {}),
19642064
}
19652065

@@ -2011,6 +2111,7 @@ def __init__(
20112111
use_base2_exp=attention_config["use_base2_exp"],
20122112
use_experimental_scheduler=attention_config["use_experimental_scheduler"],
20132113
ulysses_shards=attention_config["ulysses_shards"],
2114+
ulysses_attention_chunks=attention_config["ulysses_attention_chunks"],
20142115
)
20152116
# None axes corresponds to the stacked weights across all blocks
20162117
# 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

0 commit comments

Comments
 (0)