vLLM-Ascend启动失败排查:libatb.so、EngineCore与et_env.sh全解析
2026/9/18 8:35:13 网站建设 项目流程

vLLM-Ascend 启动失败这个问题,最近在昇腾推理部署群里出现的频率明显高了不少。很多人拿着 vLLM 在 GPU 上的排查经验来套,结果发现三个报错反复绕不过去:libatb.so加载失败、EngineCore初始化异常、et_env.sh相关环境问题。这三个故障各有各的脾气,有的直接让进程起不来,有的却只是卡住不报错,非常容易误判方向。这篇文章我会把三类问题的完整排查链路拆开讲,包括现象特征、判断顺序、修复手法和验证方式,希望能帮你减少几次无效的试错。

先说清楚一个事实:vLLM-Ascend 和原版 vLLM 在启动路径上有很多相似之处,但底层依赖完全不同。GPU 版本遇到加载失败,查 CUDA、cuDNN 基本能定位;昇腾版本则需要把 CANN 运行库、torch_npu、HCCL 集合通信、图编译缓存全部纳入考虑范围。这也是为什么同样的报错,在 GPU 环境里不存在,在昇腾环境里却反复出现。下面我按照实际排查中遇到的频率顺序,把这三个问题逐个拆解。

1. 从调用链看启动:一条启动命令背后的完整时间线

1.1 昇腾上启动 vLLM 时,代码究竟经历了什么

要排查启动失败,先得知道正常的启动链路是什么样子。我习惯把 vLLM-Ascend 的启动过程理解成四段接力:

第一段是 Python 进程导入阶段。你执行vllm serve或者写一段离线推理脚本时,Python 解释器会先加载 vllm 包,然后触发 vLLM-Ascend 的插件注册逻辑。这一步会导入torch_npu,请求 CANN 的 ACL runtime 初始化,同时把昇腾相关的加速组件注册进 vLLM 的算子分发机制里。libatb.so就是在这一阶段被动态加载的,它属于昇腾 Transformer Boost 相关的底层加速库,如果你的环境缺少这个库或者路径没配置好,进程会在非常早期就直接退出。

第二段是资源检查与配置解析。vLLM 会读取模型路径、tensor parallel size、设备数量、KV Cache 分配策略等参数,并通过torch_npu查询当前可用的 NPU 设备。这个阶段如果卡住,通常和et_env.sh中设置的 HCCL 相关变量有关,因为多卡场景下的资源配置需要依赖集群通信的初始化结果。

第三段是 EngineCore 子进程拉起。vLLM 在昇腾上延续了 GPU 版本的架构,会用一个独立的引擎子进程来管理模型加载、图编译和推理执行。这个子进程内部会再次初始化 CANN、分配显存、编译计算图、建立 KV Cache。EngineCore相关的故障大量集中在这一段,因为子进程的初始化失败不会直接显示在主进程的 Python traceback 里,而是被封装成一段简短的状态信息。

第四段是服务就绪。当 EngineCore 子进程完成模型权重加载和计算图编译后,会通过 IPC 告诉主进程准备完毕,主进程才开始对外提供服务。如果前面的任何一个环节出现异常,服务就起不来。

1.2 三个故障点在整条链路中的位置

把时间线拉出来之后,你会发现三个故障点其实分布在不同阶段,这意味着排查时先看"卡在哪个阶段",比直接翻报错文本更有效。

libatb.so加载失败几乎只出现在第一阶段,是纯环境问题。进程还没到资源检查阶段就会抛ImportError,报错信息很直白,包含动态链接库的文件名。这类问题解决起来通常比较快,属于"找到库、配上路径、再启动"就能搞定的类型。

et_env.sh相关的问题横跨第一阶段和第二阶段的交界处。因为它本质上是初始化脚本,负责给当前 shell 注入 CANN、HCCL、芯片拓扑相关的环境变量。脚本执行时机不对、变量被覆盖、或者脚本里引用的路径不存在,都会让后续的 CANN 初始化和 HCCL 通信初始化拿到错误配置。典型表现是:进程没有立刻退出,而是卡在某个初始化提示上,或者反复重试直到超时。

EngineCore初始化异常则集中在第三阶段,也是最容易让人迷惑的。因为主进程看起来是"活着"的,甚至还能打印一些启动日志,但 EngineCore 子进程内部已经失败了。主进程收到的只是一个抽象的错误摘要,真正的根因藏在子进程自己的日志里。

理解了这条链路,后续的排查就有了坐标系。下面先说如何快速判型,再逐个深挖。

2. 故障判型:三类问题在现象上如何快速区分

2.1 三类故障的现象对照表

实际处理问题时,很多人第一反应是去翻日志的最后几行,看到 traceback 就开始搜错误码。但在昇腾环境里,这个习惯会浪费大量时间。我建议先观察进程状态和报错出现的时机,判断故障属于哪一类。

这里给一张我在排查时经常用来对照的表:

故障类型典型现象出现时机进程状态日志落点
libatb.so 加载失败ImportError: libatb.so: cannot open shared object filePython 导入阶段,启动后几秒内直接退出,无重试stderr 或 shell 终端
EngineCore 初始化失败主进程提示EngineCore process ... failedengine_core failed to start,随后主进程退出/退出码非 0启动日志打印到"初始化引擎"之后子进程退出,主进程可能尝试重试EngineCore 专属日志文件
et_env.sh 相关问题启动日志正常打印环境信息,但停在"Initializing HCCL"或"waiting for device"处,不再继续启动初期或集群初始化阶段进程存活但无进展,或循环重试主进程标准输出、HCCL 日志

这张表的重点在于:libatb.so是快速失败,EngineCore是延迟失败,et_env.sh是"假性成功"。这三类问题的修复路径完全不同,如果判型错误,后面做的所有操作可能都是无效的。

2.2 判型的三步动作

拿到一个启动失败现场,我建议按下面的顺序做三步判型:

第一步,看退出方式。进程是直接退出了,还是卡住不动,还是反复重启?直接退出且报错信息很短,优先怀疑动态库;卡住不动,优先怀疑 HCCL 或设备初始化;反复重启,优先怀疑 EngineCore 子进程。

第二步,找到关键日志文件。vLLM-Ascend 在启动时会创建独立的日志目录,EngineCore 子进程的日志通常是单独存放的。很多人只在终端里看主进程输出,忽略了子进程日志,这会导致 EngineCore 的问题被当成灵异事件。

第三步,执行一次最小化环境检查。跑一个只导入 torch_npu 的 Python 脚本,验证 CANN 和 NPU 驱动是否正常。如果这一步都过不去,那不管 vllm serve 后面跟什么参数,都不可能成功。

3. libatb.so 加载失败的排查:两种常见形态与一套判断顺序

3.1 形态 A:文件缺失或被安装裁剪

libatb.so加载失败最常见的报错是:

ImportError: libatb.so: cannot open shared object file: No such file or directory

看到这个报错,第一反应不是去重装 vLLM-Ascend,而是先确认系统里到底有没有这个动态库。在昇腾的 CANN 安装目录下,libatb.so通常位于ascend-toolkit/latest/.../lib64或类似路径下。执行:

find /usr/local/Ascend -name "libatb.so" 2>/dev/null

如果找不到文件,说明 CANN 工具链安装不完整,或者安装过程中某些加速库组件没有装上。昇腾 CANN 的安装包通常包含多个组件,部分部署方式为了精简体积会裁剪掉一些加速组件,libatb.so所在的那部分一旦缺失,vLLM-Ascend 在导入阶段就无法完成算子加速的注册。

这种形态的修复路径很明确:重新安装或补齐 CANN 对应组件。具体来说,确认你安装的是完整版 CANN Toolkit 而不是精简版,或者检查 Docker 镜像里是否遗漏了运行库层。

3.2 形态 B:库存在但版本错配或符号冲突

比文件缺失更隐蔽的是这种情况:find能找到/usr/local/Ascend/ascend-toolkit/latest/.../libatb.so,但启动时依然报错,只是错误信息变成了undefined symbolversion GLIBCXX not found

这类报错说明动态库文件本身存在,但加载顺序或版本不对。昇腾环境里经常出现同一个动态库被安装多份的情况,比如 CANN 的多个版本共存、conda 环境和系统环境的路径互相污染,导致 Python 进程通过LD_LIBRARY_PATH找到的libatb.so和 vLLM-Ascend 期望的版本不一致。

还有一种情况是编译器版本和运行环境不匹配。libatb.so是 C++ 编译的产物,如果编译时用的 GCC 版本比运行环境的 libstdc++ 高,加载时会报 GLIBCXX 版本错误。此时用下面的命令检查:

ldd $(python -c "import vllm_ascend, os; print(os.path.dirname(vllm_ascend.__file__))")/lib/libatb.so 2>/dev/null | grep "not found"

定位到哪个依赖缺失,再针对性地调整环境变量。

3.3 一套固定的判断顺序

对于动态库加载问题,我总结了一套固定的判断顺序,能覆盖 90% 的 libatb.so 报错:

  1. 确认文件存在性:find /usr/local/Ascend -name "libatb.so"
  2. 确认LD_LIBRARY_PATH是否覆盖该目录:echo $LD_LIBRARY_PATH | tr ':' '\n' | grep Ascend
  3. 确认当前 Python 解释器实际加载的是哪个路径:python -c "import ctypes; ctypes.CDLL('libatb.so')",如果成功,再打印ldconfig -p | grep libatb
  4. 检查依赖完整性:ldd找出缺失的依赖
  5. 检查版本符号:strings /path/to/libatb.so | grep GLIBCXX对比当前系统的strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX

提示:昇腾环境特别容易出现"在 shell 里 source 过 set_env.sh 能跑,一放到 systemd 或后台脚本里就报 libatb.so 缺失"的情况。根本原因是 systemd 服务没有继承登录 shell 的环境变量,这时最好在服务配置里显式加载 CANN 的 set_env.sh 或 et_env.sh,而不是依赖 shell 的 auto source。

修复之后,验证方法很简单:再执行一次最小导入测试。

python -c "import torch, torch_npu; import vllm_ascend; print('import ok')"

这条命令能过,libatb.so 这个拦路虎就算解决了。

4. EngineCore 初始化异常:问题往往藏在日志中间段

4.1 先给 EngineCore 一个定位

EngineCore 是 vLLM 用来隔离模型运行时的一个独立进程。vLLM 在 GPU 版本里用独立的 worker 进程做模型管理,昇腾版本沿用了这套架构,但底层换成了 CANN 的接口。EngineCore 子进程负责模型权重加载、图编译、KV Cache 分配、推理循环执行。

这个设计本身是为了稳定,但它给排查带来了一个麻烦:子进程报错时,主进程拿到的只是一个封装过的失败标记,真正的 traceback 在子进程的日志里。如果你只盯着终端看主进程的报错,很可能看到的是:

[EngineCore] EngineCore process 123456 exited with code 1

然后就没有然后了。很多人卡在这里不知道该查什么。

4.2 日志分段读法

EngineCore 的日志会被单独写到日志目录下,常见命名是engine_core_*.log或直接写在 vLLM 的日志目录里。拿到这份日志,我建议分段读,而不是直接翻末尾。

日志的第一段是环境初始化,会打印 CANN 版本、NPU 设备数量、当前进程绑定的设备编号。这一段如果报错,说明 torch_npu 或 CANN runtime 没初始化成功,属于环境问题。

日志的中间段是模型加载和图编译,也是我最关注的部分。模型权重读取、算子编译、图优化都在这一段完成。嵌入式平台的日志打印习惯通常是每一大步打印一行状态,如果连续多行正常输出之后突然出现异常,配合时间戳能看到它到底卡在哪个算子或哪层图编译。

日志的最后一段是"启动成功"或"启动失败"的汇总。这里有个坑:EngineCore 失败时,最后一段可能包含一个很长的 C++ 栈回溯,但根因并不在栈顶,而在中间段的某个警告或错误里。我遇到过不少案例,最后的 C++ 栈都指向同一个算子,但实际原因是显存不足导致的分配失败,真正有用的信息在中间段那句malloc failed上面。

4.3 从初始化日志反推硬件与资源状态

EngineCore 初始化失败的根因,很大比例集中在硬件状态和资源不足上。启动另一个终端,执行:

npu-smi info

看 NPU 设备是否处于正常状态。如果设备状态是 Abnormal 或离线,任何初始化都会失败。还有一种常见情况:上一轮推理任务结束之后,进程没有完全退出,显存被残留进程占着。EngineCore 分配 KV Cache 时发现自己想用的显存不足,直接失败。

执行:

ps -ef | grep -E "vllm|engine_core|python" | grep -v grep

检查是否有残留进程。有的话,根据实际需要 kill 掉再重启服务。这个操作在 GPU 环境里对应nvidia-smi查看显存占用,昇腾环境就是用npu-smi info配合进程检查。

4.4 另一类隐蔽根因:图编译缓存损坏

昇腾的图编译过程会产生缓存文件,通常存放在用户目录的.cache下。这些缓存能在模型第二次启动时大幅缩短编译时间,但它们也有隐患:CANN 版本升级、模型文件改动、缓存文件写入不完整,都可能导致缓存与当前环境不匹配。启动时如果加载到损坏的缓存,EngineCore 可能在图编译阶段崩溃。

判断方法:记录报错信息里是否提到cacheaoegraph相关字眼。如果怀疑缓存问题,直接把昇腾图编译缓存目录备份后清空,再重新启动。注意这里不能像 GPU 环境那样只清~/.cache/torch,昇腾的缓存目录还要关注和 CANN 相关的调优目录。

提示:清缓存是成本最低的验证手段,但优先级不要放在最前面。正确顺序是先确认硬件状态和显存占用,再确认依赖库完整,最后才清缓存。因为缓存问题往往具有偶发性,而硬件问题和资源问题具有必然性,先用必然性排查排除掉大概率因素,能省很多时间。

5. et_env.sh 引发的“假启动”:环境脚本出问题不会直接报错

5.1 最棘手的一类:进程活着,但什么也没发生

前两类故障都有明确的报错,et_env.sh 相关的问题则是另一副面孔。一个典型的场景是:你在终端执行了启动命令,日志正常打印出 CANN 版本、模型路径、设备信息,然后停在某个地方,CPU 占用却在涨,好像在做大量计算。等了很多分钟,服务还是没起来。

这种"假启动"比直接报错更折磨人。因为从主进程的视角看,它还在等待 EngineCore 返回就绪信号;从 EngineCore 的日志看,它可能卡在 HCCL 初始化或者设备拓扑探测上。而这一切的起点,往往是 et_env.sh 这类环境脚本没有把该设置的环境变量设置对。

5.2 et_env.sh 到底做了什么

在昇腾的部署环境里,et_env.sh这类初始化脚本很常见,它做的事情大致包括:

  • 设置 CANN 工具链的安装根目录,比如ASCEND_HOMEASCEND_TOOLKIT_HOME
  • 把 CANN 的lib64bintools目录注入LD_LIBRARY_PATHPATH
  • 设置 HCCL 集合通信相关的环境变量,比如单机多卡或跨机通信时的网卡、拓扑配置
  • 设置 NPU 设备可见性相关的变量

这些变量看似零散,但它们共同决定了一个推理进程能不能正确找到设备、能不能建立多卡通信、能不能编译执行算子。别小看一个ASCEND_RT_VISIBLE_DEVICES没设置或设置错误的问题,它可能导致 vLLM 认为自己只有 0 张卡可用,或者明明插了 8 张卡,HCCL 却只探测到 1 张。

5.3 最容易出问题的三个细节

我在踩过不少坑之后,总结了 et_env.sh 相关问题的三个高发点。

第一个是 source 的时机。et_env.sh必须在启动 vLLM-Ascend 之前 source,而且必须在同一个 shell 会话里 source。如果你在 Dockerfile 里 source 了一份,在容器启动时又覆盖了环境变量,或者用 systemd 启动服务时没有 source,那一切等于白配。我见过有人把 source 写进.bashrc,然后通过非登录 shell 执行 vllm serve,变量完全没生效,排查了很久才发现是这个原因。

第二个是环境变量覆盖。多个初始化脚本一起执行时,先后顺序会互相影响。比如你同时 source 了 CANN 的 set_env.sh 和某个自定义的 et_env.sh,后者如果重新定义了LD_LIBRARY_PATH,用的是相对路径而不是追加,就会把前面脚本已经加进去的 CANN 路径冲掉。排查看不到报错,因为路径还是存在的,只是指向不完整的路径集。

检查环境变量是否完整的命令:

env | grep -E "ASCEND|HCCL|NPU|LD_LIBRARY_PATH" | sort

对比正常环境和故障环境的输出差异,往往一眼就能看出问题。

第三个是 HCCL 初始化超时。多卡环境下,HCCL 需要在多个设备之间建立通信域。如果et_env.sh里针对网络拓扑或通信算法设置的参数不当,HCCL 初始化可能一直等待,直到超时。这个阶段的外部表现就是进程卡住很久之后,才报一个华为集合通信库相关的时间超时错误。排查时可以把 HCCL 的日志级别调高,观察它停在哪一步,再回去检查脚本中的网卡和拓扑配置。

修复这类问题通常不需要改代码,重点检查脚本里的变量值、source 的路径是否真实存在、多个脚本的执行顺序是否正确。改完之后,单纯重启 vllm 是不够的,因为当前 shell 里的环境变量还是旧的。正确做法是开一个全新的终端,重新 source,再启动。

6. 给一线部署者的现场排查工具箱

6.1 快速采集现场信息的一组命令

遇到启动故障,第一时间收集完整的现场信息比急着修复更重要。因为故障现场一旦被改变(比如重启、清理缓存),很多线索就丢了。我建议在动手之前,先执行下面这一组命令,把现场记录下来:

npu-smi info > npu_smi.log 2>&1 env | grep -E "ASCEND|HCCL|NPU|LD_LIBRARY_PATH|PATH" > env.log 2>&1 find /usr/local/Ascend -maxdepth 4 -name "set_env.sh" -o -name "et_env.sh" > env_scripts.log 2>&1 python -c "import torch, torch_npu; print(torch.__version__, torch_npu.__version__); print(torch_npu.npu.is_available())" > python_env.log 2>&1

这四份 log 分别对应硬件状态、环境变量、初始化脚本位置、Python 层依赖状态。有了这四份文件,无论是自己继续排查,还是把问题带到社区求助,别人都能快速定位,不用在群里反复追问"你 npu-smi 是什么结果"。

6.2 问题复现的最小化实验模板

复现问题不一定要完整启动 vllm serve,因为那样太慢,而且会被很多无关因素干扰。我推荐做最小化验证,把问题尽量收敛到某一层:

第一步,验证 NPU 驱动和 CANN 基础层:

python -c "import torch, torch_npu; print(torch_npu.npu.is_available())"

这一步能过,说明 CANN 和驱动层没有问题。

第二步,验证 vLLM-Ascend 导入层:

python -c "import vllm_ascend; print('vllm_ascend loaded')"

这一步能过,说明 libatb.so 相关的动态库加载没有问题。

第三步,再执行完整的 vllm serve 启动。如果前两步都能过而第三步还是失败,问题基本就集中在 EngineCore 和 HCCL 层,这时候带着第三部分的核对思路去看,方向的把握会大很多。

把这三步的输入输出都保留下来,连同前面的四份 log 一起归档。下次不管是自己排查还是求助他人,你都能省掉大量来回确认的时间。我自己现在遇到昇腾部署问题,第一件事永远是先跑这套最小化实验,因为它能快速把故障域从"整个 vLLM 启动链路"缩小到具体某一段。

排查环境问题,顺序感是最重要的。先判断故障发生在启动链路的哪一段,再决定用什么工具和日志,最后才动手修。这个习惯能让你在遇到没见过的报错时不慌,也能让你在向别人描述问题时三句话说清楚。希望这篇文章的排查思路能帮你少走点弯路,有问题的话也欢迎一起交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询