简介:《深度学习模型轻量化-YOLOv11移动端部署与性能优化实战》是一份面向目标检测开发者的技术文档,围绕YOLOv11模型轻量化与移动端部署的核心痛点,系统讲解从剪枝、量化、知识蒸馏到Android/iOS端集成与性能调优的完整路径。资源为单份PDF文档,共38页,大小仅1.97MB,支持目录章节跳转与左侧大纲定位,便于按需查阅。已有164人学习使用。文档内容涵盖模型轻量化概述、YOLOv11创新点解析、轻量化网络结构设计、移动端环境搭建与模型转换,并结合智能安防、智能交通、智能家居等实战案例,给出具体的性能优化策略与效果评估方法,适合希望在手机或嵌入式设备上高效部署目标检测模型的算法工程师与研究人员参考。
1. 移动端不是“训练完直接装上”:YOLOv11 轻量化到底在解决什么
把 YOLOv11 这类目标检测模型搬到手机或嵌入式设备上,训练往往不是最大障碍,真正卡住的是模型轻量化:参数一多,手机跑不动;浮点精度一高,端侧算力扛不住;帧率一低,功耗和发热双双超标。做过几轮移动端部署后,我的体会是,模型侧省下的每一 GFLOPs,最后都会变成端侧帧率表里的具体数字。这条部署路径的核心,是先把 YOLOv11 选型、剪枝和蒸馏做扎实,再处理 ONNX 导出、INT8 量化和推理引擎适配,最后用真机压测数据回改参数。它适合刚入门深度学习的读者搭一条可复现的流水线,也适合部署过 YOLOv8 的工程师对照着把 YOLOv11 性能挖到位。
2. 模型侧轻量化选型:从 YOLOv11 变体参数对比到剪枝与蒸馏
2.1 YOLOv11n/s/m 怎么选:参数量、GFLOPs 与移动端算力匹配
很多朋友一上来就想着怎么剪枝、怎么量化,结果选个 YOLOv11m 当基线,到手机上一测只有 3 帧,后面怎么调都救不回来。我的习惯是先把模型规格表摆出来,算清楚手头设备的算力上限,再决定要不要动刀。
YOLOv11 这一族模型按尺寸分为 n、s、m、l、x 几档,移动端真正值得考虑的只有 n 和 s。以常见的 640x640 输入为例,n 档参数量大约 2.6M、计算量在 6.5 GFLOPs 上下;s 档参数量约 9.4M、计算量约 21.5 GFLOPs;m 档参数和计算量成倍往上走,已经接近 68 GFLOPs。普通手机 CPU 跑 n 档勉强能到 20~30 帧,s 档在高端芯片上才有机会,m 档基本不用想。
| 型号 | 参数量量级 | FLOPs 量级 | 移动端 CPU 适配度 |
|---|---|---|---|
| YOLOv11n | ~2.6M | ~6.5G | 中低端可实时 |
| YOLOv11s | ~9.4M | ~21.5G | 高端芯片可实时 |
| YOLOv11m | ~20M | ~68G | 通常不可用 |
选型之前,我会先在本机把候选模型的参数量和 FLOPs 算一遍,而不是只看官方给的表格。下面的脚本用 torchinfo 输出 YOLOv11n 和 s 的参数量与理论计算量,作为选型依据:
from ultralytics import YOLO from torchinfo import summary for name in ["yolov11n.pt", "yolov11s.pt"]: model = YOLO(name) m = model.model summary( m, input_size=(1, 3, 640, 640), device="cpu", verbose=0, )这段脚本的逻辑很简单:加载 Ultralytics 模型后,把网络结构传入summary,它会递归遍历各层统计参数和乘加次数。torchinfo 在遇到某些自定义层时可能报 shape 推断错误,我会把verbose调到 1 看具体是哪个模块卡住,或者直接用手动统计方式,遍历model.parameters()累加参数量、再按已知层结构估算 FLOPs。这里的关键是别只看参数量,FLOPs 对移动端实时性影响更大,因为端侧算力瓶颈往往在卷积乘法次数上。
选型定下来之后,如果 n 档跑不到目标帧率,才轮到剪枝和蒸馏。这个顺序不要反,基线模型选大了,后面剪枝要还的债非常多。
2.2 通道剪枝的敏感度分析方法与微调安排
通道剪枝是移动端轻量化里收益最直接的手段,原理是去掉不重要的卷积通道,让网络结构变窄。YOLOv11 的卷积层大量使用 BN,所以很多开源方案走的是“基于 BN 缩放因子稀疏化”的路子:训练时给 BN 的 gamma 加 L1 稀疏惩罚,让部分通道的缩放因子逼近 0,剪枝时才敢把这些通道删掉。
我的做法是先做敏感度分析,再决定剪哪些层、剪多少。所谓敏感度,就是逐层按不同比例剪枝后,观察模型在验证集上的 mAP 掉点幅度。有的层剪掉 30% 只掉 0.5 个点,有的层剪掉 10% 就崩了,这类层必须少剪甚至不剪。
稀疏化训练阶段,我在 loss 上额外加一个 BN gamma 的正则项:
import torch def bn_sparsity_loss(model, weight=1e-4): reg_loss = 0.0 for m in model.modules(): if isinstance(m, torch.nn.BatchNorm2d): reg_loss = reg_loss + m.weight.abs().sum() return reg_loss * weight # 训练循环里叠加到总 loss total_loss = cls_loss + box_loss + dfl_loss + bn_sparsity_loss(model, 1e-4)这段代码的思路是遍历模型所有 BN 层,把 gamma 的 L1 范数累加后乘一个稀疏系数,作为额外损失加到原有检测损失上。系数1e-4是我常用的起点,值太大会让精度明显下降,值太小稀疏化效果又不够。训练 30~50 个 epoch 后,统计一下 BN gamma 的分布,如果大部分通道的 gamma 集中在 0 附近,就可以进入剪枝阶段。
剪枝本身我建议用结构化剪枝框架自动做,但切出来的模型一般会有 1~2 个百分点的 mAP 掉点。剪完必须做一轮微调,通常用原训练集的子集跑 10~20 个 epoch,学习率放低到正常训练的十分之一左右。这里有个容易忽略的坑:剪枝后的 BN 统计量是失效的,微调前需要先跑一遍前向把 BN running_mean 和 running_var 重新估计一下,否则第一轮微调 loss 会跳得很反常。
2.3 知识蒸馏落地:大模型当老师,小模型接棒
剪枝剪到一定程度,精度损失就不是微调能完全追回来的。这时候知识蒸馏是让 YOLOv11 小模型“接棒”大模型能力的主要手段,核心思路是让大模型当老师,把小模型的输出逼近老师的预测分布。
移动端部署里我更常用的是 logit 蒸馏,也就是让学生的分类和回归输出向老师看齐,配合原本的 ground truth 一起训练。分类分支用 KL 散度,回归分支可以用 smooth L1 或者直接蒸馏 DFL 输出。下面是一个比较通用的蒸馏损失模板:
import torch.nn.functional as F def distill_loss(student_logits, teacher_logits, temperature=4.0): """teacher_logits 由大模型冻结前向得到,不反传梯度""" s = F.log_softmax(student_logits / temperature, dim=-1) t = F.softmax(teacher_logits.detach() / temperature, dim=-1) return F.kl_div(s, t, reduction="batchmean") * (temperature ** 2) # 最终蒸馏训练 loss = 原检测 loss + alpha * distill_losstemperature是温度参数,调高会让概率分布更平滑,突出类别之间的相似关系;alpha控制蒸馏损失的权重,我一般从 0.5 起步,看验证集 mAP 再微调。这里的关键是老师模型必须冻结参数,而且最好和学生在同一输入尺寸下前向,避免特征图对齐的麻烦。
实际项目里蒸馏对文本类和小目标类别的迁移效果比较明显,但对原本就学得很好的中大型目标,提升有限。性价比最高的做法是只在训练后期加蒸馏,比如前 80% epoch 正常训练,后 20% epoch 开启蒸馏,这样既能保住模型早期的收敛速度,又能让小模型在收尾阶段学到老师的分布细节。如果蒸馏后精度还是不够,那就回头检查剪枝比例是否激进,而不是继续加大蒸馏权重。
3. 从 PyTorch 到端侧模型:ONNX 导出、INT8 量化与格式转换
3.1 ONNX 导出固定尺寸模型:detect 头去留与输出形状核对
模型训练和轻量化做完,接下来就是把 PyTorch 权重导出成端侧引擎能吃的中间格式。ONNX 是必经的一站,但导出时有两个决定性问题:要不要带 detect 头、要不要开动态尺寸。
我的建议是固定尺寸导出,动态尺寸留到调试阶段再用。原因很简单,移动端推理引擎为了极致性能,通常会针对固定输入尺寸做显存和计算图优化,动态 shape 会让很多端的算子融合失效。用一个固定尺寸 640x640 导出,最稳:
from ultralytics import YOLO model = YOLO("yolov11n.pt") model.export( format="onnx", imgsz=640, opset=12, dynamic=False, simplify=True, )opset 我一般锁在 12 上下,太高会让 NCNN 等老牌引擎解析不了;simplify=True会走一遍 ONNX 图优化,去掉冗余节点。导出后务必做一次输出形状核对,不要直接丢给引擎:
import onnxruntime as ort import numpy as np sess = ort.InferenceSession( "yolov11n.onnx", providers=["CPUExecutionProvider"], ) x = np.random.randn(1, 3, 640, 640).astype(np.float32) outs = sess.run(None, {sess.get_inputs()[0].name: x}) for i, o in enumerate(outs): print(f"output {i}: shape={o.shape}")输出形状这步经常有翻车,因为不同版本的 YOLOv11 导出后 detect 头可能输出一个拼接张量,也可能是多个尺度的特征图。端侧引擎对标准形状的张量支持最好,如果你在后续转换时发现算子兼容性报错,最省事的方案是导出时只保留 backbone 加 neck 的输出,detect 解码放到端侧用普通循环实现。
关于是否带 NMS:我非常不建议在 ONNX 里带 NMS 节点。NMS 在端侧引擎里属于最不稳定的算子,不同引擎对它的支持参差不齐,而且 NMS 的阈值要调优,放端侧改起来更灵活。所以导出模型时,我通常把后处理完全摘出去。
3.2 PTQ 量化的校准集构造和精度回退排查顺序
移动端推理引擎普遍偏好 INT8 量化,因为 INT8 乘法在 ARM 平台有专门指令加速,模型体积也能缩小到原来的四分之一左右。训练后量化(PTQ)是成本最低的路径,不需要重新训练,只需要一堆校准图片和一次转换流程。
校准集决定 PTQ 的上限,这是我最想强调的一点。校准集不是随便找几百张图就完事,它的分布必须覆盖目标检测实际会遇到的场景:目标大小、光照、类别分布都要和业务一致。比如模型在白天街景训练,你就是拿 300 张夜间图片去校准,量化跑完推理结果大概率漂移。
TFLite 路线的 PTQ 量化代码长这样:
import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model("yolov11n_saved_model") converter.optimizations = [tf.lite.Optimize.DEFAULT] def representative_dataset(): # 从验证集里随机采样 200 张,覆盖各目标类别 for img in sampled_images: data = preprocess_to_uint8(img) # 归一化 + 转成 (1,640,640,3) 的输入 yield [data] converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type = tf.uint8 converter.inference_output_type = tf.uint8 model_tflite = converter.convert()这里的representative_dataset是量化校准的入口,每次 yield 一个 batch 的输入数据,转换器会用它统计激活值的动态范围。校准图片数量 100 到 500 张之间比较稳妥,太少统计不稳,太多增加转换耗时。inference_input_type = tf.uint8会让端侧必须以 uint8 输入,如果后续代码觉得还得转 float,就不开这两行,交给引擎内部做量化。
NCNN 路线的量化则需要先借助工具生成校准表,再执行 INT8 转换。常见做法是准备一个图片列表文件,然后跑 ncnn2table 和 ncnn2int8:
# image_list.txt 每行一张需要量化的图片路径 ./ncnn2table yolov11n.param yolov11n.bin yolov11n.table image_list.txt 8 256 mean=128 norm=0.0078125 # 读取校准表,产出 int8 权重 ./ncnn2int8 yolov11n.param yolov11n.bin yolov11n_int8.param yolov11n_int8.bin yolov11n.tablencnn2table的第三个参数是线程数,第四个是每张图片上的迭代次数;mean和norm必须和训练前处理一致,否则第一层输入就偏了。转换完成后,至少准备 50 张带标签的图片,分别用 FP32 模型和 INT8 模型跑一遍推理,对比检测结果。如果精度退回超过 2 个点,优先怀疑校准集覆盖度,而不是量化本身。
3.3 ONNX、TFLite、NCNN、MNN 的转换参数与兼容性对照
ONNX 是桥,但最终跑在手机上的引擎在 Android 平台基本是 NCNN、TFLite、MNN 三选一。它们的转换路径和算子兼容性差别不小,我把常用选择整理成下面的对照思路:
| 引擎 | 输入格式 | INT8 量化方式 | 算子兼容性 | 典型适用场景 |
|---|---|---|---|---|
| NCNN | .param / .bin | ncnn2table 生成校准表 | 对老算子支持好,新算子需适配 | Android CPU 部署,社区案例多 |
| TFLite | .tflite | converter 内置 PTQ/QAT | 依赖 TF 生态,部分 ONNX 算子需转换 | 跨 Android/iOS,与 TF 生态集成 |
| MNN | .mnn | 内置量化工具,支持混精度 | 算子补齐快,但踩坑资料少 | 阿里系应用,有专用 ARM 优化 |
转换时的兼容性问题,本质上是对算子支持范围的差异。YOLOv11 的 DFL 层和一些特殊激活函数,在 onnx2ncnn 阶段经常报 Unsupported。到了这一步,我一般直接绕过难缠的检测头,只转换 backbone 和 neck,输出特征图交给端侧代码手动解码。这样牺牲一点转换便捷度,换来了极高的稳定性。
如果你的业务同时要支持 Android 和 iOS,TFLite 是阻力最小的路径;如果主要面向 Android 且对低端机性能更敏感,NCNN 的 ARM 优化更值得投入;MNN 适合项目已经和阿里云生态绑定的情况,否则资料少,出了怪问题排查成本高。决策不要太纠结,选一个引擎深入调,比三引擎都浅尝辄止有效率得多。
4. 推理引擎选型与运行时调优:把 YOLOv11 端侧帧率拉满
4.1 NCNN、TFLite、MNN 在移动端 YOLO 部署上的取舍
引擎选型决定后面几个月踩坑的密度。NCNN 在 Android 上的优化程度深,社区里也已经有人把 YOLO 系列跑通,遇到算子问题通常搜一下就有解决方案;TFLite 胜在生态完整,跨端一致性好,量化工具链成熟;MNN 的 ARM 汇编优化同样扎实,但可参考的移动端 YOLO 部署案例相对少。
我建议用下面的条件快速筛选:如果团队里 Android 工程师多、对低端机帧率有硬指标,首选 NCNN;如果是要快速做出跨端 Demo 验证业务,TFLite 更省时间;如果后续打算深度定制底层算子,再考虑 MNN。很多项目最后翻车不是引擎性能不够,而是中途换引擎导致所有适配工作推倒重来。
4.2 线程绑定、FP16 开关和内存池:NCNN 初始化参数怎么设
选好引擎后,参数配置会直接影响帧率稳定性。以 NCNN 为例,初始化阶段有两类参数特别关键:线程策略和存储精度。下面的代码是 NCNN 加载 YOLOv11 模型的典型初始化方式:
#include "net.h" static ncnn::Net g_net; int load_yolov11_model() { ncnn::Option opt; opt.num_threads = 4; opt.use_packing_layout = true; opt.use_fp16_storage = false; opt.use_fp16_arithmetic = false; opt.use_bf16_storage = true; g_net.opt = opt; if (g_net.load_param("yolov11n_int8.param") != 0) { return -1; } if (g_net.load_model("yolov11n_int8.bin") != 0) { return -1; } return 0; }num_threads不要无脑设成手机核心数,很多手机是大核加小核的异构架构,线程开太多反而在调度上浪费时间。我通常从 4 线程起步,拿同一台设备分别测 4 线程和 8 线程的真机帧率,再决定最终值。use_packing_layout负责 SIMD 打包优化,在 ARM 平台收益明显,但在某些模拟器或 x86 设备上会降速,建议真机上验证。
use_fp16_storage对 INT8 量化模型没有意义,一般用在 FP16 半精度模型上;如果端侧不支持 FP16 计算,硬开会让部分算子回退到 FP32,性能反而更差。对已经量化成 INT8 的模型,我通常保持 FP16 相关选项关闭,模型体积和速度都稳定。这里还有个血泪经验:初始化后要确认load_param和load_model的返回值,NCNN 对模型文件版本很敏感,一旦参数文件不匹配会加载失败,但有些版本只打日志不报错,很容易埋雷。
推理阶段,我会复用同一个ncnn::Net实例和Extractor,避免每帧都重新加载模型。预热也很关键,初始化后立刻跑 3 到 5 次空推理,把底层缓存和内存池先填满,否则首帧延迟会非常难看。
4.3 端侧后处理与 NMS 的常规实现
端侧推理输出通常是一堆原始张量,如果不把后处理写稳,帧率再高也没用。YOLOv11 的 detect 头输出包含目标的 box 坐标、置信度和类别概率,需要先做置信度过滤,再做 NMS。常见做法是在 CPU 端用循环解码,避免依赖引擎的 NMS 算子。
struct Object { float x1, y1, x2, y2, score; int label; }; std::vector<Object> decode_output(const float* data, int num_anchors, int num_cls, float conf_thresh) { std::vector<Object> objs; for (int i = 0; i < num_anchors; ++i) { const float* ptr = data + i * (5 + num_cls); float obj_score = ptr[4]; if (obj_score < conf_thresh) continue; int label = 0; float max_cls = 0.0f; for (int c = 0; c < num_cls; ++c) { if (ptr[5 + c] > max_cls) { max_cls = ptr[5 + c]; label = c; } } float score = obj_score * max_cls; if (score < conf_thresh) continue; // 坐标解析,按不同输出格式读 x1,y1,x2,y2 或 cx,cy,w,h objs.push_back({ptr[0], ptr[1], ptr[2], ptr[3], score, label}); } return objs; }conf_thresh通常在 0.25 到 0.45 之间调,调太低会输出大量低质量框,NMS 计算时间暴涨;调太高又容易漏检测,尤其是小目标。NMS 我会用线性遍历的 IOU 抑制,几百个候选框的量级在手机上耗时可以压到 1ms 以内,不需要引入复杂优化。如果发现 IOU 阈值对互相靠近的目标误伤严重,优先小幅调低 NMS 阈值,而不是去动输入分辨率。
5. YOLOv11 移动端部署避坑指南:五类高发问题的现象与处方
5.1 量化后精度崩成玄学:先查校准集而不是模型
现象:FP32 模型在服务器上 mAP 正常,INT8 量化后掉点 5 个点以上,甚至检测结果大面积乱框。一开始我还以为是 NCNN 的 INT8 实现有问题,反复换版本、换转换命令,结果问题一直复现。
原因:校准集分布和真实场景不匹配。我之前用 COCO 的验证集随机抽了 100 张图做量化校准,但业务实际场景全是低照度监控画面,目标尺寸也偏小。校准集统计出的激活动态范围完全不能代表真实输入,量化时数值截断误差被放大,精度自然保不住。
解决:校准集必须从训练集或贴近业务的验证集里重新采样,覆盖各种光照、目标大小和类别组合。我从训练集里按类别均衡抽样 300 张,重新生成校准表后,INT8 模型精度立刻回到可接受范围。这之后再遇到精度问题,我会按这个顺序排查:校准集分布、校准图片数量、敏感层是否该保留 FP32,最后才怀疑引擎的量化实现。
5.2 ONNX 导出报 Unsupported 算子:拆开检测头手动补解码
现象:ONNX 导出成功,但 onnx2ncnn 转换时报错,提示某个算子不支持,换 opset、加 simplify 都没用。时间花了很多,卡在转换这一关动不了。
原因:YOLOv11 后处理里的某些结构性算子,比如 DFL 层的 Reshape 组合、Gather 或者特定 Gemm 模式,在 ONNX 图里表述方式和 NCNN 支持的算子列表对不上。这类问题不是模型本身有错,而是不同框架的算子表达偏好不同。
解决:我改用“半导出”方案——导出模型结构时不带完整 detect 头,只保留 backbone 加 neck 的特征图输出,然后用端侧代码手动实现解码和 NMS。具体做法是把模型对象里的 detect 头剥离,或者导出后直接用 ONNX 工具把末尾节点剪掉。这样转换稳定,后处理逻辑也完全在自己掌控之下,后续调整 conf_thresh 或 NMS 阈值都不需要重新导出模型。
5.3 首帧延迟 500ms:预热推理和启动期分配被忽略
现象:帧率统计挺高,但用户点开摄像头到第一帧检测结果出现的延迟接近半秒,体感就是“卡一下才开始”。很多人因为这个把模型换小,其实问题不在模型。
原因:手机端第一次运行推理时,模型加载、底层内核初始化、tensor 内存分配都挤在首帧,这部分耗时完全没被计入帧率统计。如果只测稳态帧率,测出来的数据是虚的。
解决:初始化阶段做一次完整的前处理加推理的预热循环,跑 3 到 5 帧空数据,把缓存预热起来。同时把推理需要的输入输出缓冲区提前分配好,避免首帧临时 malloc。真机测试时也要把“冷启动首帧”单独记录,和稳态帧率分开看,这样才知道用户真正感受到的延迟是哪里来的。
5.4 内存随推理次数持续上涨:Tensor 容器没有复用
现象:App 刚启动时内存占用 200MB,连续运行一段时间后涨到 500MB 甚至被系统杀掉。查模型大小才 10MB,怎么想都不对劲。
原因:端侧推理代码在每帧里创建了很多临时容器,比如std::vector<Object>后处理结果、多个ncnn::Mat中间张量,这些对象频繁分配和释放会产生大量内存碎片,峰值越来越高。如果模型加载也放在每帧路径里,内存上涨会更明显。
解决:把推理中会复用的容器提升成成员变量或全局静态实例,后处理结果用预分配数组,避免每帧都触发堆分配。前处理里的颜色转换和缩放,也尽量复用同一块ncnn::Mat缓冲区。调整后在压测脚本里连续跑 1000 帧,内存曲线应该是一条水平线,这是判断内存问题是否解决的标准。
5.5 小目标漏检:输入分辨率与 NMS 阈值没配合好
现象:大目标检测正常,小目标经常漏掉,即使把置信度阈值降到 0.2 也没明显改善。项目里小目标占比一高,整机表现就让客户不满意。
原因:端侧为了性能把输入分辨率从 640 降到 320,小目标在低分辨率下只有几个像素,特征基本丢失,这是分辨率造成的硬伤;另外 NMS 的 IOU 阈值如果设得偏严,距离近的小目标会相互抑制,导致原本检测到的框被去掉。
解决:先确认当前输入分辨率下小目标的可检测性,用一批小目标样本单独测试。如果确实分辨率不够,优先把输入提到 416 或 480,同时把 NMS 的 IOU 阈值适当放松,比如从 0.5 调到 0.6,保留重叠度较高的小框。还可以调整图像预处理里的缩放策略,避免小目标在等比缩放时被压得太狠。这类问题往往是分辨率、置信度和 NMS 三者组合调优的结果,单动一个参数很难见效。
6. 可复现的端侧压测脚本:用数据决定下一步优化方向
6.1 控制变量的真机压测流程
真机压测最忌讳的就是一边充电一边测、后台应用乱跑、屏幕亮度忽高忽低,这些变量对帧率和温度的影响比模型优化还大。我一般会准备一台固定设备、固定系统和固定亮度,亮屏但不操作,后台进程清空,然后跑一组标准化压测。
压测脚本的核心是记录三个指标:稳态帧率、首帧延迟、内存峰值。每轮测试跑 100 帧,丢掉前 5 帧预热数据,取后续帧的均值作为帧率;首帧单独记录,不参与均值计算。我习惯把冷启动和热启动分开测,因为用户实际体验更接近冷启动。测试完成后,把同一模型在不同线程配置、不同量化方案下的数据摆在一起对比:
| 指标 | 目标值 | 实测值 |
|---|---|---|
| 稳态帧率 | ≥ 25 FPS | 待测 |
| 首帧延迟 | ≤ 200 ms | 待测 |
| p50 单帧延迟 | ≤ 40 ms | 待测 |
| 内存峰值 | ≤ 300 MB | 待测 |
这轮数据出来后,优化方向就很清楚了。如果 p50 达标但 p90 很高,说明帧率波动大,优先查线程调度、内存复用和 FP16 开关;如果首帧超标,回到预热和启动期分配的问题;如果内存持续上涨,大概率是容器复用没做干净。
6.2 用延迟分位和温度走势判断是否达到上线标准
除了 FPS,我强烈建议把 p90 延迟和温度一起纳入上线标准。FPS 是平均值,掩盖了波动;p90 则能反映用户在复杂场景下的最差体验。一套带温度监控的压测流程,能让“体感流畅”变成一个可验收的数字。跑 30 分钟连续推理,记录温度从 30 度升到 45 度之间的帧率变化,如果掉帧超过 20%,说明散热功耗有问题,需要降低线程数或考虑关掉 FP16 算术加速。
我吃过一次亏,就是在测试机上跑出 35 FPS 后急着上线,结果用户低端机上只有 15 FPS 而且发热严重。后来我把压测设备换成项目实际的最低配机器,并把温度写进测试报告,才真正把性能验收落地。顺便提醒一句,端侧输出的后处理结果最好落盘保存,把 YOLOv11 在端侧的推理输出存成 JSON 或图片,拿回 PC 端和原模型对比,能快速发现量化导致的偶发错误。希望这些配置习惯能帮你少走一段弯路,希望帮到你。
本文还有配套的精品资源,点击获取