执行摘要
- 一句话:INT8 MoE 权重预打包职责下沉到内核
- 推荐动作:值得精读,尤其适合关注 MoE 内核架构与量化权重处理链路的工程师。核心看点是 prepack 归属权的设计取舍:为什么从 backend 下沉到 kernel、如何通过
process_weights_after_loading 对齐既有 FusedMoEExperts API,以及后续新增 CPU 内核(如 s390x)时如何只改内核类即可。建议同时阅读 compressed_tensors_moe_w8a8_int8.py 与 online/int8.py 两处入口的调用顺序,理解内核创建与权重打包的先后依赖。
功能与动机
PR body 明确指出:目前 weight prepack 由 MoE Backend 拥有而非 MoE kernel 拥有,这在 CPU 上造成问题——CPU 只有一个 MoE backend 但有多个 kernel(AArch64、x86),选择正确的 packing 函数需要两层选择,且会随新 backend/kernel 的加入越来越复杂。与此同时,当前流程不符合 FusedMoEExperts 暴露的 process_weights_after_loading API,该 API 本就是为了抽象这一复杂度。本 PR 旨在让 MoE kernel 拥有 prepack,与 CompressedTensorsW4A4Nvfp4MoEMethod 等其他量化 MoE 路径保持一致,并声明 GPU backend(triton、humming)因不实现 prepack 方法而行为不变。
实现拆解
实现按以下步骤拆解:
-
删除 backend 级分发函数:在 vllm/model_executor/layers/fused_moe/experts/cpu_moe.py 中移除模块级函数 prepare_int8_moe_layer_for_cpu(原逻辑为按 current_platform.get_cpu_architecture() 分发到 ARM 的 cpu_prepack_moe_weight_int8(..., "neon") 或 x86 的 torch.ops._C.convert_weight_packed),从根上消除“backend 选函数、函数再选 ISA”的两层分发。
-
将打包逻辑内联进内核类:在 cpu_moe.py 中,x86 VNNI 路径的 CPUExpertsInt8.process_weights_after_loading 直接调用 torch.ops._C.convert_weight_packed,AArch64 路径的对应 process_weights_after_loading 直接调用 cpu_prepack_moe_weight_int8(..., "neon"),两个内核类各自拥有打包细节,后续新增 CPU 内核只需实现自己的 process_weights_after_loading 即可。
-
后端层瘦身:在 vllm/model_executor/layers/fused_moe/oracle/int8.py 的 convert_to_int8_moe_kernel_format 中删除 Int8MoeBackend.CPU 分支及其对 prepare_int8_moe_layer_for_cpu 的导入,改为 elif int8_backend not in (Int8MoeBackend.TRITON, Int8MoeBackend.CPU) 的白名单校验;TRITON 与 CPU 均由原来的“可能被改写”变为直接原样返回权重张量。
-
在量化入口补挂内核回调用:在 vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w8a8_int8.py 的 process_weights_after_loading 与 vllm/model_executor/layers/quantization/online/int8.py 的 _setup_kernel 中,于 make_int8_moe_kernel(...) 创建内核后追加 self.moe_kernel.fused_experts.process_weights_after_loading(layer),让内核内部的 experts 实现接管 prepack,同时保持 HUMMING 这类需要在创建内核前改写层属性的特殊路径。
-
测试与配置配套:本次未新增或修改任何测试文件,作者说明通过本地测试与 CI 验证;由于改动涉及 CPU MoE 主路径且无直接测试覆盖,属于需要依赖 CI 回归把关的配套缺口。
关键文件:
vllm/model_executor/layers/fused_moe/experts/cpu_moe.py(模块 CPU 内核;类别 source;类型 data-contract;符号 prepare_int8_moe_layer_for_cpu, CPUExpertsInt8, process_weights_after_loading): 核心变更文件:删除模块级 prepare_int8_moe_layer_for_cpu 分发函数,将 x86 VNNI 与 AArch64 SMMLA 打包逻辑分别内联进两个 experts 类的 process_weights_after_loading,确立“内核拥有 prepack”的新契约。
vllm/model_executor/layers/fused_moe/oracle/int8.py(模块 后端分发;类别 source;类型 data-contract;符号 convert_to_int8_moe_kernel_format): 后端路由层瘦身:convert_to_int8_moe_kernel_format 删除 CPU prepack 分支,TRITON 与 CPU 统一原样返回权重,后端不再承担 CPU 打包职责。
vllm/model_executor/layers/quantization/compressed_tensors/compressed_tensors_moe/compressed_tensors_moe_w8a8_int8.py(模块 压缩量化;类别 source;类型 data-contract;符号 process_weights_after_loading): CompressedTensors 的 INT8 MoE 量化入口:在创建 moe_kernel 后追加 fused_experts.process_weights_after_loading(layer) 调用,使内核接管 prepack。
vllm/model_executor/layers/quantization/online/int8.py(模块 在线量化;类别 source;类型 data-contract;符号 _setup_kernel): Online INT8 量化入口:_setup_kernel 同样在创建内核后追加 fused_experts.process_weights_after_loading(layer),与 CompressedTensors 路径保持一致。
关键符号:process_weights_after_loading, prepare_int8_moe_layer_for_cpu, convert_to_int8_moe_kernel_format, _setup_kernel
关键源码片段
vllm/model_executor/layers/fused_moe/experts/cpu_moe.py
核心变更文件:删除模块级 prepare_int8_moe_layer_for_cpu 分发函数,将 x86 VNNI 与 AArch64 SMMLA 打包逻辑分别内联进两个 experts 类的 process_weights_after_loading,确立“内核拥有 prepack”的新契约。
# cpu_moe.py:移除模块级 prepare_int8_moe_layer_for_cpu 后,
# 各 CPU experts 内核直接在自己的 process_weights_after_loading 中完成 ISA 专属打包。
# x86 VNNI 路径(CPUExpertsInt8)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
"""VNNI-prepack INT8 MoE weights for CPU kernel."""
from vllm.model_executor.utils import replace_parameter
# 之前经由 prepare_int8_moe_layer_for_cpu 按架构分发,现在直接调用 oneDNN 打包原语
w13 = torch.ops._C.convert_weight_packed(layer.w13_weight)
w2 = torch.ops._C.convert_weight_packed(layer.w2_weight)
replace_parameter(layer, "w13_weight", w13)
replace_parameter(layer, "w2_weight", w2)
# AArch64 / SMMLA 路径(同文件另一 experts 类)
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
from vllm.model_executor.utils import replace_parameter
# 直接用 neon 打包,不再由后端函数按 CPU 架构二次分发
w13 = cpu_prepack_moe_weight_int8(layer.w13_weight, "neon")
w2 = cpu_prepack_moe_weight_int8(layer.w2_weight, "neon")
replace_parameter(layer, "w13_weight", w13)
replace_parameter(layer, "w2_weight", w2)
vllm/model_executor/layers/fused_moe/oracle/int8.py
后端路由层瘦身:convert_to_int8_moe_kernel_format 删除 CPU prepack 分支,TRITON 与 CPU 统一原样返回权重,后端不再承担 CPU 打包职责。
# oracle/int8.py:backend 层不再负责 CPU 的权重打包,
# 仅保留 Humming 特殊转换与 backend 白名单校验。
def convert_to_int8_moe_kernel_format(
int8_backend: Int8MoeBackend,
w13: torch.Tensor,
w2: torch.Tensor,
layer: torch.nn.Module | None = None,
w13_scale: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Convert INT8 MoE weights to backend-specific kernel format."""
if int8_backend == Int8MoeBackend.HUMMING:
# Humming 读取 CT 格式的 w*_weight_scale;
# 在线 int8 的 per-channel 权重 scale 需要先转成 (E, N, 1) 布局再交给 loader
... # 原始循环与 convert_to_humming_moe_kernel_format 保持不变
return layer.w13_weight, layer.w2_weight
elif int8_backend not in (Int8MoeBackend.TRITON, Int8MoeBackend.CPU):
raise ValueError(f"Unsupported Int8 MoE backend: {int8_backend.value}")
# TRITON 与 CPU backend 原样返回权重:
# CPU 的 prepack 已由内核的 process_weights_after_loading 负责
return w13, w2
评论区精华
该 PR 几乎没有实质性技术讨论:作者在 issue 评论中提示 "@bigPYJ1151 , @mgoin - this should be a simple review",Claude bot 自动审核因 fork 来源被禁用,维护者 bigPYJ1151 直接 Approve,无任何 review comments。核心设计取舍(prepack 归属权从 backend 下沉到 kernel)已在 PR body 中说明清楚,未引发争议。
- 无实质性技术讨论,快速合入 (other): 无遗留技术疑虑,PR 按预期合并。
风险与影响
关联脉络
- PR #50219 [CPU][s390x] Optimize inference perf and add oneDNN INT8 GEMM for s390x: 同属 CPU INT8 MoE 内核路径演进;本 PR 将 prepack 下沉到内核正是为了让新增 CPU 内核(如 s390x)可以各自在 process_weights_after_loading 中负责打包,二者在 CPU MoE 内核矩阵方向上是连续的工作。
参与讨论