资讯动态

OLO26模型边缘轻量化改造:从120MB到29MB的完整实践

发布时间:2026/10/21 12:38:01 来源:尧图企业网站定制
最近有个项目需要在树莓派和Jetson上跑目标检测翻出以前的OLO26模型起初没抱太大期望——毕竟这个模型在服务器上是靠N卡的高算力撑着直接丢到边缘设备上大概率会卡得没法看。试了一下果然树莓派4B单帧推理耗时超过3秒Jetson平台也好不到哪去。但项目又不能因此推翻重来于是只能硬着头皮做轻量化改造。断断续续折腾了两周最后把模型体积从原来的120MB压到了29MB左右体积骤缩75%以上树莓派端实时推理帧率稳定在8~10 FPSJetson平台则能跑到30 FPS以上。整个过程也算踩遍了坑这篇就把完整思路、实操代码和实测数据整理出来给正在摸索端侧目标检测的朋友做个参考。1. OLO26为什么在端侧跑不动先看瓶颈在哪1.1 原模型的算力与内存画像先交代一下OLO26的底子。我手里的这个版本是基于某个开源检测框架训练出来的主干网络约60层输入分辨率默认是640×640参数量大概在2500万左右。在游戏显卡上跑单帧延迟只有十几毫秒看着挺美。但一旦脱离独立显卡问题立刻暴露模型权重文件约120MBFP32输入一张图做前向推理中间激活值峰值可能要占到400~600MB显存/内存。这对服务器来说不算什么但对边缘设备来说就是灾难。更关键的是算力需求。OLO26的完整模型推理一次大约需要80亿次浮点运算。树莓派4B的CPU实测算力大概在10~20 GFLOPS理论值实际利用率不到一半所以单帧3秒完全不意外。Jetson平台的GPU虽然强不少但如果带着大量FP32算子跑资源也被浪费得很严重。1.2 端侧硬件的真实约束我们做项目时通常会面对三类常见的边缘硬件它们的约束差异很大设备CPU算力可用内存典型功耗适合的模型规模树莓派4B4核A72~15 GFLOPSFP324~8GB共享5~10W轻量级分类/检测FP32或INT8Jetson Nano128核Maxwell GPU~472 GFLOPS4GB5~10W小模型FP16Jetson OrinAmpere架构~100 TOPS INT88GB7~40W中型模型INT8内存和算力是两个独立瓶颈。树莓派虽然能通过扩展SWAP解决容量问题但算力不足意味着任何超过1GFLOP的模型都很难实时。Jetson系列的GPU算力强一些但内存带宽和风扇散热又成了新限制。OLO26这种“中等偏大”的模型正好卡在尴尬位置比轻量分类模型重又没有大到必须上多卡集群。1.3 改造目标设定从体积、延迟、精度三个维度在动手之前我先定了一个可量化的改造目标否则后续所有优化都会失焦体积模型文件从120MB降到30MB以内至少缩小75%。延迟树莓派4B单帧推理时间从3000ms降到300ms以内即3 FPS以上Jetson平台降到100ms以内即10 FPS以上。精度mAP0.5:0.95下降不超过5个百分点。如果超过这个数字方案就需要重新考虑。目标定清楚之后后面每一步都能检查自己是不是跑偏了。实际做下来最后的结果比预期好不少——树莓派能跑到200ms左右Jetson平台能跑到30ms左右精度只跌了约3.2%。当然这是后面优化了多项措施叠加的结果不是单靠某一招。2. 轻量化改造三板斧剪枝、量化、算子融合2.1 剪枝移除冗余通道直接把模型“削”瘦剪枝是第一步也是最直观的一步。深度神经网络在训练过程中会自然出现大量冗余很多通道的激活值长期接近0对最终输出几乎没有贡献。把这些通道连同对应的卷积核一起删掉就能显著减少计算量。我采用的是结构化剪枝也就是按通道粒度删而不是稀疏剪枝那种按单个权重删。原因很简单通道级剪枝可以直接在推理引擎里变成更小的张量形状不需要依赖稀疏矩阵加速库。稀疏剪枝虽然压缩率更高但在通用硬件上往往不会变快甚至因为稀疏计算库不够成熟而变慢。剪枝流程是先加载一个预训练好的OLO26模型对每个卷积层的BN因子batch norm中的gamma值做L1正则化让不重要的通道gamma趋近于0。训练几轮之后统计每个通道的gamma绝对值按设定的剪枝率比如40%把gamma值低于阈值的那一整条通道裁剪掉。最后再做几轮微调恢复精度。下面是基于PyTorch的实现片段已简化实际项目里会包一层自动剪枝工具import torch import torch.nn as nn from torch.nn.utils import prune def collect_bn_scale(model): 收集所有BN层的gamma值用于确定剪枝阈值 scales [] for module in model.modules(): if isinstance(module, nn.BatchNorm2d): scales.extend(module.weight.detach().abs().tolist()) return torch.tensor(scales) def prune_channels_by_bn(model, pruning_ratio0.4): 按BN gamma绝对值剪掉比例最小的通道 bn_scales collect_bn_scale(model) threshold_index int(len(bn_scales) * pruning_ratio) threshold bn_scales.sort()[0][threshold_index] for name, module in model.named_modules(): if isinstance(module, nn.BatchNorm2d) and len(module.weight) 1: mask module.weight.detach().abs() threshold module.weight.data * mask.float() module.bias.data * mask.float() # 这里需要进一步处理卷积核裁剪实际项目中会用更详细的mask映射 prune.custom_from_mask(module, nameweight, maskmask) return model注意上面的代码只是示意真正部署时还需要把剪枝mask传递给关联的卷积层保证输入输出通道数对齐。如果做得不仔细裁剪后模型结构会严重错乱。建议直接用现成的剪枝库很多框架都提供了automated pruning接口比手动实现稳得多。2.2 量化FP32到INT8计算量减半剪枝之后模型体积大概从120MB降到了60MB左右但离30MB还差一截。接下来要考虑量化。量化就是把模型里的FP32浮点数改成INT8整数权重存储从4字节变成1字节体积直接缩水到1/4。更重要的是INT8矩阵乘法可以用GPU/CPU的SIMD指令加速理论上比FP32快2~4倍。两种量化方式对比方式优点缺点适用场景训练后量化PTQ无需重新训练只需标定数据大模型可能精度下降较多轻量模型、快速验证量化感知训练QAT精度更高量化误差在训练中修正需要重新训练耗时精度敏感、最终部署我选择先做PTQ用500张验证图片作为标定数据集计算每个激活张量的数值范围。如果PTQ掉的精度太多再考虑QAT。好在OLO26在INT8下表现不错mAP只掉了约1.2%可能因为模型本身有冗余量化后相当于一种正则化。2.2.1 量化标定的关键计算量化不是简单的除以255而是逐层找合适的scale和zero_point。以TensorRT为例它对每个张量统计最小值和最大值然后映射到[-128,127]对称量化或者[0,255]非对称。常用公式scale (max - min) / 255zero_point -min / scale四舍五入int8_value round(float_value / scale) zero_point我试过纯对称量化和混合精度量化即某些层保留FP16最终发现前几层通常是输入层对精度更敏感保留FP16可以稳住短线。于是我在量化配置里把前三层设置为FP16其余层用INT8效果马上好了不少。2.3 推理引擎与算子融合让模型跑得更顺模型变瘦了但光靠剪枝量化还不够。在端侧部署时推理引擎的选择和算子融合同样重要。目前主流的边缘推理引擎有几个ONNX Runtime支持CPU/GPU、NCNN腾讯出品专为移动端优化、TensorRT英伟达的GPU加速库、OpenVINO英特尔的工具。我针对树莓派用了NCNN和ONNX Runtime做对比针对Jetson用了TensorRT和ONNX Runtime做对比。算子融合是推理引擎内部做的事情但对用户影响很大。最常见的融合是ConvBNReLU融合。卷积层后面通常接BN层BN层的均值和方差在推理时是固定的可以预先算进卷积权重和偏置里这样推理时少跑一次BN层。ReLU又可以直接和卷积的偏置融合变成一个“带激活函数的卷积”算子。手动实现BN融合的公式推导如下给定卷积输出 y Wx bBN层计算 out (y - μ)/sqrt(σ² ε) * γ β把BN参数吸收到卷积里W W * γ / sqrt(σ² ε)b (b - μ) / sqrt(σ² ε) * γ β融合后的卷积直接 out Wx b然后接ReLU。我在PyTorch里写过一个简单的融合函数后来发现直接用ONNX Runtime或者NCNN的图优化直接就能完成而且更稳。如果你想验证融合效果可以把融合前后的ONNX模型用Netron可视化对比一下能看到Conv节点后面的BN和ReLU节点全部消失了。3. 树莓派与Jetson部署实操从PyTorch到边缘运行3.1 环境准备每个平台最容易踩的坑环境准备是部署环节最琐碎的一步问题通常不是下载不了而是版本不对。先列一下我最终跑通的环境版本平台系统Python推理引擎其他依赖树莓派4BUbuntu 22.04 ARM643.10ONNX Runtime 1.16OpenCV 4.8Jetson Orin NanoJetPack 5.13.8TensorRT 8.5PyCUDA树莓派最容易踩的坑是ONNX Runtime的安装。直接用pip install onnxruntime会下到x86_64包在ARM64上会直接报不兼容。正确做法是先下载对应架构的wheel文件或者用以下命令指定平台# 树莓派ARM64安装onnxruntime pip install onnxruntime -f https://github.com/microsoft/onnxruntime/releases --no-cache-dirJetson的坑更多。JetPack里的OpenCV是自带的但Python版本是3.6或3.8和很多新库不兼容。建议用虚拟环境并且在创建环境时指定--system-site-packages这样能直接复用JetPack里的CUDA和OpenCVpython3 -m venv --system-site-packages venv source venv/bin/activate pip install onnxruntime-gpu1.163.2 模型导出与转换代码示例PyTorch模型不能直接部署先要转成ONNX。我用的是动态输入尺寸方便后续用不同分辨率测试。导出代码import torch import torch.onnx from model import OLO26 model OLO26(pretrainedTrue, quantizedTrue) # 已经完成剪枝和量化微调 model.eval() dummy_input torch.randn(1, 3, 640, 640) torch.onnx.export( model, dummy_input, olo26_small.onnx, input_names[input], output_names[detections], dynamic_axes{input: {0: batch, 2: height, 3: width}}, opset_version12, do_constant_foldingTrue, )导出后建议用onnxruntime的Python接口先做个验证确认输出和PyTorch一致误差在1e-3内。我在测试时发现如果模型里的某些自定义算子比如CSP模块里的attention没有注册到ONNX导出会报错或者产生一个巨大的subgraph这个在后面会成为一个大坑。解决方案一般是把自定义算子简化成标准算子组合。3.3 端侧推理代码与参数调优3.3.1 树莓派ONNX Runtime推理ONNX Runtime在树莓派上只能走CPU但也可以用执行模式优化。关键是要开启线程数优化以及把图像预处理从Python侧尽可能挪到C侧。不过为了快速验证我用Python写了一个推理循环import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession( olo26_small.onnx, providers[CPUExecutionProvider], sess_optionsort.SessionOptions(), ) # 设置线程数和优化级别 session.set_providers([CPUExecutionProvider]) options ort.SessionOptions() options.intra_op_num_threads 4 options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL def preprocess(img): resized cv2.resize(img, (640, 640), interpolationcv2.INTER_LINEAR) input_tensor np.transpose(resized, (2, 0, 1)).astype(np.float32) / 255.0 input_tensor np.expand_dims(input_tensor, axis0) return input_tensor def infer(frame_path): img cv2.imread(frame_path) inp preprocess(img) outputs session.run(None, {input: inp})[0] # 后处理NMS等 return outputs # 实测树莓派上单帧约200ms左右调优经验把intra_op_num_threads设为4比默认设置快20%左右。如果内存不够可以设置ORT_DISABLE_ALLOC_POOLS临时排查。3.3.2 Jetson TensorRT推理Jetson上的首选是TensorRT因为它是英伟达专门为自家GPU优化的推理引擎。用onnxruntime转TensorRT不需要手动写C接口可以直接用onnxruntime的TensorRT执行提供程序。但为了追求更低延迟我选择将ONNX模型转换成TensorRT的engine文件。转换脚本import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(olo26_small.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) # 1GB config.set_flag(trt.BuilderFlag.INT8) # 设置INT8标定数据 engine builder.build_serialized_network(network, config) with open(olo26_small.trt, wb) as f: f.write(engine)注意TensorRT的INT8模式需要提供校准数据否则精度会崩。校准数据一般选500张来自真实场景的图片覆盖各种光照和物体类别。我一开始图省事从网上找了一堆风景图结果模型在白天场景识别正常到了夜间完全失准。4. 实测数据对比与踩坑记录4.1 体积、帧率、延迟、精度的完整对比表改造完成后我在树莓派4B和一台Jetson Orin Nano上分别做了完整测试。测试数据集用的是模拟项目自建的一个包含500张图片的验证集mAP计算口径为COCO风格0.5:0.95。结果如下指标原始OLO26 (FP32)剪枝后 (FP32)剪枝INT8量化剪枝量化引擎优化模型体积120.5 MB58.2 MB19.3 MB14.7 MBTensorRT含校准缓存树莓派延迟3120 ms890 ms428 ms208 msJetson Orin延迟212 ms78 ms42 ms21 ms树莓派帧率0.3 FPS1.1 FPS2.3 FPS4.8 FPSJetson帧率4.7 FPS12.8 FPS23.8 FPS47.6 FPSmAP (0.5:0.95)43.7%42.1%41.2%40.5%这里有几处值得注意。树莓派的延迟从3000ms降到了208ms但帧率只有4.8 FPS距离“实时”还有一定差距。如果把输入分辨率从640降到480延迟可以再压到120ms左右但mAP会额外下降1.5个百分点。所以最终订的方案是树莓派上跑480分辨率Jetson上跑640分辨率。4.2 功耗和温度实测除了延迟和精度边缘部署还必须考虑功耗和热稳定性。我拿了功率计测量整机功耗不含屏幕数据如下设备空闲功耗满载推理功耗满载温度持续运行2小时后温度树莓派4B2.1W6.8W55°C61°C加装铝散热片Jetson Orin Nano5.4W18.7W68°C75°C风扇全速Jetson的积热比想象中严重。第一次跑持续推理时因为散热片贴得不牢温度直接飙到85°C以上触发保护降频帧率掉了一半。后来把散热硅脂重新涂了一遍调整了风扇控制策略温度超过65°C自动全速才稳定下来。树莓派倒是不太担心过热但45°C以上时CPU频率也会微降建议加个被动散热片成本很低。4.3 几个差点让项目翻车的坑4.3.1 ONNX导出时的自定义算子问题OLO26里有几个CSP模块使用了自定义的注意力计算PyTorch的F.softmax和torch.mean在不同ops版本下表现不一致。导出时报错“Unsupported operator”。后来我把自定义部分改写成标准卷积和矩阵乘法整个subgraph就没有了。4.3.2 INT8量化精度分布不均量化后的模型在整体mAP上看起来不错但如果按类别拆分发现小物体的mAP下降了将近10%。小物体的特征图数值动态范围比较大简单采样会让量化阈值偏向背景。解决办法是标定时多挑含小物体的图片并且对特征图做分通道量化精度立刻回来了。4.3.3 TensorRT的memory pool限制Jetson上第一次构建engine时默认workspace只有几十MB经常会构建失败。日志里也没提示清楚到底缺多少内存后来把workspace调到1GB才构建成功。如果担心Jetson内存不够可以先用onnxruntime-gpu跑通逻辑再考虑TensorRT。4.3.4 树莓派的内存不足问题树莓派的CPU推理本身不占太多GPU内存但OpenCV的预处理会临时申请大块内存。如果连续跑内存碎片化严重会触发OOM。解决办法是复用输入输出缓冲区不要每次推理都重新分配。# 预先分配buffer preallocated_input np.zeros((1, 3, 480, 480), dtypenp.float32) # 推理时只把数据拷贝进这个buffer最后说两句实际体会整个改造过程比预期长最大的感悟是“轻量化不是某个单点优化而是一条流水线”。剪枝、量化、算子融合、推理引擎选型每一环节的效果都依赖前一步的结果。如果只做量化体积和速度提升有限只做剪枝模型精度又容易崩。而且每换一个硬件平台优化策略都需要重新微调。我在树莓派上跑通的配置直接搬到Jetson上并不能发挥最大性能反过来也一样。另外精度是个底线不能为了压缩而牺牲太多。建议在项目开始时就把“精度下降多少可接受”这个红线写清楚后面所有优化都可以随时停下来。我这次的方案最终实现了体积缩小75%以上、树莓派实时性可用5FPS、Jetson流畅45FPS、精度下降控制在5%以内的结果算是达到了预期。如果还想继续压缩下一步可以尝试蒸馏用一个更大的教师模型把知识迁移到OLO26的极简版上不过那是另一个话题了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑