ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Kubernetes Device Plugin的RK3588 NPU调度方案实践

基于Kubernetes Device Plugin的RK3588 NPU调度方案实践 先交代一下背景。我自己手里有几块 RK3588 的开发板做边缘集群测试用跑的目标比较明确把 yolov8 这类模型丢到 NPU 上推理而不是烧 CPU。硬件本身没问题RK3588 的 NPU 算力大致有 6 TOPS虽然比不上独立显卡但对边缘场景足够用了。真正让我难受的是软件生态这一侧——Kubernetes 对 GPU 的调度已经成熟到有官方设备插件可 RK3588 的 NPU 呢官方压根没有对应方案K8s 默认连“看到了这个 NPU”都做不到。如果你只是单机部署直接调 RKNN 的 API开几个线程就完事了问题不大。但一旦你要把三四块 RK3588 组成一个小集群想用 K8s 统一编排推理任务就立刻发现一个尴尬事实Pod 永远只会被调度到 CPU 和内存上都满足的节点根本没人在乎那个节点上还有没有 NPU 可用。调度器不会自动把一个需要跑模型的 Pod 送上 NPU 空闲的板子更不会把 NPU 算力当成一种需要被管理的资源。我写这篇东西的目的就是想聊聊我自己怎么把这个缺口补上的——准确点说是基于 Kubernetes 的 Device Plugin 机制给 RK3588 的 NPU 做了一套可用的调度方案。文章里不会有那种“深入浅出介绍 XX”的废话全是能落地的代码思路、数据结构设计、调度策略取舍和踩坑记录。如果你正好也在搞 RK3588 集群希望把 yolov8 或别的模型用 K8s 编排起来这篇文章应该能帮你省掉好几天的排查时间。1. 为什么 K8s 官方对 NPU 熟视无睹很多人第一次听到这个问题的反应是K8s 连 GPU 都能调度调度个 NPU 有什么难的确实从调度原理层面来说NPU 和 GPU 没有本质区别——都是某种计算加速设备都需要通过设备插件暴露给 kubelet然后让 apiserver 知道有多少可用资源。但你真的去查一遍生态就会发现GPU 的场景是被主流云厂商反复喂大的从 nvidia-device-plugin 到各种共享 GPU 方案层出不穷。而 NPU 这边的现状是主流的几家厂商也提供了一些插件但更多是面向自家云环境的普通用户手上的 NPU特别是 RK3588 这种面向嵌入式和边缘场景的芯片基本处于“无人管”状态。1.1 K8s 调度器眼中的世界只有两种资源Kubernetes 的资源模型从设计之初就有明确的边界。内置的 CPU、内存这些属于无限粒度的可压缩资源调度器通过 requests/limits 做编排而外接设备比如 GPU则需要走自定义扩展资源的路线。扩展资源有天然的约束它必须是整数不能像 CPU 那样写 100m它必须在节点启动时就被上报不能做超卖它不经过 cgroup 做配额控制完全靠设备插件自己处理。这直接带来一个结果K8s 的核心调度器根本不会感知“NPU 利用率高不高”“有几个核闲着”这类事它只关心一个数字——这个节点上的扩展资源还剩多少够不够 Pod 声明的数量。也就是说K8s 官方没做的事本质上不是“做不到”而是“没人给 NPU 提供这个外层抽象”。1.2 RK3588 NPU 的硬件身份一个接口加三个核RK3588 的 NPU 是一块集成了三个核心的硬件单元媒体处理、视觉加速都能干但对外暴露的方式却“朴素”得很。系统里通常只有一个设备节点路径一般是 /dev/rknpu再往上就是驱动层提供的 ioctl 调用、运行时库 librknnrt.so 提供的推理接口。如果你还想基于它做 AI 任务调度首先得有根有据地把它的工作时长按核心拆分出来把每个核的单位分配封装成资源数量否则一个核满载会导致整颗 NPU 对外表现为持续占用状态。我最初想从驱动层面获取三维核的使用状态。试过直接读 /sys 下的相关节点也在 dmesg 里翻过内核上报信息但这些信息要么太粗粒度要么压根不是标准姿势。最后我决定把“整颗 NPU”拆分成资源单位比如把它当成 3 个可调度的核来用——这听起来有点像在做虚拟化但实际上只是给上层调度器一个最小可分配单位。1.3 官方不做的背后没有 Device Plugin 生态RK3588 官方 SDK 提供了完善的单机推理库也支持在容器里挂载 /dev/rknpu 来跑模型但官方文档止步于此——它没有给出任何“如何在多机集群里统一管理 NPU”的建议更别提为 Kubernetes 提供标准的 Device Plugin 实现。这也不怪瑞芯微毕竟做芯片的上游厂商通常不像云厂商那样深度介入容器编排生态。但对我们这群想把 NPU 池化的人来说这一层空白就要自己补了。2. 方案选型补丁怎么打最合理补上这块缺口可以走两条路。一条是绕过 K8s在集群上层封装自己的调度层任务不直接走 K8s而是通过一个外部的队列系统分发——这适合典型的高性能计算场景但缺点很明显你失去了 K8s 的故障恢复、滚动更新、服务发现能力。另一条就是深度绑定 K8s 的扩展机制让调度器认为 NPU 是一种普通的扩展资源Pod 声明了数量就正常编排这也是我最终采用的方式。2.1 为什么选择扩展资源 Device Plugin要回答这个问题先要理解 K8s 调度器的执行顺序。kubelet 定期调用设备插件的 ListAndWatch 方法获取设备的健康状态然后通过上报扩展资源让调度器知道节点上有多少可用资源。调度器做预选时会拿着 Pod 里声明的扩展资源数量和节点上报的数量做比较只有余量足够的节点才进入过滤之后的决策环节。真正把设备注入容器的动作发生在 Pod 调度到某个节点之后kubelet 调用设备插件的 Allocate 方法拿到环境变量、设备列表、挂载点等运行时信息再把这些信息追加到容器配置里。因为逻辑是现成的我只需要写一个 Device Plugin 插进去就能让 K8s 把 NPU 当成一种与 GPU 平级的资源。这个过程不需要修改调度器的任何代码也不需要引入 CRD所有的逻辑都依附在成熟稳重的外部机制里出问题的概率小将来还原方便。2.2 为什么不用 CRD 自定义调度器有些做 AI 平台的人偏爱用 CRD 定义“NPU 池”“模型实例”再写一个自定义调度器去调节任务盒子。这种方案非常灵活能做的精细操作也很多比如根据模型大小挑最合适的节点、把两个模型安排到同一个核这种复杂策略。但代价是你必须自己实现调度逻辑里最难的失败处理、抢占、配额控制。我更看重的是稳妥与简洁。一个 CRD 至少需要写 controller需要处理机制和事件循环还需要考虑多 controller 之间的协调这个工作量比写一个设备插件大一个量级。设备插件的好处是“小、快、直接”——我把模型的卡点完全交给 K8s 原生调度器虽然它不够智能但它稳定可靠而且凭我有三块板子的场景不需要特别聪明的策略。2.3 从“整卡”到“按核”调度颗粒度的取舍最初我的设想是直接上报“1 个 NPU”调度器只允许一个 Pod 占满整个 NPU 资源一台 RK3588 同时最多运行一个推理任务这样代码简单出了问题也容易查。但实际测试之后发现这个方案浪费严重——yolov8 的推理任务很多时候只占一个核剩余两个核处于空转状态一个核跑满了另一个核还用不上整卡语义的隔离性太粗糙了。于是我只用了很小的改动就把设备数量改成 3含义等于 3 个 NPU 核心Pod 声明“npu.rockchip/cores: 1”时实质是声明要一个核。但这里必须说清楚一个实际上的约束真正的硬件在执行某个推理操作时未必能完全按核心隔离。RKNN 运行时在流水调度上可能会把一个小模型的工作分散到多个核上以达到单核不够但数核齐全的目的。对于这种情况我只能通过用户态的互斥来控制。即在 Allocate 阶段根据请求的量返回对应数量的设备句柄并在容器环境变量里写上“你的核心上限”而具体放到哪个核仍由运行时库决定。后面第三部分我会详说这部分实现的细节。3. 核心实现写一个 RKNN Device Plugin如果你熟悉 Nvidia 的 Device Plugin 的逻辑理解这个实现就没什么难度。整个插件是围绕 kubelet 的 Device Plugin 服务实现的 gRPC 服务核心接口就三个GetDevicePluginOptions、ListAndWatch、Allocate。我尽量把代码写简洁没有做完美的错误处理但直接把主流程贴了出来方便你对照自己的场景改造。3.1 主流程框架grpc 服务 心跳上报插件启动后先主动 kubelet 注册然后开始循环上报设备状态。关键的伪代码逻辑大概是这样的func main() { // 直接使用环境变量获取 socket 路径不写死 socketPath : os.Getenv(DEVICE_PLUGIN_SOCKET) if socketPath { socketPath /var/lib/kubelet/device-plugins/npu-plugin.sock } // 注册插件握手 conn, _ : grpc.Dial(unix://socketPath, grpc.WithInsecure()) client : pluginapi.NewRegistrationClient(conn) client.Register(context.Background(), pluginapi.RegisterRequest{ Version: pluginapi.Version, Endpoint: npu-plugin.sock, ResourceName: npu.rockchip/cores, }) // 启动 gRPC 服务实现 ListAndWatch / Allocate server : grpc.NewServer() pluginapi.RegisterDevicePluginServer(server, NPUDevicePlugin{}) server.Serve(listener) }值得注意的是ResourceName 必须以“域名/名称”的格式出现且域名部分不能包含一些系统保留字样。如果你喜欢用 npu.rockchip/cores那就得保证节点上的 kubelet 重启后不会因为 device-plugin 的重新注册把资源名称搞混。3.2 ListAndWatch设备健康状态与资源上报ListAndWatch 本质上就是持续往信道上推设备状态。这个接口不需要我主动考虑“并发分配”的锁问题kubelet 每次想知道状态时都会再调一次这个方法。我的实现里维护一个简单的状态数组三个设备 ID分别对应 RK3588 内部编号的 core0、core1、core2。每次上报时都检查某个文件句柄写一个简单的探针去确保 NPU 驱动是通的func (p *NPUDevicePlugin) ListAndWatch(empty *pluginapi.Empty, stream pluginapi.DevicePlugin_ListAndWatchServer) error { devices : []*pluginapi.Device{ {ID: npu-core-0, Health: pluginapi.Healthy}, {ID: npu-core-1, Health: pluginapi.Healthy}, {ID: npu-core-2, Health: pluginapi.Healthy}, } stream.Send(pluginapi.ListAndWatchResponse{Devices: devices}) for { time.Sleep(30 * time.Second) // 实际项目中这里会检查驱动健康状态 // 如果 /dev/rknpu 不存在或者某个探针失败就把对应设备标记成 Unhealthy // 这里简单跳过保持一直健康 } }这里有一个很重要的坑必须提醒设备 ID 绝不能重复而且不能随便起名。某些版本的 kubelet 在分配时会以设备 ID 作为 key如果 ID 冲突allocated 状态就全乱了。我当时第一次写插件随手用了 core0、core1、core2结果多个节点之间没冲突但单个节点上反复重启插件后kubelet 缓存里残留旧 ID就出过状态不一致的问题。所以命名要稳定重启插件时最好带个节点标识或者干脆从 Node 名称中派生。3.3 Allocate设备句柄、挂载点与环境变量注入Allocate 是核心中的核心。Pod 通过调度器落到节点上之后kubelet 会调你的 Allocate你要告诉它这个容器需要哪些设备文件、哪些环境变量、哪些挂载目录。这里最主要的任务就是把 NPU 设备节点映射给容器同时注入核心数限制信息。func (p *NPUDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) { var responses []*pluginapi.ContainerAllocateResponse for _, req : range reqs.ContainerRequests { // 请求的设备 ID 是一串我们简单统计数量按数量给环境变量配额 coreCount : len(req.DevicesIDs) resp : pluginapi.ContainerAllocateResponse{ Devices: []*pluginapi.DeviceSpec{{ HostPath: /dev/rknpu, ContainerPath: /dev/rknpu, Permissions: rw, }}, Envs: map[string]string{ RKNN_CORE_COUNT: strconv.Itoa(coreCount), // 告诉容器里的参数库当前容器最多可用几个核 }, } responses append(responses, resp) } return pluginapi.AllocateResponse{ContainerResponses: responses}, nil }这段代码有几层深意。第一我没有把三个核映射成 /dev/rknpu0、/dev/rknpu1、/dev/rknpu2因为硬件里根本没有这样的设备节点实际只有一个 /dev/rknpu。设备划分靠的是环境变量在用户态上让 RKNN 运行时限制线程并发度。第二环境变量名叫 RKNN_CORE_COUNT 是我自己定义的不是 RKNN 官方约定你需要在你自己的模型启动脚本里读取它然后显式传给推理配置。我后来用一个小 shell 包装器在容器启动时根据这个变量决定跑 1 还是 3 个推理线程。从调度视角看设备数上报为 3Allocate 只是发了一个环境变量本质上没有真实的设备文件隔离。这里必须对你们说实话——现实中不能把“整数”当成绝对隔离边界K8s 的调度永远只能保证数量不能保证运行时性能隔离。如果三个 pod 恰好都声明 1 个核不影响它们之间通过驱动去抢占整颗 NPU 的计算能力。3.4 环境变量约定容器内怎么识别“我能用几个核”调度器把设备分配给 Pod 的时候环境变量是唯一传送信息到应用的干净途径。我的约定很简单export RKNN_CORE_COUNT${RKNN_CORE_COUNT:-3}然后模型启动脚本里写类似这样的逻辑if [ $RKNN_CORE_COUNT -eq 1 ]; then export RKNN_PROCESS_MODE0 # 使用单核 elif [ $RKNN_CORE_COUNT -ge 2 ]; then export RKNN_PROCESS_MODE1 # 让运行时自行分配 fi这些开关不是标准 API需要针对官方 SDK 的 rknn_init、rknn_run 等接口做一些适配。最可行的做法是把获取环境变量的地方放在应用层也就是你在代码里主动去查这个变量然后调整一个全局的线程池或者推理实例的数量。具体落地时我是在推理服务启动时根据它创建对应数量的推理请求队列每个队列绑定一个独立线程同时在底层调用 rknn_run 时对操作做互斥避免同一个上下文被多线程并发。4. 调度与运维从“能用”到“好用”写完 Device Plugin 只是第一步真正决定这套系统用起来顺不顺畅的是调度策略。这里拿我自己集群的配置做例子看看从部署到摸清门道都经历了什么。4.1 节点标记怎么让调度器认出一台“有 NPU”的节点Kubernetes 原生的调度器在进行预选时只看资源量的报表。它并不关心节点的标签但为了防止一个 CPU 密集型的任务被调度到没有 NPU 空闲的板子上我会打上一个标签同时只允许带标签的 Pod 运行的开关不受影响。最基本的做法是在需要跑推理任务的节点上加上kubectl label node rk3588-node-01 ai.rockchip/nputrue然后推理 Pod 的 YAML 写nodeSelector: ai.rockchip/npu: true这样可以避免把推理 Pod 随便调度到没插靠 NPU 的节点。我一开始犯过一个小错误一个节点上装了驱动但忘了打标签调度器看到该节点扩展资源为 0却因为其他计算资源充足把推理 pod 扔了上去结果容器一直起不来。打了标签并且资源充足才不会出现这种情况。4.2 Pod 声明requests 和 limits 怎么填扩展资源在 K8s 里只能写 limits不能写 requests否则会直接报错。这句话值得反复强调。很多初学者从 CPU/内存的模型迁移过来自然而然写上 requests: npu.rockchip/cores: 1结果调度器不认因为 apiserver 会拒绝这个 Pod。正确写法是resources: limits: npu.rockchip/cores: 1如果你的 Pod 对稳定性要求更高想要抢占式调度那可以给 Pod 加上 PriorityClass。但注意NPU 算力不能超卖——至少不能像 CPU 那样超卖——所以我给每个节点上报 3 个核单核 Pod 最多同时运行三个再多了只能排队等调度器重试。4.3 部署案例yolov8 推理服务如何接入我把一个 yolov8s 的模型封装成了 HTTP 推理服务镜像开始做压测。模型是用 RKNN-Toolkit2 转换好的镜像里安装好 librknnrt同时挂载设备插件分配好的 /dev/rknpu。部署文件核心内容apiVersion: apps/v1 kind: Deployment metadata: name: yolov8s-infer spec: replicas: 2 template: spec: nodeSelector: ai.rockchip/npu: true containers: - name: infer image: edgeai/yolov8s-rk3588:latest resources: limits: npu.rockchip/cores: 1部署两个副本如果集群有 3 块 RK3588调度器会把两个副本尽量打散到不同节点上除非某个节点上其他资源满足不了要求它才会把两个副本压到同一个节点。我用负载生成器压了大概一千张 640x640 的图片单核吞吐大约 45 FPS延迟基本稳定在 22ms 上下这和直接在宿主机上跑没有明显差别——说明这套“半虚半实”的设备插件对性能的损失是可以忽略的。4.4 与 GPU 调度场景的差异NPU 生态的坑不能按 GPU 经验套GPU 的插件生态已经非常成熟比如 nvidia-smi 可以监控利用率、显存占用pod 也可以通过在容器里挂载 nvidia-smi 来精确查看某个进程占用了多少显存。而 RK3588 的 NPU 没有这种“管控通道”它只有一个 /dev/rknpu 暴露出来除非你自己写 ioctl 去查询底层状态否则你无法知道某个那个进程到底占用了多少算力。正因为这种不可见性多 Pod 共享一颗 NPU 时只要三个 Pod 都跑在同一颗 NPU 上性能下降可能很严重。我给调度系统加了一个比较简单的约束声明为 1 个核的 Pod 只能放在标签为“allow-sharingtrue”的节点上如果对性能有强要求就必须标记“allow-sharingfalse”这样调度器会把它们隔离开。5. 监控与可观测NPU 资源不再是黑盒设备插件的存在只是让调度器“能用”NPU但要是想去判断系统是否健康光靠“Pod 在跑”这个结果是不够的。推理任务的稳定性与 NPU 温度、驱动状态、任务实际并发量都有关而这些统统不会出现在 K8s 原生指标里。所以还需要另做一些监控能力才能把 NPU 资源池从黑盒变成白盒。5.1 指标从哪来一个轻量的 prometheus exporterLinux 下对 NPU 状态的查询没有标准工具瑞芯微提供的一些调试手段又过于底层。我的做法是起一个轻量 exporter通过驱动接口定期去读 NPU 的利用率、运行频率和温区。如果你觉得麻烦也可以退一步直接把“容器内存”、“goroutine 数”这类与应用层状态相关的指标暴露出去间接衡量 NPU 是否处于饱和状态。更直接的方案是封装一个指标端点模拟 yolov8 推理服务的请求响应时间把每秒处理的图片数量作为外部指标连到 prometheus 做告警。这只是业务层指标但它能真实反映瓶颈——当 cpu 使用率并不高但吞吐量下降基本能断定卡在 NPU 排队上了。5.2 告警与排查哪些指标值得盯从我的实践看这三个指标最关键设备插件上报的健康状态一旦变为 unhealthykubelet 会把该节点上的 NPU 资源清零所有推理 Pod 都会陷入调度不了的状态推理请求的平均延迟如果延迟突然从 22ms 涨到 100ms 以上通常是多个 Pod 同时抢一个核或 NPU 过热降频节点的内存可用量RK3588 的内存一般也就 8G 到 16G跑着若干个模型之后很容易被边缘业务挤爆进而触发 oom-killer 直杀进程。挂在 prometheus 上之后grafana 面板可以做个简单的行表按节点看资源剩余和健康状况。还有一个实用小技巧是给 exporter 加一个 about 指标上报 driver 版本、固件版本这样出了问提能立刻看出是不是某块板子驱动没刷到一致版本。5.3 磁盘与系统环境的隐患别让存储拖垮推理热词里有一条“rk3588 刚烧写的 ubuntu20.04 磁盘就没空间了”我映象太深了。很多板子都是 16G 或 32G 的 eMMC烧完系统后系统本身占了一半再装几个推理镜像转储文件就没空间了。容器日志、镜像缓存、模型缓存目录全都堆在系统分区上。遇到这种情况我的建议是尽早把 Docker/containerd 的数据目录迁移到外置存储或更大的分区而且要定期做镜像瘦身——把基础镜像用 alpine 或 debian-slim模型文件用独立卷挂载不要打进去。另外当前版本的 RKNN 工具链会依赖 Python 环境转换模型的容器如果要跑在 K8s 里需要预装 numpy、opencv-python 等一堆依赖。如果镜像太大调度到某个节点时拉镜像时间会特别长那种 “冷静不下来” 的等待其实也是在空耗集群资源有条件就做持久化缓存镜像仓库。6. 踩坑实录我的问题排查速查表这部分是压箱底的经验。凡是给 RK3588 容器环境做 NPU 调度的人早晚会遇到下面几类问题。我把最典型的列出来附上排查思路。6.1 容器内报找不到 librknnrt.so这个出现频率极高。镜像使用官方 RKNN 环境时常常某一依赖库缺失。换而言之要么在 Dockerfile 里把 /usr/lib/librknnrt.so 显式拷贝进去要么在启动命令里通过 LD_LIBRARY_PATH 指向挂载目录。我自己最后选择把 librknnrt.so 放到一个固定路径并统一由基础镜像提供避免每个服务镜像重复解决这个问题。这里还有一个隐性坑要确认驱动版本与 runtime 版本匹配两者版本不一致时初始化函数返回的错误往往是被吞掉的表现出来就是随机段错误。6.2 Pod 一直是 Pending调度不上去第一步肯定是 inspect 节点的 pod 列表看 kubelet 日志是否有“npu.rockchip/cores not found”之类的报错。如果没有找到一个合理的释放原因再查节点上的 resourcesK8s 的 kubectl describe node 里没有扩展资源条目说明插件注册失败或上报被拒。常见原因就是 socket 路径写死kubelet 实际监听的路径不对。6.3 多个 Pod 同时跑推理性能忽高忽低这个是必然现象因为使用的“核”是用户态约定不是硬件隔离。我把每个 Pod 都加了一个薄薄的限流/打满逻辑当同一时刻有三个 pod 在打不满测压时效果才接近线性。你也可以在应用层引入一个全局的信号量让校内抢不到资源的 Pod 主动 sleep而不是一直死磕 NPU 算子否则做完合并后的性能损伤会非常剧烈。6.4 RK3588 烧写 ubuntu 后存储不足前面提过不再展开。只补充一个通用的排查步骤看到磁盘满先跑df -h查看挂载du --max-depth1 -h / | sort -h看哪个目录占了空间。通常是 /var/log/journal 和 /var/lib/docker 两个重灾区。这两个目录都应该迁移到非系统分区而不是简单手工把日志滚动掉——因为“一删了之”的治标方法撑不过三天。6.5 热词里那些“ffmpeg 推流”和“mipi 输入”的事要不要一起说标题是纯调度的范畴但只要你用 RK3588 做真实业务一定避不开和视频线程抢资源的场景。我的教训是如果你的推理任务和 ffmpeg 推流必须同时跑cpu 用 requests 给足量并且给 ffmpeg 绑定不同核心否则推流线程一旦被调度到某个核推理延迟就会明显抖动。官方 SDK 对 mipi 输入信号的处理并不透明出现丢帧时你先千万别怀疑是 K8s 资源分配问题多半是摄像头输入信号源或 v4l2 的参数没配对。结尾这套东西在仓库里跑了大半年了。我避开了一种过度设计的冲动没有去实现“按模型类型自动挑节点”“支持共享 1.5 核”这种花哨功能而是用功能最弱的扩展资源、轻量设备插件加上少量环境变量约定把 RK3588 的 NPU 塞进了 Kubernetes 的资源模型里。代价是有的比如多 Pod 共享时性能不算稳定比如不是硬件级别的隔离但好处是显而易见——调度器可以自动把它当成与 CPU、内存平级的资源Pod 也能自动识别节点上是否有可用的 NPU 算力。如果你也想在自己的集群里引入 RK3588 的 NPU 调度我的建议是从最小闭环开始先跑通一个 Python 推理脚本能绑定单核再写设备插件最后只做一层容器配额。等这三个步骤都过了再去考虑共享、优先级、监控多样性的花活也不迟。别一上来就试图把“集群级的 AI 推理调度平台”做完那只会让排错变得痛苦。后续我打算给这套插件加两个能力一是真正的并发统计通过 ioctl 在上层查询 NPU 的实时占用率把纯静态的核数资源变成打上时间戳的动态资源二是尝试接入一套简单的预填充队列在调度器层面就阻止多个 Pod 挤到同一块板上而不是等到运行时才发现互相干扰。如果有谁也在搞类似的事欢迎交流毕竟 NPU 的 K8s 生态还早得很靠一个人填不完这些坑。
返回列表