Download auto_quant.py from ProCreations/auto-200m-2-int4: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/ProCreations/auto-200m-2-int4/resolve/main/auto_quant.py
- Command line
-
hf download hf://ProCreations/auto-200m-2-int4/auto_quant.py
-
curl -L -o auto_quant.py https://huggingface.co/ProCreations/auto-200m-2-int4/resolve/main/auto_quant.py
14.3 kB
| """Weight-only int8 / int4 checkpoints for Auto's ModernBERT classifiers, in plain PyTorch. | |
| Runs wherever PyTorch runs (CPU, Apple MPS, NVIDIA CUDA, AMD ROCm): weights stay quantized in memory and each layer | |
| dequantizes its own weight just before the matrix multiply. Activations keep the device's float dtype, so no | |
| quantized kernels are needed. Requires torch and transformers (5.x); nothing is compiled. | |
| from auto_quant import load, score | |
| model, tokenizer = load("ProCreations/auto-200m-2-int4") # device and dtype chosen automatically | |
| p_deny = score(model, tokenizer, text) # one request, any length up to 65,536 tokens | |
| By default attention is Auto's exact memory-linear attention (the same as the Auto runtime): no N x N score matrix or | |
| mask is ever built, so a 65,536-token request fits in a few GB on CPU or Apple MPS. It scores one unpadded request | |
| at a time through score(). Pass attention="sdpa" (or any Transformers attention implementation) to call | |
| model(**inputs) on padded batches instead. | |
| Format (quant_config.json, "format": "auto-quant-v1"): symmetric integers, one float16 scale per output channel | |
| (int8) or per group of `group_size` consecutive input weights (int4). Dequantized weight = float32(q) * | |
| float32(scale), then cast to the compute dtype. int4 values are stored as q + 8 in nibbles, two per byte, low first. | |
| """ | |
| import json | |
| import os | |
| import types | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| FORMAT = "auto-quant-v1" | |
| RANGES = {8: (-127, 127), 4: (-8, 7)} | |
| # ------------------------------------------------------------------------------------------------ quantized weights | |
| def pack_int4(q): | |
| """int8 tensor in [-8, 7] with an even last dimension -> uint8 with two values per byte (low nibble first).""" | |
| u = (q.to(torch.int16) + 8).to(torch.uint8) | |
| return u[..., 0::2] | (u[..., 1::2] << 4) | |
| def unpack_int4(packed): | |
| low = (packed & 15).to(torch.int8) - 8 | |
| high = (packed >> 4).to(torch.int8) - 8 | |
| return torch.stack([low, high], dim=-1).flatten(-2) | |
| def dequantize(qweight, scales, bits, group_size, dtype): | |
| """Float weight from stored integers and scales (rows = output channels / embedding rows).""" | |
| q = unpack_int4(qweight) if bits == 4 else qweight | |
| if bits == 8: | |
| w = q.float() * scales.float()[..., None] | |
| else: | |
| cols = q.shape[-1] | |
| w = (q.float().reshape(*q.shape[:-1], cols // group_size, group_size) * scales.float()[..., None]).reshape(q.shape) | |
| return w.to(dtype) | |
| def quantize(weight, bits, group_size, scales): | |
| """Integers for `weight` given its scales (rounded to float16): per row for int8, per row and group for int4.""" | |
| lo, hi = RANGES[bits] | |
| w = weight.detach().float() | |
| s = scales.to(torch.float16).float() | |
| if bits == 8: | |
| q = torch.clamp(torch.round(w / s[:, None]), lo, hi) | |
| else: | |
| rows, cols = w.shape | |
| q = torch.clamp(torch.round(w.reshape(rows, cols // group_size, group_size) / s[..., None]), lo, hi).reshape(rows, cols) | |
| q = q.to(torch.int8) | |
| return (pack_int4(q) if bits == 4 else q), scales.to(torch.float16) | |
| class QuantLinear(nn.Module): | |
| def __init__(self, in_features, out_features, bits, group_size, bias=False, device=None): | |
| super().__init__() | |
| self.in_features, self.out_features, self.bits, self.group_size = in_features, out_features, bits, group_size | |
| cols = in_features // 2 if bits == 4 else in_features | |
| groups = in_features // group_size if bits == 4 else None | |
| self.register_buffer("qweight", torch.empty(out_features, cols, dtype=torch.uint8 if bits == 4 else torch.int8, device=device)) | |
| self.register_buffer("scales", torch.empty((out_features, groups) if groups else (out_features,), dtype=torch.float16, device=device)) | |
| self.register_buffer("bias", torch.empty(out_features, dtype=torch.float16, device=device) if bias else None) | |
| def forward(self, x): | |
| w = dequantize(self.qweight, self.scales, self.bits, self.group_size, x.dtype) | |
| return F.linear(x, w, None if self.bias is None else self.bias.to(x.dtype)) | |
| class QuantEmbedding(nn.Module): | |
| def __init__(self, num_embeddings, embedding_dim, bits, group_size, dtype=torch.float32, device=None): | |
| super().__init__() | |
| self.num_embeddings, self.embedding_dim, self.bits, self.group_size = num_embeddings, embedding_dim, bits, group_size | |
| self.dtype = dtype | |
| cols = embedding_dim // 2 if bits == 4 else embedding_dim | |
| groups = embedding_dim // group_size if bits == 4 else None | |
| self.register_buffer("qweight", torch.empty(num_embeddings, cols, dtype=torch.uint8 if bits == 4 else torch.int8, device=device)) | |
| self.register_buffer("scales", torch.empty((num_embeddings, groups) if groups else (num_embeddings,), dtype=torch.float16, device=device)) | |
| def forward(self, input_ids): | |
| # Only the looked-up rows are dequantized. | |
| return dequantize(self.qweight[input_ids], self.scales[input_ids], self.bits, self.group_size, self.dtype) | |
| def targets(model): | |
| """Quantized modules: token embeddings, every encoder Linear and the head's dense layer. LayerNorm weights and | |
| the final 2-way classifier stay in float16.""" | |
| names = ["model.embeddings.tok_embeddings"] | |
| for name, module in model.named_modules(): | |
| if isinstance(module, nn.Linear) and (name.startswith("model.layers.") or name == "head.dense"): | |
| names.append(name) | |
| return names | |
| def swap(model, config, dtype=None, device=None): | |
| """Replace the target modules with (empty) quantized modules; then load with load_state_dict(strict=True).""" | |
| bits, group = config["bits"], config.get("group_size") | |
| dtype = dtype or next(model.parameters()).dtype | |
| for name in config["modules"]: | |
| parent_name, _, child = name.rpartition(".") | |
| parent = model.get_submodule(parent_name) | |
| old = getattr(parent, child) | |
| dev = device if device is not None else old.weight.device | |
| if isinstance(old, nn.Embedding): | |
| new = QuantEmbedding(old.num_embeddings, old.embedding_dim, bits, group, dtype=dtype, device=dev) | |
| else: | |
| new = QuantLinear(old.in_features, old.out_features, bits, group, bias=old.bias is not None, device=dev) | |
| setattr(parent, child, new) | |
| return model | |
| # -------------------------------------------------------------- memory-linear exact attention (from the Auto runtime) | |
| LOCAL_BLOCK, MLP_CHUNK = 1024, 8192 | |
| SCORE_BUDGET = {"cpu": 256 * 2**20} # bytes of temporary scores per call if only the plain math kernel applies | |
| GPU_SCORE_BUDGET = 1024 * 2**20 | |
| try: | |
| from torch.nn.attention import SDPBackend, sdpa_kernel | |
| _FUSED = {"cuda": [b for b in ("FLASH_ATTENTION", "EFFICIENT_ATTENTION", "CUDNN_ATTENTION") if hasattr(SDPBackend, b)], | |
| "cpu": ["FLASH_ATTENTION"]} | |
| except ImportError: # torch < 2.3 | |
| sdpa_kernel, _FUSED = None, {} | |
| _UNAVAILABLE = set() | |
| def _sdpa(query, key, value, mask, scaling): | |
| return F.scaled_dot_product_attention(query, key, value, attn_mask=mask, dropout_p=0.0, is_causal=False, scale=scaling) | |
| def _rows(device, heads, keys, element_size): | |
| return max(1, SCORE_BUDGET.get(device.type, GPU_SCORE_BUDGET) // max(1, heads * keys * max(4, element_size))) | |
| def _global_attention(query, key, value, scaling): | |
| kind = query.device.type | |
| if sdpa_kernel is not None and kind in _FUSED and (kind, query.dtype) not in _UNAVAILABLE: | |
| try: # one unmasked call restricted to memory-efficient fused kernels | |
| with sdpa_kernel([getattr(SDPBackend, b) for b in _FUSED[kind]]): | |
| return _sdpa(query, key, value, None, scaling) | |
| except RuntimeError: | |
| _UNAVAILABLE.add((kind, query.dtype)) | |
| n = query.shape[-2] | |
| block = _rows(query.device, query.shape[1], n, query.element_size()) | |
| if block >= n: | |
| return _sdpa(query, key, value, None, scaling) | |
| out = torch.empty_like(query) | |
| for s in range(0, n, block): | |
| out[:, :, s : s + block] = _sdpa(query[:, :, s : s + block], key, value, None, scaling) | |
| return out | |
| def _local_attention(query, key, value, radius, scaling): | |
| """Each query i attends to keys j with |i - j| <= radius, one block of queries at a time.""" | |
| n = query.shape[-2] | |
| block = max(1, min(LOCAL_BLOCK, _rows(query.device, query.shape[1], LOCAL_BLOCK + 2 * radius, query.element_size()))) | |
| out, cached = torch.empty_like(query), None | |
| for start in range(0, n, block): | |
| end = min(n, start + block) | |
| low, high = max(0, start - radius), min(n, end + radius) | |
| interior = low == start - radius and high == end + radius and end - start == block | |
| if interior and cached is not None: | |
| mask = cached | |
| else: | |
| mask = (torch.arange(start, end, device=query.device)[:, None] - torch.arange(low, high, device=query.device)[None, :]).abs() <= radius | |
| if interior: | |
| cached = mask | |
| out[:, :, start:end] = _sdpa(query[:, :, start:end], key[:, :, low:high], value[:, :, low:high], mask, scaling) | |
| return out | |
| def linear_attention(module, query, key, value, attention_mask=None, scaling=None, dropout=0.0, sliding_window=None, **kwargs): | |
| """Transformers attention interface (registered as "auto_linear"); one unpadded example, no mask.""" | |
| if query.shape[0] != 1 or attention_mask is not None: | |
| raise ValueError('attention="linear" scores one unpadded request at a time: use score(), or load with attention="sdpa"') | |
| out = _global_attention(query, key, value, scaling) if sliding_window is None else _local_attention(query, key, value, sliding_window - 1, scaling) | |
| return out.transpose(1, 2).contiguous(), None | |
| def _chunked_mlp(self, hidden): | |
| if hidden.shape[-2] <= MLP_CHUNK or torch.is_grad_enabled(): | |
| return self._full_forward(hidden) | |
| # The GLU acts on each token independently, so chunking along the sequence is exact. | |
| return torch.cat([self._full_forward(hidden[..., s : s + MLP_CHUNK, :]) for s in range(0, hidden.shape[-2], MLP_CHUNK)], dim=-2) | |
| # ------------------------------------------------------------------------------------------------------ public API | |
| def _resolve(path_or_repo, revision): | |
| if os.path.isdir(path_or_repo): | |
| return path_or_repo | |
| from huggingface_hub import snapshot_download | |
| return snapshot_download(path_or_repo, revision=revision) | |
| def load(path_or_repo, device="auto", dtype=None, revision=None, attention="linear", **model_kwargs): | |
| """Load a quantized Auto checkpoint -> (model, tokenizer), in eval mode on `device` ("auto": CUDA/ROCm, then | |
| Apple MPS, then CPU). dtype defaults to bfloat16 on GPUs that support it and float32 elsewhere. No full-precision | |
| copy of the weights is ever materialized.""" | |
| from safetensors.torch import load_file | |
| from transformers import AutoConfig, AutoModelForSequenceClassification, AutoTokenizer | |
| from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS | |
| from transformers.models.modernbert.modeling_modernbert import ModernBertRotaryEmbedding | |
| path = _resolve(path_or_repo, revision) | |
| with open(os.path.join(path, "quant_config.json"), encoding="utf-8") as f: | |
| qconfig = json.load(f) | |
| if qconfig.get("format") != FORMAT: | |
| raise ValueError(f"unsupported quantization format {qconfig.get('format')!r}") | |
| if device == "auto": | |
| device = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu") | |
| device = torch.device(device) | |
| if dtype is None: | |
| dtype = torch.bfloat16 if device.type == "cuda" and torch.cuda.is_bf16_supported() else torch.float32 | |
| implementation = attention | |
| if attention == "linear": | |
| ALL_ATTENTION_FUNCTIONS.register("auto_linear", linear_attention) | |
| implementation = "auto_linear" | |
| config = AutoConfig.from_pretrained(path) | |
| with torch.device("meta"): | |
| model = AutoModelForSequenceClassification.from_config(config, attn_implementation=implementation, dtype=dtype, **model_kwargs) | |
| swap(model, qconfig, dtype=dtype) | |
| model.to_empty(device=device) | |
| model.load_state_dict(load_file(os.path.join(path, qconfig["weights"]), device=str(device)), strict=True) | |
| with torch.device(device): # rotary tables are computed, not stored | |
| model.model.rotary_emb = ModernBertRotaryEmbedding(config=config) | |
| if attention == "linear": | |
| for layer in model.model.layers: | |
| layer.mlp._full_forward = layer.mlp.forward | |
| layer.mlp.forward = types.MethodType(_chunked_mlp, layer.mlp) | |
| model.auto_attention = attention | |
| return model.eval(), AutoTokenizer.from_pretrained(path) | |
| def logits(model, input_ids): | |
| """Classifier logits for one unpadded request (input_ids of shape [1, n]) on the model's device.""" | |
| input_ids = input_ids.to(model.device) | |
| if getattr(model, "auto_attention", None) != "linear": | |
| return model(input_ids=input_ids).logits | |
| hidden = model.model(input_ids=input_ids, attention_mask={"full_attention": None, "sliding_attention": None})[0] | |
| pooled = hidden[:, 0] if model.config.classifier_pooling == "cls" else hidden.mean(dim=1) | |
| return model.classifier(model.drop(model.head(pooled))) | |
| def score(model, tokenizer, text): | |
| """P(deny) for one serialized request (see build_input); deny if >= 0.5.""" | |
| ids = tokenizer(text, return_tensors="pt", truncation=False)["input_ids"] | |
| if ids.shape[-1] > model.config.max_position_embeddings: | |
| raise ValueError(f"{ids.shape[-1]} tokens exceed the {model.config.max_position_embeddings}-token context") | |
| return logits(model, ids).float().softmax(-1)[0, 1].item() | |
| def build_input(user_request, history, call): | |
| """The serialization every Auto model was trained on: proposed call, user request, agent history.""" | |
| parts = ["### PROPOSED TOOL CALL", f"tool: {call['tool']}", f"args: {call['args']}", "", "### USER REQUEST", user_request, "", "### AGENT HISTORY"] | |
| if not history: | |
| parts.append("(no prior actions)") | |
| else: | |
| for i, h in enumerate(history): | |
| parts.append(f"[{i + 1}] {h['tool']}({h['args']})\n-> {h.get('result', '')}") | |
| return "\n".join(parts) | |