Prhub

#44763 Add weights padding for fp8 per-block online quantization

原始 PR 作者 yma11 合并时间 2026-06-23 23:08 文件变更 1 提交数 1 评论 3 代码增减 +55 / -0

执行摘要

FP8 per-block 在线量化权重补齐

FP8 per-block 在线量化要求隐藏维度能被 block size(128)整除,但模型如 Qwen3-30B-A3B 在 TP=4 时 intermediate size 为 192,不满足整除条件,导致 silu_and_mul_per_block_quant 抛出 RuntimeError: hidden_size must be divisible by group_size。PR 描述中包含了完整的错误堆栈。

该 PR 值得合并,修复了明确的 bug。建议后续添加单元测试(如模拟不可整除的维度,验证 _zero_paddingmaybe_roundup_sizes 的正确性),以及对所有 online MoE 量化方法统一考虑维度对齐问题。

讨论亮点
  • 审核者 yewentao256 要求添加完整复现命令和错误输出,以及 lm_eval 指标以证明精度无损。提交者 yma11 按要求补充了复现命令和错误堆栈,并提供了 TP=2(无填充)和 TP=4(有填充)的 gsm8k 准确率对比图。
  • 提交者 yma11 在第一个评论中询问:“我注意到离线也不支持 block 量化的权重填充,所以想知道这种断裂是否是预期的——因为填充会带来计算/内存开销?或者我们应该支持这些情况但添加警告?” 此问题在后续讨论中未获直接回应,但最终方案选择了填充。

实现拆解

  1. 新增导入:在 vllm/model_executor/layers/quantization/online/fp8.py 中添加 from vllm.utils.math_utils import round_up

  2. 重写 maybe_roundup_sizes:在 Fp8PerBlockOnlineMoEMethod 中重写父类方法,先调用父类逻辑,再将 hidden_sizeintermediate_size_per_partition 分别向上取整到 weight_block_size[0](即 128),使得后续创建的张量尺寸满足 block 整除要求。

  3. 新增 _zero_padding:在 process_weights_after_loading 开始时调用。该方法将权重张量中超出原始 unpadded 尺寸的区域置零,确保新增的填充部分不会影响推理结果。具体处理 w13_weight(分为上下两半,分别对应 gate 和 up 投影)、w2_weight、以及可选的 w13_biasw2_bias

  4. 调用集成:在 process_weights_after_loading 中,加载权重后先执行 _zero_padding,再进行 FP8 量化等后续处理。

  5. 测试与验证:PR 提供了基于 Qwen3-30B-A3B 的手动测试命令和 gsm8k lm_eval 结果(TP=2 无填充 vs TP=4 有填充),两个配置下 accuracy 接近,表明填充不影响模型精度。

文件 模块 状态 重要度
vllm/model_executor/layers/quantization/online/fp8.py 量化层 modified 7.88

关键符号

maybe_roundup_sizes _zero_padding

关键源码片段

vllm/model_executor/layers/quantization/online/fp8.py data-contract

唯一修改的文件;在 Fp8PerBlockOnlineMoEMethod 中添加 maybe_roundup_sizes 和 _zero_padding 方法,并修改 process_weights_after_loading 调用链。

# vllm/model_executor/layers/quantization/online/fp8.py
# 新增导入 round_up
from vllm.utils.math_utils import round_up
​
​
class Fp8PerBlockOnlineMoEMethod(_Fp8OnlineMoEBase):
    # ... 省略构造函数 ...
​
    def maybe_roundup_sizes(
        self,
        hidden_size: int,
        intermediate_size_per_partition: int,
        act_dtype: torch.dtype,
        moe_parallel_config,
    ) -> tuple[int, int]:
        # 先调用父类逻辑
        hidden_size, intermediate_size_per_partition = super().maybe_roundup_sizes(
            hidden_size=hidden_size,
            intermediate_size_per_partition=intermediate_size_per_partition,
            act_dtype=act_dtype,
            moe_parallel_config=moe_parallel_config,
        )
        assert self.weight_block_size is not None
        block_size = self.weight_block_size[0] # 即 128
        # 将维度向上取整到 block_size 的整数倍
        return (
            round_up(hidden_size, block_size),
            round_up(intermediate_size_per_partition, block_size),
        )
​
    def _zero_padding(self, layer: Module) -> None:
        """将权重张量中填充区域(超出 unpadded 尺寸的部分)置零。"""
        hidden_size = layer.moe_config.hidden_dim_unpadded
        intermediate_size = layer.moe_config.intermediate_size_per_partition_unpadded
​
        w13_half_size = layer.w13_weight.shape[1] // 2
        # 处理 w13_weight:前一半和后一半分别清零填充部分
        if w13_half_size > intermediate_size:
            layer.w13_weight[:, intermediate_size:w13_half_size, :] = 0
            layer.w13_weight[
                :, w13_half_size + intermediate_size : 2 * w13_half_size, :
            ] = 0
        if layer.w13_weight.shape[2] > hidden_size:
            layer.w13_weight[:, :, hidden_size:] = 0
​
        if layer.w2_weight.shape[1] > hidden_size:
            layer.w2_weight[:, hidden_size:, :] = 0
        if layer.w2_weight.shape[2] > intermediate_size:
            layer.w2_weight[:, :, intermediate_size:] = 0
​
        # 处理可选的 bias
        if getattr(layer, "w13_bias", None) is not None:
            w13_bias_half_size = layer.w13_bias.shape[1] // 2
            if w13_bias_half_size > intermediate_size:
                layer.w13_bias[:, intermediate_size:w13_bias_half_size] = 0
                layer.w13_bias[
                    :, w13_bias_half_size + intermediate_size : 2 * w13_bias_half_size
                ] = 0
        if (
            getattr(layer, "w2_bias", None) is not None
            and layer.w2_bias.shape[1] > hidden_size
        ):
            layer.w2_bias[:, hidden_size:] = 0
​
    def process_weights_after_loading(self, layer: Module) -> None:
        if getattr(layer, "_already_called_process_weights_after_loading", False):
            return
        # 先清零填充区域,再进行后续量化
        self._zero_padding(layer)
        # 后续 FP8 量化逻辑保持不变 ...

评论区精华

复现命令与错误输出 正确性

审核者 yewentao256 要求添加完整复现命令和错误输出。

结论:提交者 yma11 在 PR 描述中补充了完整命令和错误堆栈。 · 已解决

精度验证 测试

审核者 yewentao256 要求添加 lm_eval 指标证明精度无损。

结论:提交者提供了 TP=2(无填充)和 TP=4(有填充)的 gsm8k 准确率对比图,精度接近。 · 已解决

填充开销与设计取舍 设计

提交者 yma11 询问是否应该支持填充,或仅添加警告。

结论:最终实现了填充方案,但讨论中未深入探讨性能权衡。 · 已解决

风险与影响

  • 回归风险:较小的风险。仅修改了 Fp8PerBlockOnlineMoEMethod 类,该路径仅在启用 --quantization=fp8_per_block 且 MoE 中间维度不能被 128 整除时才激活。对于原本就能整除的模型(如多数标准配置),行为与之前完全一致。
  • 精度影响_zero_padding 将填充区域置零,理论上不会影响原本有效区域的数学计算。提交者提供的 lm_eval 结果也证实精度无损。
  • 性能风险:填充会增加张量尺寸,导致额外的计算和显存占用。但对于当前场景,填充量通常很小(例如从 192 补齐到 256),影响可接受。
  • 无测试配套:PR 未添加单元测试或集成测试,仅靠手动验证。建议后续补充测试用例,覆盖不可整除的常见场景。
  • 用户影响:修复了 Qwen3-30B-A3B 等模型在 TP=4 且使用 fp8_per_block 量化时的崩溃问题。用户现在可以正常使用这些配置。
  • 系统影响:仅影响在线 FP8 per-block 量化路径,其他量化方法和非 MoE 层不受影响。
  • 团队影响:低。改动集中在单一文件,逻辑清晰,容易维护。
核心路径变更 缺少测试覆盖

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论