- Published on
KServe 모델 서빙 완벽 가이드: InferenceService·Canary 배포·Transformer·InferenceGraph 프로덕션 운영
- Authors
- Name
- 들어가며
- InferenceService CRD 심화
- Canary 배포 전략
- Custom Transformer 구현
- InferenceGraph: DAG 기반 복합 추론
- 오토스케일링 전략
- v0.15 신규 기능
- 실패 사례와 트러블슈팅
- 운영 시 주의사항
- 참고자료

들어가며
ML 모델을 학습시키는 것과 프로덕션 환경에서 안정적으로 서빙하는 것은 본질적으로 다른 엔지니어링 과제다. 학습 단계에서는 GPU 활용률과 수렴 속도가 핵심이지만, 서빙 단계에서는 레이턴시, 처리량, 버전 관리, 안전한 롤아웃, 장애 복구가 결정적이다. 특히 여러 모델이 상호작용하는 복합 추론 파이프라인에서는 단순히 Flask에 모델을 올리는 방식으로는 운영 복잡도를 감당할 수 없다.
KServe(구 KFServing)는 이러한 프로덕션 모델 서빙 문제를 Kubernetes 네이티브 방식으로 해결하기 위해 설계된 CNCF Incubating 프로젝트다. InferenceService CRD를 통해 선언적으로 모델을 배포하고, Knative 기반 오토스케일링으로 트래픽 변동에 대응하며, Canary 배포로 안전한 롤아웃을 수행하고, InferenceGraph로 DAG 기반 복합 추론을 구성할 수 있다.
KServe는 2019년 KFServing이라는 이름으로 Kubeflow 프로젝트의 일부로 시작되었으며, 2021년에 독립 프로젝트로 분리되면서 KServe로 리브랜딩되었다. 2023년 CNCF Sandbox에 합류한 후 빠르게 성장하여 2025년 Incubating 단계로 승격되었다. TFServing, TorchServe, Triton, vLLM 등 주요 추론 런타임을 모두 지원하며, LLM 시대에 맞춰 vLLM 백엔드와 Envoy AI Gateway 연동까지 확장하고 있다.
이 글에서는 KServe의 핵심 아키텍처부터 프로덕션 운영 전략까지, 실전에서 필요한 모든 것을 코드와 함께 다룬다.
InferenceService CRD 심화
Predictor / Transformer / Explainer 아키텍처
KServe의 InferenceService는 세 가지 핵심 컴포넌트로 구성된다.
Client Request
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Transformer │────▶│ Predictor │────▶│ Explainer │
│ (전처리/ │ │ (모델 추론) │ │ (설명 생성) │
│ 후처리) │◀────│ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
Client Response
- Predictor: 핵심 추론 엔진. TFServing, TorchServe, Triton, XGBoost, LightGBM, Sklearn, vLLM 등 다양한 런타임을 지원한다.
- Transformer: 추론 요청의 전처리와 응답의 후처리를 담당한다. 이미지 리사이즈, 토큰화, 피처 엔지니어링 등을 Predictor와 분리하여 독립적으로 스케일링할 수 있다.
- Explainer: 추론 결과에 대한 설명을 생성한다. Alibi Explainer, AIF360 등을 통해 모델의 예측 근거를 제공한다.
지원 런타임 비교
| 런타임 | 프레임워크 | GPU 지원 | 동적 배칭 | LLM 서빙 | 주요 용도 |
|---|---|---|---|---|---|
| TFServing | TensorFlow | O | O | X | TF SavedModel 서빙 |
| TorchServe | PyTorch | O | O | X | PyTorch 모델 서빙 |
| Triton | 멀티 프레임워크 | O | O | O | 멀티 모델 동시 서빙 |
| vLLM | PyTorch/HuggingFace | O | O | O | LLM 추론 최적화 |
| Sklearn | Scikit-learn | X | X | X | 경량 ML 모델 |
| XGBoost | XGBoost | O | X | X | 그래디언트 부스팅 |
| LGBM | LightGBM | X | X | X | 그래디언트 부스팅 |
기본 InferenceService YAML
가장 기본적인 형태의 InferenceService는 Predictor만 정의한다. 아래는 S3에 저장된 Sklearn 모델을 서빙하는 예시다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: sklearn-iris
namespace: ml-serving
annotations:
serving.kserve.io/deploymentMode: Serverless
spec:
predictor:
minReplicas: 1
maxReplicas: 10
scaleTarget: 5
scaleMetric: concurrency
model:
modelFormat:
name: sklearn
storageUri: 's3://ml-models/sklearn/iris/v1'
resources:
requests:
cpu: '500m'
memory: '512Mi'
limits:
cpu: '1'
memory: '1Gi'
이 YAML을 적용하면 KServe 컨트롤러가 Knative Service를 생성하고, Istio VirtualService를 통해 라우팅을 설정하며, Knative Pod Autoscaler가 concurrency 기반으로 오토스케일링을 수행한다.
vLLM 백엔드 LLM 서빙
KServe v0.13부터 vLLM을 1급 런타임으로 지원한다. OpenAI 호환 API를 자동으로 노출하므로 기존 OpenAI 클라이언트 코드를 그대로 사용할 수 있다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llama3-vllm
namespace: ml-serving
spec:
predictor:
minReplicas: 1
maxReplicas: 4
model:
modelFormat:
name: vLLM
storageUri: 'pvc://llm-model-cache/Meta-Llama-3.1-8B-Instruct'
args:
- '--max-model-len=8192'
- '--gpu-memory-utilization=0.90'
- '--enable-chunked-prefill'
- '--max-num-batched-tokens=16384'
- '--tensor-parallel-size=2'
resources:
requests:
cpu: '8'
memory: '32Gi'
nvidia.com/gpu: '2'
limits:
cpu: '16'
memory: '64Gi'
nvidia.com/gpu: '2'
tolerations:
- key: 'nvidia.com/gpu'
operator: 'Exists'
effect: 'NoSchedule'
nodeSelector:
gpu-type: 'a100'
storageUri에 PVC를 지정하면 대용량 LLM 가중치를 미리 노드에 캐싱해 두고 빠르게 로드할 수 있다. tensor-parallel-size=2는 2장의 GPU에 모델을 분산 배치하여 단일 GPU 메모리 한계를 극복한다.
Canary 배포 전략
canaryTrafficPercent를 활용한 점진적 롤아웃
프로덕션에서 모델을 업데이트할 때 가장 위험한 순간은 새 버전 배포 직후다. KServe는 canaryTrafficPercent 필드를 통해 트래픽을 점진적으로 새 버전으로 이동시키는 Canary 배포를 네이티브로 지원한다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: fraud-detector
namespace: ml-serving
annotations:
serving.kserve.io/deploymentMode: Serverless
spec:
predictor:
canaryTrafficPercent: 10
minReplicas: 2
maxReplicas: 20
model:
modelFormat:
name: sklearn
storageUri: 's3://ml-models/fraud/v2'
resources:
requests:
cpu: '1'
memory: '2Gi'
limits:
cpu: '2'
memory: '4Gi'
위 설정은 새 모델(v2)에 전체 트래픽의 10%만 라우팅하고, 나머지 90%는 기존 안정 버전으로 보낸다. 배포 후 모니터링을 통해 새 버전의 성능을 검증한 뒤, canaryTrafficPercent를 단계적으로 올린다.
단계별 전환 스크립트
점진적 롤아웃을 자동화하는 스크립트를 작성하면 수동 실수를 줄일 수 있다.
#!/bin/bash
# canary-promote.sh - 점진적 Canary 프로모션 스크립트
ISVC_NAME="fraud-detector"
NAMESPACE="ml-serving"
STAGES=(10 30 50 80 100)
MAX_ERROR_RATE=0.05
OBSERVE_MINUTES=10
for pct in "${STAGES[@]}"; do
echo "=== Canary traffic를 ${pct}%로 업데이트 ==="
kubectl patch inferenceservice "$ISVC_NAME" -n "$NAMESPACE" \
--type='json' \
-p="[{\"op\": \"replace\", \"path\": \"/spec/predictor/canaryTrafficPercent\", \"value\": $pct}]"
echo "${OBSERVE_MINUTES}분간 메트릭 관찰 중..."
sleep $((OBSERVE_MINUTES * 60))
# Prometheus에서 canary 에러율 조회
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
--data-urlencode "query=rate(revision_request_count{revision_name=~\".*canary.*\",response_code!=\"200\"}[5m]) / rate(revision_request_count{revision_name=~\".*canary.*\"}[5m])" \
| jq -r '.data.result[0].value[1] // "0"')
if (( $(echo "$ERROR_RATE > $MAX_ERROR_RATE" | bc -l) )); then
echo "에러율 ${ERROR_RATE}이 임계치 ${MAX_ERROR_RATE}를 초과. 롤백 수행."
kubectl patch inferenceservice "$ISVC_NAME" -n "$NAMESPACE" \
--type='json' \
-p='[{"op": "replace", "path": "/spec/predictor/canaryTrafficPercent", "value": 0}]'
exit 1
fi
echo "에러율 ${ERROR_RATE} - 정상 범위. 다음 단계로 진행."
done
echo "=== Canary 프로모션 완료. v2가 100% 트래픽을 처리합니다. ==="
이 스크립트는 10% 에서 100%까지 5단계로 트래픽을 전환하며, 각 단계에서 10분간 에러율을 모니터링한다. 에러율이 5%를 초과하면 자동으로 0%로 롤백한다.
Custom Transformer 구현
전처리/후처리 파이프라인 설계
실제 프로덕션에서는 클라이언트가 보내는 원시 데이터(이미지 URL, 텍스트, JSON)를 모델이 기대하는 텐서 형식으로 변환해야 한다. Transformer를 Predictor와 분리하면 다음과 같은 이점이 있다.
- 독립적 스케일링: 전처리가 CPU 집약적이고 추론이 GPU 집약적일 때 각각 다른 스케일링 정책 적용 가능
- 재사용성: 동일한 Transformer를 여러 모델 버전에 재사용
- 배포 독립성: Transformer 로직 변경 시 모델 재배포 없이 업데이트 가능
kserve.Model 상속과 핸들러 구현
KServe Python SDK의 kserve.Model 클래스를 상속하여 Custom Transformer를 구현한다.
import kserve
from kserve import InferRequest, InferResponse, InferInput
from typing import Dict, List
import numpy as np
from PIL import Image
import requests
from io import BytesIO
import logging
logger = logging.getLogger(__name__)
class ImageTransformer(kserve.Model):
"""이미지 URL을 받아 전처리 후 Predictor로 전달하는 Transformer"""
def __init__(self, name: str, predictor_host: str):
super().__init__(name)
self.predictor_host = predictor_host
self.target_size = (224, 224)
self.mean = np.array([0.485, 0.456, 0.406])
self.std = np.array([0.229, 0.224, 0.225])
self.ready = False
def load(self):
"""모델 초기화. Warm-up 로직을 여기에 배치."""
logger.info("ImageTransformer 초기화 완료")
self.ready = True
def preprocess(
self, payload: Dict, headers: Dict = None
) -> InferRequest:
"""이미지 URL을 받아 정규화된 텐서로 변환"""
instances = payload.get("instances", [])
processed_images = []
for instance in instances:
image_url = instance.get("image_url")
response = requests.get(image_url, timeout=10)
response.raise_for_status()
image = Image.open(BytesIO(response.content)).convert("RGB")
image = image.resize(self.target_size)
# numpy 배열로 변환 후 정규화
img_array = np.array(image, dtype=np.float32) / 255.0
img_array = (img_array - self.mean) / self.std
img_array = np.transpose(img_array, (2, 0, 1)) # CHW
processed_images.append(img_array)
input_tensor = np.stack(processed_images)
infer_input = InferInput(
name="input",
shape=list(input_tensor.shape),
datatype="FP32",
data=input_tensor.tolist(),
)
return InferRequest(
model_name=self.name,
infer_inputs=[infer_input],
)
def postprocess(
self, response: InferResponse, headers: Dict = None
) -> Dict:
"""모델 출력을 사람이 읽기 좋은 형식으로 변환"""
predictions = response.outputs[0].data
class_names = ["cat", "dog", "bird", "fish", "other"]
results = []
for pred in predictions:
if isinstance(pred, list):
probs = np.array(pred)
else:
probs = np.array([pred])
top_idx = int(np.argmax(probs))
results.append({
"class": class_names[top_idx],
"confidence": float(probs[top_idx]),
"all_scores": {
name: float(score)
for name, score in zip(class_names, probs)
},
})
return {"predictions": results}
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--predictor_host", required=True)
parser.add_argument("--model_name", default="image-classifier")
args = parser.parse_args()
transformer = ImageTransformer(
name=args.model_name,
predictor_host=args.predictor_host,
)
transformer.load()
kserve.ModelServer(workers=4).start([transformer])
Transformer 포함 InferenceService YAML
Transformer를 InferenceService에 통합하면 KServe가 자동으로 Transformer와 Predictor 간의 내부 라우팅을 설정한다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: image-classifier
namespace: ml-serving
spec:
transformer:
minReplicas: 2
maxReplicas: 15
scaleTarget: 10
scaleMetric: concurrency
containers:
- name: image-transformer
image: registry.example.com/ml/image-transformer:v1.2.0
args:
- '--model_name=image-classifier'
resources:
requests:
cpu: '1'
memory: '2Gi'
limits:
cpu: '2'
memory: '4Gi'
env:
- name: STORAGE_URI
value: ''
predictor:
minReplicas: 1
maxReplicas: 8
model:
modelFormat:
name: pytorch
storageUri: 's3://ml-models/image-classifier/resnet50-v2'
resources:
requests:
cpu: '2'
memory: '4Gi'
nvidia.com/gpu: '1'
limits:
cpu: '4'
memory: '8Gi'
nvidia.com/gpu: '1'
이 구성에서 Transformer는 CPU 노드에서 최대 15개까지 스케일아웃되고, Predictor는 GPU 노드에서 최대 8개까지 스케일아웃된다. 전처리가 병목인 경우 Transformer만 증설하면 되므로 비용 효율적이다.
InferenceGraph: DAG 기반 복합 추론
4가지 노드 유형
InferenceGraph는 여러 InferenceService를 DAG로 연결하여 복합 추론 파이프라인을 구성하는 KServe의 고급 기능이다. v0.11부터 GA로 승격되었으며, 4가지 노드 유형을 지원한다.
| 노드 유형 | 실행 방식 | 입력 전달 | 주요 Use Case | 설명 |
|---|---|---|---|---|
| Sequence | 순차 실행 | 이전 노드 출력을 다음 입력으로 | 전처리 체인 | A 결과를 B 입력으로 전달 |
| Switch | 조건 분기 | 조건에 따라 하나의 노드 선택 | A/B 테스트, 라우팅 | 조건 기반 단일 경로 선택 |
| Ensemble | 병렬 실행 + 결합 | 동일 입력을 모든 노드에 전달 | 앙상블 추론 | 여러 모델 결과를 합산/투표 |
| Splitter | 가중 분배 | 비율에 따라 하나의 노드 선택 | 트래픽 분할, Canary | 가중치 기반 트래픽 라우팅 |
A/B 테스트와 앙상블 패턴
실제 프로덕션에서 자주 사용되는 패턴은 앙상블과 A/B 테스트의 조합이다. 예를 들어, 사기 탐지 시스템에서 규칙 기반 모델, XGBoost 모델, 딥러닝 모델을 동시에 실행하고 결과를 앙상블하면 단일 모델 대비 정확도를 높일 수 있다.
apiVersion: serving.kserve.io/v1alpha1
kind: InferenceGraph
metadata:
name: fraud-detection-ensemble
namespace: ml-serving
annotations:
serving.kserve.io/propagateHeaders: 'x-request-id,x-trace-id'
spec:
nodes:
root:
routerType: Sequence
steps:
- name: feature-enrichment
serviceName: feature-enricher
weight: 100
- name: ensemble-node
nodeName: model-ensemble
model-ensemble:
routerType: Ensemble
steps:
- name: xgboost-model
serviceName: fraud-xgboost
weight: 40
- name: deep-model
serviceName: fraud-deep-learning
weight: 40
- name: rule-engine
serviceName: fraud-rule-engine
weight: 20
result-combiner:
routerType: Sequence
steps:
- name: weighted-average
serviceName: ensemble-combiner
weight: 100
이 InferenceGraph는 다음과 같이 동작한다.
- root 노드 (Sequence): 먼저 feature-enricher가 원시 데이터를 풍부한 피처로 변환한 뒤, 결과를 model-ensemble 노드로 전달한다.
- model-ensemble 노드 (Ensemble): XGBoost, 딥러닝, 규칙 엔진 세 모델이 동일한 입력으로 병렬 실행된다. 각 모델의 weight는 최종 결과 합산 시 가중치로 사용된다.
- 최종 결과는 세 모델의 가중 평균으로 산출된다.
A/B 테스트를 위한 Splitter 패턴
apiVersion: serving.kserve.io/v1alpha1
kind: InferenceGraph
metadata:
name: recommendation-ab-test
namespace: ml-serving
spec:
nodes:
root:
routerType: Splitter
steps:
- name: model-v1-stable
serviceName: recommender-v1
weight: 80
- name: model-v2-experiment
serviceName: recommender-v2
weight: 20
Splitter는 트래픽의 80%를 안정 버전으로, 20%를 실험 버전으로 분배한다. InferenceService의 Canary와 달리, InferenceGraph의 Splitter는 완전히 독립된 InferenceService 간의 트래픽 분할이므로 서로 다른 모델 아키텍처 간의 A/B 테스트에 적합하다.
오토스케일링 전략
Knative Pod Autoscaler (KPA) vs HPA vs KEDA
KServe는 세 가지 오토스케일러를 지원한다. 워크로드 특성에 따라 적합한 스케일러가 다르다.
| 특성 | KPA (Knative) | HPA (Kubernetes) | KEDA |
|---|---|---|---|
| Scale-to-Zero | O | X | O |
| 스케일링 메트릭 | concurrency, rps | cpu, memory, custom | 외부 메트릭 (Prometheus, CloudWatch 등) |
| 반응 속도 | 빠름 (1-2초) | 보통 (15-30초) | 보통 (15초) |
| GPU 워크로드 | 제한적 | 적합 | 매우 적합 |
| 커스텀 메트릭 | 제한적 | Metrics API 필요 | 풍부한 Scaler 지원 |
| Cold Start 대응 | activationScale | N/A | minReplicaCount |
| 설정 복잡도 | 낮음 | 중간 | 높음 |
Scale-to-Zero for GPU 워크로드
GPU 인스턴스는 비용이 높으므로 Scale-to-Zero가 중요하다. 하지만 GPU 모델은 cold start 시간이 길어(수십 초에서 수 분) 주의가 필요하다. KPA를 사용할 때 scaledown 지연을 적절히 설정해야 한다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model
namespace: ml-serving
annotations:
# Scale-to-Zero 활성화 (기본값)
serving.kserve.io/enable-scale-to-zero: 'true'
# 마지막 요청 후 Scale-to-Zero까지 대기 시간
autoscaling.knative.dev/scale-to-zero-pod-retention-period: '15m'
# 스케일다운 지연 (급격한 축소 방지)
autoscaling.knative.dev/scale-down-delay: '5m'
# 동시 처리 가능 요청 수 (GPU 모델은 낮게 설정)
autoscaling.knative.dev/target: '2'
autoscaling.knative.dev/metric: 'concurrency'
spec:
predictor:
minReplicas: 0
maxReplicas: 4
model:
modelFormat:
name: pytorch
storageUri: 's3://ml-models/large-model/v1'
resources:
requests:
nvidia.com/gpu: '1'
limits:
nvidia.com/gpu: '1'
KEDA + vLLM 메트릭 오토스케일링
LLM 서빙에서는 CPU/메모리가 아닌 vLLM 자체 메트릭(대기 중인 요청 수, KV 캐시 사용률)을 기반으로 스케일링하는 것이 효과적이다. KEDA의 Prometheus Scaler를 활용하면 이를 구현할 수 있다.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: llama3-vllm-scaler
namespace: ml-serving
spec:
scaleTargetRef:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
name: llama3-vllm
minReplicaCount: 1
maxReplicaCount: 8
pollingInterval: 15
cooldownPeriod: 300
advanced:
restoreToOriginalReplicaCount: true
horizontalPodAutoscalerConfig:
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 120
triggers:
# vLLM 대기 요청 수 기반 스케일링
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
metricName: vllm_waiting_requests
query: |
avg(vllm:num_requests_waiting{model_name="llama3-vllm"})
threshold: '5'
# KV Cache 사용률 기반 스케일링
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
metricName: vllm_kv_cache_usage
query: |
avg(vllm:gpu_cache_usage_perc{model_name="llama3-vllm"})
threshold: '0.85'
이 설정은 두 가지 조건 중 하나라도 충족되면 스케일아웃을 트리거한다.
- vLLM 대기 요청이 평균 5개를 초과하면 Pod를 추가한다.
- GPU KV 캐시 사용률이 85%를 초과하면 Pod를 추가한다.
스케일다운 시에는 5분의 안정화 윈도우와 2분당 1개씩 감소 정책을 적용하여 급격한 축소를 방지한다.
v0.15 신규 기능
Envoy AI Gateway 연동
KServe v0.15(2025년 6월)에서 가장 주목할 기능은 Envoy AI Gateway 연동이다. LLM 서빙에 특화된 라우팅과 관측성을 제공한다.
- 토큰 기반 Rate Limiting: API 키별로 분당/시간당 토큰 사용량을 제한
- 모델 레벨 라우팅: 요청의
model필드를 기반으로 적절한 백엔드(vLLM, Triton, 외부 API)로 라우팅 - Semantic Caching: 유사한 프롬프트에 대한 응답을 캐싱하여 비용과 레이턴시 절감
- Usage Tracking: 토큰 사용량, 레이턴시, 에러율 등을 모델/사용자/팀 단위로 추적
LocalModelCache 멀티 노드 그룹
대규모 LLM 가중치를 매번 S3에서 다운로드하면 cold start가 수 분씩 소요된다. v0.15의 LocalModelCache는 노드 로컬 디스크에 모델을 미리 캐싱하여 startup 시간을 획기적으로 단축한다.
apiVersion: serving.kserve.io/v1alpha1
kind: LocalModelCache
metadata:
name: llama3-cache
namespace: ml-serving
spec:
sourceModelUri: 's3://ml-models/Meta-Llama-3.1-8B-Instruct'
nodeGroups:
- name: a100-nodes
nodeSelector:
gpu-type: 'a100'
persistentVolumeClaimSpec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
storageClassName: local-nvme
이 설정은 gpu-type: a100 레이블이 있는 모든 노드에 50Gi NVMe 볼륨을 생성하고, S3에서 모델 가중치를 미리 다운로드해 둔다. Pod가 시작될 때 S3 대신 로컬 디스크에서 가중치를 로드하므로, cold start 시간이 수 분에서 수 초로 단축된다.
LLM 서빙 최적화
v0.15에서는 LLM 워크로드에 대한 최적화가 다수 포함되었다.
- LoRA 어댑터 Hot-Swapping: 기본 모델을 유지하면서 LoRA 어댑터만 동적으로 교체. 멀티테넌트 환경에서 고객별 파인튜닝 모델을 효율적으로 서빙
- Speculative Decoding 지원: vLLM 백엔드에서 Draft Model을 활용한 추측 디코딩으로 토큰 생성 속도 2-3배 향상
- Prefix Caching: 시스템 프롬프트 등 반복되는 프리픽스의 KV 캐시를 공유하여 TTFT(Time To First Token) 단축
실패 사례와 트러블슈팅
사례 1: Transformer OOM으로 추론 파이프라인 중단
증상: 이미지 분류 파이프라인에서 Transformer Pod가 간헐적으로 OOMKilled 되면서 전체 추론 체인이 실패.
원인 분석: Transformer에서 대용량 이미지(8K 해상도)를 메모리에 로드할 때 PIL이 압축 해제된 원본 크기만큼 메모리를 할당. 동시 요청 10개가 들어오면 10 x 200MB = 2GB가 순간적으로 필요했으나, 메모리 limit이 1Gi로 설정되어 있었다.
해결 방법:
# 이미지 크기 제한을 전처리 초기 단계에서 적용
from PIL import Image
# PIL DecompressionBomb 방지 설정
Image.MAX_IMAGE_PIXELS = 89_478_485 # 약 9500x9500
def safe_load_image(image_bytes: bytes, max_size: int = 2048):
"""메모리 안전한 이미지 로드"""
img = Image.open(BytesIO(image_bytes))
# 원본 크기가 max_size를 초과하면 즉시 리사이즈
if max(img.size) > max_size:
ratio = max_size / max(img.size)
new_size = (int(img.width * ratio), int(img.height * ratio))
img = img.resize(new_size, Image.LANCZOS)
return img.convert("RGB")
추가로 Transformer의 메모리 limit을 4Gi로 상향하고, concurrency target을 5로 낮춰 동시 처리량을 제한했다.
사례 2: Scale-to-Zero에서 Cold Start 지연
증상: GPU 모델이 Scale-to-Zero 상태에서 첫 요청 시 60초 이상 타임아웃 발생.
원인 분석: 모델 가중치(2GB)를 S3에서 다운로드하고 GPU 메모리에 로드하는 시간이 Knative의 기본 타임아웃(30초)을 초과.
해결 방법:
progressDeadline을 600초로 확장- LocalModelCache를 적용하여 노드 로컬에 모델 사전 캐싱
minReplicas: 1로 설정하여 최소 1개 Pod 유지 (비용이 허용되는 경우)
디버깅 체크리스트
KServe 배포 문제를 디버깅할 때 체계적으로 확인해야 할 항목들이다.
- InferenceService 상태 확인:
kubectl get isvc -n ml-serving에서 READY가 True인지 확인 - Pod 상태 확인:
kubectl get pods -n ml-serving -l serving.kserve.io/inferenceservice=MODEL_NAME으로 Pod 상태 점검 - 이벤트 확인:
kubectl describe isvc MODEL_NAME -n ml-serving에서 Events 섹션 확인 - Knative Revision 확인:
kubectl get revisions -n ml-serving에서 revision 상태 점검 - 스토리지 접근 확인: StorageInitializer 로그에서 S3/GCS 접근 오류가 없는지 확인
- Istio 라우팅 확인:
kubectl get virtualservice -n ml-serving에서 트래픽 라우팅 규칙 점검 - 리소스 부족 확인:
kubectl describe node에서 GPU 할당 가능량과 메모리 여유 확인
운영 시 주의사항
GPU 노드 스케줄링과 tolerations
GPU 노드에는 일반적으로 taint가 설정되어 있으므로, InferenceService에 반드시 tolerations과 nodeSelector를 명시해야 한다. 이를 누락하면 Pod가 Pending 상태에 머무른다.
spec:
predictor:
tolerations:
- key: 'nvidia.com/gpu'
operator: 'Exists'
effect: 'NoSchedule'
nodeSelector:
gpu-type: 'a100'
model:
modelFormat:
name: vLLM
# ... 모델 설정
모델 캐싱 전략
대규모 모델을 운영할 때는 계층적 캐싱 전략이 필수다.
- 1차 캐시 (노드 로컬): LocalModelCache를 활용하여 NVMe SSD에 모델 가중치 저장. 가장 빠른 로드 속도
- 2차 캐시 (PVC): PersistentVolumeClaim을 활용하여 네트워크 스토리지에 캐싱. 노드 간 공유 가능
- 3차 원본 (Object Storage): S3, GCS 등에 모델의 원본을 보관. 가장 느리지만 내구성이 높음
리소스 Quota 관리
네임스페이스별 ResourceQuota를 설정하여 팀 간 GPU 자원 경합을 방지한다.
apiVersion: v1
kind: ResourceQuota
metadata:
name: ml-serving-quota
namespace: ml-serving
spec:
hard:
requests.cpu: '64'
requests.memory: '256Gi'
requests.nvidia.com/gpu: '8'
limits.cpu: '128'
limits.memory: '512Gi'
limits.nvidia.com/gpu: '8'
persistentvolumeclaims: '20'
이 Quota는 ml-serving 네임스페이스에서 최대 GPU 8장, CPU 128코어, 메모리 512Gi까지 사용할 수 있도록 제한한다. 팀별로 네임스페이스를 분리하고 각각 적절한 Quota를 할당하면 안정적인 멀티테넌트 운영이 가능하다.