@@ -766,24 +766,47 @@ def wrap_ulysses_attention(query, key, value):
766766 return x
767767
768768
769- def _usp_groups (num_shards : int , ulysses_degree : int ):
770- """Factorizes the `context` axis into a U x R (Ulysses x Ring) grid.
769+ INTERNAL_RING_AXIS = "ring"
770+ INTERNAL_ULYSSES_AXIS = "ulysses"
771+
772+
773+ def _replace_mesh_axis (axis_spec , old_axis , new_axes ):
774+ """Replace a mesh-axis name (or one nested inside a tuple) with new_axes."""
775+ if axis_spec == old_axis :
776+ return new_axes
777+ if isinstance (axis_spec , tuple ):
778+ replacement = []
779+ for axis in axis_spec :
780+ if axis == old_axis :
781+ replacement .extend (new_axes )
782+ else :
783+ replacement .append (axis )
784+ return tuple (replacement )
785+ return axis_spec
786+
787+
788+ def _replace_mesh_axis_names (axis_names , old_axis , new_axes ):
789+ return jax .sharding .PartitionSpec (* (_replace_mesh_axis (a , old_axis , new_axes ) for a in axis_names ))
771790
772- Device index along the `context` axis is laid out u-major: `c = u*R + r`, so
773- - the Ulysses all-to-all group (fixed r, varying u) is `[u*R + r for u]`, and
774- - the Ring rotation group (fixed u, varying r) is `[u*R + r for r]`.
775791
776- Returns `(U, R, ulysses_groups, ring_perm)` where `ulysses_groups` is the
777- `axis_index_groups` for the all-to-all and `ring_perm` is a `ppermute` perm
778- that rotates K/V by +1 *within each ring sub-group only*.
792+ def _create_internal_ulysses_ring_mesh (
793+ mesh , ring_shards , ulysses_shards , ring_axis = INTERNAL_RING_AXIS , ulysses_axis = INTERNAL_ULYSSES_AXIS
794+ ):
795+ """Split the public `context` mesh axis into private (ring, ulysses) axes.
796+
797+ Ported from origin/main's tested implementation (commit c104db51). The reshape
798+ is `(..., ring_shards, ulysses_shards, ...)` with the Ulysses axis INNERMOST, so
799+ for ulysses_shards==2 the Ulysses group is consecutive device ids (the two cores
800+ of one chip): the all-to-all stays intra-chip while the ring rotates across chips.
779801 """
780- U = ulysses_degree
781- if num_shards % U != 0 :
782- raise ValueError (f"ulysses_degree={ U } must divide context shard count { num_shards } ." )
783- R = num_shards // U
784- ulysses_groups = [[u * R + r for u in range (U )] for r in range (R )]
785- ring_perm = [(u * R + r , u * R + (r + 1 ) % R ) for u in range (U ) for r in range (R )]
786- return U , R , ulysses_groups , ring_perm
802+ mesh_axis_names = tuple (mesh .axis_names )
803+ context_axis_index = mesh_axis_names .index ("context" )
804+ devices = mesh .devices
805+ new_shape = devices .shape [:context_axis_index ] + (ring_shards , ulysses_shards ) + devices .shape [context_axis_index + 1 :]
806+ new_axis_names = (
807+ mesh_axis_names [:context_axis_index ] + (ring_axis , ulysses_axis ) + mesh_axis_names [context_axis_index + 1 :]
808+ )
809+ return Mesh (devices .reshape (new_shape ), new_axis_names )
787810
788811
789812def _ulysses_ring_attention (
@@ -802,49 +825,66 @@ def _ulysses_ring_attention(
802825 use_base2_exp : bool = True ,
803826 use_experimental_scheduler : bool = False ,
804827) -> jax .Array :
805- """Hybrid Ulysses + Ring (USP) sequence parallelism with the custom kernel.
806-
807- Splits the single `context` mesh axis into a U x R grid using collective
808- sub-groups (no extra mesh axes needed):
809- 1. all-to-all WITHIN each Ulysses group (size U): trade sequence for heads,
810- so each device holds the full sequence of its ring-chunk but heads/U heads;
811- 2. ring (ppermute) WITHIN each Ring group (size R): rotate K/V R times and
812- merge via online softmax, using the custom dense kernel per step;
828+ """Hybrid Ulysses + Ring (USP) with the CUSTOM splash kernel on main's mesh.
829+
830+ Uses origin/main's explicit internal `(ring, ulysses)` mesh
831+ (`_create_internal_ulysses_ring_mesh`, commit c104db51) instead of single-axis
832+ collective sub-groups: the public `context` axis is reshaped with the Ulysses
833+ axis innermost, so the Ulysses all-to-all stays INTRA-chip and the ring rotates
834+ ACROSS chips. The per-shard attention is our custom splash kernel
835+ (`make_custom_ring_attention`), not the tokamax_ring kernel main uses.
836+
837+ 1. all-to-all over the (intra-chip) Ulysses axis: trade sequence for heads;
838+ 2. ring (full ppermute) over the (cross-chip) ring axis, online-softmax merge;
813839 3. all-to-all back to restore the sequence-sharded / full-heads layout.
814840
815- U is read from the env var ULYSSES_RING_DEGREE (default 2); R = context // U.
816- U=context reduces to pure Ulysses, U=1 to pure Ring.
841+ U = ULYSSES_RING_DEGREE (default 2); R = context // U. U=context -> pure
842+ Ulysses, U=1 -> pure Ring (all on the same custom kernel) .
817843 """
818844 axis_name = "context"
819- num_shards = mesh .shape [axis_name ]
820- ulysses_degree = int (os .environ .get ("ULYSSES_RING_DEGREE" , "2" ))
821- U , R , ulysses_groups , ring_perm = _usp_groups (num_shards , ulysses_degree )
845+ num_context_shards = mesh .shape [axis_name ]
846+ num_ulysses_shards = int (os .environ .get ("ULYSSES_RING_DEGREE" , "2" ))
847+ if num_ulysses_shards <= 0 or num_context_shards % num_ulysses_shards != 0 :
848+ raise ValueError (
849+ f"ULYSSES_RING_DEGREE={ num_ulysses_shards } must be a positive divisor of "
850+ f"context shard count { num_context_shards } ."
851+ )
852+ num_ring_shards = num_context_shards // num_ulysses_shards
853+ # Wrap-free (bidirectional) ring schedule for a non-wrapping ring axis (e.g. the
854+ # cut size-4 z line of a v7x-16 slice). Set USP_WRAP_FREE_RING=1 to A/B it.
855+ wrap_free_ring = os .environ .get ("USP_WRAP_FREE_RING" , "0" ) == "1"
822856
823- query , orig_q_seq_len = _reshape_data_for_flash (query , heads , num_shards )
824- key , _ = _reshape_data_for_flash (key , heads , num_shards )
825- value , _ = _reshape_data_for_flash (value , heads , num_shards )
857+ query , orig_q_seq_len = _reshape_data_for_flash (query , heads , num_context_shards )
858+ key , _ = _reshape_data_for_flash (key , heads , num_context_shards )
859+ value , _ = _reshape_data_for_flash (value , heads , num_context_shards )
826860 num_heads = query .shape [1 ]
827- if num_heads % U != 0 :
828- raise ValueError (f"Ulysses+Ring requires heads divisible by the Ulysses degree U={ U } , got heads={ num_heads } ." )
861+ if num_heads % num_ulysses_shards != 0 :
862+ raise ValueError (f"Ulysses+Ring requires heads divisible by U={ num_ulysses_shards } , got heads={ num_heads } ." )
829863
830864 bq , bkv , bkv_compute , bkv_compute_in , heads_per_tile , vmem_limit_bytes = _extract_custom_block_sizes (flash_block_sizes )
831865 if heads_per_tile > 1 :
832866 raise NotImplementedError ("ulysses_ring_custom currently supports heads_per_tile == 1 only." )
833867
868+ internal_mesh = _create_internal_ulysses_ring_mesh (mesh , num_ring_shards , num_ulysses_shards )
869+ ring_axis = INTERNAL_RING_AXIS
870+ ulysses_axis = INTERNAL_ULYSSES_AXIS
871+
834872 q_axis_names = nn .logical_to_mesh_axes (axis_names_q )
835873 kv_axis_names = nn .logical_to_mesh_axes (axis_names_kv )
874+ internal_q_axis_names = _replace_mesh_axis_names (q_axis_names , axis_name , (ring_axis , ulysses_axis ))
875+ internal_kv_axis_names = _replace_mesh_axis_names (kv_axis_names , axis_name , (ring_axis , ulysses_axis ))
836876
837877 @functools .partial (
838878 jax .shard_map ,
839- mesh = mesh ,
840- in_specs = (q_axis_names , kv_axis_names , kv_axis_names ),
841- out_specs = q_axis_names ,
879+ mesh = internal_mesh ,
880+ in_specs = (internal_q_axis_names , internal_kv_axis_names , internal_kv_axis_names ),
881+ out_specs = internal_q_axis_names ,
842882 check_vma = False ,
843883 )
844884 def wrap_ulysses_ring_attention (query , key , value ):
845- # (1) Ulysses all-to-all within each U-group : heads -> sequence swap, so each
846- # device now holds the full ring-chunk sequence with heads/U heads.
847- a2a = functools .partial (jax .lax .all_to_all , axis_name = axis_name , axis_index_groups = ulysses_groups , tiled = True )
885+ # (1) Ulysses all-to-all over the (intra-chip) ulysses axis : heads -> sequence,
886+ # so each device holds the full ring-chunk sequence with heads/U heads.
887+ a2a = functools .partial (jax .lax .all_to_all , axis_name = ulysses_axis , tiled = True )
848888 query = a2a (query , split_axis = 1 , concat_axis = 2 )
849889 key = a2a (key , split_axis = 1 , concat_axis = 2 )
850890 value = a2a (value , split_axis = 1 , concat_axis = 2 )
@@ -857,7 +897,7 @@ def wrap_ulysses_ring_attention(query, key, value):
857897 value , _ , _ = _pad_data_for_flash (value , heads , bkv )
858898
859899 bsizes = custom_splash ._BlockSizes (block_q = bq , block_kv = bkv , block_kv_compute = bkv_compute )
860- # (2) Ring over R within each U-group (rotation restricted by ring_perm) .
900+ # (2) Ring (full ppermute over the cross-chip ring axis) with the custom kernel .
861901 ring_kernel = tokamax_ring_attention_kernel .make_custom_ring_attention (
862902 block_sizes = bsizes ,
863903 bkv_compute_in = bkv_compute_in ,
@@ -866,19 +906,20 @@ def wrap_ulysses_ring_attention(query, key, value):
866906 use_base2_exp = use_base2_exp ,
867907 use_experimental_scheduler = use_experimental_scheduler ,
868908 vmem_limit_bytes = vmem_limit_bytes ,
869- ring_axis = axis_name ,
870- ring_size = R ,
871- perm = ring_perm ,
909+ ring_axis = ring_axis ,
910+ ring_size = num_ring_shards ,
911+ bidirectional = wrap_free_ring ,
872912 )
873913 vmapped_ring = jax .vmap (ring_kernel , in_axes = (0 , 0 , 0 ))
874914 attention_output = vmapped_ring (query , key , value )
875915 attention_output = attention_output [:, :, :query_seq_len , :kv_size ].astype (query .dtype )
876916
877- # (3) Ulysses all-to-all back: sequence -> heads swap , restoring the layout.
917+ # (3) Ulysses all-to-all back: sequence -> heads, restoring the layout.
878918 attention_output = a2a (attention_output , split_axis = 2 , concat_axis = 1 )
879919 return attention_output
880920
881921 x = wrap_ulysses_ring_attention (query , key , value )
922+ x = jax .lax .with_sharding_constraint (x , q_axis_names )
882923 x = x [:, :, :orig_q_seq_len , :]
883924 x = _reshape_heads_to_head_dim (x )
884925 return x
0 commit comments