AI Grimoire
Sheet
statuscommon
difficultyadvanced
timeO(r·d)
described2024
revised7w ago

DoRA

Decompose each weight column into a magnitude and a unit direction, adapt the direction with LoRA and the magnitude with a scalar. Recovers most of the gap to full fine-tuning at low rank.

[fine-tuning][peft]Commonly used

Standing

Commonly usedEstablished and frequently the right choice, but competing with live alternatives rather than having settled the question.

A consistent small improvement over LoRA at a small extra cost. Adopted where the margin matters.

judged as of 2026-09 · what the labels mean

Theory

The motivating measurement comes first. Take a full fine-tune, decompose the weight change into a change in each column’s magnitude and a change in its direction, and plot the two against each other. Full fine-tuning shows a negative correlation — it makes large directional changes with small magnitude changes, or the reverse. LoRA shows a positive one: it moves both together, because a rank-rr additive update cannot easily do otherwise.

DoRA adopts the decomposition as the parameterisation. Write WW as a per-column magnitude vector mm times a column-normalised direction matrix, then adapt the direction with LoRA and let mm train freely.

W=mW0+BAW0+BAcW' = m \odot \frac{W_0 + BA}{\lVert W_0 + BA \rVert_c}
eq. 1 — ‖·‖_c is the column-wise 2-normLiu et al. §4

mm is initialised to W0c\lVert W_0 \rVert_c and BB at zero, so training starts exactly at W0W_0 as it does for LoRA. The normalisation is what decouples the two: however far BABA pushes a column, the magnitude is set by mm alone.

r(d+k)LoRA  +  kmagnitude\underbrace{r(d + k)}_{\text{LoRA}} \;+\; \underbrace{k}_{\text{magnitude}}
eq. 2 — parameters added, per adapted matrix

The magnitude vector costs one scalar per output column — a rounding error against the low-rank factors, and the reason DoRA is compared against LoRA at equal rank rather than equal parameters.

Where it earns its keep is at rank 4 and 8, where LoRA is capacity-starved. By rank 64 the two converge, and the extra forward-pass cost stops paying for itself.

Implementation

python · torch ≥ 2.1
import math
import torch
from torch import Tensor, nn


class DoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, r: int = 8, alpha: int = 16):
        super().__init__()
        self.base = base
        self.base.requires_grad_(False)

        self.A = nn.Parameter(torch.empty(r, base.in_features))
        self.B = nn.Parameter(torch.zeros(base.out_features, r))
        nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
        self.scale = alpha / r

        # one magnitude per output column, initialised from the base weight
        norm = base.weight.norm(dim=1, keepdim=True)
        self.magnitude = nn.Parameter(norm.detach().clone())

    def forward(self, x: Tensor) -> Tensor:
        weight = self.base.weight + (self.B @ self.A) * self.scale
        direction = weight / weight.norm(dim=1, keepdim=True).detach()
        return nn.functional.linear(x, self.magnitude * direction)

The .detach() on the norm is from the paper and is not optional: letting the gradient flow through the normaliser roughly doubles the backward-pass memory for no measured gain. Note that the merged weight must be materialised each forward pass to compute the column norms, which is precisely the cost LoRA avoids by keeping the factors separate.

Related

References

[1]Liu et al. — DoRA: Weight-Decomposed Low-Rank Adaptation (2024)arXiv:2402.09353
[2]Hu et al. — LoRA: Low-Rank Adaptation of Large Language Models (2021)arXiv:2106.09685
[3]Salimans & Kingma — Weight Normalization (2016)arXiv:1602.07868