资讯动态

YOLOv11在RDK S100P上的INT8量化部署实战

发布时间:2026/10/21 4:44:44 来源:尧图企业网站定制
以前在GPU上做目标检测模型训完随便一跑爽是真的爽。可一旦要把同样的YOLOv11模型搬到RDK S100P这种地瓜派板子上问题马上冒出来浮点模型直接部署帧率难看得没法看内存还顶着上限。我这次把YOLOv11从PyTorch一路量化到INT8再部署到RDK S100P上中间踩了不少坑也把整套流程彻底理清楚了。这篇东西适合谁看手里有RDK S100P想把YOLOv11这类检测模型真正跑起来的开发者以及对量化部署一知半解、以为跑个转换命令就行的人。我会把模型导出、ONNX校验、INT8量化校准、板端推理和后处理全流程都写明白尤其是那些不写在官方文档里的坑尽量一次说透。1. 为什么非要在RDK S100P上做量化部署浮点模型跑不动是常态1.1 板卡资源决定了你不能直接扔一个FP32模型上去先把结论放前面在RDK S100P上直接跑FP32的YOLOv11不是慢一点的问题而是资源完全不够用。YOLOv11n这种轻量版本输入640x640一次前向推理的乘加运算量也在13GFLOPs上下。GPU能轻松吃掉这个量但边缘板子的CPU通常只有几个ARM核靠它硬算单帧推理时间直接奔着几百毫秒去这还没算预处理和后处理。RDK S100P的算力主要来自BPU这样的神经网络加速单元它的设计目标就是定点计算对FP32浮点运算的支持非常有限。你让BPU去跑一个浮点模型很多算子都得回退到CPU模拟速度优势全没了。所以在这类板子上模型量化不是可选项是能否上线的前置条件。1.2 INT8量化到底是什么权重和激活怎么压缩量化这个词听起来玄本质就是把模型的权重和中间激活从FP32的32位浮点数映射到INT8的8位整数。以对称量化为例公式很简单scale max(|r|) / 127 q round(r / scale)FP32的时候权重是类似0.0372这种连续小数INT8则把它变成一个接近的整数。这个scale就是缩放系数推理时NPU拿整数做矩阵乘法最后再乘回scale恢复数值范围。因为卷积和矩阵乘对scale的运算可以融合到相邻层所以实际推理时你甚至感觉不到多余的运算开销。问题是映射过程中必然损失精度尤其当权重分布有极端离群值时一个大的离群值会把整个量化范围撑大让大多数权重分到的量化步长变粗。这就是为什么同样一个模型有的人量化后mAP掉0.2个点有的人能掉4个点。怎么选量化范围、用什么校准策略后面我会细讲。1.3 RDK S100P这套开发链路对量化的支持边界RDK S100P的部署链路大致是PyTorch训练好的模型先导出成ONNX然后用板卡配套的模型转换工具做算子和量化处理生成板端能加载的量化模型文件。这个工具链对YOLO系列的检测头是有专门优化的但前提是ONNX来源要干净。有个很重要的边界要提前知道工具链对算子的支持不是100%覆盖的。一些复杂的算子比如动态形状相关的NonMaxSuppression、某些特殊上采样方式很可能不被NPU支持。解决办法是让这些算子在ONNX里保留为可回退的CPU节点或者干脆把后处理放到板子的CPU侧来做。我在第三部分会详细写转换失败的排查思路。2. 第一步不是转换是先把YOLOv11从PyTorch里干净地导出2.1 导出前先确定权重、输入尺寸和类别数很多人一上来就执行导出命令根本没想过导出结果要满足工具链的胃口。我建议先想清楚三个问题用哪个权重、输入尺寸多少、类别数对不对。权重建议用训练过程中验证集最优的best.pt而不是最后一轮的last.pt。一些项目习惯用带EMA的权重导出前记得把EMA权重合入。用Ultralytics训练框架的话best.pt本身就带着EMA状态直接导出即可。输入尺寸方面如果你要部署的分辨率是640x640就别用1280训练的模型硬导。RDK S100P这类板子的NPU对输入尺寸有对齐要求通常要求宽高是16或32的倍数。640x640是安全的但如果你用608x608这类尺寸建议先检查工具链配置里有没有对齐报错。类别数量直接决定输出张量的大小。YOLOv11的检测头输出是(batch, 4 num_classes, num_anchors)的结构。80类目标时640x640输入对应的输出就是(1, 84, 8400)。8400是三个尺度特征图累加出来的anchor点总和。你如果训练的是自己的数据集只有5类那输出就是(1, 9, 8400)。这个维度后面写后处理代码时要严格对应错一个数字整个解析就崩了。2.2 ONNX导出的opset、simplify、动态轴设置导出命令本身不复杂但有几个参数值得抠一下。我的习惯是保留一份原始导出的ONNX再导出一份simplify之后的ONNX两份都要用python -c from ultralytics import YOLO model YOLO(yolo11n.pt) model.export(formatonnx, opset11, imgsz640, dynamicFalse, simplifyFalse) 然后另跑一次simplifyTrue。为什么搞两份因为工具链转换报错的时候你很难判断是模型本身的问题还是ONNX图冗余造成的保留原始版本方便回溯。opset推荐11或12。有些工具链对新版opset的支持会滞后opset太高很可能遇到节点类型不匹配。dynamicFalse是必须的量化部署阶段不需要动态维度动态轴会让校准过程复杂化而且板端推理时动态shape往往不支持。simplifyTrue依赖onnxsim运行时会做常量折叠、冗余算子删除。但它也会重写部分节点名称后面做层间精度对比时你会看到大量节点名变了的现象。所以我强烈建议先确认能正常转换再用简化版。2.3 用onnxruntime做导出后的数值一致性校验导出ONNX这一步很多人跳过了校验结果模型转换半天板端推理全错最后回头查才发现导出时数值就不对。其实校验很简单用onnxruntime加载导出的ONNX喂同一个输入和PyTorch模型的输出做对比即可。import numpy as np import onnxruntime as ort import torch # pytorch推理 model.eval() dummy_input torch.randn(1, 3, 640, 640) with torch.no_grad(): torch_output model(dummy_input).cpu().numpy() # onnxruntime推理 ort_session ort.InferenceSession(yolo11n.onnx) ort_output ort_session.run(None, {images: dummy_input.numpy()})[0] print(max abs diff:, np.abs(torch_output - ort_output).max())这里有个最常见的坑PyTorch模型默认输入是RGB、CHW、0-1归一化后的数据而YOLOv11在推理框架里实际用的预处理是BGR、HWC、除以255。你如果拿一张经过BGR通道处理的图直接喂给ONNX校验对比结果差异会非常大。正确的做法是让PyTorch和ONNX都吃同一个张量即模型内部约定的输入格式。通常max abs diff在1e-4量级以上就得警惕如果你看到差异达到1e-1基本可以确定前处理或通道顺序出了问题。3. 模型转换与INT8量化校准这一步决定板子跑得好不好3.1 校准数据集的准备数量、分布和预处理量化校准的过程简单说就是拿一批真实数据喂给待量化的模型统计每一层激活值的数值分布然后据此确定量化范围。所以校准数据的质量直接决定量化参数的质量。数量上300到500张图是多数项目的合理区间。太少了统计出来分布不稳定太多了校准时间呈线性增长收益却迅速饱和没必要。分布上校准集必须覆盖你实际场景的变化。做行人检测就多放不同光照、不同遮挡程度的图做工业缺陷检测就把几种缺陷类型都放足。注意不要全用简单样本简单样本的激活分布通常很集中量化后遇到复杂场景就原形毕露。我是从训练集里按类别和场景分层抽样保证每类目标出现次数大体均衡。预处理上校准图的预处理必须和部署时的预处理完全一致。这个完全一致包括缩放方式、填充颜色、通道顺序、归一化参数。很多时候精度掉点不是量化本身的问题而是校准阶段的预处理和部署阶段的预处理不一致。后面5.2节我会用实际项目数据说明这一点。3.2 量化配置里最关键的两个参数量化方式和校准方法RDK S100P的工具链一般提供一个yaml配置来控制转换过程核心参数大概长这样model_type: yolov11 input_type: rgb input_shape: [1, 3, 640, 640] calibration_type: percent calibration_percent: 99.99 quant_type: per_channel preprocess: type: norm mean: [0, 0, 0] std: [255, 255, 255] keep_aspect_ratio: truequant_type这个参数很关键。per_tensor是整层共用一个scale简单但对小模型不友好per_channel按输出通道分别算scale精度保留能力更强代价是推理时计算稍复杂。我建议能开per_channel就开YOLOv11这种小模型对per-tensor的精度损失尤其敏感。calibration_type常见有max、percent、entropy。max就是拿激活的绝对最大值做范围简单但容易被离群点带偏。percent取99.99分位数等于主动丢弃一小部分极端值来换取大多数数据的精度这是我最常用的。entropy是让量化前后的信息熵损失最小理论上最优但有些工具链实现得一般需要实测对比。preprocess里的mean/std要和导出时匹配。YOLOv1系列在训练时普遍只做除以255归一化不按ImageNet减均值所以这里通常是0和255。你要是配了个mean: [0.485, 0.456, 0.406]量化校准的分布就全错了检测头输出会离谱。3.3 跑一次转换看懂输出日志里的精度对比表正常跑转换时工具链会输出大量日志看着很唬人其实核心要看的就几类信息。首先要看算子映射报告。ONNX里的算子会被映射成NPU内置算子映射不上的会被回退到CPU。日志里通常有一行汇总比如CPU fallback ops: 3。回退的算子多了推理耗时会被拖住而且某些算子回退到CPU反而比NPU慢两个量级。YOLO系列的NMS回退是正常的只要Conv、Relu这些主流算子不大量回退就行。其次看每一层量化前后的数据范围对比。工具链会打印每层激活的min/max如果发现某个中间层激活范围异常大比如达到几百这一层很可能有数值稳定性问题会导致量化误差被放大。最后是转换完成后的精度对比报告。工具链会在校准集上跑FP32模型和INT8模型给出每层激活误差和整体的余弦相似度。这里的数字不用追求极端但检测模型更推荐用实际mAP来验证量化损失而不是只看余弦相似度。因为检测头输出的坐标和置信度对数值误差的敏感度是非线性的。3.4 一次失败的转换算子不支持的排查思路我遇到过一次转换直接报错提示某个节点类型不被支持一看是GridSampler。这是模型里用了重采样层才会出现的算子YOLOv11标准版本不会带但如果你在neck部分改过结构或者拼接了别的模块就会引入新算子。排查思路是先用onnx.graphsurgeon之类工具找到报错节点的名字和输入输出定位到模型的具体位置。然后看有没有等价替换方案比如把GridSampler换成多个标准算子的组合或者把这一段子图抽出来单独作为CPU预处理。实际操作中最省事的办法是修改模型结构把有问题的模块替换成标准卷积或最近邻上采样重新训练或微调一下保证整体结构符合工具链的算子支持列表。如果实在不想动模型结构另一个思路是切开模型把报错的子图砍掉在板端推理时先用NPU跑前段再用CPU跑那段特殊算子最后再接回NPU后段。这样能用但工程复杂度上了一个台阶不建议优先采用。4. 板端部署把量化后的模型真正用起来4.1 部署工程的目录结构和模型bin的加载方式量化转换成功后拿到的不再是onnx文件而是板端专用的模型格式比如.bin或类似的自定义格式。这个文件里已经包含了量化参数、算子调度序列、融合后的层结构。部署时你不需要再关心权重怎么存、量化参数怎么设置全部打包好了。我的工程目录一般是这样的project/ ├── model/ │ └── yolo11n_quant.bin ├── src/ │ ├── main.cpp │ ├── yolo_postprocess.cpp │ └── img_preprocess.cpp ├── config/ │ └── deploy_config.json └── build/模型bin的加载本质上是把文件读入内存再通过推理接口创建一个模型句柄。板子上的推理库通常会提供一个类似load_model(path)的接口返回一个模型对象。这里要注意模型文件很大也可以加载但板端内存是有限的跑模型的同时还要跑系统服务建议上线前先单独跑一个最小推理程序测一下空闲内存余量。4.2 推理数据管线图像前处理、张量申请、输出解析板端推理的数据管线和GPU上差别很大。在GPU上你可以把原图直接resize再归一化然后一次性拷贝到显存在BPU上输入张量对内存对齐和格式有要求。我一般先做CPU侧预处理再把数据拷贝到NPU输入张量。下面是一个结构化的伪代码示意具体API名以你手上固件头文件为准import numpy as np # 1. 读取图像并做letterbox img cv2.imread(test.jpg) img_letterbox letterbox(img, (640, 640), fill(114, 114, 114)) # 2. BGR转RGBHWC转CHW并除以255 input_blob img_letterbox[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 input_blob np.ascontiguousarray(input_blob).reshape(1, 3, 640, 640) # 3. 申请模型输入张量并拷贝 input_tensor net.get_input_tensor(0) input_tensor.copy_from_numpy(input_blob) # 4. 执行推理 net.forward() # 5. 获取输出张量 output_tensor net.get_output_tensor(0) output_data output_tensor.to_numpy() # shape: (1, 84, 8400)这里最容易出问题的三件事一是letterbox的填充值必须是训练时的值YOLO系列默认是114但你要是训练代码里改过部署必须跟着改二是输入格式是BGR还是RGB一旦搞反检测效果会突然变得极差但又不是完全失效很有迷惑性三是内存连续性np.ascontiguousarray这步不能省否则数据在内存里的排列和NPU预期不符。4.3 后处理解算坐标映射、置信度阈值、NMS从NPU拿到的输出是(1, 84, 8400)的原始检测头输出不是最终的检测框。84的含义是4个坐标分量加80个类别得分。4个分量对YOLOv11来说是中心点坐标和宽高的相对编码值需要先解码才能转成实际的框。解码过程大致是把8400个anchor点对应到三个特征图尺度然后根据每个anchor的位置、步长和预测的偏移量还原出绝对坐标。这一步你可以用纯numpy实现也可以直接调用一些现成的检测库但核心逻辑必须自己心里清楚。坐标还原之后乘以letterbox的缩放系数和填充偏移把坐标映射回原图尺寸。然后做置信度阈值过滤和NMS。我用的是cv2.dnn.NMSBoxes在板子上也能跑几千个候选框过滤一遍的耗时非常低。一个重要提示如果你想让后处理更省事可以尝试把NMS也放进模型里但工具链对NMS的支持通常有限而且很多时候板端NMS是CPU回退的。亲自测试后我发现纯CPU侧做NMS的稳定性反而更高也更容易调试。5. 板端实测和那些我踩过的坑5.1 耗时、内存和温度的实测记录我实际部署YOLOv11n量化模型后记录了一组数据放在这里供参考。测试环境RDK S100P官方Debian系统单路视频流输入640x640YOLOv11n INT8量化版本。指标数值NPU单帧推理耗时约18ms整体端到端耗时(含预处理后处理)约24ms帧率约40FPS峰值内存占用约320MB满载运行15分钟后芯片温度约68°C说明一下这个数据不是官方标称值和固件版本、系统负载、是否开启多线程都有关系。同一个模型在不同版本固件上可能差3到5毫秒所以别拿我的数字当基准更重要的是掌握方法自己测自己调。让我比较欣慰的是同样的模型用FP32跑CPU单帧推理就要400ms以上量化部署的收益是数量级的提升。这也是为什么说量化部署不是可选项而是必选项。5.2 量化后精度掉点最常见的根因是预处理不一致第一次做这个项目时量化后mAP掉了整整4个点我当时差点以为是量化方法选错了。后来反复排查才发现问题出在预处理上。训练脚本里的letterbox填充值是(114, 114, 114)但我在写校准数据预处理时习惯性地用了(0, 0, 0)。校准阶段给模型喂的图像分布和实际推理时的图像分布出现了系统性偏差激活值的统计自然就乱了量化参数全部偏向错误方向。把校准预处理改成和部署完全一致的(114, 114, 114)之后量化模型和浮点模型的mAP差距从4个点缩到了0.3个点以内。这等于告诉我们量化的精度损失确实可以把控但前提是每个环节的输入分布必须对齐。5.3 检测框整体偏移anchor与解码逻辑的错位另一个很有迷惑性的坑是量化模型在板子上跑检测框位置整体偏移但偏移方向一致比如总是往右上角偏。第一反应以为是量化精度不够实际上是因为输出解码时用错了坐标基准。YOLOv11的输出在不同导出方式下可能是不同的布局。标准Ultralytics导出的ONNX输出是(1, 84, 8400)解码时要用anchor点坐标乘以下采样步长来还原。但如果你在导出时把检测头某些层优化了工具链可能直接输出解码后的(x, y, w, h)格式。这种情况下你还按anchor解码逻辑再算一遍就会得到叠加的偏移。排查方法是打印输出张量的数值分布看看坐标分量的范围。如果坐标值很小集中在0到1之间多半是相对编码需要解码如果坐标值已经和输入分辨率同级大概率是解码后的坐标。发现布局不对改后处理逻辑即可不需要重新量化。5.4 多路并发和动态batch怎么安排单路跑通之后很自然的下一步就是多路并发。RDK S100P的NPU支持多个任务并行处理但要注意几点。一是模型实例不要开太多。每个模型实例都会复制一份运行时状态占内存。实测开4路视频时我选择只加载一个模型实例用四个线程轮流喂数据让NPU按任务队列调度整体吞吐比开四个实例要高内存也省很多。二是输入batch可以适当加大。如果你的工具链支持batch推理把多帧图像拼成一个batch输入能提高NPU利用率。但batch增大会成倍占用内存需要权衡。我在4路场景下选择batch2两帧一提交性价比最高。三是动态shape尽量避开。板端推理时用固定输入尺寸不要试图在运行时改输入分辨率。工具链对动态shape支持有限一旦开了这个口子性能优化空间会被堵死。最后说一下我的整体体会。量化部署这件事90%的问题出在分布一致性上校准数据分布、图像预处理分布、输出解码逻辑任何一个环节和训练时不一致最终都会在精度或性能上体现出来。RDK S100P这套流程跑通后我的固定套路是先校准集小验证再板端小规模精度对比最后才压测多路并发。每一步都留好日志和中间文件出了奇怪问题能快速回溯。如果你也在做类似的事希望这篇记录能帮你少走几步弯路。

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

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

免费获取报价 →
↑