不换业务,只换芯片:异构边缘计算平台的架构与工程实践
2026/9/19 13:07:51 网站建设 项目流程

做边缘计算这几年,我越来越觉得“异构”这个词被说得太轻松了。人人都在谈开源、异构边缘算力平台,可真到项目现场,往往是 RK3588、Jetson、树莓派、各类国产 NPU、RISC-V 板子各来一批,业务代码在每个芯片上都有一份“私人定制”。后来我参与维护一个开源异构边缘算力平台,整个项目追求的目标就是标题里那句大实话:不换业务,只换芯片。这里说的“不换业务”,不是让业务代码纹丝不动、连重新编译都不做,而是从架构上把业务算法和底层芯片彻底解耦,迁移时不需要重写逻辑、不需要为每颗芯片单独维护一套推理管线。

文章想把平台的拆解思路、落地路径、真实迁移案例,以及开源之后暴露出来的边界,一次讲清楚。适合正在做边缘侧算法落地的人、准备做硬件选型的团队,以及那些被“换一颗芯片重写一遍业务”折磨过的嵌入式工程师读。我会尽量避免只讲概念,更多说代码怎么写、模块怎么切、坑在哪里。

1. 边缘侧“碎片化”的本质:不只芯片不同,是整条软件栈被绑定

很多团队的迷茫,是从“为什么边上一堆板子,每个板子都要单独开发”开始的。表面看是 CPU 指令集不同,实际是整条软件栈被绑死了。如果不先把这个“绑死”的结构看透,后面做统一平台很容易做成一个花架子。

1.1 指令集、加速器和系统环境,三种异构混在一起

我习惯把边缘计算里的异构拆成三种,它们经常同时出现,但解决方式完全不同。

第一种是指令集异构。ARM、x86、RISC-V 之间的差异,主要体现在编译工具链和基础指令层面。这一层相对好办,只要业务代码用 C/C++ 标准库或者高级语言编写,编译期针对指令集重新出包即可。真正的麻烦在于,很多人会把平台相关的代码,比如线程库、IPC、设备驱动调用,直接散落在算法里,结果每次换指令集都要改一堆平台分支。

第二种是加速器异构。NPU、GPU、DSP、FPGA 各有各的编程模型。RK3588 上的 NPU 用 RKNN,Jetson 上用 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), interpolation="bilinear")) 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_classes=80, conf_thresh=0.25, nms_thresh=0.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_typeshapedtypedata以及“是否共享外部内存”的标记。实现时做“零拷贝 + 必要时显式搬运”两套机制。同一个设备内部,尽可能只在业务入口和出口做一次数据类型转换。跨设备执行时,支持to(device, async=True)异步搬运和事件同步。

实际测试中,同一台 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 编译,需要大量算子融合和精度验证。我们做的事情是:

  1. 在平台里新增jetson_trt后端插件。
  2. 让该插件读取detect_vehicle.onnx,自动构建 TensorRT engine。
  3. 复用同一个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或对应的架构说明。关注它如何描述设备抽象、算子抽象、数据流。

其次看backendplugins目录。这决定了平台新增硬件的工作量。如果一个开源项目只支持一个厂商的 SDK,后端抽象基本形同虚设。

再看测试目录里有没有跨后端的一致性测试。有这套测试,说明维护者把“换芯片后结果是否一致”当成核心问题处理过。如果没有,项目更多停留在 demo 阶段。

最后看examples目录。里面能不能找到从模型转换到设备部署的完整流程,有没有覆盖“从一张图片到最终检测结果”的全链路示例。只看“hello world”是不行的。

开源项目的好处是代码都可以自己看。拿到代码后第一件事,我会建议先跑一遍它支持的两种硬件,从 CPU 和某一个 NPU 开始,把同一个示例模型迁移过去,观察业务代码改动了多少。这个实验最能反映平台是否值得引入。

5.3 对“只换芯片”这个口号保持清醒

我参与了这个平台才知道,真正让“只换芯片”成立的,不是某一行魔法代码,而是后面一整套持续投入的适配、验证和优化体系。开源可以让这套体系被更多人使用,但也意味着不同硬件的问题、不同算子的实现风格、不同社区的节奏会涌进来。忽视测试就会回退到碎片化,忽视边界就会让平台越来越重。

对一些团队来说,可能只需要“覆盖两到三颗主流芯片”就够用;对另一些团队来说,则需要把大量非标准化接入沉淀成插件。这条路线没有标准答案。我的体会是:把业务和硬件的边界画清晰,把算子一致性测试做扎实,把调度策略的可干预能力留好,这三个底座立住了,再逐步扩展芯片覆盖范围,平台才真正值得长期投入。

最后再分享一个小习惯。每加入一种新的后端,我至少会拿两种极端设备做冒烟测试:一个是算力较强的 NPU/GPU,一个是资源受限的 CPU/RISC-V。这样不仅能看到性能上限,也能看到平台在最弱硬件上的表现。不要只测试“豪华板子”,边缘场景里大量真正需要异构平台的设备,恰恰是那些算力普通、量级很大、一年后用着省钱的小板子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询