File size: 8,259 Bytes
e5235fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""T2.1 acceptance: fast path = reference, STE identity, Q-A zero fraction, packing round-trips,
idempotence. CPU only."""
import pytest
import torch
from hypothesis import given, settings
from hypothesis import strategies as st

from lowbit.quant.ternary import (
    QuantSpec,
    canonical_scale,
    dequant_packed,
    export_packed,
    fake_quant,
    init_scale,
    pack_2bit,
    pack_trits,
    reference_codes_and_scale,
    ternary_codes,
    unpack_2bit,
    unpack_trits,
    zero_fraction,
)

BF16_ULP = 2.0**-7


def gen(shape, seed=0, std=0.02):
    return torch.randn(*shape, generator=torch.Generator().manual_seed(seed)) * std


@pytest.mark.parametrize("method", ["absmean", "twn"])
@pytest.mark.parametrize("group", [64, 128])
def test_fast_matches_reference(method, group):
    spec = QuantSpec(method=method, group_size=group)
    w = gen((12, 384), seed=group)
    ref_q, ref_s = reference_codes_and_scale(w, spec)
    s = init_scale(w, spec)
    q = ternary_codes(w, s, spec)
    # scales: fp32 vs fp64 summation order may differ by <1 bf16 ulp; everything else is exact
    assert torch.allclose(s, ref_s, rtol=BF16_ULP, atol=0)
    same = (s == ref_s).repeat_interleave(group, -1)
    assert same.float().mean() > 0.99
    assert torch.equal(q[same], ref_q[same])


@pytest.mark.parametrize("method", ["absmean", "twn"])
def test_stacked_experts_match_per_expert(method):
    spec = QuantSpec(method=method)
    w = gen((5, 48, 384), seed=3)  # [E, out, in]
    stacked = fake_quant(w, spec)
    per = torch.stack([fake_quant(w[e], spec) for e in range(5)])
    assert torch.equal(stacked, per)


def test_forward_is_exact_and_bf16_safe():
    spec = QuantSpec()
    w = gen((32, 1024), seed=1)
    w_hat = fake_quant(w, spec)
    s = init_scale(w, spec)
    q = ternary_codes(w, s, spec).float()
    expect = (q.unflatten(-1, (-1, 128)) * s.unsqueeze(-1)).flatten(-2)
    assert torch.equal(w_hat, expect)
    # the bf16 cast used by the matmul loses nothing: values are bf16(s) * {-1,0,1}
    assert torch.equal(w_hat.to(torch.bfloat16).float(), w_hat)
    assert set(torch.unique(q).tolist()) <= {-1.0, 0.0, 1.0}


@pytest.mark.parametrize("method", ["absmean", "twn"])
def test_plain_ste_gradient_is_identity(method):
    w = gen((16, 256), seed=2).requires_grad_()
    g = gen((16, 256), seed=9, std=1.0)
    fake_quant(w, QuantSpec(method=method)).backward(g)
    assert torch.equal(w.grad, g)


def test_clipped_ste_masks_large_ratios():
    spec = QuantSpec(ste="clipped")
    w = gen((16, 256), seed=4)
    w[0, :5] = 1.0  # far outside 1.5 s
    w.requires_grad_()
    g = torch.ones_like(w)
    fake_quant(w, spec).backward(g)
    s = init_scale(w.detach(), spec).repeat_interleave(128, -1)
    keep = (w.detach().abs() / s) <= 1.5
    assert torch.equal(w.grad, keep.float())
    assert not keep[0, :5].any()


def test_lsq_scale_gets_gradient_and_init_matches_absmean():
    w = gen((8, 256), seed=5)
    s0 = init_scale(w, QuantSpec(method="lsq"))
    assert torch.equal(s0, init_scale(w, QuantSpec(method="absmean")))
    s = s0.clone().requires_grad_()
    wl = w.clone().requires_grad_()
    out = fake_quant(wl, QuantSpec(method="lsq"), scale=s)
    assert torch.equal(out.detach(), fake_quant(w, QuantSpec()))
    out.pow(2).sum().backward()
    assert s.grad is not None and s.grad.abs().sum() > 0
    assert torch.equal(wl.grad, 2 * out.detach())  # identity STE to the latent


def test_lsq_scale_gradient_matches_finite_difference_outside_rounding_edges():
    # away from rounding boundaries q is locally constant, so d(sum(s*q))/ds = sum(q) per group,
    # and LSQ's (q - w/s) term only differs inside; check the outside branch exactly
    w = torch.tensor([[3.0, -3.0, 2.0, 2.5] * 32])
    s = torch.ones(1, 1).requires_grad_()
    fake_quant(w, QuantSpec(method="lsq"), scale=s).sum().backward()
    q_sum = ternary_codes(w, s.detach(), QuantSpec(method="lsq")).float().sum()
    assert torch.allclose(s.grad, (q_sum / 128**0.5).reshape(1, 1))


def test_q_a_zero_fraction_on_gaussian():
    w = torch.randn(2048, 1024, generator=torch.Generator().manual_seed(0))
    s = init_scale(w, QuantSpec())
    zf = zero_fraction(ternary_codes(w, s, QuantSpec()))
    assert abs(zf - 0.31) <= 0.01, zf


@pytest.mark.parametrize("method", ["absmean", "twn"])
@pytest.mark.parametrize("group", [64, 128])
def test_quantising_ternary_is_idempotent(method, group):
    """Codes are idempotent under every quantiser. Values are idempotent under Q-B (its scale is the mean
    of the nonzeros) but not Q-A: the absmean of a ternary group is s * (nonzero fraction), so the codes
    survive and the scale shrinks (DECISIONS.md D-09)."""
    spec = QuantSpec(method=method, group_size=group)
    w = gen((24, 384), seed=6)
    once = fake_quant(w, spec)
    s1, s2 = init_scale(w, spec), init_scale(once, spec)
    q1, q2 = ternary_codes(w, s1, spec), ternary_codes(once, s2, spec)
    assert torch.equal(q1, q2)
    if method == "twn":
        assert torch.equal(fake_quant(once, spec), once)
    else:
        nz = (q1 != 0).float().unflatten(-1, (-1, group)).mean(-1)
        assert torch.allclose(s2, canonical_scale(s1 * nz), rtol=BF16_ULP, atol=0)


def test_lsq_requantising_with_its_own_scale_is_idempotent():
    spec = QuantSpec(method="lsq")
    w = gen((24, 384), seed=10)
    s = init_scale(w, spec)
    once = fake_quant(w, spec, scale=s)
    assert torch.equal(fake_quant(once, spec, scale=s), once)


def _llama_quantize_row_q2_0_ref(x: list[float]) -> tuple[float, bytes]:
    """Port of mainline llama.cpp quantize_row_q2_0_ref for one 64-element block (ggml-quants.c):
    d = max|x|, q = clamp(roundf(x / d) + 1, 0, 3), element j at bits 2*(j%4) of byte j/4."""
    import math
    d = max(abs(v) for v in x)
    inv = 1.0 / d if d > 0 else 0.0
    qs = bytearray(16)
    for j, v in enumerate(x):
        r = v * inv
        q = int(math.floor(abs(r) + 0.5)) * (1 if r >= 0 else -1) + 1  # roundf: half away from zero
        qs[j // 4] |= max(0, min(3, q)) << ((j % 4) * 2)
    return d, bytes(qs)


def test_pack_2bit_matches_llama_q2_0_layout():
    # a g128 ternary group exports as two g64 Q2_0 blocks sharing its scale (T2.7)
    spec = QuantSpec()
    w = gen((4, 256), seed=11)
    p = export_packed(w, spec)
    deq = dequant_packed(p, torch.float32)
    for r in range(4):
        for blk in range(4):  # 256 / 64
            vals = deq[r, blk * 64:(blk + 1) * 64].tolist()
            d, qs = _llama_quantize_row_q2_0_ref(vals)
            ours = bytes(p["codes"][r, blk * 16:(blk + 1) * 16].tolist())
            assert qs == ours
            assert d == float(p["scales"][r, blk // 2]) or not any(vals)


def test_scales_are_exact_in_fp16():
    w = gen((64, 1024), seed=7, std=0.03)
    s = init_scale(w, QuantSpec())
    assert torch.equal(s.to(torch.float16).float(), s)
    assert torch.equal(canonical_scale(s), s)


@settings(max_examples=50, deadline=None)
@given(st.lists(st.integers(-1, 1), min_size=1, max_size=300))
def test_trit_pack_round_trip(vals):
    q = torch.tensor(vals, dtype=torch.int8)
    p = pack_trits(q)
    assert p.numel() == -(-len(vals) // 5)
    assert torch.equal(unpack_trits(p, len(vals)), q)


@settings(max_examples=50, deadline=None)
@given(st.integers(1, 64).flatmap(lambda n: st.lists(st.integers(-1, 1), min_size=4 * n, max_size=4 * n)))
def test_2bit_pack_round_trip(vals):
    q = torch.tensor(vals, dtype=torch.int8)
    p = pack_2bit(q)
    assert p.dtype == torch.uint8 and p.numel() == len(vals) // 4
    assert torch.equal(unpack_2bit(p), q)


def test_packed_export_reproduces_fake_quant_exactly():
    for spec in [QuantSpec(), QuantSpec(method="twn", group_size=64)]:
        w = gen((3, 64, 384), seed=8)
        expect = fake_quant(w, spec).to(torch.bfloat16)
        assert torch.equal(dequant_packed(export_packed(w, spec)), expect)


def test_marco_expert_shapes():
    # gate/up: [232, 384, 1024] -> 8 groups/row; down: [232, 1024, 384] -> 3 groups/row
    spec = QuantSpec()
    assert init_scale(torch.zeros(2, 384, 1024) + 0.01, spec).shape == (2, 384, 8)
    assert init_scale(torch.zeros(2, 1024, 384) + 0.01, spec).shape == (2, 1024, 3)
    with pytest.raises(ValueError):
        init_scale(torch.zeros(4, 100), spec)