Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions tests/model_executor/layers/test_fused_shared_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from vllm.config import VllmConfig, set_current_vllm_config
from vllm.model_executor.layers.fused_moe import utils as fused_moe_utils
from vllm.model_executor.layers.fused_moe.layer import determine_expert_counts
from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
from vllm.model_executor.layers.quantization.utils.config_utils import (
is_shared_expert_quant_fse_compatible,
Expand Down Expand Up @@ -690,6 +691,98 @@ def test_quark_packed_layer_config_must_match_global_config() -> None:
)


def make_block_fp8_config(ignored_layers: list[str] | None = None) -> Fp8Config:
return Fp8Config(
is_checkpoint_fp8_serialized=True,
activation_scheme="dynamic",
ignored_layers=ignored_layers,
weight_block_size=[128, 128],
)


_FP8_SHARED_EXPERT_PROJECTIONS = [
"model.layers.0.mlp.shared_experts.gate_up_proj",
"model.layers.0.mlp.shared_experts.down_proj",
]


def test_fp8_shared_expert_fse_allows_uniformly_quantized_experts() -> None:
compatible, reason = is_shared_expert_quant_fse_compatible(
make_block_fp8_config(),
"model.layers.0.mlp.experts",
"model.layers.0.mlp.shared_experts",
)

assert compatible
assert reason is None


def test_fp8_shared_expert_fse_ignores_non_expert_exclusions() -> None:
"""Excluding layers that are not experts must not disable FSE."""
compatible, reason = is_shared_expert_quant_fse_compatible(
make_block_fp8_config(
[
"model.layers.0.input_layernorm",
"model.layers.0.mlp.gate",
"lm_head",
]
),
"model.layers.0.mlp.experts",
"model.layers.0.mlp.shared_experts",
)

assert compatible
assert reason is None


def test_fp8_shared_expert_fse_allows_both_expert_groups_unquantized() -> None:
compatible, reason = is_shared_expert_quant_fse_compatible(
make_block_fp8_config(
["model.layers.0.mlp.experts", *_FP8_SHARED_EXPERT_PROJECTIONS]
),
"model.layers.0.mlp.experts",
"model.layers.0.mlp.shared_experts",
)

assert compatible
assert reason is None


@pytest.mark.parametrize(
"ignored_layers",
[
pytest.param(_FP8_SHARED_EXPERT_PROJECTIONS, id="shared-experts-excluded"),
pytest.param(["model.layers.0.mlp.experts"], id="routed-experts-excluded"),
pytest.param(_FP8_SHARED_EXPERT_PROJECTIONS[:1], id="one-projection-excluded"),
],
)
def test_fp8_shared_expert_fse_rejects_asymmetric_exclusions(
ignored_layers: list[str],
) -> None:
compatible, reason = is_shared_expert_quant_fse_compatible(
make_block_fp8_config(ignored_layers),
"model.layers.0.mlp.experts",
"model.layers.0.mlp.shared_experts",
)

assert not compatible
assert reason == (
"FP8 uses different quantization configurations for routed and "
"shared experts at model.layers.0.mlp.shared_experts"
)


def test_fp8_shared_expert_fse_requires_serialized_checkpoint() -> None:
compatible, reason = is_shared_expert_quant_fse_compatible(
Fp8Config(is_checkpoint_fp8_serialized=False),
"model.layers.0.mlp.experts",
"model.layers.0.mlp.shared_experts",
)

assert not compatible
assert reason == "FP8 FSE requires an fp8-serialized checkpoint"


def test_non_quark_shared_expert_fse_is_incompatible() -> None:
compatible, reason = is_shared_expert_quant_fse_compatible(
object(),
Expand Down
37 changes: 37 additions & 0 deletions vllm/model_executor/layers/quantization/utils/config_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def is_shared_expert_quant_fse_compatible(
if quant_config is None:
return True, None

from vllm.model_executor.layers.quantization.fp8 import Fp8Config
from vllm.model_executor.layers.quantization.quark.quark import QuarkConfig
from vllm.models.deepseek_v4.quant_config import DeepseekV4FP8Config

Expand Down Expand Up @@ -144,6 +145,42 @@ def get_projection_quant_configs(layer_name: str) -> list[object]:
f"shared experts at {shared_expert_prefix}",
)

# NOTE: must stay below the DeepseekV4FP8Config branch, which subclasses
# Fp8Config and needs its own MXFP4 handling.
if isinstance(quant_config, Fp8Config):
from vllm.model_executor.layers.quantization.utils.quant_utils import (
is_layer_skipped,
)

# Only fp8-serialized checkpoints have been validated with FSE.
if not quant_config.is_checkpoint_fp8_serialized:
return False, "FP8 FSE requires an fp8-serialized checkpoint"

# Fp8Config applies a single `weight_block_size` and `activation_scheme`
# to every layer it quantizes, so routed and shared experts share one
# quantization config as long as they are either both quantized or both
# listed in `ignored_layers`.
def is_fse_layer_skipped(prefix: str) -> bool:
return is_layer_skipped(
prefix=prefix,
ignored_layers=quant_config.ignored_layers,
fused_mapping=quant_config.packed_modules_mapping,
match_mode=quant_config.ignored_layers_match_mode,
)

expert_skipped = is_fse_layer_skipped(expert_prefix)
if any(
is_fse_layer_skipped(f"{shared_expert_prefix}.{projection_name}")
!= expert_skipped
for projection_name in projection_names
):
return (
False,
"FP8 uses different quantization configurations for routed and "
f"shared experts at {shared_expert_prefix}",
)
return True, None

# TODO: Extend FSE support detection to other quantization methods. Typically,
# one would check that the experts and shared_experts use the same
# quantization config. This may be refactored as part of QuantizationConfig later.
Expand Down
Loading