LoRA Quality — PiSSA, ReLoRA, Per-Pattern Rank, Surgical Patches (v0.39.0)

Five PEFT-surface improvements that LlamaFactory and Axolotl maintain.

yaml
training:
  quantization: none              # required: the schema default is 4bit; PiSSA and ReLoRA need a float base
  lora:
    init_strategy: pissa          # 'random' (default), 'pissa', 'olora'
    rank_pattern:                 # per-target-module rank override; keys are REGEXES
    # Since v0.75.1 each key is length-capped at 512, compiled explicitly, and
    # put through the same structural regex check as lr_groups, because peft
    # matches every key as a regex against every module name in the model.
      q_proj: 8
      v_proj: 16
    alpha_pattern:                # per-target-module alpha override
      q_proj: 16
  relora_steps: 500               # merge and re-initialise the adapter every 500 steps
  relora_warmup_ratio: 0.1        # skip first 10% of training
  relora_prune_ratio: 0.9         # deprecated: kept so old YAML loads, ignored
  relora_reset_optimizer: true    # clear optimizer state after each restart

PiSSA

PiSSA initializes the LoRA pair from the SVD of the base weight, giving faster early convergence than random init at the cost of one extra SVD pass on the first epoch.

If you set one of these before v0.76.0, the adapter you got was plain LoRA. Through v0.75.2 lora.init_strategy (pissa, olora, loftq), lora.use_olora, lora.loftq_iter, lora.loftq_bits and lora.use_vera were accepted by the schema and read by no trainer, so a run that set one trained an ordinary random-init LoRA, exit 0, normal loss curve. v0.76.0 sends every transformers trainer's adapter through one builder: PiSSA, OLoRA and LoftQ change the base weight at attach time, and VeRA builds a PEFT VeraConfig instead of a LoRA one. Re-run to get the variant.

Since v0.76.0 (#1037) the strategy is checked when the config loads. pissa and loftq need training.quantization: none, because PEFT computes on the floating-point base weights: under the default 4bit the config is refused with training.lora.init_strategy='pissa' requires training.quantization='none'. A non-random init_strategy, use_dora and use_vera are refused on backend: unsloth and backend: mlx, which build their own adapters. olora still loads at 4bit.

init_strategy: olora is also accepted; setting the legacy use_olora: true auto-aligns for back-compat.

ReLoRA

ReLoRA fires every N global steps. Each restart merges the LoRA update (B @ A, with PEFT scaling) into the frozen base weight, re-initialises lora_A (Kaiming) and lora_B (zeros), optionally clears the optimizer state of those adapter parameters (relora_reset_optimizer, default true), and re-warms the learning rate: the first post-restart step uses 1/(W+1) of the target rate for a warmup of W steps, then rises by that increment to the full rate. Useful for very long training runs where the LoRA capacity saturates.

ReLoRA is a behaviour break in v0.76.0 (#693). Through v0.75.2 it magnitude-pruned the adapter instead of restarting it, and relora_prune_ratio set how much; that key is now deprecated and ignored (a restart no longer prunes). If you ran with relora_steps before, you trained a different procedure than the one now described.

  • relora_steps requires an explicit quantization: none, since a merge needs a writable float base; under the default 4bit the config is refused. It is also refused with lora.use_vera, lora.use_dora, stream_layers and use_fsdp2_compile, and on backend: mlx.
  • Restarts accumulate faithfully only on an fp32 base; a bf16 or fp16 base loses the merge delta to rounding.
  • The final output is a standalone dense model: Soup merges the active adapter into the accumulated in-memory base and saves it. Load or export that output directly; do not run soup merge on it.
  • --resume and --hf-resume are refused when relora_steps is set, because intermediate checkpoints do not encode the accumulated restart state.

ReLoRA is wired into the preference and RL trainers too (DPO / GRPO / KTO and the rest).

Per-pattern rank/alpha

Map module name patterns to integer ranks. Useful in MoE configs where expert FFNs need lower rank than attention.

Caps: 256 keys × value 1024. bool rejected on values. Incompatible with use_vera (VeRA shares one rank). Since v0.76.0 (#923) the two pattern keys are applied by every trainer that attaches a LoRA adapter, asr, classifier, distill, embedding, ppo, unlearn and layer streaming included. Through v0.75.2 only SFT and pretraining built their adapter through the shared builder, so on dpo, orpo, simpo, kto, bco, ipo, grpo, ppo, online_dpo, reward_model, embedding, classifier, distill and asr the keys were ignored and the adapter was uniform-rank; re-run if you set them there.

LoRA-FA (frozen-A LoRA)

training.use_lorafa: true (v0.76.0, #725) freezes the random LoRA A matrices and trains only the B matrices, using PEFT's create_lorafa_optimizer (arXiv:2308.03303).

yaml
training:
  lr: 2e-4
  use_lorafa: true
  lora:
    r: 64
    alpha: 16

It needs backend: transformers and a LoRA adapter (lora.r of at least 1), and it applies to sft, pretrain and embedding. It is refused with loraplus_lr_ratio (LoRA+ tunes the A matrices that LoRA-FA freezes), use_galore, lora.use_vera, a non-AdamW optimizer, lora.r: 0 and backend: mlx. It has not been validated under stream_layers: true.

Upstream's measurement is a micro-benchmark on a 2-layer, hidden-256 model: it trains exactly 50.0% fewer adapter parameters per adapted projection and halves the AdamW state for square projections, and the activation retained for the adapter backward is analytically r / d_in of the input. These are not total or peak VRAM savings, a throughput result or a quality claim; task quality and end-to-end throughput against standard LoRA are unmeasured. The record is in the public benchmarks/ directory.

Surgical patches

  • Gemma 4 ClippableLinear swap (auto-detected via word-boundary regex on model name)
  • Fused-MoE 3-D expert lora_dropout strip

Both auto-fire when the model name and architecture match. Both are gated and silent on unrelated models.

Template registry

The 21 built-in templates now live as soup_cli/templates/*.yaml with a manifest.json index. soup init --template <name> reads the YAML; the inline copies in schema.py stay as a back-compat fallback, deprecated in favour of the YAML registry.

load_template containment-checks the resolved path against _templates_dir() so a tampered manifest.json cannot read files outside the package directory.

Scope

Wired into the SFT trainer + transformers backend in v0.39.0. ReLoRA is wired into the preference and RL trainers too (DPO / GRPO / KTO and the rest). The MLX backend refuses relora_steps and the non-random init_strategy values at config load.

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.