读 ncnn 源码(XXI):`fuse_convolutiondepthwise_mul`——为深度可分离卷积“乘”胜追击
读 ncnn 源码(XXI):fuse_convolutiondepthwise_mul——为深度可分离卷积“乘”胜追击
在第十八篇中,我们分析了标准 Convolution 如何融合后续的逐通道乘法 (fuse_convolution_mul)。鉴于深度可分离卷积(Depthwise Separable Convolution)在现代轻量级网络中的广泛应用,ncnn 自然也提供了针对 ConvolutionDepthWise 后接逐通道乘法的特定融合优化:fuse_convolutiondepthwise_mul。
本篇,我们将剖析该函数的源码,理解其如何将 fuse_convolution_mul 的逻辑应用于 ConvolutionDepthWise 这一特殊卷积形式。
TL;DR
目标: 将 ConvolutionDepthWise 层后接一个执行逐通道乘法(Per-Channel Scaling,由 BinaryOp(Mul) + MemoryData 实现)的操作进行融合。
模式匹配: 查找 ConvolutionDepthWise -> BinaryOp ...
读 ncnn 源码(XX):`fuse_convolutiondepthwise_batchnorm`——为深度可分离卷积“减负”
读 ncnn 源码(XX):fuse_convolutiondepthwise_batchnorm——为深度可分离卷积“减负”
在第十七篇中,我们深入分析了标准 Convolution 与 BatchNorm 的融合机制。深度可分离卷积(Depthwise Separable Convolution)作为现代轻量级网络(如 MobileNet, EfficientNet)的基础构件,其 ConvolutionDepthWise 部分后通常也紧跟着 BatchNorm 层。因此,针对性地优化 ConvolutionDepthWise -> BatchNorm 这一常见模式,对于提升这些模型的推理性能至关重要。
本篇,我们将剖析 fuse_convolutiondepthwise_batchnorm 的源码,理解 ncnn 如何将针对标准卷积的融合原理,平滑地应用于深度可分离卷积的 Depthwise 部分。
TL;DR
目标: 将 ConvolutionDepthWise 层后紧随的 BatchNorm 层进行融合,消除 BatchNorm 在推理时的计算开销。
模式匹配 ...
读 ncnn 源码(XIX):`fuse_convolution_add`——融合逐通道加法,进一步合并线性计算
读 ncnn 源码(XIX):fuse_convolution_add——融合逐通道加法,进一步合并线性计算
本篇,我们将继续沿着合并连续线性计算的优化思路,分析 ncnn 中另一个重要的融合 Pass:fuse_convolution_add。
此 Pass 专门针对 **Convolution 层后紧跟一个特定模式的逐通道加法(通过 BinaryOp 实现)**的场景。这种情况可能出现在某些网络结构(如 ResNet bottleneck 中的 residual connection 如果退化为加偏置)或者模型转换过程中。将其融合,可以消除冗余的加法操作,进一步提升推理效率。
TL;DR
目标: 将 Convolution 层后接一个扮演逐通道加法 (Per-Channel Bias Addition) 角色的 BinaryOp 层(操作类型为 Add,且第二个输入来自 MemoryData)进行融合。
模式匹配: 查找 Convolution -> BinaryOp 结构,并附加严格条件:
BinaryOp 的类型必须是加法 (op_type == 0)。
Bi ...
读 ncnn 源码(XVI):`ncnnoptimize`——神经网络图优化的“炼金术”
读 ncnn 源码(XVI):ncnnoptimize——神经网络图优化的“炼金术”
在之前的篇章中,我们已经深入了解了 ncnn 如何加载模型 (Net::load_param/model)、执行推理 (Extractor::extract) 以及内部精巧的设计模式 (Pimpl)。现在,我们将目光投向一个独立但至关重要的工具——ncnnoptimize。这个工具扮演着“炼金术士”的角色,它接收原始的 ncnn 模型文件,通过一系列图优化 (Graph Optimization) 操作,将其“精炼”成一个更小、更快、更高效的版本,而完全不改变模型的数学等价性。
本篇,我们将剖析 ncnnoptimize.cpp 的 main 函数流程,并以 fuse_batchnorm_scale 为例,初步探索 ncnn 图优化的基本原理和实现方式。
TL;DR
ncnnoptimize 的定位: 一个离线 (Offline) 模型优化工具。它读取 ncnn 的 .param 和 .bin 文件,执行一系列图优化 Pass,然后输出优化后的新 .param 和 .bin 文件。
核心机制 ...
读 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, ...



















