1. 当边缘算力不再是锦上添花:为什么我盯上了ArmNN
做端侧AI这两年,最深的体会就是“模型上板容易,跑得舒服难”。手里有个训好的模型,要么是塞进手机SoC里被厂商的私有Runtime绑得死死的,要么是转到NPU上被算子的不支持列表劝退,要么是硬啃C++手写推理代码,一个算子优化掉半条命。直到我认真开始做Arm架构下的源码级评测,ArmNN这个名字才真正从“听说过”变成“值得深挖”。
ArmNN是Arm官方开源的推理引擎,官方定位是面向Arm Cortex-A系列CPU、Mali GPU以及Arm NPU(Ethos-U/Ethos-N系列)的高性能推理框架。它和TensorFlow Lite、ONNX Runtime这类通用引擎最大的区别在于:它天生就是为Arm体系设计的,背后不是“兼容”Arm,而是“压榨”Arm。换句话说,同样是跑在RK3588、树莓派5、飞腾D2000、麒麟990这类板子上,ArmNN在算子调度、内存布局、SIMD指令利用上,吃得更透。
这篇东西我不是来贴官方文档的,而是带着源码审计的视角,把ArmNN的架构分层、核心执行流、算子注册机制、后端调度策略这些关键点全部翻一遍,再结合我实际在一台飞腾ARM64板卡上的部署过程,把能复用的实操步骤和踩过的坑一并写出来。适合已经在做端侧推理、准备从零接入ArmNN、或者正被TFLite算子兼容性坑得想骂人的朋友。
2. 全景拆解:ArmNN的架构分层,或者说它凭什么比通用框架更懂Arm
2.1 从输入到输出的完整链路:图优化、工作负载调度、后端执行
先不说太细的代码,我把ArmNN跑一个模型时的整体流程画在脑子里:你往引擎里丢一个输入张量,它先走前端解析层把模型文件解析成内部IR(ArmNN的Graph对象),然后是中间优化层做算子融合、布局转换、常量折叠,再往后是工作负载调度层把优化后的图拆成一个个可执行的Workload,最后由后端执行层交给具体的计算后端(CPU的NEON后端、GPU的CL后端、NPU的EthosN后端)去跑。
这个架构看起来和TVM/ONNX Runtime的分层设计差不多,但ArmNN的关键差异在于:它每一层都是围绕Arm硬件特性来设计的。前端解析层不是简单地把ONNX/TFLite的节点“翻译”成自己的算子,而是会记录原始模型的布局信息——比如TFLite默认的NHWC布局,在ArmNN内部会被融合进后续的布局优化里,避免在CPU后端上做无谓的transpose。
中间优化层更狠,它有一个基于“可替换子图”的优化机制。比如一个Conv2D后面紧跟BatchNormalization,很多框架的优化器只是在图层面做fold,ArmNN则是在构建Workload前会尝试把BN的缩放因子直接折叠进卷积权重里。这个机制不是靠一堆硬编码pattern实现的,而是通过Graph的拓扑排序和Layer的visit机制,一层一层去检查“当前层是否支持与相邻层融合”,逻辑非常干净。
工作负载调度层是最常被忽略但又最关键的一层。每个Layer在优化完成后会被“分配”到一个后端上,ArmNN用IWorkloadFactory接口解耦图结构和底层硬件。这意味着同一个图,在支持GPU的设备上会自动把卷积类算子的工作负载丢给CL后端,在只有CPU的环境里则走NEON后端。审计源码时你会发现,这个调度决策不是全局统一切换,而是逐算子进行的——也就是说,一个模型里可能卷积走了GPU、池化走了CPU、最后的Softmax又回到CPU,这种异构调度的精细度,是很多通用框架不具备的。
2.2 源码仓库结构审计:我在ArmNN里找什么
ArmNN的源码仓库(GitHub上的Arm-software/armnn)目录结构其实非常规整,但第一次进去的人容易迷路。我按审计的习惯,把核心目录过了一遍,挑重点说:
src/armnn/:核心库,Graph、Layer、Workload、Optimizer全在这。这是源码审计的主战场。src/backends/:各后端实现,neon/、cl/、ethosn/、reference/都在这里。每个后端目录下都有WorkloadFactory、LayerSupport、Backend等核心类。src/armnnTfLiteParser/、src/armnnOnnxParser/:解析器,把外部模型格式转成内部Graph。tests/:单元测试和集成测试,很多对源码行为的理解可以靠测试用例反推。
我做源码审计的套路是先看src/armnn/Graph.cpp里的AddLayer、Connect、TopologicalSort这些核心方法,搞清楚一张图是怎么被构建出来的;再去看IRuntime和IWorkloadFactory的接口定义,理解执行期发生了什么;最后进到后端目录里,看NEON后端的NeonWorkloadFactory是怎么把ArmNN的Layer映射成arm_compute库的Function。
这里有个很重要的点:ArmNN的CPU后端不是自己写卷积实现,而是封装了Arm自家的Compute Library(ACL)。所以ArmNN的NEON后端本质上是“ArmNN的图结构 + ACL的高性能内核”。审计NEON后端时会发现大量arm_compute::NEFunction的调用,比如NEElementwiseOperations、NEConvolutionLayer这些。理解了这层关系,排查性能问题时你就能迅速区分是ArmNN调度开销还是ACL内核本身的开销。
3. 执行引擎源码审计:工作负载是怎么跑起来的
3.1 从Graph到Workload:三层抽象之间的关键流转
我先说结论:ArmNN的执行模型可以概括为“Graph构建期做结构、Optimizer期做化简、Runtime期做映射”。源码里最核心的几个类分别是Graph、Layer、Workload和IWorkloadFactory。
当我写一个模型推理程序时,第一步通常是构建INetwork,往里AddLayer。这一步在ArmNN内部会创建Layer对象并插入Graph。值得留意的是,此时还没有任何后端绑定,Layer只是“逻辑节点”。直到你调用Optimize()方法,传入后端列表,ArmNN才会为每个Layer选择合适的后端,并生成对应的Workload对象。
我审计时重点看的是Optimize()的流程,它在src/armnn/Optimizer.cpp里。大致分三步:
- 图级优化:遍历Graph,执行算子融合和布局优化。这里的优化规则很多,比如
ConvertFp32ToFp16、OptimizeConv2d、OptimizeBatchNorm等,代码集中在Optimizer.cpp里,每个优化器是一个独立的pass,用Graph::TopologicalSort驱动遍历顺序。 - 后端分配:对每个Layer询问
ILayerSupport接口,判断该层在当前后端下是否受支持、是否更快。这一步的结果存到Assignment里。 - Workload构建:根据后端分配结果,调用对应后端的
IWorkloadFactory::CreateWorkload(),创建出真正的执行对象。
真正跑到IRuntime::EnqueueWorkload()时,ArmNN已经把整个图变成了一个IWorkload列表。执行时就是逐个调用Execute()。对于CPU后端,这个调用最终会穿透到ACL的run()方法;对于GPU后端,则是在OpenCL的command queue上提交kernel。
从审计视角来看,这段链路最重要的收获是:在ArmNN里做算子扩展,你永远需要动三层——Layer定义与解析(新Layer类型)、Optimizer中的融合规则(要不要做图级优化)、后端Workload实现(真正的计算函数)。少了任何一层,你的自定义算子要么根本跑不起来,要么能跑但性能难看。
3.2 工作负载调度与内存管理的暗礁:SubGraphView与TensorHandle
如果只看到Workload列表逐个执行,你可能会以为ArmNN的Runtime是极度线性的。实际上,ArmNN支持SubGraphView的拆分执行,这对异构模型至关重要。
简单解释一下:一个模型如果卷积输出要接一个CPU-only的算子,那你不能把整个模型都丢到GPU上跑。ArmNN通过SplitSubGraph能力把图切成多个SubGraphView,每个SubGraphView可以绑定不同的后端。源码中的SubGraphViewer负责找到“可用同一后端连续执行的层段”,然后用SubGraphView对象描述这一段。执行时,IRuntime::EnqueueWorkload会按顺序执行每个SubGraphView,中间自动插入张量拷贝(比如GPU多出来的中间结果要拷回CPU内存)。
这块代码你可以在src/armnn/SubGraphView.cpp和src/armnn/Runtime.cpp里追。我曾经为了在RK3588上跑一个大模型,发现某些层被分到了不同后端,然后在内存拷贝上吃了不少延迟——这个问题的根源就是SubGraphView切得太碎。解决办法是在OptimizeOptions里控制后端的优先级,尽量让同一类算子聚集到同一个后端上,减少跨后端的Copy。
TensorHandle这块也有坑。ArmNN的内存管理并不是简单的malloc/free,它通过ITensorHandle封装了内存分配和生命周期。在NEON后端,TensorHandle的Allocate()最终会走到ACL的Tensor::allocator()->allocate()上;在CL后端则走OpenCL的clCreateBuffer。审计时要注意TensorHandleFactoryRegistry这个类,它负责创建并缓存TensorHandle,生命周期管理做得不好会导致显存泄漏,这点在实际部署中比性能还致命。
3.3 源码级调优的两个关键开关:FP16与Winograd
对于想在端侧把性能榨干的同学,ArmNN源码里有两个“白给”的优化点:FP16低精度推理和Winograd卷积。
先看FP16。在src/backends/cl/ClLayerSupport.cpp里,有IsFp16Supported的相关判断。CL后端天生支持FP16,NEON后端在Armv8.2-A及以上CPU上也可以开FP16(用ACL的F16数据类型)。我在飞腾D2000上实测,FP16推理相比FP32性能提升在1.4~1.8倍,内存占用直接砍半。但注意,不是所有层都支持FP16,源码里会通过LayerSupport做逐算子检查,不支持的就自动fallback到FP32,这个机制保证了图不会因为一个算子不支持而崩溃。
再看Winograd。ArmNN在ClConvolution2dWorkload和NeonConvolution2dWorkload里集成了ACL的Winograd实现。你不需要在API层显式开关,ACL内部会根据卷积核大小(通常是3x3)、步长、输入尺寸自动选择。但这里有个隐藏坑:Winograd对FP32的精度损失在某些模型上很敏感,OCR或者人脸关键点这类对数值敏感的模型,我建议把首层卷积关掉Winograd(用NCHW布局或显式设FastMath选项),防止误差累积到不可接受。
4. 边缘推理引擎落地方案:在ARM64板卡上从零部署ArmNN
4.1 环境准备:交叉编译还是板载直编,这是个战略选择
部署ArmNN的第一步不是下载源码,而是想清楚你打算在哪编、往哪跑。我遇到过很多朋友一上来就在树莓派上git clone然后硬编,等编译耗掉两小时又开始怀疑人生。这里我直接给结论:
- 板载直编:适合临时验证、小模型、PC和板子架构一致的情况。好处是环境简单,缺点是真的慢(飞腾D2000编一次ArmNN加ACL,全核跑也要半小时起步)。
- 交叉编译:适合正式集成阶段。在x86主机上用aarch64交叉工具链把ArmNN和ACL编好,再推到板子上。
我自己这次用的是交叉编译方案,工具链用的是gcc-aarch64-linux-gnu。需要说明的是,网上有些人推荐用Arm官方Compiler 5/6系列,那个主要是针对嵌入式裸机或老平台;如果你跑的是Linux+glibc环境,直接系统自带的aarch64-linux-gnu-gcc更省心,版本够新就行。我主机是Ubuntu 22.04,装了gcc-aarch64-linux-gnu、g++-aarch64-linux-gnu和crossbuild-essential-arm64。
ArmNN编译高度依赖两个外部库:Arm Compute Library(ACL)和Protobuf。ACL是必须的,因为NEON/CL后端全靠它;Protobuf是给模型解析器用的(TFLite/ONNX解析器都依赖它编pb格式)。如果在交叉编译时想省事,可以只编ACL的arm64版本,然后再编ArmNN时把BUILD_ACL关掉,直接链接预编译好的ACL库。
4.2 核心编译步骤:ACL与ArmNN的依赖顺序
我按自己的实操顺序来写,这里的每一步都是踩过坑后的最优解。
第一步:编译ACL
git clone https://github.com/ARM-software/ComputeLibrary.git -b v23.08 cd ComputeLibrary scons arch=arm64-v8a neon=1 opencl=0 embed_kernels=1 \ extra_cxx_flags="-fPIC" -j8几个参数必须说清楚:arch=arm64-v8a对应当前主流ARM64板卡(含飞腾、鲲鹏、RK3588、树莓派4B/5);neon=1启用NEON指令集;opencl=0是因为我目标平台不开GPU,纯CPU部署;embed_kernels=1把OpenCL内核编进库文件里,虽然这里用不到,但如果你后面要切GPU,这个scons参数可以省很多部署时的麻烦;extra_cxx_flags="-fPIC"一定要加,不然生成的是非位置无关代码,ArmNN去链接时直接报重定位错误。
第二步:编译ArmNN
git clone https://github.com/ARM-software/armnn.git -b v23.08 cd armnn mkdir build && cd build cmake .. -DARMCOMPUTE_ROOT=/path/to/ComputeLibrary \ -DARMCOMPUTENEON=1 -DARMCOMPUTECL=0 \ -DBUILD_TF_LITE_PARSER=1 \ -DBUILD_ONNX_PARSER=1 \ -DPROTOBUF_ROOT=/usr/local \ -DCMAKE_CROSS_COMPILE=ON \ -DCMAKE_SYSTEM_NAME=Linux \ -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \ -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ make -j8这个cmake命令里,ARMCOMPUTENEON=1是必选,表示启用CPU的NEON后端;ARMCOMPUTECL=0关掉GPU后端;BUILD_TF_LITE_PARSER和BUILD_ONNX_PARSER按需开,如果你只需要TFLite模型就只开前者;CMAKE_CROSS_COMPILE=ON告诉工具链这是交叉编译场景。
第三步:在板卡上部署
编译完成后,你会在build/下看到libarmnn.so和一堆测试工具。部署时把这些库文件推到板子上,同时注意ACL的libarm_compute.so也要一同部署。还有一个容易忽略的文件是libprotobuf.so,因为ArmNN的解析器依赖它。如果没有,板子上跑模型时会报error while loading shared libraries,这问题我在头一次部署时就踩了,排查方式是用ldd看依赖。
真正跑模型时还需要TFLite/ONNX模型文件。这里建议用TFLite模型入手,因为ArmNN对TFLite的算子覆盖度明显高于ONNX,尤其是量化模型方面。转换模型时,用tf.lite.TFLiteConverter按常规流程导出,但有一点特别重要:尽量导出FP32模型,让ArmNN在板子上动态选择精度,而不是在PC上先转成INT8再喂进去,因为ArmNN自己有一个针对NEON后端的int8优化链路,你提前量化反而限制了它。
4.3 在飞腾ARM64板上跑通第一个模型:完整命令与实测现象
我这边目标板子是飞腾D2000的工控机,系统是银河麒麟V10 ARM64。部署完后,直接写一个最小测试程序调用ArmNN的API。我这里用C++举例,因为ArmNN的C API虽然存在,但功能覆盖不完整,做正经项目还是C++更稳。
先写一个armnn_infer.cpp,核心流程如下:
#include <armnn/INetwork.hpp> #include <armnn/IRuntime.hpp> #include <armnn/Descriptors.hpp> #include <armnnTfLiteParser/ITfLiteParser.hpp> int main() { // 1. 创建运行时 armnn::IRuntime::CreationOptions options; auto runtime = armnn::IRuntime::Create(options); // 2. 解析TFLite模型 auto parser = armnnTfLiteParser::ITfLiteParser::Create(); auto network = parser->CreateNetworkFromBinaryFile("model.tflite"); // 3. 优化模型(绑定后端) std::vector<armnn::BackendId> backends = {"CpuAcc"}; auto optimized = armnn::Optimize(*network, backends, runtime->GetDeviceSpec()); // 4. 加载网络到运行时 armnn::NetworkId networkId; runtime->LoadNetwork(networkId, std::move(optimized)); // 5. 获取输入输出张量信息 auto inputTensorInfo = runtime->GetInputTensorInfo(networkId, 0); auto outputTensorInfo = runtime->GetOutputTensorInfo(networkId, 0); // 6. 构造输入输出缓冲 std::vector<float> inputData(inputTensorInfo.GetNumElements(), 1.0f); std::vector<float> outputData(outputTensorInfo.GetNumElements()); armnn::TensorInfo inputInfo = inputTensorInfo; armnn::TensorInfo outputInfo = outputTensorInfo; inputInfo.SetConstant(true); armnn::InputTensors inputTensors{{0, armnn::ConstTensor(inputInfo, inputData.data())}}; armnn::OutputTensors outputTensors{{0, armnn::Tensor(outputInfo, outputData.data())}}; // 7. 执行推理 runtime->EnqueueWorkload(networkId, inputTensors, outputTensors); // 8. 打印输出 for (float v : outputData) std::cout << v << " "; return 0; }编译这条命令,记得链接ArmNN和ACL:
aarch64-linux-gnu-g++ armnn_infer.cpp -o armnn_infer \ -I/path/to/armnn/include \ -L/path/to/armnn/build -larmnn \ -L/path/to/ComputeLibrary/build -larm_compute \ -lprotobuf -lpthread推到板子上跑,我实测的一个MobileNetV2(输入224x224x3,FP32)推理耗时在飞腾D2000上大概是85ms一帧。这个数字比TFLite的XNNPACK后端差不多,但ArmNN的优势主要体现在大模型多线程伸缩性和算子覆盖上。
值得一提的现象是,ArmNN默认是多线程的,它在NEON后端内部会用std::thread::hardware_concurrency()自动探测核数。我测试时D2000是8核,不加额外配置就能跑满多核。如果你发现推理时CPU占用只有一核满,大概率是ACL编译时没开neon=1,或者系统CPU亲和性设置有问题。
4.4 算子覆盖度与模型兼容性:部署前该做哪些检查
部署ArmNN最难受的其实不是性能,而是算子不支持导致的“图优化失败”。ArmNN内置的算子数量远不如TFLite,很多新算子(如TransposeConv的某些变体、GatherND、Range)要么不支持,要么只在特定后端支持。
我的建议是在部署前用ArmNN自带的armnnConverter工具先把模型转化一遍,它会输出详细的算子支持日志。比如:
./armnnConverter --f model.tflite --t tflite --o model.armnn这个命令会直接把TFLite转成ArmNN二进制格式,如果中间有算子不支持,日志会明确打印Unsupported layer: X。我当时转一个语义分割模型时,日志里就报了个ResizeBilinear的unsupported,换成了ResizeNearestNeighbor才过。
算子覆盖度问题,可以提前在PC上用armnn_tflite_parser的单测样例摸个底,也可以看GitHub上的算子支持矩阵。实话说,ArmNN目前支持TFLite的算子数量大约在150~200个左右,覆盖70%~80%的常见CV/NLP模型,够用但不宽裕。
5. 踩过的坑与性能调优实操手册
5.1 交叉编译时最隐蔽的坑:Protobuf版本与ACL的fPIC问题
先说交叉编译这个环节。很多人卡在“编译ArmNN时找不到protobuf”,这是因为ArmNN的TFLite/ONNX解析器是依赖protobuf生成的头文件来解析模型文件的。如果主机上装的是protobuf v22+,而ArmNN编译时用的HOST protoc又是旧版本,生成的代码版本不匹配,就会出现一堆protobuf相关的编译错误。
我这里的解法是固定用系统自带protobuf(Ubuntu 22.04自带libprotobuf-dev是3.12.4),并且交叉编译时把PROTOBUF_ROOT指到交叉环境的/usr/aarch64-linux-gnu下,同时确保protoc(HOST端)和libprotobuf(TARGET端)版本一致。
ACL的fPIC问题前面提了一句,这里再展开:如果scons时没加extra_cxx_flags="-fPIC",那么编译ArmNN时会报类似“relocation R_AARCH64_ADR_PREL_PG_HI21 cannot be used against symbol”的错误。这个问题的根因是ACL生成的静态库里的目标文件没有位置无关代码,没法被ArmNN的共享库链接。解决办法就是重新scons一遍,加fPIC,或者退一步用ACL的预编译包。
另一个想提醒的是不要用Arm官方老版本Compiler 5/6去编ArmNN。网上搜“arm compiler 5.06u7下载”之类的内容多半是搞MCU裸机或者老平台开发的,ArmNN需要相对现代的C++17标准,老编译器一编就废。在64位Linux环境上一律优先系统GCC或Clang。
5.2 模型推理精度异常:从输出全0到梯度爆炸的排障思路
部署推理引擎后最容易遇到的一个“产品级问题”是:模型推理结果和PC端不一致,甚至输出全是0。这类问题在ArmNN里我总结出三个高频根因:
- 输入数据预处理不一致。ArmNN的输入Tensor默认不做任何归一化,如果你在PC端喂的是归一化后的数据、上板后忘了归一化,结果自然不同。排查时先确保输入预处理和训练时完全一致。
- FP16精度截断。如果你开了FP16或者模型本身是FP16权重,但校验逻辑对误差特别敏感,会出现几个百分点的偏差。可以在
Optimize时把ReduceFp32ToFp16的优化选项关掉,用纯FP32跑一遍对比。 - 量化参数不对。TFLite量化模型在ArmNN上需要正确的
per-channel或per-tensor量化因子。尤其是一些自己训练的模型转换时,量化因子计算可能有误。最简单的方法是先用FP32模型验证整条链路没问题,再切量化。
还有一种情况是输出全0,这通常不是模型本身的问题,而是输出张量没有初始化。我在前面给的代码里已经用outputData初始化为0,但如果你创建一个空向量再当输出缓冲用,Flush内存里的内容可能就全是0了。这问题虽然低级,但确实常有。
5.3 性能调优:从85ms到49ms,我做了三件事
前面提到MobileNetV2在飞腾D2000上默认耗时85ms。经过三轮调优后,我压到了49ms左右,整体提升接近42%。这三件事分别是:
第一件,打开ACL的多线程配置。ArmNN本身是多线程的,但ACL内部的线程池默认可能被限制。可以在初始化时调用arm_compute::Scheduler::get().set_num_threads(8),强制线程数。修改后从85ms降到了72ms。
第二件,走FP16推理。因为飞腾D2000支持Armv8.2-A的FP16扩展,我把模型用TFLite转换时设inference_type=tf.float16,然后在ArmNN里用Optimize的ReduceFp32ToFp16选项让它自动转FP16。这一步磁盘模型变小一半,耗时从72ms降到54ms。
第三件,绑定CPU核与调整内存分配策略。用sched_setaffinity把推理线程钉在4~7核,避开系统中断占用的0~3核,同时修改TensorHandleFactory的分配策略,把内存分配模式从默认的Malloc切换为AlignedAllocator(32字节对齐),这样NEON指令搬运数据时不再出现跨cacheline访问。最终耗时为49ms。
这个调优过程说明,ArmNN的性能潜力不只是在算子内核本身,调度的精细度、内存策略、线程配置都会叠加影响最终效果。所以你在板子上跑得慢,别急着怀疑框架,先上面三件事逐个试一遍。
5.4 交叉编译产物在目标板上常见运行错误排查表
在目标板上跑ArmNN推理程序时,我整理了下面这张故障排查表。遇到问题时建议直接对照,能省掉一大半排查时间。
| 现象 | 直接原因 | 解决方案 |
|---|---|---|
报错error while loading shared libraries: libarmnn.so | 库路径没配置 | 在板子上执行export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH或把so拷贝到/usr/lib |
报错Illegal instruction | 编ACL时选了不匹配的arch | 统一用arch=arm64-v8a,确认CPU支持FP16/NEON |
| 推理结果全0 | 输出缓冲未初始化,或输入预处理差异 | 先用全1输入测通,再逐一核对预处理逻辑 |
编译时报GLIBCXX_3.4.29 not found | 板子系统gcc版本过低 | 不要用太新的GCC交叉编译,尽量用与板载系统匹配的版本 |
模型转出时报Unsupported layer | 模型包含ArmNN未支持的算子 | 用armnnConverter导出支持日志,替换成等价算子 |
5.5 往NPU迁移的扩展思路:从ArmNN到Ethos-U的路径
最后说一个前瞻性话题。ArmNN不只是CPU/GPU推理引擎,它对Arm自家NPU(Ethos-U55、Ethos-U65等)的支持是官方路径。在源码里,src/backends/ethosn/就是一个独立的后端实现。
如果你手上是Cortex-M55/M85搭配Ethos-U55这类MCU+NPU组合,ArmNN的推理图可以经由EthosNBackend把算子编译成NPU可执行的命令流,剩下的算子仍然CPU兜底。这种“CPU+NPU混合调度”的能力,在TFLite Micro生态里是极其难得的设计。
但要注意,Ethos-U后端要求模型是INT8量化模型,且支持的算子清单非常有限,基本上是Conv2D、DepthwiseConv2D、Add、MaxPool2D、Reshape这些基础算子的排列组合。如果模型结构较复杂,建议先用Arm的Vela编译器(也是基于ArmNN图优化思路的)做模型转换,确认支持情况后再跳到ArmNN的Ethos-N后端。
6. 写在实际部署之后
ArmNN这套引擎,源码质量在我接触过的推理框架里属于第一梯队。它不像TensorFlow那样浑身上下都是历史包袱,也不像TVM那样为了极致的自动调优把工程复杂度拉满,它的思路非常清晰:做好Arm硬件上的调度、算子和内存管理,把其他复杂度交给生态工具去解决。对想要在ARM64板卡上实现低延迟、稳定推理的团队来说,ArmNN确实值得认真考虑。
我个人最推荐的使用路径是:先用TFLite做快速原型验证,然后切到ArmNN做正式部署,中间用armnnConverter做算子兼容性检查,最后按FP16、多线程、内存对齐的顺序做三轮调优。这套组合拳几乎适用于所有Cortex-A系列Linux平台,包括树莓派、RK3588、飞腾、鲲鹏。
如果你手头有项目正卡在“模型能跑但太慢”或者“TFLite不支持某些算子”的窘境,不妨把ArmNN拉出来遛一遛。源码审计的乐趣不在于读懂了多少行代码,而在于你终于知道框架在关键路径上替你做了什么、没替你做什么。搞清楚这两点,端侧AI落地这件事,会简单很多。