- Published on
PyTorch INT8 양자화로 CPU 지연 2배 줄이기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에서 GPU 없이 PyTorch 모델을 CPU로 서빙하다 보면, 지연(latency)과 비용이 동시에 병목이 됩니다. 특히 선형 레이어(Linear)와 어텐션 계열 연산이 많은 모델은 FP32로는 캐시 미스와 메모리 대역폭 한계에 빨리 부딪힙니다. 이때 가장 현실적인 최적화 카드가 INT8 양자화(quantization) 입니다.
이 글에서는 CPU 추론 지연을 2배 수준으로 줄이는 것을 목표로, PyTorch에서 INT8 양자화를 적용하는 방법을 동적(dynamic)·정적(static)·PT2E(2.x 계열) 관점에서 정리합니다. 또한 “왜 내 모델은 빨라지지 않았나”를 디버깅하는 체크리스트까지 포함합니다.
관련해서 더 공격적인 저비트(4bit)에서 자주 터지는 이슈는 별도 글로 정리해두었습니다: PyTorch 모델 4bit 양자화 실패 7가지와 해법
INT8 양자화가 CPU에서 특히 잘 먹히는 이유
CPU에서 INT8이 유리한 핵심은 다음 3가지입니다.
메모리 트래픽 감소 FP32는 4바이트, INT8은 1바이트입니다. 가중치와 활성값을 INT8로 내리면 L1/L2 캐시에 더 많은 데이터를 담을 수 있어, 대기 시간이 줄어듭니다.
벡터화된 INT8 GEMM 커널 활용 x86에서는 AVX2/AVX512-VNNI, ARM에서는 DOTPROD 같은 명령어로 INT8 행렬곱이 최적화되어 있습니다. PyTorch는 백엔드(FBGEMM, oneDNN 등)를 통해 이를 활용합니다.
선형 레이어 비중이 큰 모델 구조 MLP, Transformer의
Linear가 지배적인 경우 INT8의 체감이 큽니다. 반대로 컨볼루션, 커스텀 연산 비중이 높으면 기대치가 낮아질 수 있습니다.
시작 전 체크: 내 모델이 INT8 타깃인지 판단하기
아래 조건을 만족할수록 성공 확률이 높습니다.
- CPU에서 서빙하며, 배치가 작거나(
batch=1근처) 지연이 중요하다. - 모델의 대부분이
Linear,MatMul,Addmm같은 연산이다. - 입력 분포가 크게 흔들리지 않는다(정적 양자화에서 특히 중요).
- 추론 코드가
torch.no_grad()및model.eval()을 준수한다.
반대로 아래는 “2배”가 어려울 수 있습니다.
- 모델이 이미 병목이 I/O, 토크나이저, 전처리/후처리에 있다.
- 연산이 커스텀 C++/Python 루프에 치우쳐 있다.
- 스레드 설정이 꼬여서 오히려 컨텍스트 스위칭이 많다.
접근 1: 동적 양자화(Dynamic Quantization)로 빠르게 성과 내기
동적 양자화는 가중치만 INT8로 내리고, 활성값은 런타임에 스케일을 잡아 양자화합니다. 캘리브레이션 데이터가 필요 없고 적용이 쉬워서, CPU 지연 개선을 빠르게 확인하기 좋습니다.
동적 양자화 예제
import time
import torch
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, in_dim=768, hidden=2048, out_dim=2):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, hidden),
nn.ReLU(),
nn.Linear(hidden, out_dim),
)
def forward(self, x):
return self.net(x)
def benchmark(model, x, iters=200, warmup=30):
model.eval()
with torch.no_grad():
for _ in range(warmup):
_ = model(x)
t0 = time.perf_counter()
for _ in range(iters):
_ = model(x)
t1 = time.perf_counter()
return (t1 - t0) * 1000 / iters
torch.set_num_threads(1) # 비교를 위해 고정
fp32 = MLP().eval()
x = torch.randn(1, 768)
int8_dynamic = torch.quantization.quantize_dynamic(
fp32,
{nn.Linear},
dtype=torch.qint8,
)
lat_fp32 = benchmark(fp32, x)
lat_int8 = benchmark(int8_dynamic, x)
print(f"FP32: {lat_fp32:.3f} ms")
print(f"INT8(dynamic): {lat_int8:.3f} ms")
동적 양자화의 장단점
장점
- 적용이 매우 쉽다.
Linear위주 모델에서 지연이 잘 줄어든다.- 캘리브레이션이 없어 운영 부담이 낮다.
단점
- 활성값은 완전 INT8이 아니어서, 정적 양자화 대비 성능 상한이 낮을 수 있다.
- 정확도는 대체로 괜찮지만, 모델/입력 분포에 따라 변동이 있다.
운영에서 “일단 1.5배 이상만 빨라져도 이득”이라면 동적 양자화부터 추천합니다.
접근 2: 정적 양자화(Static Quantization)로 더 큰 이득 노리기
정적 양자화는 가중치와 활성값 모두 INT8로 내리며, 사전에 캘리브레이션 데이터로 스케일을 결정합니다. 제대로 맞으면 동적보다 더 큰 개선이 가능하지만, 캘리브레이션 품질이 성패를 가릅니다.
PyTorch의 전통적인 FX Graph Mode 또는 eager 방식이 있지만, 최신 흐름은 torchao 또는 PT2E 쪽으로 이동 중입니다. 다만 실무에서는 여전히 환경 제약 때문에 FX 기반을 많이 씁니다.
FX 기반 정적 양자화 개요
- 모델을
eval()로 전환 qconfig지정(백엔드에 맞는 observer 설정)prepare_fx로 observer 삽입- 캘리브레이션 데이터로 몇 백~몇 천 배치 흘려보내기
convert_fx로 INT8 모델 생성
FX 기반 예제(개념 코드)
환경과 버전에 따라 API가 조금씩 달라질 수 있어, 아래는 “구조 이해용”으로 보세요.
import torch
import torch.nn as nn
import torch.ao.quantization as aq
from torch.ao.quantization.quantize_fx import prepare_fx, convert_fx
model = MLP().eval()
# 백엔드 선택: x86 서버라면 보통 fbgemm
backend = "fbgemm"
torch.backends.quantized.engine = backend
qconfig = aq.get_default_qconfig(backend)
qconfig_dict = {"": qconfig}
example_inputs = (torch.randn(1, 768),)
prepared = prepare_fx(model, qconfig_dict, example_inputs=example_inputs)
# 캘리브레이션: 실제 트래픽과 유사한 입력 분포가 중요
with torch.no_grad():
for _ in range(500):
x = torch.randn(1, 768)
_ = prepared(x)
quantized_model = convert_fx(prepared)
캘리브레이션 품질을 올리는 팁
- “랜덤 텐서”가 아니라 실제 운영 입력 샘플을 사용하세요.
- 길이/스케일이 다양한 입력을 포함하세요(예: 텍스트 길이 분포, 이미지 밝기 분포).
- 캘리브레이션 배치 수는 모델에 따라 다르지만, 최소 수백 배치부터 시작하세요.
접근 3: PyTorch 2.x PT2E(Export) 기반 양자화 흐름
PyTorch 2.x에서는 torch.export 및 PT2E(프로그램 변환 기반)로 양자화를 구성하는 흐름이 강화되고 있습니다. 장점은 그래프 캡처와 변환이 더 명확해지고, 컴파일(torch.compile)과의 결합도 좋아진다는 점입니다.
다만 실제 프로젝트에서는 버전 호환성, 연산 지원 범위, 디버깅 난이도 때문에 “동적 양자화로 빠르게 성과 확인 후, 정적 또는 PT2E로 확장”이 안전합니다.
벤치마크: 2배 지연 개선을 재현하려면 측정부터 바로잡기
INT8로 바꿨는데도 “빨라진 것 같지 않다”의 원인은 대부분 벤치마크 문제입니다.
측정 체크리스트
model.eval()+torch.no_grad()적용- 워밍업 필수(캐시, JIT 경로, 스레드 풀 준비)
- 스레드 수 고정 후 비교
- 입력 텐서 shape 고정(동적 shape은 오버헤드 증가)
- p50/p95 같이 분포로 보거나, 최소한 반복 횟수 충분히 확보
스레드/런타임 설정 예시
import os
import torch
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
torch.set_num_threads(1)
# torch.set_num_interop_threads(1) # 필요 시
서빙 환경에서는 코어 수에 맞춰 튜닝해야 합니다. 다만 “INT8이 FP32보다 빠르다”를 증명하려면 먼저 단일 스레드로 깔끔하게 비교하는 것이 좋습니다.
정확도(품질) 검증: INT8은 성능만 보면 실패합니다
INT8 적용 후에는 반드시 품질 검증이 필요합니다.
- 분류/회귀: 기존 FP32 대비 지표(Accuracy, F1, RMSE) 비교
- 랭킹/추천: NDCG, Recall@K 비교
- LLM/생성: 자동 지표 + 휴먼 평가(특히 작은 왜곡이 누적될 수 있음)
실무 팁은 “오프라인 지표가 같아도 온라인에서 미세하게 흔들리는” 경우가 있으니, 안전장치로 섀도우 트래픽 또는 카나리 릴리즈를 권장합니다.
흔한 함정 7가지: INT8인데 왜 안 빨라지나
1) 병목이 모델이 아니라 전처리/후처리
토크나이징, 이미지 디코딩, JSON 파싱이 지연의 대부분이면 INT8 효과가 작습니다. 프로파일링으로 모델 구간 비중부터 확인하세요.
2) 대상 레이어가 Linear가 아니다
동적 양자화는 보통 Linear 중심입니다. 컨볼루션 위주 모델은 정적 양자화나 다른 경로가 필요할 수 있습니다.
3) 스레드 설정이 과도해 오히려 느려짐
코어 수보다 많은 스레드는 컨텍스트 스위칭과 캐시 오염을 유발합니다. p95 지연이 악화되는 전형적인 케이스입니다.
4) 캘리브레이션 데이터가 실제 분포와 다름
정적 양자화에서 가장 치명적입니다. 실제 입력보다 분산이 작거나 크면 스케일이 틀어져 정확도도 성능도 같이 망가질 수 있습니다.
5) 연산이 그래프에서 끊겨 폴백(fallback) 발생
일부 연산이 양자화 커널로 가지 못하고 FP32로 폴백되면, 변환 비용만 추가돼 손해가 날 수 있습니다.
6) 배치가 큰 환경에서는 개선폭이 달라짐
INT8은 메모리/캐시 이득이 커서 작은 배치에서 특히 좋지만, 큰 배치에서는 다른 병목이 등장할 수 있습니다.
7) 서빙 인프라에서 리소스 제한(OOM, CPU quota)
컨테이너 CPU quota가 낮거나 노드가 과밀하면 스케줄링 지연이 섞여 “모델 최적화 효과”가 묻힙니다. 운영에서 파드가 재시작/메모리 압박을 겪는다면 먼저 안정화가 필요합니다. 이런 류의 장애 추적은 K8s CrashLoopBackOff에서 OOMKilled 원인 추적도 같이 참고할 만합니다.
운영 적용 전략: 안전하게 INT8로 갈아타는 순서
- 동적 양자화로 빠르게 PoC
- 목표: 지연 개선 가능성 확인, 품질 영향 범위 파악
- 벤치마크/프로파일링 자동화
- CI에서 성능 회귀를 잡는 체계를 만드세요. 캐시가 성능/빌드 시간을 좌우한다면 GitHub Actions 캐시 미적중? 키 설계 7원칙도 도움이 됩니다.
- 정적 양자화로 확장(필요 시)
- 목표: 더 큰 성능, 더 예측 가능한 커널 경로
- 카나리 릴리즈 + 모니터링
- p50/p95 지연, 오류율, 품질 지표를 같이 봅니다.
결론: “2배”는 가능하지만, 조건과 측정이 전부다
PyTorch INT8 양자화는 CPU 추론에서 가장 ROI가 좋은 최적화 중 하나이고, 특히 Linear 비중이 큰 모델이라면 지연 2배 수준 개선도 충분히 가능합니다. 다만 성공의 핵심은 다음 3가지입니다.
- 올바른 경로 선택: 동적부터 시작하고 필요하면 정적으로 확장
- 벤치마크 정교화: 워밍업, 스레드 고정, shape 고정
- 품질 검증: 캘리브레이션과 온라인 검증까지 포함
다음 단계로는, 실제 모델(예: Transformer encoder, 추천 MLP)에서 어떤 레이어가 양자화되는지 확인하는 방법과, 폴백을 줄이는 그래프 정리 패턴을 다루면 좋습니다. 원하시면 사용 중인 모델 구조와 입력 shape, 목표 지연(p95), CPU 종류(예: AVX512 지원 여부)를 알려주시면 그 조건에 맞춰 최적의 양자화 경로와 설정을 더 구체적으로 제안하겠습니다.