<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Text_generation on 酒中仙</title><link>https://hanguangwu.github.io/blog/tags/text_generation/</link><description>Recent content in Text_generation on 酒中仙</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>hanguangwu</copyright><lastBuildDate>Tue, 24 Mar 2026 13:34:25 -0800</lastBuildDate><atom:link href="https://hanguangwu.github.io/blog/tags/text_generation/index.xml" rel="self" type="application/rss+xml"/><item><title>手撕大模型文本生成策略</title><link>https://hanguangwu.github.io/blog/p/%E6%89%8B%E6%92%95%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90%E7%AD%96%E7%95%A5/</link><pubDate>Tue, 24 Mar 2026 13:34:25 -0800</pubDate><guid>https://hanguangwu.github.io/blog/p/%E6%89%8B%E6%92%95%E5%A4%A7%E6%A8%A1%E5%9E%8B%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90%E7%AD%96%E7%95%A5/</guid><description>&lt;h1 id="手撕大模型文本生成策略"&gt;手撕大模型文本生成策略
&lt;/h1&gt;&lt;p&gt;前两节我们通过 Llama2 和 MoE，深入理解了大模型的&lt;strong&gt;网络架构&lt;/strong&gt;（即“大脑”是如何构造的）。但仅有架构还不够，模型前向传播输出的仅仅是概率分布（Logits），如何将这些概率一步步转化为流畅的文本，就是本节要探讨的核心——&lt;strong&gt;解码策略&lt;/strong&gt;。我们将回到成熟的 &lt;strong&gt;Transformers&lt;/strong&gt; 库，以 GPT 模型为例，避开繁琐的数学公式，直接通过&lt;strong&gt;代码调试&lt;/strong&gt;的方式，探究 &lt;code&gt;model.generate()&lt;/code&gt; 的底层工作原理，看看从“输入 Prompt”到“输出文本”的完整数据流是如何在代码中流转的。&lt;/p&gt;
&lt;h2 id="一逐-token-生成在做什么"&gt;一、“逐 token 生成”在做什么
&lt;/h2&gt;&lt;h3 id="11-从-pipeline-入手调试准备"&gt;1.1 从 Pipeline 入手调试准备
&lt;/h3&gt;&lt;p&gt;重新打开在第五章中实现的 &lt;a class="link" href="https://github.com/datawhalechina/base-nlp/blob/main/code/C5/02_gpt_usage.py" target="_blank" rel="noopener"
&gt;GPT 实战代码&lt;/a&gt;，如图 6-16 注释掉其他内容，只保留文件最后的 &lt;code&gt;# pipeline 应用&lt;/code&gt; 的部分以及相关初始化变量。具体调试方法可以参考第八章 &lt;a class="link" href="https://github.com/datawhalechina/base-nlp/blob/main/docs/chapter8/02_data_processing.md#211-%E8%B0%83%E8%AF%95%E8%A7%82%E5%AF%9F%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84" target="_blank" rel="noopener"
&gt;NER 项目的数据处理&lt;/a&gt;中的简单说明。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_1.png" width="90%" alt="Pipeline 调试代码准备" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-16 Pipeline 调试代码准备&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;接下来按下面步骤进行调试：&lt;/p&gt;
&lt;p&gt;（1）在 &lt;code&gt;pipeline_outputs = generator(...)&lt;/code&gt; 这一行打断点。Debug 运行脚本，程序停住后，点击&lt;strong&gt;步入（Step Into）&lt;/strong&gt;，如图 6-17 我们就进入 Transformers 包内部代码。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_2.png" width="90%" alt="Pipeline 源码" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-17 Pipeline 源码&lt;/em&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;可以看到图中源码的猫咪 emoji（🐈 🐈 🐈）。Hugging Face 的三位创始人都是法国人，而在法语中 “chat” 就是猫的意思，所以工程师用它暗示这里有“一堆 chats（猫）”。哈哈，极客幽默。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（2）言归正传，接下来我们需要在 &lt;code&gt;text_generation.py&lt;/code&gt; 文件中的四个位置下断点。分别在 &lt;code&gt;preprocess()&lt;/code&gt; 最后找到 &lt;code&gt;return inputs&lt;/code&gt; 这一行；在 &lt;code&gt;_forward()&lt;/code&gt; 中找到 &lt;code&gt;output = self.model.generate(&lt;/code&gt;；在 &lt;code&gt;postprocess()&lt;/code&gt; 的循环里找到 &lt;code&gt;text = self.tokenizer.decode(&lt;/code&gt; 和 &lt;code&gt;if return_type == ReturnType.FULL_TEXT:&lt;/code&gt;，然后把断点打在这四行。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;调试器命中断点时通常会停在该行“执行之前”，所以如果某个变量是“在这一行才刚被赋值/更新”的，需要&lt;strong&gt;单步执行一次&lt;/strong&gt;或把断点下在&lt;strong&gt;下一行&lt;/strong&gt;，才能看到它的最终值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（3）断点下好后，点击 &lt;strong&gt;恢复程序（Continue / Resume）&lt;/strong&gt;，程序会依次停在这四个位置。&lt;/p&gt;
&lt;h3 id="12-pipeline-接口的输入与输出结构"&gt;1.2 Pipeline 接口的输入与输出结构
&lt;/h3&gt;&lt;p&gt;（1）停在 &lt;code&gt;preprocess()&lt;/code&gt;：看模型“吃进去”的是什么&lt;/p&gt;
&lt;p&gt;在前处理阶段，展开 &lt;code&gt;inputs&lt;/code&gt; 我们看看模型“吃进去”的到底是什么。如图 6-18 所示，会发现&lt;strong&gt;文本被 tokenizer 变成了 token ids 以及 mask&lt;/strong&gt;（见 &lt;code&gt;input_ids&lt;/code&gt; 和 &lt;code&gt;attention_mask&lt;/code&gt;），同时 pipeline 还会保留原始输入 &lt;code&gt;prompt_text&lt;/code&gt;，这是为了后处理时能拼回 &lt;strong&gt;FULL_TEXT&lt;/strong&gt;。在图中可以看到 &lt;code&gt;input_ids&lt;/code&gt; 和 &lt;code&gt;attention_mask&lt;/code&gt; 形状是 &lt;code&gt;Tensor: (1, 4)&lt;/code&gt;，对应我们的输入 &lt;code&gt;I like eating fried&lt;/code&gt;，第 1 维 = batch size = 1（本次只输入 1 条 prompt），第 2 维 = 序列长度 = 4（这条 prompt 被 tokenizer 切成了 4 个 token）。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_3.png" width="90%" alt="preprocess 阶段：input_ids / attention_mask" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-18 前处理阶段的 inputs 结构&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（2）停在 &lt;code&gt;_forward()&lt;/code&gt;：确认真正的解码发生在 &lt;code&gt;model.generate()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这里我们可以看到 &lt;strong&gt;Pipeline 只是把参数整理好，真正的解码策略发生在 &lt;code&gt;model.generate()&lt;/code&gt; 里&lt;/strong&gt;。如图 6-19 这里就是“最终生效”的生成参数，这里传入了我们在前处理阶段得到的 &lt;code&gt;input_ids&lt;/code&gt; 和 &lt;code&gt;attention_mask&lt;/code&gt;，同时还有我们在 &lt;code&gt;generator()&lt;/code&gt; 中传入的 &lt;code&gt;max_new_tokens=5&lt;/code&gt;、&lt;code&gt;num_return_sequences=1&lt;/code&gt; 两个参数。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_4.png" width="90%" alt="_forward 阶段：generate_kwargs" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-19 推理生成阶段传递生成参数&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（3）停在 &lt;code&gt;postprocess()&lt;/code&gt;：把 token ids 还原成文本，并决定返回 FULL/NEW&lt;/p&gt;
&lt;p&gt;后处理阶段的任务就是把生成出来的 token ids 用 &lt;code&gt;tokenizer.decode(...)&lt;/code&gt; 还原成文本，并决定返回 &lt;strong&gt;FULL_TEXT/NEW_TEXT&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如图 6-20，程序首先停在 &lt;code&gt;text = self.tokenizer.decode(&lt;/code&gt; 的位置。这里有个关键参数 &lt;code&gt;sequence&lt;/code&gt;：通过 &lt;code&gt;Ctrl + B&lt;/code&gt;（转到定义）回溯可以发现，这个值来自 &lt;code&gt;model_outputs[&amp;quot;generated_sequence&amp;quot;][0]&lt;/code&gt;。所以实际上在 Transformers 的实现里，模型推理结束后会得到一个字典，包含 &lt;code&gt;'generated_sequence'&lt;/code&gt;、&lt;code&gt;'input_ids'&lt;/code&gt; 以及 &lt;code&gt;'prompt_text'&lt;/code&gt;。接下来 &lt;code&gt;generated_sequence&lt;/code&gt; 会被转换成 Python 列表，并在 &lt;code&gt;for idx, sequence in enumerate(generated_sequence):&lt;/code&gt; 里逐条取出，因此此处的 &lt;code&gt;sequence&lt;/code&gt; 本质上就是“一条生成序列”的 token id 列表。观察 &lt;code&gt;sequence&lt;/code&gt; 的值能够发现，前面的四个 &lt;code&gt;40, 588, 6600, 23018&lt;/code&gt; 是我们的输入 token，而后面五个 &lt;code&gt;9015, 553, 531, 262, 582&lt;/code&gt; 则是模型新生成的内容，并且与 &lt;code&gt;max_new_tokens=5&lt;/code&gt; 的设置对应。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_5.png" width="90%" alt="后处理阶段的 sequence" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-20 后处理阶段的 sequence&lt;/em&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再次恢复程序会停到 &lt;code&gt;if return_type == ReturnType.FULL_TEXT:&lt;/code&gt;。此时重点看 &lt;code&gt;prompt_length&lt;/code&gt; 和 &lt;code&gt;all_text&lt;/code&gt;：&lt;code&gt;prompt_length&lt;/code&gt; 表示“prompt 在 decode 后的长度”，Pipeline 用它把 &lt;code&gt;text&lt;/code&gt; 的前半段（prompt 部分）裁掉，得到“新增内容” &lt;code&gt;all_text&lt;/code&gt;（也就是 &lt;strong&gt;NEW_TEXT&lt;/strong&gt;）。如图 6-21 所示，本次推理的结果被 decode 成字符串后的结果是 &lt;code&gt;' chicken, but I like'&lt;/code&gt;，而 &lt;code&gt;prompt_length=19&lt;/code&gt; 也刚好对应 &lt;strong&gt;“&amp;ldquo;I&amp;rdquo;(1) + 空格(1) + &amp;ldquo;like&amp;rdquo;(4) + 空格(1) + &amp;ldquo;eating&amp;rdquo;(6) + 空格(1) + &amp;ldquo;fried&amp;rdquo;(5) = 19”&lt;/strong&gt;。如果设置的是 &lt;strong&gt;FULL_TEXT&lt;/strong&gt;（默认值），Pipeline 会在后面把 &lt;code&gt;prompt_text&lt;/code&gt; 再拼回去，所以最终输出会包含原始 prompt。如果想看到最后输出的结果，也可以在当前代码文件尾部 &lt;code&gt;return records&lt;/code&gt; 的位置继续下断点，这里就不再赘述。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_6.png" width="90%" alt="后处理阶段的 prompt_length 与 all_text" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-21 后处理阶段的 prompt_length 与 all_text&lt;/em&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="13-分析解码流程"&gt;1.3 分析解码流程
&lt;/h3&gt;&lt;p&gt;刚才提到了真正的解码策略发生在 &lt;code&gt;model.generate()&lt;/code&gt; 里，那么接下来我们取消掉除了 &lt;code&gt;output = self.model.generate(&lt;/code&gt; 这行之外的其他三处断点，然后重新调试。&lt;/p&gt;
&lt;p&gt;（1）&lt;strong&gt;步入&lt;/strong&gt;进去 &lt;code&gt;text_generation.py&lt;/code&gt; 后恢复程序会停到 &lt;code&gt;output = self.model.generate(&lt;/code&gt; 这行，然后我们对 &lt;code&gt;generate()&lt;/code&gt; 方法 Ctrl + B 转到定义，这时一般会跳到 &lt;code&gt;generation/utils.py&lt;/code&gt;（或同类路径）里定义 &lt;code&gt;generate()&lt;/code&gt; 方法的地方。接着在下面找到 &lt;code&gt;generation_config, model_kwargs = self._prepare_generation_config(&lt;/code&gt; 下完断点后恢复程序。&lt;/p&gt;
&lt;p&gt;（2）如图 6-22，我们可以看到 &lt;code&gt;GenerationConfig&lt;/code&gt; 的配置如下，这些配置在本地下载的模型 &lt;code&gt;config.json&lt;/code&gt; 文件中也有体现。同时我们还可以看一下 &lt;code&gt;kwargs&lt;/code&gt; 中的内容，这就是我们传入 &lt;code&gt;generate()&lt;/code&gt; 的参数。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;span class="lnt"&gt;7
&lt;/span&gt;&lt;span class="lnt"&gt;8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GenerationConfig {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;bos_token_id&amp;#34;: 50256,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;do_sample&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;eos_token_id&amp;#34;: 50256,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;max_length&amp;#34;: 50,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;max_new_tokens&amp;#34;: 256,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;temperature&amp;#34;: 0.7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;其中，&lt;code&gt;bos_token_id&lt;/code&gt; 是生成起始 token（当 &lt;code&gt;inputs=None&lt;/code&gt; 时会用它来“起一个头”）；&lt;code&gt;eos_token_id&lt;/code&gt; 是结束 token（生成到它或满足停止条件就停止）；&lt;code&gt;do_sample&lt;/code&gt; 表示是否采样（&lt;code&gt;False&lt;/code&gt; 更确定，&lt;code&gt;True&lt;/code&gt; 更有随机性/多样性）；&lt;code&gt;temperature&lt;/code&gt; 是采样温度（越大越随机，越小越保守）；&lt;code&gt;max_length&lt;/code&gt; 是总长度上限（prompt+新生成）；&lt;code&gt;max_new_tokens&lt;/code&gt; 是新增 token 上限（如果同时设置了 &lt;code&gt;max_length&lt;/code&gt;，以最终合并后的配置为准）。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_7.png" width="90%" alt="默认的 GenerationConfig" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-22 默认的 GenerationConfig&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（3）接着在 &lt;code&gt;generation_mode = generation_config.get_generation_mode(assistant_model)&lt;/code&gt; 这行也下个断点。恢复程序后，我们可以看到多了个变量 &lt;code&gt;model_kwargs&lt;/code&gt;，其中包括了 &lt;code&gt;input_ids&lt;/code&gt; 和 &lt;code&gt;attention_mask&lt;/code&gt;。同时 &lt;code&gt;GenerationConfig&lt;/code&gt; 中的默认值（如 &lt;code&gt;max_new_tokens&lt;/code&gt;）也被传参覆盖。&lt;/p&gt;
&lt;p&gt;（4）我们下一步看看本次推理会走哪种解码策略，可以选择在 &lt;code&gt;decoding_method = getattr(type(self), GENERATION_MODES_MAPPING[generation_mode])&lt;/code&gt; 这行下个断点，也可以直接步过（Step Over）。如图 6-23 可以看到 &lt;code&gt;generation_mode&lt;/code&gt; 的值是 &lt;code&gt;&amp;lt;GenerationMode.SAMPLE: 'sample'&amp;gt;&lt;/code&gt;，说明本次会走**采样（Sampling）**分支，通常对应 &lt;code&gt;do_sample=True&lt;/code&gt; 且 &lt;code&gt;num_beams=1&lt;/code&gt;（区别于 &lt;code&gt;do_sample=False,num_beams=1&lt;/code&gt; 的贪心，以及 &lt;code&gt;num_beams&amp;gt;1&lt;/code&gt; 的 beam 系列策略；如果 &lt;code&gt;do_sample=True&lt;/code&gt; 且 &lt;code&gt;num_beams&amp;gt;1&lt;/code&gt;，则会走 &lt;code&gt;BEAM_SAMPLE&lt;/code&gt;）。采样的含义是每一步不是“永远选概率最大的 token”，而是在（可能经过 &lt;code&gt;temperature/top_k/top_p&lt;/code&gt; 等处理后的）概率分布上随机采样一个 token，所以输出通常会更有多样性。这部分具体判定代码可以使用 Ctrl + B 转到 &lt;code&gt;generation_config.get_generation_mode(...)&lt;/code&gt; 的代码定义进行查看。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_8.png" width="90%" alt="采样分支" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-23 解码策略分支选择&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（5）继续往下走，我们看一下输入张量 &lt;code&gt;inputs_tensor&lt;/code&gt; 怎么变成 &lt;code&gt;input_ids&lt;/code&gt;。我们首先把断点下在 &lt;code&gt;if &amp;quot;inputs_tensor&amp;quot; in inspect.signature(decoding_method).parameters.keys():&lt;/code&gt; 这行。此时 &lt;code&gt;_prepare_model_inputs()&lt;/code&gt; 执行完成后，如图 6-24 我们可以看到 &lt;code&gt;model_kwargs&lt;/code&gt; 中的 &lt;code&gt;input_ids&lt;/code&gt; 被拆成了 &lt;code&gt;inputs_tensor&lt;/code&gt;，而且有一个新变量 &lt;code&gt;model_input_name&lt;/code&gt;。&lt;code&gt;model_input_name&lt;/code&gt; 相当于一个标签，代表 &lt;code&gt;inputs_tensor&lt;/code&gt; 对应的是哪一种输入类型。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_9.png" width="90%" alt="inputs_tensor 拆分为 input_ids 并产生 model_input_name" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-24 model_input_name 标记输入类型&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（6）如果 &lt;code&gt;num_return_sequences&amp;gt;1&lt;/code&gt;（一次要返回多条候选）或 &lt;code&gt;num_beams&amp;gt;1&lt;/code&gt;（beam search 需要多条 beam 路径），那么同一条 prompt 的 &lt;code&gt;input_ids&lt;/code&gt;（以及 &lt;code&gt;attention_mask&lt;/code&gt;）会在 batch 维度被复制 &lt;code&gt;expand_size=max(num_beams, num_return_sequences)&lt;/code&gt; 份，用来并行生成；如果这两个值都是 1，就会看到这里“几乎没变化”。另外我们还可以在 &lt;code&gt;if generation_config.token_healing:&lt;/code&gt; 这行下断点，此时能看到“扩展后”的 &lt;code&gt;input_ids&lt;/code&gt;（以及 &lt;code&gt;model_kwargs&lt;/code&gt; 里的 &lt;code&gt;attention_mask&lt;/code&gt;）形状，同时也能确认 &lt;code&gt;generation_config.token_healing&lt;/code&gt; 是否开启（开启时下一行会对 &lt;code&gt;input_ids&lt;/code&gt; 做一次 &lt;code&gt;heal_tokens&lt;/code&gt; 处理）。&lt;/p&gt;
&lt;p&gt;（7）当前代码文件的最后一处断点我们可以下在 &lt;code&gt;result = decoding_method(&lt;/code&gt;，这里是真正的逐 token 生成循环入口。当恢复程序代码停在这里后，如图 6-25 我们可以看一下传入 &lt;code&gt;decoding_method(...)&lt;/code&gt; 的几个参数。这里主要说明一下 &lt;code&gt;prepared_logits_processor&lt;/code&gt; 跟 &lt;code&gt;prepared_stopping_criteria&lt;/code&gt; 的作用，其他几个参数可能具体值有些变化不过作用没什么变化就不再赘述。当前的 &lt;code&gt;prepared_logits_processor&lt;/code&gt; 可以理解为“每一步选 token 之前对 logits 做规则化修正”的一串处理器（例如最小长度、坏词过滤、重复惩罚等都会在这里把某些 token 的概率压低/置为 &lt;code&gt;-inf&lt;/code&gt;），而 &lt;code&gt;prepared_stopping_criteria&lt;/code&gt; 则是一组“什么时候该停止生成”的判定条件（例如达到最大长度、遇到 &lt;code&gt;eos_token_id&lt;/code&gt; 或满足自定义停止条件时就结束循环）。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_10.png" width="90%" alt="decoding_method(...) 入参" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-25 decoding_method(...) 入参&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（8）最后在这行多次点击步入后，我们就进入了本次推理阶段实际执行的解码循环 &lt;code&gt;_sample()&lt;/code&gt; 方法，这部分我们暂时先简单总结一下代码逻辑。&lt;code&gt;_sample()&lt;/code&gt; 方法的内部是一个 while 循环，每一轮先用 &lt;code&gt;prepare_inputs_for_generation(...)&lt;/code&gt; 基于当前 &lt;code&gt;input_ids&lt;/code&gt; 准备本轮模型输入，然后做一次 forward 得到 &lt;code&gt;outputs.logits[:, -1, :]&lt;/code&gt;（只取“最后一个位置”的 logits），再把 logits 交给 &lt;code&gt;logits_processor(...)&lt;/code&gt; 做规则化修正，随后根据 &lt;code&gt;do_sample&lt;/code&gt; 选择采样（&lt;code&gt;torch.multinomial&lt;/code&gt;）或贪心（&lt;code&gt;argmax&lt;/code&gt;）得到 &lt;code&gt;next_tokens&lt;/code&gt;，把新 token 拼回 &lt;code&gt;input_ids&lt;/code&gt;（&lt;code&gt;torch.cat&lt;/code&gt;）作为下一轮输入；最后用 &lt;code&gt;stopping_criteria(input_ids, scores)&lt;/code&gt; 判断是否满足停止条件（例如到达最大长度或遇到 &lt;code&gt;eos_token_id&lt;/code&gt;），满足则跳出循环并返回生成的序列。如图 6-26，最终我们得到的这个 &lt;code&gt;(1, 9)&lt;/code&gt; 的 &lt;code&gt;input_ids&lt;/code&gt; 跟我们之前在后处理阶段看到的 &lt;code&gt;model_outputs[&amp;quot;generated_sequence&amp;quot;][0]&lt;/code&gt; 是一样的。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_11.png" width="90%" alt="_sample() 返回值" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-26 _sample() 返回值&lt;/em&gt;
&lt;/p&gt;
&lt;h3 id="14-从-pipeline-到底层循环的完整调用链"&gt;1.4 从 Pipeline 到底层循环的完整调用链
&lt;/h3&gt;&lt;p&gt;经过上述对 &lt;code&gt;Pipeline&lt;/code&gt; 和 &lt;code&gt;model.generate()&lt;/code&gt; 的深度调试，我们可以将一次完整的文本生成任务归纳为以下 &lt;strong&gt;5 个核心步骤&lt;/strong&gt;的接力跑：&lt;/p&gt;
&lt;p&gt;（1）&lt;strong&gt;预处理（Preprocess）&lt;/strong&gt;：Pipeline 的 &lt;code&gt;preprocess()&lt;/code&gt; 方法调用 &lt;code&gt;tokenizer&lt;/code&gt;，将原始字符串 &lt;code&gt;prompt_text&lt;/code&gt; 转换为模型可识别的 &lt;strong&gt;Token ID 张量&lt;/strong&gt; (&lt;code&gt;input_ids&lt;/code&gt;) 和 &lt;strong&gt;Attention Mask&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;（2）&lt;strong&gt;入口分发&lt;/strong&gt;：Pipeline 的 &lt;code&gt;_forward()&lt;/code&gt; 方法携带处理好的张量调用 &lt;code&gt;model.generate()&lt;/code&gt;；在 &lt;code&gt;generate()&lt;/code&gt; 内部，会先合并用户参数与 &lt;code&gt;GenerationConfig&lt;/code&gt; 默认配置，确定最终的生成参数（如 &lt;code&gt;max_new_tokens&lt;/code&gt;、&lt;code&gt;do_sample&lt;/code&gt; 等）。&lt;/p&gt;
&lt;p&gt;（3）&lt;strong&gt;策略选择&lt;/strong&gt;：&lt;code&gt;generate()&lt;/code&gt; 根据配置自动判断解码模式（Greedy / Sampling / Beam Search 等），并动态分发给对应的具体实现方法（如 &lt;code&gt;_sample()&lt;/code&gt;, &lt;code&gt;_greedy_search()&lt;/code&gt;, &lt;code&gt;_beam_search()&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;（4）&lt;strong&gt;解码循环&lt;/strong&gt;：进入具体方法（如 &lt;code&gt;_sample()&lt;/code&gt;）后，开启 &lt;code&gt;while&lt;/code&gt; 循环。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;准备输入&lt;/strong&gt;：&lt;code&gt;prepare_inputs_for_generation()&lt;/code&gt; 处理缓存 (&lt;code&gt;past_key_values&lt;/code&gt;) 和当前输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模型前向&lt;/strong&gt;：执行 &lt;code&gt;model()&lt;/code&gt; 得到最新的 &lt;code&gt;logits&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规则修正&lt;/strong&gt;：&lt;code&gt;LogitsProcessor&lt;/code&gt; 修改概率分布（如惩罚重复、限制词表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;采样选择&lt;/strong&gt;：根据概率分布采样（&lt;code&gt;multinomial&lt;/code&gt;）或贪心选择（&lt;code&gt;argmax&lt;/code&gt;）得到 &lt;strong&gt;Next Token&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拼接更新&lt;/strong&gt;：将新 Token 拼接到 &lt;code&gt;input_ids&lt;/code&gt; 末尾。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停止判定&lt;/strong&gt;：检查 &lt;code&gt;StoppingCriteria&lt;/code&gt;（如是否遇到 EOS 或达到最大长度），决定是否跳出循环。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（5）&lt;strong&gt;后处理（Postprocess）&lt;/strong&gt;：生成结束后得到的完整 &lt;code&gt;sequence&lt;/code&gt; 会被送回 Pipeline 的 &lt;code&gt;postprocess()&lt;/code&gt;，再调用 &lt;code&gt;tokenizer.decode()&lt;/code&gt; 将 Token ID 序列还原为人类可读的字符串，并根据配置处理 &lt;code&gt;FULL_TEXT&lt;/code&gt; / &lt;code&gt;NEW_TEXT&lt;/code&gt; 的截取逻辑，最终返回给用户。&lt;/p&gt;
&lt;h2 id="二logits-规则链与常用解码策略"&gt;二、logits 规则链与常用解码策略
&lt;/h2&gt;&lt;p&gt;我们刚才已经能顺着断点走到 &lt;code&gt;result = decoding_method(&lt;/code&gt;，并进一步步入到本次实际执行的策略方法（例如 &lt;code&gt;_sample()&lt;/code&gt;）。接下来还有两个问题需要解决。第一，&lt;code&gt;get_logits_processor&lt;/code&gt;（源码里常见的是 &lt;code&gt;_get_logits_processor&lt;/code&gt;）到底往生成流程里“塞了哪些规则”，以及这些规则是在每一步如何修改 logits 的；第二，在这些规则生效之后，Greedy/Sampling/Beam 这几类策略分别是“怎么选下一 token”的。&lt;/p&gt;
&lt;h3 id="21-logits-规则链是怎么构造出来的"&gt;2.1 logits 规则链是怎么构造出来的
&lt;/h3&gt;&lt;p&gt;（1）第一步“基操”，我们在 &lt;code&gt;prepared_logits_processor = self._get_logits_processor(&lt;/code&gt; 下个断点，其他不需要的断点都可以取消，然后重新以调试方式运行代码。&lt;/p&gt;
&lt;p&gt;（2）程序停下后先看看 &lt;code&gt;_get_logits_processor&lt;/code&gt; 内传入了哪些参数。如图 6-27，我们可以看到 &lt;code&gt;input_ids_length&lt;/code&gt; 的值为 4，那很显然“值如其名”这就是 &lt;code&gt;input_ids&lt;/code&gt; 的长度。然后关注一下 &lt;code&gt;model_kwargs&lt;/code&gt;，这里面多了两个不认识的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;logits_to_keep&lt;/code&gt; 是“本次 forward 只保留/只计算最后多少个位置的 logits”的提示参数（很多模型支持它来节省显存与计算量，当前取值是 1，表示只需要最后一个 token 位置的 logits 就够做 next-token 选择了）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;past_key_values&lt;/code&gt; 是自回归解码的 KV Cache（缓存每一层注意力的 key/value），用于在下一步生成时复用历史计算结果，避免每一步都把整个序列从头算一遍，从而显著加速逐 token 生成。当前的这个 &lt;code&gt;DynamicCache(layers=[DynamicLayer, ...])&lt;/code&gt; 就说明用的是 &lt;code&gt;DynamicCache&lt;/code&gt; 这种缓存结构，而 &lt;code&gt;layers=[DynamicLayer, ...]&lt;/code&gt; 有 12 个则表示模型有 12 层 Transformer block，每一层都有一份对应的缓存（每层一个 &lt;code&gt;DynamicLayer&lt;/code&gt;）。缓存会随着生成过程逐步增长（每生成一个 token，每层的 K/V 都会多一列），用于下一步注意力计算直接复用历史 K/V，从而加速逐 token 生成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_12.png" width="90%" alt="_get_logits_processor 调用" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-27 _get_logits_processor 调用&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（3）接着点几次步入就进入了 &lt;code&gt;_get_logits_processor()&lt;/code&gt; 的具体实现中，这时会看到 &lt;code&gt;processors&lt;/code&gt; 还是一个空的 &lt;code&gt;LogitsProcessorList()&lt;/code&gt;（因为规则链是“边判断边 append”的）。然后我们直接把断点下到最后 &lt;code&gt;return processors&lt;/code&gt; 的位置。恢复程序停在这行之后，如图 6-28 我们观察一下 &lt;code&gt;processors&lt;/code&gt; 的值具体包含了什么。我们会发现它已经变成一个“规则链”，里面依次追加了采样相关的 Warper，例如 &lt;code&gt;TemperatureLogitsWarper(temperature=0.7)&lt;/code&gt;（温度缩放，控制随机性强弱）和 &lt;code&gt;TopKLogitsWarper(top_k=50, filter_value=-inf)&lt;/code&gt;（只保留 top-k 候选，其余置为 &lt;code&gt;-inf&lt;/code&gt;，避免在全词表里乱抽）。如果我们同时设置了 &lt;code&gt;top_p/typical_p/epsilon_cutoff/eta_cutoff&lt;/code&gt;，这里也会出现对应的 Warper。所以实际上 &lt;code&gt;_get_logits_processor()&lt;/code&gt; 的作用就是把我们在 generation_config/kwargs 里写的“生成控制参数”，翻译成一个“每一步生成都会执行的 logits 处理规则链”（LogitsProcessorList）并按顺序组装好返回。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_13.png" width="90%" alt="processors 列表包含 Temperature/TopK 等 Warper" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-28 processors（logits 规则链）示例&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（4）回到 &lt;code&gt;_sample()&lt;/code&gt; 的循环，在 &lt;code&gt;next_token_scores = logits_processor(input_ids, next_token_logits)&lt;/code&gt; 这一行下个断点。恢复程序后，我们可以看几个变量。首先找到 &lt;code&gt;outputs&lt;/code&gt;，这个就是模型推理的输出，里面有个 &lt;code&gt;logits&lt;/code&gt; 属性。当前 &lt;code&gt;logits&lt;/code&gt; 的形状是 &lt;code&gt;(batch_size, seq_len, vocab_size)&lt;/code&gt;，这里会看到 &lt;code&gt;(1, 1, 50257)&lt;/code&gt;：其中第一个 &lt;code&gt;1&lt;/code&gt; 表示本次只推理 1 条输入（batch size = 1），第二个 &lt;code&gt;1&lt;/code&gt; 表示本次 forward 只保留了 1 个位置的 logits（因为 &lt;code&gt;logits_to_keep=1&lt;/code&gt;，只需要最后一个位置来做 next-token 选择），而 &lt;code&gt;50257&lt;/code&gt; 就是词表大小（vocab_size），表示对词表里的每个 token 都给了一个分数。再看 &lt;code&gt;next_token_logits&lt;/code&gt; 这个值来自 &lt;code&gt;outputs.logits[:, -1, :]&lt;/code&gt;，形状会变成 &lt;code&gt;(batch_size, vocab_size)&lt;/code&gt;（只取最后一个位置）。&lt;/p&gt;
&lt;p&gt;（5）点击步入，我们会来到 &lt;code&gt;logits_process.py&lt;/code&gt; 文件中 &lt;code&gt;LogitsProcessorList&lt;/code&gt; 类的魔法方法。可以看到程序停在了 &lt;code&gt;for processor in self:&lt;/code&gt;，这里的 self 是 &lt;code&gt;LogitsProcessorList&lt;/code&gt;（本质是一个 list 容器），所以这个循环是在依次遍历列表里的每一个 warper 对象（本次推理的 warper 对象是 &lt;code&gt;TemperatureLogitsWarper&lt;/code&gt; 和 &lt;code&gt;TopKLogitsWarper&lt;/code&gt;）。接着点击几次步过条件判断语句会把我们带到 &lt;code&gt;scores&lt;/code&gt; 赋值。然后点击步入，对于本次推理我们就跳转到了 &lt;code&gt;TemperatureLogitsWarper&lt;/code&gt; 类的魔术方法，这里可以看到 &lt;code&gt;scores_processed = scores / self.temperature&lt;/code&gt; 对 logits 做缩放（等价于把 logits 除以温度 $T$，$T&amp;lt;1$ 分布更尖更“确定”，$T&amp;gt;1$ 分布更平更“随机”）；&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 &lt;code&gt;TemperatureLogitsWarper&lt;/code&gt; 里，形参名叫 &lt;code&gt;scores&lt;/code&gt;，但它传进来的就是 softmax 之前的分数，所以也可以称其为 logits。这次缩放后面代入 softmax 后，就会让分布在 $T&amp;lt;1$ 时更尖锐、$T&amp;gt;1$ 时更平坦。假设模型输出为 $z=[2,1,0]$，温度缩放后用的是 $\mathrm{softmax}(z/T)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 $T=1$：就是原始 softmax，$e^2=7.389,e^1=2.718,e^0=1$，总和 $11.107$，所以概率约为 $[0.665,0.245,0.090]$。&lt;/li&gt;
&lt;li&gt;当 $T=0.5$（更小）：先除以 $0.5$ 得到 $[4,2,0]$，$e^4=54.598,e^2=7.389,e^0=1$，总和 $62.987$，概率约为 $[0.867,0.117,0.016]$，最大项更接近 1——分布更“尖”。&lt;/li&gt;
&lt;li&gt;当 $T=2$（更大）：先除以 $2$ 得到 $[1,0.5,0]$，$e^1=2.718,e^{0.5}=1.649,e^0=1$，总和 $5.367$，概率约为 $[0.506,0.307,0.186]$，更平均——分布更“平”。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;（6）接着步过这次调用，我们就又回到了 &lt;code&gt;LogitsProcessorList&lt;/code&gt; 类。然后重复刚才的步骤我们就来到了 &lt;code&gt;TopKLogitsWarper&lt;/code&gt; 类的魔术方法。它会先用 &lt;code&gt;torch.topk(scores, top_k)[0][..., -1, None]&lt;/code&gt; 取出“第 $k$ 大”的分数作为阈值，然后计算 &lt;code&gt;indices_to_remove = scores &amp;lt; threshold&lt;/code&gt; 得到一个布尔 mask；步过后看到 &lt;code&gt;indices_to_remove&lt;/code&gt; 里 &lt;strong&gt;True 表示“需要被过滤掉”的 token（不在 top-k 里）&lt;/strong&gt;，False 表示保留；最后用 &lt;code&gt;scores.masked_fill(indices_to_remove, -inf)&lt;/code&gt; 把这些 True 的位置统一置为 &lt;code&gt;-inf&lt;/code&gt;，这样进入 softmax 后它们的概率就是 0，采样时永远不会被选到。类似地：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;top_p&lt;/code&gt;（nucleus sampling）会按概率从高到低排序，只保留累计概率达到 $p$ 的最小 token 集合，其余置为 &lt;code&gt;-inf&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typical_p&lt;/code&gt;（typical sampling）会按“局部典型性（local typicality）”来筛选 token，保留满足阈值的 token 集合（官方文档的表述是：local typicality 衡量“预测某个 token 的条件概率”与“从该分布随机抽一个 token 的期望条件概率”有多相似）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;epsilon_cutoff&lt;/code&gt; 会在 &lt;strong&gt;softmax 概率空间&lt;/strong&gt;做阈值过滤：当 &lt;code&gt;epsilon_cutoff&lt;/code&gt; 设为 $0 &amp;lt; \epsilon &amp;lt; 1$ 时，只允许&lt;strong&gt;条件概率&lt;/strong&gt;大于 &lt;code&gt;epsilon_cutoff&lt;/code&gt; 的 token 参与采样，其余过滤；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eta_cutoff&lt;/code&gt; 属于 &lt;strong&gt;eta sampling&lt;/strong&gt;：它是 “locally typical sampling + epsilon sampling” 的混合形式。按官方文档描述，当 &lt;code&gt;eta_cutoff&lt;/code&gt; 设为 $0 &amp;lt; \eta &amp;lt; 1$ 时，一个 token 只有在满足以下条件之一时才会被考虑。其概率 $p$ 大于 &lt;code&gt;eta_cutoff&lt;/code&gt;，或 $p &amp;gt; \sqrt{\text{eta_cutoff}} \cdot e^{-\text{entropy}}$ （其中 &lt;code&gt;entropy&lt;/code&gt; 指当前分布的熵）。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;不管是 &lt;code&gt;top_k&lt;/code&gt; 还是 &lt;code&gt;top_p&lt;/code&gt; 以及其他的这些，本质上都是“裁剪规则”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;top_k&lt;/code&gt;：每一步只保留分数最高的 &lt;strong&gt;K 个 token&lt;/strong&gt;，其余置为 &lt;code&gt;-inf&lt;/code&gt;（候选数量固定）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;top_p&lt;/code&gt;：每一步按概率从高到低累加，保留“累计概率 ≥ p 的最小 token 集合”，其余置为 &lt;code&gt;-inf&lt;/code&gt;（候选数量动态，取决于分布“尖不尖”）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此 &lt;code&gt;top_k&lt;/code&gt; 越小，越容易反复选到高分 token，输出更稳定，但也更容易变得模板化/重复；&lt;code&gt;top_p&lt;/code&gt; 越小，也会更早截断到一小撮高概率 token，候选更少。
实际应用中 &lt;code&gt;temperature&lt;/code&gt; 和 &lt;code&gt;top_p&lt;/code&gt; 是“分工明确”的一对。&lt;code&gt;temperature&lt;/code&gt; 把分布变尖/变平（决定“随机性强弱”），&lt;code&gt;top_p&lt;/code&gt; 就是把低概率尾巴裁掉（决定“从哪些 token 里抽”）。&lt;code&gt;top_p&lt;/code&gt; 和 &lt;code&gt;top_k&lt;/code&gt; 没什么联动，如果同时开启，相当于是双重过滤，最后能被采样的 token 会落在“满足 top_k 的集合”和“满足 top_p 的集合”的交集里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;（7）继续步过，我们就跳出了 &lt;code&gt;logits_processor(...)&lt;/code&gt;，而这个时候的 &lt;code&gt;next_token_scores&lt;/code&gt; 就是被规则链修正后的 &lt;code&gt;logits&lt;/code&gt;。接着步过，如图 6-29，经过 softmax 和按 &lt;code&gt;probs&lt;/code&gt; 的概率分布随机抽样后我们就得到了第一个预测 token “&lt;strong&gt;2057&lt;/strong&gt;”。打开模型的 &lt;code&gt;vocab.json&lt;/code&gt; 文件，检索一下可以看到这个词是 “&lt;strong&gt;\u0120food&lt;/strong&gt;”，“&lt;strong&gt;\u0120&lt;/strong&gt;”其实就是我们在学习 GPT 的过程中学习过的“&lt;strong&gt;Ġ&lt;/strong&gt;”，这个前缀表示一个词的开始。那么本次推理的第一个单词显然就是 “&lt;strong&gt;food&lt;/strong&gt;”。继续步过，下面这行 &lt;code&gt;if has_eos_stopping_criteria:&lt;/code&gt; 就是判断某条序列是否已经“结束了”，如果结束了就把它后续每一步的 next_token 设成 pad_token_id，避免它继续生成乱七八糟的 token，同时保持 batch 里所有序列长度一致。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_14.png" width="90%" alt="第一个预测 token" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-29 第一个预测 token&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;（8）最后继续步过，执行完 &lt;code&gt;input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)&lt;/code&gt; 我们就能看到新的 token 已经追加到了 &lt;code&gt;input_ids&lt;/code&gt; 后面。后面的 &lt;code&gt;unfinished_sequences = unfinished_sequences &amp;amp; ~stopping_criteria(input_ids, scores)&lt;/code&gt; 就是判断本次 &lt;code&gt;while&lt;/code&gt; 循环是否结束的，如图 6-30 所示，如果判断不停止就继续循环推理并生成下一个词。调试结束后就得到了本次的生成结果 “I like eating fried food. I&amp;rsquo;m in”。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_15.png" width="90%" alt="继续循环推理" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-30 继续循环推理&lt;/em&gt;
&lt;/p&gt;
&lt;h3 id="22-常用解码策略"&gt;2.2 常用解码策略
&lt;/h3&gt;&lt;p&gt;（1）Greedy Search（默认策略之一）：确定性地选最大值&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;num_beams=1&lt;/code&gt; 且 &lt;code&gt;do_sample=False&lt;/code&gt; 时，&lt;code&gt;generate()&lt;/code&gt; 通常会选择 Greedy Search（我们前面看到的 &lt;code&gt;GenerationMode.GREEDY_SEARCH&lt;/code&gt;）。底层循环的核心非常简单：每一步 forward 得到 &lt;code&gt;outputs.logits[:, -1, :]&lt;/code&gt;，先过一遍 &lt;code&gt;logits_processor(...)&lt;/code&gt;，然后直接 &lt;code&gt;argmax&lt;/code&gt; 选出分数最大的 token 作为 &lt;code&gt;next_tokens&lt;/code&gt;，再把它拼回 &lt;code&gt;input_ids&lt;/code&gt; 进入下一轮，直到 &lt;code&gt;stopping_criteria&lt;/code&gt; 判定停止。Greedy 的好处是速度快、结果稳定，适合做 baseline 或需要强确定性的场景；缺点是容易陷入局部最优，开放式续写时更容易产生重复或“套路化”的输出。&lt;/p&gt;
&lt;p&gt;（2）Sampling（抽样，LLM 开放式生成中最常用的家族）&lt;/p&gt;
&lt;p&gt;在对话/开放式文本生成里，更常见的做法是开启采样（&lt;code&gt;do_sample=True&lt;/code&gt;），并配合 &lt;code&gt;temperature/top_p/top_k&lt;/code&gt; 来控制多样性与稳定性。以你已经步入过的 &lt;code&gt;_sample()&lt;/code&gt; 为例：每一步拿到 &lt;code&gt;next_token_scores&lt;/code&gt; 后先做 softmax 得到概率分布，再用 &lt;code&gt;torch.multinomial&lt;/code&gt; 从该分布中抽一个 token；而 &lt;code&gt;temperature/top_p/top_k&lt;/code&gt; 这些“看起来是策略参数”的东西，通常就是在 &lt;code&gt;logits_processor&lt;/code&gt;（或同类 warper）里提前把分布处理好，保证采样只在合理候选集里进行。也就是说：Sampling 的“随机性”不是乱抽，而是“先按规则改分布，再从改过的分布里抽”。（Transformers 官方也把这类方法归为 generation strategies 的核心内容，可参考 &lt;a class="link" href="https://huggingface.co/docs/transformers/main/generation_strategies" target="_blank" rel="noopener"
&gt;Hugging Face 的 generation strategies 文档&lt;/a&gt;。）&lt;/p&gt;
&lt;p&gt;（3）Beam Sample（束采样）：beam 框架 + 采样&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;num_beams&amp;gt;1&lt;/code&gt; 且 &lt;code&gt;do_sample=True&lt;/code&gt;（并且 &lt;code&gt;num_beam_groups==1&lt;/code&gt;）时，&lt;code&gt;generate()&lt;/code&gt; 通常会选择 &lt;code&gt;GenerationMode.BEAM_SAMPLE&lt;/code&gt;。你可以把它理解成“beam search 的框架不变，但每一步的扩展不再是纯 top-k 硬选，而是引入采样带来的随机性/多样性”。它的适用场景通常是既希望保留多条候选路径的搜索能力，又希望输出不要过于死板；但在大模型开放式对话里，工程上更常见的仍是直接 Sampling（&lt;code&gt;num_beams=1&lt;/code&gt;）+ top-p/temperature，Beam Sample 相对少见一些，你可以把它当作“可选的折中策略”。&lt;/p&gt;
&lt;p&gt;（4）Beam Search（束搜索）：更“序列级”的搜索，但更慢也更保守&lt;/p&gt;
&lt;p&gt;当 &lt;code&gt;num_beams&amp;gt;1&lt;/code&gt; 且 &lt;code&gt;do_sample=False&lt;/code&gt; 时，&lt;code&gt;generate()&lt;/code&gt; 通常会进入 &lt;code&gt;GenerationMode.BEAM_SEARCH&lt;/code&gt;。beam 框架的核心是：每一步同时维护 &lt;code&gt;num_beams&lt;/code&gt; 条候选序列（beams），对每条序列扩展出若干候选 token，并根据“累计分数”（常见做法是对 log 概率求和，并可能加上 &lt;code&gt;length_penalty&lt;/code&gt;）进行排序剪枝，只保留最好的 &lt;code&gt;num_beams&lt;/code&gt; 条继续滚动；这能更接近“序列级最优”，因此在翻译/摘要等任务里经常很稳。但在开放式生成中，beam search 往往更保守，也更容易出现某些重复/模板化模式，同时计算开销更大，所以在大语言模型的聊天式生成里并不是首选策略之一（这也是为什么很多推理默认更偏向 sampling 家族）。&lt;/p&gt;
&lt;p&gt;（5）其他策略&lt;/p&gt;
&lt;p&gt;如果我们在 &lt;code&gt;generation_mode&lt;/code&gt; 里遇到下面这些分支，一般知道“它们解决什么问题”就够了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Group Beam Search&lt;/strong&gt;：把 beam 分组来增加多样性，缓解 beam search 的同质化；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contrastive Search&lt;/strong&gt;：用 &lt;code&gt;penalty_alpha&lt;/code&gt; 等机制在“高概率/低重复”之间折中；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrained Beam Search&lt;/strong&gt;：在 beam search 上加硬约束（例如必须包含某些词/短语），用于强控制生成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三调试技巧"&gt;三、调试技巧
&lt;/h2&gt;&lt;blockquote&gt;
&lt;p&gt;老东西，终于把焚决交出来了！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过这整个调试过程，可以发现为了教学方便这里的步骤是一个线性的过程。不过实际上在面对不熟悉的代码时，真实的调试过程并不是这样的。还是以这个 GPT 的代码为例，假设我们是第一次拿到这段 pipeline 的代码。如图 6-31，除了我们认识的导库、环境配置的一些操作，唯一不认识的就是这两行代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;text-generation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pipeline_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_en&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_new_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_return_sequences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_16.png" width="90%" alt="化繁为简" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-31 化繁为简&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;那么首先我们就在 &lt;code&gt;pipeline_outputs = generator(...)&lt;/code&gt; 下个断点，看看 &lt;code&gt;generator&lt;/code&gt; 是什么类型。发现这是个对象后，那我们唯二不认识的代码行就只剩一行了，第一行无非就是创建了一个对象。接着步入这段代码会来到有三只小猫注释的“魔法方法”。这里主要是一些输入格式判断和改写，我们可以直接跳到最后的 &lt;code&gt;return super().__call__(...)&lt;/code&gt; 并继续步入。再次来到了一大段看起来像“框架胶水”的代码，也还是先定位到最后的 &lt;code&gt;return&lt;/code&gt; 并步入。通过 &lt;code&gt;return self.run_single(...)&lt;/code&gt; 步入之后我们就来到了图 6-32 所示的 &lt;code&gt;run_single()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_17.png" width="90%" alt="run_single 主流程" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-32 run_single() 主流程&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;虽然这里我们不知道 &lt;code&gt;run_single()&lt;/code&gt; 里面这几段是干什么的，不过通过变量名还是多少能猜出来这里就是我们要找的主要流程，从模型输入（&lt;code&gt;model_inputs&lt;/code&gt;）到模型输出（&lt;code&gt;outputs&lt;/code&gt;），还有眼熟的 &lt;code&gt;forward&lt;/code&gt;。猜不到也没关系，从 &lt;code&gt;model_inputs = self.preprocess(...)&lt;/code&gt; 开始继续步入，就来到了 &lt;code&gt;preprocess()&lt;/code&gt; 方法，依然是一堆看起来没什么大用的代码；继续往下走回到 &lt;code&gt;run_single()&lt;/code&gt;。但是在这步结束后我们是能够看到输出，也就是 &lt;code&gt;model_inputs&lt;/code&gt; 的值，如果觉得这个输出包含有用的东西，那说明 &lt;code&gt;preprocess()&lt;/code&gt; 中有我们需要的代码逻辑，可以在 &lt;code&gt;model_inputs = self.preprocess(...)&lt;/code&gt; 下个断点，然后重新开始调试。现在先不管这部分，步入 &lt;code&gt;forward()&lt;/code&gt; 方法。如图 6-33，我们又看到了两处眼熟的 &lt;code&gt;forward&lt;/code&gt;，那就步过，看程序会走哪处判断，接着继续步入 &lt;code&gt;model_outputs = self._forward(...)&lt;/code&gt;。&lt;/p&gt;
&lt;p align="center"&gt;
&lt;img src="https://cdn.jsdelivr.net/gh/Hanguangwu/MyImageBed01/img/6_3_18.png" width="90%" alt="forward() 方法" /&gt;
&lt;br /&gt;
&lt;em&gt;图 6-33 forward() 方法&lt;/em&gt;
&lt;/p&gt;
&lt;p&gt;到了 &lt;code&gt;_forward()&lt;/code&gt; 方法后，继续步过。需要注意在步过的同时我们还需要关注变量窗口，看看有没有什么可能有用的变量。当我们步过 &lt;code&gt;output = self.model.generate(...)&lt;/code&gt;，明显会得到一个大概率有用的 output 变量，那就在 &lt;code&gt;output = self.model.generate(...)&lt;/code&gt; 这行下个断点，然后中止调试后重启调试。回到 &lt;code&gt;output = self.model.generate(...)&lt;/code&gt; 后，两次步入就来到了 &lt;code&gt;generate()&lt;/code&gt; 方法。在 &lt;code&gt;generate()&lt;/code&gt; 中可以看到清晰的 1~9 的步骤注释，这有助于我们定位到需要分析的代码行。当然不是所有代码中都有这么清晰的注释，所以当前我们已知 &lt;code&gt;output&lt;/code&gt; 的值是最后 return 来的，那就直接定位到最后的 &lt;code&gt;return result&lt;/code&gt;。然后开始通过 Ctrl + B 转到定义回溯这个 &lt;code&gt;result&lt;/code&gt; 是哪来的，这样就定位到了 &lt;code&gt;result = decoding_method(&lt;/code&gt;，在这里下个断点。接着看 &lt;code&gt;decoding_method()&lt;/code&gt; 中传入的参数，比如我想了解 &lt;code&gt;stopping_criteria=prepared_stopping_criteria&lt;/code&gt; 这个参数是干嘛的，继续通过 Ctrl + B 转到定义回溯这个 &lt;code&gt;prepared_stopping_criteria&lt;/code&gt; 是哪来的，然后下断点。以此类推，最后我们就回到了 &lt;code&gt;generate()&lt;/code&gt; 开头的地方，这个时候需要的断点也已经都下好了。恢复程序后，顺着所下断点进行步过步入等操作后，我们就能完整分析出整个过程的数据流。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="参考资料"&gt;参考资料
&lt;/h2&gt;</description></item></channel></rss>