推理时不能并行计算所有位置的主要原因在于生成文本的过程是自回归的,也就是说,生成每个新的单词都依赖于之前已经生成的单词。这个过程需要一步一步地进行,因为每一步的输出会成为下一步的输入。下面是对这个过程的详细解释:
自回归模型的推理过程
自回归模型(如GPT-2)在推理时是基于已经生成的上下文来预测下一个单词的。具体来说:
- 初始化:从一个初始种子文本(例如 “and”)开始。
- 逐步生成:模型使用当前的输入序列来预测下一个单词。
- 更新输入:将新生成的单词追加到输入序列中。
- 重复:重复上述步骤,使用更新后的输入序列来生成下一个单词。
为什么不能并行计算所有位置
依赖关系:每个新生成的单词依赖于之前生成的所有单词。这意味着在生成下一个单词之前,必须先生成当前的单词。
例如,如果输入是 “and” 并且模型生成了 “established”,那么在生成 “established” 之前不能生成 “himself”。
逐步更新:输入序列会在每一步更新,新生成的单词会追加到输入序列中作为下一步的输入。这种更新是顺序的,不能提前知道之后的单词。
非平行计算:因为每一步的输出依赖于前一步的结果,所以不能并行计算每一步的输出。每一步都必须等待前一步的结果,然后才能继续。
训练时的并行计算
在训练过程中,模型已经拥有整个目标序列,可以一次性计算所有位置的预测。这是因为训练数据包含了完整的输入和目标序列( 通过右移动一位,获得了多个训练样本
),模型可以在一次前向传播中计算所有位置的预测结果,并与目标序列进行比较来计算损失。
示例:逐步生成 vs. 并行计算
逐步生成
以下是逐步生成文本的示例代码,每一步都依赖于前一步的结果:
import torch
from transformers import GPT2Tokenizer, GPT2LMHeadModel
# 加载预训练模型和tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
model = GPT2LMHeadModel.from_pretrained('gpt2')
# 初始种子文本
input_text = "and"
input_ids = tokenizer.encode(input_text, return_tensors='pt')
# 逐步生成文本
for _ in range(5): # 假设生成5个词
outputs = model(input_ids)
logits = outputs.logits
next_token_id = torch.argmax(logits[:, -1, :], axis=-1)
input_ids = torch.cat([input_ids, next_token_id.unsqueeze(-1)], axis=-1)
next_token = tokenizer.decode(next_token_id)
print(f"Current input: {tokenizer.decode(input_ids[0])}")
print(f"New token: {next_token}\n")
并行计算(仅在训练时有效)
以下是训练时的一次性计算所有位置预测结果的示例代码:
import torch
import torch.nn as nn
from transformers import GPT2Tokenizer, GPT2Config, GPT2Model
class GPT2LMHeadModel(nn.Module):
def __init__(self, config):
super(GPT2LMHeadModel, self).__init__()
self.transformer = GPT2Model(config)
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
def forward(self, input_ids, labels=None):
transformer_outputs = self.transformer(input_ids)
hidden_states = transformer_outputs.last_hidden_state
# 通过语言模型头(线性层)生成logits
logits = self.lm_head(hidden_states)
loss = None
if labels is not None:
# Shift logits and labels for loss computation
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Compute the loss using CrossEntropy
loss_fct = nn.CrossEntropyLoss()
loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
return (loss, logits) if loss is not None else logits
# 加载配置和模型
config = GPT2Config()
model = GPT2LMHeadModel(config)
# 加载预训练的tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
# 准备输入
input_text = "and established himself in a manner that was both deliberate and efficient."
input_ids = tokenizer.encode(input_text, return_tensors='pt')
# 模型前向传播
outputs = model(input_ids, labels=input_ids)
# 提取损失和logits
loss, logits = outputs
print(f"Loss: {loss.item()}")
print(f"Logits shape: {logits.shape}")
并行计算:
在训练过程中,所有token的自注意力计算是同时进行的,这意味着每个token都能在一次计算中获取到所有其他预测的token的信息。
这使得模型能够一次性计算所有位置的预测结果。
总结
1、训练时候上下文是已知的可并行
2、训练时候是一次得到所有预测的token
3、推理时候一次只能得到一个token
这说明了训练时候用GPU的高效率,其实推理时候可以用CPU的,如果推理时候CPU里的并行计算矩阵也有,那么cpu成本更低。其次例如特有的LPU(语言处理器)为什么这么快了
在推理过程中,生成新单词必须依赖之前生成的单词,因此每一步都需要等待前一步的结果,这使得并行计算变得不可能。而在训练过程中,由于模型可以一次性获取整个输入和目标序列,所以可以并行计算所有位置的预测结果。这个区别是自回归模型固有的特性,确保模型在生成文本时能够逐步利用上下文信息。