1
1
1
DCU技术达人
新人贴
编译成功,不代表海光 DCU 正在跑你的 kernel:一套 vLLM ROCm 增量编译验真流程
2026-09-20 17:01:58
51
我在海光 DCU 上修改 vLLM kernel 时,最浪费时间的一次并不是编译报错。 那次编译正常结束,服务能够启动,接口也能返回结果,性能测试甚至给出了一个看起来可以解释的变化。后来重新核对运行环境,我才发现一个更麻烦的问题:源码、构建产物和服务实际加载的文件并不是同一套状态。此前围绕性能做出的判断,只能全部作废。 这件事改变了我的调试顺序。现在每次改完 ROCm kernel,我不会立刻看吞吐,而是先回答一个更基础的问题: > 正在海光 DCU 上执行的,究竟是不是我刚刚修改、编译并准备测试的那份代码? 本文记录的是我在 gfx936 架构海光 DCU 上实际使用过的一套 vLLM 增量编译与运行时验真方法。重点不是介绍某个更快的算子,而是把从 `.cu` 源码到真实 kernel dispatch 之间容易藏住旧状态的环节逐一拆开。 ## 实验环境 本文涉及的实测环境如下。后文的时间和二进制信息只对这套环境负责,不外推为其他 DCU、DTK 或 vLLM 版本的固定结果。 | 项目 | 实测环境 | | -------------- | ---------------------------------------- | | 加速卡 | 海光 DCU,gfx936 架构,设备名显示为 `BW` | | DTK | 26.04 系列环境 | | Python | 3.10 | | PyTorch | 2.10.0 | | 构建工具 | CMake、Ninja、hipify、ccache 4.5.1 | | vLLM ROCm 扩展 | `vllm._rocm_C` | 在这份 vLLM 源码中,`_rocm_C` 由三个文件构成: ```text csrc/rocm/torch_bindings.cpp csrc/rocm/skinny_gemms.cu csrc/rocm/attention.cu ``` `skinny_gemms.cu` 和 `attention.cu` 会先经过 hipify,再由 DTK 的编译器面向 `gfx936` 生成目标代码,最后与算子注册代码一起链接为 `_rocm_C.abi3.so`。 ## 一个 kernel 在运行前至少有六种“身份” 只看源码目录,很容易误以为修改已经生效。实际链路要长得多: ```text skinny_gemms.cu | | hipify v skinny_gemms.hip | | clang++ --offload-arch=gfx936 v cmake-build-rocm/_rocm_C.abi3.so | | cmake --install / 手工安装 v Python 环境中的 vllm/_rocm_C.abi3.so | | 服务进程加载 v /proc//maps 中的已加载共享库 | | shape、dtype、layout 等运行时条件 v 本次请求实际选择的 kernel 分支 ``` ![1784272089839.png](/api-forum/file/RedirectImage?path=20260920/1789894792004_1784272089839.png '01_海光DCU_vLLM_kernel身份链路') 这里任何一层没有对齐,都会出现一种很危险的状态:程序能跑,但跑的不是你以为的实现。 因此,“编译成功”最多只能证明第三层生成了文件。它不能证明文件被安装到了当前 Python 环境,更不能证明已经启动的 vLLM 进程重新加载了它,最后也不能证明目标请求真的命中了新 dispatch。 ## 我实际跑通的增量编译 在已有构建目录的前提下,我修改 `csrc/rocm/skinny_gemms.cu` 后使用的是下面两条命令: ```bash cmake --build --preset rocm-release --target _rocm_C cmake --install cmake-build-rocm --component _rocm_C ``` 这套环境里有一个不太显眼的差异:仅放置 `CMakeUserPresets.json` 时,当前镜像的构建流程没有正确识别;换成实际使用的 `CMakePresets.json` 后才稳定工作。Preset 中至少要明确以下信息: ```text VLLM_TARGET_DEVICE=rocm PYTORCH_ROCM_ARCH=gfx936 ROCM_PATH=/opt/dtk CMAKE_HIP_COMPILER_LAUNCHER=ccache ``` 一次真实的增量构建日志可以压缩为下面几行: ```text [1/4] ... hipify.py ... skinny_gemms.cu attention.cu skinny_gemms.cu -> skinny_gemms.hip [ok] attention.cu -> attention.hip [skipped, already hipified] Successfully preprocessed all matching files. Total number of unsupported CUDA function calls: 0 Total number of replaced kernel launches: 19 [2/3] clang++ ... --offload-arch=gfx936 ... skinny_gemms.hip [3/3] ... -o _rocm_C.abi3.so ``` 这几行比最后一句 `build success` 更有用。它们同时证明了四件事: 1. 修改过的 `.cu` 被重新 hipify; 2. 未修改的 `attention.cu` 没有被无意义地重新转换; 3. 编译命令确实以 `gfx936` 为目标架构; 4. 最终发生了共享库重链接。 同一次本地实验还记录了生成扩展的状态: ```text size: 21018624 bytes sha256: 0e3280734c80bf7d33e101c04eecd9cdee0844feb3447b2141a3326ad6ef7549 torch: 2.10.0 LLMM1_registered: True ``` 这个哈希只用于标识当时那一份二进制,并不代表其他正确构建也应该得到相同哈希。 ### 共享目录会拖慢配置阶段 我还对比过同一套源码在两种位置执行 CMake configure 的时间: | 构建位置 | 实测 configure 时间 | | ---------------- | ------------------: | | 共享目录 | 约 111 秒 | | 容器本地 `/root` | 13.5 秒 | ![1784272089914.png](/api-forum/file/RedirectImage?path=20260920/1789894803748_1784272089914.png '02_CMake_configure时间对比') 两者相差约 8.2 倍。这个结果并不说明 `/root` 在任何机器上都会快 8.2 倍,它说明的是:当源码树、CMake 探测和大量小文件操作都落在高延迟共享存储上时,配置阶段本身就可能成为迭代瓶颈。 我的做法是把临时源码 overlay 和 `cmake-build-rocm` 放在容器本地盘,编译完成后只把需要保存的日志、源码补丁和最终 `.so` 复制回持久化目录。这样既保留证据,也避免每次 configure 都在共享目录上重复做元数据访问。 ## 四种“看起来成功”的失败状态 下面四种情况我都在实际调试中遇到过。它们之所以麻烦,是因为服务不一定报错。 ### 1. `import vllm` 成功,但 ROCm 扩展并不存在 一次重新安装 wheel 后,Python 包恢复了,`import vllm` 也正常,但默认安装中没有 `_rocm_C.abi3.so`。如果只用 `import vllm` 验证环境,就会把“Python 包存在”误当成“自定义 ROCm 扩展已安装”。 更可靠的检查是直接导入扩展并确认目标算子完成注册: ```bash python3 - <<'PY' import importlib.util import os import torch spec = importlib.util.find_spec("vllm._rocm_C") print("extension:", None if spec is None else spec.origin) if spec is None: raise SystemExit("vllm._rocm_C is missing") import vllm._rocm_C # noqa: F401 op_name = os.environ.get("DCU_VERIFY_OP", "LLMM1") print("registered:", op_name, hasattr(torch.ops._rocm_C, op_name)) PY ``` 这里的 `LLMM1` 是我当时验证的算子名。换成其他工程时,应将 `DCU_VERIFY_OP` 设为自己真正需要测试的注册名。 ### 2. wheel 重装后,只复制了当前修改的一个文件 另一轮实验中,重装 wheel 会把 `site-packages` 恢复为 wheel 默认状态。我当时只复制了眼前正在修改的 Python 文件,却漏掉了候选实现继承的其他已修改文件。服务仍能启动,benchmark 也能完成,但那不是完整候选,结果没有比较价值。 从那以后,我不再按“这次手改了几个文件”决定同步范围,而是按完整源码差异决定: ```bash git diff --name-only <基准版本>...HEAD ``` 所有会影响运行时的差异文件都要安装、计算哈希并进入备份与恢复清单。对于 Python 文件,还应在启动服务前执行 `python -m py_compile`,避免把传输损坏、编码异常或残缺文件留到几十分钟后的模型加载阶段才发现。 ### 3. 磁盘上的 `.so` 更新了,旧进程仍在使用原映射 覆盖 `_rocm_C.abi3.so` 不会让已经运行的 Python 进程自动卸载旧共享库。更换二进制后必须完整重启服务;如果使用了 CUDAGraph,还应重新完成图捕获,不能沿用旧进程中的图状态。 我遇到过一次更隐蔽的情况:前一轮服务退出后,一个 `VLLM::EngineCore` 进程仍然存活并占用显存。下一轮服务虽然启动了,但环境已经不再干净。 在启动新服务前,我现在至少做两项只读检查: ```bash ps -eo pid,ppid,cmd | grep -E 'vllm|VLLM::EngineCore' | grep -v grep ss -ltnp | grep ':8000 ' ``` 确认旧服务、EngineCore、benchmark driver 和监听端口都已清理,再通过正常的服务管理方式重新启动。启动后还可以查看目标进程的映射: ```bash PID=$(pgrep -n -f 'VLLM::EngineCore') grep '_rocm_C.abi3.so' "/proc/${PID}/maps" ``` 它至少能回答“当前进程从哪个路径加载了扩展”。如果路径不符合预期,后面的性能数据没有继续分析的必要。 ### 4. 文件名一样,二进制来源却不同 有一次清理现场时,我记录到运行环境中的 `_rocm_C` 哈希前缀是: ```text 7aee47576aa8... ``` 而准备作为对照的已知扩展哈希前缀是: ```text f2c5393af460... ``` 两个文件都叫 `_rocm_C.abi3.so`,目标算子也都能运行,但它们不是同一份二进制。没有哈希记录时,这种差异很容易被文件名掩盖。 我现在会同时记录构建目录和实际安装位置的哈希: ```bash BUILD_SO=cmake-build-rocm/_rocm_C.abi3.so INSTALLED_SO=$(python3 - <<'PY' import importlib.util spec = importlib.util.find_spec("vllm._rocm_C") if spec is None: raise SystemExit(1) print(spec.origin) PY ) sha256sum "$BUILD_SO" "$INSTALLED_SO" cmp -s "$BUILD_SO" "$INSTALLED_SO" \ && echo "binary match" \ || echo "binary mismatch" ``` ## 仅有哈希还不够:必须证明请求命中了新 dispatch 哈希一致只能证明服务加载了目标扩展,不能证明某个请求选择了新 kernel。vLLM 中的 dispatch 往往同时受 shape、dtype、layout、序列阶段和缓存配置影响。条件少命中一个,程序就可能安静地回退到原路径。 我的做法是在候选分支里保留一个受环境变量控制的诊断 marker,只在真正选择候选 kernel 的那一行打印: ```python if os.getenv("VLLM_DCU_KERNEL_DIAG") == "1": logger.warning( "DCU_KERNEL_DIAG selected=%s shape=%s dtype=%s", selected, tuple(x.shape), x.dtype, ) ``` marker 的关键不是“功能已启用”,而是 `selected=True` 必须来自实际 dispatch 结果。我的自动化 runner 会从服务日志提取 marker,并把“出现预期 shape、关键参数正确、selected=True”作为 benchmark 有效的前置条件。 这一点对海光 DCU 上的 ROCm kernel 尤其重要。很多实验的 fallback 仍然是可用且性能不错的 vendor 或 Triton 路径,因此“服务没有报错”完全不能证明新 kernel 被执行。 ## 我现在使用的七道验真门 下面这张表是我最终固定下来的检查顺序。前一项没有通过,就不进入下一项。 | 顺序 | 要证明什么 | 最低证据 | | ---: | ---------------- | --------------------------------------------------- | | 1 | 源码状态完整 | 完整 diff 清单,不只看当前脏文件 | | 2 | 编译目标正确 | 日志出现 `--offload-arch=gfx936` 和目标源文件重编译 | | 3 | 安装的是新二进制 | build `.so` 与 import 实际路径的 SHA256 一致 | | 4 | 算子注册成功 | 直接 import `vllm._rocm_C`,目标 op 为 `True` | | 5 | 进程状态干净 | 无残留 EngineCore、旧服务、旧 driver 和端口占用 | | 6 | 真实请求命中 | 服务日志出现带真实 shape 的 `selected=True` marker | | 7 | 结果可用于比较 | 输出、请求数和失败数满足预先约定,再讨论性能 | 这套流程看起来比“编译后直接跑 benchmark”多了几步,但多数检查只需要几秒。真正耗时的是跳过它们之后,花几个小时分析一组其实来自旧二进制、半套源码或 fallback 路径的数据。 ## 一段只读验真脚本 下面是我把常用检查压缩后的版本。它不修改文件,也不停止进程,适合在服务启动前后各执行一次。算子名可通过环境变量替换。 ```bash #!/usr/bin/env bash set -euo pipefail export DCU_VERIFY_OP="${DCU_VERIFY_OP:-LLMM1}" python3 - <<'PY' import importlib.util import os import torch import vllm print("vllm:", vllm.__file__) spec = importlib.util.find_spec("vllm._rocm_C") print("_rocm_C:", None if spec is None else spec.origin) if spec is None: raise SystemExit("missing vllm._rocm_C") import vllm._rocm_C # noqa: F401 name = os.environ["DCU_VERIFY_OP"] ok = hasattr(torch.ops._rocm_C, name) print("op:", name, "registered=", ok) if not ok: raise SystemExit("target op is not registered") PY SO=$(python3 - <<'PY' import importlib.util print(importlib.util.find_spec("vllm._rocm_C").origin) PY ) ls -l "$SO" sha256sum "$SO" echo "--- active vLLM processes ---" ps -eo pid,ppid,cmd | grep -E 'vllm|VLLM::EngineCore' | grep -v grep || true ``` 如果构建目录中的 `.so`、Python 实际 import 的 `.so`、进程映射中的 `.so` 和 dispatch marker 能形成一条闭合证据链,我才会把后续结果写进实验结论。 ## 这套方法的边界 本文有三点需要明确: 1. `111 秒 -> 13.5 秒` 是特定容器与共享存储条件下的观测,不是海光 DCU 的固定性能指标; 2. `21018624` 字节和文中的 SHA256 属于一次具体构建,只用于说明如何记录二进制身份; 3. 这套流程证明“测试对象正确”,并不自动证明 kernel 更快或数值正确,性能与正确性仍需单独验证。 不过,对我而言,这一步已经成为海光 DCU kernel 开发中最值得保留的工程习惯。HIPify、DTK 编译器、CMake 构建树、Python 安装目录、长期运行的 vLLM 进程和运行时 dispatch,每一层都可能保留旧状态。把它们逐层验明,远比盯着最后一行 `build success` 更可靠。 我现在改完 kernel 后,第一反应不再是看数字涨了多少,而是先确认:源码对不对,二进制对不对,进程对不对,这个请求到底有没有走到它。多花的只是几分钟,省下的往往是一整轮错误实验。
全部评论(0)
只看楼主
暂无数据
--
楼主
Level.4
0 帖子
楼主热帖
暂无数据