Skip to content

Commit 0d936f8

Browse files
committed
feat: experimental two-phase (head-chunked) Ulysses all-to-all
Add an opt-in ULYSSES_ATTENTION_CHUNKS env var to split the Ulysses all-to-all into per-head-group passes, so XLA's async-collective scheduler can overlap one group's attention compute with the next group's all-to-all. Defaults to 1 (current single-shot path, no behavior change). Numerically identical to single-shot since heads are independent. Notes: - Requires async-collective LIBTPU flags to actually overlap. - Needs heads % (context_shards * chunks) == 0. - Gain is largest when all-to-all is a meaningful fraction of attention time (high context-parallelism / shorter sequences); at WAN 2.2 720p (seq~75600) it is compute-bound so the win is small (~3% in microbench).
1 parent 08566b1 commit 0d936f8

1 file changed

Lines changed: 33 additions & 1 deletion

File tree

src/maxdiffusion/models/attention_flax.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import contextlib
1616
import functools
1717
import math
18+
import os
1819
from typing import Optional, Callable, Tuple, Any, Dict
1920
import flax.linen as nn
2021
from flax import nnx
@@ -605,6 +606,21 @@ def _ulysses_attention(
605606
"Ulysses attention requires the number of heads to be divisible by the context shard count, "
606607
f"got heads={num_heads} and context_shards={num_shards}."
607608
)
609+
610+
# EXPERIMENTAL: split the all-to-all into `num_chunks` head-groups so XLA's
611+
# async-collective scheduler can overlap one chunk's attention compute with
612+
# the next chunk's all-to-all. Gated on an env var so it stays opt-in. The
613+
# math is identical to the single-shot path (heads are independent); requires
614+
# async-collective LIBTPU flags to actually overlap, and the per-chunk head
615+
# count must still be shardable across the context axis.
616+
num_chunks = int(os.environ.get("ULYSSES_ATTENTION_CHUNKS", "1"))
617+
if num_chunks > 1:
618+
if num_heads % (num_shards * num_chunks) != 0:
619+
raise ValueError(
620+
"ULYSSES_ATTENTION_CHUNKS requires heads divisible by (context_shards * chunks), "
621+
f"got heads={num_heads}, context_shards={num_shards}, chunks={num_chunks}."
622+
)
623+
608624
if not use_custom_kernel:
609625
block_sizes = _select_flash_block_sizes(query, key, flash_block_sizes, dtype, "flash")
610626

@@ -721,7 +737,23 @@ def wrap_ulysses_attention(query, key, value):
721737
"Warning, batch dimension should be shardable among the devices in data and fsdp"
722738
f" axis, batch dimension: {query.shape[0]}, devices_in_batch_sharding: {devices_in_batch_sharding}"
723739
)
724-
x = wrap_ulysses_attention(query, key, value)
740+
if num_chunks > 1:
741+
# EXPERIMENTAL two-phase path: run the all-to-all + attention per head-group.
742+
# Heads are independent, so this is numerically identical to the single-shot
743+
# path; the goal is to let XLA overlap one group's compute with the next
744+
# group's all-to-all (requires async-collective LIBTPU flags).
745+
head_step = num_heads // num_chunks
746+
chunk_outputs = [
747+
wrap_ulysses_attention(
748+
query[:, i * head_step : (i + 1) * head_step],
749+
key[:, i * head_step : (i + 1) * head_step],
750+
value[:, i * head_step : (i + 1) * head_step],
751+
)
752+
for i in range(num_chunks)
753+
]
754+
x = jnp.concatenate(chunk_outputs, axis=1)
755+
else:
756+
x = wrap_ulysses_attention(query, key, value)
725757
x = x[:, :, :orig_q_seq_len, :]
726758
x = _reshape_heads_to_head_dim(x)
727759

0 commit comments

Comments
 (0)