- Published on
PyTorch 모델 INT8 양자화 실패 원인 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
PyTorch에서 INT8 양자화는 모델 크기와 지연 시간을 줄이는 강력한 방법이지만, 실제 적용 과정에서는 “적용은 됐는데 정확도가 붕괴”하거나 “변환 자체가 실패”하는 일이 흔합니다. 특히 torch.ao.quantization(Eager), FX Graph Mode, 그리고 torch.compile/백엔드 조합까지 고려하면 실패 지점이 다양해집니다.
이 글은 실무에서 자주 마주치는 INT8 양자화 실패 원인 7가지를 증상, 왜 발생하는지, 어떻게 진단하고 고치는지 관점에서 정리합니다. 예시는 PyTorch 2.x 기준이며, CPU INT8(주로 FBGEMM) 시나리오를 중심으로 설명합니다.
0. 먼저 확인할 것: “실패”의 정의를 분리하기
INT8 양자화 실패는 보통 아래 3가지 중 하나입니다.
- 변환 실패:
prepare/convert단계에서 에러가 나거나 변환된 모듈이 비정상 - 성능 실패: INT8로 바뀌었는데도 느리거나 오히려 느림
- 정확도 실패: 변환은 되지만 정확도가 크게 하락
원인을 빠르게 좁히려면, 변환 후에 실제로 INT8 연산이 들어갔는지부터 확인해야 합니다.
import torch
import torch.nn as nn
def print_quantized_modules(m: nn.Module):
for name, mod in m.named_modules():
# Quantized 모듈은 보통 torch.ao.nn.quantized 또는 quantized.dynamic 아래에 존재
if "quantized" in mod.__class__.__module__:
print(name, mod.__class__)
또한 CPU 백엔드 설정이 맞는지도 기본 점검입니다.
import torch
print(torch.__version__)
print("supported engines:", torch.backends.quantized.supported_engines)
# x86 서버라면 보통 fbgemm
torch.backends.quantized.engine = "fbgemm"
print("engine:", torch.backends.quantized.engine)
1) 대표 원인: 지원되지 않는 연산자 또는 모델 패턴
증상
convert시점에 특정 op에서 에러- 변환은 되지만 핵심 연산이 FP32로 남아 INT8 이점이 거의 없음
왜 발생하나
PyTorch INT8 양자화는 모든 연산자를 커버하지 않습니다. 특히 아래가 흔한 실패 포인트입니다.
- 커스텀
autograd.Function - 일부 activation 조합,
einsum, 특정 reshape 패턴 - attention 류(특히 직접 구현한 scaled dot-product)
LayerNorm중심 구조(Transformer)는 PTQ로는 난이도가 높고, 많은 부분이 FP로 남기 쉬움
진단
FX Graph Mode(가능하면)로 그래프를 뽑아 어떤 노드가 quantize/dequantize로 둘러싸였는지 확인하는 게 가장 빠릅니다.
import torch
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
from torch.ao.quantization import get_default_qconfig
model = ...
model.eval()
qconfig = get_default_qconfig("fbgemm")
qconfig_dict = {"": qconfig}
example = (torch.randn(1, 3, 224, 224),)
prepared = prepare_fx(model, qconfig_dict, example_inputs=example)
# calibration
with torch.no_grad():
prepared(*example)
quantized = convert_fx(prepared)
print(quantized.graph)
해결 팁
- 지원되는 패턴으로 모듈 퓨전을 먼저 적용(Conv+BN+ReLU 등)
- 지원이 약한 구간은 부분 양자화로 타협(핵심 Conv/Linear만)
- Transformer 계열은 PTQ 대신 QAT 또는 다른 런타임(TensorRT-LLM, ONNX Runtime QDQ 등) 검토
2) 모듈 퓨전(fuse) 누락으로 인한 성능/정확도 실패
증상
- INT8로 변환은 됐는데 속도 이득이 미미
- 정확도가 예상보다 크게 떨어짐
왜 발생하나
Eager 모드 양자화는 많은 경우 fuse_modules로 Conv-BN-ReLU 같은 패턴을 묶어야 최적의 INT8 커널로 떨어집니다. 퓨전이 안 되면 관측자 삽입 위치가 나빠지거나, QDQ 경계가 늘어나 성능이 떨어집니다.
진단
퓨전 전후 모듈 구조를 출력해 확인합니다.
import torch
import torch.nn as nn
import torch.ao.quantization as aq
class M(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Conv2d(3, 16, 3, stride=1, padding=1, bias=False)
self.bn = nn.BatchNorm2d(16)
self.relu = nn.ReLU(inplace=False)
def forward(self, x):
return self.relu(self.bn(self.conv(x)))
m = M().eval()
print("before:", m)
# Eager fuse 예시
m_fused = aq.fuse_modules(m, [["conv", "bn", "relu"]], inplace=False)
print("after:", m_fused)
해결 팁
- Eager 모드면 퓨전 리스트를 정확히 구성
- FX 모드면 패턴 매칭이 더 잘 되지만, 여전히 모델 구조에 따라 수동 개입이 필요할 수 있음
3) 캘리브레이션(PTQ) 데이터/스텝 부족 또는 분포 불일치
증상
- 변환 성공하지만 정확도 급락
- 특정 클래스/구간에서만 오차 폭발
왜 발생하나
PTQ는 관측자(observer)가 activation 분포를 보고 scale/zero-point를 결정합니다. 캘리브레이션 데이터가 적거나, 실제 트래픽과 분포가 다르면 clipping이 심해지고 INT8 오차가 커집니다.
진단
- 캘리브레이션에 사용한 샘플 수, 전처리 동일성 확인
- outlier가 많은 입력에서만 실패하는지 확인
해결 팁
- 캘리브레이션 샘플을 늘리기(수백~수천)
- 전처리(정규화, 리사이즈, 토크나이징)를 서빙과 동일하게
- activation 관측자를
HistogramObserver또는MovingAverageMinMaxObserver로 바꿔보기
from torch.ao.quantization import QConfig
from torch.ao.quantization.observer import HistogramObserver, MinMaxObserver
qconfig = QConfig(
activation=HistogramObserver.with_args(reduce_range=False),
weight=MinMaxObserver.with_args(dtype=torch.qint8)
)
4) QConfig/Observer 설정 오류로 인한 “양자화는 됐는데 품질이 망함”
증상
- 특정 레이어에서만 오차가 크게 증가
- per-tensor weight quantization으로 인해 정확도 하락
왜 발생하나
가중치는 보통 per-channel이 정확도에 유리합니다(특히 Conv/Linear). 그런데 기본 설정이나 커스텀 qconfig에서 per-tensor로 강제되면 품질이 급격히 떨어질 수 있습니다.
진단
변환된 모듈의 weight qscheme을 확인합니다.
import torch
# quantized 모델의 특정 레이어(예: quantized.conv2d)에서
w = quantized.conv.weight()
print(w.qscheme())
# 기대: torch.per_channel_affine 또는 per_channel_symmetric
해결 팁
get_default_qconfig("fbgemm")를 출발점으로 삼고, 변경은 최소화- weight per-channel 설정이 들어갔는지 점검
5) 학습/평가 모드 또는 BatchNorm/Dropout 처리 실수
증상
- 캘리브레이션 결과가 매번 달라짐
- 정확도가 심하게 흔들림
prepare이후 성능/정확도가 이상
왜 발생하나
양자화는 기본적으로 평가 모드에서 수행해야 합니다. model.train() 상태에서 Dropout이 켜져 있거나 BatchNorm이 러닝 스탯을 업데이트하면 관측자 통계가 불안정해집니다.
진단 및 해결
prepare전/후, 캘리브레이션 전 반드시eval()강제torch.no_grad()로 캘리브레이션 수행
model.eval()
prepared = prepare_fx(model, qconfig_dict, example_inputs=example)
prepared.eval()
with torch.no_grad():
for _ in range(200):
prepared(torch.randn(1, 3, 224, 224))
6) 백엔드/디바이스 미스매치: CPU INT8인데 CUDA에서 돌리려는 경우
증상
- INT8 변환 후 GPU에서 실행 시 에러
- CPU에서는 되는데 배포 환경에서만 실패
- 속도 개선이 없고 오히려 느림
왜 발생하나
PyTorch의 전통적인 INT8 양자화는 주로 CPU용입니다(FBGEMM/QNNPACK). CUDA에서 INT8을 기대하면 다른 스택이 필요할 수 있습니다(예: TensorRT, torch_tensorrt, 또는 특정 커널).
또한 ARM 계열(모바일/라즈베리파이)은 QNNPACK을 써야 하는데 서버 설정을 그대로 가져오면 문제가 납니다.
진단
- 실행 디바이스와 quantized engine 점검
- 배포 컨테이너의 CPU 아키텍처 확인
import platform, torch
print(platform.machine())
print(torch.backends.quantized.supported_engines)
print(torch.backends.quantized.engine)
해결 팁
- x86 서버:
fbgemm - ARM/mobile:
qnnpack - GPU INT8이 목적이면 PyTorch 양자화만으로 해결하려 하지 말고 런타임 선택부터 재검토
7) 동적 양자화와 정적 양자화 혼용, 혹은 기대치 불일치
증상
quantize_dynamic을 했는데 속도가 거의 안 나옴- Conv가 많은 모델에서 효과가 없음
- 정확도는 괜찮은데 지연 시간이 그대로
왜 발생하나
동적 양자화는 주로 Linear/RNN 계열에 효과가 큽니다. Conv 중심 비전 모델에 quantize_dynamic을 적용하면 대부분 FP32로 남아 효과가 제한적입니다. 반대로 정적 양자화는 캘리브레이션이 필요하고 적용 난이도가 높습니다.
진단
어떤 모듈이 실제로 quantized로 바뀌었는지 확인합니다.
import torch
import torch.nn as nn
from torch.ao.quantization import quantize_dynamic
model = ...
model.eval()
qd = quantize_dynamic(model, {nn.Linear}, dtype=torch.qint8)
print_quantized_modules(qd)
해결 팁
- NLP/추천/MLP: 동적 양자화부터 시도
- CNN/Conv 중심: 정적 양자화(PTQ) 또는 QAT 고려
- 목표가 “모델 용량”인지 “CPU 지연 시간”인지에 따라 접근을 분리
재현 가능한 최소 예제: FX 정적 양자화 파이프라인
아래는 가장 흔한 “정적 양자화 PTQ”의 뼈대입니다. 이 흐름에서 어긋나는 지점이 있으면 앞의 7가지 원인 중 하나일 확률이 높습니다.
import torch
import torch.nn as nn
from torch.ao.quantization import get_default_qconfig
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
class SmallCNN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1),
nn.ReLU(),
nn.Conv2d(16, 16, 3, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)),
)
self.classifier = nn.Linear(16, 10)
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, 1)
return self.classifier(x)
model = SmallCNN().eval()
torch.backends.quantized.engine = "fbgemm"
qconfig_dict = {"": get_default_qconfig("fbgemm")}
example_inputs = (torch.randn(32, 3, 64, 64),)
prepared = prepare_fx(model, qconfig_dict, example_inputs=example_inputs)
prepared.eval()
# calibration
with torch.no_grad():
for _ in range(50):
prepared(torch.randn(32, 3, 64, 64))
quantized = convert_fx(prepared)
# sanity check
print_quantized_modules(quantized)
with torch.no_grad():
y = quantized(torch.randn(8, 3, 64, 64))
print(y.shape)
운영 환경에서의 “양자화 실패”를 줄이는 체크리스트
정확도 실패
- 캘리브레이션 데이터가 실제 입력 분포를 대표하는가
eval()상태에서 관측자 통계를 안정적으로 모았는가- weight per-channel 설정이 유지되는가
- outlier가 많은 입력에서만 깨지는가
성능 실패
- 진짜로 quantized 커널이 쓰였는가(모듈 타입 확인)
- 퓨전이 되었는가
- CPU 백엔드가 올바른가(
fbgemm/qnnpack) - 스레드/배치/입력 크기가 INT8에 유리한가
변환 실패
- 지원되지 않는 op가 있는가(FX 그래프로 확인)
- 부분 양자화로 우회 가능한가
디버깅 관점 팁: “관측 가능성”을 먼저 만든다
양자화는 모델 내부에서 무슨 일이 벌어지는지 보기가 어렵습니다. 그래서 실패를 줄이려면, 먼저 관측 가능한 디버깅 루틴을 갖추는 게 좋습니다. 분산 환경에서 장애를 추적할 때 인터셉터나 데드라인 전파를 점검하듯이, 양자화도 “어디서 깨졌는지”를 추적하는 장치가 필요합니다.
- 분산 호출 추적처럼 경계 지점을 명확히 하는 방법은 gRPC Interceptor로 분산 트레이싱 전파 오류 잡기에서의 접근과 유사합니다.
- 타임아웃/데드라인이 전파되지 않아 원인 파악이 어려운 문제는 gRPC 데드라인 전파 실패, 원인과 진단법처럼 “전파 경로”를 먼저 고정하면 해결 속도가 빨라집니다.
양자화에서도 마찬가지로, FX 그래프 출력, 변환된 모듈 타입 나열, 입력 분포 점검 같은 기본 관측을 자동화해두면 “감”이 아니라 “증거”로 원인을 좁힐 수 있습니다.
마무리
PyTorch INT8 양자화 실패는 대개 한 가지 원인이라기보다, 지원 범위(연산자/패턴), 전처리/분포, 퓨전/백엔드, qconfig 설계가 맞물려 발생합니다. 위 7가지를 순서대로 점검하면 대부분의 실패는 재현 가능하게 만들 수 있고, 재현이 되면 해결도 가능합니다.
원하시면 사용 중인 모델 구조(예: ResNet 계열, U-Net, Transformer), 목표 환경(CPU 종류, 배치 크기, 지연 시간 목표), 현재 사용한 양자화 방식(Eager, FX, dynamic)을 알려주시면 그 조건에 맞춰 “가장 실패 확률이 낮은” 적용 경로와 qconfig 조합을 구체적으로 제안할 수 있습니다.