想象一家繁忙的大排档:大厨按一本厚厚的配方同时招待满堂客人,一个人实在忙不过来。于是他把配方竖着撕成几份,分别交给几位帮厨。每位帮厨面前都放着完整的一筐食材,只按自己手里那几列配方操作;炒完后,把各自的半成品拼成一整桌菜上桌。客人反馈味道时,几位帮厨又要凑在一起互通点评,才能确定下一步怎么调整。这个“竖着撕配方、人手一份食材、最后拼盘、反向再互通”的过程,就是列并行。它是大模型训练里张量并行的基础操作:把一个矩阵按列切给多张卡,多卡各算各的,部分场景甚至不需要互相说话。

岗位关联

理解列并行,就能看懂 Megatron 等分布式训练框架中最核心的“切分与通信”逻辑,是面试和工程实战中绕不开的地基。

目录
  1. 可视化演示
  2. 列并行概念:竖着撕配方
  3. 分块矩阵乘法:为什么可以这样切
  4. 前向计算流程:备料、翻炒、拼盘
  5. 反向传播特点:菜品评价的互通
  6. 行并行对比:换一个方向切
  7. Transformer组合:厨房流水线
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示FIFO与LRU页面置换算法在有限物理帧中的执行过程,可视化缺页中断、页面命中和替换策略的差异。

本节配有交互动效(进入视口后自动加载,约 3KB)。

LRU页面置换算法 物理帧容量为3,引用串长度为20 绿色表示页面命中,无需替换 红色表示缺页中断,需加载或替换页面 LRU替换最近最久未使用的页面 底部显示最近的命中和缺页历史

列并行概念:竖着撕配方

大排档里,大厨不再独自处理整本配方,而是把配方竖着撕开:帮厨 A 拿左边一列,帮厨 B 拿右边一列。所有帮厨面前仍摆着同一筐完整食材。也就是说,输入数据不切分,权重矩阵按列切分。每个帮厨只按自己手里的配方做一部分菜,最后把半成品拼在一起,就是一整桌菜。

在大模型训练中,一个线性层可以写成矩阵乘法 $Y = XW$。列并行把权重矩阵 $W$ 沿输出维度切成 $p$ 块,分别放到 $p$ 张卡上。第 $j$ 张卡拥有 $W_j$,并计算 $Y_j = XW_j$。因为 $X$ 对所有人都是完整的,所以每张卡都能独立完成自己的矩阵乘法,不需要在计算过程中找别人商量。

所以列并行最直观的口诀是“权重分列、输入全量、输出拼接”。就像几位帮厨各自做出半成品,拼成一整道菜;客人看到的是完整的一桌菜,不会关心是哪位帮厨做的。

X[W1W2⋯Wp]=[XW1XW2⋯XWp]X \begin{bmatrix} W_1 & W_2 & \cdots & W_p \end{bmatrix} = \begin{bmatrix} X W_1 & X W_2 & \cdots & X W_p \end{bmatrix}
这个公式的意思是:把权重矩阵横着摆开,按列切成若干块;输入矩阵 $X$ 作为公共因子,分别和每个列块相乘,再把结果按列拼起来。
  1. 切分权重把权重矩阵按列切成 $p$ 块,每块形状相同。
    Wj∈Rdin×(dout/p)W_j \in \mathbb{R}^{d_{\text{in}} \times (d_{\text{out}}/p)}
  2. 输入不切输入矩阵 $X$ 不切分,直接作为公共因子参与每一块乘法。
    XWj=YjX W_j = Y_j
  3. 拼接输出把 $p$ 个输出分片按列拼接,恢复完整输出。
    Y=[Y1,Y2,…,Yp]Y = \left[ Y_1, Y_2, \ldots, Y_p \right]

分块矩阵乘法:为什么可以这样切

有人会问:把配方竖着撕,难道不会把一道菜做坏吗?不会,因为矩阵乘法天生就允许“一列一列地算”。每个输出列只由输入矩阵和权重矩阵的对应列决定,和其他列没有关系。这就像配方的“酱油用量”只影响酱油这一列的效果,帮厨 A 只需要把酱油相关的那几列做好。

从数学上看,矩阵乘法 $Y = XW$ 中,输出矩阵的第 $j$ 列,是输入矩阵每一列按权重第 $j$ 列加权求和的结果。也就是说,每一列输出是独立计算的。因此,把权重矩阵按列切成若干块后,每块对应的输出列也是相互独立的,最后拼起来不会有任何信息丢失。

这正好解释了为什么列并行是“零风险的切法”:数学上本来就可以分块。帮厨们不需要讨论各自配方之间怎么协调,只要最后把成品按顺序摆好即可。

XW=X[W:, 1  W:, 2]=[XW:, 1  XW:, 2]XW = X \left[ W_{:,\,1} \; W_{:,\,2} \right] = \left[ XW_{:,\,1} \; XW_{:,\,2} \right]
这里 $W_{:,\,1}$ 表示权重矩阵的第一部分列,$W_{:,\,2}$ 表示第二部分列。乘法分配律告诉我们:公共因子 $X$ 可以分别乘进去,再按列并排。
  1. 约定维度设输入 $X$ 形状为 $B \times d_{\text{in}}$,权重 $W$ 形状为 $d_{\text{in}} \times d_{\text{out}}$。
    X∈RB×din,W∈Rdin×doutX \in \mathbb{R}^{B \times d_{\text{in}}}, \quad W \in \mathbb{R}^{d_{\text{in}} \times d_{\text{out}}}
  2. 按列分块把 $W$ 横向切成左右两块,每块列数各为 $d_{\text{out}}/2$。
    W=[W1  W2]W = \left[ W_1 \; W_2 \right]
  3. 分配律展开矩阵乘法满足分配律,于是可以先分别乘再拼接。
    X[W1  W2]=[XW1  XW2]X \left[ W_1 \; W_2 \right] = \left[ X W_1 \; X W_2 \right]

前向计算流程:备料、翻炒、拼盘

列并行的前向过程,就像大排档的备菜流程。第一步是“备料”:把完整食材 $X$ 复制一份放到每口锅前。第二步是“翻炒”:每位帮厨只用自己的配方 $W_j$ 处理这些食材,得到自己的半成品 $Y_j$。第三步是“拼盘”:把所有半成品按顺序拼在一起,得到完整输出 $Y$。

整个过程里,设备之间不需要通信。因为每个设备都拿到了完整的 $X$,并且只负责输出维度上的一段;它既不需要等待别人的结果,也不需要把自己的结果发给别人。唯一要做的,只是最后在逻辑上把输出分片拼接起来。在工程实现里,这种拼接有时候是显式的 concat,有时候是让下一层直接消费分片,省去实际拼接。

前向不通信,是列并行最舒服的地方。帮厨们埋头炒菜,互不打扰;只有在上菜前才需要把拼盘摆好。

Yj=XWj,Y=Concat(Y1,Y2,…,Yp)Y_j = X W_j, \qquad Y = \text{Concat}\left(Y_1, Y_2, \ldots, Y_p\right)
第一个式子代表每个设备上的本地计算,第二个式子代表按列拼接。前向阶段没有跨设备的求和、求平均等操作,所以通信开销接近零。
  1. 广播输入把输入矩阵 $X$ 完整发送给所有设备,每个设备都有一份。
    X→设备jX \rightarrow \text{设备}_j
  2. 本地乘算设备 $j$ 用自己保存的权重列块 $W_j$ 做矩阵乘法。
    Yj=XWjY_j = X W_j
  3. 拼接输出将各设备的 $Y_j$ 沿列方向拼接成完整 $Y$。
    Y=[Y1,Y2,…,Yp]Y = \left[ Y_1, Y_2, \ldots, Y_p \right]

反向传播特点:菜品评价的互通

菜上桌后,客人给出反馈:哪里咸了、哪里淡了。这个反馈就是损失对输出的梯度。因为输出是拼接出来的,每位帮厨只需要拿到自己那块菜对应的评价,这不需要通信。但他还需要知道“整桌菜”的反馈对自己手里的食材 $X$ 有多大责任,这就必须把各人的理解汇总起来。

在数学上,反向传播要求计算 $\partial L / \partial X$。由于 $Y = [Y_1, \dots, Y_p]$,输入梯度是所有输出分片对输入贡献的总和。每个设备只能算出自己那一份局部输入梯度,必须通过 All-Reduce 把大家的结果求和,再广播给每个设备,这样每个设备手里的 $X$ 副本都能得到完整梯度。

这就是列并行“反向 All-Reduce”的由来。帮厨们不能只埋头炒菜,还得在客人反馈后凑在一起对一下账,否则每个人都不知道食材 $X$ 到底该往哪个方向调整。

∂L∂X=∑j=1p∂L∂YjWjT\frac{\partial L}{\partial X} = \sum_{j=1}^{p} \frac{\partial L}{\partial Y_j} W_j^T
这个公式说明:对输入 $X$ 的梯度,等于每个输出分片梯度先乘上对应权重转置,再全部加起来。这一步就是 All-Reduce 中的“求和”。
  1. 切分输出梯度把输出梯度按列切给各设备,每个设备只拿自己输出分片对应的梯度。
    Gj=∂L∂YjG_j = \frac{\partial L}{\partial Y_j}
  2. 计算局部输入梯度每个设备用本地权重转置,算出自己对输入梯度的贡献。
    Hj=GjWjTH_j = G_j W_j^T
  3. All-Reduce 汇总把 $p$ 份局部输入梯度求和,并广播给所有设备。
    ∂L∂X=∑j=1pHj\frac{\partial L}{\partial X} = \sum_{j=1}^{p} H_j
  4. 权重梯度本地算每个设备的权重梯度只依赖本地输入和输出梯度,无需通信。
    ∂L∂Wj=XTGj\frac{\partial L}{\partial W_j} = X^T G_j

行并行对比:换一个方向切

列并行是“竖着撕配方”,行并行则是“横着分食材”:把完整的一筐食材分成两份,帮厨 A 拿上半筐,帮厨 B 拿下半筐,但两人手里都有完整的配方。最后两人炒出来的菜必须倒进同一口锅里拌匀,因为每个人只做了半道菜。这个“拌匀”的过程,就是前向计算里的 All-Reduce。

从公式看,行并行把输入 $X$ 按特征维切成 $X_1, X_2$,把权重 $W$ 按行切成上下两块,然后做两个局部矩阵乘,再把结果相加:

对比两种切法,就像帮厨分工的两个极端:一个按配方列分工,最后拼盘;一个按食材量分工,最后混炒。理解了这两个方向,就能明白为什么现代框架要在不同位置选择不同切法。

XW=[X1X2][W1W2]=X1W1+X2W2XW = \begin{bmatrix} X_1 & X_2 \end{bmatrix} \begin{bmatrix} W_1 \\ W_2 \end{bmatrix} = X_1 W_1 + X_2 W_2
行并行与列并行正好互补:列并行前向不需要通信、反向需要 All-Reduce;行并行前向需要 All-Reduce 来求和,反向反而只需要把局部输入梯度拼起来,不需要额外通信。
  1. 切分输入把输入矩阵按特征维横切成两块。
    X=[X1  X2]X = \left[ X_1 \; X_2 \right]
  2. 上下切分权重把权重矩阵按行切成上下两块。
    W=[W1W2]W = \begin{bmatrix} W_1 \\ W_2 \end{bmatrix}
  3. 前向求和两个局部乘积需要通过 All-Reduce 相加,才能得到完整输出。
    Y=X1W1+X2W2Y = X_1 W_1 + X_2 W_2

Transformer组合:厨房流水线

实际的 Transformer 层里,MLP 通常有两个线性层:第一个把维度扩大,第二个把维度缩回去。如果两个线性层都用列并行,中间就需要一次拼接,第二个线性层的前向又要通信,整体通信次数多。更聪明的做法是让第一个线性层用列并行,第二个线性层用行并行,拼出一条流水线。

设第一个线性层的权重 $W_1$ 按列切成 $W_1^{(j)}$,第二个线性层的权重 $W_2$ 按行切成 $W_2^{(j)}$。设备 $j$ 上先算出隐藏分片 $H_j$,这个 $H_j$ 恰好就放在设备 $j$ 上,正好是第二个线性层行并行需要的输入分片。于是两个线性层之间不需要任何通信,只在最后输出时做一次 All-Reduce 求和。

这种“列并行 + 行并行”的组合,就像大排档的流水线:前半段各帮厨按配方列备料,后半段把各人手里的半成品直接倒进同一口锅,只在一开始和最后各通一次气。Transformer 中的注意力 QKV 投影也常用列并行,输出投影用行并行,原理完全一样。

O=∑j=1pHjW2(j),Hj=σ(XW1(j))O = \sum_{j=1}^{p} H_j W_2^{(j)}, \qquad H_j = \sigma\left(X W_1^{(j)}\right)
这里 $H_j$ 是第一个线性层在设备 $j$ 上的输出分片,$W_2^{(j)}$ 是第二个线性层在设备 $j$ 上的行分片。两者天然在同一张卡上,所以中间无需通信,最后只需要一次求和。
  1. 列并行投影第一个线性层用列并行,每个设备算出隐藏分片。
    Hj=σ(XW1(j))H_j = \sigma\left(X W_1^{(j)}\right)
  2. 行并行消费第二个线性层用行并行,直接消费本地已有的 $H_j$。
    Oj=HjW2(j)O_j = H_j W_2^{(j)}
  3. 最终归约把各设备的部分输出相加,得到完整输出。
    O=∑j=1pOjO = \sum_{j=1}^{p} O_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 演示

python52 行
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)
目的:用 NumPy 模拟一个列并行线性层的完整前向和反向过程,直观展示“权重按列切分、前向拼接输出、反向对输入梯度做 All-Reduce”三个核心步骤。
  1. self.local_W = [self.W[:, j * self.per_device:(j + 1) * self.per_device] for j in range(world_size)]把权重矩阵按列切成多块,每块分给一个“设备”。
  2. y = np.concatenate(self.local_y, axis=1)前向把每个设备的输出分片按列拼回完整输出。
  3. self.local_grad_W = [self.x.T @ gy for gy in grad_y_list]每个设备独立计算自己的权重梯度,不需要通信。
  4. grad_x_list = [gy @ w.T for gy, w in zip(grad_y_list, self.local_W)]每个设备算出一份对输入 $X$ 的局部梯度。
  5. 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. 列并行把权重矩阵按什么方向切分?

单选题 · easy

  • A.按行切分
  • B.按列切分← 正确答案
  • C.按批次切分
  • D.按通道切分

答案:B。列并行顾名思义是沿权重矩阵的列方向切分,也就是输出维度切分。按行切分是行并行,按批次切分是数据并行,按通道切分不是标准张量并行术语。

Q2. 列并行前向传播时,输入矩阵 $X$ 在各设备上如何分配?

单选题 · easy

  • A.按行切成多块
  • B.每台设备都保留完整 X← 正确答案
  • C.按列切成多块
  • D.随机采样一部分

答案:B。列并行的前提是每个设备持有完整输入 $X$,各自乘自己那块权重列,因此前向阶段各设备不需要互相通信。

Q3. 列并行反向传播中,哪一项需要通过 All-Reduce 汇总?

单选题 · mid

  • A.输出梯度 G
  • B.权重梯度 dW_j
  • C.输入梯度 dX← 正确答案
  • D.损失标量

答案:C。每个设备只算出一部分 $\partial L / \partial X$,需要 All-Reduce 求和并广播。输出梯度 G 本来就在各设备自己手上,权重梯度 dW_j 可以本地算完,损失标量也不需要跨设备归约。

Q4. 列并行中,每个设备本地计算权重梯度的公式是?

单选题 · mid

  • 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. 行并行前向传播的最后一步通常需要做什么?

单选题 · mid

  • A.All-Reduce 求和← 正确答案
  • B.拼接输出
  • C.广播输入
  • D.本地归一化

答案:A。行并行把输入 $X$ 按行切开,各部分乘完整权重后得到的是部分和,必须用 All-Reduce 把所有部分和相加,才能得到完整输出。

Q6. 当设备数为 $p$ 时,列并行中每个设备的输出列数是完整输出列数的多少?

单选题 · mid

  • A.1/p← 正确答案
  • B.p 倍
  • C.p 的平方
  • D.保持不变

答案:A。完整输出维度 $d_{\text{out}}$ 被切成 $p$ 份,每台设备只负责 $d_{\text{out}}/p$ 列,因此每个设备的输出列数是完整输出的 $1/p$。

Q7. Transformer 的 MLP 采用“列并行 + 行并行”组合,相比两个线性层都用列并行,主要收益是什么?

单选题 · hard

  • A.中间层通信更少← 正确答案
  • B.不需要激活函数
  • C.权重精度更高
  • D.不依赖残差连接

答案:A。列并行输出隐藏分片后,紧接着的行并行可以直接消费同一设备上的分片,两层的中间过渡不需要通信,相比两层都做列并行省去了一次 All-Reduce。

Q8. 关于列并行,下列说法正确的有?

多选题 · mid

  • A.前向设备间不通信← 正确答案
  • B.反向需要对输入梯度做 All-Reduce← 正确答案
  • C.权重梯度可以本地计算← 正确答案
  • D.输入 X 需要按列切分

答案:ABC。列并行把权重按列切分,输入 $X$ 是完整广播的,因此 D 错。前向不需要通信,反向对输入梯度需要 All-Reduce,权重梯度由本地 $X$ 和输出梯度算出。

Q9. 关于行并行与列并行的对比,正确的有?

多选题 · hard

  • A.行并行前向需要 All-Reduce← 正确答案
  • B.列并行反向输入梯度需要 All-Reduce← 正确答案
  • C.行并行反向输入梯度需要 All-Reduce
  • D.列并行反向权重梯度需要 All-Reduce

答案:AB。行并行前向要把局部结果相加,所以需要 All-Reduce;列并行反向要把输入梯度分片求和广播,所以需要 All-Reduce。行并行反向只需要拼接本地梯度,列并行权重梯度本来就在本地。

Q10. 关于 Megatron 风格 MLP 的列并行 + 行并行组合,正确的有?

多选题 · hard

  • A.第一个 Linear 权重按列切← 正确答案
  • B.第二个 Linear 权重按行切← 正确答案
  • C.两个 Linear 之间无需额外通信← 正确答案
  • D.最终输出需要两次 All-Reduce

答案:ABC。第一个 Linear 用列并行产出隐藏分片,第二个 Linear 用行并行直接消费这些分片,中间无需通信,最终只做一次 All-Reduce 得到完整输出,因此 D 错。