Prhub

#53536 [XPU] Ensure unquantized linear weight is N-contiguous

原始 PR 作者 zufangzhu 合并时间 2026-08-31 16:01 文件变更 2 提交数 7 评论 4 代码增减 +16 / -0

执行摘要

XPU 新增可选 N-contiguous 权重重排开关

XPU 上的未量化 GEMM 走 torch.nn.functional.linear,权重为 N-contiguous 的 (N, K) 布局时性能更优。但 PR body 明确说明默认不能直接改布局:oneDNN matmul 包含 "non-deterministic constructs such as atomic operations",会破坏 run-to-run 可复现性,因此改为新增 VLLM_XPU_FORCE_N_CONTIG_WEIGHT 环境变量由用户按部署场景自行权衡。PR body 的性能数据表明只有 K > N(如 down_proj (12288, 4096))在 B60/B70 两个 SKU 上均稳定获胜,且收益与 M 无关——这很关键,因为权重布局在加载时只固定一次,而 M 是动态的,所以不能按 forward 临时选择布局,只能靠加载期开关一次决定。

值得精读的是 PR body 的性能表格与权衡论证——「M 动态变化、权重布局只能在加载期固定一次」这一约束推导出「只能按形状而非按 batch 选择布局」的结论,分析扎实,对类似加载期优化有通用参考价值。代码本身很小,但「何时应 opt-in 而非自动判定」的决策过程值得学习。建议跟踪两个未决点:按 K > N 自动判定的可能性,以及 tensorizer 加载路径的布局不一致问题。

讨论亮点

核心讨论只有一个线程,发生在 vllm/model_executor/layers/linear.py 第 222 行新增的条件表达式上:

jikunshang:if (envs.VLLM_XPU_FORCE_N_CONTIG_WEIGHT or (conditions)): xxx this makes more sense? —— 即建议除环境变量外,对满足形态条件的层(如 K > N)自动启用 N-contiguous 重排。

zufangzhu(回复):"I modified the default behavior to preserve the original layout because oneDNN operators produce some non-deterministic results. Users can manually optimize performance via environment variables." —— 即因为 oneDNN 算子输出非确定性,默认保持原布局,把优化决策交给用户通过环境变量手动开启。

最终方案保持 opt-in,评审人随后 APPROVED。有价值的信息是:PR body 强调 "K > N guard" 是安全前提,但代码实际没有按 K > N 自动判定,两处说法的不一致正是这个 review 线程的焦点。

实现拆解

实现按以下 3 步拆解:

  1. 注册环境变量(vllm/envs.py):在环境变量类属性区加入 VLLM_XPU_FORCE_N_CONTIG_WEIGHT: bool = False(默认关闭,保持既有布局),并在 _resolve_... 解析字典中加入对应 lambda:bool(int(os.getenv("VLLM_XPU_FORCE_N_CONTIG_WEIGHT", "0")))。解析方式与 VLLM_XPU_ENABLE_XPU_GRAPH、VLLM_XPU_USE_SAMPLER_KERNEL 等既有 XPU 开关保持一致,方便用户按同套命名习惯发现和设置。

  2. 权重重排逻辑(vllm/model_executor/layers/linear.py):在 create_weights 的配套后置钩子 process_weights_after_loading 中,CPU 分支之后新增 elif current_platform.is_xpu() 分支;当开关开启、weight.ndim == 2 且 weight.stride(0) != 1(即不是 N-contiguous)时,执行 layer.weight.data = weight.t().contiguous().t(),一次性把 (N, K) 权重改为 N 维连续。该操作只在权重加载后执行一次,forward 路径零额外开销;stride(0) != 1 的检查保证已满足条件的层不被重复拷贝。

  3. 设计权衡与演进:实现刻意不按层形状自动判定(如检测到 K > N 自动启用),而是采用「全局开关 + 默认关闭」:review 中 jikunshang 建议改为 if (envs.VLLM_XPU_FORCE_N_CONTIG_WEIGHT or (conditions)) 自动启用,作者以 oneDNN 非确定性为由未采纳。注意 PR body 声称 "The K > N guard is what makes this safe",但最终代码并没有 K > N 判断,安全性实际由「默认关闭 + 用户自行评估」兜底,二者存在张力。

  4. 测试与配套:本 PR 没有任何测试文件变更;第 4 个 commit 曾为 TensorizerLoader 增加布局恢复逻辑(该 loader 会跳过 process_weights_after_loading,导致 tensorizer 加载的权重丢失 N-contiguous 布局),后续被 revert,说明该兼容问题被主动排除在本 PR 范围之外。其余 commit 为格式修复和 main 分支合并。

文件 模块 状态 重要度
vllm/model_executor/layers/linear.py 线性层 modified 6.15
vllm/envs.py 环境变量 modified 4.73

关键符号

process_weights_after_loading

关键源码片段

vllm/model_executor/layers/linear.py core-logic

核心行为变更:在 process_weights_after_loading 中新增 XPU 分支,按环境变量 opt-in 将未量化权重重排为 N-contiguous;(N, K) 权重 stride 属于数据契约,所有模型共享该路径。

def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
    if current_platform.is_cpu():
        # MLA 的 kv_b_proj(见 _cpu_skip_gemm_dispatch):非性能关键路径,
        # 跳过打包,直接使用普通 fallback。
        if getattr(layer, "_cpu_skip_gemm_dispatch", False):
            layer.cpu_linear = torch.nn.functional.linear
            return
​
        from vllm.model_executor.layers.utils import dispatch_cpu_unquantized_gemm
​
        dispatch_cpu_unquantized_gemm(layer, remove_weight=True)
    elif current_platform.is_xpu():
        # 可选优化:XPU 上 F.linear 在权重为 N-contiguous 的 (N, K) 布局时更快,
        # 但 oneDNN 的 ab-weights matmul 不是逐次 bitwise 可复现的,因此默认关闭。
        # 用户设置 VLLM_XPU_FORCE_N_CONTIG_WEIGHT=1 后,这里会对所有未量化
        # 2D 权重做一次转置 - 拷贝 - 转置,把 N 维变为连续;仅在权重重载时执行一次。
        weight = layer.weight.data
        if (
            envs.VLLM_XPU_FORCE_N_CONTIG_WEIGHT
            and weight.ndim == 2
            and weight.stride(0) != 1 # 已经是 N-contiguous 则跳过,避免重复拷贝
        ):
            layer.weight.data = weight.t().contiguous().t()
vllm/envs.py configuration

新增 VLLM_XPU_FORCE_N_CONTIG_WEIGHT 环境变量的类型注解默认值与运行时解析逻辑,是开关生效的入口;默认 False 保证零行为变化。

# 类属性区:声明默认值,默认关闭以保持既有布局行为
VLLM_XPU_FORCE_N_CONTIG_WEIGHT: bool = False
​
# 解析字典区:与其它 XPU 开关保持一致的 int 解析方式
# Force N-contiguous weight layout for all XPU unquantized linears.
"VLLM_XPU_FORCE_N_CONTIG_WEIGHT": lambda: bool(
    int(os.getenv("VLLM_XPU_FORCE_N_CONTIG_WEIGHT", "0"))
),

评论区精华

VLLM_XPU_FORCE_N_CONTIG_WEIGHT 是否应叠加自动条件判定 设计

jikunshang 在 linear.py 新增条件处评论:`if (envs.VLLM_XPU_FORCE_N_CONTIG_WEIGHT or (conditions)): xxx this makes more sense?`,即建议除环境变量外,对满足形态条件的层(如 K > N)自动启用 N-contiguous 重排。

结论:zufangzhu 回复:已将默认行为改为保持原布局,因为 oneDNN 算子产生非确定性输出;用户可自行通过环境变量手动开启性能优化。即放弃自动判定,采用全局 opt-in 开关,评审人随后 APPROVED。 · 已解决

风险与影响

具体风险如下:

  1. oneDNN 输出非确定性(启用后):重排后走 oneDNN matmul,输出不再是逐次 bitwise 可复现,对依赖确定结果的评测、回归比对和调试有直接影响;代码注释和 PR body 均已明确提示这是默认关闭的原因。

  2. 全局开关不分层形状的性能反向风险:VLLM_XPU_FORCE_N_CONTIG_WEIGHT 是全局生效的,一旦打开,所有未量化 linear 层都会被重排,包括 K < N 的 qkv_proj、gate_up_proj——实测在 B60 decode 下这些层会慢到约 0.46x/0.54x(近 2 倍降速)。用户若不了解层形状分布,可能整体变慢。

  3. tensorizer 加载路径不生效:TensorizerLoader 会跳过 process_weights_after_loading,因此同一开关在普通加载与 tensorizer 加载下行为不一致;相关修复 commit 已被 revert,这是遗留问题。

  4. torch.compile stride 特化风险:第 4 个 commit 信息提到 torch.compile 会按参数 stride 特化,若编译时与运行时权重布局不一致可能导致断言失败;该问题在演进中被识别但最终未在本次处理。

  5. 缺少测试覆盖:PR 无任何测试变更,开关与重排逻辑均无回归保护,后续重构 process_weights_after_loading 时容易被破坏。

影响范围:仅限 XPU(Intel GPU)平台的未量化 linear 层;默认关闭,所有既有部署零行为变化。开启后,K > N 的层(如 down_proj)在 prefill 场景最多约 2 倍加速(B60 2.00x、B70 1.68x),代价是全局非确定性和 K < N 层的潜在降速;开销方面,权重加载期多一次一次性转置拷贝,forward 无额外成本。对 CUDA / CPU 平台完全无影响。对团队而言,该 PR 确立了 XPU 线性层性能调优的 opt-in 开关模式,与 VLLM_XPU_INC_WNA16_BACKEND 等既有开关风格一致,后续可基于此演进为自动形状判定或按层粒度开关。

默认关闭的性能开关 oneDNN 输出非确定性 缺少测试覆盖 tensorizer 加载路径不生效 全局开关不分层形状

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论