Optimum Quanto

repository·main·Indexed 22 days ago

https://github.com/huggingface/optimum-quanto

A PyTorch quantization backend for the Hugging Face Optimum library. It provides workflows for quantizing LLMs and Diffusers models using various bitwidths (int2, int4, int8, float8) with support for both weight and activation quantization. The library includes high-level APIs like QuantizedModelForCausalLM and a low-level API for vanilla PyTorch models, as well as device-specific optimized kernels for CPU, CUDA, MPS, and XPU.

Tokens
5K
Snippets
13
Records
19
Agent score
77%

What's inside optimum-quanto

  1. How Tensors and Modules work in Quanto

    main

    Tensors

    Quanto uses a Tensor subclass that handles projection and mapping:

    • Projection: Maps source Tensors to the optimal range for the destination type to minimize saturated or zeroed values. Projection is symmetric per-tensor/per-channel for int8/float8, and group-wise affine for lower bitwidths.
    • Mapping: Uses native PyTorch Tensor.to() for floating-point types and torch.round() for integer types.

    Modules

    Quanto replaces standard torch modules with quantized versions:

    • Weight Quantization: Weights are typically quantized per-channel along the first dimension (output features). They are dynamically converted until the model is freeze()ed.
    • Bias Handling: Biases are not quantized to preserve accuracy and avoid the complexity of extremely small scales.
    • Activation Quantization: Activations are dynamically quantized per-tensor using static scales (defaulting to [-1, 1]). Calibration is recommended to find optimal scales.

    Supported Modules:

    • QLinear (from torch.nn.Linear): Weights are quantized; biases are not. Inputs/outputs can be quantized.
    • QConv2D (from torch.nn.Conv2d): Weights are quantized; biases are not. Inputs/outputs can be quantized.
    • LayerNorm (from torch.nn.LayerNorm): Weights and biases are not quantized. Outputs can be quantized.
  2. Avoid pitfalls when quantizing activations

    main

    When quantizing activations with optimum-quanto, keep the following constraints and best practices in mind:

    • Per-tensor quantization: Activations are always quantized per-tensor because most linear algebra operations are incompatible with per-axis inputs.
    • Weight quantization: Unlike activations, weights involved in matrix multiplications are always quantized along their first axis.
    • Dequantization of outputs: The outputs of a quantized matrix multiplication are always dequantized. This is because accumulated values use a higher bitwidth (typically int32 or float32) than the activation bitwidth, and they may be combined with a float bias.
    • Handling outliers: Quantizing activations per-tensor to int8 can cause significant errors if the tensors contain large outlier values, often resulting in quantized tensors where most values are zero except for the outliers.

    Recommended solutions for activation outliers:

    1. Use float8: Representing activations using float8 is the preferred option to mitigate outlier issues.
    2. Static Smoothing: You can 'smooth' activations statically (similar to the SmoothQuant method). A script for smoothing certain model architectures is available in the external/smoothquant directory of this repository.
  3. Convert OPT or Bloom models using the SmoothQuant script

    main

    The smoothquant.py script converts Hugging Face transformers models (specifically OPT or Bloom architectures) into a "smoothed" version as described in the SmoothQuant paper. This process prepares the model for more accurate post-training quantization.

    Usage Command:

    python smoothquant.py --model <model_id> --save-path <output_directory>

    Limitations: Due to hard-coded architectural assumptions, this script currently only supports OPT models that apply layer_norm before the attention mechanism (do_layer_norm_before=true in config.json). For example, it works for facebook/opt-1.3b but not for facebook/opt-350m.

    $ python smoothquant.py --model facebook/opt-1.3b --save-path smoothed-models/facebook/opt-1.3b
  4. Add a new operation implementation to the MPS extension

    main

    To implement a new operation for the Metal Performance Shaders (MPS) extension that corresponds to an operation defined in library./ops.py, follow these steps:

    1. Create a new .mm file containing the Metal implementation.
    2. Add the .mm file to the list of sources in __init__.py.
    3. Add a binding for the new operation in pybind_module.cpp.
    4. Provide a Python implementation that calls the new binding in __init__.py.

    Requirement: Torch JIT extensions for MPS require the Xcode command-line tools to be installed on your system.

  5. Quantize Hugging Face LLM models

    main

    Use QuantizedModelForCausalLM to quantize Large Language Models. This helper class automatically handles quantization, saving, and reloading. Note that using this high-level API freezes the quantized weights by default. To keep weights unfrozen for training, use the low-level quantize API directly.

    from transformers import AutoModelForCausalLM
    from optimum.quanto import QuantizedModelForCausalLM, qint4
    
    model = AutoModelForCausalLM.from_pretrained('meta-llama/Meta-Llama-3-8B')
    # Quantize weights to 4-bit integer, excluding the lm_head
    qmodel = QuantizedModelForCausalLM.quantize(model, weights=qint4, exclude='lm_head')
    
    # Save the quantized model
    qmodel.save_pretrained('./Llama-3-8B-quantized')
    
    # Reload the quantized model later
    qmodel = QuantizedModelForCausalLM.from_pretrained('Llama-3-8B-quantized')
  6. Install dependencies for Stable Diffusion quantization examples

    main

    To run the Stable Diffusion quantization scripts, it is highly recommended to install quanto from source to ensure compatibility with the latest example requirements. Follow these steps in a new virtual environment:

    1. Clone and install quanto in editable mode:
      git clone https://github.com/huggingface/quanto
      cd quanto
      pip install -e .
    2. Navigate to the example directory and install specific requirements:
      cd examples/vision/StableDiffusion
      pip install -r requirements.txt
    git clone https://github.com/huggingface/quanto
    cd quanto
    pip install -e .
    cd examples/vision/StableDiffusion
    pip install -r requirements.txt
  7. How to add a new C++ kernel implementation to Quanto

    main

    To extend Quanto with a new C++ kernel implementation for an operation defined in library./ops.py, follow these steps:

    1. Create a new .cpp file containing the kernel implementation. Kernels must use standard C++ syntax and can utilize any PyTorch operation defined under the aten:: or c10:: namespaces.
    2. Register the new file by adding it to the list of sources in __init__.py.
    3. Create a binding for the implementation in pybind_module.cpp.
    4. Provide the final implementation that calls the binding within __init__.py.
    # Steps to add a new implementation:
    # 1. Create .cpp file (using aten:: or c10::)
    # 2. Add .cpp to sources in __init__.py
    # 3. Add binding to pybind_module.cpp
    # 4. Provide implementation calling binding in __init__.py
  8. Implement device-specific operations for Quanto

    main

    Quanto uses torch.library to provide device-specific implementations for its operations. Implementations are available for CPU (via generic C++), CUDA, MPS (Metal), and XPU (SYCL).

    Overriding existing operations

    To provide a specialized implementation for an operation that already has a default (e.g., unpack), use the @torch.library.impl decorator specifying the operation name and the target device(s).

    Declaring new operations

    To add a new device-specific operation to the quanto:: namespace, you must first declare its schema using torch.library.define before providing the implementation with @torch.library.impl.

    # 1. Overriding an existing operation
    @torch.library.impl("quanto::unpack", ["CPU", "CUDA"])
    def unpack(packed: torch.Tensor, bits: int) -> torch.Tensor:
        return ext.unpack(t, bits)
    
    # 2. Declaring and implementing a new operation
    torch.library.define(
        "quanto::gemm_f16i4",
        "(Tensor input, Tensor other, Tensor other_scale, Tensor other_shift, int group_size) -> Tensor",
    )
    
    @torch.library.impl("quanto::gemm_f16i4", ["CUDA"])
    def gemm_f16i4(
        input: torch.Tensor,
        other: torch.Tensor,
        scales: torch.Tensor,
        shift: torch.Tensor,
        group_size: int,
    ) -> torch.Tensor:
        ...
  9. How to add a new operation to the Quanto CUDA extension

    main

    To implement a new operation within the Quanto generic CUDA extension, you must follow a three-step process involving C++/CUDA source files, Pybind11 bindings, and Python implementation logic:

    1. Implement the kernel: Create a new .cpp or .cu file containing the kernel logic. These kernels can use both C++ and CUDA syntax and can leverage any PyTorch operation defined under the aten:: or c10:: namespaces.
    2. Register the source and binding:
      • Add the new .cpp or .cu file to the list of sources in __init__.py.
      • Add a binding for the new operation in pybind_module.cpp.
    3. Expose the operation in Python: Provide a Python implementation in __init__.py that calls the newly created binding.
  10. How to add a new Quanto operation

    main

    To extend the library with a new operation, you must follow these three steps:

    1. Define the operation: Add a new definition in library/ops.py.
    2. Provide a Python fallback: Implement a default version using only PyTorch operators in library/python. This ensures the operation works even without optimized kernels.
    3. Implement optimized kernels: Provide device-specific optimized kernels in library/ext for all supported hardware.
  11. Quantization workflow for vanilla PyTorch models (low-level API)

    main

    For non-Hugging Face models, follow this manual workflow. By default, weights are dynamically quantized until freeze() is called.

    1. Quantize: Convert float model to a dynamic quantized model using quantize(model, weights=..., activations=...).
    2. Calibrate (Optional): If quantizing activations, use the Calibration context manager to record activation ranges using representative samples.
    3. Tune (Optional): Perform Quantization-Aware-Training (QAT) to recover accuracy.
    4. Freeze: Replace float weights with integer weights using freeze(model).
    5. Serialize: Save the state_dict (using safetensors recommended) and the quantization_map (using quantization_map(model)).
    6. Reload: Use requantize(new_model, state_dict, quantization_map) to load weights into an empty model instance.
    # 1. Quantize
    from optimum.quanto import quantize, qint8
    quantize(model, weights=qint8, activations=qint8)
    
    # 2. Calibrate
    from optimum.quanto import Calibration
    with Calibration(momentum=0.9):
        model(samples)
    
    # 3. Tune (QAT)
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad()
        output = model(data).dequantize()
        loss = torch.nn.functional.nll_loss(output, target)
        loss.backward()
        optimizer.step()
    
    # 4. Freeze
    from optimum.quanto import freeze
    freeze(model)
    
    # 5. Serialize
    from safetensors.torch import save_file
    import json
    from optimum.quanto import quantization_map
    
    save_file(model.state_dict(), 'model.safetensors')
    with open('quantization_map.json', 'w') as f:
        json.dump(quantization_map(model), f)
    
    # 6. Reload
    import torch
    from safetensors.torch import load_file
    from optimum.quanto import requantize
    
    state_dict = load_file('model.safetensors')
    with open('quantization_map.json', 'r') as f:
        q_map = json.load(f)
    
    with torch.device('meta'):
        new_model = ... # Instantiate your model architecture
    requantize(new_model, state_dict, q_map, device=torch.device('cuda'))