大模型预处理技术- 去除无效填充序列

193 阅读5分钟

背景

大模型计算中,往往会引入Padding操作处理不等长序列,但是这些Padding的token会引发冗余计算的问题,因此需要把Padding的内容丢掉,直接使用有效的token进行attention的计算。在Paddle中,是使用get_padding_offset操作完成这一步骤的。get_padding_offset函数的定义在这里

def get_padding_offset(bsz, max_seq_len, seq_lens_this_time):
    # 计算每个序列需要填充的长度,并累加得到累计偏移量
    cum_offsets_now = paddle.cumsum(max_seq_len - seq_lens_this_time)
    # 创建累计偏移量张量,长度为bsz+1,第一个元素为0
    cum_offsets = paddle.zeros(shape=(bsz + 1), dtype="int32")
    cum_offsets[1:] = cum_offsets_now # 将累加结果放入cum_offsets[1:]

    # 计算所有序列的实际token总数(不包含填充部分)
    token_num = paddle.sum(seq_lens_this_time)

    # 初始化填充偏移量张量,长度为所有实际token数
    padding_offsets = paddle.zeros(shape=(token_num), dtype="int32")
    # 初始化查询Q和键K的累计序列长度张量
    cu_seqlens_q = paddle.zeros(shape=(bsz + 1), dtype="int32")
    cu_seqlens_k = paddle.zeros(shape=(bsz + 1), dtype="int32")

    # 遍历批次中的每个序列
    for i in range(bsz):
        seq_len_now = seq_lens_this_time[i] # 当前序列的实际长度
        cum_offset = cum_offsets[i] # 当前序列的累计偏移量
        # 遍历当前序列中的每个token
        for j in range(seq_len_now): 
            # 计算该token在预填充后序列中的位置,并设置其预填充偏移量
            padding_offsets[i * max_seq_len + j - cum_offset] = cum_offset
            # 计算累计序列长度(用于高效实现注意力机制)
            cum_seq_len = (i + 1) * max_seq_len - cum_offsets[i + 1]
            cu_seqlens_q[i + 1] = cum_seq_len
            cu_seqlens_k[i + 1] = cum_seq_len
    return padding_offsets, cum_offsets[:-1], cu_seqlens_q, cu_seqlens_k

get_padding_offset说明

作用:get_padding_offset主要用于批处理中不同序列长度的填充问题,生成注意力机制中所需要的偏移量和累计序列长度信息。在Transformer模型的优化实现中,特别是在处理不等长序列时经常会用到类似的方法。

参数说明:

  • bsz: 批次大小
  • max_seq_len: 序列的最大长度
  • seq_lens_this_time: 当前批次中每个序列的实际长度(Tensor)

核心功能:生成四个关键输出

  • padding_offsets: 每个token在填充后的序列中的偏移量
  • cum_offsets: 每个序列的累计偏移量
  • cu_seqlens_q: 查询矩阵Q的累计序列长度
  • cu_seqlens_k: 键矩阵K的累计序列长度

数学原理

这个函数实现了一种高效处理不等长序列的方法,主要用于优化Transformer模型中的注意力计算。在处理不等长序列时,通常有两种策略

  • 填充:将所有序列填充到长度相同,会导致计算冗余
  • 压缩:将所有有效token排列到一起,避免计算冗余 这个函数实现了压缩策略,通过生成padding_offsets和cu_seqlens张量,可以
  • 高效的将填充后的张量转换为压缩形式
  • 在注意力机制时,快速定位每个token在原始序列中的位置
  • 避免在预填充位置上进行无用的计算,提高内存和计算效率

具体解释

假设bsz=3, max_seq_len=5, seq_lens_this_time=[3,4,2]

1、首先计算便宜量

cum_offsets_now = paddle.cumsum(max_seq_len - seq_lens_this_time)
# 填充长度 = [5-3, 5-4, 5-2] = [2, 1, 3]
# 累积填充长度 = [2, 2+1, 2+1+3] = [2, 3, 6]

2、初始化累计偏移量数组

cum_offsets = [0, 2, 3, 6]  # 长度为bsz+1=4

3、计算token总数

token_num = sum([3, 4, 2]) = 9

4、填充偏移量和累计序列长度计算

序列1:(长度=3)

  • 累计偏移量cum_offset=cum_offsets[0] = 0
  • 循环填充padding_offsetscu_seqlens
ji*max_seq_len - cum_offset + jpadding_offsets 值cum_seq_lencu_seqlens_q/k[1]
00*5 - 0 + 0 = 005-2=33
10*5 - 0 + 1 = 105-2=33
20*5 - 0 + 2 = 205-2=33

序列2:(长度=4)

  • 累计偏移量cum_offset = cum_offsets[1] = 2
  • 循环填充padding_offsetscu_seqlens
ji*max_seq_len - cum_offset + jpadding_offsets 值cum_seq_lencu_seqlens_q/k[2]
01*5 - 2 + 0 = 3210-3=77
11*5 - 2 + 1 = 4210-3=77
21*5 - 2 + 2 = 5210-3=77
31*5 - 2 + 3 = 6210-3=77

序列 3 (长度 = 2)

  • 累积偏移量 cum_offset = cum_offsets[2] = 3
  • 循环填充 padding_offsetscu_seqlens
ji*max_seq_len - cum_offset + jpadding_offsets 值cum_seq_lencu_seqlens_q/k[3]
02*5 - 3 + 0 = 7315-6=99
12*5 - 3 + 1 = 8315-6=99

5、最终结果

padding_offsets = [0, 0, 0, 2, 2, 2, 2, 3, 3]
cum_offsets = [0, 2, 3]  # 去掉最后一个元素
cu_seqlens_q = [0, 3, 7, 9]
cu_seqlens_k = [0, 3, 7, 9]

实际使用中的意义

假设我们有三个填充后的序列

seq1: [a1, a2, a3, 0, 0]
seq2: [b1, b2, b3, b4, 0]
seq3: [c1, c2, 0, 0, 0]

填充后的矩阵形状是[3,5], 但实际有效token只有9个,使用我们计算padding_offsets和cu_seqlens, 我们可以

1、将填充后的矩阵压缩为[9]的一维数组

compressed = [a1, a2, a3, b1, b2, b3, b4, c1, c2]

2、在计算注意力时,快速定位每个token的位置

  • padding_offsets告诉我们每个token在原始填充序列的偏移量
  • cu_seqlens告诉我们每个序列在压缩数组中的起始位置

这种方法避免了在填充位置上进行无用注意力计算,显著提高了计算效率,特别是在处理大量不等长序列时。