← Tiled Thoughts

Quantization in MLIR: Types, Scales, and Where to Put the q

Contents
  1. 1. Quick mental model: What are we even quantizing?
  2. 2. Quantized types: putting scale and zero point in the type system
  3. 3. quantize and dequantize ops: where do we switch domains?
  4. 4. Quantization and linalg: generic ops, concrete kernels
  5. 5. Quantization-friendly fusion and vectorization
  6. 6. Where to put the “q”: design considerations
  7. 7. Design questions to ponder

Quantization is one of those things where everyone unanimously agrees “it’s important”, but the details are often fuzzy; I mean, no one really wants to think about it in their compiler’s IR, right?

In most stacks, quantization lives in an (awkward?) place:

MLIR is actually a sweet spot to make quantization less cursed:

You can make quantization a first-class citizen of your IR: visible in types, explicit in ops, and modular in passes.

In this post, I want to walk through:

  1. What “quantization” means in this context,
  2. How you can represent quantized tensors in MLIR types,
  3. Where to insert quantize and dequantize ops in your IR,
  4. How this plays with dialects like Linalg.
  5. Some design considerations worth arguing about.

1. Quick mental model: What are we even quantizing?

Most ML inference quantization schemes boil down to this:

The classic affine mapping is:

real_value = scale * (quantized_value - zero_point)

So if you want this integrated in the IR, we need to answer:


2. Quantized types: putting scale and zero point in the type system

MLIR’s type system is rich enough that quantization metadata does not have to float around in comments or attributes. You can make it part of the type.

A conceptual example:

// An 8-bit signed quantized element type
!qint = !quant.uniform<i8:f32, scale=0.02, zero_point=-5>

// A quantized tensor type
!qtensor = tensor<128x128x64x!qint>

Here, !quant.uniform<i8:f32, scale=0.02, zero_point=-5> defines a quantized integer type with:

And, we get some nice properties:

We can also define different quantization schemes (e.g., symmetric, asymmetric, per-channel) by extending the type system further.

E.g. for per-channel quantization

// Per-channel quantization along the C dimension
!qconv_weight = tensor<64x3x3x3x!quant.uniform<i8:f32,
                                               scales = [0.01, 0.02, ...],
                                               zero_points = [0, 0, ...],
                                               axis = 0>>

Per-channel quantization allows each output channel to have its own scale and zero point, which is common in convolutional weights.


3. quantize and dequantize ops: where do we switch domains?

At some point, we need to convert between real-valued tensors and quantized tensors. This is where quantize and dequantize ops come in.

%f = ... : tensor<...xf32>

// Float to quantized
%q = "quant.quantize" %f : tensor<...xf32> to tensor<...x!qint>

// Quantized to float
%f2 = "quant.dequantize" %q : tensor<...x!qint> to tensor<...xf32>

The big question is: Where do we place these ops in the IR?

Two common patterns:

  1. Early quantization, late dequantization:

Pros:

Cons:

  1. Late quantization (backend-specific):

Pros:

Cons:

In practice, a hybrid approach could be used:


4. Quantization and linalg: generic ops, concrete kernels

MLIR’s linalg dialect is a great fit for quantization because it provides high-level, generic operations that can be specialized for quantized types.

For example,

//linalg.matmul on quantized types
linalg.matmul ins (%A_q, %B_q : tensor<MxKx!qint>, tensor<KxNx!qint>) 
               outs (%C_q : tensor<MxNx!qint>)

But what does this mean semantically?

A reasonable pattern is

  1. Keep linalg ops element-type-generic: they operate on whatever types are given (quantized or real).

  2. Add passes that:

    • Lower quantized linalg ops into int kernels + explicit rescaling.
    // Pseudocode lowering
    %acc "linalg.matmul" ins(%A_int8, %B_int8) : tensor<MxKxi8>, tensor<KxNxi8> -> tensor<MxNxi32>
    %out = "quant.rescale" (acc) 
    {scale = ..., zero_point = ...} : tensor<MxNxi32> to tensor<MxNxi8>
    • Or directly lower to backend-specific quantized kernels that understand the quantization parameters.
  3. Let backend-specific passes choose instructions based on the quantization parameters encoded in types.

The key idea:

linalg gives you structured loops; quantization adds semantic constraints.

The compiler’s job is to bridge the two: ensuring that quantized semantics are respected while still leveraging the high-level structure of linalg ops.


5. Quantization-friendly fusion and vectorization

Quantization graphs are full of patterns like:

To make these fast, you want to

In MLIR terms, this means:

%q = quant.quantize %f : tensor<...xf32> to tensor<...x!qint>
%y = "mydialect.conv"(%q, %w) : tensor<...x!qint>, tensor<...x!qint> -> tensor<...x!qint>
%z = "mydialect.relu"(%y) : tensor<...x!qint> -> tensor<...x!qint>
%out = quant.dequantize %z : tensor<...x!qint> to tensor<...xf32>

into a single fused op that stays in the quantized domain:

%out = "mydialect.fused_conv_relu"(%f, %w) :
    tensor<...xf32>, tensor<...x!qint> -> tensor<...xf32>
%vA = vector.transfer_read %A_q ... : tensor<...x!qint> to vector<...xi8>
%vB = vector.transfer_read %B_q ... : tensor<...x!qint> to vector<...xi8>
%VAcc = "vector.dot_qi8"(%vA, %vB) : vector<...xi8>, vector<...xi8> -> vector<...xi32>

Again, whether you use actual quant.* dialect types or domain-specific ones, the pattern is


6. Where to put the “q”: design considerations

Zooming out, a plausible high-level storyline for an MLIR-based quantization pipeline could be:

  1. Model Import: Load a floating-point model (e.g., from ONNX, TensorFlow).
  2. Quantization Pass: Analyze the model, decide which tensors/ops to quantize, attach parameters to types, insert quantize/dequantize ops.
  3. Quantized Optimization Passes: Fuse quantized ops, fold unnecessary conversions, vectorize quantized computations.
  4. Structured Lowering: Lower linalg and other high-level ops to quantized kernels, respecting quantization semantics.
  5. Backend Lowering:Final mapping to LLVM or target-specific codegen, leveraging quantized instructions.

7. Design questions to ponder

None of this is set in stone, quantization is a moving target with many open questions:

These are exciting questions for compiler and MLIR designers to explore.