本文是一套完整实战的合集,记录了在混合异构(瑞芯微 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 部署过程和国产化环境离线部署。支持 arm64amd64 架构国产操作系统,已适配芯片 + 操作系统如下。

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 部分):

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 项目:

chmod +x create_project_harbor.sh && ./create_project_harbor.sh

注意事项:如果系统提示缺少 iptables,则需要安装先安装 iptables。

4. 创建 k8s 集群

./kt create cluster -f config-sample.yaml -a artifact-arm-k8s12317tar.gz

此命令 kt 会自动将离线制品中的镜像推送到 harbor 私有仓库。

执行后会有如下提示,输入 yes/y 继续执行。

等待最后提示安装成功:

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

需要安装内核模块,再创建 k8s。

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

这里还需要修改 kube-proxykube-flannel 配置:

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 服务,否则系统重启该节点通信失败:

dnf install systemd-resolved
systemctl enable --now systemd-resolved
cat /run/systemd/resolve/resolv.conf

都修改完成后重启服务,等待一会。

查看节点状态

kubectl get nodes -owide

可以看到所有节点状态均已 Ready,共有 3 个管理节点和 3 个工作节点,其中管理节点也充当工作节点。

查看 pod 运行情况

kubectl get pod -A -owide

可以看到所有 pod 已成功运行(ps:以下截图为装完 ks 和插件的截图)

5. 部署 KubeSphere

使用 helm 命令通过私有仓库安装 ks:

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 状态

npu-smi info

驱动正常,芯片健康。

2.2 跑官方 device-plugin 镜像

按照官方文档:https://www.hiascend.com/developer/ascendhub/detail/a592da7bd2ab4dffa8864abd4eac5068

使用 device-plugin:v7.3.1 版本一直报错,即使加入了所有的依赖,最终依然报错 HDC 初始化失败,不论是否使用 Ascend Docker Runtime 都不行。最终决定直接使用 docker 运行 device-plugin 镜像,依然失败:

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

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 崩溃

运行结果:

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 问题现象

错误日志非常明确:

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:入口

// server/manager.go:162
cnt, err := hdm.manager.GetChipAiCoreCount()

调用链 2:DCMI 查询

// 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

// 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:常量定义(问题所在)

// pkg/common/constants.go:340
MinAICoreNum = 8
MaxAICoreNum = 36

3.3 根因总结

问题出在 constants.go 的第 340 行:

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 中,将 MinAICoreNum8 改为 1

// 修复前
MinAICoreNum = 8

// 修复后——310B1 只有 1 个 AI Core
MinAICoreNum = 1

仅需一行代码改动。但问题在于:device-plugin 的编译依赖 CGO(通过 dlopen 动态加载 DCMI 库),而且最终需要运行在 aarch64 架构上。

4.2 多阶段 Dockerfile

我们采用多阶段构建策略:第一阶段用 Go 交叉编译,第二阶段用轻量 Ubuntu 做运行时:

# ===== 阶段 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.21ubuntu:22.04,在国内环境建议通过 DaoCloud 代理拉取:

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 来记录设备信息:

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 核心配置
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 构建镜像

tar -xzf device-plugin-310B1-fixed.tar.gz
bash build.sh
# 输出: Successfully tagged ascend-k8sdeviceplugin:v26.0.0.fixed

5.2 部署到 K8s

kubectl apply -f device-plugin-310B1.yaml

输出:

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 查看日志

kubectl logs -n kube-system -l name=ascend-device-plugin-ds --tail=20

关键日志行:

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 验证节点资源

$ 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

八、第二部分小结

这次实战经历可以总结为三个层次的工作:

  1. 适配层:理解 NPU 驱动(DCMI)和 device-plugin 之间的数据交互协议,发现浮点数格式差异
  2. 源码层:深入 device-plugin 源码,定位 AI Core 数量校验逻辑的硬编码边界,一行代码修复
  3. 工程层:处理 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 里是什么设备

在动手写代码之前,必须先搞清楚设备的实际形态。很多教程直接告诉你路径,但在不同系统版本上路径可能完全不同。

正确的做法是先看内核日志:

dmesg | grep -i npu

输出关键信息:

[    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。公式是:

设备路径 = /dev/dri/renderD(128 + minor)
         = /dev/dri/renderD(128 + 1)
         = /dev/dri/renderD129

验证一下:

ls -la /dev/dri/
# 能看到 renderD128 (GPU) 和 renderD129 (NPU)

ls /dev | grep -E 'galcore|rknpu'
# 什么都没有 ← 很多人在这里卡住,以为驱动没装

这也解释了为什么很多同学按网上教程找 /dev/galcore 找不到:那是旧版内核或特定发行版的设备名,主线 rknpu 驱动根本不创建这个节点。

二、Kubernetes Device Plugin 机制简介

K8s 通过 Device Plugin API 实现对自定义硬件资源的调度。整个流程如下:

┌─────────────────────────────────────────────────┐
│  Kubernetes 调度层                                │
│                                                  │
│  Pod 申请 rk3588.ai/npu: 1                       │
│        ↓                                         │
│  Scheduler 寻找有足够 rk3588.ai/npu 资源的节点     │
│        ↓                                         │
│  kubelet 调用 Device Plugin 的 Allocate()         │
│        ↓                                         │
│  Plugin 返回: 挂载哪些设备文件、注入哪些环境变量    │
└─────────────────────────────────────────────────┘

每个节点上运行一个 Device Plugin 进程(通过 DaemonSet 部署),它通过 Unix Socket 与 kubelet 通信,实现两件事:

  1. 上报本节点有多少个该资源
  2. 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 动态发现:

/sys/class/drm/renderD*/device/driver → 软链接指向驱动目录
                                         包含 "rknpu" → 就是 NPU 设备

找不到时才 fallback 到硬编码路径(renderD129renderD128/dev/galcore)。

挂载 /dev/dri 整目录

Allocate 时挂载整个 /dev/dri/ 而非单个 renderD129,原因是 librknnrt.so 在打开设备时可能需要同时访问 card 节点和 render 节点。

四、代码实现

项目结构

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

module rk3588-device-plugin

go 1.20

require (
    google.golang.org/grpc v1.56.3
    k8s.io/kubelet v0.27.4
)

main.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 ""
}
上报设备,含健康检查
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

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 好:

# 用 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"

构建镜像

docker buildx build --platform linux/arm64 \
  -t your-registry/rk3588-device-plugin:v1.0.1 \
  --load .

验证架构:

docker inspect rk3588-device-plugin:v1.0.1 \
  --format 'Arch: {{.Architecture}}'
# Arch: arm64  ← 必须是这个

六、部署到集群

节点打标签

# 给所有 3588 节点打标签,后续扩容新节点只需打标签即可
kubectl label node node2 node3 node5 hardware-type=rk3588

DaemonSet

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
kubectl apply -f deploy/daemonset.yaml

七、踩过的坑(重点)

坑 1:设备不是 /dev/galcore

几乎所有教程和 AI 给的答案都是 /dev/galcore。但在使用主线 rknpu 驱动(v0.9.x)的系统上,根本没有这个文件。NPU 走的是 DRM render node,路径是 /dev/dri/renderD129

定位方法:

dmesg | grep -i "Initialized rknpu"
# 找到 on minor N,设备就是 /dev/dri/renderD(128+N)

坑 2:GetPreferredAllocation 方法缺失导致编译失败

K8s kubelet API v0.27+ 的 DevicePluginServer 接口要求实现 GetPreferredAllocation,不实现会报:

*RK3588NPUPlugin does not implement v1beta1.DevicePluginServer
(missing method GetPreferredAllocation)

这个方法的功能是"让 plugin 推荐优先分配哪些设备ID",对于 NPU 这种不需要感知具体设备的场景,返回空响应即可:

func (m *RK3588NPUPlugin) GetPreferredAllocation(
    ctx context.Context, req *pluginapi.PreferredAllocationRequest,
) (*pluginapi.PreferredAllocationResponse, error) {
    return &pluginapi.PreferredAllocationResponse{}, nil
}

八、验证

1. 确认 Plugin 注册成功

kubectl -n kube-system logs daemonset/rk3588-npu-device-plugin

预期输出:

[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. 确认节点资源

kubectl describe node node5 | grep rk3588

预期输出:

3. 验证调度

# 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"
kubectl apply -f test-pod.yaml
kubectl logs test-rk3588-npu

预期在容器内看到 /dev/dri/renderD129RKNN_NPU_DEVICE=/dev/dri/renderD129 环境变量。

九、在推理容器中使用

业务 Pod 只需要在 resources 里声明资源,不需要再写 nodeAffinity,调度器会自动找有 rk3588.ai/npu 资源的节点:

resources:
  limits:
    rk3588.ai/npu: "1"
  requests:
    rk3588.ai/npu: "1"

推理容器镜像需要自带 librknnrt.so,可以从节点的 /usr/lib/librknnrt.so 拷一份到镜像里:

# 在你的推理镜像 Dockerfile 里
COPY librknnrt.so /usr/lib/librknnrt.so
RUN ldconfig

十、第三部分小结

整个过程从"官方无文档"到"资源出现在 Allocatable",核心路径是:

  1. 先看 dmesg — 不要假设设备路径,让内核告诉你实际情况
  2. 理解 K8s Device Plugin 接口 — 5 个方法,实现完整才能通过编译
  3. 离网构建 — go mod vendor + 本地 arm64 基础镜像,避免 proxy 问题
  4. 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 芯片 + 云原生的结合正在快速发展,生态工具也在持续迭代。希望本文的实战记录能为走在这条路上的朋友节省一些排查时间。如果本文对你有帮助,欢迎分享给更多的同行。

Logo

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

更多推荐