Published on

PyTorch→ONNX→TensorRT INT8 양자화 실패 7가지

Authors

서빙 환경에서 지연 시간을 줄이기 위해 PyTorch → ONNX → TensorRT 파이프라인으로 INT8 양자화를 시도하다 보면, 생각보다 자주 “빌드는 되는데 정확도가 망가짐” 혹은 “아예 엔진 생성이 실패”하는 상황을 만납니다. 문제는 대부분 양자화 자체의 품질이 아니라, 그래프 표현(ONNX)과 TensorRT가 기대하는 INT8 패턴 사이의 불일치에서 시작됩니다.

이 글에서는 TensorRT INT8 엔진 빌드/실행 단계에서 자주 터지는 실패 원인 7가지를, 전형적인 로그 패턴과 함께 빠르게 진단하고 고치는 방법으로 정리합니다. (원인별로 “어디서 막히는지”가 다르므로, 체크 순서대로 훑는 것이 효율적입니다.)

추가로, 양자화 전반 비교가 필요하면 로컬 LLM 양자화 - bitsandbytes vs GPTQ 비교도 함께 참고하면 “INT8이 항상 정답이 아닌 경우”를 판단하는 데 도움이 됩니다.

0. 실패를 분류하는 3가지 관점

INT8 실패는 크게 아래로 나뉩니다.

  1. 엔진 빌드 실패: TensorRT가 ONNX를 파싱/최적화하다가 중단
  2. 엔진 빌드는 성공, 실행 실패: 런타임에서 shape, dtype, plugin 문제
  3. 엔진은 돌아가나 정확도 붕괴: 캘리브레이션/스케일/그래프 패턴 문제

아래 7가지 원인은 이 3가지 범주를 모두 포함합니다.

1) ONNX 내보내기에서 동적 shape와 옵셋이 어긋남

전형적 증상

  • trtexec 또는 TensorRT 파서에서 shape 관련 에러
  • 동적 배치 혹은 가변 시퀀스 모델에서 특히 빈번

흔한 로그 키워드(예시)

  • profile is not valid 같은 프로파일 관련 메시지
  • dimension mismatch 류의 shape 불일치

원인

PyTorch export 시 dynamic_axes를 대충 지정하거나, 실제 런타임 입력 shape 범위와 TensorRT optimization profile의 min/opt/max가 불일치하면 INT8 이전에 그래프 최적화 단계에서 막힙니다. INT8은 FP16/FP32보다 shape 제약에 더 민감하게 드러나는 경우가 많습니다.

해결 체크리스트

  • ONNX export 시 동적 축을 명확히 지정
  • TensorRT 빌드 시 profile 범위를 실제 트래픽 분포로 설정
# PyTorch -> ONNX export 예시 (동적 배치)
import torch

model.eval().cuda()
dummy = torch.randn(1, 3, 224, 224, device="cuda")

torch.onnx.export(
    model,
    dummy,
    "model.onnx",
    opset_version=17,
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={
        "input": {0: "batch"},
        "output": {0: "batch"},
    },
)
# TensorRT profile 설정 예시
# 모든 부등호 문자는 코드 블록 내부로만 노출
trtexec --onnx=model.onnx \
  --int8 \
  --minShapes=input:1x3x224x224 \
  --optShapes=input:8x3x224x224 \
  --maxShapes=input:32x3x224x224 \
  --saveEngine=model_int8.plan

2) 캘리브레이션 데이터가 대표성을 잃음 (정확도 붕괴 1순위)

전형적 증상

  • 엔진은 잘 만들어지고 속도도 나오는데, 정확도가 급락
  • 특정 클래스만 오분류가 증가하거나 출력 분포가 찌그러짐

원인

TensorRT INT8은 대표적으로 PTQ(Post Training Quantization) 캘리브레이션에 의존합니다. 캘리브레이션이 모델이 실제로 보게 될 입력 분포를 반영하지 못하면, activation 스케일이 틀어져 정확도가 무너집니다.

특히 아래가 치명적입니다.

  • 전처리 파이프라인이 학습/추론과 다름(정규화, resize, color order)
  • 캘리브레이션 샘플 수가 너무 적음
  • 특정 도메인 데이터가 누락됨(야간, 역광, 특정 언어 토큰 분포 등)

해결 체크리스트

  • 학습/추론과 동일한 전처리를 캘리브레이션에도 적용
  • 최소 수백~수천 샘플(모델/도메인에 따라 상이)
  • 가능한 경우 per-channel quantization이 유리한지 검토
# 캘리브레이션 데이터 로더에서 전처리 일치 여부 점검 예시
# (실제 TensorRT calibrator 구현은 프로젝트마다 다르므로 핵심만)
import cv2
import numpy as np

def preprocess(img_bgr: np.ndarray) -> np.ndarray:
    img = cv2.resize(img_bgr, (224, 224))
    img = img[:, :, ::-1].astype(np.float32) / 255.0  # BGR -> RGB
    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 - mean) / std
    img = np.transpose(img, (2, 0, 1))  # HWC -> CHW
    return img

3) QDQ 패턴 불일치: TensorRT가 기대하는 양자화 그래프 형태가 아님

전형적 증상

  • INT8로 빌드했는데 실제로는 많은 레이어가 FP16/FP32로 남음
  • 혹은 특정 노드에서 “지원하지 않는 quantize 연산” 류로 파싱 실패

원인

ONNX에서 양자화는 주로 Q/DQ(QuantizeLinear/DequantizeLinear) 패턴으로 표현됩니다. 그런데 PyTorch 기반 툴체인(예: torch quantization, onnxruntime quantizer, QAT export 등)에서 생성한 QDQ 그래프가 TensorRT가 최적으로 인식하는 패턴과 미묘하게 다르면,

  • TensorRT가 INT8 커널로 내리지 못하고 상위 precision으로 폴백
  • 또는 아예 파서가 해당 패턴을 처리하지 못함

또한 연산자별로 INT8 지원 범위가 다르며, QDQ가 삽입된 위치가 부적절하면 그래프가 깨지기도 합니다.

해결 체크리스트

  • 가능한 한 TensorRT가 권장하는 QDQ 패턴을 따르는 툴 사용
  • ONNX 그래프를 onnxsim 등으로 단순화해 불필요 노드 제거
  • “어느 레이어가 INT8로 내려갔는지”를 레이어별로 확인
# ONNX 단순화(그래프 정리) 예시
python -m onnxsim model.onnx model_simplified.onnx

# 단순화 후 다시 trtexec로 확인
trtexec --onnx=model_simplified.onnx --int8 --verbose

4) INT8 미지원 연산자 또는 플러그인 누락

전형적 증상

  • 특정 op에서 파싱 실패
  • 또는 엔진 빌드는 되지만 해당 구간이 FP16/FP32로 강제 변환

원인

TensorRT는 모든 ONNX 연산자를 동일하게 지원하지 않습니다. 특히 아래 범주에서 문제가 잦습니다.

  • NonMaxSuppression, GridSample, 일부 Resize 모드
  • 커스텀 레이어(예: 특수 activation, bespoke attention)
  • 최신 opset에서 추가된 변형 연산

이때 해결은 보통 3가지 중 하나입니다.

  1. ONNX 그래프에서 해당 연산을 더 단순한 조합으로 바꾸기
  2. TensorRT 플러그인 구현
  3. 해당 구간만 FP16/FP32로 남기는 대신 전체 파이프라인 안정화

해결 체크리스트

  • --verbose로 어떤 노드에서 막히는지 정확히 확인
  • 플러그인이 필요하면 IPluginV2DynamicExt 기반으로 INT8 지원 여부까지 구현
# 어느 노드에서 실패하는지 확인
trtexec --onnx=model.onnx --int8 --verbose 2>&1 | tee build.log

# build.log에서 실패 노드 이름을 찾은 뒤,
# Netron 등으로 해당 노드 주변 그래프를 확인

5) opset 버전/ONNX IR 버전 호환성 문제

전형적 증상

  • 파서 단계에서 즉시 실패
  • 동일 모델이 ONNX Runtime에서는 되는데 TensorRT에서만 실패

원인

TensorRT 버전마다 지원하는 ONNX opset/IR 범위가 다릅니다. PyTorch가 최신 opset으로 export하면, TensorRT가 해당 연산 정의를 모르는 상태가 됩니다.

해결 체크리스트

  • 현재 사용 중인 TensorRT 버전이 지원하는 opset 확인
  • export 시 opset을 낮춰 재시도(가능한 범위 내)
  • onnx.version_converter로 opset 변환을 시도하되, 변환 후 정확도/그래프 무결성 검증 필수
# opset을 낮춰 export하는 예시
torch.onnx.export(
    model,
    dummy,
    "model_opset13.onnx",
    opset_version=13,
    input_names=["input"],
    output_names=["output"],
)

6) FP16/INT8 혼합 시 정밀도 강제 규칙 때문에 스케일이 깨짐

전형적 증상

  • 엔진은 생성되지만 특정 레이어에서 출력이 NaN/Inf로 튐
  • 혹은 정확도가 특정 구간에서만 급락

원인

TensorRT는 네트워크 내에서 레이어별로 최적 precision을 선택합니다. 이때 INT8 구간과 FP16 구간이 섞이면서,

  • 재양자화 지점이 늘어나고
  • 스케일/클리핑이 예상보다 많이 발생
  • 민감한 레이어(예: Softmax 주변, LayerNorm 유사 연산)에서 손실이 커질 수 있습니다.

또한 일부 레이어는 INT8이 이득이 없거나 정확도 손실이 커서 FP16로 남기는 것이 낫습니다. 문제는 이 “남기는 전략”을 의도적으로 설계하지 않으면, TensorRT의 자동 선택이 서비스 품질과 어긋날 수 있다는 점입니다.

해결 체크리스트

  • 민감 레이어는 FP16 강제, 나머지는 INT8로 유도
  • 가능하면 QAT(Quantization Aware Training) 고려
  • 레이어별 precision 제어 API를 활용(환경에 따라 C++/Python 다름)
# trtexec에서 precision 제어는 제한적이므로,
# 우선은 --verbose로 어떤 레이어가 어떤 precision인지 확인하는 것이 1단계
trtexec --onnx=model.onnx --int8 --fp16 --verbose

정확도 이슈를 운영에서 다루는 방식은 “재시도/폴백/서킷브레이커” 같은 회복 패턴과도 유사합니다. 모델 엔진도 INT8이 불안정하면 FP16 엔진으로 폴백하는 전략이 필요할 수 있는데, 이런 운영 설계 관점은 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커에서 소개한 패턴을 응용할 수 있습니다.

7) 캘리브레이션 캐시/엔진 캐시 재사용으로 “틀린 결과를 계속 재현”

전형적 증상

  • 설정을 바꿨는데도 결과가 동일하게 망가짐
  • 로컬에서는 되는데 CI나 서버에서는 계속 같은 실패
  • 엔진 생성 시간이 비정상적으로 짧아짐(이미 캐시를 재사용)

원인

TensorRT INT8은 캘리브레이션 결과를 캐시로 저장해 재사용할 수 있습니다. 이 자체는 장점이지만,

  • 입력 전처리/데이터셋/shape/profile이 바뀌었는데도
  • 이전 캘리브레이션 캐시를 그대로 쓰면

“이미 잘못된 스케일”이 고착됩니다. 또한 엔진 파일(.plan)을 재사용하면서 실제로는 다른 GPU 아키텍처나 드라이버 환경에서 만든 엔진을 로딩해 문제가 나는 경우도 있습니다.

해결 체크리스트

  • 캘리브레이션 캐시 파일 삭제 후 재생성
  • 엔진 파일은 빌드 환경과 실행 환경을 일치시키거나, 최소한 GPU 아키텍처 호환을 확인
  • CI/CD에서 캐시 키를 model hash + preprocess version + profile + tensorrt version 등으로 구성
# 캐시/엔진 정리 후 재빌드 예시
rm -f calibration.cache
rm -f model_int8.plan

trtexec --onnx=model.onnx --int8 --saveEngine=model_int8.plan

운영 관점에서 캐시/디스크 문제는 모델 배포 파이프라인에서도 자주 터집니다. 빌드 환경에서 디스크가 꽉 차며 엔진 생성이 실패하는 경우도 있는데, 이런 인프라 레벨 이슈는 GitLab CI Docker 빌드 no space left 해결 가이드와 같은 방식으로 원인 분리와 대응이 가능합니다.

빠른 진단 순서(실전 체크리스트)

  1. trtexec --verbose로 실패 지점이 파서인지, 빌더인지, 런타임인지 분류
  2. ONNX를 onnxsim으로 단순화하고 동일 증상 재현 여부 확인
  3. opset/IR 호환성 확인 후 필요 시 낮춘 opset으로 재-export
  4. 동적 shape 사용 시 profile min/opt/max를 현실적으로 재설정
  5. INT8 정확도 이슈면 캘리브레이션 전처리/대표성부터 재검증
  6. 미지원 op/플러그인 필요 여부를 노드 단위로 확정
  7. 캘리브레이션 캐시/엔진 캐시를 반드시 무효화하고 재측정

마무리

PyTorch → ONNX → TensorRT에서 INT8 양자화는 “한 번에 성공하는” 작업이라기보다, 그래프 표현과 런타임 제약을 맞추는 엔지니어링에 가깝습니다. 위 7가지는 대부분의 실패를 커버하는 고빈도 원인이며, 특히 동적 shape, QDQ 패턴, 캘리브레이션 대표성, 캐시 재사용은 서로 얽혀서 문제를 복잡하게 보이게 만듭니다.

다음 단계로는, 현재 겪는 실패가 “빌드 실패”인지 “정확도 붕괴”인지에 따라 필요한 로그(예: trtexec --verbose 출력, ONNX opset, 입력 shape 범위, 캘리브레이션 샘플 수/전처리)를 정리해두면 원인 좁히기가 훨씬 빨라집니다.