洗手盆的注意事项和存储位置:流式 LLM 实现的可视化指南

洗手盆的注意事项和存储位置:流线型 LLM 实现的可视化指南

GPT-2滚动缓存文本生成器块

最近引起轰动的一篇人工智能论文是一种针对生成式预训练Transformer(GPT)模型架构的技术,可以实现高效、无限大小的上下文窗口用于文本生成。这是通过利用“注意力汇聚”的发现实现的,即下一个标记预测(自回归)中的最早标记在构建文本表示的自注意力中起到了大部分工作。这种方法非常实用,因为它不需要微调,只需要对GPT架构进行最少的修改。本文重点介绍了这些修改的详细规范,以便您可以放心地了解如何实践。

为了提醒您其重要性,普通的语言模型(LLM)在上下文长度变长时需要指数级的内存和处理时间来生成下一个标记。而且,许多模型实际上没有在非常长的输入上进行训练,所以当输入变长时它们表现不佳。每次模型生成下一个标记时,窗口都会变长。想象一下GPT写一本书的结尾。为了理解它已经写了所有内容,它需要保持一个非常长的上下文窗口,否则书的结尾将无法总结所有情节细节。

论文:

https://arxiv.org/pdf/2309.17453v1.pdf

本文的其余部分将着重介绍实际技术,而不是对其进行辩解或检查结果。论文中有关该技术的实际文本相对较少。基本上,您选择一些“注意力汇聚”,然后在该汇聚后有一个固定大小的令牌嵌入队列。在每次迭代中,当生成下一个令牌嵌入时,您保留汇聚嵌入,并且只丢弃队列末尾的标记的嵌入。

下面是一个以文本“Hmm, okay so this is some input”为开头的示例:

Hmm, okay so this is some input

假设我们有3个注意力汇聚和最大令牌长度为7。最初,我们将所有令牌通过层运算产生7个令牌嵌入,并且我们生成第8个令牌。假设它生成的下一个令牌是“text”,在粗体中是我们将要剔除的令牌。

[Hmm, okay, so, this, is, some, input] → “text”

然后,在下一次迭代中,我们滚动队列并删除尽早出现在汇聚之后的标记。

[Hmm, okay, so, is, some, input, text] → “and”

我们将继续这样做直到结束。

[Hmm, okay, so, some, input, text, and] → “this”

另一点需要记住的是,位置嵌入不会向前滚动,它们保持不变。这意味着每次迭代时与令牌相关的位置嵌入都会发生更改。

可视化细节

计算步骤将使用节点图可视化工具进行可视化展示。每个方块表示一个操作,它接受左侧的输入并生成右侧输出变量的数据。连接表示从输出传递数据到输入,并且输入处的圆圈表示数据已在原地指定且静态。

操作可以是复合操作,包含一个“解箱”图标,然后将其分解为子图,其输入为父级输入,输出为父级输出;或者它们是原始操作,意味着它们不能再进一步分解,对应于低级张量操作,如NumPy或TensorFlow中的操作。颜色表示数据类型,模式表示数据形状。蓝色表示数据类型为整数,而紫色/粉色表示它是十进制数据类型,绿色表示它是文本。实线连接表示数据形状为标量,而连接中的圆点表示数组的维度数(破折号之间的圆点数量)。在每个图的底部有一个表格,描述了模型中携带数据的每个变量的形状、类型和操作名称。

我在之前的帖子中已经介绍了并使用了可视化功能,例如为 GPT全可视化BERT全可视化 创建参考地图,并为 图注意力网络LoRA微调方法BERTScore 提供可视化演示。

可视化实现

让我们深入实现细节。下面的代码是循环的开头。在每次迭代中,我们都有下一个标记和迄今为止的连接标记。我们只将下一个新标记传递给GPT,因为我们将在每层上读取和写入缓存的嵌入。这是一种实现技巧,使其更高效,因此在每次新迭代中,我们只需获取最新标记的嵌入。

在继续之前,让我们检查模型的一些超参数(全局变量)。全局常量是图中静态的值。

我们将从包含GPT-2权重的公共数据库中读取数据,并将其缓存在指定的目录路径“gpt_2_rolling_cache”中。这些缓存路径用于存储每个权重和函数的参数,如模型参数,这些参数都保存在内存中。

您可以看到,我们将注意力池大小设置为3,标记最大数量设置为7。这意味着我们限制模型一次处理的标记不超过7个,这非常短,但这只是一个示例。通常,该数字将与用于训练的原始上下文长度匹配,对于这个小型的GPT-2模型,该长度为32。每次处理下一个标记时,我们将在注意力池之后删除最早的标记,并总共仅查看3个注意力池 + 每次迭代的最后4个标记。

但是当我们说“查看标记”时,实际上指的是什么呢?让我们深入了解各个层。仅查看第0层,您可以按照面包屑跟踪来看看我们在架构中的位置。这里,我们正在获取键和值权重的密集投影层。

在密钥滚动缓存中,我们从缓存中读取权重。请注意,我们位于一个条件块中,因此在循环的第一次迭代中,我们只会写入缓存而不读取。缓存包括上一次迭代的标记嵌入。嵌入的形状为[1, 12, 7, 64]。

  • 维度0是批处理大小(1),
  • 维度1是注意力头的数量(12),
  • 维度2是标记数量(7),
  • 维度3是隐藏大小(768)除以注意力头的数量(64)。
在每个层中从缓存中读取

围绕读取文件周围的传入链接仅针对传入的标记。在我们示例的循环的第一次迭代中,它是 [1, 12, 7, 64],然后在每个后续迭代中,它仅运行给下一个标记,即 [1, 12, 1, 64]。我们要做的第一件事是将注意力池(在维度2上)拆分开,然后沿维度2轴连接新的嵌入。注意力池权重跳转到下一个连接。在驱逐块内,我们将从队列末尾驱逐1个或多个标记。

滚动缓存逻辑

在逐出块中,您可以看到我们计算要从维度2的开头切片(即逐出,是的,逐出听起来更好)多少令牌嵌入。通常,每个新令牌会导致逐出1个令牌。

逐出

最后,我们将结果与前面的注意力汇集嵌入连接起来并向前传递。当我们在自注意操作中获取查询键值层时,对于每个层,键和值权重也是如此。

查询键值获取嵌入

最后,仅剩下位置编码。在“创建位置ID”块中,我们可以更新位置嵌入逻辑。逻辑相对简单。要么如果我们尚未达到令牌长度,我们增加下一个令牌的位置嵌入,否则我们保持它们相同并获取相同的位置嵌入。

创建位置ID

举个例子,我将GPT-2与滚动缓存和不使用滚动缓存对比,用于生成20个令牌,从我之前给出的例子“嗯,好的,这是一些输入文本”开始。这仍然很短,几乎不需要滚动缓存,但它表明滚动缓存正在工作。

GPT2不使用滚动缓存:

GPT2使用滚动缓存(最多7个令牌和3个注意力汇集):

它们是不同的,这是预期的,但一个比另一个更好吗?

谢谢阅读!完整的图形可在Github上以JSON格式获取。你觉得怎么样?我有什么疏漏吗?你想看到下一篇什么文章?请在评论中告诉我!