4.1.0
Quantized model support


ST Edge AI Core

Quantized model support


ST Edge AI Core Technology 4.1.0



r1.5

References

[1] “A Survey of Quantization Methods for Efficient Neural Network Inference” - https://arxiv.org/abs/2103.13630v3
[2] “Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference” - https://arxiv.org/abs/1712.05877
[3] “Quantization and benchmarking of deep learning models using ONNX Runtime and STM32Cube.AI Developer Cloud” - https://github.com/STMicroelectronics/stm32ai-modelzoo-services/blob/main/tutorials/notebooks

Overview

ST Edge AI Core CLI can be used to deploy quantized models. This article covers only 8-bit integer-based quantized models. Support for lower-than-8-bit quantization is detailed in the “Deep Quantized Neural Network (DQNN) support” article.

Quantization is an optimization technique [1] used to compress a 32-bit floating-point model. This compression reduces the model’s size, decreases memory usage at runtime, and enhances processing unit efficiency and latency, including power consumption, with only a minor loss in accuracy. In a quantized model, some or all operations are performed on tensors using integers instead of floating-point values. Quantization is a key component of various optimization strategies, including topology-oriented optimization, feature-map reduction, pruning, and weight compression, which are crucial for deploying models in resource-constrained environments.

There are two primary quantization methods:

Supported file format

ST Edge AI Core can import the following quantized model formats:

  • Quantized TensorFlow Lite models generated by a post-training or quantization-aware process. Calibration is performed by the TensorFlow Lite framework, primarily through the TFLite Converter, which exports a TensorFlow Lite file.
  • Quantized ONNX models based on either the operator-oriented (QOperator) or tensor-oriented (QDQ, Quantize and DeQuantize) format. The first format depends on the supported QOperators (see the “QLinearXXX* operators”) while the second is more generic. The DeQuantizeLinear(QuantizeLinear(tensor)) operators are inserted between the original operators (in float) to simulate the quantization and dequantization process. Both formats can be generated with the ONNX runtime module.
End-to-end flow to quantize a Pytorch, Tensorflow, Keras model

For performance reasons related to peak memory usage and processing capabilities, the deployed kernels support only quantized weights and activations (“integer-only quantization” mode). If this pattern is not detected by the optimization and rendering passes of the code generator, it falls back to a floating-point implementation of the operator. This fallback involves inserting QUANTIZE/DEQUANTIZE operators, a process known as fake or simulated quantization. Consequently:

  • The dynamic quantization approach, which determines quantization parameters during inference, is not supported. If this type of model is provided, a fully float32 implementation is generated.
  • Mixed or hybrid models are supported.

The analyze, validate, and generate commands can be used without limitations.

$ stedgeai analyze -m <quantized_model_file>.tflite --target stm32
$ stedgeai validate -m <quantized_model>.tflite --target stm32 -vi test_data.npz
$ stedgeai analyze -m <quantized_model_file>.onnx --target stm32

Privileged quantization scheme

target requested quantization scheme
stm32xx devices based on Arm Cortex®-M processor ss/sa per channel, binary quantization (other 8-bit quantization scheme including per-tensor are supported but not necessarily optimized)
stm32xx with Neural-ART IP ss/sa per channel
sr5xx devices based on Arm Cortex® M7 processor ss/sa per channel, binary quantization (other 8-bit quantization scheme including per-tensor are supported but not necessarily optimized)
sr6p[xx] or sr6g[xx] devices based on Arm Cortex® R52+ and Arm Cortex® M4 processors ss/sa per channel, binary quantization (other 8-bit quantization scheme are supported but not necessarily optimized)
ISPU target ss/sa per channel, binary quantization
MLC target see “ST Edge AI Core for MLC” article
ISPU target see “ST Edge AI Core for ISPU” article

Quantized tensors ([1], [2])

ST Edge AI Core supports 8-bit integer arithmetic (int8 or uint8) for quantized tensors, following the convention used by Google for quantized models ([2]). Each real number r is represented as a function of the quantized value q, a scale factor (an arbitrary positive real number), and a zero point. This quantization scheme is an affine mapping from the integer value q to the real value r. The zero point has the same integer C type as the quantized data.

Precision depends on the scale factor, and quantized values are linearly distributed around the zero point value. Unlike floating-point representation, the resolution is constant across the tensor.

8-bit affine quantization - integer precision

Per-axis vs per-tensor quantization

In per-tensor (or layer-wise) quantization, the same format (that is, scale and zero point) is used for the entire tensor, whether it contains weights or activations. In contrast, per-axis (or per-channel) quantization, which is used in convolution-based operators, assigns one scale and/or zero point to each filter. These values are independent across channels, usually improving accuracy with negligible computational overhead.

The per-axis approach is currently the standard method for quantized convolutional kernels (weight tensors). Activation tensors always use per-tensor quantization. This distinction guides the design of the optimized C kernels.

Symmetric vs Asymmetric

In asymmetric quantization, the tensor can have a zero point anywhere within the signed 8-bit range [-128, 127] or the unsigned 8-bit range [0, 255]. In contrast, symmetric quantization forces the zero point to zero. This constraint enables kernel optimizations that reduce the cost of some operations through offline precomputation.

In TFLite and ONNX quantized models, weights and biases are symmetrically quantized. For activations, both asymmetric and symmetric formats are supported.

Signed integer vs Unsigned integer - supported schemes

Implementations do not support every combination of integer type and symmetric/asymmetric format for weights and activations. Only the following integer schemes are supported:

scheme weights activations
ua/ua unsigned and asymmetric unsigned and asymmetric
ss/sa signed and symmetric signed and asymmetric
ss/ua signed and symmetric unsigned and asymmetric

Quantized TensorFlow models

ST Edge AI Core can import TensorFlow Lite models produced by quantization-aware training or post-training quantization. Earlier quantization-aware training models used the “ua/ua” scheme. TensorFlow v1.15 and v2.x quantized models now use the “ss/sa” and per-channel schemes. Activations are asymmetric and signed (int8), while weights and biases are symmetric and signed (int8). This scheme is also the privileged scheme to address efficiently the Coral Edge TPUs or TensorFlow Lite for Microcontrollers runtime.

Supported/recommended methods

method/option supported/recommended
Dynamic range quantization Not supported; only the static approach is supported.
Full integer quantization Supported; use a representative dataset for calibration.
Integer with float fallback (using default float input/output) Supported as a mixed model; use a representative dataset for calibration.
Integer only Recommended; use a representative dataset for calibration.
Weight-only quantization Not supported.
Input/output data type uint8, int8, and float32 are supported.
Float16 quantization Not supported.
Per-channel quantization The default behavior cannot be modified.
Activation type int8 with the “ss/sa” scheme.
Weight type int8 with the “ss/sa” scheme.

“Integer only” method

The following code snippet illustrates the recommended TFLiteConverter options to enforce a full integer scheme, that is, post-training quantization for all operators, including the input and output tensors.

def representative_dataset_gen():
  data = tload(...)

  for _ in range(num_calibration_steps):
    # Get sample input data as a numpy array in a method of your choosing.
    input = get_sample(data)
    yield [input]

converter = tf.lite.TFLiteConverter.from_saved_model(<saved_model_dir>)
# converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.representative_dataset = representative_dataset_gen
# This enables quantization
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# This ensures that if any ops can't be quantized, the converter throws an error
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
# These set the input and output tensors to int8
converter.inference_input_type = tf.int8  # or tf.uint8
converter.inference_output_type = tf.int8  # or tf.uint8
quant_model = converter.convert()

# Save the quantized file
with open(<tflite_quant_model_path>, "wb") as f:
    f.write(quant_model)
...

“Full integer quantization” method

Because ST Edge AI Core supports mixed models, the full integer quantization method can be used. However, it is preferable to enable the TensorFlow Lite ops (TFLITE_BUILTINS), because only a limited number of TensorFlow operators are supported (see the supported [TFLITE] operators).

...
converter = tf.lite.TFLiteConverter.from_saved_model(<saved_model_dir>)
# converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.representative_dataset = representative_dataset_gen
# This enables quantization
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# This optional option, ensures that TF Lite operators are used.
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS]
# These set the input and output tensors to float32
converter.inference_input_type = tf.float32  # or tf.uint8, tf.int8
converter.inference_output_type = tf.float32  # or tf.uint8, tf.int8
quant_model = converter.convert()
...

Tip

Quantization of the input and output tensors is optional. They can remain in floating-point format for convenience and easier deployment, for example, to keep preprocessing or postprocessing in floating-point format.

Warning

The 'tf.lite.Optimize.DEFAULT' option enables quantization. However, to ensure that both weights and activations are quantized, the 'representative_dataset' attribute must always be used. Otherwise, only the weights and parameters are quantized, reducing the generated file size by approximately a factor of four. But in this case, as for the deprecated option OPTIMIZE_FOR_SIZE, the TFLite file is deployed as a fully floating-point C-model with the weights in floating-point format (dequantized values).

TensorFlow lite, OPTIMIZE_FOR_SIZE option support

Post-quantization TensorFlow lite script (TFLiteConverter, TF 1.15) allows to generate a weight-only quantized file ('OPTIMIZE_FOR_SIZE' option). This simplest scheme (also called “hybrid” quantization) allows to reduce the size of the generated file (~by 4). Only the weights are quantized from floating-point to 8 bits of precision. At inference, weights are converted from 8 bits of precision to floating-point and computed using floating-point kernels.

This quantization scheme is not supported by ST Edge AI Core, in particular by the C-inference engine and operator implementations (network runtime library), mainly because of the devices resource constraints. Additional RAM memory to cache the uncompressed parameters would be requested to reduce the latency. If this model is imported, the parameters are converted to floating-point before code generation. Only the full 8b integer quantization of weights and activations scheme is supported.

Quantized ONNX models

Quantize and DeQuantize (QDQ) format

To quantize an ONNX model, use the quantization services provided by the ONNX Runtime module. The tensor-oriented (QDQ, Quantize and DeQuantize) format is recommended. As illustrated in the following figure, the additional DeQuantizeLinear(QuantizeLinear(tensor)) operators inserted between the original operators are automatically detected and removed so the associated optimized integer kernels can be deployed.

Notes

  • The quantize process automatically merges Batch Normalization operators. The model can also be optimized for inference before quantization; the ONNX Simplifier can be used for this purpose.

      $ python -m onnxruntime.quantization.preprocess --input model.onnx --output model-infer.onnx
  • Because the deployed kernels primarily use channel-last (NHWC) data format, and to preserve the original input data representation, a ‘Transpose’ operator is added in the generated graph. Note that, depending on the target, this operation can be performed by software and the cost may not be negligible. To improve this behavior and also to take account for application constraints, advanced options can be used to specify the expected input or output data layout (refer to “How to change the I/O data type or layout (NHWC vs NCHW)” article for more details)

  • By default, the I/O data type (float32) of the original ONNX model is not preserved but optimized to align with the activation type used by the quantization scheme. Advanced options can be used to specify the expected data type. As illustrated in the following figure, the option “–input-data-type float32” is used to insert a quantizer operation and preserve the original data type. (Refer to “How to change the I/O data type or layout (NHWC vs NCHW)” article for more details)

Supported/recommended methods/options

method/option supported/recommended
Dynamic quantization Not supported; only the static approach is supported.
Static quantization Recommended; use a representative dataset for calibration.
Quantization format 'QuantFormat.QDQ' is recommended. 'QuantFormat.QOperator' is not recommended, not tested, and has limited support for “QLinearXXX* operators”.
Activation type 'QuantType.QInt8' is recommended (default). 'QuantType.QUInt8' is not recommended and not tested.
Weight type 'QuantType.QInt8' is recommended (default). 'QuantType.QUInt8' is not recommended and not tested.
Calibration method 'CalibrationMethod.MinMax', 'CalibrationMethod.Entropy', and 'CalibrationMethod.Percentile' can be used.
Per-channel Recommended: True. Per-tensor quantization (False) is not recommended and not tested.
nodes_to_exclude/nodes_to_quantize Supported; mixed models can be deployed with ST Edge AI Core.

Warning

As mentioned in the ONNX runtime documentation, “Data type selection” section, on x86-64 machines with AVX2 and AVX512 extensions, ONNX runtime uses the VPMADDUBSW instruction for U8S8 for performance. This instruction might suffer from saturation issues: it can happen that the output does not fit into a 16-bit integer and has to be clamped (saturated) to fit. Generally, this is not a significant issue for the final result. However, if you encounter a large accuracy drop, it may be caused by saturation. In this case, try reduce_range or the U8U8 format, which does not have saturation issues. There is no such issue on other CPU architectures (x64 with VNNI and Arm).

“Static Quantization” method

Note

To illustrate the usage of ONNX quantizer, a complete notebook is available - “Quantization and benchmarking of deep learning models using ONNX runtime and STM32Cube.AI Developer Cloud” [3].

The following code snippet illustrates a typical Python script (posttraining quantization) to quantize an NN model processing images (classifier or object-detector applications). It is based on the end-to-end example from the “Quantize ONNX Models” article. Note that only the inputs (images used for the calibration) are requested and the associated data can be transposed (hwc to chw) to be conform to the expected input data representation.

import numpy
import onnxruntime
from onnxruntime.quantization import QuantFormat, QuantType, StaticQuantConfig, quantize, CalibrationMethod
from onnxruntime.quantization import CalibrationDataReader
from PIL import Image

input_model_path = 'my_model.onnx'
output_model_path = 'my_model_quantized.onnx'

calibration_dataset_path = '/path/to/data/for/calibration'

def _preprocess_images(images_folder: str, height: int, width: int):
    """
    Load a batch of images and preprocess them..
    """
    image_names = os.listdir(images_folder)
    batch_filenames = image_names
    unconcatenated_batch_data = []

    for image_name in batch_filenames:
        image_filepath = images_folder + "/" + image_name
        pillow_img = Image.new("RGB", (width, height))
        pillow_img.paste(Image.open(image_filepath).resize((width, height)))
        input_data = numpy.float32(pillow_img) - numpy.array(
            [123.68, 116.78, 103.94], dtype=numpy.float32
        )
        nhwc_data = numpy.expand_dims(input_data, axis=0)
        nchw_data = nhwc_data.transpose(0, 3, 1, 2)  # ONNX Runtime standard
        unconcatenated_batch_data.append(nchw_data)
    batch_data = numpy.concatenate(
        numpy.expand_dims(unconcatenated_batch_data, axis=0), axis=0
    )
    return batch_data

class XXXDataReader(CalibrationDataReader):
    def __init__(self, calibration_image_folder: str, model_path: str):
        self.enum_data = None

        # Use inference session to get input shape.
        session = onnxruntime.InferenceSession(model_path, None)
        (_, _, height, width) = session.get_inputs()[0].shape

        # Convert image to input data
        self.nhwc_data_list = _preprocess_images(
            calibration_image_folder, height, width)
      
        self.input_name = session.get_inputs()[0].name
        self.datasize = len(self.nhwc_data_list)

    def get_next(self):
        if self.enum_data is None:
            self.enum_data = iter(
                [{self.input_name: nhwc_data} for nhwc_data in self.nhwc_data_list]
            )
        return next(self.enum_data, None)

    def rewind(self):
        self.enum_data = None

dr = XXXDataReader(
        calibration_dataset_path, input_model_path
    )

conf = StaticQuantConfig(
    calibration_data_reader=dr,
    quant_format=QuantFormat.QDQ,
    calibrate_method=CalibrationMethod.MinMax,
    optimize_model=True,
    activation_type=QuantType.QInt8,
    weight_type=QuantType.QInt8,
    # nodes_to_exclude=['resnetv17_dense0_fwd', ..],
    # nodes_to_quantize=['resnetv17_dense0_fwd', ..],
    per_channel=True)
      
quantize(infer_model,
    output_model_path, conf)

Note that for a quick evaluation in term of inference time and memory footprint, the XXXDataReader object can be updated to generate the fake image with the random data.

import numpy

class XXXDataReader(CalibrationDataReader):
    def __init__(self, calibration_image_folder: str, model_path: str):
        self.enum_data = None

        # Use inference session to get input shape.
        session = onnxruntime.InferenceSession(model_path, None)
        (_, chnannel, height, width) = session.get_inputs()[0].shape

        # Generate the random data in the half-open interval [0.0, 1.0).
        self.nhwc_data_list = [np.random.random_sample((1, chnannel, height, width)).astype(np.float32)
                               for i in range(20)]

        self.input_name = session.get_inputs()[0].name
        self.datasize = len(self.nhwc_data_list)

    def get_next(self):
        if self.enum_data is None:
            self.enum_data = iter(
                [{self.input_name: nhwc_data} for nhwc_data in self.nhwc_data_list]
            )
        return next(self.enum_data, None)

    def rewind(self):
        self.enum_data = None

Requested ONNX Opset

Models must use opset 7 or later to be quantized. Models with an opset below 10 must be reconverted to ONNX from their original framework using a later opset. To enable advanced optimizations such as batch-normalization folding, opset 21 is recommended.

import onnx

original_model_path = 'original_model.onnx'
new_model_path = 'new_model.onnx'

new_opset = 21

onnx_model = onnx.load(original_model_path)
converted_model = onnx.version_converter.convert_version(onnx_model, new_opset)
onnx.save(converted_model, new_model_path)

ONNX Simplifier

Before quantizing an ONNX model, you can simplify it to produce a more efficient inference model for quantization or deployment.

$ onnxsim my_model.onnx my_simplified_model.onnx --overwrite-input-shape 1,3,224,224

Known limitations

  • ONNX QuantizeLinear and DequantizeLinear are supported only when the axis attribute corresponds to the channel dimension. In other cases, the generated code may be incorrect without an explicit error from ST Edge AI Core. If the following assertion is raised during model validation, quantize the ONNX model per tensor (per_channel=False) as a possible workaround.
Assertion failed: n_channel_in == ( (ai_shape_dimension*)((((&((p_tensor_weights)->shape))))->data) )[(0x0)],
file <root_file_location>\layers_conv2d_stm32_integer.c, line 3628 

Quantize Pytorch models

PyTorch models (Python models and .pth files) are not natively supported by ST Edge AI Core. To deploy a PyTorch model, convert it to ONNX format with fixed dimensions and a batch size of 1 (see the following code snippet). To quantize a PyTorch model, use the ONNX Runtime services. PyTorch provides an initial beta version of a quantization API, but its generated quantized models cannot currently be exported to a standard ONNX format that ST Edge AI Core can import.

import torch.onnx
from torch import nn

torch_model = MyPytorchModel(..)

dummy_input = torch.randn(1, 3, 224, 224) # fixed dimension
input_names = [ "actual_input" ]
output_names = [ "output" ]

torch.onnx.export(
    torch_model, # pytorch model (with the weights)
    dummy_input, # model input (or a tuple for multiple inputs)
    "my_model.onnx", # where to save the model
    do_constant_folding=True, # whether to execute constant folding for optimization
    input_names=input_names, # the model's input names
    output_names=output_names, # the model's output names
    opset_version=13, # the ONNX version to export the model to
    export_params=True, # store the trained parameter weights
    verbose=False
    )