2. DNN加速器基础:CNN基本运算与存储瓶颈

各位同学,今天我们来聊聊DNN加速器的基础。说白了,就是先搞清楚CNN到底在算什么,然后看看为什么通用处理器搞不定,最后引出专用加速器的必要性。

我个人习惯,每次讲架构之前,先回到最朴素的运算本身。你想想看,如果连卷积、池化、全连接这些基本操作都没吃透,后面谈什么数据流、脉动阵列都是空中楼阁。

2.1 卷积运算:DNN的主力

卷积是CNN里最核心、也是最耗时的操作。我当年第一次做硬件加速时,发现90%以上的计算时间都花在卷积上,一点都不夸张。

一个典型的2D卷积,输入是H×W×C的特征图,卷积核是K×K×C,输出是R×S×M。每次卷积,其实就是做一个乘累加(MAC)操作:

// 伪代码:单次卷积计算
for (r = 0; r < R; r++)
  for (s = 0; s < S; s++)
    for (m = 0; m < M; m++)
      for (c = 0; c < C; c++)
        for (i = 0; i < K; i++)
          for (j = 0; j < K; j++)
            output[r][s][m] += 
              input[r*stride+i][s*stride+j][c] * weight[i][j][c][m];

看到这个六层循环了吗?这就是计算量的来源。一个典型的AlexNet,卷积层需要做约7亿次MAC操作。嗯,这里要注意,每次MAC包含一次乘法和一次加法,所以实际浮点操作数是14亿次。

关键数据:一次卷积的MAC次数 = R × S × M × C × K × K。以AlexNet第一层为例:R=55, S=55, M=96, C=3, K=11 → 约1.05亿次MAC。

2.2 池化运算:降维与特征筛选

池化相对简单,但同样重要。我见过不少新手,觉得池化就是取个最大值或平均值,没什么好优化的。其实不然。

池化层的作用有两个:一是降低特征图的空间维度,减少后续计算量;二是提供一定的平移不变性。常见的池化有两种:

池化类型 计算方式 特点 典型应用
最大池化 取窗口内最大值 保留最显著特征,对纹理敏感 图像分类网络
平均池化 取窗口内平均值 保留整体分布,对背景敏感 全连接前降维

池化的计算量虽然不大,但它的访存模式很特殊——非连续、跨步长访问。我在项目中遇到过,有些加速器在池化层上反而因为访存冲突导致性能下降。所以别小看它。

2.3 全连接运算:参数大户

全连接层,说白了就是一个矩阵向量乘法。输入向量x,权重矩阵W,输出向量y:

y = W × x + b

全连接层的计算量其实不算特别大,但它的参数数量惊人。举个例子,VGG-16的第一个全连接层,输入是25088维,输出是4096维,光这一层就有25088×4096 ≈ 1.03亿个参数。

避坑指南:我曾经在设计一个边缘端加速器时,忽略了全连接层的参数存储需求。结果发现片上SRAM根本放不下,不得不频繁访问DDR,功耗直接翻倍。全连接层的参数占比,往往比计算量占比更值得关注。

2.4 计算与存储瓶颈分析

好了,现在我们把三种基本运算都过了一遍。接下来,我们来分析一下,为什么通用处理器搞不定这些计算。

我画了一张图,把CNN的计算与存储瓶颈梳理了一下:

CNN计算与存储瓶颈分析 计算瓶颈 MAC操作密集,并行度低 存储瓶颈 参数与中间结果巨大 带宽瓶颈 数据搬运速度跟不上 计算瓶颈细节 • 卷积:6层循环,数据重用差 • 池化:非连续访存,效率低 • 全连接:矩阵乘法,并行度受限 • 通用CPU:SIMD宽度有限 存储瓶颈细节 • 权重:VGG-16约138M参数 • 中间特征图:ResNet-50约10MB • 片上SRAM:通常仅几百KB • 片外DRAM:带宽有限,功耗高 带宽瓶颈细节 • 数据重用率低:每次MAC都需 从DRAM读取新数据 • 带宽需求:AlexNet约1.2GB/s • 实际可用:嵌入式系统<10GB/s 结论:通用处理器无法满足需求 需要专用DNN加速器

从这张图可以看得很清楚。三个瓶颈互相交织,最终指向一个结论:通用处理器搞不定。

为什么会这样?我简单算一笔账:

  • 计算方面:一个典型的CNN推理,需要做数亿到数十亿次MAC操作。CPU的ALU数量有限,即使有SIMD指令集,并行度也远远不够。GPU虽然并行度高,但功耗太大,不适合嵌入式场景。
  • 存储方面:权重参数动辄几十MB到几百MB,片上SRAM根本放不下。每次从片外DRAM读取数据,功耗是片上访问的100倍以上。
  • 带宽方面:数据重用率低导致频繁的DRAM访问。我实测过一个MobileNet在树莓派上的推理,超过60%的时间花在等待数据上。

我的经验:判断一个加速器设计好不好,我通常会先看它的数据重用策略。如果能把数据重用率提高10倍,带宽需求就能降低一个数量级。这是Eyeriss架构的核心思想之一,后面我们会详细讲。

2.5 专用加速器的必要性

好了,现在我们来回答一个核心问题:为什么需要专用加速器?

说白了,通用处理器(CPU/GPU)的设计目标是通用性,它要能跑各种程序。但DNN计算有非常特殊的模式:

  1. 计算模式固定:卷积、池化、全连接,就这三种基本操作,循环结构高度规律。
  2. 数据流模式固定:特征图、权重、偏置,数据流动方向可预测。
  3. 精度要求灵活:推理场景下,8位整数甚至更低精度就够用,不需要64位浮点。
  4. 并行度极高:每个输出像素的计算相互独立,天然适合大规模并行。

专用加速器可以针对这些特点做极致优化:

优化方向 通用处理器 专用加速器
计算单元 少量通用ALU 大量MAC阵列(如256个)
存储层次 通用缓存 专用片上存储(权重/特征图分离)
数据流 指令驱动,不可预测 静态调度,可预测
精度 FP32/FP64 INT8/INT4,甚至二值化
能效比 ~10 TOPS/W ~100 TOPS/W(Eyeriss级别)

我当年参与的第一个加速器项目,目标是在1W功耗内实现1TOPS的算力。用CPU?不可能。用GPU?功耗至少10W起步。最后我们选择了专用架构,才勉强达标。

核心观点:专用DNN加速器的本质,是用硬件资源换取能效比。把固定的计算模式固化在硬件中,去掉指令取指、译码、调度等开销,把每一焦耳的能量都用在刀刃上。