File size: 3,486 Bytes
36cdb93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
"""Minimal LoRA, used to make the DMD critic share the teacher's weights.

DMD needs three networks: the causal student, a frozen *real* score (the
original bidirectional Wan) and a trainable *fake* score that tracks the
student's own output distribution. Three full 1.4B copies plus the student's
AdamW state does not fit on a 40 GB card, and the fake score's correct
initialisation is exactly the real score anyway -- so the fake score is the same
frozen base with a low-rank adapter on top, toggled by a flag:

    with lora_enabled(base, False): v_real = ...      # teacher
    with lora_enabled(base, True):  v_fake = ...      # critic

That costs ~32 M trainable parameters instead of 1.4 B, starts the critic at the
right place by construction, and leaves the base weights bit-identical between
the two roles.
"""
from contextlib import contextmanager

import torch
import torch.nn as nn


class LoRALinear(nn.Module):
    def __init__(self, base: nn.Linear, rank=32, alpha=None):
        super().__init__()
        self.base = base
        self.base.weight.requires_grad_(False)
        if self.base.bias is not None:
            self.base.bias.requires_grad_(False)
        self.rank = rank
        self.scale = (alpha or rank) / rank
        self.a = nn.Parameter(torch.zeros(rank, base.in_features))
        self.b = nn.Parameter(torch.zeros(base.out_features, rank))
        nn.init.normal_(self.a, std=1.0 / rank)      # b stays zero -> starts as identity
        self.enabled = True

    def forward(self, x):
        y = self.base(x)
        if not self.enabled:
            return y
        h = nn.functional.linear(x.to(self.a.dtype), self.a)
        return y + nn.functional.linear(h, self.b).to(y.dtype) * self.scale


TARGETS = ('self_attn.q', 'self_attn.k', 'self_attn.v', 'self_attn.o',
           'ffn.0', 'ffn.2')


def inject_lora(model, rank=32, alpha=None, targets=TARGETS):
    """Wrap the targeted Linears of every transformer block. Returns the new
    parameters, and freezes everything else in the model."""
    for p in model.parameters():
        p.requires_grad_(False)
    n = 0
    for blk in model.blocks:
        for name in targets:
            parent, _, leaf = name.rpartition('.')
            mod = blk.get_submodule(parent) if parent else blk
            lin = getattr(mod, leaf) if not leaf.isdigit() else mod[int(leaf)]
            if isinstance(lin, LoRALinear):
                continue
            wrapped = LoRALinear(lin, rank, alpha).to(lin.weight.device)
            wrapped.a.data = wrapped.a.data.float()
            wrapped.b.data = wrapped.b.data.float()
            if leaf.isdigit():
                mod[int(leaf)] = wrapped
            else:
                setattr(mod, leaf, wrapped)
            n += 1
    params = [p for p in model.parameters() if p.requires_grad]
    total = sum(p.numel() for p in params)
    return params, n, total


def set_lora(model, on):
    for m in model.modules():
        if isinstance(m, LoRALinear):
            m.enabled = on


@contextmanager
def lora_enabled(model, on):
    prev = [m.enabled for m in model.modules() if isinstance(m, LoRALinear)]
    set_lora(model, on)
    try:
        yield
    finally:
        for m, p in zip((m for m in model.modules()
                         if isinstance(m, LoRALinear)), prev):
            m.enabled = p


def lora_state_dict(model):
    return {k: v for k, v in model.state_dict().items()
            if k.endswith('.a') or k.endswith('.b')}