深度学习模型基础:神经网络基本概念、常见算子与计算图
各位同学,今天我们聊聊深度学习模型的基础。说实话,这部分内容看起来简单,但却是整个AI芯片编译器的地基。我在做编译器优化时,发现很多性能瓶颈都源于对模型底层理解不够深。你想想看,连算子都认不全,怎么去优化它?
神经网络基本概念
神经网络,说白了就是一个多层函数组合器。每一层都在做一件事:把输入数据做某种变换,然后传给下一层。我习惯把神经网络想象成一个流水线工厂——原材料进来,经过一道道工序,最后产出成品。
这里有几个核心概念,我建议你记牢:
- 神经元:最基本的计算单元,做加权求和加激活
- 层:多个神经元组成一层,常见的有卷积层、池化层、全连接层
- 权重与偏置:网络要学习的参数,说白了就是决定变换方式的系数
- 前向传播:数据从输入到输出的计算过程
- 反向传播:根据损失函数梯度更新参数的过程
我个人经验:刚开始做编译器时,我总以为只要把前向传播优化好就行。后来发现,反向传播的算子(比如卷积的梯度计算)才是真正的性能杀手。很多芯片在推理时跑得飞快,一训练就卡住,问题往往出在这里。
常见算子详解
算子(Operator)是神经网络的基本操作单元。作为编译器开发者,你必须对每个算子的计算特征了如指掌。下面我挑几个最常用的来讲。
卷积(Convolution)
卷积是CNN的绝对核心。它做的事情很简单:用一个滑动窗口(卷积核)在输入特征图上扫一遍,每个位置做一次点积运算。
我在项目中遇到过一个问题:同样的卷积,在不同硬件上性能差异巨大。原因在于卷积的计算模式——它本质上是数据复用和内存访问的博弈。
// 卷积的伪代码示意
for (int oh = 0; oh < output_height; oh++) {
for (int ow = 0; ow < output_width; ow++) {
for (int ic = 0; ic < input_channels; ic++) {
for (int kh = 0; kh < kernel_height; kh++) {
for (int kw = 0; kw < kernel_width; kw++) {
output[oh][ow][oc] +=
input[oh+kh][ow+kw][ic] * weight[kh][kw][ic][oc];
}
}
}
}
}
嗯,这里要注意:卷积的循环嵌套顺序直接影响缓存命中率。我见过不少团队把循环顺序搞反,导致性能直接腰斩。
池化(Pooling)
池化算子,说白了就是降采样。常见的有最大池化和平均池化。它的计算量不大,但内存访问模式很特殊——非连续、跨步长访问。
避坑指南:我曾经在优化池化算子时,想当然地用了连续内存访问策略,结果发现池化的输入输出尺寸不匹配,导致大量边界判断开销。后来改用分块处理,性能提升了30%。
全连接(Fully Connected)
全连接层就是矩阵乘法。每个输出神经元和所有输入神经元相连。它的计算密度很高,但参数也最多。
你想想看,一个1024x1024的全连接层,光权重就有100万个参数。这对芯片的存储带宽是个巨大考验。我建议在做编译器调度时,把全连接层的权重尽可能放在片上缓存里。
激活函数(Activation)
激活函数给网络引入非线性。常见的包括ReLU、Sigmoid、Tanh等。从编译器角度看,激活函数是逐元素操作,计算量小,但访存开销占比高。
我个人习惯把激活函数和前面的卷积或全连接做算子融合。这样能减少一次数据搬运,性能提升很明显。
| 算子类型 | 计算特征 | 访存特征 | 优化重点 |
|---|---|---|---|
| 卷积 | 计算密集,数据复用 | 滑动窗口,局部性较好 | 循环分块、数据重排 |
| 池化 | 计算简单 | 非连续访问 | 边界处理、向量化 |
| 全连接 | 计算密集,参数多 | 连续访问 | 矩阵分块、权重缓存 |
| 激活函数 | 逐元素,计算轻量 | 访存开销占比高 | 算子融合 |
计算图与数据流图
计算图是深度学习框架的核心数据结构。它把网络表示成一个有向无环图(DAG),节点是算子,边是数据依赖关系。
为什么会用图来表示?因为图能清晰地展示算子之间的依赖关系和并行机会。我在做编译器时,第一步就是把模型解析成计算图,然后做各种图优化。
计算图 vs 数据流图
这两个概念容易混淆,我简单区分一下:
- 计算图:描述计算逻辑,节点是算子,边是张量数据
- 数据流图:描述数据流动,节点是数据操作,边是数据依赖
其实在大多数框架里,两者是等价的。但编译器更关注数据流图,因为它能告诉我们:哪些数据可以复用?哪些计算可以并行?
注意:计算图中的控制流(如if-else、循环)处理起来很麻烦。我曾经在一个动态网络模型上栽过跟头——图结构在运行时变化,导致编译器的静态优化全部失效。后来我们引入了动态形状处理机制才解决。
计算图的核心作用
从编译器角度看,计算图有三大作用:
- 依赖分析:确定算子执行顺序,找出可并行部分
- 内存规划:分析张量生命周期,优化内存复用
- 图优化:做算子融合、常量折叠、死代码消除等
下面我用一张图来展示计算图的核心逻辑:
这张图展示了一个典型的CNN子图。数据从输入开始,经过卷积、激活、池化,再到全连接,最后输出。编译器要做的就是分析这些节点之间的依赖关系,然后生成高效的执行计划。
小结
今天的内容就到这里。神经网络的基本概念、常见算子、计算图与数据流图,这些都是AI芯片编译器的基础。我个人建议你多动手画计算图,把每个算子的输入输出形状、计算量、访存量都标出来。这样做久了,你自然就能看出哪些地方可以优化。
记住一句话:理解模型,才能优化模型。编译器不是魔法,它只是把人对模型的理解转化为高效的机器指令。