Files
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

216 lines
10 KiB
Python

"""
Router Replay Utilities
Utilities for handling router replay functionality in Megatron models.
"""
import torch
from mcore_bridge import split_cp_inputs
from megatron.core import mpu
from megatron.core.tensor_parallel import scatter_to_sequence_parallel_region
from megatron.core.transformer.transformer_block import get_num_layers_to_build
from megatron.core.transformer.transformer_layer import get_transformer_layer_offset
from swift.utils import get_logger
from swift.utils.torch_utils import get_current_device
logger = get_logger()
try:
from megatron.core.transformer.moe.router_replay import RouterReplay, RouterReplayAction
from megatron.core.transformer.moe.token_dispatcher import MoEAlltoAllTokenDispatcher
ROUTER_REPLAY_AVAILABLE = True
except ImportError:
logger.warning('RouterReplay not available in current megatron-core version')
RouterReplay = None
RouterReplayAction = None
MoEAlltoAllTokenDispatcher = None
ROUTER_REPLAY_AVAILABLE = False
device_name = get_current_device()
def is_moe_layer(tf_config, layer_idx):
moe_layer_freq = getattr(tf_config, 'moe_layer_freq', None)
if isinstance(moe_layer_freq, int):
return layer_idx % moe_layer_freq == 0
elif isinstance(moe_layer_freq, list):
return moe_layer_freq[layer_idx] == 1
else:
raise ValueError(f'Unsupported moe_layer_freq type: {type(moe_layer_freq)}')
def get_moe_num_layers_to_build(tf_config, vp_stage=None, pp_rank=None):
"""Count the number of MoE layers assigned to the current rank.
When ``moe_layer_freq`` is 1 or unset, every transformer layer is an MoE
layer, so the count equals the total layer count. Otherwise only layers
whose global index satisfies the frequency predicate are counted.
Args:
config: Megatron TransformerConfig providing layer layout information.
vp_stage: Virtual-pipeline stage index (None defaults to current).
pp_rank: Pipeline-parallel rank (None defaults to current).
Returns:
Number of MoE layers on the specified rank/stage.
"""
total_layers = get_num_layers_to_build(tf_config, vp_stage=vp_stage, pp_rank=pp_rank)
layer_offset = get_transformer_layer_offset(tf_config, vp_stage=vp_stage)
local_global_indices = range(layer_offset, layer_offset + total_layers)
num_moe_layers = sum(1 for idx in local_global_indices if is_moe_layer(tf_config, idx))
return num_moe_layers
def get_local_layer_range(tf_config, vp_rank=None, only_moe_layer=True):
vp_size = tf_config.virtual_pipeline_model_parallel_size
if vp_size is not None:
vp_rank = 0 if vp_rank is None else vp_rank
offset = 0
for pre_vp_stage in range(vp_size):
if pre_vp_stage == vp_rank:
break
num_layers_to_build = get_moe_num_layers_to_build(
tf_config, pre_vp_stage) if only_moe_layer else get_num_layers_to_build(tf_config, pre_vp_stage)
offset += num_layers_to_build
else:
offset = 0
count = get_moe_num_layers_to_build(tf_config, vp_rank) if only_moe_layer else get_num_layers_to_build(
tf_config, vp_rank)
return offset, count
def get_local_topk_idx_for_current_rank(global_topk_idx, tf_config, packed_seq_params=None):
if global_topk_idx is None:
return None
# 1. pp slice
# For the hybrid model, global_topk_idx contains data from all layers
# because vLLM reports routed_experts across all transformer layers(including dense).
# However megatron only has routers for MoE layers.
# So local_topk_idx should filter only data from the MoE layer.
layer_offset = get_transformer_layer_offset(tf_config, vp_stage=0)
offset, count = get_local_layer_range(
tf_config, tf_config.virtual_pipeline_model_parallel_size, only_moe_layer=False)
num_layers = offset + count
moe_layer_idx = torch.tensor([
layer_idx for layer_idx in range(layer_offset, layer_offset + num_layers) if is_moe_layer(tf_config, layer_idx)
],
dtype=torch.long,
device=global_topk_idx.device)
local_topk_idx = torch.index_select(global_topk_idx, dim=2, index=moe_layer_idx)
# 2. cp slice
cp_size = mpu.get_context_parallel_world_size()
if cp_size > 1:
local_topk_idx = split_cp_inputs(local_topk_idx, getattr(packed_seq_params, 'cu_seqlens_q', None), 1)
# 3. sp slice
local_topk_idx = scatter_to_sequence_parallel_region(local_topk_idx.transpose(0, 1)).transpose(0, 1)
return local_topk_idx
def get_router_replay_data(tf_config, vp_rank=None):
router_instances_list = RouterReplayHelper.get_micro_batch_router_list(tf_config, vp_rank)
layers_topk_idx = []
for router in router_instances_list:
layers_topk_idx.append(router.recorded_topk_idx.to(torch.uint8))
# layer_num, seq_len, topk -> 1, seq_len, layer_num, topk
layers_topk_idx = torch.stack(layers_topk_idx).transpose(0, 1).unsqueeze(0).to(device_name)
return layers_topk_idx
def set_router_replay_data(layers_topk_idx, tf_config, vp_rank=None):
# bs, seq_len, layer_num, topk -> layer_num, total_seq_len, topk
layers_topk_idx_reshape = layers_topk_idx.flatten(0, 1).transpose(0, 1).to(device_name)
offset, count = get_local_layer_range(tf_config, vp_rank)
router_instances_list = RouterReplay.global_router_replay_instances[offset:offset + count]
for i, router in enumerate(router_instances_list):
router.set_target_indices(layers_topk_idx_reshape[i + offset].to(torch.int64))
class RouterReplayHelper:
"""Helper class to query router replay state and locate local RouterReplay instances."""
@staticmethod
def get_micro_batch_router_list(tf_config, vp_rank=None):
"""
Return the list of RouterReplay instances corresponding to the current micro-batch and local
(pp_rank, vp_stage) layer range.
When virtual pipeline (VPP) is enabled, the local range for the PP rank is expanded to include
all VP stages by multiplying the per-VP count by vp_size. The returned slice is taken from the
global RouterReplay.global_router_replay_instances list.
Args:
tf_config: Configuration object used to compute layer assignments.
vp_rank (Optional[int]): Explicit virtual pipeline stage to query. If None, the current VP
rank from Megatron parallel state is used when available.
Returns:
list: A contiguous sublist of RouterReplay.router_instances for the local layer range.
"""
offset, count = get_local_layer_range(tf_config, vp_rank)
router_instances_list = RouterReplay.global_router_replay_instances[offset:offset + count]
return router_instances_list
@staticmethod
def is_r2_record_action(tf_config, vp_rank=None) -> bool:
"""Return True if the current router_replay_action is RECORD (R2) for the local router instances.
This inspects the first local RouterReplay instance's router_replay_action and compares it to
RouterReplayAction.RECORD.
"""
router_instances_list = RouterReplayHelper.get_micro_batch_router_list(tf_config, vp_rank)
return (router_instances_list and router_instances_list[0].router_replay_action == RouterReplayAction.RECORD)
@staticmethod
def is_replay_forward_action(tf_config, vp_rank=None) -> bool:
"""Return True if the current router_replay_action is REPLAY_FORWARD for the local router instances.
This inspects the first local RouterReplay instance's router_replay_action and compares it to
RouterReplayAction.REPLAY_FORWARD.
"""
router_instances_list = RouterReplayHelper.get_micro_batch_router_list(tf_config, vp_rank)
return (router_instances_list
and router_instances_list[0].router_replay_action == RouterReplayAction.REPLAY_FORWARD)
@staticmethod
def is_replay_backward_action(tf_config, vp_rank=None) -> bool:
"""Return True if the current router_replay_action is REPLAY_BACKWARD for the local router instances.
This inspects the first local RouterReplay instance's router_replay_action and compares it to
RouterReplayAction.REPLAY_BACKWARD.
"""
router_instances_list = RouterReplayHelper.get_micro_batch_router_list(tf_config, vp_rank)
return (router_instances_list
and router_instances_list[0].router_replay_action == RouterReplayAction.REPLAY_BACKWARD)
def apply_router_replay_patch():
"""
Applies the monkey patch for MoE Router Replay functionality.
"""
logger.info('Applying Router Replay Patch...')
assert ROUTER_REPLAY_AVAILABLE, \
'The routing replay is not supported. Please upgrade megatron-core to 0.16.0 or higher'
# Patch MoEAlltoAllTokenDispatcher.preprocess to handle router replay
# When router replay is enabled, duplicate indices in top_indices can cause
# routing_map.sum() < num_tokens * topk, leading to split size mismatch in alltoall.
if MoEAlltoAllTokenDispatcher is not None and not hasattr(MoEAlltoAllTokenDispatcher, '_preprocess_patched'):
original_preprocess = MoEAlltoAllTokenDispatcher.preprocess
def patched_preprocess(self, routing_map):
"""Patched preprocess that handles router replay correctly for alltoall dispatcher."""
# Call original preprocess
result = original_preprocess(self, routing_map)
# Fix num_out_tokens when router replay is enabled
if (getattr(self.config, 'moe_enable_routing_replay', False) and not self.drop_and_pad
and self.config.moe_expert_capacity_factor is None
and not (getattr(self.config, 'moe_router_padding_for_quantization', None)
or getattr(self.config, 'moe_router_padding_for_fp8', None))):
# With router replay, duplicate indices can reduce the actual routed
# token count, so derive it from the routing map instead.
self.num_out_tokens = int(routing_map.sum().item())
return result
MoEAlltoAllTokenDispatcher.preprocess = patched_preprocess
MoEAlltoAllTokenDispatcher._preprocess_patched = True