Skip to content

Commit fc983eb

Browse files
committed
[MaxText] Implement MoE routing mismatch measurement
Calculate the mismatch rate between model-calculated routing and externally supplied forced_routed_experts. This is limited to Qwen3 and Qwen3.5 models in training mode. The mismatch rate is logged as 'learning/routing_mismatch_rate'.
1 parent da8b70a commit fc983eb

10 files changed

Lines changed: 820 additions & 107 deletions

File tree

src/maxtext/integration/tunix/tunix_adapter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __call__(
7070
attention_mask: Optional[Array], # [B, L, L] or None
7171
decoder_segment_ids: Optional[Array] = None,
7272
output_hidden_states: bool = False, # ignored
73+
forced_routed_experts: Optional[Array] = None,
7374
) -> Tuple[Array, None]:
7475
"""Forward compatible with Tunix Trainers default loss.
7576
Returns logits, None.
@@ -80,6 +81,7 @@ def __call__(
8081
decoder_input_tokens=input_tokens,
8182
decoder_positions=positions,
8283
decoder_segment_ids=decoder_segment_ids,
84+
forced_routed_experts=forced_routed_experts,
8385
)
8486
return logits, None
8587

src/maxtext/layers/decoders.py

Lines changed: 169 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,17 @@ def __call__(
107107
)
108108

109109
if self.model_mode == MODEL_MODE_PREFILL:
110-
logical_axis_names = ("activation_batch", "prefill_activation_length", "activation_embed")
110+
logical_axis_names = (
111+
"activation_batch",
112+
"prefill_activation_length",
113+
"activation_embed",
114+
)
111115
else:
112-
logical_axis_names = ("activation_batch", "activation_length", "activation_embed")
116+
logical_axis_names = (
117+
"activation_batch",
118+
"activation_length",
119+
"activation_embed",
120+
)
113121

114122
if model_mode == MODEL_MODE_PREFILL:
115123
inputs = _maybe_shard_with_logical(inputs, logical_axis_names)
@@ -250,7 +258,11 @@ def __call__(
250258
) -> jnp.ndarray:
251259
for lyr in range(self.num_decoder_layers):
252260
inputs = self.decoder_layer(
253-
config=self.config, mesh=self.mesh, name=f"layers_{lyr}", quant=self.quant, model_mode=model_mode
261+
config=self.config,
262+
mesh=self.mesh,
263+
name=f"layers_{lyr}",
264+
quant=self.quant,
265+
model_mode=model_mode,
254266
)(
255267
inputs,
256268
decoder_segment_ids,
@@ -308,7 +320,10 @@ def setup(self):
308320
pipeline_stage_module = self.get_pipeline_stage_module(self.decoder_layer)
309321
remat_policy = self.get_remat_policy()
310322
self.pipeline_module = pipeline.create_pipeline(
311-
config=self.config, mesh=self.mesh, layers=pipeline_stage_module, remat_policy=remat_policy
323+
config=self.config,
324+
mesh=self.mesh,
325+
layers=pipeline_stage_module,
326+
remat_policy=remat_policy,
312327
)
313328

314329
def minimal_policy(self, with_context=False, with_quantization=False):
@@ -398,7 +413,11 @@ def get_remat_policy(self):
398413
elif cfg.remat_policy == "qkv_proj_offloaded":
399414
policy = jax.checkpoint_policies.save_and_offload_only_these_names(
400415
names_which_can_be_saved=[],
401-
names_which_can_be_offloaded=["query_proj", "value_proj", "key_proj"],
416+
names_which_can_be_offloaded=[
417+
"query_proj",
418+
"value_proj",
419+
"key_proj",
420+
],
402421
offload_src="device",
403422
offload_dst="pinned_host",
404423
)
@@ -521,7 +540,10 @@ def map_fn(path, value):
521540
block_layer,
522541
prevent_cse=maxtext_utils.should_prevent_cse_in_remat(self.config),
523542
policy=policy,
524-
static_argnums=(4, 5), # Deterministic and model mode are static arguments.
543+
static_argnums=(
544+
4,
545+
5,
546+
), # Deterministic and model mode are static arguments.
525547
)
526548
RemattedBlockLayers.append(layer)
527549
return RemattedBlockLayers
@@ -551,15 +573,34 @@ def get_norm_layer(self, num_features: int):
551573
):
552574
return functools.partial(rms_norm, num_features=num_features, shard_mode=self.config.shard_mode)
553575
elif self.config.decoder_block == DecoderBlockType.GPT3:
554-
return functools.partial(gpt3.gpt3_layer_norm, num_features=num_features, reductions_in_fp32=False, use_bias=True)
555-
elif self.config.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
556576
return functools.partial(
557-
normalizations.Qwen3NextRMSNormLinen, num_features=num_features, shard_mode=self.config.shard_mode
577+
gpt3.gpt3_layer_norm,
578+
num_features=num_features,
579+
reductions_in_fp32=False,
580+
use_bias=True,
581+
)
582+
elif self.config.decoder_block in (
583+
DecoderBlockType.QWEN3_NEXT,
584+
DecoderBlockType.QWEN3_5,
585+
):
586+
return functools.partial(
587+
normalizations.Qwen3NextRMSNormLinen,
588+
num_features=num_features,
589+
shard_mode=self.config.shard_mode,
558590
)
559591
else:
560592
raise ValueError(f"Incorrect decoder_block name {self.config.decoder_block.value=}")
561593

562-
def scan_decoder_layers(self, cfg, decoder_layer, length, metadata_axis_name, mesh, in_axes_tuple, **kwargs):
594+
def scan_decoder_layers(
595+
self,
596+
cfg,
597+
decoder_layer,
598+
length,
599+
metadata_axis_name,
600+
mesh,
601+
in_axes_tuple,
602+
**kwargs,
603+
):
563604
"""scan decoder layers, calls `flax.linen.transforms.scan`"""
564605
initializing = self.is_mutable_collection("params")
565606
params_spec = cfg.param_scan_axis if initializing else ScanIn(cfg.param_scan_axis)
@@ -583,7 +624,11 @@ def scan_decoder_layers(self, cfg, decoder_layer, length, metadata_axis_name, me
583624
metadata_params={nn.PARTITION_NAME: metadata_axis_name},
584625
)
585626
return scan_fn(
586-
config=cfg, mesh=mesh, name=metadata_axis_name, quant=self.quant, **kwargs # pytype: disable=wrong-keyword-args
627+
config=cfg,
628+
mesh=mesh,
629+
name=metadata_axis_name,
630+
quant=self.quant,
631+
**kwargs, # pytype: disable=wrong-keyword-args
587632
)
588633

589634
def get_pipeline_stage_module(self, decoder_blocks):
@@ -674,7 +719,11 @@ def _apply_embedding(
674719
raise ValueError(f"Unsupported model_name for multimodal: {cfg.model_name}")
675720

676721
if video_embeddings is not None and cfg.use_multimodal:
677-
if cfg.model_name in ["qwen3-omni-30b-a3b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
722+
if cfg.model_name in [
723+
"qwen3-omni-30b-a3b",
724+
"qwen3.5-35b-a3b",
725+
"qwen3.5-397b-a17b",
726+
]:
678727
y = mm_utils.merge_mm_embeddings(
679728
text_embeddings=y,
680729
multimodal_embeddings=video_embeddings,
@@ -737,7 +786,12 @@ def apply_output_head(self, shared_embedding: nn.Module | nnx.Module, y, determi
737786
out_sharding = create_sharding(self.mesh, (None, None, "activation_vocab"))
738787
else:
739788
out_sharding = create_sharding(
740-
self.mesh, ("activation_embed_and_logits_batch", "activation_length", "activation_vocab")
789+
self.mesh,
790+
(
791+
"activation_embed_and_logits_batch",
792+
"activation_length",
793+
"activation_vocab",
794+
),
741795
)
742796

743797
# [batch, length, emb_dim] -> [batch, length, vocab_size]
@@ -794,6 +848,7 @@ def __call__(
794848
kv_caches: list[jax.Array] | None = None,
795849
attention_metadata=None,
796850
deepstack_visual_embeds: None | list[jnp.ndarray] = None,
851+
forced_routed_experts: jax.Array | None = None,
797852
):
798853
cfg = self.config
799854
mesh = self.mesh
@@ -863,7 +918,10 @@ def __call__(
863918
remaining_layers = self.config.num_decoder_layers - self.config.pipeline_parallel_layers
864919
if remaining_layers > 0:
865920
logical_axis_rules_pp_as_dp = sharding.logical_axis_rules_pp_act_as_dp(self.config.logical_axis_rules)
866-
with self.mesh, nn.partitioning.axis_rules(logical_axis_rules_pp_as_dp):
921+
with (
922+
self.mesh,
923+
nn.partitioning.axis_rules(logical_axis_rules_pp_as_dp),
924+
):
867925
y, _ = self.scan_decoder_layers(
868926
cfg,
869927
RemattedBlockLayers[0],
@@ -1013,19 +1071,42 @@ def __call__(
10131071
current_broadcast_args = list(broadcast_args)
10141072
current_in_axes_tuple = list(in_axes_tuple)
10151073

1074+
supports_forced_routing = cfg.decoder_block in (
1075+
DecoderBlockType.QWEN3_MOE,
1076+
DecoderBlockType.QWEN3_NEXT,
1077+
DecoderBlockType.QWEN3_5,
1078+
)
1079+
1080+
if supports_forced_routing and forced_routed_experts is not None:
1081+
# Transpose [B, L, N, E] -> [N, B, L, E] for scan
1082+
forced_routed_experts_t = jnp.transpose(forced_routed_experts, (2, 0, 1, 3))
1083+
cycle_interval = cfg.inhomogeneous_layer_cycle_interval
1084+
reshaped_forced = jnp.reshape(
1085+
forced_routed_experts_t,
1086+
(scan_length, cycle_interval) + forced_routed_experts_t.shape[1:],
1087+
)
1088+
else:
1089+
reshaped_forced = None
1090+
10161091
if kv_caches is not None:
10171092
# Stack kv_caches for scan: [num_layers, ...]
10181093
stacked_kv_cache = jnp.stack(kv_caches, axis=0)
10191094

10201095
# We pass (y, stacked_kv_cache, 0) as the carry
10211096
carry = (y, stacked_kv_cache, 0)
10221097

1023-
# We don't pass kv_cache as a scanned argument anymore
1024-
10251098
# Pass None for previous_chunk, slot, kv_cache to align with __call__ signature
10261099
current_broadcast_args.extend([None, None, None, attention_metadata])
10271100
current_in_axes_tuple.extend([nn.broadcast] * 4)
10281101

1102+
if supports_forced_routing and forced_routed_experts is not None:
1103+
if reshaped_forced is not None:
1104+
current_broadcast_args.append(reshaped_forced)
1105+
current_in_axes_tuple.append(0)
1106+
else:
1107+
current_broadcast_args.append(None)
1108+
current_in_axes_tuple.append(nn.broadcast)
1109+
10291110
max_logging.info(f"DEBUG: len(current_broadcast_args)={len(current_broadcast_args)}")
10301111
max_logging.info(f"DEBUG: current_broadcast_args={[type(a) for a in current_broadcast_args]}")
10311112

@@ -1047,8 +1128,19 @@ def __call__(
10471128
kv_caches[i] = returned_kv_cache[i]
10481129
else:
10491130
# Fallback to old behavior if kv_caches is None (not vLLM RPA)
1050-
current_broadcast_args.append(None)
1051-
current_in_axes_tuple.append(nn.broadcast)
1131+
if supports_forced_routing and forced_routed_experts is not None:
1132+
current_broadcast_args.extend([None, None, None, None])
1133+
current_in_axes_tuple.extend([nn.broadcast] * 4)
1134+
1135+
if reshaped_forced is not None:
1136+
current_broadcast_args.append(reshaped_forced)
1137+
current_in_axes_tuple.append(0)
1138+
else:
1139+
current_broadcast_args.append(None)
1140+
current_in_axes_tuple.append(nn.broadcast)
1141+
else:
1142+
current_broadcast_args.append(None)
1143+
current_in_axes_tuple.append(nn.broadcast)
10521144

10531145
y, _ = self.scan_decoder_layers(
10541146
cfg,
@@ -1077,6 +1169,10 @@ def __call__(
10771169
global_layer_idx = global_layer_idx_offset + index
10781170
kv_cache = kv_caches[index] if kv_caches is not None else None
10791171
input_tokens = decoder_input_tokens if cfg.engram_layers else None
1172+
extra_kwargs = {}
1173+
if layer_prefix == "moe_layers" and forced_routed_experts is not None:
1174+
extra_kwargs["forced_routed_experts"] = forced_routed_experts[:, :, index, :]
1175+
10801176
y, kv_cache = layer(
10811177
config=cfg,
10821178
mesh=mesh,
@@ -1095,6 +1191,7 @@ def __call__(
10951191
kv_cache=kv_cache,
10961192
attention_metadata=attention_metadata,
10971193
decoder_input_tokens=input_tokens,
1194+
**extra_kwargs,
10981195
)
10991196
if kv_caches is not None and kv_cache is not None:
11001197
kv_caches[index] = kv_cache
@@ -1114,6 +1211,7 @@ def __call__(
11141211
slot=slot,
11151212
)
11161213
else:
1214+
moe_lyr_idx = 0
11171215
for lyr in range(cfg.num_decoder_layers):
11181216
RemattedBlockLayer = RemattedBlockLayers[0]
11191217
layer_kwargs = {}
@@ -1133,23 +1231,60 @@ def __call__(
11331231
"is_nope_layer": llama4.determine_is_nope_layer(lyr, self.config.nope_layer_interval),
11341232
"is_moe_layer": llama4.determine_is_moe_layer(lyr, self.config.interleave_moe_layer_step),
11351233
}
1136-
if cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1234+
if cfg.decoder_block in (
1235+
DecoderBlockType.QWEN3_NEXT,
1236+
DecoderBlockType.QWEN3_5,
1237+
):
11371238
layer_kwargs = {"layer_idx": lyr}
11381239
kv_cache = None
1139-
if kv_caches is not None and cfg.decoder_block not in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1240+
if kv_caches is not None and cfg.decoder_block not in (
1241+
DecoderBlockType.QWEN3_NEXT,
1242+
DecoderBlockType.QWEN3_5,
1243+
):
11401244
kv_cache = kv_caches[lyr]
1141-
elif kv_caches is not None and cfg.decoder_block in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1245+
elif kv_caches is not None and cfg.decoder_block in (
1246+
DecoderBlockType.QWEN3_NEXT,
1247+
DecoderBlockType.QWEN3_5,
1248+
):
11421249
# For Qwen3Next & Qwen3.5, kv_caches is a dictionary of lists of caches.
11431250
if (lyr + 1) % cfg.inhomogeneous_layer_cycle_interval == 0:
1144-
kv_cache = (kv_caches["key_cache"][lyr], kv_caches["value_cache"][lyr])
1251+
kv_cache = (
1252+
kv_caches["key_cache"][lyr],
1253+
kv_caches["value_cache"][lyr],
1254+
)
11451255

11461256
if cfg.decoder_block == DecoderBlockType.GPT_OSS:
11471257
layer_kwargs = {"attention_type": gpt_oss.get_attention_type(layer_id=lyr)}
11481258
if cfg.decoder_block == DecoderBlockType.OLMO3:
11491259
layer_kwargs = {"attention_type": olmo3.get_attention_type(layer_id=lyr)}
11501260
layer = RemattedBlockLayer(
1151-
config=cfg, mesh=mesh, name=f"layers_{lyr}", quant=self.quant, model_mode=self.model_mode, **layer_kwargs
1261+
config=cfg,
1262+
mesh=mesh,
1263+
name=f"layers_{lyr}",
1264+
quant=self.quant,
1265+
model_mode=self.model_mode,
1266+
**layer_kwargs,
11521267
)
1268+
1269+
is_moe = False
1270+
if cfg.decoder_block in (
1271+
DecoderBlockType.QWEN3_MOE,
1272+
DecoderBlockType.QWEN3_NEXT,
1273+
DecoderBlockType.QWEN3_5,
1274+
):
1275+
is_moe = True
1276+
1277+
current_forced_routed_experts = None
1278+
if is_moe and forced_routed_experts is not None:
1279+
current_forced_routed_experts = forced_routed_experts[:, :, moe_lyr_idx, :]
1280+
moe_lyr_idx += 1
1281+
elif is_moe:
1282+
moe_lyr_idx += 1
1283+
1284+
extra_kwargs = {}
1285+
if is_moe and current_forced_routed_experts is not None:
1286+
extra_kwargs["forced_routed_experts"] = current_forced_routed_experts
1287+
11531288
y, returned_cache = layer(
11541289
y,
11551290
decoder_segment_ids,
@@ -1160,10 +1295,14 @@ def __call__(
11601295
slot=slot,
11611296
kv_cache=kv_cache,
11621297
attention_metadata=attention_metadata,
1298+
**extra_kwargs,
11631299
**layer_call_kwargs,
11641300
)
11651301
if kv_caches is not None and returned_cache is not None:
1166-
if cfg.decoder_block not in (DecoderBlockType.QWEN3_NEXT, DecoderBlockType.QWEN3_5):
1302+
if cfg.decoder_block not in (
1303+
DecoderBlockType.QWEN3_NEXT,
1304+
DecoderBlockType.QWEN3_5,
1305+
):
11671306
kv_caches[lyr] = returned_cache
11681307
elif (lyr + 1) % cfg.inhomogeneous_layer_cycle_interval == 0:
11691308
kv_caches["key_cache"][lyr] = returned_cache[0]
@@ -1265,7 +1404,12 @@ def _apply_gemma3_scanned_blocks(
12651404
# We name the remainder block with a 'remainder' suffix to avoid parameter name collisions
12661405
rem_layer_kwargs = {"num_of_layers": num_remaining_layers}
12671406
layer = RemattedGemma3Block(
1268-
config=cfg, mesh=mesh, quant=self.quant, model_mode=self.model_mode, name="layers_remainder", **rem_layer_kwargs
1407+
config=cfg,
1408+
mesh=mesh,
1409+
quant=self.quant,
1410+
model_mode=self.model_mode,
1411+
name="layers_remainder",
1412+
**rem_layer_kwargs,
12691413
) # pytype: disable=wrong-keyword-args
12701414
y, _ = layer(
12711415
y,

0 commit comments

Comments
 (0)