主题
自回归生成:为什么一个字一个字地输出
本章要回答的问题:模型为什么不能一次把整段话吐出来,而必须一个 token 一个 token 地生成?
一句话的概率分解
语言模型学的是一个条件概率:给定前面所有 token,下一个 token 是什么。
设一句话切成 token 序列 x₁, x₂, …, x_T,模型给出的联合概率可以写成连乘:
P(x₁, x₂, …, x_T) = P(x₁) · P(x₂|x₁) · P(x₃|x₁,x₂) · … · P(x_T|x₁…x_{T-1})这个分解就是"自回归"(autoregressive)的含义:每一步都用自己前面的输出作为输入。Transformer 的解码器结构天然支持这种计算——因果掩码(causal mask)保证第 t 个位置只能看到 1…t-1 的内容,训练时一次前向就能并行算出所有位置的下一 token 概率(引用 [1])。
注意这里的区别:训练可以并行,推理不能。 训练时整句话是已知的,所以能在一次前向里同时算 T 个位置。推理时第 t+1 个 token 还没生成,没有输入,只能等第 t 个 token 采样出来。
生成一步到底发生了什么
把一次完整的生成拆开,循环体是固定的四步(图 1.2-1):
- 取最后一个位置的输出向量,乘上输出投影矩阵,得到一个长度为词表大小的分数向量(logits)。
Qwen3.5-9B的词表是 248,320,所以这一步要算 24 万多个分数。 - 按采样策略从分数里选一个 token(下一节细说)。选出来的 token 是整数 id。
- 把新 token 接到序列末尾,作为下一步的输入。
- 为新 token 计算并缓存 Key/Value,供后续步骤使用(1.4 节)。
然后回到第 1 步,直到满足停止条件。整个过程没有"回头修改"的机制——已经生成的 token 不会被改写。这也是为什么模型会在开头说错一句话,后面一路圆下去。
为什么不能"一次吐出一整段"
有人会问:既然训练时能一次算出所有位置的概率,推理时为什么不能?
因为采样是顺序依赖的。第 2 个 token 的输入里包含第 1 个 token 的实际取值,而第 1 个 token 是随机采样出来的;不同的采样结果会导向完全不同的后续分布。除非把整棵概率树展开(组合爆炸),否则无法跳过中间步骤。
工程上确实有绕开的办法,但都不是"免费并行":
- 投机解码(speculative decoding):用一个小模型先猜出几个 token,再用大模型一次性并行验证,猜对的接受、猜错的回退(引用 [2])。本质是把"串行生成"换成"并行验证 + 少量回退",总延迟能降,但计算量增加。
- 非自回归生成:一次预测多个位置,再迭代修正。质量和通用性目前都不如自回归,主要用于特定任务。
对本书的场景(本地、离线、RAG 问答),默认假设仍然是逐 token 生成。所有延迟估算都建立在这个前提上。
采样:同样是"下一个词",结果可以差很远
模型输出的是概率分布,最终选哪个 token 由采样策略决定。几个常用参数(第三部分会展开):
| 参数 | 作用 | 典型影响 |
|---|---|---|
temperature | 把分布"压平"或"削尖" | 越低越保守、越容易重复;越高越发散 |
top-k | 只在概率最高的 k 个 token 里选 | 砍掉长尾,减少胡言乱语 |
top-p(nucleus) | 只在累计概率达到 p 的最小集合里选 | 比 top-k 更自适应 |
min-p | 丢弃概率低于"最高概率 × min_p"的 token | 近期较常用,对高置信分布更稳 |
temperature = 0 通常被当作"确定性输出",但它只是让采样退化为取最大值(greedy)。不保证跨进程、跨硬件逐字一致:浮点累加顺序、批大小、并行策略都可能改变极接近的 logits 的排序。要严格复现,需要固定引擎版本、批大小和硬件。
停止条件不止一个
生成不会自己知道"话说完了",它靠外部条件停下来:
- 采样到结束符(EOS);
- 达到
max_tokens/-n指定的上限; - 命中停止字符串(stop sequence);
- 对于带思考模式的模型,还要区分"思考结束"和"回答结束"两个标记。
Qwen3.5-9B 默认开启思考模式,可以通过 enable_thinking=False 关闭(引用 [3])。这一点对工程影响很大:同一个问题,开思考可能先输出几百个推理 token 再给答案,首字延迟和总时长都会明显增加。RAG 场景里,如果任务只是"从给定材料里摘出答案",关掉思考通常更划算;如果是多步推理或计算,则值得付这个成本。
工程含义
输出长度就是时间。 在带宽受限的机器上,每 token 时间基本恒定(1.5 节),所以总时长 ≈ 首字延迟 + 输出 token 数 × 每 token 时间。想快,先减输出。
流式输出是天然可行的。 因为 token 是一个一个产生的,服务端可以在第一个 token 出现时就推给前端。用户感知的"快",主要来自首字延迟,而不是总时长。
不要让模型复述材料。 在 RAG 里让模型"先总结文档再回答",等于把输入材料又生成一遍,成本翻倍且容易引入错误。要求它直接给结论和出处。
结构化输出要短。 JSON、表格这类格式的 token 开销不小,字段名越长越贵。能用短字段名就用短的。
动手实验
动手实验:温度与可复现性
假设:temperature = 0 在相同引擎、相同命令下可重复;改变批大小(-b/-ub)后可能出现差异。
命令:
bash
# llama-completion 是引擎的原始终端接口:不套对话模板,输出只有生成的 token,
# 适合逐字比较。llama-cli 从 0.7.0 起默认连续对话,需要 -st 才会答完退出,
# 而且会把启动 banner 打进标准输出,不适合重定向到文件。
printf '用一句话解释什么是 KV Cache。' > /tmp/p.txt
for i in 1 2 3; do
llama-completion -m "$MODEL" -c 2048 --temp 0 -n 128 \
-f /tmp/p.txt --no-display-prompt 2>/dev/null
echo "---"
done记录:三次输出是否逐字相同;再改 -ub 512 与 -ub 2048 各跑三次,比较差异。结论连同上一次的命令与 benchmarks/record-env.sh 的输出写入 benchmarks/determinism/。
参考资料
- Vaswani, A. et al. Attention Is All You Need. arXiv:1706.03762. https://arxiv.org/abs/1706.03762
- Leviathan, Y., Kalman, M., Matias, Y. Fast Inference from Transformers via Speculative Decoding. arXiv:2211.17192. https://arxiv.org/abs/2211.17192
- Qwen. Qwen3.5-9B 模型卡(思考模式与
enable_thinking参数). https://huggingface.co/Qwen/Qwen3.5-9B