Loop Hardening (v0.70.0 → live in v0.71.11)

Six surfaces that protect the training loop from the failure modes that cost a real GPU-hour. Shipped schema-first in v0.70.0; as of v0.71.11 every callback is live — the detectors install real GRPO/PPO TrainerCallbacks, the distillation losses compute inside the distill trainer, the RL checkpoints write real state, and soup iterative-dpo runs the full loop. Validated end-to-end on SmolLM2-135M.

--reward-hack-detector — InfoRM + RM-ensemble divergence

bash
soup train --config grpo.yaml \
  # base, task and reward_model are config keys \
  --reward-hack-detector info_rm \
  --reward-hack-halt

Two detectors:

  • info_rm — InfoRM Cluster-Separation Index (Wang et al. 2024, arXiv 2402.09345). Drops when the policy collapses onto a degenerate reward-maximising subspace.
  • rm_ensemble — mean pairwise variance across an RM ensemble (cap 32). When ensemble members disagree, the policy is exploiting one of them.

Math kernels compute_cluster_separation, compute_rm_ensemble_divergence, classify_hack_signal are LIVE with OK / WARN / HACK bands at 0.10 / 0.30 relative drop. --reward-hack-halt auto-stops on HACK (exit 2). Cross-validator: task in {grpo, ppo} only, halt=True requires detector, rejects mlx; rm_ensemble requires ≥2 reward functions. Composes with v0.34 soup why for anomaly explanation. Live as of v0.71.11 — the GRPO callback reads per-step rewards via a shared thread-safe capture buffer, classifies the verdict, logs it to state.log_history, and halts on HACK.

v0.71.26 closes the loop. These detectors no longer only *halt*: --reward-hack-mitigation kl_control|pid_lagrangian makes the trainer self-correct mid-run — raise the KL penalty, shape the reward, roll back to the last-good checkpoint, then early-stop only as a last resort. See Closed-loop reward-hacking auto-mitigation.

training.uld_strategy — cross-tokenizer Universal Logit Distillation

yaml
# soup.yaml
task: distill
training:
  uld_strategy: wasserstein   # or: topk_align
  uld_top_k: 32               # required for topk_align

Boizard et al. 2024 (arXiv 2402.12030). Llama → Mistral, Llama → Qwen — no shared vocabulary required.

  • wasserstein — 1-D Wasserstein distance over sorted teacher / student logits, no alignment (cheap, robust default)
  • topk_align — top-K teacher logits matched via BPE-overlap heuristic alignment (use when you have a good vocab-overlap heuristic and want sharper signal)
  • _MAX_VOCAB_SIZE=262144 covers multilingual SentencePiece + GPT-OSS 200K vocabularies
  • Gated to task='distill' and rejects mlx backend
  • Live as of v0.71.11 — the distill trainer computes the real Wasserstein-1 (sorted-CDF) or top-k-aligned loss, clamping teacher ids to the teacher vocab on a size mismatch. (v0.71.12 also adds distill_mode: sequence for hard-label sequence-level KD, mutually exclusive with the ULD logit path.)

training.minillm_enabled — reverse-KL with 3 stability tricks bundled

yaml
task: distill
training:
  minillm_enabled: true
  minillm_teacher_mix_ratio: 0.3
  minillm_length_normalize: true
  minillm_pretrain_anchor_weight: 0.1
  minillm_pretrain_anchor_path: ./pretrain.jsonl

Gu et al. 2024 (arXiv 2306.08543). All three §3 stability tricks bundled: teacher-mixed sampling (mix teacher samples into the on-policy rollout), length normalisation (per-token KL averaged), pretrain-loss anchor (regularise toward an anchor distribution at weight α).

There is one real CLI flag in this family, and everything above is config:

bash
soup train --config distill.yaml --minillm-on-policy

It switches the rollout from a teacher-forced approximation to a genuine on-policy one: a fresh autoregressive sample is drawn from the teacher-student mixture each step, then scored with the length-normalised reverse KL. training.minillm_rollout_length sizes it, defaulting to min(max_length, 32).

Cross-validators reject silent no-ops:

  • anchor_weight=0 with anchor_path set → error
  • anchor_weight > 0 with path = None → error

Gated to task='distill'. Live as of v0.71.11 — the teacher-mixed, length-normalised reverse-KL term plus the optional pretrain-anchor SFT term train end-to-end; the anchor reader is cwd-contained + symlink-rejecting with a per-line byte cap.

training.rl_checkpoint_save_every_steps — mid-epoch PPO/GRPO ckpt

bash
# config keys under training:, not CLI flags
#   rl_checkpoint_save_every_steps: 200
#   rl_checkpoint_keep_last: 4
#   rl_checkpoint_include_optimizer: true
#   rl_checkpoint_include_ref_model: true
#   rl_checkpoint_include_rollout_buffer: true
soup train --config ppo.yaml

TorchTune explicitly punts mid-epoch checkpointing. Soup enforces the bounds save_every_steps ∈ [1, 10M], keep_last ∈ [1, 100] (oldest pruned).

Composes with v0.32 spike recovery + v0.40 reference-model regen — recovery now hops to the most recent mid-epoch ckpt instead of restarting the epoch on a PPO crash. Live as of v0.71.11 — it writes a real adapter + optimizer state + JSON manifest every N steps and prunes to rl_checkpoint_keep_last. (v0.71.11 also makes the GRPO reference-model EMA update in place, eliminating the three model-sized allocations per step.)

soup iterative-dpo — sample → score → re-pair → retrain driver

bash
soup iterative-dpo --base-model registry://policy-v3 \
  # base, task and reward_model are config keys \
  --prompts ./prompts.jsonl \
  --output-dir ./iter-dpo \
  --rounds 4 --pairs-per-round 4000

Frozen IterativeDPOPlan with a consecutive-round_index invariant and canonical per-round artifacts:

./iter-dpo/round-01/pairs.jsonl
./iter-dpo/round-01/adapter/
./iter-dpo/round-02/pairs.jsonl
./iter-dpo/round-02/adapter/
...

So a crashed run resumes cleanly. --plan-only renders the validated plan and exits 0. Live as of v0.71.11 — each round samples completions from the previous round's adapter, then trains a fresh LoRA from the base on that round's harvested pairs.

training.echo_trap_enabled — RAGEN multi-turn n-gram repetition detector

bash
soup train --config grpo.yaml \
  # config keys under training: echo_trap_enabled \
  #   echo_trap_threshold: 0.6 \
  #   echo_trap_halt: true

Zhu et al. 2025 (arXiv 2504.14437). Pure-Python n-gram repetition rate per trajectory + a batch mean — when an agent's rollout collapses into "echoing itself" (the same n-gram pattern appearing repeatedly within and across turns), this catches it before the reward model rewards the degenerate policy.

--echo-trap-tokenizer-aware is the one real CLI flag here, and the trade-off is worth stating before you reach for it: scoring repetition over tokens rather than characters catches subword repetition that punctuation-heavy text hides, but it makes the score tokenizer-specific instead of vocabulary-agnostic, so two runs on different tokenizers stop being comparable. Everything else in this family (echo_trap_enabled, echo_trap_threshold, echo_trap_halt) is a training. config key, not a flag.

OK / WARN / TRAP bands at 0.30 / 0.60. DoS caps _MAX_NGRAM_N=32, _MAX_TRAJECTORY_TOKENS=1M, _MAX_BATCH_TRAJECTORIES=100k. Gated to task in {grpo, ppo} non-mlx. Composes with v0.53.11 GRPOStabilityCallback. Live as of v0.71.11 — the GRPO callback scores per-trajectory n-gram repetition, logs the verdict, and halts on TRAP when training.echo_trap_halt is set.

Numbers

+337 tests in v0.70.0 (11,487 → 11,824); the live wiring in v0.71.11 is part of the broader v0.71 sweep, which closed at 16,529 tests across 319 files in v0.71.41. As of v0.73.3 the suite spans 369 test files; upstream stopped publishing a total it counted at the tag, so this page names the number that reproduces and not one it cannot check.

See also

Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.