Skip to content
Published on

NVIDIA Triton Inference Server 프로덕션 가이드: GPU 모델 서빙 최적화 전략

Authors
  • Name
    Twitter
Triton Inference Server

들어가며

ML 모델을 학습하는 것과 프로덕션에서 안정적으로 서빙하는 것은 완전히 다른 엔지니어링 도전이다. 학습 시에는 GPU 활용률, 배치 크기, 수렴 속도가 핵심이지만, 서빙 시에는 레이턴시, 처리량, GPU 메모리 효율, 멀티 모델 동시 운영, 장애 복구가 결정적이다. 특히 실시간 추론이 요구되는 서비스에서는 p99 레이턴시 10ms 이내를 유지하면서 초당 수천 건의 요청을 처리해야 하므로, 단순히 Flask 위에 모델을 얹는 방식으로는 한계에 부딪힌다.

NVIDIA Triton Inference Server는 이러한 프로덕션 모델 서빙 문제를 아키텍처 수준에서 해결하기 위해 설계된 오픈소스 추론 서버다. TensorFlow, PyTorch, ONNX, TensorRT, Python 백엔드 등 주요 프레임워크를 단일 서버에서 동시에 서빙할 수 있으며, Dynamic Batching, Model Ensemble, Concurrent Model Execution, GPU 메모리 관리 등의 기능을 제공한다. 2025년 3월부터는 NVIDIA Dynamo 플랫폼의 일부로 통합되어, LLM 추론 최적화까지 확장된 생태계를 갖추고 있다.

이 글에서는 Triton Inference Server를 프로덕션 환경에서 실제로 운영할 때 필요한 아키텍처 설계, 모델 설정, 성능 최적화, Kubernetes 배포, 트러블슈팅 전략을 포괄적으로 다룬다. 단순 튜토리얼이 아닌, 수십 개의 모델을 동시에 서빙하고 초당 수만 건의 추론 요청을 처리하는 팀이 참고할 수 있는 실전 운영 가이드를 목표로 한다.

Triton Inference Server 아키텍처

핵심 컴포넌트 구조

Triton Inference Server는 크게 Model Repository, Scheduler, Backend, Inference Engine 네 가지 핵심 계층으로 구성된다. 클라이언트 요청이 HTTP/gRPC 엔드포인트를 통해 들어오면, 스케줄러가 요청을 적절한 배치로 묶고, 선택된 백엔드가 실제 추론을 수행한다.

Client (HTTP/gRPC/C API)
┌─────────────────────────────┐
Request Handler   (HTTP: 8000, gRPC: 8001)   (Metrics: 8002)├─────────────────────────────┤
Scheduler Layer│   ┌───────────────────────┐ │
│   │ Dynamic Batcher       │ │
│   │ Sequence Batcher      │ │
│   │ Ensemble Scheduler    │ │
│   └───────────────────────┘ │
├─────────────────────────────┤
Backend Layer│   ┌──────┬──────┬────────┐  │
│   │TRTONNX  │PyTorch │  │
│   │TF    │Python│OpenVINO│  │
│   └──────┴──────┴────────┘  │
├─────────────────────────────┤
GPU/CPU Execution Engine   (CUDA Streams, MIG, MPS)└─────────────────────────────┘

Model Repository 구조

Triton은 파일 시스템 기반의 Model Repository에서 모델을 로드한다. 각 모델은 고유한 디렉토리를 가지며, 버전별 서브디렉토리와 설정 파일(config.pbtxt)로 구성된다.

model_repository/
├── text_classifier/
│   ├── config.pbtxt
│   ├── 1/
│   │   └── model.onnx
│   └── 2/
│       └── model.onnx
├── image_detector/
│   ├── config.pbtxt
│   └── 1/
│       └── model.plan          # TensorRT engine
├── embedding_model/
│   ├── config.pbtxt
│   └── 1/
│       └── model.pt            # PyTorch TorchScript
└── preprocessing/
    ├── config.pbtxt
    └── 1/
        └── model.py            # Python backend

기본적으로 Triton은 가장 높은 버전 번호의 모델을 서빙하지만, config.pbtxt에서 버전 정책을 커스터마이징할 수 있다. 이를 통해 카나리 배포나 A/B 테스트를 모델 단위로 수행할 수 있다.

지원 백엔드와 프레임워크

Triton이 지원하는 백엔드는 다음과 같다.

백엔드모델 포맷최적 용도
TensorRT.plan (TensorRT Engine)최고 성능 GPU 추론, CV/NLP
ONNX Runtime.onnx프레임워크 간 호환성, CPU/GPU
PyTorch (LibTorch).pt (TorchScript)PyTorch 모델 직접 서빙
TensorFlowSavedModelTF 생태계 모델
Python.py커스텀 전처리/후처리, BLS
OpenVINOIR FormatIntel CPU 최적화
DALIPipelineGPU 가속 데이터 전처리
FILXGBoost/LightGBM트리 기반 모델 서빙

설치와 모델 배포

Docker 기반 설치

프로덕션에서 Triton을 운영하는 가장 표준적인 방법은 NVIDIA NGC 컨테이너를 사용하는 것이다.

# NVIDIA Triton Inference Server 컨테이너 실행
# NGC에서 최신 이미지를 pull (25.02 릴리스 기준)
docker pull nvcr.io/nvidia/tritonserver:25.02-py3

# 모델 리포지토리와 함께 서버 시작
docker run --gpus all \
  --rm \
  -p 8000:8000 \
  -p 8001:8001 \
  -p 8002:8002 \
  -v $(pwd)/model_repository:/models \
  nvcr.io/nvidia/tritonserver:25.02-py3 \
  tritonserver \
    --model-repository=/models \
    --strict-model-config=false \
    --log-verbose=1

# 서버 상태 확인
curl -v localhost:8000/v2/health/ready

--strict-model-config=false 옵션을 사용하면 Triton이 모델 파일로부터 입출력 텐서 정보를 자동으로 추론한다. ONNX, TensorFlow SavedModel, TensorRT Engine에서는 이 자동 설정이 잘 동작하지만, PyTorch나 Python 백엔드에서는 반드시 config.pbtxt를 명시해야 한다.

모델 설정(config.pbtxt) 작성

config.pbtxt는 Triton이 모델을 로드하고 서빙하는 방식을 결정하는 핵심 설정 파일이다. 아래는 ONNX 분류 모델의 전형적인 설정이다.

# text_classifier/config.pbtxt
name: "text_classifier"
platform: "onnxruntime_onnx"
max_batch_size: 64

input [
  {
    name: "input_ids"
    data_type: TYPE_INT64
    dims: [ 512 ]
  },
  {
    name: "attention_mask"
    data_type: TYPE_INT64
    dims: [ 512 ]
  }
]

output [
  {
    name: "logits"
    data_type: TYPE_FP32
    dims: [ 3 ]
  }
]

# Dynamic Batching 설정
dynamic_batching {
  preferred_batch_size: [ 8, 16, 32 ]
  max_queue_delay_microseconds: 100
}

# GPU 인스턴스 설정
instance_group [
  {
    count: 2
    kind: KIND_GPU
    gpus: [ 0 ]
  }
]

# 모델 버전 정책
version_policy: { latest { num_versions: 2 } }

# 최적화 설정 (ONNX Runtime)
optimization {
  execution_accelerators {
    gpu_execution_accelerator: [
      {
        name: "tensorrt"
        parameters {
          key: "precision_mode"
          value: "FP16"
        }
        parameters {
          key: "max_workspace_size_bytes"
          value: "1073741824"
        }
      }
    ]
  }
}

여기서 max_batch_size는 0이 아닌 값이면 Dynamic Batching 가능한 모델임을 의미한다. instance_groupcount는 해당 GPU에서 동시에 실행할 모델 인스턴스 수를 지정한다. GPU 메모리가 허용하는 범위에서 인스턴스 수를 늘리면 처리량이 증가하지만, 메모리 초과 시 OOM 에러가 발생하므로 주의해야 한다.

Dynamic Batching 최적화

Dynamic Batching 작동 원리

Dynamic Batching은 Triton에서 가장 큰 성능 향상을 제공하는 기능이다. 개별적으로 도착하는 추론 요청들을 서버 측에서 자동으로 배치로 묶어 GPU에 한 번에 전달한다. GPU는 데이터 병렬 처리에 특화되어 있으므로, 배치 크기가 클수록 GPU 활용률이 높아지고 단위 요청당 처리 비용이 줄어든다.

# 고급 Dynamic Batching 설정
dynamic_batching {
  # 선호 배치 크기: 이 크기에 도달하면 즉시 실행
  preferred_batch_size: [ 4, 8, 16, 32 ]

  # 최대 큐 대기 시간 (마이크로초)
  # 이 시간이 지나면 현재까지 모인 요청으로 배치 실행
  max_queue_delay_microseconds: 200

  # 요청 우선순위 설정
  priority_levels: 3
  default_priority_level: 2

  # 큐 정책: 최대 큐 크기 초과 시 거부
  default_queue_policy {
    timeout_action: REJECT
    default_timeout_microseconds: 5000000
    allow_timeout_override: true
    max_queue_size: 100
  }
}

max_queue_delay_microseconds는 레이턴시와 처리량 간의 트레이드오프를 결정하는 핵심 파라미터다. 값이 작으면 레이턴시가 줄지만 배치가 덜 채워져 GPU 활용률이 낮아진다. 값이 크면 배치가 더 많이 채워져 처리량은 높아지지만, 개별 요청의 레이턴시가 증가한다.

Dynamic Batching 튜닝 가이드

실제 프로덕션에서 Dynamic Batching 파라미터를 튜닝하려면 다음 절차를 따른다.

  1. 기준선 측정: preferred_batch_sizemax_queue_delay_microseconds 없이 기본값으로 시작한다.
  2. 배치 크기 탐색: perf_analyzer로 배치 크기를 1, 2, 4, 8, 16, 32, 64까지 늘려가며 처리량과 레이턴시 변화를 측정한다.
  3. 포화점 확인: 처리량 증가가 멈추거나 레이턴시가 급격히 증가하는 배치 크기를 확인한다. 이것이 최적 max_batch_size가 된다.
  4. 큐 지연 조정: SLA에 따라 max_queue_delay_microseconds를 조정한다. 실시간 서비스라면 50200us, 배치 프로세싱이라면 10005000us가 적절하다.
# perf_analyzer를 사용한 Dynamic Batching 성능 측정
# 동시 요청 수를 1에서 32까지 늘려가며 측정
perf_analyzer \
  -m text_classifier \
  -u localhost:8001 \
  -i grpc \
  --concurrency-range 1:32:4 \
  --measurement-interval 10000 \
  -b 1 \
  --percentile=99 \
  -f results_dynamic_batch.csv

# 결과 확인: 처리량(infer/sec)과 p99 레이턴시 변화 추이
# concurrency=1:  throughput=120 infer/sec, p99=8.3ms
# concurrency=4:  throughput=450 infer/sec, p99=9.1ms
# concurrency=8:  throughput=820 infer/sec, p99=10.2ms
# concurrency=16: throughput=1400 infer/sec, p99=12.5ms
# concurrency=32: throughput=1650 infer/sec, p99=22.7ms  <- 포화 시작

동시성 32에서 처리량 증가 대비 레이턴시 증가가 크므로, 이 모델의 경우 동시성 16 정도가 최적 운영 지점이다.

Model Ensemble 파이프라인

Ensemble 아키텍처

Model Ensemble은 여러 모델을 하나의 DAG(Directed Acyclic Graph)로 연결하여, 전처리-추론-후처리를 서버 내부에서 파이프라인으로 실행하는 기능이다. 클라이언트와 서버 사이의 네트워크 왕복을 줄이고, 중간 텐서를 GPU 메모리에 유지하여 데이터 전송 오버헤드를 제거한다.

# ensemble_pipeline/config.pbtxt
name: "ensemble_pipeline"
platform: "ensemble"
max_batch_size: 32

input [
  {
    name: "RAW_TEXT"
    data_type: TYPE_STRING
    dims: [ 1 ]
  }
]

output [
  {
    name: "PREDICTION"
    data_type: TYPE_FP32
    dims: [ 3 ]
  }
]

ensemble_scheduling {
  step [
    {
      model_name: "tokenizer"
      model_version: -1
      input_map {
        key: "TEXT_INPUT"
        value: "RAW_TEXT"
      }
      output_map {
        key: "INPUT_IDS"
        value: "tokenized_ids"
      }
      output_map {
        key: "ATTENTION_MASK"
        value: "tokenized_mask"
      }
    },
    {
      model_name: "text_classifier"
      model_version: -1
      input_map {
        key: "input_ids"
        value: "tokenized_ids"
      }
      input_map {
        key: "attention_mask"
        value: "tokenized_mask"
      }
      output_map {
        key: "logits"
        value: "raw_logits"
      }
    },
    {
      model_name: "postprocessor"
      model_version: -1
      input_map {
        key: "LOGITS"
        value: "raw_logits"
      }
      output_map {
        key: "RESULT"
        value: "PREDICTION"
      }
    }
  ]
}

위 설정에서 tokenizer(Python 백엔드) -> text_classifier(ONNX/TensorRT) -> postprocessor(Python 백엔드) 순서로 실행된다. 각 step의 input_mapoutput_map이 텐서 흐름을 정의하며, 중간 텐서 이름(tokenized_ids, raw_logits 등)이 step 간 데이터를 연결한다.

Python Backend를 활용한 전처리/후처리

Ensemble에서 전처리/후처리를 담당하는 Python 백엔드 모델 예시다.

# preprocessing/tokenizer/1/model.py
import triton_python_backend_utils as pb_utils
import numpy as np
from transformers import AutoTokenizer
import json


class TritonPythonModel:
    def initialize(self, args):
        """모델 로드 시 1회 호출. 토크나이저 초기화."""
        self.model_config = json.loads(args["model_config"])
        self.tokenizer = AutoTokenizer.from_pretrained(
            "bert-base-uncased",
            cache_dir="/models/cache"
        )
        self.max_length = 512

    def execute(self, requests):
        """추론 요청 배치 처리."""
        responses = []
        for request in requests:
            # 입력 텍스트 추출
            text_input = pb_utils.get_input_tensor_by_name(
                request, "TEXT_INPUT"
            )
            texts = [
                t.decode("utf-8")
                for t in text_input.as_numpy().flatten()
            ]

            # 토큰화 수행
            encoded = self.tokenizer(
                texts,
                padding="max_length",
                truncation=True,
                max_length=self.max_length,
                return_tensors="np"
            )

            # 출력 텐서 생성
            input_ids_tensor = pb_utils.Tensor(
                "INPUT_IDS",
                encoded["input_ids"].astype(np.int64)
            )
            attention_mask_tensor = pb_utils.Tensor(
                "ATTENTION_MASK",
                encoded["attention_mask"].astype(np.int64)
            )

            response = pb_utils.InferenceResponse(
                output_tensors=[input_ids_tensor, attention_mask_tensor]
            )
            responses.append(response)

        return responses

    def finalize(self):
        """모델 언로드 시 정리 작업."""
        print("Tokenizer model finalized.")

Python 백엔드의 execute 메서드는 배치 단위로 호출된다. 각 request는 Dynamic Batching에 의해 묶인 하나의 요청이며, 반드시 같은 수의 response를 반환해야 한다.

TensorRT 통합과 모델 최적화

TensorRT 엔진 변환

TensorRT는 NVIDIA GPU에서 최고 성능의 추론을 제공하는 딥러닝 최적화 엔진이다. ONNX 모델을 TensorRT 엔진으로 변환하면 FP32 대비 FP16에서 최대 2배, INT8에서 최대 4배의 처리량 향상을 얻을 수 있다.

# ONNX 모델을 TensorRT 엔진으로 변환
# 주의: Triton 서버와 동일한 TensorRT 버전의 컨테이너에서 변환해야 함
docker run --gpus all --rm \
  -v $(pwd):/workspace \
  nvcr.io/nvidia/tensorrt:25.02-py3 \
  trtexec \
    --onnx=/workspace/model.onnx \
    --saveEngine=/workspace/model.plan \
    --fp16 \
    --workspace=4096 \
    --minShapes=input_ids:1x512,attention_mask:1x512 \
    --optShapes=input_ids:16x512,attention_mask:16x512 \
    --maxShapes=input_ids:64x512,attention_mask:64x512 \
    --verbose

# Dynamic Shape 프로필 확인
# minShapes: 최소 배치 크기 (단일 요청)
# optShapes: 최적 배치 크기 (가장 빈번한 케이스)
# maxShapes: 최대 배치 크기 (Dynamic Batching 최대값과 일치시킬 것)

TensorRT 엔진 변환 시 가장 흔한 실수는 버전 불일치다. TensorRT 엔진은 변환 시 사용한 TensorRT 라이브러리 버전과 정확히 일치하는 런타임에서만 동작한다. Triton 컨테이너 25.02에 포함된 TensorRT 버전과 다른 버전의 trtexec로 변환한 엔진을 로드하면 UNAVAILABLE: Internal: unable to create TensorRT engine 에러가 발생한다. 반드시 동일한 NGC 컨테이너 태그를 사용해야 한다.

TensorRT 모델 설정

# image_detector/config.pbtxt
name: "image_detector"
platform: "tensorrt_plan"
max_batch_size: 32

input [
  {
    name: "images"
    data_type: TYPE_FP32
    dims: [ 3, 640, 640 ]
  }
]

output [
  {
    name: "detections"
    data_type: TYPE_FP32
    dims: [ 100, 6 ]
  }
]

dynamic_batching {
  preferred_batch_size: [ 4, 8, 16 ]
  max_queue_delay_microseconds: 100
}

instance_group [
  {
    count: 1
    kind: KIND_GPU
    gpus: [ 0 ]
  }
]

# TensorRT 전용 최적화 파라미터
parameters {
  key: "TRT_ENGINE_CACHE_ENABLE"
  value: { string_value: "1" }
}
parameters {
  key: "TRT_ENGINE_CACHE_PATH"
  value: { string_value: "/models/cache/trt" }
}

ONNX Runtime에서 TensorRT 가속 활용

ONNX 모델을 TensorRT 엔진으로 명시적으로 변환하지 않고도, ONNX Runtime 백엔드의 TensorRT Execution Provider를 통해 런타임 시점에 TensorRT 가속을 적용할 수 있다. 앞서 text_classifier 설정에서 보여준 optimization.execution_accelerators 블록이 바로 이 기능이다. 초기 로드 시 TensorRT 엔진 빌드 시간이 소요되지만, 캐시를 활성화하면 이후 재시작 시 빌드를 건너뛴다.

GPU 메모리 관리와 멀티 모델 서빙

인스턴스 그룹과 GPU 할당

Triton에서 GPU 메모리 관리의 핵심은 instance_group 설정이다. 하나의 GPU에서 여러 모델을 동시에 서빙하거나, 하나의 모델을 여러 인스턴스로 실행하여 처리량을 높일 수 있다.

# 멀티 GPU 분산 배치 설정
instance_group [
  {
    # GPU 02개 인스턴스
    count: 2
    kind: KIND_GPU
    gpus: [ 0 ]
  },
  {
    # GPU 11개 인스턴스
    count: 1
    kind: KIND_GPU
    gpus: [ 1 ]
  },
  {
    # CPU 폴백 인스턴스 (GPU 장애 시)
    count: 1
    kind: KIND_CPU
  }
]

GPU 메모리 예산 계산

프로덕션에서 여러 모델을 하나의 GPU에 배치할 때는 메모리 예산을 사전에 계산해야 한다.

# Model Analyzer를 사용한 GPU 메모리 프로파일링
# 각 모델이 GPU 메모리를 얼마나 사용하는지 측정
model-analyzer profile \
  --model-repository=/models \
  --profile-models text_classifier,image_detector,embedding_model \
  --triton-launch-mode=docker \
  --triton-docker-image=nvcr.io/nvidia/tritonserver:25.02-py3 \
  --output-model-repository-path=/output/models \
  --export-path=/output/results \
  --run-config-search-max-concurrency 16 \
  --run-config-search-max-instance-count 4

# 결과 예시 (A100 80GB 기준):
# text_classifier:  instance당 ~2.1GB, 최적 instance 수: 4
# image_detector:   instance당 ~4.8GB, 최적 instance 수: 2
# embedding_model:  instance당 ~1.5GB, 최적 instance 수: 6
# 총 사용량: 2.1*4 + 4.8*2 + 1.5*6 = 8.4 + 9.6 + 9.0 = 27.0GB
# A100 80GB에서 여유 메모리: 53GB (CUDA context, 프레임워크 오버헤드 고려)

MIG(Multi-Instance GPU)를 활용한 격리

A100, A30, H100 GPU에서는 MIG를 통해 하나의 물리 GPU를 최대 7개의 독립된 GPU 인스턴스로 분할할 수 있다. 각 인스턴스는 격리된 메모리, SM(Streaming Multiprocessor), L2 캐시를 가지므로, 모델 간 간섭 없이 안정적인 서빙이 가능하다.

# A100에서 MIG 활성화 및 인스턴스 생성
sudo nvidia-smi -i 0 -mig 1

# 3g.40gb 프로필 2개 생성 (A100 80GB 기준)
sudo nvidia-smi mig -i 0 -cgi 9,9 -C

# MIG 인스턴스 확인
nvidia-smi -L
# GPU 0: NVIDIA A100-SXM4-80GB
#   MIG 3g.40gb Device 0: (UUID: MIG-xxx-xxx)
#   MIG 3g.40gb Device 1: (UUID: MIG-xxx-xxx)

# Triton에서 MIG 인스턴스별로 모델 할당
# Docker 실행 시 특정 MIG 인스턴스만 노출
docker run --gpus '"device=0:0"' \
  -v $(pwd)/models_group_a:/models \
  nvcr.io/nvidia/tritonserver:25.02-py3 \
  tritonserver --model-repository=/models

모델 로딩 전략

Triton은 세 가지 모델 로딩 모드를 제공한다.

  • NONE: 서버 시작 시 모든 모델을 로드한다. GPU 메모리가 충분할 때 권장한다.
  • EXPLICIT: Model Control API를 통해 필요한 모델만 수동으로 로드/언로드한다.
  • POLL: 주기적으로 Model Repository를 폴링하여 새 모델이나 업데이트된 모델을 자동으로 감지한다.
# EXPLICIT 모드로 서버 시작 (수동 모델 관리)
tritonserver \
  --model-repository=/models \
  --model-control-mode=explicit

# Model Control API를 통한 모델 로드
curl -X POST localhost:8000/v2/repository/models/text_classifier/load

# 모델 언로드 (GPU 메모리 해제)
curl -X POST localhost:8000/v2/repository/models/text_classifier/unload

# 현재 로드된 모델 목록 확인
curl localhost:8000/v2/repository/index | python -m json.tool

EXPLICIT 모드는 GPU 메모리가 한정된 환경에서 모델을 동적으로 관리할 때 유용하다. 요청 패턴에 따라 자주 사용되지 않는 모델을 언로드하고, 필요한 모델을 로드하는 전략을 구현할 수 있다.

클라이언트 코드와 추론 요청

Python 클라이언트(tritonclient)

Triton은 공식 Python 클라이언트 라이브러리인 tritonclient를 제공한다. HTTP와 gRPC 프로토콜을 모두 지원하며, 프로덕션에서는 gRPC 사용을 권장한다. gRPC는 바이너리 직렬화로 HTTP 대비 데이터 전송 오버헤드가 적고, 스트리밍과 양방향 통신을 지원한다.

# Triton gRPC 클라이언트 예제
import tritonclient.grpc as grpcclient
import numpy as np
from functools import partial
import queue


def run_inference():
    """동기 gRPC 추론 요청"""
    # gRPC 클라이언트 생성
    triton_client = grpcclient.InferenceServerClient(
        url="localhost:8001",
        verbose=False
    )

    # 서버 상태 확인
    if not triton_client.is_server_ready():
        raise RuntimeError("Triton server is not ready")

    # 모델 메타데이터 조회
    metadata = triton_client.get_model_metadata("text_classifier")
    print(f"Model: {metadata.name}, Versions: {metadata.versions}")

    # 입력 데이터 준비
    input_ids = np.random.randint(0, 30000, size=(1, 512)).astype(np.int64)
    attention_mask = np.ones((1, 512), dtype=np.int64)

    # 입력 텐서 생성
    inputs = [
        grpcclient.InferInput("input_ids", input_ids.shape, "INT64"),
        grpcclient.InferInput("attention_mask", attention_mask.shape, "INT64"),
    ]
    inputs[0].set_data_from_numpy(input_ids)
    inputs[1].set_data_from_numpy(attention_mask)

    # 출력 텐서 지정
    outputs = [
        grpcclient.InferRequestedOutput("logits"),
    ]

    # 추론 실행 (동기)
    result = triton_client.infer(
        model_name="text_classifier",
        inputs=inputs,
        outputs=outputs,
        client_timeout=5.0,  # 5초 타임아웃
        headers={"request-id": "req-001"}
    )

    # 결과 추출
    logits = result.as_numpy("logits")
    print(f"Logits shape: {logits.shape}")
    print(f"Predictions: {np.argmax(logits, axis=-1)}")

    return logits


def run_async_inference():
    """비동기 gRPC 추론 요청 (고처리량 시나리오)"""
    triton_client = grpcclient.InferenceServerClient(
        url="localhost:8001"
    )

    result_queue = queue.Queue()

    def callback(result, error):
        if error:
            result_queue.put(error)
        else:
            result_queue.put(result.as_numpy("logits"))

    # 100개 요청을 비동기로 전송
    num_requests = 100
    for i in range(num_requests):
        input_ids = np.random.randint(0, 30000, size=(1, 512)).astype(np.int64)
        attention_mask = np.ones((1, 512), dtype=np.int64)

        inputs = [
            grpcclient.InferInput("input_ids", input_ids.shape, "INT64"),
            grpcclient.InferInput("attention_mask", attention_mask.shape, "INT64"),
        ]
        inputs[0].set_data_from_numpy(input_ids)
        inputs[1].set_data_from_numpy(attention_mask)

        outputs = [grpcclient.InferRequestedOutput("logits")]

        triton_client.async_infer(
            model_name="text_classifier",
            inputs=inputs,
            outputs=outputs,
            callback=partial(callback),
            request_id=f"async-req-{i}"
        )

    # 모든 결과 수집
    results = []
    for _ in range(num_requests):
        res = result_queue.get()
        if isinstance(res, Exception):
            print(f"Error: {res}")
        else:
            results.append(res)

    print(f"Completed {len(results)}/{num_requests} requests")
    return results


if __name__ == "__main__":
    run_inference()
    run_async_inference()

공유 메모리를 활용한 제로카피 전송

대규모 텐서(이미지, 비디오 프레임 등)를 전송할 때는 CUDA Shared Memory나 System Shared Memory를 활용하면 데이터 복사 오버헤드를 제거할 수 있다.

import tritonclient.grpc as grpcclient
from tritonclient import utils
import tritonclient.utils.cuda_shared_memory as cudashm
import numpy as np


def inference_with_cuda_shared_memory():
    """CUDA Shared Memory를 활용한 제로카피 추론"""
    triton_client = grpcclient.InferenceServerClient(url="localhost:8001")

    # 입력 데이터
    image_data = np.random.rand(1, 3, 640, 640).astype(np.float32)
    input_byte_size = image_data.nbytes

    # CUDA Shared Memory 영역 생성
    shm_handle = cudashm.create_shared_memory_region(
        "input_images_shm", input_byte_size, 0  # GPU device 0
    )

    # 데이터를 CUDA Shared Memory에 복사
    cudashm.set_shared_memory_region(shm_handle, [image_data])

    # Triton 서버에 Shared Memory 영역 등록
    triton_client.register_cuda_shared_memory(
        "input_images_shm",
        cudashm.get_raw_handle(shm_handle),
        0,  # GPU device 0
        input_byte_size
    )

    # 입력 텐서를 Shared Memory로 참조
    inputs = [grpcclient.InferInput("images", [1, 3, 640, 640], "FP32")]
    inputs[0].set_shared_memory("input_images_shm", input_byte_size)

    # 출력도 CUDA Shared Memory로 받기
    output_byte_size = 100 * 6 * 4  # 100 detections * 6 values * float32
    out_shm_handle = cudashm.create_shared_memory_region(
        "output_detections_shm", output_byte_size, 0
    )
    triton_client.register_cuda_shared_memory(
        "output_detections_shm",
        cudashm.get_raw_handle(out_shm_handle),
        0,
        output_byte_size
    )

    outputs = [grpcclient.InferRequestedOutput("detections")]
    outputs[0].set_shared_memory("output_detections_shm", output_byte_size)

    # 추론 실행 (데이터가 GPU 메모리에서 직접 전달)
    result = triton_client.infer(
        model_name="image_detector",
        inputs=inputs,
        outputs=outputs
    )

    # Shared Memory에서 결과 읽기
    detections = cudashm.get_contents_as_numpy(
        out_shm_handle,
        utils.triton_to_np_dtype("FP32"),
        [100, 6]
    )

    # 정리
    triton_client.unregister_cuda_shared_memory("input_images_shm")
    triton_client.unregister_cuda_shared_memory("output_detections_shm")
    cudashm.destroy_shared_memory_region(shm_handle)
    cudashm.destroy_shared_memory_region(out_shm_handle)

    return detections

성능 프로파일링과 모니터링

perf_analyzer를 활용한 벤치마킹

perf_analyzer는 Triton 전용 성능 측정 도구로, 합성 부하를 생성하여 모델의 처리량과 레이턴시를 체계적으로 측정한다.

# 기본 성능 측정 (동시성 기반)
perf_analyzer \
  -m text_classifier \
  -u localhost:8001 \
  -i grpc \
  --concurrency-range 1:64:8 \
  --measurement-interval 10000 \
  --percentile=95 \
  --stability-percentage 10 \
  -v

# 요청 빈도 기반 성능 측정 (실제 트래픽 패턴 시뮬레이션)
perf_analyzer \
  -m text_classifier \
  -u localhost:8001 \
  -i grpc \
  --request-rate-range 100:1000:100 \
  --measurement-interval 15000 \
  --percentile=99

# 실제 데이터를 사용한 성능 측정
perf_analyzer \
  -m text_classifier \
  -u localhost:8001 \
  -i grpc \
  --input-data real_data.json \
  --concurrency-range 1:32 \
  --measurement-interval 10000

Prometheus 메트릭 수집

Triton은 포트 8002에서 Prometheus 형식의 메트릭을 노출한다. 프로덕션 모니터링에 필수적인 메트릭들을 확인하자.

# Triton 메트릭 엔드포인트 확인
curl localhost:8002/metrics

# 핵심 메트릭:
# nv_inference_request_success     - 성공한 추론 요청 수
# nv_inference_request_failure     - 실패한 추론 요청 수
# nv_inference_count               - 총 추론 실행 횟수
# nv_inference_exec_count          - 배치 실행 횟수
# nv_inference_request_duration_us - 요청 처리 시간 (마이크로초)
# nv_inference_queue_duration_us   - 큐 대기 시간 (마이크로초)
# nv_inference_compute_infer_duration_us - 실제 추론 시간
# nv_gpu_utilization               - GPU 활용률
# nv_gpu_memory_used_bytes         - GPU 메모리 사용량
# nv_gpu_power_usage               - GPU 전력 소비
# prometheus/triton-alerts.yaml
# Triton 운영 필수 알림 규칙
groups:
  - name: triton-inference-alerts
    rules:
      # 추론 실패율이 1%를 초과하면 경고
      - alert: TritonHighFailureRate
        expr: |
          rate(nv_inference_request_failure[5m])
          / (rate(nv_inference_request_success[5m]) + rate(nv_inference_request_failure[5m]))
          > 0.01
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: 'Triton 추론 실패율 1% 초과'
          description: '모델 {{ $labels.model }} 실패율: {{ $value | humanizePercentage }}'

      # p99 레이턴시가 SLA 초과
      - alert: TritonHighLatency
        expr: |
          histogram_quantile(0.99,
            rate(nv_inference_request_duration_us_bucket[5m])
          ) > 50000
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: 'Triton p99 레이턴시 50ms 초과'

      # GPU 메모리 사용률 90% 초과
      - alert: TritonGPUMemoryHigh
        expr: |
          nv_gpu_memory_used_bytes / nv_gpu_memory_total_bytes > 0.9
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: 'GPU 메모리 사용률 90% 초과 - OOM 위험'

      # 큐 대기 시간 급증 (배치 처리 병목)
      - alert: TritonQueueBacklog
        expr: |
          rate(nv_inference_queue_duration_us_sum[5m])
          / rate(nv_inference_queue_duration_us_count[5m])
          > 10000
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: '추론 큐 평균 대기 시간 10ms 초과'

Kubernetes 배포

Helm 차트를 활용한 배포

# triton-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-inference-server
  namespace: ml-serving
  labels:
    app: triton
spec:
  replicas: 3
  selector:
    matchLabels:
      app: triton
  template:
    metadata:
      labels:
        app: triton
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '8002'
        prometheus.io/path: '/metrics'
    spec:
      containers:
        - name: triton
          image: nvcr.io/nvidia/tritonserver:25.02-py3
          command: ['tritonserver']
          args:
            - '--model-repository=s3://ml-models/triton-repo'
            - '--model-control-mode=poll'
            - '--repository-poll-secs=30'
            - '--strict-model-config=false'
            - '--log-verbose=0'
            - '--exit-on-error=false'
            - '--rate-limiter=execution_count'
            - '--rate-limiter-resource=R1:4:0'
          ports:
            - containerPort: 8000
              name: http
            - containerPort: 8001
              name: grpc
            - containerPort: 8002
              name: metrics
          resources:
            limits:
              nvidia.com/gpu: 1
              memory: '32Gi'
              cpu: '8'
            requests:
              nvidia.com/gpu: 1
              memory: '16Gi'
              cpu: '4'
          readinessProbe:
            httpGet:
              path: /v2/health/ready
              port: 8000
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /v2/health/live
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 15
            failureThreshold: 5
          volumeMounts:
            - name: model-cache
              mountPath: /models/cache
            - name: shm
              mountPath: /dev/shm
      volumes:
        - name: model-cache
          emptyDir:
            sizeLimit: 50Gi
        - name: shm
          emptyDir:
            medium: Memory
            sizeLimit: 8Gi
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      nodeSelector:
        cloud.google.com/gke-accelerator: nvidia-tesla-a100
---
apiVersion: v1
kind: Service
metadata:
  name: triton-inference-svc
  namespace: ml-serving
spec:
  type: ClusterIP
  ports:
    - port: 8000
      targetPort: 8000
      name: http
    - port: 8001
      targetPort: 8001
      name: grpc
    - port: 8002
      targetPort: 8002
      name: metrics
  selector:
    app: triton
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: triton-hpa
  namespace: ml-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: triton-inference-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Pods
      pods:
        metric:
          name: nv_inference_queue_duration_us
        target:
          type: AverageValue
          averageValue: '5000'
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120

Kubernetes 배포 시 /dev/shm 볼륨 마운트는 필수다. Python 백엔드가 multiprocessing을 사용할 때 shared memory가 필요하며, 기본 Docker의 64MB 제한으로는 부족하다. medium: Memory로 tmpfs를 마운트하고, 충분한 크기(최소 2Gi)를 할당해야 한다.

S3나 GCS에서 직접 모델을 로드하는 경우, model-control-mode=poll을 사용하면 모델 업데이트 시 서버 재시작 없이 새 모델 버전을 자동으로 감지하고 로드한다. CI/CD 파이프라인에서 모델을 S3에 업로드하면, 30초 내에 서빙 모델이 자동으로 교체되는 무중단 배포가 가능하다.

모델 서빙 프레임워크 비교

Triton을 도입하기 전에, 현재 시장의 주요 모델 서빙 솔루션과의 차이를 명확히 이해해야 한다.

비교 항목Triton Inference ServervLLMTGI (Text Generation Inference)BentoML
주요 용도범용 멀티 프레임워크 서빙LLM 특화 추론LLM 텍스트 생성 특화ML 모델 패키징/서빙
지원 프레임워크TensorRT, ONNX, PyTorch, TF, Python 등 8+PyTorch (Transformer 계열)PyTorch (HF 모델)PyTorch, TF, ONNX, XGBoost 등
핵심 최적화Dynamic Batching, Model Ensemble, Concurrent ExecutionPagedAttention, Continuous BatchingContinuous Batching, Flash AttentionAdaptive Batching, Runner
GPU 메모리 관리Instance Group, MIG 지원, Rate LimiterPagedAttention으로 메모리 단편화 제거기본 CUDA 메모리 관리프레임워크 기본값 사용
멀티 모델 서빙네이티브 지원 (수십~수백 모델)단일 모델에 최적화단일 모델에 최적화Service 단위 모델 관리
프로토콜HTTP, gRPC, C APIOpenAI API 호환OpenAI API 호환, gRPCHTTP REST, gRPC
Kubernetes 통합네이티브 (Helm, KServe)Deployment 직접 구성Deployment 직접 구성BentoCloud, Yatai
모니터링Prometheus 네이티브기본 메트릭Prometheus 지원Prometheus, OpenTelemetry
LLM 추론 성능TensorRT-LLM 백엔드 필요최상 (PagedAttention)우수 (Flash Attention)백엔드 의존적
설정 복잡도높음 (config.pbtxt 필수)낮음 (CLI 플래그)낮음 (환경변수)중간 (bentofile.yaml)
학습 곡선가파름완만완만중간
라이선스BSD 3-ClauseApache 2.0Apache 2.0Apache 2.0

언제 Triton을 선택해야 하는가

  • 멀티 프레임워크 모델을 동시에 서빙해야 할 때: ONNX 추천 모델, TensorRT 이미지 분류, PyTorch 임베딩 모델을 하나의 서버에서 운영
  • Model Ensemble 파이프라인이 필요할 때: 전처리-추론-후처리를 서버 내부에서 DAG로 연결
  • 엔터프라이즈급 운영 도구가 필요할 때: Model Analyzer, perf_analyzer, Rate Limiter, MIG 지원
  • GPU 활용률 극대화가 목표일 때: Concurrent Model Execution과 Dynamic Batching으로 GPU 유휴 시간 최소화

언제 다른 솔루션을 선택해야 하는가

  • LLM만 서빙한다면: vLLM의 PagedAttention이 메모리 효율과 처리량에서 압도적이다. Triton에서 LLM을 서빙하려면 TensorRT-LLM 백엔드를 별도로 구성해야 하므로 운영 복잡도가 높아진다.
  • HuggingFace 모델을 빠르게 배포하려면: TGI가 설정 없이 바로 사용 가능하다.
  • ML 모델 패키징과 배포 파이프라인이 필요하면: BentoML의 Bento 패키징과 Yatai 배포 관리가 더 적합하다.

프로덕션 트러블슈팅

장애 사례와 복구 전략

사례 1: GPU OOM(Out of Memory) 에러

증상: CUDA error: out of memory 에러와 함께 특정 모델의 추론 실패. 서버 로그에 Failed to allocate 메시지가 반복.

원인 분석: instance_group의 count를 GPU 메모리 용량을 초과하여 설정했거나, Dynamic Batching의 max_batch_size가 너무 커서 배치 처리 시 중간 텐서가 메모리를 초과.

복구 절차:

# 1. 현재 GPU 메모리 상태 확인
nvidia-smi --query-gpu=memory.used,memory.total --format=csv

# 2. 모델별 메모리 사용량 프로파일링
model-analyzer profile \
  --model-repository=/models \
  --profile-models problematic_model \
  --run-config-search-max-instance-count 1

# 3. config.pbtxt 수정: instance count 감소, max_batch_size 축소
# instance_group count: 4 -> 2
# max_batch_size: 64 -> 32

# 4. 모델 재로드 (서버 재시작 없이)
curl -X POST localhost:8000/v2/repository/models/problematic_model/unload
curl -X POST localhost:8000/v2/repository/models/problematic_model/load

사례 2: TensorRT 엔진 로드 실패

증상: 서버 시작 시 UNAVAILABLE: Internal: unable to create TensorRT engine 에러.

원인 분석: TensorRT 엔진 변환 시 사용한 TensorRT 라이브러리 버전과 Triton 컨테이너의 TensorRT 런타임 버전이 불일치. 또는 엔진 빌드 시 대상 GPU 아키텍처(sm_80, sm_86 등)와 실제 서빙 GPU 아키텍처가 다름.

복구 절차:

# 1. Triton 컨테이너의 TensorRT 버전 확인
docker run --rm nvcr.io/nvidia/tritonserver:25.02-py3 \
  dpkg -l | grep tensorrt

# 2. 동일 버전의 TensorRT 컨테이너에서 엔진 재빌드
docker run --gpus all --rm \
  -v $(pwd):/workspace \
  nvcr.io/nvidia/tritonserver:25.02-py3 \
  trtexec \
    --onnx=/workspace/model.onnx \
    --saveEngine=/workspace/model.plan \
    --fp16

# 3. GPU 아키텍처 확인
nvidia-smi --query-gpu=gpu_name,compute_cap --format=csv
# A100: compute_cap 8.0 (sm_80)
# A10G: compute_cap 8.6 (sm_86)
# H100: compute_cap 9.0 (sm_90)

사례 3: Dynamic Batching 큐 타임아웃

증상: 높은 트래픽 상황에서 Request timeout expired 에러가 급증. 클라이언트에서 Deadline Exceeded 에러.

원인 분석: Dynamic Batching의 큐가 가득 차서 새 요청이 거부됨. 모델 인스턴스의 처리 속도보다 요청 유입 속도가 빠름.

복구 절차:

  1. max_queue_size를 일시적으로 늘려 요청 드랍을 방지한다.
  2. instance_group의 count를 늘려 처리 용량을 확보한다.
  3. HPA를 통해 Triton Pod 수를 스케일아웃한다.
  4. 근본적으로는 모델을 TensorRT로 변환하여 단위 추론 시간을 줄인다.

사례 4: Python 백엔드 메모리 누수

증상: 서버 가동 시간이 길어질수록 호스트 메모리(RAM) 사용량이 지속적으로 증가. 결국 OOMKilled로 Pod 재시작.

원인 분석: Python 백엔드의 execute 메서드에서 요청마다 객체를 생성하지만 해제하지 않음. 또는 전역 리스트에 결과를 누적.

복구 절차:

# 잘못된 코드 (메모리 누수)
class TritonPythonModel:
    def initialize(self, args):
        self.results_cache = []  # 무한 증가

    def execute(self, requests):
        for req in requests:
            result = process(req)
            self.results_cache.append(result)  # 절대 정리되지 않음
        return responses

# 올바른 코드 (메모리 관리)
class TritonPythonModel:
    def initialize(self, args):
        self.model = load_model()  # 초기화 시 1회만 로드

    def execute(self, requests):
        responses = []
        for req in requests:
            result = self.model.predict(req)
            responses.append(build_response(result))
            # 지역 변수는 메서드 종료 시 자동 해제
        return responses

성능 디버깅 체크리스트

성능 문제 발생 시 다음 항목을 순서대로 확인한다.

# 1. GPU 활용률 확인 (낮으면 배치 크기 또는 인스턴스 수 증가 필요)
nvidia-smi dmon -s u -d 1

# 2. Triton 메트릭에서 큐 대기 시간 확인
curl -s localhost:8002/metrics | grep queue_duration

# 3. 모델별 추론 시간 확인
curl -s localhost:8002/metrics | grep compute_infer_duration

# 4. 배치 효율 확인 (exec_count 대비 inference_count 비율)
# inference_count / exec_count = 평균 배치 크기
curl -s localhost:8002/metrics | grep -E "(nv_inference_count|nv_inference_exec_count)"

운영 주의사항과 체크리스트

프로덕션 배포 전 체크리스트

모델 준비 단계:

  • 모델이 Triton 지원 포맷(ONNX, TensorRT, TorchScript 등)으로 정상 변환되었는가
  • config.pbtxt의 입출력 텐서 shape과 dtype이 모델과 정확히 일치하는가
  • max_batch_size가 GPU 메모리 내에서 안전하게 처리 가능한 크기인가
  • TensorRT 엔진의 빌드 환경(TRT 버전, GPU 아키텍처)이 서빙 환경과 일치하는가
  • 모델 버전 정책이 올바르게 설정되었는가 (latest, all, specific)

서버 설정 단계:

  • Dynamic Batching의 preferred_batch_size와 max_queue_delay가 SLA에 맞게 설정되었는가
  • instance_group의 GPU 할당이 메모리 예산 내인가 (Model Analyzer로 확인)
  • Rate Limiter가 GPU 자원 경합을 방지하도록 설정되었는가
  • 모델 로딩 모드(NONE/EXPLICIT/POLL)가 운영 시나리오에 적합한가
  • 서버 로그 레벨이 프로덕션에 적합한가 (verbose=0 또는 1)

인프라 단계:

  • Kubernetes readinessProbe와 livenessProbe가 설정되었는가
  • /dev/shm 볼륨이 충분한 크기로 마운트되었는가
  • HPA가 적절한 메트릭(큐 대기 시간, GPU 활용률)을 기준으로 설정되었는가
  • Prometheus 알림 규칙이 핵심 장애 시나리오를 커버하는가
  • 모델 저장소(S3/GCS)에 대한 IAM 권한이 올바른가

모니터링 단계:

  • Grafana 대시보드에 처리량, 레이턴시, 에러율, GPU 메트릭이 포함되었는가
  • 알림 채널(Slack, PagerDuty)이 연결되었는가
  • 모델별 성능 기준선(baseline)이 perf_analyzer로 측정되고 문서화되었는가
  • 롤백 절차가 테스트되었는가

운영 시 흔한 실수

  1. config.pbtxt 없이 배포: strict-model-config=false 모드에서 자동 설정에 의존하면, 예상과 다른 입출력 형태로 서빙되어 런타임 에러가 발생할 수 있다. 프로덕션에서는 반드시 명시적 config.pbtxt를 작성해야 한다.

  2. Shared Memory 미정리: CUDA Shared Memory를 사용한 후 unregister/destroy를 호출하지 않으면, GPU 메모리가 점진적으로 누수된다. try/finally 패턴으로 반드시 정리한다.

  3. 모델 핫 리로드 중 요청 손실: POLL 모드에서 모델이 리로드되는 수 초 간 해당 모델로의 요청이 실패할 수 있다. 클라이언트 측 재시도 로직과 로드밸런서의 다중 Pod 구성으로 방어한다.

  4. TensorRT Dynamic Shape 미설정: TensorRT 엔진 빌드 시 minShapes/optShapes/maxShapes를 설정하지 않으면 고정 shape만 처리할 수 있다. Dynamic Batching과 함께 사용하려면 반드시 Dynamic Shape 프로필을 설정해야 한다.

  5. ONNX Runtime TRT 가속 초기 지연: ONNX Runtime의 TensorRT Execution Provider를 처음 사용할 때, 런타임에서 TRT 엔진을 빌드하므로 첫 요청에 수분이 소요될 수 있다. 엔진 캐시를 영속 볼륨에 저장하여 재시작 시 빌드를 건너뛰도록 해야 한다.

고급 최적화 전략

Rate Limiter를 활용한 리소스 관리

여러 모델이 하나의 GPU를 공유할 때, Rate Limiter를 사용하면 모델 간 GPU 자원 경합을 제어할 수 있다.

# Rate Limiter 활성화 (서버 시작 시)
tritonserver \
  --model-repository=/models \
  --rate-limiter=execution_count \
  --rate-limiter-resource=GPU_EXEC_SLOTS:8:0
  # GPU 0에서 동시 실행 가능한 최대 슬롯 수: 8
# 무거운 모델은 더 많은 슬롯을 소비하도록 설정
# heavy_model/config.pbtxt
name: "heavy_model"
rate_limiter {
  resources [
    {
      name: "GPU_EXEC_SLOTS"
      count: 4  # 이 모델은 8개 중 4개 슬롯을 점유
    }
  ]
}

# 가벼운 모델은 적은 슬롯 소비
# light_model/config.pbtxt
name: "light_model"
rate_limiter {
  resources [
    {
      name: "GPU_EXEC_SLOTS"
      count: 1  # 1개 슬롯만 점유
    }
  ]
}

이렇게 설정하면 heavy_model 2개가 동시에 실행될 때(4+4=8 슬롯) 다른 모델은 대기하게 되어, GPU 메모리 초과나 성능 저하를 방지할 수 있다.

Response Cache

동일한 입력에 대해 반복되는 추론 요청이 많은 경우, Response Cache를 활성화하면 GPU 연산 없이 캐시된 결과를 반환할 수 있다.

# Response Cache 활성화 (64MB 캐시)
tritonserver \
  --model-repository=/models \
  --response-cache-byte-size=67108864
# 특정 모델에만 캐시 적용
# embedding_model/config.pbtxt
name: "embedding_model"
response_cache { enable: true }

캐시 적중률이 높으면 GPU 부하가 크게 줄어들지만, 캐시 메모리가 호스트 RAM을 사용하므로 적절한 크기 설정이 필요하다. 입력이 매번 다른 모델(예: 실시간 센서 데이터)에는 효과가 없다.

마무리

NVIDIA Triton Inference Server는 프로덕션 GPU 모델 서빙에서 가장 포괄적인 기능을 제공하는 오픈소스 추론 서버다. Dynamic Batching으로 GPU 활용률을 극대화하고, Model Ensemble로 전처리-추론-후처리를 서버 내부에서 완결하며, TensorRT 통합으로 최고 수준의 추론 성능을 달성할 수 있다. 멀티 모델 서빙, 버전 관리, Prometheus 메트릭, Kubernetes 네이티브 배포까지 엔터프라이즈 운영에 필요한 모든 요소를 갖추고 있다.

다만, Triton의 학습 곡선은 가파르다. config.pbtxt 기반의 모델 설정, TensorRT 엔진 빌드와 버전 관리, 복수 백엔드 간의 호환성 확인, GPU 메모리 예산 관리 등은 운영팀의 깊은 이해를 요구한다. LLM만 서빙한다면 vLLM이, 빠른 프로토타이핑이 목표라면 BentoML이나 TGI가 더 효율적인 선택일 수 있다.

중요한 것은 도구 자체가 아니라, 서빙 워크로드의 특성에 맞는 올바른 선택이다. 멀티 프레임워크 모델을 하나의 GPU 클러스터에서 효율적으로 운영하고, 엔터프라이즈급 안정성과 관측성이 필요하다면, Triton Inference Server는 현재 시점에서 가장 성숙한 선택이다.

참고자료