推理机制(概念版):为什么要 Prefill/Decode、KV Cache 到底省了什么
1. 先回答核心问题:推理在做什么
推理不是一次性“算出整段答案”,而是循环做同一件事:
- 根据当前上下文得到下一 token 概率;
- 采样/选择一个 token;
- 把它拼回上下文,继续下一轮。
这就是自回归生成。
所以推理效率的关键是:如何避免每一步都重算整段历史。
2. 为什么工业界都用“两阶段推理”
本项目接口:
prefill(token_ids, hidden_out)decode(token_id, hidden_out)
概念上:
- Prefill:把已有 prompt “吃进去”,建立历史上下文表示;
- Decode:每次只增量处理 1 个新 token。
这样拆分的好处是,重活(处理长 prompt)只做一次,后面走轻量增量路径。
3. KV Cache 解决了什么重复计算
3.1 没有 cache 时在浪费什么
如果没有 cache,第 步生成时会重新计算前 全部位置的 K/V,
本质是把历史重复算了很多遍。
3.2 cache 存了什么
对每一层,缓存历史:
新 token 到来只算当前步:
然后做:
解释:
- 查询只有当前步的 ;
- 被查询的是“历史+当前”拼起来的 ;
- 历史部分来自 cache,不再重复前向。
这就是 KV cache 的本质:把“重复算历史”变成“复用历史”。
4. 复杂度直觉:为什么 decode 从“重”变“轻”
设总长度为 。
- 无 cache:每步都重算历史,整体更接近二次增长。
- 有 cache:decode 每步主要是“当前 query 对历史 KV”,单步是线性于历史长度。
张量规模直观看:
- decode 单步 logits 约是
- prefill 全段 logits 约是
所以长文本生成时,cache 的收益会越来越明显。
5. KV Cache 的内存成本与 GQA 的关系
每层近似存两份张量(K 和 V):
全模型:
这说明两点:
- cache 省的是算力和带宽,不是“不要内存”;
- 减小 (GQA)会直接降低 cache 内存。
因此 GQA 在工程里很重要:它在质量和推理成本之间做平衡。
6. 采样不是“随便随机”:温度在调什么
给定 logits ,温度采样:
概念解释:
- :分布更尖锐,更偏向高分词(更“稳”、更像贪心)。
- :分布更平坦,随机性更强(多样性更高)。
所以温度本质上是“探索-确定性”旋钮,而不是模型能力本身。
6.1 数值稳定为什么必须做
常见稳定写法:
减去 不改变结果分布,但能防止 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
建议按“调用链”读:
inference.cpp看外层生成循环;mini_llm.cpp看 prefill/decode API 如何组织;executor_forward.cpp看每层如何使用 cache。
9. 最常见的推理错误(带原因)
- prefill 后 cache 状态没重置:导致会话串味。
- 位置索引与 cache 长度不同步:RoPE 与 attention 对齐错位。
- 温度/seed 不固定:结果不可复现,难以定位回归。
- vocab 不匹配:输出投影维度和 tokenizer 词表不一致,结果异常。