File size: 2,665 Bytes
c675b8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import torch
from shapequant import core


def _ref_sequential(W, bits, group, H, dynamic=False):
    """Unblocked reference of the error-feedback recursion."""
    W = W.clone().float(); out, inn = W.shape; maxq = 2 ** bits - 1
    sc, z = core.group_params(W, bits, group)
    U = torch.linalg.cholesky(torch.cholesky_inverse(torch.linalg.cholesky(H)), upper=True)
    Q = torch.zeros_like(W)
    for c in range(inn):
        g = c // group
        if dynamic and c % group == 0:
            s_, z_ = core.group_params(W[:, c:c + group], bits, group); sc[:, g], z[:, g] = s_[:, 0], z_[:, 0]
        q = torch.clamp(torch.round(W[:, c] / sc[:, g]) + z[:, g], 0, maxq); Q[:, c] = q
        err = (W[:, c] - (q - z[:, g]) * sc[:, g]) / U[c, c]
        W[:, c:] -= err[:, None] * U[c, c:][None, :]
    return Q


def test_identity_design_is_rtn():
    torch.manual_seed(0)
    W = torch.randn(64, 256)
    q0, s0, z0 = core.quantize(W, 4, 64)
    for dyn in (False, True):
        q1, s1, z1 = core.quantize(W, 4, 64, H=torch.eye(256), dynamic_groups=dyn)
        assert torch.equal(q0, q1) and torch.equal(s0, s1)


def test_blocked_equals_sequential():
    torch.manual_seed(1)
    W = torch.randn(48, 384); A = torch.randn(384, 384); H = A @ A.T / 384 + torch.eye(384)
    for dyn in (False, True):
        for blk in (32, 64, 128, 256):
            q, _, _ = core.quantize(W, 4, 128, H=H, block=blk, dynamic_groups=dyn)
            assert (q.float() - _ref_sequential(W, 4, 128, H, dyn)).abs().max() == 0, (dyn, blk)


def test_pack_roundtrip():
    torch.manual_seed(2)
    for bits in (4, 8):
        W = torch.randn(96, 256) * 0.05
        q, s, z = core.quantize(W, bits, 128)
        pk = core.pack(q, s, z, bits, 128)
        Wd = core.unpack(pk["qweight"], pk["qzeros"], pk["scales"], pk["g_idx"], bits)
        ref = core.dequantize(q, s.half().float(), z, 128)
        assert torch.allclose(Wd, ref, atol=0, rtol=0)
        assert pk["qweight"].dtype == torch.int32 and pk["qweight"].shape == (256 * bits // 32, 96)


def test_zero_points_v1_safe():
    W = torch.rand(32, 128)                       # all positive groups: zero-point must still be >= 1
    q, s, z = core.quantize(W, 4, 64)
    assert z.min() >= 1
    err = (W - core.dequantize(q, s, z, 64)).abs().max() / W.abs().max()
    assert err < 0.05


def test_shaping_reduces_weighted_error():
    torch.manual_seed(3)
    W = torch.randn(128, 256); A = torch.randn(256, 32); H = torch.eye(256) + A @ A.T
    def cost(q, s, z):
        R = W - core.dequantize(q, s, z, 64); return float(torch.trace(R @ H @ R.T))
    assert cost(*core.quantize(W, 4, 64, H=H)) < cost(*core.quantize(W, 4, 64))