Quantization#
The Core AI backend compresses models with coreai-opt, which is installed with the backend’s pinned requirements. ExecuTorch wraps it in two classes:
CoreAIQuantizerfor linear quantization, which stores weights (and optionally activations) in a lower-precision integer or floating-point format. It follows the PT2E prepare, calibrate and convert flow.CoreAIPalettizerfor palettization, which replaces each weight with a small lookup table of values plus, for each element, an index into that table.
Both run on your model before the final torch.export for lowering. You then export and lower the result as usual.
Supported Quantization Schemes#
Scheme |
Front-end |
Configuration |
Granularity |
|---|---|---|---|
INT8 weights and activations |
|
|
Weights per channel; activations per tensor, static |
INT8 weight-only |
|
|
Per channel |
INT4 weight-only |
|
|
Per channel |
INT4 weight-only, per block |
|
|
Blocks of 32 elements |
INT2, FP8 or FP4 weight-only |
|
A custom configuration. See Other weight formats. |
Per channel or per block |
4- or 6-bit palettization |
|
|
One lookup table per group of 16 output channels |
8-bit palettization |
|
|
One lookup table per weight |
The integer presets are all symmetric. Activation quantization is static: calibration fixes each activation’s scale, which is then stored in the model, rather than computing it at runtime. w4_per_block() gives each block of 32 weights its own scale instead of w4()’s single scale per output channel. The finer scales can preserve more accuracy, at the cost of storing more of them.
QuantizerConfig comes from coreai_opt.quantization and KMeansPalettizerConfig from coreai_opt.palettization. The presets cover common cases; construct a configuration yourself for finer control.
Linear Quantization with CoreAIQuantizer#
prepare exports the model and inserts fake quantization. You then run calibration data through the prepared model, and convert produces the quantized model:
import torch
from coreai_opt.quantization import QuantizerConfig
from executorch.backends.apple.coreai import (
CoreAIPartitioner,
get_default_compile_config,
get_default_passes,
)
from executorch.backends.apple.coreai.quantizer import CoreAIQuantizer
from executorch.exir import to_edge_transform_and_lower
quantizer = CoreAIQuantizer(model, QuantizerConfig())
prepared = quantizer.prepare(example_inputs)
with quantizer.calibration_mode():
for inputs in calibration_data:
prepared(*inputs)
quantized = quantizer.convert()
et_program = to_edge_transform_and_lower(
torch.export.export(quantized, example_inputs),
transform_passes=get_default_passes(),
partitioner=[CoreAIPartitioner()],
compile_config=get_default_compile_config(),
).to_executorch()
Calibration data: the default configuration quantizes activations, so calibrate with inputs representative of real use. The weight-only presets don’t need calibration.
Dynamic shapes: pass
dynamic_shapestoprepareif the model has dynamic inputs.Output: unlike standard PT2E, which produces generic quantize and dequantize ops,
convertreturns a model that already uses Core AI’s quantized ops, ready to export.
For quantization-aware training, train the prepared model inside quantizer.training_mode() instead of calibrating it, then call convert.
Other weight formats#
The presets cover INT8 and INT4. For other weight formats, build a weight-only configuration that sets a QuantizationSpec for each layer’s weight and leaves activations unquantized:
import torch
from coreai_opt.quantization import (
ModuleQuantizerConfig,
QuantizationSpec,
QuantizerConfig,
)
def weight_only(dtype, scale_dtype=None, granularity=None):
spec = QuantizationSpec(
dtype=dtype,
scale_dtype=scale_dtype,
granularity=granularity or {"type": "per_channel", "axis": 0},
)
return QuantizerConfig(
global_config=ModuleQuantizerConfig(
op_state_spec={"weight": spec},
op_input_spec=None,
op_output_spec=None,
)
)
config = weight_only(torch.float8_e4m3fn)
Then pass the configuration to CoreAIQuantizer as usual. These formats run on Core AI:
Format |
|
|
|
|---|---|---|---|
INT2 |
|
default |
per channel, or per block such as |
FP8 |
|
default |
per channel |
FP8 microscaling |
|
|
per block of 32: |
FP4 microscaling |
|
default (the scale is always |
per block of 32 |
INT2 keeps only four levels per weight, so check your model’s accuracy carefully. Per-block scales make the model larger than per-channel scales.
Palettization with CoreAIPalettizer#
CoreAIPalettizer uses k-means clustering to build each weight’s lookup table. It needs no calibration data:
from coreai_opt.palettization import KMeansPalettizerConfig
from executorch.backends.apple.coreai.palettizer import CoreAIPalettizer
palettizer = CoreAIPalettizer(model, KMeansPalettizerConfig.presets.w4())
palettizer.prepare(example_inputs)
palettized = palettizer.finalize()
et_program = to_edge_transform_and_lower(
torch.export.export(palettized, example_inputs),
transform_passes=get_default_passes(),
partitioner=[CoreAIPartitioner()],
compile_config=get_default_compile_config(),
).to_executorch()
finalize returns a regular nn.Module, ready to export. Palettization only compresses weights; activations stay in floating point.
Optional settings:
Sensitivity-weighted clustering: after
prepare, run data throughwith palettizer.calibration_mode(loss_fn=..., sensitivity_path=...) as calibrator:, callingcalibrator.step(output, target)for each batch. This saves per-weight sensitivities tosensitivity_path; pass that path to a laterprepareso clustering favors the weights that matter most.Large models: pass
num_workerstoprepareto parallelize clustering, andmmap_dirtofinalizeto memory-map large lookup tables rather than holding them in memory.
coreai-opt builds its k-means extension on first use, which requires ninja on your PATH (pip install ninja).