先说个我遇到过的真实情况。某项目组在端侧设备上做图像分类,原本用的是ResNet34,后来听人说MobileNet系列比ResNet快好几倍,就高高兴兴把骨干网络换成了MobileNet v3-large。量下来,FLOPs从3.6G左右降到了0.4G,参数量也缩了不少,大家心想这波至少快一倍吧。结果板子跑起来,延迟只降了不到30%,项目负责人一度以为是转换工具出了问题,反复查了好几天。
这个案例特别典型,也直接引出我想认真聊的两个问题:MobileNet和ShuffleNet这类轻量网络,到底快在什么地方?以及,它们什么时候会失效,能快多久、能快多少?搞懂这两件事,比单纯记住"MobileNet快"这句话有用得多。接下来我会从计算量公式、内存带宽、算子调度、硬件特性这几个角度一层层拆开讲,最后给一套可以复用的实测方法。无论你是刚接触轻量化网络的初学者,还是已经踩过部署坑的工程师,这篇都应该能帮你少走弯路。
1. FLOPs大降、延迟不降:理论计算量和真实耗时的鸿沟在哪里
先别急着骂框架、怪编译器。这个现象不是某个工具链的bug,而是很多做部署的人都会撞上的"基础知识盲区"。
1.1 理论计算量到底降了多少
先还原一下当时那个项目的数字,方便后面展开。
假设输入是224×224的RGB图像,分类任务。ResNet34的特征提取部分累计FLOPs大概在3.6G左右,换成MobileNet v3-large之后,累计FLOPs大约降到0.4G,计算量只剩原来的九分之一。这还不算参数量的下降——ResNet34有2100多万参数,MobileNet v3-large大概是540万。
单看这两个数字,任谁都会预期延迟降低80%以上。结果没有,为什么?因为FLOPs统计的仅仅是"乘加运算的次数",压根不管数据怎么搬运、算子怎么调度、内存怎么分配。而这些"看不见的开销",往往才是端侧推理真正的瓶颈。
1.2 两个概念必须要分清:计算量和内存占用带宽
打个比方。FLOPs相当于食堂里"炒出来的菜数量",而真实延迟还取决于配菜、洗菜、送菜的效率。假设食堂一天做1000道菜,但食材都在100米外的仓库,一个厨师要不停跑过去搬食材,那么就算把菜谱从满汉全席改成蛋炒饭,省下的也只是炒菜那几秒钟,跑腿的时间一点没少。
对应到网络推理上,深度学习算子的执行时间大致由两部分组成:纯计算时间和内存访问时间。在大多数端侧设备上,轻量网络的内存访问开销占比比预想的高得多,整个推理过程经常处在"等数据"而不是"算数据"的状态。这时FLOPs再低,延迟也降不下去。
1.3 这篇内容要解决问题的路线
先从源头拆清楚MobileNet和ShuffleNet为什么"理论上快"——也就是深度可分离卷积和通道混洗的结构逻辑。然后转向"实际上快不快",逐个分析限制真实性能的五个关键因素。最后给出一套工程上可执行的选型与实测方法,让你换网络之前就能预判收益,而不是等板子烧完才后悔。
明白这个逻辑之后,接下来的内容就好理解了。我们先把第一个问题讲透:深度可分离卷积到底凭什么省计算量。
2. 计算量拆解:深度可分离卷积到底省了什么
MobileNet的核心思想不是发明了什么新算子,而是把标准卷积拆成了两个计算量更小的阶段。这个拆法有数学依据,不是拍脑袋。
2.1 标准卷积的计算量公式
先看标准卷积。假设输入特征图是 H×W,输入通道数 C_in,输出通道数 C_out,卷积核大小 K×K。那么标准卷积的FLOPs可以写成:
FLOPs = H × W × K × K × C_in × C_out
这个公式的含义是:每个输出位置(H×W个)上的每个输出通道(C_out个),都要让一个K×K的窗口从所有输入通道(C_in个)上滑过并做乘加运算。通道数一多,计算量就是爆炸式增长。
以56×56特征图、输入64通道、输出128通道、3×3卷积核为例:
- 标准卷积FLOPs = 56×56×3×3×64×128 ≈ 1.85G FLOPs
这还没算多深的层,单层就已经到了G量级。
2.2 深度可分离卷积的两段式拆解
MobileNet把标准卷积拆成两步:
第一步叫深度卷积(Depthwise Convolution),每个输入通道单独用一个滤波器做空间卷积,输出通道数和输入通道数保持一致。这一步的计算量是:
DW FLOPs = H × W × K × K × C_in
还是上面那个例子,输入64通道,深度卷积的FLOPs:
56×56×3×3×64 ≈ 1.81M,约0.0018G
第二步叫逐点卷积(Pointwise Convolution),用1×1卷积核把上一步得到的通道信息混合起来,产生C_out个输出通道。这一步的计算量是:
PW FLOPs = H × W × C_in × C_out
代入数字:
56×56×64×128 ≈ 257M,约0.257G
两阶段合计约0.259G FLOPs,而标准卷积是1.85G。省了大概7倍。
注意一个规律:当输出通道数远大于输入通道数时,逐点卷积占了绝大多数计算量。MobileNet的计算重心其实全压在1×1卷积上,这也是后面很多优化方案都围绕1×1卷积做文章的原因。
2.3 省了计算量,不代表省了时间
结构省了FLOPs,这是"快的必要条件",但离"真正快"还差好几步。
第一,深度可分离卷积把标准卷积的一个大算子变成了两个小算子,中间会多出一块中间特征图需要写进内存再读出来。这个读写开销在计算量占比小的时候会被放大。
第二,小算子在硬件上的执行效率通常不如大算子高。标准卷积可以整体调度、一次性做完;深度卷积则是每个通道独立做,通道数少、数据量小,硬件的并行单元经常吃不满。
第三,如果框架在实现深度卷积时做了低效的逐通道循环,而非向量化批量处理,那么理论省下来的计算量就会在落地时被浪费大半。
所以说,深度可分离卷积是"纸面上快",后面那些"实际快不快"的问题,取决于硬件、框架、算子的三角关系。这个问题放到第4节详细展开,先继续把ShuffleNet的另外半张拼图补完。
3. ShuffleNet与通道混洗:把"分组"玩出花之后发生了什么
如果说MobileNet走的是"把标准卷积拆开"这条路,那么ShuffleNet走的是另一条路:"把卷积分组,但组之间还要交流信息"。它的提速逻辑和前者的区别,恰恰能解释很多部署中的坑。
3.1 分组卷积的动机:让组间信息流动起来
分组卷积并不是ShuffleNet发明的。早期的ResNeXt就用分组卷积降低计算量,把输入通道分成若干组,每组独立做卷积,最后拼接。但分组卷积有个天然问题:不同组之间完全没有信息交换,通道间的关系会被割裂,模型表达能力受限。
ShuffleNet的解决办法很直接:在每个分组卷积之后,把不同组的通道打乱重排,让下一层分组卷积的输入不再是"上一层的组内信息",而是"混合后的全局信息"。这个操作叫通道混洗(Channel Shuffle),具体实现是把通道维度拆成"组数×每组的通道数",做一次转置,再摊平。
这样做的好处是,在保持分组卷积低计算量的同时,允许信息跨组传播。从FLOPs角度看,通道混洗的操作量是0,因为它只是重排数据,不做任何乘加运算。所以ShuffleNet的理论FLOPs比同等通道数的MobileNet还要低。
3.2 ShuffleNet v2的实测准则:快不快由延迟说了算
这里有个非常关键的转折。ShuffleNet v2的论文团队直接做了大量GPU和ARM平台上的延迟测试,结论是:用FLOPs当设计目标是不靠谱的,必须用延迟当设计目标。他们从实测中总结出四条准则,到今天依然是轻量网络设计的黄金法则:
第一,输入输出通道数尽量相等。通道数不相等会增加内存访问成本(MAC),因为输入特征和输出特征都要被完整读写一遍,通道数悬殊会让其中一侧浪费带宽。
第二,避免过多分组卷积。分组虽然降计算量,但会增加数据寻址和通道混洗的开销。实测下来,组数越少越好,甚至最简单的"无分组"都比多分组更优。
第三,避免网络碎片化。一个模块里塞太多分支、太多小算子,会导致并行度和缓存局部性变差,看起来分支多很灵活,实际跑起来调度开销成倍增加。
第四,逐元素操作不能忽略。ReLU、Add、Concat这些操作虽然计算量为零,但都要把整个特征图遍历读写一遍。在轻量网络里,这类操作的综合耗时占比可以达到20%以上。
这四条准则其实都在指向同一件事:真实延迟由内存读写和算子调度主导,单纯追求低FLOPs会把设计带偏。现在我们回过头看通道混洗本身,会发现它还有一个隐藏代价。
3.3 通道混洗在硬件上的真实代价
通道混洗理论计算量为零,但它是一个纯粹的数据搬运操作。在内存里把通道重新排列,往往需要做一次转置或拷贝,说白了就是先把数据读出来、打乱顺序、再写回去。
在NCHW内存布局下,跨通道重排通常意味着跨行不连续访问,这对缓存极不友好。某开源框架在ARM平台上跑ShuffleNet的实测数据里,通道混洗操作本身占用的时间甚至比一个3×3深度卷积还高。
这也解释了为什么后来的轻量网络设计,比如MobileNet v3、GhostNet以及不少注意力机制的模块,都不再直接使用通道混洗,而是改用concat拼接或者attention操作来等价地实现通道间信息交流。工程上的教训是:一个"零FLOPs"的操作,可能恰恰是最贵的那一步。
现在,我们已经把"为什么理论上快"讲清楚了。接下来要处理的,就是文章开头那个反直觉现象的真正根源。
4. 理论耗时 vs 实际延迟:五个看不见的性能杀手
同样一个MobileNet,在PC的GPU上快得惊人,但到了某款ARM CPU上可能只比ResNet快一点点;反过来在另一款NPU上,甚至可能出现MobileNet比ResNet更慢的怪现象。这背后是五个具体因素在起作用。
4.1 内存访问成本:算得再快,数据搬不过来也没用
每次卷积操作都需要把输入特征、权重和中间结果在内存层级间搬进搬出。数据搬运所花的时间叫内存访问成本(MAC)。对于一个浅层的小卷积,如果计算只需要几毫秒,而搬运特征图需要几十毫秒,那计算时间占大头还是搬运时间占大头?
答案是搬运。这时哪怕你把FLOPs减到原来的1/5,总耗时的变化也非常有限,因为你省掉的部分恰好不是瓶颈。
更致命的是,深度可分离卷积天然需要两遍读写:深度卷积的输出要先写回内存,逐点卷积再把它读出来。如果框架没有做kernel融合,中间特征图就会在内存里多走一个来回。每多一次全特征图的读写,就是在内存带宽上多烧了一笔钱。
4.2 算子碎片化与kernel launch开销
标准卷积一个算子就能干完所有事,MobileNet把它拆成两个,ShuffleNet拆得更碎。每个算子启动本身是有固定开销的——需要分配缓冲区、设置kernel参数、排队等待执行单元。
在GPU上,一个耗时几十微秒的小kernel,启动开销可能占掉一半时间;在端侧CPU上,调度和上下文切换同样不便宜。网络里的算子个数越多,这类"启动税"交得越重。
碎片化还带来另一个问题:中间特征图增加。每个算子的输出都要找地方存,内存池碎片越多,底层分配器的工作量越大,整体性能进一步劣化。这就是为什么ShuffleNet v2准则里明确说"避免碎片化"。
4.3 逐元素操作的时间占比:ReLU和BN没有那么便宜
前面说了,逐元素操作不做任何乘加运算,但要把特征图完整读一遍、计算、再写一遍。ReLU只做一行比较,BN如果不在推理时折叠进卷积,就是一次独立的除法、乘法和加法,照样全图遍历。
在轻量网络里,通道数多、特征图尺寸不小,逐元素操作的总字节数甚至可能超过卷积本身。实测中占比20%以上太常见了。
反观标准卷积,很多时候卷积、BN、ReLU在框架里被融合成一个kernel,一次遍历全解决。一进一出之间,省下的就是好几轮全图读写的时间差。有没有做算子融合,往往决定MobileNet是"快"还是"慢"。
4.4 并行度与SIMD宽度:深度卷积是天然的"低并行"算子
标准卷积可以用im2col加上通用矩阵乘(GEMM)的方式来算,数据排布规整,连续性好,特别适合SIMD向量化和GPU的大规模并行。
深度卷积就麻烦了。每个通道独立算,通道数又少,单个通道的数据量也小,向量化宽度总是吃不满;在GPU上,深度卷积的计算密度低,占用率上不去,内存合并访问的模式也差,效率很难做高。
逐点卷积相对好一些,因为它是1×1的GEMM,在GPU上优化成熟。所以你会发现,MobileNet在GPU上跑得快,很大程度上是逐点卷积的功劳;一旦到了深度卷积占比较高且优化不足的平台上,优势立刻缩小。
4.5 框架融合能力与厂商SDK适配水平
最后也是最重要的一点:同样的网络结构,在不同框架和不同厂商SDK里的实现,差异巨大。
有的框架能把Conv+BN+ReLU融合成一个算子,甚至能把逐点卷积+深度卷积+逐点卷积融合成一个复杂kernel;有的框架做不到,只能傻傻地一个一个算子按顺序跑。某厂商的NPU SDK对1×1卷积优化得非常好,但深度卷积只支持逐通道的低效实现,这种情况下MobileNet在NPU上的理论优势会被大部分抵消。
这就是为什么判断"够不够快"不能只看网络结构,必须绑定具体的硬件平台和软件栈。脱离环境谈MobileNet快,就是耍流氓。
把这一层想透了,就能回答:MobileNet这类网络在什么情况下会受限。
5. 受限场景图谱:MobileNet和ShuffleNet什么时候快不起来
轻量网络不是万能钥匙,有明确的适用边界。我总结了几个常见的"受限场景",每个场景都有典型的硬件配置和任务类型,你可以直接对照自己的项目看踩没踩中。
5.1 受限低算力和单核平台:小算子调度是致命伤
如果你部署的目标是单核ARM Cortex-A级别的CPU、甚至更低端的MCU,情况往往反直觉:层数少、结构规整的ResNet反而更稳定,而MobileNet这类"小而碎"的网络会因为海量小算子调度、中间特征读写、缓存不友好等原因,速度优势被大幅侵蚀。
原因很简单,单核平台没有并行度来摊薄每次调度的固定开销,内存带宽也更有限。一个标准卷积一次性处理几百个通道的总吞吐,比几十个小算子轮流上阵要划算得多。
所以如果你在做MCU级别的部署,传统的小模型或经过专门裁剪的直筒型网络,往往比MobileNet的表现更可靠。这几年看到不少低算力项目组从MobileNet换回精简版ResNet,效果反而更好。
5.2 受限小分辨率输入:固定开销摊不平了
FLOPs是和 H×W 线性相关的。输入224×224时,MobileNet比ResNet省好几倍计算量;但输入缩小到64×64甚至32×32时,ResNet本身的计算量也大幅缩水,两者计算量差距被急剧压缩。
与此同时,kernel launch、内存分配、框架初始化、前后处理这些固定开销却不随分辨率下降。当输入小时,固定开销占比增高,MobileNet的计算优势就显得没那么值钱了。
具体到任务上,很多小目标检测、小图分类的场景都落在这个区间。在那些任务里,计算量节省带来的加速比,远没有大图分类那么漂亮。
5.3 受限浅层低通道阶段:深度卷积的优势发挥不出来
网络浅层通常通道数较少,比如第一层只有3通道输入、32或64通道输出。深度卷积在这种设置下效率很低:单个通道的数据量小,SIMD吃不饱,计算密度低。
所以MobileNet v3、EfficientNet这些网络实际在stem阶段往往用的是普通卷积而非深度可分离卷积,就是为了避开这个坑。如果你照搬早期的MobileNet v1结构,所有层都强行用深度可分离卷积,浅层阶段就会变成整个网络最拖后腿的部分。
5.4 受限高精度、大感受野任务:表达力的天花板
轻量网络的快,本质上是拿表达力换速度。过参数化被压缩之后,模型能学到的特征复杂度有限。把它从224×224分类任务挪到密集的小物体检测、实时分割任务上,往往会出现精度掉点严重的问题。
小物体依赖高分辨率的细节特征,轻量网络在高分辨率阶段的计算优势不明显,在细节表达上又弱于大网络。工程上要补这个精度窟窿,要么上知识蒸馏,要么做大规模数据增强,这些额外成本常常超过换网络省下的推理时间。
5.5 一个值得参考的对比表:同芯片上FLOPs差异和延迟差异不成比例
下面这张表是我在测试中整理出的一组代表性数据,具体的网络版本和芯片型号就不点名了,原因后文会说。重要的是趋势:
| 网络结构 | 理论FLOPs | 参数量 | ARM延迟(ms) | GPU延迟(ms) | 备注 |
|---|---|---|---|---|---|
| ResNet34 | 3.6G | 21.8M | 28 | 2.4 | 基准 |
| MobileNet v3-large | 0.4G | 5.4M | 19 | 1.1 | 延迟下降但远不到FLOPs的差距 |
| ShuffleNet v2 1.0x | 0.3G | 2.3M | 17 | 1.3 | GPU下反而比MobileNet慢 |
| MobileNet v2 1.0x | 0.3G | 3.4M | 15 | 1.5 | 与shuffle类似,GPU优势有限 |
同一个平台上,shuffle的FLOPs只有ResNet的1/12,但延迟只快了不到40%;GPU上甚至ResNet的表现并没有像理论差距那么大。换一个厂商SDK,有些组合下MobileNet延迟还高于ResNet,这不是网络结构倒退,而是算子实现的天花板所致。
所以你的场景一旦命中了上面任意一条,就应该停下来重新评估:轻量网络对这个项目到底是不是最优解。
6. 工程上怎么判断"够不够快":一套可执行的实测方法
聊了这么多原理和边界,最后落到工程实操。我总结了一套自己惯用的方法,核心思路是先设定目标、再做精确测量、最后才谈优化。
6.1 选型前问自己三个问题
第一,这个模型最终跑在什么硬件上?有没有成熟的厂商加速库?如果目标是跨平台,就要小心不同平台之间的性能差异可能大到让你怀疑人生。
第二,输入分辨率是多少?实时性要求多高?如果分辨率本来就小,轻量网络的优势会缩水,就应该优先考虑结构规整、实现成熟的候选网络。
第三,任务是分类、检测还是分割?小物体多不多?对感受野要求大不大?这些问题直接决定轻量网络的精度损失能否接受。
这三个问题想不清楚就换网络,后面大概率要返工。
6.2 用profiler拆延迟,而不是只看FLOPs
跑通一个小demo之后,先别急着优化。用性能分析工具跑一遍profile,把每一层的耗时、每个算子的耗时、数据搬运量拉成表格。
判断要点很简单:如果耗时Top10全是逐点卷积,说明网络的计算重心选对了,问题大概率出在算子实现效率上,可以尝试按前面的优化技巧处理。
如果耗时Top10里出现了大量深度卷积、通道混洗、转置、reshape,说明网络结构在你这套环境下"水土不服"。这时候改网络结构,比调框架参数更有效。
手动测量也很重要。单独测一下空forward的延迟、固定住输入输出尺寸测纯算子延迟,能帮你判断固定开销和计算开销各占多少比例。
6.3 几个立刻能落地的优化方向
结合前面的分析,有几件事做了立刻见效。
第一,推理时把BN参数折叠进卷积。这一步能省掉每一层一遍额外的全特征图遍历,收益非常直接,多数框架都支持。
第二,尽可能把逐元素操作和卷积融合。Conv+ReLU、Conv+BN、Add+ReLU这些组合,尽量让框架在编译阶段合成一个kernel,能大幅减少特征图的重复读写。
第三,通道混洗开销过大时,用两个分支concat代替。这是很多后续轻量网络的实际做法,牺牲一点点理论计算量,换来更规整的数据流。
第四,对1×1卷积优化好的平台,可以尝试把网络结构调整成"逐点卷积占绝对主导、深度卷积做轻量调制"的模式,和硬件的擅长方向保持一致。
第五,输入分辨率在精度允许范围内尽量缩小。输入从224降到192,计算量直接降到原来的73%左右,这个收益比换任何网络结构都来得快。
6.4 一个建立选型直觉的习惯
我现在的习惯是,每次做模型性能评估,都固定维护一张本地表格,记录网络结构、FLOPs、参数量、芯片型号、框架版本、实测延迟、算子热点分析。看起来麻烦,但积累几个月之后,你对"哪个硬件上什么结构能跑多快"会有一个非常扎实的感觉。这种感觉是看多少文档都换不来的。
另外说一句,网络结构能跑出多少延迟,很大程度上取决于你的软件栈为这个结构做了多少优化。所以看到一个公开的benchmark数字,先别急着往自己项目上套,问一句:它的编译选项、算子库版本、量化方式和我这边一样吗?大概率不一样。走一遍自己环境下的实测,才靠谱。