WAL-Ternary-8B / code /wal_tat /transforms.py
armanibadboy's picture
Add files using upload-large-folder tool
92edcfa verified
Raw
History Blame Contribute Delete
9.42 kB
"""Fixed orthogonal transforms for packed low-bit linear layers.
The randomized Hadamard transform is block diagonal over the input feature
dimension. Both stored weight rows and row-major activations use the same
forward transform ``v -> (v * D) H``. Consequently, for orthogonal ``R = H D``
``linear(x, W) == linear(transform(x), transform(W))``.
Only the transformed weight is ternary and stored. The activation transform
must remain part of the deployed operator; materializing the inverse transform
into the weight would destroy the ternary representation.
"""
from __future__ import annotations
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .quantization import hard_codes_scales
def _power_of_two(value: int) -> bool:
return value > 0 and value & (value - 1) == 0
def normalized_hadamard(value: torch.Tensor) -> torch.Tensor:
"""Apply an orthonormal Walsh-Hadamard transform on the last dimension."""
features = int(value.shape[-1])
if not _power_of_two(features):
raise ValueError("Hadamard dimension must be a positive power of two")
result = value
width = 1
prefix = value.shape[:-1]
while width < features:
paired = result.reshape(*prefix, -1, 2, width)
left = paired[..., 0, :]
right = paired[..., 1, :]
result = torch.cat((left + right, left - right), dim=-1).reshape(
*prefix, features
)
width *= 2
return result / math.sqrt(features)
def rademacher_signs(
groups: int,
group_size: int,
*,
seed: int,
device: torch.device | str,
dtype: torch.dtype,
) -> torch.Tensor:
"""Generate device-independent deterministic Rademacher signs."""
if groups < 1:
raise ValueError("groups must be positive")
if not _power_of_two(group_size):
raise ValueError("group_size must be a positive power of two")
generator = torch.Generator(device="cpu")
generator.manual_seed(int(seed))
bits = torch.randint(
0,
2,
(groups, group_size),
generator=generator,
dtype=torch.int8,
device="cpu",
)
return bits.to(device=device, dtype=dtype).mul(2).sub(1)
def blockwise_randomized_hadamard(
value: torch.Tensor,
*,
group_size: int = 128,
seed: int = 109,
) -> torch.Tensor:
"""Map row vectors to the randomized-Hadamard basis.
The last dimension must be exactly divisible by ``group_size``. Padding is
deliberately rejected because padding before an orthogonal transform would
change the represented linear operator.
"""
features = int(value.shape[-1])
if features % group_size:
raise ValueError("input features must be divisible by group_size")
groups = features // group_size
grouped = value.reshape(*value.shape[:-1], groups, group_size)
signs = rademacher_signs(
groups,
group_size,
seed=seed,
device=value.device,
dtype=value.dtype,
)
return normalized_hadamard(grouped * signs).reshape_as(value)
def inverse_blockwise_randomized_hadamard(
value: torch.Tensor,
*,
group_size: int = 128,
seed: int = 109,
) -> torch.Tensor:
"""Map transformed weight rows back to the original dense basis."""
features = int(value.shape[-1])
if features % group_size:
raise ValueError("input features must be divisible by group_size")
groups = features // group_size
grouped = value.reshape(*value.shape[:-1], groups, group_size)
signs = rademacher_signs(
groups,
group_size,
seed=seed,
device=value.device,
dtype=value.dtype,
)
return (normalized_hadamard(grouped) * signs).reshape_as(value)
class FixedTernaryLinear(nn.Module):
"""Inference-only ternary linear with identity or randomized-Hadamard input."""
def __init__(
self,
codes: torch.Tensor,
scales: torch.Tensor,
*,
compute_dtype: torch.dtype,
transform: str = "identity",
transform_seed: int = 109,
bias: torch.Tensor | None = None,
):
super().__init__()
if codes.ndim != 3 or scales.shape != codes.shape[:2]:
raise ValueError("codes must be [out, groups, group_size]")
if not torch.all((codes >= -1) & (codes <= 1)):
raise ValueError("codes must be ternary")
if transform not in {"identity", "rht"}:
raise ValueError("transform must be 'identity' or 'rht'")
self.register_buffer("ternary_codes", codes.detach().to(torch.int8))
self.register_buffer("group_scales", scales.detach().float())
if bias is not None:
self.register_buffer("bias", bias.detach().clone())
else:
self.bias = None
self.compute_dtype = compute_dtype
self.transform = transform
self.transform_seed = int(transform_seed)
self.in_features = int(codes.shape[1] * codes.shape[2])
self.out_features = int(codes.shape[0])
self.group_size = int(codes.shape[2])
self.register_buffer(
"_evaluation_weight",
(
self.ternary_codes.float()
* self.group_scales.unsqueeze(-1)
)
.reshape(self.out_features, self.in_features)
.to(compute_dtype),
persistent=False,
)
@classmethod
@torch.no_grad()
def from_weight(
cls,
weight: torch.Tensor,
*,
group_size: int = 128,
transform: str = "identity",
transform_seed: int = 109,
bias: torch.Tensor | None = None,
) -> "FixedTernaryLinear":
if weight.ndim != 2:
raise ValueError("weight must be a matrix")
if weight.shape[1] % group_size:
raise ValueError("weight input features must be divisible by group_size")
transformed = weight.detach().float()
if transform == "rht":
transformed = blockwise_randomized_hadamard(
transformed, group_size=group_size, seed=transform_seed
)
elif transform != "identity":
raise ValueError("transform must be 'identity' or 'rht'")
flat_codes, scales = hard_codes_scales(transformed, group_size)
codes = flat_codes.reshape(weight.shape[0], -1, group_size)
return cls(
codes,
scales,
compute_dtype=weight.dtype,
transform=transform,
transform_seed=transform_seed,
bias=bias,
)
def transformed_weight(self) -> torch.Tensor:
return self._evaluation_weight
def effective_weight(self) -> torch.Tensor:
value = self.transformed_weight()
if self.transform == "rht":
value = inverse_blockwise_randomized_hadamard(
value,
group_size=self.group_size,
seed=self.transform_seed,
)
return value.to(self.compute_dtype)
def forward(self, value: torch.Tensor) -> torch.Tensor:
if self.transform == "rht":
value = blockwise_randomized_hadamard(
value,
group_size=self.group_size,
seed=self.transform_seed,
)
weight = self.transformed_weight().to(value.dtype)
bias = None if self.bias is None else self.bias.to(value.dtype)
return F.linear(value, weight, bias)
def code_histogram(self) -> dict[int, int]:
values, counts = torch.unique(self.ternary_codes.cpu(), return_counts=True)
result = {-1: 0, 0: 0, 1: 0}
result.update(
{int(value): int(count) for value, count in zip(values, counts)}
)
return result
class TransformedProxyTernaryLinear(nn.Module):
"""Training wrapper for hard-forward proxy codes in a transformed basis.
``matrix.effective_weight()`` is expected to return the deployed weight in
transform space. The input transform stays explicit so no dense inverse
weight is materialized during forward.
"""
def __init__(
self,
matrix: nn.Module,
*,
transform: str = "rht",
transform_seed: int = 109,
bias: torch.Tensor | None = None,
):
super().__init__()
if transform not in {"identity", "rht"}:
raise ValueError("transform must be 'identity' or 'rht'")
if not hasattr(matrix, "effective_weight"):
raise TypeError("matrix must provide effective_weight()")
self.matrix = matrix
if bias is not None:
self.register_buffer("bias", bias.detach().clone())
else:
self.bias = None
self.transform = transform
self.transform_seed = int(transform_seed)
self.in_features = int(matrix.in_features)
self.out_features = int(matrix.out_features)
self.group_size = int(matrix.group_size)
def forward(self, value: torch.Tensor) -> torch.Tensor:
if self.transform == "rht":
value = blockwise_randomized_hadamard(
value,
group_size=self.group_size,
seed=self.transform_seed,
)
weight = self.matrix.effective_weight().to(value.dtype)
bias = None if self.bias is None else self.bias.to(value.dtype)
return F.linear(value, weight, bias)