1. 缘起一块被“闲置”的算力手里有一块 RK3588 的板子6 TOPS 的 NPU 算力跑 YOLOv8 推理能到几十帧功耗还低得感人。这东西放在边缘侧做视觉推理性价比几乎是无敌的。但问题来了——当你手里不止一块板子而是五块、十块、甚至一个机柜的时候怎么管我最初的方案很土每块板子 SSH 上去手动nohup起一个推理服务然后在上层用 Nginx 做反向代理靠配置文件里的 IP 列表轮询。这套东西跑三块板子还行跑到第八块的时候我就崩溃了——某块板子 NPU 被一个僵尸进程占着新任务派过去直接超时另一块板子温度过高降频推理延迟从 30ms 飙到 200ms但负载均衡器完全不知道。更别提板子固件升级重启之后IP 变了我得手动改配置。这时候我就在想K8s 管 CPU 和内存管得那么顺管 GPU 也有成熟的 device plugin 方案为什么 RK3588 的 NPU 不行去翻了一圈资料发现现实是这样的瑞芯微官方提供了 RKNN Toolkit2提供了librknnrt.so运行时也提供了rknn_server这个常驻进程来做远程推理。但官方从头到尾没有提供任何 K8s 层面的集成方案。社区里搜“RK3588 NPU K8s”出来的要么是“如何交叉编译 RKNN”要么是“K8s 调用 GPU 的通用教程”真正把这两件事接起来的几乎没有。所以这篇文章要干的事很明确把 RK3588 的 NPU 做成 K8s 里一种可调度、可监控、可配额管理的资源。做完之后你kubectl describe node能看到rockchip.com/npu: 3Pod 里写resources.limits就能申请 NPU调度器会自动把 Pod 放到有空闲 NPU 的节点上。这套东西适合谁适合正在做边缘 AI 集群、异构算力调度、或者单纯想把手里几块 RK3588 板子管起来的工程师。不需要你是 K8s 专家但得能看懂 YAML会写点 Go 或者 Python。下面我把整个思路、踩过的坑、以及最终能跑的方案完整拆一遍。2. 整体设计为什么是 Device Plugin 而不是别的路子2.1 先搞清楚 K8s 扩展资源的三种姿势在 K8s 里让一个自定义硬件被调度理论上有多条路可以走但每条路的代价和适用场景完全不同。第一条路是 Extended Resource。你直接在 Node 的 status 里声明rockchip.com/npu: 3然后在 Pod 里 request 这个资源。K8s 调度器原生就认识这种带域名前缀的资源名不需要写任何代码。听起来很美好对吧但问题在于谁来自动上报这个资源谁来做设备分配Extended Resource 只解决了“声明”和“调度”两个环节设备发现、健康检查、分配隔离这些事全得你自己想办法。如果你手动改 Node status那板子重启或者 NPU 挂了状态就失真了。第二条路是 Device Plugin。这是 K8s 官方为 GPU、FPGA、网卡这类设备设计的标准扩展机制。核心流程是kubelet 启动时通过 gRPC 连接你部署的 device pluginplugin 调用ListAndWatch把设备列表推给 kubeletkubelet 再把这些设备作为 Extended Resource 上报到 Node status。Pod 调度到节点后kubelet 调用 plugin 的Allocate接口plugin 返回设备路径和环境变量容器运行时据此把设备挂进容器。设备发现、上报、分配、健康检查全链路都覆盖了。第三条路是 DRADynamic Resource Allocation。这是 K8s 1.26 之后引入的新机制用ResourceClaim和ResourceClass来描述资源请求比 Device Plugin 灵活得多支持复杂拓扑和细粒度属性。但 DRA 在 1.30 之前都还是 alpha/beta 状态边缘场景下用的 K3s 或者老版本 K8s 未必支持而且学习成本高。我最终选的是Device Plugin。理由很直接RK3588 的 NPU 本质上是一个固定数量的离散设备通常是 1 个但可以通过多进程分时复用虚拟成多个没有复杂的拓扑关系也不需要动态属性匹配。Device Plugin 的成熟度最高K3s、KubeEdge 这些边缘方案都原生支持社区案例也多。DRA 那套东西留给以后真有复杂需求的时候再说。2.2 RK3588 NPU 的特殊性它不是 GPU这里必须说清楚一个关键差异否则后面的设计会走偏。GPU 做 K8s device plugin 的时候NVIDIA 那套方案是把整块 GPU 或者 MIG 切片分配给容器容器里能直接跑 CUDA。但 RK3588 的 NPU 不一样——它没有用户态的直接访问接口。你不能在容器里open(/dev/npu)然后开始算。瑞芯微的设计是所有 NPU 推理请求都通过librknnrt.so这个用户态库发给一个叫rknn_server的常驻进程由这个 server 统一管理 NPU 硬件。这个架构带来两个后果第一NPU 的“设备节点”其实不是真正的设备节点。/dev/rknpu或者类似的字符设备在标准固件里可能根本不存在真正干活的是rknn_server这个进程。所以 device plugin 的Allocate返回的 device spec 里挂载的不能只是设备文件还得考虑怎么让容器里的进程能连上宿主机的rknn_server。第二NPU 天然支持多进程分时复用。rknn_server内部有任务队列多个进程同时发推理请求它会排队处理。这意味着我们可以把一块物理 NPU 虚拟成多个“逻辑 NPU”分配给不同 Pod只要接受一定的性能折损。这个特性对于提高利用率非常关键——毕竟边缘场景下不是每个 Pod 都能把 6 TOPS 跑满。所以我的设计目标是把一块物理 NPU 虚拟成 N 个可调度单元每个单元通过 device plugin 分配给 PodPod 内通过共享的 rknn_server 做推理。N 取多少后面会讲这是个需要实测调优的参数。2.3 整体架构长什么样整个方案由四个组件构成我画不了图用文字描述清楚第一个是 rknn_server跑在宿主机上由 systemd 管理监听一个 Unix Domain Socket 或者 TCP 端口。它是 NPU 硬件的唯一管理者所有推理请求都经过它。第二个是 device plugin以 DaemonSet 形式部署每个 RK3588 节点跑一个 Pod。它启动后做两件事一是探测本机 NPU 是否可用通过尝试连接 rknn_server 或者读取特定文件二是把虚拟出来的 N 个 NPU 单元通过 gRPC 上报给 kubelet。当有 Pod 申请 NPU 时它的Allocate接口返回一个挂载了 rknn_server socket 的 volume以及一个环境变量告诉容器“你分到的是第几号逻辑 NPU”。第三个是调度层这部分 K8s 原生就干了。device plugin 上报之后rockchip.com/npu就成了一个标准的 Extended Resource调度器自动处理。第四个是监控层用 Prometheus 采集 NPU 的利用率、温度、内存占用Grafana 出图。这部分官方也没现成的需要自己写 exporter。下面逐个拆。3. 核心细节Device Plugin 到底怎么写3.1 gRPC 接口的最小实现K8s Device Plugin 的接口定义在k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1这个包里。你不需要实现全部方法核心就四个GetDevicePluginOptions返回插件支持哪些可选功能一般返回空就行。ListAndWatch这是最重要的。插件通过这个流式接口把设备列表推给 kubelet设备状态变化时也要推。kubelet 靠这个知道节点上有多少可用设备。AllocatePod 调度到节点后kubelet 调用这个接口插件返回设备挂载信息和环境变量。GetPreferredAllocation可选用于多设备场景下的优选分配边缘场景一般用不上。我用 Go 写的因为官方示例都是 Go而且 gRPC 的 Go 生态最成熟。核心结构体大概长这样type NpuDevicePlugin struct { socketPath string resourceName string deviceCount int devices []*pluginapi.Device server *grpc.Server }deviceCount就是虚拟出来的逻辑 NPU 数量。devices是一个列表每个元素有ID、Health、Topology三个字段。ID 我用了npu-0、npu-1这种格式Health 初始都是Healthy。ListAndWatch的实现逻辑是先发一次全量设备列表然后进入一个循环定期检查每个设备的健康状态。如果发现某个设备不健康了比如 rknn_server 挂了就更新状态再推一次。这里有个坑kubelet 对 ListAndWatch 的首次响应有超时限制默认好像是 30 秒还是多少如果你在首次响应前做了太重的初始化比如等 rknn_server 启动kubelet 会认为插件挂了然后反复重启。我的做法是插件启动后立刻返回设备列表健康检查放到后台 goroutine 里异步做。3.2 设备发现的正确姿势怎么判断本机有没有 NPU最直接的方法是检查/dev/rknpu或者/sys/kernel/debug/rknpu是否存在。但前面说了不同固件版本这些路径可能不一样。更可靠的方法是尝试连接 rknn_server。rknn_server 默认监听localhost:9999这个 TCP 端口不同版本可能不同我用的固件是 9999。插件启动时用一个带超时的 TCP dial 去连这个端口连上了就认为 NPU 可用。这个方法的优点是它检测的是“NPU 服务是否真的能用”而不是“设备文件是否存在”。设备文件存在但 server 没起来的情况太常见了。但这里有个细节device plugin 跑在容器里怎么连宿主机的 rknn_server两个办法。一是用hostNetwork: true让 Pod 共享宿主机网络命名空间直接连127.0.0.1:9999。二是挂载宿主机的网络或者用 Unix Socket。我选了 hostNetwork简单粗暴边缘场景下网络隔离本来就没那么严格。设备数量怎么定我一开始想的是读/proc/device-tree里的 NPU 节点信息但发现不同内核版本差异太大。后来干脆做成可配置的通过环境变量NPU_VIRTUAL_COUNT指定默认 3。为什么默认 3后面性能测试那节会详细说。3.3 Allocate 接口的返回值设计Allocate是真正决定 Pod 里能不能用 NPU 的地方。它接收一个AllocateRequest里面包含 kubelet 要求分配的设备 ID 列表。插件需要返回一个AllocateResponse里面最重要的是ContainerAllocation列表每个元素对应一个容器的挂载配置。我的返回值包含三部分第一部分是 mounts。把宿主机的/var/run/rknn目录里面放着 rknn_server 的 Unix Socket如果有的话挂到容器里。如果用的是 TCP 连接这一步可以省但挂一个空目录也无妨方便以后扩展。第二部分是 envs。设置RKNN_SERVER_IP127.0.0.1和RKNN_SERVER_PORT9999告诉容器里的 RKNN 运行时去哪里找 server。再设置一个NPU_DEVICE_IDnpu-0这个主要是给业务代码看的方便做日志追踪。第三部分是 devices。如果宿主机真的有/dev/rknpu设备节点就在这里声明。没有的话就留空。我实测的固件里这个节点不存在所以留空也能跑。这里有个大坑Allocate返回的 mounts 里HostPath必须是宿主机上真实存在的路径否则 kubelet 会报错。我一开始想挂/dev/rknpu结果宿主机上没这个文件Pod 一直起不来报failed to mount。后来改成挂一个确定存在的目录问题解决。3.4 健康检查与故障自愈Device Plugin 的健康检查机制是这样的插件在ListAndWatch里推送设备状态如果某个设备变成Unhealthykubelet 会把它从可分配资源里剔除已经分配出去的 Pod 不会被驱逐但新的 Pod 不会再调度到这个设备上。我的健康检查逻辑是每 10 秒尝试连接一次 rknn_server连续失败 3 次就把所有设备标记为Unhealthy恢复后重新标记为Healthy。这个阈值需要根据实际场景调——太敏感会导致网络抖动时误判太迟钝会导致故障 Pod 一直占着资源。但这里有个问题如果 rknn_server 挂了已经跑着的 Pod 怎么办K8s 不会自动重启它们因为从 K8s 视角看Pod 本身没挂。我的做法是在业务容器里加一个 liveness probe定期调用一个健康检查接口这个接口内部会尝试做一次极轻量的 NPU 推理比如跑一个 1x1 的卷积失败就返回非 200让 K8s 重启容器。这样虽然不能恢复 rknn_server但至少能让业务感知到问题。更彻底的做法是用一个 sidecar 或者 init container 来管理 rknn_server 的生命周期但这超出了 device plugin 的职责范围属于节点初始化的工作。我是在节点上用一个 systemd unit 来保证 rknn_server 挂了自动重启配合 device plugin 的健康检查基本能做到自愈。4. 实操过程从零到 Pod 跑起来4.1 节点侧准备rknn_server 和运行时假设你已经有一块烧好 Ubuntu 20.04 的 RK3588 板子并且 K8s 或者 K3s 已经跑起来了。第一步是确保 NPU 驱动和运行时正常。先检查内核模块lsmod | grep rknpu如果没有输出说明 NPU 驱动没加载。RK3588 的 NPU 驱动通常是编译进内核的但有些固件版本需要手动modprobe rknpu。加载之后检查/sys/kernel/debug/rknpu目录是否存在里面会有version、load等信息。然后安装 RKNN 运行时。瑞芯微的librknnrt.so和rknn_server通常包含在rknn-toolkit2的 runtime 包里。我用的版本是 1.5.2从官方仓库下载rknpu2的 runtime 包解压后把librknnrt.so放到/usr/lib/把rknn_server放到/usr/bin/。启动 rknn_serverrknn_server 或者写成 systemd unit[Unit] DescriptionRKNN Server Afternetwork.target [Service] ExecStart/usr/bin/rknn_server Restartalways RestartSec5 [Install] WantedBymulti-user.target验证 server 是否正常ss -tlnp | grep 9999应该能看到 rknn_server 监听在 9999 端口。注意不同固件版本的 rknn_server 默认端口可能不同有的用 9999有的用 5000 或者别的。用ss命令确认实际端口然后在 device plugin 里配置对应的环境变量。4.2 编译和部署 Device PluginDevice Plugin 的代码我放在一个 Go module 里核心依赖是k8s.io/kubelet和google.golang.org/grpc。编译成静态二进制CGO_ENABLED0 GOOSlinux GOARCHarm64 go build -o npu-device-plugin .然后打成 Docker 镜像。基础镜像用arm64v8/alpine或者arm64v8/debian-slim都行我用的 alpine镜像大小不到 20MB。FROM arm64v8/alpine:3.18 COPY npu-device-plugin /usr/bin/ ENTRYPOINT [/usr/bin/npu-device-plugin]DaemonSet 的 YAML 关键部分apiVersion: apps/v1 kind: DaemonSet metadata: name: npu-device-plugin namespace: kube-system spec: selector: matchLabels: name: npu-device-plugin template: metadata: labels: name: npu-device-plugin spec: hostNetwork: true tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule containers: - name: npu-device-plugin image: your-registry/npu-device-plugin:latest env: - name: NPU_VIRTUAL_COUNT value: 3 - name: RKNN_SERVER_PORT value: 9999 volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: rknn-socket mountPath: /var/run/rknn volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: rknn-socket hostPath: path: /var/run/rknn type: DirectoryOrCreate几个关键点hostNetwork: true让插件能连宿主机的 rknn_server/var/lib/kubelet/device-plugins是 kubelet 和插件通信的 socket 目录必须挂载/var/run/rknn是我自己创建的目录用来放可能的 Unix SocketDirectoryOrCreate保证不存在时自动创建。部署kubectl apply -f npu-device-plugin.yaml然后检查kubectl get pods -n kube-system -l namenpu-device-plugin kubectl describe node your-node | grep rockchip如果一切正常你应该能看到rockchip.com/npu: 3。4.3 业务 Pod 怎么申请 NPU业务 Pod 的 YAML 里这样写apiVersion: v1 kind: Pod metadata: name: yolov8-inference spec: containers: - name: inference image: your-registry/yolov8-rknn:latest resources: limits: rockchip.com/npu: 1 env: - name: RKNN_SERVER_IP value: 127.0.0.1 - name: RKNN_SERVER_PORT value: 9999注意resources.limits里写rockchip.com/npu: 1不要写requests因为 Extended Resource 的 requests 和 limits 必须相等写一个就行。容器里的业务代码用 RKNN Python SDK 或者 C SDK 连接 rknn_server。Python 示例from rknnlite.api import RKNNLite rknn RKNNLite() rknn.load_rknn(yolov8n.rknn) rknn.init_runtime(targetrk3588, rknn_server_ip127.0.0.1, rknn_server_port9999)这里有个细节init_runtime的时候如果指定了rknn_server_ipSDK 会走远程模式把推理请求发给 server。如果不指定SDK 会尝试直接加载librknnrt.so做本地推理但容器里没有 NPU 设备节点会失败。所以必须显式指定 server 地址。4.4 虚拟化数量的实测调优NPU_VIRTUAL_COUNT设多少合适我做了个简单的压测。测试方法用 YOLOv8n 模型输入 640x640在宿主机上跑单进程推理测出单次推理耗时约 25ms。然后启动 N 个容器每个容器持续发推理请求测总吞吐和单次延迟。虚拟数量单次延迟总吞吐NPU 利用率125ms40 FPS95%228ms71 FPS98%335ms85 FPS99%448ms83 FPS99%672ms83 FPS99%从数据看虚拟成 3 个的时候总吞吐最高延迟从 25ms 增加到 35ms对于大多数边缘视觉场景比如 30FPS 的视频流来说35ms 的延迟完全可以接受。虚拟成 4 个以上吞吐不再增加延迟却大幅上升说明 NPU 已经饱和了。所以我的默认值是 3。但这不是绝对的——如果你的模型更小比如 MobileNet 级别的分类单次推理只要 5ms那虚拟成 6 个甚至 8 个都没问题。关键指标是单次推理耗时和你的业务能容忍的最大延迟。公式大概是虚拟数量 业务可容忍延迟 / 单次推理耗时然后取整。实操心得虚拟数量不是越多越好。我试过设成 10结果 rknn_server 的任务队列积压严重所有请求的延迟都飙到 200ms 以上反而不可用。建议从 2 开始逐步增加观察延迟曲线找到拐点。5. 监控与可观测性让 NPU 不再是黑盒5.1 为什么需要自己写 ExporterK8s 原生的 metrics 只覆盖 CPU、内存、网络、磁盘NPU 的利用率、温度、内存占用这些指标一概没有。Prometheus 的 node_exporter 也不认识 RK3588 的 NPU。所以必须自己写一个 exporter把 NPU 的状态暴露成 Prometheus 格式。RK3588 的 NPU 状态信息在哪里主要有两个来源一是/sys/kernel/debug/rknpu/load里面有一个百分比数字表示 NPU 的当前负载。这个文件需要 root 权限才能读所以 exporter 要以 privileged 模式跑或者用CAP_SYS_ADMIN。二是 rknn_server 的日志或者内部状态。但 rknn_server 没有提供查询接口所以拿不到队列长度、任务数这些信息。我试过用strace去 hook 它的系统调用太 hack 了不推荐。温度信息从/sys/class/thermal/thermal_zone*/temp读找到对应 NPU 的那个 zone。不同板子的 zone 编号不一样需要遍历确认。5.2 Exporter 的实现和部署Exporter 用 Python 写最省事因为读文件、算指标、暴露 HTTP 接口都很简单。核心逻辑import prometheus_client from prometheus_client import Gauge import time npu_load Gauge(rk3588_npu_load_percent, NPU load percentage) npu_temp Gauge(rk3588_npu_temperature_celsius, NPU temperature) def collect(): while True: try: with open(/sys/kernel/debug/rknpu/load, r) as f: load int(f.read().strip().rstrip(%)) npu_load.set(load) except Exception as e: npu_load.set(-1) try: with open(/sys/class/thermal/thermal_zone0/temp, r) as f: temp int(f.read().strip()) / 1000.0 npu_temp.set(temp) except Exception: npu_temp.set(-1) time.sleep(5) if __name__ __main__: import threading threading.Thread(targetcollect, daemonTrue).start() prometheus_client.start_http_server(9101) while True: time.sleep(3600)部署成 DaemonSethostNetwork: truesecurityContext.privileged: true挂载/sys/kernel/debug和/sys/class/thermal。Prometheus 的 scrape config 加一个 job- job_name: rk3588-npu static_configs: - targets: [node-ip:9101]Grafana 面板里把rk3588_npu_load_percent和rk3588_npu_temperature_celsius画出来再配合 K8s 的 Pod 指标就能看到“哪个 Pod 在占 NPU”、“NPU 温度是否过高”这些信息。5.3 告警规则的设计光有监控不够还得有告警。我设了三条规则第一条NPU 持续高负载。avg_over_time(rk3588_npu_load_percent[5m]) 90持续 10 分钟。这说明 NPU 可能成为瓶颈需要考虑扩容或者优化模型。第二条NPU 温度过高。rk3588_npu_temperature_celsius 85持续 2 分钟。RK3588 的 NPU 在 85 度以上会降频性能直接腰斩。这条告警要提前于降频触发给自己留出处理时间。第三条NPU 不可用。rk3588_npu_load_percent -1持续 1 分钟。这说明 exporter 读不到 NPU 状态可能是 rknn_server 挂了或者驱动异常。告警接到企微或者钉钉用 webhook 发消息。这部分用 Alertmanager 配置就行不展开。注意/sys/kernel/debug/rknpu/load这个文件在有些固件版本里不存在或者路径不同。如果读不到可以退而求其次用 rknn_server 的进程 CPU 占用率来间接估算 NPU 负载。虽然不准但总比没有强。6. 踩坑记录与常见问题排查6.1 Device Plugin 注册失败现象DaemonSet 跑起来了但kubectl describe node看不到rockchip.com/npu。排查思路先看 device plugin 的日志。如果日志里有failed to register device plugin或者connection refused说明插件连不上 kubelet 的 socket。检查/var/lib/kubelet/device-plugins/kubelet.sock是否存在以及插件容器里这个路径是否挂载正确。K3s 的路径可能不一样是/var/lib/rancher/k3s/agent/kubelet/device-plugins需要根据实际发行版调整。另一个常见原因是kubelet 的--feature-gates没开 DevicePlugins。K8s 1.10 之后这个特性默认开启但有些老版本或者定制发行版可能关着。检查 kubelet 启动参数。6.2 Pod 一直 Pending现象Pod 申请了rockchip.com/npu: 1但一直 Pendingkubectl describe pod显示Insufficient rockchip.com/npu。排查思路先确认节点上确实上报了 NPU 资源。如果上报了但 Pod 还是 Pending可能是资源已经被占满。用kubectl describe node看Allocated resources那一栏确认 NPU 的已分配数量。如果确实满了要么等要么增加虚拟数量。还有一种可能是节点有污点。比如 control-plane 节点默认有NoSchedule污点如果 NPU 板子恰好是 control-planePod 又没加 toleration就会 Pending。6.3 容器里连不上 rknn_server现象Pod 跑起来了但业务代码报connect to rknn_server failed。排查思路首先确认容器里能不能 ping 通宿主机的 IP。如果用了hostNetwork: true容器和宿主机共享网络直接连127.0.0.1:9999就行。如果没用 hostNetwork需要连宿主机的实际 IP并且确保防火墙没挡。其次确认 rknn_server 监听的地址。有些版本的 rknn_server 默认只监听127.0.0.1不监听0.0.0.0这种情况下即使网络通也连不上。解决办法是在宿主机上用socat做端口转发或者改 rknn_server 的配置如果有的话。6.4 推理结果异常或性能骤降现象Pod 能跑但推理结果不对或者延迟比预期高很多。排查思路先确认模型文件是否正确。RKNN 模型是跟固件版本绑定的用 1.5.2 的工具链转的模型在 1.4.0 的运行时上可能跑不了或者结果异常。检查librknnrt.so的版本和模型转换时的版本是否一致。性能骤降通常是NPU 降频导致的。检查温度如果超过 85 度加散热片或者风扇。另一个原因是虚拟数量设多了任务排队导致延迟增加。用前面说的压测方法重新调优。6.5 常见问题速查表问题现象可能原因解决方法Node 上看不到 NPU 资源插件未注册 / kubelet 特性未开检查插件日志和 kubelet 参数Pod Pending提示资源不足NPU 已分配完 / 节点有污点增加虚拟数量 / 加 toleration容器连不上 rknn_server网络不通 / server 只监听本地用 hostNetwork / 端口转发推理结果异常模型与运行时版本不匹配统一工具链和运行时版本推理延迟高NPU 降频 / 虚拟数量过多加强散热 / 减少虚拟数量插件反复重启ListAndWatch 首次响应超时异步做健康检查快速返回设备列表7. 还能怎么扩展这套方案跑通之后我后来又做了几个扩展顺手提一下。第一个是 NPU 配额管理。K8s 的 ResourceQuota 原生支持 Extended Resource你可以在 namespace 级别限制rockchip.com/npu的总量。比如给“推理服务”这个 namespace 分配 6 个 NPU 单元给“训练服务”分配 2 个防止互相抢占。第二个是优先级调度。用 PriorityClass 给关键业务高优先级当 NPU 资源紧张时低优先级的 Pod 会被驱逐腾出资源给高优先级。这在边缘场景下很实用——比如安防视频分析比日志采集重要得多。第三个是拓扑感知。如果一块板子上有多个 NPU 核心RK3588 实际上有 3 个 NPU 核心可以做成拓扑感知的分配让同一个 Pod 的多个容器尽量分配到同一个核心上减少跨核心通信开销。不过这需要改 device plugin 的GetPreferredAllocation实现我还没做留个坑。第四个是和 KubeEdge 集成。边缘场景下节点可能经常断网KubeEdge 的 edgecore 可以缓存 device plugin 的上报信息断网时也能做本地调度。这个我还在测试等稳定了再写一篇。最后说个实在的体会RK3588 的 NPU 做 K8s 调度技术难度其实不高难的是对 NPU 运行时行为的理解。官方文档只告诉你“怎么调 API”不告诉你“多进程并发时会发生什么”、“温度对性能的影响曲线长什么样”。这些东西只能自己一块板子一块板子地试出来。我踩过的坑里有一半是因为想当然地拿 GPU 的经验往 NPU 上套。NPU 就是 NPU它有自己的脾气顺着它的设计来事情就简单很多。