[MOE] Standardize and abstract fused shared expert optimization selection (#51695)

Signed-off-by: Felix Marty <Felix.Marty@amd.com>

fxmarty-amd committed Aug 18, 2026 at 18:56 UTC 88b2bff2c63d0f28396451f1199d09ee0f3e2d88
20 files changed +1288 -281
tests/model_executor/layers/test_fused_shared_expert.py new
+700
@@ -0,0 +1,700 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import importlib
5 +from copy import deepcopy
6 +from types import SimpleNamespace
7 +from typing import Any, cast
8 +from unittest.mock import patch
9 +
10 +import pytest
11 +import torch
12 +from torch import nn
13 +
14 +import vllm.config as vllm_config_module
15 +from vllm.config import VllmConfig, set_current_vllm_config
16 +from vllm.model_executor.layers.fused_moe import utils as fused_moe_utils
17 +from vllm.model_executor.layers.fused_moe.layer import determine_expert_counts
18 +from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
19 +from vllm.model_executor.layers.quantization.utils.config_utils import (
20 + is_shared_expert_quant_fse_compatible,
21 +)
22 +from vllm.model_executor.models.utils import PPMissingLayer
23 +from vllm.models.deepseek_v4 import quant_config as deepseek_v4_quant_config
24 +from vllm.models.minimax_m3.amd import model as minimax_m3_model
25 +from vllm.transformers_utils.configs.minimax_m3 import MiniMaxM3TextConfig
26 +from vllm.transformers_utils.configs.qwen3_5_moe import Qwen3_5MoeTextConfig
27 +
28 +_QUARK_FSE_CONFIG: dict[str, Any] = {
29 + "global_quant_config": {
30 + "input_tensors": {
31 + "dtype": "fp4",
32 + "is_dynamic": True,
33 + "qscheme": "per_group",
34 + "ch_axis": -1,
35 + "group_size": 32,
36 + "block_size": None,
37 + "symmetric": None,
38 + "round_method": "half_even",
39 + "scale_type": "float",
40 + "scale_format": "e8m0",
41 + "scale_calculation_mode": "even",
42 + "mx_element_dtype": None,
43 + "observer_cls": "PerBlockMXObserver",
44 + "is_scale_quant": False,
45 + "enable_buffer_reuse": False,
46 + "max_input_numel": 4194304,
47 + },
48 + "output_tensors": None,
49 + "weight": {
50 + "dtype": "fp4",
51 + "is_dynamic": False,
52 + "qscheme": "per_group",
53 + "ch_axis": -1,
54 + "group_size": 32,
55 + "block_size": None,
56 + "symmetric": None,
57 + "round_method": "half_even",
58 + "scale_type": "float",
59 + "scale_format": "e8m0",
60 + "scale_calculation_mode": "even",
61 + "mx_element_dtype": None,
62 + "observer_cls": "PerBlockMXObserver",
63 + "is_scale_quant": False,
64 + "enable_buffer_reuse": False,
65 + "max_input_numel": 4194304,
66 + },
67 + "bias": None,
68 + "target_device": None,
69 + },
70 + "algo_config": None,
71 + "softmax_quant_spec": None,
72 + "quant_method": "quark",
73 + "layer_type_quant_config": {},
74 + "layer_quant_config": {},
75 + "kv_cache_quant_config": {},
76 + "kv_cache_post_rope": False,
77 + "quant_mode": "eager_mode",
78 + "version": "0.12+9d3d471cdf1",
79 + "export": {
80 + "kv_cache_group": [],
81 + "min_kv_scale": 0.0,
82 + "pack_method": "reorder",
83 + "weight_format": "real_quantized",
84 + "weight_merge_groups": None,
85 + },
86 +}
87 +
88 +
89 +def get_deepseek_v4_quark_config(exclude: list[str]) -> dict[str, Any]:
90 + """Return the DeepSeek-V4-Pro-MXFP4 FSE quantization layout."""
91 + # Mimics https://huggingface.co/amd/DeepSeek-V4-Pro-MXFP4.
92 + quantization_config: dict[str, Any] = deepcopy(_QUARK_FSE_CONFIG)
93 + quantization_config["exclude"] = exclude
94 + mxfp4_config = cast(
95 + dict[str, Any], deepcopy(quantization_config["global_quant_config"])
96 + )
97 + fp8_config = deepcopy(mxfp4_config)
98 + fp8_config["input_tensors"].update(
99 + {
100 + "dtype": "fp8_e4m3",
101 + "group_size": 128,
102 + "symmetric": True,
103 + "scale_type": None,
104 + "scale_format": None,
105 + "scale_calculation_mode": None,
106 + "observer_cls": "PerGroupMinMaxObserver",
107 + }
108 + )
109 + fp8_config["weight"].update(
110 + {
111 + "dtype": "fp8_e4m3",
112 + "qscheme": "per_block",
113 + "ch_axis": None,
114 + "group_size": None,
115 + "block_size": [128, 128],
116 + "symmetric": True,
117 + "scale_type": "float8_e8m0fnu",
118 + "scale_format": None,
119 + "scale_calculation_mode": None,
120 + "observer_cls": "PerBlock2DMinMaxObserver",
121 + }
122 + )
123 + quantization_config["layer_quant_config"] = {
124 + r"re:mtp\.0\.ffn\.experts\.\d+\.w1": mxfp4_config,
125 + r"re:mtp\.0\.ffn\.shared_experts\.w1": fp8_config,
126 + }
127 + return quantization_config
128 +
129 +
130 +def get_fse_test_model_config(
131 + model_type: str,
132 + quantization_config: dict[str, Any],
133 +) -> tuple[object, type[nn.Module]]:
134 + if model_type == "minimax_m3":
135 + config = MiniMaxM3TextConfig(
136 + hidden_size=128,
137 + intermediate_size=32,
138 + dense_intermediate_size=32,
139 + num_hidden_layers=2,
140 + num_attention_heads=1,
141 + num_key_value_heads=1,
142 + head_dim=128,
143 + num_local_experts=2,
144 + num_experts_per_tok=1,
145 + moe_layer_freq=[0, 1],
146 + sparse_attention_config=None,
147 + rotary_dim=64,
148 + quantization_config=quantization_config,
149 + )
150 + return config, minimax_m3_model.MiniMaxM3Model
151 + if model_type == "deepseek_v4":
152 + from vllm.models.deepseek_v4.amd.model import DeepseekV4Model
153 +
154 + config = SimpleNamespace(
155 + vocab_size=256,
156 + hidden_size=128,
157 + num_hidden_layers=1,
158 + num_attention_heads=1,
159 + head_dim=128,
160 + max_position_embeddings=128,
161 + sliding_window=None,
162 + compress_ratios=[1],
163 + rope_theta=10000.0,
164 + compress_rope_theta=10000.0,
165 + rope_parameters={"rope_type": "default"},
166 + rms_norm_eps=1e-6,
167 + hidden_act="silu",
168 + q_lora_rank=0,
169 + o_lora_rank=0,
170 + o_groups=1,
171 + qk_rope_head_dim=64,
172 + index_head_dim=64,
173 + index_n_heads=1,
174 + index_topk=1,
175 + n_routed_experts=2,
176 + n_shared_experts=1,
177 + num_experts_per_tok=1,
178 + n_group=1,
179 + topk_group=1,
180 + moe_intermediate_size=32,
181 + norm_topk_prob=False,
182 + num_hash_layers=0,
183 + swiglu_limit=0.0,
184 + hc_eps=1e-6,
185 + hc_mult=1,
186 + hc_sinkhorn_iters=1,
187 + expert_dtype="fp4",
188 + num_nextn_predict_layers=1,
189 + quantization_config=quantization_config,
190 + )
191 + return config, DeepseekV4Model
192 + if model_type == "qwen3_5":
193 + from vllm.model_executor.models.qwen3_5 import Qwen3_5Model
194 +
195 + config = Qwen3_5MoeTextConfig(
196 + vocab_size=256,
197 + hidden_size=128,
198 + num_hidden_layers=1,
199 + num_attention_heads=1,
200 + num_key_value_heads=1,
201 + head_dim=128,
202 + linear_key_head_dim=64,
203 + linear_value_head_dim=64,
204 + linear_num_key_heads=1,
205 + linear_num_value_heads=1,
206 + moe_intermediate_size=32,
207 + shared_expert_intermediate_size=32,
208 + num_experts_per_tok=1,
209 + num_experts=2,
210 + layer_types=["full_attention"],
211 + quantization_config=quantization_config,
212 + )
213 + return config, Qwen3_5Model
214 + if model_type == "deepseek_v2":
215 + from transformers import DeepseekV2Config
216 +
217 + from vllm.model_executor.models.deepseek_v2 import DeepseekV2Model
218 +
219 + config = DeepseekV2Config(
220 + vocab_size=256,
221 + hidden_size=128,
222 + intermediate_size=32,
223 + num_hidden_layers=1,
224 + num_attention_heads=1,
225 + num_key_value_heads=1,
226 + head_dim=128,
227 + first_k_dense_replace=0,
228 + n_routed_experts=2,
229 + n_shared_experts=1,
230 + num_experts_per_tok=1,
231 + n_group=1,
232 + topk_group=1,
233 + moe_intermediate_size=32,
234 + q_lora_rank=None,
235 + kv_lora_rank=0,
236 + qk_nope_head_dim=0,
237 + qk_rope_head_dim=0,
238 + v_head_dim=0,
239 + quantization_config=quantization_config,
240 + )
241 + return config, DeepseekV2Model
242 + if model_type == "glm4_moe":
243 + from transformers.models.glm4_moe import Glm4MoeConfig
244 +
245 + from vllm.model_executor.models.glm4_moe import Glm4MoeModel
246 +
247 + config = Glm4MoeConfig(
248 + vocab_size=256,
249 + hidden_size=128,
250 + intermediate_size=32,
251 + num_hidden_layers=1,
252 + num_attention_heads=1,
253 + num_key_value_heads=1,
254 + head_dim=128,
255 + moe_intermediate_size=32,
256 + n_routed_experts=2,
257 + n_shared_experts=1,
258 + num_experts_per_tok=1,
259 + first_k_dense_replace=0,
260 + quantization_config=quantization_config,
261 + )
262 + return config, Glm4MoeModel
263 + raise ValueError(f"Unsupported FSE test model: {model_type}")
264 +
265 +
266 +def test_determine_expert_counts_fuse_shared_experts_override(
267 + monkeypatch: pytest.MonkeyPatch,
268 +) -> None:
269 + common_args = (8, 0, 2)
270 + assert determine_expert_counts(*common_args, True)[2] == 2
271 + assert determine_expert_counts(*common_args, False)[2] == 0
272 +
273 +
274 +def test_resolve_layer_fused_shared_expert_skips_compatibility_when_disabled(
275 + monkeypatch: pytest.MonkeyPatch,
276 +) -> None:
277 + monkeypatch.setattr(
278 + fused_moe_utils.rocm_aiter_ops,
279 + "is_fusion_moe_shared_experts_enabled",
280 + lambda: False,
281 + )
282 + monkeypatch.setattr(
283 + fused_moe_utils,
284 + "is_shared_expert_quant_fse_compatible",
285 + lambda *_: pytest.fail(
286 + "compatibility must not be checked when FSE is disabled"
287 + ),
288 + )
289 +
290 + assert not fused_moe_utils.resolve_layer_fused_shared_expert(
291 + object(), "model.layers.0.mlp"
292 + )
293 +
294 +
295 +def test_resolve_layer_fused_shared_expert_normalizes_unavailable_aiter(
296 + monkeypatch: pytest.MonkeyPatch,
297 +) -> None:
298 + monkeypatch.setattr(
299 + fused_moe_utils.rocm_aiter_ops,
300 + "is_fusion_moe_shared_experts_enabled",
301 + lambda: None,
302 + )
303 +
304 + assert (
305 + fused_moe_utils.resolve_layer_fused_shared_expert(
306 + object(), "model.layers.0.mlp"
307 + )
308 + is False
309 + )
310 +
311 +
312 +def test_resolve_layer_fused_shared_expert_passes_module_prefixes(
313 + monkeypatch: pytest.MonkeyPatch,
314 +) -> None:
315 + quant_config = object()
316 + monkeypatch.setattr(
317 + fused_moe_utils.rocm_aiter_ops,
318 + "is_fusion_moe_shared_experts_enabled",
319 + lambda: True,
320 + )
321 +
322 + calls: list[tuple[object, str, str]] = []
323 +
324 + def check_compatibility(
325 + config: object, expert_prefix: str, shared_expert_prefix: str
326 + ) -> tuple[bool, str | None]:
327 + calls.append((config, expert_prefix, shared_expert_prefix))
328 + return True, None
329 +
330 + monkeypatch.setattr(
331 + fused_moe_utils, "is_shared_expert_quant_fse_compatible", check_compatibility
332 + )
333 +
334 + assert fused_moe_utils.resolve_layer_fused_shared_expert(
335 + quant_config,
336 + "model.layers.0.mlp",
337 + shared_expert_name="shared_expert",
338 + )
339 + assert calls == [
340 + (
341 + quant_config,
342 + "model.layers.0.mlp.experts",
343 + "model.layers.0.mlp.shared_expert",
344 + )
345 + ]
346 +
347 +
348 +def test_resolve_layer_fused_shared_expert_rejects_incompatible_quantization(
349 + monkeypatch: pytest.MonkeyPatch,
350 + caplog: pytest.LogCaptureFixture,
351 +) -> None:
352 + monkeypatch.setattr(
353 + fused_moe_utils.rocm_aiter_ops,
354 + "is_fusion_moe_shared_experts_enabled",
355 + lambda: True,
356 + )
357 + monkeypatch.setattr(
358 + fused_moe_utils,
359 + "is_shared_expert_quant_fse_compatible",
360 + lambda *_: (False, "shared experts are excluded"),
361 + )
362 +
363 + assert not fused_moe_utils.resolve_layer_fused_shared_expert(
364 + object(), "model.layers.0.mlp"
365 + )
366 + assert "shared experts are excluded" in caplog.text
367 +
368 +
369 +def test_deepseek_v4_shared_expert_fse_uses_mtp_quantization_config_prefix(
370 + monkeypatch: pytest.MonkeyPatch,
371 +) -> None:
372 + class DeepseekV4Config:
373 + expert_dtype = "fp4"
374 +
375 + def _is_quark_mxfp4_ocp(self, hf_config: object) -> bool:
376 + return True
377 +
378 + hf_config = SimpleNamespace(
379 + num_hidden_layers=2,
380 + quantization_config={
381 + "layer_quant_config": {
382 + r"re:mtp\.0\.ffn\.shared_experts\.w1": {"weight": {"dtype": "fp4"}}
383 + },
384 + "global_quant_config": {"weight": {"dtype": "fp8"}},
385 + },
386 + )
387 + monkeypatch.setattr(
388 + deepseek_v4_quant_config, "DeepseekV4FP8Config", DeepseekV4Config
389 + )
390 + monkeypatch.setattr(
391 + vllm_config_module,
392 + "get_current_vllm_config",
393 + lambda: SimpleNamespace(model_config=SimpleNamespace(hf_config=hf_config)),
394 + )
395 +
396 + compatible, reason = is_shared_expert_quant_fse_compatible(
397 + DeepseekV4Config(),
398 + "model.layers.2.ffn.experts",
399 + "model.layers.2.ffn.shared_experts",
400 + )
401 +
402 + assert compatible
403 + assert reason is None
404 +
405 +
406 +def test_is_model_fused_shared_expert_compatible() -> None:
407 + class MoE(nn.Module):
408 + def __init__(self, enabled: bool) -> None:
409 + super().__init__()
410 + self.is_fused_shared_expert_enabled = enabled
411 +
412 + class Layer(nn.Module):
413 + def __init__(self, enabled: bool) -> None:
414 + super().__init__()
415 + self.mlp = MoE(enabled)
416 +
417 + enabled_layers = nn.ModuleList([Layer(True)])
418 + disabled_layers = nn.ModuleList([Layer(False)])
419 + mixed_layers = nn.ModuleList([Layer(True), Layer(False)])
420 + empty_layers = nn.ModuleList()
421 + pipeline_layers = nn.ModuleList([Layer(True), PPMissingLayer()])
422 +
423 + assert fused_moe_utils.is_model_fused_shared_expert_compatible(
424 + enabled_layers, MoE, "mlp"
425 + )
426 + assert not fused_moe_utils.is_model_fused_shared_expert_compatible(
427 + disabled_layers, MoE, "mlp"
428 + )
429 + assert not fused_moe_utils.is_model_fused_shared_expert_compatible(
430 + empty_layers, MoE, "mlp"
431 + )
432 + assert fused_moe_utils.is_model_fused_shared_expert_compatible(
433 + pipeline_layers, MoE, "mlp"
434 + )
435 + with pytest.raises(NotImplementedError, match="1 enabled and 1 disabled layers"):
436 + fused_moe_utils.is_model_fused_shared_expert_compatible(
437 + mixed_layers, MoE, "mlp"
438 + )
439 +
440 +
441 +@pytest.mark.parametrize(
442 + "model_type",
443 + ["minimax_m3", "deepseek_v4", "qwen3_5", "glm4_moe", "deepseek_v2"],
444 +)
445 +@pytest.mark.parametrize(
446 + ("use_fse", "exclude"),
447 + [
448 + (False, []),
449 + (True, []),
450 + (True, ["*.shared_experts.*"]),
451 + ],
452 +)
453 +def test_models_fse_init(
454 + model_type: str,
455 + use_fse: bool,
456 + exclude: list[str],
457 + dist_init: None,
458 + monkeypatch: pytest.MonkeyPatch,
459 +) -> None:
460 + """Model construction resolves FSE consistently with Quark quantization."""
461 +
462 + quantization_config: dict[str, Any] = (
463 + get_deepseek_v4_quark_config(["layers.0.ffn.shared_experts"] if exclude else [])
464 + if model_type == "deepseek_v4"
465 + else {**_QUARK_FSE_CONFIG, "exclude": exclude}
466 + )
467 +
468 + config, model_constructor = get_fse_test_model_config(
469 + model_type, quantization_config
470 + )
471 + vllm_config = VllmConfig()
472 + vllm_config.model_config = SimpleNamespace(
473 + hf_config=config,
474 + hf_text_config=config,
475 + dtype=torch.bfloat16,
476 + max_model_len=128,
477 + is_diffusion=False,
478 + head_dtype=None,
479 + is_mm_prefix_lm=False,
480 + multimodal_config=None,
481 + quantization_config=None,
482 + )
483 + vllm_config.parallel_config.enable_expert_parallel = False
484 + if model_type == "deepseek_v4":
485 + from vllm.models.deepseek_v4.quant_config import DeepseekV4FP8Config
486 +
487 + vllm_config.cache_config.cache_dtype = "fp8_ds_mla"
488 + vllm_config.quant_config = DeepseekV4FP8Config(
489 + is_checkpoint_fp8_serialized=True,
490 + weight_block_size=[128, 128],
491 + )
492 + else:
493 + vllm_config.quant_config = QuarkConfig(quantization_config)
494 +
495 + import vllm.envs as envs
496 + from vllm._aiter_ops import rocm_aiter_ops
497 +
498 + if model_type == "deepseek_v4":
499 + from vllm.models.deepseek_v4.amd import model as deepseek_v4_model
500 +
501 + warning_logger = deepseek_v4_model.logger
502 + elif model_type == "minimax_m3":
503 + warning_logger = minimax_m3_model.logger
504 + else:
505 + warning_logger = fused_moe_utils.logger
506 +
507 + with monkeypatch.context() as mp:
508 + mp.setenv("VLLM_ROCM_USE_AITER", str(use_fse))
509 + mp.setenv("VLLM_ROCM_USE_AITER_MOE", str(use_fse))
510 + mp.setenv("VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS", str(use_fse))
511 + mp.setattr(
512 + "vllm.model_executor.layers.fused_moe.experts."
513 + "ocp_mx_emulation_moe.has_quark",
514 + lambda: True,
515 + )
516 + importlib.reload(envs)
517 + mp.setattr(envs, "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS", use_fse)
518 + rocm_aiter_ops.refresh_env_variables()
519 + aiter_fse_enabled = bool(rocm_aiter_ops.is_fusion_moe_shared_experts_enabled())
520 + # These AMD-specific models currently use the raw environment flag.
521 + # and do not rely on `rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()`.
522 + fse_enabled = use_fse and (
523 + aiter_fse_enabled or model_type in {"deepseek_v4", "minimax_m3"}
524 + )
525 +
526 + with (
527 + patch.object(warning_logger, "warning") as warning,
528 + set_current_vllm_config(vllm_config),
529 + ):
530 + model = model_constructor(vllm_config=vllm_config)
531 + mtp = None
532 + if model_type == "deepseek_v4" and use_fse and not exclude:
533 + from vllm.models.deepseek_v4.amd.mtp import (
534 + DeepSeekV4MTP,
535 + )
536 +
537 + vllm_config.speculative_config = SimpleNamespace(
538 + draft_model_config=SimpleNamespace(hf_config=config)
539 + )
540 + mtp = DeepSeekV4MTP(vllm_config=vllm_config)
541 + assert model.is_fused_shared_expert_enabled is (fse_enabled and not exclude)
542 +
543 + # The dummy quant config here uses mixed mxfp4/fp8 for experts/shared_expert
544 + # so should just raise a warning.
545 + if mtp is not None:
546 + assert not mtp.model.layers[
547 + "1"
548 + ].mtp_block.ffn.is_fused_shared_expert_enabled
549 + warning.assert_called_once()
550 + assert (
551 + "DeepSeek-V4 shared experts at mtp.0.ffn.shared_experts"
552 + in (warning.call_args.args[1])
553 + )
554 + if aiter_fse_enabled and exclude:
555 + warning.assert_called_once()
556 + assert (
557 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled"
558 + in (warning.call_args.args[0])
559 + )
560 + assert "excludes shared experts" in warning.call_args.args[1]
561 +
562 + importlib.reload(envs)
563 + rocm_aiter_ops.refresh_env_variables()
564 +
565 +
566 +@pytest.mark.parametrize(
567 + ("exclude", "expected"),
568 + [([], True), (["*.shared_expert.*"], False)],
569 +)
570 +def test_quark_shared_expert_fse_compatibility(
571 + exclude: list[str], expected: bool
572 +) -> None:
573 + compatible, reason = is_shared_expert_quant_fse_compatible(
574 + QuarkConfig(
575 + {
576 + "exclude": exclude,
577 + "global_quant_config": {},
578 + "layer_quant_config": {},
579 + }
580 + ),
581 + "model.layers.0.mlp.experts",
582 + "model.layers.0.mlp.shared_expert",
583 + )
584 +
585 + assert compatible is expected
586 + if expected:
587 + assert reason is None
588 + else:
589 + assert (
590 + reason
591 + == "Quark excludes shared experts at model.layers.0.mlp.shared_expert"
592 + )
593 +
594 +
595 +def test_quark_shared_expert_fse_requires_matching_layer_quant_configs() -> None:
596 + global_quant_config = {"weight": {"dtype": "fp4"}}
597 + quant_config = QuarkConfig(
598 + {
599 + "exclude": [],
600 + "global_quant_config": global_quant_config,
601 + "layer_quant_config": {
602 + "model.layers.0.mlp.shared_expert.gate_up_proj": {
603 + "weight": {"dtype": "fp8"}
604 + },
605 + "model.layers.0.mlp.shared_expert.down_proj": {
606 + "weight": {"dtype": "fp8"}
607 + },
608 + },
609 + }
610 + )
611 +
612 + compatible, reason = is_shared_expert_quant_fse_compatible(
613 + quant_config,
614 + "model.layers.0.mlp.experts",
615 + "model.layers.0.mlp.shared_expert",
616 + )
617 +
618 + assert not compatible
619 + assert reason == (
620 + "Quark uses different quantization configurations for routed and "
621 + "shared experts at model.layers.0.mlp.shared_expert"
622 + )
623 +
624 +
625 +def test_quark_shared_expert_fse_rejects_partial_packed_projection_override() -> None:
626 + fp4_config = {"weight": {"dtype": "fp4"}}
627 + fp8_config = {"weight": {"dtype": "fp8"}}
628 + quant_config = QuarkConfig(
629 + {
630 + "exclude": [],
631 + "global_quant_config": fp4_config,
632 + "layer_quant_config": {
633 + "model.layers.0.mlp.experts": fp8_config,
634 + "model.layers.0.mlp.shared_expert.gate_proj": fp8_config,
635 + "model.layers.0.mlp.shared_expert.down_proj": fp8_config,
636 + },
637 + }
638 + )
639 + quant_config.packed_modules_mapping = {"gate_up_proj": ["gate_proj", "up_proj"]}
640 +
641 + compatible, reason = is_shared_expert_quant_fse_compatible(
642 + quant_config,
643 + "model.layers.0.mlp.experts",
644 + "model.layers.0.mlp.shared_expert",
645 + )
646 +
647 + assert not compatible
648 + assert reason == (
649 + "Quark uses different quantization configurations for routed and "
650 + "shared experts at model.layers.0.mlp.shared_expert"
651 + )
652 +
653 +
654 +def test_quark_layer_config_from_name_checks_packed_projections() -> None:
655 + quant_config = QuarkConfig(
656 + {
657 + "global_quant_config": {"weight": {"dtype": "fp4"}},
658 + "layer_quant_config": {
659 + "model.layers.0.mlp.shared_expert.w1": {"weight": {"dtype": "fp4"}},
660 + "model.layers.0.mlp.shared_expert.w3": {"weight": {"dtype": "fp8"}},
661 + },
662 + }
663 + )
664 + quant_config.packed_modules_mapping = {"gate_up_proj": ["w1", "w3"]}
665 +
666 + with pytest.raises(ValueError, match="requires all to use the same scheme"):
667 + quant_config.get_layer_quant_config_from_name(
668 + "model.layers.0.mlp.shared_expert.gate_up_proj"
669 + )
670 +
671 +
672 +def test_quark_packed_layer_config_must_match_global_config() -> None:
673 + quant_config = QuarkConfig(
674 + {
675 + "global_quant_config": {"weight": {"dtype": "fp4"}},
676 + "layer_type_quant_config": {},
677 + "layer_quant_config": {
678 + "model.layers.0.mlp.shared_expert.w1": {"weight": {"dtype": "fp8"}},
679 + },
680 + }
681 + )
682 + quant_config.packed_modules_mapping = {"gate_up_proj": ["w1", "w3"]}
683 +
684 + with pytest.raises(ValueError, match="requires all to use the same scheme"):
685 + quant_config._find_matched_config(
686 + "model.layers.0.mlp.shared_expert.gate_up_proj", nn.Module()
687 + )
688 +
689 +
690 +def test_non_quark_shared_expert_fse_is_incompatible() -> None:
691 + compatible, reason = is_shared_expert_quant_fse_compatible(
692 + object(),
693 + "model.layers.0.mlp.experts",
694 + "model.layers.0.mlp.shared_experts",
695 + )
696 +
697 + assert not compatible
698 + assert reason == (
699 + "shared-expert FSE quantization compatibility is not implemented for object"
700 + )
vllm/model_executor/layers/fused_moe/layer.py
+5 -14
@@ -6,7 +6,6 @@ from typing import Any
6
7 import torch
8
9 -import vllm.envs as envs
9 from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.config import ParallelConfig, get_current_vllm_config
11 from vllm.distributed import (
@@ -74,23 +73,13 @@ def determine_expert_counts(
73 num_experts: int,
74 num_redundant_experts: int,
75 n_shared_experts: int | None,
77 - is_act_and_mul: bool,
76 + fuse_shared_experts: bool,
77 ) -> tuple[int, int, int]:
78 global_num_experts = num_experts + num_redundant_experts
79 logical_num_experts = num_experts
81 - # Shared-expert fusion: append the shared expert(s) as routed-expert slots
82 - # so they run in the same grouped GEMM. Gated by
83 - # VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS: either the native aiter fused-MoE
84 - # path (env + master switch, via is_fusion_moe_shared_experts_enabled) or the
85 - # backend-neutral router-append path (env alone, independent of the master
86 - # switch; e.g. the MM3 triton/flydsl mxfp8 MoE). Gated activations only.
87 - fuse_shared_enabled = (
88 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
89 - or envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS
90 - ) and is_act_and_mul
80
81 num_fused_shared_experts = (
93 - n_shared_experts if n_shared_experts is not None and fuse_shared_enabled else 0
82 + n_shared_experts if n_shared_experts is not None and fuse_shared_experts else 0
83 )
84
85 return global_num_experts, logical_num_experts, num_fused_shared_experts
@@ -132,6 +121,7 @@ def FusedMoEFactory(
121 ckpt_names: tuple[str, str, str] = ("gate_proj", "down_proj", "up_proj"),
122 is_fused_checkpoint_transposed: bool = False,
123 n_shared_experts: int | None = None,
124 + fuse_shared_experts: bool = False,
125 router_logits_dtype: torch.dtype | None = None,
126 gate: torch.nn.Module | None = None,
127 shared_experts: torch.nn.Module | None = None,
@@ -198,6 +188,7 @@ def FusedMoEFactory(
188 block scales use transposed storage.
189 n_shared_experts: Number of shared experts to fuse into the routed
190 grouped GEMM (ROCm; requires aiter FSE or the router-append path)
191 + fuse_shared_experts: Whether to enable shared-expert fusion.
192 router_logits_dtype: Data type for router logits buffers
193 gate: Pre-configured gate module
194 shared_experts: Pre-configured shared experts module
@@ -244,7 +235,7 @@ def FusedMoEFactory(
235 num_experts,
236 num_redundant_experts,
237 n_shared_experts,
247 - is_act_and_mul,
238 + fuse_shared_experts,
239 )
240 )
241
vllm/model_executor/layers/fused_moe/utils.py
+85
@@ -1,15 +1,21 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import functools
4 +from collections.abc import Iterable
5 from math import prod
6 from typing import TYPE_CHECKING
7
8 import torch
9 import torch.nn.functional as F
10 +from torch import nn
11
12 import vllm.envs as envs
13 from vllm import _custom_ops as ops
14 +from vllm._aiter_ops import rocm_aiter_ops
15 from vllm.logger import init_logger
16 +from vllm.model_executor.layers.quantization.utils.config_utils import (
17 + is_shared_expert_quant_fse_compatible,
18 +)
19 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
20 per_token_group_quant_fp8,
21 )
@@ -34,16 +40,95 @@ from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import
40 from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
41 per_tensor_dequantize,
42 )
43 +from vllm.model_executor.models.utils import PPMissingLayer
44 from vllm.platforms import current_platform
45 from vllm.triton_utils import tl, triton
46 from vllm.utils.math_utils import cdiv
47
48 if TYPE_CHECKING:
49 from vllm.model_executor.layers.fused_moe.config import FusedMoEConfig
50 + from vllm.model_executor.layers.quantization import QuantizationConfig
51
52 logger = init_logger(__name__)
53
54
55 +def resolve_layer_fused_shared_expert(
56 + quant_config: "QuantizationConfig | None",
57 + prefix: str,
58 + shared_expert_name: str = "shared_experts",
59 +) -> bool:
60 + """Resolve whether AITER fused shared-expert execution is enabled.
61 +
62 + Args:
63 + quant_config: Model quantization configuration.
64 + prefix: MoE module prefix.
65 + shared_expert_name: Shared-expert module name under ``prefix``.
66 +
67 + Returns:
68 + Whether AITER fused shared experts are enabled.
69 +
70 + Raises:
71 + ValueError: If requested shared-expert fusion is quantization-incompatible.
72 + """
73 + # NOTE: is_fusion_moe_shared_experts_enabled is decorated with @if_aiter_supported
74 + # that returns None if AITER is not available.
75 + fse_requested = bool(rocm_aiter_ops.is_fusion_moe_shared_experts_enabled())
76 + fse_compatible, fse_reason = (
77 + is_shared_expert_quant_fse_compatible(
78 + quant_config,
79 + f"{prefix}.experts",
80 + f"{prefix}.{shared_expert_name}",
81 + )
82 + if fse_requested
83 + else (True, None)
84 + )
85 + is_fused_shared_expert_enabled = fse_requested and fse_compatible
86 + if fse_requested and not is_fused_shared_expert_enabled:
87 + logger.warning(
88 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled but "
89 + "cannot be enabled: %s.",
90 + fse_reason,
91 + )
92 + return is_fused_shared_expert_enabled
93 +
94 +
95 +def is_model_fused_shared_expert_compatible(
96 + layers: nn.ModuleList | Iterable[nn.Module],
97 + moe_cls: type[nn.Module],
98 + moe_name: str,
99 +) -> bool:
100 + """Resolve one fused-shared-expert state for a model's MoE layers."""
101 +
102 + def get_moe_layer(layer: nn.Module) -> nn.Module | None:
103 + for name in moe_name.split("."):
104 + layer = getattr(layer, name, None)
105 + if layer is None:
106 + return None
107 + return layer
108 +
109 + moe_layers = (
110 + moe_layer
111 + for layer in layers
112 + if not isinstance(layer, PPMissingLayer)
113 + and (moe_layer := get_moe_layer(layer)) is not None
114 + and isinstance(moe_layer, moe_cls)
115 + )
116 +
117 + enabled = [
118 + getattr(layer, "is_fused_shared_expert_enabled", False) for layer in moe_layers
119 + ]
120 + enabled_count = sum(enabled)
121 + disabled_count = len(enabled) - enabled_count
122 + if enabled_count > 0 and disabled_count > 0:
123 + raise NotImplementedError(
124 + "Fused shared experts must be enabled for all MoE layers; found "
125 + f"{enabled_count} enabled and {disabled_count} disabled layers. "
126 + "Per-layer fused shared experts is not yet supported. Please open "
127 + "an issue."
128 + )
129 + return enabled_count > 0 and disabled_count == 0
130 +
131 +
132 @triton.jit
133 def _count_expert_num_tokens(
134 topk_ids_ptr,
vllm/model_executor/layers/quantization/quark/quark.py
+65 -37
@@ -576,63 +576,91 @@ class QuarkConfig(QuantizationConfig):
576
577 return True
578
579 - def _find_matched_config(
580 - self, layer_name: str, module: torch.nn.Module
581 - ) -> dict[str, Any]:
579 + def get_layer_quant_config_from_name(
580 + self, layer_name: str
581 + ) -> dict[str, Any] | None:
582 proj_name = layer_name.split(".")[-1]
583 if proj_name in self.packed_modules_mapping:
584 shard_proj_names = self.packed_modules_mapping[proj_name]
585 -
586 - # Convert fused_name --> [shard_names]
587 - shard_names = [
588 - layer_name.replace(proj_name, shard_proj_name)
589 - for shard_proj_name in shard_proj_names
590 - ]
591 -
585 shard_configs = []
593 - for shard_name in shard_names:
594 - if shard_name == layer_name:
595 - config = cast(
596 - dict[str, Any], self.quant_config.get("global_quant_config")
597 - )
586 + for shard_proj_name in shard_proj_names:
587 + shard_name = layer_name.replace(proj_name, shard_proj_name)
588 + if shard_name != layer_name:
589 + config = self.get_layer_quant_config_from_name(shard_name)
590 else:
599 - config = self._find_matched_config(shard_name, module)
591 + config = None
592 shard_configs.append(config)
593
602 - if not all(
603 - deep_compare(q_config, shard_configs[0]) for q_config in shard_configs
594 + matched_configs = [config for config in shard_configs if config is not None]
595 + if matched_configs and not all(
596 + deep_compare(config, matched_configs[0]) for config in matched_configs
597 ):
598 raise ValueError(
599 f"Found a different quantization configuration for "
600 f"{shard_proj_names} in {layer_name}. vLLM "
601 "requires all to use the same scheme."
602 )
610 - return shard_configs[0]
603 + if matched_configs:
604 + return matched_configs[0]
605 + return None
606 else:
607 layer_quant_config = cast(
613 - dict[str, Any], self.quant_config.get("layer_quant_config")
608 + dict[str, Any], self.quant_config.get("layer_quant_config") or {}
609 )
615 -
616 - def _matches_pattern(layer_name, pattern):
617 - if "*" not in pattern:
618 - return layer_name in pattern
619 - return fnmatch.fnmatch(layer_name, pattern)
620 -
610 for name_pattern, config in layer_quant_config.items():
622 - if _matches_pattern(layer_name, name_pattern):
611 + if "*" not in name_pattern:
612 + matches = layer_name in name_pattern
613 + else:
614 + matches = fnmatch.fnmatch(layer_name, name_pattern)
615 + if matches:
616 return config
617 + return None
618
625 - layer_type = cast(str, type(module))
626 - layer_type_quant_config = cast(
627 - dict[str, Any], self.quant_config.get("layer_type_quant_config")
628 - )
629 - if layer_type in layer_type_quant_config:
630 - return layer_type_quant_config[layer_type]
619 + def _find_matched_config(
620 + self, layer_name: str, module: torch.nn.Module
621 + ) -> dict[str, Any]:
622 + # Priority order:
623 + # 1. layer_quant_config,
624 + # 2. layer_type_quant_config,
625 + # 3. global_quant_config.
626 +
627 + layer_type = cast(str, type(module))
628 + layer_type_quant_config = cast(
629 + dict[str, Any], self.quant_config.get("layer_type_quant_config")
630 + )
631 + global_quant_config = cast(
632 + dict[str, Any], self.quant_config.get("global_quant_config")
633 + )
634 + fallback_config = layer_type_quant_config.get(layer_type, global_quant_config)
635
632 - global_quant_config = cast(
633 - dict[str, Any], self.quant_config.get("global_quant_config")
634 - )
635 - return global_quant_config
636 + proj_name = layer_name.split(".")[-1]
637 + if proj_name in self.packed_modules_mapping:
638 + shard_proj_names = self.packed_modules_mapping[proj_name]
639 + shard_configs = []
640 + for shard_proj_name in shard_proj_names:
641 + shard_name = layer_name.replace(proj_name, shard_proj_name)
642 + if shard_name == layer_name:
643 + config = fallback_config
644 + else:
645 + config = self.get_layer_quant_config_from_name(shard_name)
646 + if config is None:
647 + config = fallback_config
648 + shard_configs.append(config)
649 +
650 + if not all(
651 + deep_compare(config, shard_configs[0]) for config in shard_configs
652 + ):
653 + raise ValueError(
654 + f"Found a different quantization configuration for "
655 + f"{shard_proj_names} in {layer_name}. vLLM requires all "
656 + "to use the same scheme."
657 + )
658 + return shard_configs[0]
659 + else:
660 + layer_quant_config = self.get_layer_quant_config_from_name(layer_name)
661 + if layer_quant_config is not None:
662 + return layer_quant_config
663 + return fallback_config
664
665 def _get_scheme_from_config(
666 self, config: dict[str, Any], dynamic_mxfp4_quant: bool = False
vllm/model_executor/layers/quantization/utils/config_utils.py new
+155
@@ -0,0 +1,155 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +from typing import TYPE_CHECKING
5 +
6 +import regex as re
7 +
8 +if TYPE_CHECKING:
9 + from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
10 +
11 +
12 +def is_shared_expert_quant_fse_compatible(
13 + quant_config: "QuantizationConfig | None",
14 + expert_prefix: str,
15 + shared_expert_prefix: str,
16 + projection_names: list[str] | None = None,
17 +) -> tuple[bool, str | None]:
18 + """Check whether quantization permits fused shared-expert execution.
19 +
20 + Args:
21 + quant_config: Model quantization configuration.
22 + expert_prefix: Routed-expert module prefix.
23 + shared_expert_prefix: Shared-expert module prefix.
24 + projection_names: Shared-expert projection names.
25 +
26 + Returns:
27 + A compatibility flag and, when incompatible, the reason.
28 + """
29 + if projection_names is None:
30 + projection_names = ["gate_up_proj", "down_proj"]
31 +
32 + if quant_config is None:
33 + return True, None
34 +
35 + from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
36 + from vllm.models.deepseek_v4.quant_config import DeepseekV4FP8Config
37 +
38 + if isinstance(quant_config, DeepseekV4FP8Config):
39 + from vllm.config import get_current_vllm_config
40 + from vllm.model_executor.models.utils import extract_layer_index
41 +
42 + if quant_config.expert_dtype != "fp4":
43 + return False, "DeepSeek-V4 routed experts are not MXFP4"
44 +
45 + hf_config = get_current_vllm_config().model_config.hf_config
46 +
47 + # TODO: This is adapted from former `_shared_experts_are_fp4`, and
48 + # needs to be cleaned up this . There should not be Quark-specific
49 + # logic in DeepseekV4FP8Config.
50 + quantization_config = getattr(hf_config, "quantization_config", None)
51 + if quantization_config is None:
52 + return False, "DeepSeek-V4 has no quantization configuration"
53 +
54 + if not quant_config._is_quark_mxfp4_ocp(quantization_config):
55 + return False, "DeepSeek-v4 FSE is only implemented/tested with Quark MXFP4"
56 +
57 + layer_idx = extract_layer_index(shared_expert_prefix)
58 + if layer_idx >= hf_config.num_hidden_layers:
59 + shared_expert_prefix = (
60 + f"mtp.{layer_idx - hf_config.num_hidden_layers}.ffn.shared_experts"
61 + )
62 + else:
63 + shared_expert_prefix = f"layers.{layer_idx}.ffn.shared_experts"
64 +
65 + if any(
66 + entry.startswith(shared_expert_prefix)
67 + for entry in quantization_config.get("exclude") or []
68 + if isinstance(entry, str)
69 + ):
70 + return (
71 + False,
72 + f"DeepSeek-V4 excludes shared experts at {shared_expert_prefix}",
73 + )
74 +
75 + shared_expert_weight_name = f"{shared_expert_prefix}.w1"
76 + layer_quant_config = quantization_config.get("layer_quant_config") or {}
77 + layer_config = layer_quant_config.get(shared_expert_weight_name)
78 + if layer_config is None:
79 + layer_config = next(
80 + (
81 + config
82 + for pattern, config in layer_quant_config.items()
83 + if isinstance(pattern, str)
84 + and pattern.startswith("re:")
85 + and re.fullmatch(
86 + pattern.removeprefix("re:"), shared_expert_weight_name
87 + )
88 + ),
89 + None,
90 + )
91 + shared_weight_config = (
92 + layer_config or quantization_config.get("global_quant_config") or {}
93 + ).get("weight") or {}
94 + if shared_weight_config.get("dtype") == "fp4":
95 + return True, None
96 + return (
97 + False,
98 + f"DeepSeek-V4 shared experts at {shared_expert_prefix} are not MXFP4",
99 + )
100 +
101 + if isinstance(quant_config, QuarkConfig):
102 + # TODO: layer_type_quant_config is not taken into account here.
103 + assert "exclude" in quant_config.quant_config
104 + assert "global_quant_config" in quant_config.quant_config
105 +
106 + is_compatible = not any(
107 + "shared_expert" in str(entry)
108 + for entry in quant_config.quant_config["exclude"]
109 + )
110 + if not is_compatible:
111 + return False, f"Quark excludes shared experts at {shared_expert_prefix}"
112 +
113 + global_quant_config = quant_config.quant_config["global_quant_config"]
114 +
115 + def get_projection_quant_configs(layer_name: str) -> list[object]:
116 + module_prefix, _, projection_name = layer_name.rpartition(".")
117 + packed_projection_names = quant_config.packed_modules_mapping.get(
118 + projection_name, [projection_name]
119 + )
120 + return [
121 + quant_config.get_layer_quant_config_from_name(
122 + f"{module_prefix}.{packed_projection_name}"
123 + )
124 + or global_quant_config
125 + for packed_projection_name in packed_projection_names
126 + ]
127 +
128 + expert_quant_config = (
129 + quant_config.get_layer_quant_config_from_name(expert_prefix)
130 + or global_quant_config
131 + )
132 + shared_expert_quant_configs = [
133 + config
134 + for projection_name in projection_names
135 + for config in get_projection_quant_configs(
136 + f"{shared_expert_prefix}.{projection_name}"
137 + )
138 + ]
139 + if all(config == expert_quant_config for config in shared_expert_quant_configs):
140 + return True, None
141 + return (
142 + False,
143 + "Quark uses different quantization configurations for routed and "
144 + f"shared experts at {shared_expert_prefix}",
145 + )
146 +
147 + # TODO: Extend FSE support detection to other quantization methods. Typically,
148 + # one would check that the experts and shared_experts use the same
149 + # quantization config. This may be refactored as part of QuantizationConfig later.
150 +
151 + return (
152 + False,
153 + "shared-expert FSE quantization compatibility is not implemented for "
154 + f"{type(quant_config).__name__}",
155 + )
vllm/model_executor/models/AXK1.py
+22 -10
@@ -46,6 +46,10 @@ from vllm.model_executor.layers.fused_moe import (
46 FusedMoEFactory,
47 fused_moe_make_expert_params_mapping,
48 )
49 +from vllm.model_executor.layers.fused_moe.utils import (
50 + is_model_fused_shared_expert_compatible,
51 + resolve_layer_fused_shared_expert,
52 +)
53 from vllm.model_executor.layers.layernorm import RMSNorm
54 from vllm.model_executor.layers.linear import (
55 ColumnParallelLinear,
@@ -148,10 +152,14 @@ class AXK1MoE(nn.Module):
152 self.n_local_physical_experts = self.n_physical_experts // self.ep_size
153
154 self.is_rocm_aiter_moe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
151 - self.is_fusion_moe_shared_experts_enabled = (
152 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
153 - )
154 - if config.n_shared_experts is None or self.is_fusion_moe_shared_experts_enabled:
155 +
156 + self.is_fused_shared_expert_enabled = False
157 + if config.n_shared_experts is not None:
158 + self.is_fused_shared_expert_enabled = resolve_layer_fused_shared_expert(
159 + quant_config, prefix
160 + )
161 +
162 + if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
163 self.shared_experts = None
164 else:
165 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -189,8 +197,9 @@ class AXK1MoE(nn.Module):
197 num_redundant_experts=self.n_redundant_experts,
198 is_sequence_parallel=self.is_sequence_parallel,
199 n_shared_experts=config.n_shared_experts
192 - if self.is_fusion_moe_shared_experts_enabled
200 + if self.is_fused_shared_expert_enabled
201 else None,
202 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
203 )
204
205 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -725,6 +734,12 @@ class AXK1Model(nn.Module):
734 prefix=f"{prefix}.layers",
735 )
736
737 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
738 + self.layers,
739 + AXK1MoE,
740 + "mlp",
741 + )
742 +
743 if get_pp_group().is_last_rank:
744 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
745 else:
@@ -789,9 +804,6 @@ class AXK1Model(nn.Module):
804 return hidden_states
805
806 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
792 - rocm_aiter_moe_shared_expert_enabled = (
793 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
794 - )
807 stacked_params_mapping: list[tuple[str, str, int | str]] = [
808 # (param_name, shard_name, shard_id)
809 ("gate_up_proj", "gate_proj", 0),
@@ -822,7 +834,7 @@ class AXK1Model(nn.Module):
834 num_experts=self.config.n_routed_experts
835 + (
836 (self.config.n_shared_experts or 0)
825 - if rocm_aiter_moe_shared_expert_enabled
837 + if self.is_fused_shared_expert_enabled
838 else 0
839 ),
840 num_redundant_experts=self.num_redundant_experts,
@@ -839,7 +851,7 @@ class AXK1Model(nn.Module):
851 continue # skip spec decode layers for main model
852
853 is_fusion_moe_shared_experts_layer = (
842 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
854 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
855 )
856
857 for param_name, weight_name, shard_id in stacked_params_mapping:
vllm/model_executor/models/deepseek_mtp.py
+10 -6
@@ -7,13 +7,15 @@ import torch
7 import torch.nn as nn
8 from transformers import PretrainedConfig
9
10 -from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.compilation.decorators import support_torch_compile
11 from vllm.config import VllmConfig
12 from vllm.distributed import tensor_model_parallel_all_gather
13 from vllm.model_executor.layers.fused_moe import (
14 fused_moe_make_expert_params_mapping,
15 )
16 +from vllm.model_executor.layers.fused_moe.utils import (
17 + is_model_fused_shared_expert_compatible,
18 +)
19 from vllm.model_executor.layers.layernorm import RMSNorm
20 from vllm.model_executor.layers.logits_processor import LogitsProcessor
21 from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -255,6 +257,11 @@ class DeepSeekMTP(nn.Module, DeepseekV2MixtureOfExperts):
257 self.moe_mlp_layers.append(layer.mlp)
258 self.moe_layers.append(layer.mlp.experts)
259 self.extract_moe_parameters(example_moe)
260 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
261 + self.model.layers.values(),
262 + DeepseekV2MoE,
263 + "mtp_block.mlp",
264 + )
265
266 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
267 return self.model.embed_input_ids(input_ids)
@@ -285,9 +292,6 @@ class DeepSeekMTP(nn.Module, DeepseekV2MixtureOfExperts):
292 return self.model.compute_logits(hidden_states, spec_step_idx)
293
294 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
288 - rocm_aiter_moe_shared_expert_enabled = (
289 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
290 - )
295 stacked_params_mapping = [
296 ("gate_up_proj", "gate_proj", 0),
297 ("gate_up_proj", "up_proj", 1),
@@ -310,7 +314,7 @@ class DeepSeekMTP(nn.Module, DeepseekV2MixtureOfExperts):
314 num_experts=self.config.n_routed_experts
315 + (
316 self.config.n_shared_experts
313 - if rocm_aiter_moe_shared_expert_enabled
317 + if self.is_fused_shared_expert_enabled
318 else 0
319 ),
320 )
@@ -326,7 +330,7 @@ class DeepSeekMTP(nn.Module, DeepseekV2MixtureOfExperts):
330 if spec_layer is None:
331 continue
332 is_fusion_moe_shared_experts_layer = (
329 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
333 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
334 )
335 name = self._rewrite_spec_layer_name(spec_layer, name)
336
vllm/model_executor/models/deepseek_v2.py
+22 -10
@@ -54,6 +54,10 @@ from vllm.model_executor.layers.fused_moe import (
54 GateLinear,
55 fused_moe_make_expert_params_mapping,
56 )
57 +from vllm.model_executor.layers.fused_moe.utils import (
58 + is_model_fused_shared_expert_compatible,
59 + resolve_layer_fused_shared_expert,
60 +)
61 from vllm.model_executor.layers.layernorm import LayerNorm, RMSNorm
62 from vllm.model_executor.layers.linear import (
63 ColumnParallelLinear,
@@ -329,9 +333,13 @@ class DeepseekV2MoE(nn.Module):
333 self.n_local_physical_experts = self.n_physical_experts // self.ep_size
334
335 self.is_rocm_aiter_moe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
332 - self.is_fusion_moe_shared_experts_enabled = (
333 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
334 - )
336 +
337 + self.is_fused_shared_expert_enabled = False
338 + if config.n_shared_experts is not None:
339 + self.is_fused_shared_expert_enabled = resolve_layer_fused_shared_expert(
340 + quant_config, prefix
341 + )
342 +
343 if (
344 self.is_rocm_aiter_moe_enabled
345 and self.gate.e_score_correction_bias is not None
@@ -340,7 +348,7 @@ class DeepseekV2MoE(nn.Module):
348 # Accumulates in fp32; avoids bf16->fp32 cast.
349 self.gate.set_out_dtype(self.gate.weight.dtype)
350
343 - if config.n_shared_experts is None or self.is_fusion_moe_shared_experts_enabled:
351 + if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
352 self.shared_experts = None
353 else:
354 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -377,8 +385,9 @@ class DeepseekV2MoE(nn.Module):
385 is_sequence_parallel=self.is_sequence_parallel,
386 reduce_results=reduce_results,
387 n_shared_experts=config.n_shared_experts
380 - if self.is_fusion_moe_shared_experts_enabled
388 + if self.is_fused_shared_expert_enabled
389 else None,
390 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
391 router_logits_dtype=self.gate.out_dtype,
392 )
393
@@ -1401,6 +1410,12 @@ class DeepseekV2Model(nn.Module):
1410 prefix=f"{prefix}.layers",
1411 )
1412
1413 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
1414 + self.layers,
1415 + DeepseekV2MoE,
1416 + "mlp",
1417 + )
1418 +
1419 if get_pp_group().is_last_rank:
1420 self.norm = RMSNorm(self.hidden_size, eps=config.rms_norm_eps)
1421 else:
@@ -1511,9 +1526,6 @@ class DeepseekV2Model(nn.Module):
1526 return hidden_states
1527
1528 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1514 - rocm_aiter_moe_shared_expert_enabled = (
1515 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
1516 - )
1529 stacked_params_mapping: list[tuple[str, str, int | str]] = [
1530 # (param_name, shard_name, shard_id)
1531 ("gate_up_proj", "gate_proj", 0),
@@ -1554,7 +1566,7 @@ class DeepseekV2Model(nn.Module):
1566 num_experts=self.config.n_routed_experts
1567 + (
1568 self.config.n_shared_experts
1557 - if rocm_aiter_moe_shared_expert_enabled
1569 + if self.is_fused_shared_expert_enabled
1570 else 0
1571 ),
1572 num_redundant_experts=self.num_redundant_experts,
@@ -1582,7 +1594,7 @@ class DeepseekV2Model(nn.Module):
1594 continue # this layer has no indexer; drop its checkpoint weights
1595
1596 is_fusion_moe_shared_experts_layer = (
1585 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
1597 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
1598 )
1599
1600 if _try_load_fp8_indexer_wk(
vllm/model_executor/models/glm4_moe.py
+20 -12
@@ -39,12 +39,15 @@ from vllm.distributed import (
39 get_pp_group,
40 get_tensor_model_parallel_world_size,
41 )
42 -from vllm.logger import init_logger
42 from vllm.model_executor.layers.activation import SiluAndMul
43 from vllm.model_executor.layers.attention import Attention
44 from vllm.model_executor.layers.fused_moe import (
45 FusedMoEFactory,
46 )
47 +from vllm.model_executor.layers.fused_moe.utils import (
48 + is_model_fused_shared_expert_compatible,
49 + resolve_layer_fused_shared_expert,
50 +)
51 from vllm.model_executor.layers.layernorm import RMSNorm
52 from vllm.model_executor.layers.linear import (
53 MergedColumnParallelLinear,
@@ -72,8 +75,6 @@ from .utils import (
75 skip_spec_layers,
76 )
77
75 -logger = init_logger(__name__)
76 -
78
79 class Glm4MoeMLP(nn.Module):
80 def __init__(
@@ -159,14 +160,15 @@ class Glm4MoE(nn.Module):
160 self.n_physical_experts = self.n_logical_experts + self.n_redundant_experts
161 self.n_local_physical_experts = self.n_physical_experts // self.ep_size
162
162 - # AITER fused shared-expert (FSE) gate; mirrors the deepseek_v2.py
163 - # pattern (see Glm4MoE / MoERunner wiring there).
163 self.is_rocm_aiter_moe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
165 - self.is_fusion_moe_shared_experts_enabled = (
166 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
167 - )
164
169 - if config.n_shared_experts is None or self.is_fusion_moe_shared_experts_enabled:
165 + self.is_fused_shared_expert_enabled = False
166 + if config.n_shared_experts is not None:
167 + self.is_fused_shared_expert_enabled = resolve_layer_fused_shared_expert(
168 + quant_config, prefix
169 + )
170 +
171 + if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
172 self.shared_experts = None
173 else:
174 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -200,10 +202,9 @@ class Glm4MoE(nn.Module):
202 num_redundant_experts=self.n_redundant_experts,
203 router_logits_dtype=torch.float32,
204 n_shared_experts=(
203 - config.n_shared_experts
204 - if self.is_fusion_moe_shared_experts_enabled
205 - else None
205 + config.n_shared_experts if self.is_fused_shared_expert_enabled else None
206 ),
207 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
208 )
209
210 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -443,6 +444,12 @@ class Glm4MoeModel(nn.Module):
444 prefix=f"{prefix}.layers",
445 )
446
447 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
448 + self.layers,
449 + Glm4MoE,
450 + "mlp",
451 + )
452 +
453 if get_pp_group().is_last_rank:
454 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
455 else:
@@ -488,6 +495,7 @@ class Glm4MoeModel(nn.Module):
495 skip_spec_layers(weights, self.config),
496 n_routed_experts=self.config.n_routed_experts,
497 n_shared_experts=self.config.n_shared_experts or 1,
498 + enabled=self.is_fused_shared_expert_enabled,
499 )
500 loader = AutoWeightsLoader(self)
501 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
vllm/model_executor/models/glm4_moe_lite.py
+11 -6
@@ -34,7 +34,6 @@ from torch import nn
34 if TYPE_CHECKING:
35 from transformers.models.glm4_moe_lite import Glm4MoeLiteConfig
36
37 -from vllm._aiter_ops import rocm_aiter_ops
37 from vllm.compilation.decorators import support_torch_compile
38 from vllm.config import VllmConfig
39 from vllm.distributed import (
@@ -44,6 +43,9 @@ from vllm.logger import init_logger
43 from vllm.model_executor.layers.fused_moe import (
44 fused_moe_make_expert_params_mapping,
45 )
46 +from vllm.model_executor.layers.fused_moe.utils import (
47 + is_model_fused_shared_expert_compatible,
48 +)
49 from vllm.model_executor.layers.layernorm import RMSNorm
50 from vllm.model_executor.layers.logits_processor import LogitsProcessor
51 from vllm.model_executor.layers.vocab_parallel_embedding import (
@@ -254,6 +256,12 @@ class Glm4MoeLiteModel(nn.Module):
256 prefix=f"{prefix}.layers",
257 )
258
259 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
260 + self.layers,
261 + Glm4MoeLite,
262 + "mlp",
263 + )
264 +
265 if get_pp_group().is_last_rank:
266 self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
267 else:
@@ -320,9 +328,6 @@ class Glm4MoeLiteModel(nn.Module):
328 )
329
330 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
323 - rocm_aiter_moe_shared_expert_enabled = (
324 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
325 - )
331 stacked_params_mapping = [
332 # (param_name, shard_name, shard_id)
333 ("gate_up_proj", "gate_proj", 0),
@@ -345,7 +350,7 @@ class Glm4MoeLiteModel(nn.Module):
350 num_experts=self.config.n_routed_experts
351 + (
352 self.config.n_shared_experts
348 - if rocm_aiter_moe_shared_expert_enabled
353 + if self.is_fused_shared_expert_enabled
354 else 0
355 ),
356 )
@@ -361,7 +366,7 @@ class Glm4MoeLiteModel(nn.Module):
366 continue # skip spec decode layers for main model
367
368 is_fusion_moe_shared_experts_layer = (
364 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
369 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
370 )
371
372 for param_name, weight_name, shard_id in stacked_params_mapping:
vllm/model_executor/models/glm4_moe_lite_mtp.py
+10 -6
@@ -30,12 +30,14 @@ import torch
30 import torch.nn as nn
31 from transformers import PretrainedConfig
32
33 -from vllm._aiter_ops import rocm_aiter_ops
33 from vllm.config import VllmConfig
34 from vllm.model_executor.layers.fused_moe import (
35 MoERunner,
36 fused_moe_make_expert_params_mapping,
37 )
38 +from vllm.model_executor.layers.fused_moe.utils import (
39 + is_model_fused_shared_expert_compatible,
40 +)
41 from vllm.model_executor.layers.layernorm import RMSNorm
42 from vllm.model_executor.layers.logits_processor import LogitsProcessor
43 from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -224,6 +226,11 @@ class Glm4MoeLiteMTP(nn.Module, SupportsPP, Glm4MixtureOfExperts):
226 self.moe_mlp_layers.append(layer.mlp)
227 self.moe_layers.append(layer.mlp.experts)
228 self.extract_moe_parameters(example_moe)
229 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
230 + self.model.layers.values(),
231 + Glm4MoeLite,
232 + "mtp_block.mlp",
233 + )
234
235 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
236 return self.model.embed_input_ids(input_ids)
@@ -250,9 +257,6 @@ class Glm4MoeLiteMTP(nn.Module, SupportsPP, Glm4MixtureOfExperts):
257 return self.model.compute_logits(hidden_states, spec_step_idx)
258
259 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
253 - rocm_aiter_moe_shared_expert_enabled = (
254 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
255 - )
260 stacked_params_mapping = [
261 ("gate_up_proj", "gate_proj", 0),
262 ("gate_up_proj", "up_proj", 1),
@@ -268,7 +272,7 @@ class Glm4MoeLiteMTP(nn.Module, SupportsPP, Glm4MixtureOfExperts):
272 num_experts=self.config.n_routed_experts
273 + (
274 self.config.n_shared_experts
271 - if rocm_aiter_moe_shared_expert_enabled
275 + if self.is_fused_shared_expert_enabled
276 else 0
277 ),
278 )
@@ -282,7 +286,7 @@ class Glm4MoeLiteMTP(nn.Module, SupportsPP, Glm4MixtureOfExperts):
286 if spec_layer is None:
287 continue
288 is_fusion_moe_shared_experts_layer = (
285 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
289 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
290 )
291 name = self._rewrite_spec_layer_name(spec_layer, name)
292 for param_name, weight_name, shard_id in stacked_params_mapping:
vllm/model_executor/models/glm4_moe_mtp.py
+10 -7
@@ -31,12 +31,14 @@ import torch
31 import torch.nn as nn
32 from transformers import PretrainedConfig
33
34 -from vllm._aiter_ops import rocm_aiter_ops
34 from vllm.config import CacheConfig, ParallelConfig, VllmConfig
35 from vllm.model_executor.layers.fused_moe import (
36 MoERunner,
37 fused_moe_make_expert_params_mapping,
38 )
39 +from vllm.model_executor.layers.fused_moe.utils import (
40 + is_model_fused_shared_expert_compatible,
41 +)
42 from vllm.model_executor.layers.layernorm import RMSNorm
43 from vllm.model_executor.layers.logits_processor import LogitsProcessor
44 from vllm.model_executor.layers.quantization import QuantizationConfig
@@ -212,6 +214,11 @@ class Glm4MoeMTP(nn.Module, Glm4MixtureOfExperts):
214 self.moe_mlp_layers.append(layer.mlp)
215 self.moe_layers.append(layer.mlp.experts)
216 self.extract_moe_parameters(example_moe)
217 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
218 + self.model.layers.values(),
219 + Glm4MoE,
220 + "mtp_block.mlp",
221 + )
222
223 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
224 return self.model.embed_input_ids(input_ids)
@@ -238,10 +245,6 @@ class Glm4MoeMTP(nn.Module, Glm4MixtureOfExperts):
245 return self.model.compute_logits(hidden_states, spec_step_idx)
246
247 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
241 - # FSE weight loading mirrors glm4_moe.py / deepseek_mtp.py.
242 - rocm_aiter_moe_shared_expert_enabled = (
243 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
244 - )
248 stacked_params_mapping = [
249 # (param_name, shard_name, shard_id)
250 ("qkv_proj", "q_proj", "q"),
@@ -254,7 +257,7 @@ class Glm4MoeMTP(nn.Module, Glm4MixtureOfExperts):
257 # Params for weights, fp8 weight scales, fp8 activation scales
258 # (param_name, weight_name, expert_id, shard_id)
259 num_experts = self.config.n_routed_experts
257 - if rocm_aiter_moe_shared_expert_enabled and self.config.n_shared_experts:
260 + if self.is_fused_shared_expert_enabled and self.config.n_shared_experts:
261 num_experts += self.config.n_shared_experts
262 expert_params_mapping = fused_moe_make_expert_params_mapping(
263 self,
@@ -279,7 +282,7 @@ class Glm4MoeMTP(nn.Module, Glm4MixtureOfExperts):
282 name = self._rewrite_spec_layer_name(spec_layer, name)
283
284 is_fusion_moe_shared_experts_layer = (
282 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
285 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
286 )
287
288 for param_name, weight_name, shard_id in stacked_params_mapping:
vllm/model_executor/models/qwen3_5.py
+9 -4
@@ -29,13 +29,15 @@ from collections.abc import Iterable
29 import torch
30 from torch import nn
31
32 -from vllm._aiter_ops import rocm_aiter_ops
32 from vllm.compilation.decorators import support_torch_compile
33 from vllm.config import VllmConfig
34 from vllm.distributed import (
35 get_pp_group,
36 )
37 from vllm.logger import init_logger
38 +from vllm.model_executor.layers.fused_moe.utils import (
39 + is_model_fused_shared_expert_compatible,
40 +)
41 from vllm.model_executor.layers.layernorm import GemmaRMSNorm as Qwen3_5RMSNorm
42 from vllm.model_executor.layers.logits_processor import LogitsProcessor
43 from vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn import (
@@ -78,7 +80,6 @@ from .qwen3_next import (
80 Qwen3NextModel,
81 Qwen3NextSparseMoeBlock,
82 QwenNextMixtureOfExperts,
81 - _is_shared_expert_fse_compatible,
83 )
84 from .qwen3_vl import (
85 Qwen3_VisionTransformer,
@@ -256,6 +257,11 @@ class Qwen3_5Model(Qwen3NextModel):
257 self.start_layer, self.end_layer, self.layers = make_layers(
258 config.num_hidden_layers, get_layer, prefix=f"{prefix}.layers"
259 )
260 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
261 + self.layers,
262 + Qwen3NextSparseMoeBlock,
263 + "mlp",
264 + )
265 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
266 ["hidden_states", "residual"], config.hidden_size
267 )
@@ -274,8 +280,7 @@ class Qwen3_5Model(Qwen3NextModel):
280 if "moe" in self.config.model_type:
281 weights = maybe_fuse_shared_experts(
282 weights,
277 - enabled=rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
278 - and _is_shared_expert_fse_compatible(self.quant_config),
283 + enabled=self.is_fused_shared_expert_enabled,
284 n_routed_experts=self.config.num_experts,
285 n_shared_experts=1,
286 ckpt_prefix="mlp.shared_expert",
vllm/model_executor/models/qwen3_5_mtp.py
+11 -7
@@ -7,11 +7,13 @@ from collections.abc import Iterable
7 import torch
8 from torch import nn
9
10 -from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.compilation.decorators import support_torch_compile
12 -from vllm.config import VllmConfig, get_current_vllm_config
11 +from vllm.config import VllmConfig
12 from vllm.distributed import get_pp_group, tensor_model_parallel_all_gather
13 from vllm.logger import init_logger
14 +from vllm.model_executor.layers.fused_moe.utils import (
15 + is_model_fused_shared_expert_compatible,
16 +)
17 from vllm.model_executor.layers.linear import ColumnParallelLinear
18 from vllm.model_executor.layers.logits_processor import LogitsProcessor
19 from vllm.model_executor.layers.vocab_parallel_embedding import (
@@ -25,8 +27,8 @@ from vllm.model_executor.models.qwen3_5 import (
27 Qwen3_5RMSNorm,
28 )
29 from vllm.model_executor.models.qwen3_next import (
30 + Qwen3NextSparseMoeBlock,
31 QwenNextMixtureOfExperts,
29 - _is_shared_expert_fse_compatible,
32 )
33 from vllm.model_executor.models.utils import sequence_parallel_chunk
34 from vllm.sequence import IntermediateTensors
@@ -122,6 +124,11 @@ class Qwen3_5MultiTokenPredictor(nn.Module):
124 for idx in range(self.num_mtp_layers)
125 )
126 vllm_config.quant_config = original_quant
127 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
128 + self.layers,
129 + Qwen3NextSparseMoeBlock,
130 + "mlp",
131 + )
132 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
133 ["hidden_states", "residual"], config.hidden_size
134 )
@@ -185,10 +192,7 @@ class Qwen3_5MultiTokenPredictor(nn.Module):
192 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
193 weights = maybe_fuse_shared_experts(
194 weights,
188 - enabled=rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
189 - and _is_shared_expert_fse_compatible(
190 - get_current_vllm_config().quant_config
191 - ),
195 + enabled=self.is_fused_shared_expert_enabled,
196 n_routed_experts=getattr(self.config, "num_experts", 0),
197 n_shared_experts=1,
198 ckpt_prefix="mlp.shared_expert",
vllm/model_executor/models/qwen3_next.py
+22 -32
@@ -8,7 +8,6 @@ from itertools import islice
8 import torch
9 from torch import nn
10
11 -from vllm._aiter_ops import rocm_aiter_ops
11 from vllm.compilation.decorators import support_torch_compile
12 from vllm.config import CacheConfig, ModelConfig, VllmConfig
13 from vllm.distributed import (
@@ -18,9 +17,12 @@ from vllm.distributed import (
17 tensor_model_parallel_all_gather,
18 tensor_model_parallel_reduce_scatter,
19 )
21 -from vllm.logger import init_logger
20 from vllm.model_executor.layers.attention import Attention
21 from vllm.model_executor.layers.fused_moe import FusedMoEFactory
22 +from vllm.model_executor.layers.fused_moe.utils import (
23 + is_model_fused_shared_expert_compatible,
24 + resolve_layer_fused_shared_expert,
25 +)
26 from vllm.model_executor.layers.fused_qk_norm_rope import fused_qk_rmsnorm_rope_gate
27 from vllm.model_executor.layers.layernorm import (
28 GemmaRMSNorm as Qwen3NextRMSNorm,
@@ -73,8 +75,6 @@ from .utils import (
75 maybe_prefix,
76 )
77
76 -logger = init_logger(__name__)
77 -
78 KVCache = tuple[torch.Tensor, torch.Tensor]
79
80
@@ -90,26 +90,6 @@ def _should_use_sequence_parallel(vllm_config: VllmConfig) -> bool:
90 )
91
92
93 -def _is_shared_expert_fse_compatible(quant_config) -> bool:
94 - """Check if shared expert can be fused with routed experts.
95 -
96 - FSE requires that shared and routed expert weights use the same
97 - quantization format. Returns False when the shared expert is
98 - excluded from quantization (e.g. float32 shared in an MXFP4 model)
99 - or has a different quant spec than routed experts.
100 - """
101 - if quant_config is None:
102 - return True
103 - # Quark stores its full config dict in quant_config.quant_config
104 - raw_config = getattr(quant_config, "quant_config", None)
105 - if not isinstance(raw_config, dict):
106 - return True
107 - exclude = raw_config.get("exclude", [])
108 - if not exclude:
109 - return True
110 - return not any("shared_expert." in str(e) for e in exclude)
111 -
112 -
93 class Qwen3NextSparseMoeBlock(nn.Module):
94 def __init__(self, vllm_config: VllmConfig, prefix: str = ""):
95 super().__init__()
@@ -157,15 +137,18 @@ class Qwen3NextSparseMoeBlock(nn.Module):
137 prefix=f"{prefix}.shared_expert_gate",
138 )
139
160 - _fse_requested = rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
161 - _fse_enabled = _fse_requested and _is_shared_expert_fse_compatible(quant_config)
162 - if _fse_requested and not _fse_enabled:
163 - logger.warning(
164 - "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled but "
165 - "shared expert has a different quantization spec than routed "
166 - "experts. Falling back to non-fused shared expert path."
140 + self.is_fused_shared_expert_enabled = False
141 + if config.shared_expert_intermediate_size > 0:
142 + self.is_fused_shared_expert_enabled = resolve_layer_fused_shared_expert(
143 + quant_config,
144 + prefix,
145 + shared_expert_name="shared_expert",
146 )
168 - if _fse_enabled or config.shared_expert_intermediate_size <= 0:
147 +
148 + if (
149 + self.is_fused_shared_expert_enabled
150 + or config.shared_expert_intermediate_size <= 0
151 + ):
152 self.shared_expert = None
153 else:
154 self.shared_expert = Qwen3NextMLP(
@@ -193,6 +176,7 @@ class Qwen3NextSparseMoeBlock(nn.Module):
176 num_redundant_experts=self.n_redundant_experts,
177 is_sequence_parallel=self.is_sequence_parallel,
178 n_shared_experts=1 if self.shared_expert is None else None,
179 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
180 shared_expert_gate=self.shared_expert_gate
181 if self.shared_expert is None
182 else None,
@@ -599,6 +583,11 @@ class Qwen3NextModel(nn.Module, EagleModelMixin):
583 self.start_layer, self.end_layer, self.layers = make_layers(
584 config.num_hidden_layers, get_layer, prefix=f"{prefix}.layers"
585 )
586 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
587 + self.layers,
588 + Qwen3NextSparseMoeBlock,
589 + "mlp",
590 + )
591 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
592 ["hidden_states", "residual"], config.hidden_size
593 )
@@ -678,6 +667,7 @@ class Qwen3NextModel(nn.Module, EagleModelMixin):
667 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
668 weights = maybe_fuse_shared_experts(
669 weights,
670 + enabled=self.is_fused_shared_expert_enabled,
671 n_routed_experts=getattr(self.config, "num_experts", 0),
672 n_shared_experts=1,
673 ckpt_prefix="mlp.shared_expert",
vllm/model_executor/models/qwen3_next_mtp.py
+10
@@ -11,6 +11,9 @@ from vllm.compilation.decorators import support_torch_compile
11 from vllm.config import VllmConfig
12 from vllm.distributed import get_pp_group, tensor_model_parallel_all_gather
13 from vllm.logger import init_logger
14 +from vllm.model_executor.layers.fused_moe.utils import (
15 + is_model_fused_shared_expert_compatible,
16 +)
17 from vllm.model_executor.layers.linear import ColumnParallelLinear
18 from vllm.model_executor.layers.logits_processor import LogitsProcessor
19 from vllm.model_executor.layers.vocab_parallel_embedding import (
@@ -21,6 +24,7 @@ from vllm.model_executor.models.qwen3_next import (
24 Qwen3NextDecoderLayer,
25 Qwen3NextModel,
26 Qwen3NextRMSNorm,
27 + Qwen3NextSparseMoeBlock,
28 QwenNextMixtureOfExperts,
29 )
30 from vllm.model_executor.models.utils import sequence_parallel_chunk
@@ -91,6 +95,11 @@ class Qwen3NextMultiTokenPredictor(nn.Module):
95 for idx in range(self.num_mtp_layers)
96 )
97
98 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
99 + self.layers,
100 + Qwen3NextSparseMoeBlock,
101 + "mlp",
102 + )
103 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
104 ["hidden_states", "residual"], config.hidden_size
105 )
@@ -155,6 +164,7 @@ class Qwen3NextMultiTokenPredictor(nn.Module):
164 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
165 weights = maybe_fuse_shared_experts(
166 weights,
167 + enabled=self.is_fused_shared_expert_enabled,
168 n_routed_experts=self.config.num_experts,
169 n_shared_experts=1,
170 ckpt_prefix="mlp.shared_expert",
vllm/models/deepseek_v32/amd/mtp.py
+10 -6
@@ -8,12 +8,14 @@ from collections.abc import Callable, Iterable
8 import torch
9 import torch.nn as nn
10
11 -from vllm._aiter_ops import rocm_aiter_ops
11 from vllm.config import VllmConfig
12 from vllm.distributed import tensor_model_parallel_all_reduce
13 from vllm.model_executor.layers.fused_moe import (
14 fused_moe_make_expert_params_mapping,
15 )
16 +from vllm.model_executor.layers.fused_moe.utils import (
17 + is_model_fused_shared_expert_compatible,
18 +)
19 from vllm.model_executor.layers.layernorm import RMSNorm
20 from vllm.model_executor.layers.logits_processor import LogitsProcessor
21 from vllm.model_executor.layers.vocab_parallel_embedding import (
@@ -167,6 +169,11 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
169 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
170 )
171 self.set_moe_parameters()
172 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
173 + self.model.layers.values(),
174 + DeepseekV2MoE,
175 + "mtp_block.mlp",
176 + )
177
178 def set_moe_parameters(self):
179 self.num_moe_layers = self.config.num_nextn_predict_layers
@@ -231,9 +238,6 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
238 return name
239
240 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
234 - rocm_aiter_moe_shared_expert_enabled = (
235 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
236 - )
241 stacked_params_mapping = [
242 ("gate_up_proj", "gate_proj", 0),
243 ("gate_up_proj", "up_proj", 1),
@@ -250,7 +254,7 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
254 num_experts=self.config.n_routed_experts
255 + (
256 self.config.n_shared_experts
253 - if rocm_aiter_moe_shared_expert_enabled
257 + if self.is_fused_shared_expert_enabled
258 else 0
259 ),
260 )
@@ -266,7 +270,7 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
270 if spec_layer is None:
271 continue
272 is_fusion_moe_shared_experts_layer = (
269 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
273 + self.is_fused_shared_expert_enabled and ("mlp.shared_experts" in name)
274 )
275 name = self._rewrite_spec_layer_name(spec_layer, name)
276
vllm/models/deepseek_v32/nvidia/mtp.py
+3 -43
@@ -7,7 +7,6 @@ import torch
7 import torch.nn as nn
8
9 import vllm.envs as envs
10 -from vllm._aiter_ops import rocm_aiter_ops
10 from vllm.config import VllmConfig
11 from vllm.forward_context import get_forward_context, is_forward_context_available
12 from vllm.model_executor.layers.fused_embed_norm import (
@@ -344,9 +343,6 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
343 return name
344
345 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
347 - rocm_aiter_moe_shared_expert_enabled = (
348 - rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
349 - )
346 stacked_params_mapping = [
347 ("gate_up_proj", "gate_proj", 0),
348 ("gate_up_proj", "up_proj", 1),
@@ -360,12 +356,7 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
356 ckpt_gate_proj_name="gate_proj",
357 ckpt_down_proj_name="down_proj",
358 ckpt_up_proj_name="up_proj",
363 - num_experts=self.config.n_routed_experts
364 - + (
365 - self.config.n_shared_experts
366 - if rocm_aiter_moe_shared_expert_enabled
367 - else 0
368 - ),
359 + num_experts=self.config.n_routed_experts,
360 )
361
362 pp_missing_layer_names = get_pp_missing_layer_names(self)
@@ -378,9 +369,6 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
369 spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
370 if spec_layer is None:
371 continue
381 - is_fusion_moe_shared_experts_layer = (
382 - rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
383 - )
372 name = self._rewrite_spec_layer_name(spec_layer, name)
373
374 if _try_load_fp8_indexer_wk(
@@ -398,8 +386,6 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
386 continue
387 if ("mlp.experts." in name) and name not in params_dict:
388 continue
401 - if is_fusion_moe_shared_experts_layer:
402 - continue
389 name_mapped = name.replace(weight_name, param_name)
390 if (
391 param_name == "fused_qkv_a_proj"
@@ -415,32 +401,10 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
401 break
402 else:
403 num_chunks = 1
418 - if is_fusion_moe_shared_experts_layer:
419 - num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
420 - split_dim = (
421 - 1
422 - if ("down_proj.weight" in name and loaded_weight.ndim > 1)
423 - else 0
424 - )
425 - total = loaded_weight.shape[split_dim]
426 - assert total % num_chunks == 0
427 - chunk_size = total // num_chunks
404
405 for j in range(num_chunks):
406 chunk_name = name
407 weight_to_load = loaded_weight
432 - if is_fusion_moe_shared_experts_layer:
433 - chunk_slice = slice(j * chunk_size, (j + 1) * chunk_size)
434 - if loaded_weight.ndim == 1:
435 - weight_to_load = loaded_weight[chunk_slice]
436 - elif split_dim == 0:
437 - weight_to_load = loaded_weight[chunk_slice, :]
438 - else:
439 - weight_to_load = loaded_weight[:, chunk_slice]
440 - chunk_name = name.replace(
441 - "mlp.shared_experts",
442 - f"mlp.experts.{self.config.n_routed_experts + j}",
443 - )
408
409 is_expert_weight = False
410 for mapping in expert_params_mapping:
@@ -462,10 +426,7 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
426 return_success=True,
427 )
428 if success:
465 - if not is_fusion_moe_shared_experts_layer:
466 - name = name_mapped
467 - else:
468 - loaded_params.add(name_mapped)
429 + name = name_mapped
430 break
431 else:
432 if is_expert_weight:
@@ -485,8 +446,7 @@ class DeepseekV32MTP(nn.Module, DeepseekV2MixtureOfExperts):
446 param, "weight_loader", default_weight_loader
447 )
448 weight_loader(param, loaded_weight)
488 - if not is_fusion_moe_shared_experts_layer:
489 - loaded_params.add(name)
449 + loaded_params.add(name)
450
451 loaded_layers: set[int] = set()
452 for param_name in loaded_params:
vllm/models/deepseek_v4/amd/model.py
+44 -51
@@ -16,12 +16,16 @@ from vllm.distributed import (
16 get_tensor_model_parallel_rank,
17 get_tensor_model_parallel_world_size,
18 )
19 +from vllm.logger import init_logger
20 from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
21 from vllm.model_executor.layers.fused_moe import (
22 FusedMoEFactory,
23 GateLinear,
24 fused_moe_make_expert_params_mapping,
25 )
26 +from vllm.model_executor.layers.fused_moe.utils import (
27 + is_model_fused_shared_expert_compatible,
28 +)
29 from vllm.model_executor.layers.layernorm import RMSNorm
30 from vllm.model_executor.layers.linear import (
31 MergedColumnParallelLinear,
@@ -37,6 +41,9 @@ from vllm.model_executor.layers.mhc import (
41 MHCPreOp,
42 )
43 from vllm.model_executor.layers.quantization import QuantizationConfig
44 +from vllm.model_executor.layers.quantization.utils.config_utils import (
45 + is_shared_expert_quant_fse_compatible,
46 +)
47 from vllm.model_executor.layers.vocab_parallel_embedding import (
48 ParallelLMHead,
49 VocabParallelEmbedding,
@@ -60,6 +67,8 @@ from vllm.models.deepseek_v4.amd.rocm import DeepseekV4ROCMAiterMLAAttention
67 from vllm.platforms import current_platform
68 from vllm.sequence import IntermediateTensors
69
70 +logger = init_logger(__name__)
71 +
72
73 class DeepseekV4MLP(nn.Module):
74 def __init__(
@@ -154,48 +163,11 @@ class DeepseekV4MLP(nn.Module):
163 return x
164
165
157 -def _shared_experts_are_fp4(config, layer_idx: int | None = None) -> bool:
158 - """Whether the shared experts are MXFP4 and thus fusable.
159 -
160 - ``layer_idx=None`` resolves the model-wide default (global scheme), used by
161 - the main-model weight loader / mapper callers that operate per-model.
162 - """
163 - quant_cfg = getattr(config, "quantization_config", None)
164 - if quant_cfg is None:
165 - return False
166 - if layer_idx is None:
167 - base = None
168 - elif layer_idx >= config.num_hidden_layers:
169 - base = f"mtp.{layer_idx - config.num_hidden_layers}.ffn.shared_experts"
170 - else:
171 - base = f"layers.{layer_idx}.ffn.shared_experts"
172 - if base and any(e.startswith(base) for e in (quant_cfg.get("exclude") or [])):
173 - return False
174 - entry = (
175 - (quant_cfg.get("layer_quant_config") or {}).get(f"{base}.w1") if base else None
176 - )
177 - if entry is None:
178 - entry = quant_cfg.get("global_quant_config")
179 - return ((entry or {}).get("weight") or {}).get("dtype") == "fp4"
180 -
181 -
182 -def _fuse_shared_experts_enabled(config, prefix: str = "") -> bool:
183 - """Whether to fuse the shared expert into the routed MXFP4 grouped GEMM.
184 -
185 - Fusion fuses the shared expert into the routed experts' MXFP4 grouped GEMM,
186 - so it only applies where the shared expert is the same precision as the
187 - routed experts. Some layers may carry a shared expert in a different quantization
188 - than the routed experts; when so, it runs as its own linear and must not be fused.
189 - """
190 - if not (
191 - current_platform.is_rocm()
192 - and getattr(config, "n_shared_experts", None)
166 +def _fuse_shared_experts_enabled(config) -> bool:
167 + return bool(
168 + getattr(config, "n_shared_experts", None)
169 and envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS
170 and not get_current_vllm_config().parallel_config.enable_expert_parallel
195 - ):
196 - return False
197 - return _shared_experts_are_fp4(
198 - config, extract_layer_index(prefix) if prefix else None
171 )
172
173
@@ -255,9 +227,25 @@ class DeepseekV4MoE(nn.Module):
227
228 self.n_shared_experts = config.n_shared_experts
229
258 - self.fuse_shared_experts = _fuse_shared_experts_enabled(config, prefix)
230 + # TODO: Historically, only `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1`
231 + # is checked to enable FSE for DeepSeek-v4, despite AITER not being used.
232 + # This should be cleaned up and use `resolve_layer_fused_shared_expert`.
233 + fse_requested = _fuse_shared_experts_enabled(config)
234 + if fse_requested:
235 + fse_compatible, fse_reason = is_shared_expert_quant_fse_compatible(
236 + quant_config,
237 + f"{prefix}.experts",
238 + f"{prefix}.shared_experts",
239 + )
240 + if not fse_compatible:
241 + logger.warning(
242 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled but "
243 + "cannot be enabled: %s.",
244 + fse_reason,
245 + )
246 + self.is_fused_shared_expert_enabled = fse_requested and fse_compatible
247
260 - if config.n_shared_experts is None or self.fuse_shared_experts:
248 + if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
249 self.shared_experts = None
250 else:
251 intermediate_size = config.moe_intermediate_size * config.n_shared_experts
@@ -282,8 +270,9 @@ class DeepseekV4MoE(nn.Module):
270 self.experts = FusedMoEFactory(
271 shared_experts=self.shared_experts,
272 n_shared_experts=(
285 - config.n_shared_experts if self.fuse_shared_experts else None
273 + config.n_shared_experts if self.is_fused_shared_expert_enabled else None
274 ),
275 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
276 gate=self.gate,
277 num_experts=config.n_routed_experts,
278 top_k=config.num_experts_per_tok,
@@ -581,6 +570,11 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
570 ),
571 prefix=f"{prefix}.layers",
572 )
573 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
574 + self.layers,
575 + DeepseekV4MoE,
576 + "ffn",
577 + )
578
579 if get_pp_group().is_last_rank:
580 self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
@@ -763,7 +757,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
757 # diverge from how the module was built if per-layer quantization ever
758 # mixes fused and non-fused layers.
759 fuse_by_layer = {
766 - extract_layer_index(mod_name): mod.fuse_shared_experts
760 + extract_layer_index(mod_name): mod.is_fused_shared_expert_enabled
761 for mod_name, mod in self.named_modules()
762 if isinstance(mod, DeepseekV4MoE)
763 }
@@ -874,7 +868,7 @@ class DeepseekV4Model(nn.Module, EagleModelMixin):
868 # redirected shared-expert weights route through the expert loader.
869 n_shared = getattr(self.config, "n_shared_experts", 0) or 0
870 num_experts = self.config.n_routed_experts + (
877 - n_shared if _fuse_shared_experts_enabled(self.config) else 0
871 + n_shared if self.is_fused_shared_expert_enabled else 0
872 )
873 return fused_moe_make_expert_params_mapping(
874 self,
@@ -956,15 +950,14 @@ class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
950 config = vllm_config.model_config.hf_config
951 self.config = config
952 expert_dtype = getattr(config, "expert_dtype", "fp4")
959 - fuse_shared_experts = _fuse_shared_experts_enabled(config)
960 - if expert_dtype != "fp4" or fuse_shared_experts:
961 - self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
962 - expert_dtype, fuse_shared_experts=fuse_shared_experts
963 - )
964 -
953 self.model = self.model_cls(
954 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
955 )
956 + if expert_dtype != "fp4" or self.model.is_fused_shared_expert_enabled:
957 + self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
958 + expert_dtype,
959 + fuse_shared_experts=self.model.is_fused_shared_expert_enabled,
960 + )
961 if get_pp_group().is_last_rank:
962 self.lm_head = ParallelLMHead(
963 config.vocab_size,
vllm/models/minimax_m3/amd/model.py
+64 -30
@@ -34,6 +34,7 @@ from vllm.config import (
34 )
35 from vllm.distributed import get_pp_group, get_tensor_model_parallel_world_size
36 from vllm.forward_context import get_forward_context
37 +from vllm.logger import init_logger
38 from vllm.model_executor.layers.attention import Attention
39 from vllm.model_executor.layers.attention.attention import set_default_quant_scales
40 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
@@ -45,6 +46,9 @@ from vllm.model_executor.layers.fused_moe import (
46 GateLinear,
47 fused_moe_make_expert_params_mapping,
48 )
49 +from vllm.model_executor.layers.fused_moe.utils import (
50 + is_model_fused_shared_expert_compatible,
51 +)
52 from vllm.model_executor.layers.linear import (
53 MergedColumnParallelLinear,
54 MinimaxM3QKVParallelLinearWithIndexer,
@@ -53,6 +57,9 @@ from vllm.model_executor.layers.linear import (
57 )
58 from vllm.model_executor.layers.logits_processor import LogitsProcessor
59 from vllm.model_executor.layers.quantization import QuantizationConfig
60 +from vllm.model_executor.layers.quantization.utils.config_utils import (
61 + is_shared_expert_quant_fse_compatible,
62 +)
63 from vllm.model_executor.layers.rotary_embedding import get_rope
64 from vllm.model_executor.layers.vocab_parallel_embedding import (
65 ParallelLMHead,
@@ -108,23 +115,7 @@ from vllm.v1.kv_cache_interface import (
115 is_quantized_kv_cache,
116 )
117
111 -
112 -def _fuse_shared_experts_enabled(config: PretrainedConfig) -> bool:
113 - """Whether to fuse the shared expert with routed experts.
114 -
115 - ROCm only. Opt-in via ``VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS`` (the
116 - router-append fusion runs on both aiter and non-aiter MoE);
117 - it is disabled under expert parallelism (the shared slot is appended to
118 - the routed top-k, which the EP expert-mapping path does not handle).
119 - """
120 - from vllm.platforms import current_platform
121 -
122 - return bool(
123 - current_platform.is_rocm()
124 - and getattr(config, "n_shared_experts", None)
125 - and envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS
126 - and not get_current_vllm_config().parallel_config.enable_expert_parallel
127 - )
118 +logger = init_logger(__name__)
119
120
121 def _sparse_attention_layer_ids(config: PretrainedConfig) -> set[int]:
@@ -303,10 +294,12 @@ class MiniMaxM3MLP(nn.Module):
294 return x
295
296
306 -def _aiter_moe_fused_shared_experts_enabled(config: PretrainedConfig) -> bool:
297 +def _aiter_moe_fused_shared_experts_enabled(
298 + is_fused_shared_expert_enabled: bool,
299 +) -> bool:
300 """Whether the fused shared expert routes through aiter's grouped top-k MoE.
301
309 - A strict sub-case of :func:`_fuse_shared_experts_enabled`: shared-expert
302 + A strict sub-case of `is_fused_shared_expert_enabled`: shared-expert
303 fusion must already be opted in (``VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS``)
304 and allowed (not under expert parallelism). When additionally on gfx950 with
305 an active aiter MoE backend, the shared expert is appended inside aiter's
@@ -314,11 +307,13 @@ def _aiter_moe_fused_shared_experts_enabled(config: PretrainedConfig) -> bool:
307 vLLM router's torch concat. Otherwise FSE still runs via the vLLM top-k bias
308 router.
309 """
317 - if not _fuse_shared_experts_enabled(config):
318 - return False
310 from vllm.platforms.rocm import on_gfx950
311
321 - return on_gfx950() and rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
312 + return (
313 + on_gfx950()
314 + and is_fused_shared_expert_enabled
315 + and rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
316 + )
317
318
319 class MiniMaxM3MoE(nn.Module):
@@ -371,11 +366,46 @@ class MiniMaxM3MoE(nn.Module):
366 # MoE call as the last expert slot, so we don't build a separate module.
367 # On gfx950 with aiter MoE the append is fused inside aiter's grouped
368 # top-k kernel; otherwise it goes through the vLLM top-k bias router.
374 - self.fuse_shared_experts = _fuse_shared_experts_enabled(config)
375 - self.use_aiter_moe_fse = _aiter_moe_fused_shared_experts_enabled(config)
369 + # It is disabled under expert parallelism (the shared slot is appended to
370 + # the routed top-k, which the EP expert-mapping path does not handle).
371 + # TODO: Historically, only `VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS=1`
372 + # is checked to enable FSE for MiniMax-M3, despite AITER not being used.
373 + # This should be cleaned up and use `resolve_layer_fused_shared_expert`.
374 + fse_requested = envs.VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS
375 +
376 + self.is_fused_shared_expert_enabled = False
377 + if (
378 + fse_requested
379 + and bool(getattr(config, "n_shared_experts", None))
380 + and not get_current_vllm_config().parallel_config.enable_expert_parallel
381 + ):
382 + fse_compatible, fse_reason = is_shared_expert_quant_fse_compatible(
383 + quant_config,
384 + f"{prefix}.experts",
385 + f"{prefix}.shared_experts",
386 + )
387 + if not fse_compatible:
388 + logger.warning(
389 + "VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS is enabled but "
390 + "cannot be enabled: %s.",
391 + fse_reason,
392 + )
393 + else:
394 + self.is_fused_shared_expert_enabled = True
395 +
396 + # When additionally on gfx950 with an active aiter MoE backend, the shared
397 + # expert is appended inside aiter's an active aiter MoE backend, the shared
398 + # expert is appended inside aiter's biased grouped top-k kernel
399 + # (``num_fused_shared_experts``) instead of the vLLM router's torch concat.
400 + # Otherwise FSE still runs via the vLLM top-k bias router.
401 + # TODO: `on_gfx950()` check here should not be MiniMax-M3 specific, and
402 + # the check should be done on resolved MOE backend directly.
403 + self.use_aiter_moe_fse = _aiter_moe_fused_shared_experts_enabled(
404 + self.is_fused_shared_expert_enabled
405 + )
406
407 self.shared_experts: MiniMaxM3MLP | None = None
378 - if self.n_shared_experts and not self.fuse_shared_experts:
408 + if self.n_shared_experts and not self.is_fused_shared_expert_enabled:
409 self.shared_experts = MiniMaxM3MLP(
410 config=config,
411 intermediate_size=config.intermediate_size * self.n_shared_experts,
@@ -412,8 +442,9 @@ class MiniMaxM3MoE(nn.Module):
442 router_logits_dtype=self.gate.out_dtype,
443 shared_experts=self.shared_experts,
444 n_shared_experts=(
415 - self.n_shared_experts if self.fuse_shared_experts else None
445 + self.n_shared_experts if self.is_fused_shared_expert_enabled else None
446 ),
447 + fuse_shared_experts=self.is_fused_shared_expert_enabled,
448 quant_config=quant_config,
449 prefix=f"{prefix}.experts",
450 )
@@ -1101,6 +1132,11 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
1132 ),
1133 prefix=f"{prefix}.layers",
1134 )
1135 + self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
1136 + self.layers,
1137 + MiniMaxM3MoE,
1138 + "block_sparse_moe",
1139 + )
1140
1141 if get_pp_group().is_last_rank:
1142 self.norm = MiniMAXGemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
@@ -1155,7 +1191,7 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
1191 # expert, include the appended slot (id == num_local_experts).
1192 n_shared = getattr(self.config, "n_shared_experts", 0) or 0
1193 num_experts = self.config.num_local_experts + (
1158 - n_shared if _fuse_shared_experts_enabled(self.config) else 0
1194 + n_shared if self.is_fused_shared_expert_enabled else 0
1195 )
1196 return fused_moe_make_expert_params_mapping(
1197 self,
@@ -1187,8 +1223,6 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
1223 # (param_name, weight_name, expert_id, shard_id)
1224 expert_params_mapping = self.get_expert_mapping()
1225
1190 - _fuse_shared = _fuse_shared_experts_enabled(self.config)
1191 -
1226 params_dict = dict(self.named_parameters())
1227 loaded_params: set[str] = set()
1228 for name, loaded_weight in weights:
@@ -1206,7 +1240,7 @@ class MiniMaxM3Model(nn.Module, EagleModelMixin):
1240 # down->w2) so it loads via the routed expert loader. Runs before the
1241 # stacked/dense mappings so shared_experts.gate_proj/up_proj are not
1242 # captured by the dense gate_up_proj mapping.
1209 - if _fuse_shared and ".shared_experts." in name:
1243 + if self.is_fused_shared_expert_enabled and ".shared_experts." in name:
1244 sid = self.config.num_local_experts
1245 name = name.replace(".shared_experts.gate_proj.", f".experts.{sid}.w1.")
1246 name = name.replace(".shared_experts.up_proj.", f".experts.{sid}.w3.")