ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

K8s Device Plugin 实战:RK3588 NPU 容器化调度与监控

K8s Device Plugin 实战:RK3588 NPU 容器化调度与监控 1. 缘起一块被“闲置”的算力手里有一块 RK3588 的板子6 TOPS 的 NPU 算力跑个 YOLOv8 推理能到几十帧功耗还低得感人。这东西放在边缘侧做视觉检测、做小模型推理性价比几乎没对手。但问题来了当你手里不止一块 RK3588而是五块、十块甚至一个机柜里插了一排你怎么管我最初的方案很土每块板子 SSH 上去手动npu-smi看一眼然后scp模型过去写个 systemd 服务跑起来。三五块还能忍上了十块之后模型版本不一致、某块板子 NPU 被占满、某块板子挂了没人知道运维成本直接爆炸。这时候自然会想到 K8s——毕竟 GPU 能被 K8s 调度NPU 凭什么不行现实很骨感。翻遍 RK3588 的官方文档、Rockchip 的 GitHub、各种论坛你会发现一个尴尬的事实官方从来没有提供过 RK3588 NPU 的 K8s Device Plugin。NVIDIA 有nvidia-device-plugin华为昇腾有ascend-device-plugin连 Intel 的 GPU 都有intel-gpu-plugin唯独 RK3588 的 NPU官方只给了你一个librknnrt.so和rknn_server剩下的全靠自己。这篇文章就是记录我怎么把这个坑填上的。核心思路是用 K8s 的 Device Plugin 机制把 RK3588 的 NPU 抽象成一种可调度资源让 Pod 像申请nvidia.com/gpu一样申请rockchip.com/npu。整套方案我已经在几块 RK3588 上跑通了YOLOv8 推理服务通过 K8s 部署NPU 资源自动分配Prometheus 能监控到每块板子的 NPU 占用率。如果你也在做边缘 AI 部署手里有 RK3588 或者类似的国产 SoC想把它们纳入统一的容器编排体系这篇内容应该能帮你省掉至少两周的摸索时间。我会从 Device Plugin 的原理讲起到具体的代码实现、部署配置、监控接入最后把我踩过的坑一个个列出来。2. 先搞明白K8s 到底怎么“看见”一块 NPU2.1 Device Plugin 机制的核心逻辑K8s 原生只认识 CPU 和内存这两种资源。GPU、NPU、FPGA 这些异构设备K8s 本身是不认识的。那 NVIDIA 的 GPU 是怎么被调度的靠的就是Device Plugin这个扩展机制。Device Plugin 的本质是一个运行在节点上的 gRPC 服务它做三件事第一注册。Plugin 启动后通过 kubelet 暴露的 Unix Socket默认/var/lib/kubelet/device-plugins/kubelet.sock向 kubelet 注册自己告诉 kubelet“我是管理rockchip.com/npu这种资源的”。第二上报设备列表。kubelet 会调用 Plugin 的ListAndWatch接口Plugin 返回当前节点上所有可用的 NPU 设备 ID 列表。kubelet 拿到这个列表后会把这些设备作为扩展资源上报给 API Server。比如节点上有 3 个 NPU 核心那节点的可分配资源里就会多出rockchip.com/npu: 3。第三分配设备。当有 Pod 申请rockchip.com/npu: 1时调度器会找一个有可用 NPU 的节点然后 kubelet 调用 Plugin 的Allocate接口Plugin 返回这个 Pod 应该挂载哪些设备文件、设置哪些环境变量。kubelet 据此配置容器。整个流程里Device Plugin 不负责调度决策调度是 K8s 调度器基于 kubelet 上报的资源量做的。Plugin 只负责“如实上报”和“按需分配”。2.2 RK3588 NPU 的特殊性在哪里理解了 Device Plugin 机制再看 RK3588 的 NPU会发现几个和 GPU 不一样的地方这些差异直接决定了实现方案。第一RK3588 的 NPU 不是一个独立的 PCIe 设备。NVIDIA 的 GPU 是 PCIe 卡有独立的设备文件/dev/nvidia0、/dev/nvidiactl等。RK3588 的 NPU 是 SoC 内部的一个 IP 核它通过内核驱动暴露出来的设备节点通常是/dev/rknpu或者/dev/dri/renderD129这类。具体是哪个取决于你用的内核版本和 RKNPU 驱动版本。我用的 5.10 内核加上 RKNPU2 驱动暴露的是/dev/dri/renderD129。第二NPU 的“核心数”概念和 GPU 不同。RK3588 的 NPU 官方标称 6 TOPS实际上是 3 个 NPU 核心每个 2 TOPS。这三个核心可以独立工作也可以协同。在rknn_server的视角里它管理的是这三个核心。所以我们在 Device Plugin 里上报资源时可以按核心数上报比如rockchip.com/npu: 3。但这里有个坑如果你不做核心隔离多个 Pod 同时用 NPU 会互相干扰。这个后面细说。第三RK3588 的 NPU 依赖用户态库。光有设备文件不够容器里还需要librknnrt.so这个运行时库以及rknn_server这个后台服务。GPU 容器通常只需要挂载设备文件和驱动库但 RK3588 的 NPU 还需要一个常驻的 server 进程来管理硬件。这意味着我们的 Device Plugin 不仅要分配设备还要确保容器里有正确的库和 server。第四没有官方的设备健康检查机制。NVIDIA 的 Device Plugin 有完善的健康检查GPU 挂了会从可分配列表里移除。RK3588 的 NPU 没有现成的健康检查接口我们只能通过npu-smi或者直接读 sysfs 来判断。这个我在实现里加了一个简单的健康检查逻辑。2.3 方案选型为什么不用现成的在动手之前我调研过几个可能的替代方案。方案一用rknn_server做集中式推理服务。就是每块板子上跑一个rknn_server然后所有推理请求都通过 gRPC 发给它。这个方案的问题在于它绕过了 K8s 的调度体系。你没法用 K8s 的资源模型来管理 NPU也没法做细粒度的资源配额。而且rknn_server本身是单点的挂了就全挂了。方案二用 K8s 的 Extended Resource 手动上报。K8s 支持通过 Node 的status.capacity手动设置扩展资源比如你手动kubectl patch node加上rockchip.com/npu: 3。但这种方式只是“假装”有资源实际分配时 K8s 不会帮你做任何设备挂载和环境配置。Pod 起来了但用不了 NPU等于白搭。方案三用 Generic Device Plugin。社区有个generic-device-plugin项目可以通过配置来暴露任意设备。我试过对于简单的设备文件挂载是可行的但它不支持 RK3588 NPU 需要的rknn_server生命周期管理也不支持多核心的细粒度分配。而且它的健康检查机制太简单NPU 出问题了它发现不了。所以最终还是决定自己写一个专用的 Device Plugin。代码量其实不大核心逻辑也就几百行但能把 RK3588 NPU 的特殊需求都照顾到。3. 动手实现一个能用的 RK3588 NPU Device Plugin3.1 环境准备与依赖确认在写代码之前先把环境理清楚。我用的环境是硬件RK3588 开发板具体型号不限只要 NPU 驱动正常就行系统Ubuntu 20.04Rockchip 官方 BSP 或者 Armbian 都可以内核5.10RKNPU2 驱动K8sv1.28kubeadm 部署的单 master 集群容器运行时containerd首先确认 NPU 驱动和运行时是否正常。在板子上执行# 检查 NPU 设备节点 ls -l /dev/dri/ # 应该能看到 renderD129 或类似的节点 # 检查 rknn_server 是否在运行 ps aux | grep rknn_server # 用 rknn 工具查 NPU 信息 cat /sys/kernel/debug/rknpu/version如果/dev/dri/renderD129不存在说明 RKNPU 驱动没加载。需要先确认内核配置里CONFIG_ROCKCHIP_RKNPU是开启的然后modprobe rknpu。rknn_server通常在/usr/bin/rknn_server它是 RKNN Toolkit 的一部分。如果你只装了librknnrt.so没装rknn_server需要从 Rockchip 的 GitHub 仓库下载rknpu2的 runtime 包。注意rknn_server的版本必须和librknnrt.so的版本匹配否则会出现“版本不兼容”的错误。我一开始用了 1.4.0 的 server 配 1.5.0 的库结果推理直接段错误。后来统一到 1.5.2 才正常。3.2 Device Plugin 的代码骨架Device Plugin 的核心是实现几个 gRPC 接口。我用 Go 来写因为 K8s 生态的 Device Plugin 示例基本都是 Go库也齐全。先定义资源名称和 socket 路径const ( resourceName rockchip.com/npu socketPath /var/lib/kubelet/device-plugins/rknpu.sock serverSock /var/lib/kubelet/device-plugins/kubelet.sock )然后实现DevicePluginServer接口。核心是三个方法GetDevicePluginOptions返回 Plugin 的选项一般返回空就行。ListAndWatch这是最重要的方法。它返回一个流kubelet 通过这个流获取设备列表。我们的实现是启动时扫描/dev/dri/下的 render 节点每个节点对应一个 NPU 核心。然后定期比如每 30 秒重新扫描一次如果发现设备数量变化或者设备不健康就通过流通知 kubelet。func (p *RKNpuPlugin) ListAndWatch(empty *pluginapi.Empty, stream pluginapi.DevicePlugin_ListAndWatchServer) error { // 初始设备列表 devices : p.discoverDevices() resp : pluginapi.ListAndWatchResponse{Devices: devices} if err : stream.Send(resp); err ! nil { return err } // 定期健康检查 ticker : time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { newDevices : p.discoverDevices() if !devicesEqual(devices, newDevices) { devices newDevices resp : pluginapi.ListAndWatchResponse{Devices: devices} if err : stream.Send(resp); err ! nil { return err } } } return nil }discoverDevices的逻辑是遍历/dev/dri/renderD*对每个节点检查它是不是 NPU。怎么判断RK3588 的 NPU 对应的 render 节点通常是renderD129但这不是绝对的。更可靠的方法是读/sys/class/drm/renderD129/device/uevent看里面有没有DRIVERrknpu之类的标识。我实际用的是检查/sys/kernel/debug/rknpu/目录是否存在以及对应的 render 节点是否可读。Allocate当 kubelet 决定把某个 NPU 分配给 Pod 时会调用这个方法。我们需要返回一个AllocateResponse里面包含要挂载的设备文件比如/dev/dri/renderD129要设置的环境变量比如RKNPU_DEVICE_ID0要挂载的库文件librknnrt.sofunc (p *RKNpuPlugin) Allocate(ctx context.Context, req *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) { resp : pluginapi.AllocateResponse{} for _, container : range req.ContainerRequests { containerResp : pluginapi.ContainerAllocateResponse{ Devices: []*pluginapi.DeviceSpec{}, Envs: map[string]string{}, Mounts: []*pluginapi.Mount{}, } for _, deviceID : range container.DevicesIDs { // 根据 deviceID 找到对应的设备节点 devPath : p.deviceIDToPath(deviceID) containerResp.Devices append(containerResp.Devices, pluginapi.DeviceSpec{ ContainerPath: devPath, HostPath: devPath, Permissions: rw, }) containerResp.Envs[RKNPU_DEVICE] deviceID } // 挂载 rknn 库 containerResp.Mounts append(containerResp.Mounts, pluginapi.Mount{ ContainerPath: /usr/lib/librknnrt.so, HostPath: /usr/lib/librknnrt.so, ReadOnly: true, }) resp.ContainerResponses append(resp.ContainerResponses, containerResp) } return resp, nil }这里有个关键点rknn_server怎么处理我的做法是rknn_server不在容器里跑而是在宿主机上跑一个全局的 server。容器里的librknnrt.so通过 Unix Socket 或者网络和宿主机的rknn_server通信。这样做的原因是rknn_server需要直接访问 NPU 硬件如果每个容器都跑一个会互相抢设备。宿主机跑一个容器通过 socket 连过去既简单又稳定。所以Allocate里还需要挂载rknn_server的 socket 文件。默认路径是/var/run/rknn_server.sock或者/tmp/rknn_server.sock具体看你的rknn_server配置。3.3 注册与启动逻辑Plugin 的main函数需要做几件事连接 kubelet 的 socket注册自己。启动 gRPC server监听自己的 socket。处理信号优雅退出。func main() { // 先启动 gRPC server lis, err : net.Listen(unix, socketPath) if err ! nil { log.Fatalf(failed to listen: %v, err) } grpcServer : grpc.NewServer() plugin : NewRKNpuPlugin() pluginapi.RegisterDevicePluginServer(grpcServer, plugin) go grpcServer.Serve(lis) // 等待 socket 就绪 time.Sleep(2 * time.Second) // 连接 kubelet 并注册 conn, err : grpc.Dial(serverSock, grpc.WithInsecure(), grpc.WithBlock(), grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { return net.DialTimeout(unix, addr, timeout) })) if err ! nil { log.Fatalf(failed to connect kubelet: %v, err) } client : pluginapi.NewRegistrationClient(conn) req : pluginapi.RegisterRequest{ Version: pluginapi.Version, Endpoint: filepath.Base(socketPath), ResourceName: resourceName, } if _, err : client.Register(context.Background(), req); err ! nil { log.Fatalf(failed to register: %v, err) } // 等待退出信号 sigCh : make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) -sigCh grpcServer.Stop() }注册成功后kubelet 会在日志里打印类似Registered device plugin for rockchip.com/npu with kubelet的信息。这时候你kubectl describe node就能看到节点的可分配资源里多了rockchip.com/npu。3.4 部署为 DaemonSetDevice Plugin 需要跑在每个有 NPU 的节点上所以用 DaemonSet 部署最合适。但这里有个鸡生蛋蛋生鸡的问题DaemonSet 本身也是 Pod它需要被调度。如果节点上还没有 NPU 资源调度器不会把 Pod 调度上去。解决办法是用nodeSelector或者affinity来指定节点而不是依赖 NPU 资源。比如给所有有 NPU 的节点打上标签hardwarerk3588-npu然后 DaemonSet 的nodeSelector选这个标签。apiVersion: apps/v1 kind: DaemonSet metadata: name: rknpu-device-plugin namespace: kube-system spec: selector: matchLabels: name: rknpu-device-plugin template: metadata: labels: name: rknpu-device-plugin spec: nodeSelector: hardware: rk3588-npu tolerations: - key: node-role.kubernetes.io/master effect: NoSchedule containers: - name: rknpu-device-plugin image: your-registry/rknpu-device-plugin:v1.0 securityContext: privileged: true volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins - name: dev-dri mountPath: /dev/dri - name: rknn-lib mountPath: /usr/lib/librknnrt.so - name: rknn-sock mountPath: /var/run/rknn_server.sock volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins - name: dev-dri hostPath: path: /dev/dri - name: rknn-lib hostPath: path: /usr/lib/librknnrt.so - name: rknn-sock hostPath: path: /var/run/rknn_server.sock注意securityContext.privileged: true是必须的因为 Device Plugin 需要访问宿主机的设备节点和 kubelet socket。虽然看起来不太安全但这是 Device Plugin 的标准做法NVIDIA 的 plugin 也是 privileged。4. 实战用 K8s 部署一个 YOLOv8 推理服务4.1 构建带 RKNN 运行时的推理镜像要让 Pod 能用 NPU镜像里必须有librknnrt.so和 Python 的rknn-toolkit-lite2。我基于arm64v8/ubuntu:20.04构建了一个镜像。Dockerfile 大概长这样FROM arm64v8/ubuntu:20.04 RUN apt-get update apt-get install -y \ python3 python3-pip libglib2.0-0 libsm6 libxext6 libxrender-dev \ rm -rf /var/lib/apt/lists/* # 拷贝 RKNN 运行时库 COPY librknnrt.so /usr/lib/ COPY rknn_toolkit_lite2-1.5.2-cp38-cp38-linux_aarch64.whl /tmp/ RUN pip3 install /tmp/rknn_toolkit_lite2-1.5.2-cp38-cp38-linux_aarch64.whl # 拷贝推理脚本 COPY infer.py /app/infer.py WORKDIR /app CMD [python3, infer.py]infer.py的核心逻辑是加载 RKNN 模型然后循环推理from rknnlite.api import RKNNLite import numpy as np import cv2 rknn RKNNLite() ret rknn.load_rknn(yolov8n.rknn) ret rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) outputs rknn.inference(inputs[img]) print(outputs)这里有个细节init_runtime的core_mask参数可以指定用哪个 NPU 核心。NPU_CORE_0、NPU_CORE_1、NPU_CORE_2分别对应三个核心NPU_CORE_AUTO是自动选择。如果你在 Device Plugin 里做了核心隔离这里就可以根据环境变量RKNPU_DEVICE来指定核心。4.2 部署 YAML 与资源申请有了镜像部署就简单了。关键是resources.limits里申请 NPUapiVersion: apps/v1 kind: Deployment metadata: name: yolov8-inference spec: replicas: 3 selector: matchLabels: app: yolov8-inference template: metadata: labels: app: yolov8-inference spec: containers: - name: inference image: your-registry/yolov8-rknn:latest resources: limits: rockchip.com/npu: 1 env: - name: RKNPU_DEVICE valueFrom: fieldRef: fieldPath: metadata.annotations[rockchip.com/npu-device]注意rockchip.com/npu: 1表示申请一个 NPU 核心。如果你有 3 个核心最多可以起 3 个这样的 Pod每个节点。K8s 调度器会自动把 Pod 分散到有可用 NPU 的节点上。部署之后kubectl get pods -o wide可以看到 Pod 被调度到了哪些节点。kubectl describe node可以看到 NPU 资源的分配情况。4.3 验证 NPU 是否真的被用上了Pod 跑起来不代表 NPU 真的在工作。验证方法有几个方法一在 Pod 里执行npu-smi。如果镜像里带了npu-smi工具可以直接看 NPU 的占用率。但npu-smi通常需要访问/sys/kernel/debug/rknpu/容器里可能没有这个路径。可以在 Device Plugin 的Allocate里把这个目录也挂载进去。方法二在宿主机上看 NPU 负载。执行cat /sys/kernel/debug/rknpu/load会输出类似NPU load: 45%的信息。如果 Pod 在推理这个数字会明显上升。方法三看推理延迟。在 Pod 日志里打印每次推理的耗时。如果 NPU 正常工作YOLOv8n 在 640x640 输入下的推理时间应该在 20-40ms 左右。如果走了 CPU会慢一个数量级。我实测下来3 个 Pod 各占一个 NPU 核心同时跑 YOLOv8n每个 Pod 的推理延迟稳定在 30ms 左右NPU 总负载在 70%-80% 之间。这个表现已经能满足大部分边缘视觉场景了。5. 监控与运维让 NPU 资源看得见5.1 用 Prometheus 采集 NPU 指标K8s 集群里通常已经有 Prometheus 了。我们要做的是把 NPU 的指标暴露出来。最简单的方式是写一个小的 exporter跑在宿主机上读取/sys/kernel/debug/rknpu/load和/sys/kernel/debug/rknpu/version然后暴露成 Prometheus 格式。Exporter 的核心逻辑from prometheus_client import start_http_server, Gauge import time npu_load Gauge(rknpu_load_percent, NPU load percentage) npu_temp Gauge(rknpu_temp_celsius, NPU temperature) def collect(): with open(/sys/kernel/debug/rknpu/load, r) as f: load int(f.read().strip().split(:)[1].strip().rstrip(%)) npu_load.set(load) # 温度可能在其他路径比如 /sys/class/thermal/thermal_zone0/temp with open(/sys/class/thermal/thermal_zone0/temp, r) as f: temp int(f.read().strip()) / 1000.0 npu_temp.set(temp) if __name__ __main__: start_http_server(9101) while True: collect() time.sleep(5)然后把这个 exporter 也做成 DaemonSetPrometheus 通过hostNetwork或者 Service 来抓取。5.2 Grafana 面板配置有了指标Grafana 面板就简单了。我配了几个关键图表NPU 负载趋势用rknpu_load_percent做时间序列图可以看到每个节点的 NPU 负载变化。NPU 温度用rknpu_temp_celsius超过 80 度就要注意散热了。Pod 与 NPU 映射这个需要结合 K8s 的指标通过kube_pod_container_resource_limits和自定义标签来关联。注意RK3588 的 NPU 在满载时温度会比较高如果散热不好会降频。我在一块没有散热片的板子上跑满载推理10 分钟后 NPU 温度到了 95 度推理延迟从 30ms 涨到了 80ms。后来加了风扇才稳定。5.3 告警规则Prometheus 的告警规则可以这样配groups: - name: rknpu rules: - alert: RKNpuHighLoad expr: rknpu_load_percent 90 for: 5m labels: severity: warning annotations: summary: NPU load is above 90% for 5 minutes - alert: RKNpuHighTemp expr: rknpu_temp_celsius 85 for: 2m labels: severity: critical annotations: summary: NPU temperature is above 85°C这样当 NPU 过载或者过热时能及时收到告警。6. 踩坑记录与常见问题排查6.1 Device Plugin 注册失败现象Plugin 启动后kubelet 日志里没有注册成功的消息kubectl describe node看不到rockchip.com/npu资源。排查思路第一检查 socket 路径。kubelet 的 device plugin socket 默认在/var/lib/kubelet/device-plugins/kubelet.sock。如果你的 K8s 是用 kubeadm 部署的这个路径是对的。但如果你改了 kubelet 的--root-dir路径会变。用ps aux | grep kubelet看一下实际参数。第二检查权限。Plugin 容器需要 privileged 模式否则无法访问 kubelet socket。如果用的是 containerd还要确认securityContext.privileged被正确传递。第三看 Plugin 日志。如果注册时返回rpc error: code Unavailable通常是 kubelet socket 不存在或者权限不对。如果返回already registered说明之前有残留的 Plugin 没清理干净重启 kubelet 或者删掉 socket 文件再试。6.2 Pod 一直 Pending提示“Insufficient rockchip.com/npu”现象节点上明明有 NPU但 Pod 调度不上去事件里显示0/3 nodes are available: 3 Insufficient rockchip.com/npu。原因kubelet 上报的资源量是 Plugin 通过ListAndWatch返回的设备数量。如果 Plugin 返回了 0 个设备节点上就不会有可分配资源。排查在 Plugin 容器里执行ls /dev/dri/看能不能看到 render 节点。如果看不到说明 DaemonSet 的 volume 挂载有问题。检查hostPath是否正确以及宿主机上/dev/dri/是否存在。还有一种可能是设备被识别为“不健康”。我的 Plugin 里加了一个健康检查如果/dev/dri/renderD129存在但不可读就标记为 unhealthy。kubelet 不会把 unhealthy 的设备计入可分配资源。可以在 Plugin 日志里看到device renderD129 is unhealthy这样的信息。6.3 容器里推理报错“librknnrt.so not found”现象Pod 起来了但执行推理时报ImportError: librknnrt.so: cannot open shared object file。原因镜像里没有librknnrt.so或者路径不对。解决确认 Dockerfile 里COPY librknnrt.so /usr/lib/这一步成功了。另外librknnrt.so可能有依赖库比如libstdc的特定版本。用ldd librknnrt.so检查依赖是否齐全。我遇到过一次librknnrt.so依赖libcurl.so.4但基础镜像里没有。后来在 Dockerfile 里加了apt-get install -y libcurl4才解决。6.4 多个 Pod 同时推理时性能下降严重现象单个 Pod 推理延迟 30ms3 个 Pod 同时跑每个延迟涨到 100ms 以上。原因没有做 NPU 核心隔离。三个 Pod 可能都在抢同一个 NPU 核心或者rknn_server没有正确分配核心。解决在 Device Plugin 的Allocate里根据分配的 deviceID 设置环境变量RKNPU_DEVICE然后在推理脚本里根据这个环境变量指定core_mask。比如import os device_id int(os.environ.get(RKNPU_DEVICE, 0)) core_mask [RKNNLite.NPU_CORE_0, RKNNLite.NPU_CORE_1, RKNNLite.NPU_CORE_2][device_id] rknn.init_runtime(core_maskcore_mask)这样每个 Pod 固定用一个核心互不干扰。实测下来3 个 Pod 各占一个核心每个延迟稳定在 35ms 左右总吞吐量是单 Pod 的 2.8 倍。6.5 rknn_server 挂了导致所有推理失败现象所有 Pod 的推理突然全部报错日志显示connect to rknn_server failed。原因宿主机的rknn_server进程挂了。rknn_server本身没有守护机制崩溃后不会自动重启。解决用 systemd 给rknn_server配一个守护服务[Unit] DescriptionRKNN Server Afternetwork.target [Service] Typesimple ExecStart/usr/bin/rknn_server Restartalways RestartSec5 [Install] WantedBymulti-user.target这样rknn_server挂了会自动重启。另外Device Plugin 里也可以加一个健康检查如果发现rknn_server的 socket 不存在就把所有 NPU 设备标记为 unhealthy避免 Pod 被调度到有问题的节点上。6.6 常见问题速查表问题现象可能原因排查方法解决方案Plugin 注册失败socket 路径不对或权限不足检查 kubelet 日志和 Plugin 日志确认 socket 路径开启 privileged节点无 NPU 资源Plugin 未上报设备在 Plugin 容器里ls /dev/dri/检查 volume 挂载和驱动加载Pod Pending资源不足或节点标签不匹配kubectl describe pod看事件检查 nodeSelector 和资源量推理报错找不到库镜像缺少 librknnrt.soldd librknnrt.so在 Dockerfile 里补全依赖多 Pod 性能下降未做核心隔离看 NPU 负载分布设置 RKNPU_DEVICE 环境变量rknn_server 崩溃无守护进程ps aux | grep rknn_server用 systemd 配置自动重启NPU 温度过高散热不足读/sys/class/thermal/thermal_zone0/temp加散热片或风扇7. 还能怎么扩展这套方案跑通之后其实还有很多可以优化的地方。第一支持 NPU 核心的细粒度分配。目前是按核心数分配一个 Pod 占一个核心。但有些场景下一个 Pod 可能只需要半个核心的算力。RK3588 的 NPU 支持时间片轮转理论上可以做到更细粒度的共享。不过这需要改rknn_server的调度逻辑工作量比较大。第二集成到 K8s 的调度框架里。目前用的是 Device Plugin 的原生调度调度器只知道“有几个 NPU”不知道“NPU 的负载是多少”。可以写一个调度器扩展Scheduler Extender或者用 K8s 的NodeResourceTopologyAPI把 NPU 的实时负载也纳入调度决策。这样可以把新 Pod 调度到负载较低的节点上。第三支持模型预热和缓存。每次 Pod 启动都要重新加载 RKNN 模型如果模型比较大比如 YOLOv8x加载时间可能好几秒。可以在宿主机上做一个模型缓存Pod 启动时直接从缓存加载减少冷启动时间。第四多节点 NPU 池化。如果集群里有多种异构设备RK3588 NPU、昇腾 NPU、NVIDIA GPU可以用 K8s 的Device PluginNode Feature Discovery来做统一的异构资源池。这样用户只需要申请accelerator: 1调度器自动选择合适的设备类型。这套东西我还在持续迭代目前最稳定的还是核心隔离 宿主机 rknn_server 的方案。如果你也在折腾 RK3588 的 K8s 集成希望这篇内容能帮你少走点弯路。有问题的可以在评论区交流我尽量回复。
返回列表