☰
深度学习模型FLOPs计算实战:工具选型与踩坑排查指南
2026/10/20 2:59:30 网站建设 项目流程

算FLOPs这件事,说难不难,说简单也真不简单。做模型部署、论文消融实验、或者给别人对比模型复杂度的时候,都要用到这个数字。但这个数字恰恰又是最容易骗人的:不同工具算出来的FLOPs可能差好几倍,同一个模型换个输入尺寸结果也有偏差,更不用说模型里只要有一点自定义算子或者动态控制流,报错就能铺满一屏。我这篇文章就把自己实际调模型时踩过的计算FLOPs报错和坑系统整理一下,从工具选型到高频报错,再到复杂模型的特殊处理,尽量让你看完之后少走弯路。

这适合正在跑实验的在校同学、做模型优化的算法工程师,以及任何需要量化模型计算量的开发者。我会尽量写实际操作层面的东西,包括API怎么调、报错怎么解、自定义算子怎么补统计,顺便把为什么会出现这些问题的底层原因也讲清楚。不是说光把报错贴出来就完事,而是让你以后再遇到同类问题,自己能动手排查。

1. 工具选型:不同场景下该用哪个FLOPs计算库

拿到一个模型,第一件事可能就是打开搜索引擎,然后发现主流的计算工具有ptflops、thop、fvcore这么几个。选错工具很常见,但更常见的是根本不知道这些工具的统计口径存在差异。这一部分先把工具选型聊明白,后面再逐个讲报错才有意义。

1.1 三个主流库的统计口径差异

先说ptflops。这个包本质上是遍历模型的每个子模块,在模块的forward前后挂hook,拿到输入输出的tensor shape之后,按照算子类型去估算浮点计算量。它的统计粒度是“模块级别”,也就是说只要你的模型能正常跑通一次forward,它基本就能给出一个结果。输出格式默认是一张按层分组的表格,还会给出MACs和参数量。

这里要注意一个关键概念:ptflops返回的其实不是FLOPs,而是MACs。MACs是乘加运算次数,一次乘加在理论上等于两次浮点运算FLOPs,所以如果你看到论文里写“3.8 GFLOPs”而ptflops给的是“1.9 GMACs”,这属于正常现象,不是算错了。很多刚接触这个领域的人在这里会懵很久。

再看thop。这个包使用起来更简单,只需要把模型实例和随机输入传进去,调用profile函数就能拿到flops和params。但它比较挑算子,如果你的模型里用了某些自定义的Function或者比较新的激活函数,thop可能直接跳过不统计,或者给出一个明显偏小的数字。如果你想快速验证一个模型的参数量级,thop很方便;但是要做到细粒度、可解释的统计,thop就不太合适了。

最后是fvcore。它来自某大厂开源的那套视觉工具链,设计得相对严谨,支持按operator维度拆分统计,也就是说你可以清清楚楚地看到某一个卷积层、某一个矩阵乘法分别贡献了多少计算量。它对自定义算子的指示方式更灵活,有自己的算子注册机制。对于Transformer这种大量使用矩阵乘法和softmax的结构,我用下来fvcore的统计结果比ptflops更符合直觉。

工具返回单位细粒度支持自定义算子支持上手难度适用场景
ptflopsMACs按模块通过hook覆盖低常规CNN、快速统计
thopflops弱弱最低简单模型、粗算
fvcore实际算子次数按算子通过注册机制中Transformer、复杂模型

1.2 我实际使用的选型逻辑

我的习惯是这样的:如果是新拿到的模型,先用ptflops跑一遍,因为它对网络层级的打印最清晰,直接在终端里就能看到每一层占了多少MACs,方便快速定位是不是某个特殊层拖慢了整体。如果需要做更严谨的实验对比,尤其是对比两个变体模型的计算量差异,我会用fvcore,因为它能把差异精确到具体的算子类型。

举个例子,我手头有一个目标检测模型,骨干是某种Transformer结构,预测头是普通卷积。用ptflops统计时,输出的MACs在骨干部分和我预期的差不多,但预测头部分数值低得离谱。换成fvcore之后才发现,问题出在预测头里有一个动态索引操作,这个操作可能是0 FLOPs,但在真实推理中需要GPU做大量数据搬运,延迟很高。这个案例也引出了一个重要观念:FLOPs只是理论计算量,和实际推理速度不一定是线性关系,某些访存密集型操作在FLOPs统计里几乎隐形,却极耗时间。

选型时还要注意一点,就是工具的版本兼容性。ptflops和thop对PyTorch版本的敏感度很高,尤其当PyTorch升级到新版本后,某些模块的forward签名变了,旧版工具就会因为hook参数对不上而报错。所以我一般会在虚拟环境里固定PyTorch和这些工具的版本,避免升级之后原本能跑通的统计脚本突然罢工。

2. 高频报错实录与排查思路

真正动手统计FLOPs的时候,报错才是常态。我按自己遇到的频率给这些报错排个序,从输入尺寸问题到model内部结构问题,逐一讲清楚报错原因和解决思路。

2.1 输入尺寸格式不匹配

这类报错在ptflops里最常见。比如一个模型期望的输入是(1, 3, 224, 224),你在调用get_model_complexity_info时却写成了(224, 224, 3),那么第一次forward就会在某个卷积层报维度错误。这类报错多半长这样:

RuntimeError: Expected 4-dimensional input for 4-dimensional weight [64, 3, 7, 7], but got 3-dimensional input [3, 224, 224] instead

我见过很多朋友在这里卡住,以为模型有问题,其实只是input_res这个参数需要的是(C, H, W),你要是按(H, W, C)传进去,自然就会把通道数当成宽高来解析。另外这个参数默认是不带batch维度的,ptflops内部会自己包装成(1, C, H, W),所以不要在元组里额外加一个1,否则会变成(1, 1, C, H, W),一样的道理又错一次。

还有一个容易忽略的细节,就是模型的forward里如果写了assert x.size(2) == 224这类硬性校验,而你传入的是(3, 256, 256),那报错就会以断言失败的形式出现。这种情况不是FLOPs工具的问题,而是你的输入尺寸不符合模型本身的约束。我的建议是:在统计FLOPs之前,先准备一个真实尺寸的tensor直接跑一次model(input),保证模型能正常forward,再拿给FLOPs工具去统计,能省掉一半以上的低级报错。

2.2 input_constructor的正确打开方式

有些模型的forward不是只接收一个tensor,比如有两个输入、或者forward除了输入数据还有return_intermediate这种控制参数。这时候直接调用ptflops的get_model_complexity_info就会报这类错误:

TypeError: forward() takes 2 positional arguments but 3 were given

第一次遇到这报错时,我一度以为ptflops不能用在这个模型上,后来才发现它专门提供了input_constructor参数,用来构造一个自定义的输入生成函数。这个函数接收一个input_res,返回一个可以直接解包到forward函数里的字典或元组。

这里有个很容易踩的坑:input_constructor返回的字典里,键必须和forward的参数名完全一致,包括关键字参数。我之前在一个检测模型里想传入images和masks两个输入,写的时候把其中一个键拼错了,结果报了一个很诡异的“positional argument重复”错误。排查了很久,最后发现就是拼写问题。以后遇到这种问题,先打印input_constructor的返回值和forward的签名去逐一比对。

2.3 自定义模块导致NotImplementedError

用的工具不认识你模型里的某个自定义模块,这个报错说常见也常见,说烦人也烦人。比如你写了一个基于稀疏卷积的模块,或者在forward里调用了某些PyTorch没有注册到标准算子集合里的操作,ptflops会给出类似这样的提示:

NotImplementedError: The following modules are not supported: [<class 'MySparseBlock'>]

thop在这种情况下更“佛系”,它可能不报错,但在最终输出里直接忽略这个模块,导致统计结果少了一大块。fvcore相对好一些,它会给出warning,同时尽量把能识别到的算子统计进去。问题在于,如果你不仔细看warning,根本发现不了有些算子没被统计。

处理思路有两个,一是修改统计工具的hook映射,为自定义模块写一个专用的FLOPs计算逻辑;二是直接不依赖工具自动统计,自己写一段遍历模型参数的代码,根据每个张量的shape去手动估算。前者开发量小但不够通用,后者比较费时间但可控性强。对于生产环境中的复杂模型,我倾向于两者结合:用fvcore统计主路径,对自定义模块单独补充估算值,最后在论文或报告里注明统计口径。

2.4 动态shape模型统计失真的根源

动态shape是FLOPs统计里最容易被忽视但又最影响准确性的场景。最典型的就是Transformer类模型,输入序列长度不固定,或者模型内部有类似if token_len > 64的控制分支。FLOPs工具在统计时只能针对某一个具体的输入shape展开一次forward图,所以它输出的是“在这个shape下的理论计算量”,而不是“整个动态范围内的平均计算量”。

我处理过一个输入长度在10到200之间浮动的文本分类模型,用固定长度512去统计FLOPs,得到的数值是实际长序列推理时的好几倍,因为长序列在Softmax部分的计算量按序列长度的平方增长,而短序列下这部分几乎可以忽略。这个问题的根源在于:FLOPs统计工具默认你传入的是什么shape,它就按什么shape展开静态计算图,动态控制流不会真实执行所有分支。

这类问题的解决方案没有捷径,只能分多个长度分别统计,然后按实际业务场景给出合理的FLOPs区间。比如在模型对比时,统一采用最大输入长度去统计,至少保证对比基准一致。

3. 实操过程:覆盖算子、处理特殊层与模式切换

从报错中恢复只是第一步,真正让FLOPs统计变得可信,还需要处理自定义算子、特殊层、训练模式切换等一系列问题。这一部分我按实际操作流程来写,从代码层面给出可以直接复用的做法。

3.1 用hook补齐自定义算子的统计

当我们确定某个自定义模块被统计工具忽略或直接报错时,最直接的处理方式是为它写一个hook函数。以ptflops为例,它允许通过custom_modules_hooks传入一个字典,把具体的模块类映射到我们自己的计数函数上。

from ptflops import get_model_complexity_info def my_custom_module_macs(module, input, output): # 这里可以根据模块内部参数自行计算MACs in_shape = input[0].shape out_shape = output.shape # 以某个自定义卷积块为例:参数量 3x3,输入输出通道相同 batch_size = in_shape[0] in_ch = in_shape[1] height, width = in_shape[2], in_shape[3] macs = batch_size * in_ch * height * width * 9 * out_shape[1] return macs # 只返回该模块贡献的MACs model = MyModel() macs, params = get_model_complexity_info( model, (3, 224, 224), custom_modules_hooks={MyCustomBlock: my_custom_module_macs}, as_strings=False, verbose=False, )

这种方案的本质是告诉统计工具:遇到这个模块时,不要用默认的通用规则去猜,而是用我指定的计算逻辑。hook函数入参中的input是模块接收到的tensor列表,output是模块返回的tensor,我们可以在这两者之间完成一切所需的shape计算。写hook时最需要注意的是不要引入额外的tensor运算,因为这个函数本身也会被统计工具追踪,一旦在里面跑张量计算,可能会造成递归统计或者重复计数的奇怪问题。我通常只用shape和整数乘法来算MACs,避免踩坑。

3.2 多输入场景:构造一个可控的输入生成器

多输入模型在FLOPs统计时最大的痛点在于:工具不知道该怎么生成合适的输入数据。比如检测模型可能需要images和orig_target_sizes两个输入,前者是图像tensor,后者是缩放尺寸的列表。直接用默认方式去调用,基本都会报参数数量不匹配。

ptflops的解决方案是通过input_constructor,我们需要手工写一个输入构造函数,它接收标准化的input_res,并按我们的意图返回一组可用于前向传播的数据。下面的代码展示了一个典型的多输入场景:

from ptflops import get_model_complexity_info input_res = (3, 640, 640) def input_constructor(input_res): # 输入尺寸是(C, H, W) c, h, w = input_res images = torch.randn(1, c, h, w) orig_target_sizes = torch.tensor([[h, w]], dtype=torch.float32) return {"images": images, "orig_target_sizes": orig_target_sizes} macs, params = get_model_complexity_info( model, input_res, input_constructor=input_constructor, as_strings=True, print_per_layer_stat=True, )

像这种代码里,我把images做成随机tensor,把orig_target_sizes做成和输入分辨率一致的tensor。注意orig_target_sizes的数据类型,某些模型里它会被强制要求为浮点tensor,你给成整数tensor可能在随后post-processing阶段触发类型错误。整体来说,多输入场景的处理核心就是:宁愿花十分钟把输入构造函数写清楚,也不要寄希望于工具自动猜测。

3.3 eval模式和train模式对统计结果的影响

这个点很少被提到,但确实会影响到统计结果。模型在train模式下会开启Dropout、BatchNorm的batch统计等操作,这些操作在FLOPs统计里通常计算量很小或者为零,但问题是如果模型里带有明显的训练分支,比如某些自监督模型的predictor只在训练时启用,那么统计结果就会因为模式不同而出现明显差异。

我在跑一个自编码器对比实验时就发现,在model.train()模式下统计的FLOPs比model.eval()模式下高了不少。排查后发现,训练分支里有一条额外的全连接通路,只在训练时参与forward,这部分的矩阵乘法计算量在总统计中占比还不小。所以如果你要比较两个模型的复杂度,建议统一使用eval模式,也就是在调用FLOPs工具之前先执行一次model.eval(),确保模型处于推理状态。这样统计得出的数字才和真实部署场景更接近。

此外还要注意输入tensor是否需要梯度。统计工具通常会在torch.no_grad()环境下运行,但如果你的模型forward里存在某些依赖于梯度计算的分支,或者自定义了带有detach()语义的操作,统计过程中可能会发生异常。这种情况下,最简单的做法是在调用统计工具前显式包裹with torch.no_grad():,不给模型任何 retain graph 的机会。

3.4 用DataParallel/DDP包装模型时的小心机

模型被nn.DataParallel或nn.DistributedDataParallel包装之后,模块树的最外层变成了DataParallel,内部才是真正的模型主体。直接用FLOPs工具去统计包装后的模型,有时会得到参数量为0或者MACs为0的诡异结果,这是因为工具无法把DataParallel映射到标准算子。解决方式很简单:统计FLOPs时用model.module,也就是绕过并行包装层,直接统计内部模型。

这是一个特别容易被忽略的坑,尤其在训练脚本里已经习惯了使用model对象,拿到测试脚本时忘了模型实际上是从checkpoint里加载同时又包了一层DataParallel。我建议所有做FLOPs统计的实验脚本里都统一加一个判断:

if hasattr(model, "module"): model = model.module

这行代码可以避免你在半夜调实验时因为一个诡异的零值统计结果而满头问号。

4. 排查技巧与结果合理性验证

解决了报错、统计出了数字,Line还不算结束。最关键的一步,是要确认统计结果是合理的,否则后续所有分析都建立在错误的地基上。对于堆过无数技术细节的开发者来说,肉眼判断一小时,不如二次确认十分钟。

4.1 从FLOPs数值本身出发的快速验证

简单模型我们可以手动核验。比如一个普通的3x3卷积层,输入是[1, 64, 112, 112],输出通道是128,输出分辨率是[1, 128, 112, 112],那么该层的MACs大概就是:

MACs = 输出元素数 × 卷积核尺寸 × 输入通道数 = (1 × 128 × 112 × 112) × (3 × 3) × 64 = 1,608,704 × 576 ≈ 926,853,120

这个数字大约是0.93 GMACs,换算成FLOPs是1.85 GFLOPs。如果统计工具输出的数值和这个手算结果差了几个数量级,那大概率是统计口径出了问题。对于复杂模型,我一般会拆出某一个标准卷积层去单独验证它,然后再整体看汇总值。这个方法简单粗暴,但非常有效,尤其是能快速甄别工具是否漏统计了某些层。

还有一种验证思路是改变输入尺寸。卷积层的FLOPs随输入分辨率线性增长,Transformer的自注意力部分随序列长度平方增长。如果你把输入从224改成448,发现FLOPs按4倍增长而不是2倍,那说明模型和工具统计的确实是平方项主导的模式,这个结果和理论预期一致。如果出现非线性增长又不明原因,就要怀疑是不是有动态索引或数据搬运类操作混入了统计范围。

4.2 常见报错信息速查表

把实操中遇到的报错整理成表格,以后遇到相同问题直接查表定位,比重新翻一遍文档快很多。

报错现象常见原因处理建议
Expected 4-dimensional input输入尺寸参数顺序写错确认为(C, H, W),不要带batch维
forward() takes 2 positional arguments模型forward有多个参数使用input_constructor构造完整输入
NotImplementedError: modules not supported存在自定义模块为模块编写自定义hook或改用fvcore
No operators found for XXX工具不识别某算子输出warning检查,补充手动估算
KeyError: 'onnx::Gather'基于ONNX推理时遇到不兼容算子避免用ONNX做FLOPs统计,用PyTorch原图
统计结果为0或明显偏小DataParallel包裹、模式错误用model.module并设置model.eval()
多次运行结果不一致模型内部存在随机控制流固定随机种子或按最大路径统计

4.3 跨工具验证与版本锁定

我现在的习惯是,重要的FLOPs数字至少同时用ptflops和fvcore各跑一遍,两边的数量级一致才敢写进报告里。如果两边差很多,我不会盲目相信其中任意一个,而是逐个检查差异来源,通常是某个模块在一个工具里被统计了,在另一个工具里被忽略了。比如某些库对torch.nn.functional.linear的支持程度不同,就会导致全连接层被计或不计的差别。找出差异层、明确统计口径,再写结论,这才是一个严谨的工程做法。

版本锁定也是重要一环。我吃过一次亏:某次PyTorch小版本升级后,torch.nn.MultiheadAttention内部实现变化,ptflops旧版本不能识别新的forward签名,统计结果直接少了一块。之后我在所有涉及FLOPs统计的项目里都建立了独立的环境,并用requirement文件固定了ptflops、fvcore、torch的版本。如果你的环境升级过,而FLOPs统计数字出现了不合理的变化,优先排查这几个库的版本兼容性。

5. 几个经常被忽略的使用细节

FLOPs统计这件事表面上只是调用一个函数,但实际上涉及很多细节。这里集中写几个我在StatFLOPs过程中学到的细节,希望能帮你在实际使用中少走一些弯路。

5.1 统计口径对结论的影响:MACs与FLOPs别混着用

MACs和FLOPs的混淆是使用中最常见的问题。我见过不少人在分享模型对比时,把ptflops输出的GMACs直接当作GFLOPs写入论文,导致计算量数字变成原先一半。正常情况下,一次乘加运算(MAC)包含一次乘法和一次加法,在GPU上可以并行完成,所以理论上1 MACs = 2 FLOPs。不同库的默认输出也不一样,ptflops输出MACs,fvcore输出FLOPs,thop则因版本而异。同一篇文章里如果混用了两种口径,结论就没有可比性。

所以我的建议是:在实验记录里同时存下原始输出和工具名,或者统一换算成FLOPs再记录。最稳妥的做法是脚本里显式转换:

# ptflops返回的macs是MACs,乘以2得到FLOPs flops = macs * 2

这样可以避免在后续整理数据时出现单位认知混乱。

5.2 输入分辨率对FLOPs的影响不是线性的

网络里的不同层对输入尺寸变化的敏感度不同。卷积层的FLOPs随空间尺度线性变化;全连接层完全不受输入分辨率影响,只和特征维度有关;自注意力的复杂度与序列长度呈平方关系。所以统计FLOPs时必须明确写出输入分辨率,不然别人看到“这个模型FLOPs为15G”却不知道是在多大输入下算出来的,这个数字就没有意义。

模型对比时可能需要在多个分辨率下分别统计,再画一条FLOPs-分辨率曲线。尤其检测模型往往使用多尺度训练,如果你只统计最大尺度下的FLOPs,结果会显得很高;只统计最小尺度又会显得很低,需要结合实际部署场景给出合理数值。

5.3 激活函数对FLOPs统计的贡献

很多统计工具对于ReLU、GELU这类element-wise操作默认不计入FLOPs,因为它们的计算量在整层网络里占比太小。但我在处理一些极轻量级网络时发现,激活函数占比会被放大,比如一个只有两层卷积的微型模型,GELU的计算量可能占整体的5%以上。如果你的实验对象是轻量模型,建议选用fvcore这类支持element-wise算子统计的工具,同时在对比时注明是否包含激活函数。

FLOPs本身只是一个理论值,它不反映访存开销,也不反映算力利用率。同一个FLOPs的模型,在GPU上可能因为访存模式不同而表现出两倍的推理速度差异。所以面对FLOPs统计工具给出的数字,保持清醒,至少确认它是什么口径、覆盖了哪些算子,再去下结论。

5.4 我的一些个人习惯

每次跑FLOPs统计,我都会在代码里先加一个断点,确认模型已经处于eval状态,并且输入张量先正常走一次forward再交工具统计。这个习惯帮我过滤了大量因为模型未初始化完全或状态错误导致的报错。顺便一提,统计FLOPs的时间最好是刚加载完checkpoint之后,此时模型参数已经定型,统计结果不会被随机初始化的Module影响。

还有一个细节是:如果模型包含了像torch.einsum这种自定义的维度变换式子,手写hook时容易漏算其中某个维度的乘积。遇到einsum时我常用fvcore去追trace,看看内部展开成哪种算子,再按实际展开后的算子去估算。这么做比直接猜公式或者直接忽略要可靠得多。

6. 最后的经验之谈

作为经常和模型复杂度打交道的人,我越来越觉得FLOPs工具只是辅助,不是权威。不同工具、不同版本、不同输入尺寸,算出来的数字都可能不一样。真正重要的是我们自己清楚每个数字是怎么来的,统计口径是什么,覆盖了哪些算子,而不是拿着一个现成数字放进论文里就万事大吉。写这系列踩坑记录,也是希望读到这篇文章的人能少走我走过的弯路。也建议你养成一个习惯:每次统计FLOPs都把工具名称、版本、输入尺寸、模型状态一起记录下来,遇到数字异常时回溯排查会顺利得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询