sakasurya's picture
judicialmind/greenleaf-law-embed-tiny: initial release
bff06c9
Raw
History Blame Contribute Delete
5.53 kB
"""GreenLeaf Law Embed — quantization modules.
Provides native int8 and binary quantization for embedding vectors,
enabling efficient storage and fast similarity search without
requiring post-hoc compression.
Quantizers use straight-through estimation: the forward pass applies
hard quantization while gradients flow through the soft (continuous)
approximation during training.
"""
import torch
import numpy as np
from typing import Literal
from sentence_transformers.models import Module
class BaseQuantizer(torch.nn.Module):
"""Base quantizer with straight-through gradient estimation.
During inference (hard=True), applies discrete quantization.
During training, gradients bypass the discretization step so
the model can learn through the quantization bottleneck.
"""
def __init__(self, hard: bool = True):
"""
Args:
hard: If True, output is discretized. If False, output is
the soft continuous approximation.
"""
super().__init__()
self._hard = hard
def _hard_quantize(self, x, *args, **kwargs) -> torch.Tensor:
raise NotImplementedError
def _soft_quantize(self, x, *args, **kwargs) -> torch.Tensor:
raise NotImplementedError
def forward(self, x, *args, **kwargs) -> torch.Tensor:
soft = self._soft_quantize(x, *args, **kwargs)
if not self._hard:
return soft
# Straight-through estimator: forward uses hard quantization,
# backward passes gradients through the soft path unchanged.
return (
self._hard_quantize(x, *args, **kwargs).detach()
+ soft - soft.detach()
)
class Int8EmbeddingQuantizer(BaseQuantizer):
"""Quantizes embeddings to signed 8-bit integers via tanh scaling.
The soft path applies tanh to squash values into [-1, 1], then
the hard path scales to [-128, 127] and rounds to integers.
"""
def __init__(self, hard: bool = True):
super().__init__(hard=hard)
self.qmin = -128
self.qmax = 127
def _soft_quantize(self, x, *args, **kwargs):
return torch.tanh(x)
def _hard_quantize(self, x, *args, **kwargs):
soft = self._soft_quantize(x)
int_x = torch.round(soft * self.qmax)
return torch.clamp(int_x, self.qmin, self.qmax)
class BinaryEmbeddingQuantizer(BaseQuantizer):
"""Quantizes embeddings to {-1, +1} via sign function.
The soft path uses scaled tanh as a differentiable approximation
to the sign function. The hard path applies the actual sign.
"""
def __init__(self, hard: bool = True, scale: float = 1.0):
super().__init__(hard)
self._scale = scale
def _soft_quantize(self, x, *args, **kwargs):
return torch.tanh(self._scale * x)
def _hard_quantize(self, x, *args, **kwargs):
return torch.where(x >= 0, 1.0, -1.0)
class PackedBinaryEncoder:
"""Packs binary {-1, +1} embeddings into uint8 bit-arrays.
Each embedding dimension maps to 1 bit, reducing storage by 32x
compared to float32. Useful for large-scale retrieval with
Hamming distance.
"""
def __call__(self, x: torch.Tensor) -> torch.Tensor:
bits = np.where(x.cpu().numpy() >= 0, True, False)
packed = np.packbits(bits, axis=-1)
return torch.from_numpy(packed).to(x.device)
class FlexibleQuantizer(Module):
"""Sentence-transformers module that applies optional quantization.
Default behavior: pass through raw float embeddings (bf16/fp32).
Quantization is applied only when explicitly requested.
Supported modes:
- None: raw float embeddings (default, no quantization)
- "int8": signed 8-bit integer embeddings
- "binary": {-1, +1} binary embeddings
- "ubinary": packed binary as uint8 bit-arrays
"""
def __init__(self):
super().__init__()
self._int8_quantizer = Int8EmbeddingQuantizer()
self._binary_quantizer = BinaryEmbeddingQuantizer()
self._packed_binary_encoder = PackedBinaryEncoder()
def forward(
self,
features: dict[str, torch.Tensor],
quantization: Literal["int8", "binary", "ubinary"] | None = None,
**kwargs,
) -> dict[str, torch.Tensor]:
if quantization is None:
# Default: return raw float embeddings, no quantization
return features
elif quantization == "int8":
features["sentence_embedding"] = self._int8_quantizer(
features["sentence_embedding"]
)
elif quantization == "binary":
features["sentence_embedding"] = self._binary_quantizer(
features["sentence_embedding"]
)
elif quantization == "ubinary":
features["sentence_embedding"] = self._packed_binary_encoder(
features["sentence_embedding"]
)
else:
raise ValueError(
f"Unknown quantization mode: '{quantization}'. "
f"Supported modes: 'int8', 'binary', 'ubinary'."
)
return features
@classmethod
def load(
cls,
model_name_or_path: str,
subfolder: str = "",
token: bool | str | None = None,
cache_folder: str | None = None,
revision: str | None = None,
local_files_only: bool = False,
**kwargs,
):
return cls()
def save(self, output_path: str, *args, **kwargs) -> None:
return