Spaces:
Running on Zero
Running on Zero
File size: 10,471 Bytes
855e1f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | """
OrbitQuant weight-only quantization backend (orbit2 / orbit3 / orbit4 qtypes).
Implements the weight half of "OrbitQuant: Data-Agnostic Quantization for Image and
Video Diffusion Transformers" (arXiv:2607.02461) as an OstrisQuantizer backend (see
toolkit/util/ostris_quant.py). Each linear weight is rotated with a randomized
permuted block-Hadamard (RPBH) rotation shared per input dimension, split into
per-row norms and unit directions, and the directions are quantized with a Lloyd-Max
codebook fit to the fixed post-rotation coordinate marginal N(0, 1/d).
No calibration data is needed.
At runtime the forward rotation is applied to the activations instead of un-rotating
the weight, so the two cancel in the matmul (rotate = multiply by P):
W' = W P^T, y = dequant(W') (P x) ~= W x
Activations are not quantized. This is meant for holding a frozen base model at low
bit-width while training adapters on top; activation quantization would only add
noise without fused low-bit kernels.
Quantized state attached to each module:
orbit_packed packed codebook indices (uint8 bitstream)
orbit_row_norms per output row l2 norms of the rotated weight (original dtype)
orbit_codebook Lloyd-Max centroids for N(0, 1/in_features) (float32)
orbit_perm / orbit_inv_perm / orbit_signs shared RPBH rotation for in_features
orbit_bits / orbit_block bit-width and Hadamard block size
"""
import math
from typing import Dict, Tuple
import torch
import torch.nn.functional as F
from toolkit.print import print_acc
from toolkit.util.ostris_quant import OstrisQuantizer
# qtype name -> bits per weight coordinate
ORBIT_QTYPES = {"orbit2": 2, "orbit3": 3, "orbit4": 4}
# below this Hadamard block size the rotated coordinates are not gaussian enough for
# the shared N(0, 1/d) codebook to be valid, so conversion is skipped for the layer
MIN_HADAMARD_BLOCK = 32
_normal_codebook_cache: Dict[int, torch.Tensor] = {}
_rotation_cache: Dict[int, Tuple[torch.Tensor, torch.Tensor]] = {}
_skip_warned = set()
def gaussian_lloyd_max(bits: int, iters: int = 200) -> torch.Tensor:
"""MSE-optimal (Lloyd-Max) centroids for the standard normal, 2**bits levels,
returned in ascending order as float32. Cached per bit-width."""
if bits in _normal_codebook_cache:
return _normal_codebook_cache[bits]
levels = 2 ** bits
# init at the gaussian quantile midpoints
q = (torch.arange(levels, dtype=torch.float64) + 0.5) / levels
c = math.sqrt(2.0) * torch.erfinv(2.0 * q - 1.0)
inf = torch.tensor([math.inf], dtype=torch.float64)
for _ in range(iters):
edges = (c[:-1] + c[1:]) / 2.0
lo = torch.cat([-inf, edges])
hi = torch.cat([edges, inf])
# centroid update: E[X | lo < X < hi] = (phi(lo) - phi(hi)) / (Phi(hi) - Phi(lo))
phi_lo = torch.exp(-lo * lo / 2.0) / math.sqrt(2.0 * math.pi)
phi_hi = torch.exp(-hi * hi / 2.0) / math.sqrt(2.0 * math.pi)
cdf_lo = 0.5 * (1.0 + torch.erf(lo / math.sqrt(2.0)))
cdf_hi = 0.5 * (1.0 + torch.erf(hi / math.sqrt(2.0)))
c = (phi_lo - phi_hi) / (cdf_hi - cdf_lo)
c = c.to(torch.float32)
_normal_codebook_cache[bits] = c
return c
def hadamard_block_size(d: int) -> int:
# largest power of two dividing d
return d & (-d)
def rpbh_params(d: int) -> Tuple[torch.Tensor, torch.Tensor]:
"""Permutation (int64) and Rademacher signs (int8) of the RPBH rotation for input
dimension d, on cpu. Sampled once per dimension with a seed derived from d so the
rotation is identical across layers, runs, and resumes."""
if d in _rotation_cache:
return _rotation_cache[d]
g = torch.Generator().manual_seed(0x0EB17 + d)
perm = torch.randperm(d, generator=g)
signs = torch.randint(0, 2, (d,), generator=g, dtype=torch.int8) * 2 - 1
_rotation_cache[d] = (perm, signs)
return perm, signs
def _fwht(x: torch.Tensor, h: int) -> torch.Tensor:
"""Orthonormal fast Walsh-Hadamard transform applied to each contiguous block of h
coordinates along the last dimension (h must be a power of two dividing the dim)."""
shape = x.shape
x = x.reshape(-1, h)
m = x.shape[0]
step = 1
while step < h:
y = x.view(m, h // (2 * step), 2, step)
x = torch.stack((y[:, :, 0] + y[:, :, 1], y[:, :, 0] - y[:, :, 1]), dim=2).view(m, h)
step *= 2
return (x * h ** -0.5).view(shape)
def rpbh_forward(x: torch.Tensor, perm: torch.Tensor, signs: torch.Tensor, h: int) -> torch.Tensor:
"""y = blkdiag(H_h D) P x applied to the last dimension of x."""
y = torch.index_select(x, -1, perm) * signs.to(x.dtype)
return _fwht(y, h)
def rpbh_inverse(y: torch.Tensor, inv_perm: torch.Tensor, signs: torch.Tensor, h: int) -> torch.Tensor:
"""Inverse of rpbh_forward (the rotation is orthogonal, H is self-inverse)."""
z = _fwht(y, h) * signs.to(y.dtype)
return torch.index_select(z, -1, inv_perm)
def pack_codes(codes: torch.Tensor, bits: int) -> torch.Tensor:
"""Pack integer codes (values < 2**bits, any shape) into a flat uint8 bitstream."""
flat = codes.flatten().to(torch.uint8)
pad = (-flat.numel()) % 8
if pad:
flat = torch.cat([flat, flat.new_zeros(pad)])
shifts = torch.arange(bits - 1, -1, -1, device=flat.device, dtype=torch.uint8)
bit_mat = (flat.unsqueeze(-1) >> shifts) & 1 # (n, bits)
byte_mat = bit_mat.view(-1, 8)
weights = torch.tensor([1 << i for i in range(7, -1, -1)], device=flat.device, dtype=torch.uint8)
return (byte_mat * weights).sum(-1, dtype=torch.uint8)
def unpack_codes(packed: torch.Tensor, bits: int, numel: int) -> torch.Tensor:
"""Inverse of pack_codes. Returns a flat uint8 tensor of length numel."""
shifts = torch.arange(7, -1, -1, device=packed.device, dtype=torch.uint8)
bit_mat = ((packed.unsqueeze(-1) >> shifts) & 1).view(-1, bits)
weights = torch.tensor([1 << i for i in range(bits - 1, -1, -1)], device=packed.device, dtype=torch.uint8)
codes = (bit_mat * weights).sum(-1, dtype=torch.uint8)
return codes[:numel]
@torch.no_grad()
def _quantize_rows(
w_fp32: torch.Tensor,
perm: torch.Tensor,
signs: torch.Tensor,
h: int,
codebook: torch.Tensor,
bits: int,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Rotate weight rows into the RPBH basis and quantize their unit directions
against the codebook. Returns (packed codes, float32 row norms)."""
w_rot = rpbh_forward(w_fp32, perm, signs, h)
row_norms = w_rot.norm(dim=1)
unit = w_rot / (row_norms + 1e-10).unsqueeze(1)
edges = (codebook[:-1] + codebook[1:]) / 2
codes = torch.bucketize(unit, edges, out_int32=True).to(torch.uint8)
return pack_codes(codes, bits), row_norms
class OrbitQuantizer(OstrisQuantizer):
"""OrbitQuant backend. One instance per bit-width, shareable across modules."""
def __init__(self, bits: int):
self.bits = bits
def can_quantize(self, module: torch.nn.Linear) -> bool:
d = module.in_features
h = hadamard_block_size(d)
if h < MIN_HADAMARD_BLOCK:
if d not in _skip_warned:
_skip_warned.add(d)
print_acc(
f"OrbitQuant: skipping linears with in_features={d} "
f"(power-of-two block {h} is too small for the rotation)"
)
return False
return True
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
d = module.in_features
h = hadamard_block_size(d)
device = weight_fp32.device
perm_cpu, signs_cpu = rpbh_params(d)
perm = perm_cpu.to(device=device, dtype=torch.int32)
inv_perm = torch.argsort(perm_cpu).to(device=device, dtype=torch.int32)
signs = signs_cpu.to(device)
codebook = (gaussian_lloyd_max(self.bits) * d ** -0.5).to(device)
packed, row_norms = _quantize_rows(weight_fp32, perm, signs, h, codebook, self.bits)
module.register_buffer("orbit_packed", packed, persistent=False)
module.register_buffer("orbit_row_norms", row_norms.to(module.weight.dtype), persistent=False)
# stored as a uint8 byte view so module.to(dtype=...) can't cast the fp32
# codebook (nn.Module._apply dtype-casts every floating buffer)
module.register_buffer("orbit_codebook", codebook.view(torch.uint8), persistent=False)
module.register_buffer("orbit_perm", perm, persistent=False)
module.register_buffer("orbit_inv_perm", inv_perm, persistent=False)
module.register_buffer("orbit_signs", signs, persistent=False)
module.orbit_bits = self.bits
module.orbit_block = h
def _dequantize_rotated(self, module, dtype: torch.dtype) -> torch.Tensor:
"""Materialize the rotated-basis weight W' = W P^T in the given dtype."""
numel = module.out_features * module.in_features
codes = unpack_codes(module.orbit_packed, module.orbit_bits, numel)
codebook = module.orbit_codebook.view(torch.float32)
w = torch.index_select(codebook.to(dtype), 0, codes.to(torch.int32))
w = w.view(module.out_features, module.in_features)
return w * module.orbit_row_norms.to(dtype).unsqueeze(1)
def dequantize(self, module) -> torch.Tensor:
w = self._dequantize_rotated(module, torch.float32)
return rpbh_inverse(w, module.orbit_inv_perm, module.orbit_signs, module.orbit_block)
def requantize_(self, module, fp_weight: torch.Tensor) -> None:
w = fp_weight.to(device=module.orbit_packed.device, dtype=torch.float32)
packed, row_norms = _quantize_rows(
w, module.orbit_perm, module.orbit_signs, module.orbit_block,
module.orbit_codebook.view(torch.float32), module.orbit_bits,
)
module.orbit_packed = packed
module.orbit_row_norms = row_norms.to(module.ostris_orig_dtype)
def forward(self, module, x: torch.Tensor) -> torch.Tensor:
# rotate the activation instead of un-rotating the weight; the rotations
# cancel in the matmul. the weight is frozen, so build it outside autograd;
# gradients still flow to x through the rotation and the matmul
with torch.no_grad():
w = self._dequantize_rotated(module, x.dtype)
x_rot = rpbh_forward(x, module.orbit_perm, module.orbit_signs, module.orbit_block)
return F.linear(x_rot, w, module.bias)
|