Tests reference the kernel-quantized operands; reciprocal quantization diverges one ulp at ties
Browse files- tests/test_bitnet_cpu.py +37 -18
tests/test_bitnet_cpu.py
CHANGED
|
@@ -6,47 +6,66 @@ import kernels
|
|
| 6 |
bitnet = kernels.get_kernel("phanerozoic/bitnet-cpu", version=1, trust_remote_code=True)
|
| 7 |
|
| 8 |
|
| 9 |
-
def
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
absmax = xf.abs().amax(dim=-1, keepdim=True).clamp(min=1e-5)
|
| 13 |
-
scale = absmax / 127.0
|
| 14 |
-
q = (xf / scale).round().clamp(-127, 127)
|
| 15 |
-
acc = q @ W.float().t()
|
| 16 |
-
return acc * scale * scale_wt.float().unsqueeze(0)
|
| 17 |
|
| 18 |
|
| 19 |
@pytest.mark.kernels_ci
|
| 20 |
@pytest.mark.parametrize("M", [1, 4, 16, 128])
|
| 21 |
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
|
| 22 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
torch.manual_seed(0)
|
| 24 |
N, K = 512, 2560
|
| 25 |
W = torch.randint(-1, 2, (N, K), dtype=torch.int8)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
x = torch.randn(M, K, dtype=dtype)
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
|
| 37 |
@pytest.mark.kernels_ci
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
torch.manual_seed(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
x = torch.randn(8, 1024, dtype=torch.bfloat16)
|
| 41 |
q, s = bitnet.quantize_activation(x)
|
| 42 |
assert q.dtype == torch.int8 and s.dtype == torch.bfloat16
|
|
|
|
| 43 |
recon = q.float() * s.float().unsqueeze(-1)
|
| 44 |
torch.testing.assert_close(recon, x.float(), rtol=2e-2, atol=2e-2)
|
| 45 |
|
| 46 |
|
| 47 |
@pytest.mark.kernels_ci
|
| 48 |
def test_bitlinear_module():
|
| 49 |
-
torch.manual_seed(
|
| 50 |
lin = torch.nn.Linear(2560, 512, bias=False)
|
| 51 |
bl = bitnet.BitLinear.from_dense(lin)
|
| 52 |
x = torch.randn(4, 2560, dtype=torch.bfloat16)
|
|
|
|
| 6 |
bitnet = kernels.get_kernel("phanerozoic/bitnet-cpu", version=1, trust_remote_code=True)
|
| 7 |
|
| 8 |
|
| 9 |
+
def unpack_ternary(wp):
|
| 10 |
+
cols = [((wp >> (2 * j)) & 3).to(torch.int16) - 2 for j in range(4)]
|
| 11 |
+
return torch.stack(cols, dim=-1).reshape(wp.shape[0], wp.shape[1] * 4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
@pytest.mark.kernels_ci
|
| 15 |
@pytest.mark.parametrize("M", [1, 4, 16, 128])
|
| 16 |
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float32])
|
| 17 |
+
def test_gemm_matches_exact_reference(M, dtype):
|
| 18 |
+
"""The integer path is exact: feeding the kernel's own quantized operands
|
| 19 |
+
into an f32 reference bounds the residual by bf16 output rounding alone
|
| 20 |
+
(one ulp, 2^-8 relative). An independent Python re-quantization is not a
|
| 21 |
+
valid reference: dividing by amax/127 and multiplying by 127/amax differ
|
| 22 |
+
by one ulp at rounding boundaries and flip occasional codes."""
|
| 23 |
torch.manual_seed(0)
|
| 24 |
N, K = 512, 2560
|
| 25 |
W = torch.randint(-1, 2, (N, K), dtype=torch.int8)
|
| 26 |
+
wp = bitnet.pack_weights(W)
|
| 27 |
+
sw = (torch.rand(N) * 0.5 + 0.5).to(torch.bfloat16)
|
| 28 |
x = torch.randn(M, K, dtype=dtype)
|
| 29 |
|
| 30 |
+
q, s = bitnet.quantize_activation(x)
|
| 31 |
+
y = bitnet.bitnet_gemm(q, wp, s, sw).float()
|
| 32 |
+
ref = (q.float() @ unpack_ternary(wp).float().t()) * s.float().unsqueeze(-1) * sw.float().unsqueeze(0)
|
| 33 |
+
rel = ((y - ref).abs() / ref.abs().clamp(min=1.0)).max().item()
|
| 34 |
+
assert rel < 8e-3, f"max rel {rel}"
|
| 35 |
|
| 36 |
|
| 37 |
@pytest.mark.kernels_ci
|
| 38 |
+
@pytest.mark.parametrize("M", [1, 4, 15])
|
| 39 |
+
def test_fused_path_matches_gemm_path(M):
|
| 40 |
+
"""The fused (M<16) path quantizes internally with the same code as
|
| 41 |
+
quantize_activation; outputs agree to bf16 rounding of the scale."""
|
| 42 |
torch.manual_seed(1)
|
| 43 |
+
N, K = 1024, 4096
|
| 44 |
+
W = torch.randint(-1, 2, (N, K), dtype=torch.int8)
|
| 45 |
+
wp = bitnet.pack_weights(W)
|
| 46 |
+
sw = torch.ones(N, dtype=torch.bfloat16)
|
| 47 |
+
x = torch.randn(M, K, dtype=torch.bfloat16)
|
| 48 |
+
y_fused = bitnet.bitnet_gemv_fused(x, wp, sw).float()
|
| 49 |
+
q, s = bitnet.quantize_activation(x)
|
| 50 |
+
y_split = bitnet.bitnet_gemm(q, wp, s, sw).float()
|
| 51 |
+
rel = ((y_fused - y_split).abs() / y_split.abs().clamp(min=1.0)).max().item()
|
| 52 |
+
assert rel < 8e-3, f"max rel {rel}"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.mark.kernels_ci
|
| 56 |
+
def test_quantize_activation_roundtrip():
|
| 57 |
+
torch.manual_seed(2)
|
| 58 |
x = torch.randn(8, 1024, dtype=torch.bfloat16)
|
| 59 |
q, s = bitnet.quantize_activation(x)
|
| 60 |
assert q.dtype == torch.int8 and s.dtype == torch.bfloat16
|
| 61 |
+
assert (q.abs() <= 127).all()
|
| 62 |
recon = q.float() * s.float().unsqueeze(-1)
|
| 63 |
torch.testing.assert_close(recon, x.float(), rtol=2e-2, atol=2e-2)
|
| 64 |
|
| 65 |
|
| 66 |
@pytest.mark.kernels_ci
|
| 67 |
def test_bitlinear_module():
|
| 68 |
+
torch.manual_seed(3)
|
| 69 |
lin = torch.nn.Linear(2560, 512, bias=False)
|
| 70 |
bl = bitnet.BitLinear.from_dense(lin)
|
| 71 |
x = torch.randn(4, 2560, dtype=torch.bfloat16)
|