2323import jax .numpy as jnp
2424
2525from jax import lax
26- from jax .sharding import NamedSharding , Mesh
26+ from jax .sharding import NamedSharding , Mesh , PartitionSpec
2727from jax .ad_checkpoint import checkpoint_name
2828
2929from flax import nnx
3838from maxtext .utils import max_logging
3939from maxtext .utils import max_utils
4040from maxtext .utils .sharding import maybe_shard_with_logical
41+ from maxtext .utils .sharding import maybe_shard_with_name
42+ from maxtext .utils .sharding import get_physical_spec_without_axes
43+ from maxtext .utils .sharding import FSDP_MESH_AXES
4144
4245
4346def _convert_to_activation_function (fn_or_string : str | Callable [..., Any ]) -> Callable [..., Any ]:
@@ -121,6 +124,9 @@ def __init__(
121124 shard_mode : ShardMode = ShardMode .AUTO ,
122125 matmul_precision : str = "default" ,
123126 parameter_memory_host_offload : bool = False ,
127+ mesh : Mesh | None = None ,
128+ use_two_stage_all_gather : bool = False ,
129+ debug_sharding : bool = False ,
124130 * , # Following arguments are keyword-only
125131 rngs : nnx .Rngs = None ,
126132 ):
@@ -140,6 +146,13 @@ def __init__(
140146 shard_mode: auto or explicit shard mode.
141147 matmul_precision: Precision for matrix multiplication.
142148 parameter_memory_host_offload: Determines whether to offload params to host
149+ mesh: Mesh of devices and physical axes, needed for two-stage all-gather.
150+ use_two_stage_all_gather: when the kernel is sharded on both the fsdp and
151+ fsdp_transpose axes, gather the two axes with two separate all-gather
152+ calls (separated by an optimization barrier) to avoid the relayout
153+ transpose XLA emits for a single combined 2-axis all-gather.
154+ debug_sharding: when True, log the logical/physical sharding of the
155+ two-stage all-gather constraints to the sharding dump files.
143156 rngs: RNG state for initialization in nnx.
144157 """
145158 self .in_features_shape = canonicalize_tuple (in_features_shape )
@@ -154,6 +167,9 @@ def __init__(
154167 self .shard_mode = shard_mode
155168 self .matmul_precision = matmul_precision
156169 self .parameter_memory_host_offload = parameter_memory_host_offload
170+ self .mesh = mesh
171+ self .use_two_stage_all_gather = use_two_stage_all_gather
172+ self .debug_sharding = debug_sharding
157173
158174 # Parameter initialization
159175 kernel_shape = self .in_features_shape + self .out_features_shape
@@ -200,6 +216,39 @@ def quant_dot_general(self) -> nnx_wrappers.ToNNX | None:
200216 return None
201217 return getattr (self , self ._quant_dot_general_name )
202218
219+ def _maybe_two_stage_all_gather (self , kernel ):
220+ """Gather a 2D-FSDP-sharded MLP kernel with two single-axis all-gathers.
221+
222+ When the kernel is sharded on both the `fsdp` and `fsdp_transpose` mesh axes,
223+ a single combined 2-axis all-gather forces XLA to materialize an interleave
224+ transpose to fix the layout. Splitting into two single-axis gathers separated
225+ by an `optimization_barrier` makes each stage produce a contiguous layout, so
226+ no transpose is emitted. Mirrors `moe_fsdp_use_two_stage_all_gather`.
227+ """
228+ if (
229+ not self .use_two_stage_all_gather
230+ or self .mesh is None
231+ or self .mesh .shape .get ("fsdp" , 1 ) <= 1
232+ or self .mesh .shape .get ("fsdp_transpose" , 1 ) <= 1
233+ ):
234+ return kernel
235+
236+ # kernel_axes is a plain tuple of logical names; wrap it so the logical-to-physical
237+ # lookup treats it as a single spec rather than a pytree of strings.
238+ full_logical = PartitionSpec (* self .kernel_axes )
239+ # Stage 1 gathers fsdp_transpose, stage 2 gathers the remaining fsdp.
240+ stage1 = get_physical_spec_without_axes (full_logical , self .mesh , ("fsdp_transpose" ,))
241+ stage2 = get_physical_spec_without_axes (full_logical , self .mesh , FSDP_MESH_AXES )
242+ if stage1 .spec == stage2 .spec :
243+ # Not sharded on both FSDP axes, so a single all-gather is already optimal.
244+ return kernel
245+
246+ shard = functools .partial (maybe_shard_with_name , shard_mode = self .shard_mode , debug_sharding = self .debug_sharding )
247+ kernel = shard (kernel , stage1 )
248+ kernel = jax .lax .optimization_barrier (kernel )
249+ kernel = shard (kernel , stage2 )
250+ return kernel
251+
203252 def __call__ (self , inputs : Array , _initializing : bool = False , out_sharding : NamedSharding | None = None ) -> Array :
204253 """Applies a linear transformation to the inputs along multiple dimensions.
205254
@@ -230,6 +279,8 @@ def __call__(self, inputs: Array, _initializing: bool = False, out_sharding: Nam
230279 kernel = jax .device_put (kernel , max_utils .device_space ())
231280 kernel = jnp .asarray (kernel , self .dtype )
232281
282+ kernel = self ._maybe_two_stage_all_gather (kernel )
283+
233284 # out_sharding should be None for auto mesh axis
234285 if self .shard_mode != ShardMode .EXPLICIT :
235286 out_sharding = None
@@ -422,6 +473,9 @@ def __init__(
422473 use_bias = self .use_bias ,
423474 shard_mode = self .config .shard_mode ,
424475 matmul_precision = self .config .matmul_precision ,
476+ mesh = self .mesh ,
477+ use_two_stage_all_gather = self .config .dense_fsdp_use_two_stage_all_gather ,
478+ debug_sharding = self .config .debug_sharding ,
425479 rngs = rngs ,
426480 )
427481 else :
@@ -438,6 +492,9 @@ def __init__(
438492 use_bias = self .use_bias ,
439493 shard_mode = self .config .shard_mode ,
440494 matmul_precision = self .config .matmul_precision ,
495+ mesh = self .mesh ,
496+ use_two_stage_all_gather = self .config .dense_fsdp_use_two_stage_all_gather ,
497+ debug_sharding = self .config .debug_sharding ,
441498 rngs = rngs ,
442499 )
443500 setattr (self , dense_name , module )
@@ -453,6 +510,9 @@ def __init__(
453510 use_bias = self .use_bias ,
454511 shard_mode = self .config .shard_mode ,
455512 matmul_precision = self .config .matmul_precision ,
513+ mesh = self .mesh ,
514+ use_two_stage_all_gather = self .config .dense_fsdp_use_two_stage_all_gather ,
515+ debug_sharding = self .config .debug_sharding ,
456516 rngs = rngs ,
457517 )
458518
0 commit comments