想象你走进音乐厅,一支大型合唱团正在排练《欢乐颂》。作曲家把总谱写成厚厚一叠——这张总谱记录了所有声部的旋律,就像机器学习中的权重矩阵 W,每一行都是一条旋律线。为了让女高音、女中音、男高音、男低音四个声部同时开唱,指挥拿起剪刀,把总谱按声部拆成四份,每份只含几行旋律。歌词也顺势分成四句,每个声部各唱一句。当所有人同时开口,四个声部的声音在音乐厅里自然叠加,观众的耳朵里便收到了完整的和声。 这就是“行并行”的智慧:把一个巨大的矩阵乘法拆成多个小矩阵乘法,让多块 GPU 分头计算,最后用一次 All-Reduce 把结果汇聚起来。行并行是张量并行的两大基石之一,与列并行像一对默契搭档,共同撑起了 Megatron-LM 等框架中 Transformer 模型的并行训练。本文用合唱团的比喻,从切分原理、前向传播、反向传播、组合设计到工程实现,把行并行讲得透透的。
掌握行并行的切分逻辑与通信模式,是理解大模型张量并行组合设计的钥匙。无论你做训练加速、推理优化还是性能分析,这套知识都能帮你准确判断多卡通信瓶颈,是分布式训练工程师的必备素养。
可视化演示
动态演示行并行Row Paralle的核心机制,通过总览/传输/转换/输出四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
行并行Row Paralle:行并行切 行并行切分方式与输入分片要求 前向All-Reduce求和与 核心机制可视化 行并行Row Paralle整体架构与关键组件
1. 行切分原理
想象你走进音乐厅,看到一支大型合唱团正在排练《欢乐颂》。作曲家把总谱写成厚厚一叠——这就是权重矩阵 W,每一行都是一条旋律线。为了让女高音、女中音、男高音、男低音四个声部同时开唱,指挥拿起剪刀,把总谱按声部拆成四份,每份只含几行旋律。这个动作,就是行并行的核心直觉:把权重矩阵按行切开。
不过,光切歌谱还不够。每位歌手要唱,手里还得有对应的歌词;歌词来自输入 X。既然歌谱按行切了,歌词也得跟着按列切成四句,每个声部拿一句。于是输入矩阵 X 被切成 X₁、X₂、……、Xₚ,连同权重分片一起分给各台 GPU。
用数学语言说:设权重 W 的形状是 (k, n),输入 X 的形状是 (b, k)。行并行把 W 沿行方向切成 p 块,每块 Wᵢ 的形状是 (k/p, n);同时把 X 沿列方向切成 p 块,每块 Xᵢ 的形状是 (b, k/p)。设备 i 拿着 (Xᵢ, Wᵢ) 这对数据,在本地做矩阵乘法。虽然每台设备手里的“谱子”变小了,但 p 块拼在一起,参数总量和切分前分毫不差。
每个声部歌手手里只有几行谱子和一句歌词,但他们知道自己唱的是整首歌的一部分。这些“部分”如何变成完整的和声?答案就在下一节的前向传播。
- 权重按行切块把 (k, n) 的权重矩阵沿行方向均分成 p 块,每个 Wᵢ 是一个 (k/p, n) 的子矩阵。
- 输入按列切块把 (b, k) 的输入矩阵沿列方向均分成 p 块,每个 Xᵢ 是一个 (b, k/p) 的子矩阵。
- 配对分发设备 i 拿到自己的 (Xᵢ, Wᵢ) 后,在本地算出部分结果 Yᵢ。
2. 前向传播流程
演出时刻到了。指挥的手一抬,四个声部同时开口——女高音唱出明亮的高音区旋律,男低音托起沉稳的低音线。单独听任何一声部,都只是碎片;但当所有声音在大厅中叠加,观众听到的是一首完整的《欢乐颂》。这个“声音叠加”的过程,对应行并行的 All-Reduce 汇聚。
技术上的 All-Reduce 不是在空气中传播声音,而是把各设备算出的局部结果 Yᵢ 做逐元素相加。为什么是加法而不是拼接?因为矩阵分块乘法告诉我们:输入按列切、权重按行切后,整体乘积等于各局部乘积之和。每一个局部乘积只是“部分和”,所有部分和加起来才是正确答案。
完整的前向流程分三步:第一步,每台设备用本地数据算 Yᵢ = XᵢWᵢ;第二步,执行一次 All-Reduce(归约操作为加法),把 p 个 Yᵢ 加起来;第三步,每台设备都拿到完整的输出 Y,交给下一层。注意,这里每台设备都持有完整 Y,而知道 Y 的梯度 dY 也是完整共享的——这为反向传播“零通信”埋下了伏笔。
合唱的魅力正在于此:每个声部只发出 1/p 的声音,观众听到的却是完整旋律;每台设备只算了 1/p 的活,下一层拿到的 Y 与串行计算结果完全一致。声音叠加是瞬时的,但 All-Reduce 要花通信时间——这个代价留到第五节细算。
- 局部乘积设备 i 计算 Xᵢ 与 Wᵢ 的乘积,结果的形状是 (b, n)。
- All-Reduce 求和所有局部结果经过一次求和归约,累加为完整输出。
- 与整体等价由分块矩阵乘法性质,部分和之和等于整体乘积。
3. 反向传播特性
演出结束,指挥没有让大家集合复盘,而是说:“今晚表现不错,各声部自己回去练自己的部分就好了。”为什么可以各练各的?因为每个声部的问题只和自己的谱子与歌词有关。行并行的反向传播正是这样:不需要任何跨卡通信。
数学上,损失 L 对输出 Y 的梯度 dY 由上层反向传来,对每台设备来说它都是已知的。由于 Y = ΣYᵢ,而求和运算的梯度就是“广播”——∂L/∂Yᵢ = ∂L/∂Y = dY,所以每台设备手里的 dY 可以直接用于本地计算,不需要等任何人。
于是两个梯度都在本地完成:权重梯度 dWᵢ = Xᵢᵀ · dY,只需要自己的输入分片和全局输出的梯度;输入梯度 dXᵢ = dY · Wᵢᵀ,只需要全局输出梯度和自己的权重分片。其中 dXᵢ 恰好就是完整输入梯度 dX 的第 i 列块,天然按位置存放,不需要 All-Gather 拼接。这与列并行恰好对称:列并行是“前向 All-Gather、反向 All-Reduce”,行并行是“前向 All-Reduce、反向零通信”。
每个声部回家自己练声,互不打扰。这份“静音”的优雅,在与列并行组合时体现得淋漓尽致——列并行负责反向的 All-Reduce 通信,行并行负责前向的 All-Reduce 通信,两队声部交错发声,谁都不抢谁的麦。
- 输出梯度可复用因为输出是局部结果之和,所以每个局部输出对损失的偏导都等于 dY。
- 权重梯度本地算输入分片的转置乘输出梯度,得到第 i 块权重梯度。
- 输入梯度本地算输出梯度乘权重分片的转置,得到完整输入梯度的第 i 列块。
4. 与列并行组合
合唱团里通常分“高声部组”和“低声部组”,两组既要独立训练,又要默契配合。列并行与行并行正是这样一对黄金搭档:一个按“列”切矩阵,一个按“行”切矩阵,在 Transformer 的 MLP 层中配合得天衣无缝。
Megatron-LM 的 MLP 包含两个线性层,中间夹一个激活函数。设计者的安排是:第一层用列并行(权重 W₁ 按列切),第二层用行并行(权重 W₂ 按行切)。为什么这么配?关键在输出与输入的“咬合”——列并行算出的激活输出在特征维度上天然是切分的,而行并行恰好要求输入按列切分。两者一拼,中间根本不需要把激活值拼接完整再分发。
具体流程是:设备 i 持有 W₁ 的第 i 个列块 W₁,ᵢ 和 W₂ 的第 i 个行块 W₂,ᵢ。前向时,设备 i 先算出 Uᵢ = XW₁,ᵢ(列并行局部输出,不需要通信),接着就地过激活函数得到 Yᵢ = σ(Uᵢ),然后直接做行并行的局部乘法 Zᵢ = YᵢW₂,ᵢ,最后 All-Reduce 得到完整 Z。全程 Y 从未被拼成完整矩阵,省掉了一次 All-Gather 加一次切分的通信开销。
高声部组把旋律劈开分给组员(列并行),低声部组在同一时间接力竖切和声(行并行),两组之间不需要停下来把声音合一遍再重新分工。这种“中间状态零拼接”的配合,正是大模型训练中省通信的精妙设计。
- 列并行的局部输出每台设备用完整 X 乘 W₁ 的第 i 个列块,得到未经激活的部分特征。
- 激活就地作用GELU / ReLU 是逐元素操作,可以放心作用于每个分片。
- 行并行接力求和Yᵢ 正好是行并行所需的输入分片,乘 W₂ 的行块后求和。
5. 通信开销分析
和声虽美,但把几十个声部的声音汇聚起来是有代价的——团员们得移动位置、对齐节奏。在多卡训练中,通信比计算更昂贵。所以我们需要精确地知道:一次 All-Reduce 到底搬了多少数据?
行并行前向只有一次 All-Reduce,它的通信量由输出矩阵 Y 的大小决定。设批大小为 B,输出特征维度为 N,每台设备的局部结果 Yᵢ 就是一个 B×N 的矩阵。All-Reduce 要在 p 台设备之间把这个矩阵的所有局部版本归约成一份。
一次 All-Reduce 的通信量怎么算?理想化的环形归约中,每台设备总共发送和接收各约 (p-1)/p 份数据。因此单次通信的流量(以元素个数计)约为 2BN(p-1)/p。这个数字与模型参数量无关,只和输出形状及设备数挂钩——当 N 很大时,通信占比会明显上升,这也是为什么张量并行一般只在单机多卡(NVLink 高速互联)内使用。
合唱团人越多,每个团员需要和其他人对齐的比例反而越高((p-1)/p 趋向 1)。但妙的是,列并行把类似的开销放在了反向,行并行把它放在前向——两个声部组交错发声,通信高峰被削平,整体训练更顺畅。
- 局部输出大小每台设备的 Yᵢ 有 B 行 N 列,共 B×N 个元素。
- 归约交换比例p 台设备做全归约时,每台实际需要交换 (p-1)/p 份数据。
- 收发双向计数发送和接收各算一次流量,所以乘 2。
6. 工程实现要点
成熟的合唱团靠纪律保证演出质量:谱子怎么分、谁领唱、座位怎么排,都有讲究。工程上落地行并行同样有几条“排练纪律”,稍不留神就会跑调。
首先是权重初始化。由于每台设备只持有一块 Wᵢ,初始化随机种子必须按分片对齐,使得所有分片拼接后整体分布与不切分的权重一致。否则可能出现某些设备初始化过大,模型训练直接发散。出于同样的道理,优化器状态(如 Adam 的一阶/二阶动量)也应随分片存放在各设备上,显存自然均摊。
其次是偏置处理。行并行的输出是求和得到的,偏置 b 的形状与输出维度一致,不能按设备切分。如果每台设备都把 b 加在各自的局部结果上,再 All-Reduce,最终偏置会被放大 p 倍。正确做法是:All-Reduce 之后再统一加一次 b;或让某台设备加 b,借 All-Reduce 自然广播。还有一点是显存负载均衡:输入按列切分后,各设备存储的激活量与特征分片成正比,与列并行配对的层必须保证特征维度对齐,避免某块 GPU 变成“显存热点”。
分谱清晰(初始化对齐)、领唱明确(偏置只加一次)、座位合理(负载均衡)——把这些纪律执行到位,行并行才能真正发挥“人多力量大”的优势。Megatron-LM、TensorRT-LLM 等框架正是靠这些细节,让千万亿参数的模型训练成为可能。
- 错误的局部加偏置若每台设备都加 b 再归约,偏置会被累加 p 次。
- 正确的全局加偏置先归约局部结果,再加一次偏置。
- 分片权重等价性切分后再拼接,应当与原始权重的分布一致。
AI 应用场景
场景1:Megatron-LM 大规模语言模型训练
阶段:分布式训练优化
解决的问题:GPT-3 这类千亿参数模型,单张 GPU 显存完全装不下 MLP 层权重。传统做法要么把整个权重放一个设备计算,要么用数据并行反复复制模型——前者放不下,后者通信爆炸。行并行配合列并行,把每个 Transformer 块中的 MLP 权重拆分到多卡上,使模型规模突破单卡显存上限,同时把通信开销控制到最小。
具体案例:训练 175B 参数的 GPT-3 时,Megatron-LM 对每个 Transformer 的 MLP 层应用“列并行 → 激活 → 行并行”的组合:第一层 W₁ 按列切分到 8 张 GPU,激活函数在分片上就地计算,第二层 W₂ 按行切分接力,最后 All-Reduce 汇聚。8 卡协同工作,仿佛 8 个声部同唱一首歌,训练吞吐量大幅提升。
场景2:大模型推理引擎的多卡张量并行
阶段:模型推理部署
解决的问题:推理时模型权重需加载到 GPU 显存,单卡放不下百亿级模型;同时推理延迟要求高,不能频繁跨卡搬运权重。行并行把权重行分片分布到多卡,配合列并行共同服务一次前向推理,在保持低延迟的同时扩展显存容量。
具体案例:vLLM、TensorRT-LLM 等推理引擎在加载 LLaMA-70B 时,将每个线性层的权重同时按“列切”和“行切”分配到 4 张 A100 上。每张卡只算自己那几分之一的矩阵乘法,最后 All-Reduce 合并输出。用户无感知地获得接近单卡的速度,模型可用的显存却翻了 4 倍。
Python 演示
import numpy as np def row_parallel_forward(X, W, p=2): """Simulate row-parallel forward with p devices.""" batch, in_f, out_f = X.shape[0], W.shape[0], W.shape[1] chunk = in_f // p # 1. split weight by rows, split input by columns W_chunks = [W[i * chunk:(i + 1) * chunk, :] for i in range(p)] X_chunks = [X[:, i * chunk:(i + 1) * chunk] for i in range(p)] # 2. each device computes local product local_out = [X_chunks[i] @ W_chunks[i] for i in range(p)] # 3. All-Reduce: sum all local outputs Y_parallel = local_out[0] for i in range(1, p): Y_parallel = Y_parallel + local_out[i] return Y_parallel def row_parallel_backward(dY, X, W, p=2): """Simulate row-parallel backward: zero cross-device communication.""" batch, in_f, out_f = X.shape[0], W.shape[0], W.shape[1] chunk = in_f // p W_chunks = [W[i * chunk:(i + 1) * chunk, :] for i in range(p)] X_chunks = [X[:, i * chunk:(i + 1) * chunk] for i in range(p)] # local gradients, no communication needed dW_chunks = [X_chunks[i].T @ dY for i in range(p)] dX_chunks = [dY @ W_chunks[i].T for i in range(p)] return dW_chunks, dX_chunks if __name__ == "__main__": rng = np.random.default_rng(42) batch, in_f, out_f = 8, 6, 4 X = rng.normal(size=(batch, in_f)) W = rng.normal(size=(in_f, out_f)) dY = rng.normal(size=(batch, out_f)) # forward: compare with reference Y_ref = X @ W Y_row = row_parallel_forward(X, W, p=2) print("Forward match:", np.allclose(Y_ref, Y_row)) # backward: compare gradients dW_ref, dX_ref = X.T @ dY, dY @ W.T dW_parts, dX_parts = row_parallel_backward(dY, X, W, p=2) dW_cat = np.concatenate(dW_parts, axis=0) # stack row chunks dX_cat = np.concatenate(dX_parts, axis=1) # stack column chunks print("dW match:", np.allclose(dW_ref, dW_cat)) print("dX match:", np.allclose(dX_ref, dX_cat))
W_chunks = [W[i * chunk:(i + 1) * chunk, :] for i in range(p)]把权重矩阵按行切成 p 块,每台设备拿一块。X_chunks = [X[:, i * chunk:(i + 1) * chunk] for i in range(p)]把输入按列切成 p 块,与行块配对。local_out = [X_chunks[i] @ W_chunks[i] for i in range(p)]每台设备只做自己的局部矩阵乘法。Y_parallel = Y_parallel + local_out[i]模拟 All-Reduce 的求和归约过程。dW_chunks = [X_chunks[i].T @ dY for i in range(p)]权重梯度只用本地输入分片计算,不跨卡。dX_chunks = [dY @ W_chunks[i].T for i in range(p)]输入梯度也在本地算,天然按列分片存放。np.allclose(Y_ref, Y_row)验证并行结果与串行参考结果完全一致。
常见误区
误区我以为行并行就是把输出矩阵按行切开,每个 GPU 负责输出的一部分行,这样输出维度就被分到不同卡上了。
正解行并行切的是权重矩阵的行,也就是输入特征维度 K。权重被切成 p 个行块后,输出维度 N 保持完整,每台设备算出的 Yᵢ 形状是完整的 (batch, N),最后通过 All-Reduce 相加。真正把输出维度切开的是列并行(按列切权重)。
为何常见:矩阵乘法的行和列都有“切分”操作,初学者很容易把权重行切分和输出行切分混为一谈;加上行并行名字里的“行”字具有误导性。
误区我认为既然前向要做一次 All-Reduce 汇聚结果,反向传播时自然也要再做一次 All-Reduce 把梯度汇总回来。
正解行并行的 All-Reduce 只出现在前向。反向传播时,由于输出梯度 dY 是全局共享的,每个设备用本地的 Xᵢ 和 Wᵢ 就能算出 dWᵢ 和 dXᵢ,不需要任何跨卡通信。真正的“反向 All-Reduce”发生在列并行中。
为何常见:对称联想导致——列并行是前向 All-Gather、反向 All-Reduce,人们想当然地认为行并行也应该两边都有通信。
误区我觉得每台 GPU 都应该看到完整的输入 X,这样才能独立算出自己的部分结果,行并行应该只切权重不切输入。
正解行切分权重后,输入 X 必须按列切成 p 块,与行块一一配对。因为 XW 的乘法中,X 的列维度必须与 W 的行维度匹配。若给每台设备完整 X,X 与 Wᵢ 的形状根本对不上(列数 k ≠ k/p)。
为何常见:列并行是“输入完整、输出切分”,容易让人误会行并行也类似;忽略了行并行要求输入切分才能相乘。
误区我以为 All-Reduce 就是把各 GPU 算出的部分结果拼在一起,像拼图一样组成完整输出,名字里既然有 All 就是大家把数据合起来。
正解All-Reduce 是“归约后广播”:先把各设备的数据做逐元素相加(或其他归约操作如 max),再把结果发给所有设备。行并行用的是求和归约。拼接操作叫 All-Gather。虽然列并行前向用到 All-Gather,但行并行用的是 All-Reduce——两者都是“合”,方式完全不同。
为何常见:英文术语 All-Reduce 和 All-Gather 都带 All 前缀,中文翻译都接近“聚合”,初学者极易混淆。
误区既然权重都切成块了,偏置 b 也应该跟着切成几块,每台设备存一份,反正都是“模型参数的一部分”。
正解偏置 b 的形状与输出维度 N 一致,而行并行中输出 N 没有被切分。如果每台设备都把 b 加到局部结果再归约,最终偏置会被放大 p 倍。正确做法是在 All-Reduce 之后统一加一次 b,或者由某一台设备加上后靠归约广播到所有设备。
为何常见:参数切分的直觉被过度推广——忽略了偏置与输出维度的绑定关系,以及归约求和会累加的特性。
面试题
Q1. 行并行中权重矩阵 W 按什么方向切分?
- A.按行切分← 正确答案
- B.按列切分
- C.按块切分
- D.不切分
答案:A。行并行的名字已经揭示了切法:把权重矩阵沿行的方向切成 p 个水平条带,每台设备分到一个条带。这就像合唱团把总谱按声部行撕开,每个声部只拿自己那几行。列并行才是按列切分,别搞混。
Q2. 行并行中 All-Reduce 的主要作用是什么?
- A.拼接各设备的输出
- B.对各设备局部结果求和← 正确答案
- C.复制完整输出到各卡
- D.切分输出到各设备
答案:B。行并行前向最后要求所有局部结果相加,All-Reduce 做的就是“求和后广播”。它不是拼接(那是 All-Gather),也不是简单的复制或分发。和声的“叠加”正是 All-Reduce 的灵魂。
Q3. 行并行中局部输出 Yᵢ 的形状正确的是?
- A.批长乘输出维度 [b, n]← 正确答案
- B.批长乘输入分片 [b, k/p]
- C.输入分片乘输出 [k/p, n]
- D.减半批长乘输出 [b/p, n]
答案:A。每台设备虽然只算了一个局部乘积,但局部结果的形状与完整输出一致,都是 (batch, N)。因为输出维度没有被切分,切掉的是输入特征维度。就像每个声部唱出来的旋律片段,时长和音域都是完整的,只是缺少其他声部的和声。
Q4. 行并行中每台设备需要哪些数据分片?
- A.权重矩阵的部分行← 正确答案
- B.输入矩阵的部分列← 正确答案
- C.完整的权重矩阵
- D.完整的输入矩阵
答案:AB。行并行中每台设备需要权重矩阵的部分行(Wᵢ)和输入矩阵的部分列(Xᵢ),两者配对才能做矩阵乘法。完整的权重和完整输入是列并行的需求,行并行不需要。
Q5. 行并行的反向传播需要通信吗?
- A.需要,前反向都要通信
- B.不需要,反向零通信← 正确答案
- C.反向需要 All-Gather
- D.与权重大小相关
答案:B。行并行的反向传播是零通信:梯度 dY 全局共享,权重梯度 dWᵢ 用本地 Xᵢ 算,输入梯度 dXᵢ 用本地 Wᵢ 算。这像合唱结束后各声部自己回家练声,不需要凑在一起开会。
Q6. 行并行中输入矩阵 X 如何切分?
- A.按列切分为 p 块← 正确答案
- B.按行切分为 p 块
- C.不切分直接广播
- D.按块切分为 p² 块
答案:A。输入 X 必须按列切成 p 块,这是因为 X 的列数必须与 W 的行数匹配。权重按行切了多少块,输入就按列切多少块,一一对应。把输入广播给所有设备是列并行的做法。
Q7. 行并行中 dXᵢ(输入梯度的分片)如何计算?
- A.由 dY 与 Wᵢ 转置的本地积← 正确答案
- B.从其他设备广播获取
- C.All-Gather 拼接得到
- D.先 Reduce 再 Scatter
答案:A。dXᵢ 的计算只需要本设备的权重分片 Wᵢ 和全局输出梯度 dY,即 dY @ Wᵢᵀ。它是完整输入梯度 dX 的第 i 列块,按位置存放即可。不需要广播,也不需要拼接。
Q8. 关于行并行的描述,正确的是?
- A.前向需要 All-Reduce 求和← 正确答案
- B.反向不需要跨卡通信← 正确答案
- C.权重梯度可以本地计算← 正确答案
- D.输入必须全量广播到各卡
答案:ABC。行并行的三个特征:前向 All-Reduce 求和、反向零通信、权重梯度本地计算。D 错得离谱——输入需要的是按列切分而非全量广播。这是行并行与列并行最核心的差异。
Q9. MLP 中第一层用列并行、第二层用行并行的核心原因是?
- A.列并行计算速度更快
- B.免去层间特征拼接通信← 正确答案
- C.行并行无法配合激活函数
- D.为了减少整体参数量
答案:B。MLP 第一层列并行输出的是特征维度的分片,第二层行并行输入的恰好是特征维度的分片,两者咬合后中间激活值无需拼接和再切分,省掉一次 All-Gather 加一次切分的通信。不是为了快,也不是为了砍参数。
Q10. 若偏置 b 在每台设备局部相加后再 All-Reduce,最终输出会怎样?
- A.偏置被放大 p 倍← 正确答案
- B.偏置被缩小 p 倍
- C.没有任何影响
- D.All-Reduce 自动修正偏置
答案:A。偏置 b 与输出维度绑定,行并行中输出维度没被切分,所以 b 不能分片。若每台设备局部加 b 再归约,偏置会被求和 p 次,放大 p 倍。正确做法是归约后再加一次 b,就像合唱的领唱只能有一个,不能四个声部各唱一遍主旋律。