本文记录 YOLOv8s 在地平线 J6M 上的最终可复现部署流程。正式方案统一采用 Raw6 六路输出、PTQ INT8、NV12 Runtime 输入、CPU 后处理。文中命令需要根据实际 SDK 安装目录、服务器地址和开发板目录调整。
1. 环境搭建
1.1 创建 OpenExplorer 容器
目的:使用固定版本的 OpenExplorer 工具链,隔离模型转换和编译环境。
本文使用的镜像为:
先确认镜像存在:
准备宿主机目录:
创建容器:
参数说明:
参数 | 作用 |
|---|---|
--name xy_j6_381 | 指定容器名称 |
--gpus all | 允许容器使用宿主机 GPU |
--shm-size=16g | 增大共享内存 |
-v ~/projects:/workspace/projects | 挂载项目目录 |
-v ~/docker-data/xy_j6_381:/workspace/data | 挂载持久化数据目录 |
1.2 验证工具链和挂载目录
进入容器后检查基础环境:
检查挂载目录:
建议把本次模型转换文件集中放到一个目录中:
1.3 常用容器命令
退出容器:
启动并重新进入容器:
检查容器状态:
2. 最终部署方案
2.1 整体流程
正式部署主线如下:
2.2 为什么导出 Raw6
Ultralytics 默认导出的检测模型通常把 DFL、坐标解码和输出拼接放在图中,最终形成单路检测输出。该形式便于通用推理,但解码算子和大张量拼接可能增加量化误差或工具链适配难度。
Raw6 方案只保留三个尺度的回归分支和分类分支,把以下操作移到 CPU:
这样可以逐层对齐 PyTorch、ONNX、量化模型和板端结果,也便于定位精度问题。
2.3 最终模型输入输出约定
模型训练侧和校准侧输入:
板端 Runtime 输入:
Raw6 输出约定:
输出名称、顺序、布局和数据类型必须以 ONNX 检查结果、编译报告及 hrt_model_exec model_info 的实际输出为准,不要仅依赖代码中的固定下标。
3. 导出 Raw6 ONNX
3.1 安装依赖
记录实际版本,便于复现:
3.2 修改 Detect Head 并导出
目的:让检测头在导出模式下直接返回 P3/P4/P5 的回归和分类特征,不在 ONNX 图中执行 DFL 和坐标解码。
核心逻辑示意:
3.3 检查 ONNX 输入输出
先执行 ONNX 结构检查:
再打印输入输出信息:
检查重点:
- 输入名是否为 images。
- 输入 Shape 是否为 [1,3,640,640]。
输出数量是否为 6。
输出名称和 Shape 是否与第 2.3 节一致。
ONNX 输出顺序是否与板端后处理的绑定顺序一致。
3.4 验证浮点 ONNX 精度
目的:先证明 Raw6 导出和 CPU 后处理正确,再进入量化。若 ONNX 已经偏离 PyTorch,后续 PTQ 结果没有参考价值。
对同一验证集使用相同的:
LetterBox 参数和填充值。
- RGB 转换与 /255 归一化。
置信度阈值和 NMS IoU 阈值。
DFL、Anchor Point 和 stride 约定。
标签映射与 COCO mAP 评测脚本。
至少记录:
阶段 | mAP50-95 | mAP50 | Recall | 结论 |
|---|---|---|---|---|
PyTorch yolov8s.pt | 待单独评测 | 待单独评测 | 待填写 | 原始 PyTorch 基线 |
Raw6 yolov8s_raw6.onnx | 0.537 | 0.696 | 未输出 | Raw6 导出正确 |
如果差异明显,优先排查输出顺序、DFL 维度、stride、Anchor Point、LetterBox 和 NMS,而不是继续编译。
4. PTQ 校准与 HBM 编译
4.1 准备校准图片
目的:用具有代表性的样本估计激活分布。校准图片应覆盖实际部署中的目标尺寸、光照、背景和拍摄角度。
建议从训练集或验证集中抽取有代表性的图片;样本数量应结合数据分布和工具链建议确定,不应只追求固定数量。
4.2 校准预处理流程
校准链路必须与 ONNX 输入约定一致:
核心代码:
保存前抽查:
预期约定:
4.3 生成 YAML 模板
目的:先由当前工具链生成完整模板,再修改关键字段,避免遗漏版本相关配置。
4.4 编写正式 YAML
关键约定:
4.5 编译生成 HBM
执行正式 PTQ 编译:
目标输出文件为:
编译成功只说明模型能够生成目标产物,不代表量化精度正确。
4.6 查看编译报告
重点检查:
是否存在不支持算子或 CPU fallback。
六路输出是否保留。
输入转换是否为 NV12 Runtime 到 RGB Train 输入。
各节点量化信息和异常饱和情况。
静态性能估计、BPU core 分配和内存占用。
- 最终 HBM 文件名是否为 yolov8s_raw6_640x640_nv12.hbm。
5. 量化精度评测
5.1 为什么必须先测精度
模型成功编译不等于精度正确。输入颜色、归一化、输出顺序、量化尺度或后处理中的任一处不一致,都可能在程序正常运行的情况下产生错误结果。
必须先确认 HBM 精度,再进行板端延迟和 FPS 测试。
5.2 评测阶段
按照以下顺序逐级评测:
每一级均使用同一验证集、同一前处理、同一 Raw6 后处理、同一阈值和同一指标实现。若某一级首次出现明显下降,排查范围即可收敛到该转换阶段。
建议同时进行两类对齐:
- 数据集级指标对齐:比较 mAP50-95、mAP50、Recall。
- 样本级张量对齐:保存相同图片在六路输出上的 min/max、均值、余弦相似度或误差分布,并比较解码后的候选框。
5.3 指标记录表
阶段 | 模型文件 | 输入形式 | mAP50-95 | mAP50 | Recall | 说明 |
|---|---|---|---|---|---|---|
PyTorch | yolov8s.pt | RGB FP32 | 待单独评测 | 待单独评测 | 待填写 | 原始 PyTorch 基线 |
Raw6 ONNX | yolov8s_raw6.onnx 或 *_original_float_model.onnx | NCHW RGB FP32,[0,1] | 0.537 | 0.696 | 未输出 | Raw6 导出正确 |
Optimized ONNX | *_optimized_float_model.onnx | NCHW RGB FP32,[0,1] | 0.537 | 0.696 | 未输出 | 图优化无精度影响 |
Calibrated ONNX | *_calibrated_model.onnx | NCHW RGB FP32,量化仿真 | 0.529 | 0.685 | 未输出 | INT8 校准后仅小幅下降 |
PTQ ONNX | *_ptq_model.onnx | NCHW RGB FP32,量化仿真 | 0.529 | 0.685 | 未输出 | PTQ 转换未引入额外损失 |
Quantized BC | *_quantized_model.bc | NHWC NV12 uint8 | 0.537 | 0.687 | 待填写 | 需确认六路输出反量化 |
HBM | yolov8s_raw6_640x640_nv12.hbm | NHWC NV12 uint8 | 待评测 | 待评测 | 待填写 | 最终板端模型 |
当前已测阶段的精度变化:
目前可以确认图优化没有引入可见精度损失,校准阶段出现小幅下降,PTQ 转换未继续扩大损失。由于 PyTorch、Quantized BC 和 HBM 指标尚未完成,暂时不能得出完整的端到端精度结论。
5.4 精度下降判断
精度是否可接受应以项目验收阈值为准,不应直接套用固定百分比。建议按以下顺序定位:
只有 HBM 指标达到项目要求后,才进入性能验收。
6. 板端部署与性能测试
6.1 拷贝 HBM 到开发板
先在主机确认文件:
示例拷贝命令:
服务器地址和板端目录按实际环境修改。
6.2 查看 HBM 输入输出
板端必须先执行:
本次实测环境:
项目 | 实际值 |
|---|---|
模型名称 | yolov8s_raw6_640x640_nv12 |
Runtime | UCP/DNN 3.12.1,HBRT 4.4.6 |
BPULib | 2.2.6 |
Builder | 3.5.11 |
HBDK | 4.9.7 |
HMCT | 2.7.3 |
March | nash-e |
HBM 加载到 DDR | 547.77 ms,仅为本次 model_info 记录 |
NV12 Runtime 在 HBM 中表现为两个输入 Tensor,分别承载 Y 平面和 UV 平面:
输入 | 名称 | Valid Shape | 类型 | 量化 | Stride |
|---|---|---|---|---|---|
0 | images_y | (1,640,640,1) | HB_DNN_TENSOR_TYPE_U8 | NONE | (-1,-1,1,1) |
1 | images_uv | (1,320,320,2) | HB_DNN_TENSOR_TYPE_U8 | NONE | (-1,-1,2,1) |
六路输出均为 FP32,未附带量化参数:
输出 | 名称 | Valid Shape | 类型 | Aligned Byte Size | Stride |
|---|---|---|---|---|---|
0 | p3_box | (1,64,80,80) | HB_DNN_TENSOR_TYPE_F32 | 1966080 | (1966080,30720,384,4) |
1 | p3_cls | (1,80,80,80) | HB_DNN_TENSOR_TYPE_F32 | 2457600 | (2457600,30720,384,4) |
2 | p4_box | (1,64,40,40) | HB_DNN_TENSOR_TYPE_F32 | 655360 | (655360,10240,256,4) |
3 | p4_cls | (1,80,40,40) | HB_DNN_TENSOR_TYPE_F32 | 819200 | (819200,10240,256,4) |
4 | p5_box | (1,64,20,20) | HB_DNN_TENSOR_TYPE_F32 | 163840 | (163840,2560,128,4) |
5 | p5_cls | (1,80,20,20) | HB_DNN_TENSOR_TYPE_F32 | 204800 | (204800,2560,128,4) |
实测结果确认:
6.3 测试单线程延迟
具体子命令参数可能随 SDK 版本变化,先查看帮助:
本次使用 1 线程、2000 帧进行较长时间测试:
记录:
指标 | 结果 |
|---|---|
工具线程平均延迟 | 1.778251 ms |
工具统计平均延迟 | 1.778 ms |
最小延迟 | 1.729 ms |
最大延迟 | 2.177 ms |
帧数 | 2000 |
程序运行时间 | 3675.454 ms |
帧累计延迟 | 3556.503 ms |
单线程吞吐 | 544.194 FPS |
HBM 加载到 DDR | 530.64 ms,不计入逐帧平均延迟 |
6.4 测试多线程 FPS
本次对 1、2、4、8 线程分别执行 2000 帧测试:
线程数 | Batch | 测试时长 | FPS | 平均延迟 |
|---|---|---|---|---|
1 | 1 | 3675.454 ms / 2000 帧 | 544.194 | 1.778 ms |
2 | 1 | 2945.075 ms / 2000 帧 | 679.157 | 2.890 ms |
4 | 1 | 2945.172 ms / 2000 帧 | 679.136 | 5.798 ms |
8 | 1 | 2946.025 ms / 2000 帧 | 678.933 | 11.672 ms |
从本次工具测试可以看出:
- 2 线程相对 1 线程,吞吐从 544.194 FPS 提升到 679.157 FPS,约提升 24.8%。
- 从 2 线程增加到 4 或 8 线程后,吞吐基本不再增长,稳定在约 679 FPS。
- 并发继续增加时,单帧平均延迟近似按线程数增长:2.890 → 5.798 → 11.672 ms。
如果目标是该测试条件下的最高吞吐且兼顾延迟,2 线程是更合理的配置;4、8 线程没有带来有效吞吐收益。
上述多线程测试同样未提供 --input_file,结论仅适用于当前 HBM 的工具级调度和 BPU 吞吐,不代表真实业务程序的端到端并发性能。
7. 板端推理程序
7.1 程序整体流程
7.2 NV12 预处理
预处理必须统一以下规则:
LetterBox 缩放和填充规则与校准、评测一致。
- 输出尺寸为 640×640;NV12 宽高必须满足相应对齐要求。
BGR/RGB 到 YUV 的色彩矩阵和 UV 排列与模型 Runtime 输入一致。
区分 NV12(UV 交错)和 NV21(VU 交错)。
- 按 Tensor 的 validShape、alignedShape 和 stride 写入,不能默认数据紧密排列。
建议保留的接口:
NV12 数据大小通常为:
实际写入大小和分平面方式必须以 HBM 输入 Tensor 属性及 SDK API 约定为准。
7.3 模型加载与 Tensor 内存
模型加载和属性查询顺序:
7.4 BPU 推理调度
关键调用顺序:
正式代码需要检查每个 API 的返回值,并在失败路径释放已经申请的资源。
7.5 Raw6 CPU 后处理
统一后处理流程:
三个尺度的 stride 通常为:
核心接口建议:
坐标还原:
还原后需要裁剪到原图边界。
7.6 可视化和资源释放
可视化只保留必要调用:
资源释放顺序:
完整实现建议拆分为:
7.7 交叉编译与运行
原环境使用的交叉编译器:
建议使用单层构建目录:
拷贝程序、模型和测试数据:
板端运行示例:
8. 踩坑记录
本节记录早期默认单输出 RGB Runtime 模型的排查过程。当前正式方案已经改为 Raw6 六路输出和 NV12 Runtime,因此本节中的输入和后处理实现不能直接用于最终模型。
8.1 默认 ONNX 包含 DFL 和坐标解码
现象
排查过程
对比 PyTorch、默认 ONNX 和量化输出,确认差异集中在检测头末端及单路输出。
根因
默认导出形式把对量化较敏感的解码和拼接留在模型图中,同时失去了逐尺度、逐分支对齐能力。
修改方式
修改 Detect Head,导出 P3/P4/P5 的 box 和 cls 六路原始输出;DFL、Anchor 解码和 NMS 移到 CPU。
修改前后结果
对当前方案的影响
8.2 单输出 INT8 模型精度下降
现象
早期 HBM 可以正常运行,但类别、置信度、框数量和 ONNX 明显不一致。
排查过程
依次检查 LetterBox、坐标映射、校准数据范围、YAML 归一化和 Runtime Tensor 类型。单图结果显示,问题并非只由 NMS 阈值造成。
根因
早期方案同时混用了默认单输出模型、RGB Runtime 和板端 INT8 Tensor 写入约定,导致输入分布或模型末端量化误差难以区分。
修改方式
最终改用 Raw6 + NV12:用 Raw6 缩小量化排查范围,用 NV12 Runtime 统一板端图像输入。
修改前后结果
旧方案中曾通过修正 RGB INT8 写入改善单图对齐,但这只能证明旧模型的输入编码问题得到缓解,不能代替当前 HBM 的数据集级 mAP 评测。
对当前方案的影响
旧方案结果不得作为 Raw6 + NV12 的精度结论。
8.3 RGB Runtime 的 pixel-128 问题
现象
排查过程
根因
修改方式
旧 RGB Runtime 模型使用:
修改前后结果
原笔记中的单图样例在修改后基本对齐,但没有提供完整数据集指标。
对当前方案的影响
该问题来自早期的默认单输出 RGB Runtime 模型,不适用于当前 Raw6 + NV12 最终方案。
8.4 Raw6 输出顺序和 Shape 对齐
现象
六路 Tensor 数量正确,但解码结果异常,常见表现为框尺度错误、类别置信度异常或完全无框。
排查过程
分别打印六路输出的名称、Shape、数据类型和数值范围,并与 ONNX Runtime 输出逐路比较。
根因
可能原因包括:
把 cls 分支当作 box 分支。
P3/P4/P5 顺序错误。
NCHW/NHWC 解释错误。
- 使用 validShape 访问对齐后的 Tensor 时忽略 stride。
忽略输出量化类型或反量化参数。
修改方式
修改前后结果
对当前方案的影响
Raw6 后处理必须在启动阶段验证全部六路输出,验证失败应立即终止推理。
8.5 校准前处理与 YAML 不一致
现象
编译可以完成,但 calibrated、quantized 或 HBM 阶段精度突然下降。
排查过程
根因
修改方式
统一校准输入为:
并用同一预处理函数驱动 ONNX 精度评测和校准数据生成。
修改前后结果
数据集指标必须重新评测,结果填写到第 5.3 节。
对当前方案的影响
校准链路和板端链路的数据格式不同,但它们在模型内部转换后的有效输入分布必须一致。
9. 常用 API 速查
9.1 OpenCV API
API | 作用 |
|---|---|
cv::imread | 读取 BGR 图片 |
cv::resize | 等比例缩放 |
cv::copyMakeBorder | LetterBox 填充 |
cv::cvtColor | 颜色空间转换 |
cv::dnn::NMSBoxes | 候选框 NMS;按类别调用 |
cv::rectangle | 绘制检测框 |
cv::putText | 绘制类别和置信度 |
cv::imwrite | 保存结果图 |
9.2 Horizon DNN API
API | 作用 |
|---|---|
hbDNNInitializeFromFiles | 加载 HBM 模型包 |
hbDNNGetModelNameList | 获取模型名称列表 |
hbDNNGetModelHandle | 获取具体模型句柄 |
hbDNNGetInputCount | 查询输入数量 |
hbDNNGetOutputCount | 查询输出数量 |
hbDNNGetInputTensorProperties | 查询输入 Tensor 属性 |
hbDNNGetOutputTensorProperties | 查询输出 Tensor 属性 |
hbDNNInferV2 | 创建推理任务 |
hbDNNRelease | 释放模型包 |
9.3 Horizon UCP API
API | 作用 |
|---|---|
hbUCPMallocCached | 申请 BPU/CPU 可访问的缓存内存 |
hbUCPMemFlush(...CLEAN) | 把 CPU 写入同步给 BPU |
HB_UCP_INITIALIZE_SCHED_PARAM | 初始化调度参数 |
hbUCPSubmitTask | 提交推理任务 |
hbUCPWaitTaskDone | 等待任务完成 |
hbUCPMemFlush(...INVALIDATE) | 让 CPU 读取 BPU 更新后的输出 |
hbUCPReleaseTask | 释放任务 |
hbUCPFree | 释放 Tensor 内存 |
9.4 API 调用顺序
职责划分:
10. 最终总结
本次部署的固定主线是:
部署验收必须同时满足:
Raw6 ONNX 与 PyTorch 精度对齐。
optimized、calibrated、quantized 各阶段精度变化可解释。
HBM 使用实际 NV12 链路评测且满足项目精度阈值。
板端程序根据实际 Tensor 属性处理 Y、UV 两个输入 Tensor 和六路 FP32 输出。
延迟、FPS、CPU/BPU 占用和内存满足项目要求。
所有评测均记录数据集、阈值、工具版本和运行条件。

