Prompt-lookup decoding advances stateful logits processors (CFG, SynthID watermark) on draft tokens, so output differs from generation witho

在 Transformers 中使用 prompt_lookup_num_tokens (prompt-lookup 投机解码)同时又启用了有状态的 LogitsProcessor(CFG 的 guidance_scale 、SynthID 水印等)时,草稿阶段与验证阶段会重复推进处理器内部状态,导

快速结论:在 Transformers 中使用 prompt_lookup_num_tokens(prompt-lookup 投机解码)同时又启用了有状态的 LogitsProcessor(CFG 的 guidance_scale、SynthID 水印等)时,草稿阶段与验证阶段会重复推进处理器内部状态,导致输出与不加 prompt lookup 时不一致。优先检查生成参数里是否同时出现了 prompt-lookup 与 CFG/水印配置。

适用环境:Issue 复现于 transformers 5.21.0(main @ 494470d6)、torch 2.11.0+cpu、Python 3.12、Linux (WSL2),仅 CPU;另有评论者在 transformers 5.15.0 / torch 2.13.0+cpu / Python 3.10.11 及 5.20.2 / torch 2.14.1+cpu / Python 3.10 复现,说明不是单一版本回归。

最快修复方案:暂无确认的一步修复方案。Issue 中确认的方向是:在使用 prompt lookup(以及同类 assisted 解码)时,若处理器列表中存在已知的有状态内置处理器(UnbatchedClassifierFreeGuidanceLogitsProcessor、WatermarkLogitsProcessor、SynthIDTextWatermarkLogitsProcessor),直接报错拒绝该配置,而不是静默退回普通解码;该修复由 PR #50339 落地。

注意事项:该守卫只覆盖已知的内置有状态处理器;通过 logits_processor= 传入的自定义有状态处理器仍会继续推进状态。完整的“处理器状态与已接受前缀对齐”方案需要为 LogitsProcessor 引入状态性约定,属于后续工作。同一处理器的两处调用点也服务于 assistant-model 解码,Issue 中未做测试。

问题场景

使用 model.generate() 并设置 do_sample=False 的同时开启 prompt_lookup_num_tokens,且额外传入 guidance_scale(CFG)或 watermarking_config(SynthID 文本水印)。按预期 prompt lookup 只应加速、不改变 token 结果;实际在 CFG 下从第 1 个生成 token 起分歧,在 SynthID 水印下从第 0 个 token 起分歧。普通 greedy 与无状态处理器(如 repetition_penalty)作为对照则输出一致。

报错原文

plain greedy (control): identical=True first_mismatch=None
repetition_penalty=1.3 (stateless processor, control): identical=True first_mismatch=None
guidance_scale=1.5: identical=False first_mismatch=1
SynthID watermark: identical=False first_mismatch=0

说明:上述为复现脚本的输出对比,并非异常堆栈;问题表现为开启 prompt_lookup_num_tokens 与不开启时的生成结果不同。

原因分析

Prompt lookup 会在两个普通生成不会调用的位置调用同一组处理器实例:

  • 候选筛选阶段:PromptLookupCandidateGenerator 对伪造 logits 调用 logits-processor 列表,每个草稿 token 一次(candidate_generator.py 约第 1116 行)。
  • 验证阶段:_assisted_decoding 对每个候选位置调用一次,包括第一个被拒绝的草稿 token 之后的位置(utils.py 约第 4090 行)。

UnbatchedClassifierFreeGuidanceLogitsProcessor 会跨调用保留无条件上下文及其 KV cache;SynthIDTextWatermarkLogitsProcessor 会保留 n-gram 上下文、调用计数和上下文历史。每次额外调用都会推进这些状态,且没有恢复逻辑。阶段一在验证之前执行,因此即使第一个被验证的位置也可能看到错误的 state.num_calls,这与 SynthID 在第 0 个 token 就出现分歧的现象吻合。评论中的调用计数显示:40 token 生成在无 prompt lookup 时为 40 次调用,开启后为 46 次,且处理器收到的 input_ids 长度会出现回退(如 20→21→22→20),即候选筛选阶段用未扩展的序列再次进入处理器。

环境排查

  • transformers 版本:5.15.0、5.20.2、5.21.0 均可复现,先确认是否使用了 prompt-lookup 加速。
  • torch 版本与设备:2.11.0+cpu、2.13.0+cpu、2.14.1+cpu,Issue 中均为纯 CPU 环境。
  • Python 版本:3.10.11、3.12(main 分支需 Python >=3.11)。
  • 生成参数:do_sample=False、max_new_tokens、prompt_lookup_num_tokens、guidance_scale、watermarking_config、logits_processor(是否存在自定义有状态处理器)。
  • 是否走 assistant-model 类的 assisted 解码路径(Issue 中提到同一调用点也覆盖该路径,但未测试)。

解决步骤

  1. 先定位冲突配置:检查 generate() 调用中是否同时设置了 prompt_lookup_num_tokens 与 guidance_scale,或同时设置了 prompt_lookup_num_tokens 与 SynthID 的 watermarking_config。
  2. 确认问题存在:分别在不开启与开启 prompt_lookup_num_tokens 的情况下生成,比较续写 token 序列;若出现如上 first_mismatch 且不在索引 0 或 1 处对齐,即可判定为状态被推进。
  3. 临时规避:在需要 CFG 或 SynthID 水印的生成中不要开启 prompt lookup,回到普通生成;或在使用 prompt lookup 的生成中移除这些有状态处理器。
  4. 应用包含 PR #50339 的修复版本后,在启用 prompt lookup 时配置已知有状态内置处理器将直接报错,此时按提示移除冲突配置即可。
  5. 若使用自定义 logits_processor= 且其自身持有跨调用状态,仍需自行避免与 prompt lookup 或 assisted 解码同时使用,因为窄范围守卫不覆盖自定义处理器。

验证方法

使用 Issue 中的对照脚本思路:对同一重复 prompt,分别用 prompt_lookup_num_tokens=None 和 prompt_lookup_num_tokens=3 生成 do_sample=False 的续写并逐 token 比较。无状态处理器(plain greedy、repetition_penalty)应保持 identical=True;应用修复后,CFG 与 SynthID 配置在启用 prompt lookup 时应被明确拒绝(抛出错误)而非静默产生不同输出。

参考来源

huggingface/transformers #50051(修复 PR:#50339)

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 33388

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注