读 ncnn 源码(Ⅳ):Convolution 基类与 x86/FMA 特化 —— 参数到算子的全链路
读 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_LOWER;make_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_data、weight_winograd23/43/63_data),forward()根据 kernel/stride/dilation/opt 选择最佳路径;forwardDilation_x86()专处理 dilation>1 的情况。- 可选 int8 路径(
create_pipeline_int8_x86 / forward_int8_x86)在NCNN_INT8打开时启用。
- 构造时在

1) 从 Layer 到 Convolution:基类约定
Layer 的缺省:
1 | Layer::Layer() |
Convolution 的构造:
1 | Convolution::Convolution() |
- 大多数静态卷积(权重来自模型)只吃一个 bottom,所以基类把
one_blob_only置为true。 - 如果 动态权重(
dynamic_weight=1),稍后会把它改回false(见下文)。
2) Convolution::load_param(pd):完整键位表与语义
1 | int Convolution::load_param(const ParamDict& pd) |
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=64kernel=3x3(11未给 →kernel_h=kernel_w)stride=2(13未给 →stride_h=stride_w)bias_term=1weight_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:两种常见落地方式
- 融合:在卷积内核末尾(或 GEMM/Winograd 结果写回前)做
activation_type分支; - 子层:在特化层
create_pipeline()中创建activation子Layer,把activation_params传入,forward()结束前调用一次。
- 融合:在卷积内核末尾(或 GEMM/Winograd 结果写回前)做
4) Convolution_x86_fma:如何“接管”并优化
构造与工厂入口
1 | Convolution_x86_fma::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 | // 继承 Convolution |
典型生命周期(概述)
load_param(pd):复用基类,把超参数全部接住。load_model(mb):读入weight_data与bias_data(静态权重);如int8_scale_term=1,还会读缩放表。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;
- sgemm:
- 如需激活子层:根据
activation_type/params创建activation(一次性准备)。 - 可能根据 CPU 能力与
opt.use_winograd_convolution/use_sgemm_convolution做二选一或多候选保留。
- 做 weight transform:
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 | Input data 0 1 data 0=227 1=227 2=3 |
Convolution::load_param(pd)之后,成员为:num_output=64kernel_w=kernel_h=3;stride_w=stride_h=2;dilation=1pad_*未给 →0(此时输出空间floor((227-3)/2)+1=113,得到113×113×64)bias_term=1weight_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=1时one_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 多策略择优;
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 James的成长之路!
评论





