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 ...
读 ncnn 源码(XXXVII):`eliminate_orphaned_memorydata`——图优化的“垃圾回收”
读 ncnn 源码(XXXVII):eliminate_orphaned_memorydata——图优化的“垃圾回收”
在 ncnnoptimize 的图优化篇章中,我们已经见证了多种“算子融合”和“算子消除” Pass。这些优化操作(如 fuse_memorydata_binaryop)在将计算合并、烘焙参数的同时,往往会使原图中的某些层(如 MemoryData)的输出不再被任何其他层所需要,从而使其成为“孤儿”节点。
本篇,我们将剖析 eliminate_orphaned_memorydata 这一优化 Pass,它扮演着图优化流水线中**“垃圾回收器”(Garbage Collector)**的角色,专门负责清理这些因上游优化而产生的冗余数据层。
TL;DR
目标: 识别并消除那些输出不再被任何有效层(non-fused)所消费的 MemoryData 层。
动机: 这是一个清理(Cleanup)Pass。在 fuse_memorydata_binaryop, fuse_convolution_add 等 Pass 中,MemoryData 层(作为常量/偏置的提供者 ...
读 ncnn 源码(XXXVI):算子消除——`eliminate_dropout/pooling1x1/noop/split`
读 ncnn 源码(XXXVI):算子消除——eliminate_dropout/pooling1x1/noop/split
在 ncnnoptimize 的图优化工具箱中,除了“算子融合”(如 Conv+BN)和“算子替换”(如 PReLU->ReLU),还有一类至关重要的优化 Pass:算子消除 (Operator Elimination)。这类优化的目标是识别并彻底移除那些在推理阶段计算无效或结构冗余的层。
本篇,我们将集中分析 eliminate_dropout, eliminate_pooling1x1, eliminate_noop, 和 eliminate_split 这四个函数。它们虽然处理的层类型不同,但其核心思想和图修改手法几乎完全一致:找到“无效层”,并将其从计算图中“短路”掉。
TL;DR
目标: 识别并消除四种在特定条件下对计算图无实际贡献的层:Dropout(推理时)、Pooling(1x1, s1)、Noop、Split(单路输出)。
核心原理 (Identity Mapping): 这些层在特定配置下都退化成了恒等映射 (Identity ...
读 ncnn 源码(XXXV):`fuse_binaryop_eltwise`——识别加权求和并替换为 Eltwise
读 ncnn 源码(XXXV):fuse_binaryop_eltwise——识别加权求和并替换为 Eltwise
在 ncnnoptimize 的图优化篇章中,我们已经见证了多种“算子融合”(如 Conv+BN)和“算子消除”(如 eliminate_dropout)。本篇,我们将探讨一种更高级的优化:基于模式识别的算子替换 (Pattern-based Operator Replacement)。fuse_binaryop_eltwise 就是其典型代表。
它的核心任务是识别出一种由多个 BinaryOp 层构成的、在语义上等价于**“加权求和”的计算模式,并将其替换**为一个功能更强、性能更高的 Eltwise 专用层。
TL;DR
目标: 识别 Y = (A * C0) + (B * C1) 这样的加权求和模式。A 和 B 是特征图(Tensor),C0 和 C1 是标量(Scalar)。
模式匹配: ncnnoptimize 查找一个 BinaryOp(Add, Tensor, Tensor) 作为中心,然后反向查找其两个输入 A 和 B 的生产者:
Case 1 ...
读 ncnn 源码(XXXIV):`fuse_memorydata_binaryop`——将“常量”烘焙进“算子”
读 ncnn 源码(XXXIV):fuse_memorydata_binaryop——将“常量”烘焙进“算子”
在 ncnnoptimize 的图优化篇章中,我们已经见证了多种“算子融合”(Operator Fusion),它们将两个计算层(如 Conv 和 BN)合并为一个。本篇,我们将探讨一种不同但同样重要的优化:将数据层(MemoryData)与计算层(BinaryOp)融合。
这在本质上是一种常量折叠 (Constant Folding) 或 常量传播 (Constant Propagation)。fuse_memorydata_binaryop 的核心任务是识别出 BinaryOp(如加、减、乘、除)的一个输入是来自 MemoryData 的标量(Scalar),并将这个标量“烘焙”到 BinaryOp 的参数中,将其从一个昂贵的“张量-张量”操作,降级为一个高效的“张量-标量”操作。
TL;DR
目标: 识别 BinaryOp 的一个输入是来自 MemoryData 层的单个标量值的模式。
优化: 将 BinaryOp 转换为其高效的**“标量模式”**(with ...


