들어가며
최신 컴퓨터비전 모델은 높은 정확도를 자랑하지만, 실제 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)이 증가합니다.
- 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 환경에서 극한의 성능을 추구할 때 최적입니다. 실무에서는 다음 프로세스를 권장합니다.
- PyTorch → ONNX 변환 후 ONNX Runtime으로 빠르게 검증
- 성능이 부족하면 TensorRT + INT8로 추가 최적화
- 배포 전 실제 Edge 디바이스에서 정확도/속도/메모리 테스트 필수
- Mixed Precision (중요 레이어는 FP16, 나머지는 INT8) 적극 활용
이 가이드를 바탕으로 여러분의 컴퓨터비전 모델을 Edge 디바이스에 성공적으로 배포하시길 바랍니다!
Did you find this helpful?
☕ Buy me a coffee
Leave a Reply