跳到主内容
@wquguru
精选90r/MachineLearning(Reddit)技巧与观点

TorchTitan在GH200上训练1T MoE的6条工程优化经验

Scaling to 1T MoE on TorchTitan [P]

原文
发到 X
推荐理由

做大规模分布式训练的同学必看,这篇详细拆解了1T参数MoE在GH200上的具体调优细节和踩坑复盘,全是可复用的工程干货。

Hi everyone!

大家好!

Over the last few months we've been in the trenches with the TorchTitan and PyTorch maintainers, trying to find out how far GH200 (the GPUs inside the JUPITER supercomputer) can be pushed. Short answer: far enough to train a ~1T-parameter MoE at ~1,300 tokens/s per GPU on 384 GPUs. That's 14X where we started, and our starting point was, let's say, humbling.

在过去的几个月里,我们与 TorchTitan 和 PyTorch 的维护者们并肩作战,试图探索 JUPITER 超级计算机内部的 GPU(GH200)的性能极限。简短的回答是:足以在 384 块 GPU 上以每块 GPU 约 1,300 tokens/s 的速度训练一个拥有约 1 万亿参数的 MoE 模型。这比我们的起点提升了 14 倍,而我们的起点嘛,可以说相当令人谦卑。

Along the way we collected a handful of lessons. If you've ever trained a big model, or just stared at a CUDA out of memory, some of this will feel familiar.

在此过程中,我们总结了一些经验教训。如果你曾经训练过大型模型,或者只是盯着 CUDA 内存溢出错误看过,那么其中一些内容你会感到似曾相识。

Here's the tour. If you want the full thing with all the plots, it's on the Domyn blog, and there's a condensed version on the PyTorch blog if you're short on coffee.

以下是本次分享的概览。如果你想查看包含所有图表的完整内容,请访问 Domyn 博客;如果你咖啡时间有限,PyTorch 博客上也有一个精简版。

Hope you like it!

希望你喜欢!

The setup: FSDP + EP, and why I'm not a PP fan

设置:FSDP + EP,以及我为何不喜欢 PP

A 1T MoE doesn't fit on one GPU (shocking, I know), so the first question is how to shard it. This choice ends up dictating basically every optimization that follows, so it pays to think about it. The usual suspects are:

1 万亿参数的 MoE 无法放入单块 GPU 中(我知道这很令人震惊),因此第一个问题是如何对其进行分片。这个选择最终决定了后续几乎所有的优化策略,因此值得深思。常见的方案有:

  • Pipeline Parallelism (each GPU gets a chunk of layers),
  • FSDP (each GPU holds a slice of every weight and gathers the rest just in time),
  • and Expert Parallelism (each GPU owns a subset of experts and tokens travel to them).
  • 流水线并行(Pipeline Parallelism,每块 GPU 获得一部分层),
  • FSDP(每块 GPU 持有每个权重的一部分切片,并在需要时即时收集其余部分),
  • 以及专家并行(Expert Parallelism,每块 GPU 拥有一组子集专家,token 会路由到它们那里)。

We skipped PP. To hide its bubbles you need lots of microbatches, and lots of microbatches means long steps. Long steps mean that when something breaks at scale, and something always breaks at scale, you wait forever to find out. I'd rather iterate fast.

我们跳过了 PP。为了掩盖其气泡(空闲时间),你需要大量的微批次(microbatches),而大量的微批次意味着较长的步骤时长。步骤越长,当大规模训练出现问题时(而在大规模训练中,问题总是会出现),你等待查明原因的时间就越久。我更喜欢快速迭代。

So: 384-way FSDP plus 4-way EP, with EP kept strictly inside a node. The token all-to-all for the experts is comunication that is difficult to hide so it needs to happen fast. On the other hand, comms. on FSDP are already hidden: while it computes layer i, it is already gathering i+1.

因此:采用 384 路 FSDP 加上 4 路 EP,且 EP 严格保持在节点内部。专家的 token all-to-all 通信难以隐藏,因此需要高速完成。另一方面,FSDP 的通信已经被隐藏了:当它计算第 i 层时,已经在收集第 i+1 层的数据。

Why does that matter so much? On JUPITER, InfiniBand gives you ~25 GB/s per GPU. That's 6X slower than NVLink and 36X slower than the Grace-Hopper link. Our baseline step took 84 seconds, and 68 of them were spent waiting on the network. The GPUs were mostly getting paid to sit around. The rest of this post is basically us trying to put them back to work.

为什么这如此重要?在 JUPITER 上,InfiniBand 为每块 GPU 提供约 25 GB/s 的带宽。这比 NVLink 慢 6 倍,比 Grace-Hopper 链路慢 36 倍。我们的基线步骤耗时 84 秒,其中 68 秒都花在等待网络上。GPU 大部分时间只是在闲置领薪。这篇文章的其余部分基本上就是我们如何努力让它们重新投入工作的记录。

Lesson 1: If your GPU is idle, make it work for its memory

经验教训 1:如果你的 GPU 处于空闲状态,让它为显存工作

FSDP moves weights, and the weights gathered per layer don't care how many tokens you feed through them. More tokens per sequence means more compute per byte on the wire, which is exactly what we wanted. The problem is that longer sequences need more memory, so we went shopping for it:

FSDP 移动权重,且每层收集的权重并不关心你通过它们的 token 数量。每个序列更多的 token 意味着每条线上字节对应的计算量更大,这正是我们想要的。问题是更长的序列需要更多的内存,所以我们去采购它:

  • Full activation checkpointing. Recomputing every layer in the backward sounds wasteful, until you remember the GPU was idle anyway. Free compute is free. This alone gave us 4X.
  • Offloading checkpoint activations to Grace over the 900 GB/s C2C link, and prefetching them back one layer before recompute. Grace was right there, so we used it.
  • Muon instead of Adam. One optimizer state per parameter instead of two, and that's what finally got us to 64k-token sequences.
  • 完整的激活检查点。在反向传播中重新计算每一层听起来很浪费,直到你想起 GPU 本来也是空闲的。免费的算力就是免费。仅此一项就带来了 4 倍的性能提升。
  • 通过 900 GB/s 的 C2C 链路将检查点激活卸载到 Grace 上,并在重新计算前一层时预取回它们。Grace 就在旁边,所以我们使用了它。
  • 使用 Muon 代替 Adam。每个参数只有一个优化器状态而不是两个,正是这让我们最终实现了 64k token 序列的训练。

Lession 2: If you don’t shard it, you don’t have to gather it

教训 2:如果你不分片,就不需要聚合

Quick refresher: Muon orthogonalizes each update with a few Newton-Schulz iterations, and Newton-Schulz needs the whole matrix. FSDP, meanwhile, has carefully cut every matrix into 384 slices and scattered them across the job. The naive fix is to gather each matrix on one rank, run Newton-Schulz, and scatter the result back, which means more communication, and more communication is the one thing we absolutely did not need.

快速回顾:Muon 通过几次 Newton-Schulz 迭代正交化每次更新,而 Newton-Schulz 需要整个矩阵。与此同时,FSDP 已经仔细地将每个矩阵切分为 384 个切片并分散到作业中。简单的修复方法是在一个 rank 上聚合每个矩阵,运行 Newton-Schulz,然后将结果散射回去,这意味着更多的通信,而通信正是我们绝对不需要增加的东西。

The trick is to shard smarter. With FlexShard (already in TorchTitan), you choose which rank owns which parameters, so we sharded the experts by whole expert. Every GPU owns complete expert matrices and can orthogonalize them locally with zero communication. The dense matrices (attention projections and friends) still need gathering, but they’re in transit while each rank is busy with its own experts, so that traffic hides behind compute.

诀窍在于更智能地分片。使用 FlexShard(已在 TorchTitan 中),你可以选择哪个 rank 拥有哪些参数,因此我们按完整专家进行分片。每个 GPU 都拥有完整的专家矩阵,并且可以在本地以零通信的方式对其进行正交化。密集矩阵(注意力投影等)仍然需要聚合,但它们处于传输过程中,而每个 rank 都在忙于处理自己的专家,因此这部分流量被计算所掩盖。

There was one catch. We offload the optimizer to Grace, which meant Newton-Schulz was running on the CPU too. Grace is a lovely CPU, but asking it to grind through matmuls is like asking a librarian to move furniture: technically possible, deeply inefficient. The optimizer step took 42 seconds. So we contributed a fix to TorchTitan that runs Newton-Schulz on the GPU even when the parameters live on the host. The optimizer step dropped to ~2 s, and throughput went up 35%.

这里有一个问题。我们将优化器卸载到了 Grace 上,这意味着 Newton-Schulz 也在 CPU 上运行。Grace 是一款优秀的 CPU,但让它去处理矩阵乘法就像让图书管理员搬家具:技术上可行,但效率极低。优化器步骤耗时 42 秒。因此,我们向 TorchTitan 贡献了一个修复方案,即使参数驻留在主机上,也在 GPU 上运行 Newton-Schulz。优化器步骤降至约 2 秒,吞吐量提升了 35%。

Lesson 3: You're only as fast as your slowest rank

教训 3:你的速度取决于最慢的 rank

384 GPUs in lockstep is a bit like a group hike: everyone waits for whoever's at the back. We had two stragglers:

384 个 GPU 同步运行有点像集体徒步旅行:每个人都要等最后面的人。我们遇到了两个拖后腿的情况:

  • Attention cost. A rank that gets a single 60k-token document does far more attention work than one that gets a hundred short ones, even though both see the same number of tokens. We pre-pack rows, bucket them by attention cost, and build each step from rows of similar cost. The worst rank went from 7.9X the average to 1.15X, for +13% throughput.
  • The router. Group-limited routing (two experts picked from each node's group) makes expert load perfectly even, for +7%. The catch is that you're telling the router where it's allowed to send tokens, and early runs suggest that can hurt training. Jury's still out on this one boys.
  • 注意力成本。获得单个 60k token 文档的 rank 比获得一百个短文档的 rank 执行更多的注意力工作,尽管两者看到的 token 数量相同。我们对行进行预打包,按注意力成本将它们分桶,并从成本相似的行构建每一步。最差的 rank 从平均值的 7.9 倍降低到 1.15 倍,吞吐量提升了 13%。
  • 路由器。组受限路由(从每个节点的组中选出两名专家)使专家负载完全均衡,带来 +7% 的提升。问题在于,你是在告诉路由器允许将 token 发送到哪里,而早期运行结果表明这可能会损害训练效果。对此结论尚不明确,各位。

Lesson 4: Accumulate in fp32, ship in bf16

经验教训 4:累加使用 fp32,传输使用 bf16

Gradients come out of the backward in bf16. Summing hundreds of them in bf16, though, is a great way to slowly lose precision, so the standard FSDP mixed-precision setup does the reduction in fp32. That’s the right call. The problem is how it gets there.

反向传播输出的梯度是 bf16 格式。然而,在 bf16 中对数百个梯度进行求和会迅速导致精度丢失,因此标准的 FSDP 混合精度设置会在 fp32 中进行归约操作。这是正确的做法。问题在于数据是如何到达那里的。

NCCL’s reduce-scatter runs as a ring. Each rank receives a partial sum from its neighbor, adds its own contribution, and passes the result along. The thing travelling around the ring is a running sum, so for the accumulation to happen in fp32, the data on the wire has to be fp32 too. FSDP therefore upcasts the bf16 gradients to fp32 before the collective, and every byte of gradient crosses InfiniBand at twice the size it needs to be. On a cluster where InfiniBand is already the bottleneck, that hurts.

NCCL 的 reduce-scatter 以环形方式运行。每个等级(rank)从邻居处接收部分和,加上自己的贡献,然后将结果传递下去。在环中传输的是累加和,因此要在 fp32 中进行累加,线路上传输的数据也必须是 fp32。FSDP 因此在集体通信之前将 bf16 梯度上转换(upcast)为 fp32,导致每个梯度字节在 InfiniBand 上传输时大小都翻倍。在 InfiniBand 已经是瓶颈的集群上,这会带来负面影响。

But only the summation needs fp32, not the transfer. So we swapped the reduce-scatter for an all-to-all on the expert gradients. Each rank sends every other rank the raw bf16 slice that rank owns, no partial sums involved. The owner receives all the slices, upcasts them, and sums them locally in fp32. Same math, same precision where it matters, half the bytes on the wire.

但只有求和操作需要 fp32,传输不需要。因此我们将 reduce-scatter 替换为针对专家梯度的 all-to-all 操作。每个等级向其他所有等级发送其拥有的原始 bf16 切片,不涉及部分和。所有者接收所有切片,将其上转换并在本地以 fp32 进行求和。数学运算相同,关键处的精度相同,线路上传输的字节数减半。

The result matches the fp32 reduce-scatter up to summation order.

结果与 fp32 reduce-scatter 在求和顺序上完全一致。

Lesson 5: When in doubt, send fewer bytes

经验教训 5:拿不准的时候,少传字节

Eventually longer sequences stopped helping (more on why in a second), so we went after communication volume directly:

最终,更长的序列不再有帮助(原因稍后详述),因此我们直接针对通信量下手:

  • FP8 expert all-gather with blockwise scales. Master weights stay in full precision, only the transfer gets squeezed: +18%.
  • Node-local resharding of dense weights after forward, so the backward re-gather runs over NVLink instead of fighting gradients for InfiniBand.
  • 带有块级缩放因子的 FP8 专家 all-gather。主权重保持全精度,仅压缩传输部分:+18%。
  • 前向传播后对密集权重进行节点内局部重排(resharding),以便反向传播中的重新收集(re-gather)通过 NVLink 运行,而不是与梯度争夺 InfiniBand 带宽。

And none of it moved the loss in our comparison runs, which was a relief.

而且这些改动在我们的对比运行中均未影响损失值,这让人松了一口气。

Lesson 6: Your OOM might be lying to you

经验教训 6:你的 OOM(内存溢出)可能在欺骗你

This is my favorite one. After a software upgrade, every single rank started hitting allocator retries at ~70% HBM utilization, with 25–30 GiB sitting free and cached. That's not a memory shortage, that's a memory hostage situation.

这是我最喜欢的一条。在一次软件升级后,每个等级都在 HBM 利用率约为 70%、且有 25–30 GiB 空闲并缓存的情况下开始触发分配器重试。这不是内存短缺,而是内存被劫持的局面。

The culprit: the PyTorch caching allocator keeps a separate pool per CUDA stream, and FSDP allocates buffers on its side streams. Our shiny FP8 and bf16 collectives allocated their temporaries there too, so ~21 GiB ended up parked on side streams where the compute stream couldn't touch it.

罪魁祸首:PyTorch 缓存分配器为每个 CUDA 流维护一个独立的内存池,而 FSDP 在其侧流上分配缓冲区。我们崭新的 FP8 和 bf16 集合操作也在那里分配临时变量,因此约 21 GiB 的内存被闲置在侧流上,导致计算流无法访问。

The fix was to move those allocations onto the compute stream (via FSDP2's private unshard_async_op flag, which is now on its way to becoming a public API) and release gradients a bit earlier. Retries went from 384 ranks to zero, peak reserved memory dropped by 13.5 GiB, and throughput jumped 21%, while sending exactly the same bytes.

修复方案是将这些分配操作移至计算流(通过 FSDP2 的私有 unshard_async_op 标志,该标志正逐步成为公共 API),并提前释放梯度。重试次数从 384 个秩降至零,峰值保留内存减少了 13.5 GiB,吞吐量提升了 21%,同时传输的数据量完全相同。

Wrapping up

总结

更进一步:量化金融体系

看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力

进入量化体系 →

相似阅读

关联信息,但可能不是同一事件