@@ -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+
422460def _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.
0 commit comments