Transformer 笔记:注意力机制为什么会变成系统问题

从 QKV、掩码和 KV 缓存出发,理解 Transformer 的模型结构为什么会影响推理服务的内存与调度设计。

Engineering notesSGLang runtimeAttentionKV cacheLLM serving

我以前读 Attention Is All You Need,会把注意力放在公式上:

text
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V

现在再读,我更关心它删掉了什么。

Transformer 最关键的变化不只是“用了注意力机制”,而是去掉了序列建模中按时间步传递的递归关系。RNN 的状态像一根链条,h_t 要等 h_{t-1}。Transformer 改成:同一层里,每个 token 可以直接和其它 token 建立关系。

这一步把问题从时间依赖链改成了矩阵计算,也把后来的系统问题埋下了。

flow

这篇论文真正改变的工程形状

训练阶段赢在并行矩阵计算;推理阶段则开始背 KV 状态的账。

  1. 1

    去掉递归依赖

    序列内部不再按隐藏状态串行推进

  2. 2

    关系矩阵

    token 之间的关系变成 QK^T 和 softmax

  3. 3

    适合硬件并行

    核心计算更贴近 GPU 上的大矩阵操作

  4. 4

    状态成本

    自回归推理必须保存每层历史 K/V

从一条链变成一张表

RNN 的直觉很自然:一句话从左到右读,当前状态携带过去信息。

text
x1 -> h1 -> h2 -> h3 -> h4

这条链的问题是,训练时序列内部不好并行,长距离依赖也要经过很多中间状态。

自注意力换了一个问法:如果第 4 个 token 想知道第 1 个 token 是否重要,为什么一定要靠 h2h3 一步步传?为什么不能直接算它们之间的关系?

text
第 i 个 token
  -> 为所有第 j 个 token 计算分数
  -> 按权重汇总信息

这就是注意力公式背后的工程形状。它把“序列中信息怎样流动”改成了“一个 token 如何在同一层里寻址其它 token”。

QKV 像一次寻址过程

QKV 不只是三组投影。更贴近系统直觉的理解是:

text
Query:当前 token 发出的查询
Key:  每个历史 token 暴露的地址或标签
Value:匹配以后读回来的内容

QK^T 像是在计算查询与所有键的匹配程度,softmax 得到读取权重,最后用这组权重汇总对应的值。

这样看,Q 和 K 定义“怎么找”,V 定义“找到了拿什么回来”。这比“每个词看每个词”更准确,因为匹配空间和内容空间被拆开了。

多头注意力把这件事复制多份:不同的头有不同投影空间,可以学到不同的寻址方式。后来的 MQA/GQA 之所以有工程意义,也正是因为 K/V 头数会直接影响 KV 缓存大小。

text
更多 KV 头
  -> 每个 token 产生更多 K/V 张量
  -> KV 缓存更大
  -> 推理服务承受更高的内存压力

这就是模型结构与推理系统相接的地方。

掩码和位置编码承担不同职责

自注意力给 token 之间开了直接通道,但这条通道必须受到约束。

第一个约束是掩码。

机器翻译中的解码器掩码自注意力,以及今天仅解码器大模型中的因果注意力,本质上都遵守同一条规则:生成当前位置时不能看未来。

text
第 t 个 token 可以关注:
  位置不晚于 t 的 token

第 t 个 token 不能关注:
  位置晚于 t 的 token

这条规则会一路传到 KV 缓存。缓存里的 K/V 代表“已经出现过的 token”。前缀复用、缓存命中和远端 KV 回载都不能破坏这个顺序。

第二个约束是位置信息。

去掉递归关系后,注意力机制本身不知道 token 的先后。原论文使用正弦位置编码,今天常见 RoPE、ALiBi 等变体,但它们都在补同一件事:让模型知道 token 在序列中的相对或绝对位置。

在长上下文系统里,位置信息不是小细节。复用一段 KV,不只是问 token 内容是否相同,还要问这段 K/V 在当前上下文位置下是否仍然有效。

训练阶段省掉的链,推理阶段变成了缓存

训练时,整段序列可以一起进模型,所有 token 的 Q/K/V 在每层批量算出来。

推理时不一样。自回归生成每吐一个新 token,都要看前面的历史。如果每一步都重算完整 prompt 和已生成内容,成本会不可接受。

所以系统保存历史 token 的 K/V:

text
预填充:
  提示词 token
  -> 计算每一层的 K/V
  -> 保存 KV 缓存

解码:
  新 token
  -> 计算当前 Q/K/V
  -> Q 读取缓存中的历史 K/V
  -> 追加新的 K/V

KV 缓存不是一个附加优化,而是仅解码器大模型推理的基本状态。

它的大小随层数、序列长度、KV 头数、单头维度和数据类型增长:

text
KV 缓存大小
  ~= 层数
   * 序列长度
   * KV 头数
   * 单头维度
   * 2
   * 每个数据元素的字节数

于是注意力机制从论文里的计算结构,变成了推理引擎中最主要的内存压力之一。

FlashAttention 和 PagedAttention 不是同一种答案

沿着注意力机制继续看,经常会遇到两个名字:FlashAttention 和 PagedAttention。它们都围绕注意力展开,但解决的问题不同。

FlashAttention 更靠近计算过程。它关心注意力内核怎样减少 HBM 读写、怎样避免完整展开注意力矩阵,以及怎样提高预填充和训练效率。

PagedAttention 更靠近推理状态管理。它关心 KV 缓存怎样分块管理、怎样避免预留连续显存,以及怎样支持共享前缀和写时复制。

text
FlashAttention:
  优化注意力计算
  减少计算分数和汇总结果时的内存传输

PagedAttention:
  优化 KV 缓存的存放方式
  减少推理过程中的碎片和过量预留

如果把两者都笼统地称为注意力优化,很容易错过它们的层级差异。

evidence boundary

同一种注意力机制,两个系统方向

计算路径

  • QK^T / softmax / AV 的计算内核如何组织
  • 是否减少 HBM 数据传输
  • 预填充和训练是否更快

状态路径

  • 历史 K/V 如何保存
  • 不同请求如何共享或释放 KV
  • 调度器是否了解缓存成本

门打开以后

Attention Is All You Need 不只是用注意力机制替换 RNN。它把序列关系变成了可并行计算的矩阵,也让自回归推理不得不长期保存 K/V 状态。

这就是为什么这篇论文会一路连到 KV 缓存、PagedAttention、FlashAttention、前缀缓存、HiCache 和远端 KV 存储。注意力成为主干以后,K/V 就不再只是中间张量,而是系统资源。