Added experimental orbit quant
This commit is contained in:
133
toolkit/util/ostris_quant.py
Normal file
133
toolkit/util/ostris_quant.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Quantization-agnostic custom quantized linear.
|
||||
|
||||
OstrisLinear is a drop-in nn.Linear replacement whose weight is held by a pluggable
|
||||
quantizer backend (OstrisQuantizer). Backends own the quantized representation
|
||||
(buffers + per-module attributes) and how the forward pass computes W x from it; the
|
||||
module and the rest of the toolkit stay backend agnostic. The first backend is
|
||||
OrbitQuant (toolkit/util/orbit_quant.py) via the orbit2/orbit3/orbit4 qtypes; add new
|
||||
backends by implementing OstrisQuantizer and resolving them in get_ostris_quantizer.
|
||||
|
||||
Modules are converted in place by convert_linear_to_ostris via class swap, so the
|
||||
original module object (and any references to it, e.g. LoRA org_module or parent
|
||||
containers) stays valid.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class OstrisQuantizer:
|
||||
"""Base class for weight quantization backends used by OstrisLinear.
|
||||
|
||||
Backends are stateless with respect to tensors: everything tensor-shaped must be
|
||||
registered as a buffer on the module inside quantize_ (so device moves and dtype
|
||||
casts through nn.Module._apply keep working), and read back off the module in the
|
||||
other methods. One backend instance may be shared by many modules.
|
||||
"""
|
||||
|
||||
def can_quantize(self, module: torch.nn.Linear) -> bool:
|
||||
"""Whether this backend can quantize the given linear (e.g. shape constraints)."""
|
||||
return True
|
||||
|
||||
def quantize_(self, module: torch.nn.Linear, weight_fp32: torch.Tensor) -> None:
|
||||
"""Build the quantized representation of weight_fp32 and attach it to the
|
||||
module (register_buffer for tensors, plain attributes for scalars). Called
|
||||
while the module is still an nn.Linear, before the weight param is removed."""
|
||||
raise NotImplementedError
|
||||
|
||||
def dequantize(self, module: "OstrisLinear") -> torch.Tensor:
|
||||
"""Reconstruct the full weight in the original basis, in float32."""
|
||||
raise NotImplementedError
|
||||
|
||||
def requantize_(self, module: "OstrisLinear", fp_weight: torch.Tensor) -> None:
|
||||
"""Re-quantize in place from a full precision weight in the original basis
|
||||
(used by the continuous merge/reset method)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def forward(self, module: "OstrisLinear", x: torch.Tensor) -> torch.Tensor:
|
||||
# default: dequantize per forward and run a plain linear. backends can
|
||||
# override with a cheaper formulation. the weight is frozen, so build it
|
||||
# outside autograd; gradients still flow to x through the matmul
|
||||
with torch.no_grad():
|
||||
w = self.dequantize(module).to(x.dtype)
|
||||
return F.linear(x, w, module.bias)
|
||||
|
||||
|
||||
class OstrisLinear(torch.nn.Linear):
|
||||
"""A linear layer whose weight is quantized by an OstrisQuantizer backend.
|
||||
|
||||
Never instantiate directly: created in place by convert_linear_to_ostris. The
|
||||
weight parameter is removed; the quantized representation lives in backend-owned
|
||||
buffers, plus:
|
||||
ostris_quantizer the backend instance
|
||||
ostris_orig_dtype dtype of the original weight (used for dequantized views)
|
||||
"""
|
||||
|
||||
is_ostris_quantized = True
|
||||
|
||||
@torch.no_grad()
|
||||
def dequantize_weight(self) -> torch.Tensor:
|
||||
"""Reconstruct the weight in the original basis and dtype."""
|
||||
return self.ostris_quantizer.dequantize(self).to(self.ostris_orig_dtype)
|
||||
|
||||
@property
|
||||
def weight(self):
|
||||
# materializes the full dequantized weight. kept for code that inspects the
|
||||
# weight (shape/dtype/device) and for the network merge paths, which detect
|
||||
# the marker via toolkit.util.quantize.is_quantized_tensor
|
||||
w = self.dequantize_weight()
|
||||
w._is_ostris_weight = True
|
||||
return w
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.ostris_quantizer.forward(self, x)
|
||||
|
||||
@torch.no_grad()
|
||||
def requantize_(self, fp_weight: torch.Tensor) -> None:
|
||||
self.ostris_quantizer.requantize_(self, fp_weight)
|
||||
|
||||
def _save_to_state_dict(self, destination, prefix, keep_vars):
|
||||
# emit a plain full precision weight so full-model saves need no special casing
|
||||
destination[prefix + "weight"] = self.dequantize_weight()
|
||||
if self.bias is not None:
|
||||
destination[prefix + "bias"] = self.bias if keep_vars else self.bias.detach()
|
||||
|
||||
|
||||
def get_ostris_quantizer(qtype: str) -> Optional[OstrisQuantizer]:
|
||||
"""Resolve a qtype string to a quantizer backend instance, or None if the qtype
|
||||
does not belong to a custom backend. Add new backends here."""
|
||||
from toolkit.util.orbit_quant import ORBIT_QTYPES, OrbitQuantizer
|
||||
from toolkit.util.orbit_vq_quant import ORBIT_VQ_QTYPES, OrbitVQQuantizer
|
||||
|
||||
if qtype in ORBIT_QTYPES:
|
||||
return OrbitQuantizer(ORBIT_QTYPES[qtype])
|
||||
if qtype in ORBIT_VQ_QTYPES:
|
||||
return OrbitVQQuantizer(**ORBIT_VQ_QTYPES[qtype])
|
||||
return None
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def convert_linear_to_ostris(module: torch.nn.Linear, quantizer: OstrisQuantizer) -> bool:
|
||||
"""Quantize an nn.Linear in place (class swap). Returns True if the module was
|
||||
converted (or already was), False if it is not a candidate."""
|
||||
if isinstance(module, OstrisLinear):
|
||||
return True
|
||||
weight = getattr(module, "weight", None)
|
||||
if not isinstance(weight, torch.nn.Parameter) or not weight.dtype.is_floating_point:
|
||||
return False
|
||||
if type(weight.data) is not torch.Tensor:
|
||||
# already holds a quantized tensor subclass (e.g. torchao)
|
||||
return False
|
||||
if not quantizer.can_quantize(module):
|
||||
return False
|
||||
quantizer.quantize_(module, weight.data.to(torch.float32))
|
||||
module.ostris_quantizer = quantizer
|
||||
module.ostris_orig_dtype = weight.dtype
|
||||
del module._parameters["weight"]
|
||||
if module.bias is not None:
|
||||
module.bias.requires_grad_(False)
|
||||
module.__class__ = OstrisLinear
|
||||
return True
|
||||
Reference in New Issue
Block a user