11# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
3- from typing import TYPE_CHECKING , Optional
3+ from typing import TYPE_CHECKING , Optional , cast
44
55import torch
66
7+ from megatron .core .inference .config import AsyncScheduleMode
8+
79if TYPE_CHECKING :
810 from megatron .core .inference .contexts .dynamic_context import DynamicInferenceContext
911
@@ -23,6 +25,12 @@ class RoutingMetadata:
2325 """
2426
2527 def __init__ (self , context : 'DynamicInferenceContext' , moe_router_topk : int ):
28+ """Initialize lazy routing storage.
29+
30+ Args:
31+ context (DynamicInferenceContext): Context owning the recorded token layout.
32+ moe_router_topk (int): Number of selected experts per token.
33+ """
2634 self .context = context
2735 self .max_tokens = context .max_tokens
2836 self .moe_router_topk = moe_router_topk
@@ -31,6 +39,8 @@ def __init__(self, context: 'DynamicInferenceContext', moe_router_topk: int):
3139 # Static buffer allocated lazily in _ensure_buffer_allocated().
3240 # We defer allocation because RouterReplay instances don't exist yet at init time.
3341 self .routing_indices_buffer : Optional [torch .Tensor ] = None
42+ self .buffer_index_cuda : Optional [torch .Tensor ] = None
43+ self .layer_indices_cuda : Optional [list [torch .Tensor ]] = None
3444 self .num_moe_layers : Optional [int ] = None
3545
3646 def _ensure_buffer_allocated (self ) -> None :
@@ -41,29 +51,40 @@ def _ensure_buffer_allocated(self) -> None:
4151 if self .routing_indices_buffer is not None :
4252 return
4353
44- self .num_moe_layers = len (RouterReplay .global_router_replay_instances )
54+ self .num_moe_layers = len (RouterReplay .get_instances ( is_mtp_layer = False ) )
4555
4656 if self .num_moe_layers == 0 :
4757 return
4858
49- # Static buffer for CUDA graph compatibility.
50- # Shape: [max_tokens, num_moe_layers, moe_router_topk]
51- self .routing_indices_buffer = torch .empty (
52- (self .max_tokens , self .num_moe_layers , self .moe_router_topk ),
53- dtype = torch .int32 ,
54- device = self .device ,
55- )
56-
57- def get_routing_indices (self ) -> Optional [torch .Tensor ]:
59+ shape = (self .max_tokens , self .num_moe_layers , self .moe_router_topk )
60+ if self .context .config .async_sched_mode == AsyncScheduleMode .ASYNC :
61+ shape = (2 , * shape )
62+ self .buffer_index_cuda = torch .zeros (1 , dtype = torch .int64 , device = self .device )
63+ # Separate scalars keep every compiled input aligned, unlike slices of arange.
64+ self .layer_indices_cuda = [
65+ torch .tensor ([i ], dtype = torch .int64 , device = self .device )
66+ for i in range (self .num_moe_layers )
67+ ]
68+ self .routing_indices_buffer = torch .empty (shape , dtype = torch .int32 , device = self .device )
69+
70+ def get_routing_indices (self , buffer_index : int | None = None ) -> Optional [torch .Tensor ]:
5871 """Get the recorded routing indices.
5972
60- Automatically uses the static buffer when CUDA graphs are active,
61- otherwise retrieves from RouterReplay utility.
73+ Async scheduling reads the selected bank in both eager and graphed execution.
74+ Legacy execution reads static graph storage or stacks eager router outputs.
75+
76+ Args:
77+ buffer_index (int | None): Bank written by the async base forward, or
78+ ``None`` for legacy recording.
6279
6380 Returns:
6481 Tensor of shape [num_tokens, num_moe_layers, topk] or None if not available.
6582 """
66- if self .context .using_cuda_graph_this_step ():
83+ if buffer_index is not None :
84+ if self .routing_indices_buffer is None :
85+ return None
86+ return self .routing_indices_buffer [buffer_index , : self .context .active_token_count ]
87+ elif self .context .using_cuda_graph_this_step ():
6788 # Return view of static buffer up to current token count.
6889 if self .routing_indices_buffer is None :
6990 return None
@@ -72,13 +93,13 @@ def get_routing_indices(self) -> Optional[torch.Tensor]:
7293 return self .routing_indices_buffer [: self .context .active_token_count ]
7394 else :
7495 # Get from RouterReplay and stack into [num_tokens, num_layers, topk].
75- recorded_data = RouterReplay .get_recorded_data ()
96+ recorded_data = RouterReplay .get_recorded_data (is_mtp_layer = False )
7697 if recorded_data is None or len (recorded_data ) == 0 :
7798 return None
7899 if recorded_data [0 ] is None :
79100 return None
80101 # Stack: list of [num_tokens, topk] -> [num_tokens, num_layers, topk]
81- return torch .stack (recorded_data , dim = 1 )
102+ return torch .stack (cast ( list [ torch . Tensor ], recorded_data ) , dim = 1 )
82103
83104 def enable_static_buffer_recording (self ) -> None :
84105 """Enable recording into the static buffer for CUDA graph compatibility.
@@ -89,8 +110,13 @@ def enable_static_buffer_recording(self) -> None:
89110 """
90111 self ._ensure_buffer_allocated ()
91112 if self .routing_indices_buffer is not None :
92- RouterReplay .set_global_static_buffers (self .routing_indices_buffer )
113+ RouterReplay .set_global_static_buffers (
114+ self .routing_indices_buffer ,
115+ is_mtp_layer = False ,
116+ buffer_index = self .buffer_index_cuda ,
117+ layer_indices = self .layer_indices_cuda ,
118+ )
93119
94120 def disable_static_buffer_recording (self ) -> None :
95121 """Disable static buffer recording, reverting to normal tensor assignment."""
96- RouterReplay .clear_global_static_buffers ()
122+ RouterReplay .clear_global_static_buffers (is_mtp_layer = False )
0 commit comments