项目已开源:下载地址
一个 Kubernetes Device Plugin,把瑞芯微 RK3588 / RK3588S 的 NPU 变成像 CPU、内存一样可调度的资源。Apache-2.0,已在麒麟 V10 国防版异构集群中验证。
引子
一块 RK3588 开发板,跑 YOLOv8 的 INT8 模型,NPU 上 40 FPS 以上,整机功耗不到 8 瓦。单机玩,这颗芯片没什么好挑的。
但板子从一块变成五块,你必然要把它塞进 Kubernetes——要灰度、要配额、要和服务器上的其他业务共用一套运维体系。然后报错就来了:
Error: failed to create containerd task:
... open /dev/dri/renderD129: no such file or directory
诡异的是,这个 Pod 在 A 节点跑得好好的,换台机器就起不来。kubectl describe pod 一看,它被调度到一台 x86 服务器上了。
Kubernetes 根本不知道你的集群里有"NPU"这种东西。 在调度器眼里,一台 RK3588 和一台普通 ARM 服务器没有任何区别。
你只能退而求其次:写 nodeAffinity、绑 hostname、手工维护一份"哪台机器上有板子"的清单。
三块板子你记得住,三十块呢?清单总会过期——而且过期的时候,通常是凌晨。
一、先弄清楚
要让它被调度,第一步得知道它在操作系统这一层到底是什么。
RK3588 的 NPU 是瑞芯微自研的三核架构,8nm 制程,峰值 6 TOPS INT8 算力,支持 INT4 / INT8 / INT16 / FP16 / BF16 混合精度。三个核心可以拆任务并行——多路视频流同时做检测、分割、分类,这是它能在中端边缘算力盒子里站住的原因。
但它在 Linux 里的表现形式,和大多数人想的不一样。
它挂在 DRM(Direct Rendering Manager)框架下,以一个 render node 的形式存在。
/dev/dri/renderD129
这里必须纠正一个流传很广的说法。
网上不少教程让你去找 /dev/galcore,说那是 NPU 的设备节点,找不到就是驱动没装好。主线 rknpu 驱动不创建这个节点。 按这个思路排查,你只会在错误的方向上越走越远——驱动明明装得好好的,你却以为它没装。
正确的做法是读内核日志:
dmesg | grep -i npu
关键在这一行的结尾:
[ 4.256439] [drm] Initialized rknpu 0.9.8 20240828 for fdab0000.npu on minor 1
DRM render node 的命名规则是 renderD(128 + minor)。这里 minor 是 1,所以:
/dev/dri/renderD(128 + 1) = /dev/dri/renderD129
验证一下:
ls -la /dev/dri/
# renderD128 是 GPU,renderD129 是 NPU
顺手再确认一件事:
ls /dev | grep -E 'galcore|rknpu'
# 预期结果:什么都没有。这不代表驱动没装。
一句话总结:RK3588 的 NPU,在操作系统眼里就是一个"长得像显卡"的设备文件。
记住这一点,后面的事情就都好解释了——包括 Kubernetes 为什么看不见它。
二、K8s 为什么"看不见"它
Kubernetes 原生只认三种资源:CPU、内存、临时存储。
所有其他资源——GPU、FPGA、网卡 SR-IOV,以及今天要说的 NPU——都必须通过一个叫 Device Plugin(设备插件) 的机制,才能接进调度体系。
这个机制不复杂,一个比喻就能讲清楚:
设备插件,就是节点上的仓库管理员。
它的工作流程是这样:
- 报到。管理员把联络窗口(一个 Unix Socket)放到 kubelet 指定的目录里,然后告诉 kubelet:我是管 NPU 的。
- 报库存。管理员持续上报:"本节点有 3 个单位的 NPU 可用。"
- 被调度。kube-scheduler 看到库存,就会在调度时,把声明了"我要 1 个 NPU"的 Pod 放到这台机器上。
- 发货。Pod 真正启动前,kubelet 找管理员办手续:要发给哪个容器、挂载哪些设备文件、注入哪些环境变量。
对应到代码里,是 5 个 gRPC 接口:注册、GetDevicePluginOptions、ListAndWatch、Allocate、PreStartContainer,再加上 Kubernetes 1.27 之后强制要求实现的 GetPreferredAllocation(少了这个,编译直接不过)。
现在问题来了:这个"仓库管理员",RK3588 没有。
瑞芯微的官方仓库——airockchip/rknn-toolkit2、rknn-llm、rknn_model_zoo——提供的是模型转换工具、推理运行时、示例模型。它们解决的是"怎么把模型跑起来";而"怎么让编排系统知道 NPU 的存在",官方至今没有给任何实现。
这不是我一个人的判断。从生态的另一头看,结论更直接。
主流的异构算力调度方案 HAMi(已经进了 CNCF Incubating)覆盖的加速卡名单里,有 NVIDIA、AMD、昇腾、寒武纪、海光、壁仞、摩尔线程、昆仑芯……没有瑞芯微。
换句话说,在一颗出货量这么大的 SoC 上,云原生这一层是空的。
三、社区方案和它们的坑
开源社区当然不会完全空白。我翻了 GitHub,相关项目一共 5 个。逐个看下来,有三个坑反复出现。
坑一:设备号写死。
elct9620/rknpu-device-plugin 这个项目写得其实挺清楚,用的是 kubevirt 的 dpm 框架,MIT 协议。但它把 /dev/dri/renderD129 直接硬编码在代码里。
硬编码意味着什么?——你的板子只要不是 renderD129,它就不认。
而 renderD129 这个编号只是一个巧合:它是"NPU 在 DRM 子系统里第二个注册的设备"。换个内核版本、换个板型,甚至 HDMI 输出的初始化顺序变一下,这个编号都可能变。写死它,等于给代码绑了一个会漂移的锚。
坑二:只上报 1 个设备,而且不做健康检查。
同一个项目上报的可分配单元是 1。这意味着不管板子上有几个 NPU 核心,同一时刻只能有 1 个容器使用 NPU,粒度粗得有点可惜。
更麻烦的是它的 ListAndWatch——发完一次清单就 select{} 永久阻塞了。这带来一个隐蔽的运维风险:如果设备节点因为驱动异常掉了,kubelet 完全不知道,还会继续往这台机器上调 Pod,然后业务在启动时报错。
坑三:要么需要特权,要么绑死特定发行版。
akolk/ai-cluster 那一套要求业务 Pod 直接开 privileged: true,等于把宿主机全部权限交给容器,很多生产环境过不了安全审计。
schwankner/talos-rk3588-npu 用 CDI 方案解决了免特权注入的问题,架构是目前最现代的——但它明确要求 Talos Linux + 主线内核 6.18 以上。如果你的生产环境是麒麟 V10、openEuler 这类信创系统,这条路走不通。
(补充一句:antonioacg/rknpu-rk3588 经常被误认为设备插件,它其实是内核模块的 DKMS 移植,跟 K8s 调度没关系。)
所以,我需要的那样东西——动态识别设备、有真实健康检查、不绑死特定发行版、能在信创环境跑——社区里没有现成的。
那就自己写。
四、三条约束和三个设计
动手之前,我先定了三条约束:不硬编码、不做假上报、不绑框架。
设计一:不写门牌号,改查身份证。
设备发现走 sysfs 动态匹配,而不是写死路径。
原理很简单:去问内核——"这些 render node 里,哪个的驱动是 rknpu?"
/sys/class/drm/renderD*/device/driver
这个路径是一个软链接,指向驱动目录。遍历所有 renderD*,谁的驱动软链接里带 rknpu,谁就是 NPU。只有全都匹配不上时,才退回到静态路径兜底。
区别在哪?
写死设备号,是按门牌号找人——门牌号会变。查驱动匹配,是按身份证找人——走到哪都认得。
换个板型、换个内核,哪怕 NPU 不再是第二个注册的 DRM 设备,它都能找对。
设计二:上报 3 个单元——但我必须说清楚它到底是什么。
这里得诚实一点,因为它极容易被误解。
RK3588 有 3 个 NPU 核心。但这 3 个核心共享同一个 DRM render node——从 Kubernetes 的视角看,它只能看到"一个设备",看不到里面的三个核心。
所以插件上报的 3 个单元,准确的说法是:
K8s 侧的并发上限,不是硬件隔离。
一个容器拿到 1 个单元,含义是"这台机器最多同时有 3 个容器用 NPU";它不代表这个容器独占了一个核心,也不代表算力被切分隔离了。真正的核心级调度,是内核驱动在做。
这一点特别容易和 GPU 的 HAMi 混淆。HAMi 做的是真正的显存和算力切分,我这里只是限流——两件事性质完全不同。如果你冲着"把 NPU 切成三份、每份 2 TOPS"来的,这个项目做不到,硬件层面就没有这个口子。
设计三:30 秒一次的健康检查。
ListAndWatch 每 30 秒复查一次设备节点是否还在。如果不在了,就把设备状态标记为 Unhealthy,kubelet 就不会再把新的 Pod 调度过来。
就这么简单一件事,前面提到的竞品没做。但实际运维里,驱动异常、设备被摘掉的情况是会发生的——你需要系统知道这件事,而不是等业务 Pod 起来报错、再人肉定位。
除了这三条,还有两个工程上的选择,是为信创场景专门做的:
- 离线 vendor 构建。所有依赖
go mod vendor进仓库,CGO_ENABLED=0静态编译。信创环境经常是纯内网,拉不到 Go proxy 就构建不了镜像——这一条是刚需,不是洁癖。 - 单文件、零框架。全部逻辑放在一个
main.go里,不引入 device-plugin-manager 这类封装。好处是排查问题能直接读源码,想改资源名也就改一个常量的事。
最终镜像基于 alpine,大概 15 MB。
五、四步跑起来
前提:先确认节点上真的有 NPU(dmesg | grep -i npu 能看到 Initialized rknpu)。
第一步,构建镜像。
docker buildx build --platform linux/arm64 \
-t your-registry/rk3588-device-plugin:v1.0.0 \
--load .
这里有个坑得提醒一句:--platform=linux/arm64 只是告诉 buildx 目标架构。如果你的 FROM 基础镜像是 amd64 的,产出的二进制依然是 x86,在板子上根本跑不起来。仓库里的 Dockerfile 已经用 arm64 基础镜像配 CGO_ENABLED=0 处理好了。
第二步,给节点打标签。
kubectl label node node2 node3 node5 hardware-type=rk3588
为什么必须打标签?因为插件在检测不到 NPU 时会主动退出(这是有意设计,避免上报假资源)。如果不限定节点范围,非 RK3588 节点上的 Pod 会反复重启,日志里全是无意义的 CrashLoop。
第三步,部署。
改掉 deploy/daemonset.yaml 里的镜像地址,然后:
kubectl apply -f deploy/daemonset.yaml
kubectl -n kube-system rollout status daemonset/rk3588-npu-device-plugin
第四步,验证。
kubectl -n kube-system logs daemonset/rk3588-npu-device-plugin
kubectl describe node node2 | grep rk3588
看到这一行,就说明成功了:
rk3588.ai/npu: 3
之后,业务 Pod 拿 NPU 只需要在 resources.limits 里声明一行:
resources:
limits:
rk3588.ai/npu: "1"
不需要 nodeAffinity,不需要知道底下是哪台机器,也不用关心调度器到底挑了哪块板子。
这就是设备插件真正的价值——它把"哪台机器有什么硬件"这件事,从部署配置里彻底拿掉了,交还给调度器。
六、几个限制
我不想把它写成一个万能方案。以下几点,用之前请先确认。
1. 3 个单元不是硬件隔离。 前面反复说过了,这里再强调一次,因为它是最容易被误解的一点。
2. librknnrt.so 要你自己的业务镜像带。 它是 RKNN 的运行时库,不属于设备插件的职责范围。插件只负责把设备节点交给你,不会帮你装库。可以从官方 SDK 里取,也可以直接从板子上拷。
3. 某些接口可能仍然需要 privileged: true。 如果你用的是 rknn-toolkit2 里会去读 /proc/device-tree/compatible 判断 SoC 型号的那些接口,业务 Pod 大概率需要特权模式——因为 Kubernetes 默认会把容器里的 /proc 遮蔽掉,这个路径读不到。
这里要标注一下:这一条是参考社区项目的经验总结,我没有在麒麟 V10 上逐一验证过每一种接口,所以不下死结论。如果你遇到"无法识别平台",优先往这个方向排查。
4. 没有实现 CDI。 CDI 是更现代的免特权注入方式,但它依赖 Talos + 主线内核 6.18+,与信创系统现状冲突。这一点放在 Roadmap 里,等内核版本跟上了再说。
5. 资源名 rk3588.ai/npu 是自定义字符串,不是真实注册的域名。你的组织如果有内部域名规范,改一个常量就行。
结尾
说回开头那三块板子。
现在它们和一台装了昇腾 310B 的服务器,在同一个 Kubernetes 集群里(麒麟 V10 国防版 + openEuler 22.03,K8s v1.23.17 + KubeSphere 4.1.3)。两种国产 NPU,走同一套调度逻辑:Pod 声明自己要什么算力,调度器自己找地方。
我觉得这件事的意义,比"多了一个设备插件"要大一些。
在国产芯片的生态里,最缺的往往不是算力,而是"让算力被上层编排系统看见"的那一层胶水。
芯片厂商把驱动做出来了,把 SDK 做出来了,把工具链做出来了。但从 SDK 到"能在生产集群里被调度、被配额、被监控、被运维",中间还有一段路。
这段路不算长,但它不属于任何一家厂商的 KPI,所以常常没人走。
这个项目就是我走的那一段。已经在信创环境和异构集群里跑起来了,代码开源,Apache-2.0 协议。
github.com/gjing1st/rk3588-device-plugin
如果你手上也有 RK3588,或者正在做信创环境的算力调度,欢迎拿去试试。项目里我特意标注了几处"未验证"的地方——那些是从社区经验里引用的结论,我没有在麒麟上实测过,欢迎有条件的同学帮我补上,也欢迎提 Issue 指正。
如果它帮你省下了几天排查时间,一个 Star 就是最直接的反馈。
评论区