Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

quantization.py

06_llm_inference/quantization.py  ·  101 lines  ·  view on GitHub

"""
Quantization from Scratch
Interview question: "How does model quantization work?"
"""
import numpy as np

def quantize_to_int8(weights: np.ndarray) -> tuple:
    """
    Quantize FP32 weights to INT8
    
    Process:
    1. Find min/max of weights
    2. Calculate scale factor
    3. Quantize to INT8 range (-128 to 127)
    4. Store scale for dequantization
    
    Returns:
        (quantized_weights, scale, zero_point)
    """
    # Find range
    w_min = np.min(weights)
    w_max = np.max(weights)
    
    # Calculate scale: map [w_min, w_max] to [-128, 127]
    scale = (w_max - w_min) / 255.0
    
    # Zero point: where 0 in INT8 maps to in FP32
    zero_point = -w_min / scale
    
    # Quantize: convert to INT8
    quantized = np.round(weights / scale + zero_point).astype(np.int8)
    quantized = np.clip(quantized, -128, 127)
    
    return quantized, scale, zero_point

def dequantize_from_int8(quantized: np.ndarray, scale: float, 
                         zero_point: float) -> np.ndarray:
    """
    Dequantize INT8 back to FP32
    
    Reverse process of quantization
    """
    return (quantized.astype(np.float32) - zero_point) * scale

def quantize_to_int4(weights: np.ndarray) -> tuple:
    """
    Quantize FP32 weights to INT4 (4-bit)
    More aggressive quantization, more memory savings
    """
    w_min = np.min(weights)
    w_max = np.max(weights)
    
    # INT4 range: -8 to 7
    scale = (w_max - w_min) / 15.0
    zero_point = -w_min / scale
    
    quantized = np.round(weights / scale + zero_point).astype(np.int8)
    quantized = np.clip(quantized, -8, 7)
    
    return quantized, scale, zero_point

def calculate_compression_ratio(original: np.ndarray, quantized: np.ndarray) -> float:
    """Calculate memory compression ratio"""
    original_size = original.nbytes
    quantized_size = quantized.nbytes
    return original_size / quantized_size


# Usage Example
if __name__ == "__main__":
    print("Quantization Example")
    print("=" * 60)
    
    # Original FP32 weights
    np.random.seed(42)
    weights_fp32 = np.random.randn(1000, 1000).astype(np.float32)
    
    print(f"Original weights shape: {weights_fp32.shape}")
    print(f"Original size: {weights_fp32.nbytes / 1024 / 1024:.2f} MB")
    
    # Quantize to INT8
    weights_int8, scale, zero_point = quantize_to_int8(weights_fp32)
    print(f"\nINT8 Quantization:")
    print(f"Quantized size: {weights_int8.nbytes / 1024 / 1024:.2f} MB")
    print(f"Compression ratio: {calculate_compression_ratio(weights_fp32, weights_int8):.2f}x")
    
    # Dequantize
    weights_dequant = dequantize_from_int8(weights_int8, scale, zero_point)
    error = np.mean(np.abs(weights_fp32 - weights_dequant))
    print(f"Reconstruction error (MAE): {error:.6f}")
    
    # Quantize to INT4
    weights_int4, scale4, zp4 = quantize_to_int4(weights_fp32)
    print(f"\nINT4 Quantization:")
    print(f"Quantized size: {weights_int4.nbytes / 1024 / 1024:.2f} MB")
    print(f"Compression ratio: {calculate_compression_ratio(weights_fp32, weights_int4):.2f}x")
    
    weights_dequant4 = dequantize_from_int8(weights_int4, scale4, zp4)
    error4 = np.mean(np.abs(weights_fp32 - weights_dequant4))
    print(f"Reconstruction error (MAE): {error4:.6f}")