@@ -259,18 +259,83 @@ def _install_pg_collection_on_mappings(
259259 mapping .set_process_groups_from_pg_collection (pg_collection )
260260
261261
262+ def _resolve_num_local_experts (config : TransformerConfig , param_name : str , ep_size : int ) -> int :
263+ """Resolve the per-EP-rank expert count for an expert parameter.
264+
265+ Expert numbering during conversion must match how the model was built, so it
266+ is driven entirely by the model config — never by traversing an instantiated
267+ module. This keeps name mapping independent of runtime layout (PP/VPP, MTP,
268+ custom MoE wrappers) and of whether a live model is available.
269+
270+ The expert layout is an explicit config contract:
271+
272+ * Homogeneous models leave ``num_moe_experts_per_layer`` unset and every MoE
273+ layer uses the scalar ``config.num_moe_experts``.
274+ * Heterogeneous models set ``config.num_moe_experts_per_layer`` to a list of
275+ global expert counts indexed by global decoder-layer number (``0`` for
276+ dense layers).
277+
278+ Args:
279+ config: The Megatron model config (authoritative expert layout).
280+ param_name: Expert parameter name, already in *global* layer numbering
281+ (the PP branch of :func:`_megatron_local_name_to_global` runs first).
282+ ep_size: Expert-parallel group size.
283+
284+ Returns:
285+ The number of experts owned by each expert-parallel rank for the layer
286+ that ``param_name`` belongs to.
287+
288+ Raises:
289+ ValueError: If the configured layout is missing or inconsistent with the
290+ parameter being renumbered (unset scalar, per-layer list too short,
291+ an expert parameter on a layer declared dense, or a global count not
292+ divisible by ``ep_size``).
293+ """
294+ per_layer = getattr (config , "num_moe_experts_per_layer" , None )
295+
296+ # Multi-token-prediction layers (``mtp.layers.N``) are not decoder layers and
297+ # are not covered by the per-decoder-layer contract; match only decoder
298+ # layers so MTP experts fall through to the scalar count.
299+ layer_match = re .search (r"(?:^|\.)decoder\.layers\.(\d+)(?=\.)" , param_name )
300+
301+ if per_layer is not None and layer_match is not None :
302+ global_layer_idx = int (layer_match .group (1 ))
303+ if global_layer_idx >= len (per_layer ):
304+ raise ValueError (
305+ f"num_moe_experts_per_layer has length { len (per_layer )} but expert parameter "
306+ f"{ param_name !r} resolves to global layer { global_layer_idx } ; the list must "
307+ "cover every decoder layer (length == num_layers)."
308+ )
309+ num_global_experts = per_layer [global_layer_idx ]
310+ if not num_global_experts :
311+ raise ValueError (
312+ f"num_moe_experts_per_layer[{ global_layer_idx } ] == { num_global_experts } , but "
313+ f"expert parameter { param_name !r} exists on that layer; layers declared dense "
314+ "must not carry expert parameters."
315+ )
316+ else :
317+ num_global_experts = config .num_moe_experts
318+
319+ if num_global_experts is None :
320+ raise ValueError (
321+ f"Cannot renumber expert parameter { param_name !r} : num_moe_experts is None and no "
322+ "per-layer expert layout (num_moe_experts_per_layer) is configured."
323+ )
324+ if num_global_experts % ep_size != 0 :
325+ raise ValueError (
326+ f"Global expert count { num_global_experts } for { param_name !r} is not divisible by "
327+ f"expert-parallel size { ep_size } ."
328+ )
329+ return num_global_experts // ep_size
330+
331+
262332def _megatron_local_name_to_global (
263333 models : MegatronModule | List [MegatronModule ],
264334 config : TransformerConfig ,
265335 param_name : str ,
266336 vp_stage : Optional [int ] = None ,
267337) -> str :
268338 """Adjust layer number and expert number from local to global numbering."""
269- # ``layer_module`` is resolved lazily and shared between the PP and EP
270- # branches. It stays ``None`` on the EP-only path (PP=1, EP>1) until the EP
271- # branch resolves it, so it must never be read before being assigned.
272- layer_module = None
273-
274339 # PP
275340 pp_group = _get_pp_group (models )
276341 if "layers." in param_name and get_pg_size (pp_group ) > 1 :
@@ -295,25 +360,13 @@ def _megatron_local_name_to_global(
295360 is_expert_param = (is_grouped_expert_param or is_local_expert_param ) and ".adapter." not in param_name
296361 ep_group = _get_ep_group (models ) if is_expert_param else None
297362 if is_expert_param and ep_group is not None and get_pg_size (ep_group ) > 1 :
298- # Resolve the layer module independently of the PP branch above: on the
299- # EP-only path (PP=1, EP>1) that branch never runs, so ``layer_module``
300- # is still ``None`` here. Prefer the per-layer module count
301- # (heterogeneous-safe); fall back to the config-derived count when the
302- # model isn't available (e.g. name-only conversions in tests).
303- if layer_module is None :
304- layer_match = re .match (r"^(.+?\.layers\.\d+)" , param_name )
305- if models is not None and layer_match is not None :
306- try :
307- _ , layer_module = get_module_and_param_from_name (
308- models = models , param_name = layer_match .group (1 ), vp_stage = vp_stage
309- )
310- except (ValueError , AttributeError ):
311- layer_module = None
312-
313- if isinstance (layer_module , MegatronModule ):
314- num_experts_per_rank = layer_module .mlp .num_local_experts # per-layer, heterogeneous-safe
315- else :
316- num_experts_per_rank = config .num_moe_experts // get_pg_size (ep_group )
363+ # Expert numbering follows the explicit config contract (scalar
364+ # ``num_moe_experts`` for homogeneous models, ``num_moe_experts_per_layer``
365+ # for heterogeneous ones). ``param_name`` is already in global layer
366+ # numbering because the PP branch above runs first. This is intentionally
367+ # independent of the instantiated module so conversion does not depend on
368+ # runtime layout (PP/VPP, MTP, custom MoE wrappers) or on a live model.
369+ num_experts_per_rank = _resolve_num_local_experts (config , param_name , get_pg_size (ep_group ))
317370
318371 def _update_grouped_expert_number (param_name : str , param_type : str ) -> str :
319372 """Update expert number from local to global for weight or bias parameters."""
0 commit comments