@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 1317034379
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[ROCm][FEAT] Fuse DeepSeek shared experts into AITER fused_moe ops (#24097)

Signed-off-by: chenjun <junchen2@amd.com> Signed-off-by: kliuae <kuanfu.liu@embeddedllm.com> Co-authored-by: valarLip <103567126+valarLip@users.noreply.github.com> Co-authored-by: TJian <tunjian.tan@embeddedllm.com>

kliuae committed Oct 16, 2025 at 10:41 UTC 1317034379522f75b5876ff5b5136ef312c32b37
8 files changed +352 -88
tests/distributed/test_expert_placement.py
+3 -3
@@ -85,7 +85,7 @@ def test_expert_placement_various_sizes(expert_placement_strategy, world_size):
85 else:
86 expected_test_local = base_experts
87
88 - test_local_experts, test_expert_map = determine_expert_map(
88 + test_local_experts, test_expert_map, _ = determine_expert_map(
89 ep_size=test_ep_size,
90 ep_rank=ep_rank,
91 global_num_experts=test_global_experts,
@@ -116,7 +116,7 @@ def test_expert_placement_edge_cases(expert_placement_strategy, world_size):
116 """Test edge cases for round_robin expert placement."""
117
118 # Test case 1: ep_size = 1 (should return None for expert_map)
119 - local_num_experts, expert_map = determine_expert_map(
119 + local_num_experts, expert_map, _ = determine_expert_map(
120 ep_size=1,
121 ep_rank=0,
122 global_num_experts=8,
@@ -217,7 +217,7 @@ def test_determine_expert_map_comprehensive():
217 expected_local,
218 expected_map_pattern,
219 ) in test_cases:
220 - local_num_experts, expert_map = determine_expert_map(
220 + local_num_experts, expert_map, _ = determine_expert_map(
221 ep_size=ep_size,
222 ep_rank=ep_rank,
223 global_num_experts=global_num_experts,
tests/kernels/moe/test_moe_permute_unpermute.py
+1 -1
@@ -217,7 +217,7 @@ def test_moe_permute_unpermute(
217 expert_map = None
218 n_local_expert = n_expert
219 if ep_size != 1:
220 - n_local_expert, expert_map = determine_expert_map(ep_size, ep_rank, n_expert)
220 + n_local_expert, expert_map, _ = determine_expert_map(ep_size, ep_rank, n_expert)
221 expert_map = expert_map.cuda()
222 start_expert = n_local_expert * ep_rank
223 current_platform.seed_everything(0)
vllm/envs.py
+7
@@ -113,6 +113,7 @@ if TYPE_CHECKING:
113 VLLM_ROCM_USE_TRITON_ROPE: bool = False
114 VLLM_ROCM_USE_AITER_FP8BMM: bool = True
115 VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION: bool = False
116 + VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: bool = True
117 VLLM_ROCM_USE_SKINNY_GEMM: bool = True
118 VLLM_ROCM_FP8_PADDING: bool = True
119 VLLM_ROCM_MOE_PADDING: bool = True
@@ -914,6 +915,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
915 os.getenv("VLLM_ROCM_USE_AITER_UNIFIED_ATTENTION", "False").lower()
916 in ("true", "1")
917 ),
918 + # Whether to use aiter fusion shared experts ops.
919 + # By default is enabled.
920 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS": lambda: (
921 + os.getenv("VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS", "True").lower()
922 + in ("true", "1")
923 + ),
924 # use rocm skinny gemms
925 "VLLM_ROCM_USE_SKINNY_GEMM": lambda: (
926 os.getenv("VLLM_ROCM_USE_SKINNY_GEMM", "True").lower() in ("true", "1")
vllm/model_executor/layers/fused_moe/layer.py
+105 -11
@@ -5,6 +5,7 @@ from abc import abstractmethod
5 from collections.abc import Callable, Iterable
6 from contextlib import nullcontext
7 from enum import Enum
8 +from functools import partial
9 from typing import Literal, get_args, overload
10
11 import torch
@@ -12,7 +13,7 @@ import torch.nn.functional as F
13 from torch.nn.parameter import UninitializedParameter
14
15 import vllm.envs as envs
15 -from vllm.config import get_current_vllm_config
16 +from vllm.config import VllmConfig, get_current_vllm_config
17 from vllm.config.parallel import ExpertPlacementStrategy
18 from vllm.distributed import (
19 get_dp_group,
@@ -39,6 +40,8 @@ from vllm.model_executor.layers.fused_moe.modular_kernel import (
40 FusedMoEPrepareAndFinalize,
41 )
42 from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
43 + init_aiter_topK_meta_data,
44 + is_rocm_aiter_fusion_shared_expert_enabled,
45 is_rocm_aiter_moe_enabled,
46 )
47 from vllm.model_executor.layers.fused_moe.routing_simulator import RoutingSimulator
@@ -87,7 +90,7 @@ else:
90
91 if is_rocm_aiter_moe_enabled():
92 from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import ( # noqa: E501
90 - rocm_aiter_grouped_topk as grouped_topk,
93 + rocm_aiter_grouped_topk as grouped_topk_aiter,
94 )
95 else:
96 from vllm.model_executor.layers.fused_moe.fused_moe import grouped_topk
@@ -634,6 +637,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
637 global_num_experts=global_num_experts,
638 zero_expert_num=zero_expert_num,
639 zero_expert_type=zero_expert_type,
640 + num_fused_shared_experts=layer.num_fused_shared_experts,
641 )
642
643 if self.rocm_aiter_moe_enabled:
@@ -860,7 +864,8 @@ def determine_expert_map(
864 ep_rank: int,
865 global_num_experts: int,
866 expert_placement_strategy: ExpertPlacementStrategy = "linear",
863 -) -> tuple[int, torch.Tensor | None]:
867 + num_fused_shared_experts: int = 0,
868 +) -> tuple[int, torch.Tensor | None, torch.Tensor | None]:
869 """
870 Calculates how many experts should be assigned to each rank for EP and
871 creates a mapping from global to local expert index. Experts are
@@ -882,10 +887,16 @@ def determine_expert_map(
887 (global_num_experts,) mapping from global to local index.
888 Contains -1 for experts not assigned to the current rank.
889 Returns None if ep_size is 1.
890 + - expert_mask (Optional[torch.Tensor]): A tensor of shape
891 + (global_num_experts + num_fused_shared_experts + 1,)
892 + containing 1 for experts assigned to the current rank
893 + and 0 for sentinel.
894 + Returns None if ep_size is 1.
895 + Used only when AITER MOE is enabled.
896 """
897 assert ep_size > 0
898 if ep_size == 1:
888 - return (global_num_experts, None)
899 + return (global_num_experts, None, None)
900
901 # Distribute experts as evenly as possible to each rank.
902 base_experts = global_num_experts // ep_size
@@ -914,7 +925,26 @@ def determine_expert_map(
925 f"'{expert_placement_strategy}', expected one of "
926 f"{get_args(ExpertPlacementStrategy)}"
927 )
917 - return (local_num_experts, expert_map)
928 +
929 + expert_mask = None
930 + if is_rocm_aiter_moe_enabled():
931 + expert_mask = torch.ones(
932 + (global_num_experts + num_fused_shared_experts + 1,), dtype=torch.int32
933 + )
934 + expert_mask[-1] = 0
935 + expert_mask[:global_num_experts] = expert_map > -1
936 + expert_map = torch.cat(
937 + (
938 + expert_map,
939 + torch.tensor(
940 + [local_num_experts + i for i in range(num_fused_shared_experts)],
941 + dtype=torch.int32,
942 + ),
943 + ),
944 + dim=0,
945 + )
946 +
947 + return (local_num_experts, expert_map, expert_mask)
948
949
950 def get_compressed_expert_map(expert_map: torch.Tensor) -> str:
@@ -1040,6 +1070,7 @@ class FusedMoE(CustomOp):
1070 zero_expert_num: int | None = 0,
1071 zero_expert_type: str | None = None,
1072 expert_mapping: list[tuple[str, str, int, str]] | None = None,
1073 + n_shared_experts: int | None = None,
1074 ):
1075 super().__init__()
1076 if params_dtype is None:
@@ -1096,6 +1127,22 @@ class FusedMoE(CustomOp):
1127 self.logical_to_physical_map: torch.Tensor | None = None
1128 self.logical_replica_count: torch.Tensor | None = None
1129
1130 + # ROCm aiter shared experts fusion
1131 + self.num_fused_shared_experts = (
1132 + n_shared_experts
1133 + if n_shared_experts is not None
1134 + and is_rocm_aiter_fusion_shared_expert_enabled()
1135 + else 0
1136 + )
1137 + if (
1138 + not is_rocm_aiter_fusion_shared_expert_enabled()
1139 + and self.num_fused_shared_experts != 0
1140 + ):
1141 + raise ValueError(
1142 + "n_shared_experts is only supported on ROCm aiter when "
1143 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled"
1144 + )
1145 +
1146 # Determine expert maps
1147 if self.use_ep:
1148 if self.enable_eplb:
@@ -1129,14 +1176,16 @@ class FusedMoE(CustomOp):
1176 expert_placement_strategy = "linear"
1177
1178 self.expert_map: torch.Tensor | None
1132 - local_num_experts, expert_map = determine_expert_map(
1179 + local_num_experts, expert_map, expert_mask = determine_expert_map(
1180 ep_size=self.ep_size,
1181 ep_rank=self.ep_rank,
1182 global_num_experts=self.global_num_experts,
1183 expert_placement_strategy=expert_placement_strategy,
1184 + num_fused_shared_experts=self.num_fused_shared_experts,
1185 )
1186 self.local_num_experts = local_num_experts
1187 self.register_buffer("expert_map", expert_map)
1188 + self.register_buffer("expert_mask", expert_mask)
1189 logger.info_once(
1190 "[EP Rank %s/%s] Expert parallelism is enabled. Expert "
1191 "placement strategy: %s. Local/global"
@@ -1150,10 +1199,18 @@ class FusedMoE(CustomOp):
1199 get_compressed_expert_map(self.expert_map),
1200 )
1201 else:
1153 - self.local_num_experts, self.expert_map = (self.global_num_experts, None)
1202 + self.local_num_experts, self.expert_map, self.expert_mask = (
1203 + self.global_num_experts,
1204 + None,
1205 + None,
1206 + )
1207
1208 self.top_k = top_k
1209
1210 + self._init_aiter_shared_experts_topK_buffer(
1211 + vllm_config=vllm_config, dp_size=dp_size_
1212 + )
1213 +
1214 assert intermediate_size % self.tp_size == 0
1215 self.hidden_size = hidden_size
1216 self.intermediate_size_per_partition = intermediate_size // self.tp_size
@@ -1327,13 +1384,18 @@ class FusedMoE(CustomOp):
1384 # ep_size and ep_rank should already be updated
1385 assert self.expert_map is not None
1386 with self.expert_map.device:
1330 - local_num_experts, expert_map = determine_expert_map(
1387 + local_num_experts, expert_map, expert_mask = determine_expert_map(
1388 ep_size=self.ep_size,
1389 ep_rank=self.ep_rank,
1390 global_num_experts=self.global_num_experts,
1391 + num_fused_shared_experts=self.num_fused_shared_experts,
1392 )
1393 self.local_num_experts = local_num_experts
1394 self.register_buffer("expert_map", expert_map)
1395 + self.register_buffer("expert_mask", expert_mask)
1396 + self._init_aiter_shared_experts_topK_buffer(
1397 + vllm_config=get_current_vllm_config(), dp_size=get_dp_group().world_size
1398 + )
1399
1400 def _load_per_tensor_weight_scale(
1401 self,
@@ -1504,6 +1566,24 @@ class FusedMoE(CustomOp):
1566 return expert_id
1567 return self.expert_map[expert_id].item()
1568
1569 + def _init_aiter_shared_experts_topK_buffer(
1570 + self, vllm_config: VllmConfig, dp_size: int
1571 + ):
1572 + if is_rocm_aiter_fusion_shared_expert_enabled():
1573 + if self.num_fused_shared_experts > 0:
1574 + init_aiter_topK_meta_data(
1575 + n_routed_experts=self.global_num_experts,
1576 + n_shared_experts=self.num_fused_shared_experts,
1577 + top_k=self.top_k,
1578 + tp_rank=self.ep_rank if self.use_ep else self.tp_rank,
1579 + tp_size=self.ep_size if self.use_ep else self.tp_size,
1580 + shared_experts_score=1.0,
1581 + max_num_tokens=vllm_config.scheduler_config.max_num_batched_tokens
1582 + * dp_size,
1583 + is_EP=self.use_ep,
1584 + )
1585 + self.local_num_experts += self.num_fused_shared_experts
1586 +
1587 @overload
1588 def weight_loader(
1589 self,
@@ -1866,6 +1946,7 @@ class FusedMoE(CustomOp):
1946 global_num_experts: int | None = None,
1947 zero_expert_num: int | None = None,
1948 zero_expert_type: str | None = None,
1949 + num_fused_shared_experts: int = 0,
1950 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
1951 """
1952 Route the input hidden states to the top-k experts based on the
@@ -1900,7 +1981,16 @@ class FusedMoE(CustomOp):
1981 if use_grouped_topk:
1982 assert topk_group is not None
1983 assert num_expert_group is not None
1903 - topk_weights, topk_ids = grouped_topk(
1984 + if is_rocm_aiter_moe_enabled():
1985 + if not is_rocm_aiter_fusion_shared_expert_enabled():
1986 + assert num_fused_shared_experts == 0
1987 + grouped_topk_impl = partial(
1988 + grouped_topk_aiter,
1989 + num_fused_shared_experts=num_fused_shared_experts,
1990 + )
1991 + else:
1992 + grouped_topk_impl = grouped_topk
1993 + topk_weights, topk_ids = grouped_topk_impl(
1994 hidden_states=hidden_states,
1995 gating_output=router_logits,
1996 topk=top_k,
@@ -2119,7 +2209,9 @@ class FusedMoE(CustomOp):
2209 renormalize=self.renormalize,
2210 use_grouped_topk=self.use_grouped_topk,
2211 global_num_experts=self.global_num_experts,
2122 - expert_map=self.expert_map,
2212 + expert_map=self.expert_map
2213 + if not is_rocm_aiter_moe_enabled()
2214 + else self.expert_mask,
2215 topk_group=self.topk_group,
2216 num_expert_group=self.num_expert_group,
2217 custom_routing_function=self.custom_routing_function,
@@ -2244,7 +2336,9 @@ class FusedMoE(CustomOp):
2336 renormalize=self.renormalize,
2337 use_grouped_topk=self.use_grouped_topk,
2338 global_num_experts=self.global_num_experts,
2247 - expert_map=self.expert_map,
2339 + expert_map=self.expert_map
2340 + if not is_rocm_aiter_moe_enabled()
2341 + else self.expert_mask,
2342 topk_group=self.topk_group,
2343 num_expert_group=self.num_expert_group,
2344 custom_routing_function=self.custom_routing_function,
vllm/model_executor/layers/fused_moe/rocm_aiter_fused_moe.py
+93 -6
@@ -1,7 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from enum import IntEnum
4 -from functools import cache
4 +from functools import cache, lru_cache
5
6 import torch
7
@@ -46,6 +46,69 @@ def is_rocm_aiter_moe_enabled() -> bool:
46 )
47
48
49 +@cache
50 +def is_rocm_aiter_fusion_shared_expert_enabled() -> bool:
51 + return (
52 + envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS and is_rocm_aiter_moe_enabled()
53 + )
54 +
55 +
56 +aiter_topK_meta_data = None
57 +
58 +
59 +@lru_cache(maxsize=1)
60 +def init_aiter_topK_meta_data(
61 + n_routed_experts: int,
62 + n_shared_experts: int,
63 + top_k: int,
64 + tp_rank: int,
65 + tp_size: int,
66 + shared_experts_score: float = 1.0,
67 + max_num_tokens: int = 32768,
68 + is_EP: bool = False,
69 +):
70 + global aiter_topK_meta_data
71 + fake_expertid = n_routed_experts + n_shared_experts
72 +
73 + # all layers reuse same buffer
74 + # This extra element when EP is enabled is used as a sentinel
75 + # to mask out shared expert processing for tokens not owned by
76 + # the current EP rank. This is necessary to avoid double-processing
77 + # of shared experts.
78 + total_topk_ids = torch.empty(
79 + (max_num_tokens, top_k + n_shared_experts + is_EP),
80 + dtype=torch.int32,
81 + device="cuda",
82 + )
83 + ns_topk_ids, s_topk_ids = total_topk_ids.split(
84 + [top_k, n_shared_experts + is_EP], dim=1
85 + )
86 + shared_expert_ids = [n_routed_experts + i for i in range(n_shared_experts + is_EP)]
87 + if is_EP:
88 + s_topk_ids_list = [
89 + [fake_expertid] * (n_shared_experts + is_EP)
90 + ] * max_num_tokens
91 + for i in range(tp_rank, max_num_tokens, tp_size):
92 + s_topk_ids_list[i] = shared_expert_ids
93 + else:
94 + s_topk_ids_list = [
95 + list(range(n_routed_experts, fake_expertid))
96 + ] * max_num_tokens
97 + s_topk_ids[:] = torch.tensor(s_topk_ids_list, dtype=torch.int32, device="cuda")
98 +
99 + total_topk_weights = torch.empty(
100 + (max_num_tokens, top_k + n_shared_experts + is_EP),
101 + dtype=torch.float32,
102 + device="cuda",
103 + )
104 + ns_topk_weights, s_topk_weights = total_topk_weights.split(
105 + [top_k, n_shared_experts + is_EP], dim=1
106 + )
107 + s_topk_weights.fill_(shared_experts_score)
108 + assert aiter_topK_meta_data is None, "AITER topK meta data is already initialized"
109 + aiter_topK_meta_data = (total_topk_weights, total_topk_ids)
110 +
111 +
112 def rocm_aiter_asm_moe_tkw1_impl(
113 hidden_states: torch.Tensor,
114 w1: torch.Tensor,
@@ -300,11 +363,33 @@ def rocm_aiter_grouped_topk(
363 scoring_func: str = "softmax",
364 routed_scaling_factor: float = 1.0,
365 e_score_correction_bias: torch.Tensor | None = None,
366 + num_fused_shared_experts: int = 0,
367 ) -> tuple[torch.Tensor, torch.Tensor]:
368 token = hidden_states.shape[0]
369 device = hidden_states.device
306 - topk_ids = torch.empty((token, topk), dtype=torch.int32, device=device)
307 - topk_weights = torch.empty((token, topk), dtype=torch.float32, device=device)
370 + if is_rocm_aiter_fusion_shared_expert_enabled() and num_fused_shared_experts > 0:
371 + assert aiter_topK_meta_data is not None, (
372 + "AITER topK meta data is not initialized. "
373 + "Please ensure that init_aiter_topK_meta_data "
374 + "is called before this function."
375 + )
376 + total_topk_weights, total_topk_ids = aiter_topK_meta_data
377 + assert total_topk_weights.shape[0] >= token, (
378 + f"AITER topK meta data support {total_topk_weights.shape[0]} "
379 + f"tokens which is determined by max_num_batched_tokens, "
380 + f"but got {token} tokens now."
381 + )
382 + total_topk_weights = total_topk_weights[:token]
383 + total_topk_ids = total_topk_ids[:token]
384 + topk_weights, _ = total_topk_weights.split(
385 + [topk, total_topk_weights.shape[1] - topk], dim=1
386 + )
387 + topk_ids, _ = total_topk_ids.split(
388 + [topk, total_topk_ids.shape[1] - topk], dim=1
389 + )
390 + else:
391 + topk_ids = torch.empty((token, topk), dtype=torch.int32, device=device)
392 + topk_weights = torch.empty((token, topk), dtype=torch.float32, device=device)
393
394 if e_score_correction_bias is not None:
395 torch.ops.vllm.rocm_aiter_biased_grouped_topk(
@@ -315,6 +400,7 @@ def rocm_aiter_grouped_topk(
400 num_expert_group,
401 topk_group,
402 renormalize,
403 + routed_scaling_factor=routed_scaling_factor,
404 )
405 else:
406 assert scoring_func == "softmax" or scoring_func == "sigmoid"
@@ -326,10 +412,11 @@ def rocm_aiter_grouped_topk(
412 topk_group,
413 renormalize,
414 scoring_func,
415 + routed_scaling_factor=routed_scaling_factor,
416 )
417
331 - if routed_scaling_factor != 1.0:
332 - topk_weights = topk_weights * routed_scaling_factor
418 + if is_rocm_aiter_fusion_shared_expert_enabled() and num_fused_shared_experts > 0:
419 + return total_topk_weights, total_topk_ids
420 return topk_weights, topk_ids
421
422
@@ -354,7 +441,7 @@ def rocm_aiter_fused_experts(
441 topk_weights = topk_weights.to(torch.float32)
442 topk_ids = topk_ids.to(torch.int32)
443
357 - expert_mask = (expert_map > -1).to(torch.int32) if expert_map is not None else None
444 + expert_mask = expert_map if expert_map is not None else None
445
446 # w8a8 per-channel quantization
447 if (
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe.py
+1
@@ -1056,6 +1056,7 @@ class CompressedTensorsW8A8Fp8MoEMethod(CompressedTensorsMoEMethod):
1056 routed_scaling_factor=routed_scaling_factor,
1057 e_score_correction_bias=e_score_correction_bias,
1058 indices_type=self.topk_indices_dtype,
1059 + num_fused_shared_experts=layer.num_fused_shared_experts,
1060 )
1061
1062 per_act_token = self.input_quant.strategy == QuantizationStrategy.TOKEN
vllm/model_executor/layers/quantization/fp8.py
+1
@@ -1169,6 +1169,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
1169 global_num_experts=global_num_experts,
1170 zero_expert_num=zero_expert_num,
1171 zero_expert_type=zero_expert_type,
1172 + num_fused_shared_experts=layer.num_fused_shared_experts,
1173 )
1174
1175 #
vllm/model_executor/models/deepseek_v2.py
+141 -67
@@ -50,6 +50,10 @@ from vllm.logger import init_logger
50 from vllm.model_executor.layers.activation import SiluAndMul
51 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
52 from vllm.model_executor.layers.fused_moe import SharedFusedMoE
53 +from vllm.model_executor.layers.fused_moe.rocm_aiter_fused_moe import (
54 + is_rocm_aiter_fusion_shared_expert_enabled,
55 + is_rocm_aiter_moe_enabled,
56 +)
57 from vllm.model_executor.layers.layernorm import LayerNorm, RMSNorm
58 from vllm.model_executor.layers.linear import (
59 ColumnParallelLinear,
@@ -203,7 +207,10 @@ class DeepseekV2MoE(nn.Module):
207 self.physical_expert_start + self.n_local_physical_experts
208 )
209
206 - if config.n_shared_experts is None:
210 + if (
211 + config.n_shared_experts is None
212 + or is_rocm_aiter_fusion_shared_expert_enabled()
213 + ):
214 self.shared_experts = None
215 else:
216 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -233,11 +240,17 @@ class DeepseekV2MoE(nn.Module):
240 prefix=f"{prefix}.experts",
241 scoring_func=config.scoring_func,
242 # we do scaling outside, set factor to 1.0 to avoid double mul
236 - routed_scaling_factor=1.0,
243 + # aiter applies routed_scaling_factor internally
244 + routed_scaling_factor=1.0
245 + if not is_rocm_aiter_moe_enabled()
246 + else self.routed_scaling_factor,
247 e_score_correction_bias=self.gate.e_score_correction_bias,
248 enable_eplb=self.enable_eplb,
249 num_redundant_experts=self.n_redundant_experts,
250 is_sequence_parallel=self.is_sequence_parallel,
251 + n_shared_experts=config.n_shared_experts
252 + if is_rocm_aiter_fusion_shared_expert_enabled()
253 + else None,
254 )
255
256 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -258,16 +271,15 @@ class DeepseekV2MoE(nn.Module):
271 hidden_states=hidden_states, router_logits=router_logits
272 )
273
261 - if self.shared_experts is not None:
262 - shared_output, final_hidden_states = fused_moe_out
263 - else:
264 - shared_output = None
265 - final_hidden_states = fused_moe_out
274 + shared_output, final_hidden_states = fused_moe_out
275 + if self.shared_experts is None:
276 + assert shared_output is None
277
278 # Fix FP16 overflow
279 # See DeepseekV2DecoderLayer for more details.
280 if hidden_states.dtype != torch.float16:
270 - final_hidden_states *= self.routed_scaling_factor
281 + if not is_rocm_aiter_moe_enabled():
282 + final_hidden_states *= self.routed_scaling_factor
283 elif self.shared_experts is not None:
284 assert shared_output is not None
285 shared_output *= 1.0 / self.routed_scaling_factor
@@ -1316,7 +1328,12 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts, SupportsLoR
1328 ckpt_gate_proj_name="gate_proj",
1329 ckpt_down_proj_name="down_proj",
1330 ckpt_up_proj_name="up_proj",
1319 - num_experts=self.config.n_routed_experts,
1331 + num_experts=self.config.n_routed_experts
1332 + + (
1333 + self.config.n_shared_experts
1334 + if is_rocm_aiter_fusion_shared_expert_enabled()
1335 + else 0
1336 + ),
1337 num_redundant_experts=self.num_redundant_experts,
1338 )
1339
@@ -1330,6 +1347,11 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts, SupportsLoR
1347 if spec_layer is not None:
1348 continue # skip spec decode layers for main model
1349
1350 + is_fuse_shared_experts_layer = (
1351 + is_rocm_aiter_fusion_shared_expert_enabled()
1352 + and ("mlp.shared_experts" in name)
1353 + )
1354 +
1355 for param_name, weight_name, shard_id in stacked_params_mapping:
1356 # Skip non-stacked layers and experts (experts handled below).
1357 if weight_name not in name:
@@ -1342,6 +1364,8 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts, SupportsLoR
1364 # for mlp.experts[0].gate_gate_up_proj, which breaks load.
1365 if ("mlp.experts." in name) and name not in params_dict:
1366 continue
1367 + if is_fuse_shared_experts_layer:
1368 + continue
1369 name_mapped = name.replace(weight_name, param_name)
1370
1371 # QKV fusion is optional, fall back to normal
@@ -1366,65 +1390,115 @@ class DeepseekV2ForCausalLM(nn.Module, SupportsPP, MixtureOfExperts, SupportsLoR
1390 break
1391 else:
1392 is_expert_weight = False
1369 - for mapping in expert_params_mapping:
1370 - param_name, weight_name, expert_id, shard_id = mapping
1371 - if weight_name not in name:
1372 - continue
1373 -
1374 - # Anyway, this is an expert weight and should not be
1375 - # attempted to load as other weights later
1376 - is_expert_weight = True
1377 -
1378 - # Do not modify `name` since the loop may continue here
1379 - # Instead, create a new variable
1380 - name_mapped = name.replace(weight_name, param_name)
1381 -
1382 - if is_pp_missing_parameter(name_mapped, self):
1383 - continue
1384 -
1385 - param = params_dict[name_mapped]
1386 - # We should ask the weight loader to return success or not
1387 - # here since otherwise we may skip experts with other
1388 - # available replicas.
1389 - weight_loader = typing.cast(
1390 - Callable[..., bool], param.weight_loader
1391 - )
1392 - success = weight_loader(
1393 - param,
1394 - loaded_weight,
1395 - name_mapped,
1396 - shard_id=shard_id,
1397 - expert_id=expert_id,
1398 - return_success=True,
1399 - )
1400 - if success:
1401 - name = name_mapped
1402 - break
1403 - else:
1404 - if is_expert_weight:
1405 - # We've checked that this is an expert weight
1406 - # However it's not mapped locally to this rank
1407 - # So we simply skip it
1408 - continue
1409 -
1410 - # Skip loading extra bias for GPTQ models.
1411 - if name.endswith(".bias") and name not in params_dict:
1412 - continue
1413 -
1414 - # Remapping the name of FP8 kv-scale.
1415 - name = maybe_remap_kv_scale_name(name, params_dict)
1416 - if name is None:
1417 - continue
1418 -
1419 - if is_pp_missing_parameter(name, self):
1420 - continue
1421 -
1422 - param = params_dict[name]
1423 - weight_loader = getattr(
1424 - param, "weight_loader", default_weight_loader
1393 +
1394 + # Special handling: when AITER fusion_shared_experts is enabled,
1395 + # checkpoints may provide a single widened shared_experts tensor
1396 + # without explicit expert indices
1397 + # (e.g. ...mlp.shared_experts.gate_proj.weight).
1398 + # For models with multiple shared experts, split that tensor
1399 + # evenly into per-shared-expert slices and load them into
1400 + # appended expert slots mlp.experts.{n_routed_experts + j}.*
1401 + # accordingly.
1402 + num_chunks = 1
1403 + if is_fuse_shared_experts_layer:
1404 + num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
1405 + # Determine split axis based on op type
1406 + # gate/up: ColumnParallel → split along dim 0
1407 + # down: RowParallel → split along dim 1
1408 + split_dim = 1 if "down_proj.weight" in name else 0
1409 + total = loaded_weight.shape[split_dim]
1410 + assert total % num_chunks == 0, (
1411 + f"Shared expert weight dim {total} "
1412 + f"not divisible by num_chunks {num_chunks}"
1413 )
1426 - weight_loader(param, loaded_weight)
1427 - loaded_params.add(name)
1414 + chunk_size = total // num_chunks
1415 +
1416 + for j in range(num_chunks):
1417 + chunk_name = name
1418 + weight_to_load = loaded_weight
1419 +
1420 + if is_fuse_shared_experts_layer:
1421 + if split_dim == 0:
1422 + weight_to_load = loaded_weight[
1423 + j * chunk_size : (j + 1) * chunk_size, :
1424 + ]
1425 + else:
1426 + weight_to_load = loaded_weight[
1427 + :, j * chunk_size : (j + 1) * chunk_size
1428 + ]
1429 + # Synthesize an expert-style name so expert mapping
1430 + # can route it
1431 + chunk_name = name.replace(
1432 + "mlp.shared_experts",
1433 + f"mlp.experts.{self.config.n_routed_experts + j}",
1434 + )
1435 +
1436 + # Use expert_params_mapping to locate the destination
1437 + # param and delegate to its expert-aware weight_loader
1438 + # with expert_id.
1439 + for mapping in expert_params_mapping:
1440 + param_name, weight_name, expert_id, shard_id = mapping
1441 + if weight_name not in chunk_name:
1442 + continue
1443 +
1444 + # Anyway, this is an expert weight and should not be
1445 + # attempted to load as other weights later
1446 + is_expert_weight = True
1447 +
1448 + # Do not modify `name` since the loop may continue here
1449 + # Instead, create a new variable
1450 + name_mapped = chunk_name.replace(weight_name, param_name)
1451 +
1452 + if is_pp_missing_parameter(name_mapped, self):
1453 + continue
1454 +
1455 + param = params_dict[name_mapped]
1456 + # We should ask the weight loader to return success or
1457 + # not here since otherwise we may skip experts with
1458 + # other available replicas.
1459 + weight_loader = typing.cast(
1460 + Callable[..., bool], param.weight_loader
1461 + )
1462 + success = weight_loader(
1463 + param,
1464 + weight_to_load,
1465 + name_mapped,
1466 + shard_id=shard_id,
1467 + expert_id=expert_id,
1468 + return_success=True,
1469 + )
1470 + if success:
1471 + if not is_fuse_shared_experts_layer:
1472 + name = name_mapped
1473 + else:
1474 + loaded_params.add(name_mapped)
1475 + break
1476 + else:
1477 + if is_expert_weight:
1478 + # We've checked that this is an expert weight
1479 + # However it's not mapped locally to this rank
1480 + # So we simply skip it
1481 + continue
1482 +
1483 + # Skip loading extra bias for GPTQ models.
1484 + if name.endswith(".bias") and name not in params_dict:
1485 + continue
1486 +
1487 + # Remapping the name of FP8 kv-scale.
1488 + name = maybe_remap_kv_scale_name(name, params_dict)
1489 + if name is None:
1490 + continue
1491 +
1492 + if is_pp_missing_parameter(name, self):
1493 + continue
1494 +
1495 + param = params_dict[name]
1496 + weight_loader = getattr(
1497 + param, "weight_loader", default_weight_loader
1498 + )
1499 + weight_loader(param, loaded_weight)
1500 + if not is_fuse_shared_experts_layer:
1501 + loaded_params.add(name)
1502
1503 return loaded_params
1504