Published on

PyTorch→ONNX→TensorRT FP8 양자화 트러블슈팅

Authors

PyTorch 모델을 torch.onnx.export로 ONNX로 뽑고, TensorRT로 엔진을 빌드한 다음 FP8로 양자화하면 성능이 확 뛰는 경우가 많습니다. 하지만 실무에서는 변환 단계마다 제약이 달라서, "빌드는 되는데 정확도가 망가짐", "아예 FP8이 켜지지 않음", "특정 레이어에서만 크래시" 같은 문제가 빈번합니다.

이 글은 PyTorch → ONNX → TensorRT 경로에서 FP8 양자화를 적용할 때 발생하는 트러블을 증상별로 분해해서, 원인과 해결책을 재현 가능한 커맨드/코드 중심으로 정리합니다.

또한 컨테이너에서 GPU 런타임 문제로 TensorRT 자체가 제대로 동작하지 않는 경우가 많으니, 환경 이슈가 의심되면 먼저 아래 글도 같이 확인하는 걸 권합니다.

전체 파이프라인 체크리스트

FP8은 아무 GPU에서나 되는 옵션이 아닙니다. 문제를 줄이려면 아래를 먼저 고정하세요.

  • GPU 아키텍처: FP8은 보통 Hopper 계열(예: H100)에서 제대로 지원됩니다.
  • TensorRT 버전: FP8 지원이 포함된 버전을 사용해야 합니다.
  • ONNX opset: 너무 낮으면 변환이 부정확하거나 레이어가 깨질 수 있습니다.
  • 동적 shape 여부: 동적 shape는 빌드/캘리브레이션/프로파일 설정이 복잡해집니다.
  • 정확도 기준: FP8은 FP16/FP32와 오차 특성이 다르므로 허용 오차를 정의해야 합니다.

최소 재현 파이프라인

아래는 "일단 끝까지 이어지는" 최소 예시입니다. 실제 모델에서는 전처리/후처리, dynamic axes, 플러그인 등이 추가됩니다.

# export_onnx.py
import torch
import torchvision

model = torchvision.models.resnet50(weights=None).eval().cuda()
x = torch.randn(1, 3, 224, 224, device="cuda")

torch.onnx.export(
    model,
    x,
    "model.onnx",
    opset_version=17,
    do_constant_folding=True,
    input_names=["input"],
    output_names=["output"],
)
print("exported model.onnx")
python export_onnx.py

# ONNX 구조/정합성 확인
python -c "import onnx; m=onnx.load('model.onnx'); onnx.checker.check_model(m); print('onnx ok')"

# TensorRT 빌드(예: trtexec)
# FP8 플래그는 버전에 따라 이름/지원이 다를 수 있습니다.
trtexec --onnx=model.onnx --saveEngine=model_fp8.engine --fp16 --verbose

여기서부터는 "왜 FP8이 안 켜졌는지", "켜졌는데 왜 깨지는지"를 파고듭니다.

1) FP8 옵션을 줬는데도 실제로는 FP16로 빌드됨

증상

  • 로그에 FP8 관련 문구가 없고, 엔진 인스펙션을 해보면 FP16 커널만 잡힘
  • 성능이 기대만큼 안 나오고, 메모리 사용량도 FP16 수준

원인 후보

  1. GPU가 FP8을 지원하지 않음
  2. TensorRT가 FP8을 지원하는 빌드가 아님
  3. 네트워크에 FP8로 내릴 수 없는 레이어가 많아 자동으로 상향됨
  4. 캘리브레이션/스케일 정보가 없어서 FP8 적용이 제한됨(설정/흐름 문제)

해결 접근

A. 하드웨어/드라이버/컨테이너부터 확인

컨테이너에서 nvidia-smi는 되는데 CUDA 라이브러리 매핑이 꼬이면 TensorRT가 기능을 제한하거나 로딩에 실패합니다. 이런 경우는 아래 글의 케이스들과 겹칩니다.

B. 엔진 빌드 로그에서 "정말 FP8 커널이 선택"되는지 확인

trtexec --verbose 로그에서 레이어별 precision 선택이 나옵니다. 빌드 후에는 엔진을 검사하는 편이 확실합니다.

trtexec --loadEngine=model_fp8.engine --dumpLayerInfo --profilingVerbosity=detailed
  • 모든 레이어가 FP8일 필요는 없습니다.
  • 하지만 핵심 GEMM/Conv가 FP8로 내려가야 체감 성능이 나옵니다.

C. 강제 옵션을 켰는데도 안 내려가면 "그래프"를 의심

ONNX 그래프에 불필요한 Cast, Reshape, Transpose가 과도하면 최적화가 막히고, 결과적으로 FP8 커널 매칭이 줄어듭니다.

  • 해결: export 단계에서 불필요한 dynamic control flow 제거
  • 해결: ONNX Graph Surgeon 등으로 전처리 노드 정리

2) ONNX export는 됐는데 TensorRT에서 파싱 실패

대표 에러 패턴

  • Unsupported operator
  • Attribute missing
  • Shape inference failed
  • Node has invalid dimension

원인

  • opset 불일치
  • PyTorch에서만 의미가 있는 연산이 ONNX로 불완전하게 내려감
  • dynamic axes를 걸었는데 shape 추론이 깨짐

해결

A. opset을 올리고, export 옵션을 보수적으로

torch.onnx.export(
    model,
    x,
    "model.onnx",
    opset_version=17,
    do_constant_folding=True,
    export_params=True,
)
  • 일반적으로 opset이 너무 낮으면 변환이 더 자주 깨집니다.
  • 반대로 너무 최신 opset은 TensorRT 파서가 못 따라갈 수 있으니, "TensorRT가 잘 먹는 opset"을 선택해야 합니다.

B. dynamic shape는 TensorRT 프로파일 없으면 실패

ONNX에서 입력을 동적으로 만들었다면 TensorRT 빌드 때 프로파일을 지정해야 합니다.

trtexec --onnx=model.onnx \
  --minShapes=input:1x3x224x224 \
  --optShapes=input:8x3x224x224 \
  --maxShapes=input:16x3x224x224 \
  --saveEngine=model.engine
  • 이 단계가 빠지면 빌드가 되더라도 실행에서 shape 관련 에러가 납니다.

C. 특정 op만 문제면 "대체"하거나 "플러그인" 고려

예: GridSample, 특수 activation, custom attention 등은 버전/옵션에 따라 파서 지원이 갈립니다.

  • 우회: PyTorch 모델 구조를 TensorRT-friendly하게 변경
  • 우회: ONNX에서 해당 노드를 다른 subgraph로 치환
  • 최후: TensorRT plugin 작성

3) FP8 빌드는 되는데 정확도가 크게 무너짐

증상

  • FP16 대비 metric이 급락
  • 특정 클래스/구간에서만 오차가 폭발
  • 배치 크기에 따라 결과가 흔들림

핵심 원리

FP8은 표현 범위와 정밀도가 제한적이어서, 레이어별 activation 분포가 나쁘면 스케일링이 실패합니다. 특히 아래 패턴이 위험합니다.

  • outlier가 큰 activation(GELU 이후, attention score 등)
  • 정규화 레이어 주변(LayerNorm, BatchNorm folding)
  • softmax 및 exp/log 계열

해결 전략(실전에서 많이 씀)

A. "민감 레이어"는 FP16로 남기기

모든 레이어를 FP8로 내리는 게 정답은 아닙니다. LayerNorm, Softmax 주변은 FP16 유지가 흔한 타협점입니다.

TensorRT에서는 레이어별 precision 제어가 가능한데, 방식은 API/버전에 따라 다릅니다. 파이프라인 자동화 시에는 "화이트리스트 FP8" 또는 "블랙리스트 FP16" 정책을 두는 편이 운영에 유리합니다.

B. 캘리브레이션 데이터 품질

FP8은 대표 분포를 잘 반영한 캘리브레이션/샘플이 중요합니다.

  • 학습 데이터에서 무작위로 뽑되, 전처리까지 동일하게 적용
  • 실제 트래픽 분포가 다르면 shadow traffic 샘플 사용
  • 너무 작은 샘플 수는 스케일 추정이 불안정

C. 비교 테스트를 자동화

정확도 붕괴는 "한 번에" 잡기 어렵습니다. FP32, FP16, FP8을 동일 입력에 대해 비교하고, 오차가 큰 레이어/구간을 좁혀야 합니다.

아래는 PyTorch 출력과 TensorRT 출력의 상대 오차를 대략적으로 체크하는 예시입니다.

import numpy as np

def rel_err(a, b, eps=1e-6):
    return np.max(np.abs(a - b) / (np.maximum(np.abs(a), np.abs(b)) + eps))

# y_torch: torch model output to numpy
# y_trt: tensorrt output to numpy
print("max rel err:", rel_err(y_torch, y_trt))
  • classification은 topk 일치율도 같이 봐야 합니다.
  • detection/segmentation은 후처리까지 포함하면 오차 전파가 커질 수 있어, 중간 텐서 비교가 더 유용합니다.

4) 특정 레이어에서만 빌드/런타임 크래시

증상

  • 빌드 중 segmentation fault
  • 실행 중 CUDA error 또는 illegal memory access
  • 특정 입력 shape에서만 재현

원인

  • 플러그인/커스텀 레이어의 dtype 처리 미흡
  • dynamic shape에서 stride/contiguous 가정이 깨짐
  • 엔진이 특정 tactic을 선택했을 때만 드라이버/커널 버그가 트리거

해결

A. tactic 소거(우회)로 원인 좁히기

TensorRT는 여러 tactic(커널 후보)을 탐색합니다. 특정 tactic이 문제면 tactic 선택을 제한해 우회할 수 있습니다.

  • trtexec에서 tactic 관련 옵션을 조정하거나
  • API에서 tactic source를 제한

버전/환경별로 옵션이 달라서, 우선은 --verbose로 어떤 tactic에서 죽는지 로그를 확보하는 게 1순위입니다.

B. 입력 shape를 고정해 재현성 확보

dynamic shape에서만 죽는다면, 우선 하나의 고정 shape로 엔진을 만들고 안정성을 확인하세요.

trtexec --onnx=model.onnx --shapes=input:8x3x224x224 --saveEngine=static.engine --verbose

그 다음에 프로파일을 늘려가며 어떤 구간에서 깨지는지 찾습니다.

C. 컨테이너에서 라이브러리 ABI 충돌 점검

TensorRT, CUDA, cuDNN, driver 조합이 미묘하게 어긋나면 "어떤 커널에서만" 크래시가 납니다. 특히 호스트 드라이버와 컨테이너 CUDA 런타임 매칭이 중요합니다.

환경 이슈 점검 루틴은 아래 글의 체크리스트가 그대로 도움이 됩니다.

5) 성능이 기대보다 안 나옴(오히려 느려짐)

증상

  • FP8로 빌드했는데 latency가 FP16과 비슷하거나 더 느림
  • GPU utilization이 낮고, 메모리 바운드처럼 보임

원인

  • 병목이 GEMM/Conv가 아니라 전처리/후처리 또는 transpose/reshape에 있음
  • 작은 배치/작은 입력으로 커널 런치 오버헤드가 지배
  • dynamic shape로 인해 최적 tactic이 제한됨
  • FP8이 적용된 구간이 너무 적음(핵심 레이어는 FP16)

해결

A. 레이어별 프로파일링으로 "핫스팟" 확인

trtexec --loadEngine=model_fp8.engine --separateProfileRun --dumpProfile
  • 시간이 많이 드는 레이어가 FP8 대상인지 확인
  • 만약 Transpose 류가 상위권이면, ONNX 그래프 정리가 우선입니다.

B. 배치/프로파일 재설계

  • 서비스가 batch 1 중심이면 FP8 이득이 제한될 수 있습니다.
  • 반대로 batch를 키울 수 있는 오프라인/비동기 추론이라면 FP8 이점이 커집니다.

C. I O 포함 end-to-end 측정

TensorRT 엔진만 빠른데 전체 파이프라인은 느린 경우가 흔합니다.

  • H2D/D2H 복사
  • 토크나이저/리사이즈
  • 후처리 NMS

이 구간이 병목이면 FP8 최적화 체감이 거의 없습니다.

6) ONNX는 되는데 FP8에서만 NaN/Inf가 발생

증상

  • FP16에서는 정상, FP8에서 출력에 NaN/Inf
  • 특정 입력에서만 폭발

원인

  • exp/softmax, division, normalization에서 수치 범위 초과
  • activation outlier로 스케일링 실패

해결

  • Softmax/LayerNorm 주변을 FP16로 유지
  • 입력 정규화(스케일) 재점검: 학습 때와 동일한 mean/std 적용 여부
  • outlier 완화: clipping 또는 activation 변경(모델 수정 가능할 때)

실무 팁으로는 "NaN이 최초로 생기는 레이어"를 찾는 게 가장 빠릅니다. TensorRT 쪽에서 중간 텐서를 뽑기 어렵다면, 동일한 그래프를 PyTorch에서 FP16/FP8 유사 환경으로 시뮬레이션하거나, ONNX Runtime의 디버그 기능을 이용해 중간 출력 비교를 합니다.

7) 운영 관점: 재현 가능한 빌드와 실패 로그 남기기

FP8 트러블슈팅은 결국 "환경 + 그래프 + 데이터" 3요소 싸움이라, 재현성을 확보하면 해결 속도가 확 올라갑니다.

권장하는 운영 루틴은 아래와 같습니다.

  • 엔진 빌드 커맨드와 TensorRT 로그를 아티팩트로 저장
  • ONNX 파일도 같이 저장(모델 버전과 1:1 매칭)
  • 캘리브레이션 샘플의 해시/버전을 기록
  • 성능 측정 시 입력 shape, batch, warmup 횟수 기록

컨테이너 기반이라면, "호스트 드라이버 버전"과 "컨테이너 CUDA/TensorRT 버전" 조합을 항상 같이 남기세요. 이 조합이 달라지면 같은 ONNX라도 FP8 커널 선택이 달라질 수 있습니다.

결론: FP8은 옵션이 아니라 프로젝트다

PyTorch → ONNX → TensorRT FP8 양자화는 단순히 --fp8 같은 플래그로 끝나는 최적화가 아니라,

  • ONNX 그래프를 TensorRT가 잘 최적화하도록 다듬고
  • dynamic shape와 프로파일을 설계하고
  • 민감 레이어는 FP16로 남기는 타협을 하고
  • 캘리브레이션 데이터와 정확도 회귀 테스트를 자동화하는

일련의 엔지니어링 작업입니다.

문제가 생겼을 때는 "FP8이 안 된다"로 뭉뚱그리지 말고, 이 글의 분류대로

  1. 파싱 실패, 2) FP8 미적용, 3) 정확도 붕괴, 4) 크래시, 5) 성능 미달, 6) NaN/Inf

중 어디에 속하는지부터 확정하면 해결 시간이 크게 줄어듭니다.