张量并行详解
Tensor Parallelism 的本质是把一个”大矩阵运算”切到多个 GPU 上,让每张 GPU 只保存和计算矩阵的一部分,最后通过通信把结果拼起来。
大矩阵乘法的切分
我们考虑如何切分矩阵乘法
其中
Column Parallel Linear
如果有 4 张 GPU,那么可以把 沿着输出维度 切开:
每张 GPU 保存一部分:
于是:
每张 GPU 独立计算一部分输出:
最后把它们拼起来即可。
这就是最简单的 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
另一种切分方式是把 按行切:
同时把输入也按照对应维度切开:
于是:
每张 GPU 计算:
最后:

总结
- Column Parallel: 输出维度被切开,可以使用 AllGather 获取全部输出。
- Row Parallel: 输入维度被切开,最后必须使用 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:
我们先把 按列切分:
那么:
其中:
然后把 按行切:
那么:
先进行 Column Parallel 再进行 Row Parallel 的顺序不能颠倒,否则需要对中间计算结果进行 AllReduce。

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 计算自己的:
然后独立完成:
由于不同 head 本身是独立的,所以这里不需要 GPU 之间通信。
然后经过 output projection:
将 做 Row Parallel:
于是:
最后一次 AllReduce。

对于 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 xTensor Parallelism 的缺陷
Tensor Parallelism 最大的缺陷是计算和通信没办法重叠,而且对于通信带宽的要求极高。
Sequence Parallelism
需要注意的是 Tensor Parallelism 无法处理 Transformer Block 中的 LayerNorm 和 Residual。 虽然这两个部分的计算很简单,但是它们的激活依然占据很大的空间,因此有 SP (Sequence Parallelism) 进行进一步切分。

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