ONNX Runtime과 TensorRT로 컴퓨터비전 모델 경량화하기: Edge AI 배포 실전 가이드

Updated Feb 6, 2026

들어가며

최신 컴퓨터비전 모델은 높은 정확도를 자랑하지만, 실제 Edge 디바이스에 배포하려면 추론 속도메모리 사용량이 큰 걸림돌이 됩니다. ONNX Runtime과 TensorRT는 이러한 문제를 해결하는 대표적인 모델 최적화 프레임워크입니다. 이 글에서는 두 도구의 특징을 비교하고, 실무에서 바로 적용할 수 있는 경량화 및 배포 방법을 소개합니다.

ONNX Runtime vs TensorRT 비교

항목 ONNX Runtime TensorRT
개발사 Microsoft NVIDIA
지원 플랫폼 CPU, GPU, Edge TPU 등 다양 NVIDIA GPU 전용
최적화 수준 중상 (범용 최적화) 최상 (CUDA 특화 최적화)
사용 난이도 낮음 (간단한 API) 중상 (설정 복잡)
양자화 지원 INT8, FP16 INT8, FP16, INT4(실험적)
주요 장점 크로스 플랫폼, 빠른 적용 극한의 성능, Jetson 최적화

선택 기준: NVIDIA GPU 환경에서 최고 성능이 필요하면 TensorRT, 다양한 하드웨어 지원이 필요하면ONNX Runtime을 선택하세요.

ONNX Runtime 실전 활용

1. 모델 변환 및 추론

PyTorch 모델을 ONNX로 변환 후 추론하는 기본 흐름입니다.

import torch
import torchvision.models as models
import onnxruntime as ort
import numpy as np

# 1. PyTorch 모델 로드
model = models.resnet50(pretrained=True)
model.eval()

# 2. ONNX로 변환
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
    model,
    dummy_input,
    "resnet50.onnx",
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}
)

# 3. ONNX Runtime 세션 생성 (CPU)
sess = ort.InferenceSession("resnet50.onnx", providers=["CPUExecutionProvider"])

# 4. 추론 실행
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
output = sess.run(None, {"input": input_data})
print(f"출력 shape: {output[0].shape}")  # (1, 1000)

2. GPU 가속 및 FP16 최적화

# CUDA Execution Provider로 GPU 가속
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL

sess_gpu = ort.InferenceSession(
    "resnet50.onnx",
    sess_options,
    providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)

# FP16 변환 (NVIDIA GPU)
import onnx
from onnxconverter_common import float16

model_fp32 = onnx.load("resnet50.onnx")
model_fp16 = float16.convert_float_to_float16(model_fp32)
onnx.save(model_fp16, "resnet50_fp16.onnx")

성능 비교 (ResNet-50 기준)

설정 추론 시간 (ms) 메모리 (MB)
PyTorch (FP32) 45 230
ONNX Runtime CPU (FP32) 38 210
ONNX Runtime GPU (FP32) 12 220
ONNX Runtime GPU (FP16) 7 150

테스트 환경: RTX 3080, Batch Size=1

TensorRT 고급 최적화

1. INT8 양자화 (Calibration)

TensorRT의 핵심은 INT8 양자화입니다. 대표 데이터로 보정(calibration)하여 정확도 손실을 최소화합니다.

import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit

# Calibration 데이터셋 준비
class ImageCalibrator(trt.IInt8EntropyCalibrator2):
    def __init__(self, data_loader, cache_file):
        super().__init__()
        self.data_loader = data_loader
        self.cache_file = cache_file
        self.batch_size = 8
        self.current_index = 0
        # GPU 메모리 할당
        self.device_input = cuda.mem_alloc(self.batch_size * 3 * 224 * 224 * 4)

    def get_batch_size(self):
        return self.batch_size

    def get_batch(self, names):
        if self.current_index < len(self.data_loader):
            batch = next(iter(self.data_loader)).numpy()
            cuda.memcpy_htod(self.device_input, batch)
            self.current_index += 1
            return [int(self.device_input)]
        return None

    def read_calibration_cache(self):
        if os.path.exists(self.cache_file):
            with open(self.cache_file, "rb") as f:
                return f.read()

    def write_calibration_cache(self, cache):
        with open(self.cache_file, "wb") as f:
            f.write(cache)

# TensorRT 엔진 빌드 (INT8)
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)

with open("resnet50.onnx", "rb") as f:
    parser.parse(f.read())

config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = ImageCalibrator(calib_loader, "calibration.cache")

engine = builder.build_serialized_network(network, config)
with open("resnet50_int8.trt", "wb") as f:
    f.write(engine)

2. Dynamic Shape 지원

# 동적 배치 사이즈 설정
profile = builder.create_optimization_profile()
profile.set_shape(
    "input",
    (1, 3, 224, 224),    # min
    (4, 3, 224, 224),    # opt
    (16, 3, 224, 224)    # max
)
config.add_optimization_profile(profile)

TensorRT 성능 (Jetson Orin Nano)

모델 FP32 (ms) FP16 (ms) INT8 (ms) 정확도 손실
YOLOv8n 28 12 6 -0.2% mAP
EfficientNet-B0 35 18 9 -0.5% Top-1

Edge AI 배포 전략

1. 모델 크기 vs 정확도 트레이드오프

실제 배포 시 고려할 양자화 수준입니다.

# 양자화 레벨별 설정
quantization_levels = {
    "FP32": {"accuracy": 100, "size": 100, "speed": 1.0},
    "FP16": {"accuracy": 99.5, "size": 50, "speed": 2.0},
    "INT8": {"accuracy": 98.5, "size": 25, "speed": 3.5},
    "Mixed (FP16+INT8)": {"accuracy": 99.0, "size": 30, "speed": 3.0}
}

추천: 정확도 손실 1% 이내면 INT8, 0.5% 이내가 필수면 FP16 사용

2. Jetson 디바이스별 최적 설정

디바이스 권장 양자화 Max Batch Size 전력 모드
Nano INT8 1-2 10W
Xavier NX FP16 4-8 15W
Orin Nano INT8 8-16 15W
Orin AGX FP16 16-32 50W

3. 실시간 객체 검출 파이프라인

import cv2
import tensorrt as trt
import numpy as np

class TRTInference:
    def __init__(self, engine_path):
        self.logger = trt.Logger(trt.Logger.WARNING)
        with open(engine_path, "rb") as f:
            self.runtime = trt.Runtime(self.logger)
            self.engine = self.runtime.deserialize_cuda_engine(f.read())
        self.context = self.engine.create_execution_context()

    def infer(self, image):
        # 전처리
        input_data = cv2.resize(image, (640, 640))
        input_data = input_data.astype(np.float32) / 255.0
        input_data = np.transpose(input_data, (2, 0, 1))[np.newaxis, :]

        # 추론
        self.context.execute_v2([input_data.ctypes.data, output.ctypes.data])
        return output

# 실시간 스트림 처리
model = TRTInference("yolov8n_int8.trt")
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    detections = model.infer(frame)
    # 후처리 및 시각화...
    cv2.imshow("Detection", frame)
    if cv2.waitKey(1) == ord('q'):
        break

주요 최적화 기법 정리

1. 그래프 최적화

  • 레이어 융합: Conv + BatchNorm + ReLU를 단일 커널로 병합
  • Constant Folding: 컴파일 타임에 상수 연산 미리 계산
  • Dead Code Elimination: 사용되지 않는 연산 제거

2. 메모리 최적화

# ONNX Runtime 메모리 설정
sess_options.enable_mem_pattern = True
sess_options.enable_cpu_mem_arena = True

# TensorRT Workspace 제한 (Jetson용)
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 28)  # 256MB

3. 배치 처리 전략

단일 프레임보다 배치 처리가 효율적이지만, 지연시간(latency)이 증가합니다.

<br/>Throughput=Batch SizeInference Time<br/><br /> \text{Throughput} = \frac{\text{Batch Size}}{\text{Inference Time}}<br />

  • Throughput: 초당 처리량 (frames/sec)
  • Batch Size: 한 번에 처리할 이미지 수
  • Inference Time: 추론에 걸린 시간 (sec)

실시간 애플리케이션에서는 Batch Size=1, 오프라인 처리는 Batch Size=8~16 권장

트러블슈팅 체크리스트

ONNX Runtime 이슈

  • [ ] Opset 버전 불일치: torch.onnx.export(opset_version=13) 명시
  • [ ] 동적 축 에러: dynamic_axes 정확히 설정
  • [ ] 느린 CPU 추론: intra_op_num_threads 조정
sess_options.intra_op_num_threads = 4

TensorRT 이슈

  • [ ] Unsupported Layer: ONNX Simplifier로 전처리
pip install onnx-simplifier
python -m onnxsim model.onnx model_simplified.onnx
  • [ ] INT8 정확도 저하: Calibration 데이터 품질 확인 (최소 500장 이상)
  • [ ] Out of Memory: Workspace 크기 줄이기

실무 체크포인트

배포 전 반드시 확인할 사항입니다.

항목 확인 방법
정확도 검증 테스트셋으로 원본 vs 최적화 모델 비교
추론 속도 100회 반복 측정 후 평균/P95 확인
메모리 사용량 nvidia-smi 또는 top 모니터링
전력 소비 Jetson은 tegrastats 사용
온도 장시간 실행 후 thermal throttling 체크

마무리

ONNX Runtime은 빠른 프로토타이핑과 크로스 플랫폼 배포에 강점이 있고, TensorRT는 NVIDIA GPU 환경에서 극한의 성능을 추구할 때 최적입니다. 실무에서는 다음 프로세스를 권장합니다.

  1. PyTorch → ONNX 변환 후 ONNX Runtime으로 빠르게 검증
  2. 성능이 부족하면 TensorRT + INT8로 추가 최적화
  3. 배포 전 실제 Edge 디바이스에서 정확도/속도/메모리 테스트 필수
  4. Mixed Precision (중요 레이어는 FP16, 나머지는 INT8) 적극 활용

이 가이드를 바탕으로 여러분의 컴퓨터비전 모델을 Edge 디바이스에 성공적으로 배포하시길 바랍니다!

Did you find this helpful?

☕ Buy me a coffee

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

TODAY 461 | TOTAL 2,684