Download lora.py from epochzero-0/casehold-lora: direct link, hf CLI and curl.
- Browser
- Download file 2.61 kB
-
https://huggingface.co/epochzero-0/casehold-lora/resolve/main/lora.py
- Command line
-
hf download hf://epochzero-0/casehold-lora/lora.py
-
curl -L -o lora.py https://huggingface.co/epochzero-0/casehold-lora/resolve/main/lora.py
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 | |