gemma4-e2b-exp-quant / src /error_budget_residual.py
toxzak's picture
Initial upload: mixed-budget sub-4-bit artifacts + perplexity result
9c41926 verified
Raw
History Blame Contribute Delete
19.3 kB
from __future__ import annotations
from math import ceil, log2
from typing import Sequence
import torch
from .groupwise_int4 import pack_signed_int4, unpack_signed_int4
def pack_signed_int2(values: torch.Tensor) -> torch.Tensor:
"""Pack signed INT2 values in {-1, 0, 1}, four values per byte."""
flat = values.to(torch.int8).flatten()
if flat.numel() and (flat.min().item() < -1 or flat.max().item() > 1):
raise ValueError("signed int2 values must be in {-1, 0, 1}")
encoded = (flat + 1).to(torch.uint8)
pad = (-encoded.numel()) % 4
if pad:
encoded = torch.cat([encoded, torch.ones(pad, dtype=torch.uint8, device=encoded.device)])
encoded = encoded.reshape(-1, 4)
return (
encoded[:, 0]
| torch.bitwise_left_shift(encoded[:, 1], 2)
| torch.bitwise_left_shift(encoded[:, 2], 4)
| torch.bitwise_left_shift(encoded[:, 3], 6)
).contiguous()
def unpack_signed_int2(packed: torch.Tensor, count: int) -> torch.Tensor:
"""Unpack bytes produced by pack_signed_int2."""
if count < 0:
raise ValueError("count must be non-negative")
packed = packed.to(torch.uint8).flatten()
values = torch.stack(
(
torch.bitwise_and(packed, 0x03),
torch.bitwise_and(torch.bitwise_right_shift(packed, 2), 0x03),
torch.bitwise_and(torch.bitwise_right_shift(packed, 4), 0x03),
torch.bitwise_and(torch.bitwise_right_shift(packed, 6), 0x03),
),
dim=1,
).flatten()[:count]
return values.to(torch.int16).sub(1).to(torch.int8)
def pack_binary_sign(values: torch.Tensor) -> torch.Tensor:
"""Pack residual signs {-1, +1}, eight signs per byte."""
flat = values.to(torch.int8).flatten()
if flat.numel() and not torch.all((flat == -1) | (flat == 1) | (flat == 0)):
raise ValueError("binary residual signs must be -1, 0, or 1")
encoded = torch.where(flat > 0, torch.ones_like(flat, dtype=torch.uint8), torch.zeros_like(flat, dtype=torch.uint8))
pad = (-encoded.numel()) % 8
if pad:
encoded = torch.cat([encoded, torch.zeros(pad, dtype=torch.uint8, device=encoded.device)])
encoded = encoded.reshape(-1, 8)
shifts = torch.arange(8, dtype=torch.uint8, device=encoded.device)
return torch.sum(torch.bitwise_left_shift(encoded, shifts), dim=1).to(torch.uint8).contiguous()
def unpack_binary_sign(packed: torch.Tensor, count: int) -> torch.Tensor:
"""Unpack bytes produced by pack_binary_sign into {-1, +1} signs."""
if count < 0:
raise ValueError("count must be non-negative")
packed = packed.to(torch.uint8).flatten()
shifts = torch.arange(8, dtype=torch.uint8, device=packed.device)
bits = torch.bitwise_and(torch.bitwise_right_shift(packed.unsqueeze(1), shifts), 1).flatten()[:count]
return torch.where(bits > 0, torch.ones_like(bits, dtype=torch.int8), -torch.ones_like(bits, dtype=torch.int8))
def _validate_2d_shape(shape: Sequence[int]) -> tuple[int, int]:
if len(shape) != 2:
raise ValueError("shape must be a 2D weight matrix shape")
rows, cols = int(shape[0]), int(shape[1])
if rows <= 0 or cols <= 0:
raise ValueError("shape dimensions must be positive")
return rows, cols
def estimate_binary_residual_bpw(
shape: Sequence[int],
group_size: int = 128,
scale_bits: int = 16,
) -> float:
"""Estimate BPW for INT2 base + 1-bit residual signs + two group scales."""
if group_size <= 0:
raise ValueError("group_size must be positive")
rows, cols = _validate_2d_shape(shape)
groups_per_row = ceil(cols / group_size)
weight_bits = rows * cols * 3
scale_bits_total = rows * groups_per_row * scale_bits * 2
return (weight_bits + scale_bits_total) / (rows * cols)
def estimate_error_budget_residual_bpw(
shape: Sequence[int],
group_size: int = 128,
outliers_per_group: int = 8,
scale_bits: int = 16,
correction_bits: int = 4,
) -> float:
"""Estimate BPW for INT2 base plus sparse residual corrections.
Each group stores a base scale, a residual correction scale, and up to
outliers_per_group index+signed-correction pairs.
"""
if group_size <= 0:
raise ValueError("group_size must be positive")
if outliers_per_group < 0:
raise ValueError("outliers_per_group must be non-negative")
rows, cols = _validate_2d_shape(shape)
groups_per_row = ceil(cols / group_size)
groups = rows * groups_per_row
index_bits = max(1, ceil(log2(group_size)))
base_and_binary_bits = rows * cols * 3
base_and_binary_scale_bits = groups * scale_bits * 2
outlier_scale_bits = groups * scale_bits if outliers_per_group else 0
outlier_bits = groups * outliers_per_group * (index_bits + correction_bits)
return (base_and_binary_bits + base_and_binary_scale_bits + outlier_scale_bits + outlier_bits) / (rows * cols)
def _reshape_blocks(weight: torch.Tensor, group_size: int) -> tuple[torch.Tensor, int]:
if weight.ndim != 2:
raise ValueError("weight must be a 2D tensor")
if group_size <= 0:
raise ValueError("group_size must be positive")
source = weight.detach().to(torch.float32)
rows, cols = source.shape
groups_per_row = ceil(cols / group_size)
padded_cols = groups_per_row * group_size
if padded_cols != cols:
padded = torch.zeros((rows, padded_cols), dtype=source.dtype, device=source.device)
padded[:, :cols] = source
source = padded
return source.reshape(rows, groups_per_row, group_size), padded_cols
def quantize_binary_residual(
weight: torch.Tensor,
group_size: int = 128,
scale_dtype: torch.dtype = torch.float16,
) -> dict:
"""Quantize with an INT2 base and per-group 1-bit residual correction.
The residual code stores only the sign of the base reconstruction error plus
one learned magnitude per group. This is a compact candidate below 4 BPW.
"""
blocks, padded_cols = _reshape_blocks(weight, group_size)
rows, cols = weight.shape
base_max = blocks.abs().amax(dim=2)
safe_max = torch.where(base_max > 0, base_max, torch.ones_like(base_max))
multipliers = torch.tensor([0.375, 0.5, 0.625, 0.75, 0.875, 1.0], dtype=torch.float32, device=blocks.device)
best_sse = torch.full_like(base_max, float("inf"))
best_q = torch.zeros_like(blocks, dtype=torch.int8)
best_base_scales = torch.ones_like(base_max)
best_residual_sign = torch.zeros_like(blocks, dtype=torch.int8)
best_residual_scales = torch.zeros_like(base_max)
for multiplier in multipliers:
candidate_scales = (safe_max * multiplier).to(scale_dtype).to(torch.float32)
q = torch.round(blocks / candidate_scales.unsqueeze(-1)).clamp(-1, 1)
base_rec = q * candidate_scales.unsqueeze(-1)
residual = blocks - base_rec
residual_scales = residual.abs().mean(dim=2).to(scale_dtype).to(torch.float32)
residual_sign = torch.where(residual >= 0, 1.0, -1.0)
residual_rec = residual_sign * residual_scales.unsqueeze(-1)
sse = (residual - residual_rec).pow(2).sum(dim=2)
better = sse < best_sse
best_sse = torch.where(better, sse, best_sse)
best_base_scales = torch.where(better, candidate_scales, best_base_scales)
best_residual_scales = torch.where(better, residual_scales, best_residual_scales)
best_q = torch.where(better.unsqueeze(-1), q.to(torch.int8), best_q)
best_residual_sign = torch.where(better.unsqueeze(-1), residual_sign.to(torch.int8), best_residual_sign)
zero_groups = base_max <= 0
best_residual_sign = torch.where(
zero_groups.unsqueeze(-1),
torch.zeros_like(best_residual_sign),
best_residual_sign,
)
best_residual_scales = torch.where(zero_groups, torch.zeros_like(best_residual_scales), best_residual_scales)
return {
"format": "int2_binary_residual",
"base_bits": 2,
"residual_bits": 1,
"group_size": int(group_size),
"orig_shape": [int(rows), int(cols)],
"padded_shape": [int(rows), int(padded_cols)],
"base_packed": pack_signed_int2(best_q.cpu()),
"base_count": int(best_q.numel()),
"base_scales": best_base_scales.to(scale_dtype).cpu(),
"residual_packed": pack_binary_sign(best_residual_sign.cpu()),
"residual_count": int(best_residual_sign.numel()),
"residual_scales": best_residual_scales.to(scale_dtype).cpu(),
"bpw": estimate_binary_residual_bpw((rows, cols), group_size=group_size, scale_bits=16),
}
def _valid_block_mask(rows: int, cols: int, padded_cols: int, group_size: int, device: torch.device) -> torch.Tensor:
valid = torch.zeros((rows, padded_cols), dtype=torch.bool, device=device)
valid[:, :cols] = True
return valid.reshape(rows, padded_cols // group_size, group_size)
def quantize_error_budget_residual(
weight: torch.Tensor,
group_size: int = 128,
outliers_per_group: int = 8,
scale_dtype: torch.dtype = torch.float16,
) -> dict:
"""Quantize with an INT2 base plus sparse top-error residual corrections."""
if outliers_per_group < 0:
raise ValueError("outliers_per_group must be non-negative")
if group_size > 256:
raise ValueError("group_size must be <= 256 so residual indices fit in uint8")
blocks, padded_cols = _reshape_blocks(weight, group_size)
rows, cols = weight.shape
groups_per_row = padded_cols // group_size
valid_blocks = _valid_block_mask(rows, cols, padded_cols, group_size, blocks.device)
k = min(int(outliers_per_group), group_size)
base_max = blocks.abs().amax(dim=2)
safe_max = torch.where(base_max > 0, base_max, torch.ones_like(base_max))
multipliers = torch.tensor([0.375, 0.5, 0.625, 0.75, 0.875, 1.0], dtype=torch.float32, device=blocks.device)
best_sse = torch.full_like(base_max, float("inf"))
best_q = torch.zeros_like(blocks, dtype=torch.int8)
best_base_scales = torch.ones_like(base_max)
best_binary_sign = torch.zeros_like(blocks, dtype=torch.int8)
best_binary_scales = torch.zeros_like(base_max)
best_indices = torch.zeros((rows, groups_per_row, k), dtype=torch.long, device=blocks.device)
best_counts = torch.zeros((rows, groups_per_row), dtype=torch.long, device=blocks.device)
best_outlier_q = torch.zeros((rows, groups_per_row, k), dtype=torch.int8, device=blocks.device)
best_outlier_scales = torch.zeros_like(base_max)
for multiplier in multipliers:
candidate_scales = (safe_max * multiplier).to(scale_dtype).to(torch.float32)
q = torch.round(blocks / candidate_scales.unsqueeze(-1)).clamp(-1, 1)
base_rec = q * candidate_scales.unsqueeze(-1)
residual = blocks - base_rec
binary_scales = residual.abs().mean(dim=2).to(scale_dtype).to(torch.float32)
binary_sign = torch.where(residual >= 0, 1.0, -1.0)
binary_rec = binary_sign * binary_scales.unsqueeze(-1)
remaining = residual - binary_rec
outlier_rec = torch.zeros_like(blocks)
top_idx = torch.zeros((rows, groups_per_row, k), dtype=torch.long, device=blocks.device)
selected_counts = torch.zeros((rows, groups_per_row), dtype=torch.long, device=blocks.device)
outlier_q = torch.zeros((rows, groups_per_row, k), dtype=torch.int8, device=blocks.device)
outlier_scales = torch.zeros_like(base_max)
if k:
masked_abs = torch.where(valid_blocks, remaining.abs(), torch.full_like(remaining, -1.0))
top_abs, top_idx = torch.topk(masked_abs, k=k, dim=2)
selected = top_abs >= 0
selected_counts = selected.sum(dim=2)
gathered = torch.gather(remaining, 2, top_idx)
max_abs = torch.where(top_abs[:, :, 0] > 0, top_abs[:, :, 0], torch.zeros_like(top_abs[:, :, 0]))
outlier_scales = (max_abs / 7.0).to(scale_dtype).to(torch.float32)
denom = torch.where(outlier_scales > 0, outlier_scales, torch.ones_like(outlier_scales))
outlier_q = torch.round(gathered / denom.unsqueeze(-1)).clamp(-7, 7).to(torch.int8)
outlier_q = torch.where(selected, outlier_q, torch.zeros_like(outlier_q))
correction = outlier_q.to(torch.float32) * outlier_scales.unsqueeze(-1)
correction = torch.where(selected, correction, torch.zeros_like(correction))
outlier_rec.scatter_add_(2, top_idx, correction)
error = torch.where(valid_blocks, base_rec + binary_rec + outlier_rec - blocks, torch.zeros_like(blocks))
sse = error.pow(2).sum(dim=2)
better = sse < best_sse
best_sse = torch.where(better, sse, best_sse)
best_base_scales = torch.where(better, candidate_scales, best_base_scales)
best_q = torch.where(better.unsqueeze(-1), q.to(torch.int8), best_q)
best_binary_sign = torch.where(better.unsqueeze(-1), binary_sign.to(torch.int8), best_binary_sign)
best_binary_scales = torch.where(better, binary_scales, best_binary_scales)
best_indices = torch.where(better.unsqueeze(-1), top_idx, best_indices)
best_counts = torch.where(better, selected_counts, best_counts)
best_outlier_q = torch.where(better.unsqueeze(-1), outlier_q, best_outlier_q)
best_outlier_scales = torch.where(better, outlier_scales, best_outlier_scales)
return {
"format": "int2_error_budget_residual",
"base_bits": 2,
"correction_bits": 4,
"group_size": int(group_size),
"outliers_per_group": int(outliers_per_group),
"orig_shape": [int(rows), int(cols)],
"padded_shape": [int(rows), int(padded_cols)],
"base_packed": pack_signed_int2(best_q.cpu()),
"base_count": int(best_q.numel()),
"base_scales": best_base_scales.to(scale_dtype).cpu(),
"binary_residual_packed": pack_binary_sign(best_binary_sign.cpu()),
"binary_residual_count": int(best_binary_sign.numel()),
"binary_residual_scales": best_binary_scales.to(scale_dtype).cpu(),
"outlier_indices": best_indices.to(torch.uint8).cpu(),
"outlier_counts": best_counts.to(torch.uint8).cpu(),
"outlier_packed": pack_signed_int4(best_outlier_q.cpu()),
"outlier_count": int(best_outlier_q.numel()),
"outlier_scales": best_outlier_scales.to(scale_dtype).cpu(),
"bpw": estimate_error_budget_residual_bpw(
(rows, cols),
group_size=group_size,
outliers_per_group=outliers_per_group,
scale_bits=16,
correction_bits=4,
),
}
def dequantize_binary_residual(
entry: dict,
device: str | torch.device = "cpu",
include_residual: bool = True,
) -> torch.Tensor:
"""Dequantize an INT2 base plus optional 1-bit residual correction."""
rows, cols = [int(dim) for dim in entry["orig_shape"]]
padded_rows, padded_cols = [int(dim) for dim in entry.get("padded_shape", entry["orig_shape"])]
group_size = int(entry["group_size"])
if padded_rows != rows:
raise ValueError("padded row count must match original row count")
if padded_cols % group_size != 0:
raise ValueError("padded columns must be divisible by group_size")
groups_per_row = padded_cols // group_size
if "base_packed" in entry:
base_q = unpack_signed_int2(entry["base_packed"].to(device), int(entry["base_count"]))
else:
base_q = entry["base_q"].to(device=device, dtype=torch.int8).flatten()
base_q = base_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
base_scales = entry["base_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
restored = base_q * base_scales
if include_residual:
if "residual_packed" in entry:
residual_sign = unpack_binary_sign(entry["residual_packed"].to(device), int(entry["residual_count"]))
else:
residual_sign = entry["residual_sign"].to(device=device, dtype=torch.int8).flatten()
residual_sign = residual_sign.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
residual_scales = entry["residual_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
restored = restored + residual_sign * residual_scales
return restored.reshape(rows, padded_cols)[:, :cols].contiguous()
def dequantize_error_budget_residual(
entry: dict,
device: str | torch.device = "cpu",
include_residual: bool = True,
) -> torch.Tensor:
"""Dequantize an INT2 base plus sparse residual correction side channel."""
rows, cols = [int(dim) for dim in entry["orig_shape"]]
padded_rows, padded_cols = [int(dim) for dim in entry.get("padded_shape", entry["orig_shape"])]
group_size = int(entry["group_size"])
if padded_rows != rows:
raise ValueError("padded row count must match original row count")
if padded_cols % group_size != 0:
raise ValueError("padded columns must be divisible by group_size")
groups_per_row = padded_cols // group_size
base_q = unpack_signed_int2(entry["base_packed"].to(device), int(entry["base_count"]))
base_q = base_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
base_scales = entry["base_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
restored = base_q * base_scales
if include_residual:
binary_sign = unpack_binary_sign(
entry["binary_residual_packed"].to(device),
int(entry["binary_residual_count"]),
)
binary_sign = binary_sign.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, group_size)
binary_scales = entry["binary_residual_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
restored = restored + binary_sign * binary_scales
k = int(entry["outliers_per_group"])
if k <= 0:
return restored.reshape(rows, padded_cols)[:, :cols].contiguous()
outlier_q = unpack_signed_int4(entry["outlier_packed"].to(device), int(entry["outlier_count"]))
outlier_q = outlier_q.to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, k)
indices = entry["outlier_indices"].to(device=device, dtype=torch.long).reshape(rows, groups_per_row, k)
counts = entry["outlier_counts"].to(device=device, dtype=torch.long).reshape(rows, groups_per_row)
outlier_scales = entry["outlier_scales"].to(device=device, dtype=torch.float32).reshape(rows, groups_per_row, 1)
valid = torch.arange(k, device=device).reshape(1, 1, k) < counts.unsqueeze(-1)
correction = torch.where(valid, outlier_q * outlier_scales, torch.zeros_like(outlier_q))
restored_flat = restored.reshape(rows * groups_per_row, group_size)
group_ids = torch.arange(rows * groups_per_row, device=device).reshape(rows, groups_per_row, 1).expand_as(indices)
restored_flat.index_put_(
(group_ids.reshape(-1)[valid.reshape(-1)], indices.reshape(-1)[valid.reshape(-1)]),
correction.reshape(-1)[valid.reshape(-1)],
accumulate=True,
)
return restored.reshape(rows, padded_cols)[:, :cols].contiguous()