目 录CONTENT

文章目录

异构算力实战:RK3588 + 昇腾 310B 部署 K8s + KubeSphere 并实现 NPU 统一调度

天行1st
2026-08-23 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

本文是一套完整实战的合集,记录了在混合异构(瑞芯微 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
master1rk3588arm64麒麟V10国防版8核8G192.168.37.14
master2rk3588arm64麒麟V10国防版8核8G192.168.37.16
master3rk3588arm64麒麟V10国防版8核8G192.168.37.18
node1310Barm64欧拉22.034核12G192.168.37.12
node2rk3588arm64麒麟V10国防版8核8G192.168.37.20
node3rk3588arm64麒麟V10国防版8核8G192.168.37.22
node4310Barm64欧拉22.034核12G192.168.37.16
node5rk3588arm64麒麟V10国防版8核8G192.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)
DCMI24.1.rc1
CANN8.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 和内存一样被调度使用了。

六、踩坑要点总结

在本次实战中,我们踩过的主要坑和对应的解决方案:

#踩坑点现象解决方案
1MinAICoreNum = 8invalid ai core num 1.000000修改源码为 MinAICoreNum = 1
2可执行文件挂载为目录MountVolume 失败type: File + readOnly: true
3/dev/shm 用 emptyDirdm_send_msg fail:2改为 hostPath(共享内存文件在宿主机上)
4容器 glibc 不兼容SIGBUS 退出码 135用宿主 ld-linux-aarch64.so.1 前缀启动
5RBAC 权限不足cannot get resource "nodes"创建 SA + ClusterRole + ClusterRoleBinding
6ConfigMap 权限缺失cannot update resource "configmaps"ClusterRole 添加 configmaps CRUD 权限
7kubelet API 无法访问host ip is invalid设置 NODE_IP env var (status.hostIP)
8Golang 基础镜像拉取慢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/3control-planeRK3588
node1, node4worker昇腾设备有官方 device plugin,需要二开
node2, node3, node5workerRK3588无官方支持

昇腾那边有华为官方的 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 芯片 + 云原生的结合正在快速发展,生态工具也在持续迭代。希望本文的实战记录能为走在这条路上的朋友节省一些排查时间。如果本文对你有帮助,欢迎分享给更多的同行。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区