【Deploy】The Cost of Skip Connections

在这里插入图片描述


“skip-connections or branching can incur significant memory access costs”

意思是:跳连(skip connection)和分支结构(branching)虽然计算量不一定大,但会导致很多"搬运数据"的开销。 在真实硬件(GPU/NPU/CPU)里,👉 "搬数据"很多时候比"做计算"还慢,这是很多深度学习初学者最容易忽略的点。


一、先理解:AI 芯片最怕什么

很多人以为"卷积最耗时,因为数学运算很多",其实现代 GPU 里加法乘法(MAC,multiply-accumulate)非常快,真正慢的是:从显存读取数据、写回显存、等待数据、数据拷贝。

也就是 "算"不贵,"搬"才贵。就像小学生做 2+3 很快,但如果每次做题都要跑去隔壁教室拿纸,就很慢。

在这里插入图片描述


二、什么是 skip connection(跳连)

以 ResNet 为例:

skip 跳连

x

Conv

Conv

+ 相加

输出

输出 = Conv(Conv(x)) + x,这个 x 被"跳过去"直接加到后面,这就是 skip-connection。

在这里插入图片描述


三、为什么 skip connection 会增加 memory access cost

先看理想的普通卷积 输入 → Conv → 输出,GPU 可以"边读边算边写",数据流非常顺,像流水线工厂"材料 → 加工 → 出货"一样连续。

但 skip connection 打断了这种连续:

需先写显存暂存 x

加法时再读回
额外 memory read

x

Conv

Conv

+ 相加

输出

缓存 x
额外 memory write

于是 GPU 必须多做三件事。

1. 先保存原始 x:因为后面还要加回来,x 不能立刻丢掉,必须写入显存或缓存到 SRAM——这产生一次额外 memory write(注:若做了算子融合,x 未必真的落显存)。

2. 后面再读回来:到加法 Conv结果 + x 时,GPU 还得把 x 读出来,产生一次额外 memory read

3. 最后做 element-wise add y[i] = a[i] + b[i]:计算虽只有加法,但需要读取两个 tensor、再写回一个 tensor,数据搬运量巨大


四、真正的问题:Add 很便宜,但 Tensor 很大

比如一个 256 × 256 × 128 的 feature map,就有几十 MB。你以为 + 运算很简单,但实际上 GPU 在疯狂地"读内存、读内存、写内存"。


补充一、用 Roofline 量化:add 到底有多"内存墙"

前面都是比喻,这里给个硬数字。衡量一个算子是"缺算力"还是"缺带宽",看它的算术强度(arithmetic intensity)= 计算量 ÷ 访存量(FLOP/字节):这个值越低,越是被内存带宽卡住。

以 FP32 的 element-wise add 为例,每个元素读 a(4B)、读 b(4B)、写 y(4B),共 12 字节,却只做 1 次加法:

算子每元素计算每元素访存算术强度结论
element-wise add1 FLOP12 字节0.08 FLOP/字节极度内存受限
3×3 卷积(权重高度复用)几十~上百 FLOP少量数十 FLOP/字节相对计算受限

而现代 GPU 的"机器平衡点"(compute÷bandwidth)大约在十几到上百 FLOP/字节之间(越用低精度 Tensor Core,这个点越高)。add 的 0.08 远远落在平衡点左侧,意味着无论芯片算力堆多高,它的耗时都由带宽决定——这就是所谓"内存墙"。这也解释了第八节的现象:FLOPs 降了,但落在内存墙左侧的算子,latency 一点没动。


五、branching(分支)为什么也麻烦

以 Inception 为例,一个输入被分成多路:

输入 x

3x3 Conv
(快)

5x5 Conv
(慢)

concat
需等全部分支 + 重组内存

GPU 最喜欢"一条直线",最不喜欢"分叉",原因有三。

1. 输入要复制到多个分支x 同时喂给 branch1、branch2、branch3,虽然不一定真的 memcpy,但硬件要多次读取 x、多个 kernel 使用 x,导致 memory bandwidth 压力巨大

2. 各分支执行速度不同:3x3 conv 很快、5x5 conv 很慢,而 concat 必须等所有分支完成,于是快的计算单元开始"发呆等待",这叫 synchronization overhead(同步等待开销)

3. concat 又要重新组织内存[A, B, C] → concat 要重新申请输出 tensor、把多个 feature map 拼接,这又是大量 memory copy

在这里插入图片描述


六、为什么移动端网络特别讨厌 branching

移动端算力有限,而相对而言内存带宽/访存是更紧的瓶颈——手机 NPU 往往"算得快、搬不动"。所以很多移动端网络尽量不用复杂 branch、不用 concat、减少 skip,代表作如 ShuffleNetV2、MobileNetV2、RepVGG,都在拼命减少 memory access。(ShuffleNetV2 论文的四条设计准则,核心就是围绕 MAC 展开。)


七、一个非常形象的例子(最重要)

情况1:直线流水线(普通 Conv)——工人1 → 工人2 → 工人3,物料一路往前传,效率极高。

情况2:skip-connection——工人1 做完,还得把原材料存进仓库,等后面再取回来,仓库搬运次数增加。

情况3:branching——同一份材料分给三个车间,结果仓库频繁调货、车间互相等待、最后还要合并,整个物流系统爆炸。


八、为什么 FLOPs 很低,速度却不快

这是经典问题:很多网络 FLOPs 降了,但 latency 没降。原因是 FLOPs ≠ 真正运行时间,真正耗时的往往是内存访问成本(Memory Access Cost)

⚠️ 注意:ShuffleNetV2 论文特意用缩写 MAC 指代 Memory Access Cost,这与前面提到的乘加运算 MAC(multiply-accumulate)只是同名,含义完全不同,别混淆。


九、现代网络越来越重视"少搬数据"

RepVGG:训练时用多分支,推理时通过重参数化合并成单个 3x3 Conv,目的就是消灭 branch,因为单路卷积最适合硬件。

DBB(Diverse Branch Block):思路一致——训练时多分支增强表达能力,部署时重参数化合并成单卷积,核心原因同样是减少 memory access cost。


补充二、工程上的解药:算子融合(Operator Fusion)

换网络结构(RepVGG/DBB)是"设计侧"的省法,还有一条"编译/部署侧"的省法——算子融合:把相邻的多个算子合并成一个 kernel,让中间结果只留在片上(寄存器 / SRAM),不再往返 DRAM。以 Conv → Add 为例:

融合后:中间量留在片上

Conv + Add 融合 kernel
中间量留 SRAM/寄存器

只写一次 DRAM

融合前:每步都过 DRAM

Conv

写 DRAM

读 DRAM

Add

写 DRAM

TensorRT、XLA、TVM、torch.compile 等做的正是这件事(推理时常见的 Conv+BN+ReLU 折叠也是同理)。所以完整的"少搬数据"工具箱其实是两层:设计侧改结构(消灭 branch/skip)+ 部署侧做融合(消灭中间量的 DRAM 往返)。

DRAM:像楼下的大仓库,容量最大,但拿一次东西要跑很远。

SRAM:像灶台旁的操作台,容量较小,但伸手就能拿到。

寄存器(Register):像厨师手里正拿着的食材,最快,几乎不用移动。

算子融合(Operator Fusion):让厨师一直在手里和操作台上连续做完几道工序,最后再把成品送回仓库,避免来回跑。

寄存器 > SRAM > DRAM(速度越来越慢,容量越来越大)。而算子融合的核心目标,就是让数据尽可能停留在寄存器和 SRAM,减少访问 DRAM 的次数。


补充三、同一份数据,摆法不同也影响访存:NCHW vs NHWC

前面讲的是"搬多少",这里讲"怎么摆"。同一个 tensor、同样的 FLOPs,内存里元素的排列顺序(layout) 不同,访存效率能差一大截。原因在硬件层面:GPU 一个 warp 里的线程若访问连续地址,就能"合并访存(coalesced)"一次拉满带宽;若地址跨步(strided),就要拆成多次传输,白白浪费带宽。

两种常见摆法的"连续维"不同,各自擅长的算子也不同:

布局与算子访问模式不匹配

同样可能不匹配

NHWC / channels_last

连续维 = 同一像素的所有通道

逐像素跨通道操作友好
1×1 conv / 激活 / add
Tensor Core 混合精度偏好

NCHW(PyTorch 默认)

连续维 = 同一通道内的 H×W

逐通道空间操作友好
经典 FP32 cuDNN 优化成熟

strided / 非合并访存
白白浪费带宽

⚠️ 别记成"NHWC 永远更快"——哪种布局快,取决于算子、精度和底层库:FP16/Tensor Core 卷积普遍偏好 NHWC(PyTorch 的 channels_last 就是为此提速),而经典 FP32 路径在 NCHW 上打磨得很好。真正的坑是布局在网络里反复来回转换,每转一次都是一遍纯搬运——这本身就是一笔 memory access cost。


在这里插入图片描述

🔴 🟢 🔵
🔴 🟢 🔵
🔴 🟢 🔵


补充四、量一量:skip 那一下加法,到底多贵

回到第三节的 skip。拿文中的 256 × 256 × 128、FP32 举例,单张 feature map ≈ 32 MB。把"一个 3×3 卷积(128→128)+ 一次 identity add"当作一个残差块来算账:

部分计算量访存量
卷积主体≈ 19.3 GFLOP读入 32 + 写出 32 ≈ 64 MB
skip 的 add≈ 8.4 MFLOP(≈卷积的 0.04%读 conv 输出 32 + 读 x 32 + 写 y 32 ≈ 96 MB(≈卷积的 1.5×)

结论很直观:skip 那一下加法在计算上几乎白送(连千分之一都不到),但在访存上比卷积本身还多。在内存受限的设备上,这个"免费"的加法可能和真正的卷积一样占时间——这正是为什么"FLOPs 没涨、latency 却涨了"。

(前提:FP32、且未做算子融合。一旦按补充二把 add 融进前序卷积、中间量留在片上,这 96 MB 的大头就基本被抹掉了——所以"量出来的代价"也是"融合能省下的空间"。)

在这里插入图片描述


十、一句话总结(最核心)

skip / branch 真正的问题不是"算得多",而是 “数据来回搬运太多”。现代 AI 硬件"计算像闪电,内存像堵车",所以:

很多网络优化,本质上不是减少计算,而是减少"交通运输"。


在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值