
模型调参、发论文、横向对比算力开销只要你碰深度学习就绕不开 FLOPs 这个词。我最早开始算 FLOPs 时是抱着“跑个脚本看个数”的心态去的结果连续两周被各种报错按在地上摩擦同样的模型thop 说 2.4Gpytorch profiler 说 3.1G换了另一个工具又变成 1.17G三个数字谁都不敢信。后来踩的坑多了才慢慢把“计算 FLOPs 报错”这件事背后的原理、工具差异和排查套路摸清楚。这次就顺手把这段经历整理成一份报错集锦按实际场景把高频问题、根因和解决方法列出来给被 FLOPs 折磨过的人一份可以直接抄的排查手册。适合刚入门想评估模型复杂度的同学也适合准备横向对比模型论文指标的老手。1. 先搞清楚FLOPs 到底是怎么被算出来的很多报错其实不是“工具坏了”而是我们对统计原理的理解有偏差。先把这个底层逻辑讲清楚后面再聊报错你才知道怎么对症下药。1.1 一次损失惨重的经历数错 FLOPs 比不算还坑有一次做模拟项目 X 的版本选型需要对比两个结构非常相似但细节不同的图像模型。当时的操作很简单把模型和输入扔给某个工具看一眼输出然后把数字填进了对外文档。结果模型 A 在文档里写着 3.2G FLOPs模型 B 写着 1.8G FLOPs最终汇报时被评审追问为什么模型 A 的输入输出shape几乎和模型 B 一样FLOPs 却差了快一倍我当场愣了一下回去逐个算子排查才发现问题模型 A 的 forward 里有一个在推理阶段根本没有走到的分支而统计工具默认把模型的所有参数层都遍历了一遍无论这个分支实际运不运行它都按“存在”来计数。更离谱的是模型的 attention 部分里用了一个自定义的窗口聚合算子工具根本没识别出它的真实计算量只统计了旁边的卷积层。这次教训让我意识到统计 FLOPs 这件事比大多数人想象的要细致得多。工具给出来的数字是一个“理论参考值”它基于模型参数的静态遍历和输入 shape 的推导并不代表真实运行时的浮点运算量。之后我再也不盲信任何一行数字每次都要自己把关键层拉出来核对一遍。1.2 主流统计工具的原理与差异现在常用的 FLOPs 统计方式大致分三类每一类的统计口径和报错方式都不一样这是报错集锦里最容易被忽略的背景知识。第一类是 pytorch 自带的 torch.profiler。它的原理是把模型真实跑一遍在算子执行层面记录 CPU/CUDA 上的耗时和 flops。优势是不用额外装库而且是在真实运行时统计能准确反映“这次实际执行”的计算量。缺点也很明显它只会统计真正跑到的分支如果模型里有 if 条件分支、训练阶段才走的路径它统统看不到。另外torch.profiler 的 flops 字段对很多底层算子支持不全某些自定义 kernel 可能根本没有对应的 flops 统计。第二类是 thop也是最早流行的一批工具。thop 的做法是遍历模型所有包含参数的层通过注册 forward hook 来截获每一层的输入输出 shape再用内置的“层类型 - 计数函数”映射表去计算对应计算量。这种静态遍历方式的好处是稳定、快速缺点是映射表不完善的层就会直接抛异常或者统计为 0。这就是很多新人第一次用 thop 就撞上 NotImplementedError 的根本原因。第三类是 ptflops它的思路和 thop 类似但实现上更像是在“构造一个 dummy 计算图”然后通过钩子去记录每个模块的 MACs。它对标准卷积、全连接、归一化等常见层支持得不错输出格式也比 thop 漂亮但对嵌套输入和一些奇怪的容器模块处理起来同样僵硬。另外还有一个更手动的思路自己按照公式算卷积、全连接的计算量。这种方法最慢但最可控适合排查工具计算结果是否合理时用来做交叉验证。我把常用工具的特征整理成了一张表工具统计口径是否依赖真实运行多输入支持常见报错torch.profiler实际运行时算子是较好flops 字段缺失、算子不支持thop静态遍历带参数层否较差NotImplementedError、tuple输入报错ptflops静态构造计算图否一般维度推导失败、None返回值报错手算公式推导否完全可控容易漏层、算错尺度1.3 FLOPs 和 MACs 到底怎么换算还有一个经常把数字搞到对不上的原因MACs 和 FLOPs 的换算关系。一个 MACMultiply-Accumulate乘加运算包含一次乘法和一次加法所以理论上 1 MAC 2 FLOPs。但很多人不知道thop 的 profile 函数返回的第一个值默认是 MACs 而不是 FLOPs你要是没看文档直接把数字当成 FLOPs 去跟论文对比分母上就凭空少乘了一个 2量级直接翻倍。我自己习惯先看一眼返回值的命名再决定要不要乘 2。通常的规则是工具返回macs那么 FLOPs macs × 2工具返回flops那就直接用。千万别看着数字相似就忽略单位在脑科学、推荐系统这种对算力比较敏感的领域这一个小数点能差出十万八千里。2. 高频报错逐个拆解一上来就能复现的那种现在进入正题。下面这几个报错是群里伙伴问得最多、也是我自己在项目里反复撞过的类型。每一条我都按照“报错信息 - 出错原因 - 解决路径”来整理方便你直接对照排查。2.1 经典报错thop 说你没实现这个层的 flops第一次用 thop 统计某个带 HAT 模块的模型时直接看到NotImplementedError: If you want to count the model flops, please implement the flops of the module: class torch.nn.modules.conv.ConvTranspose2d这个错误的意思是thop 的层类型映射表里没有 ConvTranspose2d 的计数逻辑。其实不止 ConvTranspose2d常见的还有某些池化、特殊激活、上采样、自定义注意力机制里的新层。解决方案是手动注册计数字典。thop 提供了一种注册方式我当时的做法是写一个独立文件来注册缺失算子from thop import clever_register import torch.nn as nn def count_convtranspose2d(self, input, output): # 反卷积的乘法次数输出通道 * 输出宽高 * 输入通道 * 卷积核宽 * 卷积核高 / groups input input[0] batch, in_c, in_h, in_w input.shape out_c, _, kh, kw self.weight.shape out_h output.shape[-2] out_w output.shape[-1] kernel_ops in_c * kh * kw // self.groups macs batch * out_c * out_h * out_w * kernel_ops flops 2 * macs self.macs macs self.flops flops clever_register(nn.ConvTranspose2d, count_convtranspose2d)注册完再跑同一段代码报错就消失了。关键点是注册函数里必须有self.macs和self.flops这两个属性挂到层对象上thop 在汇总阶段会去读这两个字段。如果你手滑写成了别的名字统计结果不会报错但会悄悄变成 0这种“静默错误”比报错更坑人。2.2 处理不了 list/tuple 嵌套输入直接 AttributeError第二个高频报错是输入类型问题。比如模型的 forward 定义是def forward(self, x, y):并且 x 本身又是一个list这时候直接套用 thop 的 profile 经常会遇到AttributeError: tuple object has no attribute size这个报错的原因在于 thop 内部会自动把inputs包装成 tuple然后传给 hook它在里面尝试对input[0]调用.size()。当input[0]本身还是 list 或 tuple 时解析就崩了。我对付这个问题的思路是“隔离输入”。具体来说给模型包一个壳子让壳子接收一个普通的 tensor然后在壳子 forward 里拆包成原模型需要的格式import torch from thop import profile class FixedWrapper(torch.nn.Module): def __init__(self, model): super().__init__() self.model model def forward(self, x): # 假设原模型需要 list[int]这里模拟拆包 patch x.unflatten(0, (1, -1)) return self.model([patch]) model FixedWrapper(raw_model) input_tensor torch.randn(1, 112, 112) flops, params profile(model, inputs(input_tensor,))这种“包一层”的方法能绕开大部分嵌套输入解析问题而且对模型本身没有任何修改。另一个更稳妥但不那么常用的方法是直接看 forward 代码手动给模型每个层写计数逻辑不过工作量大非必要不建议。2.3 维度对不上expected input to have 3 channels这种报错一般出现在你手动构造 dummy input 时搞错了输入通道RuntimeError: Given groups1, weight of size [64, 3, 7, 7], expected input[1, 4, 224, 224] to have 3 channels, but got 4 channels instead我见过太多人把(224, 224, 3)这种 HWC 的习惯带进来下意识写成了torch.randn(1, 224, 224, 3)。PyTorch 默认是 NCHW通道在第二维。另一个相似场景是 RGB 图像模型结果你给了一个带背景通道的 4 通道输入这就不是报错与否的问题而是统计结果会直接反映一个并不存在的输入。最保险的做法是先用next(model.parameters()).shape看一眼第一个卷积层的输入通道数再构造 dummy input。比如模型第一个卷积是nn.Conv2d(3, 64, 7)那就用torch.randn(1, 3, 224, 224)分辨率大小其实不影响统计最重要的是通道数和模型定义一致。2.4 设备不一致CUDA 和 CPU 混用这种情况更容易出现在刚接触 GPU 训练的同学身上。模型已经通过.cuda()放到 GPU 上了但构造输入的时候忘了同样迁移跑 ptflops 或 torch.profiler 时报RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same解决思路非常朴素输入、模型放同一设备。如果你用 profile 工具时传的模型本身就是model.cuda()那么 dummy input 也要cuda()input_tensor torch.randn(1, 3, 224, 224).cuda()但注意thop 这类静态工具对设备没那么多要求因为很多计数逻辑只做 shape 推导不需要真的跑到 GPU。可 ptflops 在构造 dummy graph 时某些算子确实会触发真实张量运算所以设备一致性就成了硬要求。这也是为什么我更喜欢 thop 的原因之一——报错更少迁移更平滑。3. 复杂模型里的隐形地雷常规 CNN 模型统计起来相对顺利真正常踩坑的是那些结构灵活、前后依赖强的模型,比如多分支结构、动态 shape 的 Transformer、带自定义算子的注意力模块。这一章把常见的“隐性错误”拆开看。3.1 多分支结构被重复计数结果虚高有一次我要给一个双塔结构的对比模型写算力报告模型结构大致是一个共享编码器输入两路不同的特征最后拼接送到输出层。我用 thop 统计完发现总 FLOPs 比理论预期高了快一倍。排查了很久发现是共享编码器在两个分支里各被调用了一次hook 机制把同一个模块的计算量累加了两遍。这就涉及一个关键认知thop 是基于“模块被 forward 调用次数”来累计的计的是动态执行次数而不是模型定义里的唯一层数。模型只有在训练和推理中都保持单分支结构时hook 直接累加才准确。对于孪生网络、对比学习、多尺度推理这类结构你不能直接把工具输出的总和当成“一次推理成本”。我当时的处理方法是把共享编码器单独抽出来统计一次再统计除共享部分之外的其他模块最后手动求和。或者在包装模型时把共享编码器的输出通过torch.no_grad()缓存下来避免第二次前向重复触发 hook。如果你只想得到一个参考值直接在最后结果上除以分支数也勉强能看但严谨的项目不建议这么做。3.2 动态 shape 输入换尺寸后数字就变了还有人问过一个问题为什么我换了一版输入分辨率FLOPs 就差别这么大这其实不是报错是一种容易误导人的“正常现象”。因为 FLOPs 本质上是输入 shape 的函数尤其是 Transformer 里的自注意力它的计算量随序列长度平方增长输入尺寸稍微差一点最终数字可能差出一个量级。用一个简单公式说明一个形状为(B, N, C)的序列经过自注意力模块QKV 投影的计算量约为3 * B * N * C^2注意力的矩阵乘则是2 * B * N^2 * C。这里的 N 一旦翻倍第二项的 N² 就直接放大 4 倍。所以你在线对比两个模型时如果输入尺寸设置不一样数字根本没有可比性。我的建议是在准备横向对比前先约定一个标准输入尺寸比如(1, 3, 224, 224)所有模型都按这个口径统计。论文里写 FLOPs 时也一定要标注分辨率否则数据没有任何参考意义。虽然这个点不算报错但它是很多“数字对不上”纠纷的根源。3.3 view、reshape、切片到底算不算 FLOPs很多人在手工核对模型计算量时还会困惑类似view、reshape、permute这类算子工具到底有没有把他们计入 FLOPs答案取决于统计口径。thop 的映射表对这类“内存重排型”算子基本不计入 MACs因为它不包含乘加运算。而 torch.profiler 在真实执行时偶尔会把某些 copy 或者 index_select 也统计成有计算量的算子导致它给出的整体数字比 thop 高。这种差异并不是哪个工具错了而是它们衡量的是不同层面的开销。理论 FLOPs 通常默认指的是乘加类运算所以你在对外文档里引用理论 FLOPs 时建议参考 thop/ptflops 这类静态工具的口径。你完全可以用 torch.profiler 去看实际耗时但不建议把它的 flops 字段直接当论文指标用。4. 自己动手写一个统一封装工具这一节讲的是我在项目里最终落地的一套方案。与其每次被不同工具的报错折磨不如抽时间把“计算 FLOPs”这个需求封装成一个函数后续任何模型都能一键跑遇到未知算子再往注册表里补一笔记录。4.1 为什么我没有直接用现成 API直接调 thop 的 profile 其实很简单但真实需求往往不止“跑一个数字”。我需要的是能处理单输入、多输入、list 嵌套输入缺失算子的报错能很快定位到是哪个模块每一层都能输出独立 MACs方便核对遇到重复计数的多分支结构能快速过滤这些需求用现成 API 硬套每一类都要重新写一遍适配代码。干脆把适配逻辑收拢到一个文件里以后新增模型就只写一行调用。4.2 核心实现思路我最终选择以 thop 为底座在上面做了一层封装。原因是它速度快、可控性强而且注册机制足够清晰。核心函数大概是这样的import torch from thop import profile, clever_register class FlopsEvaluator: def __init__(self, model, input_creator, skip_reuseFalse): self.model model self.input_creator input_creator # 接收模型返回的输入 self.skip_reuse skip_reuse def evaluate(self): model self.model if hasattr(model, cuda): try: model model.cuda() except Exception: pass inputs self.input_creator(model) flops_total, params_total profile(model, inputs(inputs,)) summary self._flops_summary() return { flops: flops_total, macs: flops_total, params: params_total, summary: summary, } def _flops_summary(self): # 从 thop 内部缓存中拿逐层明细这里按自己的格式整理 result {} for module_name, module in self.model.named_modules(): macs getattr(module, macs, 0) if macs 0: result[module_name] macs return result这套封装帮我解决了很多实际问题。比如input_creator可以写成可插拔的函数不同模型传入不同构造逻辑兼容多输入或嵌套输入。逐层明细输出到 dict 后可以直接 check 哪一层为 0快速定位未识别算子。4.3 实测过程从报错到跑通的完整记录我用一个自制的图像分割模型来做实测结构里包含普通卷积、转置卷积、残差连接和一个自定义的局部注意力层。第一轮跑 thop报错是自定义注意力层的NotImplementedError按 2.1 的方法注册了计数函数第二轮跑不报错了但我发现局部注意力层的macs字段是 0。我检查了计数函数发现是输入里包含了 tuple而我的函数只取input[0]修正后统计正常第三轮模型里有双分支结构共享编码器被调用了两次我又在封装里加了skip_reuseTrue选项统计时对已经出现过的共享参数模块进行计数抑制。最终结果如下模块统计值MACs备注下采样卷积组1.42G正常重构反卷积组0.78G注册后成功局部注意力层0.35G修复 tuple 输入后成功分类头0.02G正常整体 MACs 约为 2.57G对应 FLOPs 约 5.14G。我把这个数字和手算核对了一遍差距在 3% 以内基本可以放心使用了。4.4 一个小技巧把“0 值层”当警报器在封装工具迭代过程中我还养成了一个习惯如果某个模块的 macs 为 0而这个模块又不是 BN 或 ReLU那我就会怀疑是不是漏统计了。这相当于把工具当成一个“报警器”让那些偷偷不生效的模块现出原形。实践中漏统计通常发生在自定义 Attention、GroupNorm、各种 Pooling 层上一旦出现 0第一反应是去查 thop 的注册表有没有覆盖这个层。5. 常见问题速查表与避坑要点把前面所有真实遇到的问题汇总成一张速查表以后遇到类似报错直接翻表查省得再翻历史记录。报错或异常根因快速解决NotImplementedErrorthop 映射表缺自定义层用 clever_register 注册缺失算子AttributeError: tuple object...forward 接收了 list/tuple 嵌套输入包一层 Wrapper把复杂输入拆成 tensorRuntimeError: expected input to have 3 channelsdummy input 通道数和模型不匹配从模型首个卷积层读取输入通道构造输入CUDA/CPU 混用报错模型和输入不在同一设备统一调.cuda()统计结果偏大接近 2 倍多分支结构或共享模块被重复计数抽取共享模块单独统计或除以分支数flops 字段为 0层类型没有被注册查看对应层的计数函数确认输入解析正确FLOPs 数值和论文差一个量级MACs 与 FLOPs 混用、分辨率口径不同统一换算 ×2并严格固定输入尺寸torch.profiler 无 flops 列算子或版本不支持 flops 字段升级 torch 版本或改用 thop 统计理论值除了这张表还有几条我觉得特别重要的原则使用 thop 时输入张量的requires_grad不会影响统计但如果你用的是torch.no_grad()包住整个统计过程某些 hook 的中间缓存行为会不一致建议默认不额外包。统计结果最好每次都在同一个 batch_size 下做。不同 batch 下普通卷积的 flops 是线性增长但对比学习这种结构可能因为矩阵乘法形状不同产生非线性变化。注册自定义算子时优先参考 PyTorch 官方算子的实现方式重点看它对 padding、stride、groups 这些参数的处理避免手写的公式和实际参数不一致。最后也是最重要的一点任何统计工具输出前都要先看一眼代码里有没有对macs和flops这两个字段做混淆。很多第三方库更新后字段名会变你写的计数函数最好自己定义字段别依赖工具的默认行为。我个人在实际操作中的体会是FLOPs 统计这项技能想真正做好不能只靠“拿工具跑一下”而是要理解每一类算子到底做了多少乘法、多少加法。报错并不是坏事每次报错都等于工具在提醒你这里有一个你以为懂了但实际没搞透的细节。把报错集锦里的每一条都走一遍你对模型的计算图理解会明显上一个台阶。后来我偶尔还会主动用一段旧版模型验证新工具的行为把两种结果放一起比对花不了几分钟但能避免很多“当时觉得没问题、后来答辩被问懵”的尴尬时刻。这大概就是踩坑之后最大的收获吧。