Kimi K3 模型结构(8):训练并行,一步训练里谁在等谁
系统篇第一篇。一句话交代 K3 预训练的并行布局,然后用「谁在等谁」一个问题串起 §5.2 的所有技巧:EP 负载不均、host 等 shape、SM 之间的偏斜、流水线气泡里的 ViT。重点推 MoonEP 的定理:每个 rank 预留 E/R 个冗余 expert 槽位,任何 router 输出都能补到完全平衡。
目录11 节
对应论文 §5.2 的开头、§5.2.1(MoonEP)、§5.2.3(视觉编码器)和附录 E。第 7 篇讲的是结构逼出来的系统工作,这一篇和后面两篇讲把 2.8T 参数训练起来、跑起来本身需要什么。三篇各用一个视角:这一篇看时间,第 9 篇看显存,第 10 篇看状态住在哪里。
先说清楚论文没给什么:§5 没有任何并行度、GPU 数、MFU 或吞吐。所以下文所有数字要么来自 config.json 的形状,要么并行度用字母代替。看到 、、 这些字母时,它们是变量,不是 K3 的真实配置。
并行布局:一句话
论文 §5.2 开头用一句话给出了预训练的并行组合:流水线并行(PP)带虚拟段(VP)、expert 并行(EP)、ZeRO-1 数据并行(DP)、Pipeline ZeRO-2 梯度分片(见 ZeRO 那篇)、上下文并行(CP,就是第 7 篇的 KCP)。MoE 层的 shared expert 在每个 EP rank 上都有一份副本;dispatch 和 combine 的 all-to-all 与计算重叠。没有张量并行。 这不奇怪:K3 每层的非 expert 参数只有几亿,routed expert 才是大头,而 expert 用 EP 切比用 TP 切自然得多。
这几种并行各切什么维度、各让谁等谁,是通用知识,放在「大模型笔记」专栏里:并行总览逐一讲了每种并行切什么、怎么通信,流水线并行推了气泡占比 和每个 rank 在途激活 份这两个后面要用的结论。这一篇默认你知道这两件事,只讲 K3 自己做的。
并行的所有问题都可以问成一句话:谁在等谁。K3 训练里的等待一共四类,下面先把它们列出来,再逐个看 §5.2.1 和 §5.2.3 怎么处理。
谁在等谁:四类等待
把一个 PP stage 上一层 MoE 的执行顺序摊开:
沿着这条线数,K3 训练里的等待一共四类。§5.2.1 和 §5.2.3 的每个技巧都对应其中一类:
| 谁在等谁 | 在哪里发生 | K3 的做法 |
|---|---|---|
| rank 等 rank | EP:token 分配不均,最慢的 rank 决定这一层的时间 | MoonEP 的完全平衡 |
| PP:warmup 和 cooldown 的气泡 | ViT 的计算塞进气泡(§5.2.3) | |
| host 等 device | 每层 MoE 的 token 数每步不同,CPU 要等 GPU 算出来才能 launch expert GEMM | 完全平衡之后 shape 静态已知,不再同步 |
| SM 等 SM | 一个 rank 内部,各 expert 的 token 数仍然偏斜,固定顺序的 GEMM 调度让部分 SM 先空下来 | 按当前 token 分布选参数的 GEMM 调度器 |
| 计算等通信 | dispatch / combine 的 all-to-all;冗余 expert 的权重预取 | 与计算重叠;shared expert 放到另一个 stream 上 |
前三类是 MoonEP 一次解决的,下面从它的目标开始推。流水线气泡里塞 ViT 的部分放在本篇最后。
MoonEP:从「完全平衡」这个目标出发
常规 EP 的问题论文说得很直白:token 负载在 rank 之间不均衡,计算不均衡拖慢吞吐;而且 routed expert 的激活 shape 每步在变,造成大量显存碎片。两个问题的根源是同一个:每个 rank 收到多少 token 由 router 决定,事先不知道。
MoonEP 把目标定得很死:每个 rank 恰好收到 份 token。这里 是 micro-batch 里的 token 数, 是每个 token 选的 expert 数, 是一个 rank 自己的 token 产生的「token 份数」。全部 个 rank 一共 份,平均下来每个 rank 正好 。目标是让每个 rank 算的份数等于这个平均值,而不是「差不多」。
达到这个目标的手段只有一个:冗余 expert。某个 expert 太热,就在别的 rank 上临时放一份它的权重副本,把一部分发给它的 token 分流过去。K3 的做法是每一层、每个 micro-batch 都根据当前 router 输出在线规划冗余 expert 的放置,在 routed expert 计算之前把权重预取过去;反向时冗余副本的梯度先在本地 reduce 缓冲里暂存,算完再 reduce 回它的 home rank。
于是关键问题变成:要预留多少冗余 expert 的槽位,才能保证不管 router 怎么输出都能补到完全平衡? 论文的回答是附录 E 的定理。
定理 1:每个 rank 最多 E/R 个冗余 expert 就够
记 是 expert 总数, 是 EP 的大小,每个 rank 本地有 个 expert。定理 1 说:对任意 router 输出,都存在一个方案,让每个 rank 恰好收到 份 token,并且每个 rank 上的冗余 expert 不超过 个。
证明是一个构造,比想象中短。先把每个 rank 按本地 token 份数分成欠载(少于 )和过载(多于 )两类。然后重复下面这一步:
- 挑一个欠载的 rank 和一个过载的 rank 。
- 从 搬 token 到 ,一次把 补满到恰好 。 搬走这些之后可能仍然过载、正好平衡,或者反过来变成欠载,把它放回对应的集合。
每一步让一个欠载 rank 变成平衡,而被补满的 rank 之后不再变动,所以最多 步就结束。这一点是整个证明的支点:每个 rank 最多被补一次,所以它收到的远端 token 全部来自同一个 rank 。而 搬出去的 token 都属于它自己的本地 expert(它只在过载时往外搬,过载的 rank 从来没收过别人的 token),最多 个。远端 token 涉及几个 expert, 就需要几个冗余 expert,所以不超过 。证毕。
把这个过程动手走一遍:
每根柱子是一个 rank 上要算的 token 份数,颜色是 token 属于哪个 expert;实心是本地 expert,斜线是从别的 rank 搬来的、需要在本 rank 放一份冗余副本的 expert。虚线是平衡值 。上限 。
多按几次「随机 router 输出」,冗余数从来不会超过 2。注意第二步里 被搬到欠载的情况:它之后会被别的过载 rank 补一次,仍然只涉及一个来源。
定理 2:这个上界基本是紧的
上界会不会太松?论文构造了一个最坏例子:rank 0 的本地 expert 一份 token 都没收到,其余 个 rank 的 个 expert 把全部 份 token 平分,每个 expert 收到
份。不管怎么规划,rank 0 都要收 份远端 token,每个 expert 最多贡献上面这么多,所以至少涉及
个 expert,取整后 rank 0 至少需要 个冗余 expert。 大时这个数约等于 ,所以没有明显更小的通用上界。demo 里的「最坏情况」按钮就是这个构造:,,rank 0 至少要 个,正好碰到上界。
代入 K3
。EP 大小论文没给,按几个可能的值算本地 expert 数,它同时也是每个 rank 要预留的冗余槽位上限:
| 每 rank 本地 expert 数 | 预留的冗余槽位 | |
|---|---|---|
| 32 | 28 | 28 |
| 56 | 16 | 16 |
| 112 | 8 | 8 |
也就是说,每个 rank 要为「最多再放一倍的 expert 权重」留出显存。这笔账 MoonEP 是主动付的,换来的是规划永远有解,训练永远不会因为找不到可行方案而停下。这正是它和前人的区别:
论文点名的 ECHO 和 UltraEP 走的是另一条路,预设冗余 expert 的数量,或者给每个 rank 设一个 token 上限。上限之内没有可行方案时训练只能停下,上限本身要手调,而且调好了也还有残余的不均衡。MoonEP 用一个定理换掉了这个手调的旋钮。
在线规划、零拷贝、静态 shape
定理只保证方案存在,每一步真的去求最优解太贵。论文的做法是离线用整数线性规划算出一些代表性 case 的精确最优解当参照,然后设计一个 GPU 上的规划内核:接近最优、开销可以忽略、并且永远不超过 的上界。所以实际跑的是启发式,定理的作用是保证启发式总能找到落在预算里的解。
完全平衡之后,通信路径也跟着简化。规划内核已经算出了每个 token 的目的地,于是 permute 和 unpermute 融合成一个算子,token 直接被送到远端 rank 上按 expert 分组好的位置,通信缓冲的视图直接交给计算,中间没有任何拷贝。这一点 DeepEP 也能做,但要付一笔缓冲的账:不知道每个 rank 会收到多少 token,就得按最坏情况准备缓冲。最坏情况是所有 token 都涌向同一个 rank,缓冲要 份;MoonEP 每个 rank 恰好收 份,缓冲就是固定的 ,差了 倍。
第三个好处是把 host 从循环里拿掉。常规 MoE 实现里每一层每个 expert 收到多少 token 都在变,CPU 必须等 GPU 算出实际数字才能 launch expert 计算,每层同步一次,流水线在层与层之间停顿。完全平衡后每个 rank 每层都是 份,所有层的计算 shape 静态已知,这个同步就没有了,host 侧的 launch 开销也跟着减轻。
rank 内部:expert 之间仍然偏斜
rank 之间平衡了,rank 内部 个 expert 各自收到多少 token 仍然是偏的。group GEMM 如果按固定顺序、不看负载地把工作分给 SM,这个偏斜就会变成 SM 之间的 makespan 差:有的 SM 早早做完,等最后一个大 expert 算完。K3 的 routed expert GEMM 用一个感知负载的调度器:launch 之前根据当前 token 分布选好调度参数,执行中不再改。参数由一个轻量启发式挑,背后是基于硬件指标的解析代价模型,关键系数用离线自动调优标定。shared expert 的 GEMM 则直接扔到另一个 stream 上,和主流的 all-to-all 与 routed GEMM 重叠。
和 QB 的关系
第 5 篇的 Quantile Balancing 也是在做负载均衡,两者不冲突,是分工。QB 通过 router 偏置改变路由本身,让 expert 的负载在统计上平衡,作用于训练动力学;MoonEP 不改路由,接受 router 给出的任何结果,通过冗余副本让每一步的执行形状完全一致,作用于系统。QB 做得越好,MoonEP 需要的冗余越少;但只要 QB 不是逐 micro-batch 精确的(它本来就不是),MoonEP 就有活干。
视觉编码器:塞进气泡里
§5.2.3 讲的是另一类 rank 等 rank:流水线 warmup 和 cooldown 的气泡。这些空隙常规上是浪费掉的,K3 往里面塞了视觉编码器的计算。先把时间线放在这里,切到「ViT 填气泡」视图看红色方块:
用同一个 micro-batch 的深浅表示它在流水线里的先后; 时颜色的明暗区分同一 rank 上的不同虚拟段。1F1B 的气泡时间占比理论值是 ,通常近似写成 ;读数里给的是 rank 0 的模拟值。
多模态训练里 MoonViT-V2 的工作量随样本剧烈变化,一张大图或一段长视频会让编码器时间暴涨,并且让持有它的设备成为拖后腿的那个。K2.5 引入过 Decoupled Encoder Process,把 ViT 和文本训练拆成独立阶段,并把视觉的前向和反向在 PP stage 之间摊平。K3 更进一步观察到了 interleaved 1F1B 的一个形状特征:头几个 micro-batch 的文本前向全挤在最开头,末几个 micro-batch 的文本反向全挤在最末尾。于是把 ViT 的计算拆开:头几个 micro-batch 的 ViT 前向同步地先做,其余的 ViT 前向塞进流水线气泡;反向对称处理。上面时间线里的红色方块就是这个安排的示意。结果是大部分 ViT 计算被藏在气泡里,视觉编码器的有效开销基本消失。
塞进气泡的前提是单个样本的编码器计算不能太长,否则塞不下,还会让持有大图的设备拖慢整条流水线。所以配套的是动态上下文并行:一张大图沿 patch 维切到多个设备上,注意力通过跨 CP rank 收集 KV(gather-KV)来算;每个 CP 组再切成若干子组,多张大图按负载均衡分配过去,让通信占比不随规模增长。这两件事一个压峰值、一个填空隙,合在一起视觉编码器才从关键路径上消失。
小结
这一篇的所有内容可以收进一张表:rank 等 rank、host 等 device、SM 等 SM、计算等通信。MoonEP 用一个定理解决前两类:每个 rank 预留 个冗余 expert 槽位,任何 router 输出都能补到每个 rank 恰好 份 token;平衡带来的静态 shape 又顺手消掉了 host 同步和 倍的通信缓冲。第三类靠感知负载的 GEMM 调度,第四类靠 stream 重叠和把 ViT 塞进流水线气泡。
时间账算平了,下一个问题是空间。流水线让 rank 0 攒着 份甚至更多的激活,冗余 expert 让每个 rank 要为一倍的 expert 权重留位置,而 2.8T 参数本身的梯度和优化器状态还没算。第 9 篇从一张每卡显存账本开始。