main
py 418 lines 16.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from enum import IntEnum
4 from typing import cast
5
6 import torch
7
8 import vllm.envs as envs
9 from vllm.config import get_current_vllm_config
10 from vllm.distributed import (
11 get_tensor_model_parallel_rank,
12 tensor_model_parallel_all_reduce,
13 )
14 from vllm.logger import init_logger
15 from vllm.model_executor.layers.fused_moe.moe_output import UnfinalizedMoEOutput
16 from vllm.model_executor.layers.fused_moe.runner.moe_runner import MoERunner, _unpack
17 from vllm.model_executor.layers.layernorm import RMSNorm
18 from vllm.platforms import current_platform
19 from vllm.utils.multi_stream_utils import maybe_execute_in_parallel
20 from vllm.utils.torch_utils import aux_stream
21
22 logger = init_logger(__name__)
23
24
25 class LatentTailTier(IntEnum):
26 """Which tail implementation the fused path runs, by token count.
27
28 The tiers share the same replicated up-projection weight, so the choice is
29 per batch and needs no weight relayout: tiers 0 and 2 read it as a
30 column-parallel row-shard, tier 1 reads it whole.
31 """
32
33 # ``_small_batch_tail``. Decode-sized (<= the op's max_num_tokens): one
34 # CuTeDSL collective fuses the latent reduce, RMSNorm and the shared
35 # reduce-scatter, then a sharded up-projection multicasts through a Lamport
36 # copy. Requires SM100, TP 8/16, BF16
37 TAIL_FUSION = 0
38
39 # ``_overlap_allreduce_tail``. The portable default, up to
40 # VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD tokens: reduce the latent,
41 # up-project the full hidden dim from the replicated weight, and add the
42 # separately reduced shared output, hiding that shared all-reduce behind the
43 # up-projection GEMM on the aux stream.
44 ALLREDUCE_OVERLAP = 1
45
46 # ``_shard_up_proj_tail``. Prefill-sized: each rank up-projects only its
47 # hidden shard and accumulates into the shared partial, so the shared
48 # all-reduce also stitches the routed shards. Same two all-reduces as tier 1
49 # at 1/tp of the up-projection FLOPs; it gives up tier 1's aux-stream
50 # overlap, since the reduce now has to follow the accumulate.
51 COLUMN_PARALLEL = 2
52
53
54 class LatentMoERunner(MoERunner):
55 """MoE runner for latent MoE with a replicated routed up-projection.
56
57 The fused path (tp>1, un-reduced combine output, shared expert, no SP)
58 dispatches over ``LatentTailTier`` by token count; see that enum for what
59 each tier does and when it applies.
60
61 Native path: the replicated up-proj produces the full hidden dim on every
62 rank, so the base runner combines routed + shared correctly at any TP size.
63 """
64
65 def __init__(
66 self,
67 *args,
68 **kwargs,
69 ) -> None:
70 super().__init__(*args, **kwargs)
71
72 # The tail-fusion kernels are tcgen05-based, so they require an
73 # SM100 NVIDIA device; the runner falls back to the default latent
74 # MoE path everywhere else.
75 self.enable_k3_latent_moe_tail_fusion = (
76 current_platform.is_cuda()
77 and current_platform.is_device_capability_family(100)
78 )
79 # Overlap the shared-expert all-reduce with the tier-1 up-projection.
80 self._shared_ar_events = (torch.cuda.Event(), torch.cuda.Event())
81 use_fused_path = self._use_fused_path()
82 if (
83 self.enable_k3_latent_moe_tail_fusion
84 and use_fused_path
85 and self.moe_config.tp_size not in (8, 16)
86 ):
87 logger.warning_once(
88 "K3 latent-MoE tail fusion currently supports TP=8 and TP=16, "
89 "but TP=%d is configured. Falling back to the default path.",
90 self.moe_config.tp_size,
91 )
92 self.enable_k3_latent_moe_tail_fusion = False
93
94 if self.enable_k3_latent_moe_tail_fusion and use_fused_path:
95 vllm_config = get_current_vllm_config()
96 if vllm_config.parallel_config.use_ubatching:
97 raise ValueError(
98 "K3 latent-MoE tail fusion does not support DBO or ubatching."
99 )
100 if vllm_config.model_config.enable_sleep_mode:
101 raise ValueError(
102 "K3 latent-MoE tail fusion does not support sleep mode."
103 )
104 transform = self.routed_output_transform
105 assert transform is not None
106 norm = transform.norm
107 assert norm is not None
108 from vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe import (
109 TrtLlmMxfp4ExpertsMonolithic,
110 )
111 from vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe import (
112 TrtLlmNvFp4ExpertsMonolithic,
113 )
114
115 experts_cls = getattr(self._quant_method, "experts_cls", None)
116 # The kernel instance is built after weight loading, while the tail
117 # must register its CuTeDSL warmup units during runner construction.
118 if (
119 experts_cls is TrtLlmMxfp4ExpertsMonolithic
120 or experts_cls is TrtLlmNvFp4ExpertsMonolithic
121 ) and self.moe_config.hidden_dim == self.moe_config.hidden_dim_unpadded:
122 self.moe_config.defer_moe_finalize()
123 from vllm.models.kimi_k3.nvidia.ops.latent_moe_tail import (
124 KimiK3LatentMoETailOp,
125 )
126
127 op = KimiK3LatentMoETailOp.initialize(
128 hidden_size=transform.up_proj.weight.shape[0],
129 latent_size=norm.weight.shape[0],
130 experts_per_token=(
131 self.moe_config.experts_per_token
132 if self.moe_config.use_deferred_moe_finalize
133 else 0
134 ),
135 dtype=norm.weight.dtype,
136 device=current_platform.current_device(),
137 rms_eps=norm.variance_epsilon,
138 )
139 self._k3_latent_moe_tail_op = op
140 if self.moe_config.use_deferred_moe_finalize:
141 self.moe_config.limit_deferred_moe_finalize(op.contract.max_num_tokens)
142 logger.info_once(
143 "K3 latent-MoE tail fusion with deferred top-k finalization "
144 "is enabled for up to %d tokens.",
145 op.contract.max_num_tokens,
146 )
147
148 def _get_zero_residual(
149 self,
150 hidden_states: torch.Tensor,
151 max_token_num: int,
152 ) -> torch.Tensor:
153 """Read-only zero ``residual_in`` for the fused AR+RMSNorm kernel.
154
155 flashinfer requires a residual buffer even when there is no residual to
156 add.
157 """
158 buf = getattr(self, "_zero_residual", None)
159 if buf is None:
160 buf = torch.zeros(
161 max_token_num * hidden_states.shape[-1],
162 dtype=hidden_states.dtype,
163 device=hidden_states.device,
164 )
165 self._zero_residual = buf
166
167 assert buf.dtype == hidden_states.dtype
168 assert buf.device == hidden_states.device
169 assert hidden_states.numel() <= buf.numel()
170
171 return buf[: hidden_states.numel()].view_as(hidden_states)
172
173 def _use_fused_path(self) -> bool:
174 # The fused path merges the latent and shared reductions into one
175 # all-reduce, so it needs actual TP parallelism, a shared expert (to
176 # concat), an un-reduced combine output, and no sequence parallelism.
177 return (
178 self.moe_config.tp_size > 1
179 and self._shared_experts is not None
180 and not self._fused_output_is_reduced
181 and not self.moe_config.is_sequence_parallel
182 )
183
184 def _select_tail_tier(
185 self,
186 fused_output: torch.Tensor,
187 shared_output: torch.Tensor,
188 ) -> LatentTailTier:
189 num_tokens = fused_output.shape[0]
190 # tier 0
191 if self.enable_k3_latent_moe_tail_fusion and (
192 0 < num_tokens <= self._k3_latent_moe_tail_op.contract.max_num_tokens
193 ):
194 return LatentTailTier.TAIL_FUSION
195
196 transform = self.routed_output_transform
197 assert transform is not None
198 # tier 1
199 if (
200 num_tokens <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD
201 and not envs.VLLM_DISABLE_SHARED_EXPERTS_STREAM
202 ):
203 return LatentTailTier.ALLREDUCE_OVERLAP
204 # tier 2
205 return LatentTailTier.COLUMN_PARALLEL
206
207 def _small_batch_tail(
208 self,
209 fused_output: torch.Tensor | UnfinalizedMoEOutput,
210 shared_output: torch.Tensor,
211 trunc_size: int | None,
212 ) -> torch.Tensor:
213 """Tier 0: the CuTeDSL operator fuses the whole tail."""
214 transform = self.routed_output_transform
215 assert transform is not None
216 norm = transform.norm
217 assert norm is not None
218
219 result = self._k3_latent_moe_tail_op(
220 fused_output,
221 shared_output,
222 norm.weight,
223 transform.up_proj.weight,
224 )
225 # The operator already reduced; this only strips padding.
226 return self._maybe_reduce_final_output(
227 result, trunc_size, output_is_reduced=True
228 )
229
230 def _overlap_allreduce_tail(
231 self,
232 fused_output: torch.Tensor,
233 shared_output: torch.Tensor,
234 trunc_size: int | None,
235 ) -> torch.Tensor:
236 """Tier 1: reduce the latent, up-project the full hidden dim from the
237 replicated weight, and add the separately reduced shared output.
238
239 Small enough batches hide that shared all-reduce behind the up-projection
240 GEMM on the aux stream.
241 """
242 transform = self.routed_output_transform
243 assert transform is not None
244 assert shared_output.size(0) <= envs.VLLM_SHARED_EXPERTS_STREAM_TOKEN_THRESHOLD
245 if transform.norm is not None:
246 fused_latent = self.allreduce_norm_latent_out(fused_output, transform.norm)
247 else:
248 fused_latent = tensor_model_parallel_all_reduce(fused_output)
249
250 # Overlap the shared-expert all-reduce with the up-projection GEMM while
251 # the batch is small enough for it to pay off.
252 result, shared_output = maybe_execute_in_parallel(
253 lambda: torch.mm(fused_latent, transform.up_proj.weight.t()),
254 lambda: tensor_model_parallel_all_reduce(shared_output),
255 self._shared_ar_events[0],
256 self._shared_ar_events[1],
257 aux_stream(),
258 )
259 result.add_(shared_output)
260
261 # Output is already fully reduced; this only strips padding.
262 return self._maybe_reduce_final_output(
263 result, trunc_size, output_is_reduced=True
264 )
265
266 def _shard_up_proj_tail(
267 self,
268 fused_output: torch.Tensor,
269 shared_output: torch.Tensor,
270 trunc_size: int | None,
271 ) -> torch.Tensor:
272 """Tier 2: column-parallel up-projection folded into the final reduce."""
273 transform = self.routed_output_transform
274 assert transform is not None
275
276 if transform.norm is not None:
277 latent = self.allreduce_norm_latent_out(fused_output, transform.norm)
278 else:
279 latent = tensor_model_parallel_all_reduce(fused_output)
280
281 weight = transform.up_proj.weight
282 shard_size = weight.shape[0] // self.moe_config.tp_size
283 shard_start = get_tensor_model_parallel_rank() * shard_size
284
285 # column-parallel
286 up_proj_shard = weight.narrow(0, shard_start, shard_size)
287 hidden_shard = shared_output.narrow(-1, shard_start, shard_size)
288
289 # hidden_shard += latent @ up_proj_shard.T, accumulated in the GEMM's
290 # beta-add epilogue so folding in the shared partial costs no kernel.
291 hidden_shard.addmm_(latent, up_proj_shard.t())
292
293 return self._maybe_reduce_final_output(
294 shared_output, trunc_size, output_is_reduced=False
295 )
296
297 def forward(
298 self,
299 hidden_states: torch.Tensor,
300 router_logits: torch.Tensor,
301 input_ids: torch.Tensor | None = None,
302 shared_experts_input: torch.Tensor | None = None,
303 ) -> torch.Tensor:
304 if self._use_fused_path():
305 return self._fused_forward(
306 hidden_states, router_logits, input_ids, shared_experts_input
307 )
308 return super().forward(
309 hidden_states, router_logits, input_ids, shared_experts_input
310 )
311
312 def _fused_forward(
313 self,
314 hidden_states: torch.Tensor,
315 router_logits: torch.Tensor,
316 input_ids: torch.Tensor | None = None,
317 shared_experts_input: torch.Tensor | None = None,
318 ) -> torch.Tensor:
319 # When the caller pre-applies the routed input transform outside the
320 # runner (e.g. to overlap it on a separate stream), it passes the
321 # already-transformed routed input as ``hidden_states`` and the original
322 # hidden states as ``shared_experts_input``; skip the transform then.
323 if shared_experts_input is None:
324 hidden_states, shared_experts_input = self.apply_routed_input_transform(
325 hidden_states
326 )
327 hidden_states, og_hidden_dim_pre_xform, og_hidden_dim_post_xform = (
328 self._maybe_pad_hidden_states(
329 shared_experts_input,
330 hidden_states,
331 )
332 )
333
334 if (
335 og_hidden_dim_pre_xform is None
336 and self.moe_config.should_defer_moe_finalize(hidden_states.shape[0])
337 ):
338 unfinalized = self._forward_impl(
339 hidden_states,
340 router_logits,
341 shared_experts_input,
342 input_ids,
343 )
344 shared_output, fused_output = _unpack(unfinalized)
345 assert shared_output is not None
346 assert isinstance(fused_output, UnfinalizedMoEOutput)
347 result = self._small_batch_tail(fused_output, shared_output, None)
348 return self._maybe_add_zero_expert_output(result)
349
350 result = self._forward_entry(
351 hidden_states,
352 router_logits,
353 shared_experts_input,
354 input_ids,
355 self._encode_layer_name(),
356 self.moe_config.hidden_dim_unpadded
357 if self._quant_method.has_unpadded_output
358 else 0,
359 )
360
361 shared_output, fused_output = cast(tuple[torch.Tensor, torch.Tensor], result)
362
363 if og_hidden_dim_pre_xform is not None:
364 fused_output = fused_output[..., :og_hidden_dim_pre_xform]
365
366 tier = self._select_tail_tier(fused_output, shared_output)
367 if tier is LatentTailTier.TAIL_FUSION:
368 latent_tail = self._small_batch_tail
369 elif tier is LatentTailTier.ALLREDUCE_OVERLAP:
370 latent_tail = self._overlap_allreduce_tail
371 else:
372 latent_tail = self._shard_up_proj_tail
373
374 result = latent_tail(fused_output, shared_output, og_hidden_dim_post_xform)
375
376 return self._maybe_add_zero_expert_output(result)
377
378 def allreduce_norm_latent_out(
379 self,
380 hidden_states: torch.Tensor,
381 norm: RMSNorm,
382 ) -> tuple[torch.Tensor, torch.Tensor]:
383 """All-reduce + add residual + (standard) RMSNorm, fused via flashinfer."""
384 from vllm.model_executor.layers.fused_allreduce_gemma_rms_norm import (
385 _AR_RESIDUAL_RMS_NORM,
386 _can_use_flashinfer,
387 flashinfer_trtllm_fused_allreduce_norm,
388 )
389
390 if self.moe_config.tp_size == 1:
391 return norm(hidden_states)
392
393 if flashinfer_trtllm_fused_allreduce_norm is not None:
394 ok, max_token_num = _can_use_flashinfer(
395 hidden_states, self.moe_config.tp_size
396 )
397 if ok:
398 norm_out = torch.empty_like(hidden_states)
399 # With norm_out provided, the kernel writes the new residual
400 # (all_reduce(hidden_states) + residual) into the hidden_states
401 # buffer and the normalized result into norm_out.
402 flashinfer_trtllm_fused_allreduce_norm(
403 allreduce_in=hidden_states,
404 residual=self._get_zero_residual(hidden_states, max_token_num),
405 rms_gamma=norm.weight,
406 rms_eps=norm.variance_epsilon,
407 world_size=self.moe_config.tp_size,
408 weight_bias=0.0,
409 launch_with_pdl=True,
410 fp32_acc=True,
411 max_token_num=max_token_num,
412 pattern_code=_AR_RESIDUAL_RMS_NORM,
413 norm_out=norm_out,
414 )
415 return norm_out
416
417 reduced = tensor_model_parallel_all_reduce(hidden_states)
418 return norm(reduced)