Home > EulerNPU > Tutorials > 05. INT8 Quantization

05. INT8 Quantization

On a small NPU, FP32 weights are expensive for two reasons: memory (4× weight bytes) and compute (one FP32 multiply ≈ 4× the area of one INT8 multiply). Quantization shrinks both ~4×.

eulernpu's quantization is:

QAT (Quantization-Aware Training) is an add-on when needed.

Spec-Level Declaration

quantization:
  mode: int8                  # fp32 | int8 | int4
  policy: symmetric           # symmetric (default) | asymmetric
  activation_bits: 8
  weight_bits: 8
  accumulator_bits: 32        # S07: ≥ activation_bits

Rule S07: accumulator_bits < activation_bits is overflow-risky → the validator blocks it.

Rule S23: a quantize / dequantize adjacency mismatch in the graph is blocked.

Quantize + Compile Flow

# 1) Compile with FP32 weights + generate golden
eulernpu compile --spec spec.yaml --weights fp32_weights.npz \
                 --backend cpu_ref --out /tmp/golden
eulernpu run     --artifact /tmp/golden --input input.npz --out /tmp/golden.out.npz

# 2) INT8 quantize (compute per-tensor scale and apply)
eulernpu quantize \
    --weights fp32_weights.npz \
    --calib   calib_data.npz \
    --out     int8_weights.npz \
    --report  /tmp/quant_report.json

# 3) INT8 compile + run
eulernpu compile --spec spec_int8.yaml --weights int8_weights.npz \
                 --backend cpu_ref --out /tmp/int8
eulernpu run     --artifact /tmp/int8 --input input.npz --out /tmp/int8.out.npz

# 4) FP32 vs INT8 accuracy comparison
eulernpu compare --got /tmp/int8.out.npz --expected /tmp/golden.out.npz --tol 0.02

Reading the QuantReport

cat /tmp/quant_report.json
{
  "tensors": {
    "fc1.weight": {"scale": 0.0127, "max_abs": 1.612, "quant_err_max": 0.0098},
    "fc2.weight": {"scale": 0.0089, "max_abs": 1.131, "quant_err_max": 0.0042}
  },
  "summary": {
    "n_tensors": 12,
    "max_err": 0.0098,
    "median_err": 0.0021
  }
}

If quant_err_max ≥ 0.05 on any layer, per-layer outlier analysis is recommended (clipping or per-channel quantization). Narrow-distribution datasets like CWRU bearing usually hold accuracy with PTQ alone (the P07 case).

Fake-Fixed-Point Host Sim

Replay INT8 math on the host with gcc before silicon:

from eulernpu.quant import FixedPointConfig, quantize_tensor

cfg = FixedPointConfig(bits=8, symmetric=True)
qw, scale = quantize_tensor(fp32_weights, cfg)
# qw: int8, scale: float — graph eval uses (qw * scale) for dequantize

This is byte-equal to what ARM does on silicon. If host_smoke passes, silicon-side math is guaranteed (silicon-only traps like register-map mismatch live in a separate category).

Per-Board INT8 Impact

The same INT8 weights have different cycle/memory implications per board:

eulernpu profile --all-boards /tmp/int8.profile.json

INT4 Also Supported

quantization:
  mode: int4
  weight_bits: 4
  activation_bits: 8
  accumulator_bits: 32

Rule S21 only allows INT4 weight + INT8 activation pairing (asymmetric mixes are blocked). Useful on ZU-class boards (XCZU9EG etc).

Next

To browse different model family examples (MLP/CNN/Transformer/LSTM/ Decoder KV) → 06. models.