lmcache

KV Cache 与 LMCache 入门(一)

2026-11-02

AAllison Row

什么是 KV cache,它从何而来

当你在聊天机器人中输入一段 prompt 时,大语言模型(LLM)会把你的文本转换成数字,让这些数字经过多层矩阵乘法运算,并根据结果预测下一个应该出现的 token。它把这个 token 追加到序列末尾,再次运行同样的过程来预测下一个 token,如此反复,直到这些数字最终变成你在屏幕上看到的回复。

模型之所以必须处理数字,是因为它的核心运算是乘法,具体来说是大型数字矩阵之间的乘法。你的 prompt 首先会被切分成 token,一个 token 可能是完整的单词,也可能是单词的片段。每个 token 随后被表示为一个向量,即模型可以对其进行运算的一组数字。这些向量会与模型自身习得的数值(即权重,weights)相乘,并经过多层计算。最终输出的是下一个 token 的概率分布。

这一过程称为推理(inference)。从界面上看,似乎只是文字流入、文字流出,但底层硬件实际上在搬运和变换大规模的数字数组。这些数组占用内存(例如 GPU 显存),计算过程中产生的中间结果也必须存放在某处,直到下一步运算使用它们。

其中有些数据只用一次,随即丢弃。但大量中间数据需要被反复使用:模型此后生成的每一个 token 都需要它们。模型不必每次都从头重新计算,而是只计算一次并存储起来以供复用。这个存储就叫作 KV cache(键值缓存)。系统是保留它还是丢弃它,决定了推理是快速且经济,还是恰恰相反。

为了理解这个缓存为何存在(以及为何管理它本身会成为一个独立的问题),我们将沿着模型自身的处理路径展开:你的 prompt 如何变成模型可以处理的数字;模型为什么必须不断“回头看”已经读过的内容;因此具体有哪些内容被计算并缓存;以及缓存不可用时要付出什么代价。

从词到向量

模型无法直接对文字进行运算。第一步是分词(tokenization):把文本切分成来自模型词表(vocabulary)的单元。一个 token 可能是一个完整的单词、单词的一部分、标点、空白字符,或其他常见的文本片段。通常一个 token 约为 ¾ 个英文单词(大约 4 个字符);常见单词是一个 token,较少见的单词则会被拆成多个片段。

每个 token 由一个整数 ID 表示。模型用这个 ID 查找对应的 embedding(嵌入向量),即包含固定数量习得数值的向量。向量本质上只是一组数字,但它为模型提供了一种可以用矩阵乘法进行变换的形式。token 到 embedding 的映射经过设计,使相关的词在空间中彼此靠近。“cat”(猫)和“dog”(狗)在这个空间中距离很近;而“cat”和“car”(汽车)尽管字面相似,距离却不近。

初始 embedding 只是起点。随着向量流经模型的各层,它的表示会根据上下文不断变化。例如,“bank”这个 token 在“river bank”(河岸)和“bank account”(银行账户)中应当发挥不同的作用。让一个 token 能够融合其他 token 信息的机制,称为注意力(attention)。

Diagram illustrating the tokenization and embedding process in a neural network, including input text 'What is attention?', token IDs, and corresponding embedding vectors.

模型为什么要回头看:注意力机制

要预测接下来的内容,模型不能只孤立地看最新的 token,还需要之前的上下文。

以句子 “The cat sat down because it was tired.”(猫坐了下来,因为它累了。)为例。当模型处理 “it” 时,与 “cat” 相关的信息可能格外重要。注意力机制让模型能够将当前 token 与之前的 token 进行比较,并整合对当前步骤有用的信息。

这一过程贯穿整个模型,而不仅限于代词这类明显的指代关系。在每一层中,注意力都允许每个 token 位置从其可见的上下文中提取信息。不同的注意力头(attention head)可以学会侧重不同的关系,例如相邻的句法结构、长距离指代、格式模式或其他有用的信号。

Illustration depicting a sequence of words with attention weights, highlighting 'cat' as the focus with strong attention arrows and weaker attention for the surrounding words.

Query、Key 与 Value

注意力基于从每个 token 当前表示中派生出的三个向量,它们共同构成一种类似“查找”的机制:每个 token 提出一个问题,提供一个供他人匹配的标签,并持有一份被选中时交出的内容。

  • Query(查询):这个 token 在寻找什么
  • Key(键):这个 token 能提供什么,供其他 token 与之比较
  • Value(值):当这个 token 被关注时,它传递出去的信息

再看前面的句子:为了决定 “it” 应该在多大程度上关注 “cat”,模型会将 “it” 的 Query 与 “cat” 的 Key 进行比较。这与最初在向量空间中摆放 token 时所用的相似度度量属于同一类。对之前的每个 token 重复这一比较,就得到一组注意力权重(attention weights):此时此刻,之前的每个 token 与我有多相关?然后,该 token 的输出由之前各 token 的 Value 按这些分数加权混合而成。Key 决定关注多少;Value 是真正被使用的内容。

对当前 token 而言,模型将其 Query 与它被允许看到的各 token 的 Key 进行比较,得到注意力分数(attention scores)。分数经归一化成为权重后,模型用它们对相应的 Value 进行加权混合。Key 帮助确定关注哪里;Value 提供被整合的信息。

这些都是习得的数值运算,而不是字面意义上的问题、标签或存储的定义。“查找”这一启发式类比有助于从概念上理解该过程:用 Query 与 Key 比较,再取回 Value 的加权组合。

技术细节:从 token 到 KV cache 的维度追踪

每个 token 最初都是一个长度为 d_model 的向量,d_model 即模型的隐藏层维度(hidden size,通常为几千;Llama 3 8B 为 4096)。三个习得的权重矩阵 W_Q、W_K、W_V 分别将该向量投影为长度为 d_k 的新向量:

q = xWQ k = xWK v = xWV

对长度为 T 的 token 序列中的每个 token 执行此操作,Q、K、V 各自就成为形状为 (T, d_k) 的矩阵,每行对应一个 token。实践中,d_k 会被拆分到多个注意力头上,每个头处理更小的一部分,但每个 token 存储的总量不变。再把层数考虑进来:模型在每一层都独立运行整个过程,每层、每个序列各产生一个 Key 矩阵和一个 Value 矩阵。完整的 KV cache 就是所有层的这些矩阵的总和。
技术细节:固定权重与随请求变化的张量

正文将 Query、Key、Value 当作单个向量来讨论。在实际实现中,一层会同时处理多个 token 位置和多个注意力头,因此这些值以矩阵或更高维的张量(tensor)形式存储。

忽略 batch 维度,设:
  • T:正在处理的 token 位置数
  • dmodel:每个 token 表示的宽度
  • HQ:Query 头的数量
  • HKV:Key/Value 头的数量
  • dhead:单个注意力头的宽度

通常,dmodel = HQdhead。某一注意力层的输入为:

X ∈ ℝT × dmodel

X 是随请求变化的数据(request-dependent data):它的第一个维度随所处理的 token 数量增长,其数值也会随 prompt 和层的不同而变化。

该层还包含习得的投影矩阵:

WQ ∈ ℝdmodel × (HQ·dhead)
WK, WV ∈ ℝdmodel × (HKV·dhead)
WO ∈ ℝ(HQ·dhead) × dmodel

这些是模型权重(model weights)。它们在训练过程中习得,在推理期间保持固定,其维度不会随 prompt 增长。有些软件会以转置形式存储这些矩阵,但数学运算是相同的。

将随请求变化的表示与固定权重相乘,即可得到 Query、Key 和 Value 激活值(activations):

Q = XWQ,K = XWK,V = XWV

拆分成多个头之后:

Q ∈ ℝT × HQ × dhead
K, V ∈ ℝT × HKV × dhead

在标准多头注意力(multi-head attention,MHA)中,HQ = HKV。在分组查询注意力(grouped-query attention,GQA)中,多个 Query 头共享同一个 Key/Value 头,因此 HKV < HQ。这减少了需要存储和读取的 K/V 数据量。
技术细节:注意力公式

对单个注意力头,缩放点积注意力(scaled dot-product attention)通常写作:

Attention(Q, K, V) = softmax( QKT ⁄ √(dk) ) · V

QK^T 计算 Query 与 Key 之间的相似度分数。除以 √d_k 可将这些分数保持在合适的量级;否则 softmax 的输出几乎会完全集中到得分最高的那个 token 上。Softmax 将分数转换为总和为 1 的权重,再与 V 相乘,得到 Value 向量的加权组合。

这些向量来自该层表示 x 的习得投影:

q = xWQ k = xWK v = xWV

实际模型通常在每个 transformer 层中使用多个注意力头执行这一运算。

两个阶段:prefill 与 decode

推理引擎通常把生成过程分为两个阶段:prefill(预填充)和 decode(解码)。

在 prefill 阶段,模型处理 prompt。由于所有 prompt token 一开始就已知,它们在每一层内的计算可以并行进行:一次矩阵乘法即可得到所有 prompt token 的 Query、Key 和 Value,另一次矩阵乘法则一次性算出每个位置相对于其他所有位置的注意力分数。(每个位置仍然只能看到自身及之前的位置,这由因果掩码(causal mask)来保证,稍后会详细介绍。)随着 prompt 在模型中逐层传递,引擎会在每一层为每个 prompt token 生成并存储一个 Key 和一个 Value。这就构成了初始的 KV cache:模型对 prompt 全部内容的理解,被保存下来以供复用。

在 decode 阶段,模型每次生成一个新 token。对每个新 token,每一层只计算该 token 的 Query、Key 和 Value,这是一次很小的计算,而不是 prefill 那样的批量计算。该 Query 与这一层已存储的 Key 进行比较,使用相应的 Value,然后继续流经模型的其余部分。该 token 处理完成后,它新的 Key 和 Value 会被追加(append)到缓存中。

Diagram illustrating the Prefill and Decode phases in a token processing model, showing the relationship between query, key, and value matrices, and how each phase computes outputs.

被选出的 token 随后被送回模型,进入下一步。这个循环(生成一个 token、将其追加、再用更长的序列生成下一个)称为自回归生成(autoregressive generation)。循环的每一步都会读取目前已构建的整个缓存,但每次只计算并追加一个新的 Key 和 Value;之前的内容都不会重做。

An illustration explaining the prefill and decode stages in a machine learning model, showing how tokens are processed in parallel and how new tokens are generated using a cache for key and value vectors.
技术细节:注意力从 prefill 到 decode 的变化

对单个注意力头,缩放点积注意力为:

Attention(Q, K, V) = softmax( QKT ⁄ √(dhead) ) · V

设 T_Q 为当前正在处理的 Query 位置数,T_K 为可作为上下文的 Key/Value 位置数。在所有 Query 头上,注意力分数张量的形状为:

HQ × TQ × TK

在 prefill 阶段,一层可能一次处理包含 T 个 token 的 prompt。此时 T_Q = T_K = T,分数张量的逻辑形状为:

HQ × T × T

因果掩码会屏蔽那些会让 token 用到未来位置的元素。经过优化的 attention kernel 可能会分块(tiling)计算分数,而不是在 GPU 显存中完整物化(materialize)整个张量。

在单 token decode 中,该层处理一个新的 Query 位置(T_Q = 1),同时对已缓存的前缀做注意力计算(即 T_K 包含整个已缓存前缀加上新位置)。当上下文长度为 T 时,分数张量约为:

HQ × 1 × T

一个重要的区别是:KV cache 避免了对之前 token 表示的重复计算,但新的 Query 仍然必须读取并关注一个随序列增长的上下文。

为什么之前的 Key 和 Value 可以复用

从左到右生成文本的模型是因果的(causal):一个 token 可以使用自身和之前的 token,但不能使用尚未生成的 token。在注意力计算中,因果掩码会屏蔽这些未来位置,这也正是 prefill 能在一次并行计算中算出所有位置的分数、同时又不让任何位置“偷看”后文的原因。

A grid displaying availability for key positions, with checkmarks indicating availability and crossed boxes indicating blocked positions. Rows represent query positions (t1 to t5) and columns represent key positions (t1 to t5).

这种单向流动意味着:一旦模型处理过某个前缀(prefix),在同一前缀后追加一个 token,并不会改变之前位置已经算好的 Key 和 Value。新 token 需要关注它们,但模型无需重新生成它们。

这一可复用的 Key 和 Value 集合(涵盖所有已缓存的 token 位置和所有 transformer 层)就是 KV cache。Query 不会以同样的方式保留,因为 Query 只在处理某个 token 的那一刻用于执行查找。后续 token 持续需要的,是之前的 Key 和 Value。

可以把它想象成开会时记笔记。有人发言时,你在笔记里添一行新内容;你不会在记下新的一行之前把之前的每一行都重写一遍。KV cache 让生成过程在已有的数值记录上继续扩展,而不是在每一步都重新构建它。

在单次生成之外复用缓存

在单次生成内部,复用 KV cache 是标准的 decode 流程。而跨不同请求复用它,则是另一个问题。

聊天界面可能显示为一段连续的对话,但后端通常把每一轮都当作一个新请求接收,其中包含到目前为止的完整对话(例如最初的 prompt、模型的回复,以及追加在末尾的你的新消息)。如果服务系统仍缓存着匹配的前缀,并且能够识别和访问它,就可以跳过这些已缓存 token 的 prefill,只处理新增的后缀(suffix)。如果缓存已被丢弃、被驱逐(evict),或者留在了另一个引擎上,同样的对话历史就必须重新处理一遍。

同样的模式出现在许多生产环境的工作负载中:

  • 聊天请求反复包含相同的 system prompt 和对话历史。
  • Agent 会发起一系列调用,持续携带指令、工具调用结果和中间状态。
  • RAG 应用可能在相关请求之间复用相同的检索文档或其他大段上下文。
  • 多个用户或请求可能以完全相同的共享前缀开头。

在上述每种情况下,应用发送的很多 token 可能已经被该模型(或另一个模型实例)处理过。可复用的 KV cache 能把这些重复的 prefill 计算转变为一次缓存查找与传输。

Flowchart illustrating a sequence of four call stages in an agent's run, highlighting new and cached components for each call, with annotations explaining the caching process.
技术细节:什么决定了 KV cache 的大小

对于常规的 transformer,KV cache 的大小约为:

2 × L × T × HKV × Dhead × B

其中 2 对应 Key 和 Value 两部分,L 为 transformer 层数,T 为已缓存的 token 数,H_KV 为 Key/Value 头数,d_head 为每个头的维度,B 为每个存储值所占的字节数。
技术细节:实例——Llama 3 8B 的 KV cache 大小

Llama 3 8B 的模型宽度为 4096,共有 32 个 transformer 层、32 个 Query 头和 8 个 Key/Value 头。因此每个头的宽度为:

dhead = 4096 ⁄ 32 = 128

对于单层,投影矩阵的数学形状为:
  • WQ:4096 × 4096
  • WK:4096 × 1024
  • WV:4096 × 1024

假设 prefill 处理 4 个 token。忽略 batch 维度:
  • 输入 X:4 × 4096
  • Query 张量:4 × 32 × 128
  • Key 张量:4 × 8 × 128
  • Value 张量:4 × 8 × 128
  • 逻辑注意力分数:32 × 4 × 4

在下一个 decode 步骤中,该层接收一个新的 token 表示:
  • 新的 Query:1 × 32 × 128
  • 新的 Key 和 Value:各为 1 × 8 × 128
  • 更新后的已缓存 Key 和 Value:各为 5 × 8 × 128
  • 新 token 的逻辑注意力分数:32 × 1 × 5

该 Query 在这一步使用后即可丢弃。新的 Key 和 Value 会被追加到缓存中,因为后续 token 还需要用到它们。

在所有层上,单个序列的 KV cache 大小约为:

2 × L × T × HKV × dhead × B

其中 B 是每个存储值所占的字节数,开头的 2 对应 Key 和 Value 两部分。

使用 BF16 数值时,Llama 3 8B 每个已缓存 token 需要:

2 × 32 × 8 × 128 × 2 = 131,072 字节

即每个序列每个 token 占用 128 KiB。100 个 token 约为 12.5 MiB,8,192 个 token 则为 1 GiB。这还未计入分配器(allocator)开销,实际大小也取决于缓存如何在多个设备间分片(sharding)。

关键的对比在于:投影权重与上下文长度无关,始终保持固定;而保留下来的 K/V 张量会随每个新缓存的 token 不断增长。

何必如此:不缓存的代价

如果没有缓存,生成第 100 个 token 就意味着要为之前全部 99 个 token 重新计算 Key 和 Value。每一步都这样做代价极高,而且随着对话变长还会越来越贵。这种代价还会累积:第 10 个 token 要重做 10 个 token 的工作量,第 100 个 token 要重做 100 个 token 的工作量,整段回复的总工作量大致随其长度的平方增长。

有了 KV cache,之前每个 token 的 Key 和 Value 只需计算一次。Decode 阶段只需让新追加的 token 流经模型,并复用此前所有内容已存储的状态。这消除了大量冗余计算,是让自回归生成切实可行的基础优化之一。

需要注意的是,缓存并不会让每一步 decode 的开销都相同。随着序列增长,新的 Query 需要读取和关注的已缓存 Key 和 Value 越来越多。因此,靠后的 token 比靠前的 token 拥有更大的注意力上下文。缓存所避免的,是仅仅为了重建引擎已经算过的状态,而对所有旧 token 位置重新跑一遍模型。

技术细节:为什么开销随长度的平方增长

如果没有缓存,生成长度为 T 的回复时,第 t 步需要重做 t−1 个 token 的 Key/Value 计算。对整段回复求和:

Σ (t = 1 … T) (t − 1) ≈ T2 ⁄ 2

即二次增长。一段 100 个 token 的回复大约需要 5,000 次这样的重复计算。把回复延长到 1,000 个 token,开销则跃升至约 500,000 次:回复长 10 倍,工作量约多 100 倍。有了缓存,每一步只贡献一次新的 Key/Value 计算,因此总开销随 T 线性增长,而非随其平方增长。

内存权衡

KV cache 通过保留中间结果来节省计算,但保留这些结果要消耗内存。可以把 KV cache 看作模型对整段交互的工作记忆(working memory);随着会话进行,它只会不断增长。具体来说,KV cache 的占用随已缓存的 token 数、transformer 层数、KV 头数、head 维度以及每个存储值的字节数而增长。此外,每个状态无法共享的活跃序列,都会各自单独增长。

存放这些数据最快的地方通常是 GPU 显存。但 GPU 还必须容纳模型权重、临时激活值、通信缓冲区以及其他运行时数据。在高并发或长上下文的工作负载下,KV cache 可能占用大量显存,以至于限制引擎能够同时服务的请求数。

因此,推理引擎会在固定的内存预算内管理缓存。它们可能以块(block)为单位分配缓存,回收已完成请求所占的块,或在其他地方需要空间时驱逐已缓存的状态。一旦某个有用的缓存不再可用(且引擎之外也没有副本),下一个匹配的请求就必须重新执行 prefill,付出全部计算成本。

这就形成了一个基本的权衡:

  • 在稀缺的 GPU 显存中保留更多 KV 数据,以便快速复用。
  • 将其丢弃以腾出空间,等相同的上下文再次出现时再付出代价重新计算。
A diagram illustrating a data retrieval hierarchy, with GPU memory at the top as the fastest and smallest, followed by CPU/host memory, local storage, and remote backend at the bottom as the largest and slowest. Key information about data movement and retrieval efficiency is included at the bottom.

LMCache 为何存在

LMCache 提供了另一种选择:将 KV cache 视为可复用的数据,让它能够存在于当前的 GPU 显存分配之外,并在有益时跨越单个请求或单个推理引擎进程而存在。

从宏观上看,LMCache 可以在一个存储层级(storage hierarchy)中移动 KV 数据,该层级包括 GPU 显存、CPU 内存、本地存储和远程后端(remote backend)。当后续请求复用已缓存的上下文时,服务系统可以取回对应的 KV 状态,而无需通过 prefill 全部重建。这可以减少重复计算、缩短首 token 延迟(time to first token,TTFT),并为更多活跃任务留出 GPU 显存。

取回缓存并非没有代价,也并不总是比重新计算更快。结果取决于多种因素,例如复用了多少上下文、缓存存放在哪里、传输带宽,以及查找是否命中(hit)。因此,系统层面的问题不只是要不要缓存,而是要保留什么、存放在哪里、何时移动,以及如何让需要它的引擎能够用上它。

这些正是 LMCache 旨在回答的问题。下一篇文章将从动机转向架构:LMCache 如何在单个推理请求的生命周期之外存储、传输、协调和提供 KV 数据。

目录

分享方式:

发表回复

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理。

博客更多内容