ML 모델 Kubernetes 배포: Triton Inference Server로 고성능 서빙

AI 기술

ML 배포KubernetesTritonGPU 클러스터모델 서빙

이 글은 누구를 위한 것인가

  • 자체 훈련 ML 모델을 프로덕션에 서빙해야 하는 팀
  • Triton Inference Server 설정이 처음인 ML 엔지니어
  • Kubernetes에서 GPU 워크로드를 운영하는 DevOps

들어가며

FastAPI로 ML 모델을 서빙하면 편하지만, GPU 활용률이 낮고 배칭이 없어 비효율적이다. Triton Inference Server는 GPU 최대 활용, 동적 배칭, 앙상블 파이프라인을 지원한다. 배포·롤아웃 단위는 Kubernetes Deployment 문서를 기준으로 잡는다.

이 글은 bluefoxdev.kr의 ML 서빙 인프라 를 참고하여 작성했습니다.


1. Triton 서빙 아키텍처

[Triton Inference Server 구성]

모델 저장소 구조:
  model_repository/
  ├── text_classifier/
  │   ├── config.pbtxt     # 모델 설정
  │   └── 1/               # 버전 1
  │       └── model.onnx
  ├── embedding_model/
  │   ├── config.pbtxt
  │   └── 1/
  │       └── model.pt
  └── ensemble_pipeline/
      ├── config.pbtxt     # 앙상블 정의
      └── 1/               # 빈 디렉토리

[동적 배칭 (Dynamic Batching)]
  개별 요청 → 큐에 적재 → 배치 구성 → GPU 실행
  최대 대기 시간: 5-20ms (레이턴시 허용 범위 내)
  배치 크기: 8-64 (GPU 메모리에 따라)
  효과: GPU 활용률 20% → 80%+

[Kubernetes GPU 리소스]
  resources:
    limits:
      nvidia.com/gpu: 1
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists

[모델 A/B 배포]
  트래픽 스플릿: canary 10% → 50% → 100%
  메트릭 모니터링: 오류율, 레이턴시, 정확도
  자동 롤백: 이상 감지 시

2. Triton 배포 설정

# triton-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: triton-inference-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: triton
  template:
    metadata:
      labels:
        app: triton
    spec:
      containers:
      - name: triton
        image: nvcr.io/nvidia/tritonserver:24.01-py3
        args:
        - tritonserver
        - --model-repository=s3://my-model-bucket/models
        - --http-port=8000
        - --grpc-port=8001
        - --metrics-port=8002
        - --log-verbose=0
        ports:
        - containerPort: 8000
        - containerPort: 8001
        - containerPort: 8002
        resources:
          limits:
            nvidia.com/gpu: "1"
            memory: "16Gi"
          requests:
            memory: "8Gi"
        readinessProbe:
          httpGet:
            path: /v2/health/ready
            port: 8000
          initialDelaySeconds: 30
          periodSeconds: 10
      tolerations:
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
# config.pbtxt 생성 유틸리티
def generate_triton_config(
    model_name: str,
    backend: str,  # "onnxruntime", "pytorch", "tensorrt"
    input_shapes: list[dict],
    output_shapes: list[dict],
    max_batch_size: int = 32,
    dynamic_batching: bool = True,
) -> str:
    
    inputs = "\n".join(f"""  {{
    name: "{inp['name']}"
    data_type: {inp['dtype']}
    dims: {inp['dims']}
  }}""" for inp in input_shapes)
    
    outputs = "\n".join(f"""  {{
    name: "{out['name']}"
    data_type: {out['dtype']}
    dims: {out['dims']}
  }}""" for out in output_shapes)
    
    dynamic_batch_config = """
dynamic_batching {
  preferred_batch_size: [ 4, 8, 16 ]
  max_queue_delay_microseconds: 10000
}""" if dynamic_batching else ""
    
    return f"""name: "{model_name}"
backend: "{backend}"
max_batch_size: {max_batch_size}

input [{inputs}]
output [{outputs}]
{dynamic_batch_config}

instance_group [
  {{
    kind: KIND_GPU
    count: 1
  }}
]"""

import tritonclient.grpc as grpcclient
import numpy as np

def triton_inference(
    server_url: str,
    model_name: str,
    inputs: dict[str, np.ndarray],
    output_names: list[str],
) -> dict[str, np.ndarray]:
    """Triton gRPC 추론 클라이언트"""
    
    client = grpcclient.InferenceServerClient(url=server_url)
    
    triton_inputs = []
    for name, array in inputs.items():
        inp = grpcclient.InferInput(name, array.shape, "FP32")
        inp.set_data_from_numpy(array)
        triton_inputs.append(inp)
    
    triton_outputs = [grpcclient.InferRequestedOutput(name) for name in output_names]
    
    response = client.infer(
        model_name=model_name,
        inputs=triton_inputs,
        outputs=triton_outputs,
    )
    
    return {name: response.as_numpy(name) for name in output_names}

def canary_deploy_model(
    old_model: str,
    new_model: str,
    traffic_pct: int = 10,
) -> callable:
    """카나리 배포: 트래픽 분할"""
    import random
    
    def route_request(request_data):
        if random.randint(1, 100) <= traffic_pct:
            return triton_inference("triton:8001", new_model, request_data, ["output"])
        else:
            return triton_inference("triton:8001", old_model, request_data, ["output"])
    
    return route_request

마무리

Triton은 단순 FastAPI 서빙 대비 GPU 활용률을 4배 이상 높인다. 동적 배칭을 켜면 레이턴시를 5-20ms 희생하고 처리량을 3-5배 늘릴 수 있다. Kubernetes HPA(Horizontal Pod Autoscaler)와 결합하면 트래픽에 따라 자동 스케일링이 가능하다.