矩阵运算

随着模型参数的增长,推理 GPU 资源需求从 1/N 卡、单卡、多卡、多节点、再到超节点,规模不断增加,之前在训练里面常用的 DP/TP/SP(CP)/EP/PP/Zero 等并行方法也在推理中逐步使用起来。

推理的并行还有着自己的一些特点,比如 PD 分离场景下 P 和 D 的部署方式存在差异、PP 场景下 Attention 与 FFN 数量可以不等。

在展开推理并行优化前,对一些基础知识进行了解是必要的。

1. 矩阵运算

并行底层的运算涉及二维矩阵拆分运算、多维矩阵切分与合并运算。

在神经网络中线性运算基本是矩阵乘法:C = A x B。矩阵运算的输入、输出可以是多维矩阵(>2 维),但维度之间有两个关键约束:

  1. 若 A 尺寸为 [M, N],B 的第一维度必须为 N。B 为 [N, K] 时,计算得到的 C 尺寸为 [M, K]。
  2. 多维矩阵的乘法都会变为二维矩阵的乘法,倒数第 1 和第 2 维必须满足第一条的 MNK 关系。
  3. 多维乘法认为是批量的二维矩阵乘法。

对第二点有个补充:多维矩阵乘除了最后两维,其它维度必须相等(或者进行广播),举个例子:A size: [10, 3, 2]、B size: [2, 4],计算 A x B

  • B 广播变为 [10, 2, 4],
  • 对应 Ai [i, :, :] 与 Bi [i, :, :] 进行相乘,
  • 得到的 C 维度为 [10, 3, 4]。

知道尺寸的约束关系后,接下来分析矩阵的分块运算。

先看二维矩阵的运算切分场景。下面的举例中都进行的矩阵二分,矩阵的多次切分可以看成二分的扩展。

1.1. 场景一:B 矩阵列切 (column split)

B 矩阵二分(列切)后,子矩阵 B1、B2 的尺寸为 [N, K/2],切分计算不改变局部元素值,但需要进行一次列维度的拼接才能获得完整矩阵 C。

图片

特点:计算量减半,多了一次 allgather 集群通信;显存空间变化不一定,若是非原地操作,则变化为 (-K/2 * N + M * K/2) * element_memory。

应用:在并行运算中,通常用两个 GPU 来完成这一个拆分运算,比如当 A 矩阵为输入激活值,B 矩阵为权重,最后结果通过 allgather 进行拼接。

矩阵切分运算可用 numpy 做个辅助验证,这里提供一个示例:

import numpy as np

# 1. 定义整数输入矩阵 (M, N) 和 (N, K)
M, N, K = 3, 4, 6
A = np.random.randint(0, 10, size=(M, N))  # 随机整数矩阵 [0, 10)
B = np.random.randint(0, 10, size=(N, K))

print("A:\n", A, "\nshape:", A.shape)
print("\nB:\n", B, "\nshape:", B.shape)

# 2. 对 B 按列切分(均分)
num_splits = 3  # 切分块数
B_splits = np.split(B, num_splits, axis=1)  # 沿列切分

print("\nB 分块结果:")
for i, B_i in enumerate(B_splits):
    print(f"B_{i}:\n", B_i, "\nshape:", B_i.shape)

# 3. 模拟并行计算:每个进程计算 A @ B_i
local_results = [A @ B_i for B_i in B_splits]

print("\n局部乘积结果:")
for i, C_i in enumerate(local_results):
    print(f"C_{i} (A @ B_{i}):\n", C_i, "\nshape:", C_i.shape)

# 4. 模拟 allgather:拼接所有局部结果
C_final = np.concatenate(local_results, axis=1)

print("\n合并后的 C_final:\n", C_final, "\nshape:", C_final.shape)

# 5. 验证结果与直接乘法的等价性
C_ground_truth = A @ B
print("\n标准乘法结果 (A @ B):\n", C_ground_truth)
print("\n验证一致性:", np.array_equal(C_final, C_ground_truth))

1.2. 场景二:A 矩阵行切 (row split)

A 矩阵二分(行切)后,子矩阵 A1、A2 的尺寸为 [M/2, N],切分不改变局部元素值,但需要进行一次行维度的拼接才能获得完整矩阵 C。

图片

特点:计算量减半,多了一次集群通信(allgather);若是非原地操作,显存变化:(K-N) * M/2 * element_memory。

应用:这类切分比较常见,如 A 矩阵是激活值,B 矩阵为权重,如果后面的还是线性运算或者元素行运算,allgather 步骤可以放到最后且只需进行一次。

如:A[M/2, N] x B x E x F … -> allgather 与原计算得到结果一致。这就是常见的激活值进行序列切分的场景:[sequence/sp, hidden_dim]。

Note:这里有个值得注意的是,仅切分一个矩阵的方式,适合任何线性运算,但不一定能够带来性能/存储空间的收益。

1.3. 场景三:A 矩阵列切 + B 矩阵行切

A、B 矩阵都进行二分后,输出矩阵 C 的大小不变,所有 C 矩阵进行元素求和,才能让结果与之前计算方式相等。

图片

特点:计算量减半,多了一次集群通信(allreduce);显存变化:输入、权重减半,输出值取决于是否为原地操作。

这种切分方式的使用场景并不多,更多的是下面这种方式:

1.4. 场景四:B 矩阵列切 + C 矩阵行切

在 A x B x C 矩阵乘法场景,B 矩阵列切,C 矩阵行切。

图片

特点:在场景三基础上,中间值的存储大小减半。

在模型并行运算的过程中,大部分都是这些应用场景的组合与变形,需要根据模型层的特点设计合适的矩阵切分策略。

2. 线性层的并行计算

线性层的并行运算,包括层内切分、层间切分以及参数冗余消除,并行方式包括了数据并行(DP)、序列并行 (SP)、张量并行(TP)、层并行(PP)、参数冗余消除(Zero)。

2.1. 层内并行

可根据输入激活值的切分维度对应不同并行策略,一般切 batch 为 DP、切序列为 SP、切隐藏层尺寸为 TP。

图片

关键特点:

  • DP 可以看成批量矩阵乘法,不会改变二维矩阵的乘法形态,所以对应的权重不需要变化。
  • SP 切分不会影响最后一维的计算,如 LayerNorm、softmax。
  • TP 切分激活值后,权重必须做对应的切分。

DP 与 SP/TP 可以解耦独立使用,SP 与 TP 一般可以配合使用(在当前推理应用中一般设计成:DP*TP=world_size,SP=DP)。

又因为:

  1. allreduce 是可以被拆分为 ReduceScatter 和 AllGather 两个步骤
  2. SP 不影响元素操作的运算,如 LayerNorm,softmax

所以在上面矩阵运算场景四的基础上,对于最后的集群通信操作有如下改进,将 allreduce 运算进行拆分,变为两个步骤。并且将 allgather 操作移动到 softmax 计算后:

图片

这样能够降低元素运算相关计算的数据大小、降低过程中激活值的大小(示例中 GPU 上的 LayerNorm 输入减半),即能节省显存、也能提升速度。

在 megatron 论文里面介绍了这种场景的应用,MLP 层中用 SP-TP-SP 的组合方式:

图片

2.2. 层间并行

模型的各个层分散放在不同 GPU 上面,激活值按照流水的方式在层间传递,这种方式称为流水线并行 PP(Pipeline Parallelism),具体可参看 megatron2 论文。

在推理中,由于特殊计算场景,比如 FFN 为稀疏的 MoE 结构时,往往 attention 与 FFN 对算力需求不同,层间并行可以变为异构模式,如 AFD,attention 层与 MoE 分离部署到不同设备上:

图片

2.3. 冗余参数消除

冗余参数消除的方式,如 DeepSpeed 的 Zero,将参数分散存储,当参数需要使用时通过集群通信将参数召回。

根据 Zero 的思想,这里介绍两种消除某一层权重冗余存储的方式。

一般而言大模型由多个 blocks 构成,blocks 中有重复的层结构。当有多个设备时,可以把这些 blocks 的权重均分到各个设备中。如下图所示,以 blocks 中线性层 B 为例,每个 GPU 存储模型的两层权重,当 GPU 需要使用到某一层权重时,先对该层的权重进行广播。

由于 GPU 支持多流,所以可以做计算、广播通信的掩盖,比如计算到 x 层时,提前把 x+1 层的权重先广播出去(权重预取)。

图片

这种方式适合于模型层权重偏小的场景,模型层权重比较大时,或者说更常使用的是权重切片(shard)的场景。

即将每一层的权重进行 n 等分,每个 GPU 设备上面存一份,当需计算时将其 allgather 回来。

图片

3. Attention 层的并行计算

Attention 层的并行计算与 FFN 或者说模型中其它线性层的计算的主要不同之处:

  • 在计算 attention 之前,会多一个 head 的维度,这个 head 维度是在线性映射(投影)运算中产生的。[bs, seq_len, heads * head_dim] -> [bs, heads, seq_len, head_dim]
  • 序列(seq_len 维度)并行需要修正运算。

在并行维度上面,将 Head 维度的切分也算到 TP 里面,而序列维度并行上称之为 CP(context parallel) 或 SP。

图片

Attention 的层间并行、冗余参数消除方式与线性层的方式一致,层内并行的主要差异是 TP 和 SP,所以重点关注到这两类上面。

3.1. 序列并行

在经过线性映射计算后,模型输入变为多头的 Q、K、V 值,其尺寸表达为:[bs, heads, seq_len, head_dim],序列并行是切分 seq_len 维度。

有个前提问题:QKV 的序列并行跟线性层的差异在哪,是否能直接拆分运算后合并?

图片

关于 Scaled Dot-Product Attention,我们分三个子问题来分析:

  • 如果只切 Q 的序列,KV 保持不变,结果拼接后是否相等?
  • 如果只切 K 的序列,QV 保持不变,结果拼接后是否相等?
  • 如果只切 V 的序列,QK 保持不变,结果拼接后是否相等?

先看问题 1,Q 切分后的尺寸为 [bs, heads, seq_len/SP, head_dim],按照 attention 计算:

  • step1:求解 score,Q x K 相当于左矩阵行切(场景二),score 尺寸:[bs, heads, seq_len/SP, seq_len]
  • step2:softmax 求解的是最后一个维度,计算元素值相同,得到 attention_weights,
  • step3:attention_weights 与 V 进行矩阵乘,还是左矩阵行切运算,元素值相同,计算得到 O 的分块结果
  • step4:将计算的 O 进行 allgather,结果相等。

所以:单独切 Q 的序列值后进行结果拼接,与原计算相等。我们可以通过如下例子来验证:

import numpy as np

def softmax(x):
    e_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
    return e_x / np.sum(e_x, axis=-1, keepdims=True)

def attention(Q, K, V):
    scores = np.matmul(Q, K.transpose(0,1,3,2)) / np.sqrt(K.shape[-1])
    weights = softmax(scores)
    return np.matmul(weights, V)

# 设置随机种子(确保可复现)
np.random.seed(42)

# 定义形状:batch_size=1, num_heads=1, seq_len=4, hidden_dim=3
bs, heads, seq_len, hidden_dim = 1, 1, 4, 3

# 随机生成 Q, K, V
Q = np.random.randn(bs, heads, seq_len, hidden_dim)
K = np.random.randn(bs, heads, seq_len, hidden_dim)
V = np.random.randn(bs, heads, seq_len, hidden_dim)

# 全局注意力计算
global_out = attention(Q, K, V)
print("全局注意力输出:\n", global_out)

# 将 Q 按 seq_len 切分为两部分
Q1 = Q[:,:,:2,:]  # 前两行
Q2 = Q[:,:,2:,:]  # 后两行

# 分片计算注意力(K/V 保持完整)
local_out1 = attention(Q1, K, V)
local_out2 = attention(Q2, K, V)

# 合并分片结果(模拟 all_gather)
local_out = np.concatenate([local_out1, local_out2], axis=2)
print("分片注意力输出:\n", local_out)

# 验证一致性
print("结果是否一致:", np.allclose(global_out, local_out, atol=1e-6))

问题 2:单独切 K 的序列。

第一步,QK 乘法得到的 score 尺寸为:

[bs, heads, seq_len, seq_len/SP]

进一步计算 softmax,由于最后一个维度的数据只有之前的一半长度,而 softmax 的计算跟整个序列相关,直接拼接会导致结果不相等。

所以,单独切 K 序列后拼接,结果不等。

问题 3:单独切 V 的序列。

softmax 之前的计算保持不变,得到 attention_weights 尺寸完整,计算 attention_weights x V,因为 V 矩阵被行切,所以 attention_weights 需要列切,根据前面的场景三,最后 allreduce 能够获得完整结果。

综上看到 attention 序列切分的关键:如果需要进行 Q、K、V 在序列维度的同时切分,要解决 softmax 的分块计算。

当前比较常用的解决方式是:QKV 按照相同方式切分,然后对计算结果进行修正,这个方法在 Blockwise Multi-Head Attention、ring attention、tree attention、merge attention 中都有应用,softmax 修正:

图片

关键点:

  1. 每个切分 Qi 要与所有的 Ki、Vi 进行一次计算,得到 Oi;
  2. 最后的分块 Oi 结果需要进行修正;

有了 softmax 的修正方式,结合推理的计算场景实现序列并行:

prefill:训练 forward/推理的 prefill 阶段,Q 的序列长度与 KV 保持一致,开启 SP 后 GPU 之间需要交换 KV 值与 Q 进行运算(ring-attention 方式),计算过程的 CP/SP 并行可以参看:[并行训练]Context Parallelism 的原理与代码浅析。至于是用 KV 传递还是 Q 传递,可以参看这篇分析。

decode:与 prefill 阶段不同的是,在 decode 阶段 Q 的长度为 1,若对应的 KV 值分散在各个 GPU 设备中,可以将 Q 复制 N 份与切片的 KV 值进行 attention 计算后,最后将结果 gather 到一个设备上再计算修正结果。

图片

这个方式意味着 GPU0 需要完成最大值、修正运算、求和运算,GPU0 可能成为瓶颈。

一种 Tree attention 算法对过程做了改进,提升了通信效率。就是把 gather 换成多次 allreduce,这种方式在跨节点场景中优势明显。

图片

prefix cache:在 prefill 阶段,还有个特殊场景 prefix cache,若开启了 prefix cache 功能,表示有一段 KV cache 可以复用。

把序列 seq_len 拆分成两部分讨论,prefix_cache_seq_len、tail_seq_len。Q/K/V 的激活值尺寸:

[bs, heads, prefix_cache_seq_len + tail_seq_len, head_dim]

其中 KV 值的 prefix_cache_seq_len 已被缓存,prefix_cache_seq_len KV 的计算量更少。Q 序列只需计算 tail_seq_len 的结果,加上前面的分析(Q 序列单独切分,不会改变元素计算结果)可知,计算分为两类场景:

  1. Q tail_seq_len 与 prefix_cache_seq_len KV 计算 attention;
  2. Q tail_seq_len 与 tail_seq_len KV 计算 attention;

在有 prefix cache 的序列切分时要考虑的:

  • 问题 1:两类计算如何分配,保证负载均衡?
  • 问题 2:已有的 prefix cache 如何从存储位置传递到计算位置(prefix cache 可能不在 GPU 上)。

这里举例一种简单的实现方式,将 prefix_cache_seq_len 和 tail_seq_len 进行均分,每个 GPU 拿取一份,同时进行 Q 分片的轮转,如下图所示:

图片

Q-pass ring attention 优势:当切分比例保持不变且 KV 值每次计算后都在本地显存中,则无须进行 KV 分片的搬运(解决了问题 2)。

当 prefix cache 在其它存储位置时比如内存中,依然要考虑重新切分和 cache 的传输问题。

该方式在计算的时候可能需要进一步拆分,因为按照 SP 切分后的 KV 序列与 Q 序列不一定相等:prefix_cache_seq_len/SP != tail_seq_len/SP。若相等,理想情况下一次迭代计算中只需要两次 attention 运算。

3.2. 张量并行

Attention 的张量并行分为两个部分:QKV 运算、线性投影运算。

QKV 运算的 TP 切分一般针对 heads 维度,相当于矩阵的并行的批量运算。attention 计算切 heads,每个 rank(GPU) 拿到不同的 head,最后进行 heads 还原(allgather);attention 前后的线性运算按照 hidden_dim/heads 方式切分。

Attention 的张量并行与其它层的并行可结合使用降低通信/计算量,这里给出 megatron 中 TP-SP 结合的例子:

图片

目前的 DeepSeek 中用的 attention 模块 MLA 与 MHA 的计算在线性层的处理中有些不同,导致其线性层的 TP 的并行策略设计也要做对应的调整。

这里做一个详细的展开讨论,首先看一下 MLA 的计算公式:

图片

MLA 相比 MHA 的主要特点是:线性运算多了一个步骤:在 QKV 生成之前,先要做降维 down_project 线性运算,然后进行升维 up_project 线性运算。分头运算是在 up_project 阶段。

根据公式绘制运算流图,非矩阵吸收的 MLA(KV up_project 运算在压缩计算后)如下所示,这种形态常用在 prefill 阶段(Q 的输入为完整序列时)。

图片

MLA 一般形态主要的计算步骤:

  • 1、Q、K、V 的下采样线性计算
  • 2、Q 上采样运算
  • 3、KV 上采样运算
  • 4、attention 计算
  • 5、O 线性运算

分别来看一下这些计算步骤的 TP 策略:

  • 下采样阶段还没有 heads,所以一般而言 Q、K、V 的下采样不进行 TP 切分。考虑到独立切分适应任何场景,也可参考前面提的矩阵切分场景来分割下采样。
  • 如果按照 heads 进行切分的话,步骤 2、3 的 W 矩阵需要进行列切 (column split),步骤 5 的 W 矩阵行切 (row split);
  • 步骤 4 的 attention 则进行 heads 维度切分 (head split)。

MLA 还有第二种形态,就是将 KV 的 up_project 运算移动到 Q 上面,即 K、V 输入没有了 up_project 计算(多 heads 通过广播实现)。MLA 吸收矩阵形态的逻辑如下所示:

图片

MLA(吸收矩阵)常用在 decode 中,这种形态的 TP 切分策略:

  • 步骤 1 不采用 TP 切分;
  • 步骤 2 的 W 矩阵需要进行列切 (column split);
  • 步骤 5 的 W 矩阵行切 (row split);
  • 步骤 4 的 attention 则进行 heads 维度切分 (head split);
  • 步骤 3 的两次线性运算均进行 heads 维度切分 (head split);

注:MLA 的 KV 矩阵吸收运算(W_UK -> W_UQ W_UV -> W_O)是重新建立一个线性映射矩阵(即,两次的 W_UK 不相等),MLA 的矩阵吸收的证明见参考文献 7。

4. 附 1:常见的集群通信方式

图片

图片

图片


5. 参考文献

  1. DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model
  2. 集群通信 - hccl
  3. [Megatron2]: Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM
  4. [Megatron3]: Reducing Activation Recomputation in Large Transformer Models
  5. Attention Is All You Need
  6. arXiv:1911.02972
  7. Ring Attention with Blockwise Transformers for Near-Infinite Context
  8. [CONTEXT PARALLELISM] 对 ring attention 实现方式分析:arXiv:2411.01783
  9. deepseek 技术解读 (1)- 彻底理解 MLA(Multi-Head Latent Attentio…
  10. [merged attention] FLASHINFER: EFFICIENT AND CUSTOMIZABLE ATTENTION ENGINE FOR LLM INFERENCE SERVING
  11. [tree-attention] arXiv:2408.04093

更多推理框架分析知识:知乎专栏《LLM 推理优化必备知识》。欢迎点赞、收藏、关注!