【昇腾】在华为昇腾 910B4 上使用 vllm-ascend 跑通 Gemma 4 31B 全记录

📅 2026/7/7 7:53:17
【昇腾】在华为昇腾 910B4 上使用 vllm-ascend 跑通 Gemma 4 31B 全记录
前提本文写在 vllm-ascend:nightly-releases-v0.22.1rc-openeuler 发布的时候这个时候 vllm-ascend 官方还没有支持 gemma 4但官方 github 中已经有关于 gemma 4 的 PRhttps://github.com/vllm-project/vllm-ascend/pull/9222只是存在合并冲突的状态但自己部署使用是没有问题的。目标910B4 × 2 上把 Gemma 4 31B 跑起来多模态可用。所有代码、镜像、补丁都已开源跟着下面的步骤走就能复现。涉及的几个外部资源先列一下:基线镜像:quay.io/ascend/vllm-ascend:v0.20.2rc1-openeulerPR 分支:https://github.com/zhangshuai404/vllm-ascend/tree/fix-gemma4-ascend-inference-clean模型:google/gemma-4-31B(约 59GB)步骤 1: 拉取基线镜像1.1 目标获取一个已经内置 CANN 9.0.0、torch、vllm 0.20.2 的 openEuler 基础镜像。这里选 vllm 0.20.2 的原因是 PR 是基于这个开发不过我估计更新版本的也能用。vllm-ascend 的 C 扩展重编一次大约 7 分钟基线里的 vllm vllm-ascend 编译产物能直接复用。1.2 操作dockerpull quay.io/ascend/vllm-ascend:v0.20.2rc1-openeulerdockerimages|grepvllm-ascend1.3 预期结果quay.io/ascend/vllm-ascend v0.20.2rc1-openeuler IMAGE_ID 25.4GB步骤 2: 编译 PR #9222 源码并生成 Layer 1 镜像2.1 目标官方 vllm-ascend 还没支持 Gemma 4需要把 PR #9222 的源码覆盖到容器里并重新编译 C 扩展。编译产物提交为新镜像vllm-ascend-gemma4:full即 Layer 1。2.2 操作# 1) 克隆 PR 分支host 上执行目录按你的环境调整cd/path/to/dirgitclone--depth1-bfix-gemma4-ascend-inference-clean\https://github.com/zhangshuai404/vllm-ascend.git# 2) 启动一个 sleep 容器作为编译环境dockerrun-d--namebuild\--networkhost\quay.io/ascend/vllm-ascend:v0.20.2rc1-openeuler\sleep3600# 3) 把 PR 源码拷进容器注意一定要用 docker cp不能用 volume 挂载# 因为后面 docker commit 不保存 volume 里的内容会导致修复丢失dockercp/path/to/dir/vllm-ascend build:/src# 4) 容器内覆盖官方 vllm-ascend编译 PR #9222 版本# 注意host 上 git clone 的目录 owner 与容器内用户不同必须加 safe.directory# 否则 setuptools_scm 和子模块拉取都会报 detected dubious ownership 错误。dockerexecbuildbash-c source /usr/local/Ascend/ascend-toolkit/set_env.sh export SOC_VERSIONascend910b4 export CMAKE_PREFIX_PATH/usr/local/python3.11.15/lib/python3.11/site-packages/torch/share/cmake git config --global --add safe.directory * cd /src pip install --no-build-isolation -e . # 5) 提交为 Layer 1dockercommit build vllm-ascend-gemma4:full2.3 预期结果dockerimages|grepgemma4vllm-ascend-gemma4 full sha256:d22f13c96ba8 27.0GB编译日志最后会看到Successfully installed vllm-ascend-0.1.dev1...和Finished processing dependencies耗时 6-8 分钟。步骤 3: 启动服务容器并完成首次运行3.1 目标用 Layer 1 镜像启动长期运行容器拉起 vllm serve确认模型能加载、API 能响应。3.2 操作# 1) 启动容器挂 NPU 设备、模型目录、driver 文件、补丁目录# --privileged -e ASCEND_RT_VISIBLE_DEVICES2,3 是实测可指定具体卡的方案# 容器内 NPU 会被重编号为 0,1其它 4 张卡被屏蔽但 devfs 层仍可见。# 其它挂载dcmi/npu-smi/driver/version.info和 --group-add 必须保留。dockerrun-d--namegemma4-pr9222-test\--networkhost\--privileged\--device/dev/davinci_manager\-eASCEND_RT_VISIBLE_DEVICES2,3\-v/usr/local/dcmi:/usr/local/dcmi\-v/usr/local/bin/npu-smi:/usr/local/bin/npu-smi\-v/usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/\-v/usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info\-v/data/models:/data/models\-v/data/wuyuming/vllm-ascend-gemma4/patches:/patches\--group-add$(getent group HwHiAiUser|cut-d:-f3)\vllm-ascend-gemma4:full\sleep3600# 2) 启动 vllm serve# 注意VLLM_USE_V10 --enforce-eager 是当前实测可启动的组合。# V1 引擎在该镜像下加载权重后 engine core 会卡在 do_poll 上不再响应# 所以先关掉图编译跑通验证流程。dockerexec-dgemma4-pr9222-testbash-c export VLLM_USE_V10 export ASCEND_RT_VISIBLE_DEVICES2,3 # 跟 docker -e 保持一致 vllm serve /data/models/gemma-4-31B \ --served-model-name gemma-4-31B \ --port 8002 \ --dtype auto \ --gpu-memory-utilization 0.9 \ --trust-remote-code \ --trust-request-chat-template \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --max-num-seqs 4 \ --mm-processor-cache-gb 0 \ --enforce-eager /tmp/vllm.log 21 # 关于 VLLM_USE_V1 / V1 引擎 / --enforce-eager## V1 引擎是 vLLM 在 v0.20.0 引入的新一代执行引擎vLLM V1 Engine# 相比 V0也叫 legacy engine:# - 统一的 Scheduler Executor 抽象吞吐更高# - 默认情况下由 VLLM_USE_V1 控制未设置时按版本自动检测# (vllm 0.20.2 默认开 V1看到 Initializing a V1 LLM engine 即为 V1)# - 在 GPU 上跑得好但 vllm-ascend 的 v0.20.2rc1 对 V1 的支持有 bug# ——EngineCore 加载完权重后会卡在 do_poll 上不再响应# service 永远不报 Application startup complete。解决方法用 VLLM_USE_V10# 切回 V0 引擎即可绕过该 bug。## --enforce-eager 是关闭 torch.compile ACL Graph 编译。在 V0 Ascend 组合下# 跳过图捕获可以避免潜在的 compilation hang启动更快但吞吐略低 (约 10-15%)。# 若未来 vllm-ascend 修复了 V1 引擎可以同时去掉 VLLM_USE_V10 和 --enforce-eager# 恢复默认配置。# 3) 等待并查看日志sleep180dockerexecgemma4-pr9222-testtail-80/tmp/vllm.log3.3 预期结果容器运行中dockerps|grepgemma4-pr9222-testdockerexecgemma4-pr9222-test npu-smi infonpu-smi info能看到 NPU 及其 HBM 使用率。模型列表接口返回正常curl-shttp://localhost:8002/v1/models|python3-mjson.tool应返回包含gemma-4-31B的模型列表。步骤 4: 修复 Chat Stop Strings 问题4.1 目标修复 vllm 在不传stop时不停止生成的问题。先复现问题再应用补丁最后验证修复效果。4.2 操作# 0) 写一个观察脚本到 host一次写完多次复用避免 shell 转义 f-string 的坑cat/tmp/observe.pyPYEOF import json, sys d json.load(sys.stdin)[choices][0] t d[text] eot end_of_turn print(ftext : {t!r}) print(ffinish : {d[finish_reason]}) print(fstop : {d.get(stop_reason)}) print(fturns : {t.count(eot)} 个 end_of_turn) print() print( * 50) print(原始生成内容) print( * 50) print(t) PYEOF# 1) 复现问题不传 stop模型生成 end_of_turn 后仍继续curl-shttp://localhost:8002/v1/completions\-HContent-Type: application/json\-d{ model: gemma-4-31B, prompt: start_of_turnuser\nWhat is 11?end_of_turn\nstart_of_turnmodel\n, max_tokens: 100, temperature: 0.0 }|python3 /tmp/observe.py# 1b) 流式观察看着更直观# 不修 stop 时会看到 2 → end_of_turn → 然后继续吐# 如# 2end_of_turn# start_of_turnuser# What is 22?end_of_turn# start_of_turnmodel# 4end_of_turn# start_of_turnuser# What is 33?end_of_turn# start_of_turnmodel# 6end_of_turn# start_of_turnuser# 2) 应用补丁patches 目录已挂到 /patchesdockerexecgemma4-pr9222-test python3 /patches/apply_all_patches.py# 3) 清 Python 缓存避免 .pyc 覆盖新打的补丁dockerexecgemma4-pr9222-testbash-c find /vllm-workspace/vllm -name __pycache__ -type d -exec rm -rf {} 2/dev/null find /vllm-workspace/vllm -name *.pyc -delete 2/dev/null # 4) 重启 vllmdockerexecgemma4-pr9222-testbash-cpkill -f vllm serve || truesleep5dockerexec-dgemma4-pr9222-testbash-c export VLLM_USE_V10 export ASCEND_RT_VISIBLE_DEVICES2,3 vllm serve /data/models/gemma-4-31B \ --served-model-name gemma-4-31B \ --port 8002 --dtype auto --gpu-memory-utilization 0.9 \ --trust-remote-code --trust-request-chat-template \ --tensor-parallel-size 2 --max-model-len 8192 --max-num-seqs 4 \ --mm-processor-cache-gb 0 \ --enforce-eager /tmp/vllm.log 21 sleep90# 5) 再次验证同一脚本对比修复前后差异curl-shttp://localhost:8002/v1/completions\-HContent-Type: application/json\-d{ model: gemma-4-31B, prompt: start_of_turnuser\nWhat is 11?end_of_turn\nstart_of_turnmodel\n, max_tokens: 100, temperature: 0.0 }|python3 /tmp/observe.py补丁核心修改涉及 4 个文件 1 个新增 utils:# (1) vllm/entrypoints/openai/chat_completion/protocol.py# to_sampling_params() 里替换 stop 字段defto_sampling_params(self,default_sampling_params):# ... 其它字段合并 ..._req_stopself.stopifself.stopelse[]ifisinstance(_req_stop,str):_req_stop[_req_stop]_default_stopdefault_sampling_params.get(stop,[])_merged_stoplist(dict.fromkeys(_req_stop_default_stop))orNone# stop_token_ids 同理returnSamplingParams.from_optional(stop_merged_stop,stop_token_ids_merged_stop_ids,...)# (2) vllm/entrypoints/openai/completion/protocol.py# 同样的 to_sampling_params() 修复# (3) vllm/entrypoints/openai/completion/serving.py# __init__ 里注入 default stop strings之前漏了fromvllm.entrypoints.openai.parser.chat_stop_utilsimport(get_default_chat_stop_strings,)# 把 get_default_chat_stop_strings(self.tokenizer) 的返回值# 合并进 self.default_sampling_params[stop]# (4) vllm/entrypoints/openai/chat_completion/serving.py# 和 (3) 同款注入——/v1/chat/completions 的 init 路径同样需要# 否则 default_sampling_params[stop] 在 chat 路径上是空的# to_sampling_params 也没东西可 merge结果就是 chat completion# 飞过 end_of_turn 一直跑到 max_tokens。fromvllm.entrypoints.openai.parser.chat_stop_utilsimport(get_default_chat_stop_strings,)# 在 self.renderer.tokenizer 上拿 stop stringsextend 进# self.default_sampling_params[stop]与 (3) 的 (3) 同款逻辑# (5) vllm/entrypoints/openai/parser/chat_stop_utils.py# 新增: 从 tokenizer 拿候选 stop strings能 tokenize 的那些defget_default_chat_stop_strings(tokenizer):candidates(end_of_turn,|eot_id|,|im_end|,|end_of_text|,)return[sforsincandidatesiftokenizer.encode(s,add_special_tokensFalse)]4.3 预期结果修复前观察脚本输出关键三个字段finish : length # 撞 max_tokens 被强制截断 stop : None # 没有 stop_reason turns : 4 个 end_of_turn # ← bug 特征模型答完后还在续写多轮 text : 2end_of_turn\nstart_of_turnuser\nWhat is...\n也就是模型输出2后遇到了end_of_turn但 vllm 没把它当停止信号继续吞 token 直到max_tokens耗尽生成伪多轮对话。修复后观察脚本输出finish : stop stop : end_of_turn turns : 1 个 end_of_turn text : 2关键标志finish_reasonstop不再是lengthstop_reasonend_of_turnvllm 真正在停字符串上停下来了text2单 token 数字2后立刻停下turns1只触发了一次end_of_turn想直观感受 streaming 行为启用 4.2 中的curl -Ns ... --data ... stream:true}即可修复前会看到2→end_of_turn→start_of_turnuser\n...继续蹦。步骤 5: 验证多模态、流式与 Chat 补全5.1 目标确认 Gemma 4 的图像理解、SSE 流式输出、以及 inline chat_template 的 chat 接口都能正常工作且 stop strings 在三种场景下都生效。5.2 操作# 1) 多模态测试host 上准备脚本# 图像占位符必须是 |image|Gemma4 processor 的 image_token,token_id 258880。cat/tmp/test_mm.pyPYEOF import requests, base64 from pathlib import Path img_b64 base64.b64encode(Path(/data/wuyuming/vllm-ascend-gemma4/test_images/text_hello.png).read_bytes()).decode() TEMPLATE ( {{ bos_token }} {% for message in messages %} start_of_turn{{ message[role] }}\n {% if message[content] is string %} {{ message[content] }}end_of_turn\n {% else %} {% for content in message[content] %} {% if content[type] text %}{{ content[text] }} {% elif content[type] image %}|image| {% endif %} {% endfor %} end_of_turn\n {% endif %} {% endfor %} start_of_turnmodel\n ) resp requests.post(http://localhost:8002/v1/chat/completions, json{ model: gemma-4-31B, messages: [{ role: user, content: [ {type: text, text: What color is this image?}, {type: image_url, image_url: {url: fdata:image/png;base64,{img_b64}}}, ], }], max_tokens: 64, temperature: 0.0, chat_template: TEMPLATE, }) print(status:, resp.status_code) print(content:, resp.json()[choices][0][message][content]) PYEOFpython3 /tmp/test_mm.py# 2) 流式 文本补全实时打出 text delta结尾给 finish/stop_reasoncurl-N-shttp://localhost:8002/v1/completions\-HContent-Type: application/json\-d{model:gemma-4-31B,prompt:start_of_turnuser\nTell me a storyend_of_turn\nstart_of_turnmodel\n,max_tokens:500,stream:true}\|python3-u-c import sys, json last None for line in sys.stdin: line line.strip() if not line.startswith(data: ): continue body line[6:] if body [DONE]: break try: ch json.loads(body)[choices][0] sys.stdout.write(ch.get(text, )); sys.stdout.flush() last ch except Exception: pass if last: fin last.get(finish_reason); sr last.get(stop_reason) print(\n\n--- finish repr(fin) stop_reason repr(sr)) # 3) chat 补全inline template提取 answer / finish / stop / 轮数curl-shttp://localhost:8002/v1/chat/completions\-HContent-Type: application/json\-d{ model: gemma-4-31B, messages: [{role: user, content: What is 11?}], max_tokens: 100, chat_template: {{ bos_token }}{% for message in messages %}start_of_turn{{ message[\role\] }}\n{{ message[\content\] }}end_of_turn\n{% endfor %}start_of_turnmodel\n }|python3-u-c import sys, json ch json.load(sys.stdin)[choices][0] msg ch[message][content] eot end_of_turn print(answer :, repr(msg[:80]) (... if len(msg) 80 else )) print(finish :, repr(ch.get(finish_reason))) print(stop :, repr(ch.get(stop_reason))) print(turns :, str(msg.count(eot)) 个 end_of_turn) 5.3 预期结果多模态每张图能拿到简短的颜色/物体描述英文字符串。流式每个 chunk 正常吐出 token且能正常停止。chat直接给2停在end_of_turn上finishstop不再续写多轮。步骤 6: 固化镜像并跑完整回归6.1 目标把打过补丁的容器封装成vllm-ascend-gemma4:pr9222-chat-stop-v3并用它直接起服务跑 stop strings 6/6 和多模态 5/5 完整回归确认 bake 出来的镜像就是能用的。6.2 操作# 1) 停掉 vllm准备 commitdockerexecgemma4-pr9222-testbash-cpkill -f vllm serve || truesleep5# 2) 把 entrypoint 拷进容器# 该脚本会先把 VLLM_USE_V10 和 ASCEND_RT_VISIBLE_DEVICES2,3 注入环境# 然后 exec vllm serve比 docker commit --changeENV ... 更可控# 可以被 docker run -e 覆盖。dockercp/data/wuyuming/vllm-ascend-gemma4/patches/entrypoint.sh gemma4-pr9222-test:/entrypoint.shdockerexecgemma4-pr9222-testchmodx /entrypoint.sh# 3) 提交为新镜像设置 ENTRYPOINT 为脚本CMD 为空让 entrypoint 走默认 vllm serve# 跑 v3 镜像时如果想覆盖默认参数docker run image --port 9000 即可dockercommit\--changeENTRYPOINT [/entrypoint.sh]\--changeCMD []\gemma4-pr9222-test vllm-ascend-gemma4:pr9222-chat-stop-v3# 3) 停掉旧容器用 v3 镜像重新启动dockerstop gemma4-pr9222-testdockerrmgemma4-pr9222-testdockerrun-d--namegemma4-pr9222-test\--networkhost--privileged\--device/dev/davinci_manager\-eASCEND_RT_VISIBLE_DEVICES2,3\-v/usr/local/dcmi:/usr/local/dcmi\-v/usr/local/bin/npu-smi:/usr/local/bin/npu-smi\-v/usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/\-v/data/models:/data/models\--group-add$(getent group HwHiAiUser|cut-d:-f3)\vllm-ascend-gemma4:pr9222-chat-stop-v3# 4) 等待服务就绪foriin$(seq130);dosleep10ifcurl-shttp://localhost:8002/v1/models/dev/null21;thenechoservice ready after${i}0sbreakfidone6.3 预期结果镜像存在dockerimages|greppr9222-chat-stop-v3输出vllm-ascend-gemma4 pr9222-chat-stop-v3 sha256:9b09fbfa79ad 27.1GB服务启动正常日志看到Application startup complete.。步骤 7: 服务日常管理7.1 目标把启停、监控、清理跑顺。7.2 操作# 启停dockerstart gemma4-pr9222-testdockerstop gemma4-pr9222-testdockerrestart gemma4-pr9222-test# 日志dockerlogs gemma4-pr9222-test--tail100-f# NPU 监控watch-n1npu-smi info# 镜像清理Layer 1 是冗余的可以删dockerrmi vllm-ascend-gemma4:full7.3 预期结果docker start后 60-90 秒服务就绪。npu-smi info显示 HBM 使用率与请求量正相关。删了vllm-ascend-gemma4:full后vllm-ascend-gemma4:pr9222-chat-stop-v3仍然能跑因为 Layer 0 还在。附: 从 Base 镜像一键重建 v3跳过中间层从官方 Base 直接 bake v3只适用于能访问外网的环境:dockerrun--rm\-v/data/wuyuming/vllm-ascend-gemma4/patches:/patches\--networkhost--privileged--device/dev/davinci_manager\quay.io/ascend/vllm-ascend:v0.20.2rc1-openeulerbash-c # 1) 装 PR #9222 源码覆盖 base 镜像里的官方 vllm-ascend git clone --depth 1 -b fix-gemma4-ascend-inference-clean \ https://github.com/zhangshuai404/vllm-ascend.git /tmp/va source /usr/local/Ascend/ascend-toolkit/set_env.sh export SOC_VERSIONascend910b4 git config --global --add safe.directory * pip install --no-build-isolation -e /tmp/va # 2) 装上游 vllm 0.20.2跟 base 镜像一致 git clone --depth 1 -b v0.20.2 \ https://github.com/vllm-project/vllm.git /vllm-workspace/vllm pip install -e /vllm-workspace/vllm/[audio] \ --extra-index https://download.pytorch.org/whl/cpu/ # 3) 应用 RoPE UB 溢出补丁 python3 /patches/apply_rope_patch.py # 4) 应用 stop strings 补丁 python3 /patches/apply_all_patches.py # 5) 安装 entrypoint 脚本让镜像自带 VLLM_USE_V10 ASCEND_RT_VISIBLE_DEVICES2,3 cp /patches/entrypoint.sh /entrypoint.sh chmod x /entrypoint.sh 之后步骤 3-6 照走最后能 bake 出一个等效的 v3 镜像。相关资源PR #9222: https://github.com/vllm-project/vllm-ascend/pull/9222PR 分支: https://github.com/zhangshuai404/vllm-ascend/tree/fix-gemma4-ascend-inference-cleanvLLM-Ascend 官方: https://github.com/vllm-project/vllm-ascend基线镜像: https://quay.io/ascend/vllm-ascend:v0.20.2rc1-openeulerGemma 4 模型: https://huggingface.co/google/gemma-4-31B