想象一家繁忙的大排档:大厨按一本厚厚的配方同时招待满堂客人,一个人实在忙不过来。于是他把配方竖着撕成几份,分别交给几位帮厨。每位帮厨面前都放着完整的一筐食材,只按自己手里那几列配方操作;炒完后,把各自的半成品拼成一整桌菜上桌。客人反馈味道时,几位帮厨又要凑在一起互通点评,才能确定下一步怎么调整。这个“竖着撕配方、人手一份食材、最后拼盘、反向再互通”的过程,就是列并行。它是大模型训练里张量并行的基础操作:把一个矩阵按列切给多张卡,多卡各算各的,部分场景甚至不需要互相说话。
理解列并行,就能看懂 Megatron 等分布式训练框架中最核心的“切分与通信”逻辑,是面试和工程实战中绕不开的地基。
目录
可视化演示
展示FIFO与LRU页面置换算法在有限物理帧中的执行过程,可视化缺页中断、页面命中和替换策略的差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
LRU页面置换算法 物理帧容量为3,引用串长度为20 绿色表示页面命中,无需替换 红色表示缺页中断,需加载或替换页面 LRU替换最近最久未使用的页面 底部显示最近的命中和缺页历史
列并行概念:竖着撕配方
大排档里,大厨不再独自处理整本配方,而是把配方竖着撕开:帮厨 A 拿左边一列,帮厨 B 拿右边一列。所有帮厨面前仍摆着同一筐完整食材。也就是说,输入数据不切分,权重矩阵按列切分。每个帮厨只按自己手里的配方做一部分菜,最后把半成品拼在一起,就是一整桌菜。
在大模型训练中,一个线性层可以写成矩阵乘法 $Y = XW$。列并行把权重矩阵 $W$ 沿输出维度切成 $p$ 块,分别放到 $p$ 张卡上。第 $j$ 张卡拥有 $W_j$,并计算 $Y_j = XW_j$。因为 $X$ 对所有人都是完整的,所以每张卡都能独立完成自己的矩阵乘法,不需要在计算过程中找别人商量。
所以列并行最直观的口诀是“权重分列、输入全量、输出拼接”。就像几位帮厨各自做出半成品,拼成一整道菜;客人看到的是完整的一桌菜,不会关心是哪位帮厨做的。
- 切分权重把权重矩阵按列切成 $p$ 块,每块形状相同。
- 输入不切输入矩阵 $X$ 不切分,直接作为公共因子参与每一块乘法。
- 拼接输出把 $p$ 个输出分片按列拼接,恢复完整输出。
分块矩阵乘法:为什么可以这样切
有人会问:把配方竖着撕,难道不会把一道菜做坏吗?不会,因为矩阵乘法天生就允许“一列一列地算”。每个输出列只由输入矩阵和权重矩阵的对应列决定,和其他列没有关系。这就像配方的“酱油用量”只影响酱油这一列的效果,帮厨 A 只需要把酱油相关的那几列做好。
从数学上看,矩阵乘法 $Y = XW$ 中,输出矩阵的第 $j$ 列,是输入矩阵每一列按权重第 $j$ 列加权求和的结果。也就是说,每一列输出是独立计算的。因此,把权重矩阵按列切成若干块后,每块对应的输出列也是相互独立的,最后拼起来不会有任何信息丢失。
这正好解释了为什么列并行是“零风险的切法”:数学上本来就可以分块。帮厨们不需要讨论各自配方之间怎么协调,只要最后把成品按顺序摆好即可。
- 约定维度设输入 $X$ 形状为 $B \times d_{\text{in}}$,权重 $W$ 形状为 $d_{\text{in}} \times d_{\text{out}}$。
- 按列分块把 $W$ 横向切成左右两块,每块列数各为 $d_{\text{out}}/2$。
- 分配律展开矩阵乘法满足分配律,于是可以先分别乘再拼接。
前向计算流程:备料、翻炒、拼盘
列并行的前向过程,就像大排档的备菜流程。第一步是“备料”:把完整食材 $X$ 复制一份放到每口锅前。第二步是“翻炒”:每位帮厨只用自己的配方 $W_j$ 处理这些食材,得到自己的半成品 $Y_j$。第三步是“拼盘”:把所有半成品按顺序拼在一起,得到完整输出 $Y$。
整个过程里,设备之间不需要通信。因为每个设备都拿到了完整的 $X$,并且只负责输出维度上的一段;它既不需要等待别人的结果,也不需要把自己的结果发给别人。唯一要做的,只是最后在逻辑上把输出分片拼接起来。在工程实现里,这种拼接有时候是显式的 concat,有时候是让下一层直接消费分片,省去实际拼接。
前向不通信,是列并行最舒服的地方。帮厨们埋头炒菜,互不打扰;只有在上菜前才需要把拼盘摆好。
- 广播输入把输入矩阵 $X$ 完整发送给所有设备,每个设备都有一份。
- 本地乘算设备 $j$ 用自己保存的权重列块 $W_j$ 做矩阵乘法。
- 拼接输出将各设备的 $Y_j$ 沿列方向拼接成完整 $Y$。
反向传播特点:菜品评价的互通
菜上桌后,客人给出反馈:哪里咸了、哪里淡了。这个反馈就是损失对输出的梯度。因为输出是拼接出来的,每位帮厨只需要拿到自己那块菜对应的评价,这不需要通信。但他还需要知道“整桌菜”的反馈对自己手里的食材 $X$ 有多大责任,这就必须把各人的理解汇总起来。
在数学上,反向传播要求计算 $\partial L / \partial X$。由于 $Y = [Y_1, \dots, Y_p]$,输入梯度是所有输出分片对输入贡献的总和。每个设备只能算出自己那一份局部输入梯度,必须通过 All-Reduce 把大家的结果求和,再广播给每个设备,这样每个设备手里的 $X$ 副本都能得到完整梯度。
这就是列并行“反向 All-Reduce”的由来。帮厨们不能只埋头炒菜,还得在客人反馈后凑在一起对一下账,否则每个人都不知道食材 $X$ 到底该往哪个方向调整。
- 切分输出梯度把输出梯度按列切给各设备,每个设备只拿自己输出分片对应的梯度。
- 计算局部输入梯度每个设备用本地权重转置,算出自己对输入梯度的贡献。
- All-Reduce 汇总把 $p$ 份局部输入梯度求和,并广播给所有设备。
- 权重梯度本地算每个设备的权重梯度只依赖本地输入和输出梯度,无需通信。
行并行对比:换一个方向切
列并行是“竖着撕配方”,行并行则是“横着分食材”:把完整的一筐食材分成两份,帮厨 A 拿上半筐,帮厨 B 拿下半筐,但两人手里都有完整的配方。最后两人炒出来的菜必须倒进同一口锅里拌匀,因为每个人只做了半道菜。这个“拌匀”的过程,就是前向计算里的 All-Reduce。
从公式看,行并行把输入 $X$ 按特征维切成 $X_1, X_2$,把权重 $W$ 按行切成上下两块,然后做两个局部矩阵乘,再把结果相加:
对比两种切法,就像帮厨分工的两个极端:一个按配方列分工,最后拼盘;一个按食材量分工,最后混炒。理解了这两个方向,就能明白为什么现代框架要在不同位置选择不同切法。
- 切分输入把输入矩阵按特征维横切成两块。
- 上下切分权重把权重矩阵按行切成上下两块。
- 前向求和两个局部乘积需要通过 All-Reduce 相加,才能得到完整输出。
Transformer组合:厨房流水线
实际的 Transformer 层里,MLP 通常有两个线性层:第一个把维度扩大,第二个把维度缩回去。如果两个线性层都用列并行,中间就需要一次拼接,第二个线性层的前向又要通信,整体通信次数多。更聪明的做法是让第一个线性层用列并行,第二个线性层用行并行,拼出一条流水线。
设第一个线性层的权重 $W_1$ 按列切成 $W_1^{(j)}$,第二个线性层的权重 $W_2$ 按行切成 $W_2^{(j)}$。设备 $j$ 上先算出隐藏分片 $H_j$,这个 $H_j$ 恰好就放在设备 $j$ 上,正好是第二个线性层行并行需要的输入分片。于是两个线性层之间不需要任何通信,只在最后输出时做一次 All-Reduce 求和。
这种“列并行 + 行并行”的组合,就像大排档的流水线:前半段各帮厨按配方列备料,后半段把各人手里的半成品直接倒进同一口锅,只在一开始和最后各通一次气。Transformer 中的注意力 QKV 投影也常用列并行,输出投影用行并行,原理完全一样。
- 列并行投影第一个线性层用列并行,每个设备算出隐藏分片。
- 行并行消费第二个线性层用行并行,直接消费本地已有的 $H_j$。
- 最终归约把各设备的部分输出相加,得到完整输出。
AI 应用场景
大规模语言模型训练:Megatron 风格分片
阶段:模型训练 / 分布式训练
解决的问题:单个 GPU 放不下大模型的权重和激活,传统单卡训练直接 OOM。列并行把线性层权重按列拆到多卡,每卡只存一部分权重,显存压力大幅下降;配合行并行组成 MLP 后,通信次数也减少了一半。
具体案例:训练 7B 参数模型时,使用 8 张 GPU,把 MLP 的第一个 Linear 按列切成 8 份,第二个 Linear 按行切成 8 份。每张 GPU 只需保存 1/8 的 MLP 参数,前向时中间隐藏分片无需跨卡拼接,仅在最终输出做一次 All-Reduce。
大模型推理部署:多卡并行加速
阶段:模型推理 / 服务部署
解决的问题:推理时单卡显存不足以加载权重,或者 batch 开不大。列并行可以把权重分散到多卡,让每卡负责一部分输出维度,降低单卡显存占用,从而支持更大的并发请求。
具体案例:在 2 张 A100 上部署 13B 模型,将注意力层的 QKV 投影用列并行切分,输出投影用行并行切分。每张卡只处理一半的投影计算,推理吞吐量提升接近线性,同时单卡峰值显存明显下降。
Python 演示
import numpy as np def all_reduce(tensor, devices): total = sum(devices) return [total for _ in devices] class ColumnParallelLayer: def __init__(self, in_features, out_features, world_size=2): assert out_features % world_size == 0 self.in_features = in_features self.out_features = out_features self.world_size = world_size self.per_device = out_features // world_size rng = np.random.default_rng(0) self.W = rng.normal(size=(in_features, out_features)).astype(np.float32) # 本地权重分片列表 每块是完整W的一段列 self.local_W = [ self.W[:, j * self.per_device:(j + 1) * self.per_device] for j in range(world_size) ] def forward(self, x): self.x = x # 每个设备只计算自己那段输出列 self.local_y = [x @ w for w in self.local_W] # 前向把列方向拼回完整输出 return np.concatenate(self.local_y, axis=1) def backward(self, grad_output): grad_y_list = np.split(grad_output, self.world_size, axis=1) # 权重梯度是本地计算 不需要通信 self.local_grad_W = [ self.x.T @ gy for gy in grad_y_list ] # 输入梯度分片 每台设备算一部分 grad_x_list = [ gy @ w.T for gy, w in zip(grad_y_list, self.local_W) ] # 输入梯度需要 All-Reduce 求和并广播 grad_x_total = sum(grad_x_list) grad_x = all_reduce(grad_x_total, grad_x_list)[0] return grad_x if __name__ == "__main__": layer = ColumnParallelLayer(4, 6, world_size=2) x = np.random.randn(2, 4).astype(np.float32) y = layer.forward(x) grad = np.random.randn(2, 6).astype(np.float32) grad_x = layer.backward(grad) print("output shape:", y.shape) print("grad_x shape:", grad_x.shape) print("local grad W shape:", layer.local_grad_W[0].shape)
self.local_W = [self.W[:, j * self.per_device:(j + 1) * self.per_device] for j in range(world_size)]把权重矩阵按列切成多块,每块分给一个“设备”。y = np.concatenate(self.local_y, axis=1)前向把每个设备的输出分片按列拼回完整输出。self.local_grad_W = [self.x.T @ gy for gy in grad_y_list]每个设备独立计算自己的权重梯度,不需要通信。grad_x_list = [gy @ w.T for gy, w in zip(grad_y_list, self.local_W)]每个设备算出一份对输入 $X$ 的局部梯度。grad_x = all_reduce(grad_x_total, grad_x_list)[0]用 All-Reduce 把所有局部输入梯度求和,并广播回所有设备。
常见误区
误区既然每个设备各算各的列,是不是整个前向反向都互不打扰?那岂不是零通信?
正解前向确实不需要通信,但反向必须对输入梯度做一次 All-Reduce。因为 $X$ 是完整复制到所有设备上的,每个设备都需要拿到完整的 $\partial L / \partial X$。只要有一台设备只拿到局部梯度,它这一份 $X$ 的更新就是不完整的,训练就会出错。
为何常见:人们容易被“各算各的列”这句话带偏,忘掉了输入 $X$ 是共享的、需要完整梯度。
误区列并行需要 All-Reduce,行并行不是也需要吗?反正都要通信,区别不大。
正解两者的通信时机完全不同。列并行是前向不通信、反向对输入梯度 All-Reduce;行并行是前向对输出求和 All-Reduce、反向不需要额外通信。把方向搞反,会在设计通信模块时浪费大量带宽,甚至直接让训练崩溃。
为何常见:只记住了“张量并行要通信”这个模糊印象,没有从矩阵乘法的梯度推导出发理解。
误区矩阵太大放不下,那我横着切成两半,一部分放卡 A,一部分放卡 B,这不也是并行吗?
正解这其实是行并行的思路,不是列并行。列并行按输出特征维度切,每一块都是完整输入特征、部分输出特征;行并行按输入特征维度切,每一块是部分输入特征、全部输出特征。两者处理的数据分布、通信模式完全不同。
为何常见:初学者对“行”和“列”的直觉不够,容易把矩阵的存储切分和并行维度混为一谈。
误区列并行前向不通信,那我切 64 份、128 份,速度一定翻倍?
正解反向传播有 All-Reduce,通信量随设备数增长;而且每个设备只算一小块矩阵,矩阵乘法的计算效率会下降。列并行适合在显存成为瓶颈时做有限度切分,不能无限增加设备数。
为何常见:只看到前向零通信的甜头,忽略了反向通信和单卡计算效率下降。
面试题
Q1. 列并行把权重矩阵按什么方向切分?
- A.按行切分
- B.按列切分← 正确答案
- C.按批次切分
- D.按通道切分
答案:B。列并行顾名思义是沿权重矩阵的列方向切分,也就是输出维度切分。按行切分是行并行,按批次切分是数据并行,按通道切分不是标准张量并行术语。
Q2. 列并行前向传播时,输入矩阵 $X$ 在各设备上如何分配?
- A.按行切成多块
- B.每台设备都保留完整 X← 正确答案
- C.按列切成多块
- D.随机采样一部分
答案:B。列并行的前提是每个设备持有完整输入 $X$,各自乘自己那块权重列,因此前向阶段各设备不需要互相通信。
Q3. 列并行反向传播中,哪一项需要通过 All-Reduce 汇总?
- A.输出梯度 G
- B.权重梯度 dW_j
- C.输入梯度 dX← 正确答案
- D.损失标量
答案:C。每个设备只算出一部分 $\partial L / \partial X$,需要 All-Reduce 求和并广播。输出梯度 G 本来就在各设备自己手上,权重梯度 dW_j 可以本地算完,损失标量也不需要跨设备归约。
Q4. 列并行中,每个设备本地计算权重梯度的公式是?
- A.X^T G_j← 正确答案
- B.X G_j^T
- C.G_j X^T
- D.W_j^T X^T
答案:A。局部权重梯度等于本地输入转置乘上本地输出梯度,即 $X^T G_j$。其他选项维度不对,也无法对应矩阵求导链式法则。
Q5. 行并行前向传播的最后一步通常需要做什么?
- A.All-Reduce 求和← 正确答案
- B.拼接输出
- C.广播输入
- D.本地归一化
答案:A。行并行把输入 $X$ 按行切开,各部分乘完整权重后得到的是部分和,必须用 All-Reduce 把所有部分和相加,才能得到完整输出。
Q6. 当设备数为 $p$ 时,列并行中每个设备的输出列数是完整输出列数的多少?
- A.1/p← 正确答案
- B.p 倍
- C.p 的平方
- D.保持不变
答案:A。完整输出维度 $d_{\text{out}}$ 被切成 $p$ 份,每台设备只负责 $d_{\text{out}}/p$ 列,因此每个设备的输出列数是完整输出的 $1/p$。
Q7. Transformer 的 MLP 采用“列并行 + 行并行”组合,相比两个线性层都用列并行,主要收益是什么?
- A.中间层通信更少← 正确答案
- B.不需要激活函数
- C.权重精度更高
- D.不依赖残差连接
答案:A。列并行输出隐藏分片后,紧接着的行并行可以直接消费同一设备上的分片,两层的中间过渡不需要通信,相比两层都做列并行省去了一次 All-Reduce。
Q8. 关于列并行,下列说法正确的有?
- A.前向设备间不通信← 正确答案
- B.反向需要对输入梯度做 All-Reduce← 正确答案
- C.权重梯度可以本地计算← 正确答案
- D.输入 X 需要按列切分
答案:ABC。列并行把权重按列切分,输入 $X$ 是完整广播的,因此 D 错。前向不需要通信,反向对输入梯度需要 All-Reduce,权重梯度由本地 $X$ 和输出梯度算出。
Q9. 关于行并行与列并行的对比,正确的有?
- A.行并行前向需要 All-Reduce← 正确答案
- B.列并行反向输入梯度需要 All-Reduce← 正确答案
- C.行并行反向输入梯度需要 All-Reduce
- D.列并行反向权重梯度需要 All-Reduce
答案:AB。行并行前向要把局部结果相加,所以需要 All-Reduce;列并行反向要把输入梯度分片求和广播,所以需要 All-Reduce。行并行反向只需要拼接本地梯度,列并行权重梯度本来就在本地。
Q10. 关于 Megatron 风格 MLP 的列并行 + 行并行组合,正确的有?
- A.第一个 Linear 权重按列切← 正确答案
- B.第二个 Linear 权重按行切← 正确答案
- C.两个 Linear 之间无需额外通信← 正确答案
- D.最终输出需要两次 All-Reduce
答案:ABC。第一个 Linear 用列并行产出隐藏分片,第二个 Linear 用行并行直接消费这些分片,中间无需通信,最终只做一次 All-Reduce 得到完整输出,因此 D 错。