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 的吞吐量能差好几倍。所以做回归测试时,一定要固定硬件环境。

我的习惯:每次跑性能测试前,先跑一个 warm-up 步骤(大概 10-20 步),等 GPU 温度稳定了再开始计时。不然冷启动的数据会偏低,容易误导判断。

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 能帮你发现更深层的问题。

关键点:MFU 的理想范围一般在 40%-60% 之间。低于 30% 说明有严重的计算瓶颈或通信瓶颈,高于 70% 基本不太现实(除非模型极度规整)。

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 反映的是计算效率。

避坑指南:我曾经在汇报时只报了 MFU,结果领导问「为什么硬件利用率这么低?」后来我才意识到,应该同时报 HFU,解释清楚哪些计算是「额外开销」。现在我的习惯是:两个指标一起看,MFU 看效率,HFU 看利用率。

1.4 建立性能回归基线

指标搞清楚了,下一步就是建立基线。没有基线,你后面做的所有优化都是盲人摸象。

我建议按以下步骤来:

  1. 固定环境:硬件型号、驱动版本、CUDA 版本、PyTorch 版本、数据路径,全部固定下来。每次跑回归测试时,环境必须一致。
  2. 跑基准测试:用默认配置跑一次完整的训练流程(至少 1000 步),记录吞吐量、MFU、HFU、显存占用、通信耗时等。
  3. 记录到表格:把结果整理成表格,方便后续对比。

下面是我常用的基线记录模板:

指标 数值 备注
硬件 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 占比
我的建议:基线数据最好用 Git 管理。每次跑完回归测试,把结果 commit 到仓库里。这样哪天性能突然下降了,你可以 git bisect 找到是哪次改动引入的。

1.5 核心知识体系

为了让你更直观地理解这些指标之间的关系,我画了一张图:

多卡训练性能指标知识体系 吞吐量 samples/s | tokens/s MFU 模型算力利用率 HFU 硬件算力利用率 需要 profiler 包含 重计算 三者关系: • 吞吐量:最直观,但受硬件影响大,不适合跨硬件对比 • MFU:衡量模型计算效率,排除通信和框架开销 • HFU:衡量硬件被利用的程度,包含所有计算开销 • 三者结合使用:吞吐量看速度,MFU 看效率,HFU 看利用率 建立基线时,三个指标都要记录,缺一不可

这张图把三个核心指标的关系梳理清楚了。你想想看,如果只盯着吞吐量,你可能会错过 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
注意:这段代码只是演示用。实际项目中,你还需要记录 MFU 和 HFU,这需要用到 profiler 工具(比如 PyTorch Profiler 或 NVIDIA Nsight)。我会在后面的章节详细讲怎么用 profiler。

好了,这一章的内容就到这里。记住:没有基线,就没有优化。先把这三个指标搞清楚,把基线建好,后面的事情才能顺理成章。

可可精品资料库