【Deploy】The Cost of Skip Connections

发布时间:2026/7/17 11:53:42
【Deploy】The Cost of Skip Connections 文章目录一、先理解AI 芯片最怕什么二、什么是 skip connection跳连三、为什么 skip connection 会增加 memory access cost四、真正的问题Add 很便宜但 Tensor 很大五、branching分支为什么也麻烦六、为什么移动端网络特别讨厌 branching七、一个非常形象的例子最重要八、为什么 FLOPs 很低速度却不快九、现代网络越来越重视少搬数据十、一句话总结最核心“skip-connections or branching can incur significant memory access costs”意思是跳连skip connection和分支结构branching虽然计算量不一定大但会导致很多搬运数据的开销。在真实硬件GPU/NPU/CPU里搬数据很多时候比做计算还慢这是很多深度学习初学者最容易忽略的点。一、先理解AI 芯片最怕什么很多人以为卷积最耗时因为数学运算很多其实现代 GPU 里加法乘法MACmultiply-accumulate非常快真正慢的是从显存读取数据、写回显存、等待数据、数据拷贝。也就是算不贵搬才贵。就像小学生做 23 很快但如果每次做题都要跑去隔壁教室拿纸就很慢。二、什么是 skip connection跳连以 ResNet 为例skip 跳连xConvConv 相加输出即输出 Conv(Conv(x)) x这个x被跳过去直接加到后面这就是 skip-connection。三、为什么 skip connection 会增加 memory access cost先看理想的普通卷积输入 → Conv → 输出GPU 可以边读边算边写数据流非常顺像流水线工厂材料 → 加工 → 出货一样连续。但 skip connection 打断了这种连续需先写显存暂存 x加法时再读回额外 memory readxConvConv 相加输出缓存 x额外 memory write于是 GPU 必须多做三件事。1. 先保存原始 x因为后面还要加回来x不能立刻丢掉必须写入显存或缓存到 SRAM——这产生一次额外 memory write注若做了算子融合x未必真的落显存。2. 后面再读回来到加法Conv结果 x时GPU 还得把x读出来产生一次额外 memory read。3. 最后做 element-wise addy[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 为例每个元素读a4B、读b4B、写y4B共 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 为例一个输入被分成多路输入 x3x3 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物料一路往前传效率极高。情况2skip-connection——工人1 做完还得把原材料存进仓库等后面再取回来仓库搬运次数增加。情况3branching——同一份材料分给三个车间结果仓库频繁调货、车间互相等待、最后还要合并整个物流系统爆炸。八、为什么 FLOPs 很低速度却不快这是经典问题很多网络 FLOPs 降了但 latency 没降。原因是FLOPs ≠ 真正运行时间真正耗时的往往是内存访问成本Memory Access Cost。⚠️ 注意ShuffleNetV2 论文特意用缩写MAC 指代 Memory Access Cost这与前面提到的乘加运算 MACmultiply-accumulate只是同名含义完全不同别混淆。九、现代网络越来越重视少搬数据RepVGG训练时用多分支推理时通过重参数化合并成单个 3x3 Conv目的就是消灭 branch因为单路卷积最适合硬件。DBBDiverse Branch Block思路一致——训练时多分支增强表达能力部署时重参数化合并成单卷积核心原因同样是减少 memory access cost。补充二、工程上的解药算子融合Operator Fusion换网络结构RepVGG/DBB是设计侧的省法还有一条编译/部署侧的省法——算子融合把相邻的多个算子合并成一个 kernel让中间结果只留在片上寄存器 / SRAM不再往返 DRAM。以Conv → Add为例融合后中间量留在片上Conv Add 融合 kernel中间量留 SRAM/寄存器只写一次 DRAM融合前每步都过 DRAMConv写 DRAM读 DRAMAdd写 DRAMTensorRT、XLA、TVM、torch.compile等做的正是这件事推理时常见的 ConvBNReLU 折叠也是同理。所以完整的少搬数据工具箱其实是两层设计侧改结构消灭 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 / 激活 / addTensor Core 混合精度偏好NCHWPyTorch 默认连续维 同一通道内的 H×W逐通道空间操作友好经典 FP32 cuDNN 优化成熟strided / 非合并访存白白浪费带宽⚠️ 别记成NHWC 永远更快——哪种布局快取决于算子、精度和底层库FP16/Tensor Core 卷积普遍偏好 NHWCPyTorch 的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 MBskip 的 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 硬件计算像闪电内存像堵车所以很多网络优化本质上不是减少计算而是减少交通运输。