本文记录 YOLOv8s 在地平线 征程6M 上的最终可复现部署流程。正式方案统一采用 Raw6 六路输出、PTQ INT8、NV12 Runtime 输入、CPU 后处理。文中命令需要根据实际 SDK 安装目录、服务器地址和开发板目录调整。
1. 环境搭建
1.1 创建 OpenExplorer 容器
目的:使用固定版本的 OpenExplorer 工具链,隔离模型转换和编译环境。
本文使用的镜像为:
openexplorer/ai_toolchain_ubuntu_22_j6_gpu:v3.8.1
先确认镜像存在:
docker images
准备宿主机目录:
mkdir -p ~/projects
mkdir -p ~/docker-data/xy_j6_381
创建容器:
docker run -it \
--name xy_j6_381 \
--gpus all \
--shm-size=16g \
-v ~/projects:/workspace/projects \
-v ~/docker-data/xy_j6_381:/workspace/data \
openexplorer/ai_toolchain_ubuntu_22_j6_gpu:v3.8.1
参数说明:
| 参数 | 作用 |
|---|---|
--name xy_j6_381 | 指定容器名称 |
--gpus all | 允许容器使用宿主机 GPU |
--shm-size=16g | 增大共享内存 |
-v ~/projects:/workspace/projects | 挂载项目目录 |
-v ~/docker-data/xy_j6_381:/workspace/data | 挂载持久化数据目录 |
1.2 验证工具链和挂载目录
进入容器后检查基础环境:
pwd
python --version
conda --version
nvidia-smi
ls /open_explorer
检查挂载目录:
ls /workspace
ls /workspace/projects
ls /workspace/data
建议把本次模型转换文件集中放到一个目录中:
mkdir -p /workspace/projects/j6m/yolov8s_raw6
cd /workspace/projects/j6m/yolov8s_raw6
1.3 常用容器命令
退出容器:
exit
启动并重新进入容器:
docker start xy_j6_381
docker exec -it xy_j6_381 /bin/bash
检查容器状态:
docker ps -a | grep xy_j6_381
2. 最终部署方案
2.1 整体流程
正式部署主线如下:
yolov8s.pt
→ 修改 Detect Head
→ 导出 yolov8s_raw6.onnx
→ 输出 P3/P4/P5 三个尺度的 box 和 cls,共六路输出
→ 准备 RGB、NCHW、float32、[0,1] 校准数据
→ 使用 PTQ INT8 编译
→ 生成 yolov8s_raw6_640x640_nv12.hbm
→ 评测 ONNX、优化、校准、量化及 HBM 各阶段精度
→ 确认 HBM 精度满足要求
→ 部署 HBM 到 J6M 开发板
→ 板端输入 NV12
→ 获取六路原始输出
→ CPU 完成 DFL、Anchor 解码、Sigmoid、NMS 和坐标还原
→ 测试延迟、FPS 和资源占用
2.2 为什么导出 Raw6
Ultralytics 默认导出的检测模型通常把 DFL、坐标解码和输出拼接放在图中,最终形成单路检测输出。该形式便于通用推理,但解码算子和大张量拼接可能增加量化误差或工具链适配难度。
Raw6 方案只保留三个尺度的回归分支和分类分支,把以下操作移到 CPU:
DFL Softmax
→ 距离加权
→ Anchor Point 解码
→ 分类 Sigmoid
→ P3/P4/P5 合并
→ 置信度筛选
→ 按类别 NMS
→ LetterBox 坐标还原
这样可以逐层对齐 PyTorch、ONNX、量化模型和板端结果,也便于定位精度问题。
2.3 最终模型输入输出约定
模型训练侧和校准侧输入:
颜色格式:RGB
布局:NCHW
数据类型:float32
单张 Shape:[3,640,640]
模型输入 Shape:[1,3,640,640]
数值范围:[0,1]
板端 Runtime 输入:
颜色格式:NV12
数据类型:uint8
分辨率:640×640
Raw6 输出约定:
p3_box:[1,64,80,80]
p3_cls:[1,80,80,80]
p4_box:[1,64,40,40]
p4_cls:[1,80,40,40]
p5_box:[1,64,20,20]
p5_cls:[1,80,20,20]
其中,64 = 4 × reg_max,YOLOv8 默认 reg_max=16;80 是 COCO 类别数。若使用自定义类别数,分类分支的通道数应以实际模型为准。
输出名称、顺序、布局和数据类型必须以 ONNX 检查结果、编译报告及
hrt_model_exec model_info的实际输出为准,不要仅依赖代码中的固定下标。
3. 导出 Raw6 ONNX
3.1 安装依赖
目的:准备与 yolov8s.pt 兼容的 Ultralytics、PyTorch、ONNX 和 ONNX Runtime 环境。
python -m pip install ultralytics onnx onnxruntime onnxsim
记录实际版本,便于复现:
python -c "import torch, ultralytics, onnx, onnxruntime; print(torch.__version__, ultralytics.__version__, onnx.__version__, onnxruntime.__version__)"
不同 Ultralytics 版本的 Detect 实现可能变化。修改前应先确认当前版本源码,不要直接套用其他版本的补丁。
3.2 修改 Detect Head 并导出
目的:让检测头在导出模式下直接返回 P3/P4/P5 的回归和分类特征,不在 ONNX 图中执行 DFL 和坐标解码。
核心逻辑示意:
def forward_raw6(self, x):
outputs = []
for i in range(self.nl):
box = self.cv2[i](x[i])
cls = self.cv3[i](x[i])
outputs.extend([box, cls])
return tuple(outputs)
需要根据当前 Ultralytics 版本把该逻辑接入 Detect.forward 或使用导出包装模块。正式导出时固定输入尺寸和输出名称:
import torch
from ultralytics import YOLO
model = YOLO("yolov8s.pt").model.eval()
dummy = torch.zeros(1, 3, 640, 640)
torch.onnx.export(
model,
dummy,
"yolov8s_raw6.onnx",
input_names=["images"],
output_names=[
"p3_box", "p3_cls",
"p4_box", "p4_cls",
"p5_box", "p5_cls",
],
opset_version=11,
do_constant_folding=True,
)
上述代码是调用框架示意。导出前必须确认 model(dummy) 已返回六个 Tensor;若仍返回单路结果,说明 Detect Head 修改尚未生效。
3.3 检查 ONNX 输入输出
先执行 ONNX 结构检查:
python -c "import onnx; m=onnx.load('yolov8s_raw6.onnx'); onnx.checker.check_model(m); print('ONNX check passed')"
再打印输入输出信息:
import onnxruntime as ort
session = ort.InferenceSession(
"yolov8s_raw6.onnx",
providers=["CPUExecutionProvider"],
)
for item in session.get_inputs():
print("input:", item.name, item.shape, item.type)
for item in session.get_outputs():
print("output:", item.name, item.shape, item.type)
检查重点:
- 输入名是否为
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 准备校准图片
目的:用具有代表性的样本估计激活分布。校准图片应覆盖实际部署中的目标尺寸、光照、背景和拍摄角度。
建议从训练集或验证集中抽取有代表性的图片;样本数量应结合数据分布和工具链建议确定,不应只追求固定数量。
mkdir -p calibration_images
mkdir -p calibration_data_rgb_f32
4.2 校准预处理流程
校准链路必须与 ONNX 输入约定一致:
OpenCV 读取 BGR 图片
→ LetterBox 到 640×640
→ BGR 转 RGB
→ HWC 转 CHW
→ 转为 float32
→ 除以 255,归一化到 [0,1]
→ 保存单张 [3,640,640] 数据
核心代码:
import cv2
import numpy as np
def preprocess_for_calibration(image_path):
bgr = cv2.imread(str(image_path))
if bgr is None:
raise ValueError(f"无法读取图片:{image_path}")
# letterbox() 必须与精度评测和板端实现使用相同的缩放、取整及填充规则
padded_bgr = letterbox(bgr, new_shape=(640, 640), color=(114, 114, 114))
rgb = cv2.cvtColor(padded_bgr, cv2.COLOR_BGR2RGB)
chw = rgb.transpose(2, 0, 1)
return np.ascontiguousarray(chw, dtype=np.float32) / 255.0
保存前抽查:
print(array.shape)
print(array.dtype)
print(float(array.min()), float(array.max()))
预期约定:
Shape:(3,640,640)
dtype:float32
范围:[0,1]
具体文件格式由当前版本 OpenExplorer 配置要求决定。若配置要求二进制文件,应使用连续内存数据写入;若接受 .npy,则保留 Shape 和 dtype 信息。
4.3 生成 YAML 模板
目的:先由当前工具链生成完整模板,再修改关键字段,避免遗漏版本相关配置。
hb_config_generator \
--full-yaml \
--model yolov8s_raw6.onnx \
--march nash-e
J6M 使用的 march 应结合当前 OpenExplorer 版本和芯片资料确认;本文按原环境采用 nash-e。
4.4 编写正式 YAML
正式配置文件统一命名为 yolov8s_raw6_config.yaml,核心配置如下:
model_parameters:
onnx_model: "./yolov8s_raw6.onnx"
march: "nash-e"
working_dir: "./model_output_raw6_int8"
output_model_file_prefix: "yolov8s_raw6_640x640_nv12"
output_nodes: ""
input_parameters:
input_name: "images"
input_type_rt: "nv12"
input_type_train: "rgb"
input_layout_train: "NCHW"
input_shape: "1x3x640x640"
mean_value: ""
scale_value: "0.003921568627451"
std_value: ""
calibration_parameters:
cal_data_dir: "./calibration_data_rgb_f32"
calibration_type: "default"
compiler_parameters:
optimize_level: "O2"
compile_mode: "latency"
core_num: 1
jobs: 16
关键约定:
校准链路:RGB + NCHW + float32 + [0,1]
板端链路:NV12 + uint8
input_name 必须与 ONNX 实际输入名一致。scale_value 与校准数据是否已经归一化存在工具链版本和配置语义差异时,应依据当前 SDK 文档和编译日志确认,避免重复归一化。
4.5 编译生成 HBM
执行正式 PTQ 编译:
hb_compile -c yolov8s_raw6_config.yaml
目标输出文件为:
model_output_raw6_int8/yolov8s_raw6_640x640_nv12.hbm
编译成功只说明模型能够生成目标产物,不代表量化精度正确。
4.6 查看编译报告
检查 model_output_raw6_int8 中实际生成的日志、HTML、JSON、BC 和 HBM 文件:
find model_output_raw6_int8 -maxdepth 2 -type f | sort
重点检查:
- 是否存在不支持算子或 CPU fallback。
- 六路输出是否保留。
- 输入转换是否为 NV12 Runtime 到 RGB Train 输入。
- 各节点量化信息和异常饱和情况。
- 静态性能估计、BPU core 分配和内存占用。
- 最终 HBM 文件名是否为
yolov8s_raw6_640x640_nv12.hbm。
5. 量化精度评测
5.1 为什么必须先测精度
模型成功编译不等于精度正确。输入颜色、归一化、输出顺序、量化尺度或后处理中的任一处不一致,都可能在程序正常运行的情况下产生错误结果。
必须先确认 HBM 精度,再进行板端延迟和 FPS 测试。
5.2 评测阶段
按照以下顺序逐级评测:
PyTorch
→ Raw6 ONNX
→ optimized BC
→ calibrated BC
→ quantized BC
→ HBM
每一级均使用同一验证集、同一前处理、同一 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 | 待评测 | 待评测 | 待填写 | 最终板端模型 |
当前已测阶段的精度变化:
Raw6 ONNX → Calibrated ONNX
mAP50-95:0.537 → 0.529,下降 0.008
mAP50: 0.696 → 0.685,下降 0.011
Calibrated ONNX → PTQ ONNX
mAP50-95:0.529 → 0.529,无额外下降
mAP50: 0.685 → 0.685,无额外下降
目前可以确认图优化没有引入可见精度损失,校准阶段出现小幅下降,PTQ 转换未继续扩大损失。由于 PyTorch、Quantized BC 和 HBM 指标尚未完成,暂时不能得出完整的端到端精度结论。
5.4 精度下降判断
精度是否可接受应以项目验收阈值为准,不应直接套用固定百分比。建议按以下顺序定位:
PyTorch ≠ ONNX
→ 检查 Raw6 导出、DFL、Anchor、stride、Sigmoid、NMS
ONNX ≠ Optimized
→ 检查图优化及算子替换
Optimized ≠ Calibrated
→ 检查校准样本和预处理
Calibrated ≠ Quantized
→ 检查敏感节点、量化参数和饱和分布
Quantized ≠ HBM
→ 检查 NV12 输入、色彩转换、stride、输出类型和板端后处理
只有 HBM 指标达到项目要求后,才进入性能验收。
6. 板端部署与性能测试
6.1 拷贝 HBM 到开发板
先在主机确认文件:
ls -lh model_output_raw6_int8/yolov8s_raw6_640x640_nv12.hbm
示例拷贝命令:
scp model_output_raw6_int8/yolov8s_raw6_640x640_nv12.hbm \
root@192.168.0.65:/userdata/xy/yolov8s_raw6/
服务器地址和板端目录按实际环境修改。
6.2 查看 HBM 输入输出
板端必须先执行:
cd /userdata/xy/yolov8s_raw6
hrt_model_exec model_info \
--model_file yolov8s_raw6_640x640_nv12.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) |
实测结果确认:
Runtime 输入:2 个 U8 Tensor,分别为 Y 平面和 UV 平面
模型输出:6 个 FP32 Tensor
输出顺序:p3_box、p3_cls、p4_box、p4_cls、p5_box、p5_cls
输入 stride 显示为动态值。应用程序不能把 -1 当作实际步长,也不能固定假设只有一个输入 Tensor。应按 SDK 的 Tensor 分配结果或明确设置的实际 stride 写入 Y、UV 两个平面。
6.3 测试单线程延迟
具体子命令参数可能随 SDK 版本变化,先查看帮助:
hrt_model_exec --help
hrt_model_exec perf --help
本次使用 1 线程、2000 帧进行较长时间测试:
hrt_model_exec perf \
--model_file yolov8s_raw6_640x640_nv12.hbm \
--thread_num 1 \
--frame_count 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,不计入逐帧平均延迟 |
本次命令没有通过 --input_file 提供真实 NV12 数据。工具为动态 stride 自动采用:
images_y: (409600,640,1,1)
images_uv:(204800,640,2,1)
因此,1.778 ms 和 544.194 FPS 可用于记录当前 HBM 的单线程工具级 BPU 性能,但不能作为真实图片端到端结果。该数据不包含图片读取、LetterBox、NV12 转换、Raw6 CPU 后处理、NMS 和可视化耗时;若模型中存在对输入范围敏感的算子,还应使用有效的 --input_file 重新测试。
6.4 测试多线程 FPS
本次对 1、2、4、8 线程分别执行 2000 帧测试:
MODEL=yolov8s_raw6_640x640_nv12.hbm
for THREADS in 1 2 4 8; do
hrt_model_exec perf \
--model_file "${MODEL}" \
--thread_num "${THREADS}" \
--frame_count 2000 \
2>&1 | tee "perf_thread_${THREADS}.log"
done
| 线程数 | 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 程序整体流程
读取原始图片或视频帧
→ LetterBox 到 640×640
→ BGR 转 NV12
→ 加载 HBM 并查询输入输出属性
→ 按实际 Tensor 属性申请连续内存
→ 写入 NV12 数据并清理输入 Cache
→ 创建、提交并等待 BPU 推理任务
→ 失效输出 Cache
→ 读取六路原始输出
→ DFL + Anchor 解码 + Sigmoid
→ 合并三尺度并执行按类别 NMS
→ 还原 LetterBox 坐标
→ 绘制或输出检测结果
→ 释放任务、Tensor 和模型资源
7.2 NV12 预处理
正式方案的板端输入是 NV12,不使用 RGB Runtime 的 pixel - 128 写入逻辑。
预处理必须统一以下规则:
- LetterBox 缩放和填充规则与校准、评测一致。
- 输出尺寸为
640×640;NV12 宽高必须满足相应对齐要求。 - BGR/RGB 到 YUV 的色彩矩阵和 UV 排列与模型 Runtime 输入一致。
- 区分 NV12(UV 交错)和 NV21(VU 交错)。
- 按 Tensor 的
validShape、alignedShape和 stride 写入,不能默认数据紧密排列。
建议保留的接口:
struct LetterBoxInfo {
float scale;
int pad_left;
int pad_top;
int input_width;
int input_height;
int original_width;
int original_height;
};
bool LetterBoxToNV12(
const cv::Mat& bgr,
int dst_width,
int dst_height,
std::vector& nv12,
LetterBoxInfo& info);
NV12 数据大小通常为:
const size_t nv12_size = width * height * 3 / 2;
实际写入大小和分平面方式必须以 HBM 输入 Tensor 属性及 SDK API 约定为准。
7.3 模型加载与 Tensor 内存
模型加载和属性查询顺序:
hbDNNInitializeFromFiles
→ hbDNNGetModelNameList
→ hbDNNGetModelHandle
→ hbDNNGetInputCount
→ hbDNNGetOutputCount
→ hbDNNGetInputTensorProperties
→ hbDNNGetOutputTensorProperties
不要只为第 0 个输出申请内存。应按实际 output_count 遍历:
std::vector output_tensors(output_count);
for (int i = 0; i < output_count; ++i) {
hbDNNGetOutputTensorProperties(
&output_tensors[i].properties,
dnn_handle,
i);
const int bytes = TensorMemSize(output_tensors[i].properties);
hbUCPMallocCached(&output_tensors[i].sysMem, bytes, 0);
}
TensorMemSize 应根据实际数据类型、对齐 Shape 和 SDK 的 Tensor 属性计算。
7.4 BPU 推理调度
关键调用顺序:
hbUCPMemFlush(&input_tensor.sysMem, HB_SYS_MEM_CACHE_CLEAN);
hbUCPTaskHandle_t task_handle = nullptr;
hbDNNInferV2(
&task_handle,
output_tensors.data(),
input_tensors.data(),
dnn_handle);
hbUCPSchedParam sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&sched_param);
sched_param.backend = HB_UCP_BPU_CORE_ANY;
hbUCPSubmitTask(task_handle, &sched_param);
hbUCPWaitTaskDone(task_handle, 0);
for (auto& tensor : output_tensors) {
hbUCPMemFlush(
&tensor.sysMem,
HB_SYS_MEM_CACHE_INVALIDATE);
}
正式代码需要检查每个 API 的返回值,并在失败路径释放已经申请的资源。
7.5 Raw6 CPU 后处理
统一后处理流程:
六路原始输出
→ 回归分支按 4×16 重排
→ 对每个方向执行 DFL Softmax
→ 用 [0,1,...,15] 加权得到 l/t/r/b 距离
→ 根据当前尺度 Anchor Point 和 stride 解码边界框
→ 分类分支执行 Sigmoid
→ 合并 P3/P4/P5 候选框
→ 置信度筛选
→ 按类别 NMS
→ LetterBox 坐标还原
三个尺度的 stride 通常为:
constexpr int kStrides[3] = {8, 16, 32};
constexpr int kRegMax = 16;
核心接口建议:
struct Detection {
cv::Rect2f box;
float score;
int class_id;
};
std::vector PostprocessRaw6(
const std::vector& outputs,
const LetterBoxInfo& letterbox,
float confidence_threshold,
float nms_threshold);
输出绑定不应只依赖数组位置。建议同时校验名称、Shape 和通道数,建立 p3_box 到 p5_cls 的明确映射。
坐标还原:
x_original = (x_input - pad_left) / scale;
y_original = (y_input - pad_top) / scale;
还原后需要裁剪到原图边界。
7.6 可视化和资源释放
可视化只保留必要调用:
cv::rectangle(image, box, color, 2);
cv::putText(image, label, origin, cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 1);
cv::imwrite(output_path, image);
资源释放顺序:
hbUCPReleaseTask(task_handle);
for (auto& tensor : input_tensors) {
hbUCPFree(&tensor.sysMem);
}
for (auto& tensor : output_tensors) {
hbUCPFree(&tensor.sysMem);
}
hbDNNRelease(packed_dnn_handle);
完整实现建议拆分为:
src/main.cpp
src/yolov8_raw6_postprocess.cpp
include/yolov8_raw6_postprocess.h
CMakeLists.txt
toolchain_aarch64.cmake
7.7 交叉编译与运行
原环境使用的交叉编译器:
/arm-gnu-toolchain-12.2.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-g++
建议使用单层构建目录:
cmake -S . -B build_aarch64 \
-DCMAKE_TOOLCHAIN_FILE=toolchain_aarch64.cmake \
-DDEPS_ROOT=/workspace/projects/j6m/horizon_j6_open_explorer_v3.8.1-py310_20260326/samples/ucp_tutorial/deps_aarch64
cmake --build build_aarch64 -j4
拷贝程序、模型和测试数据:
scp build_aarch64/run_yolov8s_raw6 \
root@192.168.0.65:/userdata/xy/yolov8s_raw6/
scp model_output_raw6_int8/yolov8s_raw6_640x640_nv12.hbm \
root@192.168.0.65:/userdata/xy/yolov8s_raw6/
板端运行示例:
cd /userdata/xy/yolov8s_raw6
./run_yolov8s_raw6 \
--model_file ./yolov8s_raw6_640x640_nv12.hbm \
--image_file ./test.jpg \
--output_file ./result.jpg \
--conf_thres 0.25 \
--nms_thres 0.45
动态库目录应根据开发板 BSP 和实际部署目录配置,不要照搬其他项目的 LD_LIBRARY_PATH。
8. 踩坑记录
本节记录早期默认单输出 RGB Runtime 模型的排查过程。当前正式方案已经改为 Raw6 六路输出和 NV12 Runtime,因此本节中的输入和后处理实现不能直接用于最终模型。
8.1 默认 ONNX 包含 DFL 和坐标解码
现象
默认导出的 ONNX 输出为 [1,84,8400],图中包含 DFL、坐标解码、拼接等操作;量化后精度下降明显或难以逐层定位。
排查过程
对比 PyTorch、默认 ONNX 和量化输出,确认差异集中在检测头末端及单路输出。
根因
默认导出形式把对量化较敏感的解码和拼接留在模型图中,同时失去了逐尺度、逐分支对齐能力。
修改方式
修改 Detect Head,导出 P3/P4/P5 的 box 和 cls 六路原始输出;DFL、Anchor 解码和 NMS 移到 CPU。
修改前后结果
修改前:单输出 [1,84,8400]
修改后:六路 Raw6 输出,Shape 见第 2.3 节
对当前方案的影响
当前 Raw6 后处理不得再按 [1,84,8400] 解析。
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 HBM 查询结果表现为 NHWC INT8 输入。直接把 uint8 像素强制转换为 int8 后,板端结果与 ONNX 不一致。
排查过程
确认 LetterBox 参数一致后,比较原始写入和 pixel - 128 写入。旧模型修正后,单图类别和置信度更接近 ONNX。
根因
该旧模型使用 RGB INT8 Runtime 输入约定,板端应把 0~255 映射到 -128~127,不能直接发生溢出式转换。
修改方式
旧 RGB Runtime 模型使用:
dst[index] = static_cast(
static_cast(pixel) - 128);
修改前后结果
原笔记中的单图样例在修改后基本对齐,但没有提供完整数据集指标。
对当前方案的影响
该问题来自早期的默认单输出 RGB Runtime 模型,不适用于当前 Raw6 + NV12 最终方案。
正式 NV12 输入流程中不要加入 pixel - 128。
8.4 Raw6 输出顺序和 Shape 对齐
现象
六路 Tensor 数量正确,但解码结果异常,常见表现为框尺度错误、类别置信度异常或完全无框。
排查过程
分别打印六路输出的名称、Shape、数据类型和数值范围,并与 ONNX Runtime 输出逐路比较。
根因
可能原因包括:
- 把 cls 分支当作 box 分支。
- P3/P4/P5 顺序错误。
- NCHW/NHWC 解释错误。
- 使用
validShape访问对齐后的 Tensor 时忽略 stride。 - 忽略输出量化类型或反量化参数。
修改方式
按名称、Shape 和通道数建立显式映射;以 hrt_model_exec model_info 和 Tensor 属性查询结果为准。
修改前后结果
修改前:按固定数组下标盲目解析
修改后:校验名称 + Shape + 类型后绑定六路输出
对当前方案的影响
Raw6 后处理必须在启动阶段验证全部六路输出,验证失败应立即终止推理。
8.5 校准前处理与 YAML 不一致
现象
编译可以完成,但 calibrated、quantized 或 HBM 阶段精度突然下降。
排查过程
检查校准文件的颜色、Shape、dtype、数值范围,并逐项核对 input_type_train、input_layout_train、mean_value、scale_value 和 std_value。
根因
常见问题包括 BGR/RGB 颠倒、HWC/CHW 颠倒、重复 /255、没有 /255、LetterBox 规则不同或校准样本不具代表性。
修改方式
统一校准输入为:
RGB + CHW + float32 + [0,1]
并用同一预处理函数驱动 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 调用顺序
hbDNNInitializeFromFiles
→ hbDNNGetModelNameList
→ hbDNNGetModelHandle
→ 查询输入输出数量和 Tensor 属性
→ hbUCPMallocCached
→ 写入 NV12 输入
→ hbUCPMemFlush(CLEAN)
→ hbDNNInferV2
→ 初始化调度参数
→ hbUCPSubmitTask
→ hbUCPWaitTaskDone
→ hbUCPMemFlush(INVALIDATE)
→ 读取六路输出并执行 Raw6 后处理
→ hbUCPReleaseTask
→ hbUCPFree
→ hbDNNRelease
职责划分:
DNN API:模型、Tensor 属性、推理任务创建
UCP API:内存、Cache、任务提交与等待
OpenCV API:图像预处理、NMS、绘制和保存
10. 最终总结
本次部署的固定主线是:
yolov8s.pt
→ yolov8s_raw6.onnx
→ RGB NCHW FP32 校准
→ PTQ INT8
→ yolov8s_raw6_640x640_nv12.hbm
→ NV12 板端输入
→ 六路 Raw6 输出
→ CPU 完成 DFL、Anchor 解码、Sigmoid、NMS 和坐标还原
部署验收必须同时满足:
- Raw6 ONNX 与 PyTorch 精度对齐。
- optimized、calibrated、quantized 各阶段精度变化可解释。
- HBM 使用实际 NV12 链路评测且满足项目精度阈值。
- 板端程序根据实际 Tensor 属性处理 Y、UV 两个输入 Tensor 和六路 FP32 输出。
- 延迟、FPS、CPU/BPU 占用和内存满足项目要求。
- 所有评测均记录数据集、阈值、工具版本和运行条件。