summaryrefslogtreecommitdiff
path: root/src/zbp_scaling/zbp/fa.py
blob: cd8830b8610fda62fdff7c244077dc49867ec4f2 (plain)
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
"""Standard (layer-wise) feedback alignment: Linear / Conv2d layers whose backward uses a fixed random
weight B in place of W^T (Lillicrap et al. 2016).  Nonlinearity derivatives are the true ones, taken at
the forward activations, as in the original algorithm.  Use `apply_fa(module)` to convert all Linear/Conv2d
layers of a block in place."""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F


class _FALinearFn(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, W, b, B):
        ctx.save_for_backward(x, B)
        ctx.has_bias = b is not None
        y = x.matmul(W.t())
        return y + b if b is not None else y

    @staticmethod
    def backward(ctx, gy):
        x, B = ctx.saved_tensors
        gx = gy.matmul(B)                                   # random feedback instead of W
        gW = gy.reshape(-1, gy.shape[-1]).t().matmul(x.reshape(-1, x.shape[-1]))
        gb = gy.reshape(-1, gy.shape[-1]).sum(0) if ctx.has_bias else None
        return gx, gW, gb, None


class _FAConvFn(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, W, b, B, stride, padding):
        ctx.save_for_backward(x, W, B)
        ctx.stride, ctx.padding = stride, padding
        ctx.has_bias = b is not None
        return F.conv2d(x, W, b, stride, padding)

    @staticmethod
    def backward(ctx, gy):
        x, W, B = ctx.saved_tensors
        gx = torch.nn.grad.conv2d_input(x.shape, B, gy, ctx.stride, ctx.padding)     # random feedback kernels
        gW = torch.nn.grad.conv2d_weight(x, W.shape, gy, ctx.stride, ctx.padding)
        gb = gy.sum((0, 2, 3)) if ctx.has_bias else None
        return gx, gW, gb, None, None, None


class FALinear(nn.Linear):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.register_buffer("B", torch.randn_like(self.weight) / math.sqrt(self.in_features))

    def forward(self, x):
        return _FALinearFn.apply(x, self.weight, self.bias, self.B)


class FAConv2d(nn.Conv2d):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        fan_in = self.in_channels * self.kernel_size[0] * self.kernel_size[1]
        self.register_buffer("B", torch.randn_like(self.weight) / math.sqrt(fan_in))

    def forward(self, x):
        return _FAConvFn.apply(x, self.weight, self.bias, self.B, self.stride, self.padding)


def apply_fa(module):
    """Replace every nn.Linear / nn.Conv2d inside `module` (recursively) by its FA variant, keeping weights."""
    for name, child in list(module.named_children()):
        if type(child) is nn.Linear:
            new = FALinear(child.in_features, child.out_features, bias=child.bias is not None)
            new.weight = child.weight
            if child.bias is not None:
                new.bias = child.bias
            new.B = new.B.to(child.weight.device)
            setattr(module, name, new)
        elif type(child) is nn.Conv2d:
            new = FAConv2d(child.in_channels, child.out_channels, child.kernel_size, child.stride, child.padding,
                           bias=child.bias is not None)
            new.weight = child.weight
            if child.bias is not None:
                new.bias = child.bias
            new.B = new.B.to(child.weight.device)
            setattr(module, name, new)
        else:
            apply_fa(child)
    return module