main
py 152 lines 5.33 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from typing import cast
4
5 import torch
6
7 from vllm.distributed import (
8 get_tensor_model_parallel_rank,
9 get_tensor_model_parallel_world_size,
10 tensor_model_parallel_all_reduce,
11 )
12 from vllm.logger import init_logger
13 from vllm.model_executor.layers.fused_moe.runner.moe_runner import MoERunner
14
15 logger = init_logger(__name__)
16
17
18 class ROCmLatentMoERunner(MoERunner):
19 """MoE runner for latent MoE with a replicated routed up-projection.
20
21 Mirrors CUDA's LatentMoERunner, but currently only the up projection
22 -sharded path is implemented. (Tier 2)
23
24 Native path: the replicated up-proj produces the full hidden dim on every
25 rank, so the base runner combines routed + shared correctly at any TP size.
26 """
27
28 def __init__(
29 self,
30 *args,
31 **kwargs,
32 ) -> None:
33 super().__init__(*args, **kwargs)
34
35 transform = self.routed_output_transform
36 up_proj = getattr(transform, "up_proj", None)
37 tp_size = get_tensor_model_parallel_world_size()
38
39 self._up_proj_shard_size = 0
40 self._tail_shardable = (
41 up_proj is not None
42 and tp_size > 1
43 and up_proj.weight.shape[0] % tp_size == 0
44 and self._shared_experts is not None
45 and not self.moe_config.is_sequence_parallel
46 and self.routed_scaling_factor == 1.0
47 )
48 if self._tail_shardable:
49 assert up_proj is not None
50 self._up_proj_shard_size = up_proj.weight.shape[0] // tp_size
51 else:
52 logger.warning_once(
53 "K3 latent-MoE tail is not shardable under this config, "
54 "falling back to the replicated up-projection.",
55 scope="global",
56 )
57 self._logged_sharded_tail = False
58
59 def _shard_up_proj_tail(
60 self,
61 fused_output: torch.Tensor,
62 shared_output: torch.Tensor,
63 trunc_size: int | None,
64 ) -> torch.Tensor:
65 """Tier 2: column-parallel up-projection folded into the final reduce."""
66 if not self._logged_sharded_tail:
67 self._logged_sharded_tail = True
68 logger.info_once(
69 "Kimi-K3 latent-MoE tail: up-projecting only this rank's "
70 "hidden shard into the shared output.",
71 scope="global",
72 )
73
74 transform = self.routed_output_transform
75 assert transform is not None
76
77 latent = tensor_model_parallel_all_reduce(fused_output)
78 if transform.norm is not None:
79 latent = transform.norm(latent)
80
81 shard_size = self._up_proj_shard_size
82 shard_start = get_tensor_model_parallel_rank() * shard_size
83 up_proj_shard = transform.up_proj.weight.narrow(0, shard_start, shard_size)
84 hidden_shard = shared_output.narrow(-1, shard_start, shard_size)
85
86 # hidden_shard += latent @ up_proj_shard.T, accumulated in the GEMM's
87 # beta-add epilogue so folding in the shared partial costs no kernel.
88 hidden_shard.addmm_(latent, up_proj_shard.t())
89
90 return self._maybe_reduce_final_output(
91 shared_output, trunc_size, output_is_reduced=False
92 )
93
94 def forward(
95 self,
96 hidden_states: torch.Tensor,
97 router_logits: torch.Tensor,
98 input_ids: torch.Tensor | None = None,
99 shared_experts_input: torch.Tensor | None = None,
100 ) -> torch.Tensor:
101 if self._tail_shardable and not self._fused_output_is_reduced:
102 return self._fused_forward(
103 hidden_states, router_logits, input_ids, shared_experts_input
104 )
105 return super().forward(
106 hidden_states, router_logits, input_ids, shared_experts_input
107 )
108
109 def _fused_forward(
110 self,
111 hidden_states: torch.Tensor,
112 router_logits: torch.Tensor,
113 input_ids: torch.Tensor | None,
114 shared_experts_input: torch.Tensor | None,
115 ) -> torch.Tensor:
116 # When the caller pre-applies the routed input transform outside the
117 # runner (e.g. to overlap it on a separate stream), it passes the
118 # already-transformed routed input as ``hidden_states`` and the original
119 # hidden states as ``shared_experts_input``; skip the transform then.
120 if shared_experts_input is None:
121 hidden_states, shared_experts_input = self.apply_routed_input_transform(
122 hidden_states
123 )
124
125 hidden_states, og_hidden_dim_pre_xform, og_hidden_dim_post_xform = (
126 self._maybe_pad_hidden_states(
127 shared_experts_input,
128 hidden_states,
129 )
130 )
131
132 result = self._forward_entry(
133 hidden_states,
134 router_logits,
135 shared_experts_input,
136 input_ids,
137 self._encode_layer_name(),
138 self.moe_config.hidden_dim_unpadded
139 if self._quant_method.has_unpadded_output
140 else 0,
141 )
142
143 shared_output, fused_output = cast(tuple[torch.Tensor, torch.Tensor], result)
144
145 if og_hidden_dim_pre_xform is not None:
146 fused_output = fused_output[..., :og_hidden_dim_pre_xform]
147
148 result = self._shard_up_proj_tail(
149 fused_output, shared_output, og_hidden_dim_post_xform
150 )
151
152 return self._maybe_add_zero_expert_output(result)