读 ncnn 源码(XXI):fuse_convolutiondepthwise_mul——为深度可分离卷积“乘”胜追击

第十八篇中,我们分析了标准 Convolution 如何融合后续的逐通道乘法 (fuse_convolution_mul)。鉴于深度可分离卷积(Depthwise Separable Convolution)在现代轻量级网络中的广泛应用,ncnn 自然也提供了针对 ConvolutionDepthWise 后接逐通道乘法的特定融合优化:fuse_convolutiondepthwise_mul

本篇,我们将剖析该函数的源码,理解其如何将 fuse_convolution_mul 的逻辑应用于 ConvolutionDepthWise 这一特殊卷积形式。

TL;DR

  1. 目标: 将 ConvolutionDepthWise 层后接一个执行逐通道乘法(Per-Channel Scaling,由 BinaryOp(Mul) + MemoryData 实现)的操作进行融合。
  2. 模式匹配: 查找 ConvolutionDepthWise -> BinaryOp 结构,附加条件与 fuse_convolution_mul 完全一致:BinaryOp 必须是 Mul (op_type == 2),非标量 (!with_scalar),且第二输入来自形状匹配通道数的 MemoryData 向量。
  3. 数学原理: fuse_convolution_mul 完全一致。因为 ConvolutionDepthWise 也是线性操作(逐通道独立),后接逐通道乘法同样构成连续线性变换链。融合公式为:
    • 新权重 Wdw,o=Wdw,oSoW'_{dw, o} = W_{dw, o} \cdot S_o (第 o 通道的 DW 卷积核乘以该通道的缩放系数 SoS_o)。
    • 新偏置 bdw,o=bdw,oSob'_{dw, o} = b_{dw, o} \cdot S_o (第 o 通道的偏置乘以 SoS_o)。
  4. 代码实现: 几乎与 fuse_convolution_mul 完全相同
    • 遍历 ConvolutionDepthWise 的每个输出通道 i
    • 将该通道对应的所有权重 (weight_per_outch = kernel_w * kernel_h 个元素) 乘以 memorydata->data[i]
    • 如果存在偏置,则将 bias[i] 也乘以 memorydata->data[i]
  5. 图结构修改: 将 ConvolutionDepthWisetop 指向原 BinaryOptop,更新 blobproducer,并将 BinaryOp 标记为 "ncnnfused"
  6. 效果: 消除了 BinaryOp 带来的冗余计算和内存访问,对于使用逐通道缩放的深度可分离卷积结构(尽管不如 BN 常见)有所优化。

1. 融合动机:合并线性链的通用性

ConvolutionDepthWise 对每个通道独立进行卷积,其计算 yo=Wdw,oxo+bdw,oy_o = W_{dw, o} * x_o + b_{dw, o} 是线性的。如果后续 BinaryOp 层执行 zo=yoSoz_o = y_o \cdot S_o 的逐通道缩放,这同样是线性的。因此,将这两个操作合并为一个等效的 ConvolutionDepthWise 具有明确的代数基础和性能优势(减少计算和访存)。


2. 代码实现:高度复用的融合逻辑

fuse_convolutiondepthwise_mul 的代码实现与 fuse_convolution_mul 高度相似,仅在层类型匹配上有所不同。

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
65
66
67
68
69
70
71
72
73
74
75
int NetOptimize::fuse_convolutiondepthwise_mul()
{
const size_t layer_count = layers.size();
for (size_t i = 0; i < layer_count; i++) // 遍历查找 ConvolutionDepthWise
{
if (layers[i]->type != "ConvolutionDepthWise") continue;
int top_blob_index = layers[i]->tops[0];

// 查找后续的 BinaryOp (Mul, 非标量, 第二输入来自 MemoryData)
// ... (模式匹配代码与 fuse_convolution_mul 完全一致) ...
size_t j = i + 1;
// ... (find BinaryOp j) ...
if (j == layer_count) continue;

ncnn::ConvolutionDepthWise* convolutiondepthwise = (ncnn::ConvolutionDepthWise*)layers[i];
ncnn::BinaryOp* binaryop = (ncnn::BinaryOp*)layers[j];

if (binaryop->op_type != 2 || binaryop->with_scalar) continue; // 必须是 Mul, 非标量

size_t k = 0;
// ... (find MemoryData k as the second input of j) ...
if (k == j) continue;

ncnn::MemoryData* memorydata = (ncnn::MemoryData*)layers[k];

int channels = convolutiondepthwise->num_output; // DWConv 的通道数

// 校验 MemoryData 形状是否为 [channels] 的向量
if (memorydata->w != channels || memorydata->h != 0 || memorydata->c != 0)
{
continue; // 形状不匹配,无法融合
}

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

// --- 参数变换核心 ---
{
// 计算每个输出通道 (group) 的权重数量
// 对于 DWConv, weight_data_size = channels * kernel_w * kernel_h
// weight_per_outch = kernel_w * kernel_h
const int weight_per_outch = convolutiondepthwise->weight_data_size / channels;

float* weight = convolutiondepthwise->weight_data; // 指向 DWConv 权重
float* bias = convolutiondepthwise->bias_data; // 指向 DWConv 偏置 (可能为 NULL)

// 遍历每个通道,更新权重和偏置
for (int ch = 0; ch < channels; ch++) // ch 即为输出通道索引 o
{
// 获取指向当前通道 ch 的权重的指针
float* conv_weight_outch = weight + weight_per_outch * ch;
float scale_factor = memorydata->data[ch]; // 获取缩放系数 S_o

// 将该通道的所有权重乘以 S_o
for (int wi = 0; wi < weight_per_outch; wi++)
{
conv_weight_outch[wi] *= scale_factor; // W'_dw = W_dw * S_o
}

// 如果存在偏置,也将偏置乘以 S_o
if (bias)
{
bias[ch] = bias[ch] * scale_factor; // b'_dw = b_dw * S_o
}
}
} // --- 参数变换结束 ---

// --- 图结构修改 (与之前所有融合 Pass 逻辑一致) ---
int top_blob_index_final = binaryop->tops[0];
convolutiondepthwise->tops[0] = top_blob_index_final;
blobs[top_blob_index_final].producer = i;
binaryop->type = "ncnnfused";
// --- 图结构修改结束 ---
}
return 0;
}

关键点:

  • 模式匹配: 与 fuse_convolution_mul 的逻辑一致,仅将查找目标从 "Convolution" 改为 "ConvolutionDepthWise"
  • 参数更新: 同样是遍历每个输出通道 ch,将该通道对应的权重(kernel_w * kernel_h 个)和偏置(如果存在)乘以 MemoryData 中对应的缩放系数 memorydata->data[ch]
  • 图修改: 标准的重定向连接 + 标记融合操作。

3. 意义与应用场景

虽然 ConvDW -> Mul(per-channel) 的模式不如 ConvDW -> BN 常见,但在某些特定情况下(例如模型转换、特定网络设计或手动插入的缩放层),此优化 Pass 仍然能发挥作用。它体现了 ncnnoptimize 试图覆盖尽可能多的可融合线性计算链的完备性。

通过提供针对 Depthwise Convolution 的特定融合规则(包括 _batchnorm, _mul, _add),ncnn 确保了基于深度可分离卷积构建的轻量级网络能够得到充分的优化。


4. 结语

fuse_convolutiondepthwise_mul 是 ncnn 图优化工具针对深度可分离卷积线性链优化的补充。其实现逻辑与标准卷积的融合高度一致,再次证明了合并连续线性变换这一优化原理的普适性。通过消除冗余的逐通道乘法操作,进一步提升了相关网络结构的推理效率。这一系列精细化的融合 Pass 共同构成了 ncnnoptimize 强大的模型“精炼”能力。

该封面图片由Kang-Rui LENGPixabay上发布