Files
blackbird/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md
T
qing 2b133f1531 docs: cv2.LUT 优化验收测试报告 + 方案文档实测数据更新
200 次均值端到端 16.1ms(3.3×),分段 3.0/12.5/0.6ms——瓶颈已转移至
NPU 本身;一致性 7 类输入 + 8 seed 随机全 0 差异;10Hz 预算占用 16%。
2026-08-17 15:28:15 +08:00

8.5 KiB
Raw Blame History

actpred Hailo 推理预处理优化:cv2.LUT 查表合并归一化与量化(已实施)

文档类型:开发方案(已实施) 模块:actpred(部署链 hailo_infer.py) 作者:自动生成 日期:2026-08-17(原型实测 + 集成实施) 前置:actpred_inference_speed_benchmark_20260813.md(速度基准)、 2026-08-17 新 640×480 数据集重测(本方案 §2 数据)

📌 版本历程

  1. 初版:numpy 查表法为核心,预期端到端 18-20ms
  2. 原型实测(§3.4):numpy 高级索引查表在 Pi5 上无加速(fancy indexing 的 gather 开销与逐元素 float 运算相当),预期不成立; 合并系数乘加仅 -5ms;cv2.LUT 实测 15.8× 加速(§3.4)
  3. 已实施(§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 附带优化(与主方案正交)

  1. 免 PIL 路径:输入已是 np.ndarray 时直接运算,跳过 Image.fromarray/convert
  2. resize no-op 跳过:image.size == (W,H) 时不调用 resize()
  3. 缓冲复用: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° 余量充足)