Hang Zhengyang
English ↗

推理机制(概念版):为什么要 Prefill/Decode、KV Cache 到底省了什么

1. 先回答核心问题:推理在做什么

推理不是一次性“算出整段答案”,而是循环做同一件事:

  1. 根据当前上下文得到下一 token 概率;
  2. 采样/选择一个 token;
  3. 把它拼回上下文,继续下一轮。

这就是自回归生成。
所以推理效率的关键是:如何避免每一步都重算整段历史。

2. 为什么工业界都用“两阶段推理”

本项目接口:

  • prefill(token_ids, hidden_out)
  • decode(token_id, hidden_out)

概念上:

  • Prefill:把已有 prompt “吃进去”,建立历史上下文表示;
  • Decode:每次只增量处理 1 个新 token。

这样拆分的好处是,重活(处理长 prompt)只做一次,后面走轻量增量路径。

3. KV Cache 解决了什么重复计算

3.1 没有 cache 时在浪费什么

如果没有 cache,第 t 步生成时会重新计算前 1…t 全部位置的 K/V,
本质是把历史重复算了很多遍。

3.2 cache 存了什么

对每一层,缓存历史:

K1:t−1,V1:t−1

新 token 到来只算当前步:

Qt,Kt,Vt

然后做:

softmax(QtK1:t⊤Dh)V1:t

解释:

  • 查询只有当前步的 Qt;
  • 被查询的是“历史+当前”拼起来的 K,V;
  • 历史部分来自 cache,不再重复前向。

这就是 KV cache 的本质:把“重复算历史”变成“复用历史”。

4. 复杂度直觉:为什么 decode 从“重”变“轻”

设总长度为 T。

  • 无 cache:每步都重算历史,整体更接近二次增长。
  • 有 cache:decode 每步主要是“当前 query 对历史 KV”,单步是线性于历史长度。

张量规模直观看:

  • decode 单步 logits 约是 H×1×t
  • prefill 全段 logits 约是 H×t×t

所以长文本生成时,cache 的收益会越来越明显。

5. KV Cache 的内存成本与 GQA 的关系

每层近似存两份张量(K 和 V):

floats per layer≈2·T·Hkv·Dh

全模型:

floats total≈L·2·T·Hkv·Dh

这说明两点:

  1. cache 省的是算力和带宽,不是“不要内存”;
  2. 减小 Hkv(GQA)会直接降低 cache 内存。

因此 GQA 在工程里很重要:它在质量和推理成本之间做平衡。

6. 采样不是“随便随机”:温度在调什么

给定 logits z,温度采样:

pi=exp(zi/Ttemp)∑jexp(zj/Ttemp)

概念解释:

  • Ttemp<1:分布更尖锐,更偏向高分词(更“稳”、更像贪心)。
  • Ttemp>1:分布更平坦,随机性更强(多样性更高)。

所以温度本质上是“探索-确定性”旋钮,而不是模型能力本身。

6.1 数值稳定为什么必须做

常见稳定写法:

softmax(z)i=ezi−zmax∑jezj−zmax

减去 zmax 不改变结果分布,但能防止 exp 溢出。
这是推理实现里最基础也最容易忽略的稳定性细节。

7. 为什么 prefill 后用“最后一行 hidden”启动生成

prompt 的最后一个 token(如 [QA])对应“当前已知完整上下文”。
它的 hidden 就是“模型看完整段输入后的状态摘要”。

因此第一步生成直接从这行 hidden 投影到词表采样,完全符合 next-token 预测定义。

8. 本项目里的代码路径(按流程看)

  • cache 数据结构:src/engine/kv_cache.*
  • 模型层桥接:CacheBridge / CacheView(src/model/model_types.h)
  • 前向执行与 cache 读写:src/model/executor_forward.cpp
  • 推理编排入口:src/model/mini_llm.cpp
  • 应用层循环采样:inference.cpp

建议按“调用链”读:

  1. inference.cpp 看外层生成循环;
  2. mini_llm.cpp 看 prefill/decode API 如何组织;
  3. executor_forward.cpp 看每层如何使用 cache。

9. 最常见的推理错误(带原因)

  1. prefill 后 cache 状态没重置:导致会话串味。
  2. 位置索引与 cache 长度不同步:RoPE 与 attention 对齐错位。
  3. 温度/seed 不固定:结果不可复现,难以定位回归。
  4. vocab 不匹配:输出投影维度和 tokenizer 词表不一致,结果异常。