@helloiamleonie:与@liquidai团队一起编写这些工程博客真是太有趣了!这是我们一直在做的:Reas…
摘要
这篇文章解释了推理模型中的“末日循环”,即模型重复诸如“Wait”之类的令牌,直到上下文填满,并介绍了FTPO(最终令牌偏好优化)作为训练时的修复方法。相关的Antidoom工具显著降低了末日循环率(例如,在Qwen3.5-4B上从22.9%降至1%)。
查看缓存全文
缓存时间: 2026/07/07 20:22
与@liquidai团队一起撰写这些工程博客真是太有趣了!这是我们一直在做的事情:推理模型可能会在思考中途卡住。我们称之为“死循环”(doom loop)。死循环发生时,模型会重复同一个词元(“Wait”、“Let me reconsider”),直到上下文窗口填满。导致死循环的三个原因: • 过度训练的词元:在不确定情况下,“Wait”和“Alternatively”占据主导 • 自强化上下文:每次重复都会提高下一次出现的概率 • 贪婪采样:在低温度下,最可能的词元总是胜出
标准的修复方案是在推理时使用repetition_penalty(重复惩罚)。它治标不治本(而且会降低性能)。但真正的根本原因是:一个词元触发了整个循环。因此他们构建了FTPO(Final Token Preference Optimization,最终词元偏好优化)来在训练时修复它:
- 识别出在生成中期开启循环的那个词元
- 将其标记为“被拒绝“,并从基础模型中采样最多20个替代词元
- 重新分配概率
训练后的死循环率: LFM2.5-2.6B: 10.2% → 1.4% Qwen3.5-4B: 22.9% → 1%
代码:http://github.com/Liquid4All/antidoom… 博客:https://liquid.ai/blog/antidoom
Liquid4All/antidoom
来源:https://github.com/Liquid4All/antidoom
Antidoom
Antidoom生成并训练针对性的偏好数据,用于减少模型重复循环(死循环)。它是一个针对特定失败模式的窄工具:对模型补全进行采样,检测重复跨度开始的位置,将第一个开启循环的词元标记为“被拒绝“,选择合理的替代后续词元,然后使用最终词元偏好优化(FTPO)训练LoRA适配器。该方法将Antislop(https://arxiv.org/abs/2510.15061)及其auto-antislop实现(https://github.com/sam-paech/auto-antislop)中的单词元偏好训练思路适配到推理过程中失控重复这一具体问题上。
前提条件
Antidoom需要一个支持PyTorch和vLLM的GPU运行时。默认路径是NVIDIA/CUDA;AMD/ROCm需要单独的环境。
- NVIDIA / CUDA: 使用固定的
uv.lock和configs/default.yaml。 - AMD / ROCm(Instinct MI系列): 不要使用默认的
uv.lock,它仅支持CUDA。请参见AMD / ROCm创建ROCm虚拟环境,并使用configs/default_amd.yaml运行,该文件添加了所需的attention_backend: TRITON_ATTN和optim: adamw_torch覆盖。
快速开始
git clone https://github.com/Liquid4All/antidoom
cd antidoom
uv sync
编辑configs/default.yaml,将model_name设置为你想进行antidoom处理的检查点。默认配置从LiquidAI/antidoom-mix-v1.0(https://huggingface.co/datasets/LiquidAI/antidoom-mix-v1.0)读取提示词,生成FTPO对,训练LoRA适配器,并将其合并。
运行完整的生成+训练流程:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name LiquidAI/LFM2.5-1.2B-Base
这会将生成的补全和FTPO对写入runs/antidoom1/,在配对文件上训练,并将适配器和合并后的模型写入同一运行目录。
为什么死循环会发生
死循环通常在三种力量同时作用时出现:
- 过度训练的词元。 常见的推理词元如
Wait、So、But、Alternatively,在大量合成推理训练后可能变得异常吸引人。当模型不确定时,这些词元可能主导下一个词元的分布,而不推动推理前进。 - 自强化上下文。 一旦出现短序列,先前的上下文会使该序列更可能再次出现。在同一循环的重复轮次中,每个词元的概率可能会攀升至确定性。
- 低温采样。 在温度为0或接近0时,模型会持续选择最高概率的延续,因此局部强化的循环没有自然的逃逸路径。
Antidoom在循环开始的词元处攻击故障。它不训练完整的正确答案,而是仅训练局部偏好:不要选择开启重复的词元;选择同一位置上可用的合理替代项之一。
工作原理
对于每个提示词,Antidoom生成一个补全并扫描其中的内部重复。当发现循环时,它会在词元空间中细化边界,使被拒绝的词元成为重复段的第一个可读词元。每个FTPO行包含:
- 一个提示词/上下文前缀,在被拒绝的词元之前结束
- 一个被拒绝的词元,即开启循环的那个词元
- 一个或多个从该位置筛选出的替代词元中选择的“被选“词元
- 关于源提示词、生成的补全和检测到的循环的元数据
训练步骤会对过度表示的被拒绝和被选词元进行正则化,使适配器学习广泛的抗循环偏好,而不是简单地抑制一个词。
数据集
默认配置使用LiquidAI/antidoom-mix-v1.0(https://huggingface.co/datasets/LiquidAI/antidoom-mix-v1.0),这是一个为此流程构建的仅含提示词的ShareGPT混合数据集。该数据集有意排除了正确答案、推理过程、隐藏测试、验证器目标和答案标签。数据集构建脚本和许可说明位于datasets/。
配置
从configs/default.yaml开始。最常见的要修改的字段包括:
model_name:基础检查点或Hugging Face模型IDgeneration.hf_dataset:提示词数据集,或设置generation.input_jsonlgeneration.prompt_field:从本地/HF行中读取的列generation.target_pairs:要收集的FTPO对数量generation.temperature或generation.temperatures:采样温度train.max_train_examples:用于训练的最大FTPO行数train.output_dir和train.merged_output_dir:适配器和合并模型目录
prompt_field具有权威性:生成阶段读取该列并根据其形状解析值。字符串被视为普通用户提示词,OpenAI风格的[{"role": ..., "content": ...}]列表用作聊天消息,ShareGPT风格的[{"from": ..., "value": ...}]列表会转换为聊天消息。解析器不会从列名推断提示词格式。
重要超参数
max_train_examples
在训练之前生成足够的偏好样本非常重要。产生的样本数量取决于生成步骤可用的提示词数量、您运行的温度遍历次数以及检查点的死循环倾向。经验法则是,从至少15k个提示词开始,目标是产生大约15k-20k个偏好行。将max_train_examples设置为低于生成的偏好行数量,例如从15k-20k的生成集中取12k行。这给rejected_regularisation_strength留出空间来削减过度表示的被拒绝词元。正则化很重要:没有它,生成集可能严重不平衡,从而导致训练结果不佳。
使用提示:将max_train_examples设置为生成数据集中实际偏好行数的最多70%。
learning_rate
Antidoom训练器可能训练不足或过训练。如果过训练,模型可能会退化并产生更多的死循环,因此正确的学习率可能需要一些试错。
使用提示:在训练约12k样本时,0.00001-0.00002是一个不错的起始范围。早停有助于避免过训练。
early_stopping_chosen_win
当chosen_win超过配置值时停止训练。chosen_win是被选词元战胜被拒绝词元的样本比例。大约0.15-0.3时,通常应该能看到死循环显著减少。超过0.5的训练可能是过训练,但需要更多消融实验。
使用提示:尝试0.4,然后据此调整。
其他超参数
generation.*
这些控制生成阶段,我们试图在该阶段引出死循环以构建偏好训练集。默认值通常是一个合理的起点。
rejected_regularisation_strength
通过剔除样本来平滑被拒绝词元频率的分布。某些被拒绝词元出现的频率远高于其他词元,因为它们是开启死循环的词元。如果训练集过于不平衡,这些词元可能被过度抑制。
使用提示:默认值0.3是一个不错的起点。
chosen_regularisation_strength
平滑被选词元频率的分布。如果某个被选词元过度表示,其概率可能增加过多,从而可能开始引起自身的循环。此步骤通常修剪被选替代项而非整个样本,因为每个样本可能包含多个被选词元。
使用提示:默认值0.5是一个不错的起点。
filter_rejected_stop_words
过滤被拒绝词元是停用词(如the或a)的样本。保持设置为false;常见词确实可能是循环开启词元,而正则化步骤是处理频率的更安全方式。
source_balance_mode
在被拒绝词元正则化之后,可选择按来源平衡训练集。默认off即可。旧配置可能将此称为balance_by_source_task。
min_chosen_tokens
一个样本被包含所需的最少被选词元数量。有时要求多于一个被选词元有助于避免在单个首选词元上模式崩溃,但被选词元正则化已经缓解了这个问题,对于Antidoom训练来说1通常没问题。
lora_r
比通常更高的LoRA秩往往能提高可学习性且退化更少。如果不确定,使用128或256。
lora_alpha
设置为与lora_r相同的值,或者lora_r的一半。
lora_dropout
尚未经过大量消融,但当前运行中0效果良好。
target_modules
之前的Antislop消融研究发现,训练所有层更容易学习,但也可能更容易退化;限制到up_proj、down_proj和lm_head在那里有帮助。Antidoom训练似乎倾向于训练所有层。安全的起点是:
["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", "lm_head"]
freeze_early_layers
这可以减少模型影响和退化,但会牺牲可学习性。在实践中,Antidoom训练在冻结关闭时效果良好。
lambda_mse_target
对被选和被拒绝logits与参考模型的MSE约束强度。
tau_mse_target
在目标logits的MSE损失生效之前,与参考模型的免惩罚logit增量。
lambda_mse
对剩余词汇(不包括被选和被拒绝logits)与参考模型的MSE约束强度。通常设置得比lambda_mse_target高,因为被选/被拒绝logits应该更自由地移动,而词汇表的其余部分保持接近参考模型。
clip_epsilon_logits
一旦被选词元以这个logit差距击败被拒绝词元,偏好损失就会关闭。
生成与训练
默认情况下,antidoom是一条流水线。它生成偏好行直到满足generation.target_pairs,然后训练。如果目标配对JSONL已经存在且有足够的行,生成步骤立即返回并开始训练。
uv run antidoom -c configs/default.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name LiquidAI/LFM2.5-1.2B-Base
使用-r runs/antidoom1,生成步骤写入:
runs/antidoom1/iter_0_generations.jsonl
runs/antidoom1/iter_0_ftpo_pairs.jsonl
训练步骤写入:
runs/antidoom1/lora
runs/antidoom1/
分阶段命令
仅运行生成:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 generate \
--model-name LiquidAI/LFM2.5-1.2B-Base
从现有配对文件训练:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 train \
--dataset-jsonl runs/antidoom1/iter_0_ftpo_pairs.jsonl \
--model-name LiquidAI/LFM2.5-1.2B-Base
合并适配器:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 merge \
--model-name LiquidAI/LFM2.5-1.2B-Base
过滤
生成步骤会跳过超过generation.max_prompt_tokens的提示词。生成的偏好数据集经过正则化,使得任何给定词元在“被选“或“被拒绝“集合中不会出现过于频繁。
输出
每个FTPO行包含如下字段:
{
"source_task": "openai/gsm8k",
"source_sample_id": "1234",
"context_with_chat_template": "... text before rejected token ...",
"rejected_decoded": " Wait",
"multi_chosen_decoded": [" Therefore", " The"],
"repetition": {
"start_char": 1024,
"repeat_start_char": 1526,
"rejected_token_index": 342
}
}
iter_0_generations.jsonl存储完整的生成补全和检测器元数据。
iter_0_ftpo_pairs.jsonl存储用于训练的偏好行。
AMD / ROCm
上述快速开始针对NVIDIA/CUDA,其固定的uv.lock仅支持CUDA,无法驱动AMD GPU。在AMD上,您需要从vLLM预构建的ROCm wheel中构建一个单独的ROCm虚拟环境,并使用configs/default_amd.yaml运行——其关键设置是attention_backend: TRITON_ATTN,因为vLLM默认的ROCM_ATTN内核在LFM2上会内存错误。使用uv run --no-sync antidoom ...运行——需要--no-sync:裸uv run(如快速开始中)会重新同步虚拟环境到仅CUDA的uv.lock,从而破坏ROCm安装。
已在**Instinct MI325 (gfx942)**上验证。vLLM的ROCm wheel支持MI200/MI300/MI350和Radeon RX 7900/9000;请参阅vLLM的GPU安装指南(https://docs.vllm.ai/en/stable/getting_started/installation/gpu.html)了解支持矩阵和ROCm要求(ROCm 6.3+,或MI350需要7.0+)。
环境设置(ROCm虚拟环境)
主机需要ROCm 6.3+(rocminfo列出您的GPU)和Python 3.12(已在.python-version中固定)。从仓库根目录,在一个命令中创建虚拟环境并安装antidoom + ROCm torch/vLLM——--extra-index-url优先使用ROCm wheel索引,因此torch/vllm会解析为它们的ROCm构建,而非CUDA PyPI wheel(参考https://docs.vllm.ai/en/stable/getting_started/quickstart/#installation):
uv venv && uv pip install -e . --extra-index-url https://wheels.vllm.ai/rocm/
验证:
uv run --no-sync python -c "import torch, vllm; print(torch.__version__, torch.version.hip, vllm.__version__)"
# torch应该打印一个'+rocm...'构建和一个非None的torch.version.hip
ROCm特定设置(已在configs/default_amd.yaml中)
configs/default_amd.yaml是在default.yaml基础上精确添加以下覆盖:
generation.vllm_kwargs
vllm_kwargs:
enforce_eager: true # eager在ROCm上稳定且快速
attention_backend: TRITON_ATTN # 修复:ROCM_ATTN在LFM2上内存错误
attention_backend值被传递给vLLM引擎。在最近的vLLM中,VLLM_ATTENTION_BACKEND环境变量是无效的——必须使用引擎参数,而vllm_kwargs会传递它。
train.optim: adamw_torch——NVIDIA默认的paged_adamw_32bit需要bitsandbytes(在ROCm上很麻烦)。adamw_torch是相同的32位AdamW数学;其“分页“CPU卸载在256 GB的MI325上毫无意义。
多GPU生成路径还在每个工作进程上设置(在generate.py中,无需配置):
HIP_VISIBLE_DEVICES(ROCm无法仅通过CUDA_VISIBLE_DEVICES可靠地固定GPU),每个工作进程的节点本地VLLM_CACHE_ROOT/TORCHINDUCTOR_CACHE_DIR/TRITON_CACHE_DIR(共享JIT缓存存在竞争→“内存访问错误”),以及探测到的空闲端口用于VLLM_PORT(避免EADDRINUSE)。这些在NVIDIA上是空操作。
在AMD上运行
与快速开始相同的命令,但使用configs/default_amd.yaml和uv run --no-sync(--no-sync阻止uv run重新同步虚拟环境回到CUDA锁):
uv run --no-sync antidoom -c configs/default_amd.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name ...
生成使用一个单…
相似文章
@LiorOnAI: 一种针对最普遍推理模型故障模式的开源修复方案。今年最大的AI趋势之一并非…
Liquid AI 发布了 Antidoom,这是一种开源方法,通过微调推理模型来打破重复的token循环(末日循环),在 Qwen3.5-4B 上无需重新训练或强化学习即可将故障率从约23%降至1%。
利用Final Token Preference Optimization减少Doom循环
Liquid AI推出了Antidoom方法,该方法使用Final Token Preference Optimization来减少小型推理模型在推理过程中的重复性Doom循环,将循环率从10.2%降至1.4%,同时提升了评估分数。
@maximelabonne: 新的训练技术来减少 doom loops!我们将其应用于 LFM2.5-2.6B (SFT checkpoint) 和 Qwen3.5-4B。通过减少 doom…
Liquid AI 发布了 Antidoom,一种用于减少推理模型中 doom loops 的开源方法,应用于 LFM2.5-2.6B 和 Qwen3.5-4B,显著降低了 doom-loop 率并提高了评估分数。
@Raman_bansal_: 如果你训练过LLM,你可能见过doom loop——LLM无休止地重复同一个token或句子,……
一篇Substack文章解释了LLM中的doom loop问题——模型会无休止地重复token,并介绍了Liquid AI提出的Final Token Preference Optimization(FTPO)方法,用于在微调过程中检测并修复此类循环。
@shannholmberg: 如何通过先设计循环再运行来优化你的 /goal 设计不当的循环会浪费 token 并快速产生垃圾,因此花时间设计循环框架至关重要
本文介绍了 Looper 工具,它通过批判性分析循环、构建验证评分标准、设置护栏和 token 预算,并导出可用于 Claude Code 的可移植工件,帮助设计高效的 AI 目标循环。