本文是一套完整实战的合集,记录了在混合异构(瑞芯微 RK3588 + 华为昇腾 310B)环境下,从零部署 Kubernetes + KubeSphere 集群,再到将两类国产 NPU 接入 K8s 统一调度的全过程。
全文分三部分:
- 第一部分:异构 K8s 集群 + KubeSphere 平台部署
- 第二部分:华为昇腾 310B NPU 接入 K8s 统一调度(含源码级 bug 定位与修复)
- 第三部分:瑞芯微 RK3588 NPU 接入 K8s 统一调度(无官方文档,自研 Device Plugin)
涉及的操作系统包括麒麟 V10 国防版、openEuler 22.03 等国产化环境,对信创场景有较高参考价值。
第一部分:异构 K8s 集群 + KubeSphere 部署
1. 产品属性
RK3588 是瑞芯微推出的旗舰级高性能 ARM 处理器,内置 6TOPS 算力的 NPU,适合边缘计算、工业控制和人工智能领域。
昇腾 310B 的相关属性在前文(第二部分)已有介绍,本节不再赘述。
CPU 和系统信息:

服务器情况:
| 主机名 | 设备 | 架构 | OS | 配置 | IP |
|---|---|---|---|---|---|
| master1 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.14 |
| master2 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.16 |
| master3 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.18 |
| node1 | 310B | arm64 | 欧拉22.03 | 4核12G | 192.168.37.12 |
| node2 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.20 |
| node3 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.22 |
| node4 | 310B | arm64 | 欧拉22.03 | 4核12G | 192.168.37.16 |
| node5 | rk3588 | arm64 | 麒麟V10国防版 | 8核8G | 192.168.37.24 |
2. 环境准备
2.1 上传安装包
将离线制品、配置文件、kt 和 sh 脚本等上传至其中一个节点(本文以 master 为例),后续在该节点操作创建集群。这里我们为追求稳定性选择了 k8s 1.23.17 版本 + ks 4.1.3 版本。
关于 kt
kt 是基于 kk 二次开发的产物,具备 kk 的所有功能。二开主要为适配信创国产化环境、简化 arm 部署过程和国产化环境离线部署。支持 arm64 和 amd64 架构国产操作系统,已适配芯片 + 操作系统如下。
kt 新增功能点
- 适配 arm 架构 harbor 和支持,部署体验与 X86 一样简单。
- 离线环境部署增强。常用国际和国产操作系统依赖,内置到安装包中。已适配芯片和操作系统如下:
./kt init-os -f config-sample.yaml一条命令完成所有节点操作系统依赖安装和初始化操作。- CPU:鲲鹏、飞腾、海光、兆芯、intel、amd 等。
- OS:Centos、Rocky Linux、Ubuntu、Debian、银河麒麟V10、麒麟V11、麒麟国防版、麒麟信安、中标麒麟V7、统信UOS、华为欧拉、移动大云、阿里龙蜥、TencentOS 等。
- kt文档: kt文档
2.2 修改配置文件
修改 config-sample.yaml,主要修改节点信息部分(如下 hosts 和 roleGroups 部分):
yaml
kind: Cluster
metadata:
name: sample
spec:
hosts:
- {name: master1, address: 192.168.137.14, internalAddress: 192.168.137.14, user: root, password: "123123", arch: "arm64"}
- {name: master2, address: 192.168.137.16, internalAddress: 192.168.137.16, user: root, password: "123123", arch: "arm64"}
- {name: master3, address: 192.168.137.18, internalAddress: 192.168.137.18, user: root, password: "123123", arch: "arm64"}
- {name: node1, address: 192.168.137.12, internalAddress: 192.168.137.12, user: root, password: "123123", arch: "arm64"}
- {name: node2, address: 192.168.137.20, internalAddress: 192.168.137.20, user: root, password: "123123", arch: "arm64"}
- {name: node3, address: 192.168.137.22, internalAddress: 192.168.137.22, user: root, password: "123123", arch: "arm64"}
roleGroups:
etcd:
- master1
- master2
- master3
control-plane:
- master1
- master2
- master3
worker:
- node1
- node2
- node3
# 如需使用 kk 自动部署镜像仓库,请设置该主机组 (建议仓库与集群分离部署,减少相互影响)
# 如果需要部署 harbor 并且 containerManager 为 containerd 时,由于部署 harbor 依赖 docker,建议单独节点部署 harbor
registry:
- master3
controlPlaneEndpoint:
## Internal loadbalancer for apiservers
internalLoadbalancer: haproxy
domain: lb.kubesphere.local
address: ""
port: 6443
kubernetes:
version: v1.23.17
clusterName: cluster.local
autoRenewCerts: true
containerManager: docker
etcd:
type: kubekey
network:
plugin: flannel
kubePodsCIDR: 10.233.64.0/18
kubeServiceCIDR: 10.233.0.0/18
## multus support. https://github.com/k8snetworkplumbingwg/multus-cni
multusCNI:
enabled: false
2.3 系统初始化
解压 kt-arm64.tar.gz 文件后执行 ./kt init-os -f config-sample.yaml
3. 创建私有仓库
执行 ./kt init resigtry -f config-sample.yaml -a artica*
等待一切安装成功后,创建 Harbor 项目:
bash
chmod +x create_project_harbor.sh && ./create_project_harbor.sh

注意事项:如果系统提示缺少 iptables,则需要安装先安装 iptables。
4. 创建 k8s 集群
bash
./kt create cluster -f config-sample.yaml -a artifact-arm-k8s12317tar.gz
此命令 kt 会自动将离线制品中的镜像推送到 harbor 私有仓库。
执行后会有如下提示,输入 yes/y 继续执行。
等待最后提示安装成功:

注意:由于该麒麟V10国防版是瑞芯微定制版,内核缺少非常多的东西,安装过程会报错。大体如下:

需要安装内核模块,再创建 k8s。
最后 k8s 创建完成,还是会报错,起初 nodelocaldns 会报错,需要安装 dummy 模块。


这里还需要修改 kube-proxy 和 kube-flannel 配置:
bash
kubectl edit cm kube-proxy -n kube-system
#修改mode由ipvs改为iptables(内核没有ipvs模块)
kubectl edit cm kube-flannel-cfg -n kube-system
#由vxlan修改微host-gw,不然路由总是出现问题


node1 昇腾设备需要安装 systemd-resolved 服务,否则系统重启该节点通信失败:
bash
dnf install systemd-resolved
systemctl enable --now systemd-resolved
cat /run/systemd/resolve/resolv.conf
都修改完成后重启服务,等待一会。
查看节点状态
bash
kubectl get nodes -owide
可以看到所有节点状态均已 Ready,共有 3 个管理节点和 3 个工作节点,其中管理节点也充当工作节点。

查看 pod 运行情况
bash
kubectl get pod -A -owide
可以看到所有 pod 已成功运行(ps:以下截图为装完 ks 和插件的截图)

5. 部署 KubeSphere
使用 helm 命令通过私有仓库安装 ks:
bash
helm upgrade --install -n kubesphere-system --create-namespace ks-core ks-core-1.1.5.tgz \
--set global.imageRegistry=dockerhub.kubekey.local/ks \
--set extension.imageRegistry=dockerhub.kubekey.local/ks \
--set ksExtensionRepository.image.tag=v1.1.6 \
--debug \
--wait
等待一会看到成功的消息:

6. 验证
- 登录页面
默认用户名为 admin,默认密码:P@88w0rd

- 首页

- 集群管理

安装监控插件
直接从扩展市场安装,这里不再记录具体过程。
- 概览

- 集群节点

- 集群状态监控

- 节点信息
昇腾 310B:

RK3588:

7. 第一部分小结
本文详细介绍了在瑞芯微 RK3588 麒麟 V10 国防版和华为昇腾 310B 欧拉异构环境下,部署 Kubernetes 集群和 KubeSphere 管理平台,并实现资源设备监控。下一步,将实现 NPU 设备统一调度(见第二、三部分)。
第二部分:华为昇腾 310B NPU 接入 K8s 统一调度
官网教程:昇腾镜像仓库详情
官方主要是对昇腾 910 和 310P 的介绍。实际落地时,device-plugin 对昇腾 310B 不兼容,需要修改源码。
之前的文章中已经写过纯 310B 以及 RK3588 和昇腾异构部署 k8s 集群。本文记录将华为昇腾 Atlas 200I A2 (310B1) NPU 接入 Kubernetes 集群实现统一调度的完整实战过程,包括环境搭建、源码级 bug 定位与修复、镜像构建、RBAC 配置及最终部署验证。希望对同样在国产 AI 芯片 + 云原生道路上探索的朋友有所帮助。
一、背景
随着 AI 推理和训练场景的普及,如何在 Kubernetes 集群中统一管理和调度异构算力芯片成为一个日益迫切的需求。Kubernetes 提供了 Device Plugin 机制,允许厂商通过标准的 gRPC 接口将专用硬件(GPU、NPU、FPGA 等)注册到集群中,实现与 CPU/内存一致资源的调度体验。
华为昇腾生态提供了 ascend-device-plugin(隶属于 MindX DL 项目),用于将昇腾 NPU 注册到 K8s。本文以 Atlas 200I A2 加速模块(搭载 310B1 芯片) 为目标硬件,记录完整的部署实战过程。
环境概览
| 组件 | 版本 |
|---|---|
| 节点系统 | openEuler (aarch64) |
| NPU 型号 | Ascend 310B1 (Atlas 200I A2) |
| DCMI | 24.1.rc1 |
| CANN | 8.0.RC1 |
| K8s 运行时 | Docker |
| 目标设备插件 | ascend-device-plugin v26.0.0 |
二、Docker 层面验证:先确保驱动能通
在接入 K8s 之前,第一步一定是先在 Docker 中验证基础功能可用。这一步我们的目标是确认 device-plugin 容器能正确调用宿主机的 DCMI 接口,成功识别出 310B1 芯片。
2.1 确认宿主 NPU 状态
bash
npu-smi info

驱动正常,芯片健康。
2.2 跑官方 device-plugin 镜像
按照官方文档:www.hiascend.com/developer/a...
使用 device-plugin:v7.3.1 版本一直报错,即使加入了所有的依赖,最终依然报错 HDC 初始化失败,不论是否使用 Ascend Docker Runtime 都不行。最终决定直接使用 docker 运行 device-plugin 镜像,依然失败:

最终通过翻看官方文档,找到一处关键地方,如下,需要映射 hdcBasic.cfg 文件。加入此映射后,docker 容器不再报错 hdc 初始化失败。

bash
docker run --rm -it --privileged --network=host \
-v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
-v /usr/lib64:/usr/lib64:ro \
-v /dev:/dev -v /sys:/sys \
-v /etc/hdcBasic.cfg:/etc/hdcBasic.cfg:ro \
-v /etc/sys_version.conf:/etc/sys_version.conf:ro \
-v /etc/ascend_install.info:/etc/ascend_install.info:ro \
-v /var/slogd:/var/slogd:ro \
-v /var/dmp_daemon:/var/dmp_daemon:ro \
-v /var/queue_schedule:/var/queue_schedule:ro \
-v /dev/shm:/dev/shm \
-e LD_LIBRARY_PATH=/usr/lib64:/usr/local/Ascend/driver/lib64/... \
ascend-k8sdeviceplugin:v7.3.1 \
/usr/lib64/ld-linux-aarch64.so.1 /usr/local/bin/device-plugin -logLevel=0
这里有几个不寻常的细节需要注意:
/var/slogd、/var/dmp_daemon、/var/queue_schedule是可执行文件 ,不是目录也不是 socket,挂载时需要type: File+readOnly/dev/shm必须用 hostPath,不要用 emptyDir------dmp_daemon在/dev/shm/iam/和/dev/shm/dmp/下有共享内存通信文件- 启动命令必须用宿主机的
ld-linux-aarch64.so.1前缀,因为容器内 glibc 版本与宿主机不兼容,直接执行会触发 SIGBUS 崩溃
运行结果:
latex
chipName: 310B1, devType: Ascend310B
[ERROR] get chip aicore count failed, err: invalid ai core num 1.000000
芯片识别到了,但报错 invalid ai core num 1.000000------这就是本文的核心问题。
三、根因分析:源码级定位
3.1 问题现象
错误日志非常明确:
latex
devicefactory/entry.go:37 init device manager failed
server/manager.go:162 get chip aicore count failed, err: invalid ai core num 1.000000
3.2 源码追踪
从 server/manager.go:162 开始追踪调用链:
调用链 1:入口
latex
// server/manager.go:162
cnt, err := hdm.manager.GetChipAiCoreCount()
调用链 2:DCMI 查询
latex
// pkg/device/ascendcommon.go:1309
func (m *AscendManager) GetChipAiCoreCount() (int64, error) {
chipAICore := float64(m.TotalResource.Computing.Aic) // DCMI 返回 1.000000
cnt, err := getAiCoreCount(chipAICore)
...
}
调用链 3:致命 range check
latex
// pkg/device/ascendcommon.go:1335
func getAiCoreCount(chipAICore float64) (int64, error) {
intAICore := int64(chipAICore)
if intAICore < common.MinAICoreNum || intAICore > common.MaxAICoreNum {
return 0, fmt.Errorf("invalid ai core num %f", chipAICore)
}
...
}
调用链 4:常量定义(问题所在)
latex
// pkg/common/constants.go:340
MinAICoreNum = 8
MaxAICoreNum = 36
3.3 根因总结
问题出在 constants.go 的第 340 行:
latex
MinAICoreNum = 8
DCMI 24.1.rc1 对 310B1 芯片返回的 AI Core 数量是 浮点数 1.000000(310B1 只有 1 个物理 AI Core)。getAiCoreCount 函数将其转为 int64(1),然后检查 [8, 36] 的合法范围------1 < 8,直接被拒绝。
换句话说:device-plugin 的 AI Core 数量范围默认假设芯片至少有 8 个 AI Core,而 310B1 只有 1 个。这是一个代码层面没有考虑 310B1 这类低功耗推理芯片的兼容性 bug。
四、修复方案
4.1 代码修复
在 pkg/common/constants.go 中,将 MinAICoreNum 从 8 改为 1:
latex
// 修复前
MinAICoreNum = 8
// 修复后------310B1 只有 1 个 AI Core
MinAICoreNum = 1
仅需一行代码改动。但问题在于:device-plugin 的编译依赖 CGO(通过 dlopen 动态加载 DCMI 库),而且最终需要运行在 aarch64 架构上。
4.2 多阶段 Dockerfile
我们采用多阶段构建策略:第一阶段用 Go 交叉编译,第二阶段用轻量 Ubuntu 做运行时:
dockerfile
# ===== 阶段 1:编译 =====
FROM golang:1.21 AS builder
WORKDIR /build
COPY ascend-device-plugin/ /build/
COPY ascend-common/ /ascend-common/
RUN go env -w GOPROXY=https://goproxy.cn,direct && \
go mod download && \
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \
go build -ldflags="-s -w" -o device-plugin .
# ===== 阶段 2:运行时 =====
FROM ubuntu:22.04
COPY --from=builder /build/device-plugin /usr/local/bin/
RUN chmod 550 /usr/local/bin/device-plugin
说明 :基础镜像分别需要 golang:1.21 和 ubuntu:22.04,在国内环境建议通过 DaoCloud 代理拉取:
bash
docker pull docker.m.daocloud.io/library/golang:1.21
docker pull docker.m.daocloud.io/library/ubuntu:22.04
docker tag docker.m.daocloud.io/library/golang:1.21 golang:1.21
docker tag docker.m.daocloud.io/library/ubuntu:22.04 ubuntu:22.04
4.3 K8s YAML 配置
完整的 DaemonSet 部署清单包含 4 个资源:ServiceAccount、ClusterRole、ClusterRoleBinding 和 DaemonSet 本体。
4.3.1 RBAC 权限
device-plugin 需要操作 Node 标签和 ConfigMap 来记录设备信息:
yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ascend-device-plugin
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: ascend-device-plugin
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["nodes/status"]
verbs: ["patch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: ascend-device-plugin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: ascend-device-plugin
subjects:
- kind: ServiceAccount
name: ascend-device-plugin
namespace: kube-system
4.3.2 DaemonSet 核心配置
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ascend-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: ascend-device-plugin-ds
template:
metadata:
labels:
name: ascend-device-plugin-ds
spec:
hostNetwork: true # 关键:访问宿主机网络
hostPID: true # 关键:访问宿主机进程
serviceAccountName: ascend-device-plugin
tolerations:
- key: CriticalAddonsOnly
operator: Exists
priorityClassName: "system-node-critical"
nodeSelector:
accelerator: huawei-Ascend310
containers:
- image: ascend-k8sdeviceplugin:v26.0.0.fixed
imagePullPolicy: Never
name: device-plugin
securityContext:
privileged: true
command: ["/usr/lib64/ld-linux-aarch64.so.1"]
args:
- "/usr/local/bin/device-plugin"
- "-logLevel=0"
env:
- name: LD_LIBRARY_PATH
value: "/usr/lib64:/usr/local/Ascend/driver/lib64:\
/usr/local/Ascend/driver/lib64/driver:\
/usr/local/Ascend/driver/lib64/common"
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: hiai-driver
mountPath: /usr/local/Ascend/driver
readOnly: true
- name: lib64
mountPath: /usr/lib64
readOnly: true
- name: dev
mountPath: /dev
- name: sys
mountPath: /sys
- name: hdc-basic-cfg # 关键配置文件
mountPath: /etc/hdcBasic.cfg
readOnly: true
- name: sys-version-conf
mountPath: /etc/sys_version.conf
readOnly: true
- name: ascend-install-info
mountPath: /etc/ascend_install.info
readOnly: true
- name: slogd # 可执行文件
mountPath: /var/slogd
readOnly: true
- name: dmp-daemon # 可执行文件
mountPath: /var/dmp_daemon
readOnly: true
- name: queue-schedule # 可执行文件
mountPath: /var/queue_schedule
readOnly: true
- name: dshm # 共享内存(hostPath)
mountPath: /dev/shm
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: hiai-driver
hostPath:
path: /usr/local/Ascend/driver
- name: lib64
hostPath:
path: /usr/lib64
type: Directory
- name: dev
hostPath: { path: /dev }
- name: sys
hostPath: { path: /sys }
- name: hdc-basic-cfg
hostPath:
path: /etc/hdcBasic.cfg
type: File
- name: sys-version-conf
hostPath:
path: /etc/sys_version.conf
type: File
- name: ascend-install-info
hostPath:
path: /etc/ascend_install.info
type: File
- name: slogd
hostPath:
path: /var/slogd
type: File
- name: dmp-daemon
hostPath:
path: /var/dmp_daemon
type: File
- name: queue-schedule
hostPath:
path: /var/queue_schedule
type: File
- name: dshm
hostPath:
path: /dev/shm
type: Directory
五、部署验证
5.1 构建镜像
bash
tar -xzf device-plugin-310B1-fixed.tar.gz
bash build.sh
# 输出: Successfully tagged ascend-k8sdeviceplugin:v26.0.0.fixed
5.2 部署到 K8s
bash
kubectl apply -f device-plugin-310B1.yaml
输出:
latex
serviceaccount/ascend-device-plugin created
clusterrole.rbac.authorization.k8s.io/ascend-device-plugin created
clusterrolebinding.rbac.authorization.k8s.io/ascend-device-plugin created
daemonset.apps/ascend-device-plugin-daemonset created
5.3 查看日志
bash
kubectl logs -n kube-system -l name=ascend-device-plugin-ds --tail=20
关键日志行:
latex
deviceManager get cardList is [0], cardList length equal to cardNum: 1
chipName: 310B1, devType: Ascend310B
init device manager success ← 初始化成功!
update node label success ← 节点标签更新成功!
register Ascend310 to kubelet success ← 向 kubelet 注册成功!
Ascend310-0 Healthy ← 设备状态健康!
5.4 验证节点资源
bash
$ kubectl describe node node1 | grep Ascend310
Capacity:
huawei.com/Ascend310: 1
huawei.com/Ascend310: 1------NPU 资源已经成功注册到 Kubernetes,可以像 CPU 和内存一样被调度使用了。
六、踩坑要点总结
在本次实战中,我们踩过的主要坑和对应的解决方案:
| # | 踩坑点 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | MinAICoreNum = 8 |
invalid ai core num 1.000000 |
修改源码为 MinAICoreNum = 1 |
| 2 | 可执行文件挂载为目录 | MountVolume 失败 | type: File + readOnly: true |
| 3 | /dev/shm 用 emptyDir |
dm_send_msg fail:2 |
改为 hostPath(共享内存文件在宿主机上) |
| 4 | 容器 glibc 不兼容 | SIGBUS 退出码 135 | 用宿主 ld-linux-aarch64.so.1 前缀启动 |
| 5 | RBAC 权限不足 | cannot get resource "nodes" |
创建 SA + ClusterRole + ClusterRoleBinding |
| 6 | ConfigMap 权限缺失 | cannot update resource "configmaps" |
ClusterRole 添加 configmaps CRUD 权限 |
| 7 | kubelet API 无法访问 | host ip is invalid |
设置 NODE_IP env var (status.hostIP) |
| 8 | Golang 基础镜像拉取慢 | Docker Hub 超时 | 通过 DaoCloud 代理 docker.m.daocloud.io |
七、生产化部署 checklist
如果你的环境有多台 310B1 节点需要接入 K8s,按以下顺序操作:
- 每台节点安装 CANN 驱动、DCMI,确认
npu-smi info正常 - 准备基础镜像(
golang:1.23+ubuntu:22.04) - 构建修复后的 device-plugin 镜像(
build.sh) - 给节点打 label:
kubectl label node <node> accelerator=huawei-Ascend310 kubectl apply -f device-plugin-310B1.yaml- 验证
kubectl describe node <node> | grep Ascend310 - 部署测试 Pod,声明
huawei.com/Ascend310: 1验证容器内可调用 NPU
八、第二部分小结
这次实战经历可以总结为三个层次的工作:
- 适配层:理解 NPU 驱动(DCMI)和 device-plugin 之间的数据交互协议,发现浮点数格式差异
- 源码层:深入 device-plugin 源码,定位 AI Core 数量校验逻辑的硬编码边界,一行代码修复
- 工程层:处理 YAML 配置、挂载、RBAC、glibc 兼容性等一系列 K8s 落地细节
整个过程耗时最长的地方不在于修复代码------而在于理解 dmp_daemon 是一个可执行文件而非目录 、/dev/shm 不能用 emptyDir、容器二进制必须用宿主动态链接器启动这些反直觉的细节。
第三部分:瑞芯微 RK3588 NPU 接入 K8s 统一调度
上一篇文章已经介绍了在混合异构 Kubernetes 集群中统一调度昇腾 NPU,本文记录为 RK3588 节点实现 NPU 统一调度的完整过程,包含原理分析、代码实现、踩坑记录和验证方法,全程无官方文档支撑,纯靠内核日志和 K8s 规范拼出来的。
1. 背景
集群配置如下:
| 节点 | 角色 | 硬件 | NPU 调度 |
|---|---|---|---|
| master1/2/3 | control-plane | RK3588 | 无 |
| node1, node4 | worker | 昇腾设备 | 有官方 device plugin,需要二开 |
| node2, node3, node5 | worker | RK3588 | 无官方支持 |
昇腾那边有华为官方的 Ascend Device Plugin,上一篇中已经在此基础上修改调整。RK3588 这边翻遍 Rockchip 和 RKNN 的所有仓库,官方没有任何 Kubernetes Device Plugin,需要自己实现。
最终实现效果:

一、先搞清楚 RK3588 NPU 在 Linux 里是什么设备
在动手写代码之前,必须先搞清楚设备的实际形态。很多教程直接告诉你路径,但在不同系统版本上路径可能完全不同。
正确的做法是先看内核日志:
bash
dmesg | grep -i npu
输出关键信息:
latex
[ 4.254405] RKNPU fdab0000.npu: Adding to iommu group 0
[ 4.254539] RKNPU fdab0000.npu: RKNPU: rknpu iommu is enabled, using iommu mode
[ 4.256439] [drm] Initialized rknpu 0.9.8 20240828 for fdab0000.npu on minor 1
关键在最后一行:on minor 1。
RK3588 的 NPU 驱动(rknpu)挂在 DRM(Direct Rendering Manager)框架下,不是字符设备,不是 /dev/galcore,而是 DRM 的 render node。公式是:
latex
设备路径 = /dev/dri/renderD(128 + minor)
= /dev/dri/renderD(128 + 1)
= /dev/dri/renderD129
验证一下:
bash
ls -la /dev/dri/
# 能看到 renderD128 (GPU) 和 renderD129 (NPU)
ls /dev | grep -E 'galcore|rknpu'
# 什么都没有 ← 很多人在这里卡住,以为驱动没装
这也解释了为什么很多同学按网上教程找 /dev/galcore 找不到:那是旧版内核或特定发行版的设备名,主线 rknpu 驱动根本不创建这个节点。
二、Kubernetes Device Plugin 机制简介
K8s 通过 Device Plugin API 实现对自定义硬件资源的调度。整个流程如下:
latex
┌─────────────────────────────────────────────────┐
│ Kubernetes 调度层 │
│ │
│ Pod 申请 rk3588.ai/npu: 1 │
│ ↓ │
│ Scheduler 寻找有足够 rk3588.ai/npu 资源的节点 │
│ ↓ │
│ kubelet 调用 Device Plugin 的 Allocate() │
│ ↓ │
│ Plugin 返回: 挂载哪些设备文件、注入哪些环境变量 │
└─────────────────────────────────────────────────┘
每个节点上运行一个 Device Plugin 进程(通过 DaemonSet 部署),它通过 Unix Socket 与 kubelet 通信,实现两件事:
- 上报本节点有多少个该资源
- Pod 调度到本节点时,告诉 kubelet 给容器挂载什么
需要实现的 gRPC 接口:
| 方法 | 作用 |
|---|---|
GetDevicePluginOptions |
返回 plugin 能力选项 |
ListAndWatch |
持续上报设备列表和健康状态 |
Allocate |
容器启动前执行,返回设备挂载/环境变量 |
GetPreferredAllocation |
返回首选分配(可空实现) |
PreStartContainer |
容器启动前钩子(可空实现) |
三、设计决策
资源数量设置为 3
RK3588 的 NPU 有 3 个计算核心(3 TOPS each),但它们共享同一个 DRM render node,内核驱动会自动做核心级别的调度。从 K8s 视角看,我们无法感知单个核心,只能把整颗 NPU 作为调度单元。
将 maxDevices 设为 3,表示该节点同时最多可以运行 3 个使用 NPU 的容器,由内核负责实际的核心分配。这是一个软件层面的并发限制,而非硬件隔离。
设备自动检测
不同板卡、不同内核版本,NPU 的 render node 编号不一定相同。为了代码的通用性,优先通过 sysfs 动态发现:
latex
/sys/class/drm/renderD*/device/driver → 软链接指向驱动目录
包含 "rknpu" → 就是 NPU 设备
找不到时才 fallback 到硬编码路径(renderD129、renderD128、/dev/galcore)。
挂载 /dev/dri 整目录
Allocate 时挂载整个 /dev/dri/ 而非单个 renderD129,原因是 librknnrt.so 在打开设备时可能需要同时访问 card 节点和 render 节点。
四、代码实现
项目结构
latex
rk3588-device-plugin/
├── main.go # 全部逻辑,单文件实现
├── go.mod
├── go.sum
├── vendor/ # 离线依赖(go mod vendor)
├── Dockerfile
└── deploy/
├── daemonset.yaml
├── test-pod.yaml
└── app-example.yaml
go.mod
go
module rk3588-device-plugin
go 1.20
require (
google.golang.org/grpc v1.56.3
k8s.io/kubelet v0.27.4
)
main.go 核心逻辑
设备自动检测
go
// detectNPU 自动检测节点上的 NPU 设备路径
func detectNPU() (devicePath string, ok bool) {
// 优先通过 sysfs 精确匹配 rknpu 驱动
if p := findRknpuRenderD(); p != "" {
log.Printf("[INFO] NPU detected via sysfs: %s", p)
return p, true
}
// Fallback: 检查常见静态路径
for _, p := range []string{
"/dev/dri/renderD129",
"/dev/dri/renderD128",
"/dev/galcore",
} {
if _, err := os.Stat(p); err == nil {
log.Printf("[INFO] NPU detected via static path: %s", p)
return p, true
}
}
return "", false
}
// findRknpuRenderD 遍历 sysfs 找 rknpu 驱动的 render 节点
func findRknpuRenderD() string {
entries, _ := os.ReadDir("/sys/class/drm")
for _, e := range entries {
name := e.Name()
if !strings.HasPrefix(name, "renderD") {
continue
}
driverLink, err := os.Readlink(
filepath.Join("/sys/class/drm", name, "device/driver"))
if err != nil {
continue
}
if strings.Contains(driverLink, "rknpu") {
return filepath.Join("/dev/dri", name)
}
}
return ""
}
上报设备,含健康检查
go
func (m *RK3588NPUPlugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListAndWatchServer) error {
devs := make([]*pluginapi.Device, maxDevices)
for i := 0; i < maxDevices; i++ {
devs[i] = &pluginapi.Device{
ID: fmt.Sprintf("rk3588-npu-%d", i),
Health: pluginapi.Healthy,
}
}
s.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
// 每 30s 做一次健康检查
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
health := pluginapi.Healthy
if _, err := os.Stat(npuDevice); os.IsNotExist(err) {
health = pluginapi.Unhealthy
}
for i := range devs {
devs[i].Health = health
}
s.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
}
return nil
}
五、镜像构建
环境说明
本次在 Windows Docker Desktop 上构建,本地离线镜像:
- 构建阶段:
golang:1.24.2-alpine3.21(arm64) - 运行阶段:
alpine:3.21.3(arm64)
⚠️ 常见误区 :--platform=linux/arm64 只是告诉 buildx 构建目标架构。如果 FROM 的镜像是 amd64 版本的 golang,产出的二进制是 x86 的,在 arm64 节点上根本跑不起来。必须用 arm64 版本的 golang 基础镜像,或者配合 --platform=$BUILDPLATFORM + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 做交叉编译。
Dockerfile
dockerfile
FROM golang:1.24.2-alpine3.21 AS builder
WORKDIR /build
COPY go.mod go.sum ./
COPY main.go .
COPY vendor/ vendor/
# vendor 模式,构建全程离线,不依赖 Go proxy
RUN CGO_ENABLED=0 go build -mod=vendor -ldflags="-s -w" -o rk3588-device-plugin .
FROM alpine:3.21.3
RUN apk add --no-cache ca-certificates
COPY --from=builder /build/rk3588-device-plugin /usr/bin/rk3588-device-plugin
ENTRYPOINT ["/usr/bin/rk3588-device-plugin"]
离线依赖准备
国内环境 proxy.golang.org 基本不通,在构建镜像前先在宿主机用 amd64 的 go 把依赖 vendor 好:
bash
# 用 amd64 golang 容器(不用管架构,vendor 只是下载代码)
docker run --rm \
-v "$(pwd)":/build \
-w /build \
-e GOPROXY=https://goproxy.cn,direct \
golang:1.24.3 \
sh -c "go mod tidy && go mod vendor"
构建镜像
bash
docker buildx build --platform linux/arm64 \
-t your-registry/rk3588-device-plugin:v1.0.1 \
--load .
验证架构:
bash
docker inspect rk3588-device-plugin:v1.0.1 \
--format 'Arch: {{.Architecture}}'
# Arch: arm64 ← 必须是这个
六、部署到集群
节点打标签
bash
# 给所有 3588 节点打标签,后续扩容新节点只需打标签即可
kubectl label node node2 node3 node5 hardware-type=rk3588
DaemonSet
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: rk3588-npu-device-plugin
namespace: kube-system
spec:
selector:
matchLabels:
name: rk3588-npu-device-plugin
template:
metadata:
labels:
name: rk3588-npu-device-plugin
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: hardware-type
operator: In
values:
- rk3588
hostNetwork: true
priorityClassName: system-node-critical
containers:
- name: rk3588-npu-plugin
image: your-registry/rk3588-device-plugin:v1.0.1
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
resources:
requests:
cpu: 50m
memory: 50Mi
limits:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: dev-dri
mountPath: /dev/dri
- name: sys-class-drm
mountPath: /sys/class/drm
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: dev-dri
hostPath:
path: /dev/dri
- name: sys-class-drm
hostPath:
path: /sys/class/drm
bash
kubectl apply -f deploy/daemonset.yaml
七、踩过的坑(重点)
坑 1:设备不是 /dev/galcore
几乎所有教程和 AI 给的答案都是 /dev/galcore。但在使用主线 rknpu 驱动(v0.9.x)的系统上,根本没有这个文件。NPU 走的是 DRM render node,路径是 /dev/dri/renderD129。
定位方法:
bash
dmesg | grep -i "Initialized rknpu"
# 找到 on minor N,设备就是 /dev/dri/renderD(128+N)
坑 2:GetPreferredAllocation 方法缺失导致编译失败
K8s kubelet API v0.27+ 的 DevicePluginServer 接口要求实现 GetPreferredAllocation,不实现会报:
latex
*RK3588NPUPlugin does not implement v1beta1.DevicePluginServer
(missing method GetPreferredAllocation)
这个方法的功能是"让 plugin 推荐优先分配哪些设备ID",对于 NPU 这种不需要感知具体设备的场景,返回空响应即可:
go
func (m *RK3588NPUPlugin) GetPreferredAllocation(
ctx context.Context, req *pluginapi.PreferredAllocationRequest,
) (*pluginapi.PreferredAllocationResponse, error) {
return &pluginapi.PreferredAllocationResponse{}, nil
}
八、验证
1. 确认 Plugin 注册成功
bash
kubectl -n kube-system logs daemonset/rk3588-npu-device-plugin
预期输出:
latex
[INFO] RK3588 NPU Device Plugin starting...
[INFO] NPU detected via static path: /dev/dri/renderD129
[WARN] librknnrt.so not found, container will need it from its own image
[INFO] Plugin started | device=/dev/dri/renderD129 | lib= | count=3
librknnrt.so not found 是正常的,plugin 自身不需要这个库,推理容器镜像需要自带。
2. 确认节点资源
bash
kubectl describe node node5 | grep rk3588
预期输出:

3. 验证调度
yaml
# test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: test-rk3588-npu
spec:
restartPolicy: Never
containers:
- name: test
image: busybox:1.36
command:
- sh
- -c
- |
echo "=== NPU 设备 ==="
ls -la /dev/dri/renderD*
echo "=== 环境变量 ==="
env | grep RKNN
sleep 30
resources:
limits:
rk3588.ai/npu: "1"
requests:
rk3588.ai/npu: "1"
bash
kubectl apply -f test-pod.yaml
kubectl logs test-rk3588-npu
预期在容器内看到 /dev/dri/renderD129 和 RKNN_NPU_DEVICE=/dev/dri/renderD129 环境变量。
九、在推理容器中使用
业务 Pod 只需要在 resources 里声明资源,不需要再写 nodeAffinity,调度器会自动找有 rk3588.ai/npu 资源的节点:
yaml
resources:
limits:
rk3588.ai/npu: "1"
requests:
rk3588.ai/npu: "1"
推理容器镜像需要自带 librknnrt.so,可以从节点的 /usr/lib/librknnrt.so 拷一份到镜像里:
dockerfile
# 在你的推理镜像 Dockerfile 里
COPY librknnrt.so /usr/lib/librknnrt.so
RUN ldconfig
十、第三部分小结
整个过程从"官方无文档"到"资源出现在 Allocatable",核心路径是:
- 先看 dmesg --- 不要假设设备路径,让内核告诉你实际情况
- 理解 K8s Device Plugin 接口 --- 5 个方法,实现完整才能通过编译
- 离网构建 --- go mod vendor + 本地 arm64 基础镜像,避免 proxy 问题
- gRPC 注册路径要准确 --- KubeletSocket 已是完整路径,不要二次拼接
实测效果:3 台 RK3588 节点(node2/3/5)每台各上报 3 个 rk3588.ai/npu 资源,总计 9 个 NPU 资源单元可被 K8s 调度,与昇腾节点的 NPU 资源在同一个集群内统一管理。
全文总结
整套实战走下来,覆盖了国产异构算力上云原生的三个关键阶段:
| 阶段 | 内容 | 关键难点 |
|---|---|---|
| 集群底座 | RK3588 + 昇腾310B 异构 K8s + KubeSphere | 麒麟国防版内核缺模块、ipvs/vxlan 不兼容、flannel 改 host-gw |
| 昇腾调度 | 310B1 device-plugin 源码修复 | MinAICoreNum 硬编码为 8、可执行文件挂载、glibc 兼容 |
| RK3588 调度 | 自研 NPU device plugin | 设备是 DRM render node 非 /dev/galcore、接口强制方法、socket 路径 |
三类国产硬件最终都在同一个 K8s 集群内以统一资源(NPU)的方式被调度,业务 Pod 只需声明资源请求,无需关心底层是昇腾还是 RK3588。
国产 AI 芯片 + 云原生的结合正在快速发展,生态工具也在持续迭代。希望本文的实战记录能为走在这条路上的朋友节省一些排查时间。如果本文对你有帮助,欢迎分享给更多的同行。