main
py 234 lines 10.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Adapted from
4 # https://github.com/sgl-project/sglang/blob/bed301a5acaa9577c9aa706468bdf242f6a43051/python/sglang/srt/layers/moe/routed_experts_capturer.py
5
6 from __future__ import annotations
7
8 import logging
9 from collections.abc import Callable
10 from functools import partial
11 from typing import Protocol, runtime_checkable
12
13 import torch
14
15 from vllm.config import VllmConfig
16 from vllm.distributed.parallel_state import get_tp_group
17 from vllm.forward_context import get_forward_context
18 from vllm.platforms import current_platform
19
20 logger = logging.getLogger(__name__)
21
22
23 @runtime_checkable
24 class RoutedExpertsCaptureSource(Protocol):
25 layer_id: int
26 capture_fn: Callable[[torch.Tensor], None] | None
27
28
29 def _get_routed_experts_shape(vllm_config: VllmConfig) -> tuple[int, int, int]:
30 model_config = vllm_config.model_config
31 num_layers = model_config.get_total_num_hidden_layers()
32 num_experts = model_config.get_num_experts()
33 num_experts_per_tok = model_config.get_num_experts_per_tok()
34 if num_layers <= 0 or num_experts <= 0 or num_experts_per_tok <= 0:
35 raise ValueError(
36 "Routed-experts capture requires positive layer, expert, and "
37 "experts-per-token counts, got "
38 f"{num_layers=}, {num_experts=}, {num_experts_per_tok=}."
39 )
40 return num_layers, num_experts, num_experts_per_tok
41
42
43 class RoutedExpertsCapturer:
44 """Worker-side capturer for routed experts, lives on GPU.
45
46 Layer-level hooks call :meth:`capture` inside the forward pass. Routing
47 rows owned by this DP rank are written into a preallocated device buffer.
48
49 The device buffer uses ``int32``. Stable snapshots use the narrowest dtype
50 that can represent every logical expert ID.
51
52 Invariants:
53 - One instance per worker; shape is fixed at init and covers the
54 worst-case step (``max_num_batched_tokens`` tokens).
55 - Every routed layer overwrites the current step's token rows.
56 """
57
58 def __init__(
59 self,
60 max_num_batched_tokens: int,
61 vllm_config: VllmConfig,
62 ) -> None:
63 num_layers, num_experts, num_experts_per_tok = _get_routed_experts_shape(
64 vllm_config
65 )
66 self.shape_per_token = (num_layers, num_experts_per_tok)
67 self.output_dtype_name = "uint8" if num_experts <= 256 else "uint16"
68 self.output_dtype = getattr(torch, self.output_dtype_name)
69 logger.info(
70 "RoutedExpertsCapturer: allocating buffer with "
71 "max_tokens=%d, num_layers=%d, num_experts_per_tok=%d "
72 "(hf_config.model_type=%s)",
73 max_num_batched_tokens,
74 num_layers,
75 num_experts_per_tok,
76 vllm_config.model_config.hf_text_config.model_type,
77 )
78 self.device_buffer = torch.zeros(
79 (max_num_batched_tokens, *self.shape_per_token),
80 dtype=torch.int32,
81 device=current_platform.device_type,
82 )
83 self.dp_rank = vllm_config.parallel_config.data_parallel_rank
84 self.tp_size = vllm_config.parallel_config.tensor_parallel_size
85
86 def capture(self, layer_id: int, topk_ids: torch.Tensor) -> None:
87 """Capture expert routing decisions for a specific layer.
88
89 Under data parallelism, ``topk_ids`` may have four different batch
90 layouts depending on where the DP combine happens and whether
91 Expert Parallelism (EP) or Sequence Parallelism (SP) is active for the
92 MoE layer:
93 - ``n == total`` (naive dispatch): all DP ranks' tokens are
94 concatenated before routing; we slice out this rank's span
95 using the cumulative per-rank counts.
96 - ``n == token_num_per_dp`` (modular-kernel path): DP combine
97 happens inside ``quant_method.apply``; ``select_experts`` only
98 ever sees this rank's tokens, so we take the whole tensor.
99 - ``n == sum(dp_metadata.local_sizes)`` (naive DP+EP dispatch):
100 sequence-parallel shards from every DP rank are gathered through
101 the flattened EP group. The shard sizes include CUDA-graph / SP
102 padding, so we use them to locate this DP rank's unpadded rows.
103 - ``n == ceil(token_num_per_dp / tp_size)`` (SP + modular-kernel
104 path): tokens were split along dim=0 across the TP group by
105 ``_sequence_parallel_context``
106 (``moe_runner_base.py:_sequence_parallel_context``), so each
107 TP rank only sees its shard. We all-gather along dim=0 to
108 reconstruct this DP rank's full routing tensor. SP pads with
109 ceil-div (see ``_compute_sp_num_tokens`` in
110 ``forward_context.py``), so the gathered tensor may contain a
111 few trailing padding rows which are trimmed by the downstream
112 ``[:token_num_per_dp]`` slice.
113
114 Args:
115 layer_id: The layer index.
116 topk_ids: Tensor of shape (batch_size, num_routed_experts).
117
118 """
119 ctx = get_forward_context()
120 if ctx.dp_metadata is None:
121 local_topk_ids = topk_ids
122 else:
123 num_tokens_dp = ctx.dp_metadata.num_tokens_across_dp_cpu
124 num_local_tokens = int(num_tokens_dp[self.dp_rank].item())
125 total = int(num_tokens_dp.sum().item())
126 n = topk_ids.shape[0]
127 shard_sizes = getattr(ctx.dp_metadata, "local_sizes", None)
128 gathered_size = sum(shard_sizes) if shard_sizes is not None else -1
129
130 if n == total:
131 end = int(num_tokens_dp[: self.dp_rank + 1].sum().item())
132 local_topk_ids = topk_ids[end - num_local_tokens : end]
133 elif n == num_local_tokens:
134 local_topk_ids = topk_ids
135 elif shard_sizes is not None and n == gathered_size:
136 # Naive DP+EP dispatch gathers the sequence-parallel shards in
137 # flattened DP-then-TP order. ``local_sizes`` is the exact
138 # all-gatherv layout, including padding. Select this DP rank's
139 # contiguous shard group, then trim its trailing padding.
140 num_dp_ranks = len(num_tokens_dp)
141 assert len(shard_sizes) % num_dp_ranks == 0
142 shards_per_dp_rank = len(shard_sizes) // num_dp_ranks
143 first_shard = self.dp_rank * shards_per_dp_rank
144 start = sum(shard_sizes[:first_shard])
145 local_topk_ids = topk_ids[start : start + num_local_tokens]
146 elif (
147 self.tp_size > 1
148 and n == (num_local_tokens + self.tp_size - 1) // self.tp_size
149 ):
150 # SP + modular-kernel path. All-gather across the TP
151 # group along dim=0 to reconstruct the full per-DP-rank
152 # tensor; keep only the first ``token_num_per_dp`` rows
153 # (trailing rows are SP ceil-div padding). The TP group
154 # is always initialized on real rollout workers, and
155 # every rank in the group reaches this branch in
156 # lockstep (bind is per-FusedMoEFactory layer, SP is a global
157 # condition), so a bare all_gather here will not
158 # deadlock -- let it raise if the precondition is
159 # violated rather than skip silently.
160 #
161 # ``topk_ids`` is already whatever the router produced
162 # (typically int32/int64, both supported by NCCL); the
163 # downstream buffer assignment narrows to the capture dtype.
164 local_topk_ids = get_tp_group().all_gather(topk_ids, dim=0)[
165 :num_local_tokens
166 ]
167 else:
168 sp_expected = (num_local_tokens + self.tp_size - 1) // self.tp_size
169 raise AssertionError(
170 "RoutedExpertsCapturer: unexpected topk_ids batch "
171 f"dim {n} (expected {total}, {num_local_tokens}, "
172 f"{gathered_size}, or {sp_expected} for "
173 f"dp_rank={self.dp_rank}, tp_size={self.tp_size})"
174 )
175
176 if not 0 <= layer_id < self.device_buffer.shape[1]:
177 raise IndexError(
178 f"routed-experts layer {layer_id} exceeds capture buffer "
179 f"layer count {self.device_buffer.shape[1]}"
180 )
181
182 self.device_buffer[: len(local_topk_ids), layer_id] = local_topk_ids
183
184 def snapshot_routing_data(self, num_tokens: int) -> torch.Tensor:
185 """Return a stable snapshot of the current routing data."""
186 return self.device_buffer[:num_tokens].to(self.output_dtype)
187
188
189 def bind_routed_experts_capturer(
190 model: torch.nn.Module,
191 capturer: RoutedExpertsCapturer,
192 ) -> None:
193 """Attach capture callbacks to the target model's MoE routers."""
194 from vllm.model_executor.layers.fused_moe.layer import MoERunner
195 from vllm.model_executor.layers.fused_moe.modular_kernel import (
196 FusedMoEExpertsMonolithic,
197 )
198 from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter
199
200 num_bound = 0
201 for module in model.modules():
202 if isinstance(module, RoutedExpertsCaptureSource):
203 module.capture_fn = partial(capturer.capture, module.layer_id)
204 num_bound += 1
205 continue
206 if not isinstance(module, MoERunner):
207 continue
208 capture_fn = partial(capturer.capture, module.layer_id)
209 quant_method = module._quant_method
210 if quant_method.is_monolithic:
211 moe_kernel = getattr(quant_method, "moe_kernel", None)
212 impl = getattr(moe_kernel, "impl", None)
213 fused_experts = getattr(impl, "fused_experts", None)
214 if not (
215 isinstance(fused_experts, FusedMoEExpertsMonolithic)
216 and fused_experts.supports_routing_replay_capture()
217 ):
218 raise ValueError(
219 "Routed-experts capture is not supported with monolithic "
220 f"MoE kernel {type(fused_experts).__name__}."
221 )
222 fused_experts.set_capture_fn(capture_fn)
223 num_bound += 1
224 elif isinstance(module.router, BaseRouter):
225 module.router.set_capture_fn(capture_fn)
226 num_bound += 1
227 else:
228 raise ValueError(
229 "Routed-experts capture is not supported with router "
230 f"{type(module.router).__name__}."
231 )
232
233 if num_bound == 0:
234 raise ValueError("No supported MoE router found for routed-experts capture.")