@helloiamleonie: Working with the @liquidai team on these engineering blogs is just so much fun! Here's what we've been working on: Reas…
Summary
The post explains doom loops in reasoning models where the model repeats tokens like 'Wait' until the context fills up, and introduces FTPO (Final Token Preference Optimization) as a training-time fix. The associated Antidoom tool reduces doom loop rates significantly (e.g., from 22.9% to 1% on Qwen3.5-4B).
View Cached Full Text
Cached at: 07/07/26, 08:22 PM
Working with the @liquidai team on these engineering blogs is just so much fun!
Here’s what we’ve been working on:
Reasoning models can get stuck mid-thought.
That’s what we call a “doom loop”.
A doom loop happens when the model repeats the same token (“Wait”, “Let me reconsider”) until the context window fills up.
3 things cause this: • 𝗢𝘃𝗲𝗿𝘁𝗿𝗮𝗶𝗻𝗲𝗱 𝘁𝗼𝗸𝗲𝗻𝘀: “Wait” and “Alternatively” dominate under uncertainty • 𝗦𝗲𝗹𝗳-𝗿𝗲𝗶𝗻𝗳𝗼𝗿𝗰𝗶𝗻𝗴 𝗰𝗼𝗻𝘁𝗲𝘅𝘁: each repeat raises the probability of the next one • 𝗚𝗿𝗲𝗲𝗱𝘆 𝘀𝗮𝗺𝗽𝗹𝗶𝗻𝗴: at low temperature, the most likely token always wins
The standard fix is repetition_penalty at inference. It patches the symptom, not the root cause (and degrades performance).
But the actual root cause is this: 𝗼𝗻𝗲 𝘁𝗼𝗸𝗲𝗻 𝘁𝗿𝗶𝗴𝗴𝗲𝗿𝘀 𝘁𝗵𝗲 𝗲𝗻𝘁𝗶𝗿𝗲 𝗹𝗼𝗼𝗽.
So they built FTPO (Final Token Preference Optimization) to fix it at training time:
- Identify the token that starts the loop mid-generation 2. Label it as rejected and sample up to 20 alternatives from the base model
- Redistribute probability
Doom-loop rate after training: LFM2.5-2.6B: 10.2% → 1.4% Qwen3.5-4B: 22.9% → 1%
Code: http://github.com/Liquid4All/antidoom… Blog: https://liquid.ai/blog/antidoom
Liquid4All/antidoom
Source: https://github.com/Liquid4All/antidoom
Antidoom
Antidoom generates and trains targeted preference data for reducing model repetition loops (doom loops).
It is a narrow tool for a narrow failure mode: sample model completions, detect where a repeated span begins, mark the first loop-starting token as rejected, choose coherent alternative next tokens, then train a LoRA adapter with Final Token Preference Optimization (FTPO).
The method adapts the single-token preference training idea from Antislop and its auto-antislop implementation to the specific problem of runaway repetition during reasoning.
Prerequisites
Antidoom needs a supported GPU runtime for PyTorch and vLLM. NVIDIA/CUDA is the default path; AMD/ROCm requires a separate environment.
-
NVIDIA / CUDA: use the pinned
uv.lockandconfigs/default.yaml. -
AMD / ROCm (Instinct MI-series): do not use the default
uv.lock; it is CUDA-only. See AMD / ROCm to create the ROCm venv and run withconfigs/default_amd.yaml, which adds the requiredattention_backend: TRITON_ATTNandoptim: adamw_torchoverrides.
Quick Start
git clone https://github.com/Liquid4All/antidoom
cd antidoom
uv sync
Edit configs/default.yaml and set model_name to the
checkpoint you want to antidoom. The default config reads prompts from
LiquidAI/antidoom-mix-v1.0, generates FTPO pairs, trains a LoRA adapter, and
merges it.
Run the full generate-and-train flow:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name LiquidAI/LFM2.5-1.2B-Base
This writes generated completions and FTPO pairs to runs/antidoom1/, trains
on the pair file, and writes the adapter plus merged model under the same run
directory.
Why Doom Loops Happen
Doom loops tend to appear when three forces line up:
- Overtrained tokens. Common reasoning tokens such as
Wait,So,But,Alternatively, can become unusually attractive after heavy synthetic reasoning training. When the model is uncertain, those tokens can dominate the next-token distribution without moving the reasoning forward. - Self-reinforcing context. Once a short sequence appears, the prior context makes that sequence more likely to appear again. Across repeated turns of the same loop, the probability of each token can climb toward certainty.
- Low-temperature sampling. At temperature 0 or near 0, the model keeps selecting the highest-probability continuation, so a locally reinforced loop has no natural escape route.
Antidoom attacks the failure at the token where the loop begins. Instead of training on full gold answers, it trains only on the local preference: do not choose the token that starts the repetition; choose one of the plausible alternatives available at that same position.
How It Works
For each prompt, Antidoom generates a completion and scans it for inner repetition. When a loop is found, it refines the boundary in token space so the rejected token is the first readable token of the repeated segment.
Each FTPO row contains:
- a prompt/context prefix ending immediately before the rejected token
- one rejected token, the token that begins the loop
- one or more chosen tokens sampled from filtered alternatives at that position
- metadata about the source prompt, generated completion, and detected loop
The training step regularises overrepresented rejected and chosen tokens so the adapter learns a broad anti-loop preference rather than simply suppressing one word.
Dataset
The default config uses LiquidAI/antidoom-mix-v1.0, a prompt-only ShareGPT mixture built for this pipeline.
The dataset intentionally excludes gold answers, rationales, hidden tests, verifier targets, and answer labels.
Dataset build scripts and license notes live in datasets/.
Configure
Start from configs/default.yaml. The most common
fields to change are:
model_name: base checkpoint or Hugging Face model idgeneration.hf_dataset: prompt dataset, or setgeneration.input_jsonlgeneration.prompt_field: column to read from local/HF rowsgeneration.target_pairs: number of FTPO pairs to collectgeneration.temperatureorgeneration.temperatures: sampling temperaturetrain.max_train_examples: maximum FTPO rows used for trainingtrain.output_dirandtrain.merged_output_dir: adapter and merged model dirs
prompt_field is authoritative: generation reads that column and parses the
value by shape. A string is treated as a plain user prompt, OpenAI-style
[{"role": ..., "content": ...}] lists are used as chat messages, and
ShareGPT-style [{"from": ..., "value": ...}] lists are converted to chat
messages. The parser does not infer prompt format from the column name.
Important Hyperparameters
max_train_examples
It is important to generate enough preference samples before training. The number of samples produced depends on the number of prompts available to the generation step, the number of temperature passes you run, and how doom-loopy the checkpoint is. As a rule of thumb, start with at least 15k prompts and aim to produce roughly 15k-20k preference rows.
Set max_train_examples below the number of generated preference rows, for
example 12k rows from a 15k-20k generated set. This gives
rejected_regularisation_strength room to shave off overrepresented rejected
tokens. Regularisation matters: without it, the generated set can be badly
unbalanced, which can create poor training outcomes.
Usage hint: set max_train_examples to at most 70% of the actual number of
preference rows in the generated dataset.
learning_rate
The Antidoom trainer can undertrain or overtrain. If overtrained, the model can degrade and produce more doom loops, so the right learning rate can take a little trial and error.
Usage hint: 0.00001-0.00002 is a good starting range when training on about
12k samples. Early stopping is useful to avoid overtraining.
early_stopping_chosen_win
This stops training when chosen_win exceeds the configured value.
chosen_win is the share of samples where the chosen token is winning against
the rejected token. Around 0.15-0.3, you should usually see a strong
reduction in doom looping. Training past 0.5 may be overtraining, though more
ablations are needed.
Usage hint: try 0.4 and adjust from there.
Other Hyperparameters
generation.*
These control the generation stage, where we try to elicit doom loops to build the preference training set. The defaults are usually a reasonable starting point.
rejected_regularisation_strength
This flattens the distribution of rejected-token frequency by culling samples. Some rejected tokens occur much more frequently than others, because they are the tokens that begin doom loops. If the training set is too unbalanced, those tokens can be suppressed too aggressively.
Usage hint: the default 0.3 is a good starting point.
chosen_regularisation_strength
This flattens the distribution of chosen-token frequency. If a chosen token is too overrepresented, its probability can be increased too much and it may start causing loops of its own. This step usually prunes chosen alternatives rather than whole samples, because each sample can contain multiple chosen tokens.
Usage hint: the default 0.5 is a good starting point.
filter_rejected_stop_words
Filters samples where the rejected token is a stop word like the or a.
Leave this set to false; common words can genuinely be loop-starting tokens
and the regularisation pass is the safer way to handle frequency.
source_balance_mode
Optionally balances the training set by source after rejected-token
regularisation. The default off is fine. Older configs may call this
balance_by_source_task.
min_chosen_tokens
The minimum number of chosen tokens required for a sample to be included. It
can sometimes be useful to require more than one chosen token to avoid mode
collapse on a single preferred token, but chosen-token regularisation already
mitigates this and 1 is usually fine for Antidoom training.
lora_r
A higher LoRA rank than usual tends to improve learnability with less
degradation for this trainer. If in doubt, use 128 or 256.
lora_alpha
Set this to the same value as lora_r, or half of lora_r.
lora_dropout
This has not been heavily ablated, but 0 works well in current runs.
target_modules
Previous Antislop ablations found that training all layers learned more easily
but could also degrade more easily; restricting to up_proj, down_proj, and
lm_head helped there. Antidoom training appears to prefer training all layers.
The safe starting point is:
["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", "lm_head"]
freeze_early_layers
This can reduce model impact and degradation at the expense of learnability. In practice, Antidoom training has worked well with freezing turned off.
lambda_mse_target
Strength of the MSE tether to the reference model for the chosen and rejected logits.
tau_mse_target
Penalty-free logit delta from the reference before the target-logit MSE loss kicks in.
lambda_mse
Strength of the MSE tether to the reference model for the remaining vocabulary,
excluding chosen and rejected logits. This is typically set higher than
lambda_mse_target, because chosen/rejected logits should move more freely
while the rest of the vocabulary stays close to the reference.
clip_epsilon_logits
Once the chosen token is beating the rejected token by this logit margin, the preference loss turns off.
Generate and Train
By default, antidoom is a one-shot pipeline. It generates preference rows
until generation.target_pairs is satisfied, then trains. If the target pair
JSONL already exists and has enough rows, generation returns immediately and
training starts.
uv run antidoom -c configs/default.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name LiquidAI/LFM2.5-1.2B-Base
With -r runs/antidoom1, generation writes:
runs/antidoom1/iter_0_generations.jsonl
runs/antidoom1/iter_0_ftpo_pairs.jsonl
and training writes:
runs/antidoom1/lora
runs/antidoom1/<auto-named-merged-model-dir>
Stage Commands
Run generation only:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 generate \
--model-name LiquidAI/LFM2.5-1.2B-Base
Train from an existing pair file:
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
Merge an adapter:
uv run antidoom -c configs/default.yaml -r runs/antidoom1 merge \
--model-name LiquidAI/LFM2.5-1.2B-Base
Filtering
Generation skips prompts that exceed generation.max_prompt_tokens. The generated preference dataset is regularized so that any given token does not occur too frequently in the chosen or rejected sets.
Outputs
Each FTPO row contains fields like:
{
"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 stores the full generated completion and detector
metadata. iter_0_ftpo_pairs.jsonl stores the preference rows used for
training.
AMD / ROCm
The Quick Start above targets NVIDIA/CUDA, whose pinned uv.lock is CUDA-only and can’t drive AMD
GPUs. On AMD you build a separate ROCm venv from vLLM’s prebuilt ROCm wheels and run with
configs/default_amd.yaml — whose key setting is
attention_backend: TRITON_ATTN, since vLLM’s default ROCM_ATTN kernel memory-faults on LFM2.
Run it with uv run --no-sync antidoom … — the --no-sync is required: a bare uv run (as in the
Quick Start) would re-sync the venv to the CUDA-only uv.lock and clobber the ROCm install.
Validated on Instinct MI325 (gfx942). vLLM’s ROCm wheels support MI200/MI300/MI350 and Radeon RX 7900/9000; see vLLM’s GPU installation guide for the support matrix and ROCm requirements (ROCm 6.3+, or 7.0+ for MI350).
Environment setup (ROCm venv)
Requires ROCm 6.3+ on the host (rocminfo lists your GPU) and Python 3.12 (already pinned in
.python-version). From the repo root, create the venv and install antidoom + ROCm torch/vLLM in a
single command — --extra-index-url gives the ROCm wheel index priority, so torch/vllm resolve to
their ROCm builds instead of the CUDA PyPI wheels
(ref):
uv venv && uv pip install -e . --extra-index-url https://wheels.vllm.ai/rocm/
Verify:
uv run --no-sync python -c "import torch, vllm; print(torch.__version__, torch.version.hip, vllm.__version__)"
# torch should print a '+rocm...' build and a non-None torch.version.hip
ROCm-specific settings (already in configs/default_amd.yaml)
configs/default_amd.yaml is default.yaml with exactly these overrides:
generation.vllm_kwargs
Thevllm_kwargs: enforce_eager: true # eager is stable and fast on ROCm attention_backend: TRITON_ATTN # the fix: ROCM_ATTN memory-faults on LFM2attention_backendvalue is forwarded to the vLLM engine. TheVLLM_ATTENTION_BACKENDenv var is a no-op in recent vLLM — it must be the engine arg, whichvllm_kwargsforwards.train.optim: adamw_torch— the NVIDIA defaultpaged_adamw_32bitneedsbitsandbytes(painful on ROCm).adamw_torchis the same 32-bit AdamW math; its “paged” CPU-offload is pointless on a 256 GB MI325.
The multi-GPU generate path additionally sets, per worker (in generate.py, no config needed):
HIP_VISIBLE_DEVICES (ROCm doesn’t reliably pin GPUs via CUDA_VISIBLE_DEVICES alone), per-worker
node-local VLLM_CACHE_ROOT/TORCHINDUCTOR_CACHE_DIR/TRITON_CACHE_DIR (shared JIT caches race →
“Memory access fault”), and a probed free port for VLLM_PORT (avoids EADDRINUSE). These are
no-ops on NVIDIA.
Run on AMD
Same commands as the Quick Start, but with configs/default_amd.yaml and uv run --no-sync (the
--no-sync stops uv run from re-syncing the venv back to the CUDA lock):
uv run --no-sync antidoom -c configs/default_amd.yaml -r runs/antidoom1 \
--temp 0.01 \
--model-name <your-lfm2-checkpoint-or-hf-id>
Generation uses one single-GPU vLLM engine per visible GPU; set CUDA_VISIBLE_DEVICES /
HIP_VISIBLE_DEVICES to choose GPUs. Training is single-GPU LoRA + merge.
Prefer a container? vLLM also ships official ROCm images — vllm/vllm-openai-rocm:latest (stable) or
:nightly — listed on the
GPU installation guide. Run
antidoom inside one with the same configs/default_amd.yaml; the TRITON_ATTN setting is still
required.
AMD troubleshooting
| symptom | cause | fix |
|---|---|---|
Memory access fault by GPU mid-generation | vLLM ROCM_ATTN kernel bug on LFM2 | use configs/default_amd.yaml (attention_backend: TRITON_ATTN) — mandatory, affects all cudagraph modes |
ImportError: You need to install bitsandbytes during train | optim: paged_adamw_32bit | use configs/default_amd.yaml (optim: adamw_torch) |
RuntimeError: Device string must not be empty at vLLM init | amdsmi missing (ROCm platform not detected) | cp -r "${ROCM_PATH:-/opt/rocm}/share/amd_smi" /tmp/amd_smi_build && uv pip install /tmp/amd_smi_build (copy first — the ROCm-shipped tree is root-owned) |
Citation
@article{liquidAI2026Antidoom,
author = {Liquid AI},
title = {Reducing Doom Loops with Final Token Preference Optimization},
journal = {Liquid AI Blog},
year = {2026},
note = {www.liquid.ai/blog/antidoom}
}
Similar Articles
@LiorOnAI: An open-source fix for one of the most common reasoning model failure modes. One of the biggest AI trends this year isn…
Liquid AI releases Antidoom, an open-source method that fine-tunes reasoning models to break repetitive token loops (doom loops), reducing failure rates from ~23% to 1% on Qwen3.5-4B without retraining or RL.
Reducing Doom Loops with Final Token Preference Optimization
Liquid AI introduces Antidoom, a method using Final Token Preference Optimization to reduce repetitive doom loops in small reasoning models during inference, cutting loop rates from 10.2% to 1.4% while improving evaluation scores.
@maximelabonne: New training technique to reduce doom loops! We applied it to LFM2.5-2.6B (SFT checkpoint) Qwen3.5-4B. By reducing doom…
Liquid AI releases Antidoom, an open-source method to reduce doom loops in reasoning models, applied to LFM2.5-2.6B and Qwen3.5-4B, significantly lowering doom-loop rates and improving eval scores.
@Raman_bansal_: If you’ve trained an LLM, you’ve may have seen doom loop, in which a LLM endlessly repeats the same token or sentence, …
A Substack article explains the 'doom loop' problem in LLMs where models repeat tokens endlessly, and introduces Final Token Preference Optimization (FTPO) from Liquid AI as a method to detect and fix such loops during fine-tuning.
@shannholmberg: how to improve your /goal by designing the loop before you run it a poorly designed loop burns tokens and hands you slo…
This article introduces Looper, a tool that helps design effective loops for AI goals by critiquing loops, building verification rubrics, setting guardrails and token budgets, and exporting portable artifacts for use with Claude Code.