Qwen3.5-397B-A17B-w8a8 双机部署排障复盘:从报错到定位损坏分片
·
Qwen3.5-397B-A17B-w8a8 双机部署排障复盘:从报错到定位损坏分片
背景
本文记录一次在两台 Atlas A2 节点上部署 Qwen3.5-397B-A17B-w8a8(vLLM Ascend)时的完整排障过程:
- 先按双节点
torchrun + tensor-parallel-size=16方式拉起服务; - 再从日志中抽取关键异常;
- 基于调用栈回溯到具体加载逻辑;
- 最终用校验脚本定位到损坏的模型分片文件。
目标不是“猜测”,而是通过可复现日志与代码路径,得到可验证结论。
1. 复现环境与启动命令
1.1 两节点启动前准备
mkdir -p /tmp/qwen_logs
1.2 节点0(节点0)
docker run -it --rm \
--name qwen35-tp16-node0 \
--net=host --ipc=host --privileged --userns=host \
--security-opt label=disable --shm-size=64g \
--device=/dev/davinci0 --device=/dev/davinci1 --device=/dev/davinci2 --device=/dev/davinci3 \
--device=/dev/davinci4 --device=/dev/davinci5 --device=/dev/davinci6 --device=/dev/davinci7 \
--device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \
-v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
-v /usr/local/Ascend/firmware:/usr/local/Ascend/firmware \
-v /usr/local/sbin/npu-smi:/usr/local/sbin/npu-smi \
-v /home/qwen35w8a8:/models/qwen35w8a8:ro \
-w /vllm-workspace/vllm \
vllm-ascend:qwen3_5-v0-a2 /bin/bash -lc '
set -euo pipefail
export PYTHONPATH=/vllm-workspace/vllm:/usr/local/Ascend/ascend-toolkit/latest/python/site-packages
source /usr/local/Ascend/ascend-toolkit/set_env.sh || true
export HCCL_IF_IP=节点0
export HCCL_SOCKET_IFNAME=bond0
export GLOO_SOCKET_IFNAME=bond0
export TP_SOCKET_IFNAME=bond0
export OMP_NUM_THREADS=1
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
torchrun \
--nnodes=2 \
--nproc_per_node=8 \
--node_rank=0 \
--master_addr=节点0 \
--master_port=29500 \
-m vllm.entrypoints.openai.api_server \
--model /models/qwen35w8a8 \
--served-model-name qwen3.5 \
--host 0.0.0.0 --port 8010 \
--distributed-executor-backend external_launcher \
--tensor-parallel-size 16 \
--max-model-len 1024 \
--max-num-batched-tokens 2048 \
--max-num-seqs 4 \
--gpu-memory-utilization 0.90 \
--quantization ascend
' 2>&1 | tee /tmp/qwen_logs/node0.log
1.3 节点1(节点1)
docker run -it --rm \
--name qwen35-tp16-node1 \
--net=host --ipc=host --privileged --userns=host \
--security-opt label=disable --shm-size=64g \
--device=/dev/davinci0 --device=/dev/davinci1 --device=/dev/davinci2 --device=/dev/davinci3 \
--device=/dev/davinci4 --device=/dev/davinci5 --device=/dev/davinci6 --device=/dev/davinci7 \
--device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \
-v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
-v /usr/local/Ascend/firmware:/usr/local/Ascend/firmware \
-v /usr/local/sbin/npu-smi:/usr/local/sbin/npu-smi \
-v /home/qwen35w8a8:/models/qwen35w8a8:ro \
-w /vllm-workspace/vllm \
vllm-ascend:qwen3_5-v0-a2 /bin/bash -lc '
set -euo pipefail
export PYTHONPATH=/vllm-workspace/vllm:/usr/local/Ascend/ascend-toolkit/latest/python/site-packages
source /usr/local/Ascend/ascend-toolkit/set_env.sh || true
export HCCL_IF_IP=节点1
export HCCL_SOCKET_IFNAME=bond0
export GLOO_SOCKET_IFNAME=bond0
export TP_SOCKET_IFNAME=bond0
export OMP_NUM_THREADS=1
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
torchrun \
--nnodes=2 \
--nproc_per_node=8 \
--node_rank=1 \
--master_addr=节点0 \
--master_port=29500 \
-m vllm.entrypoints.openai.api_server \
--model /models/qwen35w8a8 \
--served-model-name qwen3.5 \
--host 0.0.0.0 --port 8010 \
--distributed-executor-backend external_launcher \
--tensor-parallel-size 16 \
--max-model-len 1024 \
--max-num-batched-tokens 2048 \
--max-num-seqs 4 \
--gpu-memory-utilization 0.90 \
--quantization ascend
' 2>&1 | tee /tmp/qwen_logs/node1.log
2. 第一轮日志筛查:快速锁定失败阶段
先用统一关键字抓取异常:
grep -n "ERROR\|Traceback\|RuntimeError\|EngineCore failed to start" /tmp/qwen_logs/node0.log | head -n 20
grep -n "ERROR\|Traceback\|RuntimeError\|EngineCore failed to start" /tmp/qwen_logs/node1.log | head -n 20
观察到两个现象:
- 两侧都出现
EngineCore failed to start; - 但 node1 的调用栈更早进入
load_model相关路径并抛异常。
这说明问题不像纯网络/HCCL 首因(因为通信已基本建立),更像是模型加载阶段出错。
3. 第二轮定位:提取 node1 首个完整异常上下文
为了避免被截断日志误导,直接抓取首个异常附近的大段上下文:
line=$(grep -n "EngineCore failed to start" /tmp/qwen_logs/node1.log | head -n1 | cut -d: -f1)
sed -n "$((line-160)),$((line+220))p" /tmp/qwen_logs/node1.log
关键调用栈末端如下(核心片段):
.../weight_utils.py", line 724, in safetensors_weights_iterator
with safe_open(st_file, framework="pt") as f:
safetensors_rust.SafetensorError: Error while deserializing header: incomplete metadata, file not fully covered
这个错误信息非常明确:
safe_open在读取某个.safetensors文件头时失败;incomplete metadata, file not fully covered指向文件内容不完整/截断/损坏,而不是参数配置错误。
4. 排除干扰项:通信并非首要根因
从日志还能看到:
- 多个 rank 出现
Rank x is connected to 15 peer ranks,说明 16 卡组网/进程组建立过; - 也有
Rank 0 is connected to 0 peer ranks的瞬态输出,但并未直接对应最终抛错点; - 真正致命栈落在
safetensors反序列化。
结论:本次失败不是先由 HCCL/NCCL 通信错误触发,而是模型权重文件读取失败触发。
5. 最终验证:用代码逐个校验模型分片
为了把“怀疑”变成“证据”,在容器内逐个检查所有 safetensors 分片:
docker run --rm -it \
-v /home/qwen35w8a8:/models:ro \
vllm-ascend:qwen3_5-v0-a2 /bin/bash -lc '
python3 - << "PY"
import glob
from safetensors import safe_open
bad = []
for p in sorted(glob.glob("/models/*.safetensors")):
try:
with safe_open(p, framework="pt") as f:
_ = list(f.keys())[:1]
except Exception as e:
bad.append((p, str(e)))
print("bad_count =", len(bad))
for p, e in bad:
print("BAD:", p)
print("ERR:", e)
PY
'
输出:
bad_count = 1
BAD: /models/quant_model_weights-00084-of-00099.safetensors
ERR: Error while deserializing header: incomplete metadata, file not fully covered
至此,问题定位闭环完成。
6. 根因结论
本次双机启动失败的直接根因是:
- 模型分片文件损坏/不完整,具体为:
/home/qwen35w8a8/quant_model_weights-00084-of-00099.safetensors
因此 vllm 在模型加载阶段触发 safetensors_rust.SafetensorError,并导致 EngineCore failed to start。
7. 可复用排障方法论(模板)
以后遇到类似“大模型分布式启动失败”,建议按这个顺序走:
- 先抓首个致命异常:
EngineCore failed to start附近 200~400 行; - 看调用栈最底层库抛错点(不要只看上层 RuntimeError);
- 区分阶段:通信初始化、权重加载、KV cache 初始化分别对应不同根因;
- 写最小验证脚本(本例是逐个
safe_open)把根因“可执行化”; - 输出可证据化结论:具体文件、具体错误、可复现命令。
8. 一句话复盘
这次不是“参数调优问题”,也不是“网络首因问题”,而是单个 safetensors 分片损坏导致的模型加载失败。通过“日志调用栈 + 逐文件代码验证”,最终把问题定位到 00084-of-00099 这个具体文件。
更多推荐


所有评论(0)