phanerozoic's picture
bitnet-cpu v1: kernel-builder source tree
ce8679e verified
Raw
History Blame
8.5 kB
"""bitnet-cpu: ternary x INT8 GEMM for BitNet b1.58 (W1.58 A8) on CPUs.
Same 2-bit weight packing and API as phanerozoic/bitnet-tc (the CUDA member
of the stack); AVX-512 VNNI / AVX-VNNI / AVX2 paths selected at runtime,
with a portable scalar fallback.
High-level API:
pack_weights(W) ternary {-1,0,+1} int8 [N,K] -> packed uint8 [N,K//4]
quantize_activation(x) bf16/f32 [..,K] -> (int8, per-row bf16 scale)
bitnet_gemm(...) int8 activations x packed ternary weights -> bf16
bitnet_gemv_fused(...) bf16/f32 activations (fused quantize) -> bf16, M<16
bitnet_linear(x, w, sw) one-shot forward, auto-dispatches by M
BitLinear nn.Module wrapper
BitLinearKernel kernelize layer for transformers BitNet modules
"""
from typing import Optional
import torch
import torch.nn as nn
from ._ops import ops
_OK_DTYPES = (torch.bfloat16, torch.float32)
def pack_weights(W: torch.Tensor) -> torch.Tensor:
"""Pack ternary weights {-1, 0, +1} into 2-bit codes, 4 per byte.
Encoding: -1 -> 1, 0 -> 2, +1 -> 3 (the kernel decodes via byte - 2).
Identical to the phanerozoic/bitnet-tc and Microsoft bitnet.cpp packing.
Args:
W: [N, K] int8 with values in {-1, 0, +1}.
Returns:
[N, K // 4] uint8.
"""
assert W.dtype == torch.int8, f"W must be int8, got {W.dtype}"
assert W.dim() == 2, "W must be 2D"
N, K = W.shape
assert K % 4 == 0, f"K={K} must be a multiple of 4"
assert ((W >= -1) & (W <= 1)).all(), "W values must be in {-1, 0, +1}"
codes = (W.to(torch.int32) + 2).to(torch.uint8).view(N, K // 4, 4)
packed = (
codes[..., 0]
| (codes[..., 1] << 2)
| (codes[..., 2] << 4)
| (codes[..., 3] << 6)
).contiguous()
return packed
def quantize_activation(x: torch.Tensor, eps: float = 1e-5):
"""Per-token absmax INT8 quantization.
Args:
x: [..., K] bf16 or f32 tensor.
Returns:
(x_int8 [M, K], scale [M] bf16) where M is the flattened leading dim.
"""
x_flat = x.reshape(-1, x.shape[-1]).contiguous()
M, K = x_flat.shape
if x_flat.dtype in _OK_DTYPES and x_flat.device.type == "cpu":
x_int8 = torch.empty((M, K), dtype=torch.int8)
scale = torch.empty((M,), dtype=torch.bfloat16)
ops.quantize_act(x_int8, scale, x_flat)
return x_int8, scale
absmax = x_flat.abs().amax(dim=-1, keepdim=True).clamp(min=eps)
scale = absmax / 127.0
x_int8 = (x_flat / scale).round().clamp(-127, 127).to(torch.int8)
return x_int8, scale.squeeze(-1).to(torch.bfloat16)
def bitnet_gemm(
x_int8: torch.Tensor,
w_packed: torch.Tensor,
scale_act: torch.Tensor,
scale_wt: torch.Tensor,
) -> torch.Tensor:
"""Ternary x INT8 GEMM. Returns [M, N] bf16."""
M, K = x_int8.shape
N = w_packed.shape[0]
out = torch.empty((M, N), dtype=torch.bfloat16)
ops.bitnet_gemm(out, x_int8, w_packed, scale_act, scale_wt, None)
return out
def bitnet_gemv_fused(
x: torch.Tensor,
w_packed: torch.Tensor,
scale_wt: torch.Tensor,
) -> torch.Tensor:
"""Fused quantize + ternary GEMV for M < 16. Returns [M, N] bf16."""
M, K = x.shape
N = w_packed.shape[0]
out = torch.empty((M, N), dtype=torch.bfloat16)
ops.bitnet_gemv_fused(out, x, w_packed, scale_wt)
return out
def bitnet_linear(
x: torch.Tensor,
w_packed: torch.Tensor,
scale_wt: torch.Tensor,
) -> torch.Tensor:
"""One-shot BitLinear forward. Auto-dispatches fused (M<16) vs split path.
Args:
x: [..., K] bf16 or f32.
w_packed: [N, K//4] uint8 packed ternary weights.
scale_wt: [N] bf16 per-output weight scale.
Returns:
[..., N] bf16.
"""
assert x.dtype in _OK_DTYPES, f"x must be bf16 or f32, got {x.dtype}"
orig = x.shape
K = orig[-1]
x_flat = x.reshape(-1, K).contiguous()
M = x_flat.shape[0]
N = w_packed.shape[0]
if M < 16:
y = bitnet_gemv_fused(x_flat, w_packed, scale_wt)
else:
x_int8, scale_act = quantize_activation(x_flat)
y = bitnet_gemm(x_int8, w_packed, scale_act, scale_wt)
return y.view(*orig[:-1], N)
class BitLinear(nn.Module):
"""Inference-only BitLinear: ternary weights, per-token INT8 activations."""
def __init__(self, in_features: int, out_features: int, bias: bool = False):
super().__init__()
assert in_features % 32 == 0, f"in_features={in_features} must be % 32"
self.in_features = in_features
self.out_features = out_features
self.register_buffer(
"w_packed", torch.zeros(out_features, in_features // 4, dtype=torch.uint8)
)
self.register_buffer(
"scale_wt", torch.ones(out_features, dtype=torch.bfloat16)
)
if bias:
self.bias = nn.Parameter(torch.zeros(out_features, dtype=torch.bfloat16))
else:
self.bias = None
@classmethod
def from_dense(cls, lin: nn.Linear) -> "BitLinear":
"""Quantize a dense Linear via absmean ternarization (sanity-test only;
a real BitNet model is trained with QAT, not post-hoc quantized)."""
out_features, in_features = lin.weight.shape
bl = cls(in_features, out_features, bias=lin.bias is not None)
with torch.no_grad():
W = lin.weight.detach().float()
gamma = W.abs().mean(dim=-1, keepdim=True).clamp(min=1e-8)
Wq = (W / gamma).round().clamp(-1, 1).to(torch.int8)
bl.w_packed.copy_(pack_weights(Wq.cpu()))
bl.scale_wt.copy_(gamma.squeeze(-1).to(torch.bfloat16))
if bl.bias is not None:
bl.bias.copy_(lin.bias.detach().to(torch.bfloat16))
return bl
def forward(self, x: torch.Tensor) -> torch.Tensor:
y = bitnet_linear(x, self.w_packed, self.scale_wt)
if self.bias is not None:
y = y + self.bias
return y
class BitLinearKernel(nn.Module):
"""kernelize layer for the transformers BitNet linear modules.
kernelize binds this forward onto the host BitLinear / AutoBitLinear module,
so self is that module and the forward reads its attributes (weight,
weight_scale, rms_norm, bias). The ternary weight is converted to the
kernel's 2-bit layout once and cached; each call runs per-token INT8
quantization and the VNNI/AVX2 ternary GEMM.
"""
can_torch_compile: bool = False
def forward(self, input: torch.Tensor) -> torch.Tensor:
dt = input.dtype
rms = getattr(self, "rms_norm", None)
if rms is not None:
input = rms(input)
if not hasattr(self, "_bitnet_cpu_packed"):
w = self.weight
if w.dtype == torch.uint8:
# transformers stores ternary as int8 viewed through uint8 (255 -> -1).
tern = w.contiguous().view(torch.int8)
if tern.shape[0] != self.out_features: # bit-packed (out//4, in)
from transformers.integrations.bitnet import unpack_weights
tern = unpack_weights(w, dtype=torch.bfloat16)
tern = tern.round().clamp(-1, 1).to(torch.int8)
else:
g = w.detach().float().abs().mean().clamp(min=1e-5)
tern = (w.detach().float() / g).round().clamp(-1, 1).to(torch.int8)
self._bitnet_cpu_n = tern.shape[0]
self._bitnet_cpu_packed = pack_weights(tern.contiguous().cpu())
ws = getattr(self, "weight_scale", None)
if ws is not None:
s = ws.detach().float().reshape(-1)
s = s.expand(self._bitnet_cpu_n) if s.numel() == 1 else s
else:
s = w.detach().float().abs().mean().clamp(min=1e-5).reshape(1).expand(self._bitnet_cpu_n)
self._bitnet_cpu_sw = s.to(torch.bfloat16).contiguous().cpu()
shp = input.shape
x = input.reshape(-1, shp[-1])
if x.dtype not in _OK_DTYPES:
x = x.to(torch.float32)
x = x.contiguous()
out = bitnet_linear(x, self._bitnet_cpu_packed, self._bitnet_cpu_sw)
if getattr(self, "bias", None) is not None:
out = out + self.bias
return out.reshape(*shp[:-1], self._bitnet_cpu_n).to(dt)
__all__ = [
"pack_weights",
"quantize_activation",
"bitnet_gemm",
"bitnet_gemv_fused",
"bitnet_linear",
"BitLinear",
"BitLinearKernel",
]