Optimizer Zoo (v0.41.0)

A closed allowlist of 24 optimizers: HF-native (8), bitsandbytes-backed (8), the v0.41 additions that still work (6), and two that only the MLX backend builds (adamw_hf and muon). So 22 are usable on the transformers and unsloth backends. The allowlist was 32 through v0.75.2; ten names were removed or restricted in v0.76.0 (below).

Supported optimizers

HF-native (8): adamw_torch, adamw_torch_fused, adamw_torch_xla, adamw_anyprecision, adafactor, sgd, adagrad, rmsprop.

bitsandbytes-backed (8): adamw_bnb_8bit, adamw_8bit, lion_8bit, lion_32bit, paged_adamw_8bit, paged_adamw_32bit, paged_lion_8bit, paged_lion_32bit.

Still available from v0.41 (6):

  • APOLLO (apollo_adamw) — gradient-scaled training in low rank
  • lomo / adalomo — low-memory optimization with optional adaptive scaling
  • grokadamw — Grokfast-style AdamW
  • schedule_free_adamw / schedule_free_sgd

MLX backend only (2): adamw_hf and muon. Both are refused at config load on backend: transformers and backend: unsloth (#1283); on backend: mlx they load.

yaml
training:
  optimizer: adamw_bnb_8bit

validate_optimizer_name rejects non-string / empty / null-byte / >64-char inputs with actionable messages and lowercases for deterministic lookup. Unknown names fail at config-load.

Refused since v0.76.0

Eight names the v0.41 release added, or that had always been listed, are refused at config load because transformers 5.x rejects them in TrainingArguments after the model has loaded (#1269): badam, adam_mini, dion, came_pytorch, adamw_apex_fused and the three TorchAO names ao_adamw_fp8, ao_adamw_4bit, ao_adamw_8bit. Through v0.75.2 a config naming one of them loaded and then died inside the trainer. The error names adamw_torch, adamw_torch_fused, adamw_bnb_8bit and adafactor as working alternatives.

loraplus_lr_ratio cannot be combined with apollo_adamw, lomo or adalomo (#1210): LoRA+ builds its optimizer without the model, and transformers can only build those three from the model.

Per-module LR groups (accepted, not applied)

training.lr_groups is accepted by the schema, but nothing in v0.76.0 reads it: no optimizer builds parameter groups from it, so every parameter trains at training.lr. It had no reader at v0.75.2 either, so a page that said it routed learning rates by pattern was describing a feature that did not run. Set to anything other than null, it prints Warning: training.lr_groups is accepted but read by nothing; v0.77 will refuse it and the load continues; upstream says v0.77 will refuse the config (its stated deadline, #761). Delete the key to keep the config loading. To train the LoRA A and B matrices at different rates use training.loraplus_lr_ratio.

What the schema still checks, for as long as the key loads: at most 32 entries, each lr in (0, 1] and finite (NaN and ±inf rejected, bool rejected), duplicates rejected.

Each pattern must compile, and since v0.75.1 it is then checked structurally: the pattern is parsed, and nested or alternating repetition, backreferences and more than three unbounded repeats are refused. The check never runs the pattern. That is the whole point of the change, because the previous guard did run it, against a 128-character probe, which is exactly how a pattern like (a+)+b could hang config loading rather than be caught by it. A guard that executes untrusted input to decide whether executing it is safe is the vulnerability.

LoftQ quantization-aware LoRA init

yaml
training:
  quantization: none       # required: PEFT LoftQ quantizes the base itself
  lora:
    init_strategy: loftq   # random | pissa | olora | loftq
    loftq_iter: 5          # [1, 10]
    loftq_bits: 4          # 2 | 4 | 8

Builds the LoftQ config via peft (lazy import); incompatible with DoRA + VeRA. Through v0.75.2 init_strategy, loftq_iter and loftq_bits reached no trainer, so a run that set them trained plain random-init LoRA (see LoRA quality). Since v0.76.0 (#1037) the strategy is checked at config load: loftq and pissa need quantization: none (the default 4bit is refused, because both compute on the floating-point base weights), and any non-random init_strategy is refused on backend: unsloth and backend: mlx. olora still loads at 4bit. See LoRA quality.

LLaMA Pro block expansion

yaml
training:
  quantization: none            # required with expand_layers
  expand_layers: 4              # [1, 64]
  freeze_trainable_layers: 4    # must equal expand_layers

Schema landed in v0.41.0 and the block-expansion patch is applied live by the SFT and pretrain trainers. Three rules are enforced at config load since v0.76.0:

  • freeze_trainable_layers must equal expand_layers (#1409). It is a flag, not a count of layers: it freezes every parameter of the original model and trains only the appended blocks, and it does not select the top-N or bottom-N layers. Any other value, including 0 or a negative one, is refused. Without expand_layers the field is refused too (#1396).
  • expand_layers needs quantization: none (#1237). Under the default 4bit the zeroed residual projections of the appended blocks decode to -absmax rather than 0, so the new blocks are not identity.
  • Scope: task: sft or pretrain, backend: transformers, modality: text; any other combination is refused.

Through v0.75.2 the field was read only for its sign: 1, 2 and 4 all trained just the appended blocks, and 0 or a negative value silently trained the whole expanded model. A config with expand_layers: N and a different freeze_trainable_layers trained something other than its author expected; see Re-run this.

Friendly load_in_X aliases

load_in_8bit: true remaps quantization → 8bit. load_in_16bit: true remaps to none. Mutually exclusive. Combining either with an explicit Quant Menu format raises rather than silently overriding.

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.