Directory structure: └── onnx-neural-compressor/ ├── docs/ │ ├── autotune.md │ ├── calibration.md │ ├── design.md │ ├── installation_guide.md │ ├── quantization.md │ ├── quantization_layer_wise.md │ ├── quantization_weight_only.md │ └── smooth_quant.md ├── examples/ │ ├── image_recognition/ │ │ └── resnet50/ │ │ └── quantization/ │ │ └── ptq_static/ │ │ ├── main.py │ │ ├── prepare_model.py │ │ ├── run_benchmark.sh │ │ └── run_quant.sh │ └── .config/ │ └── model_params_onnxrt.json └── onnx_neural_compressor/ ├── __init__.py ├── constants.py ├── data_reader.py ├── logger.py ├── onnx_model.py ├── utility.py ├── version.py ├── algorithms/ │ ├── __init__.py │ ├── utility.py │ ├── layer_wise/ │ │ ├── __init__.py │ │ └── core.py │ ├── post_training_quant/ │ │ ├── __init__.py │ │ ├── calibrate.py │ │ ├── calibrator.py │ │ └── operators/ │ │ ├── __init__.py │ │ ├── activation.py │ │ ├── argmax.py │ │ ├── attention.py │ │ ├── base_op.py │ │ ├── binary_op.py │ │ ├── concat.py │ │ ├── conv.py │ │ ├── direct_q8.py │ │ ├── embed_layernorm.py │ │ ├── gather.py │ │ ├── gavgpool.py │ │ ├── gemm.py │ │ ├── lstm.py │ │ ├── matmul.py │ │ ├── maxpool.py │ │ ├── pad.py │ │ ├── pooling.py │ │ ├── reduce.py │ │ ├── resize.py │ │ ├── split.py │ │ └── unary_op.py │ ├── smoother/ │ │ ├── __init__.py │ │ ├── calibrator.py │ │ └── core.py │ └── weight_only/ │ ├── __init__.py │ ├── awq.py │ ├── gptq.py │ └── rtn.py └── quantization/ ├── __init__.py ├── algorithm_entry.py ├── matmul_4bits_quantizer.py ├── matmul_nbits_quantizer.py ├── quant_utils.py ├── quantize.py └── tuning.py ================================================ FILE: docs/autotune.md ================================================ AutoTune ======================================== 1. [Overview](#overview) 2. [How it Works](#how-it-works) 3. [Working with Autotune](#working-with-autotune) 4. [Get Started](#get-started) ## Overview Neural Compressor aims to help users quickly deploy low-precision models by leveraging popular compression techniques, such as post-training quantization and weight-only quantization algorithms. Despite having a variety of these algorithms, finding the appropriate configuration for a model can be difficult and time-consuming. To address this, we built the `autotune` module which identifies the best algorithm configuration for models to achieve optimal performance under the certain accuracy criteria. This module allows users to easily use predefined tuning recipes and customize the tuning space as needed. ## How it Works The autotune module constructs the tuning space according to the pre-defined tuning set or users' tuning set. It iterates the tuning space and applies the configuration on given float model then records and compares its evaluation result with the baseline. The tuning process stops when meeting the exit policy. The workflow is as below: Workflow ## Working with Autotune The `autotune` API can be used across all algorithms supported by Neural Compressor. It accepts three primary arguments: `model_input`, `tune_config`, and `eval_fn`. The `TuningConfig` class defines the tuning process, including the tuning space, order, and exit policy. - Define the tuning space User can define the tuning space by setting `config_set` with an algorithm configuration or a set of configurations. ```python # Use the default tuning space config_set = config.get_woq_tuning_config() # Customize the tuning space with one algorithm configurations config_set = config.RTNConfig(weight_sym=False, weight_group_size=[32, 64]) # Customize the tuning space with two algorithm configurations config_set = [ config.RTNConfig(weight_sym=False, weight_group_size=32), config.GPTQConfig(weight_group_size=128, weight_sym=False), ] ``` - Define the tuning order The tuning order determines how the process traverses the tuning space and samples configurations. Users can customize it by configuring the `sampler`. Currently, we provide the [`default_sampler`](https://github.com/onnx/neural-compressor/blob/main/onnx_neural_compressor/quantization/tuning.py#L210), which samples configurations sequentially, always in the same order. - Define the exit policy The exit policy includes two components: accuracy goal (`tolerable_loss`) and the allowed number of trials (`max_trials`). The tuning process will stop when either condition is met. ## Get Started The example below demonstrates how to autotune a ONNX model on four `RTNConfig` configurations. ```python from onnx_neural_compressor.quantization import config, tuning def eval_fn(model) -> float: return ... tune_config = tuning.TuningConfig( config_set=config.RTNConfig( weight_sym=[False, True], weight_group_size=[32, 128] ), tolerable_loss=0.2, max_trials=10, ) q_model = tuning.autotune(model, tune_config=tune_config, eval_fn=eval_fn) ``` ================================================ FILE: docs/calibration.md ================================================ # Calibration Algorithms in Quantization 1. [Introduction](#introduction) 2. [Calibration Algorithms](#calibration-algorithms) 3. [Support Matrix](#support-matrix) ## Introduction Quantization proves beneficial in terms of reducing the memory and computational requirements of the model. Uniform quantization transforms the input value $x ∈ [β, α]$ to lie within $[−2^{b−1}, 2^{b−1} − 1]$, where $[β, α]$ is the range of real values chosen for quantization and $b$ is the bit-width of the signed integer representation. Calibration is the process of determining the $α$ and $β$ for model weights and activations. Refer to this [link](.quantization.md#quantization-fundamentals) for more quantization fundamentals ## Calibration Algorithms Currently, Neural Compressor supports three popular calibration algorithms: - MinMax: This method gets the maximum and minimum of input values as $α$ and $β$ [^1]. It preserves the entire range and is the simplest approach. - Entropy: This method minimizes the KL divergence to reduce the information loss between full-precision and quantized data [^2]. Its primary focus is on preserving essential information. - Percentile: This method only considers a specific percentage of values for calculating the range, ignoring the remainder which may contain outliers [^3]. It enhances resolution by excluding extreme values but still retaining noteworthy data. > `kl` is used to represent the Entropy calibration algorithm in Neural Compressor. ## Reference [^1]: Vanhoucke, Vincent, Andrew Senior, and Mark Z. Mao. "Improving the speed of neural networks on CPUs." (2011). [^2]: Szymon Migacz. "Nvidia 8-bit inference width tensorrt." (2017). [^3]: McKinstry, Jeffrey L., et al. "Discovering low-precision networks close to full-precision networks for efficient embedded inference." arXiv preprint arXiv:1809.04191 (2018). ================================================ FILE: docs/design.md ================================================ Design ===== Neural Compressor features an architecture and workflow that aids in increasing performance and faster deployments across infrastructures. ## Architecture Architecture ## Workflow Workflow ================================================ FILE: docs/installation_guide.md ================================================ # Installation 1. [Installation](#installation) 1.1. [Prerequisites](#prerequisites) 1.2. [Install from Binary](#install-from-binary) 1.3. [Install from Source](#install-from-source) 2. [System Requirements](#system-requirements) 2.1. [Validated Hardware Environment](#validated-hardware-environment) 2.2. [Validated Software Environment](#validated-software-environment) ## Installation ### Prerequisites You can install Neural Compressor from binary or source. The following prerequisites and requirements must be satisfied for a successful installation: - Python version: 3.8 or 3.9 or 3.10 or 3.11 ### Install from Binary ```Shell # install stable basic version from pypi pip install onnx-neural-compressor ``` ### Install from Source ```Shell git clone https://github.com/onnx/neural-compressor.git cd neural-compressor pip install -r requirements.txt pip install . ``` ## System Requirements ### Validated Hardware Environment #### Neural Compressor supports CPUs based on [Intel 64 architecture or compatible processors](https://en.wikipedia.org/wiki/X86-64): * Intel Xeon Scalable processor (formerly Skylake, Cascade Lake, Cooper Lake, Ice Lake, and Sapphire Rapids) * Intel Xeon CPU Max Series (formerly Sapphire Rapids HBM) * Intel Core Ultra Processors (Meteor Lake, Lunar Lake) ### Validated Software Environment * OS version: CentOS 8.4, Ubuntu 22.04 * Python version: 3.10 * ONNX Runtime version: 1.18.1 ================================================ FILE: docs/quantization.md ================================================ Quantization =============== 1. [Quantization Introduction](#quantization-introduction) 2. [Quantization Fundamentals](#quantization-fundamentals) 3. [Get Started](#get-started) 3.1 [Post Training Quantization](#post-training-quantization) 3.2 [Specify Quantization Rules](#specify-quantization-rules) 3.3 [Specify Quantization Backend and Device](#specify-quantization-backend-and-device) 4. [Examples](#examples) ## Quantization Introduction Quantization is a very popular deep learning model optimization technique invented for improving the speed of inference. It minimizes the number of bits required by converting a set of real-valued numbers into the lower bit data representation, such as int8 and int4, mainly on inference phase with minimal to no loss in accuracy. This way reduces the memory requirement, cache miss rate, and computational cost of using neural networks and finally achieve the goal of higher inference performance. On Intel 3rd Gen Intel® Xeon® Scalable Processors, user could expect up to 4x theoretical performance speedup. We expect further performance improvement with [Intel® Advanced Matrix Extensions](https://www.intel.com/content/www/us/en/products/docs/accelerator-engines/advanced-matrix-extensions/overview.html) on 4th Gen Intel® Xeon® Scalable Processors. ## Quantization Fundamentals `Affine quantization` and `Scale quantization` are two common range mapping techniques used in tensor conversion between different data types. The math equation is like: $X_{int8} = round(Scale \times X_{fp32} + ZeroPoint)$. **Affine Quantization** This is so-called `Asymmetric quantization`, in which we map the min/max range in the float tensor to the integer range. Here int8 range is [-128, 127], uint8 range is [0, 255]. here: If INT8 is specified, $Scale = (|X_{max} - X_{min}|) / 127$ and $ZeroPoint = -128 - X_{min} / Scale$. or If UINT8 is specified, $Scale = (|X_{max} - X_{min}|) / 255$ and $ZeroPoint = - X_{min} / Scale$. **Scale Quantization** This is so-called `Symmetric quantization`, in which we use the maximum absolute value in the float tensor as float range and map to the corresponding integer range. The math equation is like: here: If INT8 is specified, $Scale = max(abs(X_{max}), abs(X_{min})) / 127$ and $ZeroPoint = 0$. or If UINT8 is specified, $Scale = max(abs(X_{max}), abs(X_{min})) / 255$ and $ZeroPoint = 128$. *NOTE* Sometimes the reduce_range feature, that's using 7 bit width (1 sign bit + 6 data bits) to represent int8 range, may be needed on some early Xeon platforms, it's because those platforms may have overflow issues due to fp16 intermediate calculation result when executing int8 dot product operation. After AVX512_VNNI instruction is introduced, this issue gets solved by supporting fp32 intermediate data. ### Quantization Support Matrix | Framework | Backend Library | Symmetric Quantization | Asymmetric Quantization | | :-------------- |:---------------:| ---------------:|---------------:| | ONNX Runtime | [MLAS](https://github.com/microsoft/onnxruntime/tree/master/onnxruntime/core/mlas) | Activation (int8/uint8), Weight (int8/uint8) | Activation (int8/uint8), Weight (int8/uint8) | > ***Note*** > > Activation (uint8) + Weight (int8) is recommended for performance on x86-64 machines with AVX2 and AVX512 extensions. #### Reference + MLAS: [MLAS Quantization](https://github.com/microsoft/onnxruntime/blob/master/onnxruntime/python/tools/quantization/onnx_quantizer.py) ### Quantization Approaches Quantization has two different approaches which belong to optimization on inference: 1) post training dynamic quantization 2) post training static quantization #### Post Training Dynamic Quantization The weights of the neural network get quantized into 8 bits format from float32 format offline. The activations of the neural network is quantized as well with the min/max range collected during inference runtime. This approach is widely used in dynamic length neural networks, like NLP model. #### Post Training Static Quantization Compared with `post training dynamic quantization`, the min/max range in weights and activations are collected offline on a so-called `calibration` dataset. This dataset should be able to represent the data distribution of those unseen inference dataset. The `calibration` process runs on the original fp32 model and dumps out all the tensor distributions for `Scale` and `ZeroPoint` calculations. Usually preparing 100 samples are enough for calibration. This approach is major quantization approach people should try because it could provide the better performance comparing with `post training dynamic quantization`. ## Get Started The design philosophy of the quantization interface of Neural Compressor is easy-of-use. It requests user to provide `model_input`, `model_output` and `quant_config`. Those parameters would be used to quantize and save the model. `model_input` is the ONNX model location or the ONNX model object. `model_output` is the path to save ONNX model. `quant_config` is the configuration to do quantization. User could leverage Neural Compressor to directly generate a fully quantized model without accuracy validation. Currently, Neural Compressor supports `Post Training Static Quantization` and `Post Training Dynamic Quantization`. ### Post Training Quantization ``` python from onnx_neural_compressor.quantization import quantize, config from onnx_neural_compressor import data_reader class DataReader(data_reader.CalibrationDataReader): def get_next(self): ... def rewind(self): ... calibration_data_reader = DataReader() # only needed by StaticQuantConfig qconfig = config.StaticQuantConfig(calibration_data_reader) # or qconfig = DynamicQuantConfig() quantize(model, q_model_path, qconfig) ``` ### Specify Quantization Rules Neural Compressor support specify quantization rules by operator name. Users can use `set_local` API of configs to achieve the above purpose by below code: ```python op_config = config.StaticQuantConfig(per_channel=False) quant_config = config.StaticQuantConfig( per_channel=True, ) quant_config.set_local("/h.4/mlp/fc_out/MatMul", op_config) ``` ### Specify Quantization Backend and Device Neural-Compressor will quantized models with user-specified backend or detecting the hardware and software status automatically to decide which backend should be used. The automatically selected priority is: GPU/NPU > CPU.
Backend Backend Library Support Device(cpu as default)
CPUExecutionProvider MLAS cpu
TensorrtExecutionProvider TensorRT gpu
CUDAExecutionProvider CUDA gpu
DnnlExecutionProvider OneDNN cpu
DmlExecutionProvider* OneDNN npu


> ***Note*** > > DmlExecutionProvider support works as experimental, please expect exceptions. > > Known limitation: the batch size of onnx models has to be fixed to 1 for DmlExecutionProvider, no multi-batch and dynamic batch support yet. ## Examples User could refer to [examples](../../examples) on how to quantize a new model. ================================================ FILE: docs/quantization_layer_wise.md ================================================ Layer Wise Quantization (LWQ) ===== 1. [Introduction](#introduction) 2. [Supported Framework Model Matrix](#supported-framework-model-matrix) 3. [Examples](#examples) ## Introduction Large language models (LLMs) have shown exceptional performance across various tasks, meanwhile, the substantial parameter size poses significant challenges for deployment. Layer-wise quantization(LWQ) can greatly reduce the memory footprint of LLMs, usually 80-90% reduction, which means that users can quantize LLMs even on single node using GPU or CPU. We can quantize the model under memory-constrained devices, therefore making the huge-sized LLM quantization possible. *Figure 1: The process of layer-wise quantization for ONNX model. The graph of LLM is split into several parts, and each subgraph is quantized in turn.* ## Supported Framework Model Matrix
Types/Framework ONNX Runtime
W8A8 Post Training Static Quantization
Weight-only Quantization RTN
AWQ
GPTQ
## Examples ```python from onnx_neural_compressor.quantization import matmul_4bits_quantizer algo_config = matmul_4bits_quantizer.RTNWeightOnlyQuantConfig(layer_wise_quant=True) quant = matmul_4bits_quantizer.MatMul4BitsQuantizer( model, algo_config=algo_config, ) quant.process() qmodel = quant.model ``` ================================================ FILE: docs/quantization_weight_only.md ================================================ Weight Only Quantization (WOQ) ===== 1. [Introduction](#introduction) 2. [Supported Framework Model Matrix](#supported-framework-model-matrix) 3. [Examples](#examples) 4. [WOQ Algorithms Tuning](#woq-algorithms-tuning) ## Introduction As large language models (LLMs) become more prevalent, there is a growing need for new and improved quantization methods that can meet the computational demands of these modern architectures while maintaining the accuracy. Compared to normal quantization like W8A8, weight only quantization is probably a better trade-off to balance the performance and the accuracy, since we will see below that the bottleneck of deploying LLMs is the memory bandwidth and normally weight only quantization could lead to better accuracy. Model inference: Roughly speaking , two key steps are required to get the model's result. The first one is moving the model from the memory to the cache piece by piece, in which, memory bandwidth $B$ and parameter count $P$ are the key factors, theoretically the time cost is $P*4 /B$. The second one is computation, in which, the device's computation capacity $C$ measured in FLOPS and the forward FLOPs $F$ play the key roles, theoretically the cost is $F/C$. Text generation: The most famous application of LLMs is text generation, which predicts the next token/word based on the inputs/context. To generate a sequence of texts, we need to predict them one by one. In this scenario, $F\approx P$ if some operations like bmm are ignored and past key values have been saved. However, the $C/B$ of the modern device could be to **100X,** that makes the memory bandwidth as the bottleneck in this scenario. Besides, as mentioned in many papers[1][2], activation quantization is the main reason to cause the accuracy drop. So for text generation task, weight only quantization is a preferred option in most cases. Theoretically, round-to-nearest (RTN) is the most straightforward way to quantize weight using scale maps. However, when the number of bits is small (e.g. 3), the MSE loss is larger than expected. A group size is introduced to reduce elements using the same scale to improve accuracy. There are many excellent works for weight only quantization to improve its accuracy performance, such as AWQ[3], GPTQ[4]. Neural compressor integrates these popular algorithms in time to help customers leverage them and deploy them to their own tasks. ## Supported Framework Model Matrix | Algorithms/Framework | ONNX Runtime | |--------------|----------| | RTN | ✔ | | AWQ | ✔ | | GPTQ | ✔ | > **RTN:** A quantification method that we can think of very intuitively. It does not require additional datasets and is a very fast quantization method. Generally speaking, RTN will convert the weight into a uniformly distributed integer data type, but some algorithms, such as Qlora, propose a non-uniform NF4 data type and prove its theoretical optimality. > **GPTQ:** A new one-shot weight quantization method based on approximate second-order information, that is both highly-accurate and highly efficient[4]. The weights of each column are updated based on the fixed-scale pseudo-quantization error and the inverse of the Hessian matrix calculated from the activations. The updated columns sharing the same scale may generate a new max/min value, so the scale needs to be saved for restoration. > **AWQ:** Proved that protecting only 1% of salient weights can greatly reduce quantization error. the salient weight channels are selected by observing the distribution of activation and weight per channel. The salient weights are also quantized after multiplying a big scale factor before quantization for preserving. ## Examples ### **Quantization Capability** | Config | Capability | |---|---| | weight_dtype | ['int'] | | weight_bits | [1, ..., 8] | | weight_group_size | [-1, 1, ..., $C_{in}$] | | weight_sym | ['asym', 'sym'] | | algorithm | ['RTN', 'AWQ', 'GPTQ'] | Notes: *weight_group_size = -1* refers to **per output channel quantization**. Taking a MatMul operator (input channel = $C_{in}$, output channel = $C_{out}$) for instance, when *weight_group_size = -1*, quantization will calculate total $C_{out}$ quantization parameters. Otherwise, when *weight_group_size = gs* quantization parameters are calculate with every $gs$ elements along with the input channel, leading to total $C_{out} \times (C_{in} / gs)$ quantization parameters. **RTN arguments** | rtn_args | default value | comments | |----------|-------------|-------------------------------------------------------------------| | accuracy_level | 0 | Support 0 (unset), 1(fp32 compute type of jblas kernel), 2 (fp16 compute type of jblas kernel), 3 (bf16 compute type of jblas kernel), 4 (int8 compute type of jblas kernel) | | ratios | {} | Percentile of clip | | providers | ["CPUExecutionProvider"] | Execution providers to use | | layer_wise_quant | False | Whether to quantize model layer by layer to save memory footprint. | | quant_last_matmul | True | Whether to quantize the last matmul of the model | **AWQ arguments** | awq_args | default value | comments | |----------|-------------|-------------------------------------------------------------------| | accuracy_level | 0 | Support 0 (unset), 1(fp32 compute type of jblas kernel), 2 (fp16 compute type of jblas kernel), 3 (bf16 compute type of jblas kernel), 4 (int8 compute type of jblas kernel) | | enable_auto_scale | True | Whether to search for best scales based on activation distribution | | enable_mse_search | True | Whether to search for the best clip range from range [0.91, 1.0, 0.01] | | providers | ["CPUExecutionProvider"] | Execution providers to use | | quant_last_matmul | True | Whether to quantize the last matmul of the model | **GPTQ arguments** | gptq_args | default value | comments | |----------|-------------|-------------------------------------------------------------------| | accuracy_level | 0 | Support 0 (unset), 1(fp32 compute type of jblas kernel), 2 (fp16 compute type of jblas kernel), 3 (bf16 compute type of jblas kernel), 4 (int8 compute type of jblas kernel) | | percdamp | 0.01 | Percentage of Hessian's diagonal values' average, which will be added to Hessian's diagonal to increase numerical stability| | block_size | 128 | Execute GPTQ quantization per block, block shape = [$C_{out}$, block_size] | | actorder | False | Whether to sort Hessian's diagonal values to rearrange channel-wise quantization order| | mse | False | Whether get scale and zero point with mse error | | perchannel | True | Whether quantize weight per-channel | | providers | ["CPUExecutionProvider"] | Execution providers to use | | layer_wise_quant | False | Whether to quantize model layer by layer to save memory footprint. | | quant_last_matmul | True | Whether to quantize the last matmul of the model | **Note:** Neural compressor provides `Unsigned integer for asymmetric quantization` and `Signed integer for symmetric quantization`. Please follow the below section to compress the low bit data type for saving. ### **User Code Example** ```python from onnx_neural_compressor.quantization import matmul_4bits_quantizer algo_config = matmul_4bits_quantizer.GPTQWeightOnlyQuantConfig(calibration_data_reader=calibration_data_reader) quant = matmul_4bits_quantizer.MatMul4BitsQuantizer( model, block_size=32, is_symmetric=False, algo_config=algo_config, ) quant.process() q_model = quant.model ``` ## WOQ Algorithms Tuning To find the best algorithm, users can leverage the `autotune` feature to explore a set of configurations. It automatically searches for the optimal one with the best result. Users have the option to specify their own list of potential configurations or utilize the pre-defined configuration set. **Pre-defined configurations** | WOQ configurations | Comments | |------------------|-------| |RTN_G32ASYM| {"algorithm": "RTN", "group_size": 32, "scheme": "asym"}| |GPTQ_G32ASYM| {"algorithm": "GPTQ", "group_size": 32, "scheme": "asym"}| |GPTQ_G32ASYM_DISABLE_LAST_MATMUL| {"algorithm": "GPTQ", "group_size": 32, "scheme": "asym"}
& disable last MatMul| |GPTQ_G128ASYM| {"algorithm": "GPTQ", "group_size": 128, "scheme": "asym"}| |AWQ_G32ASYM| {"algorithm": "AWQ", "group_size": 32, "scheme": "asym"}| ### **User code example** ```python from onnx_neural_compressor.quantization import tuning, config tune_config = tuning.TuningConfig(config_set=config.get_woq_tuning_config()) best_model = tuning.autotune( model_input=model, tune_config=tune_config, eval_fn=eval_fn, calibration_data_reader=data_reader, ) ``` Refer to this [link](../../examples/onnxrt/nlp/huggingface_model/text_generation/llama/quantization/weight_only) for an example of WOQ algorithms tuning on ONNX Llama models. ## Reference [1]. Xiao, Guangxuan, et al. "Smoothquant: Accurate and efficient post-training quantization for large language models." arXiv preprint arXiv:2211.10438 (2022). [2]. Wei, Xiuying, et al. "Outlier suppression: Pushing the limit of low-bit transformer language models." arXiv preprint arXiv:2209.13325 (2022). [3]. Lin, Ji, et al. "AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration." arXiv preprint arXiv:2306.00978 (2023). [4]. Frantar, Elias, et al. "Gptq: Accurate post-training quantization for generative pre-trained transformers." arXiv preprint arXiv:2210.17323 (2022). ================================================ FILE: docs/smooth_quant.md ================================================ # Smooth Quant 1. [Introduction](#Introduction) 2. [Quantization Fundamentals](#Quantization-Fundamentals) 3. [SmoothQuant and Our Enhancement](#SmoothQuant-and-Our-Enhancement) 4. [Validated Models](#Validated-Models) 5. [Usage](#Usage) ## Introduction Quantization is a common compression operation to reduce memory and accelerate inference by converting the floating point matrix to an integer matrix. For large language models (LLMs) with gigantic parameters, the systematic outliers make quantification of activations difficult. [SmoothQuant](https://arxiv.org/abs/2211.10438), a training free post-training quantization (PTQ) solution, offline migrates this difficulty from activations to weights with a mathematically equivalent transformation. ## Quantization Fundamentals Quantization is a common compression operation to reduce memory and accelerate inference; therefore, the difficulty of LLM deployment can be alleviated. Quantization converts the floating point matrix to an integer matrix. The equation of quantization is as follows: $$ X_{int8} = round(X_{fp32}/S) + Z \tag{1} $$ where $X_{fp32}$ is the input matrix, $S$ is the scale factor, $Z$ is the integer zero point. ### Per-tensor & Per-channel There are several choices of sharing quantization parameters among tensor elements, also called quantization granularity. The coarsest level, per-tensor granularity, is that all elements in the tensor share the same quantization parameters. Finer granularity means sharing quantization parameters per row or per column for 2D matrices and per channel for 3D matrices. Similarly, the finest granularity is that each element has an individual set of quantization parameters. However, due to the model accuracy and computational consumption, per-tensor or per-channel are usually adopted. **In the following part, We will show that per-channel could bring lower quantization loss but has some limitations, that is why normally we use per-channel for weight quantization and per-tensor for activation/input quantization** #### Per-tensor example Suppose the weight tensor is: ```python W = np.array( [ [0.6839, 0.4741, 0.7451], [0.9301, 0.1742, 0.6835], ] ) ``` According to the formula (1), we need scale $S$ and zero point $Z$ to calculate the integer matrix. $$ S = \frac{X_{max} - X{min}}{2^b -1} \tag{2} $$ $$ Z = -round(X_{min/}/S) \tag{3} $$ The per-tensor quantization function is: ```python def quantize(x, num_bits=8): q_min, q_max = 0, 2.0**num_bits - 1.0 scale = (np.max(x) - np.min(x)) / (2**num_bits - 1) scale = np.clip(scale, 1e-5, None) zp = (0 - (np.min(x)) / scale).round() q_x = x / scale + zp q_x = np.clip(q_x, q_min, q_max).round() print(f"scale = {scale}, zp = {zp}") return q_x, scale, zp ``` Then we can get the quantized $W_{q}$ ```bash >>> W_q, scale, zp = quantize(W) scale = 0.0029643137254901962, zp = -59.0 >>> W_q array([[172., 101., 192.], [255., 0., 172.]]) ``` With the value of scale and zp, we can dequantize the tensor. ```python def dequantize(q_x, scale, zp): return scale * (q_x - zp) ``` ```bash >>> W_dq = dequantize(W_q, 0.001, -50) >>> W_dq array([[0.222, 0.151, 0.242], [0.305, 0.05 , 0.222]]) >>> loss = ((W_dq - W)**2).mean() >>> loss 0.19833545500000002 >>> W_dq = dequantize(W_q, scale, zp) >>> W_dq array([[0.68475647, 0.4742902 , 0.74404275], [0.93079451, 0.17489451, 0.68475647]]) >>> loss = ((W_dq - W)**2).mean() >>> loss 7.384850698449426e-07 ``` The difference between $W$ and $W_{dq}$ shows that quantization affects precision and appropriate values of scale and zero point will reduce the loss of precision. #### Per-channel example Similarly, the example of per-channel quantization is as follows: ```python def quantize_per_channel(x, num_bits=8): q_min, q_max = 0, 2.0**num_bits - 1.0 x_tmp = np.reshape(x, (x.shape[0], -1)) scales = np.max(x_tmp, axis=-1, keepdims=True) / (2**num_bits - 1) zp = (0 - np.min(x_tmp, axis=-1, keepdims=True) / scales).round() q_x = x_tmp / scales + zp q_x = np.clip(q_x, q_min, q_max).round() print(f"scales = {scales}, \n zp = {zp}") return q_x, scales, zp def dequantize_per_channel(q_x, scales, zp): print(q_x, scales, zp) print(scales * (q_x - zp)) return scales * (q_x - zp) ``` ```bash >>>W_q, scales, zp = quantize_per_channel(W) scales = [[0.00292196] [0.00364745]], zp = [[-162.] [ -48.]] >>>W_q array([[ 72., 0., 93.], [207., 0., 139.]]) >>>W_dq = dequantize_per_channel(W_q, scales, zp) >>>W_dq [[0.68373882 0.47335765 0.7451 ] [0.9301 0.17507765 0.68207333]] ``` And the loss is ```bash >>> loss = ((W_dq - W)**2).mean() >>> loss.item() 5.637846469306487e-07 ``` Through this example, we can see that per-channel quantization has finer granularity and has lower loss (loss 5.6378e-07 for per-channel quantization and 7.3849e-07 for per-tensor quantization). #### Matmul quantization example For a MatMul in most model, $Y=X \cdot W$, we can quantize both the weights and activations in order to reduce the storage and accelerate inference. Using per-tensor scale quantization to show the process. ```python def quantize_per_tensor_absmax(x, n_bits=8): scales = np.max(np.abs(x)) q_max = 2 ** (n_bits - 1) - 1 scales = np.clip(scales, 1e-5, None) / q_max q_x = x / scales q_x = np.clip(q_x, -q_max, q_max).round() return q_x, scales def dequantize(q_x, scale): return scale * q_x ``` Randomly initialize the $W$ and $Y$, then calculate the result of $Y=X \cdot W$ ```bash >>>W = np.random.randn(2, 3).astype(np.float32) >>>X = np.random.randn(3, 4).astype(np.float32) >>>W array([[-0.75903535, -1.7662522 , 1.0559074 ], [ 0.47551736, 0.33230257, 0.63447773]], dtype=float32) >>>X array([[-0.9628984 , 0.5076066 , -0.54988813, 1.2411681 ], [-1.6626304 , 0.2284153 , 0.4905207 , -0.11352996], [ 1.3270313 , -0.78117365, -0.3452512 , 0.826362 ]], dtype=float32) >>>Y = np.matmul(W, X) >>>Y array([[ 5.068721 , -1.6135774 , -0.81355196, 0.13099377], [-0.16839942, -0.17835854, -0.31753427, 1.076779 ]], dtype=float32) ``` Quantize weight and activation, matmul(quantize(X), quantize(Y)) ```bash >>>W_q, W_scale = quantize_per_tensor_absmax(W) >>>X_q, X_scale = quantize_per_tensor_absmax(X) >>>print(f'{W_q}\n{W_scale}') >>>print(f'{X_q}\n{X_scale}') [[ -55. -127. 76.] [ 34. 24. 46.]] 0.013907497323404147 [[ -74. 39. -42. 95.] [-127. 17. 37. -9.] [ 101. -60. -26. 63.]] 0.013091578258304145 >>>Y_q = np.matmul(W_q, X_q) >>>Y_q array([[27875., -8864., -4365., 706.], [ -918., -1026., -1736., 5912.]], dtype=float32) >>>Y_dq = dequantize(Y_q, W_scale * X_scale) >>>Y_dq array([[ 5.0752316 , -1.6138781 , -0.7947403 , 0.12854218], [-0.16714126, -0.18680494, -0.3160754 , 1.0764043 ]], dtype=float32) ``` #### Per-channel limitation Though per-channel quantization could bring lower quantization error, we could not apply it for activations due to the difficulty of the dequantization. We would prove it in the following image and the zero point of quantization would be ignored for simplicity. The image on the left presents a normal MatMul forward with 1x2 input $x$ and 2x2 weight $w$. The results $y$ could be easily obtained by simple mathematics. In the middle image, we apply per-tensor quantization for activations and per-channel quantization for weights; the results after quantization that are denoted by $y_1$ and $y_2$, could be easily dequantized to the float results $y_{fp1}$ and $y_{fp2}$ by per channel scale $1.0/s_1s_x$ and $1.0/s_2s_x$. However, after applying per-channel quantization for activation (right image), we could not dequantize the $y_1$ and $y_2$ to float results.
## SmoothQuant and Our Enhancement ### SmoothQuant In the previous subsection, we have explained why per-channel quantization could not be applied for activation, even though it could lead to lower quantization loss. However, the quantization error loss of activation plays an important role in the accuracy loss of model quantization[^2][^3][^4]. To reduce the quantization loss of activations, lots of methods have been proposed. In the following, we briefly introduce SPIQ[^2], Outlier Suppression[^3] and Smoothquant[^4]. All these three methods share a similar idea to migrate the difficulty from activation quantization to weight quantization but differ in how much difficulty to be transferred. So **the first question is how to migrate the difficulty from activation to weights?** The solution is straightforward, that is to convert the network to an output equivalent network that is presented in the image below and apply quantization to this equivalent network. The intuition is that each channel of activation could be scaled to make it more quantization-friendly, similar to a fake per-channel activation quantization.
Please note that this conversion will make the quantization of weights more difficult, because the scales attached to weights shown above are per-input-channel, while quantization of weights is per-output-channel or per-tensor. So **the second question is how much difficulty to be migrated**, that is how to choose the **conversion per-channel scale** $s_{x1}$ and $s_{x2}$ from the above image. Different works adopt different ways. *SPIQ* just adopts the quantization scale of activations as the conversion per-channel scale. *Outlier suppression* adopts the scale of the preceding layernorm as the conversion per-channel scale. *Smoothquant* introduces a hyperparameter $\alpha$ as a smooth factor to calculate the conversion per-channel scale and balance the quantization difficulty of activation and weight. $$ s_j = max(|X_j|)^\alpha/max(|W_j|)^{1-\alpha} \tag{4} $$ j is the index of the input channels.
For most of the models such as OPT and BLOOM, $\alpha = 0.5$ is a well-balanced value to split the difficulty of weight and activation quantization. A larger $\alpha$ value could be used on models with more significant activation outliers to migrate more quantization difficulty to weights. ### Our enhancement: #### Algorithm: Auto-tuning of $\alpha$. SmoothQuant method aims to split the quantization difficulty of weight and activation by using a fixed-value $\alpha$ for an entire model. However, as the distributions of activation outliers vary not only across different models but also across different layers within a model, we hereby propose a method to obtain operator-wise optimal $\alpha$ values with the ability to tune automatically. Our proposed method consists of 8 major steps: - Hook input minimum and maximum values of operators to be smoothed. - Find a list of operators on which smoothquant could be performed. - Generate a list of $\alpha$ values of a user-defined range. - Calculate smoothing factor using $\alpha$ value, adjust parameters accordingly and forward the adjusted model given an input sample. - Perform per-channel quantization_dequantization of weights and per-tensor quantization_dequantization of activations to predict output. - Calculate the loss with respect to FP32 output, iterate the previous two steps given each $\alpha$ value and save the loss per alpha. - Apply criterion on input operator and obtain the optimal alpha values of a single input sample. - Iterate the previous three steps over a number of input samples and save the optimal $\alpha$ values. Multiple criteria (e.g min, max and mean) are supported to determine the $\alpha$ value. Both alpha range and criterion could be configured in AutoAlphaArgs. In our experiments, an $\alpha$ range of [0.0, 1.0] with a step_size of 0.1 is found to be well-balanced one for the majority of models. #### Engineering *fully automated*: users only need to pass a model and dataloader. ```python from onnx_neural_compressor.algorithms import Smoother smoother = Smoother( model, calibration_data_reader, providers=["CPUExecutionProvider"], ) smoothed_model = smoother.transform(alpha=0.7) # alpha could 'auto' to enable auto-tuning ``` *support lots of fusing patterns*: when applying the conversion per-channel scales, a mul layer needs to be inserted, which will introduce some overhead. The official code fuses this op to the previous layernorm, while we support more operator types like MatMul, Conv. Currently we only handle the operator whose scale could be fused, we are trying to support other operators, please stay tuned. ## Usage There are two ways to apply smooth quantization: 1) using a fixed `alpha` for the entire model or 2) determining the `alpha` through auto-tuning. ### Using a fixed `alpha` To set a fixed alpha for the entire model, users can follow this example: ```python from onnx_neural_compressor.quantization import config qconfig = config.StaticQuantConfig( data_reader, extra_options={"SmoothQuant": True, "SmoothQuantAlpha": 0.5, "SmoothQuantFolding": True} ) ``` Supported parameters description: "SmoothQuantAlpha": a float value. Default is 0.5. "SmoothQuantFolding": whether to fold mul into the previous operator if possible, where mul is required to update the input distribution during smoothing. ### Determining the `alpha` through auto-tuning Users can search for the best `alpha` at two levels: 1) for the entire model, and 2) for each operator. #### Auto-tune the `alpha` for the entire model The tuning process looks for the optimal `alpha` value from a list of `alpha` values provided by the user. > Please note that, it may a considerable amount of time as the tuning process applies each `alpha` to the entire model and uses the evaluation result on the entire dataset as the metric to determine the best `alpha`. Here is an example: ```python from onnx_neural_compressor.quantization import tuning, config qconfig = tuning.TuningConfig(config_set=[config.SmoothQuantConfig(alpha=np.arange(0.1, 0.5, 0.05).tolist())]) best_model = tuning.autotune( model_input=model, tune_config=qconfig, eval_fn=eval_fn, calibration_data_reader=data_reader, ) ``` #### Auto-tune the `alpha` for each operator In this case, the tuning process searches the optimal `alpha` of each operator by evaluating the loss with respect to FP32 output on a few batches of data. Here is an example: ```python from onnx_neural_compressor.quantization import quantize, config qconfig = config.StaticQuantConfig( data_reader, extra_options={ "SmoothQuant": True, "SmoothQuantAlpha": "auto", "SmoothQuantCalibIter": 1, "AutoAlphaArgs": { "alpha_min": 0.3, # min value of auto-tuning alpha search space "alpha_max": 0.7, # max value of auto-tuning alpha search space "alpha_step": 0.05, # step_size of auto-tuning alpha search space "attn_method": "min", }, }, ) quantize(model, output_model_path, qconfig) ``` ## Reference [^1]: Jason, Wei, et al. "Emergent Abilities of Large Language Models". Published in Transactions on Machine Learning Research (2022). [^2]: Yvinec, Edouard, et al. "SPIQ: Data-Free Per-Channel Static Input Quantization." Proceedings of the IEEE/CVF Winter Conference on Applications of Computer Vision. 2023. [^3]: Wei, Xiuying, et al. "Outlier suppression: Pushing the limit of low-bit transformer language models." arXiv preprint arXiv:2209.13325 (2022). [^4]: Xiao, Guangxuan, et al. "Smoothquant: Accurate and efficient post-training quantization for large language models." arXiv preprint arXiv:2211.10438 (2022). ================================================ FILE: examples/image_recognition/resnet50/quantization/ptq_static/main.py ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # pylint:disable=redefined-outer-name,logging-format-interpolation import argparse import collections import logging import os import re import time import cv2 import numpy as np import onnx import onnxruntime as ort from PIL import Image from sklearn import metrics from onnx_neural_compressor import data_reader, quantization from onnx_neural_compressor.quantization import config, tuning logger = logging.getLogger(__name__) logging.basicConfig( format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", datefmt="%m/%d/%Y %H:%M:%S", level=logging.WARN ) def _topk_shape_validate(preds, labels): # preds shape can be Nxclass_num or class_num(N=1 by default) # it's more suitable for 'Accuracy' with preds shape Nx1(or 1) output from argmax if isinstance(preds, int): preds = [preds] preds = np.array(preds) elif isinstance(preds, np.ndarray): preds = np.array(preds) elif isinstance(preds, list): preds = np.array(preds) preds = preds.reshape((-1, preds.shape[-1])) # consider labels just int value 1x1 if isinstance(labels, int): labels = [labels] labels = np.array(labels) elif isinstance(labels, tuple): labels = np.array([labels]) labels = labels.reshape((labels.shape[-1], -1)) elif isinstance(labels, list): if isinstance(labels[0], int): labels = np.array(labels) labels = labels.reshape((labels.shape[0], 1)) elif isinstance(labels[0], tuple): labels = np.array(labels) labels = labels.reshape((labels.shape[-1], -1)) else: labels = np.array(labels) # labels most have 2 axis, 2 cases: N(or Nx1 sparse) or Nxclass_num(one-hot) # only support 2 dimension one-shot labels # or 1 dimension one-hot class_num will confuse with N if len(preds.shape) == 1: N = 1 class_num = preds.shape[0] preds = preds.reshape([-1, class_num]) elif len(preds.shape) >= 2: N = preds.shape[0] preds = preds.reshape([N, -1]) class_num = preds.shape[1] label_N = labels.shape[0] assert label_N == N, "labels batch size should same with preds" labels = labels.reshape([N, -1]) # one-hot labels will have 2 dimension not equal 1 if labels.shape[1] != 1: labels = labels.argsort()[..., -1:] return preds, labels class TopK: def __init__(self, k=1): self.k = k self.num_correct = 0 self.num_sample = 0 def update(self, preds, labels, sample_weight=None): preds, labels = _topk_shape_validate(preds, labels) preds = preds.argsort()[..., -self.k :] if self.k == 1: correct = metrics.accuracy_score(preds, labels, normalize=False) self.num_correct += correct else: for p, l in zip(preds, labels): # get top-k labels with np.argpartition # p = np.argpartition(p, -self.k)[-self.k:] l = l.astype("int32") if l in p: self.num_correct += 1 self.num_sample += len(labels) def reset(self): self.num_correct = 0 self.num_sample = 0 def result(self): if self.num_sample == 0: logger.warning("Sample num during evaluation is 0.") return 0 return self.num_correct / self.num_sample class DataReader(data_reader.CalibrationDataReader): def __init__(self, model_path, dataset_location, image_list, batch_size=1, calibration_sampling_size=-1): self.batch_size = batch_size self.image_list = [] self.label_list = [] src_lst = [] label_lst = [] num = 0 with open(image_list, "r") as f: for s in f: image_name, label = re.split(r"\s+", s.strip()) src = os.path.join(dataset_location, image_name) if not os.path.exists(src): continue src_lst.append(src) label_lst.append(int(label)) if len(src_lst) == batch_size: self.image_list.append(src_lst) self.label_list.append(label_lst) num += batch_size if calibration_sampling_size > 0 and num >= calibration_sampling_size: break src_lst = [] label_lst = [] if len(src_lst) > 0: self.image_list.append(src_lst) self.label_list.append(label_lst) model = onnx.load(model_path, load_external_data=False) self.inputs_names = [input.name for input in model.graph.input] self.iter_next = iter(self.image_list) def _preprpcess(self, src): with Image.open(src) as image: image = np.array(image.convert("RGB")).astype(np.float32) image = image / 255.0 image = cv2.resize(image, (256, 256), interpolation=cv2.INTER_LINEAR) h, w = image.shape[0], image.shape[1] y0 = (h - 224) // 2 x0 = (w - 224) // 2 image = image[y0 : y0 + 224, x0 : x0 + 224, :] image = (image - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] image = image.transpose((2, 0, 1)) return image.astype("float32") def get_next(self): lst = next(self.iter_next, None) if lst is not None: return {self.inputs_names[0]: np.stack([self._preprpcess(src) for src in lst])} else: return None def rewind(self): self.iter_next = iter(self.image_list) def eval_func(model, dataloader, metric): metric.reset() sess = ort.InferenceSession(model, providers=ort.get_available_providers()) labels = dataloader.label_list for idx, batch in enumerate(dataloader): output = sess.run(None, batch) metric.update(output, labels[idx]) return metric.result() if __name__ == "__main__": logger.info("Evaluating ONNXRuntime full precision accuracy and performance:") parser = argparse.ArgumentParser( description="Resnet50 fine-tune examples for image classification tasks.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("--model_path", type=str, help="Pre-trained model on onnx file") parser.add_argument("--dataset_location", type=str, help="Imagenet data path") parser.add_argument("--label_path", type=str, help="Imagenet label path") parser.add_argument("--benchmark", action="store_true", default=False) parser.add_argument("--tune", action="store_true", default=False, help="whether quantize the model") parser.add_argument("--output_model", type=str, help="output model path") parser.add_argument("--mode", type=str, help="benchmark mode of performance or accuracy") parser.add_argument( "--intra_op_num_threads", type=int, default=4, help="intra_op_num_threads for performance benchmark" ) parser.add_argument( "--quant_format", type=str, default="QOperator", choices=["QDQ", "QOperator"], help="quantization format" ) parser.add_argument( "--batch_size", default=1, type=int, ) args = parser.parse_args() top1 = TopK() dataloader = DataReader(args.model_path, args.dataset_location, args.label_path, args.batch_size) def eval(onnx_model): dataloader.rewind() return eval_func(onnx_model, dataloader, top1) if args.benchmark: if args.mode == "performance": total_time = 0.0 num_iter = 100 num_warmup = 10 sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = args.intra_op_num_threads session = ort.InferenceSession(args.model_path, sess_options, providers=ort.get_available_providers()) ort_inputs = {} len_inputs = len(session.get_inputs()) inputs_names = [session.get_inputs()[i].name for i in range(len_inputs)] for idx, batch in enumerate(dataloader): if idx + 1 > num_iter: break tic = time.time() predictions = session.run(None, batch) toc = time.time() if idx >= num_warmup: total_time += toc - tic print("\n", "-" * 10, "Summary:", "-" * 10) print(args) throughput = (num_iter - num_warmup) / total_time print("Throughput: {} samples/s".format(throughput)) elif args.mode == "accuracy": acc_result = eval_func(args.model_path, dataloader, top1) print("Batch size = %d" % dataloader.batch_size) print("Accuracy: %.5f" % acc_result) if args.tune: calibration_data_reader = DataReader( args.model_path, args.dataset_location, args.label_path, args.batch_size, calibration_sampling_size=100 ) custom_tune_config = tuning.TuningConfig( config_set=config.StaticQuantConfig.get_config_set_for_tuning( quant_format=( quantization.QuantFormat.QOperator if args.quant_format == "QOperator" else quantization.QuantFormat.QDQ ), ) ) best_model = tuning.autotune( model_input=args.model_path, tune_config=custom_tune_config, eval_fn=eval, calibration_data_reader=calibration_data_reader, ) onnx.save(best_model, args.output_model) ================================================ FILE: examples/image_recognition/resnet50/quantization/ptq_static/prepare_model.py ================================================ import argparse import os import sys import urllib.request MODEL_URL = "https://github.com/onnx/models/raw/main/validated/vision/classification/resnet/model/resnet50-v1-12.onnx" MAX_TIMES_RETRY_DOWNLOAD = 5 def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument("--input_model", type=str, required=False, default="resnet50-v1-12.onnx") parser.add_argument("--output_model", type=str, required=True) return parser.parse_args() def progressbar(cur, total=100): percent = "{:.2%}".format(cur / total) sys.stdout.write("\r[%-100s] %s" % ("#" * int(cur), percent)) sys.stdout.flush() def schedule(blocknum, blocksize, totalsize): if totalsize == 0: percent = 0 else: percent = min(1.0, blocknum * blocksize / totalsize) * 100 progressbar(percent) def download_model(url, model_name, retry_times=5): if os.path.isfile(model_name): print(f"{model_name} exists, skip download") return True print("download model...") retries = 0 while retries < retry_times: try: urllib.request.urlretrieve(url, model_name, schedule) break except KeyboardInterrupt: return False except: retries += 1 print(f"Download failed{', Retry downloading...' if retries < retry_times else '!'}") return retries < retry_times def prepare_model(input_model, output_model): # Download model from [ONNX Model Zoo](https://github.com/onnx/models) download_model(MODEL_URL, output_model, MAX_TIMES_RETRY_DOWNLOAD) if __name__ == "__main__": args = parse_arguments() prepare_model(args.input_model, args.output_model) ================================================ FILE: examples/image_recognition/resnet50/quantization/ptq_static/run_benchmark.sh ================================================ #!/bin/bash set -x function main { init_params "$@" run_benchmark } # init params function init_params { for var in "$@" do case $var in --input_model=*) input_model=$(echo "$var" |cut -f2 -d=) ;; --dataset_location=*) dataset_location=$(echo "$var" |cut -f2 -d=) ;; --label_path=*) label_path=$(echo "$var" |cut -f2 -d=) ;; --mode=*) mode=$(echo "$var" |cut -f2 -d=) ;; --intra_op_num_threads=*) intra_op_num_threads=$(echo "$var" |cut -f2 -d=) ;; esac done } # run_benchmark function run_benchmark { python main.py \ --model_path "${input_model}" \ --dataset_location "${dataset_location}" \ --label_path "${label_path-${dataset_location}/../val.txt}" \ --mode "${mode}" \ --batch_size 1 \ --intra_op_num_threads "${intra_op_num_threads-4}" \ --benchmark } main "$@" ================================================ FILE: examples/image_recognition/resnet50/quantization/ptq_static/run_quant.sh ================================================ #!/bin/bash set -x function main { init_params "$@" run_tuning } # init params function init_params { for var in "$@" do case $var in --input_model=*) input_model=$(echo "$var" |cut -f2 -d=) ;; --output_model=*) output_model=$(echo "$var" |cut -f2 -d=) ;; --dataset_location=*) dataset_location=$(echo "$var" |cut -f2 -d=) ;; --label_path=*) label_path=$(echo "$var" |cut -f2 -d=) ;; --quant_format=*) quant_format=$(echo "$var" |cut -f2 -d=) ;; esac done } # run_tuning function run_tuning { python main.py \ --model_path "${input_model}" \ --dataset_location "${dataset_location}" \ --label_path "${label_path-${dataset_location}/../val.txt}" \ --output_model "${output_model}" \ --quant_format "${quant_format-QOperator}" \ --tune } main "$@" ================================================ FILE: examples/.config/model_params_onnxrt.json ================================================ { "onnxrt": { "llama-2-7b-rtn": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf", "main_script": "main.py", "batch_size": 1, "algorithm": "RTN" }, "llama-2-7b-rtn-with-past": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "RTN" }, "llama-2-7b-rtn-with-past-qdq": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past-opset-21", "main_script": "main.py", "batch_size": 1, "algorithm": "RTN" }, "llama-2-7b-awq": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf", "main_script": "main.py", "batch_size": 1, "algorithm": "AWQ" }, "llama-2-7b-awq-with-past": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "AWQ" }, "llama-2-7b-awq-with-past-qdq": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past-opset-21", "main_script": "main.py", "batch_size": 1, "algorithm": "AWQ" }, "llama-2-7b-gptq": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf", "main_script": "main.py", "batch_size": 1, "algorithm": "GPTQ" }, "llama-2-7b-gptq-with-past": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "GPTQ" }, "llama-2-7b-gptq-with-past-qdq": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past-opset-21", "main_script": "main.py", "batch_size": 1, "algorithm": "GPTQ" }, "llama-2-7b-woq_tune": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf", "main_script": "main.py", "batch_size": 1, "algorithm": "WOQ_TUNE" }, "llama-2-7b-woq_tune-with-past": { "model_name": "meta-llama/Llama-2-7b-hf", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Llama-2-7b-hf-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "WOQ_TUNE" }, "llama-3-8b-gptq-with-past": { "model_name": "meta-llama/Meta-Llama-3-8B", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Meta-Llama-3-8B-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "GPTQ" }, "phi-3-mini-128k-instruct-rtn-with-past": { "model_name": "microsoft/Phi-3-mini-128k-instruct", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Phi-3-mini-128k-instruct-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "RTN" }, "qwen2-7b-instruct-rtn-with-past": { "model_name": "Qwen/Qwen2-7B-Instruct", "model_src_dir": "nlp/huggingface_model/text_generation/quantization/weight_only", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/Qwen2-7B-Instruct-with-past", "main_script": "main.py", "batch_size": 1, "algorithm": "RTN" }, "bert_base_MRPC": { "model_src_dir": "nlp/bert/quantization/ptq_static", "dataset_location": "/tf_dataset/pytorch/glue_data/MRPC", "input_model": "/tf_dataset2/models/onnx/bert_base_MRPC/bert.onnx", "main_script": "main.py", "batch_size": 8 }, "bert_base_MRPC_dynamic": { "model_src_dir": "nlp/bert/quantization/ptq_dynamic", "dataset_location": "/tf_dataset/pytorch/glue_data/MRPC", "input_model": "/tf_dataset2/models/onnx/bert_base_MRPC/bert.onnx", "main_script": "main.py", "batch_size": 8 }, "resnet50-v1-12_qdq": { "model_src_dir": "image_recognition/resnet50/quantization/ptq_static", "dataset_location": "/tf_dataset2/datasets/imagenet/ImagenetRaw/ILSVRC2012_img_val", "input_model": "/tf_dataset2/models/onnx/resnet50-v1-12/resnet50-v1-13.onnx", "main_script": "main.py", "batch_size": 1 }, "resnet50-v1-12": { "model_src_dir": "image_recognition/resnet50/quantization/ptq_static", "dataset_location": "/tf_dataset2/datasets/imagenet/ImagenetRaw/ILSVRC2012_img_val", "input_model": "/tf_dataset2/models/onnx/resnet50-v1-12/resnet50-v1-12.onnx", "main_script": "main.py", "batch_size": 1 }, "sd-v1-5-sq": { "model_src_dir": "nlp/huggingface_model/text_to_image/stable_diffusion_v1_5/quantization/ptq_static", "dataset_location": "", "input_model": "/tf_dataset2/models/onnx/sd_v1_5", "main_script": "main.py", "batch_size": 1 } } } ================================================ FILE: onnx_neural_compressor/__init__.py ================================================ # Copyright (c) 2021 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Neural Compressor: An open-source Python library supporting popular model compression techniques for ONNX models.""" ================================================ FILE: onnx_neural_compressor/constants.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Class for All constants.""" import datetime from packaging import version # constants for configs GLOBAL = "global" LOCAL = "local" DEFAULT_WHITE_LIST = "*" EMPTY_WHITE_LIST = None # config name BASE_CONFIG = "base_config" COMPOSABLE_CONFIG = "composable_config" RTN = "rtn" STATIC_QUANT = "static_quant" DYNAMIC_QUANT = "dynamic_quant" SMOOTH_QUANT = "smooth_quant" GPTQ = "gptq" AWQ = "awq" DEFAULT_WORKSPACE = "./nc_workspace/{}/".format(datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")) ONNXRT116_VERSION = version.Version("1.16.0") ONNXRT1161_VERSION = version.Version("1.16.1") ONNXRT119_VERSION = version.Version("1.19.0") PRIORITY_RTN = 60 PRIORITY_GPTQ = 70 PRIORITY_AWQ = 50 PRIORITY_SMOOTH_QUANT = 80 PRIORITY_STATIC_QUANT = 70 PRIORITY_DYNAMIC_QUANT = 60 MAXIMUM_PROTOBUF = 2147483648 WHITE_MODULE_LIST = ["MatMul", "Conv"] RTN_OP_LIST = ["MatMul"] AWQ_OP_LIST = ["MatMul"] GPTQ_OP_LIST = ["MatMul"] DYNAMIC_CPU_OP_LIST = ["FusedConv", "Conv", "EmbedLayerNormalization", "MatMul", "Gather", "Attention", "LSTM"] DYNAMIC_CUDA_OP_LIST = ["FusedConv", "Conv", "EmbedLayerNormalization", "MatMul", "Gather", "Attention", "LSTM"] DYNAMIC_DML_OP_LIST = [] DYNAMIC_DNNL_OP_LIST = ["FusedConv", "Conv", "EmbedLayerNormalization", "MatMul", "Gather", "Attention", "LSTM"] DYNAMIC_TRT_OP_LIST = [] STATIC_QDQ_CPU_OP_LIST = [ "FusedConv", "Conv", "Gather", "GatherElements", "GatherND", "Tile", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "Resize", "Abs", "Shrink", "Sign", "Flatten", "Expand", "Slice", "Mod", "ReduceMax", "ReduceMin", "CenterCropPad", ] STATIC_QDQ_CUDA_OP_LIST = [ "FusedConv", "Conv", "Gather", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "Resize", "Abs", "Shrink", "Sign", "Flatten", "Expand", "Slice", "Mod", "ReduceMax", "ReduceMin", ] STATIC_QDQ_DML_OP_LIST = [ "Conv", "MatMul", "Relu", "Clip", "MaxPool", ] STATIC_QDQ_DNNL_OP_LIST = [ "FusedConv", "Conv", "Gather", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "Resize", ] STATIC_QDQ_TRT_OP_LIST = [ "Conv", "MatMul", "Attention", "LeakyRelu", "Gather", "Sigmoid", "MaxPool", "EmbedLayerNormalization", "GlobalAveragePool", "Pad", "Split", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "Resize", "Gemm", "Add", ] STATIC_QOPERATOR_CPU_OP_LIST = [ "FusedConv", "Conv", "Gather", "GatherElements", "GatherND", "Tile", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Mul", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Add", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "ArgMax", "Resize", "Abs", "Shrink", "Sign", "Flatten", "Expand", "Slice", "Mod", "ReduceMax", "ReduceMin", "CenterCropPad", ] STATIC_QOPERATOR_CUDA_OP_LIST = [ "FusedConv", "Conv", "Gather", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Mul", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Add", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "ArgMax", "Resize", "Abs", "Shrink", "Sign", "Flatten", "Expand", "Slice", "Mod", "ReduceMax", "ReduceMin", ] STATIC_QOPERATOR_DML_OP_LIST = [ "Conv", "MatMul", "Mul", "Relu", "Clip", "MaxPool", "Add", ] STATIC_QOPERATOR_DNNL_OP_LIST = [ "FusedConv", "Conv", "Gather", "MatMul", "Gemm", "EmbedLayerNormalization", "Attention", "Mul", "Relu", "Clip", "LeakyRelu", "Sigmoid", "MaxPool", "GlobalAveragePool", "Pad", "Split", "Add", "Squeeze", "Reshape", "Concat", "AveragePool", "Unsqueeze", "Transpose", "ArgMax", "Resize", ] STATIC_QOPERATOR_TRT_OP_LIST = [] STATIC_QOPERATOR_OP_LIST_MAP = { "CPUExecutionProvider": STATIC_QOPERATOR_CPU_OP_LIST, "CUDAExecutionProvider": STATIC_QOPERATOR_CUDA_OP_LIST, "DmlExecutionProvider": STATIC_QOPERATOR_DML_OP_LIST, "DnnlExecutionProvider": STATIC_QOPERATOR_DNNL_OP_LIST, "TensorrtExecutionProvider": STATIC_QOPERATOR_TRT_OP_LIST, } STATIC_QDQ_OP_LIST_MAP = { "CPUExecutionProvider": STATIC_QDQ_CPU_OP_LIST, "CUDAExecutionProvider": STATIC_QDQ_CUDA_OP_LIST, "DmlExecutionProvider": STATIC_QDQ_DML_OP_LIST, "DnnlExecutionProvider": STATIC_QDQ_DNNL_OP_LIST, "TensorrtExecutionProvider": STATIC_QDQ_TRT_OP_LIST, } DYNAMIC_OP_LIST_MAP = { "CPUExecutionProvider": DYNAMIC_CPU_OP_LIST, "CUDAExecutionProvider": DYNAMIC_CUDA_OP_LIST, "DmlExecutionProvider": DYNAMIC_DML_OP_LIST, "DnnlExecutionProvider": DYNAMIC_DNNL_OP_LIST, "TensorrtExecutionProvider": DYNAMIC_TRT_OP_LIST, } ================================================ FILE: onnx_neural_compressor/data_reader.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc class CalibrationDataReader(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass): return hasattr(subclass, "get_next") and callable(subclass.get_next) or NotImplemented @abc.abstractmethod def get_next(self) -> dict: """generate the input data dict for ONNXinferenceSession run""" raise NotImplementedError def __iter__(self): return self def __next__(self): result = self.get_next() if result is None: raise StopIteration return result @abc.abstractmethod def rewind(self): """Regenerate data.""" raise NotImplementedError ================================================ FILE: onnx_neural_compressor/logger.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os def _pretty_dict(value, indent=0): """Make the logger dict pretty.""" prefix = "\n" + " " * (indent + 4) if isinstance(value, dict): items = [prefix + repr(key) + ": " + _pretty_dict(value[key], indent + 4) for key in value] return "{%s}" % (",".join(items) + "\n" + " " * indent) elif isinstance(value, list): items = [prefix + _pretty_dict(item, indent + 4) for item in value] return "[%s]" % (",".join(items) + "\n" + " " * indent) elif isinstance(value, tuple): items = [prefix + _pretty_dict(item, indent + 4) for item in value] return "(%s)" % (",".join(items) + "\n" + " " * indent) else: return repr(value) LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper() _logger = logging.getLogger("onnx_neural_compressor") _logger.handlers.clear() _logger.setLevel(LOGLEVEL) formatter = logging.Formatter("%(asctime)s [%(levelname)s][%(filename)s:%(lineno)d] %(message)s", "%Y-%m-%d %H:%M:%S") streamHandler = logging.StreamHandler() streamHandler.setFormatter(formatter) _logger.addHandler(streamHandler) _logger.propagate = False def log(level, msg, *args, **kwargs): """Output log with the level as a parameter.""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.log(level, line, *args, **kwargs) else: _logger.log(level, msg, *args, **kwargs) def debug(msg, *args, **kwargs): """Output log with the debug level.""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.debug(line, *args, **kwargs) else: _logger.debug(msg, *args, **kwargs) def error(msg, *args, **kwargs): """Output log with the error level.""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.error(line, *args, **kwargs) else: _logger.error(msg, *args, **kwargs) def fatal(msg, *args, **kwargs): """Output log with the fatal level.""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.fatal(line, *args, **kwargs) else: _logger.fatal(msg, *args, **kwargs) def info(msg, *args, **kwargs): """Output log with the info level.""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.info(line, *args, **kwargs) else: _logger.info(msg, *args, **kwargs) def warning(msg, *args, **kwargs): """Output log with the warning level (Alias of the method warn).""" kwargs.setdefault("stacklevel", 2) if isinstance(msg, dict): for _, line in enumerate(_pretty_dict(msg).split("\n")): _logger.warning(line, *args, **kwargs) else: _logger.warning(msg, *args, **kwargs) ================================================ FILE: onnx_neural_compressor/onnx_model.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Class for ONNX model.""" import collections import copy import os import pathlib import sys import onnx import transformers from onnx_neural_compressor import constants, logger, utility class ONNXModel: """Build ONNX model.""" def __init__(self, model, **kwargs): """Initialize an ONNX model. Args: model (str or ModelProto): path to onnx model or loaded ModelProto model object. """ self.model = model if not isinstance(model, str) else onnx.load(model, load_external_data=False) self._model_path = None if not isinstance(model, str) else model self.check_is_large_model() if self._is_large_model and self._model_path is None and not kwargs.get("ignore_warning", False): logger.warning("Model size > 2GB. Please use model path instead of onnx model object to quantize") if self._is_large_model and isinstance(model, str) and kwargs.get("load_external_data", True): onnx.external_data_helper.load_external_data_for_model(self.model, os.path.dirname(self._model_path)) self._config = None if isinstance(model, str) and os.path.exists(pathlib.Path(model).parent.joinpath("config.json").as_posix()): self._config = transformers.PretrainedConfig.from_pretrained(pathlib.Path(model).parent.as_posix()) self.node_name_counter = {} self._output_name_to_node = {} self._input_name_to_nodes = {} self._get_output_name_to_node(self.model.graph.node) self._get_input_name_to_nodes(self.model.graph.node) self._graph_info = {} self._get_graph_info() self._q_config = None def output_name_to_node(self): self._output_name_to_node = {} self._get_output_name_to_node(self.model.graph.node) return self._output_name_to_node def input_name_to_nodes(self): self._input_name_to_nodes = {} self._get_input_name_to_nodes(self.model.graph.node) return self._input_name_to_nodes def _get_input_name_to_nodes(self, nodes): """Get input names of nodes.""" for node in nodes: attrs = [ attr for attr in node.attribute if attr.type == onnx.AttributeProto.GRAPH or attr.type == onnx.AttributeProto.GRAPHS ] if len(attrs) > 0: for attr in attrs: self._get_input_name_to_nodes(attr.g.node) for input_name in node.input: if len(input_name.strip()) != 0: if input_name not in self._input_name_to_nodes: self._input_name_to_nodes[input_name] = [node] else: self._input_name_to_nodes[input_name].append(node) def _get_output_name_to_node(self, nodes): """Get output names of nodes.""" for node in nodes: attrs = [ attr for attr in node.attribute if attr.type == onnx.AttributeProto.GRAPH or attr.type == onnx.AttributeProto.GRAPHS ] if len(attrs) > 0: for attr in attrs: self._get_output_name_to_node(attr.g.node) for output_name in node.output: if len(output_name.strip()) != 0: self._output_name_to_node[output_name] = node @property def model_path(self): """Return model path.""" return self._model_path @model_path.setter def model_path(self, path): """Set model path.""" self._model_path = path def check_is_large_model(self): """Check model > 2GB.""" init_size = 0 for init in self.model.graph.initializer: # if initializer has external data location, return True if init.HasField("data_location") and init.data_location == onnx.TensorProto.EXTERNAL: self._is_large_model = True return # if raise error of initializer size > 2GB, return True try: init_bytes = init.SerializeToString() init_size += sys.getsizeof(init_bytes) except Exception as e: if "exceeds maximum protobuf size of 2GB" in str(e): self._is_large_model = True return else: # pragma: no cover raise e if init_size > constants.MAXIMUM_PROTOBUF: self._is_large_model = True return self._is_large_model = False @property def is_large_model(self): """Check the onnx model is over 2GB.""" return self._is_large_model @property def framework(self): """Return framework.""" return "onnxruntime" def add_initializer(self, tensor): """Add a initializer to model.""" if tensor.name not in [i.name for i in self._model.graph.initializer]: self._model.graph.initializer.append(tensor) def add_initializers(self, tensors): """Add initializers to model.""" for tensor in tensors: self.add_initializer(tensor) @property def q_config(self): """Return q_config.""" return self._q_config @q_config.setter def q_config(self, q_config): """Set q_config.""" self._q_config = q_config @property def hf_config(self): """Return huggingface config if model is Transformer-based.""" return self._config def input(self): """Return input of model.""" return [i.name for i in self.model.graph.input] def output(self): """Return output of model.""" return [i.name for i in self.model.graph.output] @property def model(self): """Return model itself.""" return self._model @model.setter def model(self, model): """Set model itself.""" self._model = model self._graph_info = {} self._get_graph_info() self._output_name_to_node = {} self._input_name_to_nodes = {} self._get_input_name_to_nodes(self._model.graph.node) self._get_output_name_to_node(self._model.graph.node) def nodes(self): """Return model nodes.""" return self._model.graph.node def initializer(self): """Return model initializer.""" return self._model.graph.initializer def graph(self): """Return model graph.""" return self._model.graph @property def ir_version(self): """Return model ir_version.""" return self._model.ir_version @property def opset_import(self): """Return model opset_import.""" return self._model.opset_import def update(self): """Update model info.""" self._graph_info = {} self._get_graph_info() self._output_name_to_node = self.output_name_to_node() self._input_name_to_nodes = self.input_name_to_nodes() @property def graph_info(self): """Return ORT Graph Info object holding information about backend graph.""" return self._graph_info def _get_graph_info(self): """Update graph info.""" for node in self.model.graph.node: self.graph_info.update({node.name: node.op_type}) def is_graph_output(self, name): """Check whether the tensor is the graph output.""" return name in self.output() def save(self, root): """Save ONNX model.""" if os.path.split(root)[0] != "" and not os.path.exists(os.path.split(root)[0]): os.mkdir(os.path.split(root)[0]) if self.is_large_model: # pragma: no cover onnx.external_data_helper.load_external_data_for_model(self.model, os.path.split(self._model_path)[0]) onnx.save_model( self.model, root, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(root) + "_data", size_threshold=1024, convert_attribute=False, ) else: onnx.save(self.model, root) self._model_path = root if self._config is not None and not os.path.exists(os.path.join(os.path.split(root)[0], "config.json")): model_type = "" if not hasattr(self._config, "model_type") else getattr(self._config, "model_type") setattr(self._config.__class__, "model_type", model_type) output_config_file = pathlib.Path(root).parent.joinpath("config.json").as_posix() self._config.to_json_file(output_config_file, use_diff=False) def remove_initializer(self, tensor): """Remove an initializer from model.""" if tensor in self._model.graph.initializer: self._model.graph.initializer.remove(tensor) def remove_initializers(self, init_to_remove): """Remove initializers from model.""" for initializer in init_to_remove: self.remove_initializer(initializer) def get_initializer(self, name): """ "Find the initializer with specified name.""" for initializer in self.model.graph.initializer: if initializer.name == name: return initializer return None def remove_node(self, node): """Remove a node from model.""" if node in self._model.graph.node: self._model.graph.node.remove(node) def remove_nodes(self, nodes_to_remove): """Remove nodes from model.""" for node in nodes_to_remove: self.remove_node(node) def add_node(self, node): """Add a node to model.""" self._model.graph.node.extend([node]) def add_nodes(self, nodes_to_add): """Add nodes to model.""" self._model.graph.node.extend(nodes_to_add) def get_children(self, node, input_name_to_nodes=None): """Get children nodes.""" if input_name_to_nodes is None: input_name_to_nodes = self._input_name_to_nodes children = [] for output in node.output: if output in input_name_to_nodes: for child in input_name_to_nodes[output]: children.append(child) return children def get_initializer_share_num(self, name): """Get the number of shares of initializer.""" num = 0 if self.get_initializer(name) is None: return num for node in self.nodes(): if name in node.input: num += 1 return num def get_node(self, name): """Get a node by name.""" for node in self.model.graph.node: if node.name == name: return node return None def get_parent(self, node, idx, output_name_to_node=None): if output_name_to_node is None: output_name_to_node = self._output_name_to_node if len(node.input) <= idx: return None input = node.input[idx] return output_name_to_node.get(input, None) def get_parents(self, node, output_name_to_node=None): if output_name_to_node is None: output_name_to_node = self._output_name_to_node parents = [] for input in node.input: if input in output_name_to_node: parents.append(output_name_to_node[input]) return parents def get_node_by_weight(self, weight_name): """Get a node by its weight name.""" if len(self._input_name_to_nodes) == 0: self._input_name_to_nodes = self.input_name_to_nodes() nodes = self._input_name_to_nodes[weight_name] if len(nodes) == 1: return nodes[0] elif len(nodes) == 0: raise ValueError("{} is not used by any node in this model.".format(weight_name)) else: raise NotImplementedError("Models with shared weights is not supported.") def set_initializer(self, tensor, array, raw=False): """Update initializer.""" old_tensor = self.get_initializer(tensor) self.remove_initializer(old_tensor) dims = old_tensor.dims data_type = old_tensor.data_type new_tensor = ( onnx.helper.make_tensor(tensor, data_type, dims, array.flatten().tolist()) if not raw else onnx.helper.make_tensor(tensor, data_type, dims, array.tostring(), raw=raw) ) self.add_initializer(new_tensor) def get_siblings(self, node): """Get siblings nodes.""" siblings = [] for parent in self.get_parents(node): for child in self.get_children(parent): if child.name != node.name: siblings.append(child) return siblings def get_scale_zero(self, tensor): """Help function to get scale and zero_point.""" if not tensor.endswith("_quantized"): logger.debug("Find {} in the quantized graph is not quantized.".format(tensor)) return None, None if len(self._input_name_to_nodes) == 0: self._input_name_to_nodes = self.input_name_to_nodes() if len(self._output_name_to_node) == 0: self._output_name_to_node = self.output_name_to_node() def _searcher(tensor_name): """Search scale and zero point tensor recursively.""" node = self._input_name_to_nodes[tensor_name][0] parent = self._output_name_to_node[tensor_name] if tensor_name in self._output_name_to_node else None direct_int8 = ["Reshape", "Transpose", "Squeeze", "Unsqueeze", "MaxPool", "Pad", "Split"] if parent is not None and parent.op_type in direct_int8: fp32_tensor_name = ( parent.input[0] .replace("_quantized", "") .replace("_QuantizeLinear", "") .replace("_QuantizeInput", "") ) elif node.op_type in ["Gather"]: # pragma: no cover fp32_tensor_name = ( node.output[0] .replace("_quantized", "") .replace("_QuantizeLinear", "") .replace("_QuantizeInput", "") ) else: fp32_tensor_name = ( tensor_name.replace("_quantized", "").replace("_QuantizeLinear", "").replace("_QuantizeInput", "") ) scale = fp32_tensor_name + "_scale" scale_tensor = self.get_initializer(scale) zo = fp32_tensor_name + "_zero_point" zo_tensor = self.get_initializer(zo) if scale_tensor is None or zo_tensor is None: if parent is not None: scale_tensor, zo_tensor = _searcher(parent.input[0]) return scale_tensor, zo_tensor node = self._input_name_to_nodes[tensor][0] # TODO check if scale_tensor and zero_point is needed # for bias of qlinearconv, scale and zero_point is not needed if (node.op_type == "QLinearConv" and tensor == node.input[-1]) or ( node.op_type == "QGemm" and tensor == node.input[-3] ): return None, None else: scale_tensor, zo_tensor = _searcher(tensor) assert scale_tensor, "missing scale for tensor {}".format(tensor) assert zo_tensor, "missing zero point for tensor {}".format(tensor) return scale_tensor, zo_tensor @staticmethod def replace_node_input(node, old_input_name, new_input_name): """Replace input of a node.""" assert isinstance(old_input_name, str) and isinstance(new_input_name, str) for j in range(len(node.input)): if node.input[j] == old_input_name: node.input[j] = new_input_name @staticmethod def replace_node_output(node, old_output_name, new_output_name): """Replace output of a node.""" assert isinstance(old_output_name, str) and isinstance(new_output_name, str) for j in range(len(node.output)): if node.output[j] == old_output_name: node.output[j] = new_output_name def replace_input_of_all_nodes(self, old_input_name, new_input_name, white_optype=[], black_optype=[]): """Replace inputs of all nodes.""" if len(white_optype) > 0: for node in self.model.graph.node: if node.op_type in white_optype: ONNXModel.replace_node_input(node, old_input_name, new_input_name) else: for node in self.model.graph.node: if node.op_type not in black_optype: ONNXModel.replace_node_input(node, old_input_name, new_input_name) def replace_output_of_all_nodes(self, old_output_name, new_output_name, white_optype=[], black_optype=[]): """Replace outputs of all nodes.""" if len(white_optype) > 0: for node in self.model.graph.node: if node.op_type in white_optype: ONNXModel.replace_node_output(node, old_output_name, new_output_name) else: for node in self.model.graph.node: if node.op_type not in black_optype: ONNXModel.replace_node_output(node, old_output_name, new_output_name) def remove_unused_nodes(self): """Remove unused nodes.""" unused_nodes = [] for node in self.model.graph.node: # remove constant if node.op_type == "Constant": tensor = node.attribute[0].t tensor.name = node.output[0] self.add_initializer(tensor) unused_nodes.append(node) # remove identity if node.op_type == "Identity": tensor = self.get_initializer(node.input[0]) if tensor is not None: new_tensor = copy.deepcopy(tensor) new_tensor.name = node.output[0] unused_nodes.append(node) self.add_initializer(new_tensor) self.remove_nodes(unused_nodes) if len(self._input_name_to_nodes) == 0: self._input_name_to_nodes = self.input_name_to_nodes() if len(self._output_name_to_node) == 0: self._output_name_to_node = self.output_name_to_node() unvalid_nodes = [ i for i in self.model.graph.node if all(out not in self._input_name_to_nodes and out not in self.output() for out in i.output) ] while len(unvalid_nodes) > 0: self.remove_nodes(unvalid_nodes) self._input_name_to_nodes = self.input_name_to_nodes() unvalid_nodes = [ i for i in self.model.graph.node if all([out not in self._input_name_to_nodes and out not in self.output() for out in i.output]) ] ununsed_weights = [] for w in self.model.graph.initializer: if w.name not in self._input_name_to_nodes and w.name not in self.output(): ununsed_weights.append(w) # Remove from graph.input for graph_input in self.graph().input: if graph_input.name == w.name: self.graph().input.remove(graph_input) self.remove_initializers(ununsed_weights) self.update() self.topological_sort() def topological_sort(self, enable_subgraph=False): """Topological sort the model.""" if not enable_subgraph: input_name_to_nodes = {} output_name_to_node = {} for node in self.model.graph.node: for input_name in node.input: if len(input_name.strip()) != 0: if input_name not in input_name_to_nodes: input_name_to_nodes[input_name] = [node] else: input_name_to_nodes[input_name].append(node) for output_name in node.output: if len(output_name.strip()) != 0: output_name_to_node[output_name] = node else: # pragma: no cover if len(self._input_name_to_nodes) == 0: self._input_name_to_nodes = self.input_name_to_nodes() if len(self._output_name_to_node) == 0: self._output_name_to_node = self.output_name_to_node() input_name_to_nodes = self._input_name_to_nodes output_name_to_node = self._output_name_to_node all_nodes = {} q = collections.deque() wait = collections.deque() for inp in self.model.graph.input: q.extend(input_name_to_nodes[inp.name]) for n in self.model.graph.node: if all([i not in output_name_to_node and i not in self.input() for i in n.input]): q.append(n) while q: n = q.popleft() if not all([output_name_to_node[i].name in all_nodes for i in n.input if i in output_name_to_node]): if n not in wait: wait.append(n) continue all_nodes[n.name] = n for out in n.output: if out in input_name_to_nodes: q.extend([i for i in input_name_to_nodes[out] if i.name not in all_nodes and i not in q]) if len(q) == 0 and len(wait) != 0: q = copy.deepcopy(wait) wait.clear() nodes = [i[1] for i in all_nodes.items()] assert len(list(set([n.name for n in nodes]))) == len(list(set([n.name for n in self.model.graph.node]))) self.model.graph.ClearField("node") self.model.graph.node.extend(nodes) def add_tensors_to_outputs(self, tensor_names): """Add the tensors to the model outputs to gets their values. Args: tensor_names: The names of tensors to be dumped. """ added_outputs = [] for tensor in tensor_names: if tensor not in self.output(): added_tensor = onnx.helper.ValueInfoProto() added_tensor.name = tensor added_outputs.append(added_tensor) self.model.graph.output.extend(added_outputs) # pylint: disable=no-member def remove_tensors_from_outputs(self, tensor_names): """Remove the tensors from the model outputs. Args: tensor_names: The names of tensors to be removed. """ removed_outputs = [] for tensor in tensor_names: if tensor in self.output(): removed_outputs.append(self.model.graph.output[self.output().index(tensor)]) for output in removed_outputs: self.model.graph.output.remove(output) def match_first_parent(self, node, parent_op_type, output_name_to_node_dict, exclude=[]): """Find parent node based on constraints on op_type. Args: node (str): current node name. parent_op_type (str): constraint of parent node op_type. output_name_to_node (dict): dictionary with output name as key, and node as value. exclude (list): list of nodes that are excluded (not allowed to match as parent). Returns: parent: The matched parent node. None if not found. index: The input index of matched parent node. None if not found. """ for i, input in enumerate(node.input): if input in output_name_to_node_dict: parent = output_name_to_node_dict[input] if parent.op_type == parent_op_type and parent not in exclude: return parent, i return None, None def match_parent( self, node, parent_op_type, input_index=None, output_name_to_node_dict=None, exclude=[], return_indice=None, ): """Find parent node based on constraints on op_type and index. Args: node (str): current node name. parent_op_type (str): constraint of parent node op_type. input_index (int or None): only check the parent given input index of current node. output_name_to_node (dict): dictionary with output name as key, and node as value. exclude (list): list of nodes that are excluded (not allowed to match as parent). return_indice (list): a list to append the input index when input_index is None. Returns: parent: The matched parent node. """ assert node is not None assert input_index is None or input_index >= 0 if output_name_to_node_dict is None: if len(self._output_name_to_node) == 0: self._output_name_to_node = self.output_name_to_node() output_name_to_node_dict = self._output_name_to_node if input_index is None: parent, index = self.match_first_parent(node, parent_op_type, output_name_to_node_dict, exclude) if return_indice is not None: return_indice.append(index) return parent if input_index >= len(node.input): return None parent = self.get_parent(node, input_index, output_name_to_node_dict) if parent is not None and parent.op_type == parent_op_type and parent not in exclude: return parent return None def match_parent_path( self, node, parent_op_types, parent_input_index, output_name_to_node_dict=None, return_indice=None, ): """Find a sequence of input edges based on constraints on parent op_type and index. Args: node (str): current node name. parent_op_types (str): constraint of parent node op_type of each input edge. parent_input_index (list): constraint of input index of each input edge. None means no constraint. output_name_to_node (dict): dictionary with output name as key, and node as value. return_indice (list): a list to append the input index when there is no constraint on input index of an edge. Returns: parents: a list of matched parent node. """ assert len(parent_input_index) == len(parent_op_types) if output_name_to_node_dict is None: if len(self._output_name_to_node) == 0: self._output_name_to_node = self.output_name_to_node() output_name_to_node_dict = self._output_name_to_node current_node = node matched_parents = [] for i, op_type in enumerate(parent_op_types): matched_parent = self.match_parent( current_node, op_type, parent_input_index[i], output_name_to_node_dict, exclude=[], return_indice=return_indice, ) if matched_parent is None: return None matched_parents.append(matched_parent) current_node = matched_parent return matched_parents def is_smoothquant_model(self): """Check the model is smooth quantized or not. Returns: bool: the model is smooth quantized or not. """ for init in self.model.graph.initializer: if "_smooth_scale" in init.name: return True return False # below functions are used for layer-wise def find_split_node_for_layer_wise_quantization(self): """Find split node for layer wise quantization.""" # find split nodes of decoder blocks # embed -> decoder.0 -(split_node)-> ... -(split_node)-> decoder.n -(split_node)-> norm -> head # after split: embed -> decoder.0, # decoder.1, # decoder.2, # ..., # decoder.n, # norm -> head start_nodes = [] for node in self.model.graph.node: start_node, qkv_nodes_list = None, None if node.op_type == "SkipLayerNormalization": start_node = node qkv_nodes_list = [ self.match_parent_path( start_node, ["MatMul", "Reshape", "Transpose", "Reshape", "MatMul"], [None, 0, 0, 0, 0], ), self.match_parent_path( start_node, ["Add", "MatMul", "Reshape", "Transpose", "MatMul"], [1, 1, 0, 0, 0], ), ] if node.op_type == "Add": start_node = node qkv_nodes_list = [ # match base attention structure self.match_parent_path( start_node, ["Add", "MatMul", "Reshape", "Transpose", "MatMul"], [0, None, 0, 0, 0], ), self.match_parent_path( start_node, ["Add", "MatMul", "Reshape", "Transpose", "MatMul"], [1, None, 0, 0, 0] ), # match gpt attention no past structure self.match_parent_path( start_node, ["Reshape", "Gemm", "Reshape", "Reshape", "Transpose", "MatMul"], [None, 0, 0, 0, 0, 0], output_name_to_node_dict=self._output_name_to_node, return_indice=[], ), # match bart attention structure self.match_parent_path( start_node, ["Add", "MatMul", "Reshape", "Transpose", "Reshape", "MatMul"], [0, None, 0, 0, 0, 0], ), self.match_parent_path( start_node, ["Add", "MatMul", "Reshape", "Transpose", "Reshape", "MatMul"], [1, None, 0, 0, 0, 0], ), self.match_parent_path( start_node, ["MatMul", "Mul", "MatMul", "Mul", "Div", "Add"], [None, 0, None, 0, None, 0], ), self.match_parent_path( start_node, ["MatMul", "Mul", "MatMul", "SimplifiedLayerNormalization", "Add"], [None, 0, None, 0, 0], ), ] if qkv_nodes_list is not None and any(qkv_nodes_list): start_nodes.append(start_node) # can't find qkv nodes with above patterns, use Softmax nodes to split model if len(start_nodes) == 0: for node in self.model.graph.node: if node.op_type == "Softmax": start_nodes.append(node) return start_nodes def find_split_nodes(self): """Find split nodes for layer-wise quantization.""" self.remove_unused_nodes() split_nodes = self.find_split_node_for_layer_wise_quantization() return split_nodes def _infer_tensor_dtype(self): """Infer the elem_type of tensors.""" initializers = dict([(i.name, i.data_type) for i in self.model.graph.initializer]) inputs = dict([(i.name, i.type.tensor_type.elem_type) for i in self.model.graph.input]) value_info = dict([(i.name, i.type.tensor_type.elem_type) for i in self.model.graph.value_info]) outputs = dict([(i.name, i.type.tensor_type.elem_type) for i in self.model.graph.output]) for node in self.model.graph.node: if node.output[0] in value_info: continue elem_type = None if node.op_type in ["And", "Equal", "Greater", "GreaterOrEqual", "Less", "LessOrEqual", "Or", "Xor"]: elem_type = onnx.TensorProto.BOOL elif node.op_type in ["ArgMax", "ArgMin", "NonZero", "Shape"]: elem_type = onnx.TensorProto.INT64 elif node.op_type == "Cast" and len(node.attribute) > 0: elem_type = node.attribute[0].i elif node.op_type in ["Constant", "ConstantOfShape"] and len(node.attribute) > 0: elem_type = node.attribute[0].t.data_type elif len(node.input) >= 2: for inp in node.input[:2]: if inp in initializers and initializers[inp] != onnx.TensorProto.INT64: elem_type = initializers[inp] break # output elem_type aligns with input if elem_type is None and len(node.input) > 0: inp = node.input[0] if inp in value_info: elem_type = value_info[inp] elif inp in inputs: elem_type = inputs[inp] elif inp in outputs: elem_type = outputs[inp] if elem_type is not None: if node.op_type in ["Split", "Slice"]: for out in node.output: value_info.update({out: elem_type}) else: value_info.update({node.output[0]: elem_type}) return value_info def _build_input_output_tensor(self, tensor_name, value_info): if tensor_name in self.input(): return self.model.graph.input[self.input().index(tensor_name)] if tensor_name in self.output(): return self.model.graph.output[self.output().index(tensor_name)] tensor_type = value_info.get(tensor_name, onnx.TensorProto.FLOAT) return onnx.helper.make_tensor_value_info(tensor_name, tensor_type, None) def split_model_with_node( self, split_node_name, path_of_model_to_split, save_both_split_models=True, save_path=None ): """Split model into two parts at a given node. Args: split_node_name (str): name of the node where the model is split at> path_of_model_to_split (str): path of model to be split. save_both_split_models (bool): whether to save the two split models. False means only save the first split model. True means save both the two split models. Default id True. save_path (str): path to save split models. None means using self.model_path Returns: tuple: the first split model, the second split model """ # origin model : ... -> node_1 -> split_node -> node_2 -> ... # split model 1: ... -> node_1 -> split_node # split model 2: node_2 -> ... # infer elem_type of tensors to make sure layer-wise quant run successfully value_info = self._infer_tensor_dtype() split_model_part_1 = onnx.ModelProto() split_model_part_1.CopyFrom(self.model) split_model_part_1.graph.ClearField("node") split_model_part_2 = onnx.ModelProto() split_model_part_2.CopyFrom(self.model) split_model_part_2.graph.ClearField("node") split_node = None nodes = [] for node in self.model.graph.node: nodes.append(node) if node.name == split_node_name: split_node = node break assert len(split_node.output) == 1, ( "Only support split at node with 1 output tensor, while " "current split node {} has {} output tensors".format(split_node_name, len(split_node.output)) ) split_tensor_name = split_node.output[0] split_tensor = self._build_input_output_tensor(split_tensor_name, value_info) split_model_part_1.graph.node.extend(nodes) split_model_part_1.graph.output.append(split_tensor) split_model_part_1 = ONNXModel(split_model_part_1, ignore_warning=True) # remove isolated graphs which are not related to the split_node output_name_to_node = split_model_part_1.output_name_to_node() valid_nodes = [split_node] while len(valid_nodes) > 0: node = valid_nodes.pop(0) for inp in node.input: if inp in output_name_to_node: valid_nodes.append(output_name_to_node[inp]) if node in nodes: nodes.remove(node) split_model_part_1.remove_nodes(nodes) for node in self.model.graph.node: if node not in split_model_part_1.nodes(): split_model_part_2.graph.node.append(node) split_model_part_2.graph.input.append(split_tensor) split_model_part_2 = ONNXModel(split_model_part_2, ignore_warning=True) # remove unused input & output split_model_part_1._remove_unused_input_output() split_model_part_2._remove_unused_input_output() insert_output_for_model_1 = [] insert_input_for_model_2 = [] for output in split_model_part_1._output_name_to_node.keys(): if output in split_model_part_2._input_name_to_nodes.keys(): output_tensor = self._build_input_output_tensor(output, value_info) if output_tensor not in split_model_part_1.model.graph.output: insert_output_for_model_1.append(output_tensor) if output_tensor not in split_model_part_2.model.graph.input: insert_input_for_model_2.append(output_tensor) # insert model 1 output for output in insert_output_for_model_1: split_model_part_1.model.graph.output.append(output) # insert model 2 input for input in insert_input_for_model_2: split_model_part_2.model.graph.input.append(input) # remove unused init split_model_part_1.remove_unused_init() split_model_part_2.remove_unused_init() split_model_part_1.update() split_model_part_2.update() dir_of_model_to_split = os.path.dirname(path_of_model_to_split) split_model_part_1.load_model_initializer_by_tensor(dir_of_model_to_split) split_model_part_1_path = ( os.path.join(save_path, "split_model_part_1.onnx") if save_path is not None else os.path.join(dir_of_model_to_split, "split_model_part_1.onnx") ) split_model_part_1.model_path = split_model_part_1_path split_model_part_1._save_split_model(split_model_part_1_path) split_model_part_1.check_is_large_model() logger.debug("save split model part 1 to {} for layer wise quantization".format(split_model_part_1_path)) if save_both_split_models: split_model_part_2.load_model_initializer_by_tensor(dir_of_model_to_split) split_model_part_2_path = ( os.path.join(save_path, "split_model_part_2.onnx") if save_path is not None else os.path.join(dir_of_model_to_split, "split_model_part_2.onnx") ) split_model_part_2.model_path = split_model_part_2_path split_model_part_2._save_split_model(split_model_part_2_path) split_model_part_2.check_is_large_model() logger.debug("save split model part 2 to {} for layer wise quantization".format(split_model_part_2_path)) return split_model_part_1, split_model_part_2 else: return split_model_part_1, split_model_part_2 def _save_split_model(self, save_path): """Save split model as external data for layer wise quantization. Args: save_path (str): the path to save the split model """ if os.path.exists(save_path + "_data"): os.remove(save_path + "_data") self._model_path = save_path onnx.save_model( self.model, save_path, save_as_external_data=True, all_tensors_to_one_file=True, location=os.path.basename(save_path) + "_data", size_threshold=1024, convert_attribute=False, ) def _remove_unused_input_output(self): """Remove unused input & output for split model.""" remove_outputs = [] remove_inputs = [] input_name_to_nodes = self.input_name_to_nodes() output_name_to_node = self.output_name_to_node() for output in self.model.graph.output: if output.name not in output_name_to_node.keys(): remove_outputs.append(output) for input in self.model.graph.input: if input.name not in input_name_to_nodes.keys(): remove_inputs.append(input) for output in remove_outputs: self.model.graph.output.remove(output) for input in remove_inputs: self.model.graph.input.remove(input) def remove_unused_init(self): """Remove unused init.""" remov_inits = [] if len(self._input_name_to_nodes) == 0: self._input_name_to_nodes = self.input_name_to_nodes() for init in self.model.graph.initializer: if init.name not in self._input_name_to_nodes.keys(): remov_inits.append(init) self.remove_initializers(remov_inits) def load_model_initializer_by_tensor(self, data_path=None): """Load model initializer by tensor. Args: data_path (str, optional): the directory of saved initializer. Defaults to None. """ if data_path is None: data_path = os.path.dirname(self._model_path) for init in self.model.graph.initializer: if init.HasField("data_location") and init.data_location == onnx.TensorProto.EXTERNAL: onnx.external_data_helper.load_external_data_for_tensor(init, data_path) def write_external_data_to_new_location(self, external_data_location="external.data", overwrite=False): """Write external data of merged quantized model to new location to save memory. Args: external_data_location (str, optional): external data location of merged quantized model. Defaults to "external.data". overwrite (bool, optional): if True, remove existed externa data. Defaults to False. """ if overwrite and os.path.exists(os.path.join(os.path.dirname(self._model_path), external_data_location)): os.remove(os.path.join(os.path.dirname(self._model_path), external_data_location)) self.load_model_initializer_by_tensor() onnx.external_data_helper.convert_model_to_external_data(self.model, location=external_data_location) # TODO : if init is already saved, skip write it onnx.external_data_helper.write_external_data_tensors(self.model, filepath=os.path.dirname(self._model_path)) def merge_split_models(self, to_merge_model): """Merge two split model into final model.""" to_merge_model.write_external_data_to_new_location() self.add_nodes([node for node in to_merge_model.nodes()]) self.add_initializers([init for init in to_merge_model.initializer()]) self.update() # add new output for output in to_merge_model.graph().output: if output.name not in self.output(): self.model.graph.output.append(output) # remove unused output remove_output = [] for output in self.model.graph.output: if output.name in to_merge_model.input(): remove_output.append(output) for output in remove_output: self.model.graph.output.remove(output) # add new input for input in to_merge_model.graph().input: if ( input.name not in self.input() and input.name not in self.output() and input.name not in self._output_name_to_node.keys() ): self.model.graph.input.append(input) def re_org_output(self, origin_output): """Re-org output of merged model for layer-wise quantization.""" outputs = {} tmp_remove = [] for output in self.model.graph.output: outputs[output.name] = output tmp_remove.append(output) for output in tmp_remove: self.model.graph.output.remove(output) for out_name in origin_output: self.model.graph.output.append(outputs[out_name]) ================================================ FILE: onnx_neural_compressor/utility.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib import logging import os import pathlib import subprocess import time import cpuinfo import numpy as np import onnx import onnxruntime as ort import prettytable as pt import psutil from onnx_neural_compressor import constants, logger from typing import Callable, Dict, List, Tuple, Union # isort: skip # Dictionary to store a mapping between algorithm names and corresponding algo implementation(function) algos_mapping: Dict[str, Callable] = {} ####################################################### #### Options ####################################################### def check_value(name, src, supported_type, supported_value=[]): """Check if the given object is the given supported type and in the given supported value. Example:: from onnx_neural_compressor import utility def datatype(self, datatype): if utility.check_value("datatype", datatype, list, ["fp32", "bf16", "uint8", "int8"]): self._datatype = datatype """ if isinstance(src, list) and any([not isinstance(i, supported_type) for i in src]): assert False, "Type of '{}' items should be {} but not {}".format( name, str(supported_type), [type(i) for i in src] ) elif not isinstance(src, list) and not isinstance(src, supported_type): assert False, "Type of '{}' should be {} but not {}".format(name, str(supported_type), type(src)) if len(supported_value) > 0: if isinstance(src, str) and src not in supported_value: assert False, "'{}' is not in supported '{}': {}. Skip setting it.".format(src, name, str(supported_value)) elif ( isinstance(src, list) and all([isinstance(i, str) for i in src]) and any([i not in supported_value for i in src]) ): assert False, "{} is not in supported '{}': {}. Skip setting it.".format(src, name, str(supported_value)) return True class Options: """Option Class for configs. This class is used for configuring global variables. The global variable options is created with this class. If you want to change global variables, you should use functions from onnx_neural_compressor.utility.py: set_random_seed(seed: int) Args: random_seed(int): Random seed used in neural compressor. Default value is 1978. Example:: from onnx_neural_compressor import set_random_seed set_random_seed(2022) """ def __init__(self, random_seed=1978): """Init an Option object.""" self.random_seed = random_seed @property def random_seed(self): """Get random seed.""" return self._random_seed @random_seed.setter def random_seed(self, random_seed): """Set random seed.""" if check_value("random_seed", random_seed, int): self._random_seed = random_seed options = Options() def singleton(cls): """Singleton decorator.""" instances = {} def _singleton(*args, **kw): """Create a singleton object.""" if cls not in instances: instances[cls] = cls(*args, **kw) return instances[cls] return _singleton class Statistics: """The statistics printer.""" def __init__(self, data, header, field_names, output_handle=logger.info): """Init a Statistics object. Args: data: The statistics data header: The table header field_names: The field names output_handle: The output logging method """ self.field_names = field_names self.header = header self.data = data self.output_handle = output_handle self.tb = pt.PrettyTable(min_table_width=40) def print_stat(self): """Print the statistics.""" valid_field_names = [] for index, value in enumerate(self.field_names): if index < 2: valid_field_names.append(value) continue if any(i[index] for i in self.data): valid_field_names.append(value) self.tb.field_names = valid_field_names for i in self.data: tmp_data = [] for index, value in enumerate(i): if self.field_names[index] in valid_field_names: tmp_data.append(value) if any(tmp_data[1:]): self.tb.add_row(tmp_data) lines = self.tb.get_string().split("\n") self.output_handle("|" + self.header.center(len(lines[0]) - 2, "*") + "|") for i in lines: self.output_handle(i) class LazyImport(object): """Lazy import python module till use.""" def __init__(self, module_name): """Init LazyImport object. Args: module_name (string): The name of module imported later """ self.module_name = module_name self.module = None def __getattr__(self, name): """Get the attributes of the module by name.""" try: self.module = importlib.import_module(self.module_name) mod = getattr(self.module, name) except: spec = importlib.util.find_spec(str(self.module_name + "." + name)) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def __call__(self, *args, **kwargs): """Call the function in that module.""" function_name = self.module_name.split(".")[-1] module_name = self.module_name.split(f".{function_name}")[0] self.module = importlib.import_module(module_name) function = getattr(self.module, function_name) return function(*args, **kwargs) @singleton class CpuInfo(object): """CPU info collection.""" def __init__(self): """Get whether the cpu numerical format is bf16, the number of sockets, cores and cores per socket.""" self._bf16 = False self._vnni = False info = cpuinfo.get_cpu_info() if "arch" in info and "X86" in info["arch"]: cpuid = cpuinfo.CPUID() max_extension_support = cpuid.get_max_extension_support() if max_extension_support >= 7: ecx = cpuid._run_asm( b"\x31\xC9", # xor ecx, ecx b"\xB8\x07\x00\x00\x00" b"\x0f\xa2" b"\x89\xC8" b"\xC3", # mov eax, 7 # cpuid # mov ax, cx # ret ) self._vnni = bool(ecx & (1 << 11)) eax = cpuid._run_asm( b"\xB9\x01\x00\x00\x00", # mov ecx, 1 b"\xB8\x07\x00\x00\x00" b"\x0f\xa2" b"\xC3", # mov eax, 7 # cpuid # ret ) self._bf16 = bool(eax & (1 << 5)) # TODO: The implementation will be refined in the future. # https://github.com/intel/neural-compressor/tree/detect_sockets if "arch" in info and "ARM" in info["arch"]: # pragma: no cover self._sockets = 1 else: self._sockets = self.get_number_of_sockets() self._cores = psutil.cpu_count(logical=False) self._cores_per_socket = int(self._cores / self._sockets) @property def bf16(self): """Get whether it is bf16.""" return self._bf16 @property def vnni(self): """Get whether it is vnni.""" return self._vnni @property def cores_per_socket(self): """Get the cores per socket.""" return self._cores_per_socket def get_number_of_sockets(self) -> int: """Get number of sockets in platform.""" cmd = "cat /proc/cpuinfo | grep 'physical id' | sort -u | wc -l" if psutil.WINDOWS: cmd = r'wmic cpu get DeviceID | C:\Windows\System32\find.exe /C "CPU"' elif psutil.MACOS: # pragma: no cover cmd = "sysctl -n machdep.cpu.core_count" with subprocess.Popen( args=cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=False, ) as proc: proc.wait() if proc.stdout: for line in proc.stdout: return int(line.decode("utf-8", errors="ignore").strip()) return 0 def set_random_seed(seed: int): """Set the random seed in config.""" options.random_seed = seed def simple_progress_bar(total, i): """Progress bar for cases where tqdm can't be used.""" progress = i / total bar_length = 20 bar = "#" * int(bar_length * progress) spaces = " " * (bar_length - len(bar)) percentage = progress * 100 print(f"\rProgress: [{bar}{spaces}] {percentage:.2f}%", end="") def register_algo(name): """Decorator function to register algorithms in the algos_mapping dictionary. Usage example: @register_algo(name=example_algo) def example_algo(model: Union[onnx.ModelProto, pathlib.Path, str], quant_config: RTNConfig) -> onnx.ModelProto: ... Args: name (str): The name under which the algorithm function will be registered. Returns: decorator: The decorator function to be used with algorithm functions. """ def decorator(algo_func): algos_mapping[name] = algo_func return algo_func return decorator def auto_detect_ep(): eps = ort.get_available_providers() if "DnnlExecutionProvider" in eps: return "DnnlExecutionProvider" elif "DmlExecutionProvider" in eps: return "DmlExecutionProvider" elif "CUDAExecutionProvider" in eps: return "CUDAExecutionProvider" else: return "CPUExecutionProvider" def trt_env_setup(model): """Set environment variable for Tensorrt Execution Provider.""" is_int8 = False for node in model.graph.node: if node.op_type in ["QuantizeLinear", "DequantizeLinear"]: is_int8 = True break if is_int8: os.environ["ORT_TENSORRT_INT8_ENABLE"] = "1" else: os.environ["ORT_TENSORRT_INT8_ENABLE"] = "0" ================================================ FILE: onnx_neural_compressor/version.py ================================================ # Copyright (c) 2021 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Neural Compressor: An open-source Python library supporting popular model compression techniques for ONNX models.""" __version__ = "1.0" ================================================ FILE: onnx_neural_compressor/algorithms/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: onnx_neural_compressor/algorithms/utility.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import struct import sys from importlib import util import numpy as np from packaging import version from onnx_neural_compressor import constants, utility if sys.version_info < (3, 11) and util.find_spec("onnxruntime_extensions"): # pragma: no cover import onnxruntime_extensions onnx = utility.LazyImport("onnx") ort = utility.LazyImport("onnxruntime") __producer__ = "onnx.quantize" __version__ = "0.1.0" onnx_domain = "ai.onnx" ms_domain = "com.microsoft" QUANT_OP_NAME_SUFFIX = "_quant" def attribute_to_kwarg(attribute): """Convert attribute to kwarg format for use with onnx.helper.make_node.""" attribute_mapping = { 1: attribute.f, 2: attribute.i, 3: attribute.s, 4: attribute.t, 5: attribute.g, 6: attribute.floats, 7: attribute.ints, 8: attribute.strings, 9: attribute.tensors, 10: attribute.graphs, } if attribute.type in attribute_mapping: value = attribute_mapping[attribute.type] else: # pragma: no cover raise ValueError( "attribute {} has no type specified " "or unsupported type {}.".format(attribute.name, attribute.type) ) return {attribute.name: value} ONNX_INT_TYPE_RANGE = { onnx.TensorProto.UINT8: (0, 255), onnx.TensorProto.INT8: (-128, 127), } ONNX_INT_TYPE_SYMMETRIC_RANGE = { onnx.TensorProto.INT8: (-127, 127), } ONNX_INT_TYPE_REDUCED_RANGE = { onnx.TensorProto.UINT8: (0, 127), onnx.TensorProto.INT8: (-64, 64), } ONNX_STR_TYPE_RANGE = { "int1": (-1, 0), "int2": (-2, 1), "int3": (-4, 3), "int4": (-8, 7), # onnx >= 1.16.0 defines TensorProto.INT4 "int5": (-16, 15), "int6": (-32, 31), "int7": (-64, 63), "int8": (-128, 127), "uint1": (0, 1), "uint2": (0, 3), "uint3": (0, 7), "uint4": (0, 15), # onnx >= 1.16.0 defines TensorProto.UINT4 "uint5": (0, 31), "uint6": (0, 63), "uint7": (0, 127), "uint8": (0, 255), } ONNX_TENSOR_TYPE = { "bfloat16": getattr(onnx.TensorProto, "BFLOAT16", 16), "float32": getattr(onnx.TensorProto, "FLOAT", 1), "float16": getattr(onnx.TensorProto, "FLOAT16", 10), "int4": getattr(onnx.TensorProto, "INT4", 22), "uint4": getattr(onnx.TensorProto, "UNT4", 21), "int8": getattr(onnx.TensorProto, "INT8", 3), "uint8": getattr(onnx.TensorProto, "UINT8", 2), } def _qType_to_np_type(qType): if isinstance(qType, int): return onnx.helper.tensor_dtype_to_np_dtype(qType) elif isinstance(qType, str) and "uint" in qType: return np.dtype("uint8") else: return np.dtype("int8") def find_by_name(name, item_list): """Helper function to find item by name in a list.""" items = [] for item in item_list: assert hasattr(item, "name"), "{} should have a 'name' attribute defined".format(item) if item.name == name: items.append(item) if len(items) > 0: return items[0] else: return None def get_qmin_qmax_for_qType(qType, reduce_range=False, sym=False): # noqa: N802 """Get qmin, qmax for qType. Args: qType (int or str): int for onnx defined type, str for onnx not defined type reduce_range (bool, optional): whether use 7 bit for 8bit quantization sym (bool, optional): quantization scheme. Defaults to False. """ if qType == onnx.TensorProto.FLOAT8E4M3FN: raise NotImplementedError("This function is not implemented for float 8 as not needed.") qrange = None if isinstance(qType, str): qrange = ONNX_STR_TYPE_RANGE.get(qType) elif reduce_range: qrange = ONNX_INT_TYPE_REDUCED_RANGE.get(qType) elif sym and qType in ONNX_INT_TYPE_SYMMETRIC_RANGE: qrange = ONNX_INT_TYPE_SYMMETRIC_RANGE[qType] else: qrange = ONNX_INT_TYPE_RANGE.get(qType) if not qrange: raise ValueError(f"Unexpected data type {qType} requested.") return qrange def quantize_nparray(dtype, arr, scale, zero_point, low=None, high=None): """Quantize numpy array.""" q_weight = np.empty_like(np.asarray(arr), dtype=np.asarray(scale).dtype) np.divide(arr, scale, out=q_weight) np.add(q_weight, zero_point, out=q_weight) np.round(q_weight, out=q_weight) if low is not None and high is not None: np.clip(q_weight, low, high, out=q_weight) return q_weight.astype(dtype) def quantize_data_per_channel(data, axis, qType, sym, reduce_range=False): """Quantize tensor per-channel.""" quantize_range = get_qmin_qmax_for_qType(qType, reduce_range, sym) rmin = None rmax = None for i in range(len(data.shape)): if i != axis: rmin = np.min(data, axis=i, keepdims=True) if rmin is None else np.min(rmin, axis=i, keepdims=True) rmax = np.max(data, axis=i, keepdims=True) if rmax is None else np.max(rmax, axis=i, keepdims=True) rmin = np.minimum(rmin, 0) rmax = np.maximum(rmax, 0) scale, zero_point = calculate_scale_zp(rmin, rmax, qType, sym, reduce_range) dtype = _qType_to_np_type(qType) quantized_data = quantize_nparray(dtype, data, scale, zero_point, low=quantize_range[0], high=quantize_range[1]) return rmin.reshape(-1, 1), rmax.reshape(-1, 1), zero_point.reshape(-1, 1), scale.reshape(-1, 1), quantized_data def dequantize_data_with_scale_zero(tensor_value, scale_value, zo_value): """Dequantize tensor with scale and zero point.""" return (tensor_value.astype(scale_value.dtype) - zo_value.astype(scale_value.dtype)) * scale_value def dequantize_data(tensor_value, scale_value, zo_value, axis=0): """Dequantize tensor.""" if not isinstance(scale_value, np.ndarray): return dequantize_data_with_scale_zero(tensor_value, scale_value, zo_value) else: channel_count = tensor_value.shape[axis] # TBD, default from axis 0 new_per_channel_tensor_values = [] for i in range(channel_count): per_channel_tensor_value = tensor_value.take(i, axis) per_channel_scale_value = scale_value.take(i) per_channel_zero_value = zo_value.take(i) new_per_channel_tensor_values.append( dequantize_data_with_scale_zero( per_channel_tensor_value, per_channel_scale_value, per_channel_zero_value ) ) # combine per_channel_data into one reshape_dims = list(tensor_value.shape) # deep copy reshape_dims[axis] = 1 # only one per channel for reshape new_tensor_value = new_per_channel_tensor_values[0].reshape(reshape_dims) for i in range(1, channel_count): new_per_channel_tensor_value = new_per_channel_tensor_values[i].reshape(reshape_dims) new_tensor_value = np.concatenate((new_tensor_value, new_per_channel_tensor_value), axis) return new_tensor_value def calculate_scale_zp(rmin, rmax, qType, sym, reduce_range=False): """Calculate scale and zero point.""" qmin, qmax = get_qmin_qmax_for_qType(qType, reduce_range, sym) dtype = _qType_to_np_type(qType) if isinstance(rmax, np.ndarray): if sym: max_range = np.maximum(abs(rmin), abs(rmax)) rmin = -max_range rmax = max_range scale = (rmax - rmin) / (qmax - qmin) scale[abs(scale) < np.finfo(rmax.dtype).tiny] = 1 zero_point = ( np.multiply(np.ones(rmax.shape), np.round((qmax + qmin) / 2.0)).astype(dtype) if sym else np.round(qmin - rmin / scale).astype(dtype) ) else: if sym: max_range = max(abs(rmin), abs(rmax)) scale = (float(max_range) * 2) / (qmax - qmin) if max_range > 0 else 1 else: scale = (float(rmax) - float(rmin)) / (qmax - qmin) if rmin != rmax else 1 zero_point = np.round((qmax + qmin) / 2.0).astype(dtype) if sym else np.round(qmin - rmin / scale).astype(dtype) return np.float32(scale), zero_point def quantize_data(data, qType, sym, reduce_range=False, ratio=1.0, axis=None): """Quantize data. To pack weights, we compute a linear transformation - when data type == uint8 mode, from [rmin, rmax] -> [0, 2^{b-1}] and - when data type == int8, from [-m , m] -> [-(2^{b-1}-1), 2^{b-1}-1] where m = max(abs(rmin), abs(rmax)) and add necessary intermediate nodes to transform quantized weight to full weight using the equation r = S(q-z), where r: real original value q: quantized value S: scale z: zero point Args: data (array): data to quantize qType (int): data type to quantize to. Supported types UINT8 and INT8 sym (bool): whether use sym quantization. reduce_range (bool): whether use 7 bit or not. Defaults to False ratio (float, optional): percentile of clip. Defaults to 1.0 axis (int, optional): process data along a specific axis. Default is None (process the whole data) """ quantize_range = get_qmin_qmax_for_qType(qType, reduce_range, sym) rmin = np.min(np.min(data), 0) if axis is None else np.min(data, axis=axis, keepdims=True) rmax = np.max(np.max(data), 0) if axis is None else np.max(data, axis=axis, keepdims=True) rmin *= ratio rmax *= ratio scale, zero_point = calculate_scale_zp(rmin, rmax, qType, sym, reduce_range) dtype = _qType_to_np_type(qType) quantized_data = quantize_nparray(dtype, data, scale, zero_point, low=quantize_range[0], high=quantize_range[1]) return rmin, rmax, zero_point, scale, quantized_data def qdq_data(data, qType, sym, reduce_range=False, ratio=1.0, axis=None): _, _, zero_point, scale, quantized_data = quantize_data(data, qType, sym, reduce_range, ratio, axis) return scale * (quantized_data - zero_point) def is_B_transposed(node): """Whether inuput B is transposed.""" transB = [attr for attr in node.attribute if attr.name == "transB"] if len(transB): return 0 < onnx.helper.get_attribute_value(transB[0]) return False def is_quantizable_type(data_type): return data_type in [onnx.TensorProto.FLOAT, onnx.TensorProto.FLOAT16, onnx.TensorProto.BFLOAT16] def _get_blob_size(group_size, has_zp): # pragma: no cover """Get blob_size. Args: group_size (int): how many elements share one scale/zp has_zp (bool): whether zero_point is None """ if version.Version(ort.__version__) > constants.ONNXRT1161_VERSION: blob_size = group_size // 2 elif has_zp: blob_size = group_size // 2 + 4 + 1 else: blob_size = group_size // 2 + 4 return blob_size def make_weight_only_dequant_node( node: onnx.NodeProto, weight_shape: tuple, block_size: int, num_bits: int, dtype: str, q_weight: np.array, scale: np.array, zero_point: np.array, axis: int = 1, ): """Build DequantizeLinear node. Args: node: original matmul node weight_shape (tuple): original weight shape block_size (int): how many elements share one scale/zp num_bits (int): num_bits dtype (str): use uint or int q_weight (array): quantized weight scale (array): scale zero_point (array): zero point axis (int): the axis of the dequantizing dimension of the input tensor Returns: weight_only_dequant_node: DequantizeLinear node for weight dequantization new_inits: initializers of the new node """ new_inits = [] input_names = [] kwargs = {"block_size": block_size, "axis": axis} q_weight = q_weight.reshape((weight_shape[-1], -1)).T if num_bits == 4: q_weight = ((q_weight[:, ::2] & 0xF | q_weight[:, 1::2] << 4) & 0xFF).astype("uint8") qtype = ONNX_TENSOR_TYPE.get(dtype + str(num_bits), None) if qtype is None: raise ValueError( "Unsupported qtype {}, only support {}".format(dtype + str(num_bits), list(ONNX_TENSOR_TYPE.keys())) ) q_weight_tensor = onnx.helper.make_tensor( name=node.input[1] + "_Q{}G{}".format(str(num_bits), str(block_size)), data_type=qtype, dims=weight_shape, vals=q_weight.flatten().tobytes(), raw=True, ) new_inits.append(q_weight_tensor) input_names.append(q_weight_tensor.name) scale = scale.reshape((weight_shape[-1], -1)).T scale_tensor = onnx.helper.make_tensor( name=node.input[1] + "_scale", data_type=onnx.helper.np_dtype_to_tensor_dtype(scale.dtype), dims=scale.shape, vals=scale.tobytes(), raw=True, ) input_names.append(scale_tensor.name) new_inits.append(scale_tensor) # build zero_point tensor zero_point = zero_point.reshape((weight_shape[-1], -1)).T if num_bits == 4: zero_point = ((zero_point[:, ::2] & 0xF | zero_point[:, 1::2] << 4) & 0xFF).astype("uint8") zp_tensor = onnx.helper.make_tensor( name=node.input[1] + "_zp", data_type=qtype, dims=scale.shape, vals=zero_point.flatten().tobytes(), raw=True, ) input_names.append(zp_tensor.name) new_inits.append(zp_tensor) dequant_node = onnx.helper.make_node( "DequantizeLinear", inputs=input_names, outputs=[q_weight_tensor.name + "_dequant"], name=node.name + "_woq_dequant", **kwargs, ) node.input[1] = dequant_node.output[0] return dequant_node, new_inits def make_matmul_weight_only_node( node: onnx.NodeProto, weight_shape: tuple, num_bits: int, group_size: int, k_blocks: int, q_weight: np.array, scale: np.array, zero_point: np.array, accuracy_level: int = 0, ): """Build MatMulFpQ4/MatMulNBits node. Args: node (onnx.NodeProto): original matmul node weight_shape (tuple): original weight shape num_bits (int): number of bits used to represent weights. group_size (int): how many elements share one scale/zp k_blocks (int): block number q_weight (np.array): quantized weight scale (np.array): scale zero_point (np.array): zero point accuracy_level (int, optional): accuracy level. Support 0 (unset), 1(fp32 compute type of jblas kernel), 2 (fp16 compute type of jblas kernel), 3 (bf16 compute type of jblas kernel), 4 (int8 compute type of jblas kernel) Defaults to 0. Returns: matmul_weight_only_node: MatMulFpQ4 or MatMulNBits node new_inits: initializers of the new node """ blob_size = _get_blob_size(group_size, zero_point is not None) packed = np.zeros((q_weight.shape[0], blob_size), dtype="uint8") q_weight_name = node.input[1] + "_Q{}G{}".format(str(num_bits), str(group_size)) input_names = [node.input[0], q_weight_name] new_inits = [] kwargs = {} if version.Version(ort.__version__) > constants.ONNXRT1161_VERSION: op_type = "MatMulNBits" # pack quantized weight q_weight_pairs = q_weight[:, ::2] | q_weight[:, 1::2] << 4 packed[:, :] = q_weight_pairs[:, :blob_size] packed = np.reshape(packed, (-1, k_blocks, blob_size)) # build scale tensor scale = np.reshape(scale, (-1, k_blocks)) scale_tensor = onnx.helper.make_tensor( name=node.input[1] + "_scale", data_type=onnx.helper.np_dtype_to_tensor_dtype(scale.dtype), dims=scale.shape, vals=scale.tobytes(), raw=True, ) input_names.append(scale_tensor.name) new_inits.append(scale_tensor) # build zero_point tensor if zero_point is not None: if num_bits > 4: packed_zp = np.reshape(zero_point, (1, -1)).astype("uint8") else: packed_zp = np.full((zero_point.shape[0] + 1) // 2, 136, dtype="uint8") # create an index array idx = np.arange(zero_point.shape[0] // k_blocks * k_blocks).reshape(-1) # separate odd and even indices even_idx = idx[::2] odd_idx = idx[1::2] # vectorized operation for even and odd indices packed_zp[even_idx // 2] = (packed_zp[even_idx // 2] & 0xF0) | zero_point[even_idx].ravel() packed_zp[odd_idx // 2] = (packed_zp[odd_idx // 2] & 0x0F) | (zero_point[odd_idx].ravel() << 4) zp_tensor = onnx.helper.make_tensor( name=node.input[1] + "_zp", data_type=2, dims=packed_zp.shape, vals=packed_zp.tobytes(), raw=True ) input_names.append(zp_tensor.name) new_inits.append(zp_tensor) # set kwargs kwargs["K"] = weight_shape[0] kwargs["N"] = weight_shape[1] kwargs["bits"] = num_bits kwargs["block_size"] = group_size if accuracy_level > 0: # require onnxruntime > 1.16.3 kwargs["accuracy_level"] = accuracy_level else: # pragma: no cover offset = 5 if zero_point is not None else 4 op_type = "MatMulFpQ4" # pack quantized weight for i in range(q_weight.shape[0]): bf = struct.pack("f", scale[i]) packed[i][0] = bf[0] packed[i][1] = bf[1] packed[i][2] = bf[2] packed[i][3] = bf[3] if zero_point is not None: packed[i][4] = zero_point[i] packed[i][offset:] = np.bitwise_or( q_weight[i][: group_size // 2], np.left_shift(q_weight[i][group_size // 2 :], num_bits) ) packed = packed.reshape(-1) # build shape tensor shape_tensor = onnx.helper.make_tensor( name=node.input[1] + "_shape", data_type=7, dims=(2,), vals=np.array(weight_shape, dtype="int64") ) new_inits.append(shape_tensor) input_names.append(shape_tensor.name) # set kwargs kwargs["blk_quant_type"] = 1 if zero_point is not None else 0 q_weight_tensor = onnx.helper.make_tensor( name=q_weight_name, data_type=2, dims=packed.shape, vals=packed.tobytes(), raw=True, ) new_inits.append(q_weight_tensor) matmul_weight_only_node = onnx.helper.make_node( op_type, inputs=input_names, outputs=node.output, name=node.name + "_Q" + str(num_bits) if node.name else "_Q" + str(num_bits), domain="com.microsoft", **kwargs, ) return matmul_weight_only_node, new_inits def quant_matmul_weight_only( node, weight, dtype, num_bits, sym, group_size, ratio=1, quant_format=None, accuracy_level=0, ): new_nodes = [] new_inits = [] remove_nodes = [] org_w_shape = weight.shape # ic, oc group_size = group_size if group_size != -1 else org_w_shape[0] k_blocks = (org_w_shape[0] - 1) // group_size + 1 weight = pad_tensor(weight, group_size, k_blocks) if quant_format == 1: _, _, zp, scale, q_weight = quantize_data( weight.T.reshape((-1, group_size)), dtype + str(num_bits), sym, ratio=ratio, axis=1, ) dequant_node, inits = make_weight_only_dequant_node( node=node, weight_shape=org_w_shape, num_bits=num_bits, dtype=dtype, q_weight=q_weight, scale=scale.astype(weight.dtype), axis=0, block_size=group_size, zero_point=zp, ) new_nodes.append(dequant_node) new_inits.extend(inits) elif quant_format == 0: _, _, zp, scale, q_weight = quantize_data( weight.T.reshape((-1, group_size)), dtype + str(num_bits), sym, ratio=ratio, axis=1, ) q_matmul_node, inits = make_matmul_weight_only_node( node=node, weight_shape=org_w_shape, num_bits=num_bits, group_size=group_size, k_blocks=k_blocks, q_weight=q_weight, scale=scale.astype(weight.dtype), zero_point=zp if not sym else None, accuracy_level=accuracy_level, ) new_nodes.append(q_matmul_node) new_inits.extend(inits) remove_nodes.append(node) else: q_weight = qdq_data( weight.T.reshape((-1, group_size)), dtype + str(num_bits), sym, ratio=ratio, axis=1, ) q_weight = np.reshape(q_weight, (org_w_shape[1], -1)) q_weight = np.transpose(q_weight) q_weight = q_weight[: org_w_shape[0], :].astype(weight.dtype) q_weight_tensor = onnx.helper.make_tensor( name=node.input[1] + "_Q{}G{}".format(str(num_bits), str(group_size)), data_type=onnx.helper.np_dtype_to_tensor_dtype(q_weight.dtype), dims=weight.shape, vals=q_weight.tobytes(), raw=True, ) node.input[1] = q_weight_tensor.name new_inits.append(q_weight_tensor) return new_nodes, new_inits, remove_nodes def prepare_inputs(model, data_reader, providers): """Prepare inputs for weight only quantization. Args: model (ModelProto or onnx_model.ONNXModel): onnx model. data_reader (CalibrationDataReader): a calibration data reader. providers (list): providers to use. Returns: inputs: prepared inputs. so: session options """ so = ort.SessionOptions() if sys.version_info < (3, 11) and util.find_spec("onnxruntime_extensions"): # pragma: no cover so.register_custom_ops_library(onnxruntime_extensions.get_library_path()) if model.is_large_model: onnx.save_model( model.model, model.model_path + "_augment.onnx", save_as_external_data=True, all_tensors_to_one_file=True, convert_attribute=False, ) inputs_list = [] while True: inputs = data_reader.get_next() if not inputs: break inputs_list.append(inputs) return inputs_list, so def pad_tensor(weight, group_size, k_blocks): """Pad tensor rowi so that it can be is divisible by group_size. Args: weight (array): weight group_size (int): how many elements share one scale/zp k_blocks (int): the number of block Returns: weight: paded weight """ if group_size == -1: return weight org_w_shape = weight.shape padded_rows = k_blocks * group_size pad_len = padded_rows - org_w_shape[0] if pad_len > 0: weight = np.pad(weight, ((0, pad_len), (0, 0)), "constant") return weight def dump_woq_stats(model, quantize_config, white_list=["MatMul"]): res = {} dtype_set = set() for node in model.graph.node: if node.op_type in ["MatMulFpQ4", "MatMulNBits"]: optype = "MatMul" else: optype = node.op_type if optype not in white_list and optype != "DequantizeLinear": continue if optype not in res: res[optype] = {} dtype = "FP32" for inp in node.input: if re.match("^.*_Q\d*G\d*", inp): Q_position = re.search("_Q\d*", inp) full_position = re.search("_Q\d*G\d*", inp) dtype = "A32W{}G{}".format( inp[Q_position.start() + 2 : Q_position.end()], inp[Q_position.end() + 1 : full_position.end()], ) dtype_set.add(dtype) break res[optype][dtype] = res[optype].get(dtype, 0) + 1 dtype_list = list(dtype_set) for dtype in dtype_list: for optype in res.keys(): if dtype not in res[optype]: res[optype][dtype] = 0 # update stats format for dump. field_names = ["Op Type", "Total"] field_names.extend(dtype_list) output_data = [] for op_type in res.keys(): field_results = [op_type, sum(res[op_type].values())] field_results.extend([res[op_type][dtype] for dtype in dtype_list]) output_data.append(field_results) utility.Statistics(output_data, header="Mixed Precision Statistics", field_names=field_names).print_stat() def get_node_original_name(node) -> str: """Get the original name of the given node.""" node_name: str = node.name # TODO how to handle the unquantized node that has the `_quant` suffix, such as `conv_quant`? if node_name.endswith(QUANT_OP_NAME_SUFFIX): return node_name[: -len(QUANT_OP_NAME_SUFFIX)] else: # For unquantized nodes return node_name def split_shared_bias(model): """Split shared tensor.""" input_name_to_nodes = model.input_name_to_nodes() for input_name, node_list in input_name_to_nodes.items(): if len(node_list) > 1 and input_name in [i.name for i in model.model.graph.initializer]: for node in node_list[1:]: if node.op_type not in ["Conv", "FusedConv"]: continue if len(node.input) > 2 and node.input[2] == input_name: new_input_name = node.input[2] + "_nc_split_" + node.name new_input = onnx.helper.make_tensor( new_input_name, model.get_initializer(input_name).data_type, model.get_initializer(input_name).dims, model.get_initializer(input_name).raw_data, True, ) model.add_initializer(new_input) node.input[2] = new_input_name return model def remove_init_from_model_input(model): """Remove initializer from model input.""" inputs = model.model.graph.input name_to_input = {} for inp in inputs: name_to_input[inp.name] = inp for initializer in model.model.graph.initializer: if initializer.name in name_to_input: inputs.remove(name_to_input[initializer.name]) class QuantizedValue: """Represents a linearly quantized value (input/output/initializer).""" def __init__( self, name, new_quantized_name, scale_name, zero_point_name, axis=None, qType=1, ): """Initialization. Args: name (string): tensor name new_quantized_name (string): quantized tensor name scale_name (string): scale name zero_point_name (string): zero point name axis (int, optional): quantized axis. Defaults to None. qType (int, optional): quantized data type. Defaults to 1 (uint8). """ self.name = name self.q_name = new_quantized_name self.scale_name = scale_name self.zp_name = zero_point_name self.axis = axis self.qType = qType class QuantizedInitializer: """Represents a linearly quantized weight input from ONNX operators.""" def __init__( self, name, initializer, rmins, rmaxs, zero_points, scales, data=[], quantized_data=[], axis=None, qType=1, ): """Initialization. Args: name (string): initializer name initializer (onnx.onnx_ml_pb2.TensorProto): initializer rmins (list): list of min value rmaxs (list): list of max value zero_points (list): list of zero point scales (list): list of scale data (list, optional): array version of the initializer. Defaults to []. quantized_data (list, optional): quantized data. Defaults to []. axis (int, optional): quantized axis. Defaults to None. qType (int, optional): quantized data type. Defaults to 1 (uint8). """ self.name = name self.initializer = initializer # TensorProto initializer in ONNX graph self.rmins = rmins # List of minimum range for each axis self.rmaxs = rmaxs # List of maximum range for each axis # 1D tensor of zero points computed for each axis. scalar if axis is empty self.zero_points = zero_points self.scales = scales # 1D tensor of scales computed for each axis. scalar if axis is empty self.data = data # original data from initializer TensorProto self.quantized_data = quantized_data # weight-packed data from data # Scalar to specify which dimension in the initializer to weight pack. self.axis = axis # If empty, single zero point and scales computed from a single rmin and rmax self.qType = qType def dump_model_op_stats(model, quantize_config, fp32_op_list): qdq_ops = ["QuantizeLinear", "DequantizeLinear", "DynamicQuantizeLinear"] res = {} for op_type in fp32_op_list: res[op_type] = {"INT8": 0, "FP32": 0} for op_type in qdq_ops: res[op_type] = {"INT8": 0, "FP32": 0} for node in model.graph.node: if node.name.endswith("_quant"): if node.op_type.startswith("QLinear"): origin_op_type = node.op_type.split("QLinear")[-1] else: origin_op_type = node.op_type.split("Integer")[0] if origin_op_type in ["QAttention", "QGemm"]: origin_op_type = origin_op_type[1:] elif origin_op_type == "DynamicQuantizeLSTM": origin_op_type = "LSTM" elif origin_op_type == "QEmbedLayerNormalization": origin_op_type = "EmbedLayerNormalization" res[origin_op_type]["INT8"] += 1 elif node.op_type in qdq_ops: res[node.op_type]["INT8"] += 1 elif node.op_type in res: res[node.op_type]["FP32"] += 1 field_names = ["Op Type", "Total", "INT8", "FP32"] output_data = [ [ op_type, sum(res[op_type].values()), res[op_type]["INT8"], res[op_type]["FP32"], ] for op_type in res.keys() ] utility.Statistics(output_data, header="Quantization Statistics", field_names=field_names).print_stat() ================================================ FILE: onnx_neural_compressor/algorithms/layer_wise/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: onnx_neural_compressor/algorithms/layer_wise/core.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import os import pathlib import tempfile import onnx import onnxruntime as ort from onnx_neural_compressor import data_reader, logger, onnx_model from typing import Callable, List, Union # isort: skip def layer_wise_quant( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], quant_func: Callable, weight_config: dict, data_reader: data_reader.CalibrationDataReader = None, *args, **kwargs ) -> onnx_model.ONNXModel: """Quantize model layer by layer to save memory. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model. quant_func (Callable): quantization algo function. weight_config (dict): quantization config. data_reader (data_reader.CalibrationDataReader, optional): data_reader for calibration. Defaults to None. Returns: _type_: _description_ """ logger.warning( "Layer-wise quantization requires data_type info for some tensors. " "We will try to infer the data_type automatically if it doesn't exist." "You can use model with symbolic shape inference before layer-wise quantization as well like follows:\n" "import onnxruntime.tools.symbolic_shape_infer as symbolic_shape_infer\n" "model = onnx.load(your_model_path)\n" "out = symbolic_shape_infer.SymbolicShapeInference.infer_shapes(model, auto_merge=True)\n" "onnx.save_model(out, infer_shape_model_path, save_as_external_data=True)\n" ) if not isinstance(model, onnx_model.ONNXModel): model = onnx_model.ONNXModel(model, ignore_warning=True, load_external_data=False) origin_model = copy.deepcopy(model) tmp_file = tempfile.TemporaryDirectory() providers = kwargs.get("providers", ["CPUExecutionProvider"]) # get and check split nodes split_nodes = origin_model.find_split_nodes() if len(split_nodes) == 0: logger.error("Can't find split nodes for layer-wise quantization.") raise ValueError("Fail to run layer-wise quantization.") logger.info( "Will split model into {} parts to do layer-wise quantization".format( len([node.name for node in split_nodes]) + 1 ) ) logger.debug( "Will split model with these nodes for layer-wise quantization: {}".format([node.name for node in split_nodes]) ) split_idx = 1 model_to_split = [origin_model] quantized_model_merged = None require_data_reader = data_reader is not None if require_data_reader: lwq_data_reader = [data_reader] while len(model_to_split) != 0: # prepare model, node and data_reader for current split split_model = model_to_split.pop(0) split_node = split_nodes.pop(0) if require_data_reader: complete_data_reader = lwq_data_reader.pop(0) # if no remaining split nodes, it means this is the last split, and the two split models will be saved. save_both_split_models = True if len(split_nodes) == 0 else False # split model with given split node split_model_part_1, split_model_part_2 = split_model.split_model_with_node( split_node.name, model.model_path, save_both_split_models, save_path=tmp_file.name ) if not save_both_split_models: # append split_model_part_2 to do next split model_to_split.append(split_model_part_2) logger.info("Quantize split model {}".format(split_idx)) if require_data_reader: # process data_reader for current split and next split current_data_reader = _filter_data_reader_for_current_split_model( split_model_part_1.model, complete_data_reader ) # complete_data_reader contains split_model_part_1 output data complete_data_reader = _prepare_data_reader_for_next_split_model( split_model_part_1.model_path, [i.name for i in split_model_part_2.model.graph.input], complete_data_reader, providers, ) lwq_data_reader.append(complete_data_reader) # perform quantization split_model_part_1_quantized = quant_func( split_model_part_1, weight_config=weight_config, data_reader=current_data_reader, return_modelproto=False, **kwargs ) else: # perform quantization split_model_part_1_quantized = quant_func( split_model_part_1, weight_config=weight_config, return_modelproto=False, **kwargs ) # check split model is valid try: ort.InferenceSession(split_model_part_1_quantized.model_path, providers=providers) except Exception as e: logger.error( "Layer-wise quantized model {} can't be inferred correctly. " "Please check the raise exception".format(split_idx) ) raise e # merge split quantized model if quantized_model_merged is None: quantized_model_merged = split_model_part_1_quantized quantized_model_merged.write_external_data_to_new_location(overwrite=True) else: quantized_model_merged.merge_split_models(split_model_part_1_quantized) split_idx += 1 # if this is the last split, quantize the last split model if save_both_split_models: logger.info("Quantize split model {}".format(split_idx)) # quantize split model if require_data_reader: # process data_reader for current split current_data_reader = lwq_data_reader.pop(0) current_data_reader = _filter_data_reader_for_current_split_model( split_model_part_2.model, complete_data_reader ) # perform quantization split_model_part_2_quantized = quant_func( split_model_part_2, weight_config=weight_config, data_reader=current_data_reader, return_modelproto=False, **kwargs ) else: # perform quantization split_model_part_2_quantized = quant_func( split_model_part_2, weight_config=weight_config, return_modelproto=False, **kwargs ) # check split model is valid try: ort.InferenceSession(split_model_part_2_quantized.model_path, providers=providers) except Exception as e: logger.error( "Layer-wise quantized model {} can't be inferred correctly. " "Please check the raise exception".format(split_idx) ) raise e # merge split quantized model if quantized_model_merged is None: quantized_model_merged = split_model_part_2_quantized quantized_model_merged.write_external_data_to_new_location(overwrite=True) else: quantized_model_merged.merge_split_models(split_model_part_2_quantized) # reload external data to prevent external data file path errors onnx.external_data_helper.load_external_data_for_model( quantized_model_merged.model, os.path.dirname(quantized_model_merged.model_path) ) tmp_file.cleanup() return quantized_model_merged class DataReader(data_reader.CalibrationDataReader): """Data reader for layer-wise quantization.""" def __init__(self, data_list): self.data_list = data_list self.iter_next = iter(self.data_list) def get_next(self): return next(self.iter_next, None) def rewind(self): self.iter_next = iter(self.data_list) def _filter_data_reader_for_current_split_model( model: onnx.ModelProto, current_data_reader: data_reader.CalibrationDataReader, ): """Filter data reader to remove data that is not in model input. Args: model (onnx.ModelProto): onnx model. current_data_reader (data_reader.CalibrationDataReader): data reader of current split model. Returns: data_reader.CalibrationDataReader: filtered data reader. """ filter_inputs = [] input_names = [input.name for input in model.graph.input] current_data_reader.rewind() while True: inputs = current_data_reader.get_next() if not inputs: break filter_input = { input_name: input_tensor for input_name, input_tensor in inputs.items() if input_name in input_names } filter_inputs.append(filter_input) return DataReader(filter_inputs) def _prepare_data_reader_for_next_split_model( model_path: str, next_model_input_names: list, data_reader: data_reader.CalibrationDataReader, providers: List[str] = ["CPUExecutionProvider"], ): """Prepare data reader for next split model. Get data output of current split model and save for next split model. Args: model (str): path to onnx model. data_reader (data_reader.CalibrationDataReader): data reader providers (List[str], optional): providers to use. Defaults to ["CPUExecutionProvider"]. Returns: data_reader.CalibrationDataReader: data reader for next split model. """ data_reader.rewind() data_reader_for_next_split_model = [] session = ort.InferenceSession(model_path, providers=providers) output_names = [output.name for output in session.get_outputs()] input_names = [input.name for input in session.get_inputs()] while True: inputs = data_reader.get_next() if not inputs: break out = session.run(None, {name: inputs[name] for name in input_names}) filter_input = {name: value for name, value in zip(output_names, out)} for name, value in inputs.items(): if name in next_model_input_names and name not in filter_input: filter_input[name] = value data_reader_for_next_split_model.append(filter_input) return DataReader(data_reader_for_next_split_model) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/calibrate.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ------------------------------------------------------------------------- # Copyright (c) Microsoft, Intel Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- """Calibration for onnx models.""" import copy import logging import os import sys from importlib import util import numpy as np import onnx import onnxruntime from packaging import version from onnx_neural_compressor import logger, onnx_model from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant import calibrator if sys.version_info < (3, 11) and util.find_spec("onnxruntime_extensions"): import onnxruntime_extensions ONNX18_VERSION = version.Version("1.8.0") ORT112_VERSION = version.Version("1.12.0") class ONNXRTAugment: """Augment input model to dump tensor or for calibration.""" def __init__( self, model_wrapper, dataloader, dump_op_types, black_nodes=[], white_nodes=[], iterations=[], execution_provider="CPUExecutionProvider", reduce_range=False, **kwargs, ): """Initialization. Args: model_wrapper (Model): model to be augmented dataloader (object): user implemented object to read in and preprocess calibration dataset dump_op_types (list): operator types to be calibrated and quantized black_nodes (list, optional): operator names that should not be quantized. Defaults to []. white_nodes (list, optional): operator names that force to be quantized. Defaults to []. iterations (list, optional): tensor of which iteration will be collected. Defaults to []. execution_provider (list, optional): execution provider for onnxruntime. Defaults to 'CPUExecutionProvider'. reduce_range (bool, optional): use 7 bit or not. Defaults to False. """ self.model_wrapper = ( model_wrapper if isinstance(model_wrapper, onnx_model.ONNXModel) else onnx_model.ONNXModel(model_wrapper, load_external_data=True) ) self.model = self.model_wrapper.model ai_onnx_domain = [opset for opset in self.model.opset_import if not opset.domain or opset.domain == "ai.onnx"] self.opset_version = ai_onnx_domain[0].version self.dataloader = dataloader self.dump_op_types = dump_op_types self.black_nodes = black_nodes self.white_nodes = white_nodes self.augmented_model = None self.iterations = iterations self.execution_provider = execution_provider self.augment_nodes = [] self.dequantized_output = {} self.already_quantized = "DequantizeLinear" in [node.op_type for node in self.model.graph.node] self.dynamically_quantized = False self.ort_version = version.Version(onnxruntime.__version__) self.reduce_range = reduce_range def augment_graph(self): """Augment_graph. Adds nodes to all quantization_candidates op type nodes in model and ensures their outputs are stored as part of the graph output. Args: activation_only (bool, optional): whether to dump activation tensor only. Defaults to False. weight_only (bool, optional): whether to dump weight_only. Defaults to False. """ self.dequantized_output.clear() onnx_version = version.Version(onnx.__version__) if onnx_version < ONNX18_VERSION: logger.warning("Static quantization for NLP model is supported at onnx 1.8.0 and newer.") if self.already_quantized and any( [i.dims in [1, 2] for i in self.model_wrapper.initializer() if i.name.endswith("_scale")] ): if self.opset_version < 13 and self.ort_version >= ORT112_VERSION: logger.warning( "Please use onnxruntime < 1.12.0 or upgrade model opset " "version to 13 or higher to inspect per-channel quantized weight" ) model = copy.deepcopy(self.model) model_nodes_names = [node.name for node in model.graph.node] added_nodes = [] added_outputs = [] tensors_to_dump = set() for augment_node_type in self.augment_nodes: if augment_node_type not in ["DequantizeLinear"]: # pragma: no cover raise ValueError( "Unexpected augment_node {} only DequantizeLinear is supported".format(augment_node_type) ) if self.already_quantized: # mapping between fp32 node and int8 node new_white_nodes = [] for white_node in self.white_nodes: new_white_node = white_node + "_quant" assert new_white_node in model_nodes_names, "no quantized {} in the graph".format(white_node) new_white_nodes.append(new_white_node) self.white_nodes = new_white_nodes node_outputs = [] for node in model.graph.node: # pylint: disable=no-member node_outputs.extend(node.output) should_be_dump = ((node.op_type in self.dump_op_types) and (node.name not in self.black_nodes)) or ( node.name in self.white_nodes ) if should_be_dump: # add input tensors which should be dump for input in node.input: if len(input) != 0: # to prevent input is "" initializer_tensor = self.model_wrapper.get_initializer(input) if initializer_tensor is None: tensors_to_dump.add(input) # add output tensors which should be dump tensors_to_dump.update([output for output in node.output if len(output) != 0]) model_inputs = [i.name for i in model.graph.input] for tensor in tensors_to_dump: if tensor not in node_outputs and tensor not in model_inputs: continue if self.augment_nodes: for augment_node_type in self.augment_nodes: if augment_node_type in ["DequantizeLinear"]: # insert DequantizeLinear node as output if tensor.endswith("_scale") or tensor.endswith("_zero_point"): # pragma: no cover continue if not self.dynamically_quantized: tensor = ( tensor.replace("_QuantizeInput", "_quantized") if tensor.endswith("_QuantizeInput") else tensor ) else: tensor = ( tensor.replace("_output_quantized", "") if tensor.endswith("_output_quantized") else tensor ) augment_node_name = tensor + "_new_" + augment_node_type scale, zero_point = self.model_wrapper.get_scale_zero(tensor) if scale: # the tensor is in INT8 dtype nodes, output = self._dequantize(tensor, scale, zero_point) if output: added_nodes.extend(nodes) added_outputs.append( onnx.helper.make_tensor_value_info( output, onnx.TensorProto.FLOAT, () # pylint: disable=no-member ) ) # pylint: disable=no-member else: # the tensor is in FP32 dtype if tensor not in [t.name for t in model.graph.output]: added_tensor = onnx.helper.ValueInfoProto() added_tensor.name = tensor added_outputs.append(added_tensor) else: if tensor not in [t.name for t in model.graph.output]: added_tensor = onnx.helper.ValueInfoProto() added_tensor.name = tensor added_outputs.append(added_tensor) if self.augment_nodes: model.graph.node.extend(added_nodes) # pylint: disable=no-member model.graph.output.extend(added_outputs) # pylint: disable=no-member self.augmented_model = model if self.model_wrapper.is_large_model: # pragma: no cover onnx.save_model( model, self.model_wrapper.model_path + "_augment.onnx", save_as_external_data=True, all_tensors_to_one_file=True, convert_attribute=False, ) def get_activation_tensors_calib_range(self, q_config=None): """Get calib ranges of activation tensors. Args: q_config (dict, optional): quantization config. Defaults to None. Returns: dict: calib ranges """ # conduct inference session and get intermediate outputs so = onnxruntime.SessionOptions() so.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_DISABLE_ALL if sys.version_info < (3, 11) and util.find_spec("onnxruntime_extensions"): so.register_custom_ops_library(onnxruntime_extensions.get_library_path()) execution_provider = ( self.execution_provider if self.execution_provider != "TensorrtExecutionProvider" else "CUDAExecutionProvider" ) session = ( onnxruntime.InferenceSession(self.augmented_model.SerializeToString(), so, providers=[execution_provider]) if not self.model_wrapper.is_large_model else onnxruntime.InferenceSession( self.model_wrapper.model_path + "_augment.onnx", so, providers=[execution_provider] ) ) len_inputs = len(session.get_inputs()) inputs_names = [session.get_inputs()[i].name for i in range(len_inputs)] len_outputs = len(session.get_outputs()) outputs_names = [session.get_outputs()[i].name for i in range(len_outputs)] node_output_names = [ output.name if output.name not in self.dequantized_output else self.dequantized_output[output.name] for output in session.get_outputs() ] augment_model_wrapper = ( onnx_model.ONNXModel(self.augmented_model, load_external_data=False) if not self.model_wrapper.is_large_model else onnx_model.ONNXModel(self.model_wrapper.model_path + "_augment.onnx", load_external_data=False) ) input_name_to_nodes = augment_model_wrapper.input_name_to_nodes() output_name_to_node = augment_model_wrapper.output_name_to_node() name_to_node = {} for data_name in node_output_names: node = None if data_name in output_name_to_node: node = output_name_to_node[data_name] elif data_name in input_name_to_nodes: node = input_name_to_nodes[data_name][0] assert node, "{} is neither an input nor an output of nodes in augmented model.".format(data_name) name_to_node[data_name] = node.name activation_tensors_calib_range = {} intermediate_tensor = {} name_to_calibrator = {} ort_inputs_for_next_split_model = [] def _collect_data(inputs): for output_idx, output in enumerate(session.run(None, inputs)): if q_config is not None and output.size != 0: node_name = name_to_node[node_output_names[output_idx]] if node_output_names[output_idx] not in name_to_calibrator: calib_method = ( q_config[node_name]["calibrate_method"] if q_config and node_name in q_config else "MinMax" ) assert calib_method in calibrator.CALIBRATOR, "Calibration method {} is not registered.".format( calib_method ) _calibrator = calibrator.CALIBRATOR[calib_method]() else: _calibrator = name_to_calibrator[node_output_names[output_idx]] # currently, the calibration range for each iteration is collected if # the calibration method is minmax, otherwise the tensor data is collected. # TODO: for entropy and percentile method, need to support range collection # per iteration in the future. if _calibrator.method_name == "MinMax": _calibrator.collect(output) activation_tensors_calib_range[node_output_names[output_idx]] = [list(_calibrator.calib_range)] name_to_calibrator[node_output_names[output_idx]] = _calibrator else: intermediate_tensor.setdefault((node_output_names[output_idx], node_name), []).append(output) elif q_config is None: activation_tensors_calib_range.setdefault(node_output_names[output_idx], []).append(output) idx = 0 while True: inputs = self.dataloader.get_next() if not inputs: break if self.iterations != []: if idx > max(self.iterations): break if idx in self.iterations: _collect_data(inputs) else: _collect_data(inputs) idx += 1 # for entropy and percentile method, collect calibration range after all tensors are collected. merged_dict = intermediate_tensor for (output_name, node_name), datas in merged_dict.items(): if any([data is None for data in datas]): continue if any([data.dtype in [bool] for data in datas]): # output type of some ops is bool, skip continue calib_method = q_config[node_name]["calibrate_method"] if q_config and node_name in q_config else 0 _calibrator = calibrator.CALIBRATOR[calib_method]() _calibrator.collect(datas) activation_tensors_calib_range.setdefault(output_name, []).append(list(_calibrator.calib_range)) _calibrator.clear() del _calibrator return activation_tensors_calib_range def get_weight_tensors_calib_range(self): """Get calib ranges of weight tensors. Returns: dict: calib ranges """ model_nodes_names = [node.name for node in self.model.graph.node] # if augmented_model is not None, it means self.white_nodes is already updated in augment_graph func # then skip update here if self.already_quantized and self.augmented_model is None: # mapping between fp32 node and int8 node new_white_nodes = [] for white_node in self.white_nodes: new_white_node = white_node + "_quant" assert new_white_node in model_nodes_names, "no quantized {} in the " "graph".format(white_node) new_white_nodes.append(new_white_node) self.white_nodes = new_white_nodes added_outputs = set() initializer_tensors_to_dump = [] initializers = [init.name for init in self.model.graph.initializer] for node in self.model.graph.node: # pylint: disable=no-member should_be_dump = ((node.op_type in self.dump_op_types) and (node.name not in self.black_nodes)) or ( node.name in self.white_nodes ) if should_be_dump: for input in node.input: if ( (self.already_quantized and input.replace("_dequantized", "_quantized") in initializers) or (not self.already_quantized and input in initializers) ) and len(input) != 0: added_outputs.add(input) for tensor in added_outputs: if tensor not in initializers: continue if self.augment_nodes: for augment_node_type in self.augment_nodes: if augment_node_type in ["DequantizeLinear"]: if not (tensor.endswith("_scale") or tensor.endswith("_zero_point")): initializer_tensors_to_dump.append(tensor) else: initializer_tensors_to_dump.append(tensor) weight_tensors_calib_range = {} for initializer_tensor_name in initializer_tensors_to_dump: initializer_tensor = self.model_wrapper.get_initializer(initializer_tensor_name) # double check initializer tensor is not None if initializer_tensor is None: # pragma: no cover continue initializer_tensor = onnx.numpy_helper.to_array( initializer_tensor, base_dir=( os.path.dirname(self.model_wrapper.model_path) if self.model_wrapper.model_path is not None else "" ), ) _calibrator = calibrator.CALIBRATOR["MinMax"]() # use minmax method to calibrate initializer tensors if initializer_tensor.flatten().size > 0: _calibrator.collect(initializer_tensor) weight_tensors_calib_range[initializer_tensor_name] = [list(_calibrator.calib_range)] _calibrator.clear() del _calibrator return weight_tensors_calib_range def get_intermediate_outputs(self, q_config=None, activation_only=False, weight_only=False): """Gather intermediate model outputs after running inference.""" output_dicts = {} if not activation_only and not weight_only: output_dicts = self.get_activation_tensors_calib_range(q_config) output_dicts.update(self.get_weight_tensors_calib_range()) elif weight_only: output_dicts = self.get_weight_tensors_calib_range() elif activation_only: output_dicts = self.get_activation_tensors_calib_range(q_config) return list(output_dicts.keys()), output_dicts def _dequantize(self, tensor, scale_tensor, zo_tensor): """Helper function to dequantize tensor.""" int_tensor = self.model_wrapper.get_initializer(tensor) if int_tensor: # weight tensor return self._dequantize_weight(tensor, scale_tensor, zo_tensor) else: return self._dequantize_activation(tensor, scale_tensor, zo_tensor) def _dequantize_activation(self, activation_tensor_name, scale_tensor, zo_tensor): """Helper function to dequantize activation.""" added_nodes, added_output = self._add_dequantize_node(activation_tensor_name, scale_tensor, zo_tensor) self.dequantized_output[added_output] = activation_tensor_name return added_nodes, added_output def _dequantize_weight(self, weight_tensor_name, scale_tensor, zo_tensor): """Helper function to dequantize weight.""" weight_tensor = self.model_wrapper.get_initializer(weight_tensor_name) if len(scale_tensor.dims) in [1, 2] and weight_tensor.dims[0] == max(scale_tensor.dims): logger.debug("weight {} is quantized with per channel granularity.".format(weight_tensor_name)) if self.opset_version < 13 and self.ort_version >= ORT112_VERSION: logger.warning( "Skip dequantizing weight {}, please use onnxruntime < 1.12.0 " "or upgrade model opset version to 13 or higher".format(weight_tensor_name) ) return [], None node = self.model_wrapper.input_name_to_nodes()[weight_tensor_name][0] if "Conv" in node.op_type or ("Gemm" in node.op_type and quant_utils.is_B_transposed(node)): added_nodes, added_output = self._add_dequantize_transpose_node( weight_tensor_name, scale_tensor, zo_tensor, len(weight_tensor.dims) ) else: added_nodes, added_output = self._add_dequantize_node( weight_tensor_name, scale_tensor, zo_tensor, axis=1 if self.opset_version > 12 else None ) else: added_nodes, added_output = self._add_dequantize_node(weight_tensor_name, scale_tensor, zo_tensor) self.dequantized_output[added_output] = weight_tensor_name return added_nodes, added_output def _add_dequantize_node(self, tensor_name, scale_tensor, zo_tensor, axis=None): """Helper function to generate dequantize node.""" dequantize_node = onnx.helper.make_node( "DequantizeLinear", [tensor_name, scale_tensor.name, zo_tensor.name], [tensor_name + "_output"], tensor_name + "_DequantizeLinear", axis, ) return [dequantize_node], tensor_name + "_output" def _add_dequantize_transpose_node(self, tensor_name, scale_tensor, zo_tensor, dim): """Insert Transpose-DequantizelLinear-Transpose pairs.""" pre_transpose_node = onnx.helper.make_node( "Transpose", inputs=[tensor_name], outputs=[tensor_name + "_transposed"], perm=(1, 0, 2, 3) if dim == 4 else (1, 0), name=tensor_name + "_pre_transpose", ) dequantize_node = onnx.helper.make_node( "DequantizeLinear", [tensor_name + "_transposed", scale_tensor.name, zo_tensor.name], [tensor_name + "_DequantizeLinear"], tensor_name + "_DequantizeLinear", axis=1 if self.opset_version > 12 else None, ) post_transpose_node = onnx.helper.make_node( "Transpose", inputs=[tensor_name + "_DequantizeLinear"], outputs=[tensor_name + "_output"], perm=(1, 0, 2, 3) if dim == 4 else (1, 0), name=tensor_name + "_post_transpose", ) added_nodes = [pre_transpose_node, dequantize_node, post_transpose_node] return added_nodes, tensor_name + "_output" def _map_calibration(self, node_output_names, output_dicts): """Map tensor names and min/max values.""" merged_dict = {} for name, minmaxs in output_dicts.items(): for minmax in minmaxs: if len(minmax) < 2: continue merged_dict.setdefault(name + "_Min", []).append(minmax[0]) merged_dict.setdefault(name + "_Max", []).append(minmax[1]) # Characterizing distribution of a node's values across test data sets clean_merged_dict = dict((i, merged_dict[i]) for i in merged_dict) pairs = [ tuple([float(min(clean_merged_dict[name + "_Min"])), float(max(clean_merged_dict[name + "_Max"]))]) for name in node_output_names ] final_dict = dict(zip(node_output_names, pairs)) return final_dict def dump_minmax(self, q_config): """Get calib ranges of tensors.""" # pipeline of getting calib ranges of tensors during calibration: # 1. augment_graph(): insert activation tensors to model output # 2. get_intermediate_outputs(): # 2.1 get_activation_tensors_calib_range(): get calib ranges of activation tensors using the augment graph # 2.2 get_weight_tensors_calib_range(): get calib ranges of weight tensors self.augment_graph() node_output_names, output_dicts = self.get_intermediate_outputs(q_config) return self._map_calibration(node_output_names, output_dicts) def dump_calibration(self, q_config, min_max=None): """Gather calibration params for quantization. Args: q_config (dict): op-wise quantization config min_max (dict, optional): min/max values of tensors """ return ( self.calculate_quantization_params(q_config, self.dump_minmax(q_config)) if min_max is None else self.calculate_quantization_params(q_config, min_max) ) def calculate_quantization_params(self, q_config, quantization_thresholds): """Given quantization thresholds, calculate the quantization params. Args: q_config (dict): op-wise quantization config quantization_thresholds (dict): Dictionary specifying the min and max values or outputs of conv and matmul nodes, should be specified in the following format: {"param_name": [min, max]} """ if quantization_thresholds is None: raise ValueError( "quantization thresholds is required to calculate quantization \ params (zero point and scale)" ) quantization_params = {} model = self.model input_name_to_nodes = self.model_wrapper.input_name_to_nodes() output_name_to_node = self.model_wrapper.output_name_to_node() for tensor_name in quantization_thresholds.keys(): child = None if tensor_name in input_name_to_nodes: children = input_name_to_nodes[tensor_name] if len(children) == 1: child = children[0] parent = None sym = False qType = 2 # uint8 # input and output tensor follow activation_type and activation_sym if tensor_name in input_name_to_nodes and any( [i.name in q_config for i in input_name_to_nodes[tensor_name]] ): for child in input_name_to_nodes[tensor_name]: if child.name in q_config and q_config[child.name] not in ["fp32", "fp16", "bf16"]: sym = q_config[child.name]["activation_sym"] qType = q_config[child.name]["activation_type"] break elif ( tensor_name in output_name_to_node and output_name_to_node[tensor_name].name in q_config and q_config[output_name_to_node[tensor_name].name] not in ["fp32", "fp16", "bf16"] ): sym = q_config[output_name_to_node[tensor_name].name]["activation_sym"] qType = q_config[output_name_to_node[tensor_name].name]["activation_type"] if self.execution_provider in ["TensorrtExecutionProvider"]: # TensorrtExecutionProvider only support int8 qType = 3 node_thresholds = quantization_thresholds[tensor_name] node_params = self.calculate_scale_zeropoint( parent, child, node_thresholds[0], node_thresholds[1], sym, qType, ) quantization_params[tensor_name] = node_params return quantization_params def calculate_scale_zeropoint(self, last_node, next_node, rmin, rmax, sym, qType): """Given the source and destination node of tensor, return calculated zero point and scales.""" zp_and_scale = [] # adjust rmin and rmax such that 0 is included in the range. This is required # to make sure zero can be uniquely represented. rmin = min(rmin, 0) rmax = max(rmax, 0) if next_node: if next_node.op_type == "Relu": if rmin < 0: rmin = 0 elif next_node.op_type == "Clip" and len(next_node.input) == 3: if self.model_wrapper.get_initializer(next_node.input[1]) is not None: clip_min = onnx.numpy_helper.to_array(self.model_wrapper.get_initializer(next_node.input[1])) if rmin < clip_min: rmin = clip_min.tolist() if not isinstance(clip_min.tolist(), list) else clip_min.tolist()[0] if self.model_wrapper.get_initializer(next_node.input[2]) is not None: clip_max = onnx.numpy_helper.to_array(self.model_wrapper.get_initializer(next_node.input[2])) if rmax > clip_max: rmax = clip_max.tolist() if not isinstance(clip_max.tolist(), list) else clip_max.tolist()[0] if last_node: if last_node.op_type in ["Conv", "FusedConv"]: attrs = [attr for attr in last_node.attribute] attrs_names = [attr.name for attr in last_node.attribute] if "activation" in attrs_names: if attrs[attrs_names.index("activation")].s == b"Relu": rmin = max(rmin, 0) if attrs[attrs_names.index("activation")].s == b"Clip": assert ( "activation_params" in attrs_names ), "the model contains no params for clip node {}".format(last_node) clip_params = attrs[attrs_names.index("activation_params")].floats rmin = min(rmin, clip_params[0], clip_params[1]) rmax = max(rmax, clip_params[0], clip_params[1]) scale, zp = quant_utils.calculate_scale_zp(rmin, rmax, qType, sym, self.reduce_range) zp_and_scale.append(zp) zp_and_scale.append(scale) return zp_and_scale ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/calibrator.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # ------------------------------------------------------------------------- # Copyright (c) Microsoft, Intel Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- """Calibrator for onnx models.""" import copy import numpy as np from scipy import stats CALIBRATOR = {} def calib_registry(calib_method): """The class decorator used to register all Calibrator subclasses.""" def decorator_calib(cls): assert cls.__name__.endswith( "Calibrator" ), "The name of subclass of Calibrator should end with 'Calibrator' substring." if cls.__name__[: -len("Calibrator")] in CALIBRATOR: # pragma: no cover raise ValueError("Cannot have two operators with the same name.") CALIBRATOR[calib_method] = cls return cls return decorator_calib class CalibratorBase: """Base calibrator class.""" def __init__(self): """Initialize base calibrator class.""" self._calib_min = None self._calib_max = None def collect(self, datas): """Collect calibration range.""" self.collect_calib_data(datas) def clear(self): """Clear calibration range.""" self._calib_min = None self._calib_max = None def collect_calib_data(self, datas): """Collect calibration range value.""" raise NotImplementedError @property def calib_range(self): """Get calibration range value.""" return self._calib_min, self._calib_max @calib_registry(calib_method="MinMax") class MinMaxCalibrator(CalibratorBase): """MinMax calibrator class.""" def __init__(self): """Initialize minmax calibrator class.""" super(MinMaxCalibrator, self).__init__() def collect_calib_data(self, datas): """Collect calibration range.""" if isinstance(datas, list) and len(set([data.shape for data in datas])) != 1: for data in datas: if data.size == 0: # pragma: no cover continue self._collect_value(data) else: datas = np.asarray(datas) datas = datas.flatten() assert datas.size > 0, "collected intermediate data size" "should not be 0, please check augmented_model" self._collect_value(datas) def _collect_value(self, data): """Collect min/max value.""" data = np.asarray(data) local_min = np.min(data[np.isinf(data) == False]) # noqa: E712 local_max = np.max(data[np.isinf(data) == False]) # noqa: E712 if self._calib_min is None and self._calib_max is None: self._calib_min = local_min self._calib_max = local_max else: self._calib_min = np.minimum(self._calib_min, local_min) self._calib_max = np.maximum(self._calib_max, local_max) @property def method_name(self): """Get calibration method name.""" return "MinMax" @calib_registry(calib_method="Percentile") class PercentileCalibrator(CalibratorBase): """Percentile calibrator class. Args: num_bins (int, optional): number of bins to create a new histogram for collecting tensor values. Defaults to 2048. percentile (float, optional): A float number between [0, 100]. Defaults to 99.999. """ def __init__(self, num_bins=2048, percentile=99.999): """Initialize percentile calibrator class.""" super(PercentileCalibrator, self).__init__() self.collector = None self.num_bins = num_bins self.percentile = percentile def collect_calib_data(self, datas): """Collect calibration range.""" if not self.collector: self.collector = HistogramCollector(self.num_bins) self.collector.collect_data(datas) self.compute_percentile_range(self.percentile) def compute_percentile_range(self, percentile): """Compute percentile range.""" if percentile < 0 or percentile > 100: raise ValueError("Invalid percentile. Must be in range 0 <= percentile <= 100.") calib_hist, calib_bin_edges, min_range, max_range, th = self.collector.histogram total = calib_hist.sum() cdf = np.cumsum(calib_hist / total) percent_to_cut_one_side = (100.0 - percentile) / 200.0 max_idx = np.searchsorted(cdf, 1.0 - percent_to_cut_one_side) min_idx = np.searchsorted(cdf, percent_to_cut_one_side) self._calib_min = calib_bin_edges[min_idx].astype("float32") self._calib_max = calib_bin_edges[max_idx].astype("float32") if self._calib_min < min_range: self._calib_min = min_range if self._calib_max > max_range: self._calib_max = max_range def clear(self): """Clear calibration range.""" self._calib_min = None self._calib_max = None self.collector = None @property def method_name(self): """Get calibration method name.""" return "Percentile" @calib_registry(calib_method="Entropy") class EntropyCalibrator(CalibratorBase): """Entropy calibrator class. Args: num_bins (int, optional):number of bins to create a new histogram for collecting tensor values. Defaults to 128. num_quantized_bins (int, optional): number of quantized bins. Defaults to 128. """ def __init__(self, num_bins=128, num_quantized_bins=128): """Initialize entropy calibrator class.""" super(EntropyCalibrator, self).__init__() self.collector = None self.num_bins = num_bins self.num_quantized_bins = num_quantized_bins def collect_calib_data(self, datas): """Collect calibration range.""" if not self.collector: self.collector = HistogramCollector(self.num_bins) self.collector.collect_data(datas) self.compute_kl_range() def compute_kl_range(self): """Compute entropy range.""" histogram = self.collector.histogram self._calib_min, self._calib_max = self.get_kl_threshold(histogram, self.num_quantized_bins) def get_kl_threshold(self, histogram, num_quantized_bins): """Compute entropy threshold. Ref: https://github.com//apache/incubator-mxnet/blob/master/python/mxnet/contrib/quantization.py https://github.com/microsoft/onnxruntime/blob/main/onnxruntime/python/tools/quantization/calibrate.py Args: histogram (tuple): hist, hist_edges, min, max and threshold num_quantized_bins (int): number of quantized bins. Returns: float: optimal threshold """ hist = histogram[0] hist_edges = histogram[1] num_bins = hist.size zero_bin_index = num_bins // 2 num_half_quantized_bin = num_quantized_bins // 2 kl_divergence = np.zeros(zero_bin_index - num_half_quantized_bin + 1) thresholds = [(0, 0) for i in range(kl_divergence.size)] for i in range(num_half_quantized_bin, zero_bin_index + 1, 1): start_index = zero_bin_index - i end_index = zero_bin_index + i + 1 if (zero_bin_index + i + 1) <= num_bins else num_bins thresholds[i - num_half_quantized_bin] = ( float(hist_edges[start_index]), float(hist_edges[end_index]), ) sliced_distribution = copy.deepcopy(hist[start_index:end_index]) # reference distribution p p = sliced_distribution.copy() # a copy of np array left_outliers_count = sum(hist[:start_index]) right_outliers_count = sum(hist[end_index:]) p[0] += left_outliers_count p[-1] += right_outliers_count # nonzeros[i] incidates whether p[i] is non-zero nonzeros = (p != 0).astype(np.int64) # quantize p.size bins into quantized bins (default 128 bins) quantized_bins = np.zeros(num_quantized_bins, dtype=np.int64) num_merged_bins = sliced_distribution.size // num_quantized_bins # merge bins into quantized bins for index in range(num_quantized_bins): start = index * num_merged_bins end = start + num_merged_bins quantized_bins[index] = sum(sliced_distribution[start:end]) quantized_bins[-1] += sum(sliced_distribution[num_quantized_bins * num_merged_bins :]) # in order to compare p and q, we need to make length of q equals to length of p # expand quantized bins into p.size bins q = np.zeros(p.size, dtype=np.int64) for index in range(num_quantized_bins): start = index * num_merged_bins end = start + num_merged_bins norm = sum(nonzeros[start:end]) if norm != 0: q[start:end] = float(quantized_bins[index]) / float(norm) p = smooth_distribution(p) q = smooth_distribution(q) if isinstance(q, np.ndarray): kl_divergence[i - num_half_quantized_bin] = stats.entropy(p, q) else: kl_divergence[i - num_half_quantized_bin] = float("inf") min_kl_divergence_idx = np.argmin(kl_divergence) optimal_threshold = thresholds[min_kl_divergence_idx] min_value = histogram[2] max_value = histogram[3] if optimal_threshold[0] < min_value: optimal_threshold = (min_value, optimal_threshold[1]) if optimal_threshold[1] > max_value: optimal_threshold = (optimal_threshold[0], max_value) return optimal_threshold[0], optimal_threshold[1] def clear(self): """Clear calibration range.""" self._calib_min = None self._calib_max = None self.collector = None @property def method_name(self): """Get calibration method name.""" return "Entropy" class HistogramCollector: """Histogram collctor class.""" def __init__(self, num_bins=2048): """Initialize histogram collctor.""" self._num_bins = num_bins self._histogram = None def collect_data(self, datas): """Collect histogram data.""" if isinstance(datas, list) and len(set([data.shape for data in datas])) != 1: for data in datas: if data.size == 0: # pragma: no cover continue self._collect_value(data) else: datas = np.asarray(datas) datas = datas.flatten() assert datas.size > 0, "collected intermediate data size" "should not be 0, please check augmented_model" self._collect_value(datas) def _collect_value(self, data): """Collect value.""" data = np.asarray(data) min_range = np.min(data) max_range = np.max(data) th = max(abs(min_range), abs(max_range)) if self._histogram is None: hist, hist_edges = np.histogram(data, self._num_bins, range=(-th, th)) self._histogram = (hist, hist_edges, min_range, max_range, th) else: self._histogram = self.combine_histogram(self._histogram, data, min_range, max_range, th) def combine_histogram(self, old_hist, data_arr, new_min, new_max, new_th): """Combine histogram.""" (old_hist, old_hist_edges, old_min, old_max, old_th) = old_hist if new_th <= old_th: hist, _ = np.histogram(data_arr, bins=len(old_hist), range=(-old_th, old_th)) return ( old_hist + hist, old_hist_edges, min(old_min, new_min), max(old_max, new_max), old_th, ) else: # Need to generate new histogram with new_th if old_th == 0: hist, hist_edges = np.histogram(data_arr, len(old_hist), range=(-new_th, new_th)) hist += old_hist else: old_num_bins = len(old_hist) old_step = 2 * old_th / old_num_bins half_increased_bins = int((new_th - old_th) // old_step + 1) new_num_bins = half_increased_bins * 2 + old_num_bins new_th = half_increased_bins * old_step + old_th hist, hist_edges = np.histogram(data_arr, bins=new_num_bins, range=(-new_th, new_th)) hist[half_increased_bins : new_num_bins - half_increased_bins] += old_hist return ( hist, hist_edges, min(old_min, new_min), max(old_max, new_max), new_th, ) @property def histogram(self): """Get histogram.""" return self._histogram def smooth_distribution(p, eps=0.0001): """Smooth distribution. Given a discrete distribution (may have not been normalized to 1), smooth it by replacing zeros with eps multiplied by a scaling factor and taking the corresponding amount off the non-zero values. Ref: http://hanj.cs.illinois.edu/cs412/bk3/KL-divergence.pdf https://github.com//apache/incubator-mxnet/blob/master/python/mxnet/contrib/quantization.py https://github.com/microsoft/onnxruntime/blob/main/onnxruntime/python/tools/quantization/calibrate.py Args: p (array): distribution array eps (float, optional): a small probability. Defaults to 0.0001. Returns: array: smoothed distribution """ is_zeros = (p == 0).astype(np.float32) is_nonzeros = (p != 0).astype(np.float32) n_zeros = is_zeros.sum() n_nonzeros = p.size - n_zeros if not n_nonzeros: return -1 eps1 = eps * float(n_zeros) / float(n_nonzeros) assert eps1 < 1.0, "n_zeros=%d, n_nonzeros=%d, eps1=%f" % ( n_zeros, n_nonzeros, eps1, ) hist = p.astype(np.float32) hist += eps * is_zeros + (-eps1) * is_nonzeros assert (hist <= 0).sum() == 0 return hist ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Operators for onnx model.""" import glob from os import path from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op modules = glob.glob(path.join(path.dirname(__file__), "*.py")) for f in modules: if path.isfile(f) and not f.startswith("__") and not f.endswith("__init__.py"): __import__(path.basename(f)[:-3], globals(), locals(), level=1) OPERATORS = base_op.OPERATORS ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/activation.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Activation operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="LeakyRelu, Sigmoid", mode=[constants.STATIC_QUANT]) class ActivationOperator(base_op.Operator): """Activation operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ActivationOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node data_found, _, _, _, _ = self.quantizer._get_quantization_params(node.output[0]) if not data_found: return False return True def quantize(self): """Do quantizaion.""" node = self.node super().quantize() node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0 or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] child = self.quantizer.model.get_children(node)[0] inputs = [] inputs.extend(parent.input) inputs.extend(child.input[1:]) qlinear_activation_output = child.output[0] kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qlinear_activation_node = onnx.helper.make_node( "QLinear" + node.op_type, inputs, [qlinear_activation_output], node.name, **kwargs ) self.quantizer.new_nodes.append(qlinear_activation_node) self.quantizer.remove_nodes.extend([parent, child, node]) @base_op.op_registry(op_types="Relu, Clip", mode=[constants.STATIC_QUANT]) class RemovableActivationOperator(base_op.Operator): """Removable activation operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(RemovableActivationOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if node.input[0] not in self.quantizer.quantized_value_map: return False return True def quantize(self): """Do quantization.""" node = self.node if node.output[0] in [i.name for i in self.quantizer.model.model.graph.output]: self.quantizer.dequantize_tensor(node, node.input[0]) else: self.quantizer.model.replace_input_of_all_nodes(node.output[0], node.input[0]) self.quantizer.remove_nodes.append(node) @base_op.op_registry( op_types="Softmax, BiasGelu, Elu, Exp, FastGelu, Gelu, Softplus, Tanh", mode=[constants.STATIC_QUANT] ) class Float16ActivationOperator(base_op.Operator): """Float16 Activation operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(Float16ActivationOperator, self).__init__(onnx_quantizer, onnx_node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/argmax.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ArgMax operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="ArgMax", mode=[constants.STATIC_QUANT]) class ArgMaxOperator(base_op.Operator): """ArgMax operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ArgMaxOperator, self).__init__(onnx_quantizer, onnx_node) def convert_check(self): """Check if conversion can be done.""" node = self.node return True def convert(self): """Convert to quantized format.""" node = self.node origin_name = node.input[0].split("_argmax_node")[0] if origin_name in self.quantizer.quantized_value_map: node.name = node.name + "_quant" ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/attention.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Attention operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Attention", mode=[constants.DYNAMIC_QUANT, constants.STATIC_QUANT]) class AttentionOperator(base_op.Operator): """Attention operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(AttentionOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0, 1]) node.name = node.name + "_quant" def convert(self): """Convert QDQ mode to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) quantized_name = [] scale = [] zp = [] for parent in parents[:2]: if parent.op_type == "DynamicQuantizeLinear": quantized_name.append(parent.output[0]) scale.append(parent.output[1]) zp.append(parent.output[2]) elif parent.op_type == "DequantizeLinear": quantized_name.append(parent.input[0]) scale.append(parent.input[1]) zp.append(parent.input[2]) self.quantizer.remove_nodes.append(parent) inputs = [] inputs.extend(quantized_name) inputs.append(node.input[2]) inputs.extend(scale) inputs.append(node.input[3] if len(node.input) > 3 else "") inputs.extend(zp) if len(node.input) > 4: inputs.append(node.input[4]) kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qattention_node = onnx.helper.make_node("QAttention", inputs, node.output, node.name, **kwargs) self.quantizer.new_nodes.append(qattention_node) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/base_op.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Base Operator.""" from onnx_neural_compressor import constants, quantization OPERATORS = { "dynamic_quant": {}, "static_quant": {}, } def op_registry(op_types, mode): """The class decorator used to register all Operator subclasses.""" def decorator_op(cls): assert cls.__name__.endswith( "Operator" ), "The name of subclass of Operator should end with 'Operator' substring." for item in mode: if cls.__name__[: -len("Operator")] in OPERATORS[item]: # pragma: no cover raise ValueError("Cannot have two operators with the same name for {} mode.".format(item)) break for single_op_type in [op_type.strip() for op_type in op_types.split(",")]: for item in mode: OPERATORS[item][single_op_type] = cls return cls return decorator_op class Operator(object): """Base Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" self.quantizer = onnx_quantizer self.node = onnx_node node_name = self.node.name.split("_quant")[0] if node_name in self.quantizer.config: self.dtype = self.quantizer.config[node_name] self.disable_qdq_for_node_output = ( True if onnx_node.op_type in onnx_quantizer.optypes_to_exclude_output_quant else False ) self.per_channel = False self.calibrate_method = 0 # minmax self.weight_sym = True self.weight_dtype = None self.activation_dtype = None self.activation_sym = False if node_name in self.quantizer.config: if self.quantizer.config[node_name] not in self.quantizer.fallback_list: self.per_channel = self.quantizer.config[node_name]["per_channel"] self.calibrate_method = self.quantizer.config[node_name]["calibrate_method"] self.weight_sym = self.quantizer.config[node_name]["weight_sym"] self.weight_dtype = self.quantizer.config[node_name]["weight_type"] self.activation_dtype = self.quantizer.config[node_name]["activation_type"] self.activation_sym = self.quantizer.config[node_name]["activation_sym"] def quantize_check(self): """Check if quantizaion can be done.""" return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node) if not self.disable_qdq_for_node_output or self.quantizer.mode != constants.DYNAMIC_QUANT: self.quantizer.quantize_outputs(node) def convert_check(self): """Check if conversion can be done.""" node = self.node if not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" return ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/binary_op.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Binary operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Add, Mul", mode=[constants.STATIC_QUANT]) class BinaryOperator(base_op.Operator): """Binary operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(BinaryOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node data_found, _, _, _, _ = self.quantizer._get_quantization_params(node.output[0]) if not data_found: return False if self.quantizer.execution_provider == "TensorrtExecutionProvider": return True if not all([self.quantizer.is_valid_quantize_weight(i) for i in node.input]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, initializer_use_weight_qType=False) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0 or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) child = self.quantizer.model.get_children(node)[0] qlinear_binary_math_output = child.output[0] kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qlinear_binary_math_inputs = [] for parent in parents: qlinear_binary_math_inputs.extend(parent.input) qlinear_binary_math_inputs.extend(child.input[1:]) qlinear_binary_math_node = onnx.helper.make_node( "QLinear" + node.op_type, qlinear_binary_math_inputs, [qlinear_binary_math_output], node.name, **kwargs ) self.quantizer.new_nodes += [qlinear_binary_math_node] self.quantizer.remove_nodes.extend(parents) self.quantizer.remove_nodes.append(child) self.quantizer.remove_nodes.append(node) @base_op.op_registry(op_types="Mod", mode=[constants.STATIC_QUANT]) class BinaryDirect8BitOperator(base_op.Operator): """Binary operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(BinaryDirect8BitOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node data_found, _, _, _, _ = self.quantizer._get_quantization_params(node.output[0]) if not data_found: return False if not all([self.quantizer.is_valid_quantize_weight(i) for i in node.input]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, initializer_use_weight_qType=False) if not self.disable_qdq_for_node_output or self.quantizer.mode != "qdq": self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0 or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): for idx, parent in enumerate(parents): if parent.op_type == "DequantizeLinear": self.node.input[idx] = parent.input[0] self.quantizer.remove_nodes.append(parent) for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") node.output[0] = node.output[0] + "_quantized" @base_op.op_registry( op_types="Sum, Sub, Div, Pow, Equal, Greater, GreaterOrEqual, Less, LessOrEqual", mode=[constants.STATIC_QUANT] ) class Float16BinaryOperator(base_op.Operator): """Float16 Binary operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(Float16BinaryOperator, self).__init__(onnx_quantizer, onnx_node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/concat.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Concat Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Concat", mode=[constants.STATIC_QUANT]) class ConcatOperator(base_op.Operator): """Concat Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ConcatOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if len(node.input) == 1: # pragma: no cover return False inits = [i.name for i in self.quantizer.model.initializer()] if all([inp not in self.quantizer.quantized_value_map and inp not in inits for inp in node.input]) or not all( [inp in self.quantizer.quantized_value_map or inp in inits for inp in node.input] ): return False return True def quantize(self): """Do quantizaion.""" node = self.node inits = [i.name for i in self.quantizer.model.initializer()] for idx, inp in enumerate(node.input): initializer_use_weight_qType = inp not in inits self.quantizer.quantize_inputs(node, [idx], initializer_use_weight_qType) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if len(children) == 0 or len(parents) == 0 or not node.name.endswith("_quant"): return False # check input type if all([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): input_zp, input_scale, output_zp = [], [], [] input_zp = [parent.input[2] for parent in parents] input_scale = [parent.input[1] for parent in parents] output_zp = [child.input[2] for child in children if child.op_type == "QuantizeLinear"] if ( any([self.quantizer.model.get_initializer(zp) is None for zp in input_zp]) or any([self.quantizer.model.get_initializer(zp) is None for zp in output_zp]) or any([self.quantizer.model.get_initializer(scale) is None for scale in input_scale]) ): # pragma: no cover return False # check input scale is float type if any( [self.quantizer.model.get_initializer(scale).data_type != 1 for scale in input_scale] ): # pragma: no cover return False # check input zp type is the same with output zp type if any( [ self.quantizer.model.get_initializer(in_zp).data_type not in [self.quantizer.model.get_initializer(out_zp).data_type for out_zp in output_zp] for in_zp in input_zp ] ): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if all([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): inputs = [] inputs.extend([i for i in children if i.op_type == "QuantizeLinear"][0].input[1:]) for parent in parents: inputs.extend(parent.input) self.quantizer.remove_nodes.append(parent) for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") kwargs = {} for attribute in node.attribute: kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qlconcat_node = onnx.helper.make_node( "QLinearConcat", inputs, [node.output[0] + "_quantized"], node.name, **kwargs ) self.quantizer.new_nodes += [qlconcat_node] self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/conv.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Conv Operator.""" import onnx from onnx import onnx_pb as onnx_proto from onnx_neural_compressor import constants from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Conv, FusedConv", mode=[constants.DYNAMIC_QUANT]) class ConvOperator(base_op.Operator): """Conv Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ConvOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" node = self.node if node.op_type == "FusedConv": kwargs = {} for attribute in node.attribute: if attribute.name == "activation" and attribute.s in [b"Relu", b"Clip"]: continue if attribute.name == "activation_params": continue kwargs.update(quant_utils.attribute_to_kwarg(attribute)) conv = onnx.helper.make_node("Conv", node.input, node.output, node.name, **kwargs) node.CopyFrom(conv) self.quantizer.quantize_inputs(node, [0]) if self.per_channel: self.quantizer.quantize_weights_per_channel(node, [1], self.weight_dtype, self.weight_sym, 0) else: self.quantizer.quantize_inputs(node, [1]) if len(node.input) == 3: self.quantizer.quantize_bias_tensor(node) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node inputs = [] parents = self.quantizer.model.get_parents(node) if parents[0].op_type == "QuantizeLinear": inputs.append(parents[0].output[0]) inputs.append(parents[1].input[0]) inputs.append(parents[0].input[2]) inputs.append(parents[1].input[2]) scale_0 = parents[0].input[1] else: inputs.append(parents[0].output[0]) inputs.append(parents[1].input[0]) inputs.append(parents[0].output[2]) inputs.append(parents[1].input[2]) scale_0 = parents[0].output[1] scale_1 = parents[1].input[1] # quantize bias if exist quantized_bias_name = "" bias_present = False if len(node.input) == 3: quantized_bias_name = node.input[2] + "_quantized" bias_present = True conv_integer_output = node.output[0] + "_output_quantized" kwargs = {} for attribute in node.attribute: if attribute.name == "activation" and attribute.s in [b"Relu", b"Clip"]: # pragma: no cover continue if attribute.name == "activation_params": # pragma: no cover continue kwargs.update(quant_utils.attribute_to_kwarg(attribute)) conv_integer_node = onnx.helper.make_node("ConvInteger", inputs, [conv_integer_output], node.name, **kwargs) self.quantizer.new_nodes.append(conv_integer_node) # Add bias add nodes if bias_present: conv_integer_output = self.quantizer.get_bias_add_nodes( node, parents[1].input[0], conv_integer_output, quantized_bias_name ) # Add cast operation to cast convInteger output to float. cast_op_output = conv_integer_output + "_cast_output" cast_node = onnx.helper.make_node( "Cast", [conv_integer_output], [cast_op_output], conv_integer_output + "_cast", to=onnx_proto.TensorProto.FLOAT, ) self.quantizer.new_nodes.append(cast_node) # Add mul operation to multiply scales of two inputs. scales_mul_op = node.name + "_scales_mul" scales_mul_node = quant_utils.find_by_name(scales_mul_op, self.quantizer.new_nodes) if scales_mul_node is None: scales_mul_node = onnx.helper.make_node("Mul", [scale_0, scale_1], [scales_mul_op + ":0"], scales_mul_op) self.quantizer.new_nodes.append(scales_mul_node) scales_mul_op_output = scales_mul_node.output[0] # Add mul operation to multiply mul_scales_op result with output of ConvInteger # and make the output of this node the same as output of original conv node. output_scale_mul_op = node.name + "_output_scale_mul" self.quantizer.new_nodes.append( onnx.helper.make_node("Mul", [cast_op_output, scales_mul_op_output], [node.output[0]], output_scale_mul_op) ) self.quantizer.remove_nodes.extend(parents[1:]) self.quantizer.remove_nodes.append(node) @base_op.op_registry(op_types="Conv, FusedConv", mode=[constants.STATIC_QUANT]) class StaticConvOperator(ConvOperator): """Conv Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ConvOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" node = self.node if node.op_type == "FusedConv": kwargs = {} for attribute in node.attribute: if attribute.name == "activation" and attribute.s in [b"Relu", b"Clip"]: continue if attribute.name == "activation_params": continue kwargs.update(quant_utils.attribute_to_kwarg(attribute)) conv = onnx.helper.make_node("Conv", node.input, node.output, node.name, **kwargs) node.CopyFrom(conv) self.quantizer.quantize_inputs(node, [0]) if self.per_channel: self.quantizer.quantize_weights_per_channel(node, [1], self.weight_dtype, self.weight_sym, 0) else: self.quantizer.quantize_inputs(node, [1]) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) if len(node.input) == 3: self.quantizer.quantize_bias_tensor(node) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node if len(self.quantizer.model.get_children(node)) == 0 or not node.name.endswith("_quant"): # pragma: no cover return parents = self.quantizer.model.get_parents(node) child = self.quantizer.model.get_children(node)[0] qlinear_conv_inputs = [] for parent in parents[0:2]: qlinear_conv_inputs.extend(parent.input) qlinear_conv_inputs.extend(child.input[1:]) if len(parents) == 3: qlinear_conv_inputs.append(parents[-1].input[0]) qlinear_conv_output = child.output[0] kwargs = {} for attribute in node.attribute: if attribute.name == "activation" and attribute.s in [b"Relu", b"Clip"]: # pragma: no cover continue if attribute.name == "activation_params": # pragma: no cover continue kwargs.update(quant_utils.attribute_to_kwarg(attribute)) qlinear_conv_node = onnx.helper.make_node( "QLinearConv", qlinear_conv_inputs, [qlinear_conv_output], node.name, **kwargs ) self.quantizer.new_nodes.append(qlinear_conv_node) self.quantizer.remove_nodes.extend(parents) self.quantizer.remove_nodes.append(child) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/direct_q8.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Direct8Bit Operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry( op_types="Reshape, Transpose, Squeeze, Unsqueeze, Flatten, Expand, Slice, " "SpaceToDepth, DepthToSpace, Upsample, Tile, CenterCropPad", mode=[constants.STATIC_QUANT], ) class Direct8BitOperator(base_op.Operator): """Direct8Bit Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(Direct8BitOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(self.node, [0], initializer_use_weight_qType=False, direct_int8=True) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if (len(children) == 0 and len(parents) == 0) or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): for parent in parents: if parent.op_type == "DequantizeLinear": # make sure parent DequantizeLinear of input 0 is not used by other ops if len(self.quantizer.model.get_children(parent)) == 1 and not self.quantizer.model.is_graph_output( parents[0].output[0] ): self.quantizer.remove_nodes.append(parent) self.node.input[0] = parent.input[0] break for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") node.output[0] = node.output[0] + "_quantized" ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/embed_layernorm.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """EmbedLayerNormalization Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="EmbedLayerNormalization", mode=[constants.DYNAMIC_QUANT, constants.STATIC_QUANT]) class EmbedLayerNormalizationOperator(base_op.Operator): """EmbedLayerNormalization Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(EmbedLayerNormalizationOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [2, 3, 4, 5, 6]) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node parents = [i for i in self.quantizer.model.get_parents(node) if i.op_type == "DequantizeLinear"] inputs = [] # 'input_ids' inputs.extend([node.input[0]]) # 'segment_ids' inputs.extend([node.input[1]]) for parent in parents: inputs.append(parent.input[0]) # 'mask' (optional) if len(node.input) > 7: inputs.append(node.input[7]) for parent in parents: inputs.append(parent.input[1]) for parent in parents: inputs.append(parent.input[2]) kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qembed_layer_norm_node = onnx.helper.make_node( "QEmbedLayerNormalization", inputs, node.output, node.name, **kwargs ) self.quantizer.new_nodes.append(qembed_layer_norm_node) self.quantizer.remove_nodes.extend(parents) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/gather.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Gather Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry( op_types="Gather, GatherElements, GatherND", mode=[constants.DYNAMIC_QUANT, constants.STATIC_QUANT] ) class GatherOperator(base_op.Operator): """Gather Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(GatherOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0], initializer_use_weight_qType=False) if not self.disable_qdq_for_node_output or self.quantizer.mode != constants.DYNAMIC_QUANT: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if len(children) == 0 or len(parents) == 0 or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" # DQ-Gather-Q-DQ-op node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]): inputs = [] inputs.append(parents[0].input[0]) inputs.append(node.input[1]) out_scale = 1.0 out_zp = 0 gather_new_output = node.output[0] + "_quantized" # dynamic quant output name for child in children: if child.op_type == "QuantizeLinear": out_scale = onnx.numpy_helper.to_array(self.quantizer.model.get_initializer(children[0].input[1])) out_zp = onnx.numpy_helper.to_array(self.quantizer.model.get_initializer(children[0].input[2])) gather_new_output = children[0].output[0] # static quant output name self.quantizer.remove_nodes.append(child) kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) gather_node = onnx.helper.make_node(node.op_type, inputs, [gather_new_output], node.name, **kwargs) self.quantizer.new_nodes.append(gather_node) if any([i.op_type != "QuantizeLinear" for i in children]): dq_inputs = [] dq_inputs.append(gather_new_output) dq_inputs.extend(parents[0].input[1:]) dq_node = onnx.helper.make_node( "DequantizeLinear", dq_inputs, [node.output[0]], node.name + "_DequantizeLinear" ) self.quantizer.new_nodes.append(dq_node) # int8 weight will be recalculated for the first time if ( any([child.op_type == "QuantizeLinear" for child in children]) and self.quantizer.model.get_initializer(parents[0].input[0]) is not None and parents[0].input[0] not in self.quantizer.recalculate_quantized_value ): int8_tensor = onnx.numpy_helper.to_array(self.quantizer.model.get_initializer(parents[0].input[0])) in_scale = onnx.numpy_helper.to_array(self.quantizer.model.get_initializer(parents[0].input[1])) in_zp = onnx.numpy_helper.to_array(self.quantizer.model.get_initializer(parents[0].input[2])) new_int8_tensor = (((int8_tensor.astype("float32") - in_zp) * in_scale) / out_scale).round() + out_zp self.quantizer.model.set_initializer(parents[0].input[0], new_int8_tensor.astype(int8_tensor.dtype)) self.quantizer.recalculate_quantized_value.append(parents[0].input[0]) self.quantizer.remove_nodes.extend([node, parents[0]]) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/gavgpool.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """GlobalAveragePool Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="GlobalAveragePool", mode=[constants.STATIC_QUANT]) class GlobalAveragePoolOperator(base_op.Operator): """GlobalAveragePool Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(GlobalAveragePoolOperator, self).__init__(onnx_quantizer, onnx_node) def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0: # pragma: no cover return False return True def convert(self): """Convert to QOperator format.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] child = self.quantizer.model.get_children(node)[0] kwargs = {} for attribute in node.attribute: kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain kwargs["channels_last"] = 0 inputs = parent.input inputs.extend(child.input[1:]) qnode = onnx.helper.make_node("QLinear" + node.op_type, inputs, child.output, node.name + "_quant", **kwargs) self.quantizer.new_nodes += [qnode] self.quantizer.remove_nodes.append(child) self.quantizer.remove_nodes.append(parent) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/gemm.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Gemm Operator.""" import onnx from onnx_neural_compressor import constants, logger from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Gemm", mode=[constants.STATIC_QUANT]) class GemmOperator(base_op.Operator): """Gemm Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(GemmOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if len(node.input) == 3 and not quant_utils.find_by_name(node.input[2], self.quantizer.model.initializer()): logger.warning( "Bias of Gemm node '{}' is not constant. " "Exclude this node can get better performance.".format(node.name) ) if self.quantizer.quant_format != "qdq": return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0]) if self.per_channel and quant_utils.find_by_name(node.input[1], self.quantizer.model.initializer()): self.quantizer.quantize_weights_per_channel( node, [1], self.weight_dtype, self.weight_sym, 0 if quant_utils.is_B_transposed(node) else 1 ) else: self.quantizer.quantize_inputs(node, [1]) if len(node.input) == 3 and quant_utils.find_by_name(node.input[2], self.quantizer.model.initializer()): self.quantizer.quantize_bias_tensor(node) beta_attribute = [attr for attr in node.attribute if attr.name == "beta"] if len(beta_attribute): beta_attribute[0].f = 1.0 if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) qgemm_inputs = [] for parent in parents[:-1]: qgemm_inputs.extend(parent.input) qgemm_inputs.append(parents[-1].input[0]) kwargs = {} for attribute in node.attribute: if attribute.name != "beta": kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qgemm_output = node.output[0] if not self.disable_qdq_for_node_output: child = self.quantizer.model.get_children(node)[0] self.quantizer.remove_nodes.append(child) qgemm_output = child.output[0] qgemm_inputs.extend(child.input[1:]) qgemm_node = onnx.helper.make_node("QGemm", qgemm_inputs, [qgemm_output], node.name, **kwargs) self.quantizer.new_nodes.append(qgemm_node) self.quantizer.remove_nodes.extend(parents) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/lstm.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """LSTM Operator.""" import numpy import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="LSTM", mode=[constants.DYNAMIC_QUANT]) class LSTMOperator(base_op.Operator): """LSTM Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(LSTMOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" return def convert_check(self): """Check if conversion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[1]) or not self.quantizer.is_valid_quantize_weight( node.input[2] ): # pragma: no cover return False model = self.quantizer.model W = model.get_initializer(node.input[1]) R = model.get_initializer(node.input[2]) if len(W.dims) != 3 or len(R.dims) != 3: # pragma: no cover return False return True def convert(self): """Convert to QOperator format.""" node = self.node model = self.quantizer.model W = model.get_initializer(self.node.input[1]) R = model.get_initializer(self.node.input[2]) [W_num_dir, W_4_hidden_size, W_input_size] = W.dims [R_num_dir, R_4_hidden_size, R_hidden_size] = R.dims if self.per_channel: # pragma: no cover del W.dims[0] del R.dims[0] W.dims[0] = W_num_dir * W_4_hidden_size R.dims[0] = R_num_dir * R_4_hidden_size quant_input_weight_tuple = self.quantizer.quantize_weight_per_channel( node.input[1], self.weight_dtype, self.weight_sym, 0 ) quant_recurrent_weight_tuple = self.quantizer.quantize_weight_per_channel( node.input[2], self.weight_dtype, self.weight_sym, 0 ) W_quant_weight = model.get_initializer(quant_input_weight_tuple[0]) R_quant_weight = model.get_initializer(quant_recurrent_weight_tuple[0]) W_quant_array = onnx.numpy_helper.to_array(W_quant_weight) R_quant_array = onnx.numpy_helper.to_array(R_quant_weight) W_quant_array = numpy.reshape(W_quant_array, (W_num_dir, W_4_hidden_size, W_input_size)) R_quant_array = numpy.reshape(R_quant_array, (R_num_dir, R_4_hidden_size, R_hidden_size)) W_quant_array = numpy.transpose(W_quant_array, (0, 2, 1)) R_quant_array = numpy.transpose(R_quant_array, (0, 2, 1)) W_quant_tranposed = onnx.numpy_helper.from_array(W_quant_array, quant_input_weight_tuple[0]) R_quant_tranposed = onnx.numpy_helper.from_array(R_quant_array, quant_recurrent_weight_tuple[0]) model.remove_initializers([W_quant_weight, R_quant_weight]) model.add_initializer(W_quant_tranposed) model.add_initializer(R_quant_tranposed) W_quant_zp = model.get_initializer(quant_input_weight_tuple[1]) R_quant_zp = model.get_initializer(quant_recurrent_weight_tuple[1]) W_quant_scale = model.get_initializer(quant_input_weight_tuple[2]) R_quant_scale = model.get_initializer(quant_recurrent_weight_tuple[2]) if self.per_channel: # pragma: no cover W_quant_zp.dims[:] = [W_num_dir, W_4_hidden_size] R_quant_zp.dims[:] = [R_num_dir, R_4_hidden_size] W_quant_scale.dims[:] = [W_num_dir, W_4_hidden_size] R_quant_scale.dims[:] = [R_num_dir, R_4_hidden_size] inputs = [] input_len = len(node.input) inputs.extend([node.input[0]]) inputs.extend([quant_input_weight_tuple[0], quant_recurrent_weight_tuple[0]]) inputs.extend([node.input[3] if input_len > 3 else ""]) inputs.extend([node.input[4] if input_len > 4 else ""]) inputs.extend([node.input[5] if input_len > 5 else ""]) inputs.extend([node.input[6] if input_len > 6 else ""]) inputs.extend([node.input[7] if input_len > 7 else ""]) inputs.extend( [ quant_input_weight_tuple[2], quant_input_weight_tuple[1], quant_recurrent_weight_tuple[2], quant_recurrent_weight_tuple[1], ] ) kwargs = {} for attribute in node.attribute: if attribute.name == "layout": continue kwarg = quant_utils.attribute_to_kwarg(attribute) kwargs.update(kwarg) quant_lstm_name = node.name + "_quant" quant_lstm_node = onnx.helper.make_node( "DynamicQuantizeLSTM", inputs, node.output, quant_lstm_name, domain="com.microsoft", **kwargs ) self.quantizer.remove_nodes.append(node) self.quantizer.new_nodes.append(quant_lstm_node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/matmul.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """MatMul Operator.""" import onnx from onnx import onnx_pb as onnx_proto from onnx_neural_compressor import constants from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="MatMul", mode=[constants.DYNAMIC_QUANT]) class MatMulOperator(base_op.Operator): """MatMul Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(MatMulOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not all([self.quantizer.model.get_initializer(i) is None for i in node.input]): return True elif all([i not in self.quantizer.quantized_value_map for i in node.input]): return False else: return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0]) if self.per_channel and quant_utils.find_by_name(node.input[1], self.quantizer.model.initializer()): self.quantizer.quantize_weights_per_channel(node, [1], self.weight_dtype, self.weight_sym, 1) else: self.quantizer.quantize_inputs(node, [1]) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) inputs = [] quantized_name = [] scale = [] zp = [] for parent in parents: if parent.op_type == "DequantizeLinear": quantized_name.append(parent.input[0]) else: quantized_name.append(parent.output[0]) if parent.op_type == "DynamicQuantizeLinear": scale.append(parent.output[1]) zp.append(parent.output[2]) else: scale.append(parent.input[1]) zp.append(parent.input[2]) inputs.extend(quantized_name) inputs.extend(zp) matmul_integer_output = node.output[0] + "_output_quantized" matmul_integer_node = onnx.helper.make_node("MatMulInteger", inputs, [matmul_integer_output], node.name) self.quantizer.new_nodes.append(matmul_integer_node) # Add cast operation to cast matmulInteger output to float. cast_op_output = matmul_integer_output + "_cast_output" cast_node = onnx.helper.make_node( "Cast", [matmul_integer_output], [cast_op_output], matmul_integer_output + "_cast", to=onnx_proto.TensorProto.FLOAT, ) self.quantizer.new_nodes.append(cast_node) # Add mul operation to multiply scales of two inputs. scales_mul_op = node.name + "_scales_mul" scales_mul_node = quant_utils.find_by_name(scales_mul_op, self.quantizer.new_nodes) if scales_mul_node is None: scales_mul_node = onnx.helper.make_node("Mul", [scale[0], scale[1]], [scales_mul_op + ":0"], scales_mul_op) self.quantizer.new_nodes.append(scales_mul_node) scales_mul_op_output = scales_mul_node.output[0] # Add mul operation to multiply mul_scales_op result with output of MatMulInteger # and make the output of this node the same as output of original matmul node. output_scale_mul_op = node.name + "_output_scale_mul" self.quantizer.new_nodes.append( onnx.helper.make_node("Mul", [cast_op_output, scales_mul_op_output], [node.output[0]], output_scale_mul_op) ) if parents[1].op_type == "DequantizeLinear": self.quantizer.remove_nodes.append(parents[1]) self.quantizer.remove_nodes.append(node) @base_op.op_registry(op_types="MatMul", mode=[constants.STATIC_QUANT]) class StaticMatMulOperator(MatMulOperator): """MatMul Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(MatMulOperator, self).__init__(onnx_quantizer, onnx_node) def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0]) if self.per_channel and quant_utils.find_by_name(node.input[1], self.quantizer.model.initializer()): self.quantizer.quantize_weights_per_channel(node, [1], self.weight_dtype, self.weight_sym, 1) else: self.quantizer.quantize_inputs(node, [1]) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) if len(self.quantizer.model.get_children(node)) == 0 or not node.name.endswith("_quant"): # pragma: no cover return qlinear_matmul_inputs = [] if self.disable_qdq_for_node_output: for i in range(len(parents[0].input)): qlinear_matmul_inputs.extend([parent.input[i] for parent in parents]) qlinear_matmul_node = onnx.helper.make_node( "MatMulIntegerToFloat", qlinear_matmul_inputs, node.output, node.name, domain="com.microsoft" ) else: # after inserting QDQ, MatMul -> Q-DQ-MatMul-Q-DQ for parent in parents: qlinear_matmul_inputs.extend(parent.input) child = self.quantizer.model.get_children(node)[0] qlinear_matmul_output = child.output[0] qlinear_matmul_inputs.extend(child.input[1:]) qlinear_matmul_node = onnx.helper.make_node( "QLinearMatMul", qlinear_matmul_inputs, [qlinear_matmul_output], node.name ) self.quantizer.remove_nodes.append(child) self.quantizer.new_nodes.append(qlinear_matmul_node) self.quantizer.remove_nodes.append(node) # make sure parent DequantizeLinear of input 0 is not used by other ops if len(self.quantizer.model.get_children(parents[0])) == 1 and not self.quantizer.model.is_graph_output( parents[0].output[0] ): self.quantizer.remove_nodes.extend(parents) else: self.quantizer.remove_nodes.append(parents[1]) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/maxpool.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """MaxPool Operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="MaxPool", mode=[constants.STATIC_QUANT]) class MaxPoolOperator(base_op.Operator): """MaxPool Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(MaxPoolOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node # if opset version is less than 12, just no change if self.quantizer.opset_version < 12: # pragma: no cover return False if not self.quantizer.is_valid_quantize_weight(node.input[0]): # pragma: no cover return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(self.node, direct_int8=True) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0 or not node.name.endswith("_quant"): # pragma: no cover return False return True def convert(self): """Convert to QOperator format.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] children = self.quantizer.model.get_children(node) if parent.op_type != "DequantizeLinear" or all( [i.op_type != "QuantizeLinear" for i in children] ): # pragma: no cover return node.input[0] = parent.input[0] node.output[0] = node.output[0].replace("_QuantizeInput", "_quantized") for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) for n in self.quantizer.model.get_children(child): self.quantizer.model.replace_node_input(n, child.output[0], node.output[0]) self.quantizer.remove_nodes.append(parent) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/pad.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Pad Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Pad", mode=[constants.STATIC_QUANT]) class PadOperator(base_op.Operator): """Pad Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(PadOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" # if opset version is less than 11, just no change if self.quantizer.opset_version < 11: # pragma: no cover return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0]) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(node) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node children = self.quantizer.model.get_children(node) if len(children) == 0 or not node.name.endswith("_quant"): # pragma: no cover return False return True def convert(self): """Convert to QOperator format.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] child = self.quantizer.model.get_children(node)[0] kwargs = {} for attribute in node.attribute: kv = quant_utils.attribute_to_kwarg(attribute) kwargs.update(kv) if "mode" not in kwargs or kwargs["mode"] == b"constant": if len(node.input) > 2: # There is 3rd input 'constant_value' zp_tensor = self.quantizer.model.get_initializer(parent.input[2]) scale_tensor = self.quantizer.model.get_initializer(parent.input[1]) padding_constant_initializer = self.quantizer.model.get_initializer(node.input[2]) if padding_constant_initializer is not None: zp_array = onnx.numpy_helper.to_array(zp_tensor) zp_value = zp_array.item() if zp_array.ndim == 0 else zp_array[0] scale_array = onnx.numpy_helper.to_array(scale_tensor) scale_value = scale_array.item() if scale_array.ndim == 0 else scale_array[0] padding_constant_array = onnx.numpy_helper.to_array(padding_constant_initializer) quantized_padding_constant_array = quant_utils.quantize_nparray( onnx.helper.tensor_dtype_to_np_dtype(self.weight_dtype), padding_constant_array, scale_value, zp_value, ) quantized_padding_constant_name = node.input[2] + "_quantized" quantized_padding_constant_initializer = onnx.numpy_helper.from_array( quantized_padding_constant_array, quantized_padding_constant_name ) # Suppose this padding constant initializer only used by the node self.quantizer.model.remove_initializer(padding_constant_initializer) self.quantizer.model.add_initializer(quantized_padding_constant_initializer) node.input[2] = quantized_padding_constant_name else: self.quantizer.quantize_inputs(node, [2], False) node.input[2] = node.input[2] + "_DequantizeLinear" else: # pad zero_point for original zero node.input.extend([parent.input[2]]) # Create an entry for output quantized value node.input[0] = parent.input[0] node.output[0] = child.output[0] self.quantizer.remove_nodes.extend([parent, child]) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/pooling.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """AveragePool Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="AveragePool", mode=[constants.STATIC_QUANT]) class PoolOperator(base_op.Operator): """AveragePool Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(PoolOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node super().quantize() node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if len(children) == 0 or len(parents) == 0 or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if all([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): qlinear_output_name = node.output[0] + "_quantized" inputs = [] inputs.extend(parents[0].input) inputs.extend([i for i in children if i.op_type == "QuantizeLinear"][0].input[1:]) kwargs = {} for attribute in node.attribute: kwargs.update(quant_utils.attribute_to_kwarg(attribute)) kwargs["domain"] = quant_utils.ms_domain qnode = onnx.helper.make_node("QLinear" + node.op_type, inputs, [qlinear_output_name], node.name, **kwargs) self.quantizer.remove_nodes.extend(parents) for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], qnode.output[0]) self.quantizer.new_nodes.append(qnode) self.quantizer.remove_nodes.append(node) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/reduce.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Reduce Operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry( op_types="ReduceMean, ReduceLogSum, ReduceLogSumExp, " "ReduceL1, ReduceL2, ReduceProd, ReduceSum, ReduceSumSquare", mode=[constants.STATIC_QUANT], ) class ReduceOperator(base_op.Operator): """Reduce Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ReduceOperator, self).__init__(onnx_quantizer, onnx_node) @base_op.op_registry(op_types="ReduceMax, ReduceMin", mode=[constants.STATIC_QUANT]) class ReduceMinMaxOperator(base_op.Operator): """ReduceMin and ReduceMax Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ReduceMinMaxOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(self.node, [0], direct_int8=True) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if (len(children) == 0 and len(parents) == 0) or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): for parent in parents: if parent.op_type == "DequantizeLinear": self.node.input[0] = parent.input[0] self.quantizer.remove_nodes.append(parents[0]) break for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") node.output[0] = node.output[0] + "_quantized" ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/resize.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Resize Operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Resize", mode=[constants.STATIC_QUANT]) class ResizeOperator(base_op.Operator): """Resize Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(ResizeOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node # if version is less than 11, just keep this node if self.quantizer.opset_version < 11: return False if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0], direct_int8=True) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if (len(children) == 0 and len(parents) == 0) or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): for parent in parents: if parent.op_type == "DequantizeLinear" and parent.output[0] == node.input[0]: self.node.input[0] = parent.input[0] self.quantizer.remove_nodes.append(parent) break for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") node.output[0] = node.output[0] + "_quantized" ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/split.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Split Operator.""" import onnx from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Split", mode=[constants.STATIC_QUANT]) class SplitOperator(base_op.Operator): """Split Operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(SplitOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node data_found, _, _, _, _ = self.quantizer._get_quantization_params(node.output[0]) if not data_found: return False if not all([self.quantizer.is_valid_quantize_weight(i) for i in node.input]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(node, [0]) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] children = self.quantizer.model.get_children(node) if ( parent.op_type != "DequantizeLinear" or len(children) == 0 or not node.name.endswith("_quant") ): # pragma: no cover return False return True def convert(self): """Convert to QOperator format.""" node = self.node parent = self.quantizer.model.get_parents(node)[0] kwargs = {} for attribute in node.attribute: # pragma: no cover kwargs.update(quant_utils.attribute_to_kwarg(attribute)) quantized_input_names = [] quantized_input_names.append(parent.input[0]) if len(node.input) > 1: # pragma: no cover quantized_input_names.extend(node.input[1:]) outputs = [] input_name_to_nodes = self.quantizer.model.input_name_to_nodes() for output in node.output: if output in input_name_to_nodes: child = input_name_to_nodes[output][0] if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) outputs.append(child.output[0]) else: # pragma: no cover outputs.append(output) else: # pragma: no cover outputs.append(output + "_quantized") quantized_node = onnx.helper.make_node(node.op_type, quantized_input_names, outputs, node.name, **kwargs) self.quantizer.new_nodes.append(quantized_node) self.quantizer.remove_nodes.extend([parent, node]) ================================================ FILE: onnx_neural_compressor/algorithms/post_training_quant/operators/unary_op.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Unary operator.""" from onnx_neural_compressor import constants, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.post_training_quant.operators import base_op @base_op.op_registry(op_types="Exp, Log, Round, Sqrt", mode=[constants.STATIC_QUANT]) class UnaryOperator(base_op.Operator): """Unary operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(UnaryOperator, self).__init__(onnx_quantizer, onnx_node) @base_op.op_registry(op_types="Abs, Shrink, Sign", mode=[constants.STATIC_QUANT]) class UnaryDirect8BitOperator(base_op.Operator): """Unary operator.""" def __init__(self, onnx_quantizer, onnx_node): """Initialization.""" super(UnaryDirect8BitOperator, self).__init__(onnx_quantizer, onnx_node) def quantize_check(self): """Check if quantizaion can be done.""" node = self.node if not self.quantizer.is_valid_quantize_weight(node.input[0]): return False return True def quantize(self): """Do quantizaion.""" node = self.node self.quantizer.quantize_inputs(self.node, [0], direct_int8=True) if not self.disable_qdq_for_node_output: self.quantizer.quantize_outputs(self.node, direct_int8=True) node.name = node.name + "_quant" def convert_check(self): """Check if conversion can be done.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if (len(children) == 0 and len(parents) == 0) or not node.name.endswith("_quant"): return False return True def convert(self): """Convert to QOperator format.""" node = self.node parents = self.quantizer.model.get_parents(node) children = self.quantizer.model.get_children(node) if any([i.op_type == "DequantizeLinear" for i in parents]) and any( [i.op_type == "QuantizeLinear" for i in children] ): for parent in parents: if parent.op_type == "DequantizeLinear": self.node.input[0] = parent.input[0] self.quantizer.remove_nodes.append(parents[0]) break for child in children: if child.op_type == "QuantizeLinear": self.quantizer.remove_nodes.append(child) self.quantizer.model.replace_input_of_all_nodes(child.output[0], node.output[0] + "_quantized") node.output[0] = node.output[0] + "_quantized" ================================================ FILE: onnx_neural_compressor/algorithms/smoother/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: onnx_neural_compressor/algorithms/smoother/calibrator.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Calibration for smooth quant.""" import importlib.util import pathlib import sys import tempfile from typing import List import numpy as np import onnx import onnxruntime from onnx_neural_compressor import data_reader, logger, onnx_model, utility class Calibrator: """Dump information for smooth quant.""" def __init__( self, model: onnx_model.ONNXModel, dataloader: data_reader.CalibrationDataReader, iterations: List[int] = [], providers: List[str] = ["CPUExecutionProvider"], **kwargs, ): """Initialize a Calibrator to dump information. Args: model (onnx_model.ONNXModel): onnx_model.ONNXModel object. dataloader (data_reader.CalibrationDataReader): user implemented object to read in and preprocess calibration dataset. iterations (List[int], optional): tensor of which iteration will be collected. Defaults to []. providers (List[str], optional): execution provider for onnxruntime. Defaults to ["CPUExecutionProvider"]. """ self.model_wrapper = model self.dataloader = dataloader self.augmented_model = None self.iterations = iterations self.providers = providers def _check_is_group_conv(self, node): """Check the op is group wised or not(depthwise conv is excluded,return false). Args: node: The op node Returns: Bool: group wised True, otherwise False, depthwise False """ name_to_indices = {} for index, i in enumerate(self.model_wrapper.initializer()): name_to_indices[i.name] = index if node.op_type == "Conv": group = 1 for attr in node.attribute: if hasattr(attr, "name"): if attr.name == "group": group = attr.i break # currently only normal conv and depthwise conv are supported if group > 1: # group conv, need to check depthwise or not weight_name = node.input[1] weight_shape = onnx.numpy_helper.to_array( self.model_wrapper.initializer()[name_to_indices[weight_name]] ).shape input_channel = weight_shape[1] if input_channel != 1: # TODO: need to double check return True return False def _get_input_tensor_of_ops(self, op_types: List[str] = ["MatMul", "Gemm", "Conv", "FusedConv"]): """Traverse the graph and get all the data tensors flowing into layers of {op_types}. Group conv is excluded. # TODO: the tensors could be set/filtered in configuration. Args: op_types (List[str], optional): The op types whose input tensor will be dumped. Defaults to ["MatMul", "Gemm", "Conv", "FusedConv"]. Returns: dict: A dict of dumped tensor to node info """ tensors_to_node = {} initializers = {i.name: i for i in self.model_wrapper.initializer()} for node in self.model_wrapper.nodes(): if len(op_types) == 0 or node.op_type in op_types: if node.op_type in ["Conv", "FusedConv"] and self._check_is_group_conv(node): continue # also need to check whether the layer has weight if len(node.input) >= 2 and node.input[1] in initializers.keys(): tensors_to_node.setdefault(node.input[0], []).append([node.name, node.input, node.output]) return tensors_to_node def _get_max_per_channel(self, datas, percentile): """Get the max values per input channel. Args: datas: The tensors percentile: percentile of calibration to remove outliers Returns: The max values per input channel """ permute_datas = [] for data in datas: if len(data.shape) == 3: # TODO: mammul batchsize*seq*inchannel, conv:batchsize*inchannle*f*f tensor = np.abs(np.reshape(data, (-1, data.shape[-1]))) permute_datas.append(tensor) elif len(data.shape) == 4: tensor = np.swapaxes(data, 1, -1) tensor = np.abs(np.reshape(tensor, (-1, tensor.shape[-1]))) permute_datas.append(tensor) elif len(data.shape) == 2: permute_datas.append(np.abs(data)) else: assert False, "not supported" permute_datas = np.stack(permute_datas, axis=0) permute_datas = permute_datas.reshape(-1, permute_datas.shape[-1]) max_per_channels = np.percentile(permute_datas, percentile, axis=0) max_per_channels = max_per_channels.astype(np.single) return max_per_channels def get_intermediate_outputs(self): so = onnxruntime.SessionOptions() if sys.version_info < (3, 11) and importlib.util.find_spec("onnxruntime_extensions"): # pragma: no cover from onnxruntime_extensions import get_library_path so.register_custom_ops_library(get_library_path()) providers = self.providers if "TensorrtExecutionProvider" not in self.providers else ["CUDAExecutionProvider"] if self.model_wrapper.is_large_model: # pragma: no cover with tempfile.TemporaryDirectory(prefix="ort.calib.") as tmp_dir: onnx.save_model( self.model_wrapper.model, pathlib.Path(tmp_dir).joinpath("augment.onnx").as_posix(), save_as_external_data=True, all_tensors_to_one_file=True, convert_attribute=False, ) session = onnxruntime.InferenceSession( pathlib.Path(tmp_dir).joinpath("augment.onnx").as_posix(), so, providers=providers ) onnx.external_data_helper.load_external_data_for_model( self.model_wrapper.model, pathlib.Path(tmp_dir).as_posix() ) else: session = onnxruntime.InferenceSession( self.model_wrapper.model.SerializeToString(), so, providers=providers ) node_output_names = [output.name for output in session.get_outputs()] output_dicts = {} input_name_to_nodes = self.model_wrapper.input_name_to_nodes() output_name_to_node = self.model_wrapper.output_name_to_node() name_to_node = {} for data_name in node_output_names: node = None if data_name in output_name_to_node: node = output_name_to_node[data_name] elif data_name in input_name_to_nodes: node = input_name_to_nodes[data_name][0] assert node, "{} is neither an input nor an output of nodes in augmented model.".format(data_name) name_to_node[data_name] = node.name def _collect_data(ort_inputs): for output_idx, output in enumerate(session.run(None, ort_inputs)): output_dicts.setdefault(node_output_names[output_idx], []).append(output) idx = 0 while True: inputs = self.dataloader.get_next() if not inputs: break if self.iterations != []: if idx > max(self.iterations): break if idx in self.iterations: _collect_data(inputs) else: _collect_data(inputs) idx += 1 return output_dicts def calib_smooth(self, op_types, percentile: float = 99.999): """Smooth model calibration. Mainly get the max info per channel of input tensors. Args: op_types (_type_): The op types whose input tensor will be dumped. percentile (float, optional): Percentile of calibration to remove outliers. Defaults to 99.999. Returns: max_vals_per_channel: max values per channel of input tensors shape_infos: The shape information of input tensors """ logger.info("Start smooth model calibration.") # add the input tensors of {op_types} to outputs of the model tensors_to_node = self._get_input_tensor_of_ops(op_types) self.model_wrapper.add_tensors_to_outputs(tensors_to_node.keys()) output_dicts = self.get_intermediate_outputs() # remove the input tensors of {op_types} to outputs of the model self.model_wrapper.remove_tensors_from_outputs(tensors_to_node.keys()) max_vals_per_channel = {} shape_infos = {} for key, val in tensors_to_node.items(): max_val_per_channel = self._get_max_per_channel(output_dicts[key], percentile=percentile) max_vals_per_channel[key] = max_val_per_channel shape_infos[key] = output_dicts[key][0].shape for item in val: shape_infos[item[1][1]] = self.model_wrapper.get_initializer(item[1][1]).dims return max_vals_per_channel, shape_infos, tensors_to_node ================================================ FILE: onnx_neural_compressor/algorithms/smoother/core.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Smoother for onnxrt.""" import copy import os import pathlib import numpy as np import onnx import onnxruntime as ort from onnx_neural_compressor import data_reader, logger, onnx_model, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.smoother import calibrator from typing import List, Union # isort: skip def _get_quant_dequant_output(model, input_data, output_data, providers): """Get loss between fp32 output and QDQ output. Args: model (object): model input_data (numpy.ndarray): fp32 input output_data (numpy.ndarray): fp32 output providers (list): execution provider """ input_data = quant_utils.qdq_data(input_data, 2, False) sess = ort.InferenceSession(model.SerializeToString(), providers=providers) preds = sess.run(None, {model.graph.input[0].name: input_data}) loss = np.sum(np.abs(output_data - preds) ** 2) return loss def _make_sub_graph(node, inits, input_data, output_data, opset, ir_version): """Build a model with the specific node. Args: node (object): node inits (list): initializer inputs of this node input_data (numpy.ndarray): fp32 input output_data (numpy.ndarray): fp32 output opset (object): opset of the model ir_version (object): ir_version of the model """ input = onnx.helper.make_tensor_value_info( node.input[0], onnx.helper.np_dtype_to_tensor_dtype(input_data.dtype), input_data.shape, ) output = onnx.helper.make_tensor_value_info( node.output[0], onnx.helper.np_dtype_to_tensor_dtype(output_data.dtype), output_data.shape, ) graph = onnx.helper.make_graph([node], "sub_graph", [input], [output], inits) model = onnx.helper.make_model(graph, opset_imports=opset) model.ir_version = ir_version return model class Smoother: """Fake input channel quantization. For more details please refer to: [1] SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models [2] SPIQ: Data-Free Per-Channel Static Input Quantization We only support inplace mode which means the model weights will be changed, you can call recover function to recover the weights if needed. """ def __init__( self, model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], dataloader: data_reader.CalibrationDataReader, execution_provider: str = "CPUExecutionProvider", ): """Initialize the attributes of class.""" self.model = ( model if isinstance(model, onnx_model.ONNXModel) else onnx_model.ONNXModel(model, load_external_data=True) ) self.value_infos = {vi.name: vi for vi in self.model.model.graph.value_info} self.value_infos.update({ot.name: ot for ot in self.model.model.graph.output}) self.value_infos.update({it.name: it for it in self.model.model.graph.input}) self.dataloader = dataloader self.providers = [execution_provider] self.tensor_scales_info = {} self.new_added_mul_nodes = [] self.new_added_value_info = [] self.new_init_tensors = [] # scales_tensor self.scales_per_op = True self.replace_input = [] self.ops_to_absorb = [] self.max_vals_per_channel = None self.shape_info = None self.tensors_to_node = None self._build_absorb_function() def transform( self, alpha: Union[float, str] = 0.5, folding: bool = True, percentile: float = 99.999, op_types: List[str] = ["Gemm", "Conv", "MatMul", "FusedConv"], scales_per_op: bool = True, calib_iter: int = 100, auto_alpha_args: dict = {"alpha_min": 0.3, "alpha_max": 0.7, "alpha_step": 0.05, "attn_method": "min"}, *args, **kwargs ): """The main entry of smooth quant. Args: alpha (float, optional): alpha value to balance the quantization difficulty of activation and weight. Defaults to 0.5. folding (bool, optional): whether fold those foldable Mul which are inserted for smooth quant. Defaults to True. percentile (float, optional): percentile of calibration to remove outliers. Defaults to 99.999. op_types (list, optional): the op type to be smooth quantized. Defaults to ["Gemm", "Conv", "MatMul", "FusedConv"]. scales_per_op (bool, optional): True, each op will have an individual scale, mainlyfor accuracy False, ops with the same input will share a scale, mainly for performance. Defaults to True. calib_iter (int, optional): iteration num for calibration. Defaults to 100. auto_alpha_args (_type_, optional): alpha args for auto smooth. Defaults to {"alpha_min": 0.3, "alpha_max": 0.7, "alpha_step": 0.05, "attn_method": "min"}. Returns: onnx.ModelProto: A FP32 model with the same architecture as the orig model but with different weight which will be benefit to quantization """ self.scales_per_op = scales_per_op self.clean() if isinstance(alpha, float) and (alpha < 0 or alpha > 1): logger.warning("alpha should be a float value in [0, 1] or 'auto' ") if alpha < 0: alpha = 0 logger.warning("reset alpha to 0 ") elif alpha > 1.0: alpha = 1.0 logger.warning("reset alpha to 1.0 ") self._dump_op_info(percentile, op_types, calib_iter) if alpha == "auto": alpha = self._auto_tune_alpha(calib_iter, **auto_alpha_args) scales = self._get_smooth_scales(alpha) self._insert_smooth_mul_op(scales) self._adjust_weights(scales) self.model.add_nodes(self.new_added_mul_nodes) self.model.model.graph.value_info.extend(self.new_added_value_info) self.model.add_initializers(self.new_init_tensors) for node, old_input_name, new_input_name in self.replace_input: self.model.replace_node_input(node, old_input_name, new_input_name) self.model.update() if folding: self._fold_scale(scales) self.model.topological_sort() self.model.remove_unused_nodes() return self.model.model def _dump_op_info(self, percentile, op_types, iterations): """Dump op info for smooth quant. Args: percentile (float): percentile of calibration to remove outliers op_types (list): the op type to be smooth quantized iterations (int): iterations """ sq_calibrator = calibrator.Calibrator( self.model, self.dataloader, iterations=list(range(0, iterations)), execution_provider=self.providers, ) self.max_vals_per_channel, self.shape_info, self.tensors_to_node = sq_calibrator.calib_smooth( op_types, percentile ) for node in self.model.nodes(): for out in node.output: if ( out in self.tensors_to_node and node.op_type in self.could_absorb_optype and self.model.get_initializer(node.input[1]) is not None ): self.ops_to_absorb.append(node.name) def recover(self): """Recover the model weights.""" for tensor_name, nodes in self.tensors_to_node.items(): for node_info in nodes: key = node_info[0] if self.scales_per_op else tensor_name if key not in self.tensor_scales_info: continue input = node_info[1][1] weight = onnx.numpy_helper.to_array( self.model.get_initializer(input), base_dir=os.path.dirname(self.model.model_path) if self.model.model_path is not None else "", ) scale = self.tensor_scales_info[key] new_weight = weight * scale self.model.set_initializer(input, new_weight) for node, old_input_name, new_input_name in self.replace_input: self.model.replace_node_input(node, new_input_name, old_input_name) for value_info in self.new_added_value_info: self.model.model.graph.value_info.remove(value_info) self.model.remove_nodes(self.new_added_mul_nodes) self.model.remove_initializers(self.new_init_tensors) self.tensor_scales_info = {} self.new_added_mul_nodes = [] self.new_init_tensors = [] self.new_added_value_info = [] self.replace_input = [] def clean(self): """Clean data collected from calibration.""" self.tensor_scales_info = {} self.new_added_mul_nodes = [] self.new_init_tensors = [] self.new_added_value_info = [] self.replace_input = [] def _build_absorb_function(self): """Build function mapping for scale folding.""" def norm(node, scale): # pragma: no cover for idx in [1, 2]: tensor = self.model.get_initializer(node.input[idx]) new_tensor = ( onnx.numpy_helper.to_array(tensor, os.path.dirname(self.model.model_path)) * scale if self.model.model_path is not None else onnx.numpy_helper.to_array(tensor) * scale ) self.model.set_initializer(node.input[idx], new_tensor) self.tensor_scales_info[node.input[idx]] = ( 1.0 / scale if node.input[idx] not in self.tensor_scales_info else self.tensor_scales_info[node.input[idx]] * 1.0 / scale ) return True def mul(node, scale): # pragma: no cover if all([self.model.get_initializer(inp) is None for inp in node.input]): return False for inp in node.input: if self.model.get_initializer(inp) is not None: key = node.input[0].split("_smooth_output")[0] tensor = self.model.get_initializer(inp) new_tensor = ( onnx.numpy_helper.to_array(tensor, os.path.dirname(self.model.model_path)) * scale if self.model.model_path is not None else onnx.numpy_helper.to_array(tensor) * scale ) # set_initializer requires the dims of old & new initializers are same # Mul operator has broadcast mechanism self.model.remove_initializer(tensor) self.model.add_initializer( onnx.helper.make_tensor( inp, tensor.data_type, list(new_tensor.shape), new_tensor.flatten().tolist() ) ) self.tensor_scales_info[key] = ( 1.0 / scale if key not in self.tensor_scales_info else 1.0 / scale * self.tensor_scales_info[key] ) return True def conv(node, scale): # pragma: no cover if len(node.input) > 2: if self.model.get_initializer(node.input[2]) is not None: tensor = self.model.get_initializer(node.input[2]) new_tensor = ( onnx.numpy_helper.to_array(tensor, os.path.dirname(self.model.model_path)) * scale if self.model.model_path is not None else onnx.numpy_helper.to_array(tensor) * scale ) self.model.set_initializer(node.input[2], new_tensor) self.tensor_scales_info[node.input[2]] = 1.0 / scale scale = scale.reshape(-1, 1, 1, 1) tensor = self.model.get_initializer(node.input[1]) new_tensor = ( onnx.numpy_helper.to_array(tensor, os.path.dirname(self.model.model_path)) * scale if self.model.model_path is not None else onnx.numpy_helper.to_array(tensor) * scale ) self.model.set_initializer(node.input[1], new_tensor) self.tensor_scales_info[node.input[1]] = ( 1.0 / scale if node.input[1] not in self.tensor_scales_info else self.tensor_scales_info[node.input[1]] * 1.0 / scale ) return True self.could_absorb_optype = { "LayerNormalization": norm, "BatchNormalization": norm, "InstanceNormalization": norm, "SimplifiedLayerNormalization": mul, "MatMul": mul, "Gemm": mul, "Conv": conv, "FusedConv": conv, "Mul": mul, } def _fold_scale(self, scales): """Absorb the scale to the operator at output channel. Args: scales (dict): scales for smooth quant, {tensor_name: smooth quant scale} """ remove_nodes = [] for node in self.model.nodes(): if node.op_type == "Mul" and node.name.endswith("_smooth_mul") and node not in remove_nodes: parent = self.model.get_parent(node, 0) if parent is None: continue if parent.op_type in self.could_absorb_optype and len(self.model.get_children(parent)) == 1: if node.output[0].split("_smooth_output")[0] in scales: if self.could_absorb_optype[parent.op_type]( parent, 1.0 / scales[node.output[0].split("_smooth_output")[0]] ): remove_nodes.append(node) children = [i for i in self.model.nodes() if node.output[0] in i.input] for child in children: for idx, inp in enumerate(child.input): if inp == node.output[0]: child.input[idx] = node.input[0] self.model.remove_nodes(remove_nodes) def _get_output_loss(self, node_name, scale, calib_iter): """Get output loss of specific node after inserting QDQ pair. Args: node_name (str): node name scale (float): scale of the specific node calib_iter (int): iterations """ node = [i for i in self.model.nodes() if i.name == node_name] loss = 0 if len(node) > 0: node = node[0] orig_outputs = self.model.output() added_tensors = [node.input[0], node.output[0]] self.model.add_tensors_to_outputs(added_tensors) session = ( ort.InferenceSession(self.model.model_path + "_augment.onnx", providers=self.providers) if self.model.is_large_model else ort.InferenceSession(self.model.model.SerializeToString(), providers=self.providers) ) base_dir = "" if not self.model.is_large_model else os.path.dirname(self.model.model_path) weight = onnx.numpy_helper.to_array(self.model.get_initializer(node.input[1]), base_dir) weight_q = quant_utils.qdq_data(weight, 3, True) self.model.set_initializer(node.input[1], weight_q) inits = [self.model.get_initializer(i) for i in node.input if self.model.get_initializer(i) is not None] model = None idx = 1 while True: inputs = self.dataloader.get_next() if not inputs: break if idx > calib_iter: break outputs = session.run(added_tensors, inputs) if model is None: model = _make_sub_graph( node, inits, outputs[0], outputs[1], self.model.model.opset_import, self.model.model.ir_version, ) loss += _get_quant_dequant_output(model, outputs[0] * scale, outputs[1], self.providers) self.model.remove_tensors_from_outputs([i for i in added_tensors if i not in orig_outputs]) self.model.set_initializer(node.input[1], weight) return loss def _reshape_scale_for_input(self, tensor, key): """Reshape the scale for input feature in channel. Args: tensor (str): tensor name key (str): scale key of this tensor """ if len(self.shape_info[tensor]) == 4: scale = np.reshape(self.tensor_scales_info[key], (1, self.tensor_scales_info[key].shape[1], 1, 1)) else: scale = np.reshape(self.tensor_scales_info[key], (1, self.tensor_scales_info[key].shape[0])) return scale def _auto_tune_alpha( self, calib_iter, alpha_min: float = 0.3, alpha_max: float = 0.7, alpha_step: float = 0.05, attn_method: str = "min", ): """Perform alpha-tuning to obtain layer-wise optimal alpha values and adjust parameters accordingly. Args: calib_iter (int): iterations alpha_min (float): min value of alpha search space. alpha_max (float): max value of alpha search space. alpha_step (float): step size of alpha search space. attn_method (str): criterion method used on attention ops; currently min, max and mean are supported. """ logger.info("auto tuning alpha") alpha_space = np.arange(alpha_min, alpha_max, alpha_step).tolist() optimal_alphas = {} if self.model.is_large_model: onnx.save_model( self.model.model, self.model.model_path + "_augment.onnx", save_as_external_data=True, all_tensors_to_one_file=True, location="weights.pb", convert_attribute=False, ) ## Searching optimal alphas for tensor_name, node_infos in self.tensors_to_node.items(): for node_info in node_infos: loss_alpha = {} key = node_info[0] if self.scales_per_op else tensor_name node = self.model.get_node(node_info[0]) for alpha in alpha_space: scale = self._get_smooth_scales(alpha, [key]) self._adjust_weights(scale) input_scale = ( self._reshape_scale_for_input(tensor_name, key) if not (node.op_type == "Gemm" and quant_utils.is_B_transposed(node)) else self.tensor_scales_info[key] ) loss = self._get_output_loss(node_info[0], input_scale, calib_iter) loss_alpha[alpha] = loss if key not in optimal_alphas: # Update alpha results optimal_alphas[key] = alpha else: optimal_alphas[key] = ( alpha if optimal_alphas[key] in loss_alpha and loss < loss_alpha[optimal_alphas[key]] else optimal_alphas[key] ) self.recover() logger.info("auto tuning alpha done") if self.model.is_large_model: onnx.external_data_helper.load_external_data_for_model( self.model.model, os.path.split(self.model.model_path)[0] ) os.remove(self.model.model_path + "_augment.onnx") os.remove(os.path.join(os.path.dirname(self.model.model_path), "weights.pb")) return optimal_alphas def _get_smooth_scales(self, alpha, target_list=[]): """Get the smooth scales for. The ops with the same input will share one mul layer. TODO support individual scales for each layer. Args: alpha: smooth alpha in paper target_list: target objects to get scale, [] means get all scales Returns: the smooth scales for weights, currently one input tensor only have one scale """ scales = {} for tensor, nodes in self.tensors_to_node.items(): # if scales_per_op the key of scales is the node name, otherwise the activation of node if self.scales_per_op: for node_info in nodes: node = self.model.get_node_by_weight(node_info[1][1]) if len(target_list) > 0 and node_info[0] not in target_list: continue weight = onnx.numpy_helper.to_array( self.model.get_initializer(node_info[1][1]), base_dir=os.path.dirname(self.model.model_path) if self.model.model_path is not None else "", ) if (len(weight.shape) == 4 and weight.shape[1] != 1) or ( node.op_type == "Gemm" and quant_utils.is_B_transposed(node) ): weight = np.moveaxis(weight, 0, 1) specific_alpha = alpha[node_info[0]] if isinstance(alpha, dict) else alpha scales[node_info[0]] = self._get_smooth_scale(weight, specific_alpha, tensor) else: if len(target_list) > 0 and tensor not in target_list: continue weights_in_channel_max = [] for node_info in nodes: node = self.model.get_node_by_weight(node_info[1][1]) weight = onnx.numpy_helper.to_array( self.model.get_initializer(node_info[1][1]), base_dir=os.path.dirname(self.model.model_path) if self.model.model_path is not None else "", ) if (len(weight.shape) == 4 and weight.shape[1] != 1) or ( node.op_type == "Gemm" and quant_utils.is_B_transposed(node) ): weight = np.moveaxis(weight, 0, 1) weight = weight.reshape(weight.shape[0], -1) cur_max = np.amax(weight, axis=-1) weights_in_channel_max.append(cur_max) weights_stack = np.stack(weights_in_channel_max, axis=-1) specific_alpha = alpha[tensor] if isinstance(alpha, dict) else alpha scales[tensor] = self._get_smooth_scale(weights_stack, specific_alpha, tensor) return scales def _get_smooth_scale(self, weights, specific_alpha, tensor): """Get smooth scale for specific weight. Args: weights (numpy.ndarray): weight data specific_alpha (float): current alpha for this weights tensor (str): tensor name """ weights = np.abs(weights.reshape(weights.shape[0], -1)) weights_max = np.amax(weights, axis=-1) input_power = np.power(self.max_vals_per_channel[tensor], specific_alpha) weight_power = np.power(weights_max, 1 - specific_alpha) weight_power = np.clip(weight_power, a_min=1e-5, a_max=None) scale = np.clip(input_power / weight_power, a_min=1e-5, a_max=None) return scale def _insert_smooth_mul_op(self, scales): """Insert the Mul after inupt. The ops with the same input will share one mul layer. Args: scales (dict): The smooth scales """ for key in scales.keys(): input_name = key if not self.scales_per_op else self.model.get_node(key).input[0] weight_name = ( self.tensors_to_node[key][0][1][1] if not self.scales_per_op else self.model.get_node(key).input[1] ) scale_factor = 1.0 / scales[key] if ( len(self.shape_info[weight_name]) == 3 or len(self.shape_info[weight_name]) == 2 ): # the last dim is input channel pass elif len(self.shape_info[weight_name]) == 4: scale_factor = np.reshape(scale_factor, (1, -1, 1, 1)) else: assert False, "not support" name = key + "_" + "smooth_scale" scale_tensor = onnx.helper.make_tensor( name=key + "_" + "smooth_scale", data_type=onnx.TensorProto.FLOAT, dims=scale_factor.shape, vals=scale_factor.flatten().tolist(), ) self.new_init_tensors.append(scale_tensor) mul_output_name = key + "_smooth_output" mul_node = onnx.helper.make_node( "Mul", inputs=[input_name, key + "_" + "smooth_scale"], outputs=[mul_output_name], name=key + "_smooth_mul", ) self.new_added_mul_nodes.append(mul_node) if input_name in self.value_infos: value_info = copy.deepcopy(self.value_infos[input_name]) value_info.name = mul_node.output[0] self.new_added_value_info.append(value_info) if self.scales_per_op: self.replace_input.append([self.model.get_node(key), input_name, mul_output_name]) else: for node_info in self.tensors_to_node[key]: self.replace_input.append([self.model.get_node(node_info[0]), key, mul_output_name]) def _adjust_weights(self, scales): """Adjust the weights with scale. Args: scales (dict): The input scales """ for idx, (tensor_name, nodes) in enumerate(self.tensors_to_node.items()): utility.simple_progress_bar(len(self.tensors_to_node), idx + 1) for node_info in nodes: key = node_info[0] if self.scales_per_op else tensor_name if key not in scales: continue input = node_info[1][1] node = self.model.get_node_by_weight(input) weight = onnx.numpy_helper.to_array( self.model.get_initializer(input), base_dir=os.path.dirname(self.model.model_path) if self.model.model_path is not None else "", ) if len(weight.shape) == 2: scale = ( np.expand_dims(scales[key], axis=0) if node.op_type == "Gemm" and quant_utils.is_B_transposed(node) else np.expand_dims(scales[key], axis=-1) ) new_weight = weight * scale elif len(weight.shape) == 4: # TODO need to check conv node = self.model.get_node_by_weight(input) if ( weight.shape[1] == 1 and "group" in [i.name for i in node.attribute] and [i for i in node.attribute if i.name == "group"][0].i > 1 ): scale = np.reshape(scales[key], (-1, 1, 1, 1)) else: scale = np.reshape(scales[key], (1, -1, 1, 1)) new_weight = weight * scale else: assert False, "not support" self.tensor_scales_info[key] = 1.0 / scale new_tensor = onnx.numpy_helper.from_array(new_weight, input) self.model.get_initializer(input).CopyFrom(new_tensor) ================================================ FILE: onnx_neural_compressor/algorithms/weight_only/__init__.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: onnx_neural_compressor/algorithms/weight_only/awq.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import os import pathlib import numpy as np import onnx import onnxruntime as ort from packaging import version from onnx_neural_compressor import constants, data_reader, logger, onnx_model from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.weight_only import rtn from typing import List, Union # isort: skip def _get_weight_scale(weight, group_size): """Get the scale of weight.""" org_shape = weight.shape weight = np.reshape(weight, (-1, group_size)) if group_size != -1 else weight scale = np.mean(np.reshape(np.abs(weight) / np.max(np.abs(weight), axis=1, keepdims=True), org_shape), axis=0) return scale def _apply_awq_scale(model, weight_config, absorb_pairs, output_dicts): """Apply scale for salient weight.""" best_scales = {} new_init_tensors = [] new_added_mul_nodes = [] replace_input = [] updated_nodes = [] base_dir = os.path.dirname(model.model_path) if model.model_path is not None else "" input_name_to_nodes = model.input_name_to_nodes() for parent, nodes in absorb_pairs.items(): if any([node.input[0] not in output_dicts for node in nodes]): # pragma: no cover logger.warning( "Miss input tensors of nodes {} during AWQ, skip it!".format( ", ".join([node.name for node in nodes if node.input[0] not in output_dicts]) ) ) continue inp = np.concatenate(output_dicts[nodes[0].input[0]], axis=0) inp_scale = np.mean(np.reshape(np.abs(inp), (-1, inp[0].shape[-1])), axis=0) dtype = None weight = [] org_out = [] weight_dtype = weight_config[nodes[0].name].get("weight_dtype", "int") num_bits = weight_config[nodes[0].name].get("weight_bits", 4) group_size = weight_config[nodes[0].name].get("weight_group_size", 32) sym = weight_config[nodes[0].name].get("weight_sym", True) accuracy_level = weight_config[nodes[0].name].get("accuracy_level", 0) # use same params for all children of one parent for node in nodes: weight_config.setdefault(node.name, {}).update({"weight_dtype": weight_dtype}) weight_config.setdefault(node.name, {}).update({"weight_bits": num_bits}) weight_config.setdefault(node.name, {}).update({"weight_group_size": group_size}) weight_config.setdefault(node.name, {}).update({"weight_sym": sym}) # search scale best_error = float("inf") best_ratio = -1 best_scale = None n_grid = 20 for ratio in range(n_grid): ratio = ratio * 1 / n_grid loss = 0 for node in nodes: weight = onnx.numpy_helper.to_array(model.get_initializer(node.input[1]), base_dir) if len(weight.shape) != 2: continue org_out = np.matmul(inp, weight) org_w_shape = weight.shape group_size = group_size if group_size != -1 else org_w_shape[0] w_scale = _get_weight_scale(weight.T, weight.shape[0]) scales = np.clip(np.power(inp_scale, ratio) / np.power(w_scale, (1 - ratio)), 1e-4, None) scales = scales / np.sqrt(np.max(scales) * np.min(scales)) weight = weight.T * scales weight = quant_utils.pad_tensor(weight.T, group_size, (org_w_shape[0] + group_size - 1) // group_size) q_weight = quant_utils.qdq_data( weight.reshape((-1, group_size)), weight_dtype + str(num_bits), sym, ).reshape(weight.shape) q_weight = q_weight[: org_w_shape[0], :] / np.expand_dims(scales, axis=-1) out = np.matmul(inp, q_weight) loss += np.mean(np.power((org_out - out), 2)) is_best = loss < best_error if is_best: best_error = loss best_ratio = ratio best_scale = scales for node in nodes: init_share_num = model.get_initializer_share_num(node.input[1]) weight_tensor = model.get_initializer(node.input[1]) tensor = onnx.numpy_helper.to_array(weight_tensor, base_dir) dtype = tensor.dtype tensor = tensor.T * best_scale tensor = (tensor.T).astype(dtype) new_tensor = onnx.helper.make_tensor( name=node.input[1] + "_scaled", data_type=onnx.helper.np_dtype_to_tensor_dtype(dtype), dims=tensor.shape, vals=tensor.tobytes(), raw=True, ) model.add_initializer(new_tensor) node.input[1] = new_tensor.name if init_share_num == 1: model.remove_initializer(weight_tensor) if parent is None: continue parent = model.get_node(parent) if parent is None or parent.name in updated_nodes: continue if parent.op_type in ["LayerNormalization", "BatchNormalization", "InstanceNormalization"] and len( input_name_to_nodes[nodes[0].input[0]] ) == len( nodes ): # pragma: no cover for idx in [1, 2]: tensor = onnx.numpy_helper.to_array(model.get_initializer(parent.input[idx]), base_dir) dtype = tensor.dtype new_tensor = tensor / np.reshape(best_scale, (1, -1)) model.set_initializer(parent.input[idx], new_tensor.astype(dtype), raw=True) updated_nodes.append(parent.name) output_dicts[parent.output[0]] = output_dicts[parent.output[0]] / np.reshape(best_scale, (1, -1)) elif ( parent.op_type in ["SimplifiedLayerNormalization", "MatMul", "Gemm", "Mul"] and not all([model.get_initializer(inp) is None for inp in parent.input]) and len(input_name_to_nodes[nodes[0].input[0]]) == len(nodes) ): # pragma: no cover for inp in parent.input: if model.get_initializer(inp) is not None: tensor = onnx.numpy_helper.to_array(model.get_initializer(inp), base_dir) dtype = tensor.dtype new_tensor = tensor / np.reshape(best_scale, (1, -1)) model.set_initializer(inp, new_tensor.astype(dtype), raw=True) updated_nodes.append(parent.name) output_dicts[parent.output[0]] = output_dicts[parent.output[0]] / np.reshape(best_scale, (1, -1)) elif parent.op_type in ["Conv", "FusedConv"] and len(input_name_to_nodes[nodes[0].input[0]]) == len( nodes ): # pragma: no cover tensor = onnx.numpy_helper.to_array(model.get_initializer(parent.input[2]), base_dir) dtype = tensor.dtype new_tensor = tensor / np.reshape(best_scale, (1, -1)) model.set_initializer(parent.input[2], new_tensor.astype(dtype), raw=True) updated_nodes.append(parent.name) output_dicts[parent.output[0]] = output_dicts[parent.output[0]] / np.reshape(best_scale, (1, -1)) else: # insert mul scale_tensor = onnx.helper.make_tensor( name=parent.output[0] + "_weight_only_scale", data_type=onnx.helper.np_dtype_to_tensor_dtype(dtype), dims=best_scale.shape, vals=(1.0 / best_scale).flatten().tolist(), ) new_init_tensors.append(scale_tensor) mul_output_name = parent.output[0] + "_weight_only_out" mul_node = onnx.helper.make_node( "Mul", inputs=[nodes[0].input[0], scale_tensor.name], outputs=[mul_output_name], name=nodes[0].input[0] + "_weight_only_mul", ) new_added_mul_nodes.append(mul_node) for node in nodes: replace_input.append([node, node.input[0], mul_node.output[0]]) updated_nodes.append(parent.name) output_dicts[mul_node.output[0]] = output_dicts[mul_node.input[0]] / np.reshape(best_scale, (1, -1)) model.add_nodes(new_added_mul_nodes) model.add_initializers(new_init_tensors) for node, old_input_name, new_input_name in replace_input: model.replace_node_input(node, old_input_name, new_input_name) return model, output_dicts def _apply_awq_clip(model, weight_config, absorb_pairs, output_dicts): """Apply clip for weight by checking mse.""" base_dir = os.path.dirname(model.model_path) if model.model_path is not None else "" ratios = {} for parent, nodes in absorb_pairs.items(): if any([node.input[0] not in output_dicts for node in nodes]): logger.warning( "Miss input tensors of nodes {} during AWQ, skip it!".format( ", ".join([node.name for node in nodes if node.input[0] not in output_dicts]) ) ) continue inp = np.concatenate(output_dicts[nodes[0].input[0]], axis=0) for node in nodes: weight_dtype = weight_config[node.name].get("weight_dtype", "int") num_bits = weight_config[node.name].get("weight_bits", 4) group_size = weight_config[node.name].get("weight_group_size", 32) sym = weight_config[node.name].get("weight_sym", True) accuracy_level = weight_config[node.name].get("accuracy_level", 0) org_weight = onnx.numpy_helper.to_array(model.get_initializer(node.input[1]), base_dir=base_dir) org_w_shape = org_weight.shape # ic, oc group_size = group_size if group_size != -1 else org_w_shape[0] org_out = np.matmul(inp, org_weight) # n_token, oc k_blocks = (org_w_shape[0] - 1) // group_size + 1 org_weight = quant_utils.pad_tensor(org_weight, group_size, k_blocks) org_weight = np.transpose(org_weight) best_error = float("inf") best_ratio = 1 for i_s in range(10): ratio = 1 - i_s / 100 weight = copy.deepcopy(org_weight) weight = quant_utils.qdq_data( weight.reshape((-1, group_size)), weight_dtype + str(num_bits), sym, ratio=ratio, ).reshape(org_weight.shape) cur_out = np.matmul(inp, weight[:, : org_w_shape[0]].T) loss = np.mean(np.power((org_out - cur_out), 2)) is_best = loss < best_error if is_best: best_error = loss best_ratio = ratio ratios[node.input[1]] = best_ratio return ratios def awq_quantize( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], data_reader: data_reader.CalibrationDataReader, weight_config: dict = {}, enable_auto_scale: bool = True, enable_mse_search: bool = True, providers: List[str] = ["CPUExecutionProvider"], ) -> onnx.ModelProto: """Quant the model with Activation-aware Weight quantization(AWQ) method. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model. data_reader (data_reader.CalibrationDataReader): data_reader for calibration. weight_config (dict, optional): quantization config For example, weight_config = { '(fc2, "MatMul")': { 'weight_dtype': 'int', 'weight_bits': 4, 'weight_group_size': 32, 'weight_sym': True, 'accuracy_level': 0 } }. Defaults to {}. enable_auto_scale (bool, optional): whether to search for best scales based on activation distribution. Defaults to True. enable_mse_search (bool, optional): whether to search for the best clip range from range [0.91, 1.0, 0.01]. Defaults to True. providers (list, optional): providers to use. Defaults to ["CPUExecutionProvider"]. Returns: onnx.ModelProto: quantized onnx model. """ if not isinstance(model, onnx_model.ONNXModel): model = onnx_model.ONNXModel(model) output_dicts = {} full_ratio = {} if enable_mse_search: inputs, so = quant_utils.prepare_inputs(model, data_reader, providers) del data_reader org_output = copy.deepcopy(model.model.graph.output) model.remove_tensors_from_outputs([i.name for i in org_output]) output_names = [] for node in model.nodes(): # check op_type of node is MatMul # check op_name in quantization config # check dim 1 of input is weight tensor if ( node.op_type in ["MatMul"] and node.name in weight_config and model.get_initializer(node.input[1]) is not None ): output_names.append(node.input[0]) output_names = list(set(output_names)) model.add_tensors_to_outputs(output_names) if model.is_large_model: # pragma: no cover onnx.save_model( model.model, model.model_path + "_augment.onnx", save_as_external_data=True, all_tensors_to_one_file=True, convert_attribute=False, ) session = ( ort.InferenceSession(model.model.SerializeToString(), so, providers=providers) if not model.is_large_model else ort.InferenceSession(model.model_path + "_augment.onnx", so, providers=providers) ) output_name_to_node = model.output_name_to_node() input_name_to_nodes = model.input_name_to_nodes() for input_name in output_names: # input_name maybe the input of graph and there is no parent node parent = output_name_to_node[input_name].name if input_name in output_name_to_node else None dump_pairs = {parent: []} for node in input_name_to_nodes[input_name]: # check op_type of node is MatMul # check op_name in quantization config # check dim 1 of input is weight tensor if ( node.op_type in ["MatMul"] and node.name in weight_config and model.get_initializer(node.input[1]) is not None ): dump_pairs[parent].append(model.get_node(node.name)) if len(dump_pairs[parent]) == 0: # pragma: no cover continue output_dicts = {} for inp in inputs: output = session.run([input_name], inp) output_dicts.setdefault(input_name, []).append(output) if enable_auto_scale: model, output_dicts = _apply_awq_scale( model, weight_config, dump_pairs, output_dicts, ) if enable_mse_search: ratios = _apply_awq_clip( model, weight_config, dump_pairs, output_dicts, ) del output_dicts del dump_pairs full_ratio.update(ratios) model.remove_tensors_from_outputs(output_names) model.model.graph.output.MergeFrom(org_output) model = rtn.rtn_quantize( model=model, weight_config=weight_config, ratios=full_ratio, providers=providers, ) return model def apply_awq_on_model( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], quant_config: dict, calibration_data_reader: data_reader.CalibrationDataReader, enable_auto_scale: bool = True, enable_mse_search: bool = True, providers: List[str] = ["CPUExecutionProvider"], ) -> onnx.ModelProto: """Apply Activation-aware Weight quantization(AWQ) on onnx model. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): nnx model. quant_config (dict): quantization config. calibration_data_reader (data_reader.CalibrationDataReader): data_reader for calibration. Returns: onnx.ModelProto: quantized onnx model. """ # set model params kwargs = { "enable_auto_scale": enable_auto_scale, "enable_mse_search": enable_mse_search, "providers": providers, } q_model = awq_quantize(model, data_reader=calibration_data_reader, weight_config=quant_config, **kwargs) quant_utils.dump_woq_stats(q_model, quant_config) return q_model ================================================ FILE: onnx_neural_compressor/algorithms/weight_only/gptq.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import os import pathlib import numpy as np import onnx import onnxruntime as ort from onnx_neural_compressor import constants, data_reader, onnx_model, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.layer_wise import core from onnx_neural_compressor.algorithms.weight_only import rtn from onnx_neural_compressor.quantization import config from typing import List, Union # isort: skip def _gptq( W: np.array, H: np.array, num_bits: int = 4, group_size: int = 32, sym: bool = False, block_size: int = 128, percdamp: float = 0.01, actorder: bool = False, mse: bool = False, perchannel: bool = True, ): """Quant the weight with GPTQ method. Args: W (np.array): weight. H (np.array): Hessian matrix. num_bits (int, optional): num_bits. Default is 4. group_size (int, optional): how many elements share one scale/zp. Default is 32. sym (bool, optional): sym or asym. Defaults to False. block_size (int, optional): block_size to quantize weight. percdamp (float, optional): percent of the average Hessian diagonal to use for dampening. actorder (bool, optional): whether rearrange Hessian matrix considering the diag's value. mse (bool, optional): whether get scale and zero point with mse error. perchannel (bool, optional): whether quantize weight per-channel. Returns: Q: fake quantized weight """ Qs = [] maxq = 2**num_bits - 1 grid = 100 maxshrink = 0.8 norm = 2.4 def find_params(weight): org_shape = weight.shape # find zp, scale if not perchannel: weight = np.expand_dims(weight.flatten(), axis=1) tmp = np.zeros(weight.shape[1]) xmin = np.minimum(np.min(weight, axis=0), tmp) xmax = np.maximum(np.max(weight, axis=0), tmp) if sym: xmax = np.maximum(np.abs(xmin), xmax) tmp = xmin < 0 if np.any(tmp): xmin[tmp] = -xmax[tmp] tmp = (xmin == 0) & (xmax == 0) xmin[tmp] = -1 xmax[tmp] = +1 scale = (xmax - xmin) / maxq if sym: zero = np.ones(scale.shape) * (maxq + 1) / 2 else: zero = np.round(-xmin / scale) if mse: best = np.ones([weight.shape[1]]) * float("inf") for i in range(int(maxshrink * grid)): p = 1 - i / grid xmin1 = p * xmin xmax1 = p * xmax scale1 = (xmax1 - xmin1) / maxq zero1 = np.round(-xmin1 / scale1) if not sym else zero q = np.clip(np.round(weight / scale1) + zero1, 0, maxq) q -= weight q = np.power(np.abs(q), norm) err = np.sum(q, 0) tmp = err < best if np.any(tmp): best[tmp] = err[tmp] scale[tmp] = scale1[tmp] zero[tmp] = zero1[tmp] if not perchannel: tmp = org_shape[1] scale = np.repeat(scale, tmp) zero = np.repeat(zero, tmp) shape = [-1] + [1] * (len(org_shape) - 1) scale = np.reshape(scale, shape) zero = np.reshape(zero, shape) return scale, zero scales = [] zps = [] shape = W.shape scale, zp = find_params(W) dead = np.diag(H) == 0 H[dead, dead] = 1 W[dead, :] = 0 # such channel makes no contribution to quantization computation # rearrange considering the diag's value if actorder: perm = np.argsort(np.diag(H))[::-1] W = W[perm, :] H = H[perm, :][:, perm] Losses = np.zeros_like(W) Q = np.zeros_like(W) damp = percdamp * np.mean(np.diag(H)) diag = np.arange(shape[0]) H[diag, diag] += damp # add a average value of H = np.linalg.cholesky(np.linalg.inv(H)).T Hinv = H for i1 in range(0, shape[0], block_size): i2 = min(i1 + block_size, shape[0]) count = i2 - i1 W1 = copy.deepcopy(W[i1:i2, :]) Q1 = np.zeros_like(W1) Err1 = np.zeros_like(W1) Losses1 = np.zeros_like(W1) Hinv1 = Hinv[i1:i2, i1:i2] for i in range(count): # within a block, channel wise w = W1[i, :] d = Hinv1[i, i] if group_size != -1: if (i1 + i) % group_size == 0: scale, zp = find_params(W[(i1 + i) : (i1 + i + group_size), :]) q = (scale * (np.clip(np.round(w[:, np.newaxis] / scale) + zp, 0, maxq) - zp)).flatten() Q1[i, :] = q Losses1[i, :] = (w - q) ** 2 / d**2 err1 = (w - q) / d W1[i:, :] -= np.matmul(np.expand_dims(Hinv1[i:, i], axis=1), np.expand_dims(err1, axis=0)) Err1[i, :] = err1 Q[i1:i2, :] = Q1 Losses[i1:i2, :] = Losses1 / 2 W[i2:, :] -= np.matmul(Hinv[i2:, i1:i2], Err1) if actorder: invperm = np.argsort(perm) Q = Q[invperm, :] Q = np.reshape(Q, W.shape) del W return Q def gptq_quantize( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], data_reader: data_reader.CalibrationDataReader, weight_config: dict = {}, percdamp: float = 0.01, block_size: int = 128, actorder: bool = False, mse: bool = False, perchannel: bool = True, providers: List[str] = ["CPUExecutionProvider"], return_modelproto: bool = True, ): """Quant the model with GPTQ method. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model. data_reader (data_reader.CalibrationDataReader): data_reader for calibration. weight_config (dict, optional): quantization config For example, weight_config = { '(fc2, "MatMul")': { 'weight_dtype': 'int', 'weight_bits': 4, 'weight_group_size': 32, 'weight_sym': True, 'accuracy_level': 0 }. Defaults to {}. percdamp (float, optional): percentage of Hessian's diagonal values' average, which will be added to Hessian's diagonal to increase numerical stability. Defaults to 0.01. block_size (int, optional): execute GPTQ quantization per block. Defaults to 128. actorder (bool, optional): whether to sort Hessian's diagonal values to rearrange channel-wise quantization order. Defaults to False. mse (bool, optional): whether get scale and zero point with mse error. Defaults to False. perchannel (bool, optional): whether quantize weight per-channel. Defaults to True. providers (list, optional): providers to use. Defaults to ["CPUExecutionProvider"]. return_modelproto (bool, optionmal): whether to return onnx.Modelproto. set False for layer-wise quant. Default to True Returns: onnx.ModelProto: quantized onnx model """ if not isinstance(model, onnx_model.ONNXModel): model = onnx_model.ONNXModel(model) base_dir = os.path.dirname(model.model_path) if model.model_path is not None else "" inputs, so = quant_utils.prepare_inputs(model, data_reader, providers) del data_reader org_output = copy.deepcopy(model.model.graph.output) model.remove_tensors_from_outputs([i.name for i in org_output]) output_names = [] for node in model.nodes(): # check op_type of node is MatMul # check op_name in quantization config # check dim 1 of input is weight tensor if ( node.op_type in ["MatMul"] and node.name in weight_config and model.get_initializer(node.input[1]) is not None ): output_names.append(node.input[0]) output_names = list(set(output_names)) model.add_tensors_to_outputs(output_names) if model.is_large_model: onnx.save_model( model.model, model.model_path + "_augment.onnx", save_as_external_data=True, all_tensors_to_one_file=True, convert_attribute=False, ) session = ( ort.InferenceSession(model.model.SerializeToString(), so, providers=providers) if not model.is_large_model else ort.InferenceSession(model.model_path + "_augment.onnx", so, providers=providers) ) input_name_to_nodes = model.input_name_to_nodes() for idx, input_name in enumerate(output_names): utility.simple_progress_bar(len(output_names), idx + 1) node_list = [] weights = [] for node in input_name_to_nodes[input_name]: # check op_type of node is MatMul # check op_name in quantization config # check dim 1 of input is weight tensor if ( node.op_type in ["MatMul"] and node.name in weight_config and model.get_initializer(node.input[1]) is not None ): weight = onnx.numpy_helper.to_array( model.get_initializer(model.get_node(node.name).input[1]), base_dir ).copy() if len(weight.shape) != 2: # pragma: no cover continue weights.append(weight) node_list.append(model.get_node(node.name)) if len(weights) == 0: # pragma: no cover continue Hs = [np.zeros((i.shape[0], i.shape[0])) for i in weights] nsamples = 0 for data in inputs: inp = session.run([input_name], data)[0] tmp = inp.shape[0] inp = np.reshape(inp, (-1, inp.shape[-1])) Hs = [i * (nsamples / (nsamples + tmp)) for i in Hs] nsamples += tmp inp = np.sqrt(2 / nsamples) * inp Hs = [i + np.matmul(inp.T, inp) for i in Hs] for ( node, weight, H, ) in zip(node_list, weights, Hs): num_bits = weight_config[node.name].get("weight_bits", 4) group_size = weight_config[node.name].get("weight_group_size", 32) sym = weight_config[node.name].get("weight_sym", True) dtype = weight_config[node.name].get("weight_dtype", "int") accuracy_level = weight_config[node.name].get("accuracy_level", 0) quant_format = getattr(weight_config[node.name].get("quant_format", None), "value", None) weight_tensor = model.get_initializer(node.input[1]) init_share_num = model.get_initializer_share_num(node.input[1]) # weight -> quant -> dequant -> q_weight q_weight = _gptq( weight, H, num_bits=num_bits, group_size=group_size, sym=sym, block_size=block_size, percdamp=percdamp, actorder=actorder, mse=mse, perchannel=perchannel, ) new_nodes, new_inits, remove_nodes = quant_utils.quant_matmul_weight_only( node=node, weight=weight, dtype=dtype, num_bits=num_bits, sym=sym, group_size=group_size, quant_format=quant_format, accuracy_level=accuracy_level, ) model.add_initializers(new_inits) model.add_nodes(new_nodes) model.remove_nodes(remove_nodes) if init_share_num == 1: model.remove_initializer(weight_tensor) model.remove_tensors_from_outputs(output_names) model.model.graph.output.MergeFrom(org_output) model.topological_sort() # reload external data to prevent external data file path errors if model.is_large_model: onnx.external_data_helper.load_external_data_for_model(model.model, os.path.split(model.model_path)[0]) if return_modelproto: return model.model else: model.save(model.model_path + "_quant.onnx") return model def apply_gptq_on_model( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], quant_config: dict, calibration_data_reader: data_reader.CalibrationDataReader, percdamp: float = 0.01, block_size: int = 128, actorder: bool = False, mse: bool = False, perchannel: bool = True, providers: List[str] = ["CPUExecutionProvider"], layer_wise_quant: bool = False, ) -> onnx.ModelProto: """Apply GPTQ on onnx model. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model. quant_config (dict): quantization config. calibration_data_reader (data_reader.CalibrationDataReader): data_reader for calibration. Returns: onnx.ModelProto: quantized onnx model. """ # set other model params quant_kwargs = { "percdamp": percdamp, "block_size": block_size, "actorder": actorder, "mse": mse, "perchannel": perchannel, "providers": providers, } if layer_wise_quant: quantized_model = core.layer_wise_quant( model, quant_func=gptq_quantize, weight_config=quant_config, data_reader=calibration_data_reader, **quant_kwargs ) else: quantized_model = gptq_quantize( model, data_reader=calibration_data_reader, weight_config=quant_config, **quant_kwargs ) if isinstance(quantized_model, onnx_model.ONNXModel): quantized_model = quantized_model.model quant_utils.dump_woq_stats(quantized_model, quant_config) return quantized_model ================================================ FILE: onnx_neural_compressor/algorithms/weight_only/rtn.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import pathlib import numpy as np import onnx import onnxruntime as ort from onnx_neural_compressor import constants, onnx_model, utility from onnx_neural_compressor.algorithms import utility as quant_utils from onnx_neural_compressor.algorithms.layer_wise import core from typing import List, Union # isort: skip def rtn_quantize( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], weight_config: dict = {}, ratios: dict = {}, providers: List[str] = ["CPUExecutionProvider"], return_modelproto: bool = True, ): """Quantize the model with round to nearst method. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model weight_config (dict, optional): quantization config For example, weight_config = { '(fc2, "MatMul")': { 'weight_dtype': 'int', 'weight_bits': 4, 'weight_group_size': 32, 'weight_sym': True, 'accuracy_level': 0 } }. Defaults to {}. ratios (dict, optional): percentile of clip. Defaults to {}. providers (list, optional): providers to use. Defaults to ["CPUExecutionProvider"]. return_modelproto (bool, optionmal): whether to return onnx.Modelproto. set False for layer-wise quant. Default to True Returns: onnx.ModelProto: quantized onnx model. """ if not isinstance(model, onnx_model.ONNXModel): model = onnx_model.ONNXModel(model) base_dir = os.path.dirname(model.model_path) if model.model_path is not None else "" new_nodes_all = [] remove_nodes_all = [] total_num = len([i for i in model.nodes() if i.op_type in ["MatMul"]]) curr_id = 0 for node in model.nodes(): if node.op_type in ["MatMul"]: curr_id += 1 utility.simple_progress_bar(total_num, curr_id) # check op_type of node is MatMul # check op_name in quantization config # check dim 1 of input is weight tensor if ( node.op_type in ["MatMul"] and node.name in weight_config and model.get_initializer(node.input[1]) is not None ): weight_tensor = model.get_initializer(node.input[1]) weight = onnx.numpy_helper.to_array(weight_tensor, base_dir=base_dir).copy() if len(weight.shape) != 2: continue dtype = weight_config[node.name].get("weight_dtype", "int") num_bits = weight_config[node.name].get("weight_bits", 4) group_size = weight_config[node.name].get("weight_group_size", 32) sym = weight_config[node.name].get("weight_sym", True) accuracy_level = weight_config[node.name].get("accuracy_level", 0) quant_format = getattr(weight_config[node.name].get("quant_format", None), "value", None) init_share_num = model.get_initializer_share_num(node.input[1]) new_nodes, new_inits, remove_nodes = quant_utils.quant_matmul_weight_only( node=node, weight=weight, dtype=dtype, num_bits=num_bits, sym=sym, group_size=group_size, ratio=ratios.get(node.input[1], 1), quant_format=quant_format, accuracy_level=accuracy_level, ) model.add_initializers(new_inits) new_nodes_all.extend(new_nodes) remove_nodes_all.extend(remove_nodes) if init_share_num == 1: model.remove_initializer(weight_tensor) model.add_nodes(new_nodes_all) model.remove_nodes(remove_nodes_all) model.topological_sort() # reload external data to prevent external data file path errors if model.is_large_model: onnx.external_data_helper.load_external_data_for_model(model.model, os.path.split(model.model_path)[0]) if return_modelproto: return model.model else: model.save(model.model_path + "_quant.onnx") return model def apply_rtn_on_model( model: Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str], quant_config: dict, ratios: dict = {}, providers: List[str] = ["CPUExecutionProvider"], layer_wise_quant: bool = False, ) -> onnx.ModelProto: """Apply RTN on onnx model. Args: model (Union[onnx.ModelProto, onnx_model.ONNXModel, pathlib.Path, str]): onnx model. quant_config (dict): quantization config. Returns: onnx.ModelProto: quantized onnx model. """ quant_kwargs = { "ratios": ratios, "providers": providers, } if layer_wise_quant: quantized_model = core.layer_wise_quant( model, quant_func=rtn_quantize, weight_config=quant_config, **quant_kwargs ) else: quantized_model = rtn_quantize(model, weight_config=quant_config, **quant_kwargs) if isinstance(quantized_model, onnx_model.ONNXModel): quantized_model = quantized_model.model quant_utils.dump_woq_stats(quantized_model, quant_config) return quantized_model ================================================ FILE: onnx_neural_compressor/quantization/__init__.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from onnx_neural_compressor.quantization.quant_utils import CalibrationMethod, QuantFormat, QuantType from onnx_neural_compressor.quantization.quantize import quantize ================================================ FILE: onnx_neural_compressor/quantization/algorithm_entry.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib import tempfile from typing import Union import onnx import onnxruntime as ort from packaging import version from onnx_neural_compressor import constants, data_reader, logger, utility from onnx_neural_compressor.algorithms.post_training_quant import calibrate, quantizer from onnx_neural_compressor.algorithms.smoother import core from onnx_neural_compressor.algorithms.weight_only import awq, gptq, rtn from onnx_neural_compressor.quantization import QuantFormat, config ort_version = version.Version(ort.__version__) ###################### RTN Algo Entry ################################## @utility.register_algo(name=constants.RTN) def rtn_quantize_entry( model: Union[pathlib.Path, str], quant_config: config.RTNConfig, *args, **kwargs ) -> onnx.ModelProto: """The main entry to apply rtn quantization.""" config_mapping = quant_config.to_config_mapping(model=model) quant_kwargs = dict( zip( quant_config.model_params_list, [getattr(quant_config, key, None) for key in quant_config.model_params_list], ) ) model = rtn.apply_rtn_on_model(model, config_mapping, **quant_kwargs) return model ###################### GPTQ Algo Entry ################################## @utility.register_algo(name=constants.GPTQ) def gptq_quantize_entry( model: Union[pathlib.Path, str], quant_config: config.GPTQConfig, calibration_data_reader: data_reader.CalibrationDataReader, *args, **kwargs, ) -> onnx.ModelProto: """The main entry to apply gptq quantization.""" assert calibration_data_reader is not None, "Please provide calibration_data_reader" assert isinstance( calibration_data_reader, data_reader.CalibrationDataReader ), "Please follow onnx_neural_compressor/data_reader.py to implement calibration_data_reader" config_mapping = quant_config.to_config_mapping(model=model) quant_kwargs = dict( zip( quant_config.model_params_list, [getattr(quant_config, key, None) for key in quant_config.model_params_list], ) ) # regenerate to ensure data exists calibration_data_reader.rewind() model = gptq.apply_gptq_on_model(model, config_mapping, calibration_data_reader, **quant_kwargs) return model ###################### AWQ Algo Entry ################################## @utility.register_algo(name=constants.AWQ) def awq_quantize_entry( model: Union[pathlib.Path, str], quant_config: config.AWQConfig, calibration_data_reader: data_reader.CalibrationDataReader, *args, **kwargs, ) -> onnx.ModelProto: """The main entry to apply awq quantization.""" assert calibration_data_reader is not None, "Please provide calibration_data_reader" assert isinstance( calibration_data_reader, data_reader.CalibrationDataReader ), "Please follow onnx_neural_compressor/data_reader.py to implement calibration_data_reader" config_mapping = quant_config.to_config_mapping(model=model) quant_kwargs = dict( zip( quant_config.model_params_list, [getattr(quant_config, key, None) for key in quant_config.model_params_list], ) ) # regenerate to ensure data exists calibration_data_reader.rewind() model = awq.apply_awq_on_model(model, config_mapping, calibration_data_reader, **quant_kwargs) return model ###################### Static quant Entry ################################## @utility.register_algo(name=constants.STATIC_QUANT) def static_quantize_entry( model: Union[pathlib.Path, str], quant_config: config.StaticQuantConfig, calibration_data_reader: data_reader.CalibrationDataReader, model_output: Union[pathlib.Path, str] = None, *args, **kwargs, ) -> onnx.ModelProto: """The main entry to apply dynamic quantization.""" if len(quant_config.op_types_to_quantize) == 0: logger.warning("No candidate op type to do quantization, exit.") exit(0) assert calibration_data_reader is not None, "Please provide calibration_data_reader" assert isinstance( calibration_data_reader, data_reader.CalibrationDataReader ), "Please follow onnx_neural_compressor/quantization/calibrate.py to implement calibration_data_reader" config_mapping = quant_config.to_config_mapping(model=model) calibration_data_reader.rewind() augment = calibrate.ONNXRTAugment( model, calibration_data_reader, dump_op_types=quant_config.op_types_to_quantize, execution_provider=quant_config.execution_provider, iterations=list(range(0, quant_config.calibration_sampling_size)), ) min_max = augment.dump_minmax(config_mapping) quantize_params = augment.dump_calibration(config_mapping, min_max=min_max) _quantizer = quantizer.StaticQuantizer( model, config_mapping, quant_format=quant_config.quant_format.name.lower(), quantization_params=quantize_params, op_types_to_quantize=quant_config.op_types_to_quantize, execution_provider=quant_config.execution_provider, optypes_to_exclude_output_quant=quant_config.optypes_to_exclude_output_quant, dedicated_qdq_pair=quant_config.dedicated_qdq_pair, add_qdq_pair_to_weight=quant_config.add_qdq_pair_to_weight, ) _quantizer.quantize_model() if model_output is not None: _quantizer.model.save(model_output) return _quantizer.model.model ###################### SmoothQuant Entry ################################## @utility.register_algo(name=constants.SMOOTH_QUANT) def smooth_quant_entry( model: Union[pathlib.Path, str], quant_config: config.SmoothQuantConfig, calibration_data_reader: data_reader.CalibrationDataReader, model_output: Union[pathlib.Path, str] = None, *args, **kwargs, ) -> Union[pathlib.Path, str, onnx.ModelProto]: """Apply smooth quant.""" assert calibration_data_reader is not None, "Please provide calibration_data_reader" assert isinstance( calibration_data_reader, data_reader.CalibrationDataReader ), "Please follow onnx_neural_compressor/data_reader.py to implement calibration_data_reader" # smooth operation calibration_data_reader.rewind() smoother = core.Smoother( model, calibration_data_reader, execution_provider=getattr(quant_config, "execution_provider", "CPUExecutionProvider"), ) smoothed_model = smoother.transform(**quant_config.get_model_params_dict()) with tempfile.TemporaryDirectory(prefix="ort.quant.") as tmp_dir: # ORT quant API requires str input onnx.save_model( smoothed_model, pathlib.Path(tmp_dir).joinpath("smooth.onnx").as_posix(), save_as_external_data=True, all_tensors_to_one_file=True, location="smooth.onnx_data", size_threshold=1024, convert_attribute=False, ) # quant operation calibration_data_reader.rewind() # exclude Mul operations which are inserted during smooth operation excluded_nodes = [i.name for i in smoothed_model.graph.node if i.name.endswith("_smooth_mul")] quant_config.nodes_to_exclude.extend(excluded_nodes) q_model = static_quantize_entry( pathlib.Path(tmp_dir).joinpath("smooth.onnx").as_posix(), quant_config, calibration_data_reader, model_output, ) return q_model ###################### Dynamic quant Entry ################################## @utility.register_algo(name=constants.DYNAMIC_QUANT) def dynamic_quantize_entry( model: Union[pathlib.Path, str], quant_config: config.DynamicQuantConfig, model_output: Union[pathlib.Path, str] = None, *args, **kwargs, ) -> onnx.ModelProto: """The main entry to apply dynamic quantization.""" if len(quant_config.op_types_to_quantize) == 0: logger.warning("No candidate op type to do quantization, exit.") exit(0) config_mapping = quant_config.to_config_mapping(model=model) _quantizer = quantizer.DynamicQuantizer( model, config_mapping, op_types_to_quantize=quant_config.op_types_to_quantize, ) _quantizer.quantize_model() if model_output is not None: _quantizer.model.save(model_output) return _quantizer.model.model ================================================ FILE: onnx_neural_compressor/quantization/matmul_4bits_quantizer.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import List, Union # isort: skip import onnx import onnxruntime as ort from onnx_neural_compressor.quantization import matmul_nbits_quantizer RTNWeightOnlyQuantConfig = matmul_nbits_quantizer.RTNWeightOnlyQuantConfig AWQWeightOnlyQuantConfig = matmul_nbits_quantizer.AWQWeightOnlyQuantConfig GPTQWeightOnlyQuantConfig = matmul_nbits_quantizer.GPTQWeightOnlyQuantConfig class MatMul4BitsQuantizer(matmul_nbits_quantizer.MatMulNBitsQuantizer): def __init__( self, model: Union[onnx.ModelProto, str], block_size: int = 128, is_symmetric: bool = False, is_signed: bool = False, accuracy_level: int = 0, nodes_to_exclude=None, algo_config: matmul_nbits_quantizer.WeightOnlyQuantConfig = None, providers: List[str] = ["CPUExecutionProvider"], optimization_level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC, ): super().__init__( model=model, block_size=block_size, is_symmetric=is_symmetric, is_signed=is_signed, accuracy_level=accuracy_level, nodes_to_exclude=nodes_to_exclude, algo_config=algo_config, n_bits=4, providers=providers, optimization_level=optimization_level, ) ================================================ FILE: onnx_neural_compressor/quantization/matmul_nbits_quantizer.py ================================================ # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import List, Union # isort: skip import pathlib import tempfile import onnx import onnxruntime as ort from onnx_neural_compressor import data_reader, logger, onnx_model, utility from onnx_neural_compressor.quantization import QuantFormat from onnx_neural_compressor.quantization import algorithm_entry as algos from onnx_neural_compressor.quantization import config class WeightOnlyQuantConfig: def __init__(self, algorithm, quant_format=QuantFormat.QOperator): """This is the Base class for Weight Only Quant Configuration. Args: algorithm: weight only quantize algorithm name. """ self.algorithm = algorithm self.quant_format = quant_format class RTNWeightOnlyQuantConfig(WeightOnlyQuantConfig): def __init__(self, ratios=None, layer_wise_quant=False, quant_format=QuantFormat.QOperator): super().__init__( algorithm="RTN", quant_format=quant_format, ) if ratios is None: ratios = {} self.ratios = ratios self.layer_wise_quant = layer_wise_quant class GPTQWeightOnlyQuantConfig(WeightOnlyQuantConfig): def __init__( self, calibration_data_reader: data_reader.CalibrationDataReader, percdamp=0.01, block_size=128, actorder=False, mse=False, perchannel=True, layer_wise_quant=False, quant_format=QuantFormat.QOperator, ): super().__init__( algorithm="GPTQ", quant_format=quant_format, ) self.calibration_data_reader = calibration_data_reader self.percdamp = percdamp self.block_size = block_size self.actorder = actorder self.mse = mse self.perchannel = perchannel self.layer_wise_quant = layer_wise_quant class AWQWeightOnlyQuantConfig(WeightOnlyQuantConfig): def __init__( self, calibration_data_reader: data_reader.CalibrationDataReader, enable_auto_scale=True, enable_mse_search=True, quant_format=QuantFormat.QOperator, ): super().__init__(algorithm="AWQ", quant_format=quant_format) self.calibration_data_reader = calibration_data_reader self.enable_auto_scale = enable_auto_scale self.enable_mse_search = enable_mse_search algorithm_config_mapping = { "RTN": RTNWeightOnlyQuantConfig, "AWQ": AWQWeightOnlyQuantConfig, "GPTQ": GPTQWeightOnlyQuantConfig, } class MatMulNBitsQuantizer: def __init__( self, model: Union[onnx.ModelProto, str], block_size: int = 128, is_symmetric: bool = False, is_signed: bool = False, accuracy_level: int = 0, nodes_to_exclude: List[str] = None, algo_config: WeightOnlyQuantConfig = None, n_bits: int = 4, providers: List[str] = ["CPUExecutionProvider"], optimization_level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC, ): if nodes_to_exclude is None: nodes_to_exclude = [] self.model = model self.block_size = block_size self.is_symmetric = is_symmetric self.is_signed = is_signed self.accuracy_level = accuracy_level self.nodes_to_exclude = list(set(nodes_to_exclude)) self.algo_config = algo_config or RTNWeightOnlyQuantConfig() self.n_bits = n_bits self.providers = providers self.algorithm = self.algo_config.algorithm self.optimization_level = optimization_level assert self.algorithm in [ "RTN", "AWQ", "GPTQ", ], "Only RTN, GPTQ and AWQ algorithms are supported, but get {} algorithm".format(self.algorithm) def _generate_nc_config(self): config_class = config.config_registry.get_cls_configs()[self.algorithm.lower()] quant_kwargs = { "weight_dtype": "int" if self.is_signed else "uint", "weight_bits": self.n_bits, "weight_group_size": self.block_size, "weight_sym": self.is_symmetric, "accuracy_level": self.accuracy_level, "providers": self.providers, "quant_format": self.algo_config.quant_format, "nodes_to_exclude": self.nodes_to_exclude, } if self.algorithm == "RTN": quant_kwargs.update( { "layer_wise_quant": self.algo_config.layer_wise_quant, } ) elif self.algorithm == "GPTQ": quant_kwargs.update( { "percdamp": self.algo_config.percdamp, "block_size": self.algo_config.block_size, "actorder": self.algo_config.actorder, "mse": self.algo_config.mse, "perchannel": self.algo_config.perchannel, "layer_wise_quant": self.algo_config.layer_wise_quant, } ) elif self.algorithm == "AWQ": quant_kwargs.update( { "enable_auto_scale": self.algo_config.enable_auto_scale, "enable_mse_search": self.algo_config.enable_mse_search, } ) nc_config = config_class(**quant_kwargs) return nc_config def int4_quant_algo(self): qconfig = self._generate_nc_config() model = self.model opt_tmp_file = tempfile.TemporaryDirectory() if getattr(self.algo_config, "layer_wise_quant", False) and not isinstance(model, str): logger.warning("Please use model path for layer-wise quantization.") # do graph optimization if not layer_wise_quant if ( not getattr(self.algo_config, "layer_wise_quant", False) and self.optimization_level != ort.GraphOptimizationLevel.ORT_DISABLE_ALL ): if not isinstance(model, str): onnx.save_model( model, pathlib.Path(opt_tmp_file.name).joinpath("tmp.onnx").as_posix(), save_as_external_data=True, all_tensors_to_one_file=True, location="tmp.onnx_data", size_threshold=1024, convert_attribute=False, ) model = pathlib.Path(opt_tmp_file.name).joinpath("tmp.onnx").as_posix() logger.info("Start graph optimization...") sess_options = ort.SessionOptions() sess_options.graph_optimization_level = self.optimization_level sess_options.optimized_model_filepath = pathlib.Path(opt_tmp_file.name).joinpath("opt.onnx").as_posix() sess_options.add_session_config_entry( "session.optimized_model_external_initializers_file_name", "opt.onnx_data" ) sess_options.add_session_config_entry( "session.optimized_model_external_initializers_min_size_in_bytes", "1024" ) session = ort.InferenceSession(model, sess_options, providers=["CPUExecutionProvider"]) model = sess_options.optimized_model_filepath del session logger.info("Graph optimization done.") logger.info(f"start to quantize model with {self.algorithm} algorithm...") if self.algorithm == "RTN": self.model = algos.rtn_quantize_entry(model, qconfig) elif self.algorithm == "GPTQ": self.model = algos.gptq_quantize_entry(model, qconfig, self.algo_config.calibration_data_reader) elif self.algorithm == "AWQ": self.model = algos.awq_quantize_entry(model, qconfig, self.algo_config.calibration_data_reader) logger.info(f"complete quantization of model with {self.algorithm} algorithm.") opt_tmp_file.cleanup() def process(self): self.int4_quant_algo() ================================================ FILE: onnx_neural_compressor/quantization/quant_utils.py ================================================ # Copyright (c) 2023 MIT HAN Lab # This source code is licensed under the MIT license # # Copyright (c) 2024 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import enum import onnx class QuantType(enum.Enum): # pragma: no cover """Represent QuantType value.""" QInt8 = 0 QUInt8 = 1 QInt4 = 4 QUInt4 = 5 @property def tensor_type(self): if self == QuantType.QInt8: return onnx.TensorProto.INT8 if self == QuantType.QUInt8: return onnx.TensorProto.UINT8 if self == QuantType.QInt8: return onnx.TensorProto.INT4 if self == QuantType.QUInt4: return onnx.TensorProto.UINT4 raise ValueError(f"Unexpected value qtype={self!r}.") class QuantFormat(enum.Enum): QOperator = 0 QDQ = 1 class CalibrationMethod(enum.Enum): MinMax = 0 Entropy = 1 Percentile = 2 Distribution = 3 ================================================ FILE: onnx_neural_compressor/quantization/quantize.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pathlib import tempfile from typing import Union import onnx import onnxruntime as ort from onnxruntime.quantization.quantize import QuantConfig from onnx_neural_compressor.quantization import algorithm_entry as algos from onnx_neural_compressor.quantization import config # ORT-like user-facing API def quantize( model_input: Union[str, pathlib.Path, onnx.ModelProto], model_output: Union[str, pathlib.Path], quant_config: config.BaseConfig, optimization_level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC, ): with tempfile.TemporaryDirectory(prefix="ort.opt.") as tmp_dir: if optimization_level != ort.GraphOptimizationLevel.ORT_DISABLE_ALL: sess_options = ort.SessionOptions() sess_options.graph_optimization_level = optimization_level sess_options.optimized_model_filepath = pathlib.Path(tmp_dir).joinpath("opt.onnx").as_posix() sess_options.add_session_config_entry( "session.optimized_model_external_initializers_file_name", "opt.onnx_data" ) sess_options.add_session_config_entry( "session.optimized_model_external_initializers_min_size_in_bytes", "1024" ) session = ort.InferenceSession(model_input, sess_options, provides=["CPUExecutionProvider"]) del session model_input = sess_options.optimized_model_filepath if isinstance(quant_config, config.StaticQuantConfig): if quant_config.extra_options.get("SmoothQuant", False): algos.smooth_quant_entry( model_input, quant_config, quant_config.calibration_data_reader, model_output=model_output ) else: algos.static_quantize_entry( model_input, quant_config, quant_config.calibration_data_reader, model_output=model_output ) elif isinstance(quant_config, config.DynamicQuantConfig): algos.dynamic_quantize_entry(model_input, quant_config, model_output=model_output) else: raise TypeError( "Invalid quantization config type, it must be either StaticQuantConfig or DynamicQuantConfig." ) ================================================ FILE: onnx_neural_compressor/quantization/tuning.py ================================================ # Copyright (c) 2023 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import os import pathlib import shutil import tempfile import traceback import uuid import onnx import onnxruntime as ort from onnx import external_data_helper from onnx_neural_compressor import data_reader, logger, utility from onnx_neural_compressor.quantization import config from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Sized, Tuple, Union # isort: skip class EvaluationFuncWrapper: def __init__(self, eval_fn: Callable, eval_args=None): """Evaluation function wrapper. Args: eval_fn: a function for evaluated the float or quantized model eval_args: positional arguments for `eval_fn` """ self.eval_fn = eval_fn self.eval_args = eval_args def evaluate(self, model) -> Union[float, int]: result = self.eval_fn(model, *self.eval_args) if self.eval_args else self.eval_fn(model) return result class Evaluator: """Evaluator is a collection of evaluation functions. Note: will deprecate this class in the future. Examples: def eval_acc(model): ... def eval_perf(molde): ... # Usage user_eval_fns1 = eval_acc user_eval_fns2 = {"eval_fn": eval_acc} user_eval_fns3 = {"eval_fn": eval_acc, "weight": 1.0, "name": "accuracy"} user_eval_fns4 = [ {"eval_fn": eval_acc, "weight": 0.5}, {"eval_fn": eval_perf, "weight": 0.5, "name": "accuracy"}, ] """ EVAL_FN = "eval_fn" WEIGHT = "weight" FN_NAME = "name" EVAL_FN_TEMPLATE: Dict[str, Any] = {EVAL_FN: None, WEIGHT: 1.0, FN_NAME: None} def __init__(self) -> None: self.eval_fn_registry: List[Dict[str, Any]] = [] def evaluate(self, model) -> float: """Evaluate the model using registered evaluation functions. Args: model: The fp32 model or quantized model. Returns: The overall result of all registered evaluation functions. """ result = 0 for eval_pair in self.eval_fn_registry: eval_fn = eval_pair[self.EVAL_FN] eval_result = eval_fn(model) result = self._update_the_objective_score(eval_pair, eval_result, result) return result def _update_the_objective_score(self, eval_pair, eval_result, overall_result) -> float: return overall_result + eval_result * eval_pair[self.WEIGHT] def get_number_of_eval_functions(self) -> int: return len(self.eval_fn_registry) def _set_eval_fn_registry(self, user_eval_fns: List[Dict]) -> None: self.eval_fn_registry = [ { self.EVAL_FN: user_eval_fn_pair[self.EVAL_FN], self.WEIGHT: user_eval_fn_pair.get(self.WEIGHT, 1.0), self.FN_NAME: user_eval_fn_pair.get( self.FN_NAME, getattr(user_eval_fn_pair[self.EVAL_FN], "__name__", "custom_func") ), } for user_eval_fn_pair in user_eval_fns ] def set_eval_fn_registry(self, eval_fns: Optional[Union[Callable, Dict, List[Dict]]] = None) -> None: # About the eval_fns format, refer the class docstring for details. if eval_fns is None: return elif callable(eval_fns): # single eval_fn eval_fn_pair = copy.deepcopy(self.EVAL_FN_TEMPLATE) eval_fn_pair[self.EVAL_FN] = eval_fns eval_fn_pair[self.FN_NAME] = eval_fns.__name__ eval_fns = [eval_fn_pair] elif isinstance(eval_fns, Dict): eval_fns = [eval_fns] elif isinstance(eval_fns, List): assert all([isinstance(eval_fn_pair, Dict) for eval_fn_pair in eval_fns]) else: raise NotImplementedError(f"The eval_fns should be a dict or a list of dict, but got {type(eval_fns)}.") self._set_eval_fn_registry(eval_fns) def self_check(self) -> None: # check the number of evaluation functions num_eval_fns = self.get_number_of_eval_functions() assert num_eval_fns > 0, "Please ensure that you register at least one evaluation metric for auto-tune." logger.info("There are %d evaluations functions.", num_eval_fns) evaluator = Evaluator() class ConfigSet: def __init__(self, config_list: List[config.BaseConfig]) -> None: self.config_list = config_list def __getitem__(self, index) -> config.BaseConfig: assert 0 <= index < len(self.config_list), f"Index {index} out of range." return self.config_list[index] def __len__(self) -> int: return len(self.config_list) @classmethod def _from_single_config(cls, fwk_config: config.BaseConfig) -> List[config.BaseConfig]: config_list = [] config_list = fwk_config.expand() return config_list @classmethod def _from_list_of_configs(cls, fwk_configs: List[config.BaseConfig]) -> List[config.BaseConfig]: config_list = [] for fwk_config in fwk_configs: config_list += cls._from_single_config(fwk_config) return config_list @classmethod def generate_config_list(cls, fwk_configs: Union[config.BaseConfig, List[config.BaseConfig]]): # There are several cases for the input `fwk_configs`: # 1. fwk_configs is a single config # 2. fwk_configs is a list of configs # For a single config, we need to check if it can be expanded or not. config_list = [] if isinstance(fwk_configs, config.BaseConfig): config_list = cls._from_single_config(fwk_configs) elif isinstance(fwk_configs, List): config_list = cls._from_list_of_configs(fwk_configs) else: raise NotImplementedError(f"Unsupported type {type(fwk_configs)} for fwk_configs.") return config_list @classmethod def from_fwk_configs(cls, fwk_configs: Union[config.BaseConfig, List[config.BaseConfig]]) -> "ConfigSet": """Create a ConfigSet object from a single config or a list of configs. Args: fwk_configs: A single config or a list of configs. Examples: 1) single config: config.RTNConfig(weight_group_size=32) 2) single expandable config: config.RTNConfig(weight_group_size=[32, 64]) 3) mixed 1) and 2): [config.RTNConfig(weight_group_size=32), config.RTNConfig(weight_group_size=[32, 64])] Returns: ConfigSet: A ConfigSet object. """ config_list = cls.generate_config_list(fwk_configs) return cls(config_list) class Sampler: def __init__(self, config_source: Optional[ConfigSet]) -> None: pass def __iter__(self) -> Iterator[config.BaseConfig]: """Iterate over indices of config set elements.""" raise NotImplementedError class SequentialSampler(Sampler): """Samples elements sequentially, always in the same order. Args: config_source (_ConfigSet): config set to sample from """ config_source: Sized def __init__(self, config_source: Sized) -> None: self.config_source = config_source def __iter__(self) -> Iterator[int]: return iter(range(len(self.config_source))) def __len__(self) -> int: return len(self.config_source) default_sampler = SequentialSampler class ConfigLoader: def __init__( self, config_set: ConfigSet, sampler: Sampler = default_sampler, skip_verified_config: bool = True ) -> None: self.config_set = ConfigSet.from_fwk_configs(config_set) self._sampler = sampler(self.config_set) self.skip_verified_config = skip_verified_config self.verify_config_list = list() def is_verified_config(self, config): for verified_config in self.verify_config_list: if config == verified_config: return True return False def __iter__(self) -> Generator[config.BaseConfig, Any, None]: for index in self._sampler: new_config = self.config_set[index] if self.skip_verified_config and self.is_verified_config(new_config): logger.debug("Skip the verified config:") logger.debug(new_config.to_dict()) continue self.verify_config_list.append(new_config) yield new_config class TuningConfig: """Config for auto tuning pipeline. Examples: from onnx_neural_compressor.quantization import tuning tune_config = tuning.TuningConfig( config_set=[config1, config2, ...], max_trials=3, tolerable_loss=0.01) The tuning process stops when either of the following conditions is met: 1) The number of trials reaches the maximum trials. 2) The metric loss is within the tolerable loss. For condition 2), we calculate the metric loss as follows: relative_loss = (fp32_baseline - eval_result_of_q_model) / fp32_baseline If relative_loss <= tolerable_loss, we stop the tuning process. For example: tolerable_loss = 0.01 fp32_baseline = 100 eval_result_of_q_model = 99 relative_loss = (100 - 99) / 100 = 0.01 The metric loss is within the tolerable loss, so the tuning process is stopped. """ def __init__( self, config_set: Union[config.BaseConfig, List[config.BaseConfig]] = None, sampler: Sampler = default_sampler, tolerable_loss=0.01, max_trials=100, ): """Initial a TuningConfig. Args: config_set: A single config or a list of configs. Defaults to None. sampler: tuning sampler that decide the trials order. Defaults to default_sampler. tolerable_loss: This float indicates how much metric loss we can accept. The metric loss is relative, it can be both positive and negative. Default is 0.01. max_trials: Max tuning times. Combine with `tolerable_loss` field to decide when to stop. Default is 100. """ self.config_set = config_set self.sampler = sampler self.tolerable_loss = tolerable_loss self.max_trials = max_trials class _TrialRecord: @staticmethod def _generate_unique_id(): unique_id = str(uuid.uuid4()) return unique_id def __init__(self, trial_index: int, trial_result: Union[int, float], quant_config: config.BaseConfig): # The unique id to refer to one trial self.trial_id = _TrialRecord._generate_unique_id() self.trial_index = trial_index self.trial_result = trial_result self.quant_config = quant_config class TuningMonitor: def __init__(self, tuning_config: TuningConfig) -> None: self.tuning_config = tuning_config self.trial_cnt = 0 self.tuning_history: List[_TrialRecord] = [] self.baseline = None def add_trial_result( self, trial_index: int, trial_result: Union[int, float], quant_config: config.BaseConfig ) -> None: self.trial_cnt += 1 trial_record = _TrialRecord(trial_index, trial_result, quant_config) self.tuning_history.append(trial_record) def set_baseline(self, baseline: float): self.baseline = baseline logger.info(f"Fp32 baseline is {self.baseline}") def get_number_of_trials(self): return len(self.tuning_history) def need_skip(self, config) -> bool: """Check whether the expanded quant config is verified.""" if len(self.tuning_history) > 0 and any([config == i.quant_config.config_mapping for i in self.tuning_history]): logger.warning("Skip the verified config mapping.") logger.debug(config) return True return False def need_stop(self) -> bool: """Check if need to stop tuning. Either accuracy goal is met, max trials is reached or timeout is reached. Returns: stop_flag: True if need to stop, otherwise False. """ # reach max trials reach_max_trials = self.trial_cnt >= self.tuning_config.max_trials # reach accuracy goal meet_accuracy_goal = ( False if self.baseline is None else self.tuning_history[-1].trial_result >= (self.baseline * (1 - self.tuning_config.tolerable_loss)) ) # [-1] is the last element representing the latest trail record. return reach_max_trials or meet_accuracy_goal def print_config_diff(self, config): if len(self.tuning_history) == 0: logger.info("quant config: {}".format(config)) else: logger.info("quant config difference: {}".format(config.get_diff_dict(self.tuning_history[0].quant_config))) class TuningLogger: """A unified logger for the tuning/quantization process. It assists validation teams in retrieving logs. """ @classmethod def tuning_start(cls) -> None: logger.info("Tuning started.") @classmethod def trial_start(cls, trial_index: int = None) -> None: logger.info("%d-trail started.", trial_index) @classmethod def quantization_start(cls, stacklevel=2) -> None: logger.info("Quantization started.", stacklevel=stacklevel) @classmethod def quantization_end(cls, stacklevel=2) -> None: logger.info("Quantization end.", stacklevel=stacklevel) @classmethod def evaluation_start(cls) -> None: logger.info("Evaluation started.") @classmethod def evaluation_end(cls) -> None: logger.info("Evaluation end.") @classmethod def trial_end(cls, trial_index: int = None) -> None: logger.info("%d-trail end.", trial_index) @classmethod def tuning_end(cls) -> None: logger.info("Tuning completed.") def init_tuning(tuning_config: TuningConfig) -> Tuple[ConfigLoader, TuningLogger, TuningMonitor]: config_loader = ConfigLoader(config_set=tuning_config.config_set, sampler=tuning_config.sampler) tuning_logger = TuningLogger() tuning_monitor = TuningMonitor(tuning_config) return config_loader, tuning_logger, tuning_monitor def get_all_config_set() -> Union[config.BaseConfig, List[config.BaseConfig]]: return config.get_all_config_set_from_config_registry() def _need_apply(quant_config: config.BaseConfig, algo_name): return quant_config.name == algo_name if hasattr(quant_config, "name") else False def _quantize( model_input: Union[pathlib.Path, str], quant_config: config.BaseConfig, calibration_data_reader: data_reader.CalibrationDataReader = None, ) -> onnx.ModelProto: """The main entry to quantize a model. Args: model_input (Union[pathlib.Path, str]): Path or str to the model to quantize. quant_config (config.BaseConfig): a quantization configuration. calibration_data_reader (data_reader.CalibrationDataReader, optional): dataloader for calibration. Defaults to None. Returns: onnx.ModelProto: The quantized model. """ registered_configs = config.config_registry.get_cls_configs() if isinstance(quant_config, dict): quant_config = config.ComposableConfig.from_dict(quant_config, config_registry=registered_configs) logger.info(f"Parsed a config dict to construct the quantization config: {quant_config}.") else: assert isinstance( quant_config, config.BaseConfig ), f"Please pass a dict or config instance as the quantization configuration, but got {type(quant_config)}." logger.debug(f"Quantize model with config: \n {quant_config} \n") # select quantization algo according to config q_model = None for algo_name, algo_func in utility.algos_mapping.items(): if _need_apply(quant_config, algo_name): logger.info(f"Start to apply {algo_name} on the model.") q_model = algo_func(model_input, quant_config, calibration_data_reader=calibration_data_reader) return q_model def autotune( model_input: Union[pathlib.Path, str], tune_config: TuningConfig, eval_fn: Callable, eval_args: Optional[Tuple[Any]] = None, calibration_data_reader: data_reader.CalibrationDataReader = None, optimization_level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC, ) -> Union[None, onnx.ModelProto]: """The main entry of auto-tune. Args: model_input (Union[pathlib.Path, str]): onnx model path. tune_config (TuningConfig): tuning config. TuningConfig is created with algorithm configs, parameters supported tuning are in their params_list. Support: Expand parameters to a list of parameters like TuningConfig(config_set=[config.RTNConfig(weight_bits=[4, 8])]) Pass a list of configs like TuningConfig(config_set=[config.RTNConfig(), config.GPTQConfig()]) eval_fn (Callable): evaluate function. During evaluation, autotune will only pass model path as the input of function. eval_args (Optional[Tuple[Any]]): evaluate arguments. Positional arguments for `eval_fn`. calibration_data_reader (data_reader.CalibrationDataReader): dataloader for calibration. optimization_level (onnxruntime.GraphOptimizationLevel): graph optimization level. Support ORT_DISABLE_ALL, ORT_ENABLE_ALL, ORT_ENABLE_BASIC, ORT_ENABLE_EXTENDED. Default is ORT_ENABLE_BASIC. Details: https://onnxruntime.ai/docs/performance/model-optimizations/graph-optimizations.html#onlineoffline-mode """ best_quant_model = None eval_func_wrapper = EvaluationFuncWrapper(eval_fn, eval_args) config_loader, tuning_logger, tuning_monitor = init_tuning(tuning_config=tune_config) tmp_folder = tempfile.TemporaryDirectory() pathlib.Path(tmp_folder.name).joinpath("./eval").mkdir() if optimization_level != ort.GraphOptimizationLevel.ORT_DISABLE_ALL: sess_options = ort.SessionOptions() sess_options.graph_optimization_level = optimization_level sess_options.optimized_model_filepath = pathlib.Path(tmp_folder.name).joinpath("model.onnx").as_posix() sess_options.add_session_config_entry( "session.optimized_model_external_initializers_file_name", "model.onnx_data" ) sess_options.add_session_config_entry("session.optimized_model_external_initializers_min_size_in_bytes", "1024") session = ort.InferenceSession(model_input, sess_options, providers=["CPUExecutionProvider"]) # copy config.json to tmp dir for evaluation, LLMs evaluation may need it if isinstance(model_input, str) and os.path.exists( pathlib.Path(model_input).parent.joinpath("config.json").as_posix() ): shutil.copyfile( pathlib.Path(model_input).parent.joinpath("config.json").as_posix(), pathlib.Path(tmp_folder.name).joinpath("config.json").as_posix(), ) model_input = sess_options.optimized_model_filepath del session try: baseline: float = eval_func_wrapper.evaluate(model_input) except Exception as e: if "'str' object has no attribute 'SerializeToString'" in str(e): logger.warning("Please refine your eval_fn to accept model path (str) as input.") if "Unable to load from type ''" in str(e): logger.warning("Please pass model path to autotune API rather than onnx.ModelProto.") print(traceback.format_exc()) exit(0) tuning_monitor.set_baseline(baseline) tuning_logger.tuning_start() for trial_index, quant_config in enumerate(config_loader): # check whether config_mapping is verified config_mapping = quant_config.to_config_mapping(model=model_input) if tuning_monitor.need_skip(config_mapping): continue if calibration_data_reader is not None: calibration_data_reader.rewind() tuning_logger.trial_start(trial_index=trial_index) tuning_logger.quantization_start() tuning_monitor.print_config_diff(quant_config) q_model = _quantize(model_input, quant_config=quant_config, calibration_data_reader=calibration_data_reader) tuning_logger.quantization_end() tuning_logger.evaluation_start() # evaluate API requires str input onnx.save_model( q_model, pathlib.Path(tmp_folder.name).joinpath("./eval/model.onnx").as_posix(), save_as_external_data=True, all_tensors_to_one_file=True, size_threshold=1024, convert_attribute=False, ) # copy config.json to tmp dir for evaluation, LLMs evaluation may need it if isinstance(model_input, str) and os.path.exists( pathlib.Path(model_input).parent.joinpath("config.json").as_posix() ): shutil.copyfile( pathlib.Path(model_input).parent.joinpath("config.json").as_posix(), pathlib.Path(tmp_folder.name).joinpath("./eval/config.json").as_posix(), ) eval_result: float = eval_func_wrapper.evaluate( pathlib.Path(tmp_folder.name).joinpath("./eval/model.onnx").as_posix() ) tuning_logger.evaluation_end() logger.info("Evaluation result: %.4f", eval_result) tuning_monitor.add_trial_result(trial_index, eval_result, quant_config) tuning_logger.trial_end(trial_index) if tuning_monitor.need_stop(): external_data_helper.load_external_data_for_model( q_model, pathlib.Path(tmp_folder.name).joinpath("./eval").as_posix() ) best_quant_model = q_model break tuning_logger.tuning_end() if best_quant_model is None: logger.info( "Don't find the quantized model which meets accuracy requirement. " "Please try other configs or adjust tolerable_loss." ) exit(0) tmp_folder.cleanup() return best_quant_model