1. 性能基准定义:理解多卡训练中的吞吐量、MFU、HFU等核心指标,建立性能回归基线
做多卡训练优化,第一件事不是调参数,而是先搞清楚——你到底在优化什么。
我见过太多团队,一上来就改这个改那个,结果性能没提上去,反而把基线搞丢了。说白了,没有基准,就没有回归。你连自己跑多快都不知道,怎么知道优化有没有效果?
这一章,我们就来聊聊多卡训练里那些绕不开的核心指标。我会结合我自己的踩坑经验,帮你把吞吐量、MFU、HFU这些概念彻底搞清楚。
1.1 吞吐量:最直观的性能标尺
吞吐量,英文叫 Throughput,是最简单粗暴的指标。它回答一个问题:单位时间内,我能处理多少数据?
常见的单位有:
- samples/s:每秒处理多少个样本
- tokens/s:每秒处理多少个 token(对 LLM 训练尤其重要)
- images/s:每秒处理多少张图片(CV 任务常用)
计算公式也很直白:
吞吐量 = 总处理样本数 / 总耗时(秒)
举个例子。你用 8 张卡训练一个模型,跑了 1000 步,每步 batch size 是 64,总共花了 200 秒。那么:
总样本数 = 1000 × 64 = 64000
吞吐量 = 64000 / 200 = 320 samples/s
嗯,这里要注意:吞吐量是跟硬件强相关的。同样的模型,A100 和 V100 的吞吐量能差好几倍。所以做回归测试时,一定要固定硬件环境。
1.2 MFU:模型算力利用率
吞吐量虽然直观,但它有个问题——它不告诉你硬件用得好不好。
你想想看,同样是 1000 samples/s,在 A100 上可能只用了 30% 的算力,在 V100 上可能已经跑满了。所以我们需要一个更公平的指标:MFU(Model FLOPS Utilization)。
MFU 的定义是:
MFU = 实际算力吞吐 / 理论峰值算力 × 100%
其中:
- 实际算力吞吐:通过 profiler 工具测出来的实际 FLOPS
- 理论峰值算力:硬件厂商给的峰值 FLOPS(比如 A100 的 FP16 理论峰值是 312 TFLOPS)
我在项目中遇到过一件事:有一次我们优化了一个模型,吞吐量提升了 15%,但 MFU 反而下降了。为什么?因为我们在通信上做了优化,但计算效率降低了。所以只看吞吐量是不够的,MFU 能帮你发现更深层的问题。
1.3 HFU:硬件算力利用率
MFU 有个小兄弟叫 HFU(Hardware FLOPS Utilization)。它俩的区别在哪?
说白了:
- MFU 只算模型本身的 FLOPS(前向+反向)
- HFU 算的是所有操作的总 FLOPS(包括 activation checkpointing、重计算、甚至一些框架开销)
所以 HFU 通常会比 MFU 低一些。举个例子:
模型前向+反向 FLOPS = 100 TFLOPS
实际总 FLOPS(含重计算等)= 130 TFLOPS
理论峰值 = 312 TFLOPS
MFU = 100 / 312 = 32.1%
HFU = 130 / 312 = 41.7%
你看,HFU 反而更高。这是因为重计算虽然增加了计算量,但也让硬件更忙了。所以 HFU 反映的是硬件被利用的程度,而 MFU 反映的是计算效率。
1.4 建立性能回归基线
指标搞清楚了,下一步就是建立基线。没有基线,你后面做的所有优化都是盲人摸象。
我建议按以下步骤来:
- 固定环境:硬件型号、驱动版本、CUDA 版本、PyTorch 版本、数据路径,全部固定下来。每次跑回归测试时,环境必须一致。
- 跑基准测试:用默认配置跑一次完整的训练流程(至少 1000 步),记录吞吐量、MFU、HFU、显存占用、通信耗时等。
- 记录到表格:把结果整理成表格,方便后续对比。
下面是我常用的基线记录模板:
| 指标 | 数值 | 备注 |
|---|---|---|
| 硬件 | 8×A100 80GB | NVLink 互联 |
| 模型 | LLaMA-7B | FP16 混合精度 |
| 吞吐量 | 1250 tokens/s | 每张卡 |
| MFU | 38.2% | 模型 FLOPS 利用率 |
| HFU | 45.6% | 硬件 FLOPS 利用率 |
| 显存占用 | 72.3 GB | 每张卡 |
| 通信耗时占比 | 12.5% | all-reduce 占比 |
1.5 核心知识体系
为了让你更直观地理解这些指标之间的关系,我画了一张图:
这张图把三个核心指标的关系梳理清楚了。你想想看,如果只盯着吞吐量,你可能会错过 MFU 下降的隐患;如果只看 MFU,你又可能忽略了硬件没跑满的事实。所以我的习惯是:三个指标一起看,互相印证。
1.6 实战:用代码记录基线
光说不练假把式。下面我给一段 Python 代码,帮你自动记录基线数据:
import torch
import time
def record_baseline(model, dataloader, num_steps=100):
"""
记录多卡训练的性能基线
"""
device = torch.device("cuda")
model.to(device)
model.train()
# warm-up
for i, batch in enumerate(dataloader):
if i >= 10:
break
batch = {k: v.to(device) for k, v in batch.items()}
output = model(**batch)
output.loss.backward()
# 正式测试
torch.cuda.synchronize()
start_time = time.time()
total_samples = 0
for i, batch in enumerate(dataloader):
if i >= num_steps:
break
batch = {k: v.to(device) for k, v in batch.items()}
output = model(**batch)
output.loss.backward()
total_samples += batch["input_ids"].size(0)
torch.cuda.synchronize()
end_time = time.time()
elapsed = end_time - start_time
throughput = total_samples / elapsed
print(f"吞吐量: {throughput:.2f} samples/s")
print(f"总耗时: {elapsed:.2f} 秒")
print(f"总样本数: {total_samples}")
return throughput
好了,这一章的内容就到这里。记住:没有基线,就没有优化。先把这三个指标搞清楚,把基线建好,后面的事情才能顺理成章。