- Published on
PyTorch→ONNX→TensorRT INT8 양자화 오류 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
PyTorch에서 학습한 모델을 배포 단계에서 PyTorch → ONNX → TensorRT로 옮기고, 최종적으로 INT8 양자화를 적용해 지연시간과 비용을 줄이는 흐름은 이제 흔한 표준이 됐습니다. 하지만 실제 현장에서는 INT8 빌드가 한 번에 성공하는 경우가 드물고, 다음과 같은 문제로 막히기 쉽습니다.
- ONNX 내보내기(export) 단계에서 그래프가 깨지거나 dynamic shape가 의도와 다르게 고정됨
- TensorRT 파서가 특정 ONNX 연산을 지원하지 않거나, 축(axis) 해석이 달라 shape mismatch 발생
- INT8 캘리브레이션(calibration) 데이터가 잘못 들어가서 정확도 폭락 혹은 빌드 실패
- Q/DQ(Quantize/Dequantize) 노드가 섞인 모델에서 TensorRT가 기대하는 양자화 흐름과 다르게 구성됨
- 엔진 빌드 시점에
kINT8를 켰지만, 실제로는 일부 레이어만 INT8로 내려가고 나머지는 FP16/FP32로 남음
이 글은 “오류 메시지별 체크리스트”가 아니라, 파이프라인을 안정화시키는 방식으로 원인을 분해하고 해결책을 제시합니다. 운영 환경에서 로그가 폭주하거나 디스크가 가득 차는 문제까지 함께 겪는 경우가 많아, 필요하면 리눅스 디스크 100%? journalctl 로그폭주 10분 차단 글도 같이 참고하면 좋습니다.
전체 파이프라인에서 자주 생기는 INT8 실패 지점
INT8 양자화 실패는 대체로 아래 3구간 중 하나에서 발생합니다.
1) PyTorch export가 “INT8 친화적”이지 않음
- 불필요한 control flow, Python 분기, 데이터 의존 shape가 ONNX로 변환되며 깨짐
- opset 버전이 너무 낮거나 너무 높아 TensorRT가 파싱 못 함
2) ONNX 그래프가 TensorRT가 기대하는 형태가 아님
Resize,GridSample,LayerNorm, 일부Gather패턴 등은 버전에 따라 파서 호환성이 갈림- dynamic shape가 있는 입력에서
trtexec프로파일을 안 주면 빌드 자체가 실패
3) TensorRT INT8 캘리브레이션/빌드 설정 문제
- 캘리브레이션 입력 dtype/shape/전처리가 학습과 다름
- 캘리브레이터가 배치 스트림을 잘못 공급하거나, device 메모리 복사가 누락됨
IInt8EntropyCalibrator2캐시를 잘못 재사용해 분포가 어긋남
이제부터는 실전에서 가장 많이 보는 오류 유형을 중심으로 해결 절차를 정리합니다.
ONNX export 단계: 안정적인 내보내기 템플릿
TensorRT로 갈 모델은 ONNX export부터 “재현 가능하게” 만들어야 합니다. 아래는 가장 흔한 실수(특히 dynamic shape 처리)를 피하는 템플릿입니다.
import torch
def export_onnx(model, onnx_path, opset=17):
model.eval().cuda()
# 예시 입력: NCHW 이미지
dummy = torch.randn(1, 3, 224, 224, device="cuda")
torch.onnx.export(
model,
dummy,
onnx_path,
export_params=True,
opset_version=opset,
do_constant_folding=True,
input_names=["input"],
output_names=["output"],
dynamic_axes={
"input": {0: "batch", 2: "h", 3: "w"},
"output": {0: "batch"},
},
)
# 사용 예
# export_onnx(model, "model.onnx")
export 관련 흔한 오류와 해결
오류 1: TensorRT 파서가 특정 op를 지원하지 않음
- 증상:
Unsupported ONNX data type또는No importer registered for op류 - 해결 방향
- opset을 너무 최신으로 올리지 말고, TensorRT 릴리스 노트가 권장하는 범위로 맞추기
- 해당 op를 PyTorch에서 다른 연산 조합으로 치환하거나, ONNX 그래프에서 패턴 변환
- 최후의 수단: TensorRT 플러그인(plugin) 구현
오류 2: dynamic shape인데 프로파일을 주지 않아 빌드 실패
- 증상:
Network has dynamic or shape inputs, but no optimization profile류 - 해결:
trtexec또는 API에서min/opt/maxshape를 반드시 지정
ONNX 검증: TensorRT 전에 반드시 해야 하는 2단계
TensorRT로 바로 던지지 말고 ONNX 자체의 정합성을 먼저 확인하면 디버깅 시간이 크게 줄어듭니다.
1) onnx.checker + shape inference
import onnx
from onnx import shape_inference
m = onnx.load("model.onnx")
onnx.checker.check_model(m)
m2 = shape_inference.infer_shapes(m)
onnx.save(m2, "model.inferred.onnx")
2) onnxruntime로 PyTorch 출력과 일치 확인
INT8 이전에 FP32 기준 출력이 맞아야 합니다.
import numpy as np
import onnxruntime as ort
import torch
# PyTorch
x = torch.randn(1, 3, 224, 224).cuda()
with torch.no_grad():
y_pt = model.eval().cuda()(x).float().cpu().numpy()
# ONNXRuntime
sess = ort.InferenceSession("model.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
y_ox = sess.run(None, {"input": x.float().cpu().numpy()})[0]
print("max abs diff:", np.max(np.abs(y_pt - y_ox)))
- 여기서 차이가 크면 TensorRT INT8은 거의 확정적으로 더 망가집니다.
- 차이가 큰 경우는 보통 전처리,
eval()누락,dropout/bn상태, 혹은 export 시 상수접기(constant folding) 영향입니다.
TensorRT INT8 빌드: trtexec로 먼저 성공시키기
Python API로 바로 들어가면 원인 분리가 어렵습니다. 먼저 trtexec로 “최소 성공 케이스”를 만든 뒤, 그 설정을 코드로 옮기는 편이 빠릅니다.
기본 빌드 커맨드(동적 입력 포함)
아래 예시에서 부등호는 MDX 빌드 에러를 피하기 위해 모두 인라인 코드로 표기합니다.
trtexec \
--onnx=model.onnx \
--saveEngine=model_int8.engine \
--int8 \
--fp16 \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:16x3x512x512 \
--workspace=4096 \
--verbose
자주 보는 빌드 실패 원인
1) INT8 not supported for this layer가 연쇄적으로 발생
- 의미: 해당 레이어는 INT8 커널이 없거나, 입력/출력 텐서가 INT8로 내려갈 수 없는 제약이 있음
- 해결
- 우선
--fp16을 같이 켜서 혼합 정밀도(mixed precision)로 엔진을 만들고, 어떤 레이어가 FP16/FP32로 남는지 로그로 확인 - 레이어 단위로 정밀도 강제는 가능하지만, 근본적으로는 그래프 구조를 INT8 친화적으로 바꾸는 게 효과적
- 우선
2) 캘리브레이션 없이 INT8을 켠 경우
- 증상: 빌드 자체가 실패하거나, 빌드는 되는데 정확도가 크게 깨짐
- 해결
- PTQ(Post Training Quantization)라면 반드시 캘리브레이션 데이터가 필요
- QAT(Quantization Aware Training)로 Q/DQ가 들어간 ONNX라면 캘리브레이션 없이도 가능하지만, 그래프가 TensorRT가 기대하는 Q/DQ 패턴이어야 함
Python API: INT8 캘리브레이터 구현 시 흔한 함정
TensorRT Python에서 IInt8EntropyCalibrator2를 구현할 때 가장 흔한 문제는 “캘리브레이션 입력이 학습 전처리와 다르다” 입니다. 그 다음이 “스트림이 shape/dtype을 잘못 준다” 입니다.
아래는 최소 구현 예시입니다.
import tensorrt as trt
import numpy as np
import pycuda.driver as cuda
import pycuda.autoinit
class ImageBatchStream:
def __init__(self, npy_paths, batch_size, shape):
self.paths = npy_paths
self.batch_size = batch_size
self.shape = shape # (N, C, H, W)
self.idx = 0
def reset(self):
self.idx = 0
def next_batch(self):
if self.idx >= len(self.paths):
return None
batch_paths = self.paths[self.idx:self.idx + self.batch_size]
self.idx += self.batch_size
batch = []
for p in batch_paths:
x = np.load(p).astype(np.float32) # 전처리 결과를 저장해둔 npy라고 가정
batch.append(x)
batch = np.stack(batch, axis=0)
return batch
class EntropyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, stream, cache_file):
super().__init__()
self.stream = stream
self.cache_file = cache_file
self.d_input = cuda.mem_alloc(stream.batch_size * np.prod(stream.shape[1:]) * np.float32().nbytes)
def get_batch_size(self):
return self.stream.batch_size
def get_batch(self, names):
batch = self.stream.next_batch()
if batch is None:
return None
cuda.memcpy_htod(self.d_input, batch)
return [int(self.d_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)
캘리브레이션에서 특히 중요한 체크리스트
- 캘리브레이션 입력은 “모델이 실제로 받는 텐서”와 동일해야 함
- dtype: 보통
float32 - 레이아웃:
NCHWvsNHWC - 정규화: mean/std, scale, color order(BGR/RGB)
- dtype: 보통
- 캘리브레이션 샘플 수는 너무 적으면 분포를 대표하지 못해 정확도 하락
- 캐시 파일은 모델/전처리/입력 해상도가 바뀌면 폐기하고 재생성
Q/DQ 기반(ONNX Quantized) 모델에서의 오류 패턴
QAT를 했거나, ONNX 그래프에 QuantizeLinear/DequantizeLinear가 이미 들어간 경우가 있습니다. 이때는 “TensorRT가 기대하는 Q/DQ 배치”가 맞지 않으면 파서 단계에서 막히거나, 일부 텐서가 INT8로 내려가지 않습니다.
자주 터지는 유형
- per-channel quantization 축(axis)이 TensorRT가 처리 가능한 형태가 아님
Conv가중치의 scale/zero-point shape가 맞지 않음- Q/DQ가 불필요하게 촘촘히 들어가 그래프가 비대해지고 최적화가 깨짐
해결 방향
- ONNX simplifier나 graph surgeon으로 Q/DQ 패턴을 정리
- TensorRT 버전 업그레이드로 Q/DQ 지원이 개선되는 경우가 많음
- 가능하면 “TensorRT가 잘 먹는 구조”로 QAT를 설계(예: Conv-BN-ReLU 패턴 정리)
정확도 폭락: INT8이 성공했는데 결과가 이상할 때
빌드는 성공했는데 정확도가 크게 떨어지는 경우, 원인은 대부분 아래 중 하나입니다.
1) 캘리브레이션 데이터 분포가 실제 트래픽과 다름
- 해결: 실제 서비스 입력에서 샘플링하거나, 최소한 조명/배경/스케일이 다양한 데이터로 구성
2) 전처리 불일치
- 학습:
RGB에mean/std적용 - 캘리브레이션:
BGR그대로, 혹은uint8를 그대로 넣음 - 해결: 전처리를 코드로 하나로 묶고, 캘리브레이션도 그 함수를 재사용
3) 레이어 일부가 FP32로 남아 스케일 전파가 꼬임
- 해결: TensorRT verbose 로그에서 레이어별 precision을 확인하고, 병목 레이어만 구조 변경 또는 플러그인 고려
실전 디버깅 순서(시간 절약용)
아래 순서를 지키면 “원인을 한 번에 하나씩” 제거할 수 있습니다.
- PyTorch
eval()+ 고정 입력으로 FP32 출력 저장 - ONNX export 후
onnx.checker통과 확인 - onnxruntime(CUDA)로 PyTorch와 출력 비교
trtexec로 FP16 엔진 먼저 빌드- 동일 ONNX로 INT8 빌드 시도(프로파일 포함)
- 캘리브레이션 입력을 학습 전처리와 100% 동일하게 맞추기
- 그래도 실패하면 문제 op를 특정해 그래프 치환 또는 TensorRT 버전/플러그인 검토
빌드/실행 로그가 너무 많이 쌓여 디스크가 차거나 노드가 불안정해지면, 먼저 로그 폭주를 제어하는 것이 디버깅 생산성을 올립니다. 이 경우 리눅스 디스크 100%? journalctl 로그폭주 10분 차단 같은 운영 팁이 의외로 큰 도움이 됩니다.
자주 묻는 질문(FAQ)
INT8을 켰는데 실제로 속도가 안 빨라집니다
- 모델이 INT8로 충분히 내려갔는지 확인해야 합니다. 많은 레이어가 FP16/FP32로 남으면 기대한 이득이 안 납니다.
- 입력 해상도/배치가 작으면 커널 런치 오버헤드가 지배적일 수 있습니다.
--int8만 주면 되나요
- PTQ라면 캘리브레이션이 필요합니다. QAT 기반 Q/DQ ONNX라면 캘리브레이션 없이도 가능할 수 있지만, 그래프 호환성이 관건입니다.
opset은 몇을 써야 하나요
- 정답은 TensorRT 버전에 따라 다릅니다. 일반적으로 너무 낮으면 표현력이 부족하고, 너무 높으면 파서가 못 따라갑니다. 팀 표준으로 TensorRT 버전과 opset을 고정하고, 모델별 예외를 최소화하는 것이 좋습니다.
결론
PyTorch → ONNX → TensorRT INT8에서 오류를 줄이는 핵심은 “INT8만” 보지 말고, (1) ONNX 정합성, (2) TensorRT 파서 호환성, (3) 캘리브레이션 입력의 일치성을 순서대로 고정하는 것입니다. 특히 캘리브레이션 데이터 전처리 불일치는 빌드 성공 여부와 무관하게 정확도를 크게 망가뜨리는 1순위 원인이라, 코드 레벨로 재사용 가능하게 묶어두는 것을 권장합니다.
원하면 사용 중인 에러 로그(예: trtexec --verbose 출력 일부)와 TensorRT/ONNX/PyTorch 버전, 입력 shape 정보를 주면, 어떤 단계에서 깨지는지 기준으로 더 구체적인 처방(그래프 치환 포인트, opset 조정, 프로파일 설정)을 함께 정리해줄 수 있습니다.