
昇腾 NPU 接入 Kubernetes 这件事我最早是被一个问题问住的“我们的容器平台里到底能不能直接跑 910B 的训练任务”当时手头有机器但没人在 K8s 里调过 NPU 资源大家默认“GPU 能接NPU 应该也能”。真上手之后才发现昇腾的接入链路比 NVIDIA 那套要繁琐不少涉及驱动、CANN、容器运行时、device-plugin 四层配套少一环任务都调度不上去。这篇文章就把 CubeStudio 在昇腾 NPU 接入 Kubernetes 时的完整落地过程做个复盘。内容包括驱动和 CANN 的安装、Ascend Docker Runtime 的配置、device-plugin 的部署方式以及后续监控怎么接。适合正在给集群加昇腾算力、或者准备做异构资源调度的运维和平台工程师照着操作基本能复现一套可用的环境。1. 整体设计与思路拆解1.1 昇腾进 K8s 的关键链路先把模型理清楚。GPU 接入 Kubernetes 靠的是 device-plugin 加 NVIDIA Container Toolkit昇腾这边则是“驱动 CANN Ascend Docker Runtime device-plugin”四段式。四者的关系大致是驱动NPU Driver负责操作系统与昇腾芯片之间的通信装好之后节点上才能识别出 NPU 设备节点/dev/davinci*。CANN ToolKit昇腾的计算库和运行框架类似 CUDA Toolkit 的角色。训练和推理任务依赖它来做算子编译和运行时管理。Ascend Docker Runtime把昇腾设备映射进容器的编译器和注入器。它负责在容器创建时把 /dev/davinci* 和设备依赖的库挂载进去。device-pluginKubernetes 里的标准扩展点。它向 kubelet 上报节点上的 NPU 资源数量并按调度结果把设备 ID 转成环境变量如 ASCEND_VISIBLE_DEVICES传给容器。这四层缺一不可。驱动没装设备节点不存在CANN 没装容器里即使看到设备也跑不了算子Ascend Docker Runtime 不配容器里看不到设备device-plugin 没部署K8s 根本不知道这节点有 NPU。所以下面每一步都不能跳。从我的经验看最容易被忽略的是 Ascend Docker Runtime。很多人以为 device-plugin 部署上去就完了结果 Pod 调度到节点上之后一查dmesg 里全是权限错误容器里 /dev/davinci0 不存在最后回头补装的 runtime。1.2 为什么选 Ascend Docker Runtime 而不是手动挂载可能有同学会问既然 device-plugin 能拿到设备 ID能不能像早期 GPU 容器那样直接在 Pod 的 YAML 里用 hostPath 手动挂载 davinci 设备技术上可行但不推荐。手动挂载的问题在于设备节点名容易漂移多卡场景下你不知道容器到底拿到了哪张卡CANN 的依赖库libascend.so、libcann.so 等分布在多个路径容易漏挂权限和 cgroup 隔离没法自动处理热插拔和故障隔离更是无从谈起。Ascend Docker Runtime 做的事情就是在 OCI 规范层面帮你把设备、驱动库、运行时环境都注入到容器里这样 Pod 内外看到的 NPU 是统一的。另外昇腾官方对 Ascend Docker Runtime 的更新频率跟 CANN 的版本是同步的用它的方式接入后续版本升级会平滑很多。手动挂载那种“能跑就行”的方案等升级或者换机时就会暴露很多坑。2. 环境准备与前置条件2.1 硬件和操作系统要求先说硬件。目前昇腾的主线产品是 Atlas 300I/300V 推理卡和 Atlas 800/900 训练服务器内部型号 910B、310P 等。CubeStudio 这边测试环境用的是 Atlas 800 训练服务器8 张 910B操作系统是 openEuler 22.03 LTS内核版本 5.10。昇腾对操作系统的兼容范围主要是 openEuler、Ubuntu20.04/22.04、CentOS 7.6/8.2 这些建议选官方文档里明确验证过的组合否则编译驱动时容易碰一鼻子灰。部署前先确认两件事BIOS 里把 SR-IOV 关掉如果只是单卡透传用途否则多卡设备的资源上报可能乱掉。内核参数iommupt或者intel_iommuon在该节点上保持默认或按昇腾文档设置驱动安装时会检查。检查硬件识别情况可以在装完驱动前先看看 PCIe 设备列表里有没有昇腾卡lspci | grep -i huawei正常能见到类似Processing accelerators: Huawei Technologies Co., Ltd. ...的输出。如果这一步都看不到设备后面的步骤先停一停先排查 PCIe 插槽和固件状态。2.2 软件版本选型策略昇腾软件栈各组件之间是严格版本绑定的。我们可以用一张表来说明典型的推荐组合组件版本说明NPU 固件与驱动23.0.rc2与 CANN 版本强关联CANN ToolKit7.0.0对应社区版比如 7.0.0Ascend Docker Runtime23.0.rc2与驱动版本配套Ascend device-plugin1.0.0社区版或官方镜像Kubernetesv1.26仅需要标准 device-plugin 接口操作系统openEuler 22.03建议与昇腾文档对齐这里要特别提醒昇腾的版本策略不太像 CUDA 那样兼容面很宽固件、驱动、CANN、runtime 四者之间有明确的校验逻辑版本不匹配安装时会直接报错或者运行时报算子编译错误。我的建议是先确定 CANN 版本再去找对应配套的驱动和 runtime。版本查下来最稳妥的办法是看 Ascend 社区的兼容性列表不要再依赖“最新版”直觉。比如 23.0.rc2 和 CANN 7.0.0 是稳定的组合我测试时也基本没踩坑。2.3 预留磁盘和内存空间还一个容易忽略的点是资源规划。CANN Toolkit 本身有几个 GB安装后还会在/usr/local/Ascend下生成一大堆库和算子包驱动安装包解压后也会占用临时空间。建议系统盘预留至少 20GB 可用空间。910B 在跑大模型训练时每个 NPU 会有独立的显存和内存映射容器如果设了严格的 memory limit建议给点余量不然会触发 OOM。实际操作中我见过一台系统盘只剩 5GB 的机器装驱动装到一半磁盘写满导致固件刷写失败、设备直接掉线的案例。教训就是装之前务必df -h看一眼。3. 驱动与 CANN 安装实操3.1 安装 NPU 固件和驱动昇腾的驱动和固件通常打在一个包里面命名类似Ascend-hdk-910b-npu-driver_23.0.rc2_linux-aarch64.run。下载后先给执行权限然后以 root 运行chmod x Ascend-hdk-910b-npu-driver_23.0.rc2_linux-aarch64.run ./Ascend-hdk-910b-npu-driver_23.0.rc2_linux-aarch64.run --full加--full是让驱动安装工具自动安装固件和驱动不用分两次操作。安装结束后至少检查三个方面是否出现了 davinci 设备节点ls /dev/davinci*正常会有/dev/davinci0、/dev/davinci1等数量和 NPU 卡数一致。驱动加载是否正常npu-smi infonpu-smi 是昇腾自带的查询工具能看到卡的温度、算力利用率、显存占用。如果这条命令报错大概率是驱动没起来或权限不对。设备是否在昇腾的管理列表里cat /usr/local/Ascend/driver/version.info这一步可以确认日期和版本号辅助核对固件和驱动版本捆绑关系。注意驱动安装过程中机器不要重启等固件刷写完成后由工具提示统一重启。固件刷写一半断电是最容易变砖的场景。3.2 安装 CANN Toolkit驱动装好之后紧接着装 CANN。CANN Toolkit 的安装包一般是Ascend-cann-toolkit_7.0.0_linux-aarch64.run。用默认安装方式chmod x Ascend-cann-toolkit_7.0.0_linux-aarch64.run ./Ascend-cann-toolkit_7.0.0_linux-aarch64.run --install安装过程中会默认装到/usr/local/Ascend/ascend-toolkit。装完之后需要把环境变量写进/etc/profile或/root/.bashrcexport ASCEND_HOME/usr/local/Ascend/ascend-toolkit/latest export PATH$ASCEND_HOME/bin:$PATH export LD_LIBRARY_PATH$ASCEND_HOME/lib64:$ASCEND_HOME/lib64/plugin/opskernel:$ASCEND_HOME/lib64/plugin/nnengine:$LD_LIBRARY_PATH export ASCEND_AICPU_PATH$ASCEND_HOME export ASCEND_OPPER_PATH$ASCEND_HOME/opp然后source /etc/profile验证which npu-smi这里有个坑npu-smi 在驱动侧也有在 CANN 侧也有。以驱动里的为准的话路径通常是/usr/local/Ascend/driver/tools。CANN 装完之后建议把 CANN 的 bin 目录放到 PATH 前面避免混用不同工具。再验证 CANN 是否能正确识别卡python3 -c import acl; print(acl.__version__)如果 import 报错检查LD_LIBRARY_PATH是否包含/usr/local/Ascend/ascend-toolkit/latest/lib64。这一步特别烦但很重要后面容器里跑任务时依赖的就是这套环境变量。3.3 一个容易忽视的步骤建 Linux 用户组昇腾驱动推荐把使用 NPU 的用户加入HwHiAiUser组或直接以 root 跑。K8s 场景下容器一般以 root 运行但如果你们的平台有 PSP 或 Pod Security 限制不让容器 root 跑那你就需要把宿主机的 davinci 设备权限处理好。实践中我在测试环境里把 davinci 设备权限设成了 660属组分配给了HwHiAiUser然后在 device-plugin 里配置了对应的 privileged 权限。这样容器内就算用非 root 用户也能访问设备。4. Ascend Docker Runtime 配置4.1 安装和注册 runtimeAscend Docker Runtime 本质上是一个 OCI runtime hook它通过 Docker 的 runtime 机制在容器创建时注入设备。装它之前要先把软件源配好这里我直接说明用安装包的方式。从昇腾社区下载对应版本的Ascend-docker-runtime_23.0.rc2_linux-aarch64.run执行chmod x Ascend-docker-runtime_23.0.rc2_linux-aarch64.run ./Ascend-docker-runtime_23.0.rc2_linux-aarch64.run --install安装完成后检查/etc/docker/daemon.json是否自动写入了 runtime{ runtimes: { ascend: { path: /usr/local/Ascend/Ascend-Docker-Runtime/ascend-docker-runtime, runtimeArgs: [] } } }如果文件里没有手动加上然后重启 Dockersystemctl daemon-reload systemctl restart docker注意如果节点用的是 containerd 而不是 DockerAscend Docker Runtime 的配置路径会不一样。需要在/etc/containerd/config.toml里配置 runtime 而不是 daemon.json。具体看你的 K8s 版本和容器运行时选型。4.2 验证 Runtime 生效Docker 重启后用下面命令验证 runtime 是否生效docker run --rm --runtimeascend -e ASCEND_VISIBLE_DEVICES0 \ ascendai/cann:7.0.0-ubuntu20.04 \ npu-smi info这个镜像不需要本地构建Ascend 官方镜像仓库里有现成的。如果能看到 NPU 信息输出说明 runtime 注入没问题。如果没有输出或者报找不到设备先看宿主机上/dev/davinci0是否存在再看 Docker runtime 是否注册成功。两步都正常还不行就要检查 Docker daemon 日志。4.3 为什么 ASCEND_VISIBLE_DEVICES 是核心变量Ascend 的 device-plugin 和 Docker Runtime 之间依靠环境变量ASCEND_VISIBLE_DEVICES来传递设备 ID。这和 NVIDIA 的NVIDIA_VISIBLE_DEVICES是同一个思路。device-plugin 在调度时已经把这个环境变量写进了 Podruntime 再根据这个变量决定挂载哪些 davinci 设备节点。手动测试时你会经常用到它。比如想明确指定容器用第 0 张卡就写ASCEND_VISIBLE_DEVICES0。想把多张卡全部暴露就写ASCEND_VISIBLE_DEVICESall或者逗号分隔的列表。如果这个变量没设runtime 默认不挂任何设备容器自然看不到 NPU。这个设计非常有用但也带来一个坑如果你在自己的 YAML 里手动设置了ASCEND_VISIBLE_DEVICES和 device-plugin 注入的值冲突最终行为以 runtime 实际执行的为准。所以平时的规范做法是 Pod 里不要手工写这个变量交给自己部署的 device-plugin 去管理。5. device-plugin 部署5.1 使用 DaemonSet 部署 device-plugin昇腾 device-plugin 本质上一个 DaemonSet用 DaemonSet 保证每个带有ascend true标签的节点都会跑一个 Pod这个 Pod 负责向 kubelet 注册设备。以下是部署的最简手段使用 Ascend 开源仓库里的 YAML。先克隆仓库git clone https://gitee.com/ascend/ascend-device-plugin.git cd ascend-device-plugin核心 YAML 大概长这样apiVersion: apps/v1 kind: DaemonSet metadata: name: ascend-device-plugin namespace: kube-system spec: selector: matchLabels: name: ascend-device-plugin template: metadata: labels: name: ascend-device-plugin spec: hostNetwork: true containers: - name: device-plugin image: ascend-device-plugin:1.0.0 imagePullPolicy: IfNotPresent securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: sys mountPath: /sys - name: log mountPath: /var/log volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: sys hostPath: path: /sys - name: log hostPath: path: /var/log留意两个点privileged 权限必须有因为 device-plugin 要访问宿主机设备/var/lib/kubelet/device-plugins是 kubelet 和设备插件通信的 socket 目录必须挂进去。5.2 给宿主机打标签和上报资源验证部署完成之后先给节点打标签确保 DaemonSet 只在有昇腾卡的节点上运行kubectl label node node-name accelerateascend然后在 DaemonSet 的模板里用 nodeSelector 限定spec: template: spec: nodeSelector: accelerate: ascend等 Pod 起来之后检查节点资源上报情况kubectl describe node node-name | grep -A 5 Capacity如果一切正常会看到类似huawei.com/Ascend910: 8如果没有这个资源项先查看 device-plugin Pod 日志kubectl logs -n kube-system ascend-device-plugin-xxxxx常见的报错是无法连接 kubelet。这类问题大多是 socket 路径不一致导致的device-plugin 默认连接/var/lib/kubelet/device-plugins/kubelet.sock如果你的 kubelet 用了非标准路径需要在配置里指定。5.3 请求 NPU 资源的 Pod 示例资源上报成功之后提交一个测试 Pod 验证调度apiVersion: v1 kind: Pod metadata: name: ascend-test spec: restartPolicy: OnFailure containers: - name: ascend-test image: ascendai/cann:7.0.0-ubuntu20.04 command: [/bin/bash, -c, npu-smi info sleep 3600] resources: limits: huawei.com/Ascend910: 1这里指定了huawei.com/Ascend910: 1device-plugin 会把 1 张卡注入到这个 Pod。Pod 启动后进容器里跑npu-smi info能看到卡信息就算基本打通。如果 Pod 一直 Pending检查节点是否有足够资源以及 scheduler 是否了解 Ascend 资源。标准 K8s scheduler 是支持扩展资源的不需要换调度器但是要注意扩展资源不支持 overcommitlimits 和 requests 必须一致。6. 监控接入与指标采集6.1 用 Ascend Exporter 采集 NPU 指标集群成功调度 NPU 之后紧接着就要考虑监控。昇腾提供了基于 Prometheus 的 Exporter可以将卡的温度、算力利用率、功耗、显存指标暴露出来。通常用 DaemonSet 部署在昇腾节点上镜像比如ascend-prometheus-exporter。部署之后Exporter 会监听一个端口默认 9100 或者你自己映射的端口输出指标格式大概是ascend_npu_ai_core_frequency { device_id0, model910B } 1800 ascend_npu_ai_core_utilization { device_id0, model910B } 72.5 ascend_npu_hbm_usage { device_id0, model910B } 42.3再把这个 exporter 接入你的 Prometheuskubernetes-service-discovery 就能自动发现节点。然后在 Grafana 里画面板重点看利用率、内存、温度。此前我们踩过一个坑910B 的功耗比较高柜级散热不足时卡温度很容易上 80 度温度过高会自动降频。Exporter 里把温度指标拿到配合告警规则能在训练任务变慢之前就发现问题。6.2 Kubernetes 事件和日志的联动除了数值型指标Pod 事件和日志也要统一收。DCGM 那套监控方式在 GPU 场景成熟但昇腾这边目前没有完全等价的官方方案。我用的是最基础的手法把 device-plugin、Ascend Exporter 的日志走 stdout交给集群日志系统Loki 或 ELK。对 kubelet 里的Failed to create pod sandbox和failed to set up container这类错误关键字配告警一旦出现就往值班群发消息。NPU 相关故障并不总是直接表现为“设备不存在”更常见的是“容器能起来但推理性能异常低”。这时候关联日志和 exporter 指标特别有用——比如把任务启动时间和 ASCEND_DRIVER_VERSION、CANN_VERSION 关联起来排查效率会高很多。7. 常见问题与排查技巧实录7.1 问题速查表我把实际操作里遇到的高频问题整理成了一张速查表先看表后面再讲典型场景。现象可能原因排查方向npu-smi info报错驱动未装好或权限不对看 dmesg、检查/dev/davinci*Pod 一直 Pending节点没有 Ascend 资源kubectl describe node确认资源上报容器里看不到 davinci 设备Ascend Docker Runtime 未配置或未启用查看 daemon.json、重试 docker run 指定 runtimePod 启动报权限错误设备 cgroup 权限不足设置 privileged 或调整用户组device-plugin 报无法连接 kubeletsocket 路径不对检查/var/lib/kubelet/device-plugins多卡任务只用了 1 卡ASCEND_VISIBLE_DEVICES 只写了一个 ID检查 Pod 的资源请求数量训练速度忽快忽慢温度降频看 exporter 温度指标7.2 踩过的坑和独家避坑技巧第一个坑是版本对齐。早期我们想“偷懒”驱动用了较新的候选版CANN 还留在 7.0.0 稳定版结果运行训练任务时直接报算子编译错误。花了一天才意识到是固件和 CANN 版本不匹配。昇腾社区对固件驱动版本和 CANN 版本是有配套关系表的一定要按那个表来选不要各自取最新。第二个坑是容器内存限制。昇腾 910B 在分配显存之外还会用到一部分 host 内存做数据拷贝和算子调度。如果 Pod 设置的内存 limit 太紧例如只给 2GB任务可能刚起没多久就被 OOM Kill。我们最后在监控里发现一个中等规模的推理任务大约需要 1.5GB 左右的 host 内存建议在 limit 基础上多给 30% 余量。第三个坑是重启后 device 节点编号漂移。某些场景下重启之后/dev/davinci0和物理卡的对应关系可能发生变化。Ascend Docker Runtime 会尽量做绑定但如果遇到过可以查看/usr/local/Ascend/driver下的 device 映射文件来确认。多机多卡场景尤其要对这个问题敏感。第四个坑是关于 kubelet 的扩展资源。扩展资源不像普通 CPU 和内存它不支持动态调整。Pod 里面如果 requests 和 limits 不写一样调度阶段就会报错。所以昇腾相关的资源在 YAML 里请务必写完全一致。7.3 还有一个容易漏掉的步骤CANN 版本环境变量K8s 跑起来的容器可能不会继承宿主机 /etc/profile 里的 CANN 环境变量。device-plugin 和 Ascend Docker Runtime 只负责挂设备和库但容器内应用启动时如果依赖ASCEND_HOME、LD_LIBRARY_PATH这些环境变量就需要你写在 Dockerfile 的 ENV 里或者在启动命令前 source 一下。官方镜像通常已经配好了但如果你用的是基于自有基础镜像的打底镜像这一点一定要检查。我在一个同事的自定义镜像上踩过这个坑现象是容器能看到/dev/davinci0但一跑 ACL 初始化就报找不到 libascendcl.so加了一行 ENV 就解决了。8. 从打通到稳定运行的经验总结昇腾 NPU 接入 Kubernetes整体链路不算特别短但一旦把驱动、CANN、Ascend Docker Runtime、device-plugin 这四层按版本对齐逐个装好后面跑训练和推理任务就顺畅多了。即便这样我依然建议你把整个过程写成 IaC 或者 Ansible Playbook因为多台机器手动敲命令太容易漏步骤。这里再补充三个我认为最重要的落地建议。第一个是“先在单机容器里验证再做 K8s 集群调度”。不要一上来就在 K8s 里折腾先把单机的 docker run 场景跑通确认 runtime 和 CANN 环境都对再往 K8s 上引。这样可以大大缩小排查范围。第二个是“把版本信息固化到镜像 tag 里”。比如镜像命名成cann7.0.0-drv23.0.rc2-ascend以后出问题一眼能看出是不是版本错配导致的。昇腾版本兼容性没那么宽容这个习惯能省下大把排障时间。第三个是“把 Ascend Exporter 纳入核心监控链路”。NPU 跟 GPU 一样很多时候性能劣化是温度和功耗造成的不是应用代码问题。有 exporter 的曲线在手边排查速度和准确性完全是两个级别。实际跑了一段时间之后我个人最深的体会是昇腾这套软件栈现在已经不是一个原型状态只要版本选型严格对齐接入 Kubernetes 的稳定性是有保障的。后续如果你们要做多集群调度或者 GPU/NPU 混合调度这套基础打通之后完全可以继续往高级方向扩展——但前提是底层这几层别留模糊地带建议全部标准化、文档化。