Mini-Infer (8): Im2Col算法完全讲解
Mini-Infer (8): Im2Col算法完全讲解
🎯 核心概念:为什么需要Im2Col?
问题:卷积计算很慢
朴素卷积需要7层嵌套循环:
123456789// 超级慢!缓存不友好for (batch) for (out_channel) for (in_channel) for (kernel_h) for (kernel_w) for (out_h) for (out_w) output += input * weight
解决方案:转换为矩阵乘法
Im2Col的魔法:
12345678卷积运算 = 矩阵乘法Output = Conv(Input, Weight) ↓ 转换Output = Weight × col_buffer然后用高度优化的GEMM库(如MKL)计算→ 速度提升5-10倍!
📊 具体例子:一步步理解
输入参数
1234567891011121314151617181920212223242526272829输入图像(灰度图): ...
Mini-Infer (7.6): 架构重构 - 用“模板元编程”消除内核注册的“样板戏”
Mini-Infer (7.6): 架构重构 - 用“模板元编程”消除内核注册的“样板戏”
1. 问题的本质:一个“模板”的“模板”
我们的问题是:KernelRegistry (注册表) 的类型,依赖于函数指针的类型,而函数指针的类型又依赖于数据类型 (float, int)。
GEMM_NT for float -> void(*)(const float*, ...)
GEMM_NT for int32 -> void(*)(const int32_t*, ...)
这是一个清晰的模板模式。我们可以把 GEMM_NT 的函数签名定义为一个“函数类型模板”:
12template<typename T>using GEMMFunc_NT = void(*)(const T* A, const T* B, T* C, int M, int N, int K);
现在,我们的问题演变为:如何创建一个通用的 KernelRegistry,它接受 GEMMFunc_NT 这样的**“模板”**作为参数,然后再由用户指定 T(如 float)?
2. 解决方 ...
Mini-Infer (7.5): 架构的“魔鬼细节” - 深入辩论“内核注册”
Mini-Infer (7.5): 架构的“魔鬼细节” - 深入辩论“内核注册”
在 Blog 7 中,我们设计了一个“自注册内核注册表”。这个设计看起来很“酷”,但也引入了大量复杂性:AutoRegister 宏、KernelRegistryInitializer…
本文讨论以下问题:
register_kernel 里的 std::sort 每注册一次就排一次,不会有性能压力吗?
KernelRegistryInitializer::initialize() 为什么需要被“显式”调用?
(最尖锐的)KernelRegistryInitializer 每次添加新内核都要修改,这难道不违反“开闭原则” (OCP) 吗?
(终极问题)既然静态库链接这么麻烦,为什么不直接用动态库 (.so/.dll) ??
本篇,我们将直面这些问题。
1. 终极问题:高性能推理框架的“链接之战” (静 vs. 动)
这是一个关乎 Mini-Infer 核心定位的战略问题。
动态库 (.so/.dll) 是“灵活性”的王者。
静态库 (.a/.lib) 是“性能”的王者。
“自动注册‘魔法’”是真 ...
Mini-Infer (7): 高性能“内核注册表” (A TensorRT-Style Kernel Registry)
Mini-Infer (7): 高性能“内核注册表” (A TensorRT-Style Kernel Registry)
1. 架构目标:从“静态分发”到“动态注册”
我们的新目标是:
解耦:GEMMKernel(调度器)不应该“知道”任何具体的实现(如 avx2_gemm_impl)。
可扩展:添加一个新的 AVX512 内核,应该不需要修改任何现有的 GEMMKernel 代码。
高性能:系统必须能自动检测硬件能力,并优先选择最快的可用内核(例如,cuBLAS > AVX2 > CPU)。
为了实现这一点,我们将构建一个“内核电话簿”(Registry),每个内核实现(AVX2、CUDA…)都会在启动时自动将其“电话号码”(函数指针)和“能力”注册到这个“电话簿”中。
2. 核心设计:KernelRegistryBase (kernel_registry.h)
这是我们的“电话簿”模板。它是一个通用的 C++ 模板类,可以为任何类型的内核(GEMM, im2col…)管理一个实现列表。
12345678910111213141516171819202122 ...
Mini-Infer (6): 点亮引擎!实现 `infer_shape`, `ReLU` 与 `GEMM` 抽象
Mini-Infer (6): 点亮引擎!实现 infer_shape, ReLU 与 GEMM 抽象
本篇,我们将真正“闭合” Engine 的执行循环。为此,我们必须完成两项核心任务:
实现 infer_shape:这是 Engine 进行“静态内存规划”的钥匙。
实现 forward:编写第一个 Operator(ReLU)的 CPU 计算代码。
我们还将实现一个更复杂的 Linear(全连接)算子,并引出一个全新的、为性能而生的架构层:Kernel 抽象。
1. 缺失的环节:infer_shape 与内存预分配
在第 5 篇中,我们的 Engine::build() 流水线卡在了 allocate_tensors()。Engine 不知道 Convolution 的输出是多大,也不知道 Linear 的输出是多大。
Operator 基类中的 infer_shape 纯虚函数就是为此而生的“合约”。它要求每个算子必须有能力“只通过输入的 *Shape*,就计算出输出的 *Shape*”。
ReLU 是最简单的例子:它不改变形状。
12345678910111213141 ...
Mini-Infer 架构深潜 (5): `Engine` - 联结万物的“总指挥”
Mini-Infer 架构深潜 (5): Engine - 联结万物的“总指挥”
1. Engine 的设计哲学:编译与执行的分离
一个推理引擎的 API 设计,最关键的一点是必须分离“一次性”的准备工作和“高频”的执行工作。
build()(编译): 加载模型、图优化、拓扑排序、内存分配。这些操作非常昂贵,但我们只需要做一次。
forward()(执行): 运行模型。这个操作必须极其轻量,因为它会被调用成千上万次。
Engine 类的接口 (engine.h) 完美地体现了这种分离。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566// mini_infer/runtime/engine.h#pragma once#include "mini_infer/graph/graph.h"#include "mini_infer/backends/backend.h"// ...name ...
Mini-Infer 架构深潜 (4): Graph 与 Node - 编织计算“神经网”
Mini-Infer 架构深潜 (4): Graph 与 Node - 编织计算“神经网”
引言:从“算子”到“网络”
在上一篇文章中,我们构建了 Operator 抽象和一个“自注册”工厂。我们现在有能力创建独立的计算单元(如 “ReLU”, “Convolution”)。
但一个神经网络不是一堆孤立的算子,它是一个有向无环图 (DAG)。数据必须从 Input 流向 Convolution,再流向 ReLU,最终到达 Output。
我们如何描述这种连接关系和执行顺序?
本篇,我们将构建 Mini-Infer 的“骨架”:Graph(图)和 Node(节点)。
1. Node:图的基本单元 (node.h)
Node 是我们图结构中最基本的“原子”。它是一个轻量级的数据容器,其职责是“连接”。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// mini_infer/graph/node.h#pragma once#include "mini_ ...
Mini-Infer 架构深潜 (3): Operator 抽象与“自注册”工厂
Mini-Infer 架构深潜 (3): Operator 抽象与“自注册”工厂
引言:缺失的“计算”拼图
在前两篇文章中,我们构建了 Mini-Infer 的数据 (Tensor) 和执行上下文 (Backend)。我们已经有了坚实的地基,但大厦至今仍是“空”的——它没有任何“功能”。
我们如何定义一个“卷积”操作?如何定义一个 “ReLU” 激活?最重要的是,我们的 Net (计算图) 如何在不“写死”依赖的情况下,动态地加载和执行这些操作?
本篇,我们将构建 Mini-Infer 的计算核心:Operator 抽象层。我们将使用 C++ 中一个极其精妙的模式——工厂 (Factory) + 自动注册 (Self-Registration)——来实现一个真正可插拔、可扩展的算子系统。
1. Operator 接口:“计算”的合约 (operator.h)
首先,我们必须定义一个标准“合约”,所有计算单元(卷积、激活、池化等)都必须遵守这个合约。这就是抽象基类 Operator 的职责。
12345678910111213141516171819202122232425262 ...
Mini-Infer 架构深潜 (2): 抽象 `Backend` 层 - 解耦异构计算
Mini-Infer 架构深潜 (2): 抽象 Backend 层 - 解耦异构计算
引言:从“数据”到“执行上下文”
在上一篇文章中,我们为 Mini-Infer 奠定了数据基石:一个健壮、内存安全的 Tensor 类。我们通过 Allocator 接口解耦了内存的“来源”。
然而,一个现代推理框架不仅要处理“来自哪里”的内存(CPU vs. CUDA),还必须处理“在哪里执行”以及“如何操作”这些内存。
在 CPU 上,memcpy (内存拷贝) 是一个简单的 std::memcpy。但在 GPU 上,它是一个必须通过 CUDA API 调用的 cudaMemcpy,一个涉及总线通信的复杂异步操作。
本篇,我们将构建 Mini-Infer 的后端抽象层 (Backend)。这是一个至关重要的层,它将“计算”与“硬件”彻底分离,使我们的框架能够驾驭 CPU、GPU 等不同的异构计算设备。
1. 基础类型:定义框架的“通用语言” (types.h)
在构建抽象层之前,我们需要一套通用的“词汇”来描述状态和设备。types.h 文件为此而生。
Status: 一个强类型的 en ...
Mini-Infer 架构深潜 (1): 构建高性能、可扩展的 `Tensor` 基石
Mini-Infer 架构深潜 (1): 构建高性能、可扩展的 Tensor 基石
引言:地基的设计哲学
在任何深度学习推理框架中,Tensor (张量) 都是其绝对的核心。它不仅是数据的载体,其设计本身也直接决定了框架的性能、内存效率和可扩展性(例如,CPU 到 GPU 的移植)。
Mini-Infer 项目的开篇,正是从构建这一核心基石开始。一个专业级的 Tensor 设计必须优雅地解决三个核心问题:
内存管理 (Allocation): Tensor 的数据存在哪里?它如何被安全、高效地申请与释放?
数据描述 (Description): Tensor 的形状 (Shape) 和数据类型 (DataType) 如何被精确表达?
资源所有权 (Ownership): Tensor 在C++中如何被传递和管理?它的拷贝、移动语义是怎样的?
本文将深度剖析 Mini-Infer foundational (基础) 层的设计,分析其如何通过 C++ 的现代特性,为这三个问题提供了健壮且高性能的答案。
1. 内存解耦:Allocator 抽象层
Tensor 设计的第一个挑战 ...
读 ncnn 源码(XLII):`ncnnoptimize` 的“编排艺术”——优化 Pass 的依赖与顺序 (ncnnoptimize 完结篇)
读 ncnn 源码(XLII):ncnnoptimize 的“编排艺术”——优化 Pass 的依赖与顺序 (ncnnoptimize 完结篇)
在 ncnnoptimize 系列的前序篇章中,我们已经像解剖学家一样,逐一分析了 fuse_...(融合)、eliminate_...(消除)和 replace_...(替换)三大类优化 Pass 的具体实现。我们理解了 Conv+BN 融合的代数原理,eliminate_dropout 对推理的净化,以及 Conv->IP 替换的语义等价性。
现在,是时候退后一步,从 ncnnoptimize.cpp 的 main 函数视角,欣赏这出优化大戏是如何编排的。main 函数中那几十行看似平铺直叙的 optimizer.fuse_...() 调用,绝非随意的罗列,而是一个经过精心设计的、存在严格依赖关系的多遍(Multi-Pass)优化流水线。本篇,我们就来揭示这个“编排”背后的工程智慧。
TL;DR
ncnnoptimize 的本质: 它是一个多遍(Multi-Pass)图优化编译器。main 函数定义了所有优化 Pass 的固 ...
读 ncnn 源码(XLI):`shape_inference`——图优化的“沙盘推演”与“状态重整”
读 ncnn 源码(XLI):shape_inference——图优化的“沙盘推演”与“状态重整”
ncnnoptimize 的工作流是一系列对计算图的破坏性修改(融合、消除、替换)。这些操作的副作用是,许多中间 Blob 的维度信息(shape)可能会变得陈旧或不明确。shape_inference Pass 的职责并非“优化”,而是一个至关重要的**“工具型 Pass”**。
它的核心任务是:通过一次模拟的“沙盘推演”(Dry Run),在不执行实际数值计算的前提下,强制 ncnn 的推理引擎(Extractor)完整地跑一遍优化后的计算图,从而“推演”并“记录”下每一个 Blob 的确切形状信息。
TL;DR
目标: 在所有图优化 Pass 执行完毕后,为网络中的每一个 Blob 重新计算并填充其 shape 属性(dims, w, h, c, elempack),并将其回填到每个 Layer 的 bottom_shapes 和 top_shapes 成员中。
为何必须:
状态更新: 优化 Pass(如 fuse_...)会改变 Layer 的参数(如 activat ...
读 ncnn 源码(XL):算子替换——当“卷积”退化为“全连接”
读 ncnn 源码(XL):算子替换——当“卷积”退化为“全连接”
在 ncnnoptimize 的优化策略中,“算子替换”是提升性能的关键手段之一。我们之前分析的 replace_prelu_with_leaky_relu 是一个典型。本篇,我们将深入探讨 replace_convolution_with_innerproduct_after_global_pooling 和 replace_convolution_with_innerproduct_after_innerproduct 这两个 Pass,它们联手优化了神经网络(尤其是分类头部)中一个极其常见的低效模式。
TL;DR
目标: 识别并替换 GlobalAveragePooling -> Convolution 和 InnerProduct -> Convolution 这样的计算模式。
核心原理 (语义等价): GlobalAveragePooling (GAP) 和 InnerProduct (IP) 的输出都是空间维度为 1x1 的张量(即 [1, 1, C] 或 [N, 1, 1])。当一个 ...
在 VS Code 的 PowerShell 终端里自动加载 VS2022 开发环境(Dev Shell)——完整指南
在 VS Code 的 PowerShell 终端里自动加载 VS2022 开发环境(Dev Shell)——完整指南
很多人用 CLion 或 VS 的“开发者命令提示符”能顺利编译 C++,但在 VS Code 的集成终端里却会报:
fatal error C1083: cannot open include file: 'memory': No such file or directory
cannot open include file: 'cstddef' ...
根因通常是:终端会话没有加载 MSVC/Windows SDK 的 INCLUDE/LIB/Path。在 Visual Studio 家族里,这件事由 DevCmd.bat 或 **DevShell(PowerShell 模块)**来完成。本文给出一套“在 VS Code 的 PowerShell 终端里自动加载 VS2022 开发环境”的稳妥方案。
TL;DR:直接可用的 VS Code 配置(Windows PowerShell)
在 VS Code 打开 设置 → 搜索 terminal profi ...
读 ncnn 源码(XXXIX):消除冗余塑形(下)——`eliminate_reshape_before_binaryop`
读 ncnn 源码(XXXIX):消除冗余塑形(下)——eliminate_reshape_before_binaryop
ncnnoptimize 的图优化 Pass 中,对 Reshape 和 Flatten 等塑形层(Shape-manipulation layers)的消除是一个重要主题。上一篇我们看到了 GlobalAveragePooling 和 InnerProduct 因其输出在语义上已是“扁平”的,从而使其后的 Reshape/Flatten 变得多余。
本篇分析的 eliminate_reshape_before_binaryop 则利用了 BinaryOp 层自身实现的健壮性(Robustness),来消除其输入端的多余 Reshape 操作。
TL;DR
目标: 识别并消除 ... -> Reshape(to 1x1xC) -> BinaryOp 这样的模式。
核心原理 (BinaryOp 的“形状无关性”): ncnn::BinaryOp(如 Add, Mul 等)在执行两个张量的逐元素操作时,其 forward 实现在根本上是“形状无关 ...
读 ncnn 源码(XXXVIII):消除冗余塑形——`eliminate_flatten/reshape` 的瘦身之道
读 ncnn 源码(XXXVIII):消除冗余塑形——eliminate_flatten/reshape 的瘦身之道
在 ncnnoptimize 的图优化工具箱中,“算子消除”是一个重要分支。它旨在移除那些对计算结果没有贡献的冗余层。Dropout(推理时)、Pooling(1x1) 和 Noop 是因其“恒等映射”特性而被消除的典型。本篇,我们将分析另一类可被消除的层:Reshape 和 Flatten,它们在特定上下文中同样是“恒等映射”。
eliminate_..._after_global_pooling 和 eliminate_flatten_after_innerproduct 这一系列 Pass 关注的就是 GlobalAveragePooling 或 InnerProduct 之后紧跟的 Flatten 或 Reshape 操作。
TL;DR
目标: 识别并消除 GlobalAveragePooling -> Flatten/Reshape 和 InnerProduct -> Flatten 这样的冗余塑形模式。
核心原理 (语义上的 No-op ...






