Prhub

#41516 [Build] Build bundled DeepGEMM `_C` per-Python so the wheel imports on every CPython

原始 PR 作者 mgoin 合并时间 2026-05-12 22:27 文件变更 6 提交数 8 评论 20 代码增减 +254 / -42

执行摘要

按 Python 版本构建 DeepGEMM _C 扩展并打包进 wheel,支持多 Python 导入

修复 issue #41476 和 #41512 描述的回归:DeepGEMM 的 _C 是 pybind11 模块,编译产生的 .so 文件只适用于构建时的 Python 版本,而之前 wheel 只包含一个 .so 文件,导致用户在非构建 Python 版本上安装 vLLM 时无法导入 DeepGEMM。本 PR 采用按 Python 版本分别编译并打包的方式规避了这一限制。

该 PR 解决了关键的跨 Python 兼容性问题,并建立了可维护的构建体系。值得合入 v0.21 版本。建议后续关注 CMake 头文件依赖扫描的改进,并考虑推动上游或使用 nanobind 简化构建。

讨论亮点

核心讨论包括:

  • wheel 大小:ZJY0516 询问 wheel 膨胀程度,mgoin 回应每个 .so 约 1.4 MB,总计约 6 MB,认为可接受。
  • nanobind 替代:Harry-Chen 建议迁移到 nanobind(支持 Python 稳定 API)以避免版本相关问题,并创建上游 issue。mgoin 表示上游不接受 PR,且需要维护 fork,当前方案足够。
  • 编译时间:Harry-Chen 担心多次编译的时间开销,mgoin 解释每次编译仅需一分钟,因为核心 kernel 是 JIT 的。
  • 空环境变量处理:claude[bot] 指出 CMake 的 if(DEFINED ENV{...}) 对空字符串为真,可能导致跳过构建。后续提交修复为 if(NOT "$ENV{...}" STREQUAL "")
  • python vs python3:claude[bot] 发现 CI 镜像中只有 python3,后修复为 python3
  • 头文件依赖跟踪:claude[bot] 指出 add_custom_command 不隐式扫描头文件,头文件变更不会触发重编。作者认为 CI 通常洁净构建,影响有限。

实现拆解

  1. 新增 tools/build_deepgemm_C.py:针对一个目标 Python 版本编译 _C.so。利用构建环境的 torch,目标 Python 只需提供头文件和 SOABI,无需安装 torch。
  2. 新增 tools/setup_deepgemm_pythons.sh:基于 pyproject.toml 中的 requires-python 字段自动推导需要支持的 Python 版本列表,利用 uv 创建裸的虚拟环境(仅包含 Python 解释器,不安装 torch)。
  3. 修改 cmake/external_projects/deepgemm.cmake:将原先使用 Python_add_library 的单一构建方式改为基于 DEEPGEMM_PYTHON_INTERPRETERS 循环的 add_custom_command,对每个 Python 调用 build_deepgemm_C.py 生成对应的 .so,并通过 cmake --install 打包到 vllm/third_party/deep_gemm/
  4. 修改 docker/Dockerfile:在 wheel 构建阶段调用 setup_deepgemm_pythons.sh 生成解释器列表,并通过环境变量传入 CMake。
  5. 新增 tools/check_wheel_deepgemm.py:在 CI 的 H100 DeepGEMM 测试步骤中验证 wheel 是否包含所有必需 Python 版本的 .so,若缺失则报错退出。
  6. 修改 .buildkite/test_areas/kernels.yaml:在 H100 DeepGEMM 测试步骤中添加 wheel 验证命令,并更新源码依赖列表以包含新脚本。
文件 模块 状态 重要度
tools/build_deepgemm_C.py 编译脚本 added 7.62
tools/check_wheel_deepgemm.py 验证脚本 added 7.01
cmake/external_projects/deepgemm.cmake CMake 配置 modified 5.31
tools/setup_deepgemm_pythons.sh 环境准备 added 4.9
docker/Dockerfile Docker 配置 modified 3.26
.buildkite/test_areas/kernels.yaml CI 配置 modified 3.14

关键符号

required_pythons

关键源码片段

tools/build_deepgemm_C.py core-logic

新增的编译脚本,负责针对单个目标 Python 版本编译 _C.so,是整个多 Python 构建的核心。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Build DeepGEMM's `_C` pybind11 extension for a target Python.Driven from `cmake/external_projects/deepgemm.cmake`. The driver is the
build interpreter (which has torch); the *target* Python is only used for
its header path and SOABI. This avoids needing torch installed in N venvs
to produce N matching `.so` files.Usage: python build_deepgemm_C.py <DEEPGEMM_SRC_DIR> <OUTPUT_DIR> <TARGET_PY>
"""import json
import os
import subprocess
import sys
from pathlib import Pathimport torch
from torch.utils import cpp_extension# 校验参数数量
if len(sys.argv) != 4:
    sys.exit(f"usage: {sys.argv[0]} <SRC> <OUT> <TARGET_PY>")src = Path(sys.argv[1]).resolve()
out = Path(sys.argv[2]).resolve()
target_py = sys.argv[3]
out.mkdir(parents=True, exist_ok=True)# 查询目标 Python 的 EXT_SUFFIX 和 INCLUDEPY,仅需解释器,无需 torch
info = json.loads(
    subprocess.check_output(
        [
            target_py,
            "-c",
            "import sysconfig, json; "
            "print(json.dumps({k: sysconfig.get_config_var(k) "
            "for k in ('EXT_SUFFIX', 'INCLUDEPY')}))",
        ]
    ).decode()
)cuda_home = cpp_extension.CUDA_HOME
if cuda_home is None:
    sys.exit("CUDA_HOME not found; cannot build DeepGEMM _C")# 构造头文件包含路径,包含 CCCL 等 DeepGEMM 依赖
includes = [
    info["INCLUDEPY"],
    f"{cuda_home}/include",
    f"{cuda_home}/include/cccl",
    str(src / "csrc"),
    str(src / "deep_gemm/include"),
    str(src / "third-party/cutlass/include"),
    str(src / "third-party/cutlass/tools/util/include"),
    str(src / "third-party/fmt/include"),
    *cpp_extension.include_paths(device_type="cuda"),
]# 使用 $CXX 或默认 g++ 编译,生成带目标 Python SOABI 的 .so 文件
cmd = [
    os.environ.get("CXX", "g++"),
    "-shared",
    "-fPIC",
    "-std=c++20",
    "-O3",
    "-g0",
    "-Wno-psabi",
    "-Wno-deprecated-declarations",
    "-DTORCH_API_INCLUDE_EXTENSION_H",
    "-DTORCH_EXTENSION_NAME=_C",
    f"-D_GLIBCXX_USE_CXX11_ABI={int(torch.compiled_with_cxx11_abi())}",
    *(f"-I{p}" for p in includes),
    str(src / "csrc/python_api.cpp"),
    *(f"-L{p}" for p in cpp_extension.library_paths(device_type="cuda")),
    f"-L{cuda_home}/lib64",
    "-ltorch",
    "-ltorch_python",
    "-ltorch_cpu",
    "-ltorch_cuda",
    "-lc10",
    "-lc10_cuda",
    "-lcudart",
    "-lnvrtc",
    "-o",
    str(out / f"_C{info['EXT_SUFFIX']}"),
]
print("[build_deepgemm_C] " + " ".join(cmd), flush=True)
subprocess.check_call(cmd)
tools/check_wheel_deepgemm.py core-logic

新增的 wheel 验证脚本,在 CI 中确保 wheel 包含所有必需 Python 版本的 .so,避免回归。

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project"""Assert the installed vLLM has a `_C.cpython-X.Y-*.so` for every CPython
covered by `requires-python`. Fails closed if a Python's `.so` is missing
from the wheel — i.e. the regression that surfaced in #41476/#41512.Run from a CI test job after vLLM is installed, e.g. the H100 deepgemm
kernel tests in .buildkite/test_areas/kernels.yaml.
"""import importlib.util
import os
import sys
from pathlib import Pathimport regex as re
import tomllibSO_RE = re.compile(r"^_C\.cpython-(\d)(\d+)-")# 从 pyproject.toml 中读取 requires-python 字段,动态推导需要支持的 Python 版本列表
def required_pythons() -> list[str]:
    pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
    spec = tomllib.loads(pyproject.read_text())["project"]["requires-python"]
    m = re.match(r">=3\.(\d+),<3\.(\d+)", spec)
    if not m:
        sys.exit(f"unexpected requires-python format: {spec!r}")
    return [f"3.{v}" for v in range(int(m[1]), int(m[2]))]# 定位 vllm.third_party.deep_gemm 包目录,通过 importlib 避免触发 deep_gemm 的 import
spec = importlib.util.find_spec("vllm.third_party.deep_gemm")
if spec is None or spec.origin is None:
    sys.exit("vllm.third_party.deep_gemm not importable; is vllm installed?")
pkg_dir = Path(spec.origin).parent# 找到目录下所有 _C.cpython-X.Y-*.so 文件,提取 Python 版本号
found = {f"{m[1]}.{m[2]}" for f in os.listdir(pkg_dir) if (m := SO_RE.match(f))}
required = required_pythons()
missing = [v for v in required if v not in found]
print(f"deepgemm _C: found {sorted(found)}, required {required}, missing {missing}")
sys.exit(1 if missing else 0)

评论区精华

wheel 尺寸影响 性能

ZJY0516 询问 'How much will this blow up the vLLM wheel size?',mgoin 回应 'each .so is 1.4 MB so roughly 6 MB, i think this is acceptable for now'

结论:接受约 6 MB 的尺寸增加,认为可接受。 · 已解决

使用 nanobind 替代 pybind11 设计

Harry-Chen 建议移植到 nanobind(支持 Python 稳定 API),并创建上游 issue #333。mgoin 回应上游不接受 PR,需要维护 fork,当前方案足够。

结论:暂不切换,维持 pybind11 的 per-Python 构建方案。 · 已解决

编译时间顾虑 性能

Harry-Chen 担心 'since we now need to compile deep_geem for about 7 times',mgoin 回应 'I think this only needs to compile 4 times, and the compilation just takes a minute since all the kernels are jitted.'

结论:编译时间可接受(4 次,各约 1 分钟)。 · 已解决

CMake 空环境变量处理 正确性

claude[bot] 指出 `if(DEFINED ENV{DEEPGEMM_PYTHON_INTERPRETERS})` 对空字符串为真,可能导致跳过 per-Python 构建。

结论:修复为 `if(NOT "$ENV{DEEPGEMM_PYTHON_INTERPRETERS}" STREQUAL "")`,确保空字符串时回退到构建解释器。 · 已解决

CI 中 python vs python3 测试

claude[bot] 发现 check_wheel_deepgemm.py 命令使用 bare `python`,但 CI 镜像只有 `python3`。

结论:将命令改为 `python3 ../tools/check_wheel_deepgemm.py`。 · 已解决

风险与影响

  1. wheel 大小增加约 6 MB,但可接受。
  2. 编译时间增加约 4-5 分钟,CI 中可接受,但可能影响本地构建测试。
  3. CMake 增量构建时,头文件变更不触发 _C.so 重编add_custom_command 不自动扫描头文件)。CI 通常洁净构建,但本地开发需注意。
  4. 版本矩阵同步pyproject.toml 中的 requires-python 变更需与脚本同步,但已通过自动推导缓解。
  5. 新增 CI 验证步骤,若验证失败会阻止发布,保障质量。

用户:vLLM wheel 现可在多个 Python 版本上正确导入 DeepGEMM,之前因 .so 缺失导致的导入错误已修复。
构建系统:构建过程更复杂,需额外依赖 uv 用于创建 venv,但运行时无额外依赖。需要维护额外的构建脚本和 CI 步骤。
团队:需要理解并维护多 Python 构建逻辑,但已有自动推导减少手动维护。与上游 DeepGEMM 的集成方式仍存在改进空间(如迁移到 nanobind)。

wheel 尺寸增加 6 MB 增量编译头文件变更不触发生成 新增约 4 次编译 自动推导版本矩阵

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论