Skip to content

Commit 0e1ced1

Browse files
committed
Integrate TE MoE block support for DSv4-related changes
1 parent 2c16460 commit 0e1ced1

6 files changed

Lines changed: 191 additions & 33 deletions

File tree

‎src/maxtext/configs/types.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5697,10 +5697,11 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
56975697
raise ValueError("te_moe_quantize_before_fsdp_all_gather=True does not support 2D FSDP weight sharding.")
56985698
if self.te_moe_block and not self.prefuse_moe_weights:
56995699
raise ValueError("te_moe_block=True requires prefuse_moe_weights=True.")
5700-
if self.te_moe_block and self.routed_bias_update_rate > 0.0:
5701-
raise ValueError("te_moe_block=True does not currently support routed_bias_update_rate > 0.")
5702-
if self.te_moe_block and self.norm_topk_prob:
5703-
raise ValueError("te_moe_block=True does not currently support norm_topk_prob=True.")
5700+
if self.te_moe_block and self.norm_topk_prob and self.routed_score_func not in ["sqrtsoftplus", "sigmoid"]:
5701+
raise ValueError(
5702+
"te_moe_block=True does not currently support norm_topk_prob=True "
5703+
"when routed_score_func is not 'sqrtsoftplus' or 'sigmoid'."
5704+
)
57045705
if self.te_moe_block and self.use_random_routing:
57055706
raise ValueError("te_moe_block=True does not support use_random_routing=True.")
57065707
if self.te_moe_block and self.decoder_block == DecoderBlockType.LLAMA4:

‎src/maxtext/layers/moe.py‎

Lines changed: 110 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ def _get_te_moe_cudnn_gmm_fusion_kwargs(disable_cudnn_gmm_fusion: bool) -> dict[
9595
return {"use_cudnn_fusion": False} if disable_cudnn_gmm_fusion else {}
9696

9797

98+
def _get_te_moe_weight_layout_kwargs(moe_fn) -> dict[str, bool]:
99+
"""Declare MaxText's standard [E, K, 2N] FC1 storage when TE accepts the layout flag.
100+
101+
TE infers the layout from the FC1 shape, which is ambiguous when K == 2N (e.g. DeepSeek V4).
102+
"""
103+
if "cudnn_native_weight_layout" in inspect.signature(moe_fn).parameters:
104+
return {"cudnn_native_weight_layout": False}
105+
return {}
106+
107+
98108
@struct.dataclass
99109
class RouteMetadata:
100110
"""EP communication state needed to undo the forward all-to-all after expert computation."""
@@ -401,8 +411,7 @@ def load_balance_updates_from_counts(expert_counts, num_experts, rate):
401411
total_tokens = jnp.sum(expert_counts, axis=-1, keepdims=True)
402412
average_load = total_tokens / num_experts
403413
direction = jnp.sign(average_load - expert_counts)
404-
output = direction * rate
405-
return output
414+
return direction * rate
406415

407416

408417
def finalize_deferred_bias_signal(partial_counts, config):
@@ -1022,6 +1031,7 @@ def __init__(
10221031
self.quant_einsums = None
10231032

10241033
gate_weight_dtype = ctypes.get_weight_dtype(self.config, "gate")
1034+
self.use_gate_bias = self.config.routed_bias and not self.is_hash_routing
10251035
self.gate = GateLogit(
10261036
in_features_shape=self.moe_expert_input_dim,
10271037
out_features_shape=self.num_experts,
@@ -1032,7 +1042,7 @@ def __init__(
10321042
quant=ctypes.get_quant(self.config, "gate", self.quant),
10331043
kernel_init=self.kernel_init,
10341044
kernel_axes=self.kernel_axes,
1035-
use_bias=self.config.routed_bias and not self.is_hash_routing,
1045+
use_bias=self.use_gate_bias,
10361046
bias_dtype=jnp.float32 if self.config.float32_gate_logits else gate_weight_dtype,
10371047
# tpu-inference applies the score function in the fused_moe_gmm kernel,
10381048
# so we don't apply it here to avoid redundant computation.
@@ -4867,6 +4877,8 @@ def _te_moe_block(
48674877
w0_bias: jax.Array | None,
48684878
w1_bias: jax.Array | None,
48694879
wo_bias: jax.Array | None,
4880+
routing_indices: jax.Array | None = None,
4881+
routing_weights: jax.Array | None = None,
48704882
out_sharding: NamedSharding | None = None,
48714883
) -> tuple[jax.Array, Optional[jax.Array], Optional[jax.Array]]:
48724884
"""Run TransformerEngine's fused EP MoEBlock using MaxText-owned params."""
@@ -4881,10 +4893,10 @@ def _te_moe_block(
48814893

48824894
if self.quant is None or not hasattr(self.quant, "get_moe_block_quantizer_sets"):
48834895
raise ValueError("te_moe_block=True requires TransformerEngine quantization or te_gmm_quantization=te_no_quant.")
4896+
if self.is_hash_routing and (routing_indices is None or routing_weights is None):
4897+
raise ValueError("TE MoE hash routing requires precomputed routing indices and weights.")
48844898

4885-
expert_bias = None
4886-
if self.config.routed_bias:
4887-
expert_bias = jnp.asarray(self.gate.bias[...], jnp.float32)
4899+
expert_bias = jnp.asarray(self.gate.bias[...], jnp.float32) if self.use_gate_bias else None
48884900

48894901
fsdp_size = self.mesh.shape.get("fsdp", 1)
48904902
ep_size = self.mesh.shape.get(self._expert_parallelism_name, 1)
@@ -4907,45 +4919,107 @@ def _te_moe_block(
49074919

49084920
checkpoint_kwargs = _get_te_moe_checkpoint_kwargs(te_moe.moe)
49094921
cudnn_gmm_fusion_kwargs = _get_te_moe_cudnn_gmm_fusion_kwargs(self.config.te_moe_disable_cudnn_gmm_fusion)
4922+
weight_layout_kwargs = _get_te_moe_weight_layout_kwargs(te_moe.moe)
4923+
4924+
# Older TE versions take the router arguments directly and do not return expert counts.
4925+
supports_router_info = hasattr(te_moe, "RouterComputationInfo")
4926+
if not supports_router_info:
4927+
if self.is_hash_routing:
4928+
raise ImportError(
4929+
"te_moe_block=True with hash routing requires TransformerEngine with "
4930+
"moe.RoutingMapInfo support. Please upgrade TransformerEngine."
4931+
)
4932+
if self.should_update_load_balance():
4933+
raise ImportError(
4934+
"te_moe_block=True with routed_bias_update_rate > 0 requires TransformerEngine with "
4935+
"MoE expert-count support. Please upgrade TransformerEngine."
4936+
)
4937+
router_kwargs = {
4938+
"gate_kernel": gate_kernel,
4939+
"num_experts_per_tok": self.num_experts_per_tok,
4940+
"expert_bias": expert_bias,
4941+
"score_function": self.config.routed_score_func or "softmax",
4942+
"use_pre_softmax": False,
4943+
"num_groups": None if self.config.n_routing_groups <= 0 else self.config.n_routing_groups,
4944+
"group_topk": None if self.config.topk_routing_group <= 0 else self.config.topk_routing_group,
4945+
"scaling_factor": self.config.routed_scaling_factor,
4946+
"aux_loss_coeff": self.config.load_balance_loss_weight,
4947+
"gate_kernel_axes": self.kernel_axes,
4948+
}
4949+
elif self.is_hash_routing:
4950+
router_kwargs = {
4951+
"router_info": te_moe.RoutingMapInfo(
4952+
routing_indices=routing_indices,
4953+
routing_weights=routing_weights,
4954+
),
4955+
"collect_expert_counts": False,
4956+
}
4957+
else:
4958+
router_kwargs = {
4959+
"router_info": te_moe.RouterComputationInfo(
4960+
gate_kernel=gate_kernel,
4961+
num_experts_per_tok=self.num_experts_per_tok,
4962+
expert_bias=expert_bias,
4963+
score_function=self.config.routed_score_func or "softmax",
4964+
use_pre_softmax=False,
4965+
num_groups=None if self.config.n_routing_groups <= 0 else self.config.n_routing_groups,
4966+
group_topk=None if self.config.topk_routing_group <= 0 else self.config.topk_routing_group,
4967+
scaling_factor=self.config.routed_scaling_factor,
4968+
aux_loss_coeff=self.config.load_balance_loss_weight,
4969+
gate_kernel_axes=self.kernel_axes,
4970+
),
4971+
"collect_expert_counts": self.should_update_load_balance(),
4972+
}
49104973

4911-
output, lb_loss, total_recv_tokens = te_moe.moe(
4974+
# Pass the weights by keyword: the older interface has gate_kernel in the second positional slot.
4975+
te_moe_outputs = te_moe.moe(
49124976
inputs,
4913-
gate_kernel,
4914-
wi_kernel,
4915-
wo_kernel,
4916-
w0_bias,
4917-
w1_bias,
4918-
wo_bias,
4919-
expert_bias,
4977+
wi=wi_kernel,
4978+
wo=wo_kernel,
4979+
wi_0_bias=w0_bias,
4980+
wi_1_bias=w1_bias,
4981+
wo_bias=wo_bias,
49204982
num_experts=self.num_experts,
4921-
num_experts_per_tok=self.num_experts_per_tok,
49224983
activation_type=self.config.mlp_activations[0],
4923-
score_function=self.config.routed_score_func or "softmax",
4924-
use_pre_softmax=False,
4925-
num_groups=None if self.config.n_routing_groups <= 0 else self.config.n_routing_groups,
4926-
group_topk=None if self.config.topk_routing_group <= 0 else self.config.topk_routing_group,
4927-
scaling_factor=self.config.routed_scaling_factor,
4928-
aux_loss_coeff=self.config.load_balance_loss_weight,
49294984
apply_topk_weights_early=True,
49304985
quantizer_sets=(fc1_quantizer_set, fc2_quantizer_set),
49314986
mesh_resource=MeshResource(dp_resource="data", fsdp_resource="fsdp", ep_resource=self._expert_parallelism_name),
49324987
input_axes=("activation_batch", "activation_norm_length", None),
4933-
gate_kernel_axes=self.kernel_axes,
49344988
wi_kernel_axes=self.wi_kernel_axes,
49354989
wo_kernel_axes=self.wo_kernel_axes,
49364990
dtype=self.dtype,
49374991
recv_capacity_per_rank=max_utils.get_te_moe_recv_capacity_per_rank(),
49384992
quant_before_fsdp_ag=self.config.te_moe_quantize_before_fsdp_all_gather,
4993+
**router_kwargs,
49394994
**cudnn_gmm_fusion_kwargs,
4995+
**weight_layout_kwargs,
49404996
**checkpoint_kwargs,
49414997
)
4998+
if supports_router_info:
4999+
output, lb_loss, total_recv_tokens, expert_counts = te_moe_outputs
5000+
else:
5001+
output, lb_loss, total_recv_tokens = te_moe_outputs
5002+
expert_counts = None
49425003
recv_capacity_per_rank = max_utils.get_te_moe_recv_capacity_per_rank()
49435004
output = output.astype(self.dtype)
49445005
if lb_loss is not None:
49455006
lb_loss = lb_loss.astype(self.dtype)
49465007
if out_sharding is not None:
49475008
output = jax.lax.with_sharding_constraint(output, out_sharding)
4948-
return output, lb_loss, (total_recv_tokens, jnp.asarray(recv_capacity_per_rank, dtype=jnp.int32))
5009+
bias_updates = None
5010+
if self.should_update_load_balance():
5011+
bias_updates = load_balance_updates_from_counts(
5012+
expert_counts, self.num_experts, self.config.routed_bias_update_rate
5013+
)
5014+
return (
5015+
output,
5016+
lb_loss,
5017+
(
5018+
total_recv_tokens,
5019+
jnp.asarray(recv_capacity_per_rank, dtype=jnp.int32),
5020+
bias_updates,
5021+
),
5022+
)
49495023

49505024
def __call__(
49515025
self,
@@ -4980,6 +5054,16 @@ def __call__(
49805054
routing_inputs = inputs if gate_inputs is None else gate_inputs.astype(gate_dtype)
49815055

49825056
if cfg.te_moe_block:
5057+
routing_indices = None
5058+
routing_weights = None
5059+
if self.is_hash_routing:
5060+
gate_logits, pre_bias_logits = self.gate(routing_inputs)
5061+
routing_weights, routing_indices = self.get_topk(
5062+
gate_logits,
5063+
pre_bias_logits,
5064+
self.rngs,
5065+
input_ids=input_ids,
5066+
)
49835067
gate_kernel = jnp.asarray(self.gate.kernel[...], self.dtype)
49845068
wi_kernel = jnp.asarray(self.wi[...], self.dtype)
49855069
wo_kernel = jnp.asarray(self.wo[...], self.dtype)
@@ -4999,7 +5083,9 @@ def __call__(
49995083
w0_bias,
50005084
w1_bias,
50015085
wo_bias,
5002-
out_sharding,
5086+
routing_indices=routing_indices,
5087+
routing_weights=routing_weights,
5088+
out_sharding=out_sharding,
50035089
)
50045090

50055091
gate_logits, pre_bias_logits = self.gate(routing_inputs)

‎src/maxtext/models/deepseek.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,16 @@ def post_process(self, layer_output, load_balance_loss, moe_bias_updates, kv_cac
249249
self.sow(nnx.Intermediate, "moe_lb_loss", load_balance_loss)
250250

251251
if self.config.te_moe_block and moe_bias_updates is not None:
252-
total_recv_tokens, recv_capacity_per_rank = moe_bias_updates
252+
total_recv_tokens, recv_capacity_per_rank, routed_bias_updates = moe_bias_updates
253253
self.sow(nnx.Intermediate, "te_moe_total_recv_tokens", total_recv_tokens)
254254
self.sow(nnx.Intermediate, "te_moe_recv_capacity_per_rank", recv_capacity_per_rank)
255255
self.sow(
256256
nnx.Intermediate,
257257
"te_moe_capacity_overflow",
258258
jnp.any(total_recv_tokens > recv_capacity_per_rank),
259259
)
260-
elif self.config.routed_bias and self.config.routed_bias_update_rate > 0.0 and moe_bias_updates is not None:
260+
moe_bias_updates = routed_bias_updates
261+
if self.config.routed_bias and self.config.routed_bias_update_rate > 0.0 and moe_bias_updates is not None:
261262
self.sow(nnx.Intermediate, "moe_bias_updates", moe_bias_updates)
262263

263264
if getattr(self.config, "record_internal_nn_metrics", False):
@@ -600,18 +601,26 @@ def extract_fn(x):
600601
self.DeepSeekMoeBlock_0,
601602
x=intermediate_inputs,
602603
mhc_type=HyperConnectionType.MLP_MOE,
604+
input_ids=decoder_input_tokens,
603605
)
604606
load_balance_loss = metadata["load_balance_loss"]
605607
moe_bias_updates = metadata["moe_bias_updates"]
606608
else:
607-
mlp_lnx, load_balance_loss, moe_bias_updates = self.mlp_op(hidden_states, deterministic)
609+
mlp_lnx, load_balance_loss, moe_bias_updates = self.mlp_op(
610+
hidden_states,
611+
deterministic,
612+
input_ids=decoder_input_tokens,
613+
)
608614
layer_output = mlp_lnx + intermediate_inputs
609615
layer_output = self.dropout_op(layer_output, deterministic=deterministic)
610616

611617
return self.post_process(layer_output, load_balance_loss, moe_bias_updates, kv_cache)
612618

613619
def mlp_op(self, x, deterministic, *args, **kwargs):
614620
mlp_lnx, load_balance_loss, moe_bias_updates = self.DeepSeekMoeBlock_0(
615-
x, intermediate_sharding=self.mlp_intermediate_sharding, out_sharding=self.out_sharding
621+
x,
622+
intermediate_sharding=self.mlp_intermediate_sharding,
623+
out_sharding=self.out_sharding,
624+
input_ids=kwargs.get("input_ids"),
616625
)
617626
return self.with_logical_constraint(mlp_lnx), load_balance_loss, moe_bias_updates

‎tests/integration/train_tests.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,50 @@ def test_gpu_te_moe_block(self):
737737
]
738738
)
739739

740+
@pytest.mark.integration_test
741+
@pytest.mark.gpu_only
742+
def test_gpu_te_moe_block_deepseek4_hash_and_routed_bias(self):
743+
"""Runs real TE MoE kernels for DSv4 hash routing and top-k routed-bias updates."""
744+
gpu_device = jax.devices("gpu")[0]
745+
compute_capability = getattr(gpu_device, "compute_capability", None)
746+
try:
747+
if float(compute_capability) < 10.0:
748+
pytest.skip("TransformerEngine MoEBlock is only supported on sm100+!")
749+
except Exception: # pylint: disable=broad-exception-caught
750+
pytest.skip("TransformerEngine MoEBlock is only supported on sm100+!")
751+
752+
train_main(
753+
TrainTests.CONFIGS["synthetic"]
754+
+ [
755+
"model_name=deepseek4-tiny",
756+
"override_model_config=True",
757+
"base_num_decoder_layers=3",
758+
"first_num_hash_layers=1",
759+
"compress_ratios=[0,4,128]",
760+
"attention=dot_product",
761+
"quantization=te_no_quant",
762+
"base_emb_dim=32",
763+
"base_num_kv_heads=1",
764+
"head_dim=32",
765+
"num_experts=4",
766+
"num_experts_per_tok=2",
767+
"shared_experts=1",
768+
"base_moe_mlp_dim=32",
769+
"routed_bias=True",
770+
"routed_bias_update_rate=0.125",
771+
"load_balance_loss_weight=0.0",
772+
"sparse_matmul=True",
773+
"megablox=False",
774+
"prefuse_moe_weights=True",
775+
"te_moe_block=True",
776+
"te_gmm_quantization=te_no_quant",
777+
"hardware=gpu_multiprocess",
778+
"ici_fsdp_parallelism=1",
779+
"ici_expert_parallelism=-1",
780+
"enable_tensorboard=False",
781+
]
782+
)
783+
740784
@pytest.mark.integration_test
741785
@pytest.mark.tpu_only
742786
def test_tpu_dropout(self):

‎tests/unit/configs_value_test.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,11 @@ def test_te_moe_block_rejects_unsupported_options_during_config_validation(self)
181181
"override_logical_axis_rules": True,
182182
}
183183
invalid_configs = (
184-
({"norm_topk_prob": True}, "te_moe_block=True does not currently support norm_topk_prob=True."),
184+
(
185+
{"norm_topk_prob": True},
186+
"te_moe_block=True does not currently support norm_topk_prob=True "
187+
"when routed_score_func is not 'sqrtsoftplus' or 'sigmoid'.",
188+
),
185189
({"use_random_routing": True}, "te_moe_block=True does not support use_random_routing=True."),
186190
(
187191
{"decoder_block": types.DecoderBlockType.LLAMA4},

‎tests/unit/moe_checkpoint_test.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,17 @@ def test_te_moe_cudnn_gmm_fusion_default_is_omitted():
8383

8484
def test_te_moe_cudnn_gmm_fusion_can_be_disabled():
8585
assert moe._get_te_moe_cudnn_gmm_fusion_kwargs(True) == {"use_cudnn_fusion": False}
86+
87+
88+
def test_te_moe_weight_layout_is_standard_when_supported():
89+
def te_moe(*, cudnn_native_weight_layout=None):
90+
del cudnn_native_weight_layout
91+
92+
assert moe._get_te_moe_weight_layout_kwargs(te_moe) == {"cudnn_native_weight_layout": False}
93+
94+
95+
def test_te_moe_weight_layout_is_omitted_for_older_signature():
96+
def te_moe():
97+
pass
98+
99+
assert not moe._get_te_moe_weight_layout_kwargs(te_moe)

0 commit comments

Comments
 (0)