@@ -413,6 +413,44 @@ def _build_padding_segment_ids(
413413 return segment_ids_cls (q = q_segment_ids , kv = kv_segment_ids )
414414
415415
416+ def _ulysses_head_chunk_ranges (num_heads : int , ulysses_shards : int , num_chunks : int ):
417+ """Build head-axis ranges for chunked Ulysses all-to-all.
418+
419+ The Ulysses all-to-all splits each local chunk's head axis over
420+ `ulysses_shards`, so every returned range length is a multiple of
421+ `ulysses_shards`. When `num_chunks` does not evenly divide the number of
422+ Ulysses-sized head groups, earlier chunks get the floor-sized range and the
423+ final chunk carries the remainder.
424+
425+ Returns:
426+ A list of `(start, end)` half-open ranges over the head axis. Concatenating
427+ tensors sliced with these ranges along the head axis restores the original
428+ head layout. For `num_chunks <= 1`, returns `[(0, num_heads)]`, which is the
429+ unchunked all-to-all path.
430+ """
431+ if num_chunks <= 1 :
432+ return [(0 , num_heads )]
433+ if num_heads % ulysses_shards != 0 :
434+ raise ValueError (
435+ "Ulysses attention requires the number of heads to be divisible by the Ulysses shard count, "
436+ f"got heads={ num_heads } and ulysses_shards={ ulysses_shards } ."
437+ )
438+
439+ head_groups = num_heads // ulysses_shards
440+ num_chunks = min (num_chunks , head_groups )
441+ regular_groups_per_chunk = max (1 , head_groups // num_chunks )
442+
443+ ranges = []
444+ start_group = 0
445+ for chunk_idx in range (num_chunks ):
446+ end_group = head_groups if chunk_idx == num_chunks - 1 else min (start_group + regular_groups_per_chunk , head_groups )
447+ if start_group >= end_group :
448+ break
449+ ranges .append ((start_group * ulysses_shards , end_group * ulysses_shards ))
450+ start_group = end_group
451+ return ranges
452+
453+
416454def _tpu_flash_attention (
417455 query : jax .Array ,
418456 key : jax .Array ,
@@ -640,6 +678,7 @@ def _ulysses_attention(
640678 use_custom_kernel : bool = False ,
641679 use_base2_exp : bool = True ,
642680 use_experimental_scheduler : bool = False ,
681+ ulysses_attention_chunks : int = 1 ,
643682) -> jax .Array :
644683 """Ulysses sequence-parallel attention.
645684
@@ -662,6 +701,7 @@ def _ulysses_attention(
662701 "Ulysses attention requires the number of heads to be divisible by the context shard count, "
663702 f"got heads={ num_heads } and context_shards={ num_shards } ."
664703 )
704+
665705 if not use_custom_kernel :
666706 block_sizes = _select_flash_block_sizes (query , key , flash_block_sizes , dtype , "flash" )
667707
@@ -762,7 +802,21 @@ def wrap_ulysses_attention(query, key, value):
762802 "Warning, batch dimension should be shardable among the devices in data and fsdp"
763803 f" axis, batch dimension: { query .shape [0 ]} , devices_in_batch_sharding: { devices_in_batch_sharding } "
764804 )
765- x = wrap_ulysses_attention (query , key , value )
805+ head_chunk_ranges = _ulysses_head_chunk_ranges (num_heads , num_shards , ulysses_attention_chunks )
806+ if len (head_chunk_ranges ) > 1 :
807+ # Run Ulysses all-to-all per head group so XLA can overlap one group's
808+ # collective with another group's head-parallel local attention compute.
809+ chunk_outputs = [
810+ wrap_ulysses_attention (
811+ query [:, start :end ],
812+ key [:, start :end ],
813+ value [:, start :end ],
814+ )
815+ for start , end in head_chunk_ranges
816+ ]
817+ x = jnp .concatenate (chunk_outputs , axis = 1 )
818+ else :
819+ x = wrap_ulysses_attention (query , key , value )
766820 x = x [:, :, :orig_q_seq_len , :]
767821 x = _reshape_heads_to_head_dim (x )
768822
@@ -787,6 +841,7 @@ def _ulysses_ring_attention(
787841 use_base2_exp : bool = False ,
788842 use_experimental_scheduler : bool = False ,
789843 ulysses_shards : int = - 1 ,
844+ ulysses_attention_chunks : int = 1 ,
790845) -> jax .Array :
791846 """2D context-parallel attention using a private Ulysses x ring mesh.
792847
@@ -916,7 +971,21 @@ def wrap_ulysses_ring_attention(query, key, value):
916971 "Warning, batch dimension should be shardable among the devices in data and fsdp"
917972 f" axis, batch dimension: { query .shape [0 ]} , devices_in_batch_sharding: { devices_in_batch_sharding } "
918973 )
919- x = wrap_ulysses_ring_attention (query , key , value )
974+ head_chunk_ranges = _ulysses_head_chunk_ranges (num_heads , num_ulysses_shards , ulysses_attention_chunks )
975+ if len (head_chunk_ranges ) > 1 :
976+ # Run the Ulysses phase per head group before the ring phase so XLA can
977+ # overlap all-to-all collectives with head-parallel local attention compute.
978+ chunk_outputs = [
979+ wrap_ulysses_ring_attention (
980+ query [:, start :end ],
981+ key [:, start :end ],
982+ value [:, start :end ],
983+ )
984+ for start , end in head_chunk_ranges
985+ ]
986+ x = jnp .concatenate (chunk_outputs , axis = 1 )
987+ else :
988+ x = wrap_ulysses_ring_attention (query , key , value )
920989 x = jax .lax .with_sharding_constraint (x , q_axis_names )
921990 x = x [:, :, :orig_q_seq_len , :]
922991 x = _reshape_heads_to_head_dim (x )
@@ -1207,6 +1276,7 @@ def ulysses_custom_kernel(q, k, v, context):
12071276 use_custom_kernel = True ,
12081277 use_base2_exp = context .get ("use_base2_exp" , True ),
12091278 use_experimental_scheduler = context .get ("use_experimental_scheduler" , False ),
1279+ ulysses_attention_chunks = context ["ulysses_attention_chunks" ],
12101280 )
12111281
12121282
@@ -1271,6 +1341,7 @@ def ulysses_kernel(q, k, v, context):
12711341 mask_padding_tokens = context ["mask_padding_tokens" ],
12721342 residual_checkpoint_name = context ["residual_checkpoint_name" ],
12731343 attention_mask = context ["attention_mask" ],
1344+ ulysses_attention_chunks = context ["ulysses_attention_chunks" ],
12741345 )
12751346
12761347
@@ -1292,6 +1363,7 @@ def ulysses_ring_kernel(q, k, v, context):
12921363 use_base2_exp = context ["use_base2_exp" ],
12931364 use_experimental_scheduler = context ["use_experimental_scheduler" ],
12941365 ulysses_shards = context ["ulysses_shards" ],
1366+ ulysses_attention_chunks = context ["ulysses_attention_chunks" ],
12951367 )
12961368
12971369
@@ -1404,6 +1476,7 @@ def _apply_attention(
14041476 use_base2_exp : bool = False ,
14051477 use_experimental_scheduler : bool = False ,
14061478 ulysses_shards : int = - 1 ,
1479+ ulysses_attention_chunks : int = 1 ,
14071480):
14081481 """Routes to different attention kernels using a module-level registry."""
14091482
@@ -1435,6 +1508,7 @@ def _apply_attention(
14351508 "use_base2_exp" : use_base2_exp ,
14361509 "use_experimental_scheduler" : use_experimental_scheduler ,
14371510 "ulysses_shards" : ulysses_shards ,
1511+ "ulysses_attention_chunks" : ulysses_attention_chunks ,
14381512 "dim_head" : dim_head ,
14391513 "split_head_dim" : split_head_dim ,
14401514 "float32_qk_product" : float32_qk_product ,
@@ -1648,11 +1722,13 @@ def __init__(
16481722 use_base2_exp : bool = False ,
16491723 use_experimental_scheduler : bool = False ,
16501724 ulysses_shards : int = - 1 ,
1725+ ulysses_attention_chunks : int = 1 ,
16511726 ):
16521727 self .dpa_layer = None
16531728 self .use_base2_exp = use_base2_exp
16541729 self .use_experimental_scheduler = use_experimental_scheduler
16551730 self .ulysses_shards = ulysses_shards
1731+ self .ulysses_attention_chunks = ulysses_attention_chunks
16561732 if attention_kernel == "cudnn_flash_te" :
16571733 from transformer_engine .jax .flax .transformer import DotProductAttention # pytype: disable=import-error
16581734
@@ -1716,6 +1792,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17161792 use_base2_exp = self .use_base2_exp if hasattr (self , "use_base2_exp" ) else False ,
17171793 use_experimental_scheduler = self .use_experimental_scheduler if hasattr (self , "use_experimental_scheduler" ) else False ,
17181794 ulysses_shards = (self .ulysses_shards if hasattr (self , "ulysses_shards" ) else - 1 ),
1795+ ulysses_attention_chunks = (self .ulysses_attention_chunks if hasattr (self , "ulysses_attention_chunks" ) else 1 ),
17191796 )
17201797
17211798
@@ -1737,6 +1814,7 @@ class AttentionOp(nn.Module):
17371814 use_base2_exp : bool = False
17381815 use_experimental_scheduler : bool = False
17391816 ulysses_shards : int = - 1
1817+ ulysses_attention_chunks : int = 1
17401818
17411819 def setup (self ):
17421820 self .dpa_layer = None
@@ -1785,6 +1863,7 @@ def apply_attention(self, query: Array, key: Array, value: Array, attention_mask
17851863 use_base2_exp = self .use_base2_exp ,
17861864 use_experimental_scheduler = self .use_experimental_scheduler ,
17871865 ulysses_shards = self .ulysses_shards ,
1866+ ulysses_attention_chunks = self .ulysses_attention_chunks ,
17881867 )
17891868
17901869
@@ -1827,6 +1906,7 @@ def __init__(
18271906 "use_base2_exp" : False ,
18281907 "use_experimental_scheduler" : False ,
18291908 "ulysses_shards" : - 1 ,
1909+ "ulysses_attention_chunks" : 1 ,
18301910 ** (attention_config or {}),
18311911 }
18321912
@@ -1878,6 +1958,7 @@ def __init__(
18781958 use_base2_exp = attention_config ["use_base2_exp" ],
18791959 use_experimental_scheduler = attention_config ["use_experimental_scheduler" ],
18801960 ulysses_shards = attention_config ["ulysses_shards" ],
1961+ ulysses_attention_chunks = attention_config ["ulysses_attention_chunks" ],
18811962 )
18821963 # None axes corresponds to the stacked weights across all blocks
18831964 # because of the use of nnx.vmap and nnx.scan.
0 commit comments