casehold-lora / lora.py
epochzero-0's picture
Adapter trained on 45,000 CaseHOLD questions, 85.92% on test
fd5a7ee verified
Raw History Blame Contribute Delete
2.61 kB
"""LoRA, written out: a frozen linear layer plus a trained low-rank correction."""
import math
import torch
import torch.nn as nn
TARGETS = ("q_proj", "k_proj", "v_proj", "o_proj")
class LoRALinear(nn.Module):
"""y = base(x) + (alpha / r) * B A x
base is the pretrained layer, frozen. A is (r, in), B is (out, r), and only they train.
B starts at zero, so at step 0 the correction is zero and the model is the pretrained one.
"""
def __init__(self, base: nn.Linear, r: int, alpha: float):
super().__init__()
self.base = base
for p in self.base.parameters():
p.requires_grad = False
w = base.weight
# A and B stay float32 whatever the base is stored in: AdamW's small updates vanish in bf16
self.A = nn.Parameter(torch.empty(r, base.in_features, dtype=torch.float32, device=w.device))
self.B = nn.Parameter(torch.zeros(base.out_features, r, dtype=torch.float32, device=w.device))
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
self.scale = alpha / r
def forward(self, x):
delta = (x.to(self.A.dtype) @ self.A.T @ self.B.T) * self.scale
return self.base(x) + delta.to(x.dtype)
def inject(model, r=16, alpha=32, targets=TARGETS):
"""Freeze every weight, then wrap the attention projections of every layer.
Returns {"layers.3.q_proj": LoRALinear, ...}."""
for p in model.parameters():
p.requires_grad = False
wrapped = {}
for i, layer in enumerate(model.model.layers):
for name in targets:
lora = LoRALinear(getattr(layer.self_attn, name), r, alpha)
setattr(layer.self_attn, name, lora)
wrapped[f"layers.{i}.{name}"] = lora
return wrapped
def lora_state(model):
"""Only the trained tensors: this is the whole adapter."""
return {k: v.detach().cpu() for k, v in model.state_dict().items() if k.endswith((".A", ".B"))}
def load_adapter(model, path, r=16, alpha=32):
"""Wrap the model and load trained A and B into it."""
inject(model, r, alpha)
if str(path).endswith(".safetensors"):
from safetensors.torch import load_file
state = load_file(path)
else:
state = torch.load(path, map_location="cpu")
missing, unexpected = model.load_state_dict(state, strict=False)
assert not unexpected and not [k for k in missing if k.endswith((".A", ".B"))]
return model
def count(model):
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
return trainable, total