02 Docker 镜像选择与补丁镜像构建:纯 Python overlay,不重编一行 CUDA
系列:GLM-5.3-NVFP4 部署实录(8×A800 / sm80)
上一篇:GLM-5.3-NVFP4 部署实战系列一部署前的硬件分析:GLM-5.3-NVFP4 为什么需要 8×A800
- 镜像选型决定了后续所有工作的形态。本文记录我们如何选定
vllm/vllm-openai:v0.28.0作为底座、如何逐文件核对 0.26→0.28 的补丁吸收情况、以及为什么最终选择"纯 Python overlay"这种几乎零风险的镜像改造方式。
1. 选型的出发点
我们的约束很明确:
- 本机已有
vllm/vllm-openai:v0.28.0官方镜像,希望直接复用。 - GLM-5.2 的部署方案基于 vLLM v0.26.0 + 社区 PR #47629(TRITON_MLA_SPARSE 稀疏注意力后端),有 8 个左右的补丁文件。
- GLM-5.3-NVFP4 与 GLM-5.2 架构完全相同 (
GlmMoeDsaForCausalLM),所以问题的本质是:0.26.0 方案的补丁,在 0.28.0 里还差多少?
2. 关键功课:逐文件核对 0.28.0 吸收的补丁
- 拿到镜像后,我们对
vllm/vllm-openai:v0.28.0做了一次逐文件静态核对 (docker run --entrypoint bash进去 grep 即可),结论直接影响工作量估算。
✅ 已被上游吸收,可以扔掉的补丁
model_executor/models/deepseek_v2.py模型类 :0.28.0 已原生内置GlmMoeDsaForCausalLM,registry 也注册了该架构。0.26.0 方案里"为 sm80 加模型类"这个最大的补丁不再需要。
❌ 仍然缺失,必须补的
- sm80 稀疏注意力后端 :0.28.0 没有
TRITON_MLA_SPARSE。原生稀疏后端全是 SM90+/SM100+(flashmla_sparse/flashattn_mla_sparse/flashinfer_mla_sparse),sm80 上会直接抛No valid attention backend found。→ 需要 vendor 三个文件:triton_mla_sparse.py、triton_mla_sparse_kernel.py、mqa_logits_triton.py。 xpu_mla_sparse.py缺计数字段 :0.28.0 的mla_attention.py对 metadata 硬断言num_decodes/num_prefills/num_decode_tokens不为 None。→ 移植 0.26.0 的计数 graft。registry.py无枚举 、platforms/cuda.py候选表无该项 → 各加一行级别的修改。indexer.py仍用has_deep_gemm()(纯 import 探测) :sm80 上 vendor 的 deep_gemm 能 import 成功,探测放行后走 deep_gemm 内核必崩。→ 改成架构感知的is_deep_gemm_supported()(0.28.0 里该函数已存在,只是调用方没用)。
⚠️ 一个新的风险点(后来实机证实必崩)
- 0.28.0 把 DSA indexer 重写成了单个 C++ op (
torch.ops.vllm.sparse_attn_indexer)。静态阶段就能看出它内部走 deep_gemm 的fp8_fp4_mqa_logits,仅支持 SM90+------这个 open risk 被我们标记为"必须实机验证的第一件事",后来果然在profile_run阶段就崩了(attention.hpp:213 Unsupported architecture)。
3. 选纯 Python overlay的原因
- 最终的改造方式是:不 fork vLLM、不重编译任何 CUDA/C++ 代码,只用 9 个 Python 文件覆盖容器内的对应路径。
bash
Dockerfile.glm53-sm80:
FROM vllm/vllm-openai:v0.28.0
COPY _port/patches_glm53_v028/vllm/v1/attention/backends/mla/triton_mla_sparse.py \
/usr/local/lib/python3.12/dist-packages/vllm/v1/attention/backends/mla/triton_mla_sparse.py
COPY ... (共 9 个文件)
9 个补丁文件一览:
| # | overlay 文件 | 类型 | 作用 |
|---|---|---|---|
| 1 | v1/attention/backends/mla/triton_mla_sparse.py |
新增 | TRITON_MLA_SPARSE 后端(PR #47629) |
| 2 | v1/attention/ops/triton_mla_sparse_kernel.py |
新增 | 稀疏 MLA Triton 内核(_DIM_QK=576,512+64 rope) |
| 3 | v1/attention/ops/mqa_logits_triton.py |
新增 | FP8 MQA logits Triton 内核(uint8+LUT 解码,替代 sm80 没有的 fp8e4nv) |
| 4 | v1/attention/backends/mla/xpu_mla_sparse.py |
改 | 计数 graft(num_decodes 等 7 个字段) |
| 5 | v1/attention/backends/registry.py |
改 | +TRITON_MLA_SPARSE 枚举 |
| 6 | vllm/platforms/cuda.py |
改 | sm80 候选表追加 TRITON_MLA_SPARSE |
| 7 | v1/attention/backends/mla/indexer.py |
改 | has_deep_gemm() → is_deep_gemm_supported() |
| 8 | model_executor/models/deepseek_v2.py |
改 | fused indexer Q 内核加 sm89+ 门控(实机阶段新增) |
| 9 | model_executor/layers/sparse_attn_indexer.py |
改 | deep_gemm logits 调用点 sm80 回退 Triton(实机阶段新增) |
这个方式的好处:
- 底座镜像的二进制全部保持原样,CUDA kernel、依赖版本与官方镜像完全一致,排除了一整类"自编译引入的问题";
- 补丁边界清晰,每个文件在 Dockerfile 里一行 COPY,diff 一眼可审------比维护
.patchdiff 文件更适合这种场景:整文件覆盖在 vLLM 小版本升级时只需重新核对每个文件的最新版,不存在上下文行漂移导致 apply 失败的问题; - 构建只需 1--2 分钟(纯文件拷贝),调试补丁的迭代速度极快。
配套的工程细节:仓库里另维护一份 _port/ 原始移植文件作为补丁源,.dockerignore 排除模型权重和编译缓存等无关内容,保证 build context 干净、构建可重复。
4. 构建命令
bash
DOCKER_BUILDKIT=0 docker build -t vllm/vllm-openai:v0.28.0-glm53-sm80 \
-f Dockerfile.glm53-sm80 .
5. 无 GPU 的静态验证三板斧
补丁打完、镜像建好之后,在上 GPU 之前可以做三件快速验证,把低级错误全部拦在前面:
- 语法编译检查 :7 个补丁文件在 python3.12 下逐个
compile()(内存编译),全部通过; - 镜像内 grep 核对 :
docker run --entrypoint bash进去 grep 确认枚举、新模块、gating、候选表、计数字段全部就位; - import 冒烟 :在镜像内
import vllm.v1.attention.backends.mla.triton_mla_sparse,确认enum has TRITON_MLA_SPARSE: True、get_supported_head_sizes() == [576](576 = 512 kv_lora + 64 rope,与 GLM-5.3 的配置对上)。
这三步只能证明"代码能加载、注册关系正确",不能 证明内核在 sm80 上能跑(需要靠实机实现)。但正是这个清晰的边界,让我们实机阶段遇到崩溃时能立刻判断:问题出在运行时兼容性,而不是打补丁的手误。
具体的命令形态(可直接复用):
bash
# ① 语法编译检查:逐文件内存编译,不落盘
python3 - <<'EOF'
import pathlib
files = pathlib.Path("_port/patches_glm53_v028/vllm").rglob("*.py")
for f in files:
compile(f.read_text(), str(f), "exec")
print("OK", f)
EOF
# ② 镜像内 grep 核对:枚举 / 候选表 / 门控是否就位
docker run --rm --entrypoint bash vllm/vllm-openai:v0.28.0-glm53-sm80 -c \
'grep -n TRITON_MLA_SPARSE \
/usr/local/lib/python3.12/dist-packages/vllm/v1/attention/backends/registry.py \
/usr/local/lib/python3.12/dist-packages/vllm/platforms/cuda.py'
# ③ import 冒烟:后端类可加载、head size 与模型配置对上
docker run --rm --entrypoint python3 vllm/vllm-openai:v0.28.0-glm53-sm80 -c \
'from vllm.v1.attention.backends.mla import triton_mla_sparse as t; \
print(t.get_supported_head_sizes())' # 期望 [576]
到了实机阶段,仓库里还提供了一个镜像内自检脚本 scripts/sanity_check.py(需要 --gpus,因为它要读设备能力),输出六项检查,逐项对应补丁的关键点:
bash
[1] vllm version
[2] TRITON_MLA_SPARSE registered: True ← 枚举注册
[3] has_deep_gemm(): True / is_deep_gemm_supported(): False
← import 能过,但架构判断正确拒绝(坑 5 的门)
[4] triton_mla_sparse / mqa_logits_triton / triton_mla_sparse_kernel /
sparse_attn_indexer / deepseek_v2 全部 import OK;GlmMoeDsa 存在
[5] TritonMLASparseBackend.supports_compute_capability(sm80): True
FlashMLASparseBackend supports sm80: (不可用/报错) ← 对照组
[6] _get_backend_priorities(use_mla=True, sm80) 列表包含 TRITON_MLA_SPARSE
- 其中第 3、5、6 项特别值得说:
has_deep_gemm()为 True 而is_deep_gemm_supported()为 False,正是"import 探测不可信"这个结论的直接体现;第 5 项用原生后端做对照组(它在 sm80 上不支持),第 6 项直接调用 vLLM 的后端选择函数模拟运行时决策。 - 这三项加起来,把"后端为什么命中/不命中"变成了可以在启动前回答的问题。
小结
- 镜像选型 = 官方底座 + 纯 P y t h o n o v e r l a y ,零 C U D A 重编译 镜像选型 = 官方底座 + 纯 Python overlay,零 CUDA 重编译 镜像选型=官方底座+纯Pythonoverlay,零CUDA重编译;
- 版本迁移的功课做在前面,逐文件核对哪些补丁被 0.28.0 吸收,工作量立刻收敛;
- 静态验证三板斧把"手误类"问题全部拦截在 GPU 之前。
下一篇:03 首次部署踩坑总览------静态验证全过之后,实机仍然崩了 13 次。