官方匹配关系
截至 2026 年 6 月 8 日,推荐先使用稳定版:
| 组件 | 版本 |
|---|---|
| 镜像 | quay.io/ascend/vllm-ascend:v0.18.0-a3 |
| vLLM | 0.18.0 |
| vLLM-Ascend | 0.18.0 |
| PyTorch | 2.9.0 |
| torch-npu | 2.9.0.post2 |
| CANN/NNAL | 9.0.0 |
| 硬件目标 | Atlas A3,ascend910_9391 |
官方表格在:编辑vLLM-Ascend v0.18.0 安装文档
镜像仓库在:Quay vllm-ascend Tags
原理是:
宿主机:A3 固件 + NPU 驱动 ↓ 设备和驱动库挂进容器 容器:CANN 9.0.0 + torch-npu 2.9.0.post2 + torch 2.9.0 + vLLM 0.18.0 + vLLM-Ascend 0.18.0
1. 检查宿主机
cat /etc/os-release uname -m docker --version npu-smi info ls -1 /dev/davinci[0-9]* 2>/dev/null | sort -V cat /usr/local/Ascend/driver/version.info 2>/dev/null
重点确认:
- npu-smi info 能正常显示 A3 卡。
- 至少存在 /dev/davinci0 到 /dev/davinci3。
- Docker 正常。
- 如果系统是 Ubuntu,使用 v0.18.0-a3。
- 如果系统是 openEuler,使用 v0.18.0-a3-openeuler。
2. 拉取官方镜像
Ubuntu:
export IMAGE=quay.io/ascend/vllm-ascend:v0.18.0-a3 docker manifest inspect "$IMAGE" >/dev/null docker pull "$IMAGE"
openEuler:
export IMAGE=quay.io/ascend/vllm-ascend:v0.18.0-a3-openeuler docker pull "$IMAGE"
国内网络较慢时,可以使用官方 FAQ 推荐的镜像代理:
docker pull m.daocloud.io/quay.io/ascend/vllm-ascend:v0.18.0-a3 docker tag m.daocloud.io/quay.io/ascend/vllm-ascend:v0.18.0-a3 \ quay.io/ascend/vllm-ascend:v0.18.0-a3
查看是否拉取成功:
docker images | grep vllm-ascend docker image inspect "$IMAGE" --format '{``{index .RepoDigests 0}}'
最后一条显示的是镜像不可变的 digest,可以保存下来做复现实验。
3. 准备模型目录
假设官方 BF16 权重在:
export MODEL_HOST=/data/models/Qwen3.5-35B-A3B test -f "$MODEL_HOST/config.json" ls -lh "$MODEL_HOST" | head du -sh "$MODEL_HOST"
第一轮不要使用训练导出的权重,也不要使用 W8A8。先用官方 BF16 权重排除权重转换和量化问题。
4. 启动 TP4 容器
mkdir -p /data/vllm-logs docker run --rm -it \ --name qwen35-vllm-a3 \ --net=host \ --shm-size=32g \ --device /dev/davinci0 \ --device /dev/davinci1 \ --device /dev/davinci2 \ --device /dev/davinci3 \ --device /dev/davinci_manager \ --device /dev/devmm_svm \ --device /dev/hisi_hdc \ -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 /etc/ascend_install.info:/etc/ascend_install.info \ -v "$MODEL_HOST:/models/Qwen3.5-35B-A3B:ro" \ -v /data/vllm-logs:/logs \ "$IMAGE" bash
官方示例映射 A3 的 16 个设备;你现在做 TP4 单机实验,先映射 0,1,2,3 即可。
5. 在容器里检查版本
进入容器后执行:
npu-smi info python - <<'PY' import importlib.metadata as md import torch import torch_npu for pkg in ["vllm", "vllm-ascend", "torch", "torch-npu"]: print(f"{pkg:14s} = {md.version(pkg)}") print("NPU available =", torch.npu.is_available()) print("NPU count =", torch.npu.device_count()) print("NPU 0 =", torch.npu.get_device_name(0)) x = torch.ones(4, device="npu:0") print("NPU calculation =", x + 1) PY
期望看到:
vllm = 0.18.0 vllm-ascend = 0.18.0 torch = 2.9.0 torch-npu = 2.9.0.post2 NPU available = True NPU count = 4 NPU calculation = tensor([2., 2., 2., 2.], device='npu:0')
如果这里失败,先不要启动 Qwen。此时问题属于容器、驱动或设备映射,不属于 MTP。
6. 检查模型是否真的带 MTP
python - <<'PY' import json from pathlib import Path model = Path("/models/Qwen3.5-35B-A3B") config = json.loads((model / "config.json").read_text()) print("mtp_num_hidden_layers =", config.get("mtp_num_hidden_layers")) index = json.loads( next(model.glob("*.safetensors.index.json")).read_text() ) names = [x for x in index["weight_map"] if ".mtp." in x] print("MTP tensor count =", len(names)) for name in names[:10]: print(name) PY
应当看到:
mtp_num_hidden_layers = 1 MTP tensor count = 非零 model.language_model.mtp.layers.0....
7. 启动 MTP 推理
仍在容器内:
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3 export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True export HCCL_BUFFSIZE=512 export OMP_PROC_BIND=false export OMP_NUM_THREADS=1 export TASK_QUEUE_ENABLE=1 vllm serve /models/Qwen3.5-35B-A3B \ --host 0.0.0.0 \ --port 8000 \ --served-model-name qwen35-mtp \ --tensor-parallel-size 4 \ --distributed-executor-backend mp \ --max-model-len 4096 \ --max-num-seqs 16 \ --max-num-batched-tokens 4096 \ --gpu-memory-utilization 0.90 \ --mm-processor-cache-gb 0 \ --no-enable-prefix-caching \ --trust-remote-code \ --speculative_config \ '{"method":"qwen3_5_mtp","num_speculative_tokens":3,"enforce_eager":true}' \ --compilation-config \ '{"cudagraph_mode":"FULL_DECODE_ONLY"}' \ 2>&1 | tee /logs/qwen35_mtp.log
不要在这个官方容器里重新 pip install vllm、torch 或 torch-npu,否则"匹配容器"的意义就没了。
启动成功后,用持续请求触发统计,然后检查:
grep "SpecDecoding metrics" /data/vllm-logs/qwen35_mtp.log | tail -20
判定成功的关键不是服务能生成文字,而是:
Drafted > 0 Accepted > 0 Mean acceptance length > 1.00 Avg Draft acceptance rate > 0%
如果这个官方容器加官方 BF16 权重能接受,而 VERL 中仍为零,就能基本锁定为 VERL 动态更新主模型,却没有正确更新 MTP drafter 权重。
参考:编辑官方安装文档、编辑镜像下载 FAQ、CANN 9.0.0 版本说明。
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
export HCCL_BUFFSIZE=512
export OMP_PROC_BIND=false
export OMP_NUM_THREADS=1
export TASK_QUEUE_ENABLE=1
vllm serve /models/Qwen3.5-35B-A3B \
--host 0.0.0.0 \
--port 8000 \
--served-model-name qwen35-mtp \
--tensor-parallel-size 4 \
--distributed-executor-backend mp \
--max-model-len 4096 \
--max-num-seqs 16 \
--max-num-batched-tokens 4096 \
--gpu-memory-utilization 0.90 \
--mm-processor-cache-gb 0 \
--no-enable-prefix-caching \
--trust-remote-code \
--speculative_config \
'{"method":"qwen3_5_mtp","num_speculative_tokens":3,"enforce_eager":true}' \
--compilation-config \
'{"cudagraph_mode":"FULL_DECODE_ONLY"}' \
2>&1 | tee /logs/qwen35_mtp.log
不用依赖 pgrep 找模型。你这个脚本和日志已经把路径确认出来了:
/efs_rl/weights/Qwen3.5-35B-A3B
脚本中:
HF_MODEL_PATH=${HF_MODEL_PATH:-"/efs_rl/weights/Qwen3.5-35B-A3B"} actor_rollout_ref.model.path=${HF_MODEL_PATH}
含义是:
如果外部设置过 HF_MODEL_PATH → 使用外部值 否则 → 使用 /efs_rl/weights/Qwen3.5-35B-A3B
日志也明确记录:
'path': '/efs_rl/weights/Qwen3.5-35B-A3B' Loading from /efs_rl/weights/Qwen3.5-35B-A3B
为什么 pgrep 是空的
VERL 启动的 vLLM 通常是 Ray 管理的内嵌 Worker:
VERL TaskRunner └── Ray Worker └── vLLMHttpServer └── vLLM Engine
它不一定以 vllm serve 或 api_server 出现在命令行里。训练停止以后,这些临时 Worker 也会退出。
因此:
pgrep -af "vllm|api_server"
为空只说明现在没有相关服务运行,不代表模型不存在。
第一步:确认当前容器能看到模型
在准备启动 vLLM 的容器中执行:
export MODEL=/efs_rl/weights/Qwen3.5-35B-A3B ls -ld "$MODEL" ls -lh "$MODEL/config.json" ls -lh "$MODEL" | head -30 du -sh "$MODEL"
正常应该能看到:
config.json tokenizer.json 或 tokenizer_config.json model-00001-of-xxxxx.safetensors model.safetensors.index.json
如果提示:
No such file or directory
说明当前容器没有挂载 /efs_rl。需要退出容器,在宿主机检查:
ls -ld /efs_rl/weights/Qwen3.5-35B-A3B docker inspect 容器名 --format '{``{json .Mounts}}' | python -m json.tool
重新启动容器时增加:
-v /efs_rl:/efs_rl
第二步:检查是不是带 MTP 的原始权重
python - <<'PY' import glob import json import os root = os.environ["MODEL"] with open(root + "/config.json") as f: config = json.load(f) text_config = config.get("text_config", {}) mtp_layers = config.get( "mtp_num_hidden_layers", text_config.get("mtp_num_hidden_layers") ) print("模型目录:", root) print("model_type:", config.get("model_type")) print("mtp_num_hidden_layers:", mtp_layers) indexes = glob.glob(root + "/*.safetensors.index.json") if not indexes: raise RuntimeError("没有找到 *.safetensors.index.json") with open(indexes[0]) as f: weight_map = json.load(f)["weight_map"] mtp_names = [name for name in weight_map if ".mtp." in name] print("总 tensor 数:", len(weight_map)) print("MTP tensor 数:", len(mtp_names)) print("前 10 个 MTP tensor:") for name in mtp_names[:10]: print(" ", name) PY
期望结果:
mtp_num_hidden_layers: 1 MTP tensor 数: 大于 0 model.language_model.mtp.layers.0....
如果 MTP tensor 数为零,这份权重只能跑普通推理,不能用它验证 MTP。
第三步:确认当前 vLLM 支持专用 MTP
python - <<'PY'
from importlib.metadata import version
print("vllm:", version("vllm"))
print("vllm-ascend:", version("vllm-ascend"))
from vllm.model_executor.models.qwen3_5_mtp import Qwen3_5MoeMTP
print("Qwen3_5MoeMTP:", Qwen3_5MoeMTP)
print("检查通过")
PY
如果这里 ImportError,就不要继续启动,因为当前容器缺少 Qwen3.5 MTP 实现。
终端 A:启动独立 vLLM
先确认 VERL 已经停掉,并检查卡空闲:
npu-smi info
然后启动:
(
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
export HCCL_BUFFSIZE=512
export OMP_PROC_BIND=false
export OMP_NUM_THREADS=1
export TASK_QUEUE_ENABLE=1
vllm serve /efs_rl/weights/Qwen3.5-35B-A3B \
--host 0.0.0.0 \
--port 8000 \
--served-model-name qwen35-mtp \
--tensor-parallel-size 4 \
--distributed-executor-backend mp \
--max-model-len 4096 \
--max-num-seqs 16 \
--max-num-batched-tokens 4096 \
--gpu-memory-utilization 0.90 \
--mm-processor-cache-gb 0 \
--no-enable-prefix-caching \
--trust-remote-code \
--speculative-config \
'{"method":"qwen3_5_mtp","num_speculative_tokens":3,"enforce_eager":true}' \
--compilation-config \
'{"cudagraph_mode":"FULL_DECODE_ONLY"}' \
2>&1 | tee /tmp/qwen35_mtp.log
)
等待出现:
Application startup complete Uvicorn running on http://0.0.0.0:8000
这个终端不能关。
终端 B:发请求
重新进入容器,先检查服务:
curl -s http://127.0.0.1:8000/health curl -s http://127.0.0.1:8000/v1/models
持续发送 16 个并发请求:
python - <<'PY'
import json
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
URL = "http://127.0.0.1:8000/v1/chat/completions"
prompts = [
"详细解释大语言模型的训练和推理过程。",
"写一篇关于人工智能未来发展的文章。",
"用通俗语言解释量子计算的基本原理。",
"介绍新能源技术未来十年的发展方向。",
]
def send(i):
body = {
"model": "qwen35-mtp",
"messages": [{
"role": "user",
"content": prompts[i % len(prompts)]
}],
"temperature": 0,
"max_tokens": 256,
"ignore_eos": True,
"stream": False
}
request = urllib.request.Request(
URL,
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(request, timeout=600) as response:
result = json.load(response)
return i, result.get("usage", {}).get("completion_tokens")
with ThreadPoolExecutor(max_workers=4) as pool:
jobs = [pool.submit(send, i) for i in range(16)]
for job in as_completed(jobs):
i, tokens = job.result()
print(f"请求 {i:02d} 完成,生成 {tokens} tokens")
PY
终端 C:看接受率
tail -f /tmp/qwen35_mtp.log |
grep --line-buffered "SpecDecoding metrics"
或者请求结束后:
grep "SpecDecoding metrics" /tmp/qwen35_mtp.log | tail -20
最终判断:
Drafted > 0 且 Accepted > 0:单独 vLLM MTP 跑通
Drafted > 0 但 Accepted = 0:权重加载或 MTP 实现仍有问题
没有 SpecDecoding metrics:MTP 没启用或统计没打开
服务起不来:先看 /tmp/qwen35_mtp.log 的第一个 ERROR
所以你现在最先执行的是前面的模型目录检查、MTP tensor 检查、Qwen3_5MoeMTP import 检查。这三项通过后,才启动服务。