3131on the block are pytest parametrize values rather than separate test
3232classes:
3333
34- * ``test_forward`` covers the forward across a curated set of
35- configurations (softmax/sigmoid scoring, optional non-zero
36- expert_bias). Each config asserts shape, dtype, finiteness and
37- numerical parity vs the reference in one run.
38- * ``test_backward`` mirrors that for gradients.
34+ * ``test_forward`` covers BF16 and MXFP8 forward execution across a
35+ curated set of configurations (softmax/sigmoid scoring, optional
36+ non-zero expert_bias). Each config asserts shape, dtype, finiteness
37+ and numerical parity vs the same BF16 reference in one run.
38+ * ``test_backward`` mirrors that for gradients. BF16 and MXFP8 share
39+ the full test body and differ only in the grouped-GEMM quantizer sets.
3940* ``TestTeEpMoeAuxLoss`` covers the second return value end-to-end
4041 (returned + parity + aux-only grad propagates to gate + combined
4142 main+aux grads stay finite) in two consolidated tests.
@@ -118,8 +119,14 @@ def _read_mp_options():
118119 )
119120
120121from transformer_engine .jax .flax import _MoEBlock as MoEBlock
121- from transformer_engine .jax .moe import _ALIGN_SIZE , moe , record_ep_bootstrap_signature_for_moe
122+ from transformer_engine .jax .moe import (
123+ _ALIGN_SIZE ,
124+ get_moe_recv_capacity_per_rank ,
125+ moe ,
126+ record_ep_bootstrap_signature_for_moe ,
127+ )
122128from transformer_engine .jax .ep import ep_bootstrap
129+ from transformer_engine .common .recipe import MXFP8BlockScaling
123130from transformer_engine .jax .sharding import MeshResource , global_shard_guard
124131
125132
@@ -148,60 +155,38 @@ def _read_mp_options():
148155DTYPE = jnp .bfloat16
149156BATCH = EP_SIZE * FSDP_SIZE * 2 # 8 on 4-GPU, 16 on 8-GPU
150157SEQ = 32
151- HIDDEN = 64
158+ HIDDEN = 128
152159INTER = 128
153160NUM_EXPERTS = 8
154161TOPK = 2
155162
156- # bf16 grouped_gemm + softmax-topk + ep all-to-all stack drifts ~1e-1 vs a
157- # fp32 numpy reference. Keep these tight enough to catch real bugs but
158- # loose enough to absorb expected bf16 rounding.
159- FWD_ATOL = 5e-2
160- FWD_RTOL = 5e-2
161- GRAD_FFN_ATOL = 1e-1
162- GRAD_FFN_RTOL = 1e-1
163- GRAD_GATE_ATOL = 5e-1
164- GRAD_GATE_RTOL = 5e-1
165-
166- # Two TE EP runs that should be bitwise-equal modulo XLA fusion order
167- # (slot alignment rounding, etc.).
168- TE_TO_TE_ATOL = 5e-3
169- TE_TO_TE_RTOL = 5e-3
163+ # MXFP8 grouped GEMMs have measurably more quantization drift than BF16.
164+ # These bounds are rounded slightly above the worst error observed across
165+ # the forward and backward configuration matrix.
166+ FWD_TOLERANCE = {
167+ "bf16" : {"atol" : 5e-4 , "rtol" : 5e-4 },
168+ "mxfp8" : {"atol" : 7e-3 , "rtol" : 7e-3 },
169+ }
170+ GRAD_FFN_TOLERANCE = {
171+ "bf16" : {"atol" : 1e-7 , "rtol" : 1e-7 },
172+ "mxfp8" : {"atol" : 1.3e-6 , "rtol" : 1.3e-6 },
173+ }
174+ GRAD_GATE_TOLERANCE = {
175+ "bf16" : {"atol" : 7e-8 , "rtol" : 7e-8 },
176+ "mxfp8" : {"atol" : 8e-7 , "rtol" : 8e-7 },
177+ }
170178
171179# Aux loss is computed in float32 from the SAME logits as the routing
172180# path. Numerical drift between TE-EP and the reference is dominated by
173181# the bf16-rounded softmax inside the topk kernel.
174- AUX_ATOL = 1e-3
175- AUX_RTOL = 1e-3
182+ AUX_TOLERANCE = {"atol" : 1e-6 , "rtol" : 1e-6 }
176183
177184
178185# -----------------------------------------------------------------------------
179186# Fixtures
180187# -----------------------------------------------------------------------------
181188
182189
183- def _compute_worst_case_recv_pr ():
184- """Per-rank recv buffer the bootstrap must reserve.
185-
186- NCCL EP HT expert-major uses one flat recv buffer with variable
187- per-expert zones. Each non-empty expert zone is padded to
188- ``_ALIGN_SIZE`` slots, so the reserve must cover the worst-case
189- total assignments plus independent per-zone padding.
190- """
191- num_procs = jax .device_count ()
192- num_local_experts = NUM_EXPERTS // EP_SIZE
193- max_tokens_per_rank = (BATCH // num_procs ) * SEQ
194- tokens_per_ep_group = EP_SIZE * max_tokens_per_rank
195- max_local_assignments = tokens_per_ep_group * min (TOPK , num_local_experts )
196- max_nonempty_experts = min (num_local_experts , max_local_assignments )
197- padded_total_bound = max_local_assignments + (_ALIGN_SIZE - 1 ) * max_nonempty_experts
198- aligned_total_bound = ((padded_total_bound + _ALIGN_SIZE - 1 ) // _ALIGN_SIZE ) * _ALIGN_SIZE
199- per_expert_bound = (
200- num_local_experts * ((tokens_per_ep_group + _ALIGN_SIZE - 1 ) // _ALIGN_SIZE ) * _ALIGN_SIZE
201- )
202- return min (per_expert_bound , aligned_total_bound )
203-
204-
205190@pytest .fixture (scope = "module" )
206191def mesh ():
207192 if jax .device_count () < NUM_DEVICES_REQUIRED :
@@ -217,7 +202,15 @@ def mesh():
217202
218203 num_procs = jax .process_count ()
219204 max_tokens_per_rank = (BATCH // num_procs ) * SEQ
220- recv_capacity_per_rank = _compute_worst_case_recv_pr ()
205+ # Worst-case recv capacity per rank
206+ # TODO(jberchtold) support configurations other than worst-case by refactoring tests
207+ # but if possible avoid bootstrap/teardown for each test
208+ recv_capacity_per_rank = get_moe_recv_capacity_per_rank (
209+ num_experts = NUM_EXPERTS ,
210+ num_experts_per_tok = TOPK ,
211+ max_tokens_per_rank = max_tokens_per_rank ,
212+ ep_size = EP_SIZE ,
213+ )
221214
222215 # Eager bootstrap: ep_bootstrap does a host-side NCCL UID allgather
223216 # and cannot run from inside jax.jit. Sized to the worst-case recv_pr
@@ -275,8 +268,7 @@ def mesh():
275268def _pure_jax_moe_reference (
276269 x ,
277270 gate_kernel ,
278- wi_0 ,
279- wi_1 ,
271+ wi ,
280272 wo ,
281273 expert_bias = None ,
282274 * ,
@@ -317,6 +309,7 @@ def _pure_jax_moe_reference(
317309 # FFN. ``apply_topk_weights_early`` is a fusion knob that doesn't
318310 # change the math (wo is linear), so the reference is identical for
319311 # both placements.
312+ wi_0 , wi_1 = jnp .split (wi , 2 , axis = - 1 )
320313 layer_w0 = jnp .einsum ("th,ehm->tem" , x_2d , wi_0 )
321314 layer_w1 = jnp .einsum ("th,ehm->tem" , x_2d , wi_1 )
322315 # Activation runs in x.dtype (typically bf16) to mirror the impl --
@@ -365,6 +358,7 @@ def _make_block(
365358 score_function = "softmax" ,
366359 expert_bias_init = None ,
367360 input_axes = ("batch" , None , None ),
361+ quantization_recipe = None ,
368362):
369363 kwargs = dict (
370364 num_experts = NUM_EXPERTS ,
@@ -377,6 +371,7 @@ def _make_block(
377371 score_function = score_function ,
378372 dtype = DTYPE ,
379373 input_axes = input_axes ,
374+ quantization_recipe = quantization_recipe ,
380375 )
381376 # Custom expert_bias_init lets tests inject a non-zero expert_bias without
382377 # poking variables['params'] post-init.
@@ -436,7 +431,14 @@ def _init_apply(block, mesh, x, key):
436431 return variables , output , aux
437432
438433
439- def _grad_step (block , variables , mesh , x , * , include_aux = False ):
434+ def _grad_step (
435+ block ,
436+ variables ,
437+ mesh ,
438+ x ,
439+ * ,
440+ include_aux = False ,
441+ ):
440442 """Run jax.grad of mean(out^2) [+ aux if include_aux] vs (params, x).
441443
442444 Returns ``(grads_variables, grad_x)`` so callers can check both the
@@ -503,6 +505,13 @@ def _make_inputs(key):
503505 return jax .random .normal (key , (BATCH , SEQ , HIDDEN ), dtype = DTYPE )
504506
505507
508+ def _quantization_recipe (quantization ):
509+ if quantization == "bf16" :
510+ return None
511+ assert quantization == "mxfp8"
512+ return MXFP8BlockScaling ()
513+
514+
506515# -----------------------------------------------------------------------------
507516# Tests
508517# -----------------------------------------------------------------------------
@@ -546,6 +555,13 @@ def _make_inputs(key):
546555 ),
547556]
548557
558+ _QUANTIZATION_CASES = [
559+ pytest .param ("bf16" , id = "bf16" ),
560+ ]
561+
562+ if get_device_compute_capability (0 ) >= 100 :
563+ _QUANTIZATION_CASES .append (pytest .param ("mxfp8" , id = "mxfp8" ))
564+
549565
550566def _reference_kwargs_from_config (config , params_np ):
551567 """Pick out the reference-relevant pieces of a parametrize config."""
@@ -564,8 +580,9 @@ class TestTeEpMoeForward:
564580 finiteness AND numerical parity vs the pure-JAX reference."""
565581
566582 @pytest .mark .parametrize ("config" , _CONFIGS )
567- def test_forward (self , mesh , config ):
568- block = _make_block (** config )
583+ @pytest .mark .parametrize ("quantization" , _QUANTIZATION_CASES )
584+ def test_forward (self , mesh , config , quantization ):
585+ block = _make_block (** config , quantization_recipe = _quantization_recipe (quantization ))
569586 x = _make_inputs (jax .random .PRNGKey (0 ))
570587 variables , output , aux = _init_apply (block , mesh , x , jax .random .PRNGKey (1 ))
571588
@@ -584,8 +601,7 @@ def test_forward(self, mesh, config):
584601 out_ref , _ = _pure_jax_moe_reference (
585602 jnp .asarray (x_np ),
586603 jnp .asarray (params_np ["gate_kernel" ]),
587- jnp .asarray (params_np ["wi_0" ]),
588- jnp .asarray (params_np ["wi_1" ]),
604+ jnp .asarray (params_np ["wi" ]),
589605 jnp .asarray (params_np ["wo" ]),
590606 num_experts = NUM_EXPERTS ,
591607 num_experts_per_tok = TOPK ,
@@ -594,9 +610,8 @@ def test_forward(self, mesh, config):
594610 np .testing .assert_allclose (
595611 out_te_np .astype (np .float32 ),
596612 np .asarray (jax .device_get (out_ref )).astype (np .float32 ),
597- atol = FWD_ATOL ,
598- rtol = FWD_RTOL ,
599- err_msg = f"forward parity breach for config={ config } " ,
613+ ** FWD_TOLERANCE [quantization ],
614+ err_msg = f"forward parity breach for config={ config } , quantization={ quantization } " ,
600615 )
601616
602617
@@ -605,8 +620,9 @@ class TestTeEpMoeBackward:
605620 grads finite, non-zero AND parity vs the pure-JAX reference."""
606621
607622 @pytest .mark .parametrize ("config" , _CONFIGS )
608- def test_backward (self , mesh , config ):
609- block = _make_block (** config )
623+ @pytest .mark .parametrize ("quantization" , _QUANTIZATION_CASES )
624+ def test_backward (self , mesh , config , quantization ):
625+ block = _make_block (** config , quantization_recipe = _quantization_recipe (quantization ))
610626 x = _make_inputs (jax .random .PRNGKey (2 ))
611627 variables , _ , _ = _init_apply (block , mesh , x , jax .random .PRNGKey (3 ))
612628 grads_te , grad_x_te = _grad_step (block , variables , mesh , x )
@@ -623,8 +639,7 @@ def loss_fn(params, x):
623639 out , _ = _pure_jax_moe_reference (
624640 x ,
625641 params ["gate_kernel" ],
626- params ["wi_0" ],
627- params ["wi_1" ],
642+ params ["wi" ],
628643 params ["wo" ],
629644 ref_expert_bias ,
630645 num_experts = NUM_EXPERTS ,
@@ -640,22 +655,19 @@ def loss_fn(params, x):
640655 grads_ref_np = {k : np .asarray (jax .device_get (v )) for k , v in grads_ref .items ()}
641656 grad_x_ref_np = np .asarray (jax .device_get (grad_x_ref ))
642657
643- for name in ("gate_kernel" , "wi_0" , "wi_1 " , "wo" ):
658+ for name in ("gate_kernel" , "wi " , "wo" ):
644659 # Per-tensor: finite + non-zero + parity in one pass.
645660 g_te = _to_global_numpy (_unwrap (grads_te ["params" ][name ]), mesh )
646661 assert np .all (np .isfinite (g_te )), f"{ name } grad has NaN/Inf [config={ config } ]"
647662 assert np .any (g_te != 0.0 ), f"{ name } grad identically zero [config={ config } ]"
648- atol , rtol = (
649- (GRAD_GATE_ATOL , GRAD_GATE_RTOL )
650- if name == "gate_kernel"
651- else (GRAD_FFN_ATOL , GRAD_FFN_RTOL )
652- )
663+ tolerances = GRAD_GATE_TOLERANCE if name == "gate_kernel" else GRAD_FFN_TOLERANCE
653664 np .testing .assert_allclose (
654665 g_te .astype (np .float32 ),
655666 grads_ref_np [name ].astype (np .float32 ),
656- atol = atol ,
657- rtol = rtol ,
658- err_msg = f"grad parity breach on { name } [config={ config } ]" ,
667+ ** tolerances [quantization ],
668+ err_msg = (
669+ f"grad parity breach on { name } [config={ config } , quantization={ quantization } ]"
670+ ),
659671 )
660672
661673 # d_x: the gradient propagated back to the previous layer. Checks
@@ -675,9 +687,8 @@ def loss_fn(params, x):
675687 np .testing .assert_allclose (
676688 grad_x_te_np .astype (np .float32 ),
677689 grad_x_ref_np .astype (np .float32 ),
678- atol = GRAD_FFN_ATOL ,
679- rtol = GRAD_FFN_RTOL ,
680- err_msg = f"d_x parity breach [config={ config } ]" ,
690+ ** GRAD_FFN_TOLERANCE [quantization ],
691+ err_msg = f"d_x parity breach [config={ config } , quantization={ quantization } ]" ,
681692 )
682693
683694
@@ -710,8 +721,7 @@ def test_aux_loss(self, mesh):
710721 _ , aux_ref = _pure_jax_moe_reference (
711722 jnp .asarray (x_np ),
712723 jnp .asarray (params_np ["gate_kernel" ]),
713- jnp .asarray (params_np ["wi_0" ]),
714- jnp .asarray (params_np ["wi_1" ]),
724+ jnp .asarray (params_np ["wi" ]),
715725 jnp .asarray (params_np ["wo" ]),
716726 num_experts = NUM_EXPERTS ,
717727 num_experts_per_tok = TOPK ,
@@ -720,8 +730,7 @@ def test_aux_loss(self, mesh):
720730 np .testing .assert_allclose (
721731 float (aux_np ),
722732 float (jax .device_get (aux_ref )),
723- atol = AUX_ATOL ,
724- rtol = AUX_RTOL ,
733+ ** AUX_TOLERANCE ,
725734 )
726735
727736 # Aux-only bwd must propagate to gate_kernel — proves the
@@ -741,7 +750,7 @@ def test_combined_loss_grads(self, mesh):
741750 x = _make_inputs (jax .random .PRNGKey (22 ))
742751 variables , _ , _ = _init_apply (block , mesh , x , jax .random .PRNGKey (23 ))
743752 grads , _ = _grad_step (block , variables , mesh , x , include_aux = True )
744- for name in ("gate_kernel" , "wi_0" , "wi_1 " , "wo" ):
753+ for name in ("gate_kernel" , "wi " , "wo" ):
745754 g_local = np .asarray (jax .device_get (_unwrap (grads ["params" ][name ]).addressable_data (0 )))
746755 assert np .all (np .isfinite (g_local )), f"{ name } grad NaN/Inf under main+aux"
747756 assert np .any (g_local != 0.0 ), f"{ name } grad zero under main+aux"
0 commit comments