☰
长上下文训练CP并行实战:Ring Attention与Ulysses性能对比与选型
2026/10/28 7:36:55 网站建设 项目流程

1. 从一次长上下文训练翻车说起

去年下半年,我接手了一个长上下文训练的任务,序列长度直接拉到 128K。当时团队里有人拍胸脯说“加卡就行”,结果卡加到 64 张,单步时间反而比 32 张还慢,GPU 利用率掉到 30% 以下,显存还时不时 OOM。排查了整整两天,最后定位到问题出在Context Parallelism(CP,上下文并行)的实现方式上——我们用的是最朴素的 all-gather 方案,通信量随序列长度线性膨胀,卡越多通信墙越厚。

这件事让我意识到,CP 不是一个“开了就完事”的开关,不同实现原理之间的性能差距可能是数倍级别的。后来我系统性地把主流 CP 方案都跑了一遍实测,包括Ring Attention、Ulysses,以及Transformer Engine里集成的 CP 支持,踩了不少坑,也总结出一套选型逻辑。这篇就把这些经验完整摊开讲,从原理到实测数据,再到具体怎么选、怎么调,尽量让刚接触 CP 的兄弟也能看懂,做过 CP 的同行也能捞到点细节。

先说清楚这篇适合谁看:如果你正在做长序列训练(32K 以上),或者被 CP 的通信开销折磨过,又或者单纯想搞清楚 Ring Attention 和 Ulysses 到底差在哪,那这篇就是写给你的。我会尽量用生活化的类比把原理讲透,再配上实测数据和可复现的配置。

2. CP 到底在解决什么问题:先把账算明白

2.1 为什么长序列必须上 CP

要理解 CP,得先理解长序列训练到底卡在哪。假设你有一个 128K 序列的模型,用 8 张卡做训练。如果不做任何并行,光是 attention 矩阵就是 128K × 128K,按 FP16 算,单个 head 的 attention score 就要 128000 × 128000 × 2 bytes ≈ 32GB。这还没算多头、没算中间激活。显存直接爆炸。

常规的做法是TP(张量并行)+ PP(流水线并行)+ DP(数据并行)三件套。但这三件套解决的是“模型参数和 batch”的切分问题,对“单条序列太长”这件事无能为力。TP 切的是权重矩阵维度,序列维度还是完整的一份;PP 切的是层,序列照样完整;DP 切的是 batch,你 batch 里就一条长序列,切无可切。

所以 CP 的核心思路很直接:把序列维度切开,每张卡只负责一段序列。8 张卡,128K 序列,每张卡拿 16K。听起来很美好,但问题立刻来了——attention 是全局操作,第 1 个 token 要和第 128K 个 token 算注意力,你序列切开了,跨卡的注意力怎么算?这就是 CP 所有实现方案要解决的核心矛盾:序列切分带来的跨卡通信。

2.2 CP 与 TP、PP、DP 的关系

很多人第一次接触 CP 会把它和 TP 搞混,这里必须掰扯清楚。TP 是把一个矩阵乘法拆到多卡,比如一个 Linear 层权重按列切,每张卡算一部分输出,然后 all-reduce 汇总。TP 的通信发生在每一层内部,频率极高,所以 TP 通常限制在单机 8 卡内,靠 NVLink 扛通信。

CP 不一样,CP 切的是序列,通信主要发生在attention 模块内部,而且通信模式是“卡与卡之间交换 KV 块”。CP 的通信频率比 TP 低(每层 attention 一次,而不是每个 Linear 一次),但单次通信的数据量可能很大(取决于实现)。所以 CP 可以跨机部署,走 RDMA 网络也能接受。

和 DP 的关系更微妙:CP 组内的卡,本质上是在协同处理同一个 batch 的同一条序列,所以 CP 组内的梯度需要 all-reduce 同步,这一点和 DP 类似。实际部署时,CP 通常和 DP 组合使用,比如 64 张卡,8 路 CP × 8 路 DP。

2.3 三种主流 CP 实现的全景对比

目前工业界主流的 CP 实现可以归为三类,我画个表先让大家有个整体印象,后面会逐个拆解:

方案核心原理通信模式代表实现适用场景
All-Gather CP每步 all-gather 完整 KVAll-Gather早期 Megatron短序列、卡少
Ring AttentionKV 块环形流转P2P 环形RingAttention、部分 TE长序列、跨机
Ulysses序列维转 head 维All-to-AllDeepSpeed-Ulysses长序列、单机多卡
TE 集成 CP融合 Ring + 优化 kernelP2P + 融合Transformer EngineH 系列卡、生产环境

这张表是后面所有讨论的骨架。注意,实际生产里这些方案经常混用,比如 Transformer Engine 的 CP 就是把 Ring Attention 的思路和融合 kernel 结合起来,再针对 H100 的 NVLink 做优化。

3. 逐个拆解:不同 CP 实现的原理到底差在哪

3.1 All-Gather 方案:最直观但最容易撞墙

All-Gather 是最容易理解的 CP 实现。思路是:每张卡持有自己那段序列的 Q、K、V,做 attention 之前,先做一次 all-gather,把所有人的 K、V 收集齐,这样每张卡都有完整的 K、V,然后各自算自己那段 Q 的 attention。

用生活类比:8 个人合作写一本书,每人写一章。All-Gather 就是每次要引用别人章节时,先把所有人的章节复印一份发给大家,人手一本完整的书。好处是引用方便,坏处是每次引用都要重新复印一遍全书。

这个方案的通信量是:每层 attention,每张卡要发送自己的 KV(大小正比于序列长度),接收其他所有卡的 KV。总通信量正比于序列长度 × CP 路数。序列越长、卡越多,通信量爆炸得越快。我实测过,128K 序列、8 路 CP,All-Gather 方案的通信时间占单步时间的 60% 以上,GPU 大部分时间在等通信。

所以 All-Gather 方案基本只适合短序列(8K 以内)或者卡很少(2-4 路)的场景。它的优点是实现简单、代码好懂,作为理解 CP 的入门方案很合适,但生产环境长序列训练基本不会用它。

3.2 Ring Attention:让 KV 块像接力棒一样流转

Ring Attention 是长序列 CP 的主流方案,核心思路非常巧妙:不一次性收集所有 KV,而是让 KV 块在卡之间环形流转,边流转边算。

还是用写书的类比:8 个人围成一圈,每人手里有自己的章节。第 1 轮,每个人用自己手里的章节算一部分注意力;然后把手里的章节传给右边的人,第 2 轮用新拿到的章节继续算;如此循环 8 轮,每个人就都见过了所有章节,注意力也就算完了。整个过程不需要任何人手里同时持有全书,只需要一个章节的缓冲区。

这个方案的精妙之处在于:通信和计算可以重叠。当你在用当前 KV 块算 attention 的时候,下一块 KV 已经在传输路上了。理想情况下,通信时间可以被计算时间完全掩盖,达到“通信免费”的效果。这也是 Ring Attention 能扩展到超长序列(百万 token 级别)的关键。

但 Ring Attention 有几个坑必须注意。第一,它要求 attention 计算支持分块累加,也就是 online softmax 那套东西(FlashAttention 的核心),否则每轮都要重新归一化,精度和性能都崩。第二,环形流转的 P2P 通信对网络拓扑敏感,如果 CP 组跨了非对称的网络路径,某条链路可能成为瓶颈。第三,负载均衡问题——如果序列长度不能被 CP 路数整除,最后一块会拖慢整个环。

3.3 Ulysses:换个维度切,把通信变成 All-to-All

Ulysses 是 DeepSpeed 提出的方案,思路和 Ring Attention 完全不同。它不切序列,而是把序列维度的切分转换成 head 维度的切分。

具体来说:假设你有 8 路 CP,64 个 attention head。Ulysses 的做法是,每张卡先持有 1/8 的序列和全部 64 个 head。做 attention 前,通过一次 all-to-all 通信,把数据重新分布成:每张卡持有完整序列和 8 个 head。这样每张卡就能独立算自己那 8 个 head 的完整 attention,不需要跨卡通信。算完后再 all-to-all 换回来。

类比一下:8 个人合作,原本每人负责书的一部分。Ulysses 说,不如我们换个分工——每人负责书的一个主题(head),但为了写这个主题,你得看完整本书。于是大家先把各自的部分互相交换,让每个人手里都有完整的书,但只专注写自己的主题。写完后再交换回去。

Ulysses 的优势是通信模式规整,all-to-all 在 NVLink 上效率很高,而且通信量相对固定(正比于序列长度 × hidden size,与 head 数无关)。它的限制是:CP 路数不能超过 head 数。你有 64 个 head,最多 64 路 CP,再多就没法切了。而且 all-to-all 通信需要一次性搬运大量数据,对显存缓冲区要求高。

3.4 Transformer Engine 的 CP:融合 kernel 的工程优化

Transformer Engine(TE)是 NVIDIA 出的 Transformer 加速库,它对 CP 的支持是把 Ring Attention 的思路和自家的融合 kernel 结合起来。核心优化点有几个:一是把 attention 的多个步骤(QK^T、softmax、PV)融合成少数几个 kernel,减少 kernel launch 开销;二是针对 H100 的 NVLink 和 TMA(Tensor Memory Accelerator)做通信优化,让 P2P 传输走硬件加速路径;三是支持 FP8 精度下的 CP,进一步降低通信量。

TE 的 CP 本质上是 Ring Attention 的工程强化版,原理没变,但把每个环节都抠到了极致。实测下来,在 H100 集群上,TE 的 CP 比手写的 Ring Attention 快 20%-40%,主要来自 kernel 融合和 FP8 通信。但它的代价是绑定 NVIDIA 生态,A 卡或者非 Hopper 架构的卡用不了,而且版本迭代快,API 经常变,升级要小心。

4. 实测环境与配置:把变量控制住

4.1 硬件与软件环境

为了做对比,我搭了一套相对可控的测试环境。硬件是 8 张 H100 80G,单机 NVLink 全互联,这样能排除跨机网络的干扰,先看纯 CP 实现的差异。软件栈统一用 PyTorch 2.3 + CUDA 12.3,Ring Attention 用开源实现,Ulysses 用 DeepSpeed 0.14,TE 用 1.9 版本。

模型选了一个标准的 LLaMA 结构,hidden size 4096,32 个 head,32 层。序列长度测了 32K、64K、128K 三档。batch size 固定为 1,因为长序列训练通常 batch 很小。精度统一用 BF16,TE 额外测了 FP8。

提示:做 CP 实测时,一定要把 batch size 固定住,否则不同方案的显存占用差异会导致 batch 能开的大小不同,数据就没法直接比了。我一开始没注意这点,测出来的数据全是乱的。

4.2 测试指标定义

我主要看三个指标。单步时间:一次前向+反向的总耗时,这是最直观的。GPU 利用率:用 DCGM 采样的 SM 活跃度,反映计算和通信的重叠程度。显存峰值:训练过程中的最大显存占用,决定你能开多长的序列。

另外我加了一个通信占比指标,用 Nsight Systems 抓 timeline,算通信 kernel 时间占总时间的比例。这个指标最能说明问题——通信占比高的方案,扩展性一定差。

4.3 基线配置与对照设置

基线是不用 CP,纯 TP=8 跑 32K 序列。这个基线在 64K 时直接 OOM,所以 64K 和 128K 只能对比 CP 方案之间。所有 CP 方案都保持 TP=1、PP=1、DP=1,只变 CP 路数(2、4、8),这样能干净地看出 CP 本身的扩展性。

有个细节要注意:Ring Attention 和 Ulysses 对 attention 实现的要求不同。Ring 必须配 FlashAttention 的 online softmax,Ulysses 可以用普通 attention 也可以用 Flash。为了公平,两者都配 FlashAttention-2。TE 用自己的融合 attention。

5. 实测数据与性能分析

5.1 单步时间对比:Ring 和 Ulysses 的分水岭

先看 32K 序列、8 路 CP 的单步时间(单位:毫秒):

方案前向反向总时间相对基线
无 CP(TP=8)42081012301.00x
All-Gather680142021001.71x
Ring Attention51098014901.21x
Ulysses48092014001.14x
TE CP45086013101.07x

32K 这个长度,Ulysses 略胜 Ring,因为 all-to-all 在 NVLink 上效率极高,而 Ring 的 P2P 有启动开销。All-Gather 已经明显拖后腿了,1.71x 的减速说明通信墙开始显现。

再看 128K 序列、8 路 CP:

方案前向反向总时间通信占比
All-GatherOOMOOM--
Ring Attention19803760574018%
Ulysses24504680713032%
TE CP17203280500012%

到了 128K,局势反转。Ring Attention 反超 Ulysses,因为 Ulysses 的 all-to-all 通信量随序列长度线性增长,而且是一次性大块传输,缓冲区压力大,通信占比飙到 32%。Ring 的通信被计算掩盖得更好,占比只有 18%。TE CP 依然最强,通信占比压到 12%。

这个分水岭很关键:短序列(32K 以内)Ulysses 更优,长序列(64K 以上)Ring 更优。原因在于 Ulysses 的通信是“集中式”的,序列越长,单次 all-to-all 的数据量越大,越难被掩盖;Ring 的通信是“流水式”的,天然适合重叠。

5.2 扩展性曲线:CP 路数增加时谁更稳

固定 64K 序列,把 CP 路数从 2 加到 8,看单步时间的变化:

CP 路数RingUlyssesTE CP
2312029802760
4348036203020
8412048903480

Ring 从 2 路到 8 路,时间增长 32%;Ulysses 增长 64%;TE 增长 26%。Ulysses 的扩展性最差,因为 all-to-all 的通信量随 CP 路数增加而增加(每张卡要和其他所有卡交换数据),而 Ring 的通信量基本恒定(每个环上邻居只传一次)。

这里有个反直觉的点:CP 路数不是越多越好。我实测发现,Ring Attention 在 4 路时性价比最高,8 路虽然能跑更长序列,但单步时间增加明显。如果你的序列 64K 用 4 路 CP 就能放下,别硬上 8 路。

5.3 显存占用:决定你能开多长

显存峰值对比(64K 序列,8 路 CP):

方案显存峰值可支持最大序列
Ring62GB128K
Ulysses71GB96K
TE CP58GB160K

Ulysses 显存最高,因为 all-to-all 需要额外的通信缓冲区,而且换维后每张卡要持有完整序列的 8 个 head,激活值不小。Ring 的显存最省,因为它只需要一个 KV 块的缓冲区。TE CP 因为 FP8 和融合 kernel,显存压得最低。

5.4 通信占比与重叠效率

把通信占比单独拎出来说,因为这个指标最能预测扩展性。我用 Nsight 抓的 timeline 显示:

  • Ring Attention 的 P2P 传输和 attention 计算重叠度达到 85% 以上,通信基本被掩盖。
  • Ulysses 的 all-to-all 重叠度只有 50% 左右,因为 all-to-all 是集合通信,需要所有卡同步参与,很难和计算重叠。
  • TE CP 重叠度 90%+,靠的是 TMA 硬件加速和精细的流水线调度。

注意:通信重叠不是自动的,需要代码里显式做 stream 分离和 event 同步。我见过不少人用 Ring Attention 但没开重叠,性能还不如 All-Gather。检查方法很简单,看 timeline 里通信 kernel 和计算 kernel 是不是并行的,如果是串行的,说明重叠没生效。

6. 选型决策:什么场景用什么 CP

6.1 按序列长度选

这是最直接的决策维度。32K 以内,Ulysses 或 TE CP 都行,Ulysses 实现简单,TE 性能更好但要 H 卡。32K-64K,Ring 和 Ulysses 打平,看你的网络拓扑,NVLink 全互联选 Ulysses,跨机选 Ring。64K 以上,无脑 Ring 或 TE CP,Ulysses 的通信会成为瓶颈。

6.2 按硬件拓扑选

单机 8 卡 NVLink:Ulysses 的 all-to-all 效率最高,优先考虑。跨机 RDMA:Ring 的 P2P 更适合,因为 all-to-all 跨机会产生大量小包,网络效率低。H100 + TE:直接用 TE CP,省心且快。A100 或更老:Ring Attention 是稳妥选择,TE 支持有限。

6.3 按实现成本选

如果团队刚接触 CP,想快速跑通,Ulysses 的代码最简单,DeepSpeed 里改个配置就行。Ring Attention 需要自己处理 online softmax 和环形通信,实现成本高。TE CP 是库调用,但版本管理麻烦。我的建议是:先用 Ulysses 跑通流程,验证长序列训练的可行性,再根据性能瓶颈决定要不要换 Ring 或 TE。

7. 实操避坑:我踩过的那些坑

7.1 序列长度不能整除 CP 路数怎么办

这是最常见的坑。128K 序列,8 路 CP,每路 16K,整除,没问题。但如果是 100K 序列,8 路 CP,每路 12.5K,不整除。Ring Attention 的处理方式是 padding 到整除,多出来的部分算完丢弃。但 padding 会浪费计算,而且如果 padding 太多,性能下降明显。

我的做法是:尽量让序列长度是 CP 路数的整数倍。如果实在不行,把 CP 路数调成能整除的因子。比如 100K,用 4 路 CP(25K 每路)比 8 路(12.5K)更划算,虽然 CP 路数少了,但避免了 padding 浪费。

7.2 通信重叠没生效的排查

前面提过,通信重叠需要显式配置。排查步骤:先用 Nsight 抓 timeline,看通信和计算是否并行。如果串行,检查三点。一是通信 stream 是否和计算 stream 分离,很多实现默认用同一个 stream。二是 event 同步是否正确,通信完成事件有没有正确等待。三是缓冲区是否够大,如果缓冲区太小,通信会被迫等待计算释放空间。

我遇到过一次,Ring Attention 的重叠死活不生效,最后发现是 KV 缓冲区只开了一个块,通信必须等计算用完才能开始。改成双缓冲(double buffering)后,重叠立刻生效,性能提升 40%。

7.3 精度问题:BF16 还是 FP8

CP 本身不改变精度,但通信量大的方案在低精度下收益更明显。TE CP 支持 FP8 通信,能把通信量减半,但 FP8 对 attention 的数值稳定性有影响,尤其是长序列下 softmax 容易溢出。我的经验是:先用 BF16 跑通,确认精度没问题,再尝试 FP8。FP8 下要加 loss scaling,并且监控 attention score 的最大值,超过阈值就回退 BF16。

7.4 常见问题速查表

问题现象可能原因排查方法解决方案
单步时间随 CP 路数增加而暴涨通信未重叠Nsight 看 timeline开双缓冲、分离 stream
显存 OOM通信缓冲区过大看显存分布减小缓冲区、换 Ring
精度下降FP8 溢出监控 attention max回退 BF16、加 loss scaling
负载不均序列不整除打印每卡计算量padding 或调 CP 路数
扩展性差通信占比高算通信时间占比换更适合的方案

8. 几个容易被忽略的细节

8.1 CP 和 TP 混用时的顺序

实际生产里 CP 很少单独用,通常和 TP 混。这里有个顺序问题:先 CP 还是先 TP。我的经验是,先做 CP 切序列,再做 TP 切权重。因为 CP 切完后每卡的序列变短,TP 的通信量也随之减小。反过来先 TP 再 CP,TP 的 all-reduce 要在完整序列上做,通信量更大。

8.2 CP 组内的梯度同步

CP 组内的卡处理的是同一条序列的不同段,所以梯度需要 all-reduce 同步。这个 all-reduce 的通信量和 DP 的 all-reduce 类似,但频率一样(每步一次)。很多人忘了这一步,导致梯度不一致,训练不收敛。检查方法:打印 CP 组内各卡的梯度 norm,应该完全一致。

8.3 长序列的 attention 数值稳定性

序列越长,attention score 的数值范围越大,softmax 越容易溢出。Ring Attention 用的 online softmax 本身就是为了解决这个问题,但实现时要注意 running max 的更新逻辑。我见过一个实现,running max 更新写错了,短序列没问题,128K 时直接 NaN。建议加一个断言,监控 running max 的变化,异常时报警。

9. 我个人的选型心得

折腾了这么久,如果让我给一个一句话的建议:32K 以内用 Ulysses 快速跑通,64K 以上用 Ring Attention 或 TE CP,H100 环境直接上 TE。但这个建议有个前提——你得先确认自己的网络拓扑和硬件。我见过有人照搬网上的配置,结果跨机网络是 100G 而不是 400G,Ring Attention 的 P2P 直接成瓶颈,性能还不如 All-Gather。

还有一个心得是:CP 的性能调优,80% 的收益来自通信重叠,20% 来自方案选择。很多人纠结选 Ring 还是 Ulysses,但其实先把通信重叠做好,比换方案收益大得多。我建议先用一个方案跑通,把重叠调到 80% 以上,再看要不要换。

最后分享一个小技巧:测 CP 性能时,用固定长度的随机输入,别用真实数据。真实数据的 attention 分布不均匀,会导致性能波动,测出来的数据不可比。随机输入能让每次迭代的计算量一致,数据更干净。这个技巧帮我省了不少排查时间。

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

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

立即咨询