准备模型前向传播的输入¶
目的¶
执行模型前向传播所需的信息:
- 输入数据
- 输入数据对应的注意力元数据
下图展示了模型推理需要准备的内容。
因此,只要具备上述两类信息,即可执行模型的前向传播。
本文档将说明**如何获取输入及其对应的注意力元数据**。
概述¶
1. 获取输入¶
获取输入的工作流程:
-
获取
token positions:每个token在其请求序列中的相对位置。 -
获取
token indices:每个被调度token在token表中的索引。 -
获取
Token IDs:通过token索引从**token id表**中检索Token ID。
最后,这些Token IDs需要输入模型,同时positions也需要传入模型以生成RoPE(旋转位置编码)。这两者都是模型的输入。
注意:Token IDs是模型的输入,因此我们也称其为Input IDs。
2. 构建输入注意力元数据¶
模型在前向传播过程中需要以下注意力元数据:
query start location:每个请求对应被调度token的起始和结束位置。sequence length:每个请求的长度,包括已计算token和新调度的token。number of computed tokens:每个请求已计算的token数量。number of requests:当前批次中的请求数量。number of tokens:当前批次中被调度的token总数。block table:将每个块的逻辑地址(在其序列内)转换为设备内存中的全局物理地址。max query len:当前请求批次中最长的被调度token长度。slot mapping:每个输入token将被存储到的索引位置。attention mask:在softmax之前应用于注意力分数的掩码矩阵,用于控制哪些token可以相互关注(通常为因果注意力)。
开始之前¶
主要存在三种类型的变量。
- token级别:表示每个被调度token对应的一个属性,因此该变量的长度为被调度token的数量。
- request级别:表示每个被调度请求的一个属性,其长度通常为被调度请求的数量。(
query start location是一个特例,它多一个元素。) - 系统级别:
- Token IDs表:存储每个请求的token ID(即模型的输入)。该表的形状为
(max num request, max model len)。其中,max num request是前向批次中允许的最大并发请求数,max model len是该模型中单个请求序列能处理的最大token数量。 - Block表:将每个块的逻辑地址(在其序列内)转换为设备内存中的全局物理地址。该表的形状为
(max num request, max model len / block size)
注意:这两个表都来自**准备输入**之前的_update_states方法。如需更多启发,可以查看该方法。
提示¶
简单来说,token ID是一个**整数**(通常为int32),代表一个token。
Token ID示例:
| Token ID | Token |
|--------------|---------------|
| 0 | [PAD] |
| 1 | <|endoftext|> |
| 2 | <|start|> |
| 3 | [SEP] |
| 4 | I |
| 5 | the |
| 6 | be |
| 7 | of |
| 8 | and |
| ... | ... |
| ... | ... |
| vocab_size-1 | <|im_end|> |
详细流程¶
假设条件:
- 单次最大可调度token数:10
block size:2- 总共调度3个请求。它们的提示长度分别为3、2和8。
max model length:12(模型中单个请求序列能处理的最大token数量)。
这些假设条件在启动vLLM时配置。它们并非固定不变,您可以手动设置。
步骤1:所有请求处于预填充阶段¶
获取输入¶
由于单次最大可调度token数为10,每个请求的被调度token可表示为{'0': 3, '1': 2, '2': 5}。注意,request_2使用了分块预填充,剩余3个提示token未被调度。
1. 获取token位置¶
首先,确定每个token所属的请求:token 0–2分配给**request_0**,token 3–4分配给**request_1**,token 5–9分配给**request_2**。为了表示这种映射关系,我们使用request indices,例如request indices:[0, 0, 0, 1, 1, 2, 2, 2, 2, 2]。
对于每个请求,使用**已计算token数** + 当前被调度token的相对位置(request_0: [0 + 0, 0 + 1, 0 + 2],request_1: [0 + 0, 0 + 1],request_2: [0 + 0, 0 + 1,..., 0 + 4]),然后将它们拼接在一起([0, 1, 2, 0, 1, 0, 1, 2, 3, 4])。
注意:在实际代码中,有一种更高效的方法(使用request indices)来创建位置。
最终,token positions为[0, 1, 2, 0, 1, 0, 1, 2, 3, 4]。该变量属于**token级别**。
2. 获取token索引¶
当前**Token IDs表**的形状为(max num request, max model len)。
为什么表中存在T_3_5、T_3_6、T_3_7这些未被调度的token?
- 我们将一个请求序列中的所有Token ID一次性填入该表,但只检索本次调度的token。剩余的Token ID将在下次检索。
| T_0_0 | T_0_1 | T_0_2 | ? | ? | ? | ? | ? | ? | ? | ? | ? |
| T_1_0 | T_1_1 | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
| T_2_0 | T_2_1 | T_3_2 | T_3_3 | T_3_4 | T_3_5 | T_3_6 | T_3_7 | ? | ? | ? | ? |
| ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? |
......
......
......
注意,T_x_x 是一个 int32 类型。
假设 M = max model len。然后我们可以使用 token positions 以及每个 token 的 request indices 来构建 token indices。
因此 token indices = [0 + 0 * M, 1 + 0 * M, 2 + 0 * M, 0 + 1 * M, 1 + 1 * M, 0 + 2 * M, 1 + 2 * M, 2 + 2 * M, 3 + 2 * M, 4 + 2 * M] = [0, 1, 2, 12, 13, 24, 25, 26, 27, 28]
3. 获取 Token ID¶
我们使用 token indices 从 token 表中选出对应的 Input IDs。伪代码如下:
如前所述,我们将这些 Token IDs 称为 Input IDs。
Input IDs=[T_0_0, T_0_1, T_0_2, T_1_0, T_1_1, T_2_0, T_2_1, T_3_2, T_3_3, T_3_4]
构建输入注意力元数据¶
在当前 Block Table 中,我们使用第一个 block(即 block_0)来标记未使用的 block。该 block 的形状为 (max num request, max model len / block size),其中 max model len / block size = 12 / 2 = 6。
| 1 | 2 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 |
| 4 | 5 | 6 | 0 | 0 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 |
......
......
......
设备内存中的 KV 缓存 block 如下所示:
假设 K = max model len / block size = 6,我们可以得到 token 的 device block number。
实现 slot mapping 的工作流程:
- 使用
block table indices、K和positions获取request indices。
目的:对于每个 token,它可用于从 device block number 中选择 block table。
- 使用
device block number获取block table indices。
目的:device block number 指示每个 token 属于哪个设备 block。
- 使用
block offsets和positions获取block size。
目的:block offsets 指示每个 token 在 block 内的偏移量。
- 使用
slot mapping和device block number构建block offsets。
目的:我们可以使用 slot mapping 将 Token ID 存储到 token 槽位中。
详细信息:
- (Token 级别) 使用简单公式计算
block table indices:request indices * K + positions / block size。因此结果为[0 * 6 + 0 / 2, 0 * 6 + 1 / 2, 0 * 6 + 2 / 2, 1 * 6 + 0 / 2, 1 * 6 + 1 / 2, 2 * 6 + 0 / 2, 2 * 6 + 1 / 2, 2 * 6 + 2 / 2, 2 * 6 + 3 / 2, 2 * 6 + 4 / 2] = [0, 0, 1, 6, 6, 12, 12, 13, 13, 14]。这可用于从device block number中选择block table。 - (Token 级别) 使用
block table indices为每个调度的 token 选出device block number。伪代码为block_numbers = block_table[block_table_indices]。因此device block number=[1, 1, 2, 3, 3, 4, 4, 5, 5, 6] - (Token 级别)
block offsets可通过block offsets = positions % block size = [0, 1, 0, 0, 1, 0, 1, 0, 1, 0]计算。 - 最后,使用
block offsets和device block number创建slot mapping:device block number * block size + block_offsets = [2, 3, 4, 6, 7, 8, 9, 10, 11, 12]
(Request 级别) 由于我们知道调度的 token 数量为 [3, 2, 5]:
- (Request 级别) 使用前缀和计算
query start location:[0, 3, 5, 10]。 - (Request 级别) 步骤 1 中的所有 token 都处于 prefill 阶段,且已计算的 token 数量为 0;因此
sequence length=[3, 2, 5]。 - (Request 级别) 如上所述,
number of computed tokens全为 0:[0, 0, 0]。 number of requests:3- (Request 级别)
number of tokens:[3, 2, 5] max query len:5- (Token 级别)
slot mapping:[2, 3, 4, 6, 7, 8, 9, 10, 11, 12] attention mask:对于所有发起 prefill 过程的请求,我们仅创建一个掩码矩阵以便在不同请求间复用。该掩码矩阵的形状为5 * 5:
步骤 2:分块预填充¶
在步骤 2 中,我们不再提供解释或进行计算,而是直接给出最终结果。
获取输入¶
每个请求调度的 token:{'0': 1, '1': 1, '2': 3}
request indices:[0, 1, 2, 2, 2]-
token positions:[3, 2, 5, 6, 7]当前 Token ID 表:
| T_0_0 | T_0_1 | T_0_2 | T_0_3 | ? | ? | ? | ? | ? | ? | ? | ? | | T_1_0 | T_1_1 | T_1_2 | ? | ? | ? | ? | ? | ? | ? | ? | ? | | T_2_0 | T_2_1 | T_3_2 | T_3_3 | T_3_4 | T_3_5 | T_3_6 | T_3_7 | ? | ? | ? | ? | | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ? | ...... ...... ......注意:T_0_3、T_1_2 分别是 request_0 和 request_1 的新 Token ID。它们是从模型输出中采样得到的。
-
token indices:[3, 14, 29, 30, 31] Input IDs:[T_0_3, T_1_2, T_3_5, T_3_6, T_3_7]
构建输入注意力元数据¶
我们将 block 7 和 8 分别分配给 request_1 和 request_2,因为它们在 token 生成或分块预填充后需要在设备上占用更多空间来存储 KV 缓存。
当前 Block Table:
| 1 | 2 | 0 | 0 | 0 | 0 |
| 3 | 7 | 0 | 0 | 0 | 0 |
| 4 | 5 | 6 | 8 | 0 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 |
......
......
......
设备内存中的 KV 缓存 block:
- (Token 级别)
block table indices:[1, 7, 14, 15, 15] - (Token 级别)
device block number:[2, 7, 6, 8, 8] - (Token 级别)
block offsets:[1, 0, 1, 0, 1] - (Token 级别)
slot mapping:[5, 14, 13, 16, 17]
调度的 token 数量:[1, 1, 3]
-
query start location:[0, 1, 2, 5] -
sequence length:[4, 3, 8] -
number of computed tokens:[3, 2, 5] -
number of requests:3 -
max query len:3 -
slot mapping:[5, 14, 13, 16, 17] -
attention mask:5 * 8
每个 token 都有一个 1 * 8 的向量,共有 5 个调度的 token。
最后¶
如果你理解了步骤 1 和步骤 2,你就会明白后续的所有步骤。
希望本文档能帮助你更好地理解 vLLM 如何为模型前向传播准备输入。如果你有任何好的想法,欢迎向我们贡献。