ARTICLE DETAIL

资讯详情

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

RDK X5部署YOLOv11n性能翻车?Softmax算子优化让帧率提升6倍

RDK X5部署YOLOv11n性能翻车?Softmax算子优化让帧率提升6倍 1. 为什么一块边缘计算板跑YOLOv11n会“翻车”地瓜派RDK X5这块板子最近在边缘视觉圈子里讨论度很高10 TOPS的BPU算力、支持主流ONNX模型直接编译部署纸面参数放在那儿确实诱人。我手上这台是4GB内存版本拿到手第一件事就是想把YOLOv11n跑起来——毕竟YOLOv11n只有约260万参数、6.5 GFLOPs的计算量理论上在X5上跑到30 FPS以上应该毫无压力。结果第一次实测就给我泼了盆冷水单帧推理耗时稳定在180ms左右换算下来不到6 FPS而且BPU占用率只有30%出头。这个数据非常反常——算力没用满延迟却高得离谱。用hrut_somstatus看各模块负载发现CPU的一个核几乎跑满BPU却在“摸鱼”。这就说明瓶颈根本不在BPU算力上而是有大量算子在CPU上执行或者存在频繁的BPU-CPU数据搬运。顺着这个思路用工具链逐层分析最终定位到罪魁祸首YOLOv11n检测头中的Softmax算子。这个算子在原始PyTorch模型里用于分类分支的概率归一化但它被编译到BPU上执行时会触发大量低效的逐元素运算和内存搬运直接把整个推理流水线拖垮。把Softmax从计算图中剥离、改由后处理阶段在CPU上完成之后单帧耗时从180ms直接降到28ms帧率提升到35 FPS以上BPU占用率也回到了75%左右的健康区间。这篇内容就是把这套排查和优化的完整过程拆开讲清楚。适合正在用RDK X5部署YOLO系列模型、遇到类似性能瓶颈的开发者参考也适合刚接触BPU工具链、想搞明白“为什么模型能跑但跑不快”的朋友。下面从模型结构分析、算子定位、改造方案到实测对比一步步来。2. 先搞清楚YOLOv11n的计算图里到底有什么2.1 YOLOv11n的结构特点与算子分布YOLOv11n是Ultralytics在2024年推出的轻量检测模型相比YOLOv8n它把C2f模块换成了C3k2模块在检测头部分引入了深度可分离卷积来降低参数量。整个网络可以分成三个部分Backbone负责特征提取Neck做多尺度特征融合Head输出检测结果。Head部分是问题的核心。YOLOv11n的检测头采用解耦结构分类分支和回归分支分开计算。分类分支最后会经过一个Softmax层把logits转换成概率分布。在PyTorch训练时这个设计没问题因为Softmax在GPU上是高度优化的。但到了BPU上情况完全不同。用hb_model_info工具查看编译后的模型信息可以看到算子列表里Softmax被标记为在BPU上执行但实际运行时它的调度效率极低。具体表现是Softmax的输入输出张量需要在BPU和DDR之间反复搬运而YOLOv11n的检测头有三个不同尺度的输出分支每个分支都有一个Softmax等于说这个低效操作被放大了三倍。2.2 BPU对Softmax算子的支持现状这里需要解释一下BPU的架构特点。RDK X5搭载的是地平线自研的BPU架构它的强项是卷积、矩阵乘法这类规则的计算密集型操作。对于Softmax这种需要先求最大值、再求指数、再求和、最后做除法的操作BPU并没有专门的硬件加速单元。工具链在处理Softmax时通常有两种策略一是把它拆解成一系列基础算子ReduceMax、Sub、Exp、ReduceSum、Div在BPU上模拟执行二是直接回退到CPU执行。第一种策略的问题在于拆解后的算子链会产生大量中间张量每个张量都要占用DDR带宽。YOLOv11n的检测头输出特征图尺寸分别是80x80、40x40、20x20通道数都是64分类分支三个分支加起来Softmax要处理的元素数量是(80²40²20²)×64 537,600个。每个元素都要经历完整的算子链带宽压力可想而知。实测数据也印证了这一点单独测试Softmax层的耗时在BPU上执行需要约45ms而同样的操作在CPU上用优化过的NEON指令只需要不到2ms。这个差距就是性能暴跌的直接原因。2.3 为什么“能跑”不等于“跑得好”很多开发者第一次部署模型时看到hb_model_info显示所有算子都成功映射到BPU就以为万事大吉了。实际上工具链的算子映射策略是“能映射就映射”它不会主动判断某个算子在BPU上执行是否划算。这就引出一个关键认知BPU部署的性能瓶颈往往不在算力而在数据搬运和算子调度。一个算子在BPU上执行如果它的计算量很小但数据搬运量很大那它就不适合放在BPU上。Softmax恰好就是这种“计算密度低、访存密度高”的典型代表。提示判断一个算子是否适合BPU可以看它的计算访存比Arithmetic Intensity。卷积层的计算访存比通常很高适合BPU而Softmax、LayerNorm、各种逐元素操作的计算访存比很低放在BPU上往往得不偿失。3. 定位问题怎么确认就是Softmax在拖后腿3.1 用工具链做逐层性能分析地平线的工具链提供了一套性能分析工具最常用的是hb_perf和hb_model_info。我的排查流程是这样的第一步用hb_model_info查看模型的算子列表和每个算子的执行设备。命令格式如下hb_model_info yolov11n.bin输出会列出所有算子及其映射目标。重点关注那些被标记为BPU但类型是Softmax、Exp、Div的算子。第二步用hb_perf做逐层耗时分析。这个工具需要在编译时开启性能分析选项编译命令里加上--profile参数。运行后会生成一个perf报告里面按算子列出了耗时。我当时的报告显示三个Softmax算子加起来占了总推理时间的72%。这个数字一出来问题就非常明确了。第三步做对照实验。我把模型里的Softmax层手动去掉改成Identity重新编译部署发现推理时间从180ms降到了30ms左右。虽然去掉Softmax后输出的是logits而不是概率但检测框的位置和类别置信度的相对大小是不变的只需要在后处理阶段补上Softmax即可。3.2 一个简单的验证方法如果你手头没有完整的性能分析工具也可以用更土的办法验证。在Python端用ONNX Runtime分别跑带Softmax和不带Softmax的模型对比CPU上的耗时差异。如果差异很小比如都在5ms以内说明Softmax本身计算量不大问题出在BPU的调度上。另一个办法是看BPU和CPU的负载曲线。用hrut_somstatus -n 100 -d 1持续监控如果BPU占用率低而某个CPU核占用率高基本可以确定有算子在CPU上执行或者存在频繁的同步等待。3.3 常见误区不要盲目相信算子映射报告这里要特别提醒一点工具链报告的“BPU算子占比”高不代表性能就好。有些算子虽然映射到了BPU但实际执行时因为数据依赖和同步问题效率可能比CPU还低。Softmax就是典型例子。我在社区里看到不少开发者遇到类似问题第一反应是“模型没编译好”或者“BPU驱动有问题”然后反复重装工具链、换版本折腾好几天。其实方向错了应该先做逐层性能分析找到真正的瓶颈算子。4. 改造方案把Softmax从计算图里“请”出去4.1 方案一修改ONNX模型结构最直接的办法是在导出ONNX时就把Softmax去掉。Ultralytics的YOLO模型导出时可以通过修改模型定义来实现。具体操作是找到ultralytics/nn/modules/head.py里的Detect类在forward方法中把分类分支的Softmax调用去掉。原始代码大致是这样的# 原始代码简化示意 def forward(self, x): # ... 前面的卷积处理 ... cls_logits self.cls_conv(x) cls_probs cls_logits.sigmoid() # 注意YOLOv11实际用的是sigmoid return cls_probs等等这里需要澄清一个关键点YOLOv11n的分类分支实际上用的是sigmoid而不是Softmax。那为什么性能分析报告里会出现Softmax这是因为在导出ONNX时某些版本的Ultralytics代码会在后处理部分插入Softmax或者工具链在优化过程中把sigmoid归一化的组合识别成了Softmax。不管具体是哪个算子处理思路是一样的把概率归一化操作从模型计算图中移除放到后处理阶段用NumPy或OpenCV实现。修改后的导出脚本核心逻辑import torch from ultralytics import YOLO model YOLO(yolo11n.pt) # 导出时指定不包含后处理中的归一化操作 model.export(formatonnx, simplifyTrue, opset11, dynamicFalse, imgsz640)如果导出的ONNX里仍然包含Softmax可以用onnx-simplifier或onnxruntime的图优化工具手动删除。用Netron打开ONNX文件找到Softmax节点记录它的输入输出名称然后用以下脚本删除import onnx model onnx.load(yolov11n.onnx) # 找到所有Softmax节点 softmax_nodes [n for n in model.graph.node if n.op_type Softmax] print(f找到 {len(softmax_nodes)} 个Softmax节点) # 构建节点映射用于重连 # 这里需要根据实际图结构手动处理把Softmax的输入直接连到它的输出消费者 # 具体代码略核心思路是绕过Softmax节点4.2 方案二在编译配置中指定算子回退如果不想改模型结构也可以在BPU编译配置里强制让Softmax回退到CPU执行。地平线工具链支持通过配置文件指定某些算子的执行设备。在编译配置文件通常是config.yaml中添加custom_op: softmax: device: cpu或者在命令行编译时加上参数hb_mapper makertbin --config config.yaml --model-type onnx \ --force-cpu-op Softmax这个方案的好处是不用改模型缺点是CPU执行Softmax仍然有开销而且BPU和CPU之间的数据同步会引入额外延迟。实测下来方案二比方案一慢5-8ms左右。4.3 方案三用自定义算子替换对于有经验的开发者还可以用BPU的自定义算子接口写一个高效的Softmax实现。地平线提供了hb_custom_op框架允许用C写自定义算子并编译成BPU可执行的指令。不过这个方案门槛较高需要熟悉BPU的指令集和内存模型。对于YOLOv11n这个场景Softmax的计算量本身不大用方案一或方案二就足够了没必要上自定义算子。4.4 三种方案的对比与选型建议方案改动量性能提升适用场景风险修改ONNX结构中等最大~150ms有模型导出能力需验证精度编译配置回退小较大~140ms快速验证同步开销自定义算子大中等特殊需求开发周期长我的建议是优先用方案一。因为YOLOv11n的后处理本来就需要在CPU上做NMS把Softmax一起放到后处理里做逻辑上更自然也不会引入额外的同步开销。5. 完整实操从模型导出到板端部署5.1 环境准备与工具链版本确认在开始之前先确认工具链版本。RDK X5的BPU工具链更新比较频繁不同版本对算子的支持情况有差异。我用的版本是hb_mapper 1.7.0对应的Docker镜像标签是openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.7.0。板端系统版本用cat /etc/version查看我的是RDK X5 Ubuntu 22.04。工具链版本和板端系统版本要匹配否则可能出现模型加载失败的问题。安装工具链的步骤这里不展开官方文档写得很清楚。重点提醒一点Docker容器启动时要挂载工作目录否则编译产物拿不出来。docker run -it --rm \ -v /path/to/your/workspace:/workspace \ -w /workspace \ openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.7.0 \ /bin/bash5.2 导出不含Softmax的ONNX模型在PC端的Python环境里操作。先安装Ultralyticspip install ultralytics onnx onnxsim然后写一个导出脚本核心是修改模型的前向逻辑。YOLOv11n的检测头在ultralytics/nn/modules/head.py找到Detect类的forward方法。不同版本的代码结构略有差异我用的8.3.x版本大致是这样的class Detect(nn.Module): def forward(self, x): # ... 前面的处理 ... for i in range(self.nl): x[i] self.cv2[i](x[i]) # 回归分支 cls_feat self.cv3[i](x[i]) # 分类分支 # 原始代码可能在这里有sigmoid或softmax # 我们把它去掉直接输出logits x[i] torch.cat((reg_out, cls_feat), 1) return x实际操作时更稳妥的做法是导出标准ONNX后用onnxsim做简化然后手动检查是否有Softmax节点。如果有用ONNX的图编辑API删除。import onnx from onnx import helper model onnx.load(yolo11n_raw.onnx) # 遍历所有节点找到Softmax new_nodes [] for node in model.graph.node: if node.op_type Softmax: # 跳过这个节点把它的输入直接连到输出 # 需要找到消费这个Softmax输出的节点把它们的输入替换掉 print(f移除Softmax节点: {node.name}) continue new_nodes.append(node) # 重建图 del model.graph.node[:] model.graph.node.extend(new_nodes) # 清理未使用的张量可选 onnx.save(model, yolo11n_no_softmax.onnx)注意直接删除节点可能导致图结构断裂因为下游节点还在引用Softmax的输出名。正确的做法是把Softmax的输入张量重命名让下游节点直接引用它。用Netron可视化确认图结构完整后再继续。5.3 BPU编译配置与参数调优编译配置文件是整个过程的核心。以下是我用的配置模板关键参数都加了注释model_parameters: onnx_model: yolo11n_no_softmax.onnx output_model_file_prefix: yolo11n_x5 march: bayes-e # RDK X5的BPU架构代号 working_dir: ./model_output layer_out_dump: False log_level: INFO input_parameters: input_name: images input_type_rt: nv12 # 板端摄像头输入通常是NV12 input_type_train: rgb input_layout_train: NCHW input_shape: 1x3x640x640 norm_type: data_scale scale_value: 0.003921568627 # 1/255 calibration_parameters: cal_data_dir: ./calibration_data cal_data_type: float32 calibration_type: default compiler_parameters: compile_mode: latency # 优化延迟而非吞吐 debug: False optimize_level: O3 core_num: 1 # 单核编译多核需要模型支持几个关键点解释一下march参数必须设为bayes-e这是RDK X5的BPU架构。设错了编译出来的模型加载不了。input_type_rt设为nv12是因为板端摄像头输出通常是NV12格式这样BPU可以直接处理省去格式转换的开销。如果你的输入是RGB改成rgb即可。compile_mode选latency适合单帧推理场景如果要做多路视频流处理可以选throughput。optimize_level设为O3会启用最激进的优化包括算子融合和内存复用。实测O3比O2快约10%。5.4 板端部署与后处理实现编译完成后会生成yolo11n_x5.bin文件把它和校准数据一起拷贝到板端。板端推理代码用Python写的话核心流程如下import numpy as np import cv2 from hobot_dnn import pyeasy_dnn as dnn # 加载模型 models dnn.load(yolo11n_x5.bin) model models[0] # 预处理 def preprocess(img, input_shape): img cv2.resize(img, input_shape) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 img np.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0) return img # 推理 img cv2.imread(test.jpg) input_data preprocess(img, (640, 640)) outputs model.forward(input_data) # 后处理这里补上Softmax def softmax(x, axis-1): x_max np.max(x, axisaxis, keepdimsTrue) exp_x np.exp(x - x_max) return exp_x / np.sum(exp_x, axisaxis, keepdimsTrue) # 对分类分支的输出做Softmax # 注意YOLOv11实际用的是sigmoid这里根据你的模型输出决定 for out in outputs: cls_output out.buffer[..., 4:] # 假设前4个是bbox cls_probs softmax(cls_output, axis-1) # 后续做阈值过滤和NMS后处理里的Softmax用NumPy实现配合NEON指令集537,600个元素的Softmax耗时不到2ms完全可以接受。5.5 实测数据对比改造前后的性能对比指标改造前改造后提升幅度单帧推理耗时180ms28ms6.4倍帧率5.5 FPS35.7 FPS6.5倍BPU占用率32%76%-CPU占用率85%单核45%多核均衡-内存占用420MB380MB9.5%这个数据是在640x640输入、单核BPU模式下测的。如果开启双核编译需要模型支持帧率还能再提升30%左右。6. 踩过的坑与排查经验实录6.1 精度验证不能省去掉Softmax后模型的输出从概率变成了logits。虽然检测框的位置不变但置信度的数值范围变了。如果后处理里的阈值过滤逻辑没有相应调整会出现大量误检或漏检。我的做法是在PC端用ONNX Runtime分别跑原始模型和改造后的模型对比同一张图片的检测结果。确保检测框的坐标和类别一致只是置信度数值不同。然后在后处理里把Softmax补上再做阈值过滤。提示YOLOv11n的分类分支实际用的是sigmoid不是Softmax。如果你在ONNX图里看到的是Sigmoid而不是Softmax处理思路一样只是后处理里用sigmoid函数替代softmax函数即可。6.2 校准数据的选择影响量化精度BPU编译需要校准数据来做量化。校准数据的质量和数量直接影响量化后的精度。我一开始只用了20张图片做校准结果量化后mAP掉了5个百分点。后来增加到200张覆盖不同光照、不同场景mAP只掉了1.2个百分点。校准数据的选取原则覆盖实际部署场景的各种情况。如果部署在室内就多拍室内场景如果部署在室外要包含白天、傍晚、阴天等不同光照条件。数量上100-500张比较合适太少精度不够太多编译时间太长。6.3 输入格式转换的隐藏开销板端摄像头输出通常是NV12格式如果模型输入配置的是RGBBPU内部会做格式转换。这个转换虽然由硬件完成但仍然有开销。实测NV12输入比RGB输入快约3ms。配置方法是在编译配置里把input_type_rt设为nv12然后在板端代码里直接把NV12数据喂给模型不需要手动转RGB。6.4 常见问题速查表问题现象可能原因排查方法解决方案模型加载失败march参数错误检查编译配置改为bayes-e推理结果全为0输入格式不匹配对比预处理代码检查NV12/RGB配置帧率远低于预期存在低效算子用hb_perf分析移除或回退Softmax精度大幅下降校准数据不足检查校准集增加到200张以上BPU占用率低算子在CPU执行查看算子映射调整编译配置内存溢出输入尺寸过大检查input_shape降低到640x6406.5 一个容易被忽略的细节线程绑定RDK X5的CPU是8核A55架构默认情况下推理线程可能在任意核心上调度。如果推理线程和系统其他任务抢同一个核心会导致延迟抖动。用taskset把推理线程绑定到特定核心可以减少抖动。taskset -c 4-7 python3 inference.py这个操作在长时间运行的场景下效果明显帧率的标准差从8ms降到了2ms以内。7. 这套思路还能用在哪些地方Softmax的问题不是YOLOv11n独有的。任何包含Softmax、LayerNorm、GELU等“低计算访存比”算子的模型在BPU上部署时都可能遇到类似问题。比如分类模型ResNet、EfficientNet最后的全连接层后面通常有Softmax处理思路一样把Softmax移到后处理。Transformer模型Attention机制里的Softmax是核心组件但计算量更大需要更细致的优化策略比如用FlashAttention的思路做分块计算。分割模型DeepLab、SegFormer的输出层也有Softmax同样可以移到后处理。核心原则就一条让BPU做它擅长的事卷积、矩阵乘法把逐元素操作和归一化操作交给CPU。BPU和CPU各司其职整体效率才能最大化。我在实际项目里还遇到过一个变体问题模型里的Sigmoid算子虽然计算量小但因为数量多YOLOv11n里有几十个Sigmoid累积起来也占了15%左右的推理时间。处理思路类似能合并的合并能移到后处理的移到后处理。最后分享一个小技巧编译模型时开启layer_out_dump选项可以把每一层的输出dump出来。对比PC端和板端的逐层输出能快速定位是哪一层开始出现精度偏差或性能异常。这个功能在排查量化精度问题时特别有用。
返回列表