MiniMind 学习笔记 09:LLM 推理生成,KV Cache、temperature、top-k 与 top-p

训练时,模型可以并行预测一个序列中的很多位置。

推理时,情况不同。

LLM 生成文本通常是自回归的:

输入 prompt
-> 生成第 1 个 token
-> 把第 1 个 token 接回上下文
-> 生成第 2 个 token
-> ...

MiniMind 的生成逻辑在 MiniMindForCausalLM.generate 和 eval_llm.py 中可以看到。

这篇文章整理推理生成的主线。

1. eval_llm.py 是怎么调用生成的

eval_llm.py 会先准备输入:

inputs = tokenizer.apply_chat_template(
    conversation,
    tokenize=False,
    add_generation_prompt=True
)

再 tokenizer:

inputs = tokenizer(inputs, return_tensors="pt", truncation=True)

最后调用:

generated_ids = model.generate(
    inputs=inputs["input_ids"],
    attention_mask=inputs["attention_mask"],
    max_new_tokens=args.max_new_tokens,
    do_sample=True,
    top_p=args.top_p,
    temperature=args.temperature,
    repetition_penalty=1
)

这里的几个参数决定了生成行为:

max_new_tokens
temperature
top_p
do_sample
repetition_penalty

2. 为什么只取最后一个位置的 logits

模型 forward 输出:

logits: [batch_size, seq_len, vocab_size]

每个位置都有一个词表分布。

但推理生成下一 token 时,只需要最后一个位置的预测:

logits = outputs.logits[:, -1, :]

形状变成:

[batch_size, vocab_size]

也就是:

当前上下文后面,下一个 token 是词表中每个 token 的分数。

3. temperature 为什么作用在 logits 上

代码中常见:

logits = outputs.logits[:, -1, :] / temperature

temperature 作用在 softmax 之前:

probs = softmax(logits / T)

如果:

T < 1

高分 token 会更突出,分布更尖锐,生成更确定。

如果:

T > 1

分布更平,低分 token 也更有机会被采样,生成更随机。

如果不走采样,而是直接 argmax:

next_token = torch.argmax(logits)

那么 temperature 基本没有实际意义。因为除以一个正数不会改变 logits 的大小顺序。

4. top-k 是什么

top-k 是只保留分数最高的 k 个 token。

例如词表里有 6400 个 token:

top_k = 50

就只允许从概率最高的 50 个 token 里采样。

其他 token 的 logits 会被设成:

-inf

softmax 后概率为 0。

top-k 的作用是:

防止模型采到概率极低的离谱 token。

5. top-p 是什么

top-p 也叫 nucleus sampling。

它不是固定保留 k 个 token,而是按累计概率保留候选集合。

例如:

top_p = 0.9

表示从高到低排序 token 概率,保留累计概率达到 0.9 的最小集合。

如果模型很确定,可能只保留几个 token。

如果模型不确定,可能保留很多 token。

所以 top-p 是动态候选集合。

6. top-k 和 top-p 为什么存在

直接从完整 softmax 分布采样会有问题:

词表很大;
低概率 token 很多;
偶尔采到低质量 token 会带偏后续生成。

top-k / top-p 的意义是限制采样空间:

既保留一定随机性;
又避免太离谱的 token。

可以粗略理解:

参数控制方式
temperature调整概率分布的尖锐程度。
top-k固定保留前 k 个 token。
top-p动态保留累计概率达到 p 的 token。

实际使用中经常组合:

temperature = 0.7 ~ 1.0
top_p = 0.8 ~ 0.95

7. multinomial 和 argmax

MiniMind 里生成下一个 token 的逻辑可以简化成:

next_token = (
    torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1)
    if do_sample
    else torch.argmax(logits, dim=-1, keepdim=True)
)

如果 do_sample=True

先 softmax 得到概率;
再按概率随机采样。

如果 do_sample=False

直接选 logits 最大的 token。

前者更有随机性,后者更稳定但容易单调。

8. repetition_penalty 是什么

生成时模型可能重复:

很好很好很好很好...

repetition_penalty 是一种工程手段,用来降低已经出现过的 token 再次出现的概率。

常见逻辑是:

score = logits[i, seen]
logits[i, seen] = torch.where(
    score > 0,
    score / repetition_penalty,
    score * repetition_penalty
)

直觉是:

如果某个已出现 token 原本分数高,就压低它;
如果原本分数为负,就让它更负。

它确实有点像工程补丁,但公开模型和生成库里也常见类似设计。

它不是网络结构的一部分,而是 decoding 阶段的 logits processor。

9. KV Cache 为什么重要

如果每生成一个 token,都把完整上下文重新跑一遍,成本会很高。

假设已经有:

prompt + 生成历史 = 1000 tokens

下一步只新增 1 个 token。

历史 token 的 K/V 其实已经算过了,不需要重复计算。

KV Cache 的做法是:

prefill 阶段:
  一次性处理 prompt,缓存每层 K/V。

decode 阶段:
  每次只处理新 token,复用历史 K/V。

所以 forward 中会看到:

input_ids[:, past_len:]

含义是:

如果 past_key_values 里已经有历史,只把新 token 送进模型。

这能显著降低 decode 阶段的重复计算。

10. prefill 和 decode 的区别

推理可以分成两段:

prefill:
  处理完整输入 prompt。
  可以并行。
  主要成本和输入长度相关。

decode:
  一个 token 一个 token 生成。
  每步依赖上一步结果。
  主要成本和输出长度相关。

虽然 decode 每次只处理一个新 token,但它要反复执行多次。

所以 API 计费里,输出 token 往往更贵。

原因包括:

decode 串行;
每个输出 token 都要跑一轮模型;
需要访问 KV Cache;
吞吐不像 prefill 那么容易并行。

11. generate 主流程

MiniMind 的 generate 可以概括为:

初始化 past_key_values
初始化 generated

for step in max_new_tokens:
    forward 当前输入
    取最后位置 logits
    应用 temperature
    应用 repetition_penalty
    应用 top-k / top-p
    sample 或 argmax 得到 next_token
    拼接 next_token
    更新 past_key_values
    如果遇到 eos,停止

这就是 LLM 推理生成的核心循环。

12. 小结

推理生成可以抓住这条主线:

forward
-> 取最后一个位置 logits
-> 调整 logits
-> 采样或 argmax
-> 拼接 token
-> 更新 KV Cache
-> 重复

其中:

  • temperature 控制随机性。
  • top-k 固定限制候选数。
  • top-p 动态限制候选概率质量。
  • repetition_penalty 抑制重复。
  • KV Cache 避免重复计算历史 token。

训练时模型一次并行学习很多 token;推理时模型必须一步一步生成。这是 LLM 训练和推理最重要的差异之一。

发表评论