From 8a5162c912bbe1aa4bdf28d826d509aa885bfb5e Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Mon, 17 Aug 2026 14:48:26 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E8=AE=A2=20Hailo=20=E9=A2=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BC=98=E5=8C=96=E6=96=B9=E6=A1=88=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=9F=A5=E8=A1=A8=E6=B3=95=E5=AE=9E=E6=B5=8B=E8=AF=81?= =?UTF-8?q?=E4=BC=AA=EF=BC=8C=E6=94=B9=E5=90=88=E5=B9=B6=E4=B9=98=E5=8A=A0?= =?UTF-8?q?=E4=B8=BA=E4=B8=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原型实测:numpy 高级索引查表无加速(26.6 vs 28.5ms),预期 18-20ms 不成立。 主方案改为合并系数乘加(-5ms,0 LSB 等价),cv2.LUT 列为可选激进路径; 预期收益修正为 ~47ms(纯 numpy)/ ~25-30ms(cv2)。 --- ...o_preprocess_optimization_plan_20260817.md | 107 ++++++++++++------ 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md b/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md index dd37cbe..66720a8 100644 --- a/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md +++ b/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md @@ -1,12 +1,19 @@ -# actpred Hailo 推理预处理优化方案:查表法合并归一化与量化 +# actpred Hailo 推理预处理优化方案(修订版):合并归一化与量化 > 文档类型:开发方案 > 模块:actpred(部署链 hailo_infer.py) > 作者:自动生成 -> 日期:2026-08-17 +> 日期:2026-08-17(原型实测修订) > 前置:`actpred_inference_speed_benchmark_20260813.md`(速度基准)、 > 2026-08-17 新 640×480 数据集重测(本方案 §2 数据) +> ⚠️ **修订说明(2026-08-17)** +> 初版以"numpy 查表法"为核心方案,预期端到端降至 18-20ms。 +> 原型实测(§3.4)证明 **numpy 高级索引查表在 Pi5 上无加速**(fancy +> indexing 的 gather 开销与逐元素 float 运算相当),该预期不成立。 +> 本修订版改以**合并系数乘加**(纯 numpy,-5ms)为主方案, +> **cv2.LUT**(需引入 opencv 依赖)列为可选的激进路径。 + --- ## 1. 背景与动机 @@ -23,7 +30,7 @@ 输入尺寸已与模型一致(resize 变 no-op),**预处理仍是端到端延迟的主导项**(68%)。 旧基准报告(2026-08-13)已记录同类问题:1080p 采集的预处理缩放约 45ms 主导端到端延迟, 当时通过把默认分辨率改回 640×480 缓解;本次分析证明 640×480 下 Python 端预处理 -仍有约 36ms 的优化空间。 +仍有约 36ms 的优化空间——但空间比初版估计小(见 §3.4 实测)。 ## 2. 瓶颈分析(100 次均值分段计时) @@ -36,71 +43,97 @@ **关键冗余**:当前流程先做 float 归一化(`_frame_float`),随后又量化回 uint8(`_bind_frames`)—— 中间 float 帧是纯中间态,归一化与量化可以**合并为一步**。 -## 3. 优化方案:查表法(LUT)合并归一化+量化 +## 3. 优化方案 -### 3.1 核心思路 +### 3.1 主方案:合并系数乘加(纯 numpy,无新依赖) HEF 帧输入是 uint8 量化值:`q = round(((x/255 - mean)/std) / q_scale + q_zp)`。 -对 uint8 原始像素 x∈[0,255],该映射是**单变量函数**——预计算 256 项查表: +把 4 步运算合并为**一次乘加**(系数预计算,按通道): ``` -lut[c] = clip(round(((c/255 - mean[c])/std[c]) / q_scale + q_zp), 0, 255) # c = 0..255 +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 ``` -推理时对每个通道:`out = lut[x]`(numpy 高级索引,纯内存搬运 + 索引,无 float 运算)。 +- 现流程 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` -- 数学上与现流程完全等价(仅合并了中间 float 步骤,取整路径不变) -- 三张表(RGB 通道 mean/std 不同),HEF 的 `quant_info`(qp_scale/qp_zp)每输入一个 -- 表随 HEF 加载时构建(`_normalize_frames` 判定负零偏后决定走查表还是直通) +### 3.2 附带优化(与主方案正交) -### 3.2 附带优化 - -1. **免 PIL 路径**:输入已是 np.ndarray 时直接用 `lut[x]` 索引,跳过 - `Image.fromarray/convert`;仅需 resize 时(尺寸不一致)才走 PIL +1. **免 PIL 路径**:输入已是 np.ndarray 时直接运算,跳过 `Image.fromarray/convert` 2. **resize no-op 跳过**:`image.size == (W,H)` 时不调用 `resize()` -3. 直通模式(`_normalize_frames=False`,原始像素校准的 HEF)同样受益: - `out = clip(round(x/q_scale+q_zp))` 也是查表 +3. **缓冲复用**:`np.multiply/np.add` 带 `out=` 参数,减少临时数组分配 -### 3.3 预期收益 +### 3.3 可选激进路径:cv2.LUT(需引入 opencv 依赖) -| 阶段 | 现状 | 优化后(估) | -|---|---|---| -| 帧预处理(float+量化) | 27.2 + 9.1 ms | **~3-6 ms**(三帧查表 + 绑定) | -| 端到端 predict | 52.4 ms | **~18-20 ms** | +`cv2.LUT(img, lut)` 是 C 实现的 uint8→uint8 查表,单次遍历,无 float 中间态—— +理论上可把帧预处理降到 ~3ms。但当前环境 **cv2 未安装**;Pi5 arm64 上 opencv-python +安装包较重,是否引入需权衡(见 §7)。 -闭环 10Hz(100ms 预算)下占用率从 52% 降至 ~20%,为 20Hz 控制率留出空间。 +### 3.4 原型实测评估(2026-08-17,100 次均值,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 | —(cv2 未安装) | 理论 ~3ms | — | + +**结论**: +- numpy fancy indexing 的 gather 开销与逐元素 float 运算相当,查表法**无收益** +- 纯 numpy 可达的最佳是合并乘加(-5ms),受限于 92 万像素 float32 数组的内存吞吐 +- 要数量级提升只能靠 cv2.LUT(C 查表),代价是引入 opencv 依赖 + +### 3.5 预期收益(修订) + +| 路径 | 帧预处理 | 端到端 predict | 依赖 | +|---|---|---|---| +| 现状 | 35.9 ms | 52.4 ms | — | +| 主方案(合并乘加) | ~31 ms | **~47 ms** | 无 | +| 激进路径(cv2.LUT) | ~13 ms | **~25-30 ms** | opencv-python | + +闭环 10Hz(100ms 预算)下,主方案占用率 52%→47%;激进路径降至 ~28%,为 20Hz 留出空间。 ## 4. 一致性约束(关键) - **训练侧不动**:`dataset.py` 的 `Resize+ToTensor+Normalize` 保持 float 路径—— - 查表只是推理侧对"归一化+量化"的合并实现,数值等价(±1 LSB 取整差) + 合并乘加只是推理侧对"归一化+量化"的合并实现,数值等价(实测 0 LSB) - **三后端口径**:`predict.py`(PyTorch,float 输入不量化)、`onnx_infer.py`(ORT,float) - 不受影响;查表仅作用于 `hailo_infer.py` 的 uint8 量化输入路径 + 不受影响;优化仅作用于 `hailo_infer.py` 的 uint8 量化输入路径 - **帧输入判定不变**:`_normalize_frames`(量化网格负零偏判定)决定走 - 归一化查表还是直通查表,逻辑与现状一致 -- 旧 5 输入 / 旧版单图像 HEF 布局同样适用(按各自 quant_info 建表) + 归一化还是直通,逻辑与现状一致 +- 旧 5 输入 / 旧版单图像 HEF 布局同样适用(按各自 quant_info 算系数) ## 5. 实施步骤 | 步骤 | 内容 | 产出 | |---|---|---| -| 1 | `hailo_infer.py`:加载时构建 `_frame_luts`(按通道 × 256,归一化/直通两种) | 查表构建 + 单测(256 表项与 float 路径逐项比对 ≤1 LSB) | -| 2 | `_frame_float` / `_bind_frames` 改为查表路径(ndarray 直通、PIL 仅 resize 时用) | 推理链路改造 + 单测(与现 float 路径输出逐帧比对) | -| 3 | NPU 实测:新 640×480 数据集 100 次均值 + 分段计时 | 基准对比(52.4 → ~20ms) | +| 1 | `hailo_infer.py`:加载时按 quant_info 预计算通道系数 `A/B`(归一化/直通两种) | 系数构建 + 单测(与 float 路径逐点比对 = 0 LSB) | +| 2 | `_frame_float` / `_bind_frames` 改为合并乘加路径(ndarray 直通、免 PIL、resize no-op 跳过) | 推理链路改造 + 单测(逐帧输出比对) | +| 3 | NPU 实测:新 640×480 数据集 100 次均值 + 分段计时 | 基准对比(52.4 → ~47ms) | | 4 | `_npu_test.py` 4 角 + HEF vs torch 对比复验 | 量化误差不回归(≤1°) | | 5 | 全套单测(TestHailoPredictor 等) | 188 tests OK | +| 6(可选) | 引入 cv2.LUT 路径(配置开关,默认关闭) | 激进路径 + 依赖权衡记录 | ## 6. 风险与对策 | 风险 | 对策 | |---|---| -| 查表取整与 float 路径存在 ±1 LSB 差异 | 数值等价性单测:表项逐点比对,差异仅取整边界;NPU 输出 vs torch 误差预算不变(≤1°) | -| 非 256 深度的特殊量化网格 | 表仅适用于 8-bit 输入(HEF 帧输入恒 uint8,`FormatType.UINT8` 分支已断言) | -| 未来输入尺寸变更 | 查表与尺寸无关(逐像素映射);resize 仍需走 PIL,仅影响附带优化 2 | +| 乘加系数浮点误差 | 实测 0 LSB(系数用 float32 与现路径同精度);NPU 输出 vs torch 误差预算不变(≤1°) | +| 主方案收益有限(仅 -5ms) | 如实记录(§3.4/§3.5);若需数量级提升走 cv2.LUT,否则可接受现状不优化 | +| cv2 依赖体积/安装 | 激进路径默认关闭、配置开关;Pi arm64 opencv 安装成本单独评估 | +| 未来输入尺寸变更 | 系数与尺寸无关(逐像素映射);resize 仍需走 PIL,仅影响附带优化 2 | ## 7. 决策记录 -- **查表法 vs 直接 float 合并**:`x*a+b` 一次乘加也能省一步,但仍是 92 万像素×3 通道的 - float 运算(Pi5 上约 10-15ms);查表把该运算降为内存索引(预计 1-3ms),且同时覆盖直通路径 -- **训练侧不改**:训练用 float 归一化(增强/数值精度),推理侧查表——两侧口径在 - "归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.5° 实测余量充足) +- **查表法(初版核心)实测证伪**:numpy 高级索引 gather 在 Pi5 上与逐元素 float + 运算同速(26.6 vs 28.5ms),"查表降为内存索引即快"的假设不成立——教训: + 性能方案须原型实测,不能凭直觉估计 +- **主方案选合并乘加**:纯 numpy 无新依赖、数值 0 LSB 等价、实施简单;-5ms 收益 + 虽小但零风险。激进 cv2.LUT 留作可选(需权衡 opencv 依赖) +- **是否优化的取舍**:现状 52.4ms 已满足 10Hz 闭环;若控制率需求不上探 20Hz, + 可仅做主方案(低成本)甚至不优化。激进路径仅在明确需要更高控制率时启用 +- **训练侧不改**:训练用 float 归一化(增强/数值精度),推理侧合并实现——两侧 + 口径在"归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.5° 余量充足)