读 ncnn 源码(XXXIII):激活融合——ConvDWDeconvInnerProduct 的“一体化”改造

第三十二篇中,我们深入剖析了 fuse_convolution_activation 这一至关重要的优化 Pass。其核心思想是:将 Conv -> Activation 这一内存密集型(Memory-bound)的两步操作,融合为 Conv 计算微核内部的一次计算密集型(Compute-bound)操作,从而消除昂贵的内存读写。

ncnn 将这一优化思想贯彻到底,并将其推广到了几乎所有可能后接激活层的计算密集型层。本篇,我们就将集中分析 fuse_convolutiondepthwise_activationfuse_deconvolution_activationfuse_deconvolutiondepthwise_activationfuse_innerproduct_activation 这四个 Pass,探究 ncnn 如何实现激活融合的全面覆盖。

TL;DR

  1. 目标: 将 ConvDW, Deconv, DeconvDW, InnerProduct 等层与其后紧跟的非线性激活层(ReLU, Clip, Sigmoid, Mish, HardSwish 等)进行融合。
  2. 原理与动机: fuse_convolution_activation 完全一致。这些层(ConvDW, Deconv 等)与 Convolution 一样,都是计算密集型层,其 forward 实现都会产生一个中间结果 blob。如果其后紧跟一个逐元素的激活层,就会产生一次冗余的“写入主存 -> 读回主存”操作。
  3. 融合机制: 采用离线参数交接 + 运行时微核融合的策略。
    • 离线 (ncnnoptimize): 遍历计算图,匹配 Layer -> Activation 模式。将 Activation 层的类型(如 ReLU, Clip)和参数(如 slope, min/max转移到前驱层(ConvDW, Deconv 等)的 activation_typeactivation_params 成员变量中。
    • 图结构修改: 将前驱层的 top 指向原 Activation 层的 top,并将 Activation 层标记为 "ncnnfused" 以便在推理时跳过。
  4. 运行时 (Inference): 各个层(ConvDW, Deconv 等)的 forward 实现(尤其是 SIMD 优化的微核)会检查 activation_type 标志。如果被设置,它们会在计算出结果后,不立即写回主存,而是在寄存器中直接应用对应的激活函数(max(0, x), clip(x, min, max) 等),最后只将激活后的最终结果写入主存。
  5. 代码实现: fuse_convolutiondepthwise_activation, fuse_deconvolution_activation 等函数的源码结构与 fuse_convolution_activation 几乎完全相同,只是将模式匹配的第一步从 "Convolution" 替换为了 "ConvolutionDepthWise", "Deconvolution" 等,展现了该优化逻辑的高度可复用性。

1. 激活融合:一项通用的性能优化

激活融合的核心价值在于消除内存带宽瓶颈。对于任何计算层 LL -> Activation 这种模式都会引入一次不必要的内存往返(L 写入 blobActivation 读出 blob)。

由于 Activation 都是逐元素的非线性操作,其计算量与 L(如卷积)相比微不足道。因此,最优解是将 Activation 的计算“塞入” L 的计算循环末尾,在数据尚在 CPU 寄存器或 L1 缓存中时就完成激活计算。

ncnn 正是抓住了这一通用原理,为所有主要的线性/计算密集型层都配备了激活融合的能力。


2. 源码分析:高度一致的融合逻辑

分析 fuse_convolutiondepthwise_activation, fuse_deconvolution_activation 等函数的源码,我们会发现它们遵循着完全相同的模式。我们以 fuse_convolutiondepthwise_activation 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int NetOptimize::fuse_convolutiondepthwise_activation()
{
const size_t layer_count = layers.size();
for (size_t i = 0; i < layer_count; i++)
{
// 1. 模式匹配:查找 ConvolutionDepthWise
if (layers[i]->type != "ConvolutionDepthWise")
continue;

int top_blob_index = layers[i]->tops[0];

// 2. 模式匹配:查找紧随其后的、可融合的 Activation
size_t j = i + 1;
for (; j < layer_count; j++)
{
// (支持的激活层列表)
if (layers[j]->type != "ReLU" && ... && layers[j]->type != "HardSwish")
continue;
if (layers[j]->bottoms.size() != 1) continue;
if (layers[j]->bottoms[0] == top_blob_index) break; // 确认连接
}

if (j == layer_count) continue; // 未找到

// 3. 获取层指针
ncnn::ConvolutionDepthWise* convolutiondepthwise = (ncnn::ConvolutionDepthWise*)layers[i];
ncnn::Layer* activation = layers[j];

fprintf(stderr, "fuse_convolutiondepthwise_activation %s %s\n", convolutiondepthwise->name.c_str(), activation->name.c_str());

// 4. 离线参数交接 (与 fuse_convolution_activation 完全一致)
if (activation->type == "ReLU")
{
ncnn::ReLU* relu = (ncnn::ReLU*)activation;
if (relu->slope == 0.f)
{
convolutiondepthwise->activation_type = 1; // 1 = ReLU
}
else
{
convolutiondepthwise->activation_type = 2; // 2 = Leaky ReLU
convolutiondepthwise->activation_params = ncnn::Mat(1);
convolutiondepthwise->activation_params[0] = relu->slope;
}
}
else if (activation->type == "Clip")
{
ncnn::Clip* clip = (ncnn::Clip*)activation;
convolutiondepthwise->activation_type = 3; // 3 = Clip
convolutiondepthwise->activation_params = ncnn::Mat(2);
convolutiondepthwise->activation_params[0] = clip->min;
convolutiondepthwise->activation_params[1] = clip->max;
}
// ... (对 Sigmoid, Mish, HardSwish 等的类似处理) ...

// 5. 图结构修改 (标准融合操作)
int top_blob_index_final = activation->tops[0];
convolutiondepthwise->tops[0] = top_blob_index_final;
blobs[top_blob_index_final].producer = i;
activation->type = "ncnnfused";
}

return 0;
}

关键点:

  • fuse_deconvolution_activation, fuse_deconvolutiondepthwise_activation, 和 fuse_innerproduct_activation 的代码与此几乎完全相同,唯一的区别就是第 1 步匹配的层类型不同(分别是 "Deconvolution", "DeconvolutionDepthWise", "InnerProduct")。
  • Deconvolution 及其 DepthWise 变体支持的激活函数列表稍短(例如,不包含 HardSwish),这取决于 ncnn 内部是否为这些层的微核实现了对应的融合计算逻辑。
  • 所有这些层(Convolution, ConvDW, Deconv, DeconvDW, InnerProduct)都在其基类或自身定义中包含了 activation_type (int) 和 activation_params (Mat) 这两个成员变量,为这种融合提供了标准化的数据存储接口。

3. 运行时(Runtime)的协同

ncnnoptimize 的工作只是“播种”。真正的“收获”发生在推理时。ncnn 中 ConvolutionDepthWise, Deconvolution 等层的 forward 实现(特别是 SIMD 优化的版本)会包含与 Convolution 类似的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Deconvolution::forward 伪代码
void Deconvolution::forward(const Mat& bottom_blob, Mat& top_blob, const Option& opt) const
{
// ... 大量的计算 ...
// ... 得到一个在寄存器中的结果 _sum ...

// 检查是否有融合的激活
if (activation_type == 1) // ReLU
{
_sum = _mm_max_ps(_sum, _mm_setzero_ps());
}
else if (activation_type == 2) // Leaky ReLU
{
__m128 _slope = _mm_load_ps(activation_params.data);
_sum = ... // 执行 Leaky ReLU 计算
}
// ... else if (Clip, Sigmoid...) ...

// 将最终激活后的结果写入内存
_mm_store_ps(top_blob_ptr, _sum);
}

通过这种离线标记 + 运行时检查的机制,ncnn 成功地将激活函数的计算开销“吸收”到了前驱层的计算微核中。


4. 结语

激活融合是 ncnn 图优化中覆盖面最广、收益最显著的策略之一。ncnnoptimize 通过为 Convolution, ConvolutionDepthWise, Deconvolution, DeconvolutionDepthWise, InnerProduct 等所有核心计算层提供几乎一致的 fuse_..._activation Pass,系统性地消除了计算图中大量的 Layer -> Activation 内存往返。

这种将优化思想(消除内存瓶颈)贯彻到所有相关算子的设计哲学,充分展现了 ncnn 作为高性能推理框架的工程严谨性和对性能的极致追求。