矩阵 Resize 优化:Intel oneAPI 前后处理加速

2023 年 8 月 20 日 星期日
/ , , , , ,
4
摘要
背景复原模型的 CPU 前后处理加速。Intel oneAPI + IPP + TBB,通过消除 computeImg 独立遍历、融合到上采样步骤,21004μs → 13015μs。

矩阵 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 是单独一次全图遍历。但实际上 fgrbgrmask1 的上采样本身就要遍历输出图像,完全可以把逐像素乘加融合进去。这一刀直接把 computeImg 从 9135μs 压到 0——不是真的不计算,而是并入了上采样步骤。

2. 用 IPP 替代 OpenCV resize

Intel IPP 的 ippiResize 在 x86 上有更好的 SIMD 利用率。对双线性插值这种密集计算,替换后 upSample 从 ~5500μs 压到更低。

3. TBB 并行化

xref 的预处理互相独立,可以并行:

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 拷贝可能抵消收益

优化效果

版本splitTransposeupSamplecomputeImgthresholdMaskcomputeOutTotal
旧版 (ori)672 μs5,346 μs9,135 μs1,323 μs4,526 μs21,004 μs
新版 (mod)653 μs5,553 μs0 μs1,432 μs5,376 μs13,015 μs

Total 从 21ms 压到 13ms,大约 38% 的提升。最大的变化就是消除了 computeImg 的独立遍历。

工程经验

  1. 性能分析要拆到步骤级别。如果只看总耗时 23ms,很容易觉得"CPU 就是这样",但实际上一个步骤占了 43% 的时间。
  2. 最有效的优化往往不是让某个操作更快,而是消除这个操作——把计算融合到已经绕不开的遍历里。
  3. IPP 和 TBB 这类库的价值不只是"快",而是它们已经替你把 SIMD、线程池、cache 这些事情处理好了。除非有非常特殊的数据布局,否则手写 kernel 很难超过。
  4. 内存重排(chw ↔ hwc)是图像处理里最容易忽略的开销。设计 pipeline 时就应该让数据布局尽量一致,而不是每条边都转换一次。

这个模块最终没有和 CUDA 去雾主链路合并——它服务的是另一个背景复原模型——但优化思路是一脉相承的:把重复计算找出来,合并到绕不开的遍历里,用成熟的矢量化库而不是手写 SIMD。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...