Prhub

#44260 Add the QuantizedActivation linear-kernel contract

原始 PR 作者 mgoin 合并时间 2026-06-13 04:48 文件变更 13 提交数 6 评论 10 代码增减 +327 / -27

执行摘要

引入 QuantizedActivation 契约,融合 kernel 可跳过线性层输入量化

Issue #43224 推动将 torch.compile 融合迁移为手动融合。本 PR 实现消费者侧契约,使融合 kernel 能直接传递预量化激活给线性层,避免重复量化,为后续 norm+quant 等融合铺路。

值得精读,尤其是 quant_activation.py 的契约设计和 as_quantized_activation 的防御式验证模式。这是 vllm 量化融合架构迁移的核心基石,理解它有助于参与后续手动融合工作。

讨论亮点

BadrBasowid 建议提取 apply_mm 静态方法减少 apply_weights 分支,mgoin 回应分支逻辑必须保留在 kernel 内。HDCharles 质疑 as_quantized_activation API 的复杂度,mgoin 表示是风格选择可后续修改。zyongye 提议将 padding/alignment 编码进 QuantKey 以减少额外 kernel launch,mgoin 认为是后续课题。HDCharles 询问是否假设无 zero point,mgoin 确认现阶段无 fused kernel 使用 zero point。

实现拆解

  1. 新增 vllm/model_executor/layers/fusion/quant_activation.py,定义 QuantizedActivation 数据类(data, scale, orig_dtype, orig_shape, quant_key),以及 expose_input_quant_key(将 kernel 声明的 key 设置到 layer.input_quant_key)和 as_quantized_activation(验证并拆包预量化激活或返回 None)两个函数。
  2. 在 MMLinearLayerConfig 基类和 ScaledMMLinearKernel 基类中添加 input_quant_key() 默认返回 None 的方法,子类可覆盖声明支持的 QuantKey。
  3. 在 FlashInferCutlassNvFp4LinearKernel 和 CutlassFP8ScaledMMLinearKernel 中覆盖 input_quant_key() 返回对应 key,FlashInferFP8ScaledMMLinearKernel 同样覆盖;其余 kernel 返回 None 维持原行为。
  4. 在 FP8ScaledMMLinearKernel.apply_weights 和 FlashInferCutlassNvFp4LinearKernel.apply_weights 中添加入口检测:调用 as_quantized_activation(x) 判断输入是否为 QuantizedActivation,若是则直接使用预量化 data/scale 并恢复原始 shape/dtype,否则执行原有的量化流程。
  5. 新增 tests/fusion/test_quant_activation_contract.py,包含四个测试:测试只有声明的三个 kernel 返回非 None key;测试这些 kernel 的 apply_weights 中实际调用 as_quantized_activation;测试 expose_input_quant_key 正确标记 layer;测试 as_quantized_activation 在 key 不匹配时抛出 AssertionError。
  6. 在 .buildkite/test_areas/quantization.yaml 中添加新测试文件的执行路径,确保 CI 覆盖。
文件 模块 状态 重要度
vllm/model_executor/layers/fusion/quant_activation.py 融合层 added 8.63
tests/fusion/test_quant_activation_contract.py 测试契约 added 7.83
vllm/model_executor/kernels/linear/scaled_mm/ScaledMMLinearKernel.py 线性 kernel modified 7.61
vllm/model_executor/kernels/linear/nvfp4/flashinfer.py 线性 kernel modified 7.56
vllm/model_executor/kernels/linear/base.py 线性 kernel modified 6.28
.buildkite/test_areas/quantization.yaml CI 配置 modified 3.98

关键符号

QuantizedActivation expose_input_quant_key as_quantized_activation ScaledMMLinearKernel.input_quant_key FP8ScaledMMLinearKernel.apply_weights FlashInferCutlassNvFp4LinearKernel.input_quant_key FlashInferCutlassNvFp4LinearKernel.apply_weights

关键源码片段

vllm/model_executor/layers/fusion/quant_activation.py data-contract

核心契约文件,定义 QuantizedActivation 数据类和两个辅助函数,消费者侧契约入口。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
QuantizedActivation 是融合 kernel 产生的预量化激活,
线性层可以直接消费,跳过自身的输入量化步骤。
"""from dataclasses import dataclassimport torchfrom vllm.model_executor.layers.quantization.utils.quant_utils import QuantKey
​
​
@dataclass
class QuantizedActivation:
    """预量化激活及其缩放因子和原始元数据。    quant_key 描述数据格式(dtype、scale 粒度、值打包)。
    layout/padding 等细节需遵循 kernel 约定。
    TODO(mgoin): 将 layout 和 padding 要求编码进契约。
    """
    data: torch.Tensor # 量化后的数据
    scale: torch.Tensor # 缩放因子
    orig_dtype: torch.dtype # 原始的浮点 dtype
    orig_shape: torch.Size # 原始 shape
    quant_key: QuantKey # 量化键,标识量化方案
​
​
def expose_input_quant_key(layer: torch.nn.Module, kernel) -> None:
    """将 kernel 支持的预量化 key 暴露到 layer.input_quant_key 属性。    如果 kernel.input_quant_key() 返回 None,则不设置属性,
    从而非支持后端不会收到 QuantizedActivation。    TODO(mgoin): 生产者也需要消费方的量化 scale(如 static input scale、global scale),
    此处后续应暴露这些信息,避免生产者访问 kernel 特有属性。
    """
    key = kernel.input_quant_key()
    if key is not None:
        layer.input_quant_key = key
​
​
def as_quantized_activation(
    x: "torch.Tensor | QuantizedActivation", expected_key: QuantKey | None
) -> "QuantizedActivation | None":
    """验证并拆包预量化激活。    如果 x 是 QuantizedActivation 且 quant_key 匹配 expected_key,返回它;
    如果 x 是普通 tensor,返回 None(调用方自行量化);
    如果 key 不匹配,触发断言,防止静默错误。
    """
    if not isinstance(x, QuantizedActivation):
        return None
    assert x.quant_key == expected_key, (
        f"QuantizedActivation key {x.quant_key} != consumer kernel "
        f"input_quant_key {expected_key}"
    )
    return x
tests/fusion/test_quant_activation_contract.py test-coverage

契约测试文件,验证所有已知 kernel 的 input_quant_key 声明和 apply_weights 中 pre-quantized 消费。

# SPDX-License-Identifier: Apache-2.0
"""Contract tests for the QuantizedActivation linear-kernel integration."""import pytest
import torchfrom vllm.model_executor.kernels.linear import (
    _POSSIBLE_FP8_BLOCK_KERNELS, _POSSIBLE_FP8_KERNELS,
    _POSSIBLE_INT8_KERNELS, _POSSIBLE_NVFP4_KERNELS,
)
from vllm.model_executor.kernels.linear.nvfp4.base import NvFp4LinearKernel, NvFp4LinearLayerConfig
from vllm.model_executor.kernels.linear.nvfp4.flashinfer import (
    FlashInferCutlassNvFp4LinearKernel, FlashInferTrtllmNvFp4LinearKernel,
)
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import CutlassFP8ScaledMMLinearKernel
from vllm.model_executor.kernels.linear.scaled_mm.flashinfer import FlashInferFP8ScaledMMLinearKernel
from vllm.model_executor.kernels.linear.scaled_mm.ScaledMMLinearKernel import (
    FP8ScaledMMLinearLayerConfig, Int8ScaledMMLinearKernel, Int8ScaledMMLinearLayerConfig,
)
from vllm.model_executor.layers.fusion.quant_activation import (
    QuantizedActivation, as_quantized_activation, expose_input_quant_key,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import kFp8StaticTensorSym, kNvfp4Dynamic
from vllm.platforms import current_platform# 唯一支持预量化的三个 kernel
SUPPORTING = {
    CutlassFP8ScaledMMLinearKernel,
    FlashInferFP8ScaledMMLinearKernel,
    FlashInferCutlassNvFp4LinearKernel,
}def _all_kernel_classes() -> list[type]:
    """汇总所有已注册的线性 kernel 类。"""
    seen: dict[type, None] = {}
    for registry in (_POSSIBLE_FP8_KERNELS, _POSSIBLE_FP8_BLOCK_KERNELS,
                     _POSSIBLE_INT8_KERNELS, _POSSIBLE_NVFP4_KERNELS):
        for kernels in registry.values():
            for cls in kernels:
                seen.setdefault(cls, None)
    return list(seen)def _probe(cls: type):
    """创建一个轻量 kernel 实例(跳过 __init__),以便查询 input_quant_key。"""
    obj = cls.__new__(cls)
    if issubclass(cls, NvFp4LinearKernel):
        obj.config = NvFp4LinearLayerConfig()
    elif issubclass(cls, Int8ScaledMMLinearKernel):
        obj.config = Int8ScaledMMLinearLayerConfig(
            is_static_input_scheme=True, is_channelwise=False, input_symmetric=True)
    else:
        obj.config = FP8ScaledMMLinearLayerConfig(
            weight_quant_key=kFp8StaticTensorSym, activation_quant_key=kFp8StaticTensorSym,
            weight_shape=(16,16), input_dtype=torch.bfloat16, out_dtype=torch.bfloat16)
    return objdef test_only_known_backends_support_prequantized_input():
    """确保只有 SUPPORTING 中的 kernel 返回非 None 的 input_quant_key。"""
    declarers = {c for c in _all_kernel_classes() if _probe(c).input_quant_key()}
    assert declarers == SUPPORTINGdef test_supporting_backend_declares_consume_via_helper():
    """确保 SUPPORTING 中的 kernel 的 apply_weights 调用了 as_quantized_activation。"""
    for cls in SUPPORTING:
        fn = _resolved_apply_weights(cls)
        assert "as_quantized_activation" in fn.__code__.co_names, cls.__name__def test_as_quantized_activation_validates_key():
    """确保 as_quantized_activation 在 key 不匹配时抛出 AssertionError。"""
    qa = QuantizedActivation(
        data=torch.zeros(2, 4, dtype=current_platform.fp8_dtype()),
        scale=torch.tensor(1.0),
        orig_dtype=torch.bfloat16,
        orig_shape=torch.Size([2, 4]),
        quant_key=kFp8StaticTensorSym,
    )
    with pytest.raises(AssertionError):
        as_quantized_activation(qa, kNvfp4Dynamic)
    with pytest.raises(AssertionError):
        as_quantized_activation(qa, None)
    # 普通 tensor 应返回 None
    assert as_quantized_activation(torch.zeros(2, 4), kFp8StaticTensorSym) is None
    # 匹配的 QuantizedActivation 应返回自身
    assert as_quantized_activation(qa, kFp8StaticTensorSym) is qa
vllm/model_executor/kernels/linear/scaled_mm/ScaledMMLinearKernel.py data-contract

展示了 input_quant_key 基类方法和 FP8ScaledMMLinearKernel.apply_weights 的 QuantizedActivation 消费模式。

class ScaledMMLinearKernel(Generic[_ConfigT, _ParamsT], ABC):
    # ... 构造函数、抽象方法等 ...
​
    def input_quant_key(self) -> QuantKey | None:
        """返回本 kernel 支持消费的预量化激活 QuantKey。        手动融合据此决定是否将量化外提到上游融合 kernel。
        返回 None 表示 kernel 需要自行量化(有自定义 padding 或 swizzling 等)。
        若返回非 None,apply_weights 必须通过 as_quantized_activation 消费输入。
        """
        return None
​
​
class FP8ScaledMMLinearKernel(ScaledMMLinearKernel[FP8ScaledMMLinearLayerConfig, _FP8ParamsT], ABC):
    def apply_weights(
        self,
        layer: torch.nn.Module,
        x: torch.Tensor | QuantizedActivation,
        bias: torch.Tensor | None = None,
    ) -> torch.Tensor:
        w, w_s, x_s, x_s_ub = self._get_layer_params(layer)
​
        # 尝试将输入作为预量化激活拆包
        qa = as_quantized_activation(x, self.input_quant_key())
        if qa is not None:
            # 预量化路径:直接使用 data 和 scale,恢复原始 shape/dtype
            x_data, x_s = qa.data, qa.scale
            orig_shape, orig_dtype = qa.orig_shape, qa.orig_dtype
            assert x_data.dtype == fp8_dtype
        else:
            # 普通 tensor 路径:执行内部量化
            assert isinstance(x, torch.Tensor)
            x_data = x
            orig_shape, orig_dtype = x.shape, x.dtype
​
        x_2d = x_data.view(-1, x_data.shape[-1])
        output_shape = [*orig_shape[:-1], w.shape[1]]
        out_dtype = orig_dtype if self.config.out_dtype is None else self.config.out_dtype
​
        # 只有在没有预量化时才执行量化
        if qa is None:
            x_2d, x_s = self.quant_fp8(x_2d, x_s, x_s_ub)
​
        return self.apply_scaled_mm(
            A=x_2d, B=w, out_dtype=out_dtype, As=x_s, Bs=w_s, ...
        )

评论区精华

apply_mm 静态方法提议 设计

BadrBasowid 建议在 kernel 接口中添加 apply_mm 静态方法,专门处理 GEMM 而不涉及量化,以减少 apply_weights 中的分支。

结论:mgoin 认为分支逻辑必须保留在 kernel 内部,因为 kernel 知道自己的预处理(如 NVFP4 的 padding),提取成统一方法会丢失 kernel 特定信息。 · 已解决

as_quantized_activation API 设计 设计

HDCharles 质疑为什么需要 as_quantized_activation 而不是简单的 if is_quantized_activation(x) 判断。

结论:mgoin 认为是风格选择,as_quantized_activation 可以封装验证逻辑和拆包,隐藏细节。可后续修改。 · 已解决

padding/alignment 加入 QuantKey 设计

zyongye 提议将 padding、alignment 等布局要求编码进 QuantKey,这样 producer 可以输出符合 consumer 预期的格式,避免额外的 kernel launch。

结论:mgoin 认为是后续课题,且涉及融合 kernel 选型权衡(不同 fused kernel 可能支持不同 padding)。暂不实现。 · 待处理

zero point 支持 question

HDCharles 询问是否假设融合 kernel 不会涉及 zero point,以及 QuantizedActivation 是否支持。

结论:mgoin 确认目前 fused kernel 都不使用 zero point(仅 FP8、NVFP4),所以无需在 QuantKey 中表示 zero point。 · 已解决

风险与影响

新契约为已知 kernel 设计了默认返回 None,不影响原有行为。但存在以下风险:

1) 如果 kernel 声明了 input_quant_key 但 apply_weights 未调用 as_quantized_activation,输入会被当作普通张量处理导致静默错误。测试 test_supporting_backend_declares_consume_via_helper 通过字节码检查调用,但可能因装饰器或间接调用产生漏报。
2) 新接口增加了 kernel 实现者的认知负担,需要理解契约。
3) 未处理 layout/padding 编码,可能导致融合 producer 输出不符合 kernel 预期。

用户:无直接可见变化,但为后续手动融合优化建立了基础。系统:新增量化融合架构扩展点,线性 kernel 需声明 input_quant_key 并处理 QuantizedActivation。团队:需遵循此契约编写 fusion producer 代码;现有 kernel 若支持预量化需添加 input_quant_key 覆盖和 apply_weights 适配。

kernel 可能遗漏调用 as_quantized_activation 测试依赖字节码检测不稳健 layout/padding 未编码可能致错

关联 Issue

#43224 [RFC]: Porting compiler fusions to manual fusion

完整报告

参与讨论