Skip to content

07-16 下午:分布式大模型训练与推理

最后更新于·约 12696 字

下午这节课讨论的是模型和 batch1 都很大时,一张 GPU 已经放不下或算不动,怎样把一次训练和一次推理拆到多张卡上。后面的 DP、ZeRO2、PP、TP、长上下文并行名字很多,拆法也各不同,但绕不开的三个问题始终是每张卡保存哪一部分状态、负责哪一段计算、数据何时要交给别的卡。

自回归语言模型推理

把一句 prompt3 交给语言模型后,模型一次只预测下一个 token4,条件是已有全部历史;把刚预测出的 token 接回输入,再预测下一个,直到结束符;这种逐 token 生成方式叫自回归(autoregressive)5。一个 token 在模型中先被映射为长度为 \(H\) 的向量,形成形状约为 \([B,S,H]\) 的张量6

  • \(B\) 是 batch size,一次并行处理多少个样本;
  • \(S\) 是 sequence length,即 prompt 的 token 数;
  • \(H\) 是 hidden size,单个 token 的隐藏向量维度。

这三个维度的读法要固定下来。\([2,16,4096]\) 表示两个请求,每个请求当前有 16 个 token,每个 token 是 4096 个浮点数。若只看其中第 1 个请求,可以去掉 batch 维得到 \([16,4096]\);再看其中第 3 个 token,又得到长度 4096 的向量。矩阵乘法中的许多形状错误,来自没有写明当前张量的行和列各代表什么。

张量读法自测

一个日志中出现的张量形状是 [3, 7, 1024]。这三个数分别表示什么?去掉 batch 维后形状是什么?取出第 2 个请求的第 5 个 token 后形状是什么?

分别是 3 个请求、每个请求 7 个 token、每个 token 1024 维;去掉 batch 维是 [7,1024];取出一个 token 后是 [1024]。读分布式日志时保持这个顺序,能快速判断当前操作是在请求级、序列级还是 token 级。

一次前向传播可以还原成下面几类操作。embedding 查表把 token 编号变成 [B,S,H];attention 的线性投影把最后一维从 H 变成 Q/K/V 所需维度;\(QK^T\) 在每个 head 中产生 [B,heads,S,S] 的注意力分数;softmax 后乘 V 仍回到 [B,heads,S,head_dim];FFN 对每个 token 独立做 [B,S,H] × [H,4H][B,S,4H] × [4H,H] 的矩阵乘;最后输出投影得到 [B,S,V] 的 logits。

embedding 只给 token 一个可计算的向量表示,并不含词序。位置编码或位置嵌入再把它位于第几个位置补进去。随后张量经过一层又一层 Transformer block;每层由 attention 和 FFN/MLP 等部分组成,最后经输出投影和 softmax 得到词表上的概率分布。

课件的 Transformer Block 与 Masked (Causal) Attention:attention 只能看到当前及之前的 token,当前 token 之后的位置被遮蔽,保证生成时只依赖已知历史。

图:因果(masked)注意力让每个位置只看它之前的 token;这是自回归语言模型能够逐 token 生成的关键结构。

同一模型的两种时间尺度

处理已有 prompt 时,可以并行计算许多位置,矩阵乘法大而规则;逐 token 生成时,每一步依赖上一步采样结果,只能把不同请求拼在一起提高并行度。二者使用同一套权重,却会由不同资源限制。

训练时的过程比推理多了反向传播。前向传播得到预测分布,与目标 token 算交叉熵损失;反向传播从损失倒着求每个参数的梯度;优化器根据梯度更新参数。以后看到并行训练的代码,先在脑中把它还原成这条链:forward -> loss -> backward -> optimizer.step()。并行策略没有改变这件事,只是把每一步拆到多张卡上。

训练语言模型时,loss 并不是只在最后一个 token 上计算。输入序列 \([x_0,x_1,\ldots,x_{S-1}]\) 的目标是右移一位的 \([x_1,x_2,\ldots,x_S]\)。第 1 个位置的输入是 \(x_0\),模型要预测 \(x_1\);第 2 个位置的输入包含 \(x_0,x_1\),要预测 \(x_2\)。因果 mask 保证每个位置只能用左侧信息。这样一次前向能同时提供 S 个训练样本,而不必把每个上下文长度单独跑一遍。

Prefill、Decode 与推理调度

prefill7 对已有 prompt 的所有 token 做前向并建立 KV cache8,query-key 交互与矩阵乘法规模较大,通常更接近计算密集;decode9 每步只生成一个新 token,却要读取所有历史 KV,常受显存带宽和单请求延迟限制。continuous batching 把不同请求的 decode step 合在一个 batch,并在请求结束时补入新请求。它必须维护每个序列的长度、采样状态和 KV block 映射,不能像训练那样把所有样本简单 padding 到固定长度。

用一个短例子区分 prefill 和 decode。用户输入“今天 天气 很”,prefill 一次处理这三个 token,并保存每一层对它们的 K、V。第一步 decode 只把新 token“好”送入模型,attention 查询它和前四个 token 的 K/V,输出“。”;第二步只把“。”送入,前面四个 token 的 K/V 不必重算。若没有 KV cache,每生成一个 token 都要把完整历史重新跑一遍,序列越长浪费越大。

continuous batching 解决的是另一个问题。请求 A 已经生成到第 100 个 token,请求 B 刚到达,请求 C 在第 8 个 token 结束。传统静态 batch 必须等最长请求结束;continuous batching 可以在每个 decode step 结束后移除 C、补入 B,让设备里的 token 数尽量稳定。它换来的是调度复杂度、变长张量处理和 KV 空间管理。

小游戏:判断该进 prefill 还是 decode 队列

  • 请求 D 刚收到 800 个 token 的长 prompt,尚未生成任何 token。
  • 请求 E 已生成 30 个 token,正在等第 31 个 token。
  • 请求 F 刚在第 17 个 token 满足停止条件。
  • 请求 G 的 prompt 有 3 个 token,刚加入系统。

D 和 G 应进入 prefill;E 在 decode 队列;F 应移出 batch 并释放或标记 KV block。若把长 prefill 和低延迟 decode 混在同一个队列,交互请求可能被 800 token 的矩阵乘阻塞,因此服务系统常将二者分开调度或设置抢占策略。

课件画出 LLM 推理的两个阶段:Prefill 一次性处理全部输入 token 并建立 KV cache,之后的 decode 每步生成一个新 token、读取已有 KV。

图:prefill 大而规则、偏计算密集;decode 每步只出一个 token 却反复读 KV,偏带宽/延迟受限。两者决定了推理调度的不同取舍。

KV cache 是按层保存的 K/V 张量,其字节数近似为 layers × tokens × 2 × kv_heads × head_dim × bytes_per_element。若采用 grouped-query attention,kv_heads 小于 query heads,缓存会显著下降;若上下文达到显存上限,调度器必须限制并发、换出、分页管理或拒绝请求。吞吐、首 token 延迟和每 token 延迟互相牵制,单报 token/s 无法描述服务行为。

系统通常同时关注首 token 时间(TTFT)、后续 token 间延迟(TPOT)和总体吞吐。为了吞吐积累很大的 batch,可能伤害交互请求的首 token 延迟;连续批处理、分页式 KV cache、分开调度 prefill 与 decode,都是在处理这个矛盾。

训练和推理也会用 TP、PP、序列并行,但目标不同:训练更在意吞吐、全局 batch 和模型状态;推理更在意 KV cache 能否容纳、请求到达的动态性与用户实际等待时间。分析一个方案之前,先分清正在解决哪一种负载,后面的优化才有明确含义。

思考题

prefill 和 decode 的计算模式有什么不同?为什么它们适合不同的优化方向?

答案

prefill 一次处理整个 prompt,可以在序列维度做批量矩阵乘,算力较容易喂满;decode 每步通常只生成一个 token,主要反复读取 KV Cache 和权重,更受显存带宽和调度影响。因此 prefill 常看 GEMM 与并行策略,decode 常看 KV Cache、batching 和访存。

思考题

训练语言模型时,为什么目标序列是输入序列右移一位?

答案

第 0 个位置看 \(x_0\) 要预测 \(x_1\),第 1 个位置看 \(x_0,x_1\) 要预测 \(x_2\)。因果 mask 保证每个位置只使用左侧 token,因此一次前向能同时得到多个“预测下一个 token”的训练样本。

思考题

KV cache 保存 K 和 V,为什么不保存每个历史 token 的 Q?

答案

生成第 \(t+1\) 个 token 时,新的 query 只来自新输入 token,历史 token 只需要作为 key 和 value 被查询。历史 Q 对后续注意力没有直接用处,保存它只会增加显存。反向传播需要重算或保存激活是训练时的问题,推理没有这条需求。

分布式训练的约束

模型参数不断增长,而单卡算力和显存并不能无限按同样速度增长。即便参数本身勉强放得下,训练还要保存梯度、优化器状态和中间激活10;长序列的 attention 又会放大激活与 KV cache 的开销。于是模型是否放得下和每一步是否来得及算完是两个不同问题。

为什么会需要多卡:单卡装不下、也跑不动

你在单周期 CPU 课里接触的程序很小,一个内存就装下了,也不会去考虑放不下这回事。大模型则完全不同,一个 175B 参数的模型,仅 BF16 权重就近 350 GB,训练还要额外保存梯度和优化器状态,单张 GPU(常见的几十 GB 显存)根本装不下。就算恰好塞得进,算一个这么大的前向加反向也要极久。

于是只能把问题拆开、放到多张卡上。拆的方式决定了后面几节的内容:可以按数据分(每张卡拿不同 batch,都有一份完整模型,最后同步梯度),按分(流水线,每张卡负责若干层),或在一层内部按矩阵分(张量并行11)。它们用不同的通信代价换来了单卡塞不下、单卡算得动不了的缓解。

这里的关键是,多卡并行不是免费的。拆散之后,卡与卡之间必须交换数据(梯度、中间张量),而这些交换就落在 GPU 之间的互连上。通信一旦跟不上,再多卡也不一定更快。所以整个并行世界基本在回答一个问题,怎么拆,才让必须的通信最少、又能被计算隐藏掉

训练显存至少有四类对象。参数是模型文件里的权重;梯度是损失对各参数的偏导,形状通常和参数相同;优化器状态是 Adam 等算法额外保存的一阶动量、二阶动量和主权重;激活是前向传播中为了反向求导而保存的中间张量。推理通常只需要参数和 KV cache,训练则四类都会出现。看到“7B 模型 14GB”时,那是 BF16 权重的下限,并不等于训练所需显存。

分布式训练并不是简单地把一块 GPU 换成多块。若多张卡各自拿到不同数据、各自更新同一个模型副本,却从不通信,它们很快就会变成不同的模型。多卡训练成立的前提是:在该同步的地方同步,在可以独立计算的地方不通信,并尽量把必须发生的通信藏在计算时间里。

思考题

一个 7B 模型的 BF16 权重约 14GB,为什么训练显存远大于 14GB?

答案

训练还要保存梯度、Adam 的 FP32 主权重、一阶动量和二阶动量,以及反向传播需要的 activation。仅常见模型状态就可能超过 100GB。权重文件大小只是推理或初始化的下限,不是训练显存。

通信:点对点与集合操作

点对点通信(point-to-point, P2P)是一个 rank 向另一个 rank send,对方 recv。集合通信(collective communication)由一个通信组里的所有 rank 共同参加,例如 4 张 GPU 同时执行一次 AllReduce12。它最终仍由许多点对点传输组成,但由 NCCL、MPI、Gloo 等通信库选择拓扑和实现。

集合操作有一个容易忽略的约束:参与者必须按相同顺序进入同一个 collective。一个 rank 没有进入、卡住或退出,其他 rank 会一直等待,最后通常表现为 timeout。因此调试分布式程序时,不能只看报错的那张卡;要找第一个没有到达同步点的 rank。

常见集合通信

设每个 rank 上有一个局部张量,collective 的名称描述数据在组内怎样变化。

操作 结束后数据在哪里 常见场景
Broadcast 根 rank 的同一份数据出现在所有 rank 初始化参数、同步控制信息
Reduce 各 rank 数据规约到根 rank 汇总和、最大值
AllReduce 规约结果出现在所有 rank 数据并行梯度同步
Scatter / Gather 根张量分块下发 / 各分块收回根 输入分发、结果汇总
AllGather 每个 rank 都得到所有分块的拼接 临时凑齐分片参数或激活
ReduceScatter 先规约,再让每个 rank 留一个分块 分片梯度与状态
All-to-All 每个 rank 给所有其他 rank 发送不同分块 MoE token 路由

用四个 rank 的 AllReduce 看语义。设局部向量分别是

rank0: [1, 0]    rank1: [0, 2]
rank2: [3, 0]    rank3: [0, 4]

若做 sum AllReduce,四个 rank 结束后都得到 [4,6]。实现可以是 ring reduce-scatter 再 all-gather,也可以是树形规约;使用者关心的是语义和通信量,底层库关心的是拓扑和算法。AllGather 则不同,若 rank0 有 [1,0]、rank1 有 [3,4],结束后两个 rank 都得到 [1,0,3,4],中间没有把对应元素相加。

集合通信自测

两个 rank 分别持有梯度分片 g0=[1,2,3]g1=[4,5,6]。执行 sum AllReduce 后各自得到什么?执行 AllGather 后各自得到什么?

AllReduce 后都是 [5,7,9];AllGather 后都是 [1,2,3,4,5,6]。前者适合让所有数据并行13副本得到同一份全局梯度,后者适合临时拼出完整张量。

Scatter 将一份输入分给多个进程;Gather 将各进程持有的结果收回。

图:Scatter 改变数据所有权,Gather 恢复集中表示;每个箭头都对应实际通信量。

课件对比 Allgather、Reduce-Scatter 与 Allreduce:前者拼接各进程的 shard,后者先规约再让每进程保留一个分块或得到完整结果。

图:三种集合通信都围绕每个进程持有一块的数据展开,区别在于是拼回去还是规约后分发。

不要把 AllGatherAllReduce 当作只差一个字的函数。前者的结果是拼接后的原始数据;后者的结果是逐元素相加、平均等规约后的数据。训练中究竟该用哪一个,取决于你需要的是完整参数、完整激活,还是一致的全局梯度。

思考题

数据并行同步梯度时,为什么常用 AllReduce 而不是 Reduce?

答案

数据并行的每张卡都要用同一份全局梯度继续更新自己的参数。Reduce 只把结果送到一个 rank,其他 rank 得不到结果。AllReduce 让所有 rank 结束后都拿到同一份规约结果。

通信延迟与带宽

课上用一个常见的近似模型表示一次消息传输:

\[ T_{\mathrm{comm}} \approx \alpha + \frac{n}{\mathrm{BW}} = \alpha + n\beta \]

课件给出通信时间模型 \(\alpha + n\beta\):\(\alpha\) 是建联/调度延迟,\(n/\mathrm{BW}\) 是传输时间;小消息受 \(\alpha\) 主导,大消息受 \(n\beta\) 主导。

图:通信时间由固定延迟与随消息大小增长的传输时间组成;据此决定是合并小消息还是用 ring 处理大消息。

\(\alpha\) 是启动延迟,例如建立/调度一次传输的固定开销;\(n\) 是消息大小;\(\mathrm{BW}\) 是有效带宽,\(\beta=1/\mathrm{BW}\)。消息很小时,主要输在 \(\alpha\),此时合并小消息、减少调用次数比盯着峰值带宽更重要。消息很大时,\(n\beta\) 主导,链路带宽、拓扑和传输算法才成为重点。

例如 AllGather 可以用 ring 实现。把 \(p\) 个 rank 排成环,每个 rank 先持有自己的第 \(i\) 块;每一轮把当前块传给下家,同时从上家接收一块。经过 \(p-1\) 轮,每个 rank 都收到了其余所有块。每轮只搬一块较小的数据,链路可同时工作,因而对大消息常有很好的带宽利用率。Ring AllReduce 通常可理解为一段 ReduceScatter 加一段 AllGather。

课件的 Ring Algorithm:把节点排成逻辑环,每轮每个节点向邻居传一块数据,经过多轮后每个节点都拿到完整结果。

图:ring 让所有链路的带宽同时被利用,适合大消息;多轮交换是它的代价。

这也解释了为什么通信库会按消息大小选算法:小张量未必适合 ring 的多轮启动,大张量又不适合让一个根节点成为瓶颈。实际耗时还会受 PCIe、NVLink/NVSwitch、跨节点网络和物理拓扑影响,不能只用 GPU 数量推断。

课件说明 NCCL 会根据 message size 自动选择通信算法:小消息图省启动、大消息图带宽,用户一般不必手工指定。

图:通信库按消息大小在启动开销优先与带宽优先之间切换实现;这正对应 \(\alpha\)\(n\beta\) 哪个主导的判断。

思考题

AllReduce、AllGather、ReduceScatter 的结果分别落在哪里?各适合什么场景?

答案

ReduceScatter 把各 rank 数据归约后按片段分给所有 rank;AllGather 把各 rank 的片段收集成完整结果并分发给所有 rank;AllReduce 等价于先 ReduceScatter 再 AllGather,让每个 rank 都拿到同一份归约结果。数据并行的梯度平均常用 AllReduce,张量并行前向收集常用 AllGather。

思考题

四个 rank 约定执行 AllReduce,但 rank3 还在上一段计算中,前三个 rank 已经进入集合通信。系统可能表现成什么?调试时应先看哪里?

答案

前三个 rank 会在集合通信中等待,表面现象常是通信超时或进程挂起。真正的问题可能在 rank3 的前序计算、异常退出、条件分支或不同进程的通信顺序不一致。应找第一个没有到达同步点的 rank,并检查所有分支是否进入同一个 collective。

数据并行

数据并行(data parallelism, DP)沿 batch 维切分数据。假设有四张 GPU,GPU 0 处理 mini-batch 的第 0 份,GPU 1 处理第 1 份;每张卡上却都保留完整的模型参数 \(\theta\)。它们分别执行前向与反向,得到局部梯度 \(g_0,g_1,g_2,g_3\)

课件表明数据并行的前提:单张 GPU 能容纳完整的模型副本,各卡拿不同 batch,通过通信保持参数一致。

图:每卡都有一份完整模型,输入按 batch 切分;计算是并行的,参数靠通信保持一致。

要让四份模型继续保持一致,更新前需要计算全局平均梯度:

\[ g=\frac{1}{p}\sum_{i=0}^{p-1}g_i,\qquad \theta\leftarrow\theta-\eta g. \]

AllReduce 可以同时完成求和和结果分发,所以每张 GPU 得到同一个 \(g\),再各自执行同样的 optimizer step,最终参数仍相同。这里的通信发生在反向传播产生梯度之后;没有它,四张卡等价于各自训练四个互不一致的模型。

DP 的加速上限受通信约束。若单卡一个 step 的计算时间是 \(C\),AllReduce 梯度时间是 \(M\),理想多卡时间仍至少有 \(C+M\) 的一部分。GPU 数增加后,单卡 batch 变小、固定开销占比上升,通信量也会随梯度大小变化。因此吞吐曲线常常先近似线性增长,再逐渐弯曲,最后增加卡数不再划算。

较早的做法是 parameter server:多个 worker 把梯度交给服务器,服务器汇总、更新参数后再发回。它的逻辑直接,但服务器既是吞吐瓶颈又是单点故障。现代同步 DP 更常让 worker 直接集合通信,不再保留一个集中式参数服务器。

课件的 Parameter Server:多个 Worker 把梯度发给集中的 Parameter Server,服务器汇总更新后把新参数发回各 Worker。

图:集中式参数服务器逻辑直观,但所有梯度都汇集到一点,既是吞吐瓶颈也是单点故障。

课件展示去中心化的 Allreduce DP:每个 worker 与相邻 worker 直接组成环/树做规约,无需集中式服务器。

图:现代 DP 让 worker 直接通过 AllReduce 交换梯度,去掉了集中式参数服务器这个单点。

Global Batch 与梯度累积

若每卡 batch 为 \(b\),数据并行大小为 \(p\),没有梯度累积时全局 batch 是 \(B_{\text{global}}=pb\)。显存不够而又希望维持大 batch 时,可连续跑 \(k\) 个 micro-batch,只累加梯度、不更新参数,最后再同步/更新;此时全局 batch 为 \(pkb\)

这不是单纯的工程细节。batch 变大,单步梯度的统计特性、每个 epoch 的更新次数和学习率设置都会变。吞吐提升不自动意味着同样的收敛效果,训练配置通常要和并行规模一起重新检查。

AllReduce 的归约语义

数据并行的每个 rank 持有同一组参数、处理不同 mini-batch。若本地梯度为 \(g_r\),同步 SGD 需要得到

\[ g=\frac{1}{P}\sum_{r=0}^{P-1}g_r, \]

然后所有 rank 用同一个 \(g\) 更新同一份参数。AllReduce 完成的是求和,除以世界大小可能由框架或优化器完成;把平均做两次会让学习率实际缩小 \(P\) 倍。梯度累积时,多个 micro-batch 的局部梯度要在正确边界再同步,no_sync 一类接口的含义正是推迟 collective,而不是跳过它。

通信-计算重叠依赖梯度产生的顺序。反向传播从最后一层开始;当某个 bucket 的所有梯度准备好,就可在通信 stream 上启动 AllReduce,同时计算前面层的梯度。bucket 太小,collective 启动次数多;太大,首个通信启动太晚。真正的重叠需要 profiler 时间线证明,并要求网络、GPU DMA 和计算资源没有已经互相饱和。

思考题

数据并行为什么每个 GPU 都保存完整模型?这个特点带来什么通信和显存代价?

答案

数据并行的划分对象是小批量数据,每个 GPU 用自己的 micro batch 前向和反向,模型参数与优化器状态通常完整复制。反向后需要同步梯度,例如 AllReduce。它实现直接、扩展训练吞吐容易,但显存重复保存,模型变大后单卡装不下,于是需要 ZeRO、FSDP 或模型并行。

训练显存

模型状态与混合精度

\(\Psi\) 表示参数量。若模型权重和梯度用 BF16,各自约需 \(2\Psi\) 字节;使用 Adam 时,通常还有 FP32 主权重、一级动量和二级动量,各占约 \(4\Psi\) 字节。于是仅模型状态的常见粗略估算是:

$$ 2\Psi\;\text{(BF16 参数)} + 2\Psi\;\text{(BF16 梯度)} + 4\Psi\;\text{(FP32 主参数)} + 4\Psi\;\text{(一阶动量)} + 4\Psi\;\text{(二阶动量)} =16\Psi \text{bytes}.

把这笔账落到 7B 模型上,可以逐项写出来。参数 14GB、梯度 14GB、FP32 主权重 28GB、一阶动量 28GB、二阶动量 28GB,合计约 112GB。若有 8 张 80GB A100,仅模型状态占用就低于总显存的一半,但 activation 会随 batch、序列长度和 checkpointing 策略继续增加。估算显存时宁可保守,因为 kernel 临时空间、通信 buffer、框架缓存和碎片都会消耗额外空间。 $$

对一个 7B 参数模型,这一部分已约为 112 GB,尚未计入 activation、临时 buffer、通信 buffer 和框架开销。具体数值会随混合精度、优化器、是否保存主权重而变,但课上要传达的是:训练显存里最占空间的常常是训练要附带保留的状态,而不只是模型文件有多大。

以一个 7B 参数模型为例,仅权重和训练附带状态就可能占据极大的显存。

课件以 LLaMA-2 7B 为例拆解参数量来源:embedding、attention 投影、FFN 等的参数加起来,决定模型文件与显存的最小量级。

图:参数量由各层的权重矩阵之和决定;它是显存账本的起点,却只是训练显存的一部分。

这里每个位置用什么精度是有讲究的。权重和梯度在计算过程中可以用 bf16 这类低精度来省显存、省带宽;但优化器状态和主权重往往需要更高的 fp32 精度。原因是梯度要反复累积、更新,低精度累加很容易把很小的更新量损失掉,导致数值不稳定。于是常见的混合精度做法是,前向/反向用 bf16 跑得快,梯度算完后以 fp32 精度更新主权重,优化器的动量、方差也按 fp32 保存。这也是为什么训练显存里会同时出现 bf16 计算副本和 fp32 精度副本,账本要按两种精度分别估算。

Activation 与完整显存账本

激活是另一笔账。反向传播要用到前向阶段的中间结果,因此常规训练会保存多层 activation;sequence length、batch、hidden size 和层数一大,activation 也可能反过来成为首要瓶颈。activation checkpointing 的思路是少保存一部分,反向时重算,以额外计算换显存;它与 ZeRO 解决的不是同一笔开销。

checkpointing 可以用一条简单的时间线理解。普通训练在每个矩阵乘后保留中间结果,反向直接用;checkpointing 只保留某些边界处的输入,反向到该段时重算中间结果并接着求梯度。它减少的是激活显存,增加的是前向计算次数,通常不会减少优化器状态。ZeRO/FSDP 减少的则是参数、梯度和优化器状态的副本或分片。

训练与推理的账本也不同:推理主要保存权重和 KV cache,训练还要额外保留梯度、优化器状态和 activation,两者量级不能混用。

课件对比训练与推理的状态内存:训练要保留参数、梯度、优化器状态和 activation,推理则主要是权重与 KV cache,二者账本不同。

图:训练显存大头在模型状态与 activation,推理大头在权重与 KV cache;估算先分清在解决哪种负载。

checkpointing(activation recomputation)只保存若干边界 activation,反向时重跑中间前向以换取显存。它减少的是保存量,增加的是计算和可能的通信;不能把它与持久化检查点混为一谈。后者是故障恢复用的模型/优化器快照。选择并行策略前应分别列出参数、梯度、优化器状态、activation 与通信 buffer,而不是只报一个显存占用。

思考题

混合精度训练为什么通常仍要保存 FP32 权重副本?这和前向使用 FP16/BF16 是否矛盾?

答案

前向和部分计算用低精度提高速度、降低激活显存,但参数更新往往很小。若只在低精度权重上累加,小更新可能被舍入掉,长期训练不稳定。FP32 主权重保留更新精度,前向再转换成低精度,两者职责不同。

ZeRO / FSDP:把模型状态分片

ZeRO 的三个阶段可以按切分对象记。ZeRO-1 切优化器状态,ZeRO-2 再切梯度,ZeRO-3 连参数也切。对于前述 Adam 的 16 bytes/参数,如果四张卡完全分片,每张卡的模型状态平均可降到约 4 bytes/参数;通信则从梯度 AllReduce 变为前向或反向按需 AllGather 参数,以及 ReduceScatter 梯度。

FSDP 可以理解为 PyTorch 中这类分片训练的工程实现。某一层计算前,各 rank 临时 AllGather 出该层完整参数;计算后释放完整副本,反向再按需收集,梯度用 ReduceScatter 分片写回。它和 ZeRO-3 的核心思想一致,把“每卡都复制完整模型状态”改成“每卡长期只保存一个 shard”。

普通 DP 的问题很明显:每张卡都存完整参数、完整梯度和完整优化器状态,计算按数据分了,状态却没有分。ZeRO(Zero Redundancy Optimizer)按阶段去掉这些冗余副本;FSDP(Fully Sharded Data Parallel)是围绕参数完全分片的一类实现。

  • ZeRO-1:分片优化器状态。每个 rank 只长期保存约 \(1/p\) 的 Adam 状态。
  • ZeRO-2:在 ZeRO-1 基础上再分片梯度。完成反向后用 ReduceScatter,让每张卡只留下自己负责的梯度块。
  • ZeRO-3 / FSDP:连模型参数也分片。每张卡长期只持有参数、梯度和优化器状态的一个分片。

分得越彻底,单卡常驻状态越少,但运行时需要更多通信。ZeRO-3 在算某一层之前先 AllGather,把该层的完整参数临时凑齐;用完前向后释放不再需要的完整参数。反向到这一层时再 AllGather 参数,计算出梯度后用 ReduceScatter 把已规约的梯度留在对应 owner 上,由 owner 更新自己的参数分片。

课件画 ZeRO-3/FSDP 的生命周期:前向前先 AllGather 各参数 shard 拼成完整参数,算完释放;反向后用 ReduceScatter 把梯度留给各自 owner。

图:FSDP 在每次用到某层参数前临时 AllGather,算完即释放;通信换取了常驻显存的大幅下降。

以 GPT-3 175B 为例,课件比较普通 DP 和各级 ZeRO 的单卡状态内存;代价是更多通信。

图:ZeRO 依次分片优化器状态、梯度和参数;常驻显存下降,运行时 AllGather/ReduceScatter 增加。

因此 ZeRO 并不等于免费省显存。它选择了用通信换内存:模型因为显存根本无法训练时,这个交换是必要的;网络带宽不足、分片太细或每层过小而频繁 AllGather 时,吞吐反而可能下降。看配置时应区分两类问题:参数状态太大时考虑 ZeRO/FSDP;activation 太大时还要考虑 checkpoint、序列切分或减少 micro-batch。

思考题

ZeRO/FSDP 把参数和优化器状态切开后,为什么仍会引入通信?

答案

某一层需要完整参数时,各 rank 必须临时 AllGather 自己持有的分片;反向得到局部梯度后,又要通过 ReduceScatter 聚合并切回分片。显存占用下降,通信调度和重叠成为新的关键问题。

思考题

activation checkpointing 为什么能省显存,又为什么会让训练变慢?

答案

它不保存每个前向中间结果,只保存少量边界值,反向需要时重新执行一段前向来恢复激活。显存减少来自少存中间张量,时间增加来自重算。适合显存不足时换取更大的模型、batch 或序列长度,属于用计算换显存。

流水线并行

流水线并行14(pipeline parallelism, PP)沿网络深度切分。若模型有很多层,可以让 GPU 0 负责前几层,GPU 1 负责中间层,GPU 2 负责后几层。前向时 activation 从第一个 stage 依次传到后面;反向时梯度再反向传回。每张卡只放本 stage 的层,因而可容纳单卡放不下的深模型。

最朴素的排程会产生 pipeline bubble:GPU 0 先忙,其他 stage 在等 activation;前向全部结束后,反向又从最后一个 stage 才开始,前面的卡再次等待。卡的数量越多、每轮只有一个大 batch,空闲比例越明显。

课件画出 naive pipeline:一次前向/反向交替时,许多 stage 处在等待状态,空闲时间占比即 bubble ratio。

图:朴素流水线里,一个 batch 串行穿过各 stage,前后期的 stage 大量空闲;这种空闲比例叫 bubble。

处理办法是把 batch 切为多个 micro-batch,形成填充流水线。GPU 0 算完 micro-batch 0 后就把它交给 GPU 1,自己立刻开始 micro-batch 1;GPU 1 接到 0 后开始计算,后续 stage 也依次被填满。等流水线稳定下来,多个 stage 可以同时处理不同 micro-batch 的前向或反向。bubble 没有消失,但被更多 micro-batch 摊薄。

课件展示 GPipe:把输入 batch 拆成多个 micro-batch,逐块流水推进,让各 stage 尽量保持忙碌。

图:micro-batch 把大 batch 切小,流水线各 stage 因而能同时处理不同 micro-batch,摊薄 bubble。

PP 的通信主要是相邻 stage 间的 activation/gradient P2P 传输,延迟和负载均衡都很重要。如果某个 stage 的层更慢,整条流水线会被它卡住;切层时不能只按层数平均,还要考虑每层计算量、激活大小和显存。

流水线 bubble 可以用一个极小调度表看清楚。假设 4 个 stage、2 个 micro-batch,F 表示前向,B 表示反向,横轴是时间。若没有 micro-batch 交错,GPU 0 在第 1 个前向完成后要等后续 stage 逐步处理,反向又从 GPU 3 开始返回。画在时间线上,两端 GPU 有明显空闲,这些空闲就是 bubble。

GPU0: F0 F1 ........ B1 B0
GPU1: .... F0 F1 ... B1 B0
GPU2: ....... F0 F1 B1 B0
GPU3: .......... F0 B0 F1 B1

增加 micro-batch 数量、让不同 micro-batch 的前向和反向交错,可以填掉一部分空档,但通信次数和调度复杂度也随之增加。

思考题

流水线并行的 bubble 是怎么来的?

答案

前后 stage 之间有依赖,前面的 stage 要等激活送来,后面的 stage 在最早几个 micro-batch 时可能没有工作;反向阶段又会出现对称的空闲。把 batch 切成更多 micro-batch 可以降低 bubble 比例,但会增加调度和通信复杂度。

张量并行

PP 是按层切,张量并行(tensor parallelism, TP)则把一层内部的大矩阵乘法切到多张 GPU。Transformer 的 attention 投影和 FFN 都包含很大的矩阵乘法,这往往是计算最重的地方。

考虑线性层 \(Y=XW\)。若按 \(W\) 的列切分:

\[ W=[W_0\;W_1\;\cdots\;W_{p-1}],\qquad Y=[XW_0\;XW_1\;\cdots\;XW_{p-1}], \]

每张 GPU 可独立算一个 \(XW_i\),得到 \(Y\) 的一部分列;若后续需要完整的 \(Y\),再做 AllGather。若按行切分,则每张卡先算局部乘积,最后用 AllReduce 把各部分相加。不同的切法决定通信发生在算子前还是算子后。

用数值说明列切和行切的区别。设

\[ X=\begin{bmatrix}1&2\end{bmatrix},\qquad W=\begin{bmatrix}1&1\\2&3\end{bmatrix}. \]

完整结果是 \(XW=[5,7]\)。若按列切 \(W_0=[1,2]^T\)\(W_1=[1,3]^T\),GPU 0 算 \(XW_0=[5]\),GPU 1 算 \(XW_1=[7]\),需要时 AllGather 拼成 [5,7]。若按行切 \(W\),两块分别是 [[1,1]][[2,3]],GPU 0 得 [3],GPU 1 得 [4],必须相加才有 [7]。列切天然得到输出列分片,行切天然得到需要求和的部分和。

FFN 可以写成

\[ \operatorname{FFN}(X)=\phi(XW_1+b_1)W_2+b_2, \]

其中 \(\phi\) 是逐元素激活函数。第一层把 hidden size 扩到较大的中间维度,第二层再投回去。逐元素激活不会混合不同列,因此可在各 GPU 的局部结果上直接执行;恰当安排 \(W_1\) 的列切分与 \(W_2\) 的行切分,可把本来两次的全量通信压缩为较少的同步点。这就是 TP 在代码里常见列并行线性层和行并行线性层配对的原因。

TP 的通信频率很高,因为几乎每层都要在组内交换中间结果。因此通常把一个 TP group 放在同一台机器、使用 NVLink/NVSwitch 等高速互连;把频繁的 TP 通信跨慢网络,会非常不划算。

课件展示 Megatron-LM 的张量并行 MLP:把 FFN 的矩阵按列/行切到多卡,层内用多次 allreduce 同步中间结果。

图:张量并行把一层内的矩阵乘切开,卡间需要频繁 allreduce;因此 TP group 应放在 NVLink 等高速互连内。

思考题

把一个 [H,4H] 权重按列切成两份后,两个 rank 各自得到的输出形状是什么?为什么还需要通信才能得到完整 FFN 结果?

答案

输入 [B,S,H] 乘每份 [H,2H],两个 rank 分别得到 [B,S,2H]。这只是中间结果的一部分,FFN 需要把这些片段合并成 [B,S,4H] 再过激活和第二层。因此张量并行在层内引入 AllGather 或 ReduceScatter,通信频率比数据并行高,通常放在高速互连内。

思考题

FFN 的第一层权重按列切,第二层权重按行切。这两种切法各自解决什么通信问题?

答案

第一层按列切后,每个 rank 得到输出特征的一段,不需要先通信输入的不同列。第二层按行切时,各 rank 局部输出可以直接按分块矩阵乘法相加,最后通过 AllReduce 得到完整结果。切分方式要顺着矩阵乘法的形状选择,避免为了凑形状引入多余通信。

多维并行与通信计算重叠

真实的大模型训练常把几种策略叠起来:节点内做 TP,按层做 PP,剩余维度做 DP,再用 ZeRO/FSDP 分片状态。这常被称为 3D 并行,但不必被名字吓住。读一组并行度配置时,把总 GPU 数写成各维度的乘积,再逐维问:

课件给出 6D 并行框架:经典的 3D(数据/张量/流水线)之上再叠加序列并行、上下文并行与专家并行,并附一个 GPT-3 175B 的训练配置实例。

图:总 GPU 数等于各并行维度的乘积,例如 GPT-3 的配置 \(8\times 8\times 60=3840\) 块 A100;维度越多,切分越细,通信模式也越多样。

以 GPT-3 175B 为例,可把 8 块 A100 通过 NVLink 组成一个节点作为 TP 组,8 个节点按层做 PP,再复制 60 份做 DP,得到 3840 块 GPU 的集群配置。每个维度都对应一类通信:TP 在节点内频繁 AllReduce,PP 在相邻 stage 之间传 activation/gradient,DP 需要全局 AllReduce 梯度。多出来的 SP、CP 沿序列维切分,EP 则按专家切分并触发 All-to-All。面对一个具体配置,先算清这些 GPUs 是怎么分组的、组间传什么,比背维度名字有用。

检查并行配置时,把总卡数和乘积逐项写出,例如 3840 与 \(8\times8\times60\),随后给每个维度标注通信对象;TP 是节点内高频集合通信,PP 是相邻 stage 的点对点,DP 是全局梯度同步。最后核对每个组的大小是否匹配物理拓扑。TP 放在慢网络两端是最常见的错误之一,因为每层都可能等待通信。

  • DP 组里的卡拿不同数据,何时 AllReduce 梯度?
  • TP 组里的卡共享一层计算,哪个线性层后要 AllGather 或 AllReduce?
  • PP 的相邻 stage 之间传什么 activation 和 gradient?
  • ZeRO 分片后,哪一层前需要临时 AllGather 参数?

通信不一定必须停在计算之后才开始。反向传播从最后一层往前走,某层梯度就绪后可以立刻启动对应 bucket 的 AllReduce,同时 GPU 继续计算更前面的层。若通信在计算结束前完成,它的耗时被隐藏;若网络仍未完成,GPU 才需要等待。bucket 的大小过小会让启动延迟累积,过大又会推迟通信启动,因而是一个需要实测的折中。

课件用反向传播的时间线说明通信-计算重叠:反向算出一部分梯度就立刻发起通信,与继续计算重叠,从而把通信时间藏进计算里。

图:梯度按层/按 bucket 就绪即启动集合通信,不等整个反向结束;重叠是否成立要看网络是否在计算完成前清空。

手动把 TP/PP/DP/SP/CP/EP 组合成一套合理配置并不容易,于是有了自动并行。它把怎么切当作问题交给机器:输入模型计算图与硬件拓扑(若干节点、节点内 NVLink、节点间 IB),输出每个算子的切分方式、流水线 stage 划分和 micro-batch 大小。

课件介绍 Alpa 的分层自动并行:先做算子间 stage 划分,再对 stage 内算子选切分策略,最后把逻辑设备网格映射到物理拓扑,可达到手工调好的 Megatron 级性能。

图:自动并行的关键是分而治之——层间划分、层内切分、设备映射三者解耦,让每一层找局部最优,整体逼近手调结果。

这类工具(Alpa 之外,PyTorch DTensor、OneFlow SBP 也是同类思路)把设备网格抽象出来,统一描述张量在设备间如何分布,从而让同一份模型代码在不同并行度配置下运行。它的价值不在替代工程师手调,而在把可复现、可搜索的并行空间交给算法,减少每次换模型/换集群都要重写配置的负担。

思考题

总 GPU 数是 64,配置为 TP=4、PP=2、DP=8。这三组分别是什么?

答案

4 卡组成一个张量并行组,共同计算一层内被切开的矩阵;2 个流水线 stage 按层前后连接;8 份数据并行副本同时处理不同 micro-batch 并同步梯度。\(4\times2\times8=64\),读配置时先确认每组大小和物理拓扑是否匹配。

Transformer 层的算力估算

要判断长序列到底难在哪,先把一层 Transformer 的算力分清楚。以一层的 MLP 与 attention 为例,以 batch \(B\)、序列长 \(S\)、隐宽 \(H\) 计,各部分计算量大致如下。

课件给出每层算力估算表:Q/K/V/O 四个投影约 \(8BSH^2\),attention 的 \(QK^T\) 与 \(Softmax\times V\) 各约 \(2BS^2H\),FFN(SwiGLU 三个矩阵)约 \(6BSH\)。

图:投影与 FFN 随 \(S\) 线性增长、随 \(H\) 平方增长;attention 的两个矩阵运算随 \(S\) 平方增长、只随 \(H\) 线性增长。

关键在 attention 的 \(S^2H\) 项。把这一层的总计算写成 \(8BSH^2+4BS^2H+6BSH^2\),当 \(S\ll H\)(短序列)时,\(SH^2\) 项占主导,FFN 与投影是瓶颈;当 \(S>H\)(长序列)时,\(S^2H\) 项爆炸,attention 变成瓶颈。

这里的系数来自矩阵乘的形状。一次 \([m,k]\times[k,n]\) 矩阵乘大约需要 \(2mkn\) 次乘加。QKV 和输出投影的形状都围绕 \(S\times H\)\(H\times H\),合起来给出 \(BSH^2\) 量级;\(QK^T\)\(S\times H\)\(H\times S\),乘 \(V\)\(S\times S\)\(S\times H\),所以各自都含 \(S^2H\)。做实验时,把每个矩阵的 shape 代入这个公式,就能解释 profiler 里 GEMM 和 attention kernel 的相对耗时。

课件画出 attention 与 MLP 计算量随序列长 \(S\) 的双对数曲线:attention \(\propto S^2H\) 增长更快,在 \(S\) 超过隐宽附近反超 MLP。

图:横轴是序列长 \(S\),纵轴是计算量;attention 的二次增长让它在一段序列长度后反超 MLP,成为长上下文的主要成本。

这正是序列并行、FlashAttention 和 ring attention 要处理的对象:attention 的 \(S^2\) 项既带来巨大计算量,也让中间 attention 矩阵和 KV cache 无法完整物化或驻留单卡。把范围缩小到 \(S\ll H\)\(S>H\) 两种情况,再决定投影/FFN 该切分还是 attention 该分块,比一刀切更有依据。

长上下文与序列并行

长上下文会让 attention 的中间量和 KV cache 迅速增大。即使参数已经由 TP 或 FSDP 分片,单张卡仍可能放不下完整的序列。序列并行(sequence parallelism)和上下文并行(context parallelism)于是沿 sequence 维分割 token 或 K/V 块。

以 ring attention 为例,每张卡保存一段 Q/K/V。它先拿本地 Q 与本地 K/V 算一块 attention,然后把 K/V 块沿环传给下一个 rank,同时接收上一个 rank 的块;经过若干轮,每个 Q 块都和完整上下文的 K/V 做过计算。这样不必把所有 K/V 同时复制到每张卡,峰值内存随序列分片下降,代价是多轮环形通信。

上下文并行沿序列维切分 K/V,各 rank 沿环交换分段以覆盖完整上下文。

图:每张卡只持有部分 K/V 块,通过循环传递与本地 Q 逐步和全局 K/V 交互;通信次数随分片数上升,显存峰值随之下降。

这里有一个数值细节,softmax 不能简单把每一块各自归一化后再相加,因为全局分母依赖所有块。可维护运行中的最大值 \(m\)、归一化量 \(l\) 和加权结果,块到来时用新的最大值重新缩放旧累计量。这类 online softmax 保持数值稳定,使得 attention 可以按块流式计算;FlashAttention 等高效实现同样依赖分块计算、不物化巨大注意力矩阵的思想。

假设前一块 softmax 的分母是 \(l_1=2\),累计加权和是 \(u_1\);后一块的最大值更大,旧分数在全局 softmax 中必须整体缩小。online softmax 记录当前全局最大值 \(m\),遇到新块时先算 \(e^{m_{old}-m_{new}}\),用它缩放旧的 \(l\)\(u\),再累加新块。这样不需要先看到全部分数,也能得到和完整 softmax 相同的数学结果,同时避免大量 \(e^x\) 上溢或下溢。

思考题

online softmax 为什么不能把每块 softmax 的结果直接相加?

答案

全局 softmax 的分母依赖所有块的指数和。若新块的最大值更大,旧块分数的指数都必须按新的最大值重新缩放。online softmax 维护当前最大值、累计分母和累计加权和,遇到新块时先校正旧累计量再累加。

思考题

ring attention 已经把序列切到多张卡,为什么 attention 的总通信量通常不会消失?

答案

每个 Q 块最终仍要看到全局 K/V。若每个 rank 只保存一段 K/V,就必须通过环形传递让所有块相遇。显存峰值下降,因为不必同时在一张卡上保存完整序列,但通信轮数和传输量随分片数增加。它改变的是数据的放置和流动,不改变注意力需要全局交互的性质。

MoE 与 Expert Parallel

Mixture of Experts15(MoE)层用 router 为每个 token 选择少数几个 expert,而不是让所有 token 经过同一个 FFN。总参数可以非常大,单个 token 的计算量却只与被选中的少数专家有关。这是 MoE 能扩大模型容量的原因。

代价落在路由和网络上:某张卡上的 token 可能需要送去别张卡拥有的 expert,计算后结果还要按原 token 顺序送回,通常形成 All-to-All。router 若总偏向少数专家,就会出现有的专家/卡很忙、有的很闲;因此需要容量限制和负载均衡损失。MoE 的瓶颈不一定是矩阵乘法,可能是 token 重排、all-to-all、专家不均衡或跨节点拓扑。

一个 token 在 MoE 层里的路径可以这样读。设每层有 64 个 expert,每个 token 选 top-2。输入仍是 [B,S,H],router 对每行输出 64 个分数,softmax 后取最大两个,例如 expert 7 权重 0.6、expert 42 权重 0.4。系统把所有 token 按 expert 分组,expert 7 收到自己的 token 矩阵后执行 FFN,expert 42 同样执行;最后每个 token 的输出是 \(0.6E_7(x)+0.4E_{42}(x)\),再放回原序列位置。参数规模来自 64 个 expert,单个 token 计算量却只对应 2 个 expert。

思考题

MoE 的路由为什么必须考虑负载均衡?token 全都送往少数 expert 会发生什么?

答案

若路由把大部分 token 集中到少数 expert,这些 expert 所在 GPU 会成为瓶颈,其他计算资源闲置,还可能触发容量上限丢 token。负载均衡损失、容量因子和路由统计用于控制这种偏斜。MoE 的收益来自条件计算,前提是路由能同时兼顾质量和系统负载。


  1. 一批同时处理的样本或请求。 

  2. ZeRO 与 Fully Sharded Data Parallel。把参数、梯度或优化器状态分片保存,减少每卡常驻显存。 

  3. 提示词。用户输入给语言模型的文本。 

  4. 分词器切出的最小处理单位,可能是词片、字、标点或字节片段。 

  5. autoregressive,自回归。用已有输出作为后续输入,逐步生成下一个结果。 

  6. 多维数组。一维 tensor 是向量,二维 tensor 是矩阵,\([B,S,H]\) 是三维 tensor。 

  7. 预填充阶段。一次处理已有 prompt 的全部 token,并建立 KV cache。 

  8. 推理时按层保存已经算过的 key 和 value 向量,避免生成下一个 token 时重复计算历史。 

  9. 解码阶段。每步只输入新 token,利用 KV cache 预测下一个 token。 

  10. 激活。前向传播中为反向求导保存的中间张量。 

  11. tensor parallel,张量并行。把单个大矩阵或注意力投影切到多张卡上共同计算。 

  12. 集合通信操作。所有参与方共同得到同一份求和或归约结果。 

  13. data parallel,数据并行。不同设备处理不同数据副本,随后同步梯度。 

  14. pipeline parallel,流水线并行。把模型按层切成多个 stage,依次传递 activation。 

  15. Mixture of Experts,混合专家模型。router 为每个 token 选择少量 expert 分支计算。 

有用的话请给我个 star => Stars 本站总浏览