# Adapter support

Poros trains **frozen-base adapters only**. At `prepare()` time it inspects
the model (pure inspection, no torch import needed) and classifies the adapter;
the status decides which guarantee you can get.

## Status matrix

| Status | Adapter | Guarantee impact |
|---|---|---|
| `validated` | PEFT LoRA / QLoRA on a frozen base | eligible for `validated_bitwise` (with positively detected NF4 + validated arch) |
| `compatible` | DoRA, RSLoRA, LoRA with small explicit `modules_to_save` | trains fine, but the exact topology has no committed parity evidence, so the label caps at `smoke_only` |
| `experimental` | Poros native LoRA, other PEFT types (IA3, LoHa/LoKr, prompt/prefix tuning, ...) | refused under `guarantee="validated"`; otherwise runs with no parity claim |
| `unsupported` | no trainable adapter params, or full fine-tune | raises before training (see below) |

Detection is authoritative: `prepare(adapter=...)` hints are advisory, except
`adapter="unsafe_any_trainable"`, the explicit full-finetune opt-out.

## The two errors and the fix

- **`PorosFullFinetuneDetectedError`** — trainable parameters that are not
  adapter parameters (the message reports the trainable fraction). Fix: freeze
  the base and attach a PEFT LoRA:

  ```python
  from peft import LoraConfig, get_peft_model
  model = get_peft_model(model, LoraConfig(r=16, lora_alpha=32))
  model = poros.prepare(model)
  ```

  Escape hatch: `unsafe_allow_full_trainable=True` — runs, but the guarantee
  label drops to `none` (no claims).

- **`PorosAdapterUnsupportedError`** — an `experimental` adapter while
  `guarantee="validated"` was requested. Fix: use a validated adapter, or
  accept the weaker label via `guarantee="best_effort"` /
  `unsafe_allow_unvalidated_adapter=True`.

## What counts as an adapter parameter

Parameter names matching the LoRA family: `lora_A`/`lora_B` (and lowercase),
`lora_embedding_A/B`, `lora_magnitude` (DoRA). Non-adapter trainable parameters count
toward the full-finetune gate, which trips only when they exceed **2%** of all
parameters (`FULL_FINETUNE_FRACTION_THRESHOLD`). Below that threshold Poros
warns rather than raising — and those parameters receive no gradients in
offloaded blocks, so unfreezing norms or embeddings alongside LoRA silently
trains the adapters only.

## Saving

With the PEFT backend (the default), `PorosTrainer.save()` and standard PEFT
`save_pretrained()` both work — Poros changes residency, not the adapter
format — and saved adapters load with vanilla PEFT; nothing Poros-specific
is needed at inference time (see
[examples/inference.py](../examples/inference.py)).

The experimental native backend saves a Poros-specific `adapter_weights.pt`
instead: it is NOT vanilla-PEFT loadable and cannot be merged by
`poros export`. Use `adapter_backend="peft"` for anything you intend to
share or deploy.

Full `prepare()` semantics: [auto.md](auto.md).
