Perform Quantization

The following command downloads 100 images from Open Images v7 (approximately 34MB). You will use these to calibrate during the quantization.

Run the command on your development machine:

    

        
        
module-2-cnn-inference-with-quantization/scripts/aot/create_calibration_images.py \
  --data-dir datasets \
  --out module-2-cnn-inference-with-quantization/artifacts/mobilenetv2/calibration_images \
  --count 100

    

You should see output similar to this:

    

        
        
Downloading 100 Open Images validation images to datasets/open-images-v7-validation...
Verified 100 Open Images files.
Wrote 100 calibration images across 20 categories to module-2-cnn-inference-with-quantization/artifacts/mobilenetv2/calibration_images

    

The script downloads five images from each of 20 broad object categories and verifies every file with SHA256. A production calibration set should represent the cameras, lighting, composition, and objects that the deployed application will receive; this set is purely to demonstrate the workflow.

Quantize and lower the model

    

        
        
module-2-cnn-inference-with-quantization/scripts/aot/quantize_mobilenetv2_xnnpack.py \
  --spec module-2-cnn-inference-with-quantization/artifacts/mobilenetv2/model_spec.json \
  --calibration-images module-2-cnn-inference-with-quantization/artifacts/mobilenetv2/calibration_images \
  --num-calib 100

    

The script runs PT2E post-training quantization with the XNNPACK quantizer, and calibrates the prepared graph, converts it, lowers it to ExecuTorch with XNNPACK, and writes the INT8 .pte artifact.

You should receive a quantized artifact of similar size to that shown in the table below:

ArtifactBackendPrecision.pte size
mobilenetv2_xnnpack.pteXNNPACKFP3213.99 MB
mobilenetv2_int8_xnnpack.pteXNNPACKINT87.59 MB

In this run, the INT8 artifact was 7.59 MB rather than 13.99 MB, a reduction of approximately 46%.

An INT8 value uses one quarter of the storage of an FP32 value. That ratio does not apply automatically to the complete .pte artifact. The file also contains graph and backend metadata, not every component is quantized, and this lab leaves the classifier head in FP32 by default.

Understand the code

The script configures the quantizer as follows:

    

        
        
quantizer = XNNPACKQuantizer()
quantizer.set_global(get_symmetric_quantization_config(is_per_channel=True))

if not args.quantize_classifier_head:
    quantizer.set_filter_function(skip_classifier_head)

    
ExecuTorch API Highlight

XNNPACKQuantizer is the quantization interface supplied by the ExecuTorch XNNPACK backend. set_global() applies the selected quantization configuration to supported graph operations. The optional set_filter_function() call excludes selected operations—in this case the classifier head—from quantization. prepare_pt2e() and convert_pt2e() are PyTorch PT2E APIs that apply this backend policy.

The global configuration requests symmetric per-channel weight quantization where supported, using is_per_channel=True. Per-channel quantization gives each output channel its own scale, helping preserve precision when convolution filters have different weight ranges. Activations use one scale per tensor, determined during calibration.

By default, the script leaves the final classifier head in FP32. The classifier head is the last layer that maps MobileNetV2 features to ImageNet class scores. This is a conservative compromise between optimization and classification stability: most of the model’s compute is in the convolutional feature extractor, while the classifier head directly produces the final ImageNet scores. You can optionally pass --quantize-classifier-head as a follow-up experiment. It may reduce size or latency a little more, but rerun the delegation, latency, and top-5 checks because the final scores may shift.

Next, the script prepares the exported model for calibration by adding observer modules:

    

        
        
exported = export(model, example_inputs)
prepared = prepare_pt2e(exported.module(), quantizer)
move_exported_model_to_eval(prepared)

    

The observers collect values during calibration; their presence alone does not make inference faster. The calibration loop supplies the observed ranges:

    

        
        
for index, (image, _) in enumerate(calibration_loader):
    if index >= args.num_calib:
        break
    prepared(image)

    

Calibration quality depends on input quality. A set with different lighting, composition, preprocessing, or subject matter from the deployed workload can produce activation ranges that do not represent real use. Module 1 did not include this step because its transformer comparisons remained in FP32.

After calibration, conversion creates the quantized model. The script exports and lowers that converted graph:

    

        
        
quantized_model = convert_pt2e(prepared)
exported_quantized = export(quantized_model, example_inputs)
edge = to_edge_transform_and_lower(
    exported_quantized,
    partitioner=[XnnpackPartitioner()],
    compile_config=EdgeCompileConfig(_check_ir_validity=False),
)
program = edge.to_executorch()

    

The resulting graph is no longer the original FP32 program. It contains the quantized operations and delegated regions that ExecuTorch will execute.

Optional further reading

Learn more about Quantization

CNN classifiers are often a practical starting point for quantization because convolution, activation, and pooling patterns are common in backend quantization flows; their input shape is usually fixed; and top-5 classification often tolerates small score changes.

XNNPACK also supports dynamic activation quantization for selected operations. Static quantization uses activation scales established during calibration; dynamic quantization calculates them from the actual activations during inference. Static quantization avoids runtime scale estimation. Dynamic quantization can help when activation ranges vary substantially, as they can in transformer projections, but requires runtime scale estimation and has different operator coverage. See the XNNPACK quantization documentation for supported schemes.

Not every model will fit a chosen backend’s supported quantization patterns. Some may need selective quantization, mixed precision, or changes to their PyTorch implementation. For example, a custom layer may perform a supported computation using a pattern the quantizer does not recognize. It can sometimes be replaced with an equivalent supported representation, but the replacement’s FP32 outputs should be checked against the original on identical inputs before quantization.

When quantization reduces output quality, a calibration set that better matches the application’s images may help. Quantization-aware training (QAT) is another option: it simulates quantization effects during training or fine-tuning so that the model can adapt before final INT8 conversion.

See the PyTorch 2 Export QAT documentation , which matches this module’s PT2E and XNNPACKQuantizer approach. The broader torchao QAT workflow explains fake quantization and the prepare, train, and convert sequence.

Previous page Complete and continue