Files
ai-toolkit/toolkit/util/convrot_quant.py

701 lines
26 KiB
Python

"""
ConvRot quantization backends (convrot4 / convrot8 qtypes).
convrot4 is the paper's W4A4 NVFP4 method described below. convrot8 pairs the same
rotation with per-token / per-output-channel symmetric int8 (W8A8) and
torch._int_mm: near-lossless (~1% weight error), and the fast path runs on any int8
tensor-core gpu (Ampere+), not just Blackwell. The rotation is what makes the coarse
per-row scales safe — it spreads outliers so a whole row shares one scale without
clipping damage (the classic SmoothQuant failure mode).
Implements "ConvRot: Rotation-Based Plug-and-Play 4-bit Quantization for Diffusion
Transformers" (arXiv:2512.03673) as an OstrisQuantizer backend, self-contained on
top of torch (no torchao version requirements).
Method: weights and activations are rotated with a block *regular* Hadamard
transform (R4 = [[1,1,1,-1],[1,1,-1,1],[1,-1,1,1],[-1,1,1,1]]/2 Kronecker-powered
to rot_size, a power of 4, default 256). Unlike the standard Hadamard whose all-ones
row concentrates the block mean into one coordinate, the regular Hadamard has
constant row sums, smoothing row-wise and column-wise outliers symmetrically. The
rotation is folded into the weight offline and applied to the activation at runtime,
so it cancels in the matmul. Both sides are then quantized to NVFP4 (fp4 e2m1 values,
fp8 e4m3 scale per 16 elements, plus one fp32 per-tensor scale) and multiplied with
the Blackwell fp4 tensor cores via torch._scaled_mm — a real ~5-6x gemm speedup, ~2x
at the layer level after rotation + activation-quant overhead.
Paths:
- inference (no grad): rotate -> fused triton nvfp4 activation quant ->
hardware fp4 gemm. Requires sm_100+ (Blackwell); otherwise falls back to the
dequantized matmul below.
- training (grad enabled): rotate -> straight-through fake-quant of the
activation (so adapters train against the same W4A4 numerics that deployment
uses) -> bf16 matmul with the dequantized rotated weight. Fully differentiable
w.r.t. the input.
Everything is deterministic: the rotation is a fixed matrix (no randomness at all)
and quantization is pure rounding.
Quantized state attached to each module:
cr_qdata packed e2m1 codes (uint8, out x in/2; low nibble = even element)
cr_scales e4m3 block scales (out x in/16)
cr_scales_blocked the same scales pre-swizzled for torch._scaled_mm
cr_pts fp32 per-tensor scale (scalar)
cr_rot / module.cr_rot_size rotation block size
"""
from typing import Optional
import torch
import torch.nn.functional as F
from toolkit.print import print_acc
from toolkit.util.ostris_quant import OstrisQuantizer
CONVROT_QTYPES = ("convrot4", "convrot8")
def get_convrot_quantizer(qtype: str):
if qtype == "convrot4":
return ConvRotQuantizer(rot_size=256)
if qtype == "convrot8":
return ConvRotInt8Quantizer(rot_size=256)
return None
F4_MAX = 6.0
F8_E4M3_MAX = 448.0
BLOCK = 16 # nvfp4 scale block
_E2M1_VALS = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0]
_E2M1_EDGES = [0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0]
_hadamard_cache = {}
_vals_cache = {}
_edges_cache = {}
_skip_warned = set()
def _cached(cache, key, build):
if key not in cache:
cache[key] = build()
return cache[key]
def regular_hadamard(rot_size: int, device, dtype=torch.bfloat16) -> torch.Tensor:
"""The ConvRot rotation: Kronecker powers of the 4x4 regular Hadamard matrix,
orthonormal. Symmetric and orthogonal, so it is its own inverse."""
key = (rot_size, str(device), dtype)
def build():
r4 = torch.tensor(
[[1.0, 1, 1, -1], [1, 1, -1, 1], [1, -1, 1, 1], [-1, 1, 1, 1]],
dtype=torch.float64,
)
h = r4.clone()
while h.shape[0] < rot_size:
h = torch.kron(h, r4)
if h.shape[0] != rot_size:
raise ValueError(f"rot_size {rot_size} is not a power of 4")
return (h / rot_size**0.5).to(device=device, dtype=dtype)
return _cached(_hadamard_cache, key, build)
def largest_pow4_divisor(d: int) -> int:
h = 1
while d % (h * 4) == 0:
h *= 4
return h
def rotate(x: torch.Tensor, rot_size: int) -> torch.Tensor:
"""Apply the block regular-Hadamard rotation along the last dim (self-inverse)."""
if rot_size == 1:
return x
h = regular_hadamard(rot_size, x.device, x.dtype)
shape = x.shape
xb = x.reshape(-1, shape[-1] // rot_size, rot_size)
return torch.matmul(xb, h).reshape(shape)
def to_blocked(m: torch.Tensor) -> torch.Tensor:
"""Rearrange an (R, C) scale matrix into the swizzled layout torch._scaled_mm
expects for block-scaled fp4 (cublas 128x4-tile layout)."""
rows, cols = m.shape
rb, cb = -(-rows // 128), -(-cols // 4)
if (rows, cols) != (rb * 128, cb * 4):
padded = torch.zeros(rb * 128, cb * 4, device=m.device, dtype=m.dtype)
padded[:rows, :cols] = m
m = padded
blocks = m.view(rb, 128, cb, 4).permute(0, 2, 1, 3)
return blocks.reshape(-1, 4, 32, 4).transpose(1, 2).reshape(-1, 32, 16).flatten()
def quantize_nvfp4(x: torch.Tensor, pts: Optional[torch.Tensor] = None):
"""Quantize (rows, K) to nvfp4. Returns (packed uint8 (rows, K/2),
e4m3 scales (rows, K/16), fp32 per-tensor scale)."""
rows, K = x.shape
xf = x.float()
if pts is None:
pts = xf.abs().amax() / (F4_MAX * F8_E4M3_MAX)
pts = torch.where(pts > 0, pts, torch.ones_like(pts))
xb = xf.view(rows, K // BLOCK, BLOCK)
scales = (xb.abs().amax(dim=-1) / (F4_MAX * pts)).to(torch.float8_e4m3fn)
denom = (scales.float() * pts).unsqueeze(-1)
z = (xb / torch.where(denom > 0, denom, torch.ones_like(denom))).clamp(
-F4_MAX, F4_MAX
)
edges = _cached(
_edges_cache, str(x.device), lambda: torch.tensor(_E2M1_EDGES, device=x.device)
)
mag = torch.bucketize(z.abs(), edges).to(torch.uint8)
codes = (mag | ((z < 0).to(torch.uint8) << 3)).view(rows, K)
packed = ((codes[:, 1::2] << 4) | codes[:, ::2]).contiguous()
return packed, scales, pts
def dequantize_nvfp4(
packed: torch.Tensor,
scales: torch.Tensor,
pts: torch.Tensor,
rows: int,
K: int,
dtype: torch.dtype,
) -> torch.Tensor:
codes = torch.stack([packed & 15, packed >> 4], dim=-1).view(rows, K)
vals = _cached(
_vals_cache,
str(packed.device),
lambda: torch.tensor(_E2M1_VALS, device=packed.device),
)
mag = torch.index_select(vals, 0, (codes & 7).flatten().to(torch.int32)).view(
rows, K
)
v = mag * torch.where((codes & 8) > 0, -1.0, 1.0)
v = v.view(rows, K // BLOCK, BLOCK) * (scales.float() * pts).unsqueeze(-1)
return v.view(rows, K).to(dtype)
# ---------------- fused triton activation quant ----------------
_triton_ok = None
def _triton_available() -> bool:
global _triton_ok
if _triton_ok is None:
try:
import triton # noqa: F401
import triton.language as tl # noqa: F401
_triton_ok = True
except Exception:
_triton_ok = False
print_acc(
"ConvRot: triton is not available. The fused activation-quant kernel is "
"disabled and activations will be quantized with plain torch ops instead "
"— inference gets slower (most of the fp4 speedup is lost), but quality "
"and training are unaffected."
)
return _triton_ok
_kernel = None
def _get_kernel():
global _kernel
if _kernel is not None:
return _kernel
import triton
import triton.language as tl
@triton.jit
def nvfp4_act_quant_kernel(
x_ptr,
out_ptr,
scale_ptr,
pts_ptr,
K,
n_col_tiles,
BLOCK_K: tl.constexpr,
BLOCKED_SCALES: tl.constexpr,
):
pid_m = tl.program_id(0)
pid_k = tl.program_id(1)
pts = tl.load(pts_ptr)
offs = pid_k * BLOCK_K + tl.arange(0, BLOCK_K)
mask = offs < K
x = tl.load(x_ptr + pid_m * K + offs, mask=mask, other=0.0).to(tl.float32)
xb = tl.reshape(x, (BLOCK_K // 16, 16))
amax = tl.max(tl.abs(xb), axis=1)
scale8 = (amax / (6.0 * pts)).to(tl.float8e4nv)
denom = scale8.to(tl.float32) * pts
denom = tl.where(denom > 0, denom, 1.0)
# note: triton fp32 division on this backend is ~1ulp off ieee (even with
# tl.fdiv ieee_rounding=True), so values landing exactly on a code boundary
# can round to the adjacent code vs the torch path. ties are equidistant, so
# this changes nothing quantitatively; activation codes are transient (never
# stored), and the kernel itself is deterministic.
z = xb / denom[:, None]
z = tl.minimum(tl.maximum(z, -6.0), 6.0)
az = tl.abs(z)
# strict > so exact midpoints go to the lower code, matching torch.bucketize
code = (
(az > 0.25).to(tl.uint8)
+ (az > 0.75).to(tl.uint8)
+ (az > 1.25).to(tl.uint8)
+ (az > 1.75).to(tl.uint8)
+ (az > 2.5).to(tl.uint8)
+ (az > 3.5).to(tl.uint8)
+ (az > 5.0).to(tl.uint8)
)
code = code | ((z < 0).to(tl.uint8) << 3)
lo, hi = tl.split(tl.reshape(code, (BLOCK_K // 2, 2)))
byte = lo | (hi << 4)
offs_b = pid_k * (BLOCK_K // 2) + tl.arange(0, BLOCK_K // 2)
tl.store(out_ptr + pid_m * (K // 2) + offs_b, byte, mask=offs_b < K // 2)
s_idx = pid_k * (BLOCK_K // 16) + tl.arange(0, BLOCK_K // 16)
if BLOCKED_SCALES:
# store straight into the cublas 128x4-tile swizzle (see to_blocked)
r_t = pid_m // 128
r_in = pid_m % 128
c_t = s_idx // 4
c = s_idx % 4
offs_s = (
((r_t * n_col_tiles + c_t) * 32 + (r_in % 32)) * 16
+ (r_in // 32) * 4
+ c
)
else:
offs_s = pid_m * (K // 16) + s_idx
tl.store(scale_ptr + offs_s, scale8, mask=s_idx < K // 16)
_kernel = nvfp4_act_quant_kernel
return _kernel
def quantize_nvfp4_fused(x: torch.Tensor, blocked_scales: bool = False):
"""Triton path of quantize_nvfp4 for the inference hot loop: one read of x,
writes packed codes + e4m3 scales (row-major, or directly in the swizzled
layout torch._scaled_mm wants when blocked_scales=True). Falls back to the
torch ops (row-major only)."""
rows, K = x.shape
if not (_triton_available() and x.is_cuda and K % 16 == 0):
packed, scales, pts = quantize_nvfp4(x)
return (
(packed, to_blocked(scales), pts)
if blocked_scales
else (packed, scales, pts)
)
pts = x.float().abs().amax() / (F4_MAX * F8_E4M3_MAX)
pts = torch.where(pts > 0, pts, torch.ones_like(pts))
x = x.contiguous()
packed = torch.empty(rows, K // 2, device=x.device, dtype=torch.uint8)
n_col_tiles = -(-(K // BLOCK) // 4)
if blocked_scales:
# zero-init: rows are padded to 128-tiles and the pad region must be zero
scales = torch.zeros(
(-(-rows // 128)) * 128 * n_col_tiles * 4,
device=x.device,
dtype=torch.float8_e4m3fn,
)
else:
scales = torch.empty(
rows, K // BLOCK, device=x.device, dtype=torch.float8_e4m3fn
)
BLOCK_K = 2048 if K >= 2048 else K
grid = (rows, -(-K // BLOCK_K))
_get_kernel()[grid](
x,
packed,
scales,
pts,
K,
n_col_tiles,
BLOCK_K=BLOCK_K,
BLOCKED_SCALES=blocked_scales,
num_warps=4,
)
return packed, scales, pts
# ---------------- backend ----------------
_warned_no_fp4 = False
def _fp4_gemm_supported(device) -> bool:
global _warned_no_fp4
device = torch.device(device)
supported = (
device.type == "cuda"
and torch.cuda.is_available()
and torch.cuda.get_device_capability(device)[0] >= 10 # Blackwell
)
if not supported and not _warned_no_fp4:
_warned_no_fp4 = True
print_acc(
f"ConvRot: no fp4 tensor-core support on this device ({device}; needs an "
"NVIDIA Blackwell GPU, sm_100+). Inference falls back to dequantized bf16 "
"matmuls: correct output but NO speedup, and inference activations stay "
"unquantized (W4A16 numerics instead of W4A4). The training path is "
"unaffected (it always simulates W4A4 via fake-quant)."
)
return supported
class ConvRotQuantizer(OstrisQuantizer):
"""ConvRot W4A4 backend. One instance per qtype, shareable across modules."""
def __init__(self, rot_size: int = 256):
self.rot_size = rot_size
def _rot_for(self, d: int) -> int:
return min(self.rot_size, largest_pow4_divisor(d))
def can_quantize(self, module: torch.nn.Linear) -> bool:
d = module.in_features
rot = self._rot_for(d)
if d % BLOCK != 0 or module.out_features % BLOCK != 0 or rot < 16:
if d not in _skip_warned:
_skip_warned.add(d)
print_acc(
f"ConvRot: skipping linears with in_features={d} "
f"(needs in/out divisible by 16 and a power-of-4 block >= 16)"
)
return False
return True
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
rot = self._rot_for(module.in_features)
w_rot = rotate(weight_fp32, rot)
packed, scales, pts = quantize_nvfp4(w_rot)
# scales/pts are stored as uint8 byte views: nn.Module._apply dtype-casts
# every floating buffer (module.to(dtype=...) would silently convert the
# e4m3 scales to bf16 and fp32 pts to bf16, corrupting them). integer
# buffers are only ever moved, never cast.
module.register_buffer("cr_qdata", packed, persistent=False)
module.register_buffer("cr_scales", scales.view(torch.uint8), persistent=False)
module.register_buffer(
"cr_scales_blocked", to_blocked(scales).view(torch.uint8), persistent=False
)
module.register_buffer(
"cr_pts",
pts.detach().clone().reshape(1).view(torch.uint8),
persistent=False,
)
module.cr_rot_size = rot
@staticmethod
def _pts(module) -> torch.Tensor:
return module.cr_pts.view(torch.float32).reshape(())
def _dequantize_rotated(self, module, dtype: torch.dtype) -> torch.Tensor:
return dequantize_nvfp4(
module.cr_qdata,
module.cr_scales.view(torch.float8_e4m3fn),
self._pts(module),
module.out_features,
module.in_features,
dtype,
)
def dequantize(self, module) -> torch.Tensor:
w = self._dequantize_rotated(module, torch.float32)
return rotate(w, module.cr_rot_size) # self-inverse
def requantize_(self, module, fp_weight: torch.Tensor) -> None:
w = fp_weight.to(device=module.cr_qdata.device, dtype=torch.float32)
w_rot = rotate(w, module.cr_rot_size)
packed, scales, pts = quantize_nvfp4(w_rot)
module.cr_qdata = packed
module.cr_scales = scales.view(torch.uint8)
module.cr_scales_blocked = to_blocked(scales).view(torch.uint8)
module.cr_pts = pts.detach().clone().reshape(1).view(torch.uint8)
def forward(self, module, x: torch.Tensor) -> torch.Tensor:
rot = module.cr_rot_size
in_f, out_f = module.in_features, module.out_features
x_rot = rotate(x, rot)
x2d = x_rot.reshape(-1, in_f)
m = x2d.shape[0]
if x.requires_grad:
# training path: straight-through fake-quant of the activation so
# adapters see the same W4A4 numerics as deployment, then a
# differentiable bf16 matmul against the dequantized rotated weight.
# gated on requires_grad alone (not is_grad_enabled) so both passes of
# gradient checkpointing take the same branch and recompute identically
with torch.no_grad():
aq, a_scales, a_pts = quantize_nvfp4(x2d.detach())
x_dq = dequantize_nvfp4(aq, a_scales, a_pts, m, in_f, x.dtype)
w = self._dequantize_rotated(module, x.dtype)
x_ste = x2d + (x_dq - x2d).detach()
out = F.linear(x_ste, w, module.bias)
return out.reshape(*x.shape[:-1], out_f)
if _fp4_gemm_supported(x.device):
pad = (-m) % BLOCK
if pad:
x2d = F.pad(x2d, (0, 0, 0, pad))
aq, a_scales_blocked, a_pts = quantize_nvfp4_fused(x2d, blocked_scales=True)
out = torch._scaled_mm(
aq.view(torch.float4_e2m1fn_x2),
module.cr_qdata.view(torch.float4_e2m1fn_x2).t(),
a_scales_blocked.view(torch.float8_e4m3fn),
module.cr_scales_blocked.view(torch.float8_e4m3fn),
out_dtype=x.dtype,
)
if pad:
out = out[:m]
s = (a_pts * self._pts(module)).to(x.dtype)
if module.bias is not None:
out = torch.addcmul(module.bias, out, s)
else:
out = out * s
return out.reshape(*x.shape[:-1], out_f)
# no fp4 hardware: dequantized matmul (correct, no speedup)
w = self._dequantize_rotated(module, x.dtype)
out = F.linear(x2d, w, module.bias)
return out.reshape(*x.shape[:-1], out_f)
# ---------------- convrot8: W8A8 int8 backend ----------------
def quantize_int8_rows(x: torch.Tensor):
"""Symmetric per-row int8 quantization. Returns (int8 (rows, K), fp32 scales (rows,))."""
xf = x.float()
scales = xf.abs().amax(dim=1) / 127.0
scales = torch.where(scales > 0, scales, torch.ones_like(scales))
q = torch.round(xf / scales.unsqueeze(1)).clamp_(-127, 127).to(torch.int8)
return q, scales
_int8_kernels = None
def _get_int8_kernels():
global _int8_kernels
if _int8_kernels is not None:
return _int8_kernels
import triton
import triton.language as tl
from triton.language.extra import libdevice
@triton.jit
def int8_act_quant_kernel(x_ptr, q_ptr, s_ptr, K, BLOCK_K: tl.constexpr):
row = tl.program_id(0)
base = row * K
acc = tl.zeros((BLOCK_K,), tl.float32)
for k0 in range(0, K, BLOCK_K):
offs = k0 + tl.arange(0, BLOCK_K)
v = tl.load(x_ptr + base + offs, mask=offs < K, other=0.0).to(tl.float32)
acc = tl.maximum(acc, tl.abs(v))
amax = tl.max(acc, axis=0)
scale = tl.where(amax > 0, amax / 127.0, 1.0)
for k0 in range(0, K, BLOCK_K):
offs = k0 + tl.arange(0, BLOCK_K)
mask = offs < K
v = tl.load(x_ptr + base + offs, mask=mask, other=0.0).to(tl.float32)
# rint = round-half-to-even, matching torch.round in the reference path
q = libdevice.rint(v / scale)
q = tl.minimum(tl.maximum(q, -127.0), 127.0)
tl.store(q_ptr + base + offs, q.to(tl.int8), mask=mask)
tl.store(s_ptr + row, scale)
@triton.jit
def int8_epilogue_kernel(
i_ptr,
as_ptr,
ws_ptr,
b_ptr,
o_ptr,
N,
HAS_BIAS: tl.constexpr,
BLOCK_N: tl.constexpr,
):
row = tl.program_id(0)
cb = tl.program_id(1)
offs = cb * BLOCK_N + tl.arange(0, BLOCK_N)
mask = offs < N
acc = tl.load(i_ptr + row * N + offs, mask=mask, other=0).to(tl.float32)
a_s = tl.load(as_ptr + row)
w_s = tl.load(ws_ptr + offs, mask=mask, other=0.0)
out = acc * (a_s * w_s)
if HAS_BIAS:
out += tl.load(b_ptr + offs, mask=mask, other=0.0).to(tl.float32)
tl.store(o_ptr + row * N + offs, out.to(o_ptr.dtype.element_ty), mask=mask)
_int8_kernels = (int8_act_quant_kernel, int8_epilogue_kernel)
return _int8_kernels
def quantize_int8_rows_fused(x: torch.Tensor):
"""Triton path of quantize_int8_rows: one extra read of x instead of the
multi-kernel torch chain. Falls back to the torch ops."""
rows, K = x.shape
if not (_triton_available() and x.is_cuda):
return quantize_int8_rows(x)
x = x.contiguous()
q = torch.empty(rows, K, device=x.device, dtype=torch.int8)
scales = torch.empty(rows, device=x.device, dtype=torch.float32)
kernel, _ = _get_int8_kernels()
kernel[(rows,)](x, q, scales, K, BLOCK_K=2048 if K >= 2048 else K, num_warps=8)
return q, scales
def _int8_epilogue(
i32: torch.Tensor,
a_scales: torch.Tensor,
w_scales: torch.Tensor,
bias,
out_dtype: torch.dtype,
) -> torch.Tensor:
"""out = i32 * a_scales[:, None] * w_scales[None, :] (+ bias), in out_dtype."""
m, n = i32.shape
if _triton_available() and i32.is_cuda:
out = torch.empty(m, n, device=i32.device, dtype=out_dtype)
_, kernel = _get_int8_kernels()
grid = (m, -(-n // 1024))
kernel[grid](
i32,
a_scales,
w_scales,
bias if bias is not None else i32,
out,
n,
HAS_BIAS=bias is not None,
BLOCK_N=1024,
num_warps=4,
)
return out
out = i32.float() * w_scales
out = out * a_scales.unsqueeze(1)
if bias is not None:
out = out + bias.float()
return out.to(out_dtype)
_int8_mm_ok = None
def _int8_gemm_supported(device) -> bool:
global _int8_mm_ok
device = torch.device(device)
if device.type != "cuda" or not torch.cuda.is_available():
supported = False
else:
if _int8_mm_ok is None:
try:
a = torch.zeros(32, 64, dtype=torch.int8, device=device)
b = torch.zeros(64, 32, dtype=torch.int8, device=device)
torch._int_mm(a, b)
_int8_mm_ok = True
except Exception:
_int8_mm_ok = False
supported = _int8_mm_ok
global _warned_no_int8
if not supported and not _warned_no_int8:
_warned_no_int8 = True
print_acc(
f"ConvRot: int8 matmul (torch._int_mm) is not usable on this device "
f"({device}). Inference falls back to dequantized bf16 matmuls: correct "
"output but NO speedup, and inference activations stay unquantized "
"(W8A16 numerics instead of W8A8). The training path is unaffected "
"(it always simulates W8A8 via fake-quant)."
)
return supported
_warned_no_int8 = False
class ConvRotInt8Quantizer(OstrisQuantizer):
"""ConvRot W8A8 backend: shared regular-Hadamard rotation + per-token /
per-output-channel symmetric int8 with torch._int_mm. One instance per qtype,
shareable across modules."""
def __init__(self, rot_size: int = 256):
self.rot_size = rot_size
def _rot_for(self, d: int) -> int:
return min(self.rot_size, largest_pow4_divisor(d))
def can_quantize(self, module: torch.nn.Linear) -> bool:
d = module.in_features
if d % BLOCK != 0 or module.out_features % 8 != 0 or self._rot_for(d) < 16:
if d not in _skip_warned:
_skip_warned.add(d)
print_acc(
f"ConvRot: skipping linears with in_features={d} "
f"(needs in divisible by 16, out by 8, and a power-of-4 block >= 16)"
)
return False
return True
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
rot = self._rot_for(module.in_features)
q, scales = quantize_int8_rows(rotate(weight_fp32, rot))
module.register_buffer("cr8_qdata", q, persistent=False)
# fp32 scales stored as a uint8 byte view (see convrot4: module.to(dtype=...)
# would otherwise cast them)
module.register_buffer("cr8_scales", scales.view(torch.uint8), persistent=False)
module.cr8_rot_size = rot
@staticmethod
def _scales(module) -> torch.Tensor:
return module.cr8_scales.view(torch.float32)
def _dequantize_rotated(self, module, dtype: torch.dtype) -> torch.Tensor:
w = module.cr8_qdata.float() * self._scales(module).unsqueeze(1)
return w.to(dtype)
def dequantize(self, module) -> torch.Tensor:
return rotate(
self._dequantize_rotated(module, torch.float32), module.cr8_rot_size
)
def requantize_(self, module, fp_weight: torch.Tensor) -> None:
w = fp_weight.to(device=module.cr8_qdata.device, dtype=torch.float32)
q, scales = quantize_int8_rows(rotate(w, module.cr8_rot_size))
module.cr8_qdata = q
module.cr8_scales = scales.view(torch.uint8)
def forward(self, module, x: torch.Tensor) -> torch.Tensor:
rot = module.cr8_rot_size
in_f, out_f = module.in_features, module.out_features
x_rot = rotate(x, rot)
x2d = x_rot.reshape(-1, in_f)
m = x2d.shape[0]
if x.requires_grad:
# training: straight-through fake-quant so adapters see deployment
# W8A8 numerics; differentiable bf16 matmul. gated on requires_grad
# alone so both gradient-checkpoint passes take the same branch
with torch.no_grad():
aq, a_s = quantize_int8_rows(x2d.detach())
x_dq = (aq.float() * a_s.unsqueeze(1)).to(x.dtype)
w = self._dequantize_rotated(module, x.dtype)
x_ste = x2d + (x_dq - x2d).detach()
out = F.linear(x_ste, w, module.bias)
return out.reshape(*x.shape[:-1], out_f)
if _int8_gemm_supported(x.device):
pad = (-m) % 32
if pad:
x2d = F.pad(x2d, (0, 0, 0, pad))
aq, a_s = quantize_int8_rows_fused(x2d)
i32 = torch._int_mm(aq, module.cr8_qdata.t())
out = _int8_epilogue(i32, a_s, self._scales(module), module.bias, x.dtype)
if pad:
out = out[:m]
return out.reshape(*x.shape[:-1], out_f)
w = self._dequantize_rotated(module, x.dtype)
out = F.linear(x2d, w, module.bias)
return out.reshape(*x.shape[:-1], out_f)