LLM 推理系列:深入解析 KV 缓存

在之前的文章中,我们介绍了 KV 缓存,这是一种常见的 LLM 推理过程优化方法,它可以使(自)注意力机制的计算需求随总序列长度(提示 + 生成的完成)线性增长,而不是二次增长。

更具体地说,KV 缓存通过在生成过程中计算时将过去标记的键和值张量存储(“缓存”)在 GPU 内存中,来避免在每个生成步骤中重新计算这些张量。

KV 缓存是一种折衷方案:我们用内存换取计算。在这篇文章中,我们将了解 KV 缓存可以增长到多大,它会带来什么挑战,以及解决这些挑战最常用的策略。

KV 缓存到底可以增长到多大?

这很简单:对于批次中每个序列的每个标记,我们需要为每个注意力层的每个注意力头存储两个向量张量(一个键张量和一个值张量),每个张量的尺寸为 $d_{head}$。每个张量参数所需的存储空间取决于精度:全精度(FP32)为 4 字节/参数,半精度(BF16,FP16)为 2 字节/参数,8 位数据类型(INT8,FP8)为 1 字节/参数,等等。

假设 $b$ 为批次大小,$t$ 为总序列长度(提示 + 完成),$n_{layers}$ 为解码器块/注意力层的数量,$n_{heads}$ 为每个注意力层的注意力头的数量,$d_{head}$ 为注意力层的隐藏维度,$p_a$ 为精度。多头注意力(MHA)模型的 KV 缓存的每个标记的内存消耗(以字节为单位)为:

$$
KV_cache_size_per_token = 2 \cdot n_{heads} \cdot d_{head} \cdot p_a
$$

注意: 我们提醒,在 MHA 模型中,$n_{heads} \cdot d_{head} = d_{model}$,但我们不会使用它来简化上面的公式。

因此,KV 缓存的总大小(以字节为单位)为:

$$
KV_cache_size = b \cdot t \cdot KV_cache_size_per_token
$$

KV 缓存的第一个挑战出现了:它随着批次大小线性增长,更重要的是随着总序列长度线性增长。由于它随着总序列长度增长,因此 KV 缓存的大小实际上是无界的,而我们的 GPU 内存显然是有限的。更糟糕的是,由于总序列长度无法提前知道,因此 KV 缓存的内存需求也是未知的,这使得内存管理特别具有挑战性。

让我们看看一些流行的 MHA 模型(表 1)的数字,包括 Meta 的 Llama-2 [1] 和 OPT [2],MosaicML 的 MPT [3] 和 BigScience 的 BLOOM [4]:

模型参数层数头数隐藏维度
Llama-2–7B7B32324096
OPT-1.3B1.3B2416768
MPT-7B7B24324096
BLOOM-176B176B608012288

表 1 – 流行的多头注意力 (MHA) 模型规格

假设参数存储在半精度(FP16,BF16)中,并选择一个较小的模型(Llama-2–7B)和一个较大的模型(BLOOM-176B)。对于 Llama-2–7B(分别为 BLOOM-176B),KV 缓存的内存消耗约为 0.5MB/标记(分别为 4MB/标记)。

让我们关注 Llama-2–7B。使用半精度,加载模型权重消耗约 14GB 内存,与缓存 28k 个标记的键和值相同。28k 个标记可能对应于 56 个长度为 512 的序列的批次,这并不算特别极端。

从上面的数字可以看出,KV 缓存的内存消耗会变得非常大,甚至可能超过加载大型序列模型权重所需的内存量。

现在让我们将这些数字与常用的 NVIDIA 数据中心 GPU 的内存容量进行比较(表 2):

GPU内存
A1024GB
A10040GB
H10080GB
H20096GB

表 2 – 常用于训练和/或服务 LLM 的 NVIDIA 数据中心 GPU 规格

让我们选择性价比比较高的 A10 GPU,坚持使用 Llama-2–7B 并计算最大 KV 缓存容量。加载模型权重后,还有 24-2×7=10 GB 可用于 KV 缓存,即总容量约为 20k 个标记,包括提示,这显然不允许在处理或生成长序列时,特别是当处理或生成长序列时,无法处理大量并发请求。

我们现在了解到,KV 缓存阻止我们处理或生成非常长的序列(即阻碍长上下文窗口)和/或处理大型批次,因此无法最大限度地提高硬件效率。

从这个角度来看,最大限度地提高处理能力意味着为 KV 缓存留出尽可能多的空间,这可以通过以下方法实现:

  • 减少模型权重的内存占用(权重量化)
  • 减少 KV 缓存的内存占用(见下文)
  • 通过将模型在多个 GPU 上分片,以网络通信为代价,从多个设备中汇集内存(模型并行),或使用其他类型的存储,如 CPU 内存或磁盘(卸载)。

由于模型权重和不断增长的 KV 缓存必须在每次前向传递时加载,因此解码步骤涉及非常大的数据传输,正如我们将在后面的文章中看到,实际上是内存带宽受限的,即我们实际上花费更多的时间移动数据,而不是进行有用的工作,即计算。在这种情况下,延迟只能通过拥有更多内存带宽(即更好的硬件)或传输更少数据来改善。更小的模型权重和 KV 缓存释放了内存,可以容纳更多序列,因此能够提高吞吐量(和/或最大序列长度)。

在这方面,内存占用减少策略具有三重用途,因为它们允许我们提高硬件利用率,从而提高成本效益,同时降低延迟并提高吞吐量。

为什么我需要为输入标记付费?

在这一点上,你应该明白为什么你需要为输入和输出标记付费。一旦输入提示被处理,即在预填充阶段结束时,我们已经消耗了 GPU 内存(用于存储每个输入标记的键和值张量)和计算(用于将提示标记通过模型)。

让我们看看一些实际数字。假设 P 参数模型的前向传递的总 FLOPs 数量约为 2.P FLOPs/标记 [5],使用 Llama-2-7B 处理提示消耗约 0.5 MB/标记的 GPU 内存(见上文)和约 14 GFLOPs/标记的 GPU 计算。对于一个 1000 个标记的提示(略小于两页),那就是约 500MB 的内存和 14 TFLOPs 的计算,而且我们还没有生成任何内容。

现在让我们看看所有可以减少 KV 缓存的内存占用的方法,方法是采用上面的公式,依次查看每个项:

减少批次大小?

在大多数情况下,我们不想减小批次大小,因为虽然它有助于减少 KV 缓存的内存占用,从而降低延迟,但它会降低硬件利用率,从而降低成本效益。在后面的文章中,我们将看到,相反,我们希望尽可能地增加批次大小。

减少对总序列长度的依赖?

不为序列中的所有标记存储键和值的一个原因是,我们明确选择在每次迭代时重新计算缺失的键和值,因为花费 FLOPs 比消耗 GPU 内存更划算(例如,因为我们是内存带宽受限的,在自回归阶段就是这样)。据我所知,这在实践中并不是我所知道的,所以我们不会深入研究这个方向。

另一个角度是,我们可以不必为模型不关注或很少关注的标记存储键和值。对于经过训练只关注序列一部分的模型(例如,使用 Mistral AI 的 Mistral-7B),或者作为内存消耗和模型精度之间的一种折衷方案,这可能是这种情况。让我解释一下。

像 Mistral-7B [6] 这样的模型经过训练不会关注整个序列。Mistral-7B 的注意力层实际上通过只关注最后(4096)个相邻标记来构建标记表示。这种注意力机制的变体称为滑动窗口注意力(SWA)或局部注意力。通过设计,局部注意力保证我们永远不会在 KV 缓存中为每个序列存储超过窗口大小(例如 4096)的张量对。

另一种方法是利用注意力层在序列中将注意力分散到标记上的方式中的模式。众所周知,注意力模块不成比例地且始终如一地将更多注意力分配给序列中的一小部分标记(图 1)。相反,许多标记始终对输出的贡献很小,那么为什么要费心存储它们的键和值呢?

通过丢弃这些标记,我们实际上将相应的注意力分数设置为零,并用更稀疏的矩阵来近似注意力矩阵。成功的近似将最大限度地减少近似误差,从而最大限度地减少对模型精度的影响(例如,使用困惑度来衡量)。

让我们看看过去几个月出现的一些方法,这些方法可以直接应用,无需任何重新训练或微调:StreamingLLM 框架、H2O(重击者预言机)、剪刀手和 FastGen。据我所知,目前还没有任何流行的 LLM 推理框架支持它们。

针对使用有限长度上下文窗口训练的模型,StreamingLLM 框架 [7] 基于这样一个观察结果:初始标记会收集大量的注意力。因此,该框架通过只在缓存中保留最开始的几个位置标记(“汇聚标记”)和最后一个相邻标记(局部注意力)来构建一个滑动窗口。因此,StreamingLLM KV 缓存的长度是固定的,既有固定部分(通常为 1 到 4 个标记),也有滑动部分。

类似的 H2O [8] 和剪刀手 [9] 方法明确地旨在通过设置一个最大缓存标记数(预算)并通过在每次达到缓存预算时丢弃标记来压缩 KV 缓存。H2O 算法一次只丢弃一个标记,而剪刀手则根据目标压缩率(例如,减少 30% 的 KV 缓存大小)丢弃尽可能多的标记。

这两种方法都基于这样一个观察结果:在给定步骤中具有影响力的标记(“关键标记”或“重击者”)在未来步骤中仍然具有影响力(剪刀手作者称之为“重要性持久性假设”)。换句话说,我们可以确保被丢弃的低影响力标记在未来步骤中将仍然相对被忽略,因此可以安全地丢弃。

这两种算法的一个关键方面显然是缓存驱逐策略。剪刀手只保留最新的标记和历史窗口内注意力分数最高的标记。H2O 丢弃累积注意力分数最低的标记,因此只保留在迭代过程中始终获得高注意力分数的标记。这两个作者团队都表明,他们的算法可以实现高达 80% 的 KV 缓存大小减少,而模型精度损失可以忽略不计。

FastGen 方法 [10](不要与无关的 DeepSpeed-FastGen 混淆)仍然基于注意力模式,但采用另一种方法,即不设置缓存预算,而是设置注意力矩阵的最大近似误差,因此侧重于保持模型精度。

FastGen 是一种两步法:首先,在预填充阶段结束时对模型的注意力层进行分析,以确定允许满足误差目标的压缩策略集。与其他方法一样,它假设所识别的注意力模式将在未来的生成步骤中保持不变。压缩策略包括:保留特殊标记、保留标点符号标记、保留最后一个相邻标记(局部注意力)等等(图 2)。如果误差目标过于严格,无法满足,FastGen 将回退到常规的 KV 缓存。然后,在每个生成步骤中将所选的压缩策略应用于 KV 缓存。

图 2 – FastGen 论文中压缩策略集的示例:特殊标记(绿色)+ 标点符号标记(橙色)+ 局部注意力(蓝色)。被丢弃的标记用灰色表示。

请注意,与其他方法不同,FastGen 为每个提示构建一个量身定制的压缩策略。FastGen 作者表明,对于给定的 KV 缓存压缩率,他们比 H2O 和剪刀手更好地保持模型精度。

在任何情况下,打破对不可预测的总序列长度的依赖都是一种解脱,因为它允许为每个序列分配一个内存预算,从而大大简化内存管理。由于数据传输是延迟的主要贡献者,因此没有一个随着序列长度线性增长的 KV 缓存,尤其是在较长的序列长度下,可以带来惊人的加速。

减少层数?

这里没有太多可以获得的。较小的模型通常具有较少的层(表 4),因此如果较小的模型在你的用例中表现良好,只需选择它即可。

模型参数层数头数隐藏维度
Llama-2–7B7B32324096
Llama-2–13B13B40404096
Llama-2–70B70B60644096
Llama-2–13B13B80804096

表 4 – Llama-2 模型规格

减少注意力头的数量?

由于对于给定的模型架构,模型大小主要由层数和头数控制,因此减少头数可能意味着选择一个更小的模型(见表 4)。

但是,如果我们仔细观察,我们会注意到,我们只需要减少键和值头的数量,查询头的数量不会影响 KV 缓存的大小。这正是多查询注意力(MQA)[11] 和分组查询注意力(GQA)[12] 架构背后的理念。这些多头注意力(MHA)变体的唯一动机是减少 KV 缓存的大小。

MQA 最早于 2019 年被引入。在 MQA 中,所有查询头共享相同的单个键和值头。换句话说,所有查询头使用相同的键计算它们的注意力分数,所有头的输出使用相同的值(但不是相同的注意力分数)计算(图 3)。

图 3 – 多头注意力(上)与多查询注意力(下)(两个注意力头)

然而,剥离所有头对于较大的模型来说相对更激进。例如,从 64 个头减少到 1 个头,与从 32 个头减少到 1 个头相比,模型的表示能力的削减更大。GQA 通过提供一种折衷方案来解决这个问题:我们不是让所有查询头共享相同的唯一 KV 头,而是将它们分成 g 个查询头的组,同一个组的查询头共享相同的唯一 KV 头。换句话说,我们不是从 $n_{heads}$ 缩减到 1 个 KV 头,而是将 KV 头的数量从 $n_{heads}$ 缩减到 $1<g<n_{heads}$。

从这个角度来看,MHA 和 MQA 都是 GQA 的特例(分别为 $g=1$ 和 $g=n_{heads}$)。QGA 允许在两个极端情况(MHA 和 MQA)之间更平滑地导航模型精度/KV 缓存大小(与延迟和吞吐量都相关)的折衷方案。

考虑到这个新的参数 g,KV 缓存大小公式变为:

$$
KV_cache_size = b \cdot t \cdot 2 \cdot g \cdot d_{head} \cdot p_a
$$

在实践中,MQA/GQA 架构已被 Google Research 的 PaLM [13],TII 的 Falcon [14] 模型,Meta 的 Llama-2 [1](仅 70B)和 Mistral AI 的 Mistral-7B [7](表 5)显著地实现。

模型参数层数头数隐藏维度注意力类型
PaLM540B526416384MQA
Falcon40B40645120MQA
Llama-2–70B70B60644096GQA
Mistral-7B7B24324096GQA

表 5 – 使用 MQA 或 GQA 的模型系列

注意力头的隐藏维度?

再次强调,如果你不准备选择另一个模型,这里没有太多可以获得的。根据模型系列的不同,头隐藏维度在不同模型大小之间可能是恒定的(例如 Llama-2,Falcon),因此选择同一个系列中较小的变体不会有帮助。

使用更少的字节数/参数?

量化 KV 缓存确实是一种可以大幅减少其大小的好方法。但是,像 AWQ [15] 或 GPTQ [16] 这样的仅权重量化算法从定义上来说不会有帮助。只有量化权重和“激活”(即任何不是权重的东西)的算法,例如 LLM.int8()[17] 或 SmoothQuant [18],才会产生量化的 KV 缓存。

请注意,在权重和激活上都起作用的量化算法的意图之一是在较低的精度下执行计算密集型矩阵乘法。如果像训练期间那样是计算受限的,这会带来性能提升,但正如我们将在后面的文章中看到,推理的自回归阶段实际上是内存带宽受限的,因此能够更快地计算不会带来太多价值。

我们实际上只对内存占用的减少感兴趣,因为它意味着更少的数据传输。从这个角度来看,像 LLM.int8() 或 SmoothQuant 这样的量化算法有点过分:在将缓存的张量移动到 GPU 内存之前对其进行量化,并在从 GPU 内存中获取相同的张量后对其进行反量化(以额外的开销为代价)应该就足够了。

一些 LLM 推理系统已经包含了这样的 KV 缓存量化功能。例如,FlexGen [19] 将 KV 缓存和模型权重都量化并存储在 4 位数据格式中。NVIDIA TensorRT-LLM 能够将 KV 缓存量化到 8 位数据格式(INT8 或 FP8)中。流行的 vLLM 框架从 0.3.0 版本开始也支持 KV 缓存(FP8)量化。由于量化是在每次迭代时动态执行的,因此不需要校准步骤。

高效内存管理的重要性

到目前为止,我们隐含地假设内存没有浪费:所有保留的内存都用于存储标记,所有可用的内存都可以被保留。在实践中,简单的内存管理策略会导致大量内存被浪费(PagedAttention 论文 [20] 表明,实际的有效内存利用率可能低至 20%,即 80% 的浪费!):

  • 由于请求的总序列长度事先未知,我们可以保留能够容纳最大序列长度的连续内存块。这种分配中很大一部分肯定永远不会被使用,并且由于无法用于其他请求,因此被浪费了(内部内存碎片)。
  • 即使序列长度事先已知,由于内存是逐渐消耗的,但内存块是为请求的生命周期保留的,因此较短的请求无法使用仍然未使用的内存块。
  • 如果我们使用像束搜索这样的解码策略,为每个请求生成多个序列,那么多个候选序列实际上可以部分共享它们的 KV 缓存。如果我们不考虑这种情况,我们不可避免地会通过存储本可以共享的重复 KV 条目来浪费内存。

这些缺点正是现在流行的 PagedAttention 算法旨在解决的问题。PagedAttention 分配固定大小且相对较小的内存块,称为块。每个块可以包含固定数量的标记,如果需要,可以跨不同的请求共享。按需分配和较小的块大小减轻了内部内存碎片,而相同大小的块消除了外部内存碎片。

总的来说,PagedAttention 实现了 KV 缓存内存的近乎零浪费(低于 4% [21])。以前被浪费的内存现在可以用来容纳更多请求,从而提高吞吐量。PagedAttention 推出时,吞吐量提高的数字与当时内存浪费的程度一样惊人。

PagedAttention 最初由 vLLM 推理系统实现,但现在所有主要的推理框架都支持它(例如 HuggingFace TGI、NVIDIA TensorRT-LLM、LMDeploy TurboMind 等)。

PagedAttention 未涵盖的另一个可能的优化是在请求之间重用键值缓存。这将适用于提示共享公共前缀的情况,这种情况在像聊天和代理这样的多轮用例中很常见,或者在使用提示模板时(图 4)。

图 4 – SGLang 论文中 KV 缓存共享示例(多轮聊天),总共四个生成请求。蓝色框表示可共享的提示部分。

能够在请求之间重用 KV 缓存将能够显著降低延迟(尤其是第一个标记的延迟)和吞吐量(通过大大减少具有共享前缀的并发请求的内存占用)。

LMSYS SGLang 论文 [22] 中介绍的 RadixAttention 技术实现了这种 KV 缓存重用。

RadixAttention 算法不是在完成生成请求后丢弃 KV 缓存,而是将其保留在 GPU 内存中,并向一个专门的数据结构(基数树)添加一个新条目,该结构将标记序列映射到它们的 KV 缓存张量。当一个新的请求进来时,调度程序使用基数树进行前缀匹配。如果有缓存命中,调度程序将重用缓存的 KV 张量来满足请求。

由于 GPU 内存有限,缓存的 KV 张量不能永远保留。因此,RadixAttention 算法包含一个驱逐策略(例如,最近最少使用 (LRU) 驱逐策略)。最佳缓存重用可能与先到先得的调度程序不兼容。因此,RadixAttention 带有一个修改后的调度程序,该调度程序优先考虑与缓存的前缀匹配的请求(缓存感知调度)。

注意: PagedAttention 和 RadixAttention 的命名有点误导,因为与人们可能认为的相反,它们不是模型注意力层的优化(如 FlashAttention),而是在模型服务器级别运行(它们帮助服务应用程序更好地管理主机上的 KV 缓存)。

如果 GPU 内存不足,为什么不“只”使用多个 GPU?或者卸载到 CPU 内存甚至磁盘?

这些是两种不同的但有效的方案。

首先关于卸载到更丰富但更慢的存储器(CPU 内存和磁盘)。并非所有推理框架都支持此功能,让我们列举 HuggingFace Accelerate、DeepSpeed-Inference 和更先进的 FlexGen。由于它涉及使用更慢的存储器,因此卸载会以较大的延迟损失为代价,因此对于对延迟敏感的用例来说,显然不应优先考虑此选项。卸载系统通常用于面向吞吐量的用例,例如离线批处理。

关于使用多个 GPU(对于较大的模型来说不可避免),将模型在多个设备上分片可以释放内存压力,因为可以从聚合的内存容量和内存带宽中获益。

如果选择管道并行 [23],模型和 KV 缓存都将在层维度上分片。如果选择张量并行 [24](更常见于推理),KV 缓存将在头维度上分片。请注意,MQA 在这种设置下效率很低:由于我们无法跨多个设备分片单个头,因此 KV 缓存必须在所有设备上复制,从而失去了 MQA 的优势。对于实现 MQA 的模型,另一种选择是跨批次大小维度分片 KV 缓存 [25]。

在任何情况下,以上所有情况都假设单个主机,我们仍然受限于我们可以得到的最大多 GPU 实例的存储容量。据我所知,目前还没有推理框架支持多主机模型并行。如果我们能够将模型和 KV 缓存都分片到多个主机上,那么我们可以处理的可用内存量和最大序列长度将实际上变得无限。这是 Infinite-LLM 论文 [26] 旨在通过引入一种新的分布式注意力算法 (DistAttention) 和调整 Ray 框架来构建多主机分布式 KV 缓存管理和调度系统 (DistKV-LLM) 来解决的问题。

总结

在这篇文章中,我们学习了选择 KV 缓存会带来额外的挑战。多头注意力 (MHA) 模型的 KV 缓存确实会消耗大量 GPU 内存,大约为 1MB/标记,并且很容易超过模型权重的内存占用。

鉴于 GPU 内存的有限性,KV 缓存内存压力导致了许多不同方向的举措:新的注意力架构(MQA、GQA、SWA)、缓存压缩策略(H2O、剪刀手、FastGen)、高效内存管理(PagedAttention、RadixAttention)、量化和存储容量扩展(卸载系统、单主机和多主机模型并行)。

正如我们将在后面的文章中看到,减少 KV 缓存的大小至关重要,不仅因为 GPU 内存有限,而且因为数据移动量实际上是每个自回归步骤的延迟的主要贡献者,因此也是整个生成过程的延迟的主要贡献者。

在下一篇文章中,我们将看看可能影响模型延迟和吞吐量的不同类型的瓶颈。我们下次再见!

参考文献:

[1]: Llama 2: Open Foundation and Fine-Tuned Chat Models (Touvron 等人,2023)
[2]: OPT: Open Pre-trained Transformer Language Models (Zhang 等人,2022)
[3]: MPT-7B(2023 年 5 月)和 MPT-30B(2023 年 6 月)的发布博客文章
[4]: BLOOM: A 176B-Parameter Open-Access Multilingual Language Model (BigScience,2023)
[5]: Scaling Laws for Neural Language Models (Kaplan 等人,2020)
[6]: Mistral 7B (Jiang 等人,2023)
[7]: Efficient Streaming Language Models with Attention Sinks (Xiao 等人,2023) + GitHub 仓库
[8]: H_2O: Heavy-Hitter Oracle for Efficient Generative Inference of Large Language Models (Zhang 等人,2023) + GitHub 仓库
[9]: Scissorhands: Exploiting the Persistence of Importance Hypothesis for LLM KV Cache Compression at Test Time (Liu 等人,2023)
[10]: Model Tells You What to Discard: Adaptive KV Cache Compression for LLMs (Ge 等人,2023)
[11]: Fast Transformer Decoding: One Write-Head is All You Need (Shazeer,2019)
[12]: GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints (Ainslie 等人,2023)
[13]: PaLM: Scaling Language Modeling with Pathways (Chowdhery 等人,2022)
[14]: The Falcon Series of Open Language Models (Almazrouei 等人,2023)
[15]: AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration (Lin 等人,2023) + GitHub 仓库
[16]: GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers (Frantar 等人,2022) + GitHub 仓库
[17]: LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale (Dettmers 等人,2022) + GitHub 仓库
[18]: SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models (Xiao 等人,2022) + GitHub 仓库
[19]: FlexGen: High-Throughput Generative Inference of Large Language Models with a Single GPU (Sheng 等人,2023) + GitHub 仓库
[20]: Efficient Memory Management for Large Language Model Serving with PagedAttention (Kwon 等人,2023) + GitHub 仓库
[21]: vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention (Kwon 等人,2023)
[22]: Efficiently Programming Large Language Models using SGLang (Zheng 等人,2023) + 博客文章
[23]: GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism (Huang 等人,2018)
[24]: Efficient Large-Scale Language Model Training on GPU Clusters Using Megatron-LM (Narayanan 等人,2021)
[25]: Efficiently Scaling Transformer Inference (Pope 等人,2022)
[26]: Infinite-LLM: Efficient LLM Service for Long Context with DistAttention and Distributed KVCache (Lin 等人,2024)

发表评论