一、项目背景

在人工智能推理项目中,为了方便算法服务部署,通常需要将 Python 应用、模型文件、运行时依赖及相关动态库打包到 Docker 镜像中。

传统部署方式往往要求目标服务器预先安装完整的 AI 计算软件开发环境,存在以下问题:

  1. 部署步骤较多,需要手动配置运行环境。

  2. 不同服务器之间的软件环境容易出现差异。

  3. 动态库依赖复杂,可能出现加载失败。

  4. 应用迁移到其他服务器时,需要重复配置运行环境。

为简化部署流程,本文采用以下方案:

将应用程序、模型文件、Python 依赖和必要的计算运行时库打包进 Docker 镜像,目标机只提供兼容的硬件驱动和必要的设备访问能力。

二、整体部署架构

整个系统分为宿主机和容器两个部分。

1. 宿主机负责提供

  • Docker 运行环境

  • 与硬件和操作系统兼容的设备驱动

  • 必要的硬件设备节点

  • 容器访问设备所需的权限

2. Docker 镜像负责提供

  • Linux 基础环境

  • Python 运行环境

  • 必要的计算运行时库

  • 推理框架及其依赖

  • Python 业务代码

  • 离线模型文件

  • 容器启动脚本

这种部署方式可以避免目标机重复安装完整的计算软件开发套件,但不能简单地把硬件驱动也当作普通 Python 依赖打包。宿主机仍然需要提供与硬件兼容的驱动及设备访问接口。

三、Docker 镜像构建

1. 整理应用文件

建议将应用代码、模型文件、依赖包和启动脚本统一组织在构建上下文中。

示例目录如下:

application/
├── Dockerfile
├── start.sh
├── app.py
├── requirements.txt
├── src/
├── models/
│   └── model.om
└── runtime-libs/

以上均为虚构的通用目录名称,不代表实际项目路径。

其中:

  • Dockerfile:定义镜像构建流程。

  • start.sh:负责初始化运行环境并启动应用。

  • models/:保存离线模型。

  • runtime-libs/:保存需要额外复制的运行时依赖。

实际目录结构应根据应用依赖调整。

2. 打包计算运行时库

在构建镜像时,将经过验证的运行时环境安装或复制到容器内部。

示意结构如下:

/usr/local/Compute/
├── runtime/
│   └── lib64/
├── framework/
│   └── lib64/
└── architecture-specific/
    ├── lib64/
    └── devlib/

这里使用通用路径代替实际软件安装目录。

需要注意:

  • 运行时库必须与目标硬件架构兼容。

  • 运行时库与宿主机驱动之间必须兼容。

  • 应区分用于开发、编译的桩库与用于实际执行的运行时库。

  • 不能仅凭文件名或文件大小判断动态库是否正确。

3. 安装 Python 依赖

如果项目使用专门的推理运行时模块,需要在镜像构建阶段安装对应依赖。

示例:

COPY wheels/ /opt/wheels/

RUN python3 -m pip install \
    /opt/wheels/inference_runtime-*.whl

以上包名为通用示例。实际使用时,需要根据 Python 版本、处理器架构和运行时兼容要求选择正确的软件包。

Python 模块可以成功导入,只能说明模块导入阶段没有报错,并不意味着后续的设备初始化和模型推理一定成功。

四、配置动态库搜索路径

Linux 程序通常通过动态链接器查找依赖的共享库。当多个目录中存在同名动态库时,实际加载的文件可能与预期不同。

可以在 Dockerfile 中设置运行时环境变量:

ENV COMPUTE_HOME=/usr/local/Compute

ENV LD_LIBRARY_PATH=/usr/local/Compute/architecture-specific/lib64:/usr/local/Compute/runtime/lib64:/usr/local/Compute/framework/lib64

上述路径为脱敏示例,需要根据实际软件包布局调整。

配置时应注意:

  1. 优先保证真正用于执行的运行时库能够被正确加载。

  2. 避免开发用桩库优先于实际运行时库。

  3. 检查动态库之间的依赖是否完整。

  4. 确认启动脚本没有再次覆盖或错误调整环境变量。

动态库的实际加载行为还可能受到 RPATH、RUNPATH 等因素影响,因此不能只通过检查环境变量来判断最终加载结果。

五、启动脚本配置

为了减少目标机启动时需要传入的参数,可以将应用环境配置固化到镜像内部。

通用启动脚本如下:

#!/bin/bash
set -e

echo "========== Container Boot =========="

# 检查设备节点
ls -l /dev/davinci* || true

# 检查运行时模块
python3 -c \
    "import inference_runtime; print('Runtime import OK')"

echo "Starting application..."

cd /app
exec python3 app.py

这里的模块名和应用目录均为通用示例。

在 Dockerfile 中配置:

COPY start.sh /app/start.sh

RUN chmod +x /app/start.sh

WORKDIR /app

CMD ["/app/start.sh"]

使用 exec 启动应用,可以使应用进程更直接地接收容器终止信号。

如果启动脚本依赖额外的环境变量,应提前在 Dockerfile 中定义,避免每次部署时重复传入。

六、问题排查:stub library cannot be used for execution

1. 问题现象

在容器启动过程中,可能出现以下错误:

Runtime import OK
Starting application...

[ERROR]: stub library cannot be used for execution,
please check your environment variables and compilation options
to make sure you use the correct ACL library.

[ERROR] acl init failed
[ERROR] device manager init devices failed.

RuntimeError: Undefined error code

这类错误说明程序在运行时初始化阶段遇到了问题。

即使 Python 推理模块导入成功,也不能据此认定整个硬件推理环境正常。

2. 什么是 stub library?

Stub library 可以理解为用于特定开发、编译或链接场景的桩库。

它不一定具备实际执行所需的运行时能力。如果程序在运行时加载了不适用于实际执行的桩库,就可能出现初始化失败。

遇到此类问题时,应重点检查:

  • 实际加载的是哪个动态库文件。

  • 动态库搜索路径是否正确。

  • 是否存在多个同名动态库。

  • 动态库依赖是否完整。

  • 宿主机驱动和硬件设备是否正常。

  • 容器是否具备访问设备的权限。

不要仅仅通过重新安装 Python 包来解决所有初始化问题。

3. 查找同名动态库

可以在容器内部执行:

find /usr/local/Compute \
    \( -name 'libascendcl.so' -o -name 'libacl*.so' \) \
    -print

这里使用了示意安装目录。实际排查时,应替换为容器内部真正的运行时目录。

检查具体文件:

ls -l /path/to/runtime-library.so

检查动态库依赖:

ldd /path/to/runtime-library.so

如果输出中出现:

not found

说明存在无法解析的动态库依赖,需要继续排查。

还可以使用:

LD_DEBUG=libs python3 -c 'import inference_runtime' 2>&1

查看动态链接器的库查找过程。

实际运行时库是否正确,最终还需要结合 ACL 初始化和设备访问结果进行验证。

七、排查动态库与设备访问问题

1. 动态库搜索路径

检查 LD_LIBRARY_PATH:

echo "$LD_LIBRARY_PATH"

确认运行时目录位于合理的位置,并检查启动脚本是否重复添加了其他目录。

如果多个目录存在同名库,应进一步确认程序实际加载的文件,而不是简单地根据目录名称判断。

2. 检查设备节点

在宿主机执行:

ls -l /dev/davinci*

检查驱动状态和设备节点是否存在。

如果容器无法访问设备,需要检查:

  • Docker 是否映射了必要的设备节点。

  • 宿主机驱动是否正常工作。

  • 设备节点权限是否满足运行要求。

  • 容器的设备访问配置是否正确。

需要强调的是,动态库问题和设备映射问题是两个不同的排查方向。即使其中一个正常,另一个仍然可能导致初始化失败。

3. 区分驱动与计算运行时

这是本次部署方案中的关键设计原则。

计算运行时库可以随应用一起打包进镜像,但硬件驱动需要由宿主机提供。

因此:

  • 不需要为了部署应用而在目标机重复安装完整的软件开发套件。

  • 需要确保目标机存在兼容的硬件驱动。

  • 需要让容器访问必要的设备节点。

  • 不应将宿主机整个软件安装目录覆盖挂载到容器中,否则可能遮蔽镜像内部已经准备好的运行时文件。

八、目标机部署方式

在目标机上,只需要传入部署所必需的参数。

下面是脱敏后的命令模板:

docker run -d \
  --name inference-service \
  --restart=unless-stopped \
  --device=/dev/davinci_manager \
  --device=/dev/davinci0 \
  --device=/dev/devmm_svm \
  --device=/dev/hisi_hdc \
  -v /usr/local/Ascend/driver:/usr/local/Ascend/driver:ro \
  -p 8080:8080 \
  <REGISTRY>/<IMAGE>:<TAG>

其中:

参数作用
--restart=unless-stopped配置容器自动重启策略
--device映射必要的硬件设备节点
-v将宿主机驱动目录只读挂载到容器
-p映射应用服务端口
<REGISTRY>/<IMAGE>:<TAG>需要替换为实际镜像引用

这里仅保留了通用的部署结构,镜像仓库、名称和标签均未使用实际项目值。。

九、验证容器是否正常运行

1. 检查容器状态

docker ps --filter name=inference-service

如果容器状态持续为 Up,说明容器进程正在运行。

2. 检查启动日志

docker logs --tail 50 inference-service

重点关注是否出现以下成功信息:

Runtime import OK
[INFO] acl init success
[INFO] open device success
[INFO] create new context
[INFO] load model success
[INFO] create model description success

具体日志文本可能因运行时和应用实现而有所不同。

如果出现动态库加载失败、设备打开失败或模型加载失败,应继续排查,不能只根据容器状态判断应用已经完全正常。

3. 检查重启次数

docker inspect \
  -f '状态={{.State.Status}} 重启次数={{.RestartCount}}' \
  inference-service

例如:

状态=running 重启次数=0

说明容器当前处于运行状态,Docker 记录的重启次数为零。

4. 如何理解 HTTP 404

如果访问服务根路径:

curl -i http://127.0.0.1:8080/

返回:

HTTP/1.0 404 NOT FOUND

这不一定代表容器启动失败。

如果应用没有定义根路径对应的路由,HTTP 服务返回 404 是正常的路由行为。

对于只需要验证容器启动、设备初始化和模型加载的场景,可以结合容器状态及启动日志判断。若要验证完整业务功能,则需要进一步测试实际业务接口。

十、最终总结

本次排查解决的核心问题,是容器中 Python 推理模块能够导入,但 ACL 初始化阶段出现 stub library 相关错误。

排查时应优先关注:

  1. 动态库搜索路径是否正确。

  2. 实际加载的是否为适用于执行的运行时库。

  3. 运行时依赖是否完整。

  4. 宿主机驱动和硬件设备是否正常。

  5. Docker 是否正确映射设备节点并提供必要权限。

最终,通过调整容器内部运行时库配置,并确保宿主机驱动和 NPU 设备正确映射,应用成功完成了 ACL 初始化、设备打开和离线模型加载。

这一方案的主要优势是:

  • 减少目标机的软件环境安装步骤。

  • 提高不同部署环境之间的一致性。

  • 将应用依赖集中管理在镜像中。

  • 简化后续部署和维护流程。

需要注意的是,打包计算运行时并不意味着可以忽略宿主机驱动。运行时库、驱动、硬件架构和模型之间仍然必须满足兼容要求。

总结:通过将应用依赖固化到 Docker 镜像,并在运行时正确配置宿主机驱动和硬件设备映射,可以实现更方便、可复现的 NPU 推理服务部署。

本文中的目录、镜像名称、标签、软件版本和内部环境标识均已泛化处理。实际使用时请以对应硬件平台及软件官方文档为准。

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐