← 返回首页
READING NOTE / 011完整原文 · 分块对照

KV Cache Engineering for LLM Serving, clearly explained

LLM 服务中的 KV Cache 工程:清晰讲解

5,983 个英文词 · 25 个原文块 · 8 张保存图片

总结

文章解释了 KV cache 为何会增长:模型权重在推理期间大致固定,而缓存会随序列中的每个 token 增长,且默认每个活跃序列都需要独立缓存。以 Llama 3.1 70B 为例,单个 128K-token 序列约需 40 GB 的 BF16 KV cache;四个满长度序列还会增加约 160 GB,尚未计入模型权重和服务开销。缓存既占用 GPU 内存,注意力层生成新 token 时也必须反复读取它。

文章按缓存公式将 12 种技术归类为减少 KV 头数、层数、保留 token 数、表示宽度、每个值的比特数,或减少重复分配与读取。架构方法包括 GQA/MQA、跨层注意力、滑动窗口、MLA、混合循环层和压缩稀疏注意力;服务引擎方法包括查询感知稀疏读取、量化、驱逐、分页、前缀复用和 GPU 外卸。作者特别区分了“减少缓存容量”和“减少注意力读取工作”:稀疏注意力或 Quest 可能减少读取与延迟,但不一定减少常驻缓存。

文中给出多项数字和示例:GQA/MQA 通过共享 KV 头减少存储,Llama 3.1 70B 在 64 个查询头、8 个 KV 头时,128K 上下文约为 40 GB BF16 cache,而相同形状使用 64 个 KV 头时为 320 GB;滑动窗口只保留最近位置;量化可将 BF16 的 16 bit payload 降至 FP8 的 8 bit 或 4-bit 格式,但会引入误差和额外 scale 元数据。CLA、MLA、混合模型及压缩稀疏注意力必须在模型训练或架构中体现,不能简单作为标准 checkpoint 的通用运行时开关。

作者建议先明确瓶颈再选择技术:选模型时检查 KV 头数、局部/全局层比例、latent-attention 维度和循环层;服务既有模型时可先测试 FP8 并规范化前缀,再通过工作负载相关质量测试评估驱逐;若问题是延迟,可测量缓存读取和注意力时间;若非活跃会话占用 GPU,则测试外卸和调度。作者也强调组合带来的容量估算会掩盖模型质量、量化误差、上下文丢弃、内核支持和训练要求等限制。

  • KV Cache
  • LLM 推理服务
  • 注意力优化
  • GPU 内存管理
  • 量化与缓存复用

英文原文 01

204 词 · 2 张图
Image

Everything you need to understand why the KV cache grows, the 12 ways models and serving engines reduce it, what each technique actually saves, and the trade-offs that decide which one fits your workload.

Loading an LLM is only the first part of GPU memory planning.

Model weights stay roughly fixed during inference. The KV cache does not. It grows with every token in the sequence, and by default, every active sequence needs its own cache.

For Llama 3.1 70B, one 128K-token sequence requires about 40 GB of BF16 KV cache. Four full-length sequences would add another 160 GB, before accounting for model weights and serving overhead.

Image

That creates two separate costs. The cache occupies GPU memory, and the attention layers must repeatedly read it while generating new tokens.

The techniques described as KV cache optimizations address different parts of this problem. Some change what the model stores. Some retain fewer tokens or use fewer bits. Others leave the logical cache intact and improve how the serving engine reads, allocates, shares, or moves it.

This article organizes twelve techniques by what they reduce, when they can be applied, and what trade-off remains. Each section includes a small runnable example that explains the main idea.

中文译文 01

375 字
Image

你需要了解的一切:KV 缓存为何会增长,模型和服务引擎如何通过 12 种方式减少它,每种技术实际节省什么,以及决定哪种技术适合你的工作负载的权衡。

加载 LLM 只是 GPU 内存规划的第一部分。

推理期间,模型权重大致保持不变。KV 缓存则不同。它会随着序列中的每个 token 增长,并且默认情况下,每个活跃序列都需要自己的缓存。

对于 Llama 3.1 70B,一个 128K token 的序列大约需要 40 GB 的 BF16 KV 缓存。四个完整长度的序列还会增加 160 GB,此计算尚未包括模型权重和服务开销。

Image

这会产生两种不同的成本。缓存会占用 GPU 内存,而注意力层在生成新 token 时必须反复读取它。

被称为 KV 缓存优化的技术,解决的是这个问题的不同部分。有些技术改变模型存储的内容。有些技术保留更少的 token,或使用更少的比特。还有一些技术保留完整的逻辑缓存,但改进服务引擎读取、分配、共享或搬运缓存的方式。

本文按照每种技术减少的对象、可以应用的时机以及仍然存在的权衡,组织介绍十二种技术。每个部分都包含一个解释核心概念的小型可运行示例。

英文原文 02

251 词 · 2 张图

The cache formula

Each attention layer stores one key and one value for every retained token.

The raw cache size for one sequence is:

Image

The leading 2 counts keys and values. BF16 uses two bytes per value, while FP8 uses one.

That formula also gives us a clean way to group the techniques:

  • GQA and MQA reduce the number of KV heads.
  • Cross-layer attention reduces the number of unique cached layers.
  • Sliding windows and eviction reduce the number of retained tokens.
  • MLA reduces the width of the stored representation.
  • Quantization reduces the bytes used by each value.
  • Hybrid recurrent layers replace a growing cache with fixed-size state.
  • Paging and prefix reuse reduce allocation waste and duplicate blocks.

Sparse attention needs a separate line. It may read fewer tokens without removing them from memory, so it can cut attention work while leaving cache capacity unchanged.

This distinction matters when the GPU is full. A faster attention kernel doesn't create room for another sequence unless it also stores fewer bytes.

Technique map

The visual below puts all 12 techniques in one place, showing what each one changes and whether its main saving comes from fewer heads, layers, tokens, dimensions, bits, or duplicated memory.

The table above represents what each technique tries to tackle.

The quadrant below takes six representative techniques and reorganizes them by whether they reduce cache memory, attention work, or both. The full set remains in the table above.

Image

The rest of the article works through the full table in order.

中文译文 02

469 字

缓存公式

每个注意力层都会为每个保留的 token 存储一个 key 和一个 value。

单个序列的原始缓存大小为:

Image

开头的 2 表示 keys 和 values 的数量。BF16 每个值使用两个字节,而 FP8 使用一个字节。

这个公式还为我们提供了一种清晰的方式来对这些技术进行分组:

  • GQA 和 MQA 减少 KV 头的数量。
  • 跨层注意力减少唯一缓存层的数量。
  • 滑动窗口和驱逐减少保留 token 的数量。
  • MLA 减少所存储表示的宽度。
  • 量化减少每个值所使用的字节数。
  • 混合循环层用固定大小的状态替代不断增长的缓存。
  • 分页和前缀复用减少分配浪费和重复块。

稀疏注意力需要单独说明。它可能在不将 token 从内存中移除的情况下读取更少的 token,因此可以减少注意力计算量,同时保持缓存容量不变。

当 GPU 已经满载时,这一区别很重要。更快的注意力内核不会为另一个序列腾出空间,除非它同时存储更少的字节。

技术图谱

下方的视觉图表将全部 12 种技术放在同一处,展示每种技术改变了什么,以及其主要节省来自更少的头、层、token、维度、比特,还是更少的重复内存。

上表代表了每种技术试图解决的问题。

下面的象限图选取了六种具有代表性的技术,并按照它们是减少缓存内存、减少注意力计算,还是两者兼顾,重新进行了整理。完整集合仍保留在上面的表格中。

Image

文章的其余部分将按照完整表格中的顺序逐项展开。

英文原文 03

216 词 · 3 张图

1. GQA and MQA cache fewer heads

The cache formula above contains a term for KV heads. Reducing that term is one of the largest architectural savings available.

Inside each transformer layer, every token produces three representations:

Image
  • The query represents what the current token needs.
  • The key describes what each earlier token can match.
  • The value carries the information returned when that match receives attention.

During generation, the current token's query is compared with cached keys.

Those comparisons produce attention weights, which combine the cached values. The layer keeps keys and values for later decoding steps. It does not keep the queries. I covered this below if you want to learn more:

Avi Chawla@_avichawla·Aug 30Why KV cache stores K and V vectors but never Q? (a popular technical LLM interview question) LLMs are autoregressive so each token is predicted from every token before it, one at a time. This autoregressive nature has a direct consequence inside the model. A forward passGIFQuoteAvi Chawla@_avichawla·Aug 28 ArticleKV, Prefix, Prompt and Semantic Caching in LLMs, clearly explainedEverything you need to understand where your input tokens are being recomputed and what to do about it. It covers the four cache layers from first principles, their trade-offs, what happens when they...932K

中文译文 03

383 字

1. GQA 和 MQA 缓存更少的头

上面的缓存公式中包含一个 KV 头数量的项。减少这一项,是目前可获得的架构层面最大节省之一。

在每个 Transformer 层内,每个 token 都会产生三种表示:

Image
  • 查询表示当前 token 需要什么。
  • 键描述每个更早 token 能够匹配什么。
  • 值携带当该匹配获得注意力时所返回的信息。

在生成过程中,当前 token 的查询会与缓存的键进行比较。

这些比较会产生注意力权重,随后这些权重会组合缓存的值。该层会保留键和值,以供之后的解码步骤使用,但不会保留查询。如果你想进一步了解,我在下面介绍过这一点:

Avi Chawla@_avichawla·8月30日为什么 KV 缓存存储 K 和 V 向量,却从不存储 Q? (一个很常见的技术类 LLM 面试问题) LLM 具有自回归特性,因此每个 token 都会根据它之前的所有 token 逐个进行预测。 这种自回归特性会直接影响模型内部的计算。 一次前向传递GIF引用Avi Chawla@_avichawla·8月28日文章LLM 中的 KV、前缀、提示词和语义缓存,清晰解读理解输入 token 在哪里被重复计算,以及如何处理这些问题所需的一切内容。文章从第一性原理出发,介绍四个缓存层及其权衡、当它们……932K

英文原文 04

208 词

Attention splits these representations into smaller parts called heads.

A query head is one slice of the token's query representation. It is not the user's prompt or a separate model request. Different heads can learn to focus on different relationships in the same sequence.

  • Standard multi-head attention, or MHA, gives each query head a matching key head and value head. A layer with eight query heads therefore produces eight key heads and eight value heads. Every attention layer performs this projection independently and keeps its own KV cache.
  • Multi-query attention, or MQA, keeps all eight query heads but produces only one key head and one value head. Every query head reads from that shared KV pair. The sharing does not cross transformer layers.
  • Grouped-query attention, or GQA, sits between these two layouts. It divides the query heads into groups and assigns one KV pair to each group. With eight query heads and two KV heads, four query heads share each KV pair.

All three methods operate inside each attention layer. They differ only in how many KV heads that layer stores. Cross-layer attention, covered next, is a separate idea that shares cached information between layers.

The GQA paper described it as an intermediate point between MHA and MQA.

中文译文 04

350 字

注意力机制将这些表示拆分成称为“头”的更小部分。

查询头是该词元查询表示的一个切片。它不是用户的提示词,也不是单独的模型请求。不同的头可以学会关注同一序列中的不同关系。

  • 标准多头注意力(MHA)为每个查询头配备一个匹配的键头和值头。因此,一个具有八个查询头的层会产生八个键头和八个值头。每个注意力层都会独立执行这一投影,并保留自己的 KV 缓存。
  • 多查询注意力(MQA)保留全部八个查询头,但只产生一个键头和一个值头。每个查询头都从这一对共享的 KV 中读取。共享不会跨越 Transformer 层。
  • 分组查询注意力(GQA)位于这两种布局之间。它将查询头划分为多个组,并为每个组分配一对 KV。对于八个查询头和两个 KV 头的情况,每四个查询头共享一对 KV。

这三种方法都在每个注意力层内部运行。它们的区别仅在于该层存储多少个 KV 头。下一节将介绍跨层注意力,这是在层之间共享缓存信息的另一种概念。

GQA 论文将其描述为 MHA 与 MQA 之间的中间点。

英文原文 05

263 词

The authors converted existing MHA checkpoints and used 5 percent of the original pretraining compute for recovery training. Their uptrained GQA models stayed close to MHA quality while approaching MQA speed.

The following example uses eight query heads. It shows which KV head each query head reads. It also counts the cached scalars for six tokens:

python
query_heads = 8
tokens = 6
head_size = 8

for name, kv_heads in [("MHA", 8), ("GQA", 2), ("MQA", 1)]:
    queries_per_kv_head = query_heads // kv_heads
    mapping = [head // queries_per_kv_head for head in range(query_heads)]
    cached_scalars = 2 * kv_heads * tokens * head_size

    print(name, "mapping:", mapping)
    print(name, "cached scalars:", cached_scalars)

# Output:
"""
MHA mapping: [0, 1, 2, 3, 4, 5, 6, 7]
MHA cached scalars: 768
GQA mapping: [0, 0, 0, 0, 1, 1, 1, 1]
GQA cached scalars: 192
MQA mapping: [0, 0, 0, 0, 0, 0, 0, 0]
MQA cached scalars: 96
"""

The mapping lists one KV head number for every query head. MHA uses a different KV head each time. GQA shares KV head 0 across four queries, then does the same with KV head 1. MQA maps every query head to KV head 0.

The cache count includes both keys and values. It then multiplies by six tokens and eight scalars per head.

Llama 3.1 70B uses 64 query heads and 8 KV heads. At 128K context, that produces a 40 GB BF16 cache instead of the 320 GB required by the same shape with 64 KV heads.

This code proves the storage ratio. It doesn't test quality, because head sharing changes what the model learns and must be evaluated after training.

中文译文 05

384 字

作者将现有的 MHA 检查点转换过来,并使用原始预训练计算量的 5% 进行恢复训练。他们经过额外训练的 GQA 模型在质量上保持接近 MHA,同时速度接近 MQA。

下面的示例使用八个查询头。它展示每个查询头读取哪个 KV 头,也统计六个词元所对应的缓存标量数量:

python
query_heads = 8
tokens = 6
head_size = 8

for name, kv_heads in [("MHA", 8), ("GQA", 2), ("MQA", 1)]:
    queries_per_kv_head = query_heads // kv_heads
    mapping = [head // queries_per_kv_head for head in range(query_heads)]
    cached_scalars = 2 * kv_heads * tokens * head_size

    print(name, "mapping:", mapping)
    print(name, "cached scalars:", cached_scalars)

# Output:
"""
MHA mapping: [0, 1, 2, 3, 4, 5, 6, 7]
MHA cached scalars: 768
GQA mapping: [0, 0, 0, 0, 1, 1, 1, 1]
GQA cached scalars: 192
MQA mapping: [0, 0, 0, 0, 0, 0, 0, 0]
MQA cached scalars: 96
"""

映射为每个查询头列出一个 KV 头编号。MHA 每次都使用不同的 KV 头。GQA 让四个查询头共享 KV 头 0,然后让另外四个查询头共享 KV 头 1。MQA 则将每个查询头都映射到 KV 头 0。

缓存计数同时包括键和值。然后再乘以六个词元,以及每个头的八个标量。

Llama 3.1 70B 使用 64 个查询头和 8 个 KV 头。在 128K 上下文下,这会产生一个 40 GB 的 BF16 缓存;如果采用相同形状但使用 64 个 KV 头,则需要 320 GB。

这段代码证明了存储比例。它不会测试质量,因为头共享会改变模型学到的内容,必须在训练后进行评估。

英文原文 06

256 词

2. Cross-layer attention

GQA shares keys and values across heads within one layer. Cross-layer attention shares them across adjacent layers.

So a sharing factor of two implies two attention layers use one set of cached keys and values. The model now stores half as many unique layer caches.

The Cross-Layer Attention (CLA) paper trained 1B and 3B models and reported another 2x KV cache reduction while keeping accuracy close to the MQA baseline.

The next script represents two layers sharing one cache allocation:

python
tokens = 32
kv_heads = 2
head_size = 8

one_layer_cache = 2 * tokens * kv_heads * head_size
shared_cache = {"stored_scalars": one_layer_cache}
layer_caches = [shared_cache, shared_cache]

separate_total = 2 * one_layer_cache
shared_total = shared_cache["stored_scalars"]

print("layers point to same cache:", layer_caches[0] is layer_caches[1])
print("two separate layer caches:", separate_total, "scalars")
print("one shared layer cache:", shared_total, "scalars")

# Output:
"""
layers point to same cache: True
two separate layer caches: 2048 scalars
one shared layer cache: 1024 scalars
"""

One layer would store 1,024 scalars here. The leading 2 counts keys and values. Two independent layers would therefore store 2,048 scalars.

Both items in layer_caches point to the same Python object. The physical storage remains 1,024 scalars even though two logical layers use it.

Cross-layer sharing compounds GQA because it reduces a different term. GQA stores fewer KV heads inside each layer. CLA stores fewer distinct layer caches across the model.

A standard checkpoint learns separate KV projections for every layer. Each layer expects the cache created by its own weights. Redirecting it to another layer's cache changes the model's computation.

中文译文 06

408 字

2. 跨层注意力

GQA 在一个层内的不同头之间共享键和值。跨层注意力则在相邻层之间共享它们。

因此,大小为 2 的共享因子意味着两个注意力层使用同一组缓存的键和值。模型现在存储的独特层缓存数量减少了一半。

跨层注意力(CLA)论文训练了 1B 和 3B 模型,并报告称,在保持准确率接近 MQA 基线的同时,又将 KV 缓存减少了 2 倍。

下一个脚本表示两个层共享一次缓存分配:

python
tokens = 32
kv_heads = 2
head_size = 8

one_layer_cache = 2 * tokens * kv_heads * head_size
shared_cache = {"stored_scalars": one_layer_cache}
layer_caches = [shared_cache, shared_cache]

separate_total = 2 * one_layer_cache
shared_total = shared_cache["stored_scalars"]

print("layers point to same cache:", layer_caches[0] is layer_caches[1])
print("two separate layer caches:", separate_total, "scalars")
print("one shared layer cache:", shared_total, "scalars")

# Output:
"""
layers point to same cache: True
two separate layer caches: 2048 scalars
one shared layer cache: 1024 scalars
"""

在这里,一个层会存储 1,024 个标量。开头的 2 计算了键和值。因此,两个相互独立的层会存储 2,048 个标量。

layer_caches 中的两个条目都指向同一个 Python 对象。尽管有两个逻辑层在使用它,物理存储仍然只有 1,024 个标量。

跨层共享与 GQA 叠加,因为它减少的是不同的项。GQA 减少每一层内部存储的 KV 头数量。CLA 减少整个模型中不同层缓存的数量。

标准检查点会为每一层学习独立的 KV 投影。每一层都期待使用由自身权重创建的缓存。将它重定向到另一层的缓存会改变模型的计算。

英文原文 07

223 词

A CLA model trains with the sharing pattern already present. Training lets each dependent layer adapt to the shared cache. The serving engine must then reproduce the same ownership pattern.

The checkpoint and engine must agree on which layer owns each cache. This is why CLA cannot be enabled as a generic runtime flag.

3. Sliding windows

CLA reduces the number of stored layer caches. Sliding-window attention reduces the tokens stored inside selected layers.

In full self-attention, each new token can attend to every earlier token. Each attention layer must therefore keep keys and values for the full sequence. As the sequence grows, every layer's KV cache grows with it.

Sliding-window attention limits a layer to recent tokens. Suppose the window contains 1,024 positions. That layer stores KV entries for only the latest 1,024 tokens. Each new entry removes the oldest one after the window fills. Its cache then stays fixed at 1,024 positions.

We call this a local layer because it sees nearby context. A global layer can still attend to every earlier token. Its cache keeps growing until the sequence ends.

Many models mix both layer types. Local layers provide most of the memory saving. Occasional global layers preserve access to distant parts of the prompt. The total cache still grows, but much slower than full attention everywhere.

中文译文 07

407 字

CLA 模型在共享模式已经存在的情况下进行训练。训练使每个依赖层都能适应共享缓存。随后,服务引擎必须复现同样的所有权模式。

检查点和引擎必须就每个缓存由哪一层拥有达成一致。这就是 CLA 不能作为通用运行时开关启用的原因。

3. 滑动窗口

CLA 减少存储的层缓存数量。滑动窗口注意力减少选定层内部存储的令牌数量。

在完整自注意力中,每个新令牌都可以关注之前的每个令牌。因此,每个注意力层都必须保存完整序列的键和值。随着序列增长,每一层的 KV 缓存也随之增长。

滑动窗口注意力将一层限制在最近的令牌范围内。假设窗口包含 1,024 个位置。该层只为最近的 1,024 个令牌存储 KV 条目。窗口填满后,每个新条目都会移除最旧的条目。这样,其缓存就会固定在 1,024 个位置。

我们将这种层称为局部层,因为它只能看到附近的上下文。全局层仍然可以关注之前的每个令牌。它的缓存会持续增长,直到序列结束。

许多模型会混合使用这两种层。局部层提供大部分内存节省。偶尔出现的全局层保留对提示中较远部分的访问能力。缓存总量仍会增长,但比处处使用完整注意力时慢得多。

英文原文 08

268 词

For instance, Gemma 3 uses a repeating pattern of five local layers followed by one global layer. Its local window is 1,024 tokens, while the global layer supports the full 128K context:

The following example tracks the positions retained by a four-token window:

python
window_size = 4
retained_positions = []

for position in range(10):
    retained_positions.append(position)

    if len(retained_positions) > window_size:
        retained_positions.pop(0)

    print(f"after token {position}: {retained_positions}")

print("maximum retained positions:", window_size)

# Output:
"""
after token 0: [0]
after token 1: [0, 1]
after token 2: [0, 1, 2]
after token 3: [0, 1, 2, 3]
after token 4: [1, 2, 3, 4]
after token 5: [2, 3, 4, 5]
after token 6: [3, 4, 5, 6]
after token 7: [4, 5, 6, 7]
after token 8: [5, 6, 7, 8]
after token 9: [6, 7, 8, 9]
maximum retained positions: 4
"""

The list fills with positions 0 through 3. When position 4 arrives, the list becomes too long. Removing its first item drops position 0.

The same step repeats for every later token. Production engines usually use a ring buffer, which overwrites old slots without moving the remaining entries.

The first four tokens fill the available slots. Token 4 then removes token 0, so the window shifts to positions 1 through 4. Every later token shifts that range forward by one. After token 9, only positions 6 through 9 remain. The allocation stays fixed at four slots throughout.

The ratio also changes with context length. Below 1,024 tokens, both layouts retain the same number of positions. The saving appears only after the sequence exceeds the local window.

中文译文 08

393 字

例如,Gemma 3 使用五个局部层后接一个全局层的重复模式。其局部窗口为 1,024 个令牌,而全局层支持完整的 128K 上下文:

下面的示例跟踪一个保留四个令牌位置的窗口:

python
window_size = 4
retained_positions = []

for position in range(10):
    retained_positions.append(position)

    if len(retained_positions) > window_size:
        retained_positions.pop(0)

    print(f"after token {position}: {retained_positions}")

print("maximum retained positions:", window_size)

# Output:
"""
after token 0: [0]
after token 1: [0, 1]
after token 2: [0, 1, 2]
after token 3: [0, 1, 2, 3]
after token 4: [1, 2, 3, 4]
after token 5: [2, 3, 4, 5]
after token 6: [3, 4, 5, 6]
after token 7: [4, 5, 6, 7]
after token 8: [5, 6, 7, 8]
after token 9: [6, 7, 8, 9]
maximum retained positions: 4
"""

列表先填入位置 0 到 3。当位置 4 到来时,列表长度超出限制。移除它的第一个元素后,位置 0 被丢弃。

对于之后的每个令牌,都会重复相同步骤。生产环境的引擎通常使用环形缓冲区,在不移动其余条目的情况下覆盖旧槽位。

前四个令牌填满可用槽位。随后,令牌 4 移除令牌 0,因此窗口移动到位置 1 到 4。之后每个令牌都会使这个范围向前移动一位。在令牌 9 之后,只剩下位置 6 到 9。整个过程中,分配的空间始终固定为四个槽位。

该比例还会随上下文长度变化。在少于 1,024 个令牌时,两种布局保留的位置数量相同。只有当序列超过局部窗口后,节省才会出现。

英文原文 09

307 词

4. Multi-head latent attention

Multi-head latent attention, or MLA, doesn't cache a complete key and value for every head. It compresses the hidden state into a smaller latent representation and stores that instead.

During decode, the model uses learned projections to recover the information needed by attention.

Implementations can absorb parts of those projections into the query and output paths, which avoids reconstructing every full KV tensor in memory.

DeepSeek-V2 introduced this design. The paper reported a 93.3 percent KV cache reduction compared with DeepSeek 67B and a 5.76x increase in maximum generation throughput:

An actual MLA layer uses learned matrices to create and use the latent representation. Those matrix operations hide the basic memory difference in a small example.

The following example counts storage for one token. The dimensions are intentionally small. They make the difference between both layouts visible.

python
kv_heads = 4
head_size = 8

standard_keys = kv_heads * head_size
standard_values = kv_heads * head_size
standard_total = standard_keys + standard_values

mla_latent = 8
mla_rotary_key = 4
mla_total = mla_latent + mla_rotary_key

print("standard cache:", standard_keys, "key +", standard_values, "value")
print("MLA cache:", mla_latent, "latent +", mla_rotary_key, "rotary key")
print("stored scalars per token:", standard_total, "->", mla_total)
print(f"example reduction: {standard_total / mla_total:.2f}x")

# Output:
"""
standard cache: 32 key + 32 value
MLA cache: 8 latent + 4 rotary key
stored scalars per token: 64 -> 12
example reduction: 5.33x
"""

Four KV heads with eight scalars each produce 32 key scalars. The values require another 32. Standard attention therefore stores 64 scalars per token.

The illustrative MLA layout stores eight latent scalars and four positional scalars. Its total is 12. Dividing 64 by 12 gives the example ratio.

The 5.33x figure belongs only to this example. Real MLA models choose different latent and positional dimensions.

MLA is not an inference flag for a standard checkpoint. Its projection weights and cache layout belong to the trained model. Converting another model still requires training.

中文译文 09

484 字

4. 多头潜在注意力

多头潜在注意力(MLA)不会为每个头缓存完整的键和值。它会将隐藏状态压缩成较小的潜在表示,并改为存储该表示。

在解码期间,模型使用学习到的投影来恢复注意力所需的信息。

实现可以将这些投影的一部分吸收到查询路径和输出路径中,从而避免在内存中重建每个完整的 KV 张量。

DeepSeek-V2 引入了这种设计。论文报告称,与 DeepSeek 67B 相比,KV 缓存减少了 93.3%,最大生成吞吐量提高了 5.76 倍:

实际的 MLA 层使用经过学习的矩阵来创建和使用潜在表示。这些矩阵运算会掩盖一个小例子中基本的内存差异。

下面的示例计算一个 token 的存储量。维度被刻意设置得很小,以便显现两种布局之间的差异。

Python
kv_heads = 4
head_size = 8

standard_keys = kv_heads * head_size
standard_values = kv_heads * head_size
standard_total = standard_keys + standard_values

mla_latent = 8
mla_rotary_key = 4
mla_total = mla_latent + mla_rotary_key

print("standard cache:", standard_keys, "key +", standard_values, "value")
print("MLA cache:", mla_latent, "latent +", mla_rotary_key, "rotary key")
print("stored scalars per token:", standard_total, "->", mla_total)
print(f"example reduction: {standard_total / mla_total:.2f}x")

# Output:
"""
standard cache: 32 key + 32 value
MLA cache: 8 latent + 4 rotary key
stored scalars per token: 64 -> 12
example reduction: 5.33x
"""

四个 KV 头、每个头包含八个标量,会产生 32 个 key 标量。value 还需要另外 32 个。因此,标准注意力每个 token 会存储 64 个标量。

示例中的 MLA 布局存储八个潜在标量和四个位置标量,总数为 12。用 64 除以 12,便得到示例中的比例。

5.33 倍这个数字只属于本示例。实际 MLA 模型会选择不同的潜在维度和位置维度。

MLA 不是标准 checkpoint 的一个推理标志。它的投影权重和缓存布局属于经过训练的模型。要转换另一个模型,仍然需要训练。

英文原文 10

276 词

5. Replace growing cache with fixed state using Hybrid models

Mamba and recurrent linear-attention layers don't store one key and value for every old token. They update a fixed-size state as the sequence grows.

Pure recurrent models keep memory nearly constant with context, but full attention remains better at exact retrieval in many settings. Recent models combine both.

Qwen3-Next has 48 layers arranged as 12 repeated blocks. Each block contains three Gated DeltaNet layers and one full-attention layer. Only the 12 full-attention layers create a standard growing KV cache.

The model card reports 16 query heads, 2 KV heads, and a head dimension of 256 for those attention layers.

The next example compares their memory growth. The attention layer stores one 64-value key and value per token. The recurrent layer keeps one fixed 64-by-64 state.

python
width = 64
bytes_per_value = 4

attention_values_per_token = 2 * width
recurrent_state_values = width * width

for tokens in [128, 4096]:
    attention_kb = tokens * attention_values_per_token * bytes_per_value // 1024
    recurrent_kb = recurrent_state_values * bytes_per_value // 1024

    print(f"{tokens} tokens")
    print("  attention KV:", attention_kb, "KB")
    print("  recurrent state:", recurrent_kb, "KB")

# Output:
"""
128 tokens
  attention KV: 64 KB
  recurrent state: 16 KB
4096 tokens
  attention KV: 2048 KB
  recurrent state: 16 KB
"""

The attention calculation multiplies storage per token by sequence length. The recurrent calculation does not use tokens at all. Its state remains the same size in both cases.

The example tracks memory only. It does not implement Mamba or Gated DeltaNet. Those architectures use learned update rules to decide what the fixed state retains.

The attention cache grows 32x between the two sequence lengths. The recurrent state remains 16 KB. Hybrid models pay linear cache growth only in their full-attention layers.

中文译文 10

470 字

5. 使用混合模型,以固定状态替代不断增长的缓存

Mamba 和循环线性注意力层不会为每个旧 token 存储一个 key 和 value。随着序列增长,它们会更新一个固定大小的状态。

纯循环模型的内存占用几乎不随上下文长度变化,但在许多设置中,完整注意力仍然更擅长精确检索。近期模型将两者结合起来。

Qwen3-Next 有 48 层,排列为 12 个重复模块。每个模块包含三个 Gated DeltaNet 层和一个完整注意力层。只有 12 个完整注意力层会创建标准的、不断增长的 KV 缓存。

模型卡报告称,对于这些注意力层,模型使用 16 个 query 头、2 个 KV 头,以及 256 的头维度。

下一个示例比较它们的内存增长。注意力层每个 token 存储一个包含 64 个值的 key 和一个包含 64 个值的 value。循环层则保留一个固定的 64×64 状态。

Python
width = 64
bytes_per_value = 4

attention_values_per_token = 2 * width
recurrent_state_values = width * width

for tokens in [128, 4096]:
    attention_kb = tokens * attention_values_per_token * bytes_per_value // 1024
    recurrent_kb = recurrent_state_values * bytes_per_value // 1024

    print(f"{tokens} tokens")
    print("  attention KV:", attention_kb, "KB")
    print("  recurrent state:", recurrent_kb, "KB")

# Output:
"""
128 tokens
  attention KV: 64 KB
  recurrent state: 16 KB
4096 tokens
  attention KV: 2048 KB
  recurrent state: 16 KB
"""

注意力计算会将每个 token 的存储量乘以序列长度。循环计算完全不使用 token 数量。它的状态在两种情况下都保持相同大小。

该示例只跟踪内存。它没有实现 Mamba 或 Gated DeltaNet。这些架构使用经过学习的更新规则来决定固定状态保留什么。

在这两个序列长度之间,注意力缓存增长了 32 倍。循环状态保持为 16 KB。混合模型只有在其完整注意力层中承担线性的缓存增长。

英文原文 11

238 词

For the Qwen3-Next attention shape, 12 full-attention layers require 3 GB of BF16 KV at 128K context.

Making all 48 layers full attention would raise that growing portion to 12 GB.

Jamba shows the same design choice with a different ratio. It interleaves one attention layer with seven Mamba layers. At 256K context, the Jamba paper reports a 4 GB KV cache, compared with 32 GB for Mixtral.

This layout belongs to the trained model. A serving engine cannot replace arbitrary attention layers with recurrent ones at runtime.

6. Compressed sparse attention

Full attention does two expensive things. It stores a separate KV entry for every earlier token, then reads every stored entry when generating the next token.

Sparse attention changes only the second part.

It reads fewer entries for each new token, but it may still keep the complete cache in memory. That can make generation faster without making the resident KV cache smaller.

Compressed sparse attention focuses on optimizing both parts.

  • First, it combines several neighbouring token entries into one compressed entry. It stores that shorter sequence instead of one long-range entry per token.
  • Then, for each new token, a selector chooses only the compressed entries that appear most relevant. Attention reads that selected subset.

DeepSeek V4 uses this design.

If the compression factor is m, every m token entries become one stored entry. So a context of n tokens produces roughly n / m compressed entries.

中文译文 11

452 字

按照 Qwen3-Next 的注意力形状,在 128K 上下文长度下,12 个完整注意力层需要 3 GB 的 BF16 KV 缓存。

如果让全部 48 层都使用完整注意力,这部分不断增长的存储量将升至 12 GB。

Jamba 展示了相同的设计选择,但比例不同。它每加入一个注意力层,就交错加入七个 Mamba 层。在 256K 上下文长度下,Jamba 论文报告的 KV 缓存为 4 GB,而 Mixtral 为 32 GB。

这种布局属于经过训练的模型。服务引擎不能在运行时任意地将注意力层替换为循环层。

6. 压缩稀疏注意力

完整注意力机制会做两件昂贵的事。它会为每个更早的词元存储一个单独的 KV 条目,然后在生成下一个词元时读取所有已存储的条目。

稀疏注意力只改变第二部分。

它为每个新词元读取更少的条目,但仍可能将完整缓存保存在内存中。这可以让生成更快,却不会让常驻 KV 缓存变小。

压缩稀疏注意力侧重于同时优化这两部分。

  • 首先,它将几个相邻的词元条目合并为一个压缩条目。它存储的是更短的序列,而不是为每个词元存储一个较长范围的条目。
  • 然后,对于每个新词元,选择器只选择看起来最相关的压缩条目。注意力读取这个选定的子集。

DeepSeek V4 使用了这种设计。

如果压缩因子为 m,那么每 m 个词元条目会变成一个存储条目。因此,包含 n 个词元的上下文会产生大约 n / m 个压缩条目。

英文原文 12

256 词

The model then selects k of those entries for attention. A small sliding window preserves finer detail for the most recent tokens.

At one million tokens, DeepSeek reports that V4-Pro uses 10 percent of the KV cache and 27 percent of the single-token inference FLOPs required by DeepSeek V3.2. V4-Flash goes further at 7 percent of the cache and 10 percent of the FLOPs

These are whole-model results. They include V4’s hybrid attention layout and precision choices, so they should not be read as the isolated effect of compression alone.

The next block isolates the two reductions. Compressing 32 long-range entries in groups of four leaves eight stored entries. If attention selects two of those eight, it reads only two compressed entries for the current step.

python
full_token_entries = 32
tokens_per_group = 4
selected_entries = 2

compressed_entries = full_token_entries // tokens_per_group

print("full token entries:", full_token_entries)
print("compressed entries stored:", compressed_entries)
print("compressed entries read:", selected_entries)
print("storage reduction:", f"{full_token_entries / compressed_entries:.0f}x")

# Output:
"""
full token entries: 32
compressed entries stored: 8
compressed entries read: 2
storage reduction: 4x
"""

Dividing 32 tokens into groups of four creates eight compressed entries. That is the storage reduction. Selecting two of those eight entries is the separate attention-work reduction.

The cache now holds eight long-range entries instead of 32. Attention reads two of them. A small local window separately preserves recent token detail.

This technique has to be built into the model and training process. A serving engine cannot convert an existing full-attention model into compressed sparse attention by changing a cache setting.

中文译文 12

442 字

随后,模型会从这些条目中选择 k 个用于注意力计算。一个较小的滑动窗口会为最近的词元保留更细致的细节。

在一百万个词元的情况下,DeepSeek 报告称,与 DeepSeek V3.2 所需的单词元推理 FLOPs 相比,V4-Pro 使用了 KV 缓存的 10% 和单词元推理 FLOPs 的 27%。V4-Flash 更进一步,分别为缓存的 7% 和 FLOPs 的 10%。

这些是整个模型的结果。其中包含 V4 的混合注意力布局和精度选择,因此不应将其解读为压缩单独带来的效果。

下一部分将这两种减少分别隔离出来。将 32 个长程条目按每组四个进行压缩后,会留下八个存储条目。如果注意力从这八个条目中选择两个,那么当前步骤只会读取两个压缩条目。

Python
full_token_entries = 32
tokens_per_group = 4
selected_entries = 2

compressed_entries = full_token_entries // tokens_per_group

print("full token entries:", full_token_entries)
print("compressed entries stored:", compressed_entries)
print("compressed entries read:", selected_entries)
print("storage reduction:", f"{full_token_entries / compressed_entries:.0f}x")

# Output:
"""
full token entries: 32
compressed entries stored: 8
compressed entries read: 2
storage reduction: 4x
"""

将 32 个词元分成每组四个,会创建八个压缩条目。这就是存储减少。再从这八个条目中选择两个,则是另一种独立的注意力工作量减少。

现在,缓存中保存的是八个长程条目,而不是 32 个。注意力会读取其中两个。一个较小的局部窗口则单独保留最近词元的细节。

这项技术必须构建在模型和训练过程中。服务引擎不能仅通过修改缓存设置,就把一个现有的完整注意力模型转换为压缩稀疏注意力模型。

英文原文 13

195 词

7. Query-aware sparse reads

The previous technique reduced both storage and attention work.

Sometimes, changing the model architecture is not an option. We may still want to reduce how much cache each decode step reads.

Recall what happens while the model generates a token. The new token produces a query, which acts like a search signal. Full attention compares it with every stored key. A long prompt therefore forces the GPU to load a large cache repeatedly.

Quest is a technique that reduces those reads without deleting any KV entries.

  • It divides the cache into pages.
  • A page is a small group of neighbouring token entries.
  • Each page also stores the minimum and maximum key values found inside it.

For every new query, Quest scores these short page summaries first. It then loads only the pages with the highest scores. The full cache remains stored because a page ignored now may matter later.

The Quest paper reports up to 7.03x lower self-attention latency. Its complete inference system reached up to 2.23x speedup.

The example below uses four pages. Each page contains two keys. A key has only two numbers here, which keeps the scoring visible.

中文译文 13

353 字

7. 面向查询的稀疏读取

前一种技术同时减少了存储和注意力工作量。

有时,改变模型架构并不是一个可选项。我们仍可能希望减少每个解码步骤读取的缓存量。

回想一下模型生成一个词元时会发生什么。新词元会产生一个查询,它像一个搜索信号。完整注意力会将它与每个已存储的键进行比较。因此,较长的提示会迫使 GPU 反复加载大量缓存。

Quest 是一种在不删除任何 KV 条目的情况下减少这些读取的方法。

  • 它将缓存划分为多个页。
  • 一页是由相邻词元条目组成的小组。
  • 每个页面还会存储其中发现的键值的最小值和最大值。

对于每个新查询,Quest 会先为这些简短的页面摘要评分。然后,它只加载得分最高的页面。完整缓存仍会被存储,因为当前被忽略的页面之后可能会变得重要。

Quest 论文报告称,自注意力延迟最高降低了 7.03 倍。其完整推理系统的加速最高达到 2.23 倍。

下面的示例使用四个页面。每个页面包含两个键。这里的键只有两个数字,这让评分过程清晰可见。

英文原文 14

236 词
python
import numpy as np

pages = np.array([
    [[1, 0], [2, 1]],
    [[5, 1], [4, 2]],
    [[0, 6], [1, 5]],
    [[-2, -1], [-1, 0]],
])
query = np.array([1.0, 0.5])

page_min = pages.min(axis=1)
page_max = pages.max(axis=1)
page_scores = np.maximum(query * page_min, query * page_max).sum(axis=1)
selected_pages = np.argsort(page_scores)[-2:]

for page, score in enumerate(page_scores):
    print(f"page {page} score: {score:.1f}")

print("selected pages:", np.sort(selected_pages).tolist())
print("KV entries read:", len(selected_pages) * 2, "of", pages.size // 2)
print("KV entries still stored:", pages.size // 2)

# Output:
"""
page 0 score: 2.5
page 1 score: 6.0
page 2 score: 4.0
page 3 score: -1.0
selected pages: [1, 2]
KV entries read: 4 of 8
KV entries still stored: 8
"""

The pages array contains eight keys in total. The next two lines create one minimum and one maximum summary per page. Quest uses similar summaries to estimate a page's best possible attention score.

The query prefers large values in both key dimensions. Pages 1 and 2 receive the highest scores, so the example reads their four keys. It leaves the other four keys untouched during this step.

If you notice the last line, the step reads four entries, but all eight remain resident. Quest reduces cache traffic and attention work. It does not reduce the memory needed to hold the full cache.

In practice, Quest uses many-dimensional keys and dedicated GPU code. It also repeats the selection for every query.

中文译文 14

338 字
python
import numpy as np

pages = np.array([
    [[1, 0], [2, 1]],
    [[5, 1], [4, 2]],
    [[0, 6], [1, 5]],
    [[-2, -1], [-1, 0]],
])
query = np.array([1.0, 0.5])

page_min = pages.min(axis=1)
page_max = pages.max(axis=1)
page_scores = np.maximum(query * page_min, query * page_max).sum(axis=1)
selected_pages = np.argsort(page_scores)[-2:]

for page, score in enumerate(page_scores):
    print(f"page {page} score: {score:.1f}")

print("selected pages:", np.sort(selected_pages).tolist())
print("KV entries read:", len(selected_pages) * 2, "of", pages.size // 2)
print("KV entries still stored:", pages.size // 2)

# Output:
"""
page 0 score: 2.5
page 1 score: 6.0
page 2 score: 4.0
page 3 score: -1.0
selected pages: [1, 2]
KV entries read: 4 of 8
KV entries still stored: 8
"""

pages 数组总共包含八个键。接下来的两行代码为每个页面创建一个最小值摘要和一个最大值摘要。Quest 使用类似的摘要来估计一个页面可能达到的最高注意力分数。

查询偏好两个键维度中的较大值。页面 1 和页面 2 获得最高分,因此示例读取了它们的四个键。在这一步中,另外四个键没有被触碰。

如果你注意最后一行,就会发现这一步读取了四个条目,但八个条目仍然全部驻留。Quest 减少了缓存流量和注意力计算量。它并没有减少容纳完整缓存所需的内存。

在实际应用中,Quest 使用多维键和专用 GPU 代码。它还会针对每个查询重复执行选择过程。

英文原文 15

285 词

8. Quantization

Quest keeps every KV entry and skips some reads. Quantization takes another route. It keeps every entry but stores each number with fewer bits.

Keys and values normally contain floating-point numbers. BF16 stores each number with 16 bits. FP8 uses 8 bits, so its raw payload is roughly half as large. A 4-bit format cuts the payload to 25%.

Quantization maps many original numbers onto a smaller set of allowed values. A scale controls the gap between those values. The engine stores the small integer code and enough scale information to interpret it later

The choice of scale affects accuracy. One extreme value can make a shared scale too wide. Most ordinary values then get rounded too aggressively.

KIVI handles keys and values differently. It groups key values by channel, which means the same feature across tokens. It groups value-cache numbers by token. The KIVI paper reports 2.6x lower peak memory and up to 4x larger batches with its 2-bit cache

The small example below shows the basic mapping. It converts six BF16-style values into signed 4-bit codes. It then converts those codes back into approximate values.

python
import numpy as np

values = np.array([-1.0, -0.55, -0.10, 0.25, 0.60, 1.0])
bits = 4
largest_code = 2 ** (bits - 1) - 1
scale = np.max(np.abs(values)) / largest_code

codes = np.rint(values / scale)
codes = np.clip(codes, -largest_code, largest_code).astype(np.int8)
restored = codes * scale

print("original values:", values.tolist())
print("4-bit codes:", codes.tolist())
print("restored values:", np.round(restored, 3).tolist())
print("raw payload:", len(values) * 16, "bits ->", len(values) * 4, "bits")

# Output:
"""
original values: [-1.0, -0.55, -0.1, 0.25, 0.6, 1.0]
4-bit codes: [-7, -4, -1, 2, 4, 7]
restored values: [-1.0, -0.571, -0.143, 0.286, 0.571, 1.0]
raw payload: 96 bits -> 24 bits
"""

中文译文 15

438 字

8. 量化

Quest 保留每个 KV 条目,但跳过部分读取。量化采取另一条路线。它保留每个条目,但用更少的位存储每个数字。

键和值通常包含浮点数。BF16 用 16 位存储每个数。FP8 使用 8 位,因此其原始载荷大约只有一半大小。4 位格式会将载荷降至 25%。

量化会将许多原始数字映射到一组更小的允许值上。一个缩放因子控制这些值之间的间隔。引擎存储较小的整数代码以及足够的缩放信息,以便之后解释它。

缩放因子的选择会影响准确性。一个极端值可能使共享缩放因子覆盖过宽的范围。这样一来,大多数普通值就会被过度舍入。

KIVI 对键和值采用不同的处理方式。它按通道对键值进行分组,也就是跨 token 使用同一特征。它按 token 对值缓存中的数字进行分组。KIVI 论文报告称,使用其 2 位缓存时,峰值内存降低了 2.6 倍,批量大小最高增加了 4 倍。

下面的小示例展示基本映射。它将六个类似 BF16 的值转换为有符号 4 位代码。然后再将这些代码转换回近似值。

python
import numpy as np

values = np.array([-1.0, -0.55, -0.10, 0.25, 0.60, 1.0])
bits = 4
largest_code = 2 ** (bits - 1) - 1
scale = np.max(np.abs(values)) / largest_code

codes = np.rint(values / scale)
codes = np.clip(codes, -largest_code, largest_code).astype(np.int8)
restored = codes * scale

print("original values:", values.tolist())
print("4-bit codes:", codes.tolist())
print("restored values:", np.round(restored, 3).tolist())
print("raw payload:", len(values) * 16, "bits ->", len(values) * 4, "bits")

# Output:
"""
original values: [-1.0, -0.55, -0.1, 0.25, 0.6, 1.0]
4-bit codes: [-7, -4, -1, 2, 4, 7]
restored values: [-1.0, -0.571, -0.143, 0.286, 0.571, 1.0]
raw payload: 96 bits -> 24 bits
"""

英文原文 16

229 词

Signed 4-bit storage provides codes from -7 through 7 in this example. Dividing by scale maps each original value to a code. Multiplying by the same scale produces the approximate value used during attention.

As you may have noticed, the restored values are close, but not identical. That rounding difference is quantization error. The final line counts only the six stored values. A real cache also stores scale metadata.

The example uses one scale for all six values. KIVI uses smaller groups because each group can fit its own numerical range. This usually reduces rounding error, though it requires more scale metadata.

If you use vLLM, it exposes FP8 cache storage directly:

bash
MODEL_ID=Qwen/Qwen2.5-7B-Instruct
vllm serve "$MODEL_ID" --kv-cache-dtype fp8

The first line defines the model name. The second starts a vLLM server and asks it to store the KV cache in FP8. This command keeps running because it launches the inference server.

Recent vLLM versions can also leave selected layers in their native type. This is useful when a sliding-window layer proves more sensitive:

bash
MODEL_ID=google/gemma-3-4b-it
vllm serve "$MODEL_ID" \
  --kv-cache-dtype fp8 \
  --kv-cache-dtype-skip-layers sliding_window

This version leaves sliding-window layers in their original format. It quantizes the remaining cache layers. The option helps when those local-attention layers lose too much accuracy under FP8.

Quantization preserves every token position. The next technique reduces memory by removing positions instead.

中文译文 16

398 字

在这个示例中,有符号 4 位存储提供从 -7 到 7 的代码。将每个原始值除以缩放因子,就会把它映射到一个代码。再乘以同一个缩放因子,就会得到注意力过程中使用的近似值。

正如你可能已经注意到的,恢复值很接近,但并不完全相同。这种舍入差异就是量化误差。最后一行只计算了六个存储值。真实缓存还会存储缩放元数据。

示例对六个值使用一个缩放因子。KIVI 使用更小的分组,因为每个分组都可以适应自己的数值范围。这通常会减少舍入误差,但需要更多缩放元数据。

如果你使用 vLLM,它会直接提供 FP8 缓存存储:

bash
MODEL_ID=Qwen/Qwen2.5-7B-Instruct
vllm serve "$MODEL_ID" --kv-cache-dtype fp8

第一行定义模型名称。第二行启动 vLLM 服务器,并要求它以 FP8 存储 KV 缓存。该命令会持续运行,因为它启动了推理服务器。

近期版本的 vLLM 还可以让选定的层保留其原生类型。当滑动窗口层被证明更加敏感时,这很有用:

bash
MODEL_ID=google/gemma-3-4b-it
vllm serve "$MODEL_ID" \
  --kv-cache-dtype fp8 \
  --kv-cache-dtype-skip-layers sliding_window

这个版本会让滑动窗口层保留原始格式,并量化其余的缓存层。当这些局部注意力层在 FP8 下损失过多精度时,该选项会有所帮助。

量化会保留每个令牌位置。下一种技术则通过移除位置来减少内存。

英文原文 17

202 词

9. Eviction

Quantization keeps all token positions. That may still be too large for a long context. Eviction sets a fixed position budget and discards entries outside it.

The difficult question is which positions should not be evicted. Recent tokens often matter because they contain the current conversation. Older tokens may still carry instructions, facts, or tool results needed later.

H2O is a technique that keeps recent positions and older heavy hitters. A heavy hitter is a token that has accumulated high attention during earlier steps. Once H2O evicts another token, its KV entry is no longer available.

Other methods estimate importance differently. SnapKV studies attention near the end of the input prompt. It uses that observation window to choose prompt positions before generation.

PyramidKV gives lower layers larger budgets and higher layers smaller ones, based on the attention patterns reported by its authors.

SnapKV reports 8.2x higher memory efficiency and 3.6x faster generation with 16K-token inputs. PyramidKV reports matching full-cache performance while retaining 12 percent of the cache in its LongBench experiments.

The following example takes a snapshot after ten tokens. Their scores represent attention accumulated during earlier decode steps. The cache can retain only six positions, including the latest two.

中文译文 17

384 字

9. 驱逐

量化会保留所有令牌位置。对于长上下文,这仍然可能过大。驱逐设定一个固定的位置预算,并丢弃超出该预算的条目。

困难的问题是哪些位置不应被驱逐。最近的令牌通常很重要,因为它们包含当前对话。较早的令牌仍可能携带之后需要的指令、事实或工具结果。

H2O 是一种保留最近位置和较早的重头令牌的技术。重头令牌是指在前面的步骤中累积了较高注意力的令牌。一旦 H2O 驱逐了另一个令牌,该令牌的 KV 条目就不再可用。

其他方法以不同方式估计重要性。SnapKV 研究输入提示末尾附近的注意力,并利用这个观察窗口在生成前选择提示中的位置。

PyramidKV 根据其作者报告的注意力模式,为较低层提供更大的预算,为较高层提供更小的预算。

在 16K 令牌输入下,SnapKV 报告了高 8.2 倍的内存效率和快 3.6 倍的生成速度。PyramidKV 报告称,在其 LongBench 实验中,仅保留 12% 的缓存即可达到与完整缓存相当的性能。

下面的示例在十个令牌之后获取一个快照。它们的分数表示此前解码步骤中累积的注意力。缓存最多只能保留六个位置,其中包括最近的两个位置。

英文原文 18

225 词
python
scores = [9.0, 0.2, 4.0, 0.1, 0.3, 6.0, 0.2, 0.4, 0.1, 0.2]
budget = 6
recent_count = 2

positions = list(range(len(scores)))
recent = positions[-recent_count:]
older = positions[:-recent_count]

heavy_hitter_count = budget - recent_count
heavy_hitters = sorted(
    older,
    key=lambda position: scores[position],
    reverse=True,
)[:heavy_hitter_count]

retained = sorted(heavy_hitters + recent)

print("heavy hitters:", sorted(heavy_hitters))
print("recent positions:", recent)
print("retained positions:", retained)
print("cache entries:", len(retained), "of", len(scores))

# Output:
"""
heavy hitters: [0, 2, 5, 7]
recent positions: [8, 9]
retained positions: [0, 2, 5, 7, 8, 9]
cache entries: 6 of 10
"""

Positions 8 and 9 are retained because they are the newest. Four slots remain. The sort selects positions 0, 2, 5, and 7 because they have the largest scores among older tokens.

The union of those two groups produces the final six-position cache. Positions 1, 3, 4, and 6 are discarded.

Real H2O updates its heavy-hitter statistics as generation proceeds. This smaller example starts with the accumulated scores so the selection rule remains visible.

That said, eviction needs a cautious evaluation. Importance changes across turns. A tool result that receives little attention now may become necessary after several more calls. System instructions and delimiters can also be disproportionately important.

Eviction evaluations should therefore use complete agent traces and delayed references. They should also include structured prompts. A good Needle-in-a-Haystack score does not cover those cases.

中文译文 18

343 字
python
scores = [9.0, 0.2, 4.0, 0.1, 0.3, 6.0, 0.2, 0.4, 0.1, 0.2]
budget = 6
recent_count = 2

positions = list(range(len(scores)))
recent = positions[-recent_count:]
older = positions[:-recent_count]

heavy_hitter_count = budget - recent_count
heavy_hitters = sorted(
    older,
    key=lambda position: scores[position],
    reverse=True,
)[:heavy_hitter_count]

retained = sorted(heavy_hitters + recent)

print("heavy hitters:", sorted(heavy_hitters))
print("recent positions:", recent)
print("retained positions:", retained)
print("cache entries:", len(retained), "of", len(scores))

# Output:
"""
heavy hitters: [0, 2, 5, 7]
recent positions: [8, 9]
retained positions: [0, 2, 5, 7, 8, 9]
cache entries: 6 of 10
"""

位置 8 和 9 因为最新而被保留。还剩四个槽位。排序选择了位置 0、2、5 和 7,因为它们在较早令牌中拥有最高分数。

这两组位置的并集产生了最终的六位置缓存。位置 1、3、4 和 6 被丢弃。

真实的 H2O 会随着生成进行而更新其重头令牌统计。这个更小的示例从累积分数开始,因此选择规则仍然清晰可见。

话虽如此,驱逐仍需要谨慎评估。重要性会在不同轮次之间变化。当前很少获得注意力的工具结果,可能在之后几次调用后变得必不可少。系统指令和分隔符也可能具有不成比例的重要性。

因此,驱逐评估应使用完整的智能体轨迹和延迟引用。评估还应包含结构化提示。一个良好的“大海捞针”分数并不能覆盖这些情况。

英文原文 19

192 词 · 1 张图

10. Paging

So far, we have looked at what one request stores or reads. Paging solves a different problem: how the serving engine allocates cache memory across many requests.

A simple allocator may reserve one continuous memory region per request. It often reserves enough space for the request's maximum length. Shorter requests leave part of that region unused. Free space also gets split into gaps as requests finish.

PagedAttention divides GPU cache memory into equal-sized blocks. Each block holds KV entries for a fixed number of tokens. A request can use blocks found anywhere in the shared pool, so its blocks do not need to lie next to each other.

Avi Chawla@_avichawla·Mar 11 ArticlePaged Attention in LLMs, clearly explained!When you’re serving LLMs at scale, memory becomes your bottleneck before compute does. The problem isn’t a lack of GPU memory; it’s how inefficiently we use it. Let’s understand why this happens and...176K

When a request finishes, the allocator returns its blocks to the pool. Another request can reuse them immediately. The PagedAttention paper reports near-zero KV cache waste and 2x to 4x higher throughput than the systems tested.

中文译文 19

338 字

10. 分页

到目前为止,我们讨论的是一个请求存储或读取什么。分页解决的是另一个问题:服务引擎如何在多个请求之间分配缓存内存。

简单的分配器可能会为每个请求预留一段连续的内存区域。它通常会按照请求的最大长度预留足够的空间。较短的请求会使该区域的一部分闲置。随着请求完成,空闲空间也会被分割成一个个间隙。

PagedAttention 将 GPU 缓存内存划分为大小相等的块。每个块存放固定数量 token 的 KV 条目。请求可以使用共享池中任意位置的块,因此其各个块不必彼此相邻。

Avi Chawla@_avichawla·3 月 11 日文章清晰解释 LLM 中的分页注意力!当你大规模提供 LLM 服务时,内存会先于计算成为瓶颈。 问题不在于 GPU 内存不足,而在于我们使用它的效率太低。 让我们了解为什么会这样,以及……176K

请求完成后,分配器会将其块归还到池中。另一个请求可以立即重用这些块。PagedAttention 论文报告称,KV 缓存浪费接近于零,并且吞吐量比受测系统高出 2 倍至 4 倍。

英文原文 20

266 词

A fixed pool can hide the effect of quantization. The engine may reserve the same 8 GB before and after enabling FP8. Smaller entries increase the number of tokens that fit inside that pool. The allocated memory shown by nvidia-smi may remain unchanged.

The example below begins with six free blocks. Request A takes three. Request B takes two. A then finishes and returns its blocks before request C arrives.

python
free_blocks = [0, 1, 2, 3, 4, 5]

request_a = free_blocks[:3]
free_blocks = free_blocks[3:]

request_b = free_blocks[:2]
free_blocks = free_blocks[2:]

free_blocks.extend(request_a)
free_after_a = free_blocks.copy()
request_c = free_blocks[:2]
free_blocks = free_blocks[2:]

print("request A blocks:", request_a)
print("request B blocks:", request_b)
print("free after A finishes:", free_after_a)
print("request C blocks:", request_c)

# Output:
"""
request A blocks: [0, 1, 2]
request B blocks: [3, 4]
free after A finishes: [5, 0, 1, 2]
request C blocks: [5, 0]
"""

After A finishes, the free pool contains block 5 and A's returned blocks. Request C takes blocks 5 and 0. Those block numbers are not adjacent, but the block table records their order for C.

This is the central paging idea. The allocator reuses available blocks instead of searching for one large continuous region.

vLLM also tracks which request owns each block. Its attention code can read the non-adjacent blocks in logical sequence order. The short example isolates allocation and reuse.

Quantization still changes the capacity inside this pool. An 8 GB pool holds 26,214 tokens at 320 KB per token, compared with 52,428 tokens at 160 KB. Total allocated GPU memory remains 8 GB in both cases.

中文译文 20

403 字

固定大小的池可能会掩盖量化的影响。启用 FP8 前后,引擎都可能预留同样的 8 GB。更小的条目会增加该内存池能够容纳的 token 数量。nvidia-smi 显示的已分配内存可能保持不变。

下面的示例从六个空闲块开始。请求 A 占用三个。请求 B 占用两个。随后 A 完成并归还其块,然后请求 C 到达。

python
free_blocks = [0, 1, 2, 3, 4, 5]

request_a = free_blocks[:3]
free_blocks = free_blocks[3:]

request_b = free_blocks[:2]
free_blocks = free_blocks[2:]

free_blocks.extend(request_a)
free_after_a = free_blocks.copy()
request_c = free_blocks[:2]
free_blocks = free_blocks[2:]

print("request A blocks:", request_a)
print("request B blocks:", request_b)
print("free after A finishes:", free_after_a)
print("request C blocks:", request_c)

# Output:
"""
request A blocks: [0, 1, 2]
request B blocks: [3, 4]
free after A finishes: [5, 0, 1, 2]
request C blocks: [5, 0]
"""

A 完成后,空闲池包含块 5 以及 A 归还的块。请求 C 取得块 5 和块 0。这些块编号并不相邻,但块表会记录它们对 C 而言的顺序。

这就是分页的核心思想。分配器会重用可用的块,而不是寻找一大片连续的区域。

vLLM 还会跟踪每个块属于哪个请求。其注意力代码可以按照逻辑顺序读取不相邻的块。这个简短示例将分配与重用过程单独展示出来。

量化仍会改变该内存池内部的容量。一个 8 GB 的内存池在每个 token 占用 320 KB 时可容纳 26,214 个 token,而在每个 token 占用 160 KB 时可容纳 52,428 个 token。两种情况下,GPU 的总已分配内存都保持为 8 GB。

英文原文 21

161 词

11. Prefix reuse

Paging reuses blocks after a request finishes. Prefix caching can share blocks while several requests are still active. It applies when those requests begin with exactly the same tokens.

This happens often with system prompts and tool definitions. Without prefix caching, the engine computes and stores another KV copy for every request. The repeated prefix consumes memory and repeats the prefill work that created it.

Automatic prefix caching stores each completed prefix block under a lookup key. When another request has the same token prefix, it points to the existing KV block. New computation begins only after the requests diverge.

vLLM builds each lookup key from the current block and its preceding blocks. It also includes details such as adapter identifiers when they affect compatibility. The vLLM prefix-caching design documents this structure.

The example below uses four-token blocks. Each lookup key contains the entire prefix up to that block. This is a readable stand-in for vLLM's chained hashes.

中文译文 21

320 字

11. 前缀重用

分页会在请求完成后重用块。前缀缓存则可以在多个请求仍处于活动状态时共享块。当这些请求以完全相同的 token 开始时,就可以应用这种机制。

这种情况经常出现在系统提示词和工具定义中。没有前缀缓存时,引擎会为每个请求重新计算并存储一份 KV 副本。重复的前缀会消耗内存,并重复执行生成它所需的预填充工作。

自动前缀缓存会将每个已完成的前缀块存储在一个查找键下。当另一个请求拥有相同的 token 前缀时,它会指向已有的 KV 块。只有在请求出现分歧之后,才会开始新的计算。

vLLM 会根据当前块及其之前的块构建每个查找键。当适配器标识符等细节会影响兼容性时,vLLM 也会将其纳入查找键。vLLM 的前缀缓存设计文档说明了这一结构。

下面的示例使用四个令牌为一块。每个查找键都包含截至该块为止的完整前缀。这是对 vLLM 链式哈希的一种易读替代表示。

英文原文 22

258 词
python
def prefix_keys(tokens, block_size):
    block_ends = range(block_size, len(tokens) + 1, block_size)
    return [tuple(tokens[:end]) for end in block_ends]

first = [10, 11, 12, 13, 20, 21, 22, 23, 30, 31, 32, 33]
second = [10, 11, 12, 13, 20, 21, 22, 23, 40, 41, 42, 43]

cache = set()
first_keys = prefix_keys(first, block_size=4)
first_hits = sum(key in cache for key in first_keys)
cache.update(first_keys)

second_keys = prefix_keys(second, block_size=4)
second_hits = sum(key in cache for key in second_keys)

print("first request reused:", first_hits, "of", len(first_keys), "blocks")
print("second request reused:", second_hits, "of", len(second_keys), "blocks")
print("second request computes:", len(second_keys) - second_hits, "new block")

# Output:
"""
first request reused: 0 of 3 blocks
second request reused: 2 of 3 blocks
second request computes: 1 new block
"""

The first request has no cache entries to reuse. It adds keys for its first four, eight, and twelve tokens. The second request has the same first eight tokens, so its first two keys match.

The final four tokens differ. That changes the last prefix key, so the second request computes one new block.

Using the full prefix prevents a false match. Two requests cannot reuse a block merely because four tokens match somewhere in the middle. Their preceding context must match as well.

Prefix reuse depends on exact tokenized prefixes. A timestamp in the system prompt, reordered JSON keys in a tool schema, or different chat-template whitespace can break the match.

Stable prompt templates therefore improve the hit rate. The model and cache format can remain unchanged while duplicate storage drops.

中文译文 22

371 字
Python
def prefix_keys(tokens, block_size):
    block_ends = range(block_size, len(tokens) + 1, block_size)
    return [tuple(tokens[:end]) for end in block_ends]

first = [10, 11, 12, 13, 20, 21, 22, 23, 30, 31, 32, 33]
second = [10, 11, 12, 13, 20, 21, 22, 23, 40, 41, 42, 43]

cache = set()
first_keys = prefix_keys(first, block_size=4)
first_hits = sum(key in cache for key in first_keys)
cache.update(first_keys)

second_keys = prefix_keys(second, block_size=4)
second_hits = sum(key in cache for key in second_keys)

print("first request reused:", first_hits, "of", len(first_keys), "blocks")
print("second request reused:", second_hits, "of", len(second_keys), "blocks")
print("second request computes:", len(second_keys) - second_hits, "new block")

# Output:
"""
first request reused: 0 of 3 blocks
second request reused: 2 of 3 blocks
second request computes: 1 new block
"""

第一次请求没有可复用的缓存条目。它为前四个、前八个和前十二个令牌添加键。第二次请求的前八个令牌相同,因此它的前两个键能够匹配。

最后四个令牌不同。这会改变最后一个前缀键,因此第二次请求需要计算一个新块。

使用完整前缀可以避免错误匹配。两个请求不能仅仅因为中间某处有四个令牌相同,就复用一个块。它们之前的上下文也必须匹配。

前缀复用取决于分词后完全一致的前缀。系统提示中的时间戳、工具架构中重新排序的 JSON 键,或不同的聊天模板空白字符,都可能破坏匹配。

因此,稳定的提示模板可以提高命中率。在模型和缓存格式保持不变的情况下,重复存储量也可以下降。

英文原文 23

288 词

12. Cache Offloading from GPU

Prefix reuse helps when another request begins with tokens the engine has already processed. Offloading handles a different problem. It moves KV blocks that are not currently needed from GPU memory to a larger but slower memory tier, usually CPU memory.

Those blocks may belong to a sequence paused by the scheduler or to a cached prefix retained for reuse. When the engine needs them again, it copies them back to the GPU.

There is one important limitation. A new OpenAI-compatible API request does not automatically resume the KV cache from an earlier request. Reuse happens only if the engine still tracks the sequence, recognizes an exact prefix-cache match, or restores the cache through an external storage system.

Offloading frees GPU memory without deleting the stored KV data. It does not reduce the total number of bytes held across GPU and CPU memory. The trade-off appears when the engine must transfer those blocks back before computation can continue.

The example below gives the GPU room for two sessions. Adding session C moves the oldest session, A, to CPU memory. Resuming A brings it back and moves B out.

python
gpu_cache = ["cache-a", "cache-b"]
cpu_cache = []

# Cache C needs GPU space, so cache A moves to CPU memory.
cpu_cache.append(gpu_cache.pop(0))
gpu_cache.append("cache-c")

print("GPU after cache C arrives:", gpu_cache)
print("CPU after cache C arrives:", cpu_cache)

# Cache A is needed again, so it returns to the GPU.
cpu_cache.remove("cache-a")
cpu_cache.append(gpu_cache.pop(0))
gpu_cache.append("cache-a")

print("GPU after cache A returns:", gpu_cache)
print("CPU after cache A returns:", cpu_cache)

# Output:
"""
GPU after cache C arrives: ['cache-b', 'cache-c']
CPU after cache C arrives: ['cache-a']
GPU after cache A returns: ['cache-c', 'cache-a']
CPU after cache A returns: ['cache-b']
"""

中文译文 23

414 字

12. 从 GPU 卸载缓存

当另一个请求以引擎已经处理过的令牌开头时,前缀复用会有所帮助。卸载处理的是另一个问题:它将当前不需要的 KV 块从 GPU 内存移到容量更大但速度更慢的内存层,通常是 CPU 内存。

这些块可能属于被调度器暂停的序列,也可能属于为重复使用而保留的缓存前缀。当引擎再次需要它们时,会将它们复制回 GPU。

有一个重要限制。新的 OpenAI 兼容 API 请求不会自动从较早请求恢复 KV 缓存。只有在引擎仍在跟踪该序列、识别出精确的前缀缓存匹配,或通过外部存储系统恢复缓存时,才会发生复用。

卸载可以释放 GPU 内存,而不会删除已存储的 KV 数据。它不会减少 GPU 和 CPU 内存合计持有的字节数。当引擎必须在继续计算前把这些块传回时,代价就会体现出来。

下面的示例让 GPU 能容纳两个会话。加入会话 C 后,最旧的会话 A 被移到 CPU 内存。恢复 A 时,会将它移回 GPU,并把 B 移出。

Python
gpu_cache = ["cache-a", "cache-b"]
cpu_cache = []

# Cache C needs GPU space, so cache A moves to CPU memory.
cpu_cache.append(gpu_cache.pop(0))
gpu_cache.append("cache-c")

print("GPU after cache C arrives:", gpu_cache)
print("CPU after cache C arrives:", cpu_cache)

# Cache A is needed again, so it returns to the GPU.
cpu_cache.remove("cache-a")
cpu_cache.append(gpu_cache.pop(0))
gpu_cache.append("cache-a")

print("GPU after cache A returns:", gpu_cache)
print("CPU after cache A returns:", cpu_cache)

# Output:
"""
GPU after cache C arrives: ['cache-b', 'cache-c']
CPU after cache C arrives: ['cache-a']
GPU after cache A returns: ['cache-c', 'cache-a']
CPU after cache A returns: ['cache-b']
"""

英文原文 24

247 词

The first move leaves caches B and C on the GPU while cache A remains available in CPU memory. When cache A is needed again, the example brings it back and moves cache B out.

The lists contain cache names instead of tensors. A real engine performs this operation on KV blocks and tracks which request or reusable prefix owns each block.

The example assumes that the engine has retained enough information to identify cache A later. Sending an unrelated API request with the same label would not restore it automatically.

vLLM can enable a 16 GB CPU offloading buffer directly:

bash
MODEL_ID=Qwen/Qwen2.5-7B-Instruct

vllm serve "$MODEL_ID" \
  --kv-offloading-size 16 \
  --kv-offloading-backend native

The first option sets the CPU buffer size in GB. The second selects vLLM's native CPU backend. Like the earlier serving command, this process keeps running after startup.

Offloading trades GPU capacity for transfer time. Measure resume latency and CPU bandwidth before enabling a large offload tier.

How to use them together

Some of these methods compose because they reduce different terms. Architecture-level methods still depend on how the checkpoint was trained.

For instance, take the 40 GB GQA cache from the first example.

  • CLA2 can cut the unique layers in half, producing 20 GB.
  • FP8 can halve the bytes per value, producing roughly 10 GB.
  • A 50 percent token budget would reduce the resident cache to roughly 5 GB.

That multiplication is useful for capacity planning, but it hides model quality and kernel support.

中文译文 24

421 字

第一次移动后,缓存 B 和 C 留在 GPU 上,而缓存 A 仍可在 CPU 内存中使用。再次需要缓存 A 时,示例将它移回 GPU,并把缓存 B 移出。

这些列表包含的是缓存名称,而不是张量。真实引擎会对 KV 块执行这一操作,并跟踪每个块属于哪个请求或可复用前缀。

示例假设引擎保留了足够的信息,以便之后识别缓存 A。发送一个带有相同标签但无关的 API 请求,并不会自动恢复它。

vLLM 可以直接启用一个 16 GB 的 CPU 卸载缓冲区:

Bash
MODEL_ID=Qwen/Qwen2.5-7B-Instruct

vllm serve "$MODEL_ID" \
  --kv-offloading-size 16 \
  --kv-offloading-backend native

第一个选项设置以 GB 为单位的 CPU 缓冲区大小。第二个选项选择 vLLM 的原生 CPU 后端。与前面的服务命令一样,该进程在启动后会继续运行。

卸载用传输时间换取 GPU 容量。在启用较大的卸载层之前,应先测量恢复延迟和 CPU 带宽。

如何将它们结合使用

其中一些方法可以组合使用,因为它们减少的是不同的项。架构层面的方法仍然取决于检查点的训练方式。

例如,考虑第一个例子中的 40 GB GQA 缓存。

  • CLA2 可以将不重复的层数减少一半,得到 20 GB。
  • FP8 可以将每个值所占的字节数减半,得到大约 10 GB。
  • 50% 的 token 预算会将常驻缓存减少到大约 5 GB。

这种乘法对于容量规划很有用,但它掩盖了模型质量和内核支持问题。

英文原文 25

233 词
  • CLA requires training.
  • FP8 introduces quantization error.
  • Eviction discards context.
  • And the serving engine must also support the resulting layout.

These are some situations you may face, along with solutions:

  • When choosing a model → Inspect KV head count, local and global layer ratios, latent-attention dimensions, and recurrent layers. These determine the starting cache before deployment.
  • When serving an existing model → Test FP8 first, then standardize prefixes. Both preserve the token set. Add eviction only after workload-specific quality tests.
  • When memory appears unchanged → Inspect cache capacity and block counts. A fixed pool can absorb the savings while total allocated GPU memory stays flat.
  • When latency is the problem → Measure cache reads and attention time. Quest-style sparse reads may help even when they don't free capacity.
  • When inactive sessions occupy the GPU → Test offloading and scheduling. Moving cold blocks can matter more than compressing the active ones further.

KV cache reduction is easier to reason about once each technique has a specific target.

This table summarizes all of the techniques we cover, what they alter, the takeaway from it, and what part they don't handle:

Start with the constraint you need to change, then pick the technique that handles it.

👉 Over to you: how do you reduce KV cache in production?

That's a wrap!

If you enjoyed this tutorial:

Find me → @_avichawla

Every day, I share tutorials and insights on DS, ML, LLMs, and RAG.

中文译文 25

437 字
  • CLA 需要训练。
  • FP8 会引入量化误差。
  • 驱逐会丢弃上下文。
  • 而且,服务引擎还必须支持由此产生的布局。

以下是你可能遇到的一些情况,以及相应的解决方案:

  • 选择模型时 → 检查 KV 头数量、本地层与全局层的比例、潜在注意力维度以及循环层。这些因素决定部署前的初始缓存。
  • 部署现有模型时 → 先测试 FP8,然后统一前缀。两者都会保留 token 集合。只有在完成针对具体工作负载的质量测试后,才添加驱逐。
  • 内存看起来没有变化时 → 检查缓存容量和块数量。固定内存池可能吸收节省的空间,使 GPU 总分配内存保持不变。
  • 延迟出现问题时 → 测量缓存读取和注意力时间。即使不能释放容量,Quest 风格的稀疏读取也可能有所帮助。
  • 非活跃会话占用 GPU 时 → 测试卸载和调度。移动冷块可能比进一步压缩活跃块更重要。

一旦每种技术都有了明确目标,KV Cache 的缩减就更容易理解。

下表总结了我们介绍的所有技术、它们改变的内容、由此得到的要点,以及它们没有处理的部分:

先确定你需要改变的约束,然后选择能够处理该约束的技术。

👉 交给你了:你在生产环境中如何减少 KV Cache?

本教程到此结束!

如果你喜欢本教程:

找到我 → @_avichawla [1]

我每天分享关于数据科学、机器学习、大型语言模型和检索增强生成的教程与见解。

来源与阅读说明

正文取自用户提交的本地文件,原始文件与原图按字节归档。原网页含 3 段视频,本保存件未取得视频文件;保留静态预览(如有),完整视频请从原文链接查看。中文译文逐单元生成并由另一次模型请求审校;未进行人工全文审校、线上来源比对或代码执行。作者与日期缺失时保留未知。

阅读补充解释本文内容与限制,不冒充独立知识专题或外部事实核验。图片保持原样,图内说明单独保存。阅读器沿用 ReadLess 公共组件及中文约 400 字分块规则。