@@ -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,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.
0 commit comments