惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
腾讯CDC
G
Google Developers Blog
Martin Fowler
Martin Fowler
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
爱范儿
爱范儿
Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
美团技术团队
WordPress大学
WordPress大学
阮一峰的网络日志
阮一峰的网络日志

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解
深度学习进阶(二十八)现代 LLM 的核心架构设计其三:Decode...
哥布林学者 · 2026-06-08 · via 博客园_首页

上一篇我们介绍了 SwiGLU,通过引入门控机制让 FFN 能够根据输入动态筛选信息,取代了标准 Transformer 沿用多年的单通路结构。

前两篇的内容都关于结构上的优化,本篇则关于一个核心工程优化。
我们知道,即使是现在的多数大模型,其生成回答的逻辑仍然是自回归生成,即逐个字往外蹦。
因此,为了让 AI 能够真实应用,而不至于 “一个问题等一天” ,就出现了一系列的工程的优化。
其中的基石就是 KV Cache

1. Decoder-Only

18 年,OpenAI 提出了 GPT-1 :Improving Language Understanding by Generative Pre-Training,其核心内容就是现在的 Decoder-Only 架构,这是目前绝大多数通用大语言模型采用的基本架构。

有了之前的基础,Decoder-Only 的逻辑并不难理解:
研究人员发现,对于语言建模任务而言,并不一定需要单独的 Encoder。只要让模型根据已有文本不断预测下一个 Token,就能够学习语言规律。

因此现代大模型直接移除了 Encoder,同时删除了依赖 Encoder 输出的 Cross Attention,最终仅保留 Decoder 中的 Masked Self-Attention 与 FFN 结构:

image.png

可以看到原始的 GPT-1 基本逻辑仍然是在预训练的模型基础上,继续用特定数据训练,让模型适应某个任务或领域,也就是 Fine-tuning,而这种基础的任务能力现在已经可以靠 prompt 来激活了。
因此,了解 Decoder-Only 架构本身的相关逻辑即可,这里展开两个细节:

1.1 用户输入是如何作用的?

既然没有 Encoder,一个自然的问题是:

用户输入由谁来理解?

答案是:

用户输入本身就是 Decoder 的输入序列。

我们直接举个例子,用户提问:

法国的首都是哪里?

这句话会直接被转换为 Token 序列:

法国 的 首都 是 哪里 ?

随后送入 Decoder。模型经过一次前向传播后,预测:

巴黎

此时序列变成:

法国的首都是哪里?巴黎

之后同理,继续生成直到生成结束标记:

法国的首都是哪里?巴黎。它位于……

这种根据已经出现的内容预测下一个 Token 的过程就是自回归生成(Autoregressive Generation)

1.2 Attention 中的重复计算问题

明白了 Decoder-Only 的生成逻辑后,现在我们来看一个推理时的问题

\(t\) 步时,模型需要计算 \(t\) 个 token 之间的自注意力,然后预测第 \(t+1\) 个 token。然后第 \(t+1\) 步,输入变成 \(t+1\) 个 token,再算一遍注意力,预测第 \(t+2\) 个。

现在,假设模型已经生成到了第 \(t\) 个 token,第 \(t\) 步计算了 token \(1\)\(t\) 之间所有注意力。到了第 \(t+1\) 步,输入变成了 token \(1\)\(t+1\),标准做法会把 token \(1\)\(t\) 的 K 和 V 重新计算一遍

问题就出在这个"再算一遍"上:

在第 \(t\) 步和 \(t+1\) 步之间,token \(1\)\(t\) 的 K 和 V 变了吗?

答案是没有。
在参数固定的推理阶段,token \(1\) 在第一步后就固定了。token \(t\) 的内容同理在第 \(t\) 步生成后就已经固定了。它们的 K 和 V 不会因为一个新 token 的加入而改变。每次重新计算都是纯粹的浪费。

显然,这里存在相当大的优化空间,这便是 KV Cache 要解决的问题。

21c1e846-9a9b-4f44-84c4-c8e2e6998ed3.png

2. KV Cache

2.1 KV Cache 的内容

KV Cache 的思路很简单:

在第 \(t\) 步算完 \(K_t\)\(V_t\) 后,把它们存起来。第 \(t+1\) 步只需要计算 \(K_{t+1}\)\(V_{t+1}\),然后把它们追加到缓存中。所有之前 token 的 K 和 V 直接从缓存读取。

就像这样:

步骤 计算内容 缓存
生成 token 1 \(K_1, V_1\),存起来 \([K_1, V_1]\)
生成 token 2 只算 \(K_2, V_2\),追加到缓存 \([K_{1,2}, V_{1,2}]\)
生成 token 3 只算 \(K_3, V_3\),追加到缓存 \([K_{1,3}, V_{1,3}]\)
每步只算一个 \((K_t, V_t)\) 缓存逐步增长

于是,原本的每一步的注意力计算是这样:

\[\text{Attention} = \text{softmax}\left(\frac{q_{new} \cdot K_{1:t}^T}{\sqrt{d}}\right)V_{1:t} \]

现在加入 KV Cache 就变成了这样:

\[\text{Attention} = \text{softmax}\left(\frac{q_{new} \cdot [K_{\text{cache}}; k_t]^T}{\sqrt{d}}\right)[V_{\text{cache}}; v_t] \]

其中: \(K_{\text{cache}} = [k_1, k_2, \dots, k_{t-1}]\)\(V_{\text{cache}} = [v_1, v_2, \dots, v_{t-1}]\) 是缓存的历史。只有 \(q_t, k_t, v_t\) 是当前步需要计算的。

很显然,通过 KV Cache 我们消灭了计算 KV 投影时的无用功。在长文本生成时,其带来的性能差距是数量级的。

2.2 为什么 Q 不需要缓存?

既然 K 和 V 都被缓存了,一个可能的问题是:

为什么不把 Q 也缓存起来复用?

区别在于它们的角色不同:
在自回归生成的每一步,当前 token 的 Q 作用是查询历史信息\(q_t\) 与所有历史 K 计算注意力分数,然后从所有历史 V 中聚合信息来预测下一个 token。

一旦这一步的预测完成,\(q_t\) 的历史使命就结束了。
未来步的 \(q_{t+1}, q_{t+2}, \dots\) 只关心它们自己与历史的匹配,不再需要回头看 \(q_t\)

\(k_t\)\(v_t\) 不一样。它们在第 \(t\) 步生成后,仍然会被未来的所有 token 查询
只要未来某一刻的 \(q_s\)\(s > t\))需要与第 \(t\) 步的信息做注意力计算,\(k_t\)\(v_t\) 就必须一直存在。

所以总结来说就是:Q 是查完即弃,无需保留。

2.3 KV Cache 的内存开销

KV Cache 不是免费的午餐,它的本质就是典型的空间换时间

在推理过程中,我们将历史 token 的 K 和 V 保存在显存中,以避免后续步骤重复计算。计算省下来了,但这些缓存需要一直保留到生成结束。

对于传统 Multi-Head Attention,每一层 KV Cache 的大小近似为:

\[\text{Memory}_{\mathrm{layer}} = 2 \times \mathrm{batch\_size} \times \mathrm{seq\_len} \times d_{\mathrm{model}} \times \mathrm{dtype\_bytes} \]

其中:

  1. 2:表示 K 和 V 各保存一份;
  2. batch_size:批次大小,推理中为并行请求数。
  3. seq_len:当前缓存中的累计 Token 数量。
  4. \(d_{\text{model}}\):隐藏层维度。
  5. dtype_bytes:数据精度占用字节数(FP32 为 4,FP16/BF16 为 2)。

很显然,这并不是一笔小开支。举个简单的例子,假设:

参数 取值
\(d_{\text{model}}\) 8192
Transformer 层数 80
上下文长度 4096
精度 BF16(2 Byte)

那么单层 KV Cache 大约需要:

\[2 \times 4096 \times 8192 \times 2 \approx 134\text{ MB} \]

80 层累计下来就是:

\[134 \times 80 \approx 10.7\text{ GB} \]

也就是说,一个拥有数千 token 上下文的大模型会话,仅 KV Cache 就可能占据十 GB 量级的显存。

如果进一步进行批量推理:\(\text{batch_size} = 64\),那么 KV Cache 占用还会近似放大 64 倍。
因此,KV Cache 虽然解决了重复计算的问题,却把压力从计算转移到了内存上。

于是这种内存压力,又反过来驱动了注意力结构的变革和一系列配套工程优化,这便是之后的内容了。