简介:这是一套面向YOLOv11目标检测开发者的移动端部署与轻量化实战文档,适合希望在手机、嵌入式开发板上高效运行模型的算法工程师与学习者。文档从深度学习模型轻量化背景切入,系统讲解模型剪枝、量化、知识蒸馏与轻量化网络结构设计,并细化结构化/非结构化剪枝、对称/非对称量化、训练后量化与量化感知训练等内容;同时完整覆盖Android与iOS环境搭建、模型转换、推理引擎选择、异步多线程优化等部署链路,结合智能安防、智能交通、智能家居三个真实案例给出性能优化思路与评估方法。资源包含1个PDF文件,全文38页,大小仅1.97MB,自带目录跳转与章节大纲,文内图表、公式与文字显示完整,可按需快速定位。目前已有164人学习使用,可作为YOLOv11从训练到移动端上线的实操参考与排错手册。 从一个很具体的场景说起:你在电脑上用 YOLOv11 训练了一个检测模型,精度漂亮,但打包到手机里一跑,一帧推理要 400 多毫秒,内存还动不动往上飙。这时候“深度学习模型轻量化”就不是一个加分项,而是能不能落地上线的硬门槛。这篇笔记围绕一个核心目标:把 YOLOv11 从服务器端的“高精度大模型”压成一个能稳定跑在移动端、保持可用精度的推理方案。我会按实际落地顺序讲:先做模型压缩,再选推理引擎,然后调性能,最后把那些最容易翻车的坑挨个摆出来。阅读对象是正在做移动端目标检测的算法工程师或嵌入式开发,默认你知道 YOLO 的基本用法,但没怎么碰过端侧部署。
1. 为什么 YOLOv11 在移动端“跑不动”:轻量化不是只看参数量
YOLOv11 网络结构里大量使用 C3k2 块和 SPPF 金字塔池化,算力集中在卷积层,而移动端芯片的算力天花板很低,CPU 多线程和 NPU 的算子支持也参差不齐。很多人误以为参数量降下来就轻量了,实际上内存带宽、算子碎片化、后处理 NMS 都是移动端帧率的真实瓶颈。这篇笔记要解决的问题,就是让一个 YOLOv11 模型从 PyTorch 权重文件出发,经过剪枝、量化、格式转换,最终在手机 CPU 或 GPU 上把单帧推理时间压到几十毫秒,同时保持可接受的 mAP。适合手里只有训练好的模型、正在为落地发愁的团队,也适合想系统了解端侧检测性能优化的人。
2. 模型轻量化三板斧:剪枝、量化与蒸馏在 YOLOv11 上的落地
2.1 先看清 YOLOv11 的网络结构再动手
轻量化不是盲人摸象,第一步是把模型结构和计算量摊开看。YOLOv11 的主干里用了 C3k2 作为基础模块,SPPF 做多尺度特征聚合,检测头是解耦头和 anchor-free 设计。要找到可压缩的部位,得先统计各层的参数量和 FLOPs。常见的做法是直接加载 YOLOv11 的 pretrained 权重,用 torch 自带工具打印。
import torch from ultralytics import YOLO from thop import profile # 注意这里用的是 yolo11n.pt,ultralytics 官方权重文件名 model = YOLO("yolo11n.pt").model model.eval() dummy_input = torch.randn(1, 3, 640, 640) flops, params = profile(model, inputs=(dummy_input,), verbose=False) print(f"FLOPs: {flops / 1e9:.2f} G, Params: {params / 1e6:.2f} M")逻辑说明:thop.profile会逐层统计 MACs 和参数量,verbose=False只输出汇总。这个数字能告诉你模型的计算主体在哪些层,以及剪枝和量化的预期收益。参数说明:输入张量形状里的 640 是训练分辨率,如果你后续想用 320 或 416,FLOPs 会按平方比例下降,但精度也会变。建议先记录原始 FLOPs 和参数量,后面每做一步轻量化就对照一次,判断操作是否真的“减重”。
我看到不少人在这一步直接跳过,凭感觉把 BN 层全去掉。其实 YOLOv11 的 C3k2 模块里 BN 层和卷积是绑定的,删 BN 会破坏数值分布,不是随便能动的。更稳的做法是先分析哪些层是冗余通道,再做结构化剪枝。
2.2 结构化剪枝:裁剪哪些层才不影响精度
剪枝的目标是丢掉不重要的通道。YOLOv11 的每个卷积层后面一般跟着 BN 层,BN 的gamma系数反映了该通道的缩放强度。训练时给gamma加 L1 稀疏化惩罚,可以让不重要的通道gamma趋近于零,然后把这些通道连同对应的输入输出连接一起剪掉。这是移动端部署最常用的结构化剪枝方案。
import torch import torch.nn as nn # 在损失函数中加入 BN gamma 的稀疏惩罚 def sparse_gamma_loss(model, lambda_l1): # lambda_l1 一般取 1e-4 sparse_loss = torch.tensor(0.0, device=next(model.parameters()).device) for m in model.modules(): if isinstance(m, nn.BatchNorm2d): sparse_loss += torch.abs(m.weight).sum() return lambda_l1 * sparse_loss # 训练循环里把这个 loss 加到原损失上 # loss = yolo_loss + sparse_gamma_loss(model, lambda_l1=1e-4) # loss.backward()逻辑说明:经过稀疏化训练后,BN 层的gamma向量会变得稀疏。接下来统计所有 BN 层的gamma分布,按阈值或剪枝比例挑出需要去掉的通道。剪枝后模型会变瘦,但必须重新 fine-tune,让剩余通道恢复精度。参数说明:lambda_l1太小则稀疏效果不明显,太大会把整个模型压坏,一般从1e-4开始调。剪枝比例建议从 30% 开始,观察 mAP 掉点,再逐步提高到 50% 以上。我见过有人对 YOLOv11 一次性剪掉 70%,结果精度掉了 15 个点,fine-tune 都救不回来。
剪枝后的模型通道数变了,导出的网络结构也跟着变。如果你用的是 NCNN 或 MNN,它们对动态 shape 支持比较保守,剪枝后最好 fixed shape 导出 ONNX,再转换到端侧引擎。
2.3 量化:FP32 到 INT8 的精度补偿
剪枝压的是模型大小和计算量,量化则是把权重和激活从 FP32 变成 INT8。移动端 INT8 矩阵乘可以使用硬件加速指令,速度提升通常有 2 到 3 倍。量化分 PTQ 和 QAT:PTQ 是训练后直接量化,简单但精度可能掉得厉害;QAT 是在训练时模拟量化误差,精度恢复能力强。移动端 YOLOv11 我一般先用 PTQ,精度不够再改 QAT。
下面是 ONNX 模型做动态量化的最小命令,适合快速看效果。
from onnxruntime.quantization import quantize_dynamic, QuantType # 动态量化:只量化权重,不量化激活,稳定但提速有限 quantize_dynamic( "yolo11n.onnx", "yolo11n_int8.onnx", weight_type=QuantType.QInt8 )逻辑说明:quantize_dynamic把所有 Conv 和 MatMul 的权重转成 INT8,但激活还是 FP32,所以精度损失较小。真正要跑快,必须用 TensorRT、NCNN 或 MNN 的 PTQ 校准,对校准集做前向统计激活范围。参数说明:校准集要选目标场景的典型图片,一般 200 到 1000 张,太少会让激活范围统计不准,量化后精度飘忽不定。比如你检测的是车辆,校准集里全是猫狗,那量化后夜间车辆场景大概率漏检。
QAT 的做法是在训练时插入FakeQuantize节点,让模型学习适应量化噪声。YOLOv11 用 ultralytics 训练时,可以把amp关掉,避免混合精度和量化模拟互相干扰。
2.4 蒸馏:用大模型带小模型
蒸馏更接近“炼丹”:用一个高精度的大模型当老师,指导学生模型去拟合。YOLOv11 蒸馏常见做法是让学生的特征图尽量逼近老师的特征图,再加上检测头的分类和回归损失。下面是一个最小化的特征蒸馏 loss 示例。
import torch.nn.functional as F def feature_distillation_loss(student_feats, teacher_feats): # 学生和老师的输出特征图可能尺寸不同,先做上采样或下采样对齐 loss = 0.0 for s_feat, t_feat in zip(student_feats, teacher_feats): loss += F.mse_loss(s_feat, t_feat.detach()) # detach 切断梯度 return loss逻辑说明:detach()让教师网络梯度不反向传播,训练时只更新学生模型。teacher 用原始 YOLOv11large,student 用剪枝后的模型,蒸馏能帮助学生恢复一部分精度。参数说明:student 的输入分辨率可以比 teacher 低,比如 teacher 用 640,student 用 320,这样学生模型 FLOPs 大幅下降,蒸馏后精度恢复效果反而好。常见的损失加权是total_loss = yolo_loss + 0.1 * distillation_loss,蒸馏权重太高会把学生的检测头带偏。
这里必须提一句:剪枝、量化、蒸馏不是每次都全做。如果模型已经很小,比如 YOLOv11n,再做结构化剪枝收益很低,直接量化就够了。我的习惯是 “先量化看掉点,再剪枝,最后蒸馏收尾”,每一步都留个 checkpoint,方便回滚。
3. 把 YOLOv11 塞进手机:移动端推理引擎选型与转换流程
3.1 推理引擎对比:NCNN、MNN、TFLite、TensorRT Lite
移动端部署不是只有一种引擎可选。NCNN 是腾讯开源的老牌 CPU 引擎,对 ARM 架构优化好,算子覆盖全;MNN 是阿里的,线程调度和内存池做得不错;TFLite 依赖 TensorFlow 生态,稳但算子覆盖不如前者;TensorRT Lite 更多用在安卓 GPU 上。下面这个表是我实际选型时的判断依据。
| 引擎 | CPU 推理速度 | 量化支持 | 算子兼容性 | 适合场景 |
|---|---|---|---|---|
| NCNN | 优秀 | INT8/FP16 | 高,经常第一个支持新算子 | 安卓 CPU 和 GPU 通用 |
| MNN | 优秀 | INT8/FP16 | 高,多端一致性好 | 需要多平台统一逻辑时 |
| TFLite | 良好 | INT8/FP16 | 一般,新算子滞后 | 已有 TF 生态 |
| TensorRT | 取决于 GPU | INT8/FP16 | 有条件限制 | 需要最大化利用 GPU |
选型核心看两点:第一,你的 YOLOv11 导出 ONNX 后能不能被引擎完整解析;第二,你目标设备的 CPU 核数和 GPU 品牌。如果只是追新,NCNN 通常最稳,因为社区对 YOLO 系模型适配得最早。如果你要上 NPU,那就要看供应商提供的 SDK 是否导出一套独立格式,通常来自 NPU 工具链,和通用引擎不同。
3.2 从 PyTorch 到 ONNX 再到平台格式的完整转换
转换链路是 YOLOv11 端侧部署的主路。先用 ultralytics 自带的导出命令生成 ONNX,再用 onnx-simplifier 去掉多余算子,最后转成目标引擎的格式。下面是导出 ONNX 的标准命令。
yolo export model=yolo11n.pt format=onnx opset=12 dynamic=False逻辑说明:dynamic=False指定固定输入尺寸,移动端引擎最怕动态 shape,固定 640 能省很多麻烦。opset=12兼顾兼容性和算子支持度,太高会被老引擎拒认。导出后建议用onnx-simplifier处理一次,它能常合并 Conv+BN、去除无用节点,让后续转换更顺。
python -m onnxsim yolo11n.onnx yolo11n_sim.onnx # 转 NCNN onnx2ncnn yolo11n_sim.onnx yolo11n.param yolo11n.bin # 优化 NCNN 模型,fp16 是移动端标配 ncnnoptimize yolo11n.param yolo11n.bin yolo11n_opt.param yolo11n_opt.bin 1逻辑说明:onnx2ncnn是老牌转换工具,对 YOLO 系列几乎不会报错。ncnnoptimize末尾的1表示开启 fp16 存储,能减小模型一半大小,同时利用 ARMv8.2 的 fp16 指令加速。参数说明:有些设备不支持 fp16 计算,转出来会崩溃。可以在 ncnn 的pipeline里看到算子是否带fp16后缀,如果发现精度异常,就把这个优化开关关掉,用回 fp32。另外不要只转完就扔一边,建议用ncnn自带的 bench 程序跑一下,看输出 shape 和数值是否正常。
3.3 移动端部署代码骨架:以 NCNN 为例
部署代码的核心是预处理、推理、后处理三段。YOLOv11 的输出是一个很大的特征图张量,需要解码成候选框再做 NMS。下面是一个极简的 NCNN 推理骨架。
#include "net.h" // 假设已经把输入缩放到 640x640 ncnn::Mat in = ncnn::Mat::from_pixels_resize(img.data, ncnn::Mat::PIXEL_BGR, img.cols, img.rows, 640, 640); // 归一化到 [0,1] in.substract_mean_div_by_255({0.f, 0.f, 0.f}, {1/255.f, 1/255.f, 1/255.f}); ncnn::Net net; net.load_param("yolo11n_opt.param"); net.load_model("yolo11n_opt.bin"); ncnn::Extractor ex = net.create_extractor(); ex.input("images", in); ncnn::Mat out; ex.extract("output0", out); // 注意 output0 的名字来自 ONNX 输出节点逻辑说明:from_pixels_resize把 OpenCV Mat 转成 NCNN 的 Mat 并同步缩放。substract_mean_div_by_255对应训练时的归一化方式。extract里的"output0"是导出 ONNX 时自动生成的输出名,如果名字不同,一定要用 Netron 打开 onnx 确认。参数说明:ex可以设置num_threads,比如ex.set_num_threads(4),但线程数不是越大越好,四核手机开 4 线程通常最优,开 6 线程反而会因为调度开销变慢。
推理得到的out还需要解码。YOLOv11 的检测头输出是 80 个类别的概率加上 4 个边界框偏移,再加上每个 anchor 的 objectness 预测,需要手动根据同类坐标映射回原图。解码和 NMS 最好放到 C++ 端做,用std::vector存候选框,避免每次 new 数组。
3.4 模型文件大小与内存占用对比
做完轻量化和转换后,模型文件大小是个硬指标。一般来看,YOLOv11n 原始的 FP32 权重大约 5 到 10MB,转成 NCNN 并开启 fp16 后会减半,再量化 INT8 又能减半。移动端安装包对模型大小很敏感,几十 MB 的模型会让用户犹豫。下面是一个典型的对比表,以我常用的 n 系列为例。
| 阶段 | 文件大小 | 说明 |
|---|---|---|
| PyTorch 原始权重 | 约 8-10 MB | 包含训练状态 |
| ONNX FP32 | 约 10 MB | 结构展开后略大 |
| NCNN fp16 | 约 5 MB | 存储减半 |
| NCNN INT8 | 约 3-4 MB | 量化后进一步压缩 |
内存占用主要看推理时输入输出和中间激活层的大小。输入 640x640 的 RGB 图像就是 1.2MB,中间层激活可能达到几十 MB,所以固定输入尺寸和内存池复用非常重要。建议在部署代码里提前分配好ncnn::Mat,避免每帧都重新分配内存,否则 GC 会拖垮 FPS。
4. 性能优化:从推理速度到端侧帧率的调参实战
4.1 推理耗时瓶颈定位方法
镜像问题是:量化也做了,模型也压到几 MB,为什么帧率还是上不去?因为性能瓶颈不只在模型本身,还在预处理、前处理内存拷贝、NMS 这三个环节。我一般先用perfetto或者简单的std::chrono打点,测量一帧从原始图像到输出结果的总耗时,再拆成 3 段。
auto t0 = steady_clock::now(); // ... 从 camera 拿图,做 resized/crop auto t1 = steady_clock::now(); // ... 推理 auto t2 = steady_clock::now(); // ... 解码和 NMS auto t3 = steady_clock::now(); // 打印每一段耗时 LOG(INFO) << "preprocess: " << duration_cast<microseconds>(t1 - t0).count(); LOG(INFO) << "inference: " << duration_cast<microseconds>(t2 - t1).count(); LOG(INFO) << "postprocess: " << duration_cast<microseconds>(t3 - t2).count();逻辑说明:这个打点能明确告诉你时间去哪了。我遇到过一帧 120ms,其中预处理 30ms,推理 70ms,后处理 20ms,优化时先处理推理,再处理后处理,最后才抠预处理。参数说明:如果你发现预处理占大头,优先检查图像缩放用了什么操作。OpenCV 的resize是 CPU 密集的,可以考虑双线性缩放在 GPU 上做,或者直接用硬件加速库如 Halide。
4.2 算子融合与硬件加速
移动端引擎会自动做算子融合,比如 Conv+ReLU 融合、Conv+BN 融合。但 YOLOv11 的结构比较复杂,有些融合需要手动配置。在 NCNN 里,常用ncnnoptimize做图优化,另外还可以开启use_vulkan_compute或use_fp16_packed。
// 开启 NCNN 的 Vulkan 加速 net.opt.use_vulkan_compute = true; net.opt.use_fp16_packed = true; // 在 ARMv8.2 上用 fp16 计算 // 另一种是使用 ARM 的 dotprod 指令,前提是 CPU 支持 net.opt.use_packing_layout = true;逻辑说明:use_vulkan_compute让卷积跑在 GPU 上,但也不总是更快。很多中低端手机 GPU 驱动不完善,Vulkan 算子频繁提交反而比 CPU 慢。我的经验是,先试 CPU 的 fp16 和 packing layout,得到基线帧率,再开 Vulkan 对比,不要默认 GPU 一定快。参数说明:use_packing_layout是让 NCNN 用 ARM NEON 的向量化布局,通常能提高卷积利用率,但对内存对齐有要求,如果你的图像尺寸不从 640 开始,padding 可能变大。
在 TensorRT 侧,FP16是默认开启的,配合TF32可以兼顾精度和速度。但 TensorRT 的算子需要自己搭建推理引擎,如果有动态 shape,会额外做 engine 缓存优化,耗时较长。移动端最好固定 shape,省去引擎 rebuild 的开销。
4.3 后处理与解码优化
YOLOv11 的检测头输出网格点很多,直接遍历每个 anchor 做 sigmoid 和 NMS 非常慢。常见的优化思路是:先用 confidence 阈值过滤掉低分框,再做快速 NMS,或者直接使用矩阵 NMS。
struct Box { float x1, y1, x2, y2, score; int label; }; std::vector<Box> decode_yolo11(const ncnn::Mat& raw, float conf_thres = 0.25f) { std::vector<Box> boxes; float* data = (float*)raw.data; int num_proposals = raw.h; int num_cls = 80; for (int i = 0; i < num_proposals; ++i) { float* record = data + i * num_cls; // 找 max class 和 score // 如果 score < conf_thres 直接跳过 // 解码 box 的中心点、宽高,并做 scale 放大回原图 } return boxes; }逻辑说明:这个函数把输出张量的每一行解释为一个候选框。conf_thres建议设到 0.25 以上,否则会进来大量低质量框,NMS 复杂度指数上升。后处理的 NMS 部分可以用std::sort按分数降序排序,再逐框比较 IoU。移动端数据量不大,直接用vector和朴素 NMS 即可,没必要上矩阵 NMS。参数说明:iou_thres一般 0.45,类别多时可以尝试 0.5,减少重叠框误删。
另外推荐把 confidence 阈值提到 0.3,甚至 0.4,移动端场景往往不需要那么高的召回,提升阈值能显著降低后处理耗时。如果你的场景是小目标检测,阈值不能提太高,否则小目标漏检率会爆炸。
4.4 多线程与内存复用
线程数直接影响 CPU 推理速度。YOLOv11 在 4 核手机上用 4 线程跑卷积,基本能跑满 60FPS 的 30% 左右;在 8 核上开 8 线程,有时反而因为迁移开销变慢。我常用的是“等于大核数量”的配置,而不是核心总数。
// 根据 CPU 类型设置推荐线程数 int num_threads = std::thread::hardware_concurrency(); #ifdef __aarch64__ // 常见手机 8 核,但 reserve 4 个性能核往往更有用 num_threads = 4; #endif逻辑说明:移动端 CPU 大小核架构很常见,如果所有核都跑重负载,功耗和温升会让频率骤降,推理反而更慢。建议先用大核绑核运行。参数说明:hardware_concurrency()返回的逻辑线程数并不等于性能核数量,你需要从/sys/devices/system/cpu读取cpu*的 level 来判断大核。如果直接全开,耗时可能从 30ms 飙到 45ms,这就是线程调度翻车的经典案例。
内存复用方面,建议创建ncnn::UnlockedPoolAllocator,提前把需要的大内存块分配好,避免每帧调用malloc。这部分和 C++ 内存管理强相关,但收益非常直观:节省几毫秒的时间,同时避免内存碎片。
5. 移动端部署避坑指南:YOLOv11 最常见的 5 个翻车现场
5.1 坑 1:导出 ONNX 后输出节点名变了
现象:代码里写ex.extract("output0", out),但运行时报错找不到输出名。
原因:ultralytics 导出的 ONNX 输出名不一定叫output0,可能是output,也可能是类似/model.24/Concat_output_0这种带路径的全名。我在转换时经常把名字写错,导致加载模型都成功,但一提取输出就崩。
解决:用Netron打开 ONNX 文件,看最后的输出节点名字,然后把 C++ 里的extract参数改成实际名字。更稳的方式是在导出 ONNX 时,用dynamic=False的固定输出名,并在 Python 里验证输出数量。
5.2 坑 2:NMS 在端侧耗时爆炸
现象:模型推理只要 20ms,后处理却要 60ms,一帧总耗时有 80% 花在 NMS 上。
原因:YOLOv11 在 640 分辨率下会产生几千个候选框,如果你把 confidence 阈值设得很低(比如 0.05),NMS 要处理几万对了,CPU 直接跑满。很多人训练时习惯低阈值,部署时忘了改。
解决:把conf_thres调到 0.25 以上,然后检查输出里的 num_proposals 是不是异常多。还可以用 TopK 提前截断,只保留最高分的 500 个框再做 NMS,这样能压到 5ms 以内。对于小目标场景,先用目标区域裁剪或缩放技巧,而不是暴力提高阈值。
5.3 坑 3:量化后精度掉到没法用
现象:INT8 模型在测试集上 mAP 掉了 8 个点,目标检测直接失效。
原因:PTQ 的校准集覆盖不够。校准集只挑了几张干净的白天图,而实际场景有夜间、雨天、暗光,激活值范围完全不一样,量化统计出的 scale 不代表性。另一个可能是量化的时候把归一化层也量化了,但移动端推理时还在用substract_mean_div_by_255,导致数值范围错位。
解决:先检查预处理是否和量化前一致。确认校准集要包含目标场景的困难样本,至少 500 张,覆盖光照、遮挡、大小目标尺度。如果还掉点,就切到 QAT,把量化噪声放进训练里,学生模型会慢慢适应。我一般会保存 PTQ 和 QAT 两个版本,用同一帧图片做对比,肉眼确认检测框和置信度差异。
5.4 坑 4:GPU 和 CPU 结果不一致
现象:同一张图,CPU 推理正常,Vulkan 或 GPU 推理时框会偏移,甚至输出 NaN。
原因:GPU 的算子和 CPU 的算子精度不同,尤其fp16半精度计算在卷积累加时容易溢出,导致数值漂移。另外,NCNN 或 TensorRT 的fp16模式在低端 GPU 上可能没有完整实现,某些算子回退到 fp32,但布局转换又引入了额外误差。
解决:对于检测模型,稍微的数值偏差影响不大,但如果是 NaN,就要强制停机检查。先把use_vulkan_compute和fp16关掉,确认基线精度;再单独开启fp16,对比输出。如果差异集中在某些层,用 Netron 看那层是不是Sigmoid或者Exp,在 GPU 上对输入范围很敏感。可以在算子层面强制用fp32计算这些易错层,NCNN 支持通过opt配置 keep_fp32。
5.5 坑 5:小目标检测在移动端完全失效
现象:模型在服务器上能检测到 20x20 像素的小目标,部署到手机上后直接漏检。
原因:移动端为速度把输入分辨率降到了 320 或 256,小目标在特征图中的响应区域减少,再加上量化对低值激活的影响,小目标很难幸存。另一个原因是 NMS 阈值在端侧设置得和服务器一致,但小目标框之间的 IoU 很小,逻辑上并不会被过滤,主要还是分辨率问题。
解决:如果业务硬需求是小目标,输入分辨率就不能降太低。我建议至少 416 起步,并且针对小目标做定制:在特征提取阶段,使用更细的 stride 输出,或者在后处理时对低分小目标框单独处理。量化时用包含小目标的校准图像,避免量化尺度把小目标结构的细微差异吞掉。如果还不行,考虑使用扫描策略,把图像切成多个 640 子块预测再合并,但这个方案会增加总耗时,要权衡。
6. 最后把“性能优化”变成习惯:从基准测试到持续回归
部署不是一次性工作,代码改一行、引擎升个版本,帧率都会变。我习惯在部署目录里放一个benchmark.sh,每次改动后跑一遍同分辨率、同线程数的压测,记录 FPS 和内存,再决定是否合并改动。验证方法很简单:固定 100 帧图片,循环推理,统计平均耗时和 P50/P95。
#!/bin/bash # 压测脚本,假设你的程序支持 --loop 和 --threads ./yolo11_bench --image test.jpg --loop 100 --threads 4 --precision fp16这里有个关键点:只看平均帧率不行,要看 P95 帧率,因为移动端受温控影响,跑久了会降频。我经常发现前 20 帧很快,到第 80 帧速度掉了 30%,这就是热降频,优化时要把长时间稳定负载考虑进去。另外,线程数和精度要作为参数暴露出来,方便回归测试。
个人习惯是“先固定分辨率,再调线程,最后碰精度”。每改一个变量,都留一张数据表:分辨率、线程数、耗时、内存、mAP。当同事说“又慢了”,这张表能最快定位是哪个改动引入的。我也常提醒自己:不要在性能优化上过度激进,端侧体验不只是 FPS,还有发热和耗电。希望这篇笔记能帮你在 YOLOv11 移动端部署路上少踩几个坑,把有限的时间花在更有价值的迭代上。
本文还有配套的精品资源,点击获取