Text Classification
Transformers
Safetensors
English
modernbert
decision-model
cross-encoder
classification
int4
text-embeddings-inference
Instructions to use grufr/WrenDecide-400M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use grufr/WrenDecide-400M with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="grufr/WrenDecide-400M")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("grufr/WrenDecide-400M") model = AutoModelForSequenceClassification.from_pretrained("grufr/WrenDecide-400M", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download decision_model/quantize.py from grufr/WrenDecide-400M: direct link, hf CLI and curl.
- Browser
- Download file 6.77 kB
-
https://huggingface.co/grufr/WrenDecide-400M/resolve/main/decision_model/quantize.py
- Command line
-
hf download hf://grufr/WrenDecide-400M/decision_model/quantize.py
-
curl -L -o quantize.py https://huggingface.co/grufr/WrenDecide-400M/resolve/main/decision_model/quantize.py
6.77 kB
| """Export and load ModernBERT encoder weights packed for 4-bit inference.""" | |
| import argparse | |
| import json | |
| import shutil | |
| from functools import cache | |
| from pathlib import Path | |
| from tempfile import TemporaryDirectory | |
| import torch | |
| from safetensors import safe_open | |
| from safetensors.torch import load_file, save_file | |
| from torch import nn | |
| from torch.nn import functional as F | |
| from transformers import AutoConfig, AutoModelForSequenceClassification | |
| from transformers.modeling_utils import no_init_weights | |
| from .io import read_json | |
| WEIGHT_FORMAT = "int4-group64" | |
| GROUP_SIZE = 64 | |
| def unpack_shader(): | |
| return torch.mps.compile_shader(""" | |
| #include <metal_stdlib> | |
| using namespace metal; | |
| kernel void unpack_int4(device float *out, device const uchar *packed, | |
| device const float2 *params, constant uint &inputs, | |
| constant uint &outputs, constant uint &group_size, | |
| uint i [[thread_position_in_grid]]) { | |
| uint row = i / (inputs / 2); | |
| uint column = (i % (inputs / 2)) * 2; | |
| uchar byte = packed[i]; | |
| float2 group = params[column / group_size * outputs + row]; | |
| out[i * 2] = (int(byte >> 4) - 8) * group.x + group.y; | |
| out[i * 2 + 1] = (int(byte & 15) - 8) * group.x + group.y; | |
| } | |
| """) | |
| class Int4Linear(nn.Module): | |
| def __init__(self, linear, unpack): | |
| super().__init__() | |
| self.unpack = unpack | |
| self.out_features = linear.out_features | |
| self.in_features = linear.in_features | |
| self.register_buffer("packed_weight", torch.empty( | |
| (self.out_features, self.in_features // 2), dtype=torch.uint8)) | |
| self.register_buffer("scales_and_zeros", torch.empty( | |
| (self.in_features // GROUP_SIZE, self.out_features, 2), dtype=torch.float32)) | |
| def forward(self, inputs): | |
| weight = self.unpack(self.packed_weight, self.scales_and_zeros, self.in_features, GROUP_SIZE) | |
| return F.linear(inputs, weight) | |
| def unpack_mps(packed, params, inputs, group_size): | |
| outputs = packed.shape[0] | |
| weight = params.new_empty((outputs, inputs)) | |
| unpack_shader().unpack_int4(weight, packed, params, inputs, outputs, group_size, threads=packed.numel()) | |
| return weight | |
| def unpack_cuda(packed, params, inputs, group_size): | |
| outputs = packed.shape[0] | |
| values = torch.stack((packed >> 4, packed & 15), dim=-1) | |
| weights = values.reshape(outputs, -1, group_size).float() | |
| scale, midpoint = params.transpose(0, 1).unsqueeze(-2).unbind(-1) | |
| return weights.sub_(8).mul_(scale).add_(midpoint).reshape(outputs, inputs) | |
| def pack_weight(weight): | |
| outputs, inputs = weight.shape | |
| if outputs % 8 or inputs % GROUP_SIZE: | |
| raise ValueError(f"Unsupported linear weight shape for 4-bit MPS: {weight.shape}") | |
| groups = weight.reshape(outputs, -1, GROUP_SIZE) | |
| minimum, maximum = groups.aminmax(dim=-1, keepdim=True) | |
| scale = ((maximum - minimum) / 15).clamp_min(1e-8) | |
| values = ((groups - minimum) / scale).round_().clamp_(0, 15).to(torch.uint8) | |
| values = values.reshape_as(weight) | |
| packed = (values[:, ::2] << 4) | values[:, 1::2] | |
| # The unpacker interprets zero as the real-valued midpoint at code 8. | |
| scales_and_zeros = torch.cat((scale, minimum + 8 * scale), dim=-1) | |
| return packed.contiguous(), scales_and_zeros.transpose(0, 1).contiguous() | |
| def export_int4(source, output): | |
| config = read_json(source / "config.json") | |
| if (config.get("model_type") != "modernbert" or config.get("decision_model_version") != 1 | |
| or config.get("decision_weight_format") or config.get("dtype") != "float32" | |
| or config.get("attention_bias") or config.get("mlp_bias")): | |
| raise ValueError("Expected an FP32 ModernBERT decision export with bias-free encoder projections.") | |
| if output.exists(): | |
| raise FileExistsError(output) | |
| print(f"Packing encoder weights from {source}", flush=True) | |
| tensors = {} | |
| with safe_open(source / "model.safetensors", framework="pt") as original: | |
| for name in original.keys(): | |
| weight = original.get_tensor(name) | |
| if name.startswith("model.layers.") and weight.ndim == 2: | |
| prefix = name.removesuffix(".weight") | |
| tensors[f"{prefix}.packed_weight"], tensors[f"{prefix}.scales_and_zeros"] = pack_weight(weight) | |
| else: | |
| tensors[name] = weight | |
| config["decision_weight_format"] = WEIGHT_FORMAT | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| with TemporaryDirectory(prefix=".quantize-", dir=output.parent) as temporary: | |
| stage = Path(temporary) / output.name | |
| stage.mkdir() | |
| save_file(tensors, stage / "model.safetensors", metadata={"format": "pt"}) | |
| (stage / "config.json").write_text(json.dumps(config, indent=2) + "\n") | |
| for name in ("tokenizer.json", "tokenizer_config.json", "special_tokens_map.json"): | |
| shutil.copyfile(source / name, stage / name) | |
| calibration = source / "calibration.json" | |
| if calibration.is_file(): | |
| shutil.copyfile(calibration, stage / calibration.name) | |
| stage.rename(output) | |
| print(f"Saved 4-bit export to {output}", flush=True) | |
| def load_int4(path, device): | |
| backend = torch.device(device).type | |
| if backend == "mps" and torch.backends.mps.is_available(): | |
| unpack_shader() | |
| unpack = unpack_mps | |
| elif backend == "cuda" and torch.cuda.is_available(): | |
| unpack = unpack_cuda | |
| else: | |
| raise ValueError("This 4-bit export requires an MPS or CUDA GPU.") | |
| config = AutoConfig.from_pretrained(path, local_files_only=True, trust_remote_code=False, | |
| reference_compile=False) | |
| if (config.model_type != "modernbert" or config.decision_model_version != 1 | |
| or config.decision_weight_format != WEIGHT_FORMAT): | |
| raise ValueError("Unsupported 4-bit decision checkpoint format.") | |
| with no_init_weights(): | |
| model = AutoModelForSequenceClassification.from_config(config, attn_implementation="sdpa") | |
| for name, layer in tuple(model.model.layers.named_modules()): | |
| if isinstance(layer, nn.Linear): | |
| model.model.layers.set_submodule(name, Int4Linear(layer, unpack)) | |
| model.load_state_dict(load_file(path / "model.safetensors"), strict=True, assign=True) | |
| return model.to(device).eval() | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("source", type=Path) | |
| parser.add_argument("output", type=Path) | |
| args = parser.parse_args() | |
| torch.set_num_threads(min(4, torch.get_num_threads())) | |
| export_int4(args.source, args.output) | |