Skip to content

Commit d9aed32

Browse files
omiroshamd-mghanimi
authored andcommitted
[ROCm][Perf] Add Fused Shared Expert (FSE) support for GLM-4.5/6/7 (vllm-project#44313)
Signed-off-by: Olga Miroshnichenko <olga.miroshnichenko@amd.com> Signed-off-by: Mehdi Ghanimifard <mehdi.ghanimifard@amd.com> Co-authored-by: Mehdi Ghanimifard <mghanimi@amd.com> Co-authored-by: Mehdi Ghanimifard <mehdi.ghanimifard@amd.com>
1 parent ce84cd0 commit d9aed32

2 files changed

Lines changed: 254 additions & 105 deletions

File tree

vllm/model_executor/models/glm4_moe.py

Lines changed: 138 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from torch import nn
3333
from transformers.models.glm4_moe import Glm4MoeConfig
3434

35+
from vllm._aiter_ops import rocm_aiter_ops
3536
from vllm.compilation.decorators import support_torch_compile
3637
from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
3738
from vllm.distributed import (
@@ -168,7 +169,16 @@ def __init__(
168169
self.physical_expert_start + self.n_local_physical_experts
169170
)
170171

171-
if config.n_shared_experts is not None:
172+
# AITER fused shared-expert (FSE) gate; mirrors the deepseek_v2.py
173+
# pattern (see Glm4MoE / FusedMoE wiring there).
174+
self.is_rocm_aiter_moe_enabled = rocm_aiter_ops.is_fused_moe_enabled()
175+
self.is_fusion_moe_shared_experts_enabled = (
176+
rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
177+
)
178+
179+
if config.n_shared_experts is None or self.is_fusion_moe_shared_experts_enabled:
180+
self.shared_experts = None
181+
else:
172182
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
173183
self.shared_experts = Glm4MoeMLP(
174184
hidden_size=config.hidden_size,
@@ -178,8 +188,6 @@ def __init__(
178188
reduce_results=False,
179189
prefix=f"{prefix}.shared_experts",
180190
)
181-
else:
182-
self.shared_experts = None
183191

184192
self.experts = FusedMoE(
185193
shared_experts=self.shared_experts,
@@ -194,12 +202,18 @@ def __init__(
194202
topk_group=config.topk_group,
195203
prefix=f"{prefix}.experts",
196204
scoring_func="sigmoid",
205+
# aiter applies routed_scaling_factor internally; see deepseek_v2.py.
197206
routed_scaling_factor=self.routed_scaling_factor,
198-
apply_routed_scale_to_output=True,
207+
apply_routed_scale_to_output=not self.is_rocm_aiter_moe_enabled,
199208
e_score_correction_bias=self.gate.e_score_correction_bias,
200209
enable_eplb=self.enable_eplb,
201210
num_redundant_experts=self.n_redundant_experts,
202211
router_logits_dtype=torch.float32,
212+
n_shared_experts=(
213+
config.n_shared_experts
214+
if self.is_fusion_moe_shared_experts_enabled
215+
else None
216+
),
203217
)
204218

205219
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
@@ -469,15 +483,25 @@ def forward(
469483
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
470484
# Params for weights, fp8 weight scales, fp8 activation scales
471485
# (param_name, weight_name, expert_id, shard_id)
486+
# FSE widens the mapping by n_shared_experts slots; see deepseek_v2.py.
487+
num_experts = self.config.n_routed_experts
488+
if (
489+
rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
490+
and self.config.n_shared_experts
491+
):
492+
num_experts += self.config.n_shared_experts
472493
return fused_moe_make_expert_params_mapping(
473494
self,
474495
ckpt_gate_proj_name="gate_proj",
475496
ckpt_down_proj_name="down_proj",
476497
ckpt_up_proj_name="up_proj",
477-
num_experts=self.config.n_routed_experts,
498+
num_experts=num_experts,
478499
)
479500

480501
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
502+
rocm_aiter_moe_shared_expert_enabled = (
503+
rocm_aiter_ops.is_fusion_moe_shared_experts_enabled()
504+
)
481505
stacked_params_mapping = [
482506
# (param_name, shard_name, shard_id)
483507
("qkv_proj", "q_proj", "q"),
@@ -494,6 +518,11 @@ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
494518
spec_layer = get_spec_layer_idx_from_weight_name(self.config, name)
495519
if spec_layer is not None:
496520
continue
521+
522+
is_fusion_moe_shared_experts_layer = (
523+
rocm_aiter_moe_shared_expert_enabled and ("mlp.shared_experts" in name)
524+
)
525+
497526
for param_name, weight_name, shard_id in stacked_params_mapping:
498527
# Skip non-stacked layers and experts (experts handled below).
499528
if weight_name not in name:
@@ -506,6 +535,8 @@ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
506535
# for mlp.experts[0].gate_gate_up_proj, which breaks load.
507536
if ("mlp.experts." in name) and name not in params_dict:
508537
continue
538+
if is_fusion_moe_shared_experts_layer:
539+
continue
509540

510541
name = name.replace(weight_name, param_name)
511542
# Skip loading extra bias for GPTQ models.
@@ -527,65 +558,109 @@ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
527558
break
528559
else:
529560
is_expert_weight = False
530-
for mapping in expert_params_mapping:
531-
param_name, weight_name, expert_id, shard_id = mapping
532-
if weight_name not in name:
533-
continue
534-
535-
# Anyway, this is an expert weight and should not be
536-
# attempted to load as other weights later
537-
is_expert_weight = True
538-
539-
# Do not modify `name` since the loop may continue here
540-
# Instead, create a new variable
541-
name_mapped = name.replace(weight_name, param_name)
542-
543-
if is_pp_missing_parameter(name_mapped, self):
544-
continue
545-
546-
param = params_dict[name_mapped]
547-
# We should ask the weight loader to return success or not
548-
# here since otherwise we may skip experts with other
549-
# available replicas.
550-
weight_loader = typing.cast(
551-
Callable[..., bool], param.weight_loader
552-
)
553-
success = weight_loader(
554-
param,
555-
loaded_weight,
556-
name_mapped,
557-
shard_id=shard_id,
558-
expert_id=expert_id,
559-
return_success=True,
560-
)
561-
if success:
562-
name = name_mapped
563-
break
564-
else:
565-
if is_expert_weight:
566-
# We've checked that this is an expert weight
567-
# However it's not mapped locally to this rank
568-
# So we simply skip it
569-
continue
570-
571-
# Skip loading extra bias for GPTQ models.
572-
if name.endswith(".bias") and name not in params_dict:
573-
continue
574-
575-
# Remapping the name of FP8 kv-scale.
576-
name = maybe_remap_kv_scale_name(name, params_dict)
577-
if name is None:
578-
continue
579-
580-
if is_pp_missing_parameter(name, self):
581-
continue
582-
583-
param = params_dict[name]
584-
weight_loader = getattr(
585-
param, "weight_loader", default_weight_loader
561+
562+
# FSE: split a widened mlp.shared_experts tensor into
563+
# n_shared_experts chunks; see deepseek_v2.py for details.
564+
num_chunks = 1
565+
split_dim = 0
566+
chunk_size = 0
567+
if is_fusion_moe_shared_experts_layer:
568+
num_chunks = getattr(self.config, "n_shared_experts", 1) or 1
569+
split_dim = (
570+
1
571+
if ("down_proj.weight" in name and loaded_weight.ndim > 1)
572+
else 0
586573
)
587-
weight_loader(param, loaded_weight)
588-
loaded_params.add(name)
574+
total = loaded_weight.shape[split_dim]
575+
if total % num_chunks != 0:
576+
raise ValueError(
577+
f"FSE shared-expert weight {name} has dim "
578+
f"{total} along axis {split_dim} which is not "
579+
f"divisible by n_shared_experts={num_chunks}."
580+
)
581+
chunk_size = total // num_chunks
582+
583+
for j in range(num_chunks):
584+
chunk_name = name
585+
weight_to_load = loaded_weight
586+
587+
if is_fusion_moe_shared_experts_layer:
588+
chunk_slice = slice(j * chunk_size, (j + 1) * chunk_size)
589+
if loaded_weight.ndim == 1:
590+
weight_to_load = loaded_weight[chunk_slice]
591+
elif split_dim == 0:
592+
weight_to_load = loaded_weight[chunk_slice, :]
593+
else:
594+
weight_to_load = loaded_weight[:, chunk_slice]
595+
# Synthesize an expert-style name for expert mapping.
596+
chunk_name = name.replace(
597+
"mlp.shared_experts",
598+
f"mlp.experts.{self.config.n_routed_experts + j}",
599+
)
600+
601+
for mapping in expert_params_mapping:
602+
param_name, weight_name, expert_id, shard_id = mapping
603+
if weight_name not in chunk_name:
604+
continue
605+
606+
# Anyway, this is an expert weight and should not be
607+
# attempted to load as other weights later
608+
is_expert_weight = True
609+
610+
# Do not modify `name` since the loop may continue here
611+
# Instead, create a new variable
612+
name_mapped = chunk_name.replace(weight_name, param_name)
613+
614+
if is_pp_missing_parameter(name_mapped, self):
615+
continue
616+
617+
param = params_dict[name_mapped]
618+
# We should ask the weight loader to return success
619+
# or not here since otherwise we may skip experts
620+
# with other available replicas.
621+
weight_loader = typing.cast(
622+
Callable[..., bool], param.weight_loader
623+
)
624+
success = weight_loader(
625+
param,
626+
weight_to_load,
627+
name_mapped,
628+
shard_id=shard_id,
629+
expert_id=expert_id,
630+
return_success=True,
631+
)
632+
if success:
633+
if not is_fusion_moe_shared_experts_layer:
634+
name = name_mapped
635+
else:
636+
loaded_params.add(name_mapped)
637+
break
638+
else:
639+
if is_expert_weight:
640+
# We've checked that this is an expert weight
641+
# However it's not mapped locally to this rank
642+
# So we simply skip it
643+
continue
644+
645+
# Skip loading extra bias for GPTQ models.
646+
if name.endswith(".bias") and name not in params_dict:
647+
continue
648+
649+
# Remapping the name of FP8 kv-scale.
650+
name = maybe_remap_kv_scale_name(name, params_dict)
651+
if name is None:
652+
continue
653+
654+
if is_pp_missing_parameter(name, self):
655+
continue
656+
657+
param = params_dict[name]
658+
weight_loader = getattr(
659+
param, "weight_loader", default_weight_loader
660+
)
661+
weight_loader(param, loaded_weight)
662+
if name is not None and not is_fusion_moe_shared_experts_layer:
663+
loaded_params.add(name)
589664

590665
return loaded_params
591666

0 commit comments

Comments
 (0)