- Published on
PyTorch→ONNX→TensorRT INT8 양자화 실수 7가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 성능을 끌어올리기 위해 PyTorch → ONNX → TensorRT 파이프라인에서 INT8 양자화를 적용하는 경우가 많습니다. 문제는 “엔진은 만들어졌는데 정확도가 박살”, “빌드는 되는데 느려짐”, “어제 되던 게 오늘 안 됨” 같은 장애가 꽤 높은 확률로 발생한다는 점입니다.
이 글은 TensorRT INT8에서 특히 많이 겪는 실수 7가지를 원인 → 증상 → 점검/해결 형태로 정리합니다. 분류상 PTQ 중심이지만, QAT에서도 동일하게 터지는 항목이 섞여 있습니다.
전체 파이프라인 한 장 요약
- PyTorch 모델을
eval모드로 고정하고 입력 전처리까지 포함한 “추론 그래프”를 만든다 - ONNX Export 시
opset,dynamic axes,constant folding등을 안정적으로 설정한다 - TensorRT에서
INT8빌드 시 캘리브레이션 데이터셋과 전처리, 동적 shape 프로파일을 맞춘다 - 엔진 검증은 반드시
FP16/FP32대비로 수치/성능을 함께 본다
아래 7개 실수는 대부분 여기서 어긋납니다.
실수 1) PyTorch를 train 모드로 export 한다
증상
- ONNX는 잘 나오는데 TensorRT 결과가 랜덤하게 흔들림
- 특히
Dropout,BatchNorm이 있는 모델에서 정확도 급락
원인
model.train() 상태로 export 하면 Dropout이 살아있거나, BatchNorm이 러닝 통계를 제대로 쓰지 못해 추론 그래프가 불안정해집니다.
해결
- export 전
model.eval()강제 - 가능하면
torch.inference_mode()로 그래프 고정
import torch
model = build_model()
ckpt = torch.load("model.pt", map_location="cpu")
model.load_state_dict(ckpt)
model.eval()
dummy = torch.randn(1, 3, 224, 224)
with torch.inference_mode():
torch.onnx.export(
model,
dummy,
"model.onnx",
opset_version=17,
input_names=["input"],
output_names=["logits"],
dynamic_axes={"input": {0: "batch"}, "logits": {0: "batch"}},
)
실수 2) 캘리브레이션 데이터 전처리가 “학습/서빙”과 다르다
증상
- INT8에서만 정확도 급락하고 FP16은 정상
- 특정 클래스만 유독 망가짐
원인
TensorRT PTQ는 캘리브레이션 데이터의 activation 분포로 스케일을 잡습니다. 이때 전처리(리사이즈, 크롭, 정규화, 색공간, 채널 순서)가 학습/실서빙과 다르면 activation 분포가 달라져 양자화 스케일이 틀어집니다.
자주 하는 실수:
BGR로 학습했는데 캘리브레이션은RGBmean/std정규화를 빼먹음letterbox사용 모델인데 캘리브레이션은 단순 리사이즈
해결
- 캘리브레이션 입력 파이프라인을 서빙 입력과 1:1로 동일하게 만듭니다
- 데이터는 최소 수백 장, 도메인 대표성이 중요합니다
import cv2
import numpy as np
def preprocess(img_bgr: np.ndarray) -> np.ndarray:
# 예시: BGR 유지, 정규화 포함
img = cv2.resize(img_bgr, (224, 224), interpolation=cv2.INTER_LINEAR)
img = img.astype(np.float32) / 255.0
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
img = (img[..., ::-1] - mean) / std # BGR 입력을 RGB로 바꾸는 경우라면 의도적으로
img = np.transpose(img, (2, 0, 1)) # CHW
return img
실수 3) 동적 shape 모델인데 TensorRT 프로파일과 캘리브레이션 shape가 불일치
증상
- 엔진 빌드 실패 또는 빌드는 되는데 특정 배치/해상도에서만 결과 이상
- 로그에 프로파일 관련 경고가 나오거나, 런타임에 shape set 실패
원인
TensorRT에서 동적 shape는 Optimization Profile로 min/opt/max를 정의합니다. INT8 캘리브레이션은 이 프로파일의 범위 내 shape를 기준으로 진행되는데, 캘리브레이션이 opt와 크게 다르거나 범위를 벗어나면 스케일이 엇나가거나 빌드가 실패합니다.
해결
- 실제 서빙 분포에 맞춰
opt를 잡고, 캘리브레이션도 그 근처로 구성 - 여러 입력 shape를 지원해야 하면 프로파일을 여러 개 만들거나, 배치/해상도 정책을 단순화
import tensorrt as trt
logger = trt.Logger(trt.Logger.INFO)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)
with open("model.onnx", "rb") as f:
assert parser.parse(f.read())
config = builder.create_builder_config()
profile = builder.create_optimization_profile()
input_name = network.get_input(0).name
# 예시: NCHW
profile.set_shape(input_name, min=(1,3,224,224), opt=(8,3,224,224), max=(16,3,224,224))
config.add_optimization_profile(profile)
실수 4) ONNX export에서 연산이 깨지거나, opset과 플러그인 의존성을 과소평가
증상
- TensorRT 파서가 특정 노드에서 실패
Unsupported operator또는Plugin required류 에러- 빌드는 되지만 결과가 PyTorch/ONNXRuntime과 다름
원인
PyTorch에서 ONNX로 내보낼 때 일부 연산이:
- 더 낮은 opset에서 비표준 형태로 변환되거나
Constant folding과정에서 shape 추론이 꼬이거나- TensorRT가 지원하지 않는 연산으로 남습니다
특히 LayerNorm, 일부 Resize, GridSample, NonMaxSuppression 계열은 버전 조합에 따라 민감합니다.
해결
- opset을 충분히 올리고, ONNXRuntime로 1차 검증
- TensorRT 지원 연산 목록을 기준으로 모델을 단순화하거나 플러그인을 사용
- 가능하면 ONNX 그래프에
simplify를 적용하되, 결과 검증 필수
import onnx
import onnxruntime as ort
import numpy as np
onnx_model = onnx.load("model.onnx")
onnx.checker.check_model(onnx_model)
sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
x = np.random.randn(1,3,224,224).astype(np.float32)
out = sess.run(None, {sess.get_inputs()[0].name: x})
print([o.shape for o in out])
실수 5) INT8인데 FP16/TF32 설정과 레이어 정밀도 강제가 뒤섞여 성능이 역전된다
증상
- INT8 엔진이 FP16 엔진보다 느림
- GPU utilization은 낮고 latency가 들쭉날쭉
원인
INT8은 “모든 레이어가 다 INT8”이 아닙니다. TensorRT는 정확도/지원 여부에 따라 일부 레이어를 FP16 또는 FP32로 올립니다. 이때:
- 재양자화
Q/DQ비용이 커지거나 - 메모리 포맷 변환이 자주 발생하거나
- 특정 레이어가 FP32로 고정되어 병목이 됩니다
또한 Ampere 이후 환경에서 TF32, FP16, INT8이 섞일 때 예상과 다른 커널 선택이 일어나기도 합니다.
해결
- 반드시
trtexec로 레이어별 정밀도와 타이밍을 확인 - 병목 레이어가 특정 연산이면 모델 구조 변경이나 플러그인 고려
- 성능 목표가 latency인지 throughput인지에 따라 배치와 프로파일을 재설계
# 엔진 빌드 및 레이어 타이밍 확인 예시
trtexec --onnx=model.onnx --int8 --fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:16x3x224x224 \
--dumpLayerInfo --dumpProfile --separateProfileRun
실수 6) 캘리브레이터 캐시를 “그대로 재사용”해서 데이터/전처리 변경이 반영되지 않는다
증상
- 캘리브레이션 데이터를 늘렸는데 정확도가 그대로거나 오히려 나빠짐
- 코드/전처리를 바꿨는데 결과가 변하지 않음
원인
TensorRT 캘리브레이션은 캐시 파일을 남길 수 있고, 동일한 네트워크/구성으로 판단되면 캐시를 재사용합니다. 그런데 데이터셋이나 전처리를 바꿨는데 캐시를 삭제하지 않으면 예전 스케일이 계속 적용됩니다.
해결
- 캘리브레이션 캐시는 “실험 단위”로 버전 관리
- 전처리/데이터/shape/profiling이 바뀌면 캐시를 폐기
from pathlib import Path
cache = Path("calib.cache")
if cache.exists():
cache.unlink() # 실험 조건이 바뀌면 과감히 삭제
운영 환경에서도 비슷한 문제가 생깁니다. 이미지/모델 아티팩트를 가져오는 과정에서 캐시가 꼬이면 재현이 어려워지는데, 쿠버네티스에서 이미지 풀 문제가 겹치면 더 혼란스럽습니다. 배포 파이프라인에서 당장 컨테이너가 안 내려받아져 실험을 못 하는 상황이라면 Kubernetes ImagePullBackOff·ErrImagePull 해결 체크리스트도 같이 점검해두면 좋습니다.
실수 7) 검증을 “정확도만” 보거나 “한두 배치”만 본다
증상
- 오프라인에서 얼추 맞는 것 같아 배포했는데 실트래픽에서 오탐/미탐 급증
- 특정 입력 크기, 특정 카메라, 특정 조명에서만 망가짐
원인
INT8은 입력 분포 변화에 민감합니다. 검증을 소량 샘플로만 하면:
- long-tail 케이스에서의 오차 증가를 놓치고
- 동적 shape에서 특정 구간만 스케일이 나빠지는 문제를 놓칩니다
또한 성능은 p50만 보면 안 되고 p95/p99가 중요합니다. INT8에서 일부 레이어가 FP32로 승격되거나 재양자화가 늘어나면 tail latency가 튈 수 있습니다.
해결
- 최소한 다음을 같이 봅니다
FP16대비 정확도 지표 하락폭- 입력 분포별 slice 평가
- latency
p50/p95/p99, throughput
- 가능하면 ONNXRuntime, TensorRT FP16, TensorRT INT8을 같은 입력으로 비교
import time
import numpy as np
import onnxruntime as ort
def benchmark_ort(path: str, n: int = 200):
sess = ort.InferenceSession(path, providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
name = sess.get_inputs()[0].name
x = np.random.randn(8,3,224,224).astype(np.float32)
# warmup
for _ in range(20):
sess.run(None, {name: x})
ts = []
for _ in range(n):
t0 = time.perf_counter()
sess.run(None, {name: x})
ts.append((time.perf_counter() - t0) * 1000)
ts = np.array(ts)
return float(np.percentile(ts, 50)), float(np.percentile(ts, 95)), float(np.percentile(ts, 99))
p50, p95, p99 = benchmark_ort("model.onnx")
print("ORT latency ms:", p50, p95, p99)
실전 체크리스트: 실패를 빨리 좁히는 순서
- PyTorch
eval고정, 입력 전처리 동일성 확인 - ONNX를 ONNXRuntime로 먼저 검증해서 “export 문제”를 분리
- TensorRT는 FP16 엔진을 먼저 만들고 결과 일치 확인
- INT8은 캘리브레이션 전처리/shape/pro파일을 고정하고 캐시를 버전 관리
trtexec로 레이어 프로파일을 보고 INT8이 느려지는 병목을 확인- 정확도는 slice 평가로, 성능은
p95/p99까지 본다
마무리
PyTorch → ONNX → TensorRT INT8에서 문제의 대부분은 “양자화 자체”보다 전처리/shape/캐시/검증 방법 같은 운영 디테일에서 발생합니다. 위 7가지는 재현과 디버깅이 어려운 편이라, 처음부터 체크리스트로 고정해두면 시행착오를 크게 줄일 수 있습니다.
배포 환경에서 실험 반복이 잦다면, 이미지 풀 실패나 레이트 리밋 같은 외부 요인도 함께 관리해야 합니다. 예를 들어 사내 레지스트리나 퍼블릭 레지스트리에서 당겨오는 과정이 불안정하면 INT8 실험 자체가 멈춰버리므로, 필요 시 EKS ImagePullBackOff 429 Too Many Requests 해결도 같이 참고해두면 좋습니다.