들어가며: 양자화 모델, 이제는 '배포'가 문제다
모델 양자화(Model Quantization)는 딥러닝 모델의 가중치를 FP16(16비트 부동소수점)에서 FP8(8비트 부동소수점)로 줄여 메모리 사용량을 절반 가까이 낮추고 추론 속도를 높이는 기술입니다. 하지만 대부분의 글이 '양자화 체크포인트 만드는 법'에서 끝나고, '실제 프로덕션에 어떻게 올리지?'라는 실무자의 고민은 해결해주지 못합니다.
이 글에서는 NVIDIA TensorRT Model Optimizer로 FP8 양자화한 CLIP(Contrastive Language-Image Pretraining) 모델을 ONNX로 내보내고, TensorRT 엔진으로 컴파일하여 FP16 대비 실질적인 성능 향상을 측정하는 전 과정을 다룹니다. 국내 AI 서비스 환경에서도 바로 적용할 수 있도록 구체적인 코드와 명령어를 제공합니다.
참고: 이 글은 NVIDIA 공식 블로그의 내용을 바탕으로 한국 개발자 커뮤니티(Velog, 요즘IT) 스타일로 재구성했습니다. 원문은 여기에서 확인할 수 있습니다.

Step 1: ModelOpt 체크포인트를 ONNX로 내보내기
첫 번째 단계는 ModelOpt로 생성한 FP8 체크포인트를 ONNX 형식으로 변환하는 것입니다. ModelOpt는 get_onnx_bytes_and_metadata라는 내장 헬퍼를 제공하여 ONNX opset 20+에서 FP8 QuantizeLinear/DequantizeLinear(Q/DQ)를 완벽 지원합니다.
핵심 포인트: ModelOpt의 익스포터는 가중치 측의 Q-DQ 페어를 FP8 저장 DQ 전용 체인으로 접어(fold) ONNX 파일 크기를 눈에 띄게 줄여줍니다.
import torch
from transformers import CLIPModel, CLIPTokenizer
from transformers.models.clip.modeling_clip import CLIPAttention
import modelopt.torch.opt as mto
import modelopt.torch.quantization as mtq
from modelopt.torch._deploy.utils import OnnxBytes, get_onnx_bytes_and_metadata
from modelopt.torch.quantization.plugins.diffusion.diffusers import _QuantAttention
# ONNX 익스포터가 단일 forward만 보도록 얇은 래퍼 클래스 정의
class TextEncoder(torch.nn.Module):
def __init__(self, m):
super().__init__(); self.m = m
def forward(self, x):
return self.m.get_text_features(x)
class ImageEncoder(torch.nn.Module):
def __init__(self, m):
super().__init__(); self.m = m
def forward(self, x):
return self.m.get_image_features(x)
def prepare_for_fp8_onnx_export(model):
"""FP8 attention fusion 활성화 및 CLIP float scale 초기화"""
for _, mod in model.named_modules():
if isinstance(mod, _QuantAttention):
mod._disable_fp8_mha = False
if isinstance(mod, CLIPAttention) and getattr(mod, "scale", None) is not None:
mod.scale = None
def export(wrapper, dummy, axis_name, out_name):
"""
ModelOpt 익스포터는 가중치 Q+DQ를 FP8 저장 DQ 체인으로 접고,
TRT 커스텀 연산자를 네이티브 ONNX QDQ로 재작성하여 TRT 엔진 빌드 준비 완료
"""
onnx_bytes, _ = get_onnx_bytes_and_metadata(
model=wrapper, dummy_input=(dummy,), model_name=out_name,
dynamic_axes={axis_name: {0: "batch"}}, onnx_opset=20, weights_dtype="fp16",
)
OnnxBytes.from_bytes(onnx_bytes).write_to_disk("./onnx_output", clean_dir=False)
# ModelOpt 체크포인트에서 FP8 양자화 CLIPModel 복원
mto.enable_huggingface_checkpointing()
mtq.QuantModuleRegistry.register({CLIPAttention: "CLIPAttention"})(_QuantAttention)
model = (
CLIPModel.from_pretrained(modelopt_ckpt, attn_implementation="sdpa", torch_dtype=torch.float16)
.eval().cuda()
)
prepare_for_fp8_onnx_export(model)
# 텍스트 인코더 ONNX 내보내기
tok = CLIPTokenizer.from_pretrained(model_ckpt)
dummy_text = tok(["a photo of a cat"], return_tensors="pt", padding="max_length", max_length=77)["input_ids"].cuda()
export(TextEncoder(model), dummy_text, "text_input", "text_clip_fp8")
# 이미지 인코더 ONNX 내보내기
dummy_image = torch.randn(16, 3, 224, 224, dtype=torch.float16).cuda()
export(ImageEncoder(model), dummy_image, "image_input", "image_clip_fp8")
결과: ONNX 파일 크기 비교
| 모델 구성 요소 | FP8 ModelOpt 체크포인트 | FP16 HuggingFace 체크포인트 | 크기 감소율 |
|---|---|---|---|
| CLIP 텍스트 인코더 ONNX | 156 MB | 237 MB | ~34% |
| CLIP 이미지 인코더 ONNX | 292 MB | 582 MB | ~50% |
참고: ONNX 파일 크기 축소는 편의성일 뿐 필수는 아닙니다. TensorRT는 엔진 빌드 시점에 가중치 측 Q 노드를 FP8 가중치로 접어(fuse) 버리기 때문입니다. ModelOpt ONNX 익스포터는 디스크 파일 크기를 작게 유지하기 위해 ONNX 측에서 먼저 접어주는 것입니다.

Step 2: ONNX 모델을 TensorRT 엔진으로 빌드 및 프로파일링
ONNX 파일이 준비되었으면 TensorRT의 trtexec 명령줄 도구를 사용하여 엔진을 빌드하고 벤치마크합니다.
중요: --stronglyTyped 플래그를 사용하여 ModelOpt가 ONNX 그래프에 심은 정밀도 어노테이션을 TensorRT가 반드시 따르도록 강제합니다. 이렇게 해야 FP8 가중치와 활성화가 실제 FP8 커널에서 실행됩니다.
# TensorRT 환경 변수 설정
export PATH=/usr/src/tensorrt/bin:$PATH
export LD_LIBRARY_PATH=/usr/src/tensorrt/lib:$LD_LIBRARY_PATH
# 텍스트 인코더 엔진 빌드 및 벤치마크
trtexec --onnx=text_clip_fp8.onnx \
--shapes=text_input:128x77 \
--stronglyTyped \
--saveEngine=text_clip_fp8.plan
# 이미지 인코더 엔진 빌드 및 벤치마크
trtexec --onnx=image_clip_fp8.onnx \
--shapes=image_input:128x3x224x224 \
--stronglyTyped \
--saveEngine=image_clip_fp8.plan
FP32 타입 캐스트 문제 해결
ModelOpt의 익스포터는 attention 스케일링을 FP32 라운드트립으로 감싸는데, --stronglyTyped 모드는 이를 거부합니다. 다음과 같이 FP32 이니셜라이저와 Cast 연산자를 FP16으로 재타이핑해야 합니다.
import numpy as np
import onnx
from onnx import TensorProto, numpy_helper, shape_inference
model = onnx.load("clip_fp8.onnx")
# 모든 FP32 이니셜라이저를 FP16으로 변환
for init in model.graph.initializer:
if init.data_type == TensorProto.FLOAT:
arr = numpy_helper.to_array(init).astype(np.float16)
init.CopyFrom(numpy_helper.from_array(arr, name=init.name))
# 모든 Cast(to=FP32)를 Cast(to=FP16)로 변경
for node in model.graph.node:
if node.op_type == "Cast":
to_attr = next(a for a in node.attribute if a.name == "to")
if to_attr.i == TensorProto.FLOAT:
to_attr.i = TensorProto.FLOAT16
model = shape_inference.infer_shapes(model, data_prop=True, check_type=False)
# 'strongtyped' 접미사를 붙여 저장
onnx.save(model, "clip_fp8_strongtyped.onnx")
벤치마크 결과 (NVIDIA RTX 6000 Ada, TensorRT 10.16, 배치 128)
| 항목 | FP16 | FP8 | 개선율 |
|---|---|---|---|
| 이미지 인코더 엔진 크기 | 588 MB | 306 MB | 48% 감소 |
| 텍스트 인코더 엔진 크기 | 238 MB | 156 MB | 34% 감소 |
| 이미지 인코더 지연시간 | 166.2 ms | 119.8 ms | 1.39x 속도 향상 |
| 텍스트 인코더 지연시간 | 13.2 ms | 9.1 ms | 1.45x 속도 향상 |
FP8 속도 향상의 비밀: Nsight Deep Learning Designer 시각화
Nsight Deep Learning Designer로 FP16과 FP8 프로파일을 나란히 놓고 보면 세 가지 차이가 명확합니다.
- GEMM(행렬 곱셈) 구간이 약 1.8ms에서 0.84ms로 2배 이상 빨라집니다. 이는 RTX 6000 Ada GPU의 FP8 Tensor Core 커널 덕분입니다.
- FP16 프로파일에 보이던 'fusion' 레이어가 FP8에서는 사라집니다. TensorRT가 전체 attention 블록을 특수 FP8 MHA(Multi-Head Attention) 커널로 라우팅하기 때문입니다.
- 정밀도 도넛 차트가 주황색(FP16)에서 보라색(FP8)으로 바뀝니다. 양자화된 가중치와 활성화가 실제 FP8 Tensor Core에서 실행되고 있음을 증명합니다.
국내 적용 팁: FP8 Tensor Core는 Ada 아키텍처(컴퓨팅 기능 8.9 이상)부터 지원됩니다. RTX 4090, A100, H100 등에서만 FP8 가속이 가능하므로, 기존 V100이나 T4 환경에서는 FP16 그대로 사용해야 합니다. 데이터센터 도입 전 GPU 아키텍처를 반드시 확인하세요.

Step 3: TensorRT 양자화 메커니즘 이해하기
TensorRT가 ONNX 모델을 가져올 때 QuantizeLinear/DequantizeLinear(Q/DQ) 노드를 찾습니다. 이 노드들은 텐서가 FP8 같은 저정밀도 데이터 타입으로 전환되는 지점을 표시합니다.
TensorRT는 양자화 가능한 모든 레이어의 입력에 Q/DQ 레이어 쌍을 필요로 합니다. 엔진 빌드 시 옵티마이저가 이 Q/DQ 노드를 인접 레이어에 융합(fuse)하고, 원래 레이어를 저정밀도 텐서에서 직접 동작하는 특수 커널로 대체합니다. 이 과정에서 양자화-역양자화 라운드트립이 제거되어 더 높은 연산 처리량과 낮은 메모리 대역폭 사용을 실현합니다.
실무 적용 시 주의사항 및 한계
- GPU 아키텍처 의존성: FP8은 Ada(RTX 4000 시리즈) 이상 또는 Hopper(H100) 이상에서만 하드웨어 가속이 가능합니다. RTX 3090(V100) 이하에서는 소프트웨어 에뮬레이션으로 오히려 느려질 수 있습니다.
- 정밀도 손실: FP8은 FP16 대비 표현 범위가 좁아 일부 모델(특히 매우 깊은 네트워크)에서 정확도가 떨어질 수 있습니다. 배포 전 반드시 검증 데이터셋으로 정확도를 측정하세요.
- 동적形状 제한: 위 예제는 고정 배치(128)로 엔진을 빌드했습니다. 가변 배치가 필요하면
--minShapes,--optShapes,--maxShapes옵션을 추가로 지정해야 합니다. - ONNX opset 버전: FP8 QDQ 노드는 ONNX opset 20 이상에서만 완전 지원됩니다. 이전 버전을 사용하면 익스포트가 실패할 수 있습니다.
다음 단계 학습 방향
- Triton Inference Server 연동:
--saveEngine으로 저장한.plan파일을 Triton 서버에 올려 REST/gRPC 엔드포인트로 서빙하는 방법을 학습해보세요. 메타의 프라이버시 보호 기술 ABP 심층 분석에서 실제 서비스에 적용하는 아키텍처를 참고할 수 있습니다. - LLM 양자화: 이 글은 CLIP(비전-언어) 모델을 다뤘지만, LLM은 TensorRT-LLM이라는 별도 파이프라인을 따릅니다. FP8 LLM 양자화 및 배포에 관심 있다면 TensorRT-LLM 튜토리얼을 확인하세요.
- Nsight Deep Learning Designer 심화: ONNX 모델 편집, 사용자 정의 커널 삽입, 메모리 대역폭 분석 등 고급 프로파일링 기능을 익히면 배포 병목을 정확히 찾아낼 수 있습니다.
마무리
FP8 양자화는 단순한 '모델 경량화'를 넘어, 실제 프로덕션 환경에서 1.4배 이상의 속도 향상과 메모리 사용량 절반 감소를 동시에 달성할 수 있는 강력한 기술입니다. 이 글에서 소개한 ModelOpt → ONNX → TensorRT 파이프라인을 따라가면 누구나 FP8 양자화 모델을 실제 서비스에 배포할 수 있습니다.
함께 보면 좋은 글: