phanerozoic commited on
Commit
4adadc1
·
verified ·
1 Parent(s): b02a470

Tests reference the kernel-quantized operands; reciprocal quantization diverges one ulp at ties

Browse files
Files changed (1) hide show
  1. 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 _reference(x, W, scale_wt):
10
- """f32 reference: quantize per-token absmax, integer matmul, rescale."""
11
- xf = x.float()
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 test_bitnet_linear_matches_reference(M, dtype):
 
 
 
 
 
23
  torch.manual_seed(0)
24
  N, K = 512, 2560
25
  W = torch.randint(-1, 2, (N, K), dtype=torch.int8)
26
- w_packed = bitnet.pack_weights(W)
27
- scale_wt = (torch.rand(N) * 0.5 + 0.5).to(torch.bfloat16)
28
  x = torch.randn(M, K, dtype=dtype)
29
 
30
- y = bitnet.bitnet_linear(x, w_packed, scale_wt).float()
31
- ref = _reference(x, W, scale_wt)
32
- # bf16 output rounding is the only tolerance needed; the integer part
33
- # of the computation is exact.
34
- torch.testing.assert_close(y, ref, rtol=1e-2, atol=1e-2)
35
 
36
 
37
  @pytest.mark.kernels_ci
38
- def test_quantize_activation_roundtrip():
 
 
 
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(2)
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)