不换业务,只换芯片:异构边缘计算平台的架构与工程实践
做边缘计算这几年我越来越觉得“异构”这个词被说得太轻松了。人人都在谈开源、异构边缘算力平台可真到项目现场往往是 RK3588、Jetson、树莓派、各类国产 NPU、RISC-V 板子各来一批业务代码在每个芯片上都有一份“私人定制”。后来我参与维护一个开源异构边缘算力平台整个项目追求的目标就是标题里那句大实话不换业务只换芯片。这里说的“不换业务”不是让业务代码纹丝不动、连重新编译都不做而是从架构上把业务算法和底层芯片彻底解耦迁移时不需要重写逻辑、不需要为每颗芯片单独维护一套推理管线。文章想把平台的拆解思路、落地路径、真实迁移案例以及开源之后暴露出来的边界一次讲清楚。适合正在做边缘侧算法落地的人、准备做硬件选型的团队以及那些被“换一颗芯片重写一遍业务”折磨过的嵌入式工程师读。我会尽量避免只讲概念更多说代码怎么写、模块怎么切、坑在哪里。1. 边缘侧“碎片化”的本质不只芯片不同是整条软件栈被绑定很多团队的迷茫是从“为什么边上一堆板子每个板子都要单独开发”开始的。表面看是 CPU 指令集不同实际是整条软件栈被绑死了。如果不先把这个“绑死”的结构看透后面做统一平台很容易做成一个花架子。1.1 指令集、加速器和系统环境三种异构混在一起我习惯把边缘计算里的异构拆成三种它们经常同时出现但解决方式完全不同。第一种是指令集异构。ARM、x86、RISC-V 之间的差异主要体现在编译工具链和基础指令层面。这一层相对好办只要业务代码用 C/C 标准库或者高级语言编写编译期针对指令集重新出包即可。真正的麻烦在于很多人会把平台相关的代码比如线程库、IPC、设备驱动调用直接散落在算法里结果每次换指令集都要改一堆平台分支。第二种是加速器异构。NPU、GPU、DSP、FPGA 各有各的编程模型。RK3588 上的 NPU 用 RKNNJetson 上用 CUDA/TensorRT有些国产 NPU 又只提供自定义 C API。算法里只要直接调用了某一家的 runtime API后面基本就被这一家锁死了。这比指令集异构严重得多因为你在业务代码里讨论的是“某个算子在某个加速器上的实现”而不是“这个算子应该做什么”。第三种是系统环境异构。有的板子是 Linux有的是 Android有的还在用老旧的内核和库。哪怕同一颗芯片不同的 SDK 版本、不同的驱动版本跑同一个模型结果都可能不一样。很多迁移项目花的时间并不是在业务逻辑本身而是在装驱动、对 SDK、处理编译依赖。这三层叠在一起就形成了一种“每换一块板子就要从驱动到 API 再到应用层全过一遍”的局面。单看任何一层好像都不难加在一起就变成项目黑洞。1.2 “换芯片重写业务”的根源在于没有稳定的中间层为什么“换芯片重写业务”会成为行业常态我认为不是芯片厂商不努力而是大多数算法团队从一开始就没给自己留一个稳定的中间层。举个例子很多人做边缘检测时会做这样的操作读摄像头帧、resize、转 RGB、归一化、送进模型、取出 NMS 结果、后处理画框。如果这些步骤全都和推理框架的 API 揉在一起那么换芯片时数据布局、内存管理、算子调用、输出张量格式全被替换看起来真的像“全部重写”。真正稳定的中间层应该像网络分层那样切。业务侧只表达“我要对一张图做目标检测”平台侧负责“把这个任务映射到某颗芯片上”。算子、内存、调度、后处理都进平台业务侧只保留“业务语义”。这样换芯片时业务代码的修改量可以收敛到配置项和外部依赖而不是从头到尾改一遍。我参与的这个开源项目最初就是从这个问题切入的。第一版代码很粗暴团队列了一张表把常用操作分成“业务逻辑可调用的接口”和“后端加速器必须实现的接口”两拨。两拨之间的调用全部通过统一接口完成。后来迭代多了这张表慢慢变成规范平台的其他模块才逐一补上。1.3 从“硬件选型绑死代码”到“业务资产沉淀”没有中间层还有一个隐性成本算法团队积累的业务资产会被硬件绑定吞噬掉。你今天在 RK3588 上验证好的检测流程明天客户要换更低成本的芯片如果所有代码都是针对 RKNN 写的那么之前调参经验、数据预处理逻辑、后处理逻辑全都要跟着重写一遍。听起来是“换芯片”实际上是把业务代码重做一遍。平台化之后业务资产沉淀在平台的上层。上层负责图像处理、结果解析、告警策略、设备管理这些和硬件无关下层只负责把统一的算子或模型描述跑起来。这样芯片换代、产品降本时上层代码可以原样带走团队的价值也就能持续累积。这也是“不换业务只换芯片”对团队管理最实际的意义。2. 平台设计的总骨架业务面、设备面、部署面的切分这部分的总体设计思路不适合一开始就写代码。先要有清晰的边界边界画对了代码才有地方落脚。2.1 业务面统一的计算图表达与推理接口业务面的核心任务是让上层开发者不感知具体芯片。我建议不要一上来就只做一个“推理接口”而是要把计算图表达也统一进平台。纯推理接口的问题在于一个模型往往由预处理、模型本体、后处理串起来很多业务还需要在模型输出后再跑一些传统算法。如果把前后处理留在业务侧那不同芯片之间图像数据格式、内存布局的差异最终还是会漏到业务代码里。所以平台应该提供一个“Pipeline”或“Graph”对象让业务侧声明完整的计算流程而不是只声明“模型推理”。伪代码大致长这样# 业务侧只描述计算流程不指定任何具体芯片 pipeline EdgePipeline(detect_pp) pipeline.add(ImageSource(camera0)) pipeline.add(Resize((640, 640), interpolationbilinear)) pipeline.add(ConvertColor(BGR, RGB)) pipeline.add(Normalize(mean[0, 0, 0], std[1, 1, 1])) pipeline.add(ModelInference(yolov8s.onnx)) pipeline.add(DecodeBoxes(num_classes80, conf_thresh0.25, nms_thresh0.45)) pipeline.add(VisualizeOnFrame()) engine.attach(pipeline)这段代码里没有任何芯片相关概念。Resize、Normalize、ModelInference 这些算子在上层只是契约具体由哪个后端执行由平台根据配置文件决定。业务侧则通过engine.predict(frame)拿到结果进一步做业务逻辑。为了让上层接口稳定还需要定义统一的数据对象。图像、张量、检测框、分割掩码这些常用类型都要有独立的数据结构。如果上层到处用裸指针和厂商自定义结构体后面解耦就是镜花水月。2.2 设备面设备注册表与算子后端设备面要做的事情是让每一种芯片以“后端插件”的形式接入平台。接入方式不要写死在框架里而是通过设备注册表和算子实现表两个结构。设备注册表描述平台当前能看到哪些算力设备能力用什么格式表示。每个设备至少要有这些字段{ device_name: rk3588_npu, backend_type: rknn, memory_capacity_mb: 512, supported_formats: [rknn, onnx], supported_quant: [int8, fp16], priority: 10, extra_hints: { batch_limit: 8, preferred_color: RGB } }算子实现表则描述该设备实际支持哪些算子或哪些模型格式。一个后端插件的核心价值不是实现一个forward把模型跑起来而是把上层统一的计算图翻译成设备侧能执行的子图。RKNN 后端负责把ModelInference(yolov8s.onnx)转成 RKNN 模型CUDA 后端负责转成 TensorRT engine纯 CPU 后端则可以直接在 Host 上执行。这个翻译过程放在平台层而不是放在业务层这就保证了业务代码的接入成本基本为 0。设备侧的适配原则上要尽量简单。理想情况下新增一种芯片时只需要写一个插件回答四个问题支持什么模型格式、支持哪些算子、如何分配内存、如何执行一个子图。写满这四类芯片就能被框架调用。这也是“不换业务只换芯片”在工程上落地的关键。2.3 部署面一次构建、多设备分发的配置体系用配置驱动硬件适配是平台日常使用中体验提升最明显的一点。配置需要区分布局配置和业务配置不能混在一起。布局配置描述当前设备的拓扑。比如一台设备有 CPU NPU GPU 三部分算力哪些模型可以跑在 NPU 上哪些算子必须回退到 CPU模型运行在什么工作模式。业务配置则描述用户希望做什么检测、告警阈值是多少、数据上报到哪里。两层配置分离之后同样的业务配置可以无缝推送到不同类型硬件只需由现场人员单独调整布局配置。推荐的配置结构可以是application: name: vehicle_detect models: - name: vehicle source: models/vehicle.onnx backend_whitelist: [rk3588_npu, jetson_gpu] preprocess: - type: resize size: [640, 640] postprocess: - type: detect_box conf_thresh: 0.25 hardware_context: device: rk3588_npu precision: int8 workload_split: auto fallback_policy: to_cpu这样的好处在于业务团队写的代码不包含任何硬件字段现场交付人员只需要在 YAML 里指定hardware_context.device rk3588_npu然后重新部署。平台内部会基于这份配置选后端、做子图切分、设定内存池最后把模型跑起来。整个过程不需要动业务代码甚至在设备上报到云端的消息结构都不用变。3. 三个最容易被忽略的工程细节内存、调度、算子验证平台能力的上限往往不在“能跑通 demo”而在长期运行时会遇到的几个工程细节上。我明确把最容易让项目翻车的三件事单独拿出来说。3.1 内存对象抽象跨设备传数据零拷贝是关键边缘异构平台最难缠的不是算子而是数据在设备之间的流转。如果你的业务里出现“从摄像头拿到数据先拷到 CPU 内存再拷到 NPU 内存处理完再拷回来”性能通常好不到哪里去。平台的数据对象应该做到“多种内存后端”的统一抽象。图像数据可能来自内存映射、来自 DMA buffer、来自 GPU 显存推理张量可能由不同的 runtime 管理。全套系统里最不该出现的就是一堆指向不同内存空间的裸指针到处传。一旦裸指针在业务层泛滥迁移到新芯片时你根本无法判断这块地址什么时候有效、这块内存谁释放。比较稳的做法是定义内部统一张量结构至少包含device_type、shape、dtype、data以及“是否共享外部内存”的标记。实现时做“零拷贝 必要时显式搬运”两套机制。同一个设备内部尽可能只在业务入口和出口做一次数据类型转换。跨设备执行时支持to(device, asyncTrue)异步搬运和事件同步。实际测试中同一台 RK3588 上跑同样的视频流检测如果每次推理前都做两次cv2.resize copy耗时可能比 NPU 本身的推理还高。接入内存池和零拷贝流转后整体耗时能降 30% 到 50%。这不只是优化是平台能否在生产环境中用得住的底线。3.2 执行调度不能只会“找最快的芯片”异构平台最常见的调度思路是把算子包丢给“跑得最快”的设备执行。这个思路看起来很直接实际生产里经常碰壁。一个模型在 NPU 上可能确实快但如果输入数据要从 USB 摄像头拷贝到 NPU 内存而内存搬运路径要经过 CPU这时整条链路的瓶颈可能不在算子计算而在数据通路。另一个典型情况是多路视频流并发每路视频都强行塞给 NPU结果 NPU 排队严重CPU 反而空闲。这种时候不能只看单算子延迟还要看设备负载和整体吞吐。所以我们平台把调度器做成了可插拔策略。默认策略是“考虑硬件能力、当前负载、数据位置和延迟预算的启发式调度”。它不会把整个模型简单丢到一个设备而是以子图为单位在芯片之间做拆解。比如大卷积和 NMS 适合 NPU不支持的算子回退 CPU数据预处理部分留在 ISP 或 CPU 上执行。调度器内部维护一张设备能力表。运行时通过查询算子能力和预估执行时间决定把子图放到哪个设备。还要支持用户手动绑定比如指定某个模型必须跑在 NPU、某个模型必须跑在 GPU。自动调度可以降低大多数人使用平台的门槛但“人工可干预”能力必须保留否则遇到特殊场景时很难调优。3.3 算子一致性测试没有它迁移就是开盲盒平台覆盖多颗芯片后最大的噩梦不是性能而是“同一套接口在不同芯片上的输出有微小差异”。尤其在量化模型上NPU 的 int8 结果和 GPU 的 fp16 结果在边界框评分上差 0.01人眼很难发现然后在后面的逻辑里被放大成误报漏报。我们很早就在 CI 里加入了算子一致性测试做法很朴素准备一组标准输入比如几百张覆盖不同光照、不同目标的图片对每组输入分别在“参考后端”和“待测后端”上跑同一个算子或同一个模型比较输出张量的误差记录最大误差和平均误差分类任务可接受误差需要看目标阈值检测任务则看同一个框的 IoU 和类别一致性。不需要追求逐比特一致。芯片浮点实现不同NPU 量化策略不同必然会带来轻微差异。但要做两个动作一是把差异量化成报告让算法团队能判断偏移是否可接受二是引入容差和适配手段例如允许每个后端通过postprocess额外微调 NMS 阈值。这样模型迁移到任何芯片都能校准到业务可接受的范围。只要离开这套测试“不换业务只换芯片”就是一句空话。因为没有测试你根本不知道该在哪个环节修只是换完芯片发现结果不对然后就只能回到老路上四处打补丁。4. 实际迁移复盘同一套检测业务跑过三种类型芯片理论说多了容易飘。我用一个真实做过的事来复盘同一套车辆检测业务在 RK3588、Jetson Orin 和一颗 RISC-V 核心板之间迁移。整个过程并不是所有芯片都跑得又快又稳但业务侧代码确实做到了不改逻辑只改配置。4.1 从 RK3588 NPU 到 Jetson Orin 的 CUDA 后端业务一开始跑在 RK3588 的 NPU 上使用 RKNN 格式的量化模型。平台已经接好了 RKNN 后端YOLO 检测模型可以在上面稳定运行。迁移到 Jetson Orin 时第一反应是换成 TensorRT。但 TensorRT 对动态输入的支持、对自定义算子的处理和老 CNNs 模型不同。原模型导出成 ONNX再通过 TensorRT 编译需要大量算子融合和精度验证。我们做的事情是在平台里新增jetson_trt后端插件。让该插件读取detect_vehicle.onnx自动构建 TensorRT engine。复用同一个DecodeBoxes后处理算子编译期不对上层暴露差异。真正的工作量不在业务而在给后端插件补全“算子翻译表”。TensorRT 支持的层比 RKNN 多但有些 ONNX 算子还会被拆成多个 TensorRT 层。我们需要把平台统一算子逐个映射过去确保模型加载成功同时做量化校准。系统跑起来后业务侧代码没有变化输出的检测框数量和置信度阈值从一开始就和 RK3588 保持同一套配置。从开始到第一次稳定跑通大概花了两三天其中大部分时间都用在校准、精度对比和异常算子兼容上而不是在“改业务逻辑”。这和以前用原生 RKNN API 写业务代码相比省掉了 80% 的重复劳动。4.2 横向对比 RISC-V CPU性能不够平台怎么补RISC-V 核心板的算力普遍不比主流 NPU。把同一套检测模型迁移到 RISC-V CPU 上一般 CPU 跑 YOLOv8s 可能只有几帧每秒没法直接在生产环境用。不少人到这里会下结论“平台没用换芯片还得重写。”这个结论有问题。平台的价值不是让每颗芯片都拥有相同算力而是在有限的算力上尽量保留业务资产。RISC-V 后端要做的是三件事一是把模型切成能在 CPU 上执行的调优算子尽可能利用 NEON/RVV 向量指令加速二是自动降低输入分辨率在可接受精度损失内换取帧率三是把原本在模型里跑但对 CPU 不友好的算子比如 NMS仍然放到通用代码里优化。实测下来RISC-V 核心板用fp32跑一个轻量级模型能跑 5 到 8 帧每秒。如果降级为int8量化并开启向量化帧率可以到 15 帧左右。虽然与 NPU 差距大但至少业务代码、云端接口、告警逻辑都没有变。这意味着在算力要求不高的低端产品上团队依然能复用同一套业务代码开发成本只集中在平台后端的优化上。4.3 这次迁移里真正省下的时间和仍然要花的时间用表格盘点一下会很清楚环节传统方式平台化方式主要省在哪模型格式转换每个芯片手工转换平台统一完成后端适配一次长期复用预处理代码每个平台各写一套平台内置统一算子减少重复开发后处理逻辑经常和 SDK 解耦天然独立业务资产可迁移算子精度调优每家芯片逐个看结果指标化对比可快速定位差异业务接口耦合 SDK统一接口业务代码基本不动仍然要花的时间一点没少包括不同芯片上的性能压测、量化参数调整、内存池大小配置、特定芯片的算子 bug 排查。这些属于硬件适配的技术债无论如何省不掉但平台把它们隔离在底层不会蔓延到上层业务里。“不换业务只换芯片”从来不是说“零成本换芯片”而是说“换芯片的成本可以被预估、被控制并在多个项目中摊薄”。这是评估平台类项目时最需要掌握的判断标准。5. 开源化落地时的几条经验与边界平台开源后我接触到更多具体场景也越来越清楚哪些坑是需要提前规避的。5.1 哪些边缘场景不适合纳入统一平台不是所有边缘场景都适合套统一平台。遇到这三类建议先想清楚别硬上。第一类是强实时控制场景比如伺服电机控制、工控 IO 协作。这类场景要求的是确定性和极低毛刺不是高吞吐。如果把算法调度放到一个通用平台上路径多了、缓存策略复杂了反而可能引入不可控抖动。通用平台更适合做视觉感知、数据推理不太适合做硬实时控制。第二类是裸算子/私有软件栈依赖极重的场景。假如业务的 90% 都依赖某个厂商的专有 API而底层算子并没有被统一表达出来那平台价值就非常小。为了适配这样的场景要么花大力气改造业务接口要么直接把厂商 SDK 包在接口后面两头的成本都必须考虑。第三类是模型结构三天一变、验证体系也不健全的场景。平台最怕的是业务侧不断上新结构、新算子而后端适配还没有跟上。如果团队连一致性基线都没有平台就会变成一个很沉重的搬运层。我建议先搭好测试基线再引入平台顺序不要反。5.2 引入开源项目时我建议看的几个关键文件如果你也想选一个开源异构边缘算力平台不要只看 README真正能看出项目深浅的是几个目录和文件。首先看docs/architecture或对应的架构说明。关注它如何描述设备抽象、算子抽象、数据流。其次看backend或plugins目录。这决定了平台新增硬件的工作量。如果一个开源项目只支持一个厂商的 SDK后端抽象基本形同虚设。再看测试目录里有没有跨后端的一致性测试。有这套测试说明维护者把“换芯片后结果是否一致”当成核心问题处理过。如果没有项目更多停留在 demo 阶段。最后看examples目录。里面能不能找到从模型转换到设备部署的完整流程有没有覆盖“从一张图片到最终检测结果”的全链路示例。只看“hello world”是不行的。开源项目的好处是代码都可以自己看。拿到代码后第一件事我会建议先跑一遍它支持的两种硬件从 CPU 和某一个 NPU 开始把同一个示例模型迁移过去观察业务代码改动了多少。这个实验最能反映平台是否值得引入。5.3 对“只换芯片”这个口号保持清醒我参与了这个平台才知道真正让“只换芯片”成立的不是某一行魔法代码而是后面一整套持续投入的适配、验证和优化体系。开源可以让这套体系被更多人使用但也意味着不同硬件的问题、不同算子的实现风格、不同社区的节奏会涌进来。忽视测试就会回退到碎片化忽视边界就会让平台越来越重。对一些团队来说可能只需要“覆盖两到三颗主流芯片”就够用对另一些团队来说则需要把大量非标准化接入沉淀成插件。这条路线没有标准答案。我的体会是把业务和硬件的边界画清晰把算子一致性测试做扎实把调度策略的可干预能力留好这三个底座立住了再逐步扩展芯片覆盖范围平台才真正值得长期投入。最后再分享一个小习惯。每加入一种新的后端我至少会拿两种极端设备做冒烟测试一个是算力较强的 NPU/GPU一个是资源受限的 CPU/RISC-V。这样不仅能看到性能上限也能看到平台在最弱硬件上的表现。不要只测试“豪华板子”边缘场景里大量真正需要异构平台的设备恰恰是那些算力普通、量级很大、一年后用着省钱的小板子。