训练时,模型可以并行预测一个序列中的很多位置。
推理时,情况不同。
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 训练和推理最重要的差异之一。