Hritik045678's picture
Upload folder using huggingface_hub
296a506 verified
Raw
History Blame Contribute Delete
6.68 kB
"""
Frox AI Morph 1.1 β€” Quantization
Morph 1.0's `_load_4bit()` / `_load_8bit()` in the inference engine were
stubs: they printed a message and then returned a full-precision model
regardless. This module makes quantized loading actually work.
Two paths:
- bitsandbytes NF4 (QLoRA-style) β€” best for training/fine-tuning
- torchao / weight-only int8 β€” best for pure inference, no bnb dependency
Both operate on an already-constructed MorphForCausalLM by replacing
nn.Linear layers in-place, since Morph is a custom architecture (not a
HuggingFace AutoModel) and can't go through `from_pretrained(...,
quantization_config=...)` directly.
"""
from __future__ import annotations
from typing import List, Optional
import torch
import torch.nn as nn
# ── bitsandbytes NF4 (4-bit) ──────────────────────────────────────
def quantize_4bit(
model: nn.Module,
compute_dtype: torch.dtype = torch.float16,
skip_modules: Optional[List[str]] = None,
) -> nn.Module:
"""
Replace nn.Linear layers with bitsandbytes Linear4bit (NF4).
Embedding and lm_head are skipped by default (quantizing the
vocab projection tanks quality for a tiny VRAM saving).
"""
try:
import bitsandbytes as bnb
except ImportError:
raise ImportError(
"bitsandbytes is required for 4-bit quantization. "
"Install with: pip install bitsandbytes"
)
skip = set(skip_modules or ["lm_head", "embed_tokens"])
replaced = 0
def _replace(module: nn.Module, prefix: str = ""):
nonlocal replaced
for name, child in module.named_children():
full_name = f"{prefix}.{name}" if prefix else name
if any(s in full_name for s in skip):
continue
if isinstance(child, nn.Linear):
new_layer = bnb.nn.Linear4bit(
child.in_features,
child.out_features,
bias=child.bias is not None,
compute_dtype=compute_dtype,
quant_type="nf4",
)
new_layer.weight = bnb.nn.Params4bit(
child.weight.data.clone(),
requires_grad=False,
quant_type="nf4",
)
if child.bias is not None:
new_layer.bias = nn.Parameter(child.bias.data.clone())
setattr(module, name, new_layer)
replaced += 1
else:
_replace(child, full_name)
_replace(model)
print(f"βœ“ 4-bit NF4 quantization applied to {replaced} linear layers")
return model
# ── Weight-only int8 (inference-only, no bnb dependency) ─────────
class Int8Linear(nn.Module):
"""
Weight-only int8 linear layer. Weights stored as int8 + per-channel
scale; activations stay in the compute dtype. ~4x smaller weights
than fp16 with a small quality cost β€” good for inference on cards
without bitsandbytes support (e.g. some ARM / edge deployments).
"""
def __init__(self, in_features: int, out_features: int, bias: bool = False):
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.register_buffer("weight_int8", torch.zeros(out_features, in_features, dtype=torch.int8))
self.register_buffer("scale", torch.ones(out_features, dtype=torch.float32))
self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None
@classmethod
def from_linear(cls, linear: nn.Linear) -> "Int8Linear":
layer = cls(linear.in_features, linear.out_features, bias=linear.bias is not None)
w = linear.weight.data.float()
scale = w.abs().max(dim=1).values / 127.0
scale = scale.clamp(min=1e-8)
w_int8 = (w / scale.unsqueeze(1)).round().clamp(-127, 127).to(torch.int8)
layer.weight_int8.copy_(w_int8)
layer.scale.copy_(scale)
if linear.bias is not None:
layer.bias.data.copy_(linear.bias.data)
return layer
def forward(self, x: torch.Tensor) -> torch.Tensor:
w = self.weight_int8.to(x.dtype) * self.scale.unsqueeze(1).to(x.dtype)
out = torch.nn.functional.linear(x, w, self.bias)
return out
def quantize_8bit(
model: nn.Module,
skip_modules: Optional[List[str]] = None,
) -> nn.Module:
"""Replace nn.Linear layers with weight-only Int8Linear (no bnb needed)."""
skip = set(skip_modules or ["lm_head", "embed_tokens"])
replaced = 0
def _replace(module: nn.Module, prefix: str = ""):
nonlocal replaced
for name, child in module.named_children():
full_name = f"{prefix}.{name}" if prefix else name
if any(s in full_name for s in skip):
continue
if isinstance(child, nn.Linear):
setattr(module, name, Int8Linear.from_linear(child))
replaced += 1
else:
_replace(child, full_name)
_replace(model)
print(f"βœ“ Weight-only int8 quantization applied to {replaced} linear layers")
return model
# ── Size estimation ────────────────────────────────────────────────
def estimate_memory_footprint(
num_params: int,
dtype: str = "float16",
) -> dict:
"""Estimate model weight memory footprint at various precisions."""
bytes_per_param = {
"float32": 4, "float16": 2, "bfloat16": 2,
"int8": 1, "nf4": 0.5, "int4": 0.5,
}
b = bytes_per_param.get(dtype, 2)
total_bytes = num_params * b
return {
"dtype": dtype,
"params_billions": round(num_params / 1e9, 3),
"weights_gb": round(total_bytes / (1024 ** 3), 2),
# Rule of thumb: inference needs weights + ~20% for activations/KV cache
"estimated_inference_vram_gb": round(total_bytes * 1.2 / (1024 ** 3), 2),
}
def print_quantization_report(model: nn.Module, dtype_label: str = "float16"):
total_params = sum(p.numel() for p in model.parameters())
footprint = estimate_memory_footprint(total_params, dtype_label)
print("\nπŸ“¦ Model Memory Footprint")
print(f" Parameters: {footprint['params_billions']}B")
print(f" Precision: {footprint['dtype']}")
print(f" Weight size: {footprint['weights_gb']} GB")
print(f" Est. inference: {footprint['estimated_inference_vram_gb']} GB "
f"(weights + activations/KV headroom)\n")