# TFLite INT8 양자화: 우리가 사용한 PTQ와 QDQ의 차이
PyTorch 모델을 Edge TPU용 INT8 TFLite로 변환하면서, 양자화 방식에 대해 정리한다.
우리가 사용한 방식은 **TFLite Native PTQ**이며, 흔히 말하는 **QDQ 방식과는 다르다**.
---
## 우리가 한 것
```
FP32 ONNX (양자화 노드 없음)
│
▼ onnx2tf
FP32 SavedModel
│
▼ TFLiteConverter + calibration 데이터
Full INT8 TFLite
```
핵심은, **ONNX 그래프에는 양자화 관련 노드가 전혀 없다**는 것이다.
ONNX는 순수 FP32 그래프로서 모델 구조를 전달하는 역할만 하고,
양자화는 마지막 단계에서 `TFLiteConverter`가 전부 처리한다.
```python
converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
```
`representative_dataset`으로 calibration 데이터(학습 데이터 200개)를 넘기면,
TFLiteConverter가 이 데이터를 FP32 그래프에 통과시키면서 각 레이어의
activation 범위(min/max)를 수집한다. 이 통계를 기반으로 레이어별
scale과 zero\_point를 계산하고, 최종적으로 INT8 TFLite를 생성한다.
결과 TFLite 파일 안에는 Q/DQ 노드 같은 것이 없다.
각 텐서의 양자화 파라미터는 **텐서 메타데이터**에 저장되고,
연산 자체가 INT8로 실행된다.
---
## QDQ 방식은 어떻게 다른가
QDQ(Quantize-DeQuantize)는 **그래프 안에 양자화 노드를 명시적으로 삽입**하는 방식이다.
`onnxruntime.quantization.quantize_static()`이 대표적인 도구다.
```
[FP32 Weight] → QuantizeLinear → DequantizeLinear → Conv2D → Q → DQ → ReLU → Q → DQ → ...
```
calibration 과정은 비슷하다. 데이터를 흘려서 activation 범위를 수집하고
scale/zero\_point를 계산한다. 다만 그 결과를 **그래프 노드로 삽입**한다는 점이 다르다.
QDQ 그래프는 여전히 FP32로 실행된다. Q→DQ 쌍이 실제 양자화 효과를 시뮬레이션할 뿐이다.
이 그래프를 TFLite나 TensorRT 같은 런타임에 넘기면, 런타임이 Q/DQ 노드를 읽어서
해당 레이어를 INT8로 실행할지 결정한다.
---
## 비교
| | QDQ | TFLite Native PTQ (우리 방식) |
|---|---|---|
| 양자화 정보 위치 | ONNX 그래프 안 (Q/DQ 노드) | TFLite 텐서 메타데이터 |
| 양자화 시점 | ONNX 단계에서 | TFLiteConverter 단계에서 |
| calibration 실행 | onnxruntime | TensorFlow |
| ONNX 그래프 상태 | Q/DQ 노드 포함 | 순수 FP32 |
| 실행 방식 | FP32 + 양자화 시뮬레이션 | 진짜 INT8 연산 |
| 양자화 결정 주체 | 변환 도구 (onnxruntime) | 런타임 (TFLiteConverter) |
---
## 왜 PTQ를 선택했는가
1. **타겟이 TFLite이다.** TFLiteConverter가 직접 양자화하면 Edge TPU가 지원하는
op set에 정확히 맞는 INT8 모델이 나온다. QDQ를 거치면 중간에 한 단계가 더
생기는데, 그 단계에서 새로운 호환성 문제가 발생할 수 있다.
2. **ONNX는 전달 역할만 하면 된다.** 우리 파이프라인에서 ONNX는
PyTorch → TensorFlow 세계로 모델 구조를 넘기는 다리일 뿐이다.
여기에 양자화 정보까지 담을 필요가 없다.
3. **디버깅이 단순하다.** 문제가 생겼을 때 "ONNX 그래프가 맞는가"와
"양자화가 맞는가"를 분리해서 확인할 수 있다. QDQ 방식은 둘이 섞여 있어
어느 쪽 문제인지 분리하기 어렵다.
---
## QDQ를 써야 하는 경우는
- **NVIDIA TensorRT**가 타겟일 때. TensorRT는 QDQ ONNX를 네이티브로 지원하고,
Q/DQ 노드를 보고 어떤 레이어를 INT8로 실행할지 판단한다.
- **레이어별로 양자화를 세밀하게 제어**하고 싶을 때. 특정 레이어는 FP16으로,
나머지는 INT8로 하는 mixed-precision이 필요하면 QDQ가 적합하다.
- **QAT(Quantization-Aware Training)**를 했을 때. QAT는 학습 중에
Q/DQ 노드를 삽입해서 양자화 효과를 시뮬레이션하는데, 그 결과 자체가
QDQ 그래프다.
우리처럼 **PyTorch → TFLite (Edge TPU)** 경로에서는,
ONNX를 FP32로 깨끗하게 유지하고 TFLiteConverter에게 양자화를 맡기는 것이
가장 단순하고 안정적인 선택이었다.