读 ncnn 源码(Ⅳ):Convolution 基类与 x86/FMA 特化 —— 参数到算子的全链路

这一篇把镜头对准 ncnn 最常用的算子之一 Convolution。我们先看 基类 Convolution 如何从 ParamDict 接住参数、管理权重与激活,再看 x86/FMA 特化层 Convolution_x86_fma 如何在运行时“接管”并进行更激进的优化(packing、Winograd/sgemm 预变换等)。最后用真实 .param 片段把字段一一落位。

TL;DR

  • Convolution::load_param(pd) 明确了 参数键位表
  • dynamic_weight=1 会把 one_blob_only=false,意味着第二个 bottom 用来动态传入权重(运行时卷积),否则权重来自 load_model()
  • int8_scale_term=1 需要编译 NCNN_INT8,并设置 support_int8_storage=true
  • padding 支持特殊值 -233=SAME_UPPER-234=SAME_LOWERmake_padding(...) 按需补边与填充值。
  • 激活既可“融合”在卷积里(activation_type != 0),也可在特化层中以 activation 子层形式完成。
  • Convolution_x86_fma
    • 构造时在 __SSE2__ 下开启 support_packing=true(允许 elempack>1 的矢量打包)。
    • 通过 DEFINE_LAYER_CREATOR(Convolution_x86_fma) 进入 层工厂,被 create_layer_cpu()FMA 路径选中。
    • create_pipeline() 通常会把权重预变换到多种布局(weight_sgemm_dataweight_winograd23/43/63_data),forward() 根据 kernel/stride/dilation/opt 选择最佳路径forwardDilation_x86() 专处理 dilation>1 的情况。
    • 可选 int8 路径(create_pipeline_int8_x86 / forward_int8_x86)在 NCNN_INT8 打开时启用。


1) 从 LayerConvolution:基类约定

Layer 的缺省:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Layer::Layer()
{
one_blob_only = false; // 默认可多输入
support_inplace = false;
support_vulkan = false;
support_packing = false; // 默认不支持打包
support_bf16_storage = false;
support_fp16_storage = false;
support_int8_storage = false;
support_tensor_storage = false;
featmask = 0;
#if NCNN_VULKAN
vkdev = 0;
#endif
userdata = 0;
typeindex = -1;
}

Convolution 的构造:

1
2
3
4
5
Convolution::Convolution()
{
one_blob_only = true; // 卷积默认只有一个输入 bottom
support_inplace = false;
}
  • 大多数静态卷积(权重来自模型)只吃一个 bottom,所以基类把 one_blob_only 置为 true
  • 如果 动态权重dynamic_weight=1),稍后会把它改回 false(见下文)。

2) Convolution::load_param(pd):完整键位表与语义

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
int Convolution::load_param(const ParamDict& pd)
{
num_output = pd.get(0, 0);
kernel_w = pd.get(1, 0);
kernel_h = pd.get(11, kernel_w);

dilation_w = pd.get(2, 1);
dilation_h = pd.get(12, dilation_w);

stride_w = pd.get(3, 1);
stride_h = pd.get(13, stride_w);

pad_left = pd.get(4, 0);
pad_right = pd.get(15, pad_left);
pad_top = pd.get(14, pad_left);
pad_bottom = pd.get(16, pad_top);
pad_value = pd.get(18, 0.f);

bias_term = pd.get(5, 0);
weight_data_size = pd.get(6, 0);

int8_scale_term = pd.get(8, 0);

activation_type = pd.get(9, 0);
activation_params = pd.get(10, Mat());

dynamic_weight = pd.get(19, 0);
if (dynamic_weight) one_blob_only = false;

if (int8_scale_term)
{
#if NCNN_INT8
support_int8_storage = true;
#else
NCNN_LOGE("please build ncnn with NCNN_INT8 enabled for int8 inference");
return -1;
#endif
}
return 0;
}

2.1 参数对照表(常用)

id 含义 备注
0 num_output 输出通道
1/11 kernel_w/kernel_h 未给 11 则默认方核
2/12 dilation_w/h 默认 1
3/13 stride_w/h 默认 1
4/15/14/16 pad_left/right/top/bottom -233=SAME_UPPER-234=SAME_LOWER
18 pad_value 常用于 AVG 池化/填零以外的 pad
5 bias_term 1=有偏置
6 weight_data_size kw*kh*in_c*out_c(或分组后相应公式)
8 int8_scale_term 1=走 int8 缩放表
9/10 activation_type/params 0=无,1=ReLU,2=LeakyReLU,3=Clip,4=Sigmoid(按实际实现为准)
19 dynamic_weight 1=第二个 bottom 传入权重

激活既可以通过 activation_type/params 在卷积里融合完成,也可能在特化层中创建一个 activation 子层执行(见 §3)。

2.2 .param 落位实例(来自你的 conv1

1
Convolution conv1 ... 0=64 1=3 3=2 5=1 6=1728 9=1
  • num_output=64
  • kernel=3x311 未给 → kernel_h=kernel_w
  • stride=213 未给 → stride_h=stride_w
  • bias_term=1
  • weight_data_size=1728 = 3×3×3×64(来自上一层输入通道 c=3
  • activation_type=1(ReLU,常见为“卷积 + ReLU 融合”)

3) padding 与激活:从成员到流程

  • padding
    • 常规 pad 直接由四个边界值指定;
    • 特殊值 -233/-234 表示 SAME_UPPER/LOWER 策略(保持输出尺寸,偏差分配在上/左或下/右);
    • 实际执行由 make_padding(...) 完成,可能产生一个临时的 bottom_blob_bordered 再进入主计算。
  • activation:两种常见落地方式
    1. 融合:在卷积内核末尾(或 GEMM/Winograd 结果写回前)做 activation_type 分支;
    2. 子层:在特化层 create_pipeline() 中创建 activationLayer,把 activation_params 传入,forward() 结束前调用一次。

4) Convolution_x86_fma:如何“接管”并优化

构造与工厂入口

1
2
3
4
5
6
7
8
9
10
11
12
Convolution_x86_fma::Convolution_x86_fma()
{
#if __SSE2__
support_packing = true; // 允许打包(elempack=4/8)以利用 SIMD
#endif
activation = 0;
nT = 0;
convolution_dilation1 = 0;
}

// 注册到层工厂
namespace ncnn { DEFINE_LAYER_CREATOR(Convolution_x86_fma) }
  • support_packing=true 让这条实现可以处理 elempack>1 的张量布局(例如打包 4/8 通道),是 SIMD 加速的基础。
  • DEFINE_LAYER_CREATOR 生成 Convolution_x86_fma_layer_creator(),使其能被 create_layer_cpu()FMA 路径选中(参见第Ⅱ篇的“按 ISA 优选”)。

类字段概览

1
2
3
4
5
6
7
8
9
10
11
12
// 继承 Convolution
Layer* activation; // 可选:把激活做成子层
int nT; // 线程/分块相关(内部使用)
Mat weight_data_tm; // “transformed” 权重(通用)
Mat weight_sgemm_data; // sgemm 路径
Mat weight_winograd23_data, weight_winograd43_data, weight_winograd63_data;
// dilation 专用
Layer* convolution_dilation1;

#if NCNN_INT8
Mat scale_in_data;
#endif

典型生命周期(概述)

  1. load_param(pd):复用基类,把超参数全部接住。
  2. load_model(mb):读入 weight_databias_data(静态权重);如 int8_scale_term=1,还会读缩放表。
  3. create_pipeline(opt):依据 kernel/stride/dilation/opt
    • weight transform
      • sgemm:im2col + sgemm 友好布局 → weight_sgemm_data
      • winograd:针对 3x3 的 F(2×3)/F(4×3)/F(6×3) 预变换 → weight_winogradXX_data
    • 如需激活子层:根据 activation_type/params 创建 activation(一次性准备)。
    • 可能根据 CPU 能力与 opt.use_winograd_convolution/use_sgemm_convolution 做二选一或多候选保留。
  4. forward(...)
    • 必要时 make_padding(...)
    • 选择最佳内核
      • dilation>1 → 走 forwardDilation_x86()
      • 否则:依据 kernel/stride/通道规模/缓存亲和/opt,选择 Winograd(多种 tile)或 sgemm
    • 进行打包/分块并发(support_packing/nT)与实际计算;
    • 写回后执行激活(融合或 activation 子层);
    • dynamic_weight=1 时,从第二个 bottom 读取权重(跳过 load_model() 的静态权重)。

注意:选择策略与实现细节取决于版本与编译选项;但从字段命名即可看出 “多路径 + 预变换” 的总体思路。


5) 结合真实 .param:把字段落回 conv1

再次拿 conv1(SqueezeNet-1.1 的第一层)举例:

1
2
Input data 0 1 data 0=227 1=227 2=3
Convolution conv1 1 1 data conv1_relu_conv1 0=64 1=3 3=2 5=1 6=1728 9=1
  • Convolution::load_param(pd) 之后,成员为:
    • num_output=64
    • kernel_w=kernel_h=3stride_w=stride_h=2dilation=1
    • pad_* 未给 → 0(此时输出空间 floor((227-3)/2)+1=113,得到 113×113×64
    • bias_term=1
    • weight_data_size=1728(校验通过)
    • activation_type=1(ReLU)
    • dynamic_weight=0 → 单输入
  • 在 x86/FMA 平台,create_layer_cpu() 会优选 Convolution_x86_fma 实现:
    • support_packing=true,后续将以 pack4/pack8 的布局加速(受 SSE/AVX 宏与运行时能力影响);
    • create_pipeline() 可能为 3×3 构建 Winograd 权重(23/43/63 三种之一)或 sgemm 版本,forward() 决策使用何者;
    • 写回后做 ReLU。

6) 实战与排错清单

  • 算子没有被 FMA 版本接管?
    • 确认二进制启用了 NCNN_RUNTIME_CPU,并在运行时 cpu_support_x86_fma() 为真。
    • 该层类型在 layer_registry_fma[] 中的 creator 不应为 0
    • 若你用“覆盖机制”接管了该层(见第Ⅱ篇),覆盖优先于 FMA 版本。
  • 输出尺寸与预期不符?
    • 检查 pad_* 是否默认 0;若想“保持尺寸”,需使用 -233/-234 或显式设置 pad。
    • dilation>1 时,forwardDilation_x86() 路径与常规路径不同,注意 kernel/stride 的组合。
  • 动态权重
    • dynamic_weight=1one_blob_only=false,请确保第二个 bottom 形状与 weight_data_size 对齐;静态 load_model() 权重不会被使用。
  • int8
    • int8_scale_term=1 需要 NCNN_INT8;否则 load_param() 将报错返回。
    • 即便 support_int8_storage=true,也要有对应的 create_pipeline_int8_x86/forward_int8_x86 路径支撑。

7) 小结

  • Convolution 基类清晰地把卷积的超参数、padding 语义、激活、int8/dynamic_weight 等“接口”统一起来;
  • x86/FMA 特化层在此基础上做“重计算、轻调度”的工程化优化:权重预变换、多核并行、SIMD 打包、Winograd/sgemm 多策略择优;

该封面图片由JohnPixabay上发布