Files
lora-lite/src/lora_lite/variants/pissa.py
T
wassname 4db5cee5a9 init
2026-04-26 14:10:20 +08:00

55 lines
2.0 KiB
Python

"""PiSSA: top-r SVD of W into A,B; replace W with W_res = W - B@A.
Meng et al. 2024 https://arxiv.org/abs/2404.02948
W_eff(t=0) = W_res + B@A = W (numerically; bf16 round-trip not bit-exact).
"""
import torch
from einops import einsum
from torch import nn
from ..variant import register, ParamSpec
@register
class PiSSA:
name = "pissa"
@staticmethod
def param_specs(d_in, d_out, cfg):
return {
"lora_A": ParamSpec((cfg.r, d_in), init="zeros", trainable=True),
"lora_B": ParamSpec((d_out, cfg.r), init="zeros", trainable=True),
}
@staticmethod
def init(layer: nn.Linear, cfg) -> None:
if type(layer) is not nn.Linear:
raise TypeError(
"PiSSA mutates layer.weight into W_res, so v1 only supports plain nn.Linear. "
"For bnb 4/8-bit, use LoRA/DeLoRA or implement explicit dequantize/requantize."
)
W = layer.weight.data.float() # (d_out, d_in)
U, S, Vh = torch.linalg.svd(W, full_matrices=False)
r = cfg.r
Ur, Sr, Vhr = U[:, :r], S[:r], Vh[:r, :]
sqrtS = Sr.sqrt()
# B @ A = Ur diag(Sr) Vhr; pick B = Ur sqrt(Sr), A = sqrt(Sr) * Vhr
B = (Ur * sqrtS).to(cfg.dtype)
A = (sqrtS[:, None] * Vhr).to(cfg.dtype)
layer.lora_B.data.copy_(B)
layer.lora_A.data.copy_(A)
# Compute BA in fp32 for the subtraction so W_res is accurate.
BA = (B.float() @ A.float())
# NOTE: PiSSA uses scale=1 (alpha==r) implicitly via init. We let the user pick;
# for fidelity at t=0, the convention is alpha==r so scale==1. Document in README.
scale = cfg.alpha / cfg.r
layer.weight.data.copy_((W - scale * BA).to(layer.weight.dtype))
@staticmethod
def forward(layer: nn.Linear, x, y):
cfg = layer._lora_cfg
scale = cfg.alpha / cfg.r
h = einsum(x, layer.lora_A, "... i, r i -> ... r")
delta = einsum(h, layer.lora_B, "... r, o r -> ... o")
return y + scale * delta