@@ -203,5 +203,68 @@ def test_nnx_ensure_scan_leading_axis_mixed(self):
203203 self .assertEqual (broadcast_state ["raw_array" ].shape , (10 ,))
204204
205205
206+ def _make_scanned_param (shape , out_sharding , partition_name ):
207+ """Build an nnx.Param mirroring a scanned decoder variable.
208+
209+ DeepSeek stacks name their scan axis via nnx.PARTITION_NAME ("dense_layers" or
210+ "moe_layers"), so the variable carries that name in its metadata rather than
211+ the caller literal "layers". Eager sharding is disabled because the sliced
212+ value rank is intentionally one less than the metadata length.
213+ """
214+ with nnx .use_eager_sharding (False ):
215+ return nnx .Param (jnp .zeros (shape ), out_sharding = out_sharding , ** {nnx .PARTITION_NAME : partition_name })
216+
217+
218+ class TestScanAxisMetadata (unittest .TestCase ):
219+ """Lock in per-variable scan-axis resolution for DeepSeek-style stacks."""
220+
221+ def test_nnx_remove_scan_axis_preserves_real_leading_axis (self ):
222+ """nnx_remove_scan_axis must drop the stack's own scan axis, not "embed"."""
223+ # Metadata carries the real fsdp axis "embed" plus the scan axis
224+ # "dense_layers"; the sliced value is rank-2, one less than the 3 names.
225+ param = _make_scanned_param ((4 , 8 ), ("embed" , "dense_layers" , "mlp" ), "dense_layers" )
226+ state = nnx .State ({"kernel" : param })
227+
228+ result = maxtext_utils_nnx .nnx_remove_scan_axis (state , "layers" )
229+
230+ out_sharding = result ["kernel" ].get_metadata ().get ("out_sharding" )
231+ # The scan axis is gone and the real leading logical axis survives. The
232+ # unfixed fallback pops index 0 and strips "embed" to ("dense_layers", "mlp").
233+ self .assertEqual (tuple (out_sharding ), ("embed" , "mlp" ))
234+
235+ def test_nnx_remove_scan_axis_moe_layers (self ):
236+ """The same resolution holds for the "moe_layers" stack axis name."""
237+ param = _make_scanned_param ((4 , 8 ), ("embed" , "moe_layers" , "mlp" ), "moe_layers" )
238+ state = nnx .State ({"kernel" : param })
239+
240+ result = maxtext_utils_nnx .nnx_remove_scan_axis (state , "layers" )
241+
242+ out_sharding = result ["kernel" ].get_metadata ().get ("out_sharding" )
243+ self .assertEqual (tuple (out_sharding ), ("embed" , "mlp" ))
244+
245+ def test_nnx_remove_scan_axis_raises_on_inconsistent_metadata (self ):
246+ """A rank mismatch with no matching scan axis is an error, not a silent pop."""
247+ # No name in the metadata matches the resolved scan axis, and the metadata
248+ # is one longer than the value rank; the fixed code raises instead of
249+ # blindly popping a real axis.
250+ param = _make_scanned_param ((4 , 8 ), ("embed" , "mlp" , "vocab" ), "dense_layers" )
251+ state = nnx .State ({"kernel" : param })
252+
253+ with self .assertRaises (ValueError ):
254+ maxtext_utils_nnx .nnx_remove_scan_axis (state , "layers" )
255+
256+ def test_nnx_add_and_sync_scan_axis_uses_partition_name (self ):
257+ """nnx_add_and_sync_scan_axis must insert the stack's own axis name, not "layers"."""
258+ # Stacked value is rank-3; metadata holds the two sliced logical axes.
259+ param = _make_scanned_param ((2 , 4 , 8 ), ("embed" , "mlp" ), "dense_layers" )
260+ state = nnx .State ({"kernel" : param })
261+
262+ result = maxtext_utils_nnx .nnx_add_and_sync_scan_axis (state , "layers" , 0 )
263+
264+ out_sharding = result ["kernel" ].get_metadata ().get ("out_sharding" )
265+ # The unfixed code inserts the literal "layers" here.
266+ self .assertEqual (tuple (out_sharding ), ("dense_layers" , "embed" , "mlp" ))
267+
268+
206269if __name__ == "__main__" :
207270 unittest .main ()
0 commit comments