200 次均值端到端 16.1ms(3.3×),分段 3.0/12.5/0.6ms——瓶颈已转移至 NPU 本身;一致性 7 类输入 + 8 seed 随机全 0 差异;10Hz 预算占用 16%。
8.5 KiB
actpred Hailo 推理预处理优化:cv2.LUT 查表合并归一化与量化(已实施)
文档类型:开发方案(已实施) 模块:actpred(部署链 hailo_infer.py) 作者:自动生成 日期:2026-08-17(原型实测 + 集成实施) 前置:
actpred_inference_speed_benchmark_20260813.md(速度基准)、 2026-08-17 新 640×480 数据集重测(本方案 §2 数据)
📌 版本历程
- 初版:numpy 查表法为核心,预期端到端 18-20ms
- 原型实测(§3.4):numpy 高级索引查表在 Pi5 上无加速(fancy indexing 的 gather 开销与逐元素 float 运算相当),预期不成立; 合并系数乘加仅 -5ms;cv2.LUT 实测 15.8× 加速(§3.4)
- 已实施(§5):
hailo_infer.py集成 cv2.LUT 快路径(可选依赖, 无 cv2 自动回退 float 路径),端到端 52.4 → 16.0 ms,输出与 float 路径逐点一致(max_diff = 0)
1. 背景与动机
2026-08-17 用新录制的 640×480 数据集(免 resize 训练样本)重测 HEF 推理速度:
| 阶段 | 640×480 输入 | 224×224 输入(旧数据) |
|---|---|---|
| 端到端 predict | 52.4 ms | 60.0 ms |
| 预处理 | 35.9 ms | 43.1 ms |
| NPU run | 12.5 ms | 12.9 ms |
| 后处理 | 0.6 ms | 0.6 ms |
输入尺寸已与模型一致(resize 变 no-op),预处理仍是端到端延迟的主导项(68%)。 旧基准报告(2026-08-13)已记录同类问题:1080p 采集的预处理缩放约 45ms 主导端到端延迟, 当时通过把默认分辨率改回 640×480 缓解;本次分析证明 640×480 下 Python 端预处理 仍有约 36ms 的优化空间——但空间比初版估计小(见 §3.4 实测)。
2. 瓶颈分析(100 次均值分段计时)
| 子步骤 | 耗时 | 说明 |
|---|---|---|
_frame_float(3 帧) |
27.2 ms | PIL fromarray/convert + resize(同尺寸 no-op 仍有开销)+ (x/255-mean)/std float 运算(92 万像素×3 通道) |
create_bindings + 帧量化绑定 |
9.1 ms | _bind_frames:float 归一化帧 → round(x/q.scale+q.zp) 再量化回 uint8 |
| history/pred 绑定 | 0.07 ms | 可忽略 |
关键冗余:当前流程先做 float 归一化(_frame_float),随后又量化回 uint8(_bind_frames)——
中间 float 帧是纯中间态,归一化与量化可以合并为一步。
3. 优化方案
3.1 主方案:合并系数乘加(纯 numpy,无新依赖)
HEF 帧输入是 uint8 量化值:q = round(((x/255 - mean)/std) / q_scale + q_zp)。
把 4 步运算合并为一次乘加(系数预计算,按通道):
q = round(x * A[c] + B[c])
A[c] = 1 / (255 * std[c] * q_scale)
B[c] = -mean[c] / (std[c] * q_scale) + q_zp
- 现流程 4 次全数组运算(
/255、-mean、/std、/q_scale+q_zp)→ 1 次乘加 - 数值完全等价(实测 max_diff = 0 LSB,见 §3.4)
- 直通模式(
_normalize_frames=False)同理:A=1/q_scale, B=q_zp
3.2 附带优化(与主方案正交)
- 免 PIL 路径:输入已是 np.ndarray 时直接运算,跳过
Image.fromarray/convert - resize no-op 跳过:
image.size == (W,H)时不调用resize() - 缓冲复用:
np.multiply/np.add带out=参数,减少临时数组分配
3.3 已实施快路径:cv2.LUT(可选依赖,无 cv2 自动回退)
cv2.LUT(img, lut) 是 C 实现的 uint8→uint8 查表,单次遍历,无 float 中间态——
把"归一化+量化"合并为一步。集成要点(hailo_infer.py):
- 惰性导入 cv2(
HAS_CV2);无 cv2 或非逐帧输入布局(ncfhw/nhwc_fused)自动回退 float 路径 - 加载时按每帧
quant_info构建 256 项表(归一化/直通两种,_normalize_frames判定) - OpenCV 5.0 的 cv2.LUT 只接受单通道表((256,3) 会断言失败),逐通道应用后
cv2.merge - 输入尺寸与模型不一致时先 PIL resize(仅此情况走 PIL)
- 依赖安装:
opencv-python-headless(5.0.0,清华镜像;官方源过慢)- 注:本方案实测后发现合并系数乘加(§3.1)未实施——cv2.LUT 收益已远超, 避免冗余代码;§3.1 保留作为无 cv2 环境下的潜在备选
3.4 原型实测评估(2026-08-17,100-200 次均值,640×480×3 帧)
| 方案 | 3 帧耗时 | 对比现 float 28.5ms | 数值差异 |
|---|---|---|---|
| 现 float 路径 | 28.5 ms | 基线 | — |
| numpy 高级索引查表 | 26.6 ms | 无加速(1.0x) | 0 LSB |
| 合并系数乘加 | 23.2 ms | 快 19%(-5ms) | 0 LSB |
| cv2.LUT(逐通道) | 1.68 ms | 快 15.8× | 0 LSB |
结论:
- numpy fancy indexing 的 gather 开销与逐元素 float 运算相当,查表法无收益
- 纯 numpy 可达的最佳是合并乘加(-5ms),受限于 92 万像素 float32 数组的内存吞吐
- cv2.LUT 的 C 查表把帧预处理降一个数量级,且数值完全等价(0 LSB)
3.5 实测收益(已实施,集成后 NPU 实测)
| 指标 | 优化前 | 优化后(cv2.LUT) |
|---|---|---|
| 帧预处理(3 帧) | 26.5 ms | 3.0 ms(分段实测含绑定) |
| 端到端 predict | 52.4 ms | 16.1 ms(3.3×)(200 次均值,见测试报告) |
| 输出一致性 | — | 与 float 路径 max_diff = 0.0000(7 类输入 + 8 seed 随机) |
| HEF vs torch | 0.50° | 0.30°(4 角复验 PASS) |
| vs PyTorch CPU | 2.1× | 6.7×(16.1 vs 107.9 ms) |
闭环 10Hz(100ms 预算)占用率 52% → 16%,20Hz 32%,30Hz(33ms)48%。
完整验收数据见
actpred_cv2_lut_benchmark_report_20260817.md(测试报告)。
4. 一致性约束(关键)
- 训练侧不动:
dataset.py的Resize+ToTensor+Normalize保持 float 路径—— 合并乘加只是推理侧对"归一化+量化"的合并实现,数值等价(实测 0 LSB) - 三后端口径:
predict.py(PyTorch,float 输入不量化)、onnx_infer.py(ORT,float) 不受影响;优化仅作用于hailo_infer.py的 uint8 量化输入路径 - 帧输入判定不变:
_normalize_frames(量化网格负零偏判定)决定走 归一化还是直通,逻辑与现状一致 - 旧 5 输入 / 旧版单图像 HEF 布局同样适用(按各自 quant_info 算系数)
5. 实施步骤(已完成 ✅)
| 步骤 | 内容 | 状态 |
|---|---|---|
| 1 | hailo_infer.py:加载时按每帧 quant_info 构建 256 项 LUT(归一化/直通两种) |
✅ _build_frame_luts |
| 2 | predict 的 nhwc_frames 分支走 cv2.LUT 快路径(尺寸不匹配时 PIL resize);无 cv2 回退 float |
✅ _frame_lut_quant + HAS_CV2 |
| 3 | NPU 实测:640×480 数据集 100 次均值 | ✅ 16.0 ms(52.4 → 16.0) |
| 4 | _npu_test.py 4 角 + HEF vs torch 对比复验 |
✅ PASS,0.30°(≤1° 预算) |
| 5 | 全套单测(TestHailoPredictor 等) | ✅ 188 tests OK |
| 6 | 依赖安装:opencv-python-headless 5.0.0(清华镜像) | ✅ venv 已装 |
6. 风险与对策
| 风险 | 对策 |
|---|---|
| 查表取整与 float 路径存在差异 | 实测 0 LSB(表项 float32 与现路径同精度);NPU 输出 vs float 路径 max_diff=0,vs torch 0.30° |
| 无 cv2 环境 | 惰性导入 + 自动回退 float 路径,行为与优化前完全一致 |
| OpenCV 5.0 多通道 LUT 断言失败 | 逐通道单表 + cv2.merge(实测无性能损失) |
| 非 nhwc_frames 布局(旧 HEF) | 仅该布局启用 cv2 路径,其余走原 float 路径 |
| 未来输入尺寸变更 | LUT 与尺寸无关(逐像素映射);尺寸不一致时才走 PIL resize |
7. 决策记录
- 查表法(初版核心)实测证伪:numpy 高级索引 gather 在 Pi5 上与逐元素 float 运算同速(26.6 vs 28.5ms),"查表降为内存索引即快"的假设不成立——教训: 性能方案须原型实测,不能凭直觉估计
- 最终方案选 cv2.LUT:C 实现查表把帧预处理从 26.5ms 降到 1.7ms(15.8×), 端到端 52.4 → 16.0ms;数值 0 LSB 等价。合并系数乘加(-5ms)收益被碾压, 不再实施(§3.1 保留为无 cv2 环境的备选)
- 依赖取舍:opencv-python-headless 作为可选依赖(惰性导入 + 自动回退), 有 cv2 环境收益 3.3×,无 cv2 环境行为不变——收益远大于依赖成本
- 训练侧不改:训练用 float 归一化(增强/数值精度),推理侧查表实现——两侧 口径在"归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.30° 余量充足)