上个月我把 CubeStudio 项目的昇腾 NPU 集群接进了 Kubernetes,整套流程走下来:驱动、固件、CANN、Ascend Docker Runtime、device-plugin、监控,一个不少。这套环境现在已经在跑训练和推理任务,调度、扩缩容、监控告警都能正常用。
网上关于昇腾 NPU 接入 K8s 的资料比较分散,要么只有驱动安装,要么只讲 device-plugin 单独一块,真正从裸机到资源调度的全流程操作分享很少。这篇就按我实际部署的顺序整理出来,适合手里有昇腾硬件、想把 NPU 纳入 Kubernetes 统一调度的同学参考。文中的路径和包名以你下载到的实际版本为准,但整体思路和排查手段是通用的。
1. 昇腾 NPU 接入 K8s 的整体方案拆解
1.1 为什么要费力把 NPU 接进 K8s
先说说项目背景。CubeStudio 这边同时有多个团队要跑模型训练和推理服务,如果每台机器都是手工分配 NPU,很容易出现某张卡闲着、某个任务排队等卡的情况。把 NPU 纳入 Kubernetes 之后,训练任务、推理服务就可以像申请 CPU 和内存一样申请 NPU 资源,K8s 负责调度和排队,运维只需要维护一个集群。
昇腾 NPU 和 NVIDIA GPU 在接入 K8s 的思路上是类似的,都是靠三件事:自定义资源让 K8s 认识设备、Device Plugin 把设备数量和健康状态上报给 kubelet、Container Runtime 在启动容器时把设备注入容器。但华为这边的软件栈名称不同,版本配套也更复杂,驱动、CANN、容器运行时、device-plugin 每个环节都有对不上的可能。
1.2 昇腾 NPU 软件栈的分层逻辑
把昇腾 NPU 从硬件到 K8s 调度,大致可以分成五层。理清楚这个分层,后面排错就知道该看哪一层了。
- 第一层:硬件层。昇腾 310 是推理卡,昇腾 910 是训练卡,其他还有 310P、910B 等型号,你得先确认手上卡的类型,因为对应的驱动、固件、CANN 版本可能不一样。
- 第二层:驱动与固件层。操作系统通过昇腾驱动认识设备,固件则负责芯片底层运行逻辑,对应包一般叫 Ascend HDK。
- 第三层:用户态库层,也就是 CANN。CANN 是昇腾的计算架构,包含 runtime、算子库、图引擎、编译器,类比 CUDA 在 NVIDIA 体系里的位置。
- 第四层:容器运行时层。Ascend Docker Runtime 是 OCI Runtime,它拿到 ASCEND_VISIBLE_DEVICES 这个环境变量后,把对应编号的 NPU 设备节点和驱动目录挂载进容器。
- 第五层:K8s 调度层。device-plugin 和 kubelet 通信,上报节点上有几张 NPU、资源名是什么,K8s 据此调度 Pod 到有资源的节点。
1.3 版本配套是第一个大坑
昇腾生态里最让人头疼的不是安装步骤多,而是版本配套。驱动、固件、CANN、容器镜像里的 torch_npu、device-plugin、Ascend Docker Runtime,甚至宿主机内核,都有对应的兼容列表。
我这次环境是 Atlas 800 推理服务器、昇腾 310P 卡、Ubuntu 20.04 内核 5.4,配套的软件组合是:Ascend HDK 24.1.rc1 版本对应的驱动和固件、CANN 8.0.RC1、Ascend Docker Runtime 24.1、社区版 device-plugin 1.0。这套组合实测下来能稳定跑。
安装前一定要去昇腾社区把版本配套表下载下来先核对一遍,别相信"最新的一定最好"。昇腾软件迭代速度很快,有些新版本对旧硬件或旧内核要求更高,盲目升版本反而容易翻车。
1.4 本次部署的环境清单
| 组件 | 版本/说明 |
|---|---|
| 服务器 | Atlas 800 (型号 3000) |
| NPU | 昇腾 310P(推理卡) |
| 操作系统 | Ubuntu 20.04.6 LTS |
| 内核 | 5.4.0-150-generic |
| 驱动/固件 | Ascend HDK 24.1.rc1 |
| CANN | 8.0.RC1 |
| Ascend Docker Runtime | 24.1 |
| device-plugin | 社区版 1.0 |
| Kubernetes | v1.28.2 |
| Docker | 24.0.5 |
2. 宿主机环境准备与驱动、固件安装实操
2.1 安装前的系统检查
驱动安装失败的情况里,十有八九是环境缺东西。装之前先花五分钟把这些检查一遍。
先确认硬件能被系统识别:
lspci | grep -i ascend如果能输出类似Huawei Technologies Co., Ltd. Device 6326这样的信息,说明 PCIe 层面的识别是正常的。如果这里什么都看不到,先检查卡是不是没插好、服务器是不是开了 PCIe slot 白名单。
接着确认内核开发包和编译工具:
uname -r apt install -y linux-headers-$(uname -r) gcc make驱动安装时需要对内核模块做编译,如果缺少 linux-headers,安装过程会在编译阶段直接中断。另外建议提前关闭 Secure Boot,或者给驱动模块做签名,否则加载内核模块时会被拦截。我这边直接在 BIOS 里关了 Secure Boot,省事。
还有一个比较容易忽略的是系统时间。NPU 设备、驱动和固件对时间比较敏感,如果时间不对,运行时握手会出各种诡异问题,先date看一眼,不对就同步一下。
2.2 驱动与固件安装步骤
昇腾的驱动和固件打包在 HDK 软件包里,命名类似Ascend-hdk-xxx_linux-aarch64.run,从昇腾社区下载对应版本。下载好之后先给执行权限:
chmod +x Ascend-hdk-xxx_linux-aarch64.run ./Ascend-hdk-xxx_linux-aarch64.run --full--full参数会同时安装 driver 和 firmware。安装完成后默认路径是/usr/local/Ascend/driver,相关二进制和库都在这个目录下。
安装完第一件事是验证驱动是否加载成功:
npu-smi info正常情况会输出板卡列表,包括芯片型号、温度、功耗、显存使用量。如果报错说没有设备,按顺序排查:
# 检查内核模块是否加载 lsmod | grep drv # 检查设备节点是否存在 ls -l /dev/davinci* ls -l /dev/davinci_manager ls -l /dev/hisi_hdc ls -l /dev/devmm_svm如果lsmod没输出,手动尝试加载模块后再看。如果设备节点缺失,多半是 udev 规则没生效,重启一次通常能解决。
驱动安装这一步还有个容易犯的错:驱动和固件虽然经常一起装,但它们其实是两个独立组件。固件版本和驱动版本不匹配时,npu-smi info有时也能显示卡,但一跑算子就出错,所以装完务必确认两个版本在同一兼容列表里。
2.3 驱动版本管理的注意事项
昇腾驱动升级不像普通软件那样直接覆盖安装。如果之前装过旧版本,建议先卸载干净再装新的:
/usr/local/Ascend/driver/script/uninstall.sh --full卸载完重启服务器,再装新版。我试过一次不卸载直接覆盖,结果旧版本的内核模块残留,npu-smi info显示两张卡一张在线一张离线,排查了半天。
驱动安装好之后,宿主机这一层就基本打通了。接下来装 CANN 时要注意,驱动是节点级共享的,CANN 可以装在宿主机,也可以只放在容器镜像里,两种方式各有适用场景,下面详细说。
3. CANN Toolkit 安装与环境变量配置
3.1 CANN 在整体架构里的位置
CANN 是昇腾的计算架构,包含运行时、算子库、图编译器和推理工具链。如果说驱动是让系统"看得见"NPU,那 CANN 就是让程序"用得上"NPU。
在 K8s 场景下,CANN 有宿主机和容器两种放法。如果业务镜像里已经内置了 CANN 和 torch_npu,宿主机只需要驱动和 runtime 就够了。但如果想在宿主机上跑atc模型转换工具,或者调试时直接执行msame推理工具,宿主机也需要一套 CANN。
我的建议是宿主机和容器镜像都装同版本的 CANN。宿主机装一份用于调试和模型转换,容器里装一份用于实际跑任务。两边版本不一致会在某些反序列化算子时出现奇怪的报错,统一版本能省掉很多麻烦。
3.2 CANN Toolkit 安装过程
下载Ascend-cann-toolkit_xxx_linux-aarch64.run,执行安装:
chmod +x Ascend-cann-toolkit_xxx_linux-aarch64.run ./Ascend-cann-toolkit_xxx_linux-aarch64.run --install安装完成后,工具链默认在/usr/local/Ascend/ascend-toolkit/latest目录下。
验证安装是否成功:
cat /usr/local/Ascend/ascend-toolkit/latest/version.cfg source /usr/local/Ascend/ascend-toolkit/set_env.sh再看一眼 Python 侧能不能正常 import 昇腾相关库:
python3 -c "import torch; import torch_npu; print(torch.__version__)"能正常输出版本号,说明 CANN 和 torch_npu 的 Python 绑定是通的。如果 torch_npu 导入报错,先确认 CANN 版本和 torch_npu 版本是否匹配,这个是继驱动配套之后第二个常见的版本坑。
3.3 环境变量的正确配置方式
CANN 装好后需要设置环境变量。手动 source 只能在当前 shell 生效,K8s 场景下我更建议把环境变量写进一个独立脚本,然后在/etc/profile.d/下加一个调用:
cat > /etc/profile.d/ascend.sh << 'EOF' source /usr/local/Ascend/ascend-toolkit/set_env.sh export ASCEND_AUTOLOG_DIR=/var/log/npu EOF chmod +x /etc/profile.d/ascend.sh这样每次登录和容器启动时都能自动加载。注意不要把所有变量一股脑写进.bashrc,因为容器里的 shell 可能不读.bashrc,还是走全局 profile 文件比较稳。
ASCEND_AUTOLOG_DIR这个变量建议提前设好,否则运行算子报错时日志散落各处,排查问题非常痛苦。日志统一之后,算子报错、设备故障都能在一个目录下找到现场。
4. Ascend Docker Runtime 配置与容器内验证
4.1 为什么 Docker 不能直接映射 NPU
Docker 默认只认识 CPU、内存和普通的 PCIe 设备。对 NPU 这种需要挂载多个设备节点、注入用户态库、配置环境变量的加速设备,原生 Docker 没办法自动完成。
手动方式也能跑,运行容器时挂--device参数,把/dev/davinci0、/dev/davinci_manager、/dev/hisi_hdc、/dev/devmm_svm都带上,再把/usr/local/Ascend/driver挂进容器,命令行参数会非常长,而且没办法配合 K8s 做自动化调度。Ascend Docker Runtime 就是来解决这个问题的:它是一个 OCI Runtime,在容器启动时根据环境变量从节点上挑选指定编号的 NPU,自动挂载正确的设备节点和驱动目录。
4.2 配置 Docker daemon 接入 ascend runtime
从昇腾社区或 GitHub 下载ascend-docker-runtime包,解压后放到固定目录:
tar -xzf ascend-docker-runtime_xxx.tar.gz mv ascend-docker-runtime /usr/local/Ascend/Ascend-Docker-Runtime然后在/etc/docker/daemon.json中注册 runtime:
{ "runtimes": { "ascend": { "path": "/usr/local/Ascend/Ascend-Docker-Runtime/ascend-docker-runtime", "runtimeArgs": [] } } }重启 Docker 让配置生效:
systemctl restart docker验证 runtime 是否注册成功:
docker info | grep -A 5 runtimes看到 ascend 出现在 runtime 列表里就说明注册成功了。
4.3 ASCEND_VISIBLE_DEVICES 的映射规则
Ascend Docker Runtime 依赖ASCEND_VISIBLE_DEVICES环境变量决定把哪张卡放进容器。这个变量支持几种写法:
| 写法 | 含义 |
|---|---|
ASCEND_VISIBLE_DEVICES=0 | 只映射编号为 0 的卡 |
ASCEND_VISIBLE_DEVICES=0,1 | 映射编号 0 和 1 两张卡 |
ASCEND_VISIBLE_DEVICES=all | 映射节点上所有卡 |
ASCEND_VISIBLE_DEVICES=none | 不映射任何卡,相当于纯 CPU 容器 |
注意这个编号是宿主机的设备编号,含义和npu-smi info里看到的编号一致。如果没设置这个变量,Runtime 默认不会映射任何 NPU 设备,容器里自然看不到卡。
4.4 容器内 NPU 验证
用宿主机装好的驱动目录挂载进容器验证:
docker run --rm -it \ --runtime=ascend \ -e ASCEND_VISIBLE_DEVICES=0 \ -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \ -v /usr/local/dcmi:/usr/local/dcmi \ ubuntu:20.04 bash进入容器后执行:
npu-smi info如果能看到对应卡的信息,说明 runtime 工作正常。
这里有个细节:容器内要执行npu-smi info,要么容器镜像本身装了 npu-smi 工具,要么把宿主机驱动目录里的工具挂进去。我上面命令里挂载了/usr/local/Ascend/driver,就是为了让容器直接用宿主机编译好的二进制。
如果容器里看不到卡,按这个顺序排查:Docker daemon 是否重启、runtime 路径是否对、ASCEND_VISIBLE_DEVICES是否设置、设备节点是否真的存在。这四个点能覆盖九成以上的问题。
5. device-plugin 部署与 Kubernetes 资源调度
5.1 device-plugin 的工作原理
Kubernetes 提供了一种叫 Device Plugin 的机制,让第三方硬件设备通过 gRPC 和 kubelet 通信。device-plugin 启动后,会在节点的/var/lib/kubelet/device-plugins/目录下创建 socket,通过这个 socket 向 kubelet 上报设备数量和健康状态。
对昇腾 NPU 来说,需要上报的资源名通常是huawei.com/Ascend310或huawei.com/Ascend910,名字取决于卡的类型。K8s 把这种自定义资源当可计数资源处理,调度时看节点上剩余数量够不够 Pod 的请求量。
需要注意一点:device-plugin 只负责让 K8s 知道节点上有多少资源,真正把设备映射进容器的还是 Ascend Docker Runtime。所以接入完整的链路需要两个组件配合:device-plugin 上报资源,runtime 注入设备,中间通过ASCEND_VISIBLE_DEVICES这个环境变量衔接。
5.2 社区版 device-plugin 的部署
有现成的昇腾 device-plugin 实现,一般是一个 DaemonSet 的部署方式。部署前先拉取对应镜像,在 yaml 里确认几个关键挂载:
volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: driver mountPath: /usr/local/Ascend/driver - name: dcmi mountPath: /usr/local/dcmi - name: log mountPath: /var/log/mindx其中/var/lib/kubelet/device-plugins是 kubelet 和 device-plugin 通信的 socket 目录,必须挂载;/usr/local/Ascend/driver和/usr/local/dcmi是 device-plugin 读取设备信息的依赖路径。
部署命令:
kubectl apply -f ascend-device-plugin.yaml kubectl get pods -n kube-system | grep ascendPod 正常运行后,查看 node 资源:
kubectl describe node <node-name> | grep Ascend正常情况下能看到huawei.com/Ascend310: 8这样的资源数量。如果这里没有输出,说明 device-plugin 和 kubelet 的通信链路有问题,去看 device-plugin 的 Pod 日志,重点找 socket 连接失败、权限不足这两类报错。
5.3 让 Pod 里的 runtime 知道该用哪张卡
到这里还有一个容易被忽略的环节。Pod 调度到节点后,容器启动时是靠ASCEND_VISIBLE_DEVICES决定用哪张卡,但 device-plugin 本身并不负责设置这个环境变量。
实际部署中要么在业务 Pod 的 yaml 里手动写明环境变量,要么部署一个 mutating webhook 自动注入。我这边前期测试时是手动写 yaml,后面任务多了就搭了一个 webhook 自动注入,省得每个 yaml 都写一遍。
手动方式的 Pod yaml 大致长这样:
apiVersion: v1 kind: Pod metadata: name: npu-test spec: containers: - name: npu-test image: ascendhub.huawei.com/public/ascend-pytorch:latest command: ["bash", "-c", "npu-smi info && sleep 3600"] resources: limits: huawei.com/Ascend310: "1" requests: huawei.com/Ascend310: "1" env: - name: ASCEND_VISIBLE_DEVICES value: "0" volumeMounts: - name: driver mountPath: /usr/local/Ascend/driver volumes: - name: driver hostPath: path: /usr/local/Ascend/driver部署后看 Pod 是否调度到有资源的节点、容器内npu-smi info是否能看到卡。如果未设置环境变量,Pod 即使调度成功,容器内也看不到 NPU,这是新手最容易踩的问题。
5.4 资源调度测试
验证调度器是否正确工作,可以同时创建多个申请 NPU 的 Pod,观察它们是均匀分布在不同节点上还是堆在同一个节点。再测试资源耗尽的情况:在节点上把所有卡都申请完,再创建一个新的 NPU Pod,如果它一直 Pending,说明调度器的资源统计是准确的。
另外一个常被问到的问题是:Pod 里能不能只请求不设置 limits。对昇腾 NPU 这种可计数设备,K8s 要求 requests 和 limits 必须一致,否则创建时会报错。这点和 CPU、内存的处理方式不一样,写 yaml 时要注意。
6. 监控体系接入与指标采集
6.1 昇腾 NPU 的监控方案选型
K8s 集群常规监控用 Prometheus 加 Grafana,NPU 部分需要额外一个 exporter 把昇腾设备状态转成 Prometheus 指标。昇腾生态里没有像 NVIDIA DCGM 那样统一的官方 exporter,可用的方案主要有三种:
- 华为云 CCE 提供的 ascend-exporter,功能完整但部分版本和社区集群集成方式有些耦合。
- 社区开源的 ascend-npu-exporter,基于 DCMI 接口读取指标,部署灵活,我这边用的就是这种。
- 自己写脚本定时调
npu-smi info输出,临时应急可以,稳定使用不建议。
对比下来,社区版 exporter 配合 DCMI 接口是最通用的方案。DCMI 是昇腾的设备管理接口,驱动安装时已经具备,不依赖额外的软件包。
6.2 exporter 部署与指标验证
exporter 一般也是部署成 Deployment,通过环境变量指定监听端口:
apiVersion: apps/v1 kind: Deployment metadata: name: ascend-exporter namespace: monitoring spec: replicas: 1 selector: matchLabels: app: ascend-exporter template: metadata: labels: app: ascend-exporter spec: containers: - name: exporter image: ascend-exporter:latest ports: - containerPort: 9100 env: - name: ASCEND_EXPORTER_LISTEN_PORT value: "9100"需要注意 exporter 必须能访问宿主机的 DCMI 设备接口,一般通过挂载/usr/local/Ascend/driver、/usr/local/dcmi以及对应的设备节点来实现。部署后验证指标接口:
curl http://<pod-ip>:9100/metrics | grep ascend能看到类似ascend_npu_temperature、ascend_npu_memory_used_bytes、ascend_npu_ai_core_utilization这样的指标就说明采集正常。
6.3 Prometheus 抓取配置
如果是 kube-prometheus-stack 部署的 Prometheus,可以通过 ServiceMonitor 来定义抓取规则:
apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: ascend-exporter namespace: monitoring spec: selector: matchLabels: app: ascend-exporter endpoints: - port: metrics interval: 15s如果 Prometheus 是手动部署的,直接在 scrape_configs 里加 job 就行。
6.4 Grafana 面板与告警规则
指标进了 Prometheus 之后,Grafana 里建面板把几个关键指标可视化:
- NPU 温度:
ascend_npu_temperature - NPU 显存占用率:
ascend_npu_memory_used_bytes / ascend_npu_memory_total_bytes - AI Core 利用率:
ascend_npu_ai_core_utilization - 板卡功耗:
ascend_npu_power_consumption
告警规则方面,我配了三个比较实用的:
- NPU 温度超过 85 度持续 5 分钟,触发警告。
- AI Core 利用率持续 5 分钟低于 10%,检查是否有任务异常退出。
- 节点 NPU 剩余数量低于阈值,提醒扩容或排队。
监控这一层不要追求指标数量多,先把温度、显存、利用率、功耗这四个基础项抓好,日常运维基本就够用了。
7. 常见问题速查与排查技巧
7.1 问题排查一览表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
npu-smi info看不到设备 | 驱动未加载 / 设备节点缺失 | lsmod | grep drv,检查/dev/davinci* |
| 驱动安装时报编译错误 | 缺少 linux-headers / 内核版本不匹配 | 安装对应版本的内核头文件 |
容器内npu-smi info无输出 | ASCEND_VISIBLE_DEVICES 未设置 | 确认环境变量和 runtime 配置 |
| Pod 一直 Pending | 节点没有可用 NPU 资源 | kubectl describe node看资源剩余 |
| Pod 启动但容器内无 NPU | 未注入设备 / 环境变量缺失 | 检查 yaml 里 env 和挂载 |
| device-plugin 日志报错 | 驱动目录没有正确挂载 | 检查 DaemonSet 的 volume 配置 |
| exporter 抓取超时 | 网络策略限制 / exporter 无法访问 DCMI | 确认 Service 端口和挂载配置 |
7.2 排错实用技巧
排错时有个快速定位思路:先在宿主机上用npu-smi info确认硬件正常,再手动运行 Docker 容器确认 runtime 正常,然后通过 DaemonSet 部署确认设备插件正常,最后看 K8s 资源。这四层逐层排查,能快速缩小问题范围。
CANN 算子运行报错时,先看日志目录下的运行日志。设置ASCEND_GLOBAL_LOG_LEVEL=1可以把日志级别调到 DEBUG,能拿到更详细的算子报错信息。定位完问题记得把日志级别调回去,否则生产环境会产生大量日志把磁盘塞满。
还有一个我踩过两次的坑:K8s 节点重启或 Docker 重启之后,device-plugin Pod 可能因为 kubelet 的 device plugin socket 重建而变成 CrashLoopBackOff。这时候直接把 DaemonSet 滚动重启一遍就行,不需要重新配置任何东西:
kubectl rollout restart daemonset ascend-device-plugin -n kube-system这类问题有时候不会立刻出现,机器重启后才暴露,所以我把 Device Plugin 的存活探针和重启策略写进了部署模板,自动恢复,省得半夜被报警钉起来。
8. 个人实操中的几点体会
昇腾 NPU 接 K8s 这套链路,单个组件装起来都不算难,难点在于版本配套和组件之间衔接。版本配套一定要在动手之前解决,建议先在测试机跑通最小验证,再往生产环境铺。
一个小技巧:在宿主机上把各环节验证命令整理成一个脚本,安装完直接跑一遍就能定位问题所在。比如驱动验证跑npu-smi info,runtime 验证跑一次容器映射测试,device-plugin 验证看 kubelet 日志里是否有设备上报记录。这样每次换机器、换版本的时候,能省下大量手工逐个排查的时间。
后续如果任务量继续增长,还可以围绕这套环境做弹性伸缩、节点池管理、模型服务自动扩容,这些都是建立在 NPU 被 K8s 纳管之后才有能力去做的方向。