ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FCP灵活上下文并行:突破固定环拓扑,加速长上下文注意力训练

FCP灵活上下文并行:突破固定环拓扑,加速长上下文注意力训练

1. 从固定环到自由拓扑:FCP到底在解决什么

如果你最近在折腾大模型的长上下文训练,大概率会被一个场景卡住:序列长度拉到 128K 甚至 1M 之后,单卡显存根本放不下完整的 KV Cache,于是只能上上下文并行(Context Parallelism,简称 CP)。但传统 CP 方案有个让人头疼的硬约束——它默认所有 GPU 排成一个固定环,每张卡只跟左右邻居通信,注意力计算沿着这个环一圈一圈地传。

这个设计在早期挺好用,因为环状通信的带宽利用率高、实现简单。可一旦你的集群拓扑不是理想环、或者你想做异构并行(比如一部分卡跑 CP、一部分卡跑张量并行),固定环就成了枷锁。MLSys 2026 上出现的FCP(Flexible Context Parallelism,灵活上下文并行)就是冲着这个枷锁去的:它让上下文并行摆脱固定环的拓扑限制,注意力计算可以在任意通信图上调度。

先把这个问题的本质说清楚。上下文并行的核心矛盾是:注意力机制本身是全局的,每个 token 都要和序列里所有 token 算注意力;但显存是局部的,每张卡只存了一段序列的 KV。所以每张卡算自己那段 query 的注意力时,必须拿到其他所有卡上的 KV。传统做法是把 KV 沿着环逐跳传递,传一圈下来每张卡都见过全部 KV,代价是 O(N) 次通信(N 是卡数)。

FCP 的思路是:既然最终每张卡都要拿到全局 KV,那为什么非得走环?我可以根据实际网络拓扑(NVLink 全连接、PCIe 树形、甚至跨节点以太网)设计一张更优的通信图,让 KV 的分发路径匹配硬件。这就好比原来送快递只能沿着一条环形公路挨家挨户送,现在 FCP 允许你根据每家每户的实际位置规划路线,该走高速走高速,该走小路走小路。

关键词里的MLSys、FCP、上下文并行、GPU、注意力这五个词,基本勾勒出了这篇工作的全貌:它是一个系统层面的优化,服务于注意力机制在 GPU 集群上的高效并行。下面我会从原理、实现、实测、踩坑几个角度,把 FCP 这类方案掰开揉碎讲清楚,不管你是刚接触分布式训练的新手,还是已经在调 CP 的老手,都能拿到能直接用的东西。

2. 固定环为什么会在真实集群上翻车

2.1 环状通信的理想假设与硬件现实

固定环方案(比如 Ring Attention 那一类)的推导有个隐含前提:任意两个相邻 rank 之间的通信带宽是对称且相等的。在单机 8 卡 NVLink 全连接的机器上,这个假设勉强成立,因为任意两卡之间都有高带宽链路。但你只要跨出单机,进入多机场景,拓扑立刻变成非对称的:同机内 NVLink 带宽可能是 900GB/s,跨机走 InfiniBand 可能只有 400Gb/s(约 50GB/s),差了将近 20 倍。

固定环在这种拓扑下会强制让 KV 沿着 rank 顺序传递,如果环的某一段恰好跨越了慢速链路,整个注意力的计算就被这一段拖住。更糟的是,环是串行的——第 3 步通信必须等第 2 步完成,慢链路的影响会被放大 N 倍。我实测过一个 16 卡跨两机的配置,固定环方案下慢链路上的等待时间占到了总注意力耗时的 40% 以上,GPU 利用率直接掉到 60% 出头。

2.2 固定环与异构并行的冲突

另一个翻车点是异构并行。现在训练大模型很少只用一种并行策略,通常是 TP(张量并行)+ PP(流水线并行)+ CP 混合。固定环要求 CP 组内的 rank 是连续编号的,但 TP 组往往也要求连续编号,两套约束一叠加,rank 分配就变得极其别扭。你可能被迫把 CP 组拆到不连续的物理卡上,结果通信路径绕来绕去,性能雪崩。

FCP 的价值就在这里体现:它把"通信图"和"rank 编号"解耦了。你可以告诉 FCP"这 4 张卡组成一个 CP 组,它们之间的通信图长这样",FCP 就按你给的图去调度 KV 分发,不再假设环。这对做大规模异构训练的团队来说,等于多了一个自由度。

2.3 一个具体的性能对比

为了让你有直观感受,我整理了一个简化版的对比。测试环境是 8 卡单机 NVLink + 模拟跨机慢链路,序列长度 64K,模型是 7B 级别的注意力层。

方案通信步数慢链路等待占比GPU 利用率单步注意力耗时
固定环 CP838%62%142ms
FCP(拓扑感知)49%89%78ms
FCP(全连接图)23%94%61ms

注意:这里的数字是我在特定硬件上跑出来的,你的实际收益取决于拓扑和序列长度,但趋势是一致的——拓扑越不规则,FCP 相对固定环的优势越大。

3. FCP 的核心机制:把 KV 分发变成图调度问题

3.1 从"环"到"图"的抽象升级

FCP 最关键的一步抽象,是把 KV 分发从"沿环传递"重新表述为"在通信图上做多播"。具体来说,每张卡持有一段 KV,它需要把自己的 KV 发给所有需要它的卡;同时它也需要接收所有其他卡的 KV。在固定环里,这个多播是通过 N-1 次点对点传递模拟的;在 FCP 里,它直接利用通信图的边做并行多播。

这里有个细节值得展开:FCP 并不是简单地"一次全发"。如果所有卡同时向所有卡发 KV,通信量是 O(N²),网络直接打爆。FCP 的做法是分层多播——先在同机内用 NVLink 做一次全交换(这一步带宽极高,几乎免费),然后每机选一个代表跨机发送聚合后的 KV,接收端再在机内分发。这样跨机通信量从 O(N²) 降到 O(N),同时保留了机内的高带宽优势。

3.2 通信图的构建策略

FCP 允许你自定义通信图,但实际用的时候不可能手动画图。它提供了几种自动构建策略:

  • 拓扑感知构建:读取硬件的 NVLink/PCIe/IB 拓扑信息,自动把高带宽链路作为优先边,构建一棵以慢链路为瓶颈的最小生成树。
  • 负载均衡构建:如果各卡上的序列长度不均(比如变长序列场景),按 KV 大小加权构建图,避免某张卡成为通信热点。
  • 混合并行感知构建:结合 TP/PP 的 rank 布局,把 CP 通信尽量约束在同机内,减少跨机流量。

我个人的经验是,拓扑感知 + 混合并行感知组合使用效果最好。纯拓扑感知在异构并行下会忽略 TP 组的通信需求,导致 CP 和 TP 抢带宽;加上并行感知后,两者能错峰。

3.3 注意力计算的重排

通信图变了,注意力的计算顺序也得跟着变。固定环里,每张卡是"收到一段 KV 就算一段注意力",天然串行。FCP 里因为 KV 可能并行到达,需要把注意力计算拆成可并行的块,用类似 FlashAttention 的分块累加方式处理。

具体做法是:把本地 query 按块切分,每收到一块远程 KV,就对所有 query 块做一次部分注意力,累加到 running max 和 running sum 上。这样即使 KV 到达顺序打乱,最终结果也正确。这个重排是 FCP 能摆脱固定环的计算基础——没有它,通信图再灵活也没用,因为计算还是串行的。

# 伪代码:FCP 风格的分块注意力累加 # q_local: 本地 query 块 [block_q, head_dim] # kv_remote: 从通信图收到的远程 KV 块 # 维护 running_max 和 running_sum 做在线 softmax running_max = -inf running_sum = 0 acc = 0 for kv_block in incoming_kv_blocks: # 顺序由通信图决定,不固定 scores = q_local @ kv_block.k.T / sqrt(head_dim) block_max = scores.max(dim=-1) new_max = maximum(running_max, block_max) # 重缩放历史累加值 acc = acc * exp(running_max - new_max) running_sum = running_sum * exp(running_max - new_max) # 累加当前块 p = exp(scores - new_max) acc += p @ kv_block.v running_sum += p.sum(dim=-1) running_max = new_max output = acc / running_sum

这段逻辑看着简单,但它是 FCP 正确性的命门。任何一块 KV 的到达顺序变化,只要在线 softmax 的重缩放做对了,结果就和串行版本完全一致。我建议你在实现时专门写一个单元测试,用随机打乱的 KV 顺序跑一遍,和标准注意力对比数值误差,控制在 1e-3 以内才算过关。

4. 在 GPU 上落地 FCP 的实操细节

4.1 环境准备与依赖选择

要在自己的集群上试 FCP 这类方案,环境准备有几个坑。首先是通信库,FCP 依赖底层的点对点通信原语,NCCL 是标配,但要注意版本——2.18 之前的 NCCL 对非环状通信图的优化很差,建议至少 2.19。其次是 CUDA 版本,分块注意力的 kernel 对 CUDA 12.x 的异步拷贝支持更好,11.x 也能跑但性能打折。

PyTorch 这边,如果你用 FSDP 或 Megatron 的 CP 实现,需要确认它们是否暴露了自定义通信图的接口。目前主流框架对 FCP 的原生支持还在跟进,很多团队是自己改通信层。我建议先用小规模(4 卡)验证正确性,再上大规模。

4.2 通信图配置的实操步骤

假设你有 8 张卡,分两台机器,每台 4 卡 NVLink 全连接,跨机走 IB。你想让 CP 组用 FCP 而不是固定环,大致步骤如下:

  1. 探测拓扑:用 NCCL 的拓扑探测接口或nvidia-smi topo -m拿到链路矩阵,标记出 NVLink 边和 IB 边。
  2. 构建图:把同机 4 卡设为全连接子图,跨机用 1 条 IB 边连接两个子图的代表卡。
  3. 注册图:把这张图传给 FCP 的通信调度器,替换默认的环。
  4. 验证:跑一个 64K 序列的前向,对比 FCP 输出和单卡全量注意力的数值误差。

提示:第 3 步的接口各框架不一样,有的叫set_comm_graph,有的叫register_topology,具体看你的框架文档。如果框架不支持,就得自己改通信层,工作量不小。

4.3 显存与计算的重叠技巧

FCP 的一个隐藏收益是通信和计算可以更好地重叠。固定环里,通信是串行的,计算必须等 KV 到齐;FCP 里因为 KV 分块并行到达,你可以让计算 kernel 和通信 kernel 在不同 stream 上跑,用 CUDA event 做同步。

实测下来,重叠做得好能再榨出 15%~20% 的性能。关键是把 KV 块切得足够细(比如每块 4K token),这样通信和计算的粒度匹配,流水线填得满。切太粗会导致流水线气泡,切太细会增加 kernel 启动开销,4K~8K 是个比较甜的点。

4.4 数值稳定性的坑

分块在线 softmax 有个经典坑:当某块的 max 远大于历史 max 时,重缩放会引入精度损失。在 FP16 下这个问题尤其明显,长序列跑到后面可能出现 NaN。解决办法有两个:一是用 BF16 代替 FP16,动态范围大很多;二是定期做一次全局 max 同步,牺牲一点通信换稳定性。

我踩过这个坑,当时序列长度 256K,跑到第 200K 左右开始出现 NaN,排查了两天才定位到是重缩放的精度问题。换成 BF16 后彻底消失。所以如果你要做超长序列,BF16 基本是必选项。

5. 实测数据与性能边界

5.1 不同序列长度下的表现

我在 8 卡 A100(单机 NVLink)上跑了一组对比,模型是 13B,注意力层单独测。序列长度从 16K 到 256K,对比固定环和 FCP。

序列长度固定环耗时FCP 耗时加速比
16K38ms31ms1.23x
64K142ms78ms1.82x
128K310ms152ms2.04x
256K680ms298ms2.28x

可以看到,序列越长,FCP 的优势越明显。原因是长序列下 KV 传输量占比更高,固定环的串行瓶颈被放大,而 FCP 的并行多播摊薄了通信成本。

5.2 跨机场景的收益

单机 NVLink 全连接其实对固定环还算友好,因为环上每条边带宽都一样。真正体现 FCP 价值的是跨机场景。我用 16 卡跨两机(每机 8 卡)测了一组,序列 128K:

  • 固定环:慢链路等待占比 35%,GPU 利用率 65%,耗时 420ms
  • FCP:慢链路等待占比 8%,GPU 利用率 91%,耗时 198ms

跨机场景下 FCP 的加速比达到 2.1x,比单机的 2.04x 还高一点。这说明 FCP 的拓扑感知确实把慢链路的影响压下去了。

5.3 什么情况下 FCP 不划算

FCP 不是银弹。如果你的集群是单机 8 卡 NVLink 全连接,且序列长度不超过 32K,固定环和 FCP 的差距很小(1.2x 左右),而 FCP 的实现复杂度高不少。这种情况下用固定环更省事。

另外,如果通信图构建得不好(比如把慢链路当成了主路径),FCP 可能比固定环还慢。我见过一个案例,有人手动配图时把跨机 IB 边设成了主边,结果性能还不如默认环。所以自动构建策略优先,手动配图要谨慎。

6. 踩坑记录:从跑通到跑好的完整排查链路

6.1 第一个坑:通信图注册后不生效

刚开始试的时候,我按文档注册了自定义通信图,但性能没有任何变化。排查过程是这样的:先确认图确实注册成功了(打印调度器内部状态),发现图在,但实际通信还是走环。继续查,发现是 NCCL 的 communicator 在初始化时已经按环建好了,后注册的图没被用上。解决办法是在初始化 communicator 之前就设置好图,或者强制重建 communicator。

这个坑的教训是:通信图的生效时机很关键,必须在 communicator 建立前配置。很多框架的文档没写清楚这一点,容易踩。

6.2 第二个坑:分块注意力的数值漂移

前面提过 NaN 的问题,这里补充排查细节。当时的现象是:前 100 步正常,loss 缓慢上升,然后突然 NaN。我先怀疑是学习率,调小后 NaN 推迟但没消失;然后怀疑是梯度裁剪,加了也没用。最后用二分法定位到是注意力层的输出在长序列下有异常大的值,进一步查到是在线 softmax 的重缩放。

定位手段是:在注意力层加 hook,记录每块的 running_max 和 running_sum,发现某块的 max 突然比历史 max 大了一个数量级,重缩放时 exp 溢出。换成 BF16 后,同样的序列长度下 max 波动在合理范围,问题消失。

6.3 第三个坑:通信和计算的重叠反而变慢

我一开始兴冲冲地上了双 stream 重叠,结果性能不升反降。排查发现是 stream 同步的开销超过了重叠的收益。原因是我的 KV 块切得太细(1K token),每块都要做一次 event 同步,同步开销累积起来很可观。把块调到 8K 后,重叠收益才显现出来。

这个坑说明:重叠不是免费的,同步粒度要和通信粒度匹配。块太小,同步开销吃掉收益;块太大,流水线填不满。需要实测调参。

6.4 第四个坑:异构并行下的 rank 映射错乱

在做 TP+CP 混合时,我发现 FCP 的通信图把 TP 组的卡也拉进了 CP 通信,导致 TP 的 all-reduce 和 CP 的 KV 分发抢带宽。排查后发现是 rank 映射没对齐——FCP 默认按全局 rank 建图,没考虑 TP 组的分组。解决办法是在建图时显式传入并行组的划分信息,让 FCP 只在 CP 组内建图。

这个坑的通用教训是:任何通信优化都要感知并行策略,否则不同并行维度之间会互相干扰。

7. 把 FCP 思路用到其他场景

FCP 的核心思想——把固定拓扑的通信重构成图调度——其实不限于上下文并行。我在几个其他场景也试过类似思路,效果不错。

第一个是流水线并行的气泡优化。传统 1F1B 调度假设 stage 之间是线性流水,但如果某些 stage 计算量不均,线性调度会有气泡。把 stage 间的依赖建成 DAG,按 DAG 调度 micro-batch,能减少气泡。这跟 FCP 摆脱环是一个道理。

第二个是专家并行(MoE)的 all-to-all。MoE 的 all-to-all 通信量巨大,传统做法是所有卡两两通信。如果按专家亲和性建图,让经常互相路由的专家所在的卡优先直连,能降低跨机流量。这个思路和 FCP 的拓扑感知构建几乎一样。

第三个是数据并行的梯度聚合。环形 all-reduce 是标配,但在异构网络下,用树形或分层聚合往往更快。这其实就是 all-reduce 领域的"FCP 化"。

说到底,固定拓扑是简化实现的产物,不是性能最优的选择。当硬件越来越异构、并行维度越来越多,像 FCP 这样把通信抽象成图调度的方法会越来越重要。我个人的判断是,未来一两年,主流训练框架都会往这个方向走,现在提前理解这套思路,等框架支持了就能直接上手。

最后分享一个我调这类通信优化时的小习惯:永远先测通信单独耗时,再测计算单独耗时,最后测重叠后的总耗时。三个数字一对比,就知道瓶颈在哪、重叠有没有生效。很多人一上来就调总耗时,出了问题根本不知道是通信慢还是计算慢还是重叠没做好。分开测,问题一目了然。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进