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:
- Symmetric per-tensor INT8 by default — weights use
scale = max|w| / 127 - Post-training (PTQ) by default
- A fake-fixed-point sim verifies accuracy on the host before silicon
- Silicon measurement confirms QuantReport error + cycle impact
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
- XC7Z020 — once INT8 weight tiles fit in BRAM, m_axi cost vanishes
- XCZU7EV — even INT4 fits in BRAM (DSP becomes the bottleneck first)
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.