矩阵 Resize 优化:Intel oneAPI 前后处理加速
编写时间:2023-08
背景复原管线里的 CPU 前后处理模块。任务很具体:对模型输入做下采样和格式转换,对模型输出做上采样、通道拆分和逐像素混合,全程只在 CPU 上跑。优化目标是把 1080P 单帧控制在 25ms 以内,不能拖模型推理的后腿。
问题定义
背景复原模型输入两帧(当前帧 x 和参考帧 ref),输出是一张融合后的去干扰图。模型本身跑在 GPU 上,但前后处理在 CPU 侧:
预处理的工作是:uchar → float、值域 [0,255] → [0,1]、hwc → chw、双线性下采样。后处理的工作是反向再做一遍,但还多了通道拆分和逐像素混合。
后处理的计算链
模型输出是 7 通道,需要拆成三部分参与运算:
核心直觉是:模型输出的是"前景修正量"和"背景修正量",mask 决定每个像素更信哪一个。threshold 控制的是:只有模型足够确信的区域,才直接保留原图 x,否则用融合结果。
初版性能瓶颈
初版使用 OpenCV 做 resize 和类型转换,在 i5-10500 上单帧约 23ms。拆开各步骤耗时:
一眼就能看出来:computeImg(逐像素乘加)和 upSample(上采样)占了近 70% 的时间。computeImg 尤其可疑——它只是浮点乘加和 clip,不应该比 resize 还慢。
优化路径
1. 消除 computeImg 的独立遍历
初版里 computeImg 是单独一次全图遍历。但实际上 fgr、bgr、mask1 的上采样本身就要遍历输出图像,完全可以把逐像素乘加融合进去。这一刀直接把 computeImg 从 9135μs 压到 0——不是真的不计算,而是并入了上采样步骤。
2. 用 IPP 替代 OpenCV resize
Intel IPP 的 ippiResize 在 x86 上有更好的 SIMD 利用率。对双线性插值这种密集计算,替换后 upSample 从 ~5500μs 压到更低。
3. TBB 并行化
x 和 ref 的预处理互相独立,可以并行:
auto future_x = std::async(process, x, x_s);
auto future_ref = std::async(process, ref, ref_s);
this->x_mat = future_x.get();
this->ref_mat = future_ref.get();后处理里的通道拆分虽然依赖同一个 model_out,但三个通道组(0:3, 3:6, 6)的处理可以流水线化。
4. 内存布局优化
初版最大的隐性开销是格式转换。模型输出是 chw 排列,OpenCV Mat 是 hwc 排列,每次 split + merge 都在做隐式的内存重排。优化方向是尽量减少中间 Mat:
5. 其他考虑
| 方向 | 判断 |
|---|---|
| AVX 向量化 | IPP 内部已做,不需要手写 |
| 循环展开 | 编译器在 -O2 下通常做得更好 |
| cache blocking | 对 1080P 有用,但 IPP 内部已处理 |
| 双线性换最邻近 | 精度损失太大,不适合此场景 |
| 核显 offload | 可以尝试,但引入 PCIe 拷贝可能抵消收益 |
优化效果
| 版本 | splitTranspose | upSample | computeImg | thresholdMask | computeOut | Total |
|---|---|---|---|---|---|---|
| 旧版 (ori) | 672 μs | 5,346 μs | 9,135 μs | 1,323 μs | 4,526 μs | 21,004 μs |
| 新版 (mod) | 653 μs | 5,553 μs | 0 μs | 1,432 μs | 5,376 μs | 13,015 μs |
Total 从 21ms 压到 13ms,大约 38% 的提升。最大的变化就是消除了 computeImg 的独立遍历。
工程经验
- 性能分析要拆到步骤级别。如果只看总耗时 23ms,很容易觉得"CPU 就是这样",但实际上一个步骤占了 43% 的时间。
- 最有效的优化往往不是让某个操作更快,而是消除这个操作——把计算融合到已经绕不开的遍历里。
- IPP 和 TBB 这类库的价值不只是"快",而是它们已经替你把 SIMD、线程池、cache 这些事情处理好了。除非有非常特殊的数据布局,否则手写 kernel 很难超过。
- 内存重排(chw ↔ hwc)是图像处理里最容易忽略的开销。设计 pipeline 时就应该让数据布局尽量一致,而不是每条边都转换一次。
这个模块最终没有和 CUDA 去雾主链路合并——它服务的是另一个背景复原模型——但优化思路是一脉相承的:把重复计算找出来,合并到绕不开的遍历里,用成熟的矢量化库而不是手写 SIMD。