读 ncnn 源码(XVIII):`fuse_convolution_mul`——融合逐通道乘法,优化线性计算链
读 ncnn 源码(XVIII):fuse_convolution_mul——融合逐通道乘法,优化线性计算链
在上一篇中,我们分析了 fuse_convolution_batchnorm 如何通过代数等价变换,将 BatchNorm 层融入前驱的 Convolution 层。这一优化思路——合并连续的线性计算——具有普遍性。本篇,我们将探讨 ncnn 中另一个重要的融合 Pass:fuse_convolution_mul,它针对的是 **Convolution 层后紧跟一个特定模式的逐通道乘法(通过 BinaryOp 实现)**的场景。
这种 Conv -> Mul(per-channel) 的结构在某些网络(例如 MobileNetV1 中的 Depthwise + Pointwise 结构有时会伴随缩放)或模型转换过程中可能出现。将其融合,可以进一步减少计算冗余,提升推理效率。
TL;DR
目标: 将 Convolution 层后接一个扮演逐通道乘法 (Per-Channel Scaling) 角色的 BinaryOp 层(操作类型为 Mul,且第二个输入来自 Memo ...
读 ncnn 源码(XVII):`fuse_convolution_batchnorm`——融合 BN,轻装前行
读 ncnn 源码(XVII):fuse_convolution_batchnorm——融合 BN,轻装前行
在上一篇《读 ncnn 源码(XVI)》中,我们初步探索了 ncnnoptimize 工具及其图优化(Graph Optimization)的基本原理,并以 fuse_batchnorm_scale 为例展示了算子融合(Operator Fusion)的威力。本篇,我们将深入分析一个更常见、影响也更广泛的融合优化:卷积层(Convolution)与批归一化层(BatchNorm)的融合,即 fuse_convolution_batchnorm。
几乎所有的现代卷积神经网络在训练阶段都会大量使用 BatchNorm 层来加速收敛、提升泛化能力。然而,在推理阶段,BatchNorm 的计算是完全线性的,可以被等效地合并到其前面的线性层(如卷积层)中。理解 fuse_convolution_batchnorm 的原理与实现,对于掌握 ncnn 乃至所有推理引擎的核心优化技术至关重要。
TL;DR
目标: 将推理阶段计算固定、但开销不小的 BatchNorm 层,通过数学等价 ...
读 ncnn 源码(XV):Pimpl 惯用法——解耦接口与实现的 C++ 设计基石
读 ncnn 源码(XV):Pimpl 惯用法——解耦接口与实现的 C++ 设计基石
经过前面十余篇对 ncnn 核心模块的深入探索,我们已经领略了其在卷积优化(Winograd, GEMM, Packing)、内存管理(Mat, Allocator)以及推理流程(Extractor, forward_layer)等方面精妙的性能工程。在本系列的最终篇,我们将目光从具体的算法优化,转向一个支撑起 ncnn 作为一个稳定、易用、可维护 C++ 库的基础设计模式——Pimpl 惯用法 (Pointer to Implementation Idiom),也就是我们在 ncnn::Net 和 ncnn::Extractor 等核心类中看到的那个神秘的 d 指针。
理解 Pimpl 不仅能帮助我们读懂 ncnn 的代码组织方式,更能为我们自己设计健壮、高内聚、低耦合的 C++ 库提供宝贵的借鉴。
TL;DR
什么是 Pimpl?: Pimpl 是一种 C++ 设计模式,它将类的私有成员变量和实现细节隐藏在一个单独的实现类中,公共类仅持有一个指向该实现类的指针(通常命名为 d 或 pim ...
读 ncnn 源码(XIV):`convert_layout`——层间数据格式的“翻译官”
读 ncnn 源码(XIV):convert_layout——层间数据格式的“翻译官”
在上一篇中,我们剖析了 Extractor::extract 如何通过 forward_layer 的递归调用,实现了惰性求值和推理流程的调度。我们看到,在真正执行每一层的计算(do_forward_layer)之前,有一个关键的准备步骤。本篇,我们将聚焦于这个步骤的核心函数——NetPrivate::convert_layout,以及它如何与具体的层实现(以 Convolution_x86_fma::forward 为例)协同工作,确保数据在层与层之间流畅、高效地传递。
convert_layout 就像一位专业的“翻译官”,负责将上一层输出的“语言”(数据格式)转换成当前层最“听得懂”、处理效率最高的“语言”。
TL;DR
convert_layout 的职责: 在 do_forward_layer 调用具体层的 forward 方法之前被执行。其核心任务是适配数据格式,确保输入 bottom_blob 的精度(Precision)和内存布局(Layout/Packing)符合当前 l ...
读 ncnn 源码(XIII):`Extractor::extract`——触发推理的引擎核心
读 ncnn 源码(XIII):Extractor::extract——触发推理的引擎核心
在之前的篇章中,我们已经探讨了 ncnn 如何加载模型、优化权重布局。当模型准备就绪,输入数据也通过 Extractor::input() 绑定后,最后一步就是调用 Extractor::extract() 来获取我们关心的输出结果。这个看似简单的函数调用,背后却隐藏着 ncnn 推理引擎的核心调度逻辑,涵盖了惰性求值、后端分发、内存管理等多个关键环节。
本篇,我们将深入 Extractor::extract 及其调用的 NetPrivate::forward_layer、NetPrivate::do_forward_layer 函数,剖析 ncnn 是如何根据依赖关系,按需、高效地执行神经网络推理的。
TL;DR
Extractor::extract 是推理触发器: 它并非立即计算整个网络,而是采用惰性求值 (Lazy Evaluation)。只有当用户请求某个特定的 blob 时,才会触发计算该 blob 所需的最少网络路径。
核心流程: extract(blob_name/ind ...
读 ncnn 源码(Ⅻ):图像预处理流水线——从像素到张量的“最后一公里”
读 ncnn 源码(Ⅻ):图像预处理流水线——从像素到张量的“最后一公里”
在深度学习推理场景中,将原始图像数据适配到神经网络输入格式,是“最后一公里”的关键挑战。这一过程,即图像预处理(Image Pre-processing),涵盖尺寸调整、格式转换、数值归一化等多个环节,其效率和精度直接影响模型推理的整体性能。
本篇,我们将深入剖析 ncnn 中从外部像素数据到内部 Mat 张量输入的完整预处理流水线,重点关注 Mat::from_pixels_resize、resize_bilinear_cX、Mat::from_pixels 以及 Mat::substract_mean_normalize 几个核心函数的协同工作,并通过源码揭示其背后的工程考量与优化策略。
TL;DR
流水线: ncnn 的图像预处理分为尺寸适配 → 格式转换与封装 → 数值归一化三阶段。
尺寸适配 (from_pixels_resize, resize_bilinear_cX):
from_pixels_resize 提供便捷接口,自动计算 stride 并分发。核心缩放由 resize_bil ...
读 ncnn 源码(Ⅺ):**Packed Kernel Transform 的“通性”**
读 ncnn 源码(Ⅺ):Packed Kernel Transform 的“通性”
优先级位于 Winograd 和 im2col+GEMM 之后,它是兜底策略,也是基础策略。
本篇把 convolution_transform_kernel_packed(...) 讲成平台无关的一套方法论:
① 为啥要重排?② 重排成啥形状?③ 怎么重排最顺手?
TL;DR
目的:把原始权重 W[out, in, k](k∈[0..kw*kh))重排成微核友好的小片(tile),以便一次处理 pb 个输出通道 × pa 个输入通道 × 全部 k 的累加。
形状:重排后张量可视为
kernel_tm.shape = [ w = pb*pa*maxk , h = Σ(inpacks) , c = Σ(outpacks) ],
其中 Σ(inpacks)/Σ(outpacks) 是把 inch/outch 用 {16,8,4,2,1} 逐级分块后块数的和。
填充顺序(核心一行):
对每个 tile(由某个 out 的 pb × 某个 in 的 pa × 全部 k 构成),按
1ti ...
读 ncnn 源码(Ⅹ):Winograd F(2×2,3×3) 的**内核变换、选路与打包**(含对比 packed sse)
读 ncnn 源码(Ⅹ):Winograd F(2×2,3×3) 的内核变换、选路与打包(含对比 packed sse)
TL;DR
入口:conv3x3s1_winograd23_transform_kernel() 把 3×3 卷积核先做 Winograd 核变换,再按 (M,K) 维度 分块打包到 AT,以便后续 batched-GEMM 高效消费。
选 tile:conv3x3s1_winograd_get_optimal_tile_mnk() 依据 L2 容量 + ISA 对齐 + 线程数,优先 不切 K,设定 (TILE_M,TILE_K[,TILE_N])。
打包:conv3x3s1_winograd_pack_A_tile() 将线程私有缓冲 A_tile 重新编织成 AT 的目标布局(以 batch=B=16 为内层 stride),方便微内核整块装载。
何时用 23/63:test_prefer_winograd{23,63} 给出基于 in/out/min(w,h) 的经验区间;3×3、s=1、d=1 且通道/空间合适时优先 Winograd。 ...
读 ncnn 源码(Ⅸ):im2col+GEMM 原理与 `Mat::reshape(w,h,c)` 的对齐与 cstep
读 ncnn 源码(Ⅸ):im2col+GEMM 原理与 Mat::reshape(w,h,c) 的对齐与 cstep
TL;DR
卷积可重写为矩阵乘:令 maxk=kw*kh、M=outch、K=inch*maxk、N=Hout*Wout,则有 A(M×K) × B(K×N) = C(M×N)。A 由权重重排得到,B 由输入经 im2col 展开得到。
为跑出 SIMD 峰值,ncnn 先把 A/B 预打包成微内核喜欢的布局(按 tile 和 elempack),再做 GEMM。
Mat::reshape(w,h,c) 不改变元素个数;关键是按 16 字节对齐计算 cstep(每个通道的步长),必要时新建缓冲并逐通道 memcpy;否则尽量 header-only 视图重构(零拷贝)。
触发拷贝的典型情况:w*h*elemsize 不是 16 字节对齐、或改变了 c 导致需要“先拉平再按新通道对齐”。
一、im2col + GEMM:把卷积“摊平”成矩阵乘
以 NCHW 为例,单张图片、步幅 s、无 dilation 的标准 2D 卷积:
A(权重矩阵):
每个输出 ...
读 ncnn 源码(Ⅷ):核心算法细讲——Activation 工厂、CPU 后端选择、im2col+GEMM 权重打包与分块
读 ncnn 源码(Ⅷ):核心算法细讲——Activation 工厂、CPU 后端选择、im2col+GEMM 权重打包与分块
承接(Ⅶ),我们这次专注在“怎么把卷积高效地变成矩阵乘”。重点是以下三个函数
create_activation_layer() 激活层工厂;
create_layer_cpu() 按 ISA 自适应选择实现;
convolution_im2col_* 系列:权重预变换(A 打包) + 最佳 tile 选择 + SIMD 转置打包微内核。
TL;DR
激活工厂:create_activation_layer() 按 activation_type 构造 ReLU/LeakyReLU/Clip/Sigmoid/Mish/HardSwish,用 ParamDict 注参后立刻 create_pipeline(opt),便于后端优化复用。
CPU 后端自适应:create_layer_cpu() 运行时探测 ISA,优先选 AVX512 → FMA/AVX → SSE2(或 LASX/LSX、MSA、RVV…)对应的注册表,拿到该层的最优实现。
卷积→G ...
读 ncnn 源码(Ⅶ):以卷积层为例——权重加载与 x86/FMA pipeline 选路
读 ncnn 源码(Ⅶ):以卷积层为例——权重加载与 x86/FMA pipeline 选路
承接(Ⅵ)的“全局模型权重加载链路”,这篇聚焦卷积层:权重怎么从 .bin 读进来、什么时候在运行时做 int8 量化、以及 x86/FMA 实现如何根据核大小/步长/膨胀/通道数/缓存选择 Winograd、sgemm 或 packed 路线。
TL;DR
Convolution::load_model(mb):
dynamic_weight==1 → 跳过读权重(权重来自第二个 bottom);
否则读 weight_data_size 个权值(type=0 自动识别 f16/int8/f32/表量化);有偏置再读 num_output 个 float;
若 NCNN_INT8 && int8_scale_term → 读缩放表,且可在运行时把 float 权重量化成 int8,替换到 weight_data。
读法由 ModelBinFromDataReader::load(w,type)决定:
0x01306B47=f16、0x000D4B38=int8 ...
读 ncnn 源码(Ⅵ):模型权重加载链路 —— DataReader / ModelBin / create_pipeline
读 ncnn 源码(Ⅵ):模型权重加载链路 —— DataReader / ModelBin / create_pipeline
前面我们把 .param 从 文本 → ParamDict → 图结构 → I/O 名单 的路径讲圆了。本篇继续往下:如何把 .bin 里的权重读入每一层,以及在“读完权重”后做的 create_pipeline() 预处理(如权重变换、缓存准备等)。
TL;DR
Net::load_model() 需要 图已建立(先 load_param)。它为每层依次执行:layer->load_model(ModelBin&) → layer->create_pipeline(opt1)。opt1 是按层 featmask 局部裁剪后的 Option。
数据来源 通过多种 DataReader 封装:FromStdio / FromMemory / FromAndroidAsset;上层统一交给 ModelBinFromDataReader 解析。
权重编码(在 .bin)由 ModelBinFromDataReader::load(w, ...
读 ncnn 源码(Ⅴ):Param 读取闭环——从 token 到图,再到 I/O 名单
读 ncnn 源码(Ⅴ):Param 读取闭环——从 token 到图,再到 I/O 名单
这一篇把 “ncnn 如何读取 .param 并把它变成一张可执行的计算图” 的最后一段细节补齐:
① ParamDict 的数据布局 / 类型系统 / 读写语义;
② 迷你词法器 vstr_* 如何把字符串切成标量/数组;
③ Blob 的生产者/消费者是怎样被填充;
④ update_input_output_indexes/names 如何自动识别模型输入输出。
我们再用一段简化的 SqueezeNet 片段做“逐 token 实录”。
TL;DR
ParamDict 内部是一块 定长槽位数组(NCNN_MAX_PARAM_COUNT):每个 id 对应一个 type + (i/f/v/s),类型码包括标量/数组/字符串。
load_param() 扫描 id=value / id=v1,v2,...,用 vstr_is_string/float 断型,vstr_to_float 手写解析。
建图时,每个 layer 读完头(type/name/bottom/top)和 Par ...
读 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_p ...
读 ncnn 源码(Ⅲ):ParamDict 解析、featmask 按层屏蔽、词法器与 blob 索引(含解析实录)
读 ncnn 源码(Ⅲ):ParamDict 解析、featmask 按层屏蔽、词法器与 blob 索引(含解析实录)
承接前两篇:我们已经理清了 Net::load_param 的主链路与层工厂/覆盖/CPU 指令集优选。本篇把镜头拉近到每一层行尾那串 k=v 参数,以及与之相关的三个关键点:
① ParamDict::load_param 的语法与类型系统;
② featmask 如何把全局 Option 变成按层屏蔽的局部选项;
③ find_blob_index_by_name / vstr_* 这些“小工具”在建图与解析中的位置。最后用一段真实 .param(SqueezeNet 的前两层)做逐 token 解析实录。
TL;DR
ParamDict::load_param(dr) 循环读取 id=value / id=v1,v2,...,支持 int / float / string / int[] / float[] 五种类型;同时兼容“旧式数组语法”(负 id 前缀 + 显式长度)。
解析结果落在 d->params[id],type 编码:2=int、 ...
读 ncnn 源码(Ⅱ):层工厂与“覆盖机制”,以及 CPU 端的指令集优选
读 ncnn 源码(Ⅱ):层工厂与“覆盖机制”,以及 CPU 端的指令集优选
延续前文对 Net::load_param 的解析,这一篇聚焦两件事:
① ncnn 如何把 字符串层名 映射到 内置层 并允许你覆盖默认实现;
② CPU 端如何在运行时按 AVX512/FMA/AVX/… 等指令集优选最快实现,并且逐层回退兜底。
TL;DR
layer_registry[] 是内置层名册:把 "Convolution" 这类字符串类型映射到typeindex,并关联一个 creator(工厂函数指针)。
create_overwrite_builtin_layer(...) 让你按 typeindex 覆盖内置实现(优先级最高,早于 Vulkan/CPU 路径)。
create_layer_cpu(...) 会在运行时检测 CPU 能力,从高到低选择 AVX512 → FMA → AVX → LASX → LSX → MSA → XTheadVector → RVV → arch 基线;若该 ISA 下此层没特化实现,自动回退到更通用实现。
Net::load_param 的创 ...
















