问题背景
大语言模型(LLM)在执行推理任务时,需完成海量矩阵运算,其中 Self-Attention 机制成为最核心的计算瓶颈。每当生成一个新 token,模型都必须重新计算所有历史 token 的 Key(K)与 Value(V)向量,这种重复性计算造成了严重的算力浪费。若要深入理解 LLM 推理延迟、显存消耗及定价策略,就必须系统掌握 Token 化原理、推理阶段划分、Attention 计算方式、KV Cache 以及 Prompt Cache 缓存机制。
核心概念
Token 与 Tokenizer
LLM 无法直接理解原始文本,必须借助 Tokenizer 将文本转化为模型可识别的数字序列。每个数字对应一个 Token,而 Token 是模型所能处理的最小语义单元。目前主流方案为 BPE(Byte Pair Encoding,字节对编码),其核心流程是:从单个字符出发,反复合并出现频率最高的相邻字符对,逐步构建出一个固定大小的词表(例如 GPT-4 的词表规模约为 100,000 个 Token)。
示例:输入文本“我喜欢猫”,Tokenizer 处理后可能得到:
"我" → Token ID: 101
"喜欢" → Token ID: 202
"猫" → Token ID: 303
最终: [101, 202, 303]
规律:
- 高频词通常整体编码为 1 个 Token(如英文
the、Hello)。 - 罕见词则会被拆分为多个子词(例如
Langfuse→Lang+f+use,共占用 3 个 Token)。 - 中文场景下,每个汉字通常对应 1 到 2 个 Token。
LLM 推理的两个阶段
一次完整的 LLM 调用过程可划分为 Prefill 阶段与 Decode 阶段。
- Prefill 阶段:该阶段并行处理全部输入 Token,一次性计算出所有 Token 的 Q、K、V,并生成 KV Cache。此阶段吞吐量高,具备良好的并行计算能力。
- Decode 阶段:该阶段逐个生成输出 Token,每一步仅处理当前新 Token 的 Q、K、V,并复用之前缓存的 KV 数据,采用串行方式执行,每次仅生成 1 个 Token。
工作原理
Self-Attention 机制
Attention 要解决的核心问题是:“在生成下一个词时,模型应该重点关注前面哪些词?” 以句子“我喜欢猫,它们很___”为例,当模型生成“___”时,需要优先关注“猫”和“它们”,而非“我”或“喜欢”。
每个 Token 会被转换为三个向量:
| 向量 | 含义 | 类比 |
|---|---|---|
| Q(Query) | “我在寻找什么信息” | 提问者 |
| K(Key) | “我能提供什么信息” | 标签或索引 |
| V(Value) | “我的实际内容” | 答案内容 |
计算公式:
Attention(Q, K, V) = softmax(Q × K^T / √d) × V
Q × K^T:衡量每个 Token 与所有其他 Token 之间的相关度得分。/ √d:缩放操作,防止数值过大导致梯度问题。softmax:将得分归一化为概率分布(所有权重之和为 1)。× V:利用权重对 Value 进行加权求和,得到最终的输出表示。
具体 Demo:极简模型计算过程
假设一个极简模型:仅包含 1 层、1 个注意力头,维度 d=4。输入 3 个 Token:["我", "喜欢", "猫"]。
Step 1:Embedding。每个 Token ID 通过查表获得 4 维向量:
"我" → e₁ = [1.0, 0.0, 0.5, 0.3]
"喜欢" → e₂ = [0.2, 1.0, 0.1, 0.8]
"猫" → e₃ = [0.6, 0.3, 1.0, 0.1]
Step 2:通过权重矩阵计算 Q、K、V。模型包含三个可学习的权重矩阵 W_Q、W_K、W_V(训练阶段学习得到,推理阶段固定不变)。假设 W_K 为 4×4 矩阵:
假设 W_K(4×4 矩阵):
┌ ┐
│ 0.1 0.2 0.0 0.3 │
│ 0.4 0.1 0.2 0.0 │
│ 0.0 0.3 0.1 0.2 │
│ 0.2 0.0 0.4 0.1 │
└ ┘
K₁ = e₁ × W_K = [1.0, 0.0, 0.5, 0.3] × W_K = [0.16, 0.35, 0.17, 0.43]
同理可算出所有 K 和 V:
K₁ = [0.16, 0.35, 0.17, 0.43] ← "我" 的 Key
K₂ = [0.30, 0.15, 0.38, 0.14] ← "喜欢" 的 Key
K₃ = [0.12, 0.45, 0.13, 0.38] ← "猫" 的 Key
V₁ = [0.50, 0.20, 0.10, 0.40] ← "我" 的 Value
V₂ = [0.30, 0.60, 0.40, 0.10] ← "喜欢" 的 Value
V₃ = [0.70, 0.10, 0.80, 0.30] ← "猫" 的 Value
这些 K 和 V 正是需要缓存的关键数据,因为在后续生成新 Token 的过程中,它们会被反复调用。
Step 3:计算 Attention(以最后一个 Token “猫”为例):
Q₃ = e₃ × W_Q = [0.25, 0.40, 0.18, 0.55]("猫" 的 Query)
# "猫" 对每个 Token 的关注度 = Q₃ · Kᵢ(点积)
score₁ = Q₃ · K₁ = 0.25×0.16 + 0.40×0.35 + 0.18×0.17 + 0.55×0.43 = 0.45
score₂ = Q₃ · K₂ = 0.25×0.30 + 0.40×0.15 + 0.18×0.38 + 0.55×0.14 = 0.28
score₃ = Q₃ · K₃ = 0.25×0.12 + 0.40×0.45 + 0.18×0.13 + 0.55×0.38 = 0.44
# 缩放 + softmax
scaled = [0.45/√4, 0.28/√4, 0.44/√4] = [0.225, 0.14, 0.22]
weights = softmax([0.225, 0.14, 0.22]) ≈ [0.36, 0.29, 0.35]
# 加权求和 Value
output₃ = 0.36×V₁ + 0.29×V₂ + 0.35×V₃ = [0.51, 0.28, 0.43, 0.28]
该 output₃ 会继续传递至 FFN(前馈神经网络)并最终经过 softmax,输出下一个 Token 的概率分布。
执行流程
KV Cache:单次请求内的缓存
问题:无 KV Cache 时的重复计算。在 Decode 阶段,每生成一个 Token 都需要对所有历史 Token 执行 Attention 操作。若不引入缓存机制,生成第 N 个 Token 时,必须重新计算从输入到第 N-1 个所有 Token 的 K、V,这会导致大量重复计算,严重浪费算力。
解决方案:存储 K 和 V。Prefill 阶段并行计算出所有输入 Token 的 K、V,并将其存入 KV Cache。在 Decode 阶段,每一步仅需计算新 Token 的 Q、K、V,然后使用 Q 与缓存中所有 K 进行点积运算,得到 Attention 输出,同时将新 Token 的 K、V 追加到缓存中。
逐步生成过程示例:
Prefill 完成后 KV Cache 状态:
┌─ KV Cache ────────────────────────────────────────┐
│ 位置 0 ("我"): K₁=[0.16,0.35,0.17,0.43] V₁=[0.50,0.20,0.10,0.40] │
│ 位置 1 ("喜欢"): K₂=[0.30,0.15,0.38,0.14] V₂=[0.30,0.60,0.40,0.10] │
│ 位置 2 ("猫"): K₃=[0.12,0.45,0.13,0.38] V₃=[0.70,0.10,0.80,0.30] │
└───────────────────────────────────────────────────┘
生成第 1 个 output Token(假设为“,”):
"," 的 embedding → 算 Q_new、K_new、V_new(1 次矩阵乘法)
Q_new 和 Cache 中的 [K₁, K₂, K₃] 做 Attention → 得到 output 向量 → FFN → softmax → 采样 → ","
Cache 追加 K₄、V₄:
┌─ KV Cache ────────────────────────────────────────┐
│ 位置 0 ("我"): K₁V₁ │
│ 位置 1 ("喜欢"): K₂V₂ │
│ 位置 2 ("猫"): K₃V₃ │
│ 位置 3 (","): K₄V₄ ← 新增 │
└───────────────────────────────────────────────────┘
生成第 2 个 output Token(假设为“它们”):
"它们" 的 embedding → 算 Q_new、K_new、V_new
Q_new 和 Cache 中的 [K₁, K₂, K₃, K₄] 做 Attention → 得到 output → "它们"
Cache 再追加:
┌─ KV Cache ────────────────────────────────────────┐
│ 位置 0 ("我"): K₁V₁ │
│ 位置 1 ("喜欢"): K₂V₂ │
│ 位置 2 ("猫"): K₃V₃ │
│ 位置 3 (","): K₄V₄ │
│ 位置 4 ("它们"): K₅V₅ ← 新增 │
└───────────────────────────────────────────────────┘
KV Cache 的大小
实际生产环境中,KV Cache 的显存占用非常可观:
KV Cache 大小 = 2(K和V) × 层数 × 注意力头数 × 头维度 × 序列长度 × 精度
以 Llama 3 70B 为例:
= 2 × 80层 × 8头(GQA) × 128维 × 8192长度 × 2字节(FP16) ≈ 2.6 GB(单个请求)
这也解释了为何 GPU 显存通常比算力更早成为瓶颈。
计算量对比
| 步骤 | 无 KV Cache | 有 KV Cache | 节省比例 |
|---|---|---|---|
| Decode 第 1 步 | 计算 3 个 Token 的 K、V | 只计算 1 个 | 节省 67% |
| Decode 第 10 步 | 计算 12 个 Token 的 K、V | 只计算 1 个 | 节省 92% |
| Decode 第 100 步 | 计算 102 个 Token 的 K、V | 只计算 1 个 | 节省 99% |
参数作用
KV Cache 的 Key 由 Token 的位置索引决定,它缓存的是每一层、每个注意力头的 K 和 V 矩阵。而 Prompt Cache 的 Key 则由 Token 序列的哈希值决定,用于实现跨请求的复用。
示例说明
Prompt Cache:跨请求的缓存
问题:多轮对话中的重复计算。KV Cache 仅在单次请求生命周期内有效,请求结束后即被释放。在多轮对话场景中,每一轮都需要重新发送完整的上下文,导致 Prefill 阶段反复计算相同前缀的 KV,造成大量冗余计算。
解决方案:跨请求复用 KV Cache。服务端将 Prefill 阶段计算出的 KV Cache 持久化存储,当后续请求的前缀相同时,可直接复用已有缓存。具体实现时,通过分块哈希机制支持最长前缀匹配:
假设 block_size = 4(实际场景中通常为 64 或 128)
Token 序列: [101, 202, 303, 404, 505, 601, 602, 603]
Block 0: hash([101, 202, 303, 404]) = "a1b2c3"
Block 1: hash([505, 601, 602, 603]) = "d4e5f6"
缓存结构(类似 Radix Tree):
┌──────────────────────────────────┐
│ "a1b2c3" → KV Cache (位置 0~3) │
│ "a1b2c3:d4e5f6" → KV Cache (0~7) │
└──────────────────────────────────┘
新请求到达时,逐块计算哈希并进行匹配:
新请求 Token: [101, 202, 303, 404, 505, 601, 999, 888]
Block 0: hash([101, 202, 303, 404]) = "a1b2c3" → ✅ 命中
Block 1: hash([505, 601, 999, 888]) = "x7y8z9" → ❌ 不匹配
结果: 复用 Block 0 的 KV Cache(前 4 个 Token),Block 1 需要重新计算。
缓存命中的条件
✅ 命中条件:
- 严格前缀匹配(从第一个 Token 开始,必须连续一致)
- 达到最小缓存粒度(Anthropic 要求 ≥ 1024 tokens,OpenAI 要求 ≥ 128 tokens)
❌ 不命中的情况:
- 中间任何一个 Token 不同 → 从该位置开始,后续全部失效
- 相同的文本但 Token 化方式不同(几乎不会发生)
- 缓存过期(TTL 通常设置为 5 到 10 分钟)
多轮对话完整 Demo
═══ 第 1 轮 ════════════════════════════════════════════════
发送: [System: "你是一个AI助手,请用中文回答问题。"] ← 50 tokens
[User: "什么是机器学习?"] ← 10 tokens
处理: 前缀匹配: 无缓存,全部 60 tokens 做 Prefill
缓存写入: hash(前60 tokens) → 存入缓存池
计费: Input: 60 tokens × 全价; Output: 200 tokens × 全价
═══ 第 2 轮 ════════════════════════════════════════════════
发送: [System: 同上] ← 50 tokens (同上)
[User: "什么是机器学习?"] ← 10 tokens (同上)
[Assistant: "机器学习是...(200 tokens)"] ← 200 tokens (新增)
[User: "能举个例子吗?"] ← 8 tokens (新增)
处理: 前缀匹配: 前60 tokens 命中缓存 ✅
→ 加载 KV Cache(几乎零 GPU 计算)
→ 只对后 208 tokens 做 Prefill
计费: Input (cached): 60 tokens × 0.1 倍价格
Input (uncached): 208 tokens × 全价
Output: 150 tokens × 全价
═══ 第 3 轮 ════════════════════════════════════════════════
发送: [前面所有内容,共468 tokens] ← 大部分和第2轮重复
[User: "还有什么应用?"] ← 8 tokens
处理: 前缀匹配: 前268 tokens 命中缓存 ✅(第2轮时已缓存)
→ 只对后 208 tokens 做 Prefill
计费: Input (cached): 268 tokens × 0.1 倍
Input (uncached): 208 tokens × 全价
Output: 180 tokens × 全价
费用模型
推理费用 = GPU 计算时间 × GPU 单价。三种 Token 的 GPU 消耗对比如下:
| Token 类型 | GPU 执行内容 | 算力消耗 | 定价策略 |
|---|---|---|---|
| Input (uncached) | 完整 Prefill:计算 Q、K、V,执行 Attention | 中等(可并行,效率较高) | 基准价 |
| Input (cached) | 从显存或内存中加载 KV Cache | 极低(主要为数据搬运) | 基准价 × 0.1~0.5 |
| Output | 完整 Decode:每个 token 执行一遍全模型 | 高(串行执行,GPU 利用率低) | 基准价 × 3~5 |
以 Claude Sonnet 4 为例:
Input: $3 / 1M tokens
Input (cached): $0.3 / 1M tokens ← 1 折
Output: $15 / 1M tokens ← 5 倍于 input
Output 价格更高的原因在于:Decode 阶段采用串行方式,GPU 利用率较低,且内存带宽瓶颈导致大部分时间耗费在数据搬运上。
优势与限制
- KV Cache 优势:大幅减少 Decode 阶段的重复计算,可节省 90% 以上的算力,有效降低推理延迟。
- KV Cache 限制:仅在同一请求内有效,请求结束后即被丢弃;显存占用随序列长度线性增长,长序列场景下容易成为瓶颈。
- Prompt Cache 优势:支持跨请求复用,显著减少 Prefill 计算量,降低推理成本与延迟;特别适用于多轮对话和固定 System Prompt 的场景。
- Prompt Cache 限制:需要严格的前缀匹配,动态内容可能导致缓存失效;缓存命中需满足最小粒度要求,且存在 TTL 过期限制。
常见误区
- 认为 Output Token 本身复杂度更高导致价格更贵。实际上,Output Token 价格高是因为 Decode 阶段串行执行且 GPU 利用率偏低,而非 Token 本身更复杂。
- 认为 Prompt Cache 缓存的是整个 Prompt 的文本。实际上,缓存的是 Token 序列对应的 K、V 矩阵,而非文本本身。
- 认为缓存命中条件较为宽松。实际要求严格的前缀匹配,动态内容(如时间戳)会破坏缓存,导致完全失效。
适用场景
- KV Cache 适用于所有 LLM 推理场景,属于标准实现。
- Prompt Cache 特别适用于:多轮对话应用、带有固定 System Prompt 的服务、Agent 中 Tool 定义固定的场景。在这些场景下,通过合理设计消息结构(将静态内容前置、动态内容后置)可最大化缓存命中率。
内容总结
LLM 推理中的缓存机制可划分为两个层级:
| 缓存类型 | 缓存内容 | Key 类型 | 作用范围 | 节省的计算量 |
|---|---|---|---|---|
| KV Cache | 每层的 K、V 矩阵 | Token 的位置索引 | 单次请求内 | Decode 阶段避免重算历史 Token |
| Prompt Cache | 整段前缀的 KV Cache | Token 序列的哈希值 | 跨请求 | Prefill 阶段避免重算相同前缀 |
深入理解这两层缓存机制,即可清晰把握 LLM 推理延迟、显存占用以及定价逻辑的根本成因。
