- Published on
PyTorch→ONNX→TensorRT INT8 양자화 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 추론 비용을 줄이려고 PyTorch → ONNX → TensorRT 파이프라인으로 옮긴 뒤, FP16까지는 잘 되는데 INT8에서만 빌드가 깨지는 경우가 많습니다. 특히 TensorRT는 INT8 모드에서만 추가 제약(캘리브레이션 데이터, shape 프로파일, 레이어별 정밀도 정책, Q/DQ 패턴 등) 이 걸리기 때문에, 같은 ONNX라도 FP16은 통과하고 INT8은 실패하는 일이 흔합니다.
이 글은 “에러 로그 한 줄 보고 감으로 고치는” 방식 대신, 오류를 유형별로 분류하고 재현 가능한 최소 설정으로 원인을 좁혀가는 방식으로 정리합니다.
문제 해결 접근은 다른 장애 분석과도 유사합니다. 예를 들어 런타임이 계속 재시작되는 상황에서 원인별로 로그와 Probe를 나눠 보는 것처럼, INT8 빌드 실패도 원인별로 분해해야 빨리 끝납니다. 참고로 운영 장애 분석 관점은 K8s CrashLoopBackOff 원인별 로그·Probe 해결 가이드도 같이 보면 도움이 됩니다.
전체 파이프라인에서 INT8만 깨지는 이유
INT8 빌드는 대개 아래 요소가 동시에 맞아야 합니다.
- TensorRT가 해당 ONNX 연산자를 INT8로 지원하거나, 최소한 FP16/FP32로 폴백이 가능해야 함
- 정확한 shape 정보: 동적 shape면
Optimization Profile이 필수 - 캘리브레이션(PTQ) 또는 Q/DQ(Quantize-Dequantize) 기반 QAT 그래프가 필요
- 입력/출력 dtype, 레이아웃, 스케일이 일관되어야 함
- 플러그인 레이어가 있다면 INT8 I/O를 지원하거나 명시적 폴백을 구성해야 함
여기서 하나라도 어긋나면 흔히 아래 같은 에러로 나타납니다.
Calibration failure류Could not find any implementation for node류Dynamic input is missing optimization profile류Network has dynamic or shape inputs, but no optimization profile has been defined류INT8 not supported for layer ...류
1) 가장 먼저 확인할 것: 버전 매트릭스
TensorRT, CUDA, cuDNN, ONNX opset, onnxruntime, torch 버전이 어긋나면 INT8에서만 미묘하게 실패합니다.
체크리스트
- TensorRT 버전과 CUDA 버전이 공식 호환 범위인지
- ONNX opset이 너무 높지 않은지(특히 최신 opset에서 TRT 파서가 미지원인 경우)
onnx패키지 버전이 너무 새/옛 버전이라 그래프가 다르게 직렬화되는지
버전 확인 커맨드 예시:
python -c "import torch, onnx; print('torch', torch.__version__); print('onnx', onnx.__version__)"
trtexec --version
nvidia-smi
권장: 재현 가능한 컨테이너로 고정하세요. Docker 빌드 캐시가 섞이면 같은 커밋인데도 환경이 달라져서 “어제는 됐는데 오늘은 안 됨”이 됩니다. 이런 류의 원인은 Docker 빌드 캐시가 무효화되는 원인 7가지처럼 환경 변동성부터 줄이는 게 빠릅니다.
2) ONNX 내보내기에서 흔한 INT8 유발 문제
문제 A: 동적 shape를 내보냈는데 TRT 프로파일을 안 줌
PyTorch export에서 dynamic_axes를 쓰면 ONNX 입력이 동적으로 됩니다. 이때 TensorRT INT8 빌드는 거의 항상 최소/최적/최대 shape가 필요합니다.
PyTorch export 예시:
import torch
model.eval()
dummy = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy,
"model.onnx",
opset_version=17,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch"},
"output": {0: "batch"},
},
)
이렇게 내보냈다면 trtexec에서 최소/최적/최대 배치를 지정해야 합니다.
trtexec \
--onnx=model.onnx \
--int8 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:32x3x224x224 \
--saveEngine=model_int8.engine \
--verbose
FP16은 운 좋게 통과했는데 INT8에서만 터진다면, --verbose 로그에서 프로파일 관련 경고가 먼저 보이는지 확인하세요.
문제 B: ONNX 그래프에 shape 연산이 많음
Reshape, Gather, Shape, NonZero 같은 shape-graph가 복잡하면 TRT 파서가 특정 패턴을 못 읽어 INT8에서 실패할 수 있습니다.
해결 방향:
- 가능하면 export 전에 모델을
torch.jit.trace로 단순화하거나 - ONNX simplifier로 그래프를 단순화
python -m pip install onnxsim
python -m onnxsim model.onnx model_simplified.onnx
단, simplifier가 동적 shape를 잘못 고정할 수 있으니, 단순화 전후로 입력 shape가 유지되는지 검증이 필요합니다.
3) PTQ 캘리브레이션 오류: 데이터/입력 파이프라인 문제
TensorRT INT8에서 가장 흔한 실패는 캘리브레이터가 데이터를 제대로 못 넣는 경우입니다.
대표 증상
Calibrator returned nullptr류Calibration failure류- 엔진 빌드는 되는데 정확도가 급락(스케일이 엉뚱하게 잡힘)
원인 1: 캘리브레이션 배치가 입력 dtype/레이아웃과 다름
예: 모델 입력이 NCHW float32인데, 캘리브레이션에서 NHWC를 넣거나 uint8를 그대로 넣는 경우.
권장: 캘리브레이션 입력은 실제 서비스 전처리와 동일해야 합니다.
Python 캘리브레이터(Entropy) 예시
아래는 TensorRT Python API 기반의 최소 캘리브레이터 골격입니다. 환경에 따라 import 경로가 다를 수 있으니, 핵심은 “get_batch에서 GPU 메모리 포인터를 반환한다”는 점입니다.
import numpy as np
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
class EntropyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, data_loader, input_name, cache_file="calib.cache"):
super().__init__()
self.data_loader = iter(data_loader)
self.input_name = input_name
self.cache_file = cache_file
self.device_input = None
def get_batch_size(self):
return 1
def get_batch(self, names):
try:
batch = next(self.data_loader)
except StopIteration:
return None
# batch: (N, C, H, W) float32 권장
batch = np.ascontiguousarray(batch.astype(np.float32))
if self.device_input is None:
self.device_input = cuda.mem_alloc(batch.nbytes)
cuda.memcpy_htod(self.device_input, batch)
return [int(self.device_input)]
def read_calibration_cache(self):
try:
with open(self.cache_file, "rb") as f:
return f.read()
except FileNotFoundError:
return None
def write_calibration_cache(self, cache):
with open(self.cache_file, "wb") as f:
f.write(cache)
원인 2: 캘리브레이션 데이터가 너무 적거나 분포가 다름
- 이미지 모델인데 캘리브레이션을 5장만 넣음
- 실제 서비스는 야간/IR/특정 카메라인데 캘리브레이션은 다른 도메인
해결:
- 최소 수백~수천 샘플 권장(모델/도메인에 따라 다름)
- 실제 트래픽 샘플 기반으로 구성
원인 3: 동적 shape인데 캘리브레이션 배치 shape가 프로파일 밖
프로파일의 min/opt/max 범위 밖 shape를 캘리브레이션이 넣으면 실패하거나 스케일이 비정상적으로 잡힙니다.
- 캘리브레이션은 opt shape 근처로 통일하는 것이 안전합니다.
4) “INT8 not supported for layer” 해결: 폴백 전략
어떤 레이어는 INT8 구현이 없거나, 플러그인이 INT8을 지원하지 않아 실패합니다.
해결 전략
- 네트워크 전체를 INT8로 강제하지 말고, 문제 레이어만 FP16/FP32로 폴백
- 가능하면 해당 연산을 다른 연산 조합으로 치환(모델 구조 변경)
- 최신 TensorRT에서 지원이 추가됐는지 확인
TensorRT Builder 설정에서 레이어별 정밀도 제어가 가능하지만, Python/C++ API로 네트워크를 직접 다루는 쪽이 필요합니다.
실전 팁:
- 우선
--int8 --fp16을 같이 켜서, INT8 불가 레이어가 FP16으로 내려가게 하세요.
trtexec \
--onnx=model.onnx \
--int8 --fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:32x3x224x224 \
--saveEngine=model_int8_mix.engine \
--verbose
이렇게 하면 “완전 INT8”은 아니어도, 대부분의 성능 이득을 얻으면서 빌드 성공률이 크게 올라갑니다.
5) QAT 또는 Q/DQ ONNX에서 생기는 파서 오류
PyTorch QAT를 거쳐 ONNX로 내보내면 그래프에 QuantizeLinear/DequantizeLinear 노드가 들어갑니다. TensorRT는 Q/DQ 패턴을 인식해 INT8로 최적화할 수 있지만, 아래가 흔한 실패 원인입니다.
- Q/DQ가 예상 패턴과 다르게 삽입됨(프레임워크/툴체인 차이)
- per-channel scale 축(axis)이 TRT 기대와 다름
- 일부 노드가 Q/DQ로 감싸져 있는데 중간에 shape 연산이나 cast가 끼어 패턴이 깨짐
해결 방향
- 가능하면 TensorRT가 공식 지원하는 Q/DQ 패턴으로 export
- 불필요한
Cast/Transpose를 줄여 패턴을 단순화 - Q/DQ 그래프를 쓸 거면 PTQ 캘리브레이션 대신 “명시적 quantization” 흐름으로 통일
검증 팁:
- Netron으로 ONNX를 열어 Q/DQ가 레이어 주변에 어떻게 붙는지 확인
trtexec --verbose에서 Q/DQ fusion 로그가 나오는지 확인
6) ONNX 파서 단계에서 실패할 때: 최소 재현으로 쪼개기
에러 로그에 특정 노드 이름이 찍히면, 그 노드를 중심으로 그래프를 잘라 최소 재현을 만들면 해결이 빨라집니다.
ONNX에서 특정 노드 주변만 추출하는 접근
onnx-graphsurgeon을 사용해 서브그래프를 뽑아 테스트- 또는 문제가 되는 연산을 PyTorch에서 다른 구현으로 바꿔 export
설치:
python -m pip install onnx-graphsurgeon
개념 코드(노드 이름 기준으로 탐색 후 정리하는 형태):
import onnx
import onnx_graphsurgeon as gs
model = onnx.load("model.onnx")
graph = gs.import_onnx(model)
# 예: 특정 노드 이름에 "NonMaxSuppression"이 들어가면 제거/대체를 고려
for node in graph.nodes:
if "NonMaxSuppression" in node.op:
print("found:", node.name, node.op)
graph.cleanup().toposort()
onnx.save(gs.export_onnx(graph), "model_gs.onnx")
실제로는 “제거”보다는 해당 구간을 플러그인으로 바꾸거나, TRT가 잘 먹는 형태로 변환하는 게 일반적입니다.
7) 자주 만나는 구체적 오류 패턴과 처방
패턴 1: Could not find any implementation for node
의미:
- TRT가 해당 노드(연산)를 지원하지 않거나
- 지원은 하는데 현재 dtype/shape/정밀도 조합에서 커널을 못 찾음
처방:
- 먼저
--int8 --fp16로 폴백 허용 - opset 낮춰 export(예: 17이 문제면 13~16 시도)
- 해당 연산을 모델에서 치환(예: 특정 activation, resize 모드, pad 모드)
패턴 2: Network has dynamic or shape inputs... no optimization profile
처방:
trtexec에--minShapes/--optShapes/--maxShapes지정- 다중 입력이면 입력별로 모두 지정
패턴 3: 엔진은 빌드되는데 정확도 폭락
원인 후보:
- 전처리 불일치(정규화, 색상 채널, resize 방식)
- 캘리브레이션 데이터 분포 불일치
- 레이어 일부가 INT8로 내려가면 안 되는 구조(특히 출력 근처)
처방:
- 캘리브레이션 입력을 서비스 전처리와 100퍼센트 동일하게
- 출력 근처 몇 개 레이어를 FP16으로 고정(가능하면)
- INT8 mixed precision으로 타협
정확도 디버깅은 “원인별로 가설을 세우고 실험으로 배제”해야 합니다. 이런 방식은 예를 들어 타입 시스템 오류를 원인별로 분기해 해결하는 글(TS 5.5 isolatedDeclarations 오류 해결 가이드)과도 결이 같습니다.
8) 권장 실행 순서(실전 체크리스트)
trtexec --onnx=... --fp16로 먼저 엔진 빌드(ONNX 파서/shape 문제 제거)- 동적 shape면 프로파일을 먼저 확정(
min/opt/max) --int8 --fp16로 mixed INT8 빌드 시도- PTQ면 캘리브레이션 데이터/전처리/shape 범위를 고정
- 실패 시
--verbose로그에서 첫 번째 실패 노드를 찾고, 해당 연산을 중심으로:- opset 변경
- ONNX 단순화
- 모델 구조 치환
- 플러그인/폴백 전략 적용
9) 재현 가능한 빌드 스크립트 예시
마지막으로, 팀에서 공유하기 좋은 형태의 trtexec 스크립트 예시입니다.
#!/usr/bin/env bash
set -euo pipefail
ONNX_PATH=${1:-model.onnx}
ENGINE_PATH=${2:-model_int8.engine}
trtexec \
--onnx="${ONNX_PATH}" \
--saveEngine="${ENGINE_PATH}" \
--int8 --fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:32x3x224x224 \
--workspace=4096 \
--timingCacheFile=timing.cache \
--verbose
--workspace는 버전에 따라 의미가 달라질 수 있지만, 빌드 중 메모리 부족을 줄이는 데 도움이 됩니다.timing cache는 반복 빌드 시간을 줄여 디버깅 효율이 올라갑니다.
마무리
PyTorch → ONNX → TensorRT INT8에서 오류가 나는 지점은 대체로 “연산자 지원”과 “동적 shape 프로파일”과 “캘리브레이션 입력 일관성” 세 축으로 정리됩니다. FP16이 되는데 INT8만 안 된다면, 모델 자체가 문제라기보다 INT8 빌드에 필요한 추가 조건이 하나 빠졌을 확률이 높습니다.
원하는 형태가 PTQ인지(Q/DQ 없는 캘리브레이션) QAT인지(Q/DQ 포함) 먼저 결정하고, 그 흐름에 맞게 ONNX와 TensorRT 설정을 일관되게 맞추면 해결 속도가 확 올라갑니다.