#GPT~从0构建
GPT早已成为大模型时代的基础。国外一位开发者发布了一篇实践指南,仅用60行代码构建GPT。
60行代码,从头开始构建GPT?
最近,一位开发者做了一个实践指南,用Numpy代码从头开始实现GPT。
你还可以将 OpenAI发布的GPT-2模型权重加载到构建的GPT中,并生成一些文本。
话不多说,直接开始构建GPT。
什么是GPT?
GPT代表生成式预训练Transformer,是一种基于Transformer的神经网络结构。
- 生成式(Generative):GPT生成文本。
- 预训练(Pre-trained):GPT是根据书本、互联网等中的大量文本进行训练的。
- Transformer:GPT是一种仅用于解码器的Transformer神经网络。
大模型,如OpenAI的GPT-3、谷歌的LaMDA,以及Cohere的Command XLarge,背后都是GPT。它们的特别之处在于, 1) 非常大(拥有数十亿个参数),2) 受过大量数据(数百GB的文本)的训练。
直白讲,GPT会在提示符下生成文本。
即便使用非常简单的API(输入=文本,输出=文本),一个训练有素的GPT也可以做一些非常棒的事情,比如写邮件,总结一本书,为Instagram发帖提供想法,给5岁的孩子解释黑洞,用SQL编写代码,甚至写遗嘱。
以上就是 GPT 及其功能的高级概述。让我们深入了解更多细节。
输入/输出
GPT定义输入和输出的格式大致如下所示:
def gpt(inputs: list[int]) -> list[list[float]]:
# inputs has shape [n_seq]
# output has shape [n_seq, n_vocab]
output = # beep boop neural network magic
return output
输入是由映射到文本中的token的一系列整数表示的一些文本:
# integers represent tokens in our text, for example:# text = "not all heroes wear capes":# tokens = "not" "all" "heroes" "wear" "capes"
inputs = [1, 0, 2, 4, 6]
Token是文本的子片段,使用分词器生成。我们可以使用词汇表将token映射到整数:
# the index of a token in the vocab represents the integer id for that token# i.e. the integer id for "heroes" would be 2, since vocab[2] = "heroes"
vocab = ["all", "not", "heroes", "the", "wear", ".", "capes"]
# a pretend tokenizer that tokenizes on whitespace
tokenizer = WhitespaceTokenizer(vocab)
# the encode() method converts a str -> list[int]
ids = tokenizer.encode("not all heroes wear") # ids = [1, 0, 2, 4]# we can see what the actual tokens are via our vocab mapping
tokens = [tokenizer.vocab[i] for i in ids] # tokens = ["not", "all", "heroes", "wear"]# the decode() method converts back a list[int] -> str
text = tokenizer.decode(ids) # text = "not all heroes wear"
简而言之:
- 有一个字符串。
- 使用分词器将其分解成称为token的小块。
- 使用词汇表将这些token映射为整数。
在实践中,我们会使用更先进的分词方法,而不是简单地用空白来分割,比如字节对编码(BPE)或WordPiece,但原理是一样的:
vocab将字符串token映射为整数索引
encode方法,可以转换str -> list[int]
decode 方法,可以转换 list[int] -> str ([2])
输出
输出是一个二维数组,其中 output[i][j] 是模型预测的概率,即 vocab[j] 处的token是下一个tokeninputs[i+1] 。例如:
vocab = ["all", "not", "heroes", "the", "wear", ".", "capes"]
inputs = [1, 0, 2, 4] # "not" "all" "heroes" "wear"
output = gpt(inputs)
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[0] = [0.75 0.1 0.0 0.15 0.0 0.0 0.0 ]
# given just "not", the model predicts the word "all" with the highest probability
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[1] = [0.0 0.0 0.8 0.1 0.0 0.0 0.1 ]
# given the sequence ["not", "all"], the model predicts the word "heroes" with the highest probability
# ["all", "not", "heroes", "the", "wear", ".", "capes"]
# output[-1] = [0.0 0.0 0.0 0.1 0.0 0.05 0.85 ]
# given the whole sequence ["not", "all", "heroes", "wear"], the model predicts the word "capes" with the highest probability
要获得整个序列的下一个token预测,我们只需获取 output[-1] 中概率最高的token:
vocab = ["all", "not", "heroes", "the", "wear", ".", "capes"]
inputs = [1, 0, 2, 4] # "not" "all" "heroes" "wear"
output = gpt(inputs)
next_token_id = np.argmax(output[-1]) # next_token_id = 6
next_token = vocab[next_token_id] # next_token = "capes"
将概率最高的token作为我们的预测,称为贪婪解码(Greedy Decoding)或贪婪采样(greedy sampling)。
预测序列中的下一个逻辑词的任务称为语言建模。因此,我们可以将GPT称为语言模型。
生成一个单词很酷,但整个句子、段落等又如何呢?
生成文本
自回归
我们可以通过迭代从模型中获得下一个token预测来生成完整的句子。在每次迭代中,我们将预测的token追加回输入:
def generate(inputs, n_tokens_to_generate):
for _ in range(n_tokens_to_generate): # auto-regressive decode loop
output = gpt(inputs) # model forward pass
next_id = np.argmax(output[-1]) # greedy sampling
inputs.append(int(next_id)) # append prediction to input
return inputs[len(inputs) - n_tokens_to_generate :] # only return generated ids
input_ids = [1, 0] # "not" "all"
output_ids = generate(input_ids, 3) # output_ids = [2, 4, 6]
output_tokens = [vocab[i] for i in output_ids] # "heroes" "wear" "capes"
这个预测未来值(回归)并将其添加回输入(自)的过程,就是为什么你可能会看到GPT被描述为自回归的原因。
采样
我们可以从概率分布中采样,而不是贪婪采样,从而为生成的引入一些随机性:
inputs = [1, 0, 2, 4] # "not" "all" "heroes" "wear"
output = gpt(inputs)
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # hats
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # capes
np.random.choice(np.arange(vocab_size), p=output[-1]) # pants
这样,我们就能在输入相同内容的情况下生成不同的句子。
如果与top-k、top-p和温度等在采样前修改分布的技术相结合,我们的输出质量就会大大提高。
这些技术还引入了一些超参数,我们可以利用它们来获得不同的生成行为(例如,提高温度会让我们的模型承担更多风险,从而更具「创造性」)。
训练
我们可以像训练其他神经网络一样,使用梯度下降法训练GPT,并计算损失函数。对于GPT,我们采用语言建模任务的交叉熵损失:
def lm_loss(inputs: list[int], params) -> float:
# the labels y are just the input shifted 1 to the left
#
# inputs = [not, all, heros, wear, capes]
# x = [not, all, heroes, wear]
# y = [all, heroes, wear, capes]
#
# of course, we don't have a label for inputs[-1], so we exclude it from x
#
# as such, for N inputs, we have N - 1 langauge modeling example pairs
x, y = inputs[:-1], inputs[1:]
# forward pass
# all the predicted next token probability distributions at each position
output = gpt(x, params)
# cross entropy loss
# we take the average over all N-1 examples
loss = np.mean(-np.log(output[y]))
return loss
def train(texts: list[list[str]], params) -> float:
for text in texts:
inputs = tokenizer.encode(text)
loss = lm_loss(inputs, params)
gradients = compute_gradients_via_backpropagation(loss, params)
params = gradient_descent_update_step(gradients, params)
return params
这是一个经过大量简化的训练设置,但可以说明问题。
请注意,我们在gpt函数签名中添加了params (为了简单起见,我们在前面的章节中没有添加)。在训练循环的每一次迭代期间:
- 对于给定的输入文本实例,计算了语言建模损失
- 损失决定了我们通过反向传播计算的梯度
- 我们使用梯度来更新我们的模型参数,以使损失最小化(梯度下降)
请注意,我们不使用显式标记的数据。相反,我们能够仅从原始文本本身生成输入/标签对。这被称为自监督学习。
自监督使我们能够大规模扩展训练数据,只需获得尽可能多的原始文本并将其投放到模型中。例如,GPT-3接受了来自互联网和书籍的3000亿个文本token的训练:
当然,你需要一个足够大的模型才能从所有这些数据中学习,这就是为什么GPT-3有1750亿个参数,训练的计算成本可能在100万至1000万美元之间。
这个自监督的训练步骤被称为预训练,因为我们可以重复使用「预训练」的模型权重来进一步训练模型的下游任务。预训练的模型有时也称为「基础模型」。
在下游任务上训练模型称为微调,因为模型权重已经经过了理解语言的预训练,只是针对手头的特定任务进行了微调。
「一般任务的前期训练+特定任务的微调」策略被称为迁移学习。
提示
原则上,最初的GPT论文只是关于预训练Transformer模型用于迁移学习的好处。
论文表明,当对标记数据集进行微调时,预训练的117M GPT在各种自然语言处理任务中获得了最先进的性能。
直到GPT-2和GPT-3论文发表后,我们才意识到,基于足够的数据和参数预训练的GPT模型,本身能够执行任何任务,不需要微调。
只需提示模型,执行自回归语言建模,然后模型就会神奇地给出适当的响应。这就是所谓的「上下文学习」(in-context learning),因为模型只是利用提示的上下文来完成任务。
语境中学习可以是0次、一次或多次。
在给定提示的情况下生成文本也称为条件生成,因为我们的模型是根据某些输入生成一些输出的。
GPT并不局限于NLP任务。
你可以根据你想要的任何条件来微调这个模型。比如,你可以将GPT转换为聊天机器人(如ChatGPT),方法是以对话历史为条件。
说到这里,让我们最后来看看实际的实现。
设置
克隆本教程的存储库:
git clone https://github.com/jaymody/picoGPT
cd picoGPT
然后安装依赖项:
pip install -r requirements.txt
注意:这段代码是用Python 3.9.10测试的。
每个文件的简单分类:
- encoder.py包含OpenAI的BPE分词器的代码,这些代码直接取自gpt-2 repo。
- utils.py包含下载和加载GPT-2模型权重、分词器和超参数的代码。- gpt2.py包含实际的GPT模型和生成代码,我们可以将其作为python脚本运行。- gpt2_pico.py与gpt2.py相同,但代码行数更少。
我们将从头开始重新实现gpt2.py ,所以让我们删除它并将其重新创建为一个空文件:
rm gpt2.py
touch gpt2.py
首先,将以下代码粘贴到gpt2.py中:
import numpy as np
def gpt2(inputs, wte, wpe, blocks, ln_f, n_head):
pass # TODO: implement this
def generate(inputs, params, n_head, n_tokens_to_generate):
from tqdm import tqdm
for _ in tqdm(range(n_tokens_to_generate), "generating"): # auto-regressive decode loop
logits = gpt2(inputs, **params, n_head=n_head) # model forward pass
next_id = np.argmax(logits[-1]) # greedy sampling
inputs.append(int(next_id)) # append prediction to input
return inputs[len(inputs) - n_tokens_to_generate :] # only return generated ids
def main(prompt: str, n_tokens_to_generate: int = 40, model_size: str = "124M", models_dir: str = "models"):
from utils import load_encoder_hparams_and_params
# load encoder, hparams, and params from the released open-ai gpt-2 files
encoder, hparams, params = load_encoder_hparams_and_params(model_size, models_dir)
# encode the input string using the BPE tokenizer
input_ids = encoder.encode(prompt)
# make sure we are not surpassing the max sequence length of our model
assert len(input_ids) + n_tokens_to_generate < hparams["n_ctx"]
# generate output ids
output_ids = generate(input_ids, params, hparams["n_head"], n_tokens_to_generate)
# decode the ids back into a string
output_text = encoder.decode(output_ids)
return output_text
if __name__ == "__main__":
import fire
fire.Fire(main)
将4个部分分别分解为:
- gpt2函数是我们将要实现的实际GPT代码。你会注意到,除了inputs之外,函数签名还包括一些额外的内容:
wte、 wpe、 blocks和ln_f是我们模型的参数。
n_head是前向传递过程中需要的超参数。
- generate函数是我们前面看到的自回归解码算法。为了简单起见,我们使用贪婪抽样。tqdm是一个进度条,帮助我们可视化解码过程,因为它一次生成一个token。
- main函数处理:
加载分词器(encoder)、模型权重(params)和超参数(hparams)
使用分词器将输入提示编码为token ID
调用生成函数
将输出ID解码为字符串
fire.Fire(main)只是将我们的文件转换为CLI应用程序,因此我们最终可以使用python gpt2.py "some prompt here"运行代码
让我们更详细地了解一下笔记本中的encoder 、 hparams和params,或者在交互式的Python会话中,运行:
from utils import load_encoder_hparams_and_params
encoder, hparams, params = load_encoder_hparams_and_params("124M", "models")
这将把必要的模型和分词器文件下载到models/124M ,并将encoder、 hparams和params加载到我们的代码中。
编码器
encoder是GPT-2使用的BPE分词器:
ids = encoder.encode("Not all heroes wear capes.")
ids
[3673, 477, 10281, 5806, 1451, 274, 13]
encoder.decode(ids)
"Not all heroes wear capes."
使用分词器的词汇表(存储在encoder.decoder中),我们可以看到实际的token是什么样子的:
[encoder.decoder[i] for i in ids]
['Not', 'Ġall', 'Ġheroes', 'Ġwear', 'Ġcap', 'es', '.']
请注意,我们的token有时是单词(例如Not),有时是单词但前面有空格(例如Ġall,Ġ表示空格),有时是单词的一部分(例如Capes分为Ġcap和es),有时是标点符号(例如.)。
BPE的一个优点是它可以对任意字符串进行编码。如果它遇到词汇表中没有的内容,它只会将其分解为它能够理解的子字符串:
[encoder.decoder[i] for i in encoder.encode("zjqfl")]
['z', 'j', 'q', 'fl']
我们还可以检查词汇表的大小:
len(encoder.decoder)
50257
词汇表以及确定如何拆分字符串的字节对合并是通过训练分词器获得的。
当我们加载分词器时,我们从一些文件加载已经训练好的单词和字节对合并,当我们运行load_encoder_hparams_and_params时,这些文件与模型文件一起下载。
超参数
hparams是一个包含我们模型的超参数的词典:
>>> hparams
{
"n_vocab": 50257, # number of tokens in our vocabulary
"n_ctx": 1024, # maximum possible sequence length of the input
"n_embd": 768, # embedding dimension (determines the "width" of the network)
"n_head": 12, # number of attention heads (n_embd must be divisible by n_head)
"n_layer": 12 # number of layers (determines the "depth" of the network)
}
我们将在代码的注释中使用这些符号来显示事物的基本形状。我们还将使用n_seq表示输入序列的长度(即n_seq = len(inputs))。
参数
params是一个嵌套的json字典,它保存我们模型的训练权重。Json的叶节点是NumPy数组。我们会得到:
import numpy as np
def shape_tree(d):
if isinstance(d, np.ndarray):
return list(d.shape)
elif isinstance(d, list):
return [shape_tree(v) for v in d]
elif isinstance(d, dict):
return {k: shape_tree(v) for k, v in d.items()}
else:
ValueError("uh oh")
print(shape_tree(params))
{
"wpe": [1024, 768],
"wte": [50257, 768],
"ln_f": {"b": [768], "g": [768]},
"blocks": [
{
"attn": {
"c_attn": {"b": [2304], "w": [768, 2304]},
"c_proj": {"b": [768], "w": [768, 768]},
},
"ln_1": {"b": [768], "g": [768]},
"ln_2": {"b": [768], "g": [768]},
"mlp": {
"c_fc": {"b": [3072], "w": [768, 3072]},
"c_proj": {"b": [768], "w": [3072, 768]},
},
},
... # repeat for n_layers
]
}
这些是从原始OpenAI TensorFlow检查点加载的:
import tensorflow as tf
tf_ckpt_path = tf.train.latest_checkpoint("models/124M")
for name, _ in tf.train.list_variables(tf_ckpt_path):
arr = tf.train.load_variable(tf_ckpt_path, name).squeeze()
print(f"{name}: {arr.shape}")
model/h0/attn/c_attn/b: (2304,)
model/h0/attn/c_attn/w: (768, 2304)
model/h0/attn/c_proj/b: (768,)
model/h0/attn/c_proj/w: (768, 768)
model/h0/ln_1/b: (768,)
model/h0/ln_1/g: (768,)
model/h0/ln_2/b: (768,)
model/h0/ln_2/g: (768,)
model/h0/mlp/c_fc/b: (3072,)
model/h0/mlp/c_fc/w: (768, 3072)
model/h0/mlp/c_proj/b: (768,)
model/h0/mlp/c_proj/w: (3072, 768)
model/h1/attn/c_attn/b: (2304,)
model/h1/attn/c_attn/w: (768, 2304)
...
model/h9/mlp/c_proj/b: (768,)
model/h9/mlp/c_proj/w: (3072, 768)
model/ln_f/b: (768,)
model/ln_f/g: (768,)
model/wpe: (1024, 768)
model/wte: (50257, 768)
下面的代码将上述TensorFlow变量转换为我们的params词典。
作为参考,以下是params的形状,但用它们所代表的hparams替换了数字:
import tensorflow as tf
tf_ckpt_path = tf.train.latest_checkpoint("models/124M")
for name, _ in tf.train.list_variables(tf_ckpt_path):
arr = tf.train.load_variable(tf_ckpt_path, name).squeeze()
print(f"{name}: {arr.shape}")
model/h0/attn/c_attn/b: (2304,)
model/h0/attn/c_attn/w: (768, 2304)
model/h0/attn/c_proj/b: (768,)
model/h0/attn/c_proj/w: (768, 768)
model/h0/ln_1/b: (768,)
model/h0/ln_1/g: (768,)
model/h0/ln_2/b: (768,)
model/h0/ln_2/g: (768,)
model/h0/mlp/c_fc/b: (3072,)
model/h0/mlp/c_fc/w: (768, 3072)
model/h0/mlp/c_proj/b: (768,)
model/h0/mlp/c_proj/w: (3072, 768)
model/h1/attn/c_attn/b: (2304,)
model/h1/attn/c_attn/w: (768, 2304)
...
model/h9/mlp/c_proj/b: (768,)
model/h9/mlp/c_proj/w: (3072, 768)
model/ln_f/b: (768,)
model/ln_f/g: (768,)
model/wpe: (1024, 768)
model/wte: (50257, 768)
基本层
在我们进入实际的GPT体系结构本身之前,最后一件事是,让我们实现一些非特定于GPT的更基本的神经网络层。
GELU
GPT-2选择的非线性(激活函数)是GELU(高斯误差线性单元),它是REU的替代方案:
它由以下函数近似表示:
def gelu(x):
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
与RELU类似,Gelu在输入上按元素操作:
gelu(np.array([[1, 2], [-2, 0.5]]))
array([[ 0.84119, 1.9546 ],
[-0.0454 , 0.34571]])
Softmax
Good ole softmax:
def softmax(x):
exp_x = np.exp(x - np.max(x, axis=-1, keepdims=True))
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
我们使用max(x)技巧来保证数值稳定性。
SoftMax用于将一组实数(介于−∞和∞之间)转换为概率(介于0和1之间,所有数字的总和为1)。我们在输入的最后一个轴上应用softmax 。
x = softmax(np.array([[2, 100], [-5, 0]]))
x
array([[0.00034, 0.99966],
[0.26894, 0.73106]])
x.sum(axis=-1)
array([1., 1.])
层归一化
层归一化将值标准化,使其平均值为0,方差为1:
def layer_norm(x, g, b, eps: float = 1e-5):
mean = np.mean(x, axis=-1, keepdims=True)
variance = np.var(x, axis=-1, keepdims=True)
x = (x - mean) / np.sqrt(variance + eps) # normalize x to have mean=0 and var=1 over last axisreturn g * x + b # scale and offset with gamma/beta params
层归一化确保每一层的输入始终在一致的范围内,这会加快和稳定训练过程。
与批处理归一化一样,归一化输出随后被缩放,并使用两个可学习向量gamma和beta进行偏移。分母中的小epsilon项用于避免除以零的误差。
由于种种原因,Transformer采用分层定额代替批量定额。
我们在输入的最后一个轴上应用层归一化。
>>> x = np.array([[2, 2, 3], [-5, 0, 1]])
>>> x = layer_norm(x, g=np.ones(x.shape[-1]), b=np.zeros(x.shape[-1]))
>>> x
array([[-0.70709, -0.70709, 1.41418],
[-1.397 , 0.508 , 0.889 ]])
>>> x.var(axis=-1)
array([0.99996, 1. ]) # floating point shenanigans
>>> x.mean(axis=-1)
array([-0., -0.])
Linear
你的标准矩阵乘法+偏差:
def linear(x, w, b): # [m, in], [in, out], [out] -> [m, out]
return x @ w + b
线性层通常称为映射(因为它们从一个向量空间映射到另一个向量空间)。
>>> x = np.random.normal(size=(64, 784)) # input dim = 784, batch/sequence dim = 64
>>> w = np.random.normal(size=(784, 10)) # output dim = 10
>>> b = np.random.normal(size=(10,))
>>> x.shape # shape before linear projection
(64, 784)
>>> linear(x, w, b).shape # shape after linear projection
(64, 10)
GPT架构
GPT架构遵循Transformer的架构:
从高层次上讲,GPT体系结构有三个部分:
文本+位置嵌入
一种transformer解码器堆栈
向单词步骤的映射
在代码中,它如下所示:
def gpt2(inputs, wte, wpe, blocks, ln_f, n_head): # [n_seq] -> [n_seq, n_vocab]
# token + positional embeddings
x = wte[inputs] + wpe[range(len(inputs))] # [n_seq] -> [n_seq, n_embd]
# forward pass through n_layer transformer blocks
for block in blocks:
x = transformer_block(x, **block, n_head=n_head) # [n_seq, n_embd] -> [n_seq, n_embd]
# projection to vocab
x = layer_norm(x, **ln_f) # [n_seq, n_embd] -> [n_seq, n_embd]
return x @ wte.T # [n_seq, n_embd] -> [n_seq, n_vocab]
把所有放在一起
把所有这些放在一起,我们得到了gpt2.py,它总共只有120行代码(如果删除注释和空格,则为60行)。
我们可以通过以下方式测试我们的实施:
python gpt2.py \"Alan Turing theorized that computers would one day become" \
--n_tokens_to_generate 8
它给出了输出:
the most powerful machines on the planet.
它成功了!
我们可以使用下面的Dockerfile测试我们的实现与OpenAI官方GPT-2 repo的结果是否一致。
docker build -t "openai-gpt-2" "https://gist.githubusercontent.com/jaymody/9054ca64eeea7fad1b58a185696bb518/raw/Dockerfile"
docker run -dt "openai-gpt-2" --name "openai-gpt-2-app"
docker exec -it "openai-gpt-2-app" /bin/bash -c 'python3 src/interactive_conditional_samples.py --length 8 --model_type 124M --top_k 1'
# paste "Alan Turing theorized that computers would one day become" when prompted
这应该会产生相同的结果:
the most powerful machines on the planet.
下一步呢?
这个实现很酷,但它缺少很多花哨的东西:
GPU/TPU支持
将NumPy替换为JAX:
import jax.numpy as np
你现在可以使用代码与GPU,甚至TPU!只需确保正确安装了JAX即可。
反向传播
同样,如果我们用JAX替换NumPy:
import jax.numpy as np
然后,计算梯度就像以下操作一样简单:
def lm_loss(params, inputs, n_head) -> float:
x, y = inputs[:-1], inputs[1:]
output = gpt2(x, **params, n_head=n_head)
loss = np.mean(-np.log(output[y]))return loss
grads = jax.grad(lm_loss)(params, inputs, n_head)
Batching
再一次,如果我们用JAX替换NumPy:
import jax.numpy as np
然后,对gpt2函数进行批处理非常简单:
gpt2_batched = jax.vmap(gpt2, in_axes=[0, None, None, None, None, None])
gpt2_batched(batched_inputs) # [batch, seq_len] -> [batch, seq_len, vocab]
推理优化
我们的实现效率相当低。你可以进行的最快、最有效的优化(在GPU+批处理支持之外)将是实现KV缓存。
训练
训练GPT对于神经网络来说是相当标准的(梯度下降是损失函数)。
当然,在训练GPT时,你还需要使用标准的技巧包(例如,使用ADAM优化器、找到最佳学习率、通过辍学和/或权重衰减进行正则化、使用学习率调度器、使用正确的权重初始化、批处理等)。
训练一个好的GPT模型的真正秘诀是调整数据和模型的能力,这才是真正的挑战所在。
对于缩放数据,你需要一个大、高质量和多样化的文本语料库。
- 大意味着数十亿个token(TB级的数据)。
- 高质量意味着您想要过滤掉重复的示例、未格式化的文本、不连贯的文本、垃圾文本等。
- 多样性意味着不同的序列长度,关于许多不同的主题,来自不同的来源,具有不同的视角等等。
评估
如何评价一个LLM,这是一个很难的问题。
停止生成
当前的实现要求我们提前指定要生成的token的确切数量。这并不是一个好方法,因为我们生成的token最终会过长、过短或在句子中途中断。
为了解决这个问题,我们可以引入一个特殊的句尾(EOS)标记。
在预训练期间,我们将EOS token附加到输入的末尾(即tokens = ["not", "all", "heroes", "wear", "capes", ".", "<|EOS|>"])。
在生成期间,只要我们遇到EOS token(或者如果我们达到了某个最大序列长度),就会停止:
def generate(inputs, eos_id, max_seq_len):
prompt_len = len(inputs)while inputs[-1] != eos_id and len(inputs) < max_seq_len:
output = gpt(inputs)
next_id = np.argmax(output[-1])
inputs.append(int(next_id))return inputs[prompt_len:]
GPT-2没有预训练EOS token,所以我们不能在我们的代码中使用这种方法。
无条件生成
使用我们的模型生成文本需要我们使用提示符对其进行条件调整。
但是,我们也可以让我们的模型执行无条件生成,即模型在没有任何输入提示的情况下生成文本。
这是通过在预训练期间将特殊的句子开始(BOS)标记附加到输入开始(即tokens = ["<|BOS|>", "not", "all", "heroes", "wear", "capes", "."])来实现的。
然后,要无条件地生成文本,我们输入一个只包含BOS token的列表:
def generate_unconditioned(bos_id, n_tokens_to_generate):
inputs = [bos_id]for _ in range(n_tokens_to_generate):
output = gpt(inputs)
next_id = np.argmax(output[-1])
inputs.append(int(next_id))return inputs[1:]
GPT-2预训练了一个BOS token(名称为<|endoftext|>),因此使用我们的实现无条件生成非常简单,只需将以下行更改为:
input_ids = encoder.encode(prompt) if prompt else [encoder.encoder["<|endoftext|>"]]
然后运行:
python gpt2.py ""
这将生成:
The first time I saw the new version of the game, I was so excited. I was so excited to see the new version of the game, I was so excited to see the new version
因为我们使用的是贪婪采样,所以输出不是很好(重复),而且是确定性的(即,每次我们运行代码时都是相同的输出)。为了得到质量更高且不确定的生成,我们需要直接从分布中抽样(理想情况下,在应用类似top-p的方法之后)。
无条件生成并不是特别有用,但它是展示GPT能力的一种有趣的方式。
微调
我们在训练部分简要介绍了微调。回想一下,微调是指当我们重新使用预训练的权重来训练模型执行一些下游任务时。我们称这一过程为迁移学习。
从理论上讲,我们可以使用零样本或少样本提示,来让模型完成我们的任务,
然而,如果你可以访问token的数据集,微调GPT将产生更好的结果(在给定更多数据和更高质量的数据的情况下,结果可以扩展)。
有几个与微调相关的不同主题,我将它们细分如下:
分类微调
在分类微调中,我们给模型一些文本,并要求它预测它属于哪一类。
例如,以IMDB数据集为例,它包含将电影评为好或差的电影评论:
--- Example 1 ---
Text: I wouldn't rent this one even on dollar rental night.
Label: Bad
--- Example 2 ---
Text: I don't know why I like this movie so well, but I never get tired of watching it.
Label: Good
--- Example 3 ---
...
为了微调我们的模型,我们将语言建模头替换为分类头,并将其应用于最后一个token输出:
def gpt2(inputs, wte, wpe, blocks, ln_f, cls_head, n_head):
x = wte[inputs] + wpe[range(len(inputs))]
for block in blocks:
x = transformer_block(x, **block, n_head=n_head)
x = layer_norm(x, **ln_f)
# project to n_classes
# [n_embd] @ [n_embd, n_classes] -> [n_classes]
return x[-1] @ cls_head
我们只使用最后一个token输出x[-1],因为我们只需要为整个输入生成单一的概率分布,而不是语言建模中的n_seq分布。
尤其,我们采用最后一个token,因为最后一个token是唯一被允许关注整个序列的token,因此具有关于整个输入文本的信息。
像往常一样,我们优化了w.r.t.交叉熵损失:
def singe_example_loss_fn(inputs: list[int], label: int, params) -> float:
logits = gpt(inputs, **params)
probs = softmax(logits)
loss = -np.log(probs[label]) # cross entropy loss
return loss
我们还可以通过应用sigmoid而不是softmax来执行多标签分类,并获取关于每个类别的二进制交叉熵损失。
生成式微调
有些任务不能被整齐地归类。例如,总结这项任务。
我们只需对输入和标签进行语言建模,就能对这类任务进行微调。例如,下面是一个总结训练样本:
--- Article ---
This is an article I would like to summarize.
--- Summary ---
This is the summary.
我们像在预训练中一样训练模型(优化w.r.t语言建模损失)。
在预测时间,我们向模型提供直到--- Summary ---的所有内容,然后执行自回归语言建模以生成摘要。
分隔符--- Article ---和--- Summary ---的选择是任意的。如何选择文本的格式由你自己决定,只要它在训练和推理之间保持一致。
注意,我们还可以将分类任务制定为生成式任务(例如使用IMDB):
--- Text ---
I wouldn't rent this one even on dollar rental night.
--- Label ---
Bad
指令微调
如今,大多数最先进的大模型在经过预寻来你后,还会经历额外的指令微调。
在这一步中,模型对数千个人类标记的指令提示+完成对进行了微调(生成)。指令微调也可以称为有监督的微调,因为数据是人为标记的。
那么,指令微调有什么好处呢?
虽然预测维基百科文章中的下一个单词能让模型擅长续写句子,但这并不能让它特别擅长遵循指令、进行对话或总结文档(我们希望GPT能做的所有事情)。
在人类标注的指令+完成对上对其进行微调,是一种教模型如何变得更有用,并使其更易于交互的方法。
这就是所谓的AI对齐,因为我们正在对模型进行对齐,使其按照我们的意愿行事。
参数高效微调
当我们在上述章节中谈到微调时,假定我们正在更新所有模型参数。
虽然这能产生最佳性能,但在计算(需要对整个模型进行反向传播)和存储(每个微调模型都需要存储一份全新的参数副本)方面成本高昂。
解决这个问题最简单的方法就是只更新头部,冻结(即无法训练)模型的其他部分。
虽然这可以加快训练速度,并大大减少新参数的数量,但效果并不是特别好,因为我们失去了深度学习的深度。
相反,我们可以选择性地冻结特定层,这将有助于恢复深度。这样做的结果是,效果会好很多,但我们的参数效率会降低很多,也会失去一些训练速度的提升。
值得一提的是,我们还可以利用参数高效的微调方法。
以Adapters 一文为例。在这种方法中,我们在transformer块中的FFN和MHA层之后添加一个额外的「适配器」层。
适配层只是一个简单的两层全连接神经网络,输入输出维度为 n_embd ,隐含维度小于 n_embd :
隐藏维度的大小是一个超参数,我们可以对其进行设置,从而在参数与性能之间进行权衡。
论文显示,对于BERT模型,使用这种方法可以将训练参数的数量减少到2%,而与完全微调相比,性能只受到很小的影响(<1%)。
参考资料:
https://twitter.com/ItakGol/status/1761852345502081327?t=MNOiF-cOqXG7jUesISmuXg&s=19
#GPT-4V医疗领域の全面测评
上海交大&上海AI Lab发布178页GPT-4V医疗案例测评,首次全面揭秘GPT-4V医疗领域视觉性能,离临床应用与实际决策尚有距离
在大型基础模型的推动下,人工智能的发展近来取得了巨大进步,尤其是 OpenAI 的 GPT-4,其在问答、知识方面展现出的强大能力点亮了 AI 领域的尤里卡时刻,引起了公众的普遍关注。
GPT-4V (ision) 是 OpenAI 最新的多模态基础模型。相较于 GPT-4,它增加了图像与语音的输入能力。该研究则旨在通过案例分析评估 GPT-4V (ision) 在多模态医疗诊断领域的性能,一共展现并分析共计了 128(92 个放射学评估案例,20 个病理学评估案例以及 16 个定位案例)个案例共计 277 张图像的 GPT-4V 问答实例(注:本文不会涉及案例展示,请参阅原论文查看具体的案例展示与分析)。
- ArXiv 链接:https://arxiv.org/abs/2310.09909
- 百度云下载地址:https://pan.baidu.com/s/11xV8MkUfmF3emJQH9awtcw?pwd=krk2
- Google Drive下载地址:https://drive.google.com/file/d/1HPvPDwhgpOwxi2sYH3_xrcaoXjBGWhK9/view?usp=sharing
总结而言,原作者希望系统地评估 GPT-4V 如下的多种能力:
- GPT-4V 能否识别医学图像的模态和成像位置?识别各种模态(如 X 射线、CT、核磁共振成像、超声波和病理)并识别这些图像中的成像位置,是进行更复杂诊断的基础。
- GPT-4V 能否定位医学影像中的不同解剖结构?精确定位图像中的特定解剖结构对识别异常、确保正确处理潜在问题至关重要。
- GPT-4V 能否发现和定位医学图像中的异常?检测异常,如 肿瘤、骨折或感染是医学图像分析的主要目标。在临床环境中,可靠的人工智能模型不仅需要发现这些异常,还需要准确定位,以便进行有针对性的干预或治疗。
- GPT-4V 能否结合多张图像进行诊断?医学诊断往往需要综合不同成像模态或视图的信息,进行整体观察。因此探究 GPT-4V 组合和分析多图信息的能力至关重要。
- GPT-4V 能否撰写医疗报告,描述异常情况和相关的正常结果?对于放射科医生和病理学家来说,撰写报告是一项耗时的工作。如果 GPT-4V 在这一过程中提供帮助,生成准确且与临床相关的报告,无疑将提高整个工作流程的效率。
- GPT-4V 能否在解读医学影像时整合患者病史?患者的基本信息和既往病史会在很大程度上影响对当前医学影像的解读。在模型预测过程中如果能综合考虑到这些信息去分析图像将使分析更加个性化,也更加准确。
- GPT-4V 能否在多轮交互中保持一致性和记忆性?在某些医疗场景中,单轮分析可能是不够的。在长时间的对话或分析过程中,尤其是在复杂的医疗环境中,保持对数据认知的连续性至关重要。
原论文的评估涵盖了 17 个医学系统,包括:中枢神经系统、头颈部、心脏、胸部、血液、肝胆、肛肠、泌尿、妇科、产科、乳腺科、肌肉骨骼科、脊柱科、血管科、肿瘤科、创伤科、儿科。
图像来自日常临床使用的 8 种模态,包括:X 光、计算机断层扫描 (CT)、磁共振成像 (MRI)、正电子发射断层扫描 (PET)、数字减影血管造影 (DSA)、 乳房 X 射线照相术、超声波检查和病理学检查。
17 个医学系统以及 8 种成像模态示意图
论文指出,虽然 GPT-4V 在区分医学影像模态和解剖结构方面表现出很强的能力,但在疾病诊断和生成综合报告方面却仍面临巨大挑战。这些发现突出表明,虽然大型多模态模型在计算机视觉和自然语言处理方面取得了重大进展,但仍远未达到有效支持真实世界的医疗应用和临床决策的要求。
测试案例挑选
原论文的放射学问答来自于 Radiopaedia,图像直接从网页下载,定位案例来自于多个医学公开分割数据集,病理图像则来自于 PathologyOutlines 。在挑选案例时作者们全面的考虑了如下方面:
- 公布时间:考虑到 GPT-4V 的训练数据极有可能异常庞大,为了避免所选到的测试案例出现在训练集中,作者只选用了 2023 年发布的最新案例。
- 标注可信度:医疗诊断本身具有争议和模糊性,作者根据 Radiopaedia 提供的案例完成度,尽量选用完成度大于 90% 的案例来保证标注或诊断的可信程度。
- 图像模态多样性:在选取案例时,作者尽可能地展示 GPT-4V 对于多种成像模态的响应情况。
在图像处理时作者也做了如下规范化以保证输入图像的质量:
- 多图选择:考虑到 GPT-4V 支持的最大图像输入上限为 4,但部分案例会有超过 4 张的相关图像,首先作者在选取案例时会尽可能避免这种情况,其次在不可避免地遇到这种案例时,作者会根据 Radiopaedia 提供的案例注释挑选最相关的图像。
- 截面选择:大量的放射图像数据为 3D(连续多帧二维图像)形式,无法直接输入 GPT-4V,必须挑选一个最有代表性的截面代替完整的 3D 图像输入 GPT-4V。根据 Radiopaedia 的案例上传规范,放射医生在上传 3D 图像时被要求选择一个最相关的截面。作者们利用了这一点,选用了 Radiopaedia 推荐的轴截面替代 3D 数据进行输入。
- 图像标准化:医疗图像的标准化设计窗宽窗位的选择,不同的视窗会突出不同的组织,作者们使用的 Radiopaedio 案例上传时放射专家所选择的窗宽窗位输入图像。对于分割数据集而言,原论文则采用了 [-300,300] 的视窗,并作 0-1 的案例级的归一化。
原论文的测试均使用了 GPT-4V 的网页版,第一轮问答用户会输入图像,然后展开多轮的问答。为了避免上下文的互相影响,对于每次新的案例,都会新建一个问答窗口进行问答。
GPT-4V 问答案例,图中红色代表错误,黄色代表不确定,绿色代表正确,Reference 中的颜色则代表对应判断的依据,未标记颜色的句子需要读者自行判断正确性,更多案例以及案例分析请参考原论文
在病理评估中,所有图像都会进行两轮对话。第一轮询问能否仅根据输入图像生成报告。这一轮的目的是评估 GPT-4V 能否在不提供任何相关医疗提示的情况下识别图像模态和组织来源。在第二轮中,用户会提供正确的组织来源,并询问 GPT-4V 是否能根据病理图像及其组织来源信息做出诊断,希望 GPT-4V 能修改报告并提供明确的诊断结果。
病理图像的案例展示,更多案例以及案例分析请参考原论文
在定位评估中,原论文采取了循序渐进的方式:首先测试 GPT-4V 是否能识别出所提供图像中目标的存在;然后要求它根据图像左上角为(x,y)=(0,0)和右下角为(x,y)=(w,h)生成目标的边界框坐标,并对每个单一定位任务重复评估多次,以获得至少 4 个预测边界框,计算它们的 IOU 分数,并选出最高的一个来证明其上限性能;然后得出平均边界框,计算 IOU 分数,以证明其平均性能。
定位问答的案例展示,更多案例以及案例分析请参考原论文
测评中的局限性
当然原作者也提到了一些测评中的不足与限制:
1. 只能进行定性而非定量的评估
鉴于 GPT-4V 只提供在线网页界面,只能手动上传测试用例,导致原评估报告在可扩展性方面受到限制,因此只能提供定性评估。
2. 样本偏差
所选样本均来自在线网站,可能无法反映日常门诊中的数据分布情况。尤其是大多数评估病例都是异常病例,这可能会给评估带来潜在偏差。
3. 注释或参考答案并不完整
从 Radiopaedia 或者 PathologyOutlines 网站上获得的参考描述大多没有结构,也没有标准化的放射学 / 病理学报告格式。特别是,这些报告中的大部分主要侧重于描述异常情况,而不是对病例进行全面描述,并不能直接作为完美的回复简单对比。
4. 只有二维切片输入
在实际临床环境中,包括 CT、MRI 扫描在内的放射图像通常采用 3D DICOM 格式。然而,GPT-4V 最多只能支持四张二维图像的输入,所以原文在测评时只能输入二维关键切片或小片段(用于病理学)。
总之,尽管评估可能并不彻底详尽,但原作者们相信,这一分析仍旧可以为研究人员和医学专业人员提供了宝贵的见解,它揭示了多模态基础模型的当前能力,并可能激励未来建立医学基础模型的工作。
重要观察结果
原测评报告根据测评案例,概括了多个观察到的 GPT-4V 的表现特点:
放射案例部分
作者们根据 92 个放射学评估案例和 20 个定位案例得出如下观察结果:
1. GPT-4V 可以辨识出医疗图像的模态以及成像位置
对于大多数图像内容的模态识别、成像部位判定以及图像平面类别判定等任务,GPT4-V 都表现出了良好的处理能力。例如,作者们指出 GPT-4V 能很容易区分核磁共振、CT、X 光等各种模态;判断图像所描述的人体具体部位;判断出核磁共振图像的轴位、失状位和冠状位等。
2. GPT-4V 几乎无法做出精确的诊断
作者们发现:一方面,OpenAI 似乎设置了安全机制,严格限制了 GPT-4V 做出直接诊断;另一方面,除了针对非常明显的诊断案例,GPT-4V 的分析能力较差,仅局限于列举出可能存在的一系列疾病,而不能给出较为精确的诊断。
3. GPT-4V 可以生成出结构化的报告,但是内容大部分并不正确
GPT-4V 在绝大多数情况下都能生成较为标准的报告,但作者们认为,相比于整合程度更高且内容更灵活的手写报告,在针对多模态或多帧图像时,它更倾向于逐图描述且缺乏综合能力。因此内容大部分参考价值较小且缺乏准确性。
4. GPT-4V 可以辨识出医学图像中的标记以及文本注释,但并不能理解其出现在图像中的意义
GPT-4V 展现出较强的文本识别、标记识别等能力,并且会尝试利用这些标记进行分析。但作者们认为,其局限性在于:其一,GPT-4V 总是会过度利用文本和标记且图像本身成为次要参考对象;其二,它鲁棒性较低,常常会误解图像中的医学注释和引导。
5. GPT-4V 可以辨识出医疗植入器械以及它们在图像中的位置
在大多数案例中,GPT4-V 都能正确识别到植入人体的医疗设备,并较为准确地定位它们的位置。并且作者们发现,甚至在一些较为困难的案例中,可能出现诊断错误,但判断医疗设备识别正确的情况。
6. GPT-4V 面对多图输入时会遇到分析障碍
作者们发现,在面对同一模态的不同视角下的图像时,GPT-4V 尽管会展现出相比于进输入单张图的更好的分析能力,但仍然倾向于分别对每张视图进行单独的分析;而在面对不同模态的图像混合输入时,GPT-4V 更难得出综合了不同模态信息的合理分析。
7. GPT-4V 的预测极易受到患者疾病史的引导
作者们发现是否提供患者疾病史会对 GPT-4V 的回答产生较大影响。在提供疾病史的情况下,GPT-4V 常常会将其作为关键点,对图中的潜在异常做出推断;而在不提供疾病史的情况下,GPT-4V 则会更倾向于将图像作为正常案例进行分析。
8. GPT-4V 并不能在医学图像中定位到解剖结构和异常
作者们认为 GPT-4V 定位效果较差主要表现为:其一,GPT-4V 在定位过程中总是会得到远离真实边界的预测框;其二,它在对同一幅图的多轮重复预测中表现出显著的随机性;其三,GPT-4V 显示出了明显的偏置性,例如:脑部 MRI 图像中小脑一定位于底部。
9. GPT-4V 可以根据用户的多轮交互,改变它的既有回答。
GPT-4V 可以在一系列的互动中修改其响应,使之正确。例如,在文中所示的例子中,作者们输入了子宫内膜异位症的 MRI 图像。GPT-4V 最初错误地将盆腔 MRI 分类为膝关节 MRI,从而得到了一个不正确的输出。但用户通过与 GPT-4V 的多轮互动对其进行纠正,最终做出了准确的诊断。
10. GPT-4V 幻觉问题严重,尤其倾向将患者叙述为正常即使异常信号极为显著。
GPT-4V 总是生成出结构上看上去非常完整详实的报告,但其中的内容却并不正确,很多时候即使图像异常区域明显它仍旧会认为患者正常。
11. GPT-4V 在医学问答上不够稳定
GPT-4V 在常见图像和罕见图像上的表现差异巨大,在不同的身体系统方面也展现出明显的性能差别。另外,对同一医学图像的分析可能会因更改 prompt 而产生不一致的结果,例如,如,GPT-4V 在 “ What is the diagnosis for this brain CT?” 的 prompt 下最初判断给定的图像为异常,但后来它生成了一个认为同一图像为正常的报告。这种不一致性强调了 GPT-4V 在临床诊断中的性能可能是不稳定和不可靠的。
12. GPT-4V 对医疗领域做了严格的安全限制
作者们发现 GPT-4V 已经在医学领域的问答中建立了防止潜在误用的安全防护措施,确保用户能够安全使用。例如,当 GPT-4V 被要求做出诊断时,"Please provide the diagnosis for this chest X-ray.",它可能会拒绝给出答案,或强调 “我不是专业医学建议的替代品”。在多数情况下,GPT-4V 会倾向于使用包含 “appears to be” 或 “could be” 之类的短语来表示不确定性。
病理案例部分
此外,作者们为了探索 GPT-4V 在病理图像的报告生成和医学诊断方面的能力,对来自不同组织的 20 种恶性肿瘤病理图像开展了图像块级别的测试,并得出以下结论:
1. GPT-4V 能够进行准确的模态识别
在所有测试案例中,GPT-4V 都可以正确地识别所有病理图像(H&E 染色的组织病理图像)的模态。
2. GPT-4V 能够生成结构化报告
给定一个没有任何医学提示的病理图像,GPT-4V 可以生成一个结构化且详细的报告来描述图像特征。在 20 个案例中,有 7 个案例能够使用如 “组织结构”、“细胞特征”、“基质”、“腺体结构”、“细胞核” 等术语明确地列出了其观察结果,甚至可以正确地从不同组织的病理图像中识别腺体结构和上皮特征。
3. GPT-4V 在 Prompt 的引导下能够对报告进行修正
当在第二轮对话的 prompt 中对组织器官进行修正时,GPT-4V 可以很大程度地修改报告修改其报告,并为预测正常的案例提供一个确切的诊断,或为预测异常的案例提供几个可能的选项。
4. GPT-4V 生成的描述大多基于知识
尽管 GPT-4V 可以为病理图像写一个结构化的报告,但许多关于细胞和细胞核的详细描述都是 H&E 染色图像的通用特征,而不是根据图像特有模式生成。此外,GPT-4V 提供的诊断结果也可能来源于通用医学知识,而不是根据病理图像的形态结构推理得到。
5. GPT-4V 的诊断性能有限
在 20 个案例中,GPT-4V 将四个肿瘤案例误诊为正常组织,正确诊断了源于膀胱、中枢神经系统和口腔组织中的 3 类癌症,对其余 13 个恶性肿瘤则给出了模糊的诊断。尤其是针对肛门和子宫组织上的癌症,GPT-4V 的诊断结果中既包含正常组织也涵盖恶性肿瘤,这表明 GPT-4V 可能并没有真正从这些病理图像中检测到异常。
总的来说,GPT-4V 在医疗领域的表现并不像 GPT-4 在医疗问答中那样惊艳,远未达到实际临床要求。
#大语言模型综述
从模型、数据和框架三个视角出发
大规模语言模型(LLMs)在很多关键任务中展现出显著的能力,比如自然语言理解、语言生成和复杂推理,并对社会产生深远的影响。然而,这些卓越的能力伴随着对庞大训练资源的需求(如下图左)和较长推理时延(如下图右)。因此,研究者们需要开发出有效的技术手段去解决其效率问题。
同时,我们从图右还可以看出,近来较为火热的高效 LLMs,例如 Mistral-7B,在确保和 LLaMA1-33B 相近的准确度的情况下可以大大减少推理内存和降低推理时延,可见已有部分可行的高效手段被成功应用于 LLMs 的设计和部署中。
在本综述中,来自俄亥俄州立大学、帝国理工学院、密歇根州立大学、密西根大学、亚马逊、谷歌、Boson AI、微软亚研院的研究者提供了对高效 LLMs 研究的系统全面调查。他们将现有优化 LLMs 效率的技术分成了三个类别,包括以模型为中心、以数据为中心和以框架为中心,总结并讨论了当下最前沿的相关技术。
- 论文:https://arxiv.org/abs/2312.03863
- GitHub: https://github.com/AIoT-MLSys-Lab/Efficient-LLMs-Survey
同时,研究者建立了一个 GitHub 仓库,用于整理综述中涉及的论文,并将积极维护这个仓库,随着新的研究涌现而不断更新。研究者希望这篇综述能够帮助研究人员和从业者系统地了解高效 LLMs 研究和发展,并激发他们为这一重要而令人兴奋的领域做出贡献。
仓库网址:https://github.com/AIoT-MLSys-Lab/Efficient-LLMs-Survey
以模型为中心
以模型为中心的方法关注算法层面和系统层面的高效技术,其中模型本身是焦点。由于 LLMs 具有数十亿甚至数万亿的参数,与规模较小的模型相比,它们具有诸如涌现等独特的特征,因此需要开发新的技术来优化 LLMs 的效率。本文详细讨论了五类以模型为中心的方法,包括模型压缩、高效预训练、高效微调、高效推理和高效模型架构设计。
1. 模型压缩
模型压缩技术主要分为了四类:量化、参数剪枝、低秩估计和知识蒸馏(参见下图),其中量化会把模型的权重或者激活值从高精度压缩到低精度,参数剪枝会搜索并删除模型权重中较为冗余的部分,低秩估计会将模型的权重矩阵转化为若干低秩小矩阵的乘积,知识蒸馏则是直接用大模型来训练小模型,从而使得小模型在做某些任务的时候具有替代大模型的能力。
2. 高效预训练
预训练 LLMs 的成本非常昂贵。高效预训练旨在提高效率并降低 LLMs 预训练过程的成本。高效预训练又可以分为混合精度加速、模型缩放、初始化技术、优化策略和系统层级的加速。
混合精度加速通过使用低精度权重计算梯度、权重和激活值,然后在将其转换回高精度并应用于更新原始权重,从而提高预训练的效率。模型缩放通过使用小型模型的参数来扩展到大型模型,加速预训练的收敛并降低训练成本。初始化技术通过设计模型的初始化取值来加快模型的收敛速度。优化策略是重在设计轻量的优化器来降低模型训练过程中的内存消耗,系统层级的加速则是通过分布式等技术来从系统层面加速模型的预训练。
3. 高效微调
高效微调旨在提高 LLMs 微调过程的效率。常见的高效微调技术分为了两类,一类是基于参数高效的微调,一类是基于内存高效的微调。
基于参数高效微调(PEFT)的目标是通过冻结整个 LLM 主干,仅更新一小组额外的参数,将 LLM 调整到下游任务。在论文中,我们又将 PEFT 详细分成了基于适配器的微调、低秩适配、前缀微调和提示词微调。
基于内存的高效微调则是重在降低整个 LLM 微调过程中的内存消耗,比如减少优化器状态和激活值等消耗的内存。
4. 高效推理
高效推理旨在提高 LLMs 推理过程的效率。研究者将常见的高效推理技术分成了两大类,一类是算法层级的推理加速,一类是系统层级的推理加速。
算法层级的推理加速又可以分成两类:投机解码和 KV - 缓存优化。投机解码通过使用较小的草稿模型并行计算令牌,为较大目标模型创建猜测性前缀,从而以加速采样过程。KV - 缓存优化指的是优化在 LLMs 推理过程中 Key-Value(KV)对的重复计算。
系统层级的推理加速则是在指定硬件上优化内存访问次数,增大算法并行量等来加速 LLM 的推理。
5. 高效模型架构设计
对 LLMs 进行高效架构设计是指通过策略性优化模型结构和计算过程,以提高性能和可扩展性,同时最小化资源消耗。我们将高效的模型架构设计依据模型的种类分成了四大类:高效注意力模块、混合专家模型、长文本大模型以及可替代 transformer 的架构。
高效注意力模块旨在优化注意力模块中的复杂计算及内存占用,混合专家模型(MoE)则是通过将 LLMs 的某些模块的推理决策使用多个小的专家模型来替代从而达到整体的稀疏化,长文本大模型是专门设计来高效处理超长文本的 LLMs, 可替代 transformer 的架构则是通过重新设计模型架构,来降低模型的复杂度并达到后 transformer 架构相当的推理能力。
以数据为中心
以数据为中心方法侧重于数据的质量和结构在提高 LLMs 效率方面的作用。研究者在本文中详细讨论了两类以数据为中心的方法,包括数据选择和提示词工程。
1. 数据选择
LLMs 的数据选择旨在对预训练 / 微调数据进行清洗和选择,例如去除冗余和无效数据,达到加快训练过程的目的。
2. 提示词工程
提示词工程通过设计有效的输入(提示词)来引导 LLMs 生成期望的输出,它的高效之处在于可以通过设计提示词,来达到和经过将繁琐的微调相当的模型表现。研究者将常见的的提示词工程技术分成了三大类:少样本的提示词工程、提示词压缩和提示词生成。
少样本的提示词工程通过向 LLM 提供有限的示例集以引导其对需要执行的任务进行理解。提示词压缩是通过压缩冗长的提示输入或学习和使用提示表示,加速 LLMs 对输入的处理。提示词生成旨在自动创建有效的提示,引导模型生成具体且相关的响应,而不是使用手动标注的数据。
以框架为中心
研究者调查了近来较为流行的高效 LLMs 框架,并列举了它们所能优化的高效任务,包括预训练、微调和推理(如下图所示)。
总结
在这份调查中,研究者为大家提供了一份关于高效 LLMs 的系统性回顾,这是一个致力于使 LLMs 更加民主化的重要研究领域。他们一开始就解释了为什么需要高效 LLMs。在一个有序的框架下,本文分别从以模型的中心、以数据的中心和以框架为中心的角度分别调查了 LLMs 的算法层面和系统层面的高效技术。
研究者相信,在 LLMs 和以 LLMs 为导向的系统中,效率将发挥越来越重要的作用。他们希望这份调查能够帮助研究人员和实践者迅速进入这一领域,并成为激发新的高效 LLMs 研究的催化剂。
#大语言模型~提高资源效率
本综述深入探讨了大型语言模型的资源高效化问题。
近年来,大型语言模型(LLM)如 OpenAI 的 GPT-3 在人工智能领域取得了显著进展。这些模型,具有庞大的参数量(例如 1750 亿个参数),在复杂度和能力上实现了飞跃。随着 LLM 的发展趋势朝着不断增大的模型规模前进,这些模型在从智能聊天机器人到复杂数据分析,乃至于多领域研究中的应用越发广泛。然而,模型规模的指数级增长带来了巨大的资源需求,尤其是在计算、能源和内存等方面。
这些资源的巨大需求使得训练或部署这样庞大的模型成本高昂,尤其是在资源受限的环境(如学术实验室或医疗领域)中更是如此。此外,由于训练这些模型需要大量的 GPU 使用,因此它们的环境影响也成为日益关注的问题,尤其是在电力消耗和碳排放方面。如何在资源有限的环境中有效部署和应用这些模型成为了一个紧迫的问题。
来自 Emory University,University of Virginia 和 Penn State University 的研究团队通过全面梳理和分析了当前 LLM 领域的最新研究,系统地总结了提高模型资源效率的多种技术,并对未来的研究方向进行了深入探讨。这些工作不仅涵盖了 LLM 的全生命周期(预训练、微调、提示等),还包括了多种资源优化方法的分类和比较,以及对评估指标和数据集的标准化。本综述旨在为学者和实践者提供一个清晰的指导框架,帮助他们在资源有限的环境中有效地开发和部署大型语言模型。
论文链接:https://arxiv.org/pdf/2401.00625
一、引言
资源高效的 LLM 需要理解 LLM 生命周期中涉及的关键资源。在这项综述中,作者将这些资源系统地归类为五个主要类别:计算、内存、能源、资金和通信成本。高效性在这里被定义为投入资源与产出的比例,一个更高效的系统能够在消耗更少资源的同时产生相同水平的输出。因此,一个资源高效的 LLM 旨在在所有这些维度上最大化性能和能力,同时最小化资源开销,从而实现更可持续和更易获取的 AI 解决方案。
资源效率在 LLM 中是一个至关重要且复杂的领域,它需要创新的解决方案来应对显著的挑战。这些挑战一共包括五个层面:
- 模型层面:自回归生成的低并行性导致了显著的延迟问题,这在大型模型或长输入长度下尤其突出,影响训练和推理的高效处理。此外,自注意力层的二次复杂性随着输入长度的增加而显著增加,成为计算瓶颈。
- 理论层面:缩放法则和收益递减指出,随着模型变大,每增加一个参数所带来的性能提升在减小。此外,理论上关于机器学习中的泛化和过拟合也对 LLM 的资源效率提出了挑战。
- 系统层面:考虑到 LLM 的庞大模型大小和训练数据集,将它们全部放入单个 GPU/TPU 的内存中变得不可行。因此,为 LLM 优化训练过程的复杂系统设计变得至关重要。
- 伦理层面:许多 LLM 依赖于大型且专有的训练数据集,这限制了提高效率的某些技术的应用。此外,许多先进的 LLM 是封闭源的,这意味着在缺乏对模型内部工作的深入了解的情况下提高效率变得更加复杂。
- 评价指标层面:LLM 的多样化和复杂性使得开发全面的资源效率评价指标面临独特挑战。与优化较小模型的一两种资源相比,LLM 呈现出多目标问题,要求在多个关键资源上同时进行优化。
为了应对上述挑战,该综述提供了以下贡献:
- 资源高效 LLM 技术的全面概述:对增强 LLM 资源效率的技术进行了全面的概述,涵盖了 LLM 整个生命周期的各种方法和策略。
- 技术按资源类型的系统分类和分类法:建立了一个系统的分类和分类法,根据它们优化的资源类型对资源高效的 LLM 技术进行组织。
- 评估指标和数据集的标准化:提出了一套专门用于评估 LLM 资源效率的评估指标和数据集的标准化。
- 识别差距和未来研究方向:对当前在创造资源高效 LLM 方面的瓶颈和未解决的挑战进行了深入探讨,并指出了未来研究的潜在途径。
二、资源高效大型语言模型的全新分类法
该综述提出了一个全面的分类法,以系统地理解和优化大型语言模型(LLM)中涉及的关键资源。这个分类法包括五个关键领域:计算、内存、能源、资金和网络通信,每个领域都针对资源利用的不同方面:
1. 资源分类
- 计算:涉及训练、微调和执行 LLM 所需的处理能力。计算效率的评估包括考虑操作数量(如浮点操作)、算法效率和处理单元(如 GPU 或 TPU)的利用。
- 内存:内存效率涉及所需的 RAM 和存储量。尤其是拥有数十亿参数的 LLM,需要大量内存来存储模型权重和处理大型数据集。
- 能源:指模型生命周期中消耗的电力。考虑到环境影响和运营成本,能源效率至关重要。这包括减少能耗的策略,如优化硬件利用、使用节能硬件等。
- 资金:财务资源是一个关键考虑因素,尤其对于小型组织和研究者。这包括硬件采购成本、运行模型的电费和潜在的云计算费用。
- 网络通信:在分布式训练和基于云的部署中,网络带宽和延迟变得重要。高效的网络通信意味着减少在分布式系统节点之间或云端与用户之间传输的数据量,这对训练时间和实时应用的响应性有重大影响。
2. 技术分类
此外,该综述还引入了一个结构化的分类法,将提升 LLM 资源效率的技术分为明确、定义清晰的层级。其中包括五个主要类别:架构设计、预训练、微调、推理和系统设计。每个类别都在高效 LLM 开发和部署的生命周期中扮演着不可或缺的角色。
- 架构设计:检查 LLM 的结构基础,分为基于 Transformer 和非 Transformer 架构。
- 预训练:审视 LLM 开发的初步阶段,包括内存效率和数据效率。
- 微调:针对预训练模型的优化,分为参数高效微调和全参数微调。
- 推理:在操作阶段,采用各种策略,如模型压缩和动态加速。
- 系统设计:关注系统层面的考虑,包括部署优化和支持基础设施等。
这个分类法旨在提供对多样化方法和策略的结构化和细致理解。这些方法和策略用于提升 LLM 的效率和加速,为当前研究领域提供了一个全面的视角。
三、方法论
1. 大型语言模型架构设计的新进展
该综述重点探讨了大型语言模型(LLM)的两大架构设计方向:高效的 Transformer 结构和非 Transformer 架构。
- 高效的 Transformer 结构:这一类别包括了通过创新技术优化 Transformer 模型的架构,旨在降低计算和内存需求。例如,Reformer 通过局部敏感哈希技术来改进注意力机制,而 Linear Transformer 则利用线性映射来减少计算复杂度。AFT 和 KDEFormer 等其他方法则通过不同方式实现时间和内存效率的大幅提升。
- 非 Transformer 架构:这一类别探索了替代 Transformer 的新型架构。例如,模块化网络(MoE)技术通过结合多个专业化模型来处理复杂任务,Switch Transformer 和 GLaM 等则利用稀疏路由技术在增加模型参数的同时保持效率。另外,像 RWKV 这样的架构则结合了 Transformer 的训练效率和 RNN 的推理效率。
这些创新方向不仅优化了 LLM 的资源效率,也推动了语言模型技术的整体发展。
2. 大型语言模型预训练:效率与创新
该综述探索了 GPT-4 等大型语言模型(LLM)的高效预训练策略,这些策略不仅注重速度,还着眼于计算资源的最优利用和创新的数据管理。
- 内存效率
- 分布式训练:将模型训练任务分配给多个节点,以加速训练过程。数据并行(DP)和模型并行(MP)是两种主要的策略。DP 通过将初始数据集分割并由多个加速器并行训练,而 MP 则将模型的层或张量分布到多个加速器上。
- 混合精度训练:这种技术通过同时使用 16 位和 32 位浮点类型来加速深度学习模型的训练,特别适用于大型语言模型的训练。
- 数据效率
- 重要性采样:这种方法通过优先处理信息丰富的训练实例来提高模型的数据效率。
- 数据增强:通过创建现有数据的修改副本,使当前数据得到充分利用。
- 训练目标:预训练目标的选择是决定数据效率的另一个因素。这通常涉及模型架构、输入 / 目标构建和遮蔽策略的设计。
通过这些策略,综述旨在展示如何以资源高效的方式预训练大型语言模型,不仅加速了训练过程,还确保了先进 LLM 的可持续和成本效益发展。
3. 大型语言模型微调:平衡性能与资源
该综述探讨了 GPT-4 等大型语言模型在特定任务上的微调策略。这些策略旨在在实现任务特定性能和维持资源效率之间找到平衡点。
- 参数高效微调
- 基于遮蔽的微调:仅更新模型参数的子集,其他参数在反向传播过程中被「冻结」或遮蔽。
- 基于适配器的微调:在预训练模型的现有层之间插入额外的轻量级层(适配器)。在微调期间,只更新这些适配器层的参数,而原始模型参数保持固定。
- 全参数微调:与参数高效微调不同,全参数微调涉及修改所有参数。尽管训练成本更高,但通常可以获得比参数高效方法更好的性能。然而,这种方法在简单数据集上可能并不总是有效,且在训练成本和 GPU 内存消耗方面也面临挑战。
通过这些策略,综述旨在展示如何在保证大型语言模型性能优化和资源限制之间达到平衡的微调方法。
4. 大型语言模型推断:追求效率与质量
该综述探讨了如 GPT 系列的大型语言模型在推断阶段的优化技术,重点是减少计算负载和内存使用,同时保持高质量输出。
- 模型压缩
- 剪枝:通过移除模型中的特定参数来降低复杂度。包括结构化剪枝(针对整体结构,如神经元或通道)和非结构化剪枝(针对单个权重或连接)。
- 量化:将模型中的浮点数转换为较少位数的表示(如整数),旨在减少模型存储需求和加快计算速度。
- 知识蒸馏:将大型模型的知识转移到更紧凑的网络中,以减少推断延迟并增强特定任务解决能力。
- 动态加速
- 早期退出:根据某些标准提前终止模型的某些层的计算,用于简化输入样本的处理。
- 输入裁剪:动态减少输入序列长度,根据内容来分配不同的计算资源给不同的输入标记。
- 标记并行:利用技术如推测执行来并行生成多个标记,而非传统的顺序方式。
通过这些策略,综述旨在展示如何在实际应用中高效部署大型语言模型,同时考虑资源限制和性能需求。
5. 大型语言模型的系统设计:优化与应用
该综述探讨了如 GPT 系列的大型语言模型在系统设计方面的关键策略,特别是在资源受限环境中的高效推断。
- 部署优化
- 硬件卸载:通过将临时不需要的数据从快速加速器转移到更慢但更大的主、辅存储(如 CPU 内存和磁盘)中,优化大型 LLM 的运行效率。有效的卸载策略对整体系统效率至关重要。
- 协作推断:多个用户或系统合作完成 LLM 的推断任务,每个参与者贡献自己的资源,如计算能力或数据,以克服个体用户或系统的限制,实现更高效、准确的推断。
- 支持基础设施
- 库:介绍了几个著名的大型语言模型框架,如 DeepSpeed、Megatron-LM、Colossal-AI、Mesh-TensorFlow 和 GPT-NeoX,它们为大规模分布式训练提供多级并行策略。
- 边缘设备:探索在边缘设备上部署 LLM 的研究趋势,这些设备通常具有有限的计算资源。例如,通过低秩适应和噪声对比估计等技术来降低 LLM 在边缘设备上的内存需求。
- 其他系统
- Tabi:提出了一个多级推断引擎的推断系统,通过使用多个 DNN 处理任务中的异构查询来减少 LLM 的推断延迟。
- 近重复序列搜索:利用最小哈希技术来提高 LLM 的近重复序列搜索的效率和可扩展性。
通过这些策略,综述旨在展示大型语言模型在各种部署场景中的系统设计如何实现效率和可扩展性的最大化。
四、大型语言模型资源效率技术分类总结
该综述探讨了应用于大型语言模型(LLM)以提升其在不同资源上的效率的多种技术。这些资源包括计算、内存、能源、财务成本和网络通信。每项技术在优化 LLM 资源效率方面扮演着重要角色。
计算效率
- 直接影响:包括具有近似和硬件感知注意力机制的变换器架构,通过简化计算密集的注意力计算来加速过程;非结构化、结构化和上下文剪枝,通过移除不重要的权重或神经元来减少冗余计算。
- 间接影响:数据并行和参数高效微调,通过分布式工作负载和减少参数更新分别间接提高计算效率。
内存效率
- 直接影响:剪枝和量化通过减少模型大小来显著节约内存;知识蒸馏通过训练较小的模型来模仿较大的模型。
- 间接影响:分布式训练,如数据和模型并行,有效管理多设备间的内存使用,减轻单个设备的负担。
能源效率
- 直接影响:结构化剪枝和量化通过减少操作数量和数据大小,降低训练和推断的能源消耗;上下文剪枝通过最小化不必要的计算来节省能源。
- 间接影响:近似注意力机制等主要面向计算效率的技术,由于减少了计算负载,间接促进能源节省。
财务成本效率
间接影响:数据效率方法,如优化的训练目标和数据增强,通过提高数据使用效果,可能缩短训练时间,减少计算资源使用;动态推断技术,如早期退出和输入裁剪,通过减少推断阶段的运算需求,降低整体部署成本。
网络通信效率
- 直接影响:混合精度训练通过减少处理器间需要通信的数据大小,直接影响数据传输效率;权重量化通过最小化通信过程中的数据负载。
- 间接影响:协作推断通过优化数据传输和处理来提高网络通信效率。
通过这些策略,该综述旨在展示如何通过多种技术提高大型语言模型在各种资源上的效率。详细的技术与资源的对应关系可见下表。
五、大型语言模型评估数据集和指标
该综述详细分析了评估大型语言模型(LLM)资源效率的多元化指标,这些指标为全面理解 LLM 的资源效率提供了关键指导。
计算效率指标
- FLOPs:浮点运算次数,量化计算效率。
- 训练时间:训练 LLM 所需的总时间,反映了模型复杂性。
- 推断时间 / 延迟:LLM 生成输出所需的时间,关键评估实际应用中的实用性。
- 吞吐量:LLM 处理请求的效率,以每秒生成的标记或完成任务的速度衡量。
- 加速比:与基准模型相比推断速度的改善程度。
- 内存效率指标
- 参数数量:LLM 神经网络中可调变量的数量。
- 模型大小:存储整个模型所需的存储空间。
能源效率指标
- 能源消耗:以瓦时或焦耳表示,反映 LLM 生命周期中的电力使用。
- 碳排放:与模型能源使用相关的温室气体排放量。
财务成本效率指标
每参数成本:训练(或运行)LLM 的总成本除以参数数量的比值。
网络通信效率指标
通信量:在特定 LLM 执行或训练过程中网络间传输的数据总量。
其他指标
- 压缩比:压缩模型与原始模型大小的比例。
- 忠诚度和保真度:衡量教师和学生模型之间预测一致性和预测概率分布对齐程度。
- 鲁棒性:衡量 LLM 对攻击后性能和查询次数。
- 帕累托最优性:在不同竞争因素间取得的最佳平衡。
数据集和基准测试
- Dynaboard:动态基准,评估内存使用、吞吐量、公平性和鲁棒性等指标。
- EfficientQA:聚焦建立准确、内存高效的开放领域问答系统。
- SustaiNLP 2020:挑战参与者开发能源高效的 NLP 模型。
- ELUE 和 VLUE:专注于评估 NLP 和视觉语言模型的效率和性能。
- Long-Range Arena:专为评估长内容任务上高效 Transformer 模型而设计。
- Efficiency-aware MS MARCO:在 MS MARCO 信息检索基准测试中增加了效率指标。
通过这些策略,该综述旨在提供一种全面评估大型语言模型资源效率的方法论。
六、大型语言模型的未来挑战和研究方向
随着大型语言模型(LLM)领域的不断进步,我们面临着多种开放性挑战,这些挑战为未来的研究方向提供了丰富的机遇。
处理资源类型的冲突:不同优化技术之间存在性能指标的权衡,如计算效率与模型参数数量的矛盾。关键挑战在于开发全面优化策略,平衡计算效率、参数计数和内存使用等多个目标。
资源效率技术的综合:有效整合多种 LLM 优化方法以增强总体资源效率是一个显著挑战。目前缺乏对这些方法如何协同作用的研究,需要系统地结合不同策略,以显著提高模型效率。
标准化和统一评估:当前缺乏专门评估 LLM 资源效率的统一标准基准。这导致无法全面一致地评估各种 LLM 在资源利用方面的表现,迫切需要专注于资源效率的标准化基准。
可解释性和鲁棒性:在追求效率的同时,也需关注 LLM 的可解释性和鲁棒性。开发既优化资源使用又保持透明度和弹性的方法,确保这些模型在不同部署场景中可靠且易于理解。
自动化机器学习(AutoML)在资源高效 LLM 中的应用:将 AutoML 集成到资源高效 LLM 的开发中是一个新兴领域。通过应用 Meta-Learning 和神经架构搜索(NAS),自动化模型优化的部分,有望减少手动超参数调整和定制模型设计的需求。
边缘计算中的 LLM:在边缘计算环境中部署 LLM 面临独特挑战,如设备的计算能力和内存资源限制。需要开发既资源高效又考虑隐私问题的 LLM 技术,以适应边缘计算场景。
理论洞察 LLM 的扩展规律:深入理解 LLM 性能如何随其规模和复杂性扩展是一个关键且未被充分探索的领域。这种理解对于开发不仅专注于模型压缩,而是针对提高 LLM 整体资源效率的方法至关重要。
七、结论
本综述深入探讨了大型语言模型(LLM)的资源效率问题,分析了当前的研究成果和挑战,并展望了未来的发展方向。它还讨论了 LLM 在计算、内存、能源、财务成本和网络通信等关键资源方面的高效技术,以及这些技术如何相互作用以提高整体效率。通过对比各种技术,综述揭示了它们在不同应用环境中的潜力和限制。
作者还强调了在资源效率评估中建立标准化和统一的评价体系的重要性。这不仅有助于更准确地比较不同 LLM 的性能,也为进一步的研究和开发提供了坚实的基础。
最后,综述探讨了 LLM 领域面临的一系列开放性挑战和潜在的研究方向,包括管理资源类型的冲突、综合资源效率技术、可解释性和鲁棒性、AutoML 的集成以及在边缘计算环境中部署 LLM。这些挑战提供了未来研究的丰富机遇,对于推动 LLM 向更高效、更可靠和更可持续的方向发展至关重要。
本综述为理解和优化 LLM 的资源效率提供了全面的视角,为未来在这一重要领域的研究提供了指导和灵感。
#量子计算机+大语言模型
本来不想发的, 我们那个可笑的片子安全公司 ,弄俩了个傻子 天天大模型和抗量子 就发发, 这是登Nature子刊,滑铁卢大学团队评论「量子计算机+大语言模型」当下与未来
模拟当今量子计算设备的一个关键挑战,是学习和编码量子比特之间发生的复杂关联的能力。基于机器学习语言模型的新兴技术已经显示出学习量子态的独特能力。
近日,加拿大滑铁卢大学的研究人员在《Nature Computational Science》发表题为《Language models for quantum simulation》 的 Perspective 文章,强调了语言模型在构建量子计算机方面所做出的贡献,并讨论了它们在量子优势竞争中的未来角色。
论文链接:https://www.nature.com/articles/s43588-023-00578-0
量子计算机已经开始成熟,最近许多设备都声称具有量子优势。经典计算能力的持续发展,例如机器学习技术的快速崛起,引发了许多围绕量子和经典策略之间相互作用的令人兴奋的场景。随着机器学习继续与量子计算堆栈快速集成,提出了一个问题:它是否可以在未来以强大的方式改变量子技术?
当今量子计算机提出的一个关键挑战是量子态的学习。近年来迅速进入该领域的生成模型给出了学习量子态的两种广泛策略。
图示:自然语言及其他领域的生成模型。(来源:论文)
首先,通过代表量子计算机测量输出的数据集,数据驱动的学习通过传统的最大似然方法进行。其次,量子态可以通过所谓的物理学方法来解决,该方法利用量子比特之间相互作用的知识来定义替代损失函数。
无论哪种情况,量子态空间(希尔伯特空间)的大小都会随着量子比特数量 N 呈指数增长,这是典型的维数灾难。这对于扩展模型中表示量子态所需的参数数量以及寻找最佳参数值的计算效率提出了严峻的挑战。基于人工神经网络的生成模型非常适合应对这一挑战。
语言模型是一种特别有前途的生成模型,它已成为解决高复杂性语言问题的强大架构。由于其可扩展性,也适用于量子计算中的问题。如今,随着工业语言模型进入数万亿个参数的范围,人们很自然地想知道类似的大型模型在物理学中可以实现什么,无论是在扩展量子计算等应用中,还是在量子物质、材料和设备的基础理论理解中。
图示:量子物理问题及其变分公式。(来源:论文)
量子计算的自回归模型
语言模型是旨在从自然语言数据推断概率分布的生成模型。
生成模型的任务是学习语料库中出现的单词之间的概率关系,允许每次生成一个标记的新短语。主要困难在于对单词之间所有复杂的依赖关系进行建模。
类似的挑战也适用于量子计算机,其中纠缠等非局部相关性会导致量子比特之间高度不平凡的依赖性。因此,一个有趣的问题是,工业界开发的强大自回归架构是否也可以应用于解决强相关量子系统中的问题。
图示:文本和量子比特序列的自回归策略。(来源:论文)
RNN 波函数
RNN 是任何包含循环连接的神经网络,因此 RNN 单元的输出取决于先前的输出。自 2018 年以来,RNN 的使用迅速扩大,涵盖了理解量子系统中各种最具挑战性的任务。
RNN 适合这些任务的一个关键优势是它们能够学习和编码量子比特之间高度重要的相关性,包括本质上非局域的量子纠缠。
图示:用于量子比特序列的 RNN。(来源:论文)
物理学家已将 RNN 用于与量子计算相关的各种创新用途。RNN 已用于根据量子比特测量重建量子态的任务。RNN 还可以用于模拟量子系统的动态特性,这被认为是量子计算最有前途的应用之一,因此也是定义量子优势的一项关键任务。RNN 已被用作构建神经纠错解码器的策略,这是容错量子计算机开发的关键要素。此外,RNN 能够利用数据驱动和物理启发的优化,从而在量子模拟中实现越来越多的创新用途。
物理学家社区继续积极开发 RNN,希望利用它们来完成量子优势时代遇到的日益复杂的计算任务。RNN 在许多量子任务中与张量网络的计算竞争力,加上它们利用量子比特测量数据的价值的天然能力,表明 RNN 将继续在未来模拟量子计算机的复杂任务中发挥重要作用。
Transformer 量子态
多年来,虽然 RNN 在自然语言任务中取得了巨大成功,但最近它们在工业中因 Transformer 的自注意力机制而黯然失色,而 Transformer 是当今大型语言模型 (LLM) 编码器-解码器架构的关键组成部分。
缩放(scaling ) Transformer 的成功,以及它们在语言任务中所展示的非平凡涌现现象所引发的重要问题,一直吸引着物理学家,对他们来说,实现缩放是量子计算研究的主要目标。
从本质上讲,Transformer 就是简单的自回归模型。然而,与 RNN 不同的是,RNN 是通过隐藏向量进行相关性的隐式编码,Transformer 模型输出的条件分布明确依赖于序列中有关自回归特性的所有其他变量。这是通过因果屏蔽的自注意力机制来完成的。
图示:注意文本和量子比特序列。(来源:论文)
与语言数据一样,在量子系统中,注意力是通过获取量子比特测量值并通过一系列参数化函数进行转换来计算的。通过训练一堆这样的参数化函数,Transformer 可以学习量子比特之间的依赖关系。有了注意力机制,就不需要将传递隐藏状态的几何结构(就像在 RNN 中一样)与量子比特的物理排列相关联。
通过利用这种架构,可以训练具有数十亿或数万亿参数的 Transformer。
对于当前一代量子计算机来说,结合数据驱动和物理启发学习的混合两步优化非常重要,已经证明了 Transformer 能够减轻当今不完美的输出数据中出现的错误,并可能形成强大的纠错协议的基础,以支持未来真正容错硬件的开发。
随着涉及量子物理 Transformer 的研究范围不断迅速扩大,一系列有趣的问题仍然存在。
量子计算语言模型的未来
尽管物理学家对它们的探索时间很短,但语言模型在应用于量子计算领域的广泛挑战时已经取得了显著的成功。这些成果预示着未来许多有前途的研究方向。
量子物理学中语言模型的另一个关键用例来自于它们的优化能力,不是通过数据,而是通过哈密顿量或 Lindbladian 的基本量子比特相互作用的知识。
最后,语言模型通过数据驱动和变分驱动优化的结合,开辟了混合训练的新领域。这些新兴的策略为减少错误提供了新的途径,并显示出对变分模拟的强大改进。由于生成模型最近已被改编为量子纠错解码器,混合训练可能为未来实现容错量子计算机的圣杯迈出了重要一步。这表明,量子计算机和在其输出中训练的语言模型之间即将出现良性循环。
图示:语言模型通过良性循环实现量子计算的扩展。(来源:论文)
展望未来,将语言模型领域与量子计算联系起来的最令人兴奋的机会在于它们展示规模和涌现的能力。
如今,随着 LLM 涌现特性的展示,一个新的领域已经被突破,提出了许多引人注目的问题。如果有足够的训练数据,LLM 是否能够学习量子计算机的数字副本?控制堆栈中包含语言模型,将如何影响量子计算机的表征和设计?如果尺度足够大,LLM 能否显示超导等宏观量子现象的出现?
当理论学家思考这些问题时,实验和计算物理学家已经开始认真地将语言模型应用于当今量子计算机的设计、表征和控制中。当我们跨越量子优势的门槛时,我们也进入了扩展语言模型的新领域。虽然很难预测量子计算机和 LLM 的碰撞将如何展开,但显而易见的是,这些技术相互作用所带来的根本性转变已经开始。
#大语言模型~优化架构
作者重点关注了基于 Transformer 的 LLM 模型体系结构在从预训练到推理的所有阶段中优化长上下文能力的进展。
ChatGPT 的诞生,让基于 Transformer 的大型语言模型 (LLM) 为通用人工智能(AGI)铺开了一条革命性的道路,并在知识库、人机交互、机器人等多个领域得到应用。然而,目前存在一个普遍的限制:由于资源受限,当前大多 LLM 主要是在较短的文本上进行预训练,导致它们在较长上下文方面的表现较差,而长上下文在现实世界的环境中是更加常见的。
最近的一篇综述论文对此进行了全面的调研,作者重点关注了基于 Transformer 的 LLM 模型体系结构在从预训练到推理的所有阶段中优化长上下文能力的进展。
论文链接:https://arxiv.org/pdf/2311.12351.pdf
论文首先分析了使用当前基于 Transformer 的模型处理长上下文输入和输出的问题。然后,提供了一个全面的分类体系,以指导 Transformer 架构升级的领域,来解决这些问题。作者对长上下文 LLM 广泛使用的评估需求进行了调研,包括数据集、度量标准和基准模型,以及一些令人惊奇的优化工具包,如库、系统和编译器,以增强 LLM 在不同阶段的效率和功效。最后,文章进一步讨论了这一领域未来研究的主要挑战和潜在方向。作者还建立了一个仓库,汇总了相关文献,并提供实时更新 https://github.com/Strivin0311/long-llms-learning。
综述概览
文章从基本的语言建模目标 (第 2.1 节) 开始,内容涵盖从典型的建模阶段到在基于 Transformer 的仅解码 LLM 中找到的关键架构模块,如图 1 (a) 所示。随后,作者对 LLM 在遇到扩展上下文窗口时的架构限制进行了简要分析 (第 2.2 节)。最后提出了一个全面的方法论分类法 (第 2.3 节),旨在通过架构创新增强 LLM 的长上下文能力 (见图 1 (b))。这个分类法作为文章的第 3、4、5、6、7 节的指南。
图 1:文章核心概述:(a) 现代基于 Transformer 的仅解码 LLMs 的典型架构解剖图,右上角有图例;(b) 用于增强 Transformer 架构模块的方法论分类法(与 (a) 相对应的颜色):高效注意力(注意力核心的子模块),长期记忆(针对 KV 缓存),外推性 PEs(针对位置嵌入模块),上下文处理(与上下文预 / 后处理有关)和杂项(整个解码器块以及损失模块通用)。
长上下文,目前有哪些难点待突破?
注意力复杂度。在典型情况下 L ≫ d,MHA 的计算复杂性可以简洁总结如下:它涉及 O (L 2d) 的时间复杂度,包括 QKV 投影的 O (Ld2),计算 P 的 O (L 2d),通过 softmax 运算获取 A 的 O (L 2 ),A 与 V 相乘的 O (L 2d),以及输出投影 O 的 O (Ld2)。它还产生 O (L 2) 的空间复杂度,包括 Q、K、V、O 的嵌入的 O (Ld),以及额外的 O (L 2) 缓冲区用于存储权重 P 和 A。因此,随着序列长度的增加,时间和空间计算成本都呈二次增加,这对于训练和推理可能都是繁重的。
上下文记忆。LLM 缺乏显式的记忆机制,完全依赖 KV 缓存来存储列表中所有先前 token 的表示。这种设计一旦在一个调用中完成查询,Transformer 在后续的调用中不会保留或召回任何先前的状态或序列,除非整个历史记录逐个 token 重新加载到 KV 缓存中。因此,Transformer 在每个调用中仅具有一个上下文工作记忆,而不是像长短时记忆 (LSTM) 这样的内在记忆机制。这种无状态性在并行性方面提供了计算优势,但在聊天机器人应用等需要长期记忆保留的任务中的挑战很明显。
最大长度约束。在训练阶段,工程师通常需要确定一个关键的超参数 max-length,本文中表示为 L_max。这个超参数代表了批次中任何训练样本的序列长度的上限,通常根据可用的计算资源设置为 1k、2k 或 4k,以避免在 GPU 上发生内存溢出 (OOM) 错误。在推理阶段,LLM 的服务提供者还必须限制用户提示的长度或自动截断它们以与预定义的 L_max 对齐,即使推理资源通常比训练阶段更丰富。需要注意的是 Transformer 的任何模块在本质上都不需要这样的限制,因为所有学习的权重仅依赖于维度大小。因此,理论上只要资源足够,Transformer 可以处理任意长度的序列。然而,当前的语言模型在处理超过 L_max 的输入序列时通常表现出明显的性能下降,经常导致重复和不切实际的输出。
改进的新方法
对于上述限制,有多种改进方法可以探索,例如在训练过程中减少注意力复杂性、设计高效的记忆机制,以及增强长度外推的能力,该模型在短序列上进行训练,但在推理过程中对更长的序列进行测试。
因此,论文全面回顾了致力于改进 LLM 长上下文能力的各个阶段的最新方法,并将它们组织成一个统一的分类法,如图 1 (b) 所示。具体而言,这些方法被分为五个主要的类别,如下:
高效注意力 (论文第 3 节):这些方法侧重于实现具有降低计算要求的高效注意力机制,甚至实现了线性复杂度。通过这样做,它们能够通过直接在预训练阶段增加 L_max 来推进 LLM 在推理期间的有效上下文长度边界。
长期记忆 (论文第 4 节):为了解决上下文工作记忆的局限性,一些方法旨在设计明确的记忆机制,弥补 LLM 中缺乏高效和有效的长期记忆的不足。
外推性 PEs (论文第 5 节):最新的研究致力于通过改进现有位置编码方案的外推性能来增强 LLM 的长度泛化能力。
上下文处理 (论文第 6 节):除了增强特定低级 Transformer 模块的方法外,一些方法涉及对现成的 LLM 与额外的上下文预 / 后处理。这些方法确保每次调用 LLM 时输入始终满足最大长度要求,并通过引入多个调用开销打破上下文窗口限制。
杂项 (论文第 7 节):探讨了各种一般且有价值的方法,这些方法不容易归入前面四类,为推进 LLM 的长上下文能力提供了更广泛的视角。
未来方向
论文的第 3、4、5、6 节中讨论了该领域取得的显著进展,但仍然存在一些挑战。下面是对一些关键挑战的探讨以及未来在增强基于 Transformer 的 LLM 的长上下文能力方面进行研究和开发的潜在方向,重点关注架构的增强。
注意力 Trade-off。在第 3 节,作者探讨了高效注意方法往往涉及在保持全尺度注意力依赖性(例如局部注意力)或通过近似注意力提高注意力分数精度以减轻标准注意内核的计算需求之间的微妙权衡。然而,随着上下文的延长,话语结构和相互关联的信息变得越来越复杂,需要捕捉全局、长距离的依赖性,同时保持精确的相关性。
解决这一挑战需要在计算效率和尽可能保留注意模式精度之间找到最佳平衡。因此,在长上下文 LLM 领域,这仍然是一个持续追求的目标。最近的创新如 Flash Attention,探索了算法级别之外的 IO 感知解决方案,极大地提高了运行时和记忆开销的效率,而不会丧失注意精度。这是在实际应用中解决这个问题的一个激动人心的潜在途径。此外,可以探索在「即插即用」替代方案中集成先前的高效策略,利用强大的 GPU 内核编程工具 (如 CUDA) 或更轻量级的 Triton。
记忆效果和效率。正如在文章第 2.1、2.2 节中前面讨论的,作者已经概述了由于缺乏明确的记忆机制,仅依赖上下文内工作记忆以及在延长上下文交互期间 KV 缓存记忆消耗显著增加而产生的限制。这些挑战共同强调了在基于 Transformer 的 LLM 领域需要更有效和高效的记忆机制。虽然第 4 节中引入了各种长期记忆机制,但它们受到其复杂启发式设计引入的额外记忆开销的限制,因此随着时间的推移可能导致性能下降。为了解决这一挑战,研究人员可以从最近的进展中汲取灵感,比如 Paged Attention,研发更有效的记忆存储策略,增强读 / 写吞吐量。
长度外推挖掘。在第 5 节中,作者对与基于 Transformer 的模型的长度外推相关的挑战进行了彻底的分析,重点关注了位置嵌入的普遍设计。文章提供了对最近突破的全面概述,特别是应用于 RoPE 的扩展策略,作者相信这在解决外推限制方面具有重要的前景。值得注意的是,这些进步往往依赖于对复杂高维位置嵌入属性的简化观察,并包含简单的启发式调整。作者对使用高维嵌入来建模序列性的理论基础提出质疑,并探索在这些启发式设计的指导下引导具有许多超参数的可学习嵌入的潜在复苏。作者认为未来的研究应该深入探讨这一领域,尤其是在 Transformer 设置下为建模序列性开发健壮的理论框架方面,比如 CLEX 所实现的内容。
特定但通用目标。前文已经为长文本建模量身定制的具体目标做了讨论,但值得注意的是,许多目标仅限于某些类型的任务,或者仅与 MLM 目标兼容,而不是如今更普遍的 CLM 目标。这突显了需要特定但普遍适用的因果语言建模目标,可以在模型训练的早期有效捕捉长距离依赖性。通过与先前提到的目标相一致,这是可能实现的。
可靠的度量需求。在评估度量方面,文章的第 8 节中研究了许多可选项。根据在评估中的先前经验,常用的度量,如 ROUGE 分数,与人类判断分数存在显著差异,后者可以看作是「神谕」。随着 LLM 在现实世界场景中的快速部署,越来越迫切地需要更可靠的度量来评估长上下文能力,特别是在生成性任务中,其中精确的真实性难以捉摸。一个有希望的途径涉及利用最先进的 LLM (如 GPT4) 的鲁棒性作为人类评审的替代,尽管相关的高成本仍然在更广泛地在研究界中采用方面带来挑战。
#大模型加速~
2023 年,大型语言模型(LLM)以其强大的生成、理解、推理等能力而持续受到高度关注。然而,训练和部署 LLM 非常昂贵,需要大量的计算资源和内存,因此研究人员开发了许多用于加速 LLM 预训练、微调和推理的方法。
最近,一位名为 Theia Vogel 的博主整理撰写了一篇长文博客,对加速 LLM 推理的方法进行了全面的总结,对各种方法展开了详细的介绍,值得 LLM 研究人员收藏查阅。
以下是博客原文内容。
之前,我使用经典的自回归采样器手动制作了一个 transformer,大致如下:
def generate(prompt: str, tokens_to_generate: int) -> str:
tokens = tokenize(prompt)
for i in range(tokens_to_generate):
next_token = model(tokens)
tokens.append(next_token)
return detokenize(tokens)
这种推理方法很优雅,是 LLM 工作机制的核心。自回归 LLM 在只有数千个参数的情况下运行得很好,但对于实际模型来说就太慢了。为什么会这样,我们怎样才能让它更快?
本文整理了这个问题的解决方案,从更好的硬件利用率到巧妙的解码技巧。
为什么简单推理这么慢?
使用普通的自回归生成函数进行推理速度缓慢,主要有两个原因:算法原因和硬件原因。
从算法上讲,生成过程必须在每个周期处理越来越多的 token,因为每个周期我们都会将一个新 token 附加到上下文中。这意味着要从 10 个 token prompt 生成 100 个 token,需要在 10 + 11 + 12 + 13 + ... + 109 = 5950 个 token 上运行!(初始 prompt 可以并行处理,这就是为什么 prompt token 在推理 API 中通常更便宜的部分原因。)这也意味着模型在生成时会变慢,因为每个连续的 token 生成都有越来越长的前缀:
注意力(至少是普通注意力)也是一种二次算法:所有 token 都关注所有 token,导致 N^2 扩展,使一切变得更糟。
硬件原因是什么呢?很简单:LLM 规模很大。即使像 GPT-2 这样相对较小的模型也有 117M 参数,并且所有数据都必须存储在 RAM 中。RAM 确实很慢,现代处理器(CPU 和 GPU)通过在靠近处理器的地方设置大量高速缓存(cache)来弥补这一点,从而使访问速度更快。其细节根据处理器的类型和型号而有所不同,但关键是 LLM 权重不适合缓存,因此需要花费大量时间等待从 RAM 加载权重。这会产生一些不直观的效果!例如,即使激活张量(tensor)大 10 倍,对 10 个 token 进行操作也不一定比对单个 token 进行操作慢很多,因为主要的时间消耗在于移动模型权重,而不是进行计算。
指标
大模型推理速度「慢」到底是什么意思?谈到 LLM 推理,人们采用的指标有很多:
- Time to First Token(TtFT)—— 收到 prompt 和返回第一个 token 之间需要多长时间?
- 生成延迟 —— 收到 prompt 和返回最终 token 之间需要多长时间?
- 吞吐量
- 硬件利用率 —— 我们使用硬件的计算、内存带宽和其他功能的效率如何?
不同的优化对这些指标的影响不同。例如,批处理可以提高吞吐量并更好地利用硬件,但会增加 TtFT 和生成延迟。
硬件
加速推理的一个直接方法就是购买更好的硬件(通常是某种加速器 ——GPU 或 TPU),或者更好地利用您拥有的硬件。
使用加速器可以显著提高速度,但请记住,CPU 和加速器之间存在传输瓶颈。如果模型不适合加速器的内存,则需要在整个前向传播过程中进行交换,这会大大减慢速度。这也是 Apple M1/M2/M3 芯片在推理方面表现出色的原因之一 —— 它们具有统一的 CPU 和 GPU 内存。
关于 CPU 和加速器推理,另一个关键是充分利用硬件,适当优化程序。例如,在 PyTorch 中将注意力写入 F.softmax (q @ k.T/sqrt (k.size (-1)) + mask) @ v,能提供正确的结果,但如果使用 torch.nn.function.scaled_dot_product_attention,会将计算委托给可用的 FlashAttention,这可以更好地利用缓存的手写内核产生 3 倍的加速。
编译器
torch.compile、TinyGrad 和 ONNX 等编译器可以将简单的 Python 代码融合到针对硬件优化的内核中。例如,我可以编写以下函数:
def foo(x):
s = torch.sin(x)
c = torch.cos(x)
return s + c
简单来说,这个函数需要:
1. x.shape () 为 s 分配的内存
2. 对 x 进行线性 scan 以计算每个元素的 sin
3. x.shape () 为 c 的另一种内存分配
4. 线性 scan x 以计算每个元素的 cos
5. x.shape () 为结果张量分配的内存
6. 线性 scan s 和 c,将它们添加到结果中
这些步骤每一个都很慢,并且某些步骤需要跨越 Python 和本机代码之间的界限。如果我使用 torch.compile 编译这个函数会怎样?
>>> compiled_foo = torch.compile(foo, options={"trace.enabled": True, "trace.graph_diagram": True})
>>> # call with an arbitrary value to trigger JIT
>>> compiled_foo(torch.tensor(range(10)))
Writing FX graph to file: .../graph_diagram.svg
[2023-11-25 17:31:09,833] [6/0] torch._inductor.debug: [WARNING] model__24_inference_60 debug trace: /tmp/...zfa7e2jl.debug
tensor([ 1.0000, 1.3818, 0.4932, -0.8489, -1.4104, -0.6753, 0.6808, 1.4109,
0.8439, -0.4990])
如果进入 debug 跟踪目录并打开其中的 output_code.py 文件,torch 就会为 CPU 生成一个优化的 C++ 内核,将 foo 融合到单个内核中。如果使用 GPU 运行此程序,torch 将为 GPU 生成 CUDA 内核。
#include "/tmp/torchinductor_user/ib/cibrnuq56cxamjj4krp4zpjvsirbmlolpbnmomodzyd46huzhdw7.h"
extern "C" void kernel(const long* in_ptr0,
float* out_ptr0)
{
{
#pragma GCC ivdep
for(long i0=static_cast<long>(0L); i0<static_cast<long>(10L); i0+=static_cast<long>(1L))
{
auto tmp0 = in_ptr0[static_cast<long>(i0)];
auto tmp1 = static_cast<float>(tmp0);
auto tmp2 = std::sin(tmp1);
auto tmp3 = std::cos(tmp1);
auto tmp4 = tmp2 + tmp3;
out_ptr0[static_cast<long>(i0)] = tmp4;
}
}
}
现在,步骤就变成了:
1. x.shape () 为结果张量分配的内存
2. 对 x (in_ptr0) 进行线性扫描,计算 sin 和 cos 并将它们相加到结果中
对于大输入来说更简单、更快!
>>> x = torch.rand((10_000, 10_000))
>>> %timeit foo(x)
246 ms ± 8.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit compiled_foo(x)
91.3 ms ± 14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# (for small inputs `compiled_foo` was actually slower--not sure why)
请注意,torch.compile 将上面的代码专门用于传入 ((10,)) 的张量的特定大小。如果我们传入许多不同大小的张量,torch.compile 将生成超过该大小的通用代码,但具有恒定大小可以使编译器在某些情况下生成更好的代码。
这是 torch.compile 的另一个函数:
>>> x = torch.rand((10_000, 10_000))
>>> %timeit foo(x)
246 ms ± 8.89 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>> %timeit compiled_foo(x)
91.3 ms ± 14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# (for small inputs `compiled_foo` was actually slower--not sure why)
该函数具有数据相关的控制流,这意味着我们会根据变量的运行时值执行不同的操作。如果以与编译 foo 相同的方式编译它,我们会得到两个图(因此有两个 debug 目录):
>>> compiled_gbreak = torch.compile(gbreak, options={"trace.enabled": True, "trace.graph_diagram": True})
>>> compiled_gbreak(torch.tensor(range(10)))
Writing FX graph to file: .../model__27_inference_63.9/graph_diagram.svg
[2023-11-25 17:59:32,823] [9/0] torch._inductor.debug: [WARNING] model__27_inference_63 debug trace: /tmp/torchinductor_user/p3/cp3the7mcowef7zjn7p5rugyrjdm6bhi36hf5fl4nqhqpfdqaczp.debug
Writing FX graph to file: .../graph_diagram.svg
[2023-11-25 17:59:34,815] [10/0] torch._inductor.debug: [WARNING] model__28_inference_64 debug trace: /tmp/torchinductor_user/nk/cnkikooz2z5sms2emkvwj5sml5ik67aqigynt7mp72k3muuvodlu.debug
tensor([ 1.0000, -0.1756, 2.6782, -0.7063, -2.5683, 2.7053, 0.9718, 0.5394,
7.6436, -0.0467])
第一个内核实现了函数的 torch.sin (x) + torch.cos (x) 和 r.sum () < 0 部分:
#include "/tmp/torchinductor_user/ib/cibrnuq56cxamjj4krp4zpjvsirbmlolpbnmomodzyd46huzhdw7.h"
extern "C" void kernel(const long* in_ptr0,
float* out_ptr0,
float* out_ptr1,
bool* out_ptr2)
{
{
{
float tmp_acc0 = 0;
for(long i0=static_cast<long>(0L); i0<static_cast<long>(10L); i0+=static_cast<long>(1L))
{
auto tmp0 = in_ptr0[static_cast<long>(i0)];
auto tmp1 = static_cast<float>(tmp0);
auto tmp2 = std::sin(tmp1);
auto tmp3 = std::cos(tmp1);
auto tmp4 = tmp2 + tmp3;
out_ptr0[static_cast<long>(i0)] = tmp4;
tmp_acc0 = tmp_acc0 + tmp4;
}
out_ptr1[static_cast<long>(0L)] = tmp_acc0;
}
}
{
auto tmp0 = out_ptr1[static_cast<long>(0L)];
auto tmp1 = static_cast<float>(0.0);
auto tmp2 = tmp0 < tmp1;
out_ptr2[static_cast<long>(0L)] = tmp2;
}
}
第二个内核实现了 return r - torch.tan (x) 分支:
#include "/tmp/torchinductor_user/ib/cibrnuq56cxamjj4krp4zpjvsirbmlolpbnmomodzyd46huzhdw7.h"
extern "C" void kernel(const float* in_ptr0,
const long* in_ptr1,
float* out_ptr0)
{
{
#pragma GCC ivdep
for(long i0=static_cast<long>(0L); i0<static_cast<long>(10L); i0+=static_cast<long>(1L))
{
auto tmp0 = in_ptr0[static_cast<long>(i0)];
auto tmp1 = in_ptr1[static_cast<long>(i0)];
auto tmp2 = static_cast<float>(tmp1);
auto tmp3 = std::cos(tmp2);
auto tmp4 = tmp0 - tmp3;
out_ptr0[static_cast<long>(i0)] = tmp4;
}
}
}
这就是所谓的「graph break」,这会让编译后的函数变慢,因为必须离开优化后的内核并返回到 Python 来评估分支。最重要的是,另一个分支(return r + torch.tan (x))尚未编译,因为它尚未被采用。这意味着它将在需要时动态编译,在不合适的时间(例如在服务用户请求的过程中)就会很糟糕。
理解 graph break 的一个方便工具是 torch._dynamo.explain:
# get an explanation for a given input
>>> explained = torch._dynamo.explain(gbreak)(torch.tensor(range(10)))
# there's a break, because of a jump (if) on line 3
>>> explained.break_reasons
[GraphCompileReason(reason='generic_jump TensorVariable()', user_stack=[<FrameSummary file <stdin>, line 3 in gbreak>], graph_break=True)]
# there are two graphs, since there's a break
>>> explained.graphs
[GraphModule(), GraphModule()]
# let's see what each graph implements, without needing to dive into the kernels!
>>> for g in explained.graphs:
... g.graph.print_tabular()
... print()
...
opcode name target args kwargs
------------- ------ ------------------------------------------------------ ------------ --------
placeholder l_x_ L_x_ () {}
call_function sin <built-in method sin of type object at 0x7fd57167aaa0> (l_x_,) {}
call_function cos <built-in method cos of type object at 0x7fd57167aaa0> (l_x_,) {}
call_function add <built-in function add> (sin, cos) {}
call_method sum_1 sum (add,) {}
call_function lt <built-in function lt> (sum_1, 0) {}
output output output ((add, lt),) {}
opcode name target args kwargs
------------- ------ ------------------------------------------------------ ----------- --------
placeholder l_x_ L_x_ () {}
placeholder l_r_ L_r_ () {}
call_function tan <built-in method tan of type object at 0x7fd57167aaa0> (l_x_,) {}
call_function sub <built-in function sub> (l_r_, tan) {}
output output output ((sub,),) {}
# pretty cool!
像 torch.compile 这样的工具是优化代码以获得更好的硬件性能,而无需使用 CUDA 编写内核。
批处理
在生成的未优化版本中,我们一次向模型传递一个序列,并在每一步要求它附加一个 token:
为了批量生成,我们一次向模型传递多个序列,在同一次前向传递中为每个序列生成一个补全。这需要使用填充 token 在左侧或右侧将序列填充到相等的长度。填充 token(这里使用 [end])被隐藏在注意力掩码中,这样它们就不会影响生成。
由于以这种方式批处理序列允许模型权重同时用于多个序列,因此一起运行整批序列比单独运行每个序列花费的时间更少。例如,在我的机器上,使用 GPT-2 生成下一个 token:
- 20 tokens x 1 sequence = ~70ms
- 20 tokens x 5 sequences = ~220ms (线性扩展~350ms)
- 20 tokens x 10 sequences = ~400ms (线性扩展~700ms)
连续批处理
在上面的示例中,「Mark is quick. He moves quickly.」在其他序列之前完成,但由于整个批次尚未完成,我们需要继续为其生成 token("Random")。
连续批处理通过在其他序列完成时在其 [end] token 之后将新序列插入批处理来解决此问题。
缩小模型权重
浮点数有不同的大小,这对性能很重要。大多数情况下,对于常规软件,我们使用 64 位(双精度)IEEE 754 浮点,而 ML 传统上使用 32 位(单精度)IEEE 754:
>>> gpt2.transformer.h[0].attn.c_attn.weight.dtype
torch.float32
模型使用 fp32 进行良好的训练和推理,这为每个参数节省了 4 个字节 (50%),这个影响是巨大的,例如 7B 参数模型在 fp64 中将占用 56Gb,而在 fp32 中仅占用 28Gb。训练和推理期间的大量时间都花在将数据从 RAM 移动到缓存和寄存器上 —— 移动的数据越少越好。
fp16(或半精度)显然可以再节省 50%!这里有两个主要选项:fp16 和 bfloat16(brain float)。
在减少 fp32 的字段时,fp16 和 bfloat16 进行了不同的权衡:fp16 试图通过缩小指数和分数字段来平衡范围和精度,而 bfloat16 通过保留 8 位指数来保留 fp32 的范围,同时将分数字段缩小到小于 fp16,损失了一些精度。范围损失有时可能会成为 fp16 训练的问题,但对于推理来说,两者都可以,如果 GPU 不支持 bfloat16,fp16 可能是更好的选择。
还能更小吗?当然可以!
一种方法是量化以更大格式(例如 fp16)训练的模型。llama.cpp 项目(以及相关的 ML 库 ggml)定义了一整套量化格式。
这些量化的工作方式与 fp16 /bfloat16 略有不同 - 没有足够的空间来容纳整个数字,因此权重以块为单位进行量化,其中 fp16 充当块尺度(scale),然后量化块每个权重都乘以该尺度。
bitsandbytes 还为非 llama.cpp 项目实现了量化。
然而,使用更广泛的参数训练的模型量化越小,它就越有可能影响模型的性能,从而降低响应的质量。因此我们要尽可能少地采用量化,才能获得可接受的推理速度。
但我们也可以使用小于 fp16 的数据类型来微调或训练模型,例如使用 qLoRA 训练量化低阶适配器。
KV cache
在 Transformer 内部,激活通过前馈层生成 qkv 矩阵,其中每一行对应一个 token:
然后,qkv 矩阵被分割成 q、k 和 v,它们与注意力结合起来,如下所示:
以生成这样的矩阵:
现在,根据该层在 Transformer 中的位置,这些行可能会(在通过 MLP 之后)用作下一个 Transformer 块的输入,或者作为下一个 token 的预测,但请注意,每个 token 都有一行!这是因为 Transformer 经过训练可以预测上下文窗口中每个 token 的下一个 token。
# the gpt2 tokenizer produces 3 tokens for this string
>>> tokens = tokenizer(" A B C").input_ids
>>> tokens
[317, 347, 327]
# if we put that into the model, we get 3 rows of logits
>>> logits = gpt2(input_ids=torch.tensor(tokens)).logits.squeeze()
>>> logits.shape
torch.Size([3, 50257])
# and if we argmax those, we see the model is predicting a next token
# for _every_ prompt token!
>>> for i, y in enumerate(logits.argmax(-1)):
... print(f"{tokenizer.decode(tokens[:i+1])!r} -> {tokenizer.decode(y)!r}")
' A' -> '.'
' A B' -> ' C'
' A B C' -> ' D'
在训练过程中,这种行为是可取的 —— 这意味着更多的信息正在流入 Transformer,因为许多 token 都被评分。但通常在推理过程中,我们关心的只是底行,即最终 token 的预测。
我们如何才能从经过训练来预测整个上下文的 Transformer 中得到这一点呢?让我们回到注意力的计算。如果 q 只有一行(对应于最后一个 token 的行)怎么办?
那么,这一行就将作为注意力结果,即最后一个 token 的结果。
但只生成 q 的最后一行,意味着我们也只能在单行上运行生成 qkv 矩阵的层。那么 k 和 v 的其余行从哪里来?这就需要「KV 缓存(KV cache)」。
在模型内部,我们将注意力期间计算的 KV 值保存在每个 Transformer 块中。然后在下一次生成时,只传入单个 token,并且缓存的 KV 行将堆叠在新 token 的 KV 行的顶部,以产生单行 Q 和多行 KV。
下面是使用 HuggingFace transformers API 进行 KV 缓存的示例,默认返回 KV cache 作为模型前向传递的一部分。
>>> tokens
[317, 347, 327] # the " A B C" string from before
>>> key_values = gpt2(input_ids=torch.tensor(tokens)).past_key_values
>>> tuple(tuple(x.shape for x in t) for t in key_values)
((torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])),
(torch.Size([1, 12, 3, 64]), torch.Size([1, 12, 3, 64])))
KV cache 有助于解决 LLM 缓慢的问题,因为现在每个步骤中只传递一个 token,所以我们不必为每个新 token 重做所有事情。然而,KV cache 的大小仍然每一步都会增长,从而减慢了注意力计算的速度。
KV cache 的大小也会带来自己的新问题,例如,对于 1000 个 token 的 KV cache,即使使用最小的 GPT-2,也会缓存 18432000 个值。如果每个值都是 fp32,那么单次生成的缓存几乎为 74MB。对大模型来说,尤其是在需要处理许多并发客户端的服务器上运行的模型,KV cache 很快就会变得难以管理。
多查询注意力
多查询注意力(Multi-Query Attention,MQA)是对模型架构的改变,通过为 Q 分配多个头,为 K 和 V 只分配一个头来缩小 KV 缓存的大小。值得注意的是,使用 MQA 的模型比使用普通注意力训练的模型可以支持 KV 缓存中更多的 token。
分页注意力(PagedAttention)
大型 KV cache 的另一个问题是,它通常需要存储在连续的张量中,无论当前是否所有缓存都在使用。这会导致多个问题:
- 需要预先分配比所需更多的空间;
- 该保留空间不能被其他请求使用,即使还不需要它;
- 具有相同前缀的请求不能共享该前缀的 KV 缓存。
PagedAttention 从操作系统处理内存的方法中汲取灵感,解决了这些问题。
PagedAttention 会为请求分配一个块表(block table),类似于内存管理单元(MMU)。每个请求没有与大量 KV 缓存项相关联,而是仅具有相对较小的块索引列表,类似于操作系统分页中的虚拟地址。这些索引指向存储在全局块表中的块。
在注意力计算期间,PagedAttention 内核会遍历请求的块索引列表,并从全局块表中获取这些块,以便按照正确的顺序正常计算注意力。
猜测解码
要理解猜测解码,需要了解三件事。
首先,由于内存访问开销,模型运行少量 token 所需的时间与运行单个 token 大约相同:
其次,LLM 为上下文中的每个 token 生成预测:
>>> for i, y in enumerate(logits.argmax(-1)):
... print(f"{tokenizer.decode(tokens[:i+1])!r} -> {tokenizer.decode(y)!r}")
' A' -> '.'
' A B' -> ' C'
' A B C' -> ' D'
最后,有些词很容易预测。例如,在单词「going」之后,单词「to」极有可能是下一个 token。
def generate(prompt: str, tokens_to_generate: int) -> str:
tokens: list[int] = tokenize(prompt)
GOING, TO = tokenize(" going to")
for i in range(tokens_to_generate):
if tokens[-1] == GOING:
# do our speculative decoding trick
logits = model.forward(tokens + [TO])
# the token the model predicts will follow "... going"
going_pred = argmax(logits[-2, :])
# the token the model predicts will follow "... going to"
to_pred = argmax(logits[-1, :])
if going_pred == TO:
# if our guess was correct, accept "to" and the next token after
tokens += [TO, to_pred]
else:
# otherwise, accept the real next token
# (e.g. "for" if the true generation was "going for broke")
tokens += [going_pred]
else:
# do normal single-token generation
logits = model.forward(tokens)
tokens += [argmax(logits[-1])]
return detokenize(tokens)
我们只需要使用一个足够小的「draft 模型」(运行速度足够快),并使用相同的 tokenizer,以避免需要一遍又一遍地对序列进行 detokenize 和 retokenize。
然而,猜测解码的性能可能非常依赖于上下文!如果 draft 模型与 oracle 模型相关性很好,并且文本很容易预测,那么您将获得大量 draft token 和快速推理。但如果模型不相关,猜测解码实际上会使推理速度变慢,因为要浪费时间生成将被拒绝的 draft token。
def generate(prompt: str, tokens_to_generate: int, n_draft: int = 8) -> str:
tokens: list[int] = tokenize(prompt)
for i in range(tokens_to_generate):
# generate `n_draft` draft tokens in the usual autoregressive way
draft = tokens[:]
for _ in range(n_draft):
logits = draft_model.forward(draft)
draft.append(argmax(logits[-1]))
# run the draft tokens through the oracle model all at once
logits = model.forward(draft)
checked = logits[len(tokens) - 1 :].argmax(-1)
# find the index of the first draft/oracle mismatch—we'll accept every
# token before it
# (the index might be past the end of the draft, if every draft token
# was correct)
n_accepted = next(
idx + 1
for idx, (checked, draft) in enumerate(
# we add None here because the oracle model generates one extra
# token (the prediction for the last draft token)
zip(checked, draft[len(tokens) :] + [None])
)
if checked != draft
)
tokens.extend(checked[:n_accepted])
return detokenize(tokens)
阈值解码
一种缓解使用固定数量 draft token 问题的方法是「阈值解码」—— 并不总是解码最大数量的 draft token,而是保留一个移动概率阈值,根据当前的 token 数量进行校准。draft token 会一直生成,直到 draft 的累积概率低于此阈值。
例如,如果阈值是 0.5,并且我们以 0.75 的概率生成 draft token「the」,继续下去。如果下一个 token「next」的概率为 0.5,则累积概率 0.375 将低于阈值,因此会停止生成并将两个 draft token 提交给 oracle 模型。然后,根据 draft 被接受的程度,向上或向下调整阈值,以尝试用实际接受率来校准 draft 模型的置信度。
def speculative_threshold(
prompt: str,
max_draft: int = 16,
threshold: float = 0.4,
threshold_all_correct_boost: float = 0.1,
):
tokens = encoder.encode(prompt)
# homegrown KV cache setup has an `n_tokens` method that returns the length
# of the cached sequence, and a `truncate` method to truncate that sequence
# to a specific token
model_kv = gpt2.KVCache()
draft_kv = gpt2.KVCache()
while True:
# generate up to `max_draft` draft tokens autoregressively, stopping
# early if we fall below `threshold`
draft = tokens[:]
drafted_probs = []
for _ in range(max_draft):
logits = draft_model.forward(draft[draft_kv.n_tokens() :], draft_kv)
next_id = np.argmax(logits[-1])
next_prob = gpt2.softmax(logits[-1])[next_id]
if not len(drafted_probs):
drafted_probs.append(next_prob)
else:
drafted_probs.append(next_prob * drafted_probs[-1])
draft.append(int(next_id))
if drafted_probs[-1] < threshold:
break
n_draft = len(draft) - len(tokens)
# run draft tokens through the oracle model
logits = model.forward(draft[model_kv.n_tokens() :], model_kv)
checked = logits[-n_draft - 1 :].argmax(-1)
n_accepted = next(
idx + 1
for idx, (checked, draft) in enumerate(
zip(checked, draft[len(tokens) :] + [None])
)
if checked != draft
)
yield from checked[:n_accepted]
tokens.extend(checked[:n_accepted])
if n_accepted <= n_draft:
# adjust threshold towards prob of last accepted token, if we
# ignored any draft tokens
threshold = (threshold + drafted_probs[n_accepted - 1]) / 2
else:
# otherwise, lower the threshold slightly, we're probably being
# too conservative
threshold -= threshold_all_correct_boost
# clamp to avoid pathological thresholds
threshold = min(max(threshold, 0.05), 0.95)
# don't include oracle token in kv cache
model_kv.truncate(len(tokens) - 1)
draft_kv.truncate(len(tokens) - 1)
此外,分阶段猜测解码为普通猜测解码添加了一些改进。
指导型生成
语法指导型生成可约束模型的输出遵循某些语法,从而提供保证匹配某些语法(例如 JSON)的输出。这对模型推理的可靠性和速度都有益。
前向解码
前向解码是一种新的猜测解码方法,不需要 draft 模型。相反,模型本身用于两个分支:预测和扩展候选 N-gram 的前向分支和验证候选的验证分支。前向分支类似于常规猜测解码中的 draft 模型,而验证分支与 oracle 模型具有相同的作用。
还有一种方法是 prompt 查找解码,其中 draft 模型被上下文中的简单字符串匹配所取代。
最后,作者从稀疏注意力(sparse attention)和非 Transformer 架构两个方面简单阐述了训练时间优化的方法。感兴趣的读者可以阅读博客原文,了解更多内容细节。
原文链接:https://vgel.me/posts/faster-inference/?cnotallow=dc32042695bb7e31fb9280e21ab25f8c
Training time optimizations
#微调预训练模型
微调预训练模型是自然语言处理(NLP)领域的一项重要实践,可以优化它们在特定任务中的性能。在Monster API,我们了解开发人员在微调模型时面临的挑战,特别是当涉及复杂设置、内存限制、高GPU成本以及缺乏标准化实践时。
地址:https://blog.monsterapi.ai/no-code-fine-tuning-llm/
这就是为什么我们很高兴推出无代码LLM微调产品,该产品旨在简化和加快微调过程,同时为您提供所需的所有功能和可能性。
什么是微调LLM?为什么它如此重要?
虽然像GPT-J、LLaMA、Falcon、StableLM和OPT这样经过预训练的模型具有广泛的语言知识,但微调使开发人员能够提高他们在特定任务中的性能。微调使模型更加准确、上下文感知,并与目标应用程序保持一致。
微调不是从头开始训练语言模型,这需要大量的数据和计算资源,而是利用预先训练的模型的现有知识,并将其调整为专门的任务。
与微调LLM相关的挑战:
微调LLaMA和其他大型语言模型带来了一些挑战。复杂的设置、内存要求、GPU成本以及缺乏标准化实践可能会阻碍微调过程,并使开发人员获得适合其需求的模型变得复杂。
然而,通过Monster API的LLM FineTuner,这些挑战得到了有效解决:
- 复杂设置:Monster API简化了为FineTuning机器学习模型设置GPU环境的复杂过程,将其封装在用户友好、直观的界面中。这种创新方法消除了手动硬件规范管理、软件依赖性安装、CUDA工具包设置和其他低级配置的需要。通过抽象这些复杂性,Monster API LLM Finetuner允许用户轻松选择所需的模型、数据集和超参数,并专注于优化机器学习模型的实际任务。
- 内存限制:微调大型语言模型(如LLaMA)可能需要大量的GPU内存,这对许多开发人员来说可能是一个限制。Monster API通过优化FineTuning过程中的内存利用率来解决这一挑战。它确保在可用的GPU内存内有效地执行该过程,从而使大型语言模型FineToning更易于访问和管理,即使存在资源限制。
- GPU成本:当微调机器学习模型时,GPU成本可能会迅速上升,这使其成为并非所有开发人员都能负担得起的奢侈品。然而,Monster API利用其分散的GPU网络实现了对超低成本GPU实例的按需访问。因此,显著降低了与访问LLM的强大计算资源相关联的成本和复杂性。
- 标准化实践:行业中缺乏标准化实践可能会使微调过程令人沮丧且耗时。Monster API通过提供直观的界面和预定义的任务,以及创建自定义任务的灵活性,简化了这一过程。我们的平台引导您了解最佳实践,无需在错综复杂的文档和论坛中导航。
可以依赖Monster API来简化和流线化复杂的微调过程,使其快速而容易地被处理。通过使用Monster API,您可以轻松地使用LoRA将大型语言模型(如LLaMA 7B)与DataBricks Dolly 15k进行3个阶段的微调。
你猜怎么着?它不会让你倾家荡产,只需花费不到20美元。
有关用于演示的LLM和数据集的上下文:
LLaMA(Large Language Model Meta AI)是Facebook AI Research(FAIR)为机器翻译任务开发的一个令人印象深刻的语言模型。LLaMA基于Transformer架构,在大量多语言数据的语料库上进行训练,使其能够在许多语言对之间进行翻译。LLaMA 7B具有70亿个参数,是该模型的最小变体。
为了展示像ChatGPT这样的交互式和引人入胜的对话能力,我们使用了Databricks Dolly V2数据集。该数据集,特别是“data bricks-doolly-15k”语料库,由Databricks员工创建的15000多条记录组成,提供了丰富多样的会话数据来源。
只需五个简单的步骤,您就可以设置微调任务并体验显著的效果。
所以,让我们一起开始探索这个过程吧!
选择一个语言模型进行微调
- 选择适合您需求的LLM。您有多种流行的开源语言模型可供选择,例如Llama 7B、GPT-J 6B或StableLM 7B
- 第二部选择或创建任务:接下来,定义用于微调LLM的任务。使用Monster API,您可以灵活地从预定义任务中选择,如“指令精细调整”或“文本分类”。如果您的任务是唯一的,您甚至可以选择“其他”选项来创建自定义任务。我们重视您的具体要求,并提供相应的选择
- 选择一个HuggingFace数据集:为了有效地训练你的LLM,你需要一个高质量的数据集。Monster API与HuggingFace数据集无缝集成,提供了广泛的选择。我们甚至根据您选择的任务建议相关的数据集。只需点击几下,您选择的数据集就会自动格式化并准备好使用。我们希望让您能够方便地访问所需的数据
- 指定超参数:Monster API根据您选择的LLM预先填充大多数超参数,从而简化了过程。但是,我们知道您可能有特定的偏好。您可以自由定制时间、学习率、截止长度、热身步骤等参数,确保您的LLM能够根据您的确切要求进行微调
- 审查并提交微调工作:设置完所有参数后,您可以在摘要页面上查看所有内容。我们知道准确性的重要性,因此我们提供此步骤以确保您能够完全控制。一旦你对细节有信心,只需提交工作即可。从那里开始,我们负责剩下的事情
就是这样!只需五个简单的步骤,您的作业就可以提交给您选择的LLM进行微调。我们致力于简化流程,这样您就可以专注于您的任务,而不会被复杂的配置所淹没。
使用Monster API成功设置微调作业后,可以通过WandB上的详细日志监控性能。我们相信为您提供所需的见解,以便您做出明智的决策并取得最佳结果。
Monster API的优点
Monster API的FineTuning LLM产品的价值在于它能够简化大型语言模型(LLM)的使用并使其民主化。通过消除技术复杂性、内存限制、高GPU成本和缺乏统一实践等常见障碍,该平台使人工智能模型微调变得容易和高效。通过这样做,它使开发人员能够充分利用LLM,促进更复杂的人工智能应用程序的开发。
#GLM-4
2024 年,国产大模型的第一个重磅消息,来自智谱 AI。最接近GPT-4的国产大模型诞生了
在 1 月 16 日举办的 2024 智谱 AI 技术开放日 Zhipu DevDay 上,智谱 AI 正式发布新一代基座大模型「GLM-4」。
经历了三个月的技术迭代,GLM-4 相比上一代基座模型 GLM-3 实现了 60% 的性能全面提升,直接逼近 GPT-4。
一方面,GLM-4 支持更长的上下文、更强的多模态能力;另一方面,GLM-4 支持更快的推理、更多并发,大大降低了推理成本。
同时,GLM-4 也增强了智能体能力,智谱 AI 正式上线了「GLM-4-All Tools」和「GLMs」个性化智能体定制能力,在产品上持续全面对标 OpenAI。
这些全新升级后的能力,目前已经在智谱 AI 开放平台上线。
「正如我们在去年年初的承诺,2023 年底要实现逼近最先进的 GPT-4 性能的全栈自主创新的 GLM-4。今天,我们来交个答卷,也希望未来能以此为基础瞄向 AGI。」智谱 AI CEO 张鹏表示。
综合能力全面跃升
国产基座大模型比肩 GPT-4
一直以来,AI 领域内的研究者和从业者都以「GPT-4」作为大模型技术的标杆。而 GLM-4 的诞生,意味着国产大模型的水平真正做到了「比肩 GPT-4」。
据张鹏介绍,GLM-4 带来了 128K 上下文窗口长度,单次提示词可处理文本达到 300 页,在总结信息、内容抽取、复杂推理、代码等多个应用场景实现了复杂长文本能力。
智谱 AI 技术团队解决了长上下文全局信息因失焦导致的精度下降问题。在 needle test 大海捞针测试中,在 128K 文本长度内, GLM-4 模型可实现几乎 100% 精度召回。
张鹏表示,GLM-4 性能已经超过 Claude 2.1,直接逼近 GPT 4 。
智谱 Al 进行的多项大模型权威评测的结果证实了这一说法,GLM-4 在 MMLU(81.5)达到 GPT-4 94% 水平,GSM8K(87.6) 达到 GPT-4 95% 水平,MATH(47.9)达到 GPT-4 91% 水平,BBH (82.25) 达到 GPT-4 99% 水平,HellaSwag (85.4) 达到 GPT-4 90% 水平,HumanEval(72)达到 GPT-4 100% 水平。
在指令跟随能力方面,GLM-4 也实现了媲美 GPT-4 的水准。根据指令跟随评估基准 IFEval 的结果,GLM-4 在 Prompt 提示词跟随(中文)方面达到了 GPT-4 88% 的水平;在指令跟随(中文)方面,达到了 GPT-4 90% 的水平。
在实际落地应用过程中,模型的中文对齐能力格外重要,GLM-4 的表现也毫不逊色。
基于公开数据集 AlignBench 的评估结果,GLM-4 超过了 GPT-4 在 6 月 13 日发布的版本,逼近 GPT-4 最新(11 月 6 日版本)效果,在专业能力、中文理解、角色扮演方面超过了最新 GPT-4 的精度,唯一有待提升的是 GLM-4 在中文推理方面的能力。
短短几个月,GLM-4 即可实现多项模型能力的飞跃,与智谱 AI 长期以来所坚持的「All in 大模型」路线密不可分。
自成立以来,智谱 AI 始终致力于打造新一代认知智能大模型,从一开始探索超大规模预训练模型算法,到训练从几十亿、几百亿到千亿级的模型,并逐步探索实现规模化的产业应用落地。
2020 年底,智谱 AI 从 0 起步研发 GLM 预训练架构,在确保性能的同时具有独立、自主、可控特性。2022 年,智谱 AI 率先推出中英双语千亿级超大规模预训练模型 GLM-130B,引发了全球关注。
在 2023 年的大模型浪潮之中,智谱 AI GLM 系列大模型保持每 3-4 个月升级一次的节奏,同时逐步具备了多模态理解、代码解释、网络搜索增强等新功能。
多模态能力的水平,是决定模型能否应用于复杂现实场景的关键因素。此次 GLM-4 的多模态能力能够实现长足的进步,正是基于不断进化的智谱 AI 多模态理解模型 CogVLM 和文生图模型 CogView。
其中,最新发布的 CogView3 效果明显超过开源最佳的 Stable Diffusion XL,逼近最新 OpenAI 发布的 DALLE・3。在对齐、保真、安全、组合布局等各个评测维度上,CogView3 的效果都达到 DALLE・3 90% 以上水平,平均达到 95% 左右的相对性能。
GLM-4-All Tools 上线
产品持续对标 OpenAI
大模型本身还不足以解决所有问题,在实际的使用过程中,我们常常需要同时借助多种工具,比如网页浏览、数据分析、图像生成。
张鹏在演讲中提到了这一难点:「一直以来,普通用户甚至是开发者,需要用像魔法咒语一样的提示词或者机器才能解读执行的代码、调用大模型的各种能力,无论是回答问题、作画还是使用外部的知识源,总觉得大模型还是没那么聪明。」
这些复杂任务,都可以依靠「Agent」来完成。
强大的 Agent 能力,同样是提升 GLM-4 模型使用体验的关键因素之一。在这一次的技术开放日,智谱 AI 正式推出了「GLM-4-All Tools」。
「GLM-4-All Tools」提供了一系列强大的内置工具,让 GLM-4 实现了自主根据用户意图,自动理解、规划复杂指令,自由调用 WebGLM 搜索增强、Code Interpreter 代码解释器和多模态生成能力以完成复杂任务。开发者和用户可以更轻松地使用 GLM-4 模型,不再需要为提示词担心。
在现场,我们也看到了多个功能演示:以文生图为例,GLM-4-All Tools 能够准确地根据上下文语境进行 AI 绘图创作:
面对涉及复杂计算的指令,GLM-4 内嵌了代码解释器,能够自动进行复杂的方程或者微积分求解。对比 GSM8K、Math 以及 Math23K 三个数据集上的结果,GLM-4 取得了与 GPT-4 All Tools 相当的性能。
此外,GLM-4-All Tools 可以自动处理各种任务,包括文件处理、数据分析、图表绘制等,处理的文件类型覆盖我们常用的 Excel、PDF、PPT 等格式。在信息检索方面,All Tools 使得 GLM-4 从早期的检索自动增强升级为目前模型对网页的自动浏览能力。
「GLM 系列模型的全家桶能力,终于实现了 All-In-One。」张鹏总结道。
从全行业的角度来说,GLM-4-All Tools 的意义或许更为突出。基于比肩 GPT-4 的基座大模型能力,这一功能的上线将为应用、行业模型和商业化落地案例开辟出更加广阔的实践空间。
立足智能体开发
携手开发者、社区构筑更繁荣生态
能力水平的高低决定了大模型能否在未来的竞争中生存下去,而围绕大模型构筑起完善的生态链能够助力它们走得更远。二者缺一不可,相互促进。
一直以来,智谱 AI 在推进自家 GLM 系列模型研发时注重能力与生态「并举」,从第一代大模型创建起便在生态建设层面下足功夫, 将广大开发者、用户、开源社区、科研界以及产业链上下游合作伙伴都纳入进来。
在这一次的技术开放日活动中,我们见证了智谱 AI 加速构建 GLM 模型生态的一系列举措。
一项重磅推出是「GLMs」个性化智能体定制能力,为包括无编程基础开发者在内的所有人提供了创建专属智能体的新渠道。
目前,该功能已经上线智谱清言官网。基于 GLM-4 模型的强大基础能力,任何用户都可以使用简单的提示词创建定制化的 GLM 智能体。
智能体创建地址:https://chatglm.cn/glms
至于效果怎样?张鹏现场让 GLM 智能体「智谱 DevDay」总结了技术开放日上午的议程,从结果来看,内容准确、没有遗漏。
GLMs 定制化智能体可以让任何人使用并充分挖掘 GLM-4 模型的潜力,结合自身所在领域以及专业知识、创意和智慧,自由创建更加多样化的智能体,实现便捷开发,进一步构建开放的大模型社区生态。同时更多人参与其中有助于更大规模地推动大模型及智能体在垂直场景和领域的部署,为应用落地提供了新的方式。
接下来,GLMs 模型应用商店以及开发者分成计划也将同期发布。这些与 GLMs 智能体一道构成了智谱 AI 在扩大开发者生态层面的重要尝试和迈出的关键步伐,让开发者应用大模型的门槛不断降低。
对于模型开源和对开源社区的贡献,智谱 AI 也一直走在国内外前列。智谱 AI 先后开源了中英双语对话大模型 ChatGLM-6B 和 ChatGLM2-6B,全球累计下载量超过了 1000 万,GitHub 星标累计超过 5.4 万。这些开源模型也「开花结果」,开发者已经在其上开发出了 600 多项优秀的大模型应用开源项目。
此次,智谱 AI 为开源社区带来了更多好消息,让我们看到了其围绕开源模型构筑更繁荣开源社区、建立更大模型生态的决心。
智谱 AI 针对开源社区发起了开源开放的大模型开源基金,概括为三个「一千」:
- 为大模型开源社区提供 1000 张卡;
- 提供 1000 万现金支持与大模型相关的开源项目;
- 为优秀的开源开发者提供 1000 亿免费 API tokens。
这些都是智谱 AI 为开发者和开源社区谋得的实打实的实惠,通过奖励为大模型普及、推广和应用做出贡献的开发者组织和个人,充分调动他们的积极性,进一步推动大模型研发,促进整个开源生态的发展。
同时面向科研界,联合中国计算机学会和中国中文信息学会社会媒体处理专委会分别发起了 CCF - 智谱大模型基金和 SMP - 智谱大模型交叉学科基金,围绕预训练大模型理论、算法、模型、应用以及与各领域的交叉创新做「深」文章。
为了助力生态伙伴走好大模型创业的「长征路」,智谱 AI 还面向全球大模型初创团队和小微企业升级了「Z 计划」创业基金,总额 10 亿人民币,覆盖大模型算法、底层算子、芯片优化、行业大模型和超级应用,旨在支持更多大模型原始创新和能力升级。
大模型的研发和应用涉及复杂的产业链条,任重而道远。从上述措施中,我们看到了智谱 AI 对于开发者生态、开源社区和客户的坚定承诺。
正如张鹏所言:「一枝独秀不是春,中国人工智能事业要繁荣、要发展,需要所有的参与者、产业链上下游合作伙伴、开发者社区、学术界一同努力。智谱不仅有意愿,而且有能力为打造繁荣的国产大模型生态贡献自己的力量。」
AGI 元年,智谱 AI 开局就要争先
自 ChatGPT 推出以来,我们见证了轰轰烈烈的百模大战、此起彼伏的 AIGC 应用落地以及成为厂商全新角逐点的 AI Agent。要想在每一个阶段都不被落下,则要事事争先、抓住每一次发展时机。
面向即将到来的更激烈的大模型之争,智谱 AI 已经做好了准备。正如此次技术开放日的一系列模型发布和能力升级,其每次都能给出及时的回应:
一方面不断夯实并升级 GLM 基座大模型的能力,并抓住 AI Agent 发展契机形成自己的 GLM 智能体发展体系,将大模型的应用做得更深、更广、更全;另一方面联动开发者、社区、科研界以及客户等各方,无死角地构筑起一个更宏大的模型生态圈层,打造更完整的生态链。
2024 年伊始,智谱 AI 带来了国产大模型带来了好消息,并以此为起点瞄准 AGI 之路。在未来的技术求索和产业应用道路上,智谱 AI 会成为最先赶超 OpenAI 的国内大模型创业力量吗?我们拭目以待。