张量并行详解

1218 字
6 分钟
张量并行详解

Tensor Parallelism 的本质是把一个”大矩阵运算”切到多个 GPU 上,让每张 GPU 只保存和计算矩阵的一部分,最后通过通信把结果拼起来。

大矩阵乘法的切分#

我们考虑如何切分矩阵乘法

Y=XWY = XW

其中

XRB×K,WRK×N,YRB×NX \in \mathbb{R}^{B \times K}, \quad W \in \mathbb{R}^{K \times N}, Y \in \mathbb{R}^{B \times N}

示例矩阵乘法
示例矩阵乘法

Column Parallel Linear#

如果有 4 张 GPU,那么可以把 WW 沿着输出维度 NN 切开:

W=[W0,W1,W2,W3]W = [W_0, W_1, W_2, W_3]

每张 GPU 保存一部分:

GPUi:WiRK×N/4GPU_i: W_i \in \mathbb{R}^{K \times N / 4}

于是:

Yi=XWiY_i = XW_i

每张 GPU 独立计算一部分输出:

Y=[Y0,Y1,Y2,Y3]Y = [Y_0, Y_1, Y_2, Y_3]

最后把它们拼起来即可。

这就是最简单的 Column Parallel Linear

Column Parallel Linear
Column Parallel Linear

下面是 Column Parallel Linear 的代码实现:

class ColumnParallelLinear(torch.nn.Module):
def __init__(self, in_features: int, out_features: int, bias: bool):
super().__init__()
self.tp_world_size = get_tp_world_size()
self.tp_rank = get_tp_rank()
self.in_features = in_features
self.out_features = out_features
assert out_features % self.tp_world_size == 0
self.output_size_per_partition = out_features // self.tp_world_size
# Allocate space for the weight and bias
# Note: torch.nn.functional.linear performs XW^T + b so we exchange the order of dimensions
self.weight = nn.Parameter(torch.Tensor(self.output_size_per_partition, self.in_features))
if bias:
self.bias = nn.Parameter(torch.Tensor(self.output_size_per_partition))
else:
self.register_parameter("bias", None)
def forward(self, x: torch.Tensor):
return F.linear(x, self.weight, self.bias)

Row Parallel Linear#

另一种切分方式是把 WW 按行切:

W=[W0W1W2W3]W = \begin{bmatrix} W_0 \\ W_1 \\ W_2 \\ W_3 \end{bmatrix}

同时把输入也按照对应维度切开:

X=[X0,X1,X2,X3]X = [X_0, X_1, X_2, X_3]

于是:

XW=X0W0+X1W1+X2W2+X3W3XW = X_0W_0 + X_1W_1 + X_2W_2 + X_3W_3

每张 GPU 计算:

Yi=XiWiY_i = X_iW_i

最后:

Y=iYiY = \sum_{i}Y_i

Row Parallel Linear
Row Parallel Linear

Note

总结

  • Column Parallel:WW 输出维度被切开,可以使用 AllGather 获取全部输出。
  • Row Parallel: WW 输入维度被切开,最后必须使用 AllReduce 求和。

下面是 Row Parallel Linear 的代码实现:

class RowParallelLinear(nn.Module):
def __init__(self, in_features: int, out_features: int, bias: bool):
super().__init__()
self.tp_world_size = ...
self.tp_rank = ...
self.in_features = in_features
self.out_features = out_features
assert in_features % self.tp_world_size == 0
self.input_size_per_partition = in_features // self.tp_world_size
self.weight = nn.Parameter(torch.Tensor(out_features, input_size_per_partition))
if bias:
self.bias = nn.Parameter(torch.Tensor(out_features))
else:
self.register_parameter("bias", None)
def forward(self, x: torch.Tensor):
# x: [batch, input_size_per_partition]
y = F.linear(x, self.weight, None)
dist.all_reduce(y, op=dist.ReduceOp.SUM) # group = ...
return y if self.bias is None else y + self.bias

在 Transformer Block 中使用 Tensor Parallelism#

Transformer Block 中最重要的两个组件是 FFN 和 Attention。 接下来介绍如何分别在 FFN 和 Attention 中使用 Tensor Parallelism。

在 FFN 中使用 Tensor Parallelism#

假设 FFN:

H=σ(XW1)Y=HW2\begin{aligned} H &= \sigma(XW_1) \\ Y &= HW_2 \end{aligned}

我们先把 W1W_1 按列切分:

W1=[W1,0,W1,1]W_1 = [W_{1,0}, W_{1,1}]

那么:

H=[H0,H1]H = [H_0, H_1]

其中:

Hi=σ(XW1,i)H_i = \sigma(XW_{1,i})

然后把 W2W_2 按行切:

W2=[W2,0W2,1]W_2 = \begin{bmatrix} W_{2,0} \\ W_{2,1} \end{bmatrix}

那么:

Y=H0W2,0+H1W2,1Y = H_0 W_{2,0} + H_1 W_{2,1}
Important

先进行 Column Parallel 再进行 Row Parallel 的顺序不能颠倒,否则需要对中间计算结果进行 AllReduce。

在 FFN 中使用 Tensor Parallelism
在 FFN 中使用 Tensor Parallelism

MLP 的 Tensor Parallel 实现如下:

class ParallelMLP(nn.Module):
def __init__(self, hidden_size: int, intermediate_size: int):
self.fc1 = ColumnParallelLinear(in_features=hidden_size, out_features=intermediate_size, bias=False)
self.fc2 = RowParallelLinear(in_features=intermediate_size, out_features=hidden_size, bias=False)
def forward(self, x: torch.Tensor):
x = self.fc1(x)
x = F.gelu(x)
x = self.fc2(x)
return x

在 Attention 中使用 Tensor Parallelism#

假设有 num_attention_heads == 32,4 张 GPU。

最简单的做法是

  • GPU0 -> heads 0 ~ 7
  • GPU1 -> heads 8 ~ 15
  • GPU2 -> heads 16 ~ 23
  • GPU3 -> heads 24 ~ 31

这种 Head Parallel 本质上属于 Tensor Parallel。

每张 GPU 计算自己的:

Qi,Ki,ViQ_i, K_i, V_i

然后独立完成:

Zi=Attentioni=Softmax(QiKiTd)ViZ_i = Attention_i = \operatorname{Softmax}(\frac{Q_i K_i^{T}}{\sqrt{d}}) V_i

由于不同 head 本身是独立的,所以这里不需要 GPU 之间通信

然后经过 output projection:

Y=ZWOY = Z W_O

WOW_O 做 Row Parallel:

WO=[WO,0WO,1WO,2WO,3]W_O = \begin{bmatrix} W_{O,0} \\ W_{O,1} \\ W_{O,2} \\ W_{O,3} \end{bmatrix}

于是:

Y=iZiWO,iY = \sum_{i} Z_i W_{O,i}

最后一次 AllReduce。

在 Attention 中使用 Tensor Parallelism
在 Attention 中使用 Tensor Parallelism

Note

对于 MQA 和 GQA 也可以使用 Tensor Parallelism。

MHA 的 Tensor Parallel 实现示例如下:

class ParallelMultiHeadAttention(nn.Module):
def __init__(self, hidden_size: int, num_heads: int):
self.tp_world_size = ...
self.tp_rank = ...
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
assert num_heads % self.tp_world_size == 0
self.heads_per_partition = num_heads // self.tp_world_size
self.qkv_proj = ColumnParallelLinear(in_features=hidden_size, out_features=3 * hidden_size, bias=False)
self.o_proj = RowParallelLinear(in_features=hidden_size, out_features=hidden_size, bias=False)
def forward(self, x: torch.Tensor, mask: torch.Tensor = None):
# x: [batch, seq_len, hidden_size]
batch, seq_len, _ = x.shape
qkv = self.qkv_proj(x) # [batch, seq_len, 3 * heads_per_partition * head_dim]
q, k, v = qkv.trunk(3, dim=-1) # [batch, seq_len, heads_per_partition * head_dim]
q = q.view(batch, seq_len, self.heads_per_partition, head_dim)
k = k.view(batch, seq_len, self.heads_per_partition, head_dim)
v = v.view(batch, seq_len, self.heads_per_partition, head_dim)
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
attn_weights = torch.matmul(q, k.transpose(-1, -2)) / (self.head_dim ** 0.5)
if mask:
attn_weights = attn_weights + mask
attn_weights = F.softmax(attn_weights, dim=-1)
out = torch.matmul(attn_weights, v) # [batch, heads_per_partition, seq_len, head_dim]
out = out.transpose(1, 2).contiguous() # [batch, seq_len, heads_per_partition, head_dim]
out = out.view(batch, seq_len, -1) # [batch, seq_len, heads_per_partition * head_dim]
return self.o_proj(out)

整个 TP Transformer Block 就是这样的:

class TPTransformerBlock(nn.Module):
def __init__(self, hidden_size, num_heads, intermediate_size):
super().__init__()
self.norm1 = nn.LayerNorm(hidden_size)
self.attn = ParallelMultiHeadAttention(hidden_size, num_heads)
self.norm2 = nn.LayerNorm(hidden_size)
self.mlp = ParallelMLP(hidden_size, intermediate_size)
def forward(self, x):
# Attention
residual = x
x = self.norm1(x)
x = self.attn(x)
x = residual + x
# MLP
residual = x
x = self.norm2(x)
x = self.mlp(x)
x = residual + x
return x
Important

Tensor Parallelism 的缺陷

Tensor Parallelism 最大的缺陷是计算和通信没办法重叠,而且对于通信带宽的要求极高。

Sequence Parallelism#

需要注意的是 Tensor Parallelism 无法处理 Transformer Block 中的 LayerNorm 和 Residual。 虽然这两个部分的计算很简单,但是它们的激活依然占据很大的空间,因此有 SP (Sequence Parallelism) 进行进一步切分。

Sequence Parallelism
Sequence Parallelism

参考#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
张量并行详解
https://llm-tech.com.cn/posts/tensor-parallelism/
作者
Ming
发布于
2026-08-13
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Ming
你是来找 Ming 学习的吗
🎉 欢迎来到 Ming 的博客
这里是我的个人博客,分享 AI Infra、LLM 等技术内容。欢迎关注交流!
分类
标签
站点统计
文章
19
分类
8
标签
16
总字数
55,114
运行时长
0
最后活动
0 天前

目录