Skip to content

Output ​

Decoder-only Causal LM 的 Decoder 输出是隐藏状态,而不是文本。模型还需依次完成最终归一化、LM Head 投影、logits 处理、token 选择与反分词,才能得到可读文本;本文以 Qwen2 和 Hugging Face Transformers 为参考实现说明这一流程。

Decoder-only Causal LM 输出流程

text
Hidden States [B, S, D]
  → Final Norm
  → Position Selection [B, K, D]
  → LM Head [B, K, V]
  → Logits Processing [B, V]
  → Token Selection [B]
  → Generation Loop
  → Tokenizer.decode

其中,B 为批大小,S 为输入序列长度,D 为隐藏维度,V 为词表大小,K 为需要计算 logits 的位置数。训练时通常取 K=S;单 token 解码时通常只需取 K=1。

1. 输出投影 ​

输出投影将 Decoder 的隐藏状态转换为词表中每个 token 的未归一化分数,即 logits。对于 Pre-Norm 模型,该阶段通常包含 Final Norm、预测位置选择和 LM Head 三个步骤。

1.1 Final Norm ​

Pre-Norm 模型在每个子模块之前执行归一化,但最后一个子模块的残差相加之后仍需一次 Final Norm。Qwen2 使用 RMSNorm:

h^=h1D∑i=1Dhi2+ϵ⊙γ

其中,h∈RD 为单个位置的隐藏状态,γ 为可学习的逐元素缩放参数,ϵ 为数值稳定项。不同模型也可以使用 LayerNorm 或其他归一化方法。

1.2 预测位置 ​

位置 t 的隐藏状态用于预测 xt+1。训练通常保留全部位置,单 token 解码只保留最后一个有效位置;Qwen2 通过 logits_to_keep 控制该范围,避免计算无用的词表投影。批量生成应使用左侧 padding,确保 [:, -1, :] 对应有效 token。

1.3 LM Head ​

LM Head 是从隐藏维度到词表维度的线性投影:

zt=h^tWlm⊤+blm,Wlm∈RV×D

其中,zt∈RV 是位置 t 的 logits,blm 为可选偏置。Qwen2 的 LM Head 不使用偏置;输入形状 [B, K, D] 经投影后变为 [B, K, V]。

📄 transformers/.../modeling_qwen2.py#L392-L480
python
# Qwen2Model.forward
for decoder_layer in self.layers:
    hidden_states = decoder_layer(hidden_states, ...)

hidden_states = self.norm(hidden_states)
return BaseModelOutputWithPast(last_hidden_state=hidden_states, ...)


# Qwen2ForCausalLM.forward
hidden_states = outputs.last_hidden_state
slice_indices = (
    slice(-logits_to_keep, None)
    if isinstance(logits_to_keep, int)
    else logits_to_keep
)
logits = self.lm_head(hidden_states[:, slice_indices, :])

1.4 Weight Tying ​

Weight Tying 让输入 Embedding 与 LM Head 共享同一参数矩阵:

Wlm=Wembed

共享权重可减少 V×D 个参数;不共享则允许输入表示与输出分类器独立学习。Qwen2 的默认配置为 tie_word_embeddings=False,但加载具体模型时仍应以检查点配置和实际参数绑定关系为准。

2. Sampling ​

单步生成只使用最后一个预测位置的 logits,形状为 [B, V]。生成器先根据约束和采样参数处理 logits,再通过贪心或采样得到形状为 [B] 的下一个 token ID。

2.1 Logits 处理 ​

logits 不是概率。Logits processor 会按配置顺序修改分数,并通常将不允许生成的 token 设为 −∞。常见策略如下:

参数处理方式作用
temperaturesi=zi/T调整采样分布的集中程度
top_k保留分数最高的 k 个 token限制候选集大小
top_p保留累计概率达到阈值的最小候选集动态限制候选集
min_p保留 pi≥min_p⋅maxjpj 的 token按最高概率的相对比例过滤

当 T<1 时,temperature 放大分数差异;当 T>1 时,分数分布更加平缓。该变换不改变 logits 的排序,因此只对采样分布有实际影响。temperature=0 不是合法的除数;确定性生成应设置 do_sample=False。

生成约束、重复惩罚和采样过滤器可能同时生效。由于这些变换通常不可交换,最终结果取决于处理器的实际执行顺序。

2.2 贪心与采样 ​

设处理后的分数为 s。Greedy Decoding 直接选择最高分 token:

xnext=arg⁡maxisi

Sampling 则先将分数归一化为概率,再从分类分布中抽样:

pi=esi∑jesj,xnext∼Categorical(p)

两种方法的输出都是 token ID。对于相同分数,Greedy Decoding 的结果确定;Sampling 的结果还取决于随机数状态。

3. 生成循环 ​

生成器将新 token 追加到当前序列,并重复执行模型前向、分数处理和 token 选择,直至所有序列满足停止条件。

3.1 Prefill 与 Decode ​

首次处理完整 prompt 的阶段称为 Prefill,之后逐 token 生成的阶段称为 Decode。启用 KV Cache 后,Decode 可以复用历史 Key 和 Value,只计算新增 token 对应的 Transformer 状态;这项优化不会改变 Final Norm、LM Head 或 token 选择的语义。

3.2 追加与停止 ​

每轮生成完成 token 选择后,生成器依次执行以下操作:

  1. 已结束序列:将后续位置填充为 padding token。
  2. 未结束序列:将新 token 追加到 input_ids。
  3. 停止判断:检查 EOS、最大长度、stop_strings、最大时间或自定义条件。
  4. 循环控制:仍有未结束序列时进入下一轮 Decode。
📄 transformers/generation/utils.py#L2857-L2952
python
outputs = self._prefill(input_ids, generation_config, model_kwargs, ...)

while self._has_unfinished_sequences(...):
    if prefill_consumed:
        next_sequence_length = 1 if model_kwargs["use_cache"] else None
        model_inputs = self.prepare_inputs_for_generation(
            input_ids,
            next_sequence_length=next_sequence_length,
            **model_kwargs,
        )
        outputs = model_forward(**model_inputs, return_dict=True)
    prefill_consumed = True

    next_token_logits = outputs.logits[:, -1].to(dtype=torch.float32)
    next_token_scores = logits_processor(input_ids, next_token_logits)

    if do_sample:
        probs = nn.functional.softmax(next_token_scores, dim=-1)
        next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
    else:
        next_tokens = torch.argmax(next_token_scores, dim=-1)

    input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)
    unfinished_sequences = unfinished_sequences & ~stopping_criteria(input_ids, scores)

4. 文本解码 ​

生成循环始终处理 token ID。循环结束后,Tokenizer 再将 token ID 序列转换为文本:

python
text = tokenizer.decode(generated_ids, skip_special_tokens=True)

token 不等同于字符或单词。对于 Byte-level Tokenizer,单个 token 还可能只包含一个 UTF-8 字符的部分字节,因此流式输出应使用增量解码器或 streamer 维护解码缓冲区,不能简单地逐 token 独立解码后拼接。

skip_special_tokens=True 只控制最终文本是否展示特殊 token;EOS 等特殊 token 仍参与模型计算和停止判断。

5. 参考 ​

  1. Transformers:Qwen2 模型实现(v5.14.1)
  2. Transformers:Qwen2 配置(v5.14.1)
  3. Transformers:文本生成配置
  4. Transformers:生成策略