Published on

PyTorch 2.1 int8 양자화 오류별 해결과 속도

Authors

PyTorch 2.1에서 int8 양자화는 “CPU 추론 가속”이라는 명확한 목표가 있을 때 가장 효과적입니다. 하지만 실제 적용 과정에서는 백엔드 선택, 연산자 지원 범위, torch.compile/Inductor와의 조합, 동적/정적 양자화 방식 차이 때문에 오류가 자주 발생합니다. 이 글은 오류 메시지 유형별로 원인과 해결책을 빠르게 매칭하고, 마지막에 속도 측정 방법과 기대 성능/병목까지 한 번에 정리합니다.

전제: 아래 내용은 주로 x86 CPU + FBGEMM 또는 ARM CPU + QNNPACK 기준입니다. GPU에서의 int8는 PyTorch 기본 양자화 파이프라인과 결이 다르고(보통 TensorRT, custom kernel, PT2E/ExecuTorch 경로 등), 본문은 “PyTorch 내장 양자화로 CPU 추론 최적화”에 초점을 둡니다.

PyTorch 2.1 int8 양자화 큰 그림

PyTorch에서 int8 양자화는 크게 3가지 경로가 있습니다.

  1. Dynamic Quantization(동적): 주로 nn.Linear 계열에 적용. 캘리브레이션 데이터 없이 빠르게 적용 가능. 정확도 손실이 비교적 적고 적용이 쉬움.
  2. Static/PTQ(정적, Post-Training Quantization): Conv/Linear 등 더 넓게 최적화 가능. 캘리브레이션 필요. 성능 이득이 더 큰 경우가 많음.
  3. QAT(Quantization Aware Training): 학습 단계에서 양자화를 모사. 정확도 보존에 유리하지만 비용이 큼.

PyTorch 2.1에서는 torch.ao.quantization(FX Graph Mode 포함)과 더불어 PT2E 계열도 존재하지만, 실무에서 “빨리 적용해서 CPU 속도 올리기”는 여전히 FX Graph Mode 기반의 PTQ 또는 Dynamic Quantization이 가장 현실적입니다.

시작 전 체크리스트(오류 예방)

1) 백엔드 엔진 설정(FBGEMM/QNNPACK)

  • x86 서버/데스크톱: fbgemm
  • ARM(모바일/라즈베리파이 등): qnnpack
import torch

# x86라면 보통 fbgemm
torch.backends.quantized.engine = "fbgemm"
print("quantized engine:", torch.backends.quantized.engine)

엔진 설정이 맞지 않으면 “지원되지 않는 커널” 또는 성능 저하로 이어집니다.

2) 모델은 반드시 eval()

양자화는 추론 그래프 기준으로 준비/변환되므로, model.eval() 누락은 캘리브레이션 통계/옵저버 동작에 영향을 줄 수 있습니다.

model.eval()

3) CPU 스레드/affinity로 속도 측정 환경 고정

속도 비교를 할 때는 스레드 수가 흔들리면 결과가 크게 출렁입니다.

import torch

torch.set_num_threads(8)
torch.set_num_interop_threads(1)

오류 1: NoQEngine / Could not run 'quantized::...'

증상

  • RuntimeError: NoQEngine
  • Could not run 'quantized::linear' with arguments from the 'CPU' backend
  • quantized::conv2d 관련 커널 미존재

원인

  • torch.backends.quantized.engine가 비어있거나, CPU 아키텍처와 맞지 않음
  • 빌드된 PyTorch가 해당 quantized kernel을 포함하지 않는 경우(특히 커스텀 빌드)

해결

  1. 엔진을 명시적으로 설정
import torch

torch.backends.quantized.engine = "fbgemm"  # x86
  1. 커스텀 빌드라면 quantization 지원 옵션 확인
  • 실무에서는 “공식 wheel”을 쓰는 것이 가장 빠른 해결입니다.
  1. ARM에서 fbgemm을 쓰고 있지 않은지 확인
import platform
print(platform.machine())

오류 2: NotImplementedError / quantized::linear 미지원 연산 조합

증상

  • NotImplementedError: Could not run 'quantized::linear' ...
  • 특정 레이어만 터지거나, 모델 일부만 양자화된 뒤 실행 중 실패

원인

  • 양자화된 연산자는 입력 dtype/메모리 포맷/연산 조합에 제약이 있습니다.
  • FX Graph Mode에서 패턴 매칭이 실패해 float 연산과 quantized 연산이 섞이는 경우도 흔합니다.

해결 전략 A: 동적 양자화로 범위를 줄여 성공률 올리기

nn.Linear 위주 모델(Transformer MLP, classifier head 등)은 동적 양자화만으로도 의미 있는 이득이 나옵니다.

import torch
import torch.nn as nn
from torch.ao.quantization import quantize_dynamic

model.eval()
qmodel = quantize_dynamic(
    model,
    {nn.Linear},
    dtype=torch.qint8,
)

해결 전략 B: FX Graph Mode PTQ에서 prepare_fx/convert_fx 사용

정적 양자화는 준비 단계에서 옵저버를 삽입하고, 캘리브레이션 후 변환합니다.

import torch
from torch.ao.quantization import get_default_qconfig
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx

torch.backends.quantized.engine = "fbgemm"
model.eval()

qconfig = get_default_qconfig("fbgemm")
qconfig_dict = {"": qconfig}

example_inputs = (torch.randn(1, 3, 224, 224),)
prepared = prepare_fx(model, qconfig_dict, example_inputs)

# calibration
with torch.no_grad():
    for _ in range(20):
        prepared(torch.randn(1, 3, 224, 224))

qmodel = convert_fx(prepared)

해결 전략 C: 양자화 제외(스킵)로 실패 지점 우회

특정 서브모듈이 지원되지 않으면 해당 블록만 float로 남기는 것이 실용적입니다.

  • FX에서 모듈별 qconfig를 다르게 지정하거나
  • 실패하는 모듈을 float로 유지

모델 구조가 다양해 “정답 코드”는 하나로 고정하기 어렵지만, 핵심은 문제 레이어를 pinpoint하고 그 구간만 제외하는 것입니다.

오류 3: Fused 패턴 실패(Conv+BN+ReLU)로 성능/정확도 악화

증상

  • 오류는 없지만 속도가 기대보다 안 나옴
  • 정적 양자화 후 정확도 급락

원인

  • 정적 양자화는 보통 Conv + BN + ReLU 같은 패턴을 fuse할수록 유리합니다.
  • fuse가 안 되면 스케일/제로포인트가 불리하게 적용되거나, 연산자 분해로 오버헤드가 늘 수 있습니다.

해결

  • FX 흐름에서는 내부적으로 패턴을 인식하지만, 모델이 커스텀 레이어/분기 구조를 가지면 실패할 수 있습니다.
  • 가능하면 표준 모듈 조합을 유지하고, eval() 상태에서 BN이 고정되어 있는지 확인합니다.

추가로, 정적 양자화는 캘리브레이션 데이터 품질이 매우 중요합니다. 학습 데이터 분포를 대표하는 샘플을 넣지 않으면 정확도 손실이 커질 수 있습니다.

오류 4: torch.compile과 같이 쓰면 느려지거나 깨짐

증상

  • torch.compile(model) 후 양자화 모델이 더 느림
  • Inductor가 quantized op를 잘 최적화하지 못해 fallback이 발생
  • 그래프 브레이크가 잦아짐

원인

PyTorch 2.x의 torch.compile은 float 그래프 최적화에 강점이 있고, quantized 연산은 백엔드/커널 특성상 Inductor 최적화 이점이 제한적이거나 오히려 방해가 될 수 있습니다.

해결 가이드

  • **원칙적으로는 “float 모델은 compile, int8 모델은 compile 없이”**부터 비교하세요.
  • 꼭 결합해야 한다면, torch._dynamo.explain 등으로 그래프 브레이크를 확인하고, 실제로 이득이 있는지 벤치마크로 검증합니다.
import torch

compiled = torch.compile(model)
# 먼저 float에서만 성능 비교

오류 5: dtype/입출력 타입 혼선(qint8, quint8, int8)

증상

  • RuntimeError로 dtype mismatch
  • 양자화 모델 입력을 int8 텐서로 넣으려다 실패

원인

PyTorch 양자화에서 내부 dtype은 torch.qint8/torch.quint8 같은 “quantized dtype”이며, 일반적인 torch.int8 텐서와 다릅니다. 보통 사용자 입력은 float 텐서를 넣고, 양자화된 모듈 내부에서 quant/dequant가 처리됩니다.

해결

  • 입력은 float 유지(일반적으로 fp32 또는 bf16)
  • 모델 변환을 통해 내부 연산만 int8로 바꾸기
x = torch.randn(1, 3, 224, 224)  # float
with torch.no_grad():
    y = qmodel(x)

오류 6: 성능이 안 나오는 “진짜 이유”들

양자화는 적용에 성공해도 “생각보다 안 빨라요”가 매우 흔합니다. 대표 원인은 아래와 같습니다.

1) 모델이 Linear 비중이 낮다

  • 동적 양자화는 주로 Linear에만 이득
  • CNN에서 conv 비중이 높으면 정적 양자화를 고려해야 합니다.

2) 배치가 너무 작아 오버헤드가 지배

  • batch=1은 메모리/런타임 오버헤드 비중이 커서 이득이 제한될 수 있음
  • 하지만 서버 추론은 batch=1이 많으므로, 측정 자체는 현실적으로 batch=1도 반드시 포함해야 합니다.

3) 스레드/NUMA/클럭 변동

  • torch.set_num_threads 고정
  • 가능하면 동일 머신에서 고정된 조건으로 반복 측정

4) 캘리브레이션 부실로 정확도 손실 → 더 큰 모델로 회귀

정확도 손실을 만회하려고 모델을 키우면 속도 이득이 상쇄됩니다. 캘리브레이션 샘플 품질을 먼저 올리는 편이 낫습니다.

속도 측정: 벤치마크 템플릿(재현성 중심)

아래는 CPU에서 float vs int8을 비교하는 최소 템플릿입니다.

import time
import torch

@torch.no_grad()
def bench(model, x, warmup=20, iters=100):
    model.eval()
    # warmup
    for _ in range(warmup):
        _ = model(x)

    t0 = time.perf_counter()
    for _ in range(iters):
        _ = model(x)
    t1 = time.perf_counter()

    return (t1 - t0) / iters

# 환경 고정
torch.set_num_threads(8)
torch.set_num_interop_threads(1)

device = "cpu"
x = torch.randn(1, 3, 224, 224, device=device)

fp_time = bench(model, x)
int8_time = bench(qmodel, x)

print("fp32 avg sec:", fp_time)
print("int8 avg sec:", int8_time)
print("speedup x:", fp_time / int8_time)

측정 팁

  • time.time() 대신 time.perf_counter() 사용
  • 워밍업 필수(캐시/스레드풀 초기화)
  • 가능하면 taskset/numactl로 코어 고정

“어떤 경우에” 얼마나 빨라지나: 현실적인 기대치

  • 동적 양자화(Linear 중심): 대략 1.2x ~ 2.5x 사이가 자주 관측됩니다(모델 구조/CPU에 따라 편차 큼).
  • 정적 양자화(Conv 포함): 잘 맞으면 더 큰 이득이 가능하지만, 캘리브레이션/패턴 지원/연산자 커버리지 변수가 큽니다.
  • 메모리 대역폭이 병목인 모델은 int8로 줄어든 메모리 풋프린트 덕에 이득이 커질 수 있습니다.

중요한 점은 “int8이라면 무조건 빠르다”가 아니라, (1) 커널이 최적화되어 있고 (2) 해당 연산 비중이 높고 (3) 런타임 오버헤드가 낮을 때 빠르다는 것입니다.

실전 트러블슈팅 플로우(오류별 빠른 분기)

  1. 커널 미지원/엔진 문제인가?
    • torch.backends.quantized.engine 확인
    • x86이면 fbgemm, ARM이면 qnnpack
  2. 적용 난이도를 낮춰 성공 경로 확보
    • 먼저 quantize_dynamicLinear만 적용해 end-to-end 성공 확인
  3. 정적 양자화로 확장
    • prepare_fx + 캘리브레이션 + convert_fx
  4. 특정 블록이 계속 실패하면 스킵
    • 해당 모듈만 float 유지
  5. 속도가 안 나오면 “스레드/배치/연산 비중/그래프 브레이크”부터 의심

운영 관점: 에러 로그를 “패턴”으로 관리하기

양자화는 한 번 맞추고 끝이 아니라, 모델 버전/torch 버전/CPU 인스턴스 타입이 바뀔 때마다 재발합니다. 따라서 에러를 문자열로만 보지 말고, 패턴 기반으로 재시도/대체 경로를 설계하는 것이 좋습니다.

  • 예: 정적 양자화가 실패하면 동적 양자화로 폴백
  • 예: 특정 CPU에서만 실패하면 엔진/스레드 설정을 분기

이런 “오류를 분류하고 자동으로 우회하는” 접근은, API 호출에서의 재시도·큐잉 패턴을 설계하는 것과 유사한 면이 있습니다. 운영에서 패턴화가 중요한 이유는 아래 글의 관점도 참고할 만합니다.

마무리

PyTorch 2.1의 int8 양자화는 “한 줄로 끝나는 마법”이라기보다, 백엔드/지원 연산자/모델 구조/캘리브레이션/벤치마크 조건이 맞물리는 엔지니어링 작업입니다.

  • 빨리 성공시키려면 quantize_dynamic로 최소 적용부터 시작하고
  • 더 큰 이득이 필요하면 FX 기반 정적 양자화로 확장하며
  • 실패하는 블록은 과감히 스킵하고
  • 성능은 반드시 고정된 조건에서 수치로 검증하세요.

원하시면 다음 정보를 알려주면, 해당 모델/환경에 맞춰 “어느 경로가 가장 빠르게 성공하는지”와 “속도 기대치”를 더 구체적으로 잡아드릴 수 있습니다.

  • CPU 종류(예: Ice Lake, Milan, Graviton)
  • 대상 모델(ResNet, BERT, LLaMA MLP 등)
  • 배치 크기/입력 shape
  • 목표(지연시간 최소화 vs 처리량 최대화)