Skip to content

Commit ec4e407

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 ec4e407

10 files changed

Lines changed: 275 additions & 20 deletions

File tree

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_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: 106 additions & 13 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,23 +832,31 @@ 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-
796-
# Fold the (CFG) batch into the heads axis around the Ulysses exchange.
797-
# Each (batch, head) pair is an independent attention problem, so
798-
# [B, H, S, D] -> [1, B*H, S, D] is mathematically identity — but it makes
799-
# XLA compile the attention path as the batch=1 case. At batch=2 XLA
800-
# otherwise places the size-2 batch in the tile sublanes ({3,0,1,2:T(2,128)}
801-
# instead of T(8,128)) which quadruples the cost of every op touching the
802-
# a2a tensors inside the scanned layers (measured 7.0 -> expected ~3.5
803-
# s/step at 720p 81f cp8 CFG).
804835
batch = query.shape[0]
805836
fold_batch = batch > 1 and (batch * num_heads) % num_shards == 0
806837
if fold_batch:
807838
query = query.reshape(1, batch * num_heads, *query.shape[2:])
808839
key = key.reshape(1, batch * num_heads, *key.shape[2:])
809840
value = value.reshape(1, batch * num_heads, *value.shape[2:])
810-
811-
x = wrap_ulysses_attention(query, key, value)
841+
effective_num_heads = batch * num_heads
842+
else:
843+
effective_num_heads = num_heads
844+
845+
head_chunk_ranges = _ulysses_head_chunk_ranges(effective_num_heads, num_shards, ulysses_attention_chunks)
846+
if len(head_chunk_ranges) > 1:
847+
# Run Ulysses all-to-all per head group so XLA can overlap one group's
848+
# collective with another group's head-parallel local attention compute.
849+
chunk_outputs = [
850+
wrap_ulysses_attention(
851+
query[:, start:end],
852+
key[:, start:end],
853+
value[:, start:end],
854+
)
855+
for start, end in head_chunk_ranges
856+
]
857+
x = jnp.concatenate(chunk_outputs, axis=1)
858+
else:
859+
x = wrap_ulysses_attention(query, key, value)
812860

813861
if fold_batch:
814862
x = x.reshape(batch, num_heads, *x.shape[2:])
@@ -836,6 +884,7 @@ def _ulysses_ring_attention(
836884
use_base2_exp: bool = False,
837885
use_experimental_scheduler: bool = False,
838886
ulysses_shards: int = -1,
887+
ulysses_attention_chunks: int = 1,
839888
) -> jax.Array:
840889
"""2D context-parallel attention using a private Ulysses x ring mesh.
841890
@@ -877,6 +926,7 @@ def _ulysses_ring_attention(
877926
query, orig_q_seq_len = _reshape_data_for_flash(query, heads, num_sequence_shards)
878927
key, _ = _reshape_data_for_flash(key, heads, num_sequence_shards)
879928
value, _ = _reshape_data_for_flash(value, heads, num_sequence_shards)
929+
num_heads = query.shape[1]
880930

881931
block_sizes = _select_flash_block_sizes(query, key, flash_block_sizes, dtype, "tokamax_ring")
882932

@@ -965,7 +1015,21 @@ def wrap_ulysses_ring_attention(query, key, value):
9651015
"Warning, batch dimension should be shardable among the devices in data and fsdp"
9661016
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
9671017
)
968-
x = wrap_ulysses_ring_attention(query, key, value)
1018+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
1019+
if len(head_chunk_ranges) > 1:
1020+
# Run the Ulysses phase per head group before the ring phase so XLA can
1021+
# overlap all-to-all collectives with head-parallel local attention compute.
1022+
chunk_outputs = [
1023+
wrap_ulysses_ring_attention(
1024+
query[:, start:end],
1025+
key[:, start:end],
1026+
value[:, start:end],
1027+
)
1028+
for start, end in head_chunk_ranges
1029+
]
1030+
x = jnp.concatenate(chunk_outputs, axis=1)
1031+
else:
1032+
x = wrap_ulysses_ring_attention(query, key, value)
9691033
x = jax.lax.with_sharding_constraint(x, q_axis_names)
9701034
x = x[:, :, :orig_q_seq_len, :]
9711035
x = _reshape_heads_to_head_dim(x)
@@ -991,6 +1055,7 @@ def _ulysses_ring_custom_attention(
9911055
use_experimental_scheduler: bool = False,
9921056
bidirectional: bool = False,
9931057
use_fixed_m: bool = False,
1058+
ulysses_attention_chunks: int = 1,
9941059
) -> jax.Array:
9951060
"""Hybrid Ulysses + Ring (USP) with the CUSTOM splash kernel on main's mesh.
9961061
@@ -1141,7 +1206,21 @@ def wrap_ulysses_ring_attention(query, key, value):
11411206
attention_output = a2a(attention_output, split_axis=2, concat_axis=1)
11421207
return attention_output
11431208

1144-
x = wrap_ulysses_ring_attention(query, key, value)
1209+
head_chunk_ranges = _ulysses_head_chunk_ranges(num_heads, num_ulysses_shards, ulysses_attention_chunks)
1210+
if len(head_chunk_ranges) > 1:
1211+
# Run the custom Ulysses phase per head group before the custom ring phase so
1212+
# XLA can overlap all-to-all collectives with head-parallel attention compute.
1213+
chunk_outputs = [
1214+
wrap_ulysses_ring_attention(
1215+
query[:, start:end],
1216+
key[:, start:end],
1217+
value[:, start:end],
1218+
)
1219+
for start, end in head_chunk_ranges
1220+
]
1221+
x = jnp.concatenate(chunk_outputs, axis=1)
1222+
else:
1223+
x = wrap_ulysses_ring_attention(query, key, value)
11451224
x = jax.lax.with_sharding_constraint(x, q_axis_names)
11461225
x = x[:, :, :orig_q_seq_len, :]
11471226
x = _reshape_heads_to_head_dim(x)
@@ -1291,6 +1370,7 @@ def ulysses_custom_kernel(q, k, v, context):
12911370
use_custom_kernel=True,
12921371
use_base2_exp=context.get("use_base2_exp", True),
12931372
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1373+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
12941374
)
12951375

12961376

@@ -1312,6 +1392,7 @@ def ulysses_ring_custom_kernel(q, k, v, context):
13121392
ulysses_shards=context["ulysses_shards"],
13131393
use_base2_exp=context.get("use_base2_exp", True),
13141394
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
1395+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
13151396
)
13161397

13171398

@@ -1364,6 +1445,7 @@ def ulysses_ring_custom_bidir_kernel(q, k, v, context):
13641445
use_base2_exp=context.get("use_base2_exp", True),
13651446
use_experimental_scheduler=context.get("use_experimental_scheduler", False),
13661447
bidirectional=True,
1448+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
13671449
)
13681450

13691451

@@ -1404,6 +1486,7 @@ def ulysses_kernel(q, k, v, context):
14041486
mask_padding_tokens=context["mask_padding_tokens"],
14051487
residual_checkpoint_name=context["residual_checkpoint_name"],
14061488
attention_mask=context["attention_mask"],
1489+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
14071490
)
14081491

14091492

@@ -1425,6 +1508,7 @@ def ulysses_ring_kernel(q, k, v, context):
14251508
use_base2_exp=context["use_base2_exp"],
14261509
use_experimental_scheduler=context["use_experimental_scheduler"],
14271510
ulysses_shards=context["ulysses_shards"],
1511+
ulysses_attention_chunks=context["ulysses_attention_chunks"],
14281512
)
14291513

14301514

@@ -1537,6 +1621,7 @@ def _apply_attention(
15371621
use_base2_exp: bool = False,
15381622
use_experimental_scheduler: bool = False,
15391623
ulysses_shards: int = -1,
1624+
ulysses_attention_chunks: int = 1,
15401625
):
15411626
"""Routes to different attention kernels using a module-level registry."""
15421627

@@ -1568,6 +1653,7 @@ def _apply_attention(
15681653
"use_base2_exp": use_base2_exp,
15691654
"use_experimental_scheduler": use_experimental_scheduler,
15701655
"ulysses_shards": ulysses_shards,
1656+
"ulysses_attention_chunks": ulysses_attention_chunks,
15711657
"dim_head": dim_head,
15721658
"split_head_dim": split_head_dim,
15731659
"float32_qk_product": float32_qk_product,
@@ -1781,11 +1867,13 @@ def __init__(
17811867
use_base2_exp: bool = False,
17821868
use_experimental_scheduler: bool = False,
17831869
ulysses_shards: int = -1,
1870+
ulysses_attention_chunks: int = 1,
17841871
):
17851872
self.dpa_layer = None
17861873
self.use_base2_exp = use_base2_exp
17871874
self.use_experimental_scheduler = use_experimental_scheduler
17881875
self.ulysses_shards = ulysses_shards
1876+
self.ulysses_attention_chunks = ulysses_attention_chunks
17891877
if attention_kernel == "cudnn_flash_te":
17901878
from transformer_engine.jax.flax.transformer import DotProductAttention # pytype: disable=import-error
17911879

@@ -1849,6 +1937,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
18491937
use_base2_exp=self.use_base2_exp if hasattr(self, "use_base2_exp") else False,
18501938
use_experimental_scheduler=self.use_experimental_scheduler if hasattr(self, "use_experimental_scheduler") else False,
18511939
ulysses_shards=(self.ulysses_shards if hasattr(self, "ulysses_shards") else -1),
1940+
ulysses_attention_chunks=(self.ulysses_attention_chunks if hasattr(self, "ulysses_attention_chunks") else 1),
18521941
)
18531942

18541943

@@ -1870,6 +1959,7 @@ class AttentionOp(nn.Module):
18701959
use_base2_exp: bool = False
18711960
use_experimental_scheduler: bool = False
18721961
ulysses_shards: int = -1
1962+
ulysses_attention_chunks: int = 1
18731963

18741964
def setup(self):
18751965
self.dpa_layer = None
@@ -1918,6 +2008,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
19182008
use_base2_exp=self.use_base2_exp,
19192009
use_experimental_scheduler=self.use_experimental_scheduler,
19202010
ulysses_shards=self.ulysses_shards,
2011+
ulysses_attention_chunks=self.ulysses_attention_chunks,
19212012
)
19222013

19232014

@@ -1960,6 +2051,7 @@ def __init__(
19602051
"use_base2_exp": False,
19612052
"use_experimental_scheduler": False,
19622053
"ulysses_shards": -1,
2054+
"ulysses_attention_chunks": 1,
19632055
**(attention_config or {}),
19642056
}
19652057

@@ -2011,6 +2103,7 @@ def __init__(
20112103
use_base2_exp=attention_config["use_base2_exp"],
20122104
use_experimental_scheduler=attention_config["use_experimental_scheduler"],
20132105
ulysses_shards=attention_config["ulysses_shards"],
2106+
ulysses_attention_chunks=attention_config["ulysses_attention_chunks"],
20142107
)
20152108
# None axes corresponds to the stacked weights across all blocks
20162109
# 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
@@ -201,6 +201,7 @@ def create_model(rngs: nnx.Rngs, wan_config: dict):
201201
"use_base2_exp": config.use_base2_exp,
202202
"use_experimental_scheduler": config.use_experimental_scheduler,
203203
"ulysses_shards": getattr(config, "ulysses_shards", -1),
204+
"ulysses_attention_chunks": getattr(config, "ulysses_attention_chunks", 1),
204205
}
205206

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

0 commit comments

Comments
 (0)