ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFU模型浮点运算利用率详解:从GPU峰值算力到训练性能调优

MFU模型浮点运算利用率详解:从GPU峰值算力到训练性能调优 先讲一个我实际遇到的场景。去年评估一次 7B 模型预训练进度时值班同事贴了一张 nvidia-smi 截图8 张 A100 的利用率全部 99%看起来一切正常。但照着日志里的 tokens/s 反推发现训练速度比理论预期少了快一半。问题出在哪出在大家习惯用“GPU 利用率”来衡量训练效率而不是用 MFUModel FLOPs Utilization模型浮点运算利用率。MFU 才是拿 GPU 峰值算力去对照训练速度的正确标尺它等于训练过程中“实际完成的有效浮点运算量”除以“GPU 在该精度下的理论峰值浮点运算量”。这篇文章我就把如何自己算峰值算力、如何估算模型总 FLOPs、如何从日志反推 MFU以及 MFU 上不去时怎么定位瓶颈讲清楚。适合做 LLM 预训练、SFT 微调、GPU 集群性能评估的工程师参考。1. 先把 MFU 的定义钉死它不是“GPU 利用率”而是对峰值算力的榨取率1.1 一个真实场景这张“99% 利用率”的卡其实在空转nvidia-smi 里的利用率统计的是“流处理器在一个采样周期内有没有在执行指令”。这其实是个非常粗糙的“忙碌比率”只要 GPU 在等数据的时候保持轮询或者哪怕在跑一些极低效率的访存型 kernel利用率也可能被顶得很高。我遇到的那次复盘就是典型nvidia-smi 显示 99%但实际算一下纯计算量发现每秒完成的有效乘法加法次数远低于这张卡的理论能力。要真正评估“训练速度”是否达标不能看利用率要看 MFU。MFU 衡量的是模型对硬件算力的榨取程度它默认一个前提我们把模型在数学上必须完成的 FLOPs 当作“有效工作量”无论底层用什么 kernel、有没有做算子融合这个工作量是固定的。一旦你测出训练一个 step 的真实耗时就能反推出来每秒完成了多少有效 FLOPs再和 GPU 的理论峰值相除就是 MFU。这里有个关键认知MFU50%不是“GPU 只工作了一半时间”而是“GPU 即便全程在跑也有一半的理论计算能力被浪费了”。浪费可能是通信、可能是访存、可能是 kernel 写得太烂也可能根本就是模型太小喂不饱显卡。MFU 这个指标的好处是直接把一切低效因素统一折算成“算力损失”方便跨机器、跨卡数、跨框架对比。1.2 公式拆解分子是有效 FLOPs分母是理论峰值MFU 的标准计算方式长这样MFU (每秒实际完成的有效 FLOPs) / (GPU 单卡理论峰值 FLOPs × GPU 数量)如果把训练过程拆成 step还可以写成MFU (每个 step 的有效 FLOPs / step 耗时) / (单卡峰值 × 卡数)分子里的“有效 FLOPs”通常指矩阵乘法和注意力计算中被真正需要的运算次数。比如训练一个 7B 模型处理每个 token 大约需要 4.2e10 次浮点运算这个估算下一节细说如果一个 step 里处理了 65536 个 token那么这个 step 的“有效 FLOPs”就是 4.2e10×65536。这个值不关心你用的是 PyTorch 还是自研推理引擎也不关心你有没有开 FlashAttention它是数学层面上绕不开的工作量。分母里的“理论峰值”也不是查一个整数就完事。它取决于精度FP32、TF32、FP16、BF16、FP8 的峰值完全不同Tensor Core 开启与否也完全不同。很多人拿 FP32 的算力做分母得出 MFU 高得离谱也有人拿 FP16 稀疏算力做分母得出 MFU 低得离谱。分母选错这个指标就废了。2. 把 GPU 峰值算力算明白A100/H100 在关键精度下的理论极限2.1 硬件参数表里藏着哪些数字先直接给两张最常见训练卡的官方标称值注意这里都是非稀疏峰值精度NVIDIA A100 80GB SXMNVIDIA H100 SXMFP3219.5 TFLOPS66.9 TFLOPSTF32 Tensor Core156 TFLOPS495 TFLOPSBF16 Tensor Core312 TFLOPS989 TFLOPSFP16 Tensor Core312 TFLOPS989 TFLOPSFP8 Tensor Core不支持1979 TFLOPS这张表是做 MFU 计算的地基。目前主流大模型预训练用的是混合精度框架GradScaler 之类机制会把主权重保持在 FP32但前向计算大量走 BF16/FP16 Tensor Core所以做 MFU 评估时我默认把分母取“BF16 Tensor Core 峰值”。H100 上 FP8 的 1979 TFLOPS 看起来很诱人但 FP8 训练对误差控制的要求很高如果走 FP8 微调路线MFU 分母可以换成 1979但分子里的“有效 FLOPs”不变。这时你会发现 MFU 直接砍半因为精度变了实际可实现的加速比并没有翻倍。所以分母必须和训练实现保持一致否则 MFU 没有可比性。2.2 为什么 Tensor Core 的算力不能直接拿 FP32 来比GPU 的标称算力不是凭空来的推导原则其实是一条公式峰值 FLOPs 处理单元数量 × 时钟频率 × 每周期可完成的浮点运算次数比如 A100 的 CUDA 核心做 FP32每周期可以做 2 次浮点运算FMA 指令一次算 a×bc计作 2 FLOPs核心总数乘上频率就能推出 19.5 TFLOPS。而 Tensor Core 走的是另一个路径H100 的每个 SM 里有第四代 Tensor Core单个时钟周期能完成一组 4×4 矩阵乘法等效算力是普通 FP32 核心的十几倍。这就是为什么同样一张 H100BF16 的 989 TFLOPS 比 FP32 的 66.9 TFLOPS 高出一个数量级。实际做训练优化时你还会遇到“如果不走 Tensor Core只用 CUDA core 跑 FP16”那算力可能只有几十 TFLOPSMFU 直接崩盘。这也是为什么第三方 kernel 库好不好用对训练速度影响那么大同一个矩阵乘手写的简单实现只能跑到三分之一的 Tensor Core 算力而用 CUTLASS 或 cuBLAS 的融合 kernel 能逼近 90% 的峰值。MFU 低不一定是硬件不行很可能是 kernel 没把硬件真正的算力通道打开。2.3 不同训练阶段和精度下用哪个峰值做分母这里给一个可复用的选择规则预训练、全量微调且用混合精度分母选 BF16/FP16 Tensor Core 峰值。A100 用 312H100 用 989。训练过程开了 TF32常见于某些框架的默认 Ampere 策略分母选 TF32 Tensor Core 峰值。A100 是 156H100 是 495。纯 FP32 训练老代码、小模型调试分母选 FP32 标称值。显存不足被迫只用 CUDA core 跑 BF16那就别谈 MFU 了先换 kernel 实现。还有一类容易被忽略的情况分母要不要乘卡数要乘。一个 8 卡企业有效算力是单卡峰值乘 8。但要注意如果日志输出的 tokens/s 是“单卡吞吐”而不是集群总吞吐分母就只乘 1不能乘 8。很多人在这上面栽过跟头后面实测部分我会给出统一口径。3. 模型要“烧”多少 FLOPs从 6NT 公式到一次 Llama-7B 的手算3.1 前向、反向的 FLOPs 如何分摊到每个 token评估训练速度光有分母还不够还得算分子。业界最常用的经验公式是训练一个稠密 Transformer处理每个 token 的平均 FLOPs 大约是 6NN 是模型参数量。拆开看前向传播每 token 约 2N FLOPs反向传播约 4N FLOPs。反向是前向的两倍因为反向不仅要算权重梯度还要算上游梯度等于把前向的矩阵运算再走两遍。这个 6N 的推导很朴素每一层参数 W 在前向时做一次矩阵乘法反向时 W 的梯度更新和输入梯度传播分别各是一次矩阵乘一次前向加两次反向每份参数平均贡献 6 次等效浮点运算。所以一个 7B 参数模型处理单个 token 的“基本功”就是 6×7e9≈4.2e10 FLOPs。但 6N 只是“参数型 FLOPs”注意力部分还有额外开销。Transformer 里每个 head 的 QK^T 和 softmax 之后的加权求和都是序列长度平方级的运算。短序列下这笔开销很小但序列一旦拉长到 8K、16K注意力 FLOPs 会变成不可忽略的存在。3.2 具体手算Llama-7B 在 8 卡 H100 上“理论最少秒数”拿 Llama-7B近似 N7e9练 1T tokens 做个完整算账。简化计算先忽略注意力额外开销公式是总 FLOPs 6N × T 6 × 7e9 × 1e12 4.2e22假设用 8 卡 H100MFU 目标是 50%那么集群每秒实际有效算力是8 × 989e12 × 0.5 ≈ 3.96e15 FLOPs/s理论最少耗时4.2e22 / 3.96e15 ≈ 1.06e7 秒约等于 123 天。这个数字对训练过 7B 规模模型的人很眼熟8 张卡训 1T tokens三个月上下正好是主流开源 7B 的中位数水平。如果你的集群 8 卡 H100 能把 7B 在不到两个月内训完那 MFU 大概率超过 60%属于第一梯队。如果换更大集群比如 256 卡 H100MFU 45% 的情况下有效算力 256 × 989e12 × 0.45 ≈ 1.14e17 FLOPs/s 耗时 4.2e22 / 1.14e17 ≈ 3.7e5 秒 ≈ 4.3 天这个结果说明训练速度不会随着卡数线性提升因为 MFU 会随卡数下降。把卡数翻 32 倍耗时不是变成 123/32 天而是 123/28.6 天左右。所以跨规模评估性能时一定要把 MFU 当成第一指标而不是直接拿总耗时说话。3.3 长序列下 attention 的额外开销怎么补当平均序列长度 s 比较长时总 FLOPs 建议修正为总 FLOPs ≈ 6N × T 4 × L × s × d × T其中 L 是层数s 是平均序列长度d 是隐藏维数。这后面的 4Lsd 就是 attention 的 QK^T 和 attention 向量加权产生的每 token 额外开销。拿 7B 模型举例如果 L32、d4096、s8192那么额外开销约为4 × 32 × 8192 × 4096 ≈ 4.3e9 FLOPs/token和 4.2e10 的基础值相比占 10% 左右。这个比例在短序列下几乎可以忽略但在 32K 长序列下会逼近 30% 以上。如果你训练的是长上下文模型还拿 6N 估算分子算出来的 MFU 会整体偏高好像机器很快实际上注意力部分早就吃掉了大量算力。另一个容易漏掉的分子来源是 embedding 和 LM head。如果 7B 的 N 已经包含这些参数那直接用 6N 没问题如果模型结构定义里把 embedding 排除在外比如 vocab_size32000、d4096那每个 token 还要加 6×2×32000×4096 的 FLOPs又是一笔不小的数字。最稳妥的做法是写代码时直接从模型配置里动态算别在常量的选择上挣扎。4. 实测 MFU 的完整换算从训练日志反推每一秒的有效算力4.1 拿到日志后先统一单位MFU 不是为了写在 PPT 上的一个数字而是要能随时从训练日志里算出来。第一步是统一单位。日志里常见的有两种输出格式一种是每个 step 的总耗时和 token 数比如“global_batch64seq_len4096step_time1.2s”另一种是直接给吞吐比如“throughput35000 tokens/s”。两种都可以但必须把“每卡”和“集群总量”分清楚。我自己的习惯是先用前面第 3 节公式算出一个“每 token FLOPs”然后只记录集群总吞吐集群总吞吐 step 内所有 GPU 处理的 token 总数 / step 耗时如果日志直接给了每卡吞吐就乘上卡数。接着每秒有效 FLOPs 每 token FLOPs × 集群总吞吐最后除以“集群理论峰值”也就是单卡峰值乘卡数。这样做的好处是不管你是看 step time 还是看 tokens/s都能算到同一个 MFU方便交叉验证。4.2 一个可以抄的小脚本下面这段代码可以直接塞进训练工程每 N 个 step 打一条 MFU 日志。核心只是几个乘法除法不依赖任何 profiler。def compute_mfu(tokens_per_sec, flops_per_token, peak_tflops_per_gpu, num_gpus): achieved_tflops tokens_per_sec * flops_per_token / 1e12 peak_tflops_total peak_tflops_per_gpu * num_gpus return achieved_tflops / peak_tflops_total # 例8×H100 训练 7B日志输出集群总吞吐 100000 tokens/s # 每 token FLOPs 约 4.2e10包含部分注意力开销 mfu compute_mfu(1e5, 4.2e10, 989.4, 8) print(f{mfu:.1%}) # 约 53.1%如果日志给的是 step_time 而不是吞吐就这么算def compute_mfu_from_step(step_time, total_flops_per_step, peak_tflops_per_gpu, num_gpus): achieved_tflops total_flops_per_step / step_time / 1e12 peak_tflops_total peak_tflops_per_gpu * num_gpus return achieved_tflops / peak_tflops_totaltotal_flops_per_step 来自每 token FLOPs × 每个 step 的 token 数其中 token 数包括梯度累积中的所有 micro-batch。注意这一步很容易错step 和 iteration 在分布式训练里的语义经常重叠日志里的 step 到底是“一次梯度更新”还是“一个 micro-batch”必须确认清楚。4.3 MFU 的波动代表什么从预热期到稳定期我第一次给一个 70B 模型记录 MFU 曲线时发现前 200 步 MFU 只有 15% 左右之后一路爬升到 40%。这不是调参调出来的而是预热阶段学习率小、优化器状态初始化、数据加载缓存没热加上部分 kernel 在第一次运行时要做 cuBLAS/FlashAttention 的 autotune。所以看 MFU 不能只看某一个时间点要取训练进入稳定期后的移动平均。稳定期里的 MFU 也不是一条水平线。如果你的训练数据里有大量 padding 或长短序列不均衡MFU 会跟着 batch 内的有效 token 数抖动如果集群里有人偶发跑别的任务抢占 NVLink 带宽MFU 也会骤降。因此我建议把 MFU 作为实时监控曲线放在 dashboard 上而不是只在复盘时算一次。异常下降往往比绝对数值更有诊断价值。5. MFU 为什么上不去通信、访存和算子实现的瓶颈定位5.1 三类瓶颈如何区分计算、访存、还是通信遇到 MFU 低的时候先别急着堆并行策略。我个人的经验是分四步照 X 光第一看计算占比。用 PyTorch Profiler 导出一份 step 的 kernel 时间线统计 Matrix Multiplication、Softmax、LayerNorm 这些算子的累计耗时占比。如果矩阵乘法的总耗时占比低于 60%说明计算没有主导瓶颈在别处。第二看访存瓶颈。GPU 算力再高数据从 HBM 搬到 SM 的带宽是有限的。LayerNorm、dropout 这种小算子计算量极低但要把整个 tensor 读一遍写一遍属于典型的访存密集算子。如果一个 step 中有大量这种小 kernelGPU 大部分时间在等待显存数据MFU 自然上不去。这类情况的迹象是MFU 不高但 nvidia-smi 的“Memory Copy”引擎利用率很高。第三看通信占比。分布式训练中 all-reduce 是逃不掉的。一次 all-reduce 的字节数大概是“梯度大小 × 2”比如 7B 模型 BF16 梯度是 14GB8 卡通过 NVLink 做 ring all-reduce理论通信耗时约 30ms。如果卡间走 PCIe 或以太网这个数字会翻几十倍一旦通信耗时超过 step 总耗时的 20%就必须处理。第四看 GPU 本身是否健康。某些云环境里可能会偶发 Xid 79、GPU Crash Dump 这类硬件事件表现为某个节点 MFU 掉零或吞吐骤降。这种时候先别优化代码去查 dmesg 和驱动日志。GPU 掉总线这样的硬故障不是算力问题却会把整轮的 MFU 统计拉出一个大坑。5.2 代码级定位直接用 profiler 看 kernel 的占比具体操作上我会把一个 step 单独跑一遍用torch.profiler抓 kernel 表然后按self CUDA time排序。重点看三个东西单个 kernel 耗时最长的前五个分别是什么Attention 相关 kernel 有没有走 FlashAttention如果看到一串自定义的 softmax/scores 计算说明优化没到位。有没有大量小于 5 微秒的小 kernel数量太多说明算子粒度太细该做融合。矩阵乘GEMM通常代表计算密度的上限如果 GEMM kernel 的占比很高但 MFU 依然很低那多半是形状太小比如 batch size 偏小导致单次矩阵乘的 M 和 N 维度不够Tensor Core 的空闲率极高。这时候调大 batch 的效果比换任何 kernel 都明显。5.3 三个非常隐蔽的 MFU 杀手第一个是 sequence packing 没做好。很多数据集的样本长度不等有人直接在每个 batch 末尾填充到固定长度。假设平均真实长度只有最大长度的一半那 GPU 有一半 FLOPs 花在 padding token 上而 MFU 的计算分子并不会把 padding 当作有效工作量于是 MFU 直接腰斩。解决方案是 seq packing 或按长度分桶把相近长度的样本放进同一个 batch。第二个是梯度累积和通信没有 overlap。开梯度累积时不少框架的做法是每个 micro-batch 算完就把梯度做 all-reduce等通信结束再算下一个 micro-batchGPU 在通信期间全程空闲。应该只做累积等最后一个 micro-batch 完成后再统一 all-reduce或者用延迟 all-reduce 把通信时间藏到下一段计算后面。第三个是 activation checkpointing 用得太粗。长时间训练大模型激活值显存爆掉很多人直接全量开 checkpoint结果每个 transformer 层都重算一遍反向计算量额外增加 30% 甚至更多。正确的做法是按显存余量只对部分层开 checkpoint或者用选择性 checkpoint 只保存激活值更小的模块。这些省下来的算力最后都能体现在 MFU 上。6. 提升 MFU 的实操清单从数据管线到 Batch Size 的调优顺序6.1 按性价比排优先级先调数据再调算子最后折腾并行策略很多团队一上来就改并行策略ZeRO、FSDP、TP、PP 全都用上结果 MFU 反而更低了。我的建议是保持并行方案不变先做低垂果实第一优先是数据管线。确认 dataloader 的预取进度是否比 GPU 计算快。用DataLoader(..., num_workers8, prefetch_factor4)这种配置只是第一步更稳妥的是把数据转成内存映射格式或使用 streaming dataset避免每 epoch 重复做 tokenize 和 shuffle 的 IO 开销。如果 dataloader 是瓶颈GPU 会周期性空转这比任何 kernel 优化都致命。第二优先是算子融合。把 LayerNorm 残差 dropout 融合成一个 kernel把 Adam 更新融合进反向传播后的 kernel 里这些都不用动模型逻辑。FlashAttention 基本是开箱收益代价只是显存少一点。这套组合拳通常能把 MFU 拉高 10 到 15 个点。第三优先才是并行策略。如果单卡 shape 已经喂不饱 Tensor Core再多的并行也不会有本质提升。先确保单卡 MFU 在 50% 以上再考虑分布式扩张带来的通信损耗。6.2 一张速查表现象与对应调整现象可能瓶颈调整方向MFU 20%但 step time 稳定数据加载跟不上或 padding 过多检查 dataloader 占用开启 seq packing日志吞吐有周期性的塌陷多机通信中出现慢节点检查网络健康度启用拓扑感知的卡间分配小模型训大卡MFU 偏低算力喂不饱加大 global batch或换更细精度的 kernelattention kernel 占据 CUDA time 前五未启用 FlashAttention切换到 fused attention 实现MFU 在预热期后仍持续下降显存碎片触发更多显存拷贝检查 PyTorch 显存分配器开启保留缓存机制单卡 MFU 高集群 MFU 低通信和计算没有 overlap延迟 all-reduce或改用 FSDP 分片MFU 抖动剧烈且伴随硬件事件日志GPU 掉总线/驱动异常先修硬件健康别优化代码这张表不是我拍脑袋写的每一条都在实际任务中踩过。比如“小模型训大卡”是很多人忽视的场景在 8 卡 H100 上微调 1B 模型哪怕代码写得再好单卡一次矩阵乘的宽度也不足以撑满 989 TFLOPSMFU 能到 20% 已经不错。这不是优化能力问题是算力和模型规模不匹配的问题。解决办法要么加大 batch要么把任务分散到更合理的卡数上。6.3 我的一次 MFU 从 18% 到 42% 的调优记录最后分享一次实际操作。某个 7B 的 SFT 任务8 卡 H100初始 MFU 只有 18%。当时日志显示集群吞吐约 35000 tokens/s明显偏低。我先用 profiler 抓 kernel发现注意力部分是一个老旧的手动实现QK^T、softmax、乘 V 拆成了三个小 kernel每个 kernel 之间的显存读写都在浪费带宽。替换成 FlashAttention 之后MFU 到了 28%。接着发现 dataloader 的 tokenize 用了动态 padding最大长度 4096平均长度只有 2100 左右一半的算力花在填充 token 上。改成按长度分桶 少量 seq packing 后MFU 跳到 37%。最后把梯度累积的通信改成延迟 all-reduce并在每个 micro-batch 结束时同步把下一 batch 的数据预取到 CPU pin memoryMFU 稳定在 42% 左右。整个过程中模型的代码结构一点没变方向和参数都没动只是把数据、算子、通信三个层面的浪费清了一遍。这个案例足够说明MFU 不是一个“天花板分数”它是每一层资源浪费的镜子。每次调优只要能把一个环节的浪费堵住MFU 就会实实在在地涨上去。最后再分享一个实用的小技巧同一份训练 log同时打印 tokens/s、step_time 和 MFU 三个量。tokens/s 让你直观知道速度step_time 让你知道调度节奏MFU 让你知道离硬件极限还有多远。下次再有人问你“这 GPU 到底跑满没有”先让他把这三项发出来再谈优化思路。
返回列表