Skip to content

07_model_handling___utilities_.md

Afonso Diela edited this page Jun 19, 2025 · 1 revision

Chapter 7: Model Handling & Utilities

Welcome to the final chapter of the TinyQ tutorial! We've covered a lot: how the Quantizer kicks off the process (Chapter 1), the different W8A32 and W8A16 methods (Chapter 2), the specialized Custom Quantized Layers, how TinyQ replaces standard layers (Chapter 4), the math behind weight quantization (Chapter 5), and how quantized models perform calculations (Chapter 6).

Now that we understand the core mechanics of quantization within TinyQ, this chapter focuses on the practical tools that allow you to integrate TinyQ into your workflow. These are essential Model Handling & Utilities – helper functions that support the main process, helping you get models into TinyQ, verify the results, and manage the quantized output.

Why Do We Need Utilities?

Think of the Quantizer and the custom layers as the specialized tools for the job (like a precision drill or a welding torch). But you still need the basic workshop equipment: tools to pick up the materials (load the model), test if your work is good (run inference), and put away the finished product (save the model).

TinyQ provides several helper functions in utils.py and methods within the Quantizer class itself to cover these common tasks. They provide the necessary infrastructure to work with your models before and after quantization.

Let's look at the key utilities TinyQ offers.

1. Loading Your Model: load_model

Before you can quantize a model with TinyQ, you need to get it into your Python program's memory as a PyTorch nn.Module object. TinyQ includes the load_model utility function in utils.py to simplify this, especially for models downloaded from the Hugging Face Hub.

Purpose: To load a model and its corresponding tokenizer from a local directory path.

How to Use:

You provide the path to the directory where you downloaded the model files (like pytorch_model.bin, config.json, tokenizer files).

# From examples.py or your script
from utils import load_model
import torch

model_path = "./models/facebook/opt-125m" # Replace with your path

# Load the model and tokenizer
model, tokenizer = load_model(
    model_path,
    device_map='cpu',       # Load to CPU initially
    torch_dtype=torch.float32 # Ensure weights are in float32 for quantization
)

print(f"Model and tokenizer loaded from {model_path}")
print(f"Model type: {type(model)}")
print(f"Tokenizer type: {type(tokenizer)}")

What Happens Inside (utils.py):

The load_model function primarily wraps the from_pretrained methods from the Hugging Face transformers library. It looks for model files (config.json, pytorch_model.bin, etc.) and tokenizer files in the specified model_path and creates the appropriate Python objects.

# Simplified from utils.py -> load_model function

from transformers import AutoTokenizer, AutoModelForCausalLM
import os

def load_model(model_path: str, **kwargs):
    # This function essentially does:
    if os.path.exists(model_path):
        model = AutoModelForCausalLM.from_pretrained(
            model_path,
            local_files_only=True, # Crucial for offline mode
            **kwargs # Pass device_map, torch_dtype, etc.
        )
        tokenizer = AutoTokenizer.from_pretrained(
            model_path,
            local_files_only=True,
            **kwargs
        )
        return model, tokenizer
    else:
        raise FileNotFoundError(...) # Handle error if path doesn't exist
    # ... error handling etc.

It's designed to work with the standard directory structure created when you download a model from the Hugging Face Hub using huggingface-cli download.

2. Testing Inference: get_generation

After quantizing your model, you'll want to make sure it still works and produces reasonable output. The get_generation utility helps you do a quick test by running a simple text generation task.

Purpose: To run a forward pass on a language model with a given prompt and retrieve the generated text.

How to Use:

You provide the model (either the original or the quantized one), the tokenizer, and a prompt string.

# Continuing from the load_model example or after quantization
from utils import get_generation
import torch

# Assume 'model' and 'tokenizer' are loaded (could be original or quantized)
# Move the model to GPU if available for inference (get_generation handles this)
# model = model.to(torch.device("cuda" if torch.cuda.is_available() else "cpu")) # get_generation does this

prompt = "Tell me a short story about a cat."

print(f"\nRunning inference with prompt: '{prompt}'")

# Run the generation function
result = get_generation(model, tokenizer, prompt)

print("\nGenerated Text:")
print(result)

What Happens Inside (utils.py):

The get_generation function prepares the input by tokenizing the prompt, moves the tensors to the correct device (GPU if available, otherwise CPU), sets the model to evaluation mode (model.eval()), and then calls the model's generate() method (a common method for language models to produce text). Finally, it decodes the output token IDs back into a human-readable string.

# Simplified from utils.py -> get_generation function

import torch

def get_generation(model, tokenizer, prompt: str, **kwargs):
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model.eval() # Set model to evaluation mode
    model = model.to(device) # Move model to device

    inputs = tokenizer(prompt, return_tensors="pt")
    inputs = {k: v.to(device) for k, v in inputs.items()} # Move inputs to device

    # Add padding token if needed (common for batching, good practice)
    pad_token_id = tokenizer.pad_token_id
    if pad_token_id is None:
        # ... add padding token ...
        pass # Details omitted for simplicity

    with torch.no_grad(): # Disable gradient calculation for inference
        outputs = model.generate(
            **inputs,
            max_new_tokens=20, # Generate a short sequence
            pad_token_id=pad_token_id,
            **kwargs # Pass other args like temperature, top_k etc.
        )
        
    # Decode the output tokens back into text
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

This utility is invaluable for quickly confirming that your model hasn't been broken by the quantization process and still produces coherent text.

3. Managing Output & Logs: setup_logging

When running quantization on large models or experimenting with different methods, it's helpful to have clear output showing what's happening, including any warnings or errors. TinyQ uses Python's standard logging module, and the setup_logging utility provides a convenient way to configure it.

Purpose: To set up a logger that outputs messages to both the console and a file.

How to Use:

You give it a name for the logger (e.g., "tinyq", "quantization_run") and a directory to store the log file.

# From examples.py or your script
from utils import setup_logging
import logging # Import standard logging module to use the logger

# Setup the main logger for your script
logger = setup_logging("my_tinyq_script", "logs")

# Now you can use the logger like this:
logger.info("Starting script...")
logger.info("Loading model...")
# ... load model ...
logger.info("Model loaded successfully.")
# ... perform quantization ...
logger.warning("Quantization might impact accuracy.")
# ... check results ...
logger.info("Script finished.")

# If an error occurs:
try:
    # ... code that might fail ...
    pass
except Exception as e:
    logger.error(f"An error occurred: {e}")

What Happens Inside (utils.py):

This function configures the root logger or a specific named logger, setting the format of the log messages (timestamp, level, message) and adding handlers to write the messages to both a file (FileHandler) and the console (StreamHandler).

# Simplified from utils.py -> setup_logging function

import logging
import os

def setup_logging(name: str, log_dir: str = "logs"):
    log_path = os.path.join(log_dir, f"{name}.log")
    os.makedirs(log_dir, exist_ok=True) # Create log directory if it doesn't exist
    
    logging.basicConfig(
        level=logging.INFO, # Set default logging level
        format='%(asctime)s - %(levelname)s - %(message)s', # Define message format
        handlers=[
            logging.FileHandler(log_path), # Log to a file
            logging.StreamHandler()       # Log to console
        ]
    )
    # Return a specific logger instance if needed
    return logging.getLogger(name)

Using setup_logging helps you keep track of your quantization runs, diagnose issues, and see informative messages provided by TinyQ internally (as the Quantizer class also accepts a logger).

4. Saving Your Quantized Model: Quantizer.save_model

Once you've successfully quantized a model, you don't want to repeat the quantization process every time you want to use it. The Quantizer class itself provides a save_model method to save the state of the quantized model.

Purpose: To save the state dictionary of the modified, quantized model to disk.

How to Use:

After creating a Quantizer instance and calling its quantize() method, you call save_model() on the same Quantizer instance, providing the path where you want to save the state dictionary (usually a .pth file).

# Assuming 'model' is loaded
from tinyq import Quantizer
import torch

# Initialize and quantize the model (as seen in Chapter 1)
quantizer = Quantizer(model)
quantized_model = quantizer.quantize(q_method="w8a32")

# Define the path to save the state dictionary
save_path = "./my_quantized_opt_w8a32.pth"

print(f"\nSaving quantized model state to {save_path}")

# Save the model state
quantizer.save_model(save_path)

print("Save complete.")

What Happens Inside (tinyq.py):

The save_model method simply accesses the quantized_model attribute (which holds the model after quantize() has been called) and uses torch.save() to save its state_dict(). The state dictionary contains all the registered parameters and buffers of the model, including our custom layer's int8_weights, scales, zero_points, and bias.

# Simplified from tinyq.py -> Quantizer class

import torch
import torch.nn as nn # Assuming nn is imported

class Quantizer:
    def __init__(self, model: nn.Module, logger=None):
        self.model = model
        self.quantized_model = None # Will be populated by quantize()
        self.logger = logger
        # ... other init ...

    def quantize(self, q_method, module_not_to_quantize=None):
        # ... performs quantization and sets self.quantized_model ...
        if self.logger:
             self.logger.info("Model quantization complete.")
        return self.quantized_model

    def save_model(self, save_path: str):
        """
        Save the state dictionary of the quantized model.
        """
        if self.quantized_model is None:
            raise RuntimeError("Model has not been quantized yet. Call .quantize() first.")
            
        # Use torch.save to save the state dictionary
        torch.save(self.quantized_model.state_dict(), save_path)
        if self.logger:
            self.logger.info(f"Quantized model state dictionary saved to {save_path}")

Loading the Saved Model:

To use the saved model later, you would typically:

  1. Recreate the structure of the quantized model (by loading the original model and running quantize on it without needing the original weights, just to get the structure with the correct custom layers).
  2. Load the saved state dictionary into this newly created structure using model.load_state_dict(torch.load(save_path)).

While there isn't a single load_quantized_model utility in utils.py, the combination of load_model (for the original structure) and standard PyTorch loading is how you would typically do this.

Putting It All Together: The Workflow

These utilities support the end-to-end TinyQ process:

  1. Setup Logging: Start your script by calling setup_logging.
  2. Load Original Model: Use load_model to get the PyTorch nn.Module and tokenizer.
  3. Initialize Quantizer: Create a Quantizer instance with the loaded model (and pass the logger).
  4. Quantize: Call quantizer.quantize() with your chosen method (W8A32 or W8A16). This is where the core quantization logic happens, using Custom Quantized Layers, Model Structure Replacement, and Weight Quantization Math.
  5. Test: Use get_generation on the quantized_model returned by quantize() to verify its output.
  6. Save: Use quantizer.save_model() to save the state dictionary of the quantized_model.

This flow is precisely what is demonstrated in the examples.py script provided with TinyQ.

Here's a sequence diagram illustrating this user workflow involving the utilities:

sequenceDiagram
    participant User as User Script
    participant Utils as utils.py
    participant Quantizer as tinyq.Quantizer
    participant OriginalModel as Original PyTorch Model
    participant QuantizedModel as Quantized PyTorch Model

    User->Utils: setup_logging("my_script")
    Utils-->User: Returns logger
    User->Utils: load_model("./my_model_path")
    Utils->OriginalModel: Load Model Files
    Utils-->User: Returns OriginalModel, Tokenizer
    User->Quantizer: Quantizer(OriginalModel, logger)
    User->Quantizer: quantize("w8a32")
    Note over Quantizer,OriginalModel: Internal Quantization Process<br/>(Replacement, Math, etc.)
    Quantizer-->User: Returns QuantizedModel
    User->Utils: get_generation(QuantizedModel, Tokenizer, "prompt")
    Note over Utils,QuantizedModel: Runs Quantized Forward Pass
    Utils-->User: Returns generated text
    User->Quantizer: save_model("./save_path.pth")
    Quantizer->QuantizedModel: Get state_dict()
    QuantizedModel-->Quantizer: Returns state_dict
    Quantizer->Quantizer: Save state_dict to file
    Quantizer-->User: Save confirmation
Loading

Conclusion

Model Handling & Utilities in TinyQ provide the essential surrounding framework for the core quantization functionality. Functions like load_model, get_generation, and setup_logging, along with the Quantizer's save_model method, allow you to easily load standard models, apply quantization, test the resulting model's behavior, and save the optimized version for later use.

They bridge the gap between the theoretical concepts of quantization and the practical steps needed to apply it to a real-world model, making the entire process straightforward and user-friendly.

This concludes our deep dive into the TinyQ project. We've explored its main components, from the orchestrating Quantizer to the low-level math and forward pass functions, and finally, the utilities that support the practical workflow. You now have a solid understanding of how TinyQ works under the hood and how to use it to quantize your PyTorch models.