自回归生成与 KV Cache
模型一次只生成一个 token,再把它拼回输入继续算;一次请求分成 prefill 与 decode 两个阶段,瓶颈完全不同。KV Cache 缓存的是已算过的注意力键值,省的是重复计算而不是首次计算 —— 它的显存占用随序列长度与并发线性增长,是长上下文成本的真正来源。
也叫:自回归 · autoregressive · KV Cache · KV 缓存 · prefill · decode · TTFT · 首 token 延迟
原理拆解出自 T0-1
| 对照项 | 没有 KV Cache | 有 KV Cache |
|---|---|---|
| 每生成一个 token | 整段前文重算一遍 | 只算新 token 这一列 |
| 注意力代价 | O(n²),n 每轮增长 | 每步 O(n) |
prefill 那一次 | 要算 | 同样省不掉 |
窗口是「第一约束」,因为三层成本叠在同一维度上:显存 —— KV Cache 大小大致正比于序列长度,还要乘以并发数;计费 —— 多轮每轮重发全部历史,成本是累加;有效性 —— 窗口大 ≠ 用得上,token 数上升,准确召回的能力会下降(context rot,T1-3)。
幻觉的根源在这里就能讲清一半:模型每一步只是在词表上给出下一个 token 的概率分布再采样,训练目标从来不是「说真话」,是「像语料那样接下去」。上下文缺答案时,「我不知道」并不是天然高概率的续写。应用层能改分布的条件,改不了目标函数。
prefill 那一次省不掉 —— KV Cache 省的是重复计算,不是首次计算,这是追问最爱落的一点。顺手记住另一句:打字机效果不是前端做的动画,是生成机制本身长这样。一次请求分成两个阶段,它们的瓶颈完全不同——这是本篇最有区分度的一段:
Prefill(预填充) Decode(解码)
输入 1200 token 一次性并行算完 → 每次只算 1 个新 token
产出:每层每个位置的 K/V ↖ 复用缓存里的 K/V
│ │
▼ ▼
决定首 token 延迟 TTFT 决定吐字速度 TPS
算力密集(compute-bound) 显存带宽密集(memory-bound)对照一下有无 KV Cache 的差别:
# 没有 KV Cache:每生成一个 token,整段前文重算一遍
tokens = prompt_tokens
for _ in range(max_new):
logits = model(tokens) # n 每轮增长,注意力代价 O(n²)
tokens.append(sample(logits[-1]))
# 有 KV Cache:前文的 K/V 存着,只算新 token 这一列
logits, cache = model(prompt_tokens, cache=None) # prefill,这一次省不掉
for _ in range(max_new):
nxt = sample(logits[-1])
logits, cache = model([nxt], cache=cache) # decode,每步 O(n)注意 prefill 那一次是省不掉的——KV Cache 省的是重复计算,不是首次计算。这一点是后面追问的高频落点。
为什么说上下文窗口是「第一约束」——因为三层成本叠在同一个维度上:
- 显存。KV Cache 的大小大致正比于
2(K 和 V)× 层数 × KV 头数 × 头维度 × 序列长度 × 精度字节,再乘以并发数。序列长度是线性项,并发是乘数,所以长上下文服务的真正天花板往往不是算力,是显存装不下多少并发。 - 计费。多轮对话每一轮都要把全部历史重新发一遍(下一篇会讲清为什么),成本随轮次是累加,不是常数。
- 有效性。窗口大 ≠ 用得上。官方口径是把上下文当成一份注意力预算:token 数上升时,模型从中准确召回信息的能力会下降,各家模型只是衰减快慢不同。这就是 context rot,T1-3 专门展开。
幻觉的根源,在这里就能讲清一半。 模型每一步在做的事,是在词表上给出下一个 token 的概率分布,然后从中采样。它的训练目标从来不是「说真话」,而是「像语料那样接下去」。当上下文里缺少答案时,「我不知道」并不是一个天然高概率的续写——除非后训练教过它、或者你的提示词与工具给了它这个出口。所以幻觉不是 bug,是这套目标函数在信息缺失时的默认行为。应用层能改的是分布的条件(给证据、给出口、给校验),改不了的是目标函数本身。
以上节选自T0-1 Token、上下文窗口与自回归生成:三个约束的同一个源头,读全文能看到前后语境。