681 lines
23 KiB
Python
681 lines
23 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import vllm.config
|
|
import vllm.ir.ops
|
|
import vllm.plugins
|
|
from tests.compile.backend import TestBackend
|
|
from tests.utils import TestFP8Layer
|
|
from vllm._aiter_ops import IS_AITER_FOUND, rocm_aiter_ops
|
|
from vllm.compilation.passes.fusion.matcher_utils import QUANT_OPS
|
|
from vllm.compilation.passes.fusion.rms_quant_fusion import (
|
|
FUSED_OPS,
|
|
FusedRMSQuantKey,
|
|
RMSNormQuantFusionPass,
|
|
)
|
|
from vllm.compilation.passes.utility.noop_elimination import NoOpEliminationPass
|
|
from vllm.compilation.passes.utility.post_cleanup import PostCleanupPass
|
|
from vllm.config import (
|
|
CompilationConfig,
|
|
CompilationMode,
|
|
ModelConfig,
|
|
PassConfig,
|
|
VllmConfig,
|
|
)
|
|
from vllm.model_executor.kernels.linear import (
|
|
AiterFp8BlockScaledMMKernel,
|
|
ChannelWiseTorchFP8ScaledMMLinearKernel,
|
|
CutlassFp8BlockScaledMMKernel,
|
|
CutlassFP8ScaledMMLinearKernel,
|
|
DeepGemmFp8BlockScaledMMKernel,
|
|
FlashInferFp8DeepGEMMDynamicBlockScaledKernel,
|
|
FlashInferFP8ScaledMMLinearKernel,
|
|
PerTensorTorchFP8ScaledMMLinearKernel,
|
|
ROCmFP8ScaledMMLinearKernel,
|
|
RowWiseTorchFP8ScaledMMLinearKernel,
|
|
TritonFp8BlockScaledMMKernel,
|
|
_KernelT,
|
|
)
|
|
from vllm.model_executor.layers.layernorm import RMSNorm, RMSNormGated
|
|
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
|
GroupShape,
|
|
create_fp8_quant_key,
|
|
)
|
|
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
|
|
cutlass_block_fp8_supported,
|
|
)
|
|
from vllm.platforms import current_platform
|
|
from vllm.utils.deep_gemm import (
|
|
is_deep_gemm_e8m0_used,
|
|
is_deep_gemm_supported,
|
|
)
|
|
|
|
FP8_DTYPE = current_platform.fp8_dtype()
|
|
|
|
RMS_ADD_OP = torch.ops._C.fused_add_rms_norm.default
|
|
|
|
# Kernel and group_shape combinations: (kernel, group_shape)
|
|
# CUDA kernels
|
|
CUDA_KERNEL_GROUPSHAPE_COMBINATIONS = [
|
|
# FlashInferFP8ScaledMMLinearKernel supports both per-tensor only
|
|
(FlashInferFP8ScaledMMLinearKernel, GroupShape.PER_TENSOR),
|
|
# CutlassFP8ScaledMMLinearKernel supports both per-tensor and per-token
|
|
(CutlassFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN),
|
|
(CutlassFP8ScaledMMLinearKernel, GroupShape.PER_TENSOR),
|
|
# PerTensorTorchFP8ScaledMMLinearKernel only supports per-tensor
|
|
(PerTensorTorchFP8ScaledMMLinearKernel, GroupShape.PER_TENSOR),
|
|
# ChannelWiseTorchFP8ScaledMMLinearKernel only supports per-token
|
|
(ChannelWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN),
|
|
# Blockwise group shapes
|
|
(FlashInferFp8DeepGEMMDynamicBlockScaledKernel, GroupShape(1, 128)),
|
|
(CutlassFp8BlockScaledMMKernel, GroupShape(1, 128)),
|
|
(DeepGemmFp8BlockScaledMMKernel, GroupShape(1, 128)),
|
|
(TritonFp8BlockScaledMMKernel, GroupShape(1, 128)),
|
|
(TritonFp8BlockScaledMMKernel, GroupShape(1, 64)),
|
|
]
|
|
|
|
# ROCm kernels
|
|
ROCM_KERNEL_GROUPSHAPE_COMBINATIONS = [
|
|
# ROCmFP8ScaledMMLinearKernel supports per-tensor only
|
|
(ROCmFP8ScaledMMLinearKernel, GroupShape.PER_TENSOR),
|
|
# RowWiseTorchFP8ScaledMMLinearKernel only supports per-token
|
|
(RowWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN),
|
|
# ChannelWiseTorchFP8ScaledMMLinearKernel only supports per-token
|
|
(ChannelWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN),
|
|
# Blockwise group shapes (no kernel abstraction)
|
|
(TritonFp8BlockScaledMMKernel, GroupShape(1, 128)),
|
|
(TritonFp8BlockScaledMMKernel, GroupShape(1, 64)),
|
|
]
|
|
|
|
KERNEL_GROUPSHAPE_COMBINATIONS = (
|
|
CUDA_KERNEL_GROUPSHAPE_COMBINATIONS
|
|
if current_platform.is_cuda()
|
|
else ROCM_KERNEL_GROUPSHAPE_COMBINATIONS
|
|
)
|
|
|
|
# For Aiter tests we toggle use_aiter_quant_op
|
|
AITER_KERNEL_GROUPSHAPE_COMBINATIONS = [
|
|
# Per-token with RowWiseTorchFP8ScaledMMLinearKernel
|
|
(RowWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN, True),
|
|
(RowWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN, False),
|
|
# Per-token with ChannelWiseTorchFP8ScaledMMLinearKernel
|
|
(ChannelWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN, True),
|
|
(ChannelWiseTorchFP8ScaledMMLinearKernel, GroupShape.PER_TOKEN, False),
|
|
# Blockwise
|
|
(AiterFp8BlockScaledMMKernel, GroupShape(1, 128), True),
|
|
]
|
|
|
|
|
|
class TestModel(torch.nn.Module):
|
|
def __init__(
|
|
self,
|
|
hidden_size: int,
|
|
eps: float,
|
|
force_kernel: type[_KernelT] | None,
|
|
group_shape: GroupShape,
|
|
dtype: torch.dtype,
|
|
use_aiter_fusion: bool = False,
|
|
use_aiter_quant: bool = False,
|
|
*args,
|
|
**kwargs,
|
|
):
|
|
super().__init__(*args, **kwargs)
|
|
self.fp8_linear_layers: list[torch.nn.Module]
|
|
self.group_shape = group_shape
|
|
self.use_aiter_quant_op = use_aiter_quant
|
|
self.use_aiter_fusion = use_aiter_fusion
|
|
self.norm = [RMSNorm(hidden_size, eps) for _ in range(4)]
|
|
self.enable_rms_norm_custom_op = self.norm[0].enabled()
|
|
|
|
# Determine if blockwise based on group_shape
|
|
is_blockwise = group_shape.is_per_group()
|
|
|
|
if is_blockwise:
|
|
block_size = group_shape.col
|
|
self.activation_quant_key = create_fp8_quant_key(
|
|
static=False, group_shape=group_shape
|
|
)
|
|
self.weight_quant_key = create_fp8_quant_key(
|
|
static=True, group_shape=GroupShape(block_size, block_size)
|
|
)
|
|
|
|
else:
|
|
is_static = group_shape == GroupShape.PER_TENSOR
|
|
self.activation_quant_key = create_fp8_quant_key(
|
|
is_static, group_shape=group_shape
|
|
)
|
|
self.weight_quant_key = create_fp8_quant_key(
|
|
static=True, group_shape=group_shape
|
|
)
|
|
|
|
self.fp8_linear_layers = [
|
|
TestFP8Layer(
|
|
weight_shape=(hidden_size, hidden_size),
|
|
activation_quant_key=self.activation_quant_key,
|
|
weight_quant_key=self.weight_quant_key,
|
|
force_kernel=force_kernel,
|
|
transpose_weights=use_aiter_fusion,
|
|
input_dtype=dtype,
|
|
)
|
|
for _ in range(3)
|
|
]
|
|
|
|
# Enable aiter quantization if requested
|
|
for layer in self.fp8_linear_layers:
|
|
layer.kernel.quant_fp8.use_aiter = use_aiter_quant
|
|
|
|
self.enable_quant_fp8_custom_op = self.fp8_linear_layers[
|
|
0
|
|
].is_quant_fp8_enabled()
|
|
|
|
def forward(self, x):
|
|
# avoid having graph input be an arg to a pattern directly
|
|
x = resid = torch.relu(x)
|
|
y = self.norm[0](x)
|
|
|
|
x2 = self.fp8_linear_layers[0](y)
|
|
# make sure resid is used for replacement to work
|
|
y2, resid = self.norm[1](x2, resid)
|
|
|
|
x3 = self.fp8_linear_layers[1](y2)
|
|
|
|
y3, resid = self.norm[2](x3, resid) # use resid here
|
|
|
|
x4 = self.fp8_linear_layers[2](y3)
|
|
|
|
y4, resid = self.norm[3](x4, resid) # use resid here
|
|
return y4
|
|
|
|
def ops_in_model_before(self):
|
|
if self.group_shape.is_per_group():
|
|
# Blockwise path
|
|
if self.use_aiter_fusion and self.use_aiter_quant_op:
|
|
return [rocm_aiter_ops.get_group_quant_op()]
|
|
if self.use_aiter_fusion:
|
|
return [torch.ops.vllm.triton_per_token_group_quant_fp8.default]
|
|
else:
|
|
if self.use_aiter_quant_op:
|
|
return [rocm_aiter_ops.get_per_token_quant_op()]
|
|
|
|
# Common path
|
|
return (
|
|
[QUANT_OPS[self.activation_quant_key]]
|
|
if self.enable_quant_fp8_custom_op
|
|
else [torch.ops.aten.reciprocal]
|
|
)
|
|
|
|
def ops_in_model_after(self):
|
|
if self.use_aiter_fusion:
|
|
if self.group_shape.is_per_group():
|
|
# Blockwise aiter fusion
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
AiterFusedAddRMSFp8GroupQuantPattern,
|
|
AiterRMSFp8GroupQuantPattern,
|
|
)
|
|
|
|
return [
|
|
AiterFusedAddRMSFp8GroupQuantPattern.FUSED_OP,
|
|
AiterRMSFp8GroupQuantPattern.FUSED_OP,
|
|
]
|
|
else:
|
|
# Per-token aiter fusion
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
AiterFusedAddRMSNormDynamicQuantPattern,
|
|
AiterRMSNormDynamicQuantPattern,
|
|
)
|
|
|
|
return [
|
|
AiterFusedAddRMSNormDynamicQuantPattern.FUSED_OP,
|
|
AiterRMSNormDynamicQuantPattern.FUSED_OP,
|
|
]
|
|
|
|
# Regular fusion
|
|
return [
|
|
FUSED_OPS[FusedRMSQuantKey(self.activation_quant_key, True)],
|
|
FUSED_OPS[FusedRMSQuantKey(self.activation_quant_key, False)],
|
|
]
|
|
|
|
def ops_in_model_before_partial(self):
|
|
return [
|
|
torch.ops.vllm_ir.rms_norm,
|
|
torch.ops.vllm_ir.fused_add_rms_norm.default,
|
|
]
|
|
|
|
|
|
def _run_fusion_test(
|
|
model,
|
|
fusion_pass,
|
|
vllm_config,
|
|
dtype,
|
|
hidden_size,
|
|
num_tokens,
|
|
):
|
|
"""Helper function for common fusion test logic.
|
|
|
|
Must be called within vllm_config context.
|
|
"""
|
|
noop_pass = NoOpEliminationPass(vllm_config)
|
|
cleanup_pass = PostCleanupPass(vllm_config)
|
|
|
|
backend = TestBackend(noop_pass, fusion_pass, cleanup_pass)
|
|
backend2 = TestBackend(noop_pass, cleanup_pass)
|
|
|
|
x = torch.rand(num_tokens, hidden_size)
|
|
torch._dynamo.mark_dynamic(x, 0)
|
|
|
|
model_fused = torch.compile(model, backend=backend)
|
|
result_fused = model_fused(x)
|
|
|
|
model_unfused = torch.compile(model, backend=backend2)
|
|
result_unfused = model_unfused(x)
|
|
|
|
if dtype == torch.float16:
|
|
ATOL, RTOL = (2e-3, 2e-3)
|
|
else:
|
|
ATOL, RTOL = (1e-2, 1e-2)
|
|
|
|
torch.testing.assert_close(result_fused, result_unfused, atol=ATOL, rtol=RTOL)
|
|
|
|
assert fusion_pass.matched_count == 3
|
|
backend.check_before_ops(model.ops_in_model_before())
|
|
backend.check_after_ops(model.ops_in_model_after())
|
|
|
|
return backend, backend2
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
|
@pytest.mark.parametrize("hidden_size", [256])
|
|
@pytest.mark.parametrize("num_tokens", [257])
|
|
@pytest.mark.parametrize("eps", [1e-5, 1e-6])
|
|
@pytest.mark.parametrize("kernel_groupshape", KERNEL_GROUPSHAPE_COMBINATIONS)
|
|
@pytest.mark.parametrize("enable_rms_norm_custom_op", [True, False])
|
|
@pytest.mark.parametrize("enable_quant_fp8_custom_op", [True, False])
|
|
@pytest.mark.skipif(
|
|
not current_platform.is_cuda_alike(), reason="Only test on CUDA and ROCm"
|
|
)
|
|
def test_fusion_rmsnorm_quant(
|
|
dtype,
|
|
hidden_size,
|
|
num_tokens,
|
|
eps,
|
|
kernel_groupshape,
|
|
enable_rms_norm_custom_op,
|
|
enable_quant_fp8_custom_op,
|
|
):
|
|
force_kernel, group_shape = kernel_groupshape
|
|
|
|
if not enable_quant_fp8_custom_op and group_shape.is_per_group():
|
|
pytest.skip("Unsupported unwrapped quant fp8 op for blockwise quantization")
|
|
|
|
if group_shape == GroupShape(1, 64) and (
|
|
cutlass_block_fp8_supported() or is_deep_gemm_supported()
|
|
):
|
|
pytest.skip("Unsupported group shape 64 for CUTLASS/DeepGemm")
|
|
|
|
# TODO(quant-rms-fusion): DeepGEMM UE8M0 activation quant on B200 lowers
|
|
# to a packed int32-scale op (per_token_group_quant_fp8_packed_for_deepgemm),
|
|
# but the rms+quant fusion pattern only matches the fp32-scale variant, so
|
|
# the fused output gets a mismatched scale layout and produces NaN. Only
|
|
# reproduces on bf16 (DeepGEMM UE8M0 on B200 is bf16-only).
|
|
# To re-enable: make rms_norm_per_block_quant emit packed UE8M0 scales
|
|
# and extend the fusion pattern to rewrite the packed activation quant.
|
|
deepgemm_kernels = (
|
|
DeepGemmFp8BlockScaledMMKernel,
|
|
FlashInferFp8DeepGEMMDynamicBlockScaledKernel,
|
|
)
|
|
if (
|
|
dtype == torch.bfloat16
|
|
and force_kernel in deepgemm_kernels
|
|
and is_deep_gemm_e8m0_used()
|
|
):
|
|
pytest.skip(
|
|
"rms+quant fusion does not yet match the packed UE8M0 DeepGEMM path"
|
|
)
|
|
|
|
custom_ops = []
|
|
if enable_rms_norm_custom_op:
|
|
custom_ops.append("+rms_norm")
|
|
if enable_quant_fp8_custom_op:
|
|
custom_ops.append("+quant_fp8")
|
|
|
|
vllm_config = VllmConfig(
|
|
model_config=ModelConfig(dtype=dtype),
|
|
compilation_config=CompilationConfig(
|
|
mode=CompilationMode.VLLM_COMPILE,
|
|
custom_ops=custom_ops,
|
|
pass_config=PassConfig(
|
|
fuse_norm_quant=True, fuse_act_quant=True, eliminate_noops=True
|
|
),
|
|
),
|
|
)
|
|
|
|
with (
|
|
vllm.config.set_current_vllm_config(vllm_config),
|
|
vllm_config.kernel_config.ir_op_priority.set_priority(),
|
|
):
|
|
# Setup device before model creation
|
|
torch.set_default_device("cuda")
|
|
torch.set_default_dtype(dtype)
|
|
torch.manual_seed(1)
|
|
|
|
fusion_pass = RMSNormQuantFusionPass(vllm_config)
|
|
|
|
model = TestModel(
|
|
hidden_size=hidden_size,
|
|
eps=eps,
|
|
force_kernel=force_kernel,
|
|
group_shape=group_shape,
|
|
dtype=dtype,
|
|
use_aiter_fusion=False,
|
|
use_aiter_quant=False,
|
|
)
|
|
|
|
backend, _ = _run_fusion_test(
|
|
model, fusion_pass, vllm_config, dtype, hidden_size, num_tokens
|
|
)
|
|
backend.check_before_ops(
|
|
model.ops_in_model_before_partial(), fully_replaced=False
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
|
@pytest.mark.parametrize("hidden_size", [256])
|
|
@pytest.mark.parametrize("num_tokens", [257])
|
|
@pytest.mark.parametrize("eps", [1e-5, 1e-6])
|
|
@pytest.mark.parametrize(
|
|
"kernel_groupshape_quant", AITER_KERNEL_GROUPSHAPE_COMBINATIONS
|
|
)
|
|
@pytest.mark.skipif(
|
|
(not current_platform.is_rocm() or not IS_AITER_FOUND),
|
|
reason="Only test on ROCm with aiter package installed",
|
|
)
|
|
def test_aiter_fusion_rmsnorm_quant(
|
|
dtype: torch.dtype,
|
|
hidden_size: int,
|
|
num_tokens: int,
|
|
eps: float,
|
|
kernel_groupshape_quant: tuple,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
force_kernel, group_shape, use_aiter_quant_op = kernel_groupshape_quant
|
|
vllm_config = VllmConfig(
|
|
model_config=ModelConfig(dtype=dtype),
|
|
compilation_config=CompilationConfig(
|
|
mode=CompilationMode.VLLM_COMPILE,
|
|
custom_ops=["+rms_norm", "+quant_fp8"],
|
|
pass_config=PassConfig(fuse_norm_quant=True, eliminate_noops=True),
|
|
),
|
|
)
|
|
|
|
with vllm.config.set_current_vllm_config(vllm_config), monkeypatch.context() as m:
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
RocmAiterRMSNormQuantFusionPass,
|
|
)
|
|
|
|
m.setenv("VLLM_ROCM_USE_AITER", "1")
|
|
|
|
rocm_aiter_ops.refresh_env_variables()
|
|
|
|
torch.set_default_device("cuda")
|
|
torch.set_default_dtype(dtype)
|
|
torch.manual_seed(1)
|
|
|
|
fusion_pass = RocmAiterRMSNormQuantFusionPass(vllm_config)
|
|
|
|
model = TestModel(
|
|
hidden_size=hidden_size,
|
|
eps=eps,
|
|
force_kernel=force_kernel,
|
|
group_shape=group_shape,
|
|
dtype=dtype,
|
|
use_aiter_fusion=True, # Always use aiter fusion ops in aiter test
|
|
use_aiter_quant=use_aiter_quant_op, # Toggle aiter quantization
|
|
)
|
|
|
|
_run_fusion_test(
|
|
model, fusion_pass, vllm_config, dtype, hidden_size, num_tokens
|
|
)
|
|
|
|
|
|
class TestGatedModel(torch.nn.Module):
|
|
"""Model that uses RMSNormGated + reshape + group FP8 quant + linear.
|
|
|
|
Mimics GatedDeltaNetAttention's output projection path where:
|
|
- RMSNormGated operates on per-head tensors (N*H, D)
|
|
- Output is reshaped to (N, H*D) before group quantization + linear
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
num_heads: int,
|
|
head_dim: int,
|
|
eps: float,
|
|
force_kernel: type[_KernelT],
|
|
group_shape: GroupShape,
|
|
dtype: torch.dtype,
|
|
use_aiter_quant: bool = True,
|
|
):
|
|
super().__init__()
|
|
self.num_heads = num_heads
|
|
self.head_dim = head_dim
|
|
hidden_dim = num_heads * head_dim
|
|
|
|
self.norm = RMSNormGated(
|
|
head_dim,
|
|
eps=eps,
|
|
group_size=None,
|
|
norm_before_gate=True,
|
|
)
|
|
|
|
self.activation_quant_key = create_fp8_quant_key(
|
|
static=False, group_shape=group_shape
|
|
)
|
|
self.weight_quant_key = create_fp8_quant_key(
|
|
static=True, group_shape=GroupShape(group_shape.col, group_shape.col)
|
|
)
|
|
|
|
self.fp8_linear = TestFP8Layer(
|
|
weight_shape=(hidden_dim, hidden_dim),
|
|
activation_quant_key=self.activation_quant_key,
|
|
weight_quant_key=self.weight_quant_key,
|
|
force_kernel=force_kernel,
|
|
transpose_weights=True,
|
|
input_dtype=dtype,
|
|
)
|
|
self.fp8_linear.kernel.quant_fp8.use_aiter = use_aiter_quant
|
|
|
|
def forward(self, x, z):
|
|
num_heads = self.num_heads
|
|
head_dim = self.head_dim
|
|
hidden_dim = num_heads * head_dim
|
|
x = torch.relu(x)
|
|
z = torch.relu(z)
|
|
x_heads = x.reshape(-1, num_heads, head_dim).reshape(-1, head_dim)
|
|
z_heads = z.reshape(-1, num_heads, head_dim).reshape(-1, head_dim)
|
|
normed = self.norm(x_heads, z_heads)
|
|
merged = normed.reshape(-1, hidden_dim)
|
|
out = self.fp8_linear(merged)
|
|
return out
|
|
|
|
def ops_in_model_after(self):
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
AiterRMSNormGatedFp8GroupQuantPattern,
|
|
)
|
|
|
|
return [AiterRMSNormGatedFp8GroupQuantPattern.FUSED_OP]
|
|
|
|
|
|
class _MockGDNLayer:
|
|
"""Minimal mock to populate static_forward_context for pass discovery.
|
|
|
|
Uses __class__ assignment to pass isinstance checks against
|
|
GatedDeltaNetAttention without requiring a full config-based init.
|
|
"""
|
|
|
|
def __init__(self, num_v_heads: int, head_v_dim: int, tp_size: int = 1):
|
|
self.num_v_heads = num_v_heads
|
|
self.head_v_dim = head_v_dim
|
|
self.tp_size = tp_size
|
|
|
|
from vllm.model_executor.layers.mamba.gdn.base import (
|
|
GatedDeltaNetAttention,
|
|
)
|
|
|
|
self.__class__ = GatedDeltaNetAttention
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
|
@pytest.mark.parametrize("num_heads", [2])
|
|
@pytest.mark.parametrize("head_dim", [128])
|
|
@pytest.mark.parametrize("num_tokens", [8])
|
|
@pytest.mark.parametrize("eps", [1e-5, 1e-6])
|
|
@pytest.mark.skipif(
|
|
(not current_platform.is_rocm() or not IS_AITER_FOUND),
|
|
reason="Only test on ROCm with aiter package installed",
|
|
)
|
|
def test_aiter_fusion_rmsnorm_gated_quant(
|
|
dtype: torch.dtype,
|
|
num_heads: int,
|
|
head_dim: int,
|
|
num_tokens: int,
|
|
eps: float,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
group_shape = GroupShape(1, 128)
|
|
vllm_config = VllmConfig(
|
|
model_config=ModelConfig(dtype=dtype),
|
|
compilation_config=CompilationConfig(
|
|
mode=CompilationMode.VLLM_COMPILE,
|
|
custom_ops=["-rms_norm", "-silu_and_mul", "-quant_fp8"],
|
|
pass_config=PassConfig(fuse_norm_quant=True, eliminate_noops=True),
|
|
),
|
|
)
|
|
|
|
with vllm.config.set_current_vllm_config(vllm_config), monkeypatch.context() as m:
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
RocmAiterRMSNormQuantFusionPass,
|
|
)
|
|
|
|
m.setenv("VLLM_ROCM_USE_AITER", "1")
|
|
rocm_aiter_ops.refresh_env_variables()
|
|
|
|
# Register a mock GDN layer so the pass discovers num_heads/head_dim
|
|
mock_gdn = _MockGDNLayer(num_v_heads=num_heads, head_v_dim=head_dim, tp_size=1)
|
|
vllm_config.compilation_config.static_forward_context["mock_gdn_layer"] = (
|
|
mock_gdn
|
|
)
|
|
|
|
torch.set_default_device("cuda")
|
|
torch.set_default_dtype(dtype)
|
|
torch.manual_seed(1)
|
|
|
|
fusion_pass = RocmAiterRMSNormQuantFusionPass(vllm_config)
|
|
|
|
model = TestGatedModel(
|
|
num_heads=num_heads,
|
|
head_dim=head_dim,
|
|
eps=eps,
|
|
force_kernel=AiterFp8BlockScaledMMKernel,
|
|
group_shape=group_shape,
|
|
dtype=dtype,
|
|
use_aiter_quant=True,
|
|
)
|
|
|
|
noop_pass = NoOpEliminationPass(vllm_config)
|
|
cleanup_pass = PostCleanupPass(vllm_config)
|
|
|
|
backend = TestBackend(noop_pass, fusion_pass, cleanup_pass)
|
|
backend2 = TestBackend(noop_pass, cleanup_pass)
|
|
|
|
hidden_dim = num_heads * head_dim
|
|
x = torch.rand(num_tokens, hidden_dim)
|
|
z = torch.rand(num_tokens, hidden_dim)
|
|
torch._dynamo.mark_dynamic(x, 0)
|
|
torch._dynamo.mark_dynamic(z, 0)
|
|
|
|
model_fused = torch.compile(model, backend=backend)
|
|
result_fused = model_fused(x, z)
|
|
|
|
model_unfused = torch.compile(model, backend=backend2)
|
|
result_unfused = model_unfused(x, z)
|
|
|
|
torch.testing.assert_close(result_fused, result_unfused, atol=1e-2, rtol=1e-2)
|
|
|
|
assert fusion_pass.matched_count == 1
|
|
backend.check_after_ops(model.ops_in_model_after())
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
|
@pytest.mark.parametrize("num_heads", [2])
|
|
@pytest.mark.parametrize("head_dim", [128])
|
|
@pytest.mark.parametrize("num_tokens", [8])
|
|
@pytest.mark.parametrize("eps", [1e-6])
|
|
@pytest.mark.skipif(
|
|
(not current_platform.is_rocm() or not IS_AITER_FOUND),
|
|
reason="Only test on ROCm with aiter package installed",
|
|
)
|
|
def test_aiter_fusion_rmsnorm_gated_quant_no_gdn_layers(
|
|
dtype: torch.dtype,
|
|
num_heads: int,
|
|
head_dim: int,
|
|
num_tokens: int,
|
|
eps: float,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""Verify that without GDN layers in static_forward_context,
|
|
the gated pattern is not registered and no matches occur."""
|
|
group_shape = GroupShape(1, 128)
|
|
vllm_config = VllmConfig(
|
|
model_config=ModelConfig(dtype=dtype),
|
|
compilation_config=CompilationConfig(
|
|
mode=CompilationMode.VLLM_COMPILE,
|
|
custom_ops=["-rms_norm", "-silu_and_mul", "-quant_fp8"],
|
|
pass_config=PassConfig(fuse_norm_quant=True, eliminate_noops=True),
|
|
),
|
|
)
|
|
|
|
with vllm.config.set_current_vllm_config(vllm_config), monkeypatch.context() as m:
|
|
from vllm.compilation.passes.fusion.rocm_aiter_fusion import (
|
|
RocmAiterRMSNormQuantFusionPass,
|
|
)
|
|
|
|
m.setenv("VLLM_ROCM_USE_AITER", "1")
|
|
rocm_aiter_ops.refresh_env_variables()
|
|
|
|
torch.set_default_device("cuda")
|
|
torch.set_default_dtype(dtype)
|
|
torch.manual_seed(1)
|
|
|
|
# No mock GDN layer registered -- pass should not register gated pattern
|
|
fusion_pass = RocmAiterRMSNormQuantFusionPass(vllm_config)
|
|
|
|
model = TestGatedModel(
|
|
num_heads=num_heads,
|
|
head_dim=head_dim,
|
|
eps=eps,
|
|
force_kernel=AiterFp8BlockScaledMMKernel,
|
|
group_shape=group_shape,
|
|
dtype=dtype,
|
|
use_aiter_quant=True,
|
|
)
|
|
|
|
noop_pass = NoOpEliminationPass(vllm_config)
|
|
cleanup_pass = PostCleanupPass(vllm_config)
|
|
|
|
backend = TestBackend(noop_pass, fusion_pass, cleanup_pass)
|
|
|
|
hidden_dim = num_heads * head_dim
|
|
x = torch.rand(num_tokens, hidden_dim)
|
|
z = torch.rand(num_tokens, hidden_dim)
|
|
torch._dynamo.mark_dynamic(x, 0)
|
|
torch._dynamo.mark_dynamic(z, 0)
|
|
|
|
model_fused = torch.compile(model, backend=backend)
|
|
model_fused(x, z)
|
|
|
|
assert fusion_pass.matched_count == 0
|