773 lines
24 KiB
Python
773 lines
24 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
from typing import cast
|
|
|
|
import huggingface_hub
|
|
import pytest
|
|
import torch
|
|
from safetensors import safe_open
|
|
|
|
import vllm.model_executor.layers.fused_moe.modular_kernel as mk
|
|
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
|
|
from vllm.model_executor.layers.fused_moe.config import (
|
|
FusedMoEConfig,
|
|
FusedMoEParallelConfig,
|
|
FusedMoEQuantConfig,
|
|
RoutingMethodType,
|
|
nvfp4_moe_quant_config,
|
|
)
|
|
from vllm.model_executor.layers.fused_moe.experts.nvfp4_emulation_moe import (
|
|
Nvfp4QuantizationEmulationTritonExperts,
|
|
)
|
|
from vllm.model_executor.layers.fused_moe.experts.triton_moe import TritonExperts
|
|
from vllm.model_executor.layers.quantization.utils import (
|
|
nvfp4_emulation_utils,
|
|
)
|
|
from vllm.model_executor.layers.quantization.utils.nvfp4_emulation_utils import (
|
|
dequantize_to_dtype,
|
|
ref_nvfp4_quant_dequant,
|
|
)
|
|
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
|
QuantKey,
|
|
kNvfp4Dynamic,
|
|
kNvfp4Static,
|
|
)
|
|
from vllm.platforms import current_platform
|
|
from vllm.triton_utils import triton
|
|
|
|
if current_platform.is_rocm():
|
|
from vllm.platforms.rocm import on_gfx950
|
|
else:
|
|
|
|
def on_gfx950() -> bool:
|
|
return False
|
|
|
|
|
|
MOE_MODEL_CONFIGS = {
|
|
"nvidia/Qwen3-30B-A3B-NVFP4": {
|
|
"shards": ["model-00001-of-00004.safetensors"],
|
|
"expert_prefix": "model.layers.9.mlp.experts.",
|
|
# Position of the expert index in the dot-split key.
|
|
"expert_idx_pos": 5,
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def loaded_model_files():
|
|
return {
|
|
model_id: huggingface_hub.snapshot_download(
|
|
repo_id=model_id, allow_patterns=config["shards"]
|
|
)
|
|
for model_id, config in MOE_MODEL_CONFIGS.items()
|
|
}
|
|
|
|
|
|
class Nvfp4QuantizationEmulationTritonExpertsReference(TritonExperts):
|
|
"""
|
|
Extension of TritonExperts to support emulated NVFP4 MoE experts.
|
|
|
|
It may be used for NVFP4 models when the device does not have
|
|
native support for this dtype.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
moe_config: FusedMoEConfig,
|
|
quant_config: FusedMoEQuantConfig,
|
|
):
|
|
super().__init__(moe_config, quant_config)
|
|
|
|
# `TritonExperts.apply` expects pre-dequantized weights,
|
|
# which we handle in `apply` below.
|
|
self.w1_scale_val = self.quant_config.w1_scale
|
|
self.w2_scale_val = self.quant_config.w2_scale
|
|
|
|
self.quant_config._w1.scale = None
|
|
self.quant_config._w2.scale = None
|
|
|
|
self.quantization_emulation = True
|
|
|
|
@property
|
|
def quant_dtype(self) -> torch.dtype | str | None:
|
|
return "nvfp4"
|
|
|
|
@property
|
|
def a1_scale(self) -> torch.Tensor | None:
|
|
return self.quant_config.a1_gscale
|
|
|
|
@property
|
|
def expects_unquantized_inputs(self) -> bool:
|
|
return True
|
|
|
|
@staticmethod
|
|
def _supports_quant_scheme(
|
|
weight_key: QuantKey | None,
|
|
activation_key: QuantKey | None,
|
|
) -> bool:
|
|
return (weight_key, activation_key) == (kNvfp4Static, kNvfp4Dynamic)
|
|
|
|
def apply(
|
|
self,
|
|
output: torch.Tensor,
|
|
hidden_states: torch.Tensor,
|
|
w1: torch.Tensor,
|
|
w2: torch.Tensor,
|
|
topk_weights: torch.Tensor,
|
|
topk_ids: torch.Tensor,
|
|
activation: MoEActivation,
|
|
global_num_experts: int,
|
|
expert_map: torch.Tensor | None,
|
|
a1q_scale: torch.Tensor | None,
|
|
a2_scale: torch.Tensor | None,
|
|
workspace13: torch.Tensor,
|
|
workspace2: torch.Tensor,
|
|
expert_tokens_meta: mk.ExpertTokensMetadata | None,
|
|
apply_router_weight_on_input: bool,
|
|
):
|
|
assert w1.dtype == torch.uint8
|
|
assert w2.dtype == torch.uint8
|
|
|
|
# Dequantize w1 from packed NVFP4 to fp16/bf16
|
|
w13_global_scale = self.quant_config.g1_alphas
|
|
|
|
w1_dequant = dequantize_to_dtype(
|
|
tensor_fp4=w1,
|
|
tensor_sf=self.w1_scale_val,
|
|
global_scale=w13_global_scale,
|
|
dtype=hidden_states.dtype,
|
|
block_size=16,
|
|
swizzle=False,
|
|
)
|
|
|
|
# Dequantize w2 from packed NVFP4 to fp16/bf16
|
|
w2_global_scale = self.quant_config.g2_alphas
|
|
|
|
w2_dequant = dequantize_to_dtype(
|
|
tensor_fp4=w2,
|
|
tensor_sf=self.w2_scale_val,
|
|
global_scale=w2_global_scale,
|
|
dtype=hidden_states.dtype,
|
|
block_size=16,
|
|
swizzle=False,
|
|
)
|
|
|
|
super().apply(
|
|
output=output,
|
|
hidden_states=hidden_states,
|
|
w1=w1_dequant,
|
|
w2=w2_dequant,
|
|
topk_weights=topk_weights,
|
|
topk_ids=topk_ids,
|
|
activation=activation,
|
|
global_num_experts=global_num_experts,
|
|
expert_map=expert_map,
|
|
a1q_scale=None,
|
|
a2_scale=self.quant_config.a2_gscale,
|
|
workspace13=workspace13,
|
|
workspace2=workspace2,
|
|
expert_tokens_meta=expert_tokens_meta,
|
|
apply_router_weight_on_input=apply_router_weight_on_input,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("config_kwargs", "expected_reason"),
|
|
[
|
|
({"has_bias": True}, "kernel does not support bias"),
|
|
({"is_lora_enabled": True}, "kernel does not support LoRA"),
|
|
],
|
|
)
|
|
def test_nvfp4_emulation_support_check_rejects_bias_and_lora(
|
|
config_kwargs: dict[str, bool],
|
|
expected_reason: str,
|
|
) -> None:
|
|
moe_config = FusedMoEConfig(
|
|
num_experts=2,
|
|
experts_per_token=1,
|
|
hidden_dim=16,
|
|
intermediate_size=16,
|
|
num_local_experts=2,
|
|
num_logical_experts=2,
|
|
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
|
|
activation=MoEActivation.SILU,
|
|
in_dtype=torch.bfloat16,
|
|
device="cuda",
|
|
routing_method=RoutingMethodType.TopK,
|
|
**config_kwargs,
|
|
)
|
|
|
|
supported, reason = Nvfp4QuantizationEmulationTritonExperts.is_supported_config(
|
|
Nvfp4QuantizationEmulationTritonExperts,
|
|
moe_config,
|
|
kNvfp4Static,
|
|
kNvfp4Dynamic,
|
|
mk.FusedMoEActivationFormat.Standard,
|
|
)
|
|
|
|
assert not supported
|
|
assert reason == expected_reason
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike(),
|
|
reason="Triton NVFP4 kernel requires CUDA.",
|
|
)
|
|
def test_triton_dequantize_nvfp4(monkeypatch, loaded_model_files) -> None:
|
|
"""Test the Triton dequantization kernel against the CPU reference
|
|
using real NVFP4 weights from a checkpoint.
|
|
|
|
Tests both 2D (attention projection) and 3D (stacked MoE experts).
|
|
"""
|
|
checkpoint_path = loaded_model_files["nvidia/Qwen3-30B-A3B-NVFP4"]
|
|
shards = cast(list[str], MOE_MODEL_CONFIGS["nvidia/Qwen3-30B-A3B-NVFP4"]["shards"])
|
|
shard_path = f"{checkpoint_path}/{shards[0]}"
|
|
block_size = 16
|
|
|
|
with safe_open(shard_path, framework="pt", device="cpu") as f:
|
|
all_keys = list(f.keys())
|
|
|
|
# 2D case: attention projection
|
|
tensor_fp4_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight")
|
|
tensor_sf_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight_scale")
|
|
global_scale_2d = f.get_tensor("model.layers.9.self_attn.k_proj.weight_scale_2")
|
|
|
|
# 3D case: stack ALL experts for layer 9 up_proj
|
|
expert_prefix = "model.layers.9.mlp.experts."
|
|
expert_indices = sorted(
|
|
int(key.split(".")[5])
|
|
for key in all_keys
|
|
if key.startswith(expert_prefix) and key.endswith(".up_proj.weight")
|
|
)
|
|
assert len(expert_indices) > 0
|
|
|
|
all_fp4 = []
|
|
all_sf = []
|
|
all_global_scale = []
|
|
for index in expert_indices:
|
|
name = f"{expert_prefix}{index}.up_proj"
|
|
all_fp4.append(f.get_tensor(f"{name}.weight"))
|
|
all_sf.append(f.get_tensor(f"{name}.weight_scale"))
|
|
all_global_scale.append(f.get_tensor(f"{name}.weight_scale_2"))
|
|
|
|
tensor_fp4_3d = torch.stack(all_fp4)
|
|
tensor_sf_3d = torch.stack(all_sf)
|
|
global_scale_3d = torch.stack(all_global_scale)
|
|
|
|
test_cases = [
|
|
("2D base", tensor_fp4_2d, tensor_sf_2d, global_scale_2d),
|
|
(
|
|
"2D 2x rows",
|
|
tensor_fp4_2d.repeat(2, 1),
|
|
tensor_sf_2d.repeat(2, 1),
|
|
global_scale_2d,
|
|
),
|
|
(
|
|
"2D 4x rows",
|
|
tensor_fp4_2d.repeat(4, 1),
|
|
tensor_sf_2d.repeat(4, 1),
|
|
global_scale_2d,
|
|
),
|
|
(
|
|
"2D 2x cols",
|
|
tensor_fp4_2d.repeat(1, 2),
|
|
tensor_sf_2d.repeat(1, 2),
|
|
global_scale_2d,
|
|
),
|
|
("3D base", tensor_fp4_3d, tensor_sf_3d, global_scale_3d),
|
|
(
|
|
"3D 2x experts",
|
|
tensor_fp4_3d.repeat(2, 1, 1),
|
|
tensor_sf_3d.repeat(2, 1, 1),
|
|
global_scale_3d.repeat(2),
|
|
),
|
|
(
|
|
"3D 2x rows",
|
|
tensor_fp4_3d.repeat(1, 2, 1),
|
|
tensor_sf_3d.repeat(1, 2, 1),
|
|
global_scale_3d,
|
|
),
|
|
(
|
|
"3D 2x cols",
|
|
tensor_fp4_3d.repeat(1, 1, 2),
|
|
tensor_sf_3d.repeat(1, 1, 2),
|
|
global_scale_3d,
|
|
),
|
|
]
|
|
|
|
quantiles = [0.5, 0.001, 0.999]
|
|
|
|
# Move the E2M1 lookup table to CUDA ahead of time, as would normally
|
|
# happen during model loading (process_weights_after_loading). Both the
|
|
# Triton and PyTorch reference paths run on CUDA.
|
|
nvfp4_emulation_utils.kE2M1ToFloat_handle.val = (
|
|
nvfp4_emulation_utils.kE2M1ToFloat_handle.val.cuda()
|
|
)
|
|
|
|
for label, tensor_fp4, tensor_sf, global_scale in test_cases:
|
|
fp4_cuda = tensor_fp4.cuda()
|
|
sf_cuda = tensor_sf.cuda()
|
|
gs_cuda = global_scale.cuda()
|
|
|
|
# Triton path
|
|
triton_result = dequantize_to_dtype(
|
|
fp4_cuda,
|
|
sf_cuda,
|
|
gs_cuda,
|
|
torch.bfloat16,
|
|
block_size,
|
|
swizzle=False,
|
|
)
|
|
|
|
# Reference path (PyTorch ops on CUDA, Triton dispatch disabled)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(
|
|
nvfp4_emulation_utils.current_platform,
|
|
"is_cuda_alike",
|
|
lambda: False,
|
|
)
|
|
reference = dequantize_to_dtype(
|
|
fp4_cuda,
|
|
sf_cuda,
|
|
gs_cuda,
|
|
torch.bfloat16,
|
|
block_size,
|
|
swizzle=False,
|
|
)
|
|
|
|
torch.testing.assert_close(triton_result, reference, atol=0, rtol=0)
|
|
|
|
# Benchmark
|
|
shape = list(tensor_fp4.shape)
|
|
|
|
def _triton_bench(
|
|
fp4_cuda=fp4_cuda,
|
|
scale_cuda=sf_cuda,
|
|
global_scale_cuda=gs_cuda,
|
|
block_size=block_size,
|
|
):
|
|
return dequantize_to_dtype(
|
|
fp4_cuda,
|
|
scale_cuda,
|
|
global_scale_cuda,
|
|
torch.bfloat16,
|
|
block_size,
|
|
swizzle=False,
|
|
)
|
|
|
|
triton_ms, triton_min, triton_max = triton.testing.do_bench(
|
|
_triton_bench, quantiles=quantiles
|
|
)
|
|
|
|
def _reference_bench(
|
|
fp4_cuda=fp4_cuda,
|
|
scale_cuda=sf_cuda,
|
|
global_scale_cuda=gs_cuda,
|
|
block_size=block_size,
|
|
):
|
|
with monkeypatch.context() as m2:
|
|
m2.setattr(
|
|
nvfp4_emulation_utils.current_platform,
|
|
"is_cuda_alike",
|
|
lambda: False,
|
|
)
|
|
dequantize_to_dtype(
|
|
fp4_cuda,
|
|
scale_cuda,
|
|
global_scale_cuda,
|
|
torch.bfloat16,
|
|
block_size,
|
|
swizzle=False,
|
|
)
|
|
|
|
ref_ms, ref_min, ref_max = triton.testing.do_bench(
|
|
_reference_bench, quantiles=quantiles
|
|
)
|
|
|
|
speedup = ref_ms / triton_ms if triton_ms > 0 else float("inf")
|
|
print(f" dequantize {label} {shape}:")
|
|
print(
|
|
f" triton: median={triton_ms:.3f}ms, "
|
|
f"min={triton_min:.3f}ms, max={triton_max:.3f}ms"
|
|
)
|
|
print(
|
|
f" reference: median={ref_ms:.3f}ms, "
|
|
f"min={ref_min:.3f}ms, max={ref_max:.3f}ms"
|
|
)
|
|
print(f" speedup: {speedup:.2f}x")
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike(),
|
|
reason="Triton NVFP4 kernel requires CUDA.",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"m, k",
|
|
[
|
|
(1, 16),
|
|
(1, 4096),
|
|
(2, 4096),
|
|
(4, 4096),
|
|
(8, 4096),
|
|
(16, 4096),
|
|
(24, 4096),
|
|
(32, 4096),
|
|
(1, 8192),
|
|
(2, 8192),
|
|
(4, 8192),
|
|
(8, 8192),
|
|
(16, 8192),
|
|
(24, 8192),
|
|
(32, 8192),
|
|
(1, 32),
|
|
(2, 48),
|
|
(7, 64),
|
|
(16, 128),
|
|
(33, 160),
|
|
(128, 256),
|
|
(256, 512),
|
|
(1024, 1024),
|
|
(5120, 2048),
|
|
(2048, 4096),
|
|
(4096, 7168),
|
|
(8192, 8192),
|
|
(128, 16384),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("global_scale_value", [0.5, 1.0, 0.001])
|
|
def test_triton_nvfp4_quant_dequant(
|
|
monkeypatch, m: int, k: int, global_scale_value: float
|
|
) -> None:
|
|
"""Test the Triton quant-dequant kernel against the CPU reference."""
|
|
block_size = 16
|
|
x = torch.randn(m, k, dtype=torch.bfloat16, device="cuda")
|
|
global_scale = torch.tensor(global_scale_value, dtype=torch.float32, device="cuda")
|
|
|
|
# Triton path
|
|
triton_result = ref_nvfp4_quant_dequant(x, global_scale, block_size)
|
|
|
|
# CPU reference path
|
|
with monkeypatch.context() as mp:
|
|
mp.setattr(
|
|
nvfp4_emulation_utils.current_platform,
|
|
"is_cuda_alike",
|
|
lambda: False,
|
|
)
|
|
reference = ref_nvfp4_quant_dequant(x.cpu(), global_scale.cpu(), block_size)
|
|
|
|
torch.testing.assert_close(triton_result.cpu(), reference, atol=0, rtol=0)
|
|
|
|
# Benchmark (both paths on CUDA tensors for fair comparison)
|
|
quantiles = [0.5, 0.001, 0.999]
|
|
|
|
def _triton_bench(
|
|
input_tensor=x, input_global_scale=global_scale, input_block_size=block_size
|
|
):
|
|
return ref_nvfp4_quant_dequant(
|
|
input_tensor, input_global_scale, input_block_size
|
|
)
|
|
|
|
triton_ms, triton_min, triton_max = triton.testing.do_bench(
|
|
_triton_bench, quantiles=quantiles
|
|
)
|
|
|
|
def _reference_bench(
|
|
input_tensor=x, input_global_scale=global_scale, input_block_size=block_size
|
|
):
|
|
with monkeypatch.context() as mp2:
|
|
mp2.setattr(
|
|
nvfp4_emulation_utils.current_platform,
|
|
"is_cuda_alike",
|
|
lambda: False,
|
|
)
|
|
ref_nvfp4_quant_dequant(input_tensor, input_global_scale, input_block_size)
|
|
|
|
ref_ms, ref_min, ref_max = triton.testing.do_bench(
|
|
_reference_bench, quantiles=quantiles
|
|
)
|
|
|
|
speedup = ref_ms / triton_ms if triton_ms > 0 else float("inf")
|
|
print(f" quant_dequant [{m}x{k}] gs={global_scale_value}:")
|
|
print(
|
|
f" triton: median={triton_ms:.3f}ms, "
|
|
f"min={triton_min:.3f}ms, max={triton_max:.3f}ms"
|
|
)
|
|
print(
|
|
f" reference: median={ref_ms:.3f}ms, "
|
|
f"min={ref_min:.3f}ms, max={ref_max:.3f}ms"
|
|
)
|
|
print(f" speedup: {speedup:.2f}x")
|
|
|
|
|
|
def _load_nvfp4_moe_weights(
|
|
model_files: dict[str, str],
|
|
model_id: str,
|
|
tensor_parallel_size: int,
|
|
max_experts: int | None = None,
|
|
):
|
|
"""Load and stack NVFP4 MoE weights from checkpoint shards.
|
|
|
|
Returns (w1, w1_scale, w1_gscale, w2, w2_scale, w2_gscale,
|
|
a1_gscale, a2_gscale, num_experts, hidden_dim,
|
|
intermediate_size).
|
|
|
|
When max_experts is set, only the first max_experts experts are loaded.
|
|
|
|
When tensor_parallel_size > 1, the N dimension of w1 and the K
|
|
dimension of w2 are narrowed to the first TP shard (simulating
|
|
column-parallel on w1 / row-parallel on w2).
|
|
"""
|
|
cfg = MOE_MODEL_CONFIGS[model_id]
|
|
shards = cast(list[str], cfg["shards"])
|
|
checkpoint_path = model_files[model_id]
|
|
|
|
expert_prefix = cfg["expert_prefix"]
|
|
idx_pos = cast(int, cfg["expert_idx_pos"])
|
|
|
|
# Collect all tensors across shards into a flat dict — an expert's
|
|
# tensors may be split across multiple shard files.
|
|
all_tensors: dict[str, torch.Tensor] = {}
|
|
for shard_name in shards:
|
|
shard_path = f"{checkpoint_path}/{shard_name}"
|
|
with safe_open(shard_path, framework="pt", device="cpu") as f:
|
|
for key in f.keys(): # noqa: SIM118
|
|
if key.startswith(expert_prefix):
|
|
all_tensors[key] = f.get_tensor(key)
|
|
|
|
expert_indices = sorted(
|
|
{
|
|
int(key.split(".")[idx_pos])
|
|
for key in all_tensors
|
|
if key.endswith(".gate_proj.weight")
|
|
}
|
|
)
|
|
if max_experts is not None:
|
|
expert_indices = expert_indices[:max_experts]
|
|
num_experts = len(expert_indices)
|
|
|
|
gate_weights, up_weights, down_weights = [], [], []
|
|
gate_scales, up_scales, down_scales = [], [], []
|
|
gate_gscales, up_gscales, down_gscales = [], [], []
|
|
a1_scales, a2_scales = [], []
|
|
|
|
for idx in expert_indices:
|
|
prefix = f"{expert_prefix}{idx}"
|
|
gate_weights.append(all_tensors[f"{prefix}.gate_proj.weight"])
|
|
gate_scales.append(all_tensors[f"{prefix}.gate_proj.weight_scale"])
|
|
gate_gscales.append(all_tensors[f"{prefix}.gate_proj.weight_scale_2"])
|
|
up_weights.append(all_tensors[f"{prefix}.up_proj.weight"])
|
|
up_scales.append(all_tensors[f"{prefix}.up_proj.weight_scale"])
|
|
up_gscales.append(all_tensors[f"{prefix}.up_proj.weight_scale_2"])
|
|
down_weights.append(all_tensors[f"{prefix}.down_proj.weight"])
|
|
down_scales.append(all_tensors[f"{prefix}.down_proj.weight_scale"])
|
|
down_gscales.append(all_tensors[f"{prefix}.down_proj.weight_scale_2"])
|
|
a1_scales.append(all_tensors[f"{prefix}.gate_proj.input_scale"])
|
|
a2_scales.append(all_tensors[f"{prefix}.down_proj.input_scale"])
|
|
|
|
# Stack into MoE format.
|
|
# w1 = [E, 2*intermediate, hidden//2] (gate + up concatenated)
|
|
w1 = torch.stack(
|
|
[torch.cat([g, u], dim=0) for g, u in zip(gate_weights, up_weights)]
|
|
).cuda()
|
|
w1_scale = torch.stack(
|
|
[torch.cat([g, u], dim=0) for g, u in zip(gate_scales, up_scales)]
|
|
).cuda()
|
|
w1_gscale = torch.stack(gate_gscales).cuda()
|
|
|
|
# w2 = [E, hidden, intermediate//2]
|
|
w2 = torch.stack(down_weights).cuda()
|
|
w2_scale = torch.stack(down_scales).cuda()
|
|
w2_gscale = torch.stack(down_gscales).cuda()
|
|
|
|
a13_scale_raw = torch.stack(a1_scales).cuda()
|
|
a2_scale_raw = torch.stack(a2_scales).cuda()
|
|
|
|
# Apply EMULATION transforms (matches oracle/nvfp4.py).
|
|
nvfp4_emulation_utils.kE2M1ToFloat_handle.val = (
|
|
nvfp4_emulation_utils.kE2M1ToFloat_handle.val.cuda()
|
|
)
|
|
a1_gscale = 1.0 / a13_scale_raw.max().to(torch.float32)
|
|
a2_gscale = 1.0 / a2_scale_raw.max().to(torch.float32)
|
|
|
|
# ── Simulate TP sharding ──
|
|
# w1 (gate_up): column-parallel → shard the N dimension (dim 1).
|
|
# w2 (down): row-parallel → shard the K dimension (dim 2,
|
|
# which is the packed K//2 dim).
|
|
# Scales follow the same sharding on the corresponding dimension.
|
|
tp = tensor_parallel_size
|
|
if tp > 1:
|
|
n1 = w1.size(1) // tp
|
|
w1 = w1[:, :n1, :].contiguous()
|
|
w1_scale = w1_scale[:, :n1, :].contiguous()
|
|
|
|
k2_packed = w2.size(2) // tp
|
|
k2_scale = w2_scale.size(2) // tp
|
|
w2 = w2[:, :, :k2_packed].contiguous()
|
|
w2_scale = w2_scale[:, :, :k2_scale].contiguous()
|
|
|
|
hidden_dim = w1.size(2) * 2
|
|
intermediate_size = w1.size(1) // 2
|
|
|
|
return (
|
|
w1,
|
|
w1_scale,
|
|
w1_gscale,
|
|
w2,
|
|
w2_scale,
|
|
w2_gscale,
|
|
a1_gscale,
|
|
a2_gscale,
|
|
num_experts,
|
|
hidden_dim,
|
|
intermediate_size,
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike(),
|
|
reason="Triton NVFP4 kernel requires CUDA.",
|
|
)
|
|
@pytest.mark.parametrize("num_tokens", [1, 2, 4, 1024])
|
|
@pytest.mark.parametrize("top_k", [4])
|
|
@pytest.mark.parametrize("model_id", list(MOE_MODEL_CONFIGS.keys()))
|
|
@pytest.mark.parametrize(
|
|
"tensor_parallel_size",
|
|
[pytest.param(val, id=f"tensor_parallel_size:{val}") for val in [1, 2, 4, 8]],
|
|
)
|
|
def test_nvfp4_moe_correctness(
|
|
loaded_model_files,
|
|
num_tokens: int,
|
|
top_k: int,
|
|
model_id: str,
|
|
tensor_parallel_size: int,
|
|
) -> None:
|
|
"""Compare Nvfp4QuantizationEmulationTritonExperts (fused weight dequant + compute)
|
|
against the unfused reference Nvfp4QuantizationEmulationTritonExpertsReference.
|
|
|
|
Both must produce bit-identical results.
|
|
"""
|
|
num_test_experts = max(8, top_k)
|
|
(
|
|
w1,
|
|
w1_scale,
|
|
w1_gscale,
|
|
w2,
|
|
w2_scale,
|
|
w2_gscale,
|
|
a1_gscale,
|
|
a2_gscale,
|
|
num_experts,
|
|
hidden_dim,
|
|
intermediate_size,
|
|
) = _load_nvfp4_moe_weights(
|
|
loaded_model_files,
|
|
model_id,
|
|
tensor_parallel_size,
|
|
max_experts=num_test_experts,
|
|
)
|
|
|
|
moe_config = FusedMoEConfig(
|
|
num_experts=num_experts,
|
|
experts_per_token=top_k,
|
|
hidden_dim=hidden_dim,
|
|
intermediate_size=intermediate_size,
|
|
num_local_experts=num_experts,
|
|
num_logical_experts=num_experts,
|
|
moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
|
|
activation=MoEActivation.SILU,
|
|
in_dtype=torch.bfloat16,
|
|
device="cuda",
|
|
routing_method=RoutingMethodType.TopK,
|
|
max_num_tokens=512,
|
|
)
|
|
|
|
def _make_quant_config():
|
|
return nvfp4_moe_quant_config(
|
|
g1_alphas=w1_gscale.clone(),
|
|
g2_alphas=w2_gscale.clone(),
|
|
a1_gscale=a1_gscale.clone(),
|
|
a2_gscale=a2_gscale.clone(),
|
|
w1_scale=w1_scale.clone(),
|
|
w2_scale=w2_scale.clone(),
|
|
)
|
|
|
|
ref_experts = Nvfp4QuantizationEmulationTritonExpertsReference(
|
|
moe_config=moe_config,
|
|
quant_config=_make_quant_config(),
|
|
)
|
|
fused_experts = Nvfp4QuantizationEmulationTritonExperts(
|
|
moe_config=moe_config,
|
|
quant_config=_make_quant_config(),
|
|
)
|
|
|
|
torch.manual_seed(42)
|
|
hidden_states = torch.randn(
|
|
num_tokens, hidden_dim, dtype=torch.bfloat16, device="cuda"
|
|
)
|
|
|
|
topk_weights = torch.randn(
|
|
num_tokens, top_k, dtype=torch.float32, device="cuda"
|
|
).softmax(dim=-1)
|
|
topk_ids = torch.stack(
|
|
[torch.randperm(num_experts, device="cuda")[:top_k] for _ in range(num_tokens)]
|
|
).to(torch.int32)
|
|
|
|
N = w1.size(1) # 2 * intermediate
|
|
K = hidden_dim
|
|
|
|
ws13_size = num_tokens * top_k * max(intermediate_size, K)
|
|
ws2_size = num_tokens * top_k * max(N, K)
|
|
|
|
workspace13_ref = torch.zeros(ws13_size, dtype=torch.bfloat16, device="cuda")
|
|
workspace2_ref = torch.zeros(ws2_size, dtype=torch.bfloat16, device="cuda")
|
|
output_ref = torch.zeros(num_tokens, K, dtype=torch.bfloat16, device="cuda")
|
|
|
|
workspace13_fused = torch.zeros_like(workspace13_ref)
|
|
workspace2_fused = torch.zeros_like(workspace2_ref)
|
|
output_fused = torch.zeros_like(output_ref)
|
|
|
|
apply_kwargs = dict(
|
|
hidden_states=hidden_states,
|
|
w1=w1,
|
|
w2=w2,
|
|
topk_weights=topk_weights,
|
|
topk_ids=topk_ids,
|
|
activation=MoEActivation.SILU,
|
|
global_num_experts=num_experts,
|
|
expert_map=None,
|
|
a1q_scale=None,
|
|
a2_scale=None,
|
|
expert_tokens_meta=None,
|
|
apply_router_weight_on_input=False,
|
|
)
|
|
|
|
# Unfused reference.
|
|
ref_experts.apply(
|
|
output=output_ref,
|
|
workspace13=workspace13_ref,
|
|
workspace2=workspace2_ref,
|
|
**apply_kwargs,
|
|
)
|
|
|
|
# Fused implementation.
|
|
fused_experts.apply(
|
|
output=output_fused,
|
|
workspace13=workspace13_fused,
|
|
workspace2=workspace2_fused,
|
|
**apply_kwargs,
|
|
)
|
|
|
|
# Not strict equality on H100, MI325, MI300 (< 0.1% elements).
|
|
# The fused on-the-fly dequant path can lower to a slightly
|
|
# different Triton/MMA tiling than the pre-dequantized
|
|
# reference; experiments with reference-like tiling/masking
|
|
# reduced some diffs were not kept because they regress
|
|
# the fused kernel speed.
|
|
# Strict equality validated on MI355.
|
|
torch.testing.assert_close(
|
|
output_fused,
|
|
output_ref,
|
|
atol=0.0 if on_gfx950() else 0.02,
|
|
rtol=0,
|
|
)
|