- Published on
KServe+Istio로 GPU 모델 카나리 배포 실전 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 중인 GPU 모델을 교체할 때 가장 무서운 순간은 새 이미지가 떴는데 성능이 나쁘거나, GPU 메모리 사용 패턴이 달라 OOM이 나거나, 지연 시간이 튀어 SLA를 깨는 상황입니다. 이때 한 번에 100% 트래픽을 새 버전으로 넘기는 대신, 카나리(canary) 배포로 일부 트래픽만 흘려보내며 지표를 확인하고 점진적으로 확대하는 것이 안전합니다.
이 글에서는 KServe InferenceService로 GPU 모델을 서빙하고, Istio로 트래픽 비율을 제어해 카나리 배포를 수행하는 과정을 실전 관점에서 정리합니다. 특히 “GPU 모델” 특유의 제약(콜드 스타트, VRAM, 노드 스케줄링, 오토스케일 지연)을 고려해, 배포 전 체크리스트부터 롤백까지 다룹니다.
아키텍처 개요: KServe와 Istio의 역할 분담
- KServe: 모델 서빙 워크로드를
InferenceService로 추상화합니다. 버전별로predictor를 배치하고, 오토스케일/롤링업데이트/트래픽 스플릿 같은 서빙 기능을 제공합니다. - Istio: 서비스 메시 레벨에서 라우팅, 가중치 분산, 헤더 기반 라우팅, mTLS, 관측(telemetry) 등을 담당합니다.
실무에서는 “KServe의 트래픽 스플릿”만으로도 카나리가 가능하지만, Istio를 함께 쓰면 다음이 좋아집니다.
- 특정 사용자/헤더에만 새 모델을 붙이는 실험 트래픽 라우팅
- 실패율/지연 시간 기반으로 빠르게 정책을 바꾸는 운영 유연성
- 조직 표준이 Istio일 때, 관측/보안/정책을 통일
전제 조건(환경 준비)
아래 가정으로 예시를 구성합니다.
- Kubernetes 클러스터에 NVIDIA GPU 노드가 있고,
nvidia-device-plugin이 설치되어nvidia.com/gpu리소스가 노출됨 - KServe 설치
- Istio 설치 및 사이드카 주입 활성화(네임스페이스 라벨링)
- 모델 서버는 예시로
triton또는 커스텀 컨테이너를 사용(핵심은 GPU 리소스 요청)
네임스페이스 예시:
kubectl create ns ml-serving
kubectl label ns ml-serving istio-injection=enabled
GPU 모델 카나리의 핵심 난제 4가지
GPU 모델 카나리는 CPU 웹앱 카나리보다 변수가 많습니다.
- 콜드 스타트 비용: 첫 로딩 시 모델 가중치 로드와 CUDA 커널 워밍업으로 지연이 큼
- VRAM OOM: 새 버전이 KV 캐시/배치 전략/정밀도 변화로 VRAM 요구량이 달라짐
- 오토스케일 지연: GPU 노드가 부족하면 파드가 Pending 상태로 오래 머무름
- 성능 회귀가 지표에 늦게 반영: 지연/오류율뿐 아니라 품질(정확도, hallucination 등)은 별도 샘플링이 필요
OOM 관련으로는 로컬/서버 환경에서의 메모리 전략도 중요합니다. 모델이 갑자기 OOM을 내면 카나리 단계에서 바로 멈춰야 합니다. 필요하면 양자화나 KV 캐시 전략을 점검하세요. 관련해서는 Transformers 로컬 LLM OOM 해결 - 4bit+KV캐시도 같이 참고하면 좋습니다.
1) 베이스라인(Stable) InferenceService 만들기
먼저 안정 버전 v1을 배포합니다. 아래는 KServe InferenceService 예시입니다.
주의: MDX 환경에서 부등호 문자가 문제될 수 있으니, 본문에서는 < > 또는 백틱 인라인 코드로 표기합니다. YAML 블록 안에서는 그대로 사용해도 안전하지만, 본문 설명에서는 < >를 사용합니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model
namespace: ml-serving
spec:
predictor:
containers:
- name: predictor
image: ghcr.io/acme/gpu-model-server:v1
ports:
- containerPort: 8080
resources:
requests:
cpu: "1"
memory: "4Gi"
nvidia.com/gpu: "1"
limits:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
env:
- name: MODEL_NAME
value: "gpu-model"
- name: LOG_LEVEL
value: "info"
배포:
kubectl apply -f gpu-model-v1.yaml
kubectl -n ml-serving get inferenceservice gpu-model -w
KServe는 내부적으로 Revision/Deployment/Service를 만들며, Istio가 있으면 Ingress Gateway를 통해 외부 트래픽을 받습니다.
2) 카나리 버전(v2) 추가: 트래픽 스플릿 기본
KServe는 canary 섹션으로 새 버전을 붙이고, canaryTrafficPercent로 비율을 조절할 수 있습니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model
namespace: ml-serving
spec:
predictor:
containers:
- name: predictor
image: ghcr.io/acme/gpu-model-server:v1
ports:
- containerPort: 8080
resources:
requests:
cpu: "1"
memory: "4Gi"
nvidia.com/gpu: "1"
limits:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
canary:
predictor:
containers:
- name: predictor
image: ghcr.io/acme/gpu-model-server:v2
ports:
- containerPort: 8080
resources:
requests:
cpu: "1"
memory: "4Gi"
nvidia.com/gpu: "1"
limits:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
canaryTrafficPercent: 10
적용:
kubectl apply -f gpu-model-canary.yaml
kubectl -n ml-serving describe inferenceservice gpu-model
이 단계에서 중요한 포인트:
- v2 파드가 Ready 되기 전에는 트래픽을 늘리지 않습니다.
- GPU 파드가 Pending이면, 사실상 카나리 비율을 올려도 v2로 못 갑니다. 이때는 GPU 노드 여유/스케줄링을 먼저 해결해야 합니다.
3) Istio로 “헤더 기반 카나리”까지 확장하기
비율 카나리만으로는 “특정 고객/테스터 그룹”에만 새 모델을 붙이기 어렵습니다. Istio의 VirtualService를 사용하면 x-canary: true 헤더가 있는 요청만 v2로 보내는 식의 제어가 가능합니다.
먼저 KServe가 만든 서비스 이름을 확인합니다. 클러스터마다 다를 수 있으니 다음으로 확인하세요.
kubectl -n ml-serving get svc | grep gpu-model
예시로 서비스가 gpu-model-predictor-default 라고 가정하고, Istio DestinationRule과 VirtualService를 구성합니다. (실제 서브셋 라벨은 워크로드 라벨에 맞춰 조정해야 합니다.)
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: gpu-model-dr
namespace: ml-serving
spec:
host: gpu-model-predictor-default.ml-serving.svc.cluster.local
subsets:
- name: stable
labels:
serving.kserve.io/canary: "false"
- name: canary
labels:
serving.kserve.io/canary: "true"
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: gpu-model-vs
namespace: ml-serving
spec:
hosts:
- gpu-model-predictor-default.ml-serving.svc.cluster.local
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: gpu-model-predictor-default.ml-serving.svc.cluster.local
subset: canary
weight: 100
- route:
- destination:
host: gpu-model-predictor-default.ml-serving.svc.cluster.local
subset: stable
weight: 100
테스트 호출:
curl -H "x-canary: true" -H "Content-Type: application/json" \
-d '{"inputs":"hello"}' \
http://YOUR_INGRESS_HOST/v1/models/gpu-model:predict
이 방식은 “비율 카나리”와 달리, 관측 가능한 실험군을 명확히 분리할 수 있습니다. 예를 들어 QA/내부 트래픽만 새 모델에 붙여 품질을 먼저 확인하고, 이후 비율 카나리로 넘어가는 식의 단계적 운영이 가능합니다.
4) GPU 카나리에서 반드시 봐야 할 지표
카나리의 성공/실패 판단 기준을 사전에 합의해두는 것이 중요합니다. 추천 지표는 다음과 같습니다.
L7 지표(요청 관점)
p50/p95/p99 latency5xx rate및429 rate(큐잉/레이트리밋)- 타임아웃 비율
GPU/런타임 지표(리소스 관점)
- GPU 메모리 사용량, 메모리 단편화 패턴
- GPU utilization, SM occupancy(가능하면)
- 컨테이너 RSS, 페이지 캐시
모델 품질 지표(도메인 관점)
- 오프라인 샘플링 기반 정확도/점수
- 프롬프트/입력 분포 변화에 대한 강건성
실무 팁:
- v2에 트래픽 10%를 주더라도, 요청이 특정 시간대에 몰리면 GPU가 포화되어 지연이 급증할 수 있습니다. 비율을 올리기 전에 부하 테스트를 짧게라도 수행하세요.
- 카나리 파드 수가 1개라면, 노드/파드 단일 장애가 곧바로 지표를 흔듭니다. 최소 2개 이상(가능하면)으로 시작하세요.
5) 롤아웃 절차(권장 시나리오)
GPU 모델 카나리는 아래 순서가 운영 리스크가 낮습니다.
- v2 배포만 먼저(트래픽 0% 또는 헤더 라우팅만 허용)
- 워밍업: 내부적으로 더미 요청을 보내 CUDA 커널/캐시를 예열
- 헤더 기반으로 QA 트래픽만 흘려 품질/로그 확인
- 비율 카나리
1%→5%→10%→25%→50%→100% - 100% 전환 후에도 일정 시간 v1 리소스를 남겨 빠른 롤백 대비
KServe 비율 변경은 다음처럼 patch로도 자주 합니다.
kubectl -n ml-serving patch inferenceservice gpu-model \
--type merge \
-p '{"spec":{"canaryTrafficPercent":25}}'
6) 장애 대응: 카나리에서 자주 터지는 3가지와 처방
6.1 ImagePullBackOff로 카나리 파드가 안 뜬다
GPU 모델 이미지는 용량이 크고, 프라이빗 레지스트리를 쓰는 경우가 많아 ImagePullBackOff가 흔합니다. 이 상태에서 카나리 비율을 올리면 “올렸는데도 새 버전으로 안 가는” 이상한 상황이 됩니다.
- 노드의 이미지 캐시 상태
- IRSA/ECR 토큰
- 레지스트리 rate limit
을 빠르게 확인하세요. 필요하면 아래 글의 체크리스트가 그대로 도움이 됩니다.
6.2 파드가 CrashLoopBackOff인데 로그가 없다
모델 서버가 시작 직후 죽으면 로그가 남지 않거나, 사이드카/로깅 파이프라인 때문에 “로그가 안 보이는” 상황이 발생합니다. GPU 드라이버/라이브러리 mismatch, 엔트리포인트 오류, 모델 파일 경로 문제 등이 원인입니다.
이때는 이벤트/컨테이너 상태/종료 코드부터 확인하는 방식이 효과적입니다.
6.3 OOMKilled 또는 GPU OOM
- 컨테이너 메모리 OOMKilled:
resources.limits.memory초과 - GPU OOM: VRAM 부족으로 프레임워크가 실패
카나리에서는 다음을 즉시 수행합니다.
- 트래픽을
0%로 내리거나, Istio 헤더 라우팅만 남기고 일반 트래픽 차단 - 배치 크기/최대 토큰/동시성 제한을 낮춤
- 필요하면 정밀도 변경(예:
fp16,int8,4bit)과 KV 캐시 정책 재검토
7) 운영 팁: GPU 노드 스케줄링과 안정성
노드 선택과 격리
GPU 모델은 노드 특성이 중요합니다. 가능하면 다음을 적용하세요.
nodeSelector또는nodeAffinity로 GPU 타입 고정tolerations로 GPU 전용 노드에만 스케줄podAntiAffinity로 같은 노드에 카나리/스테이블을 분산(노드 장애 격리)
예시(컨테이너가 아니라 파드 스펙 수준이 필요한 경우가 많아, KServe의 podSpec 확장 필드를 사용하는 패턴을 검토):
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: gpu-model
namespace: ml-serving
spec:
predictor:
podSpec:
nodeSelector:
accelerator: nvidia
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: predictor
image: ghcr.io/acme/gpu-model-server:v1
resources:
requests:
nvidia.com/gpu: "1"
limits:
nvidia.com/gpu: "1"
클러스터/버전에 따라 podSpec 지원 방식이 다를 수 있으니, 사용 중인 KServe 버전 문서를 확인하세요.
스케일 전략
GPU는 비용이 크기 때문에 minReplicas=0 같은 공격적 설정을 하고 싶지만, 카나리에서는 콜드 스타트가 치명적일 수 있습니다.
- stable은
minReplicas를 1 이상으로 유지 - canary는 최소 1로 시작해 워밍업 후 트래픽을 붙임
- HPA/KPA의 스케일 업 속도와 메트릭 소스를 미리 점검
8) 롤백: “즉시 0%”가 가능한가
카나리의 가장 큰 장점은 롤백이 빠르다는 점입니다.
KServe 스플릿 기반 롤백
kubectl -n ml-serving patch inferenceservice gpu-model \
--type merge \
-p '{"spec":{"canaryTrafficPercent":0}}'
문제가 해결될 때까지 v2는 살아있되 트래픽만 끊고, 원인 분석을 진행할 수 있습니다.
Istio 헤더 라우팅 기반 롤백
VirtualService에서 canary match 룰을 제거하거나- canary subset weight를
0으로 변경
이 방식은 “특정 테스터만 계속 v2를 보게” 유지할 수도 있어, 재현과 디버깅에 유리합니다.
9) 체크리스트(배포 직전 5분 점검)
- v2 파드가
Ready상태인가 - GPU 노드에 여유가 있어 Pending이 발생하지 않는가
- v2 워밍업 요청을 수행했는가
- 타임아웃/리트라이 정책이 모델 특성에 맞는가(과도한 리트라이는 GPU를 더 죽임)
p95 latency,5xx rate, GPU 메모리 사용량 대시보드가 stable/canary로 분리되어 있는가- 롤백 방법(KServe
0%또는 Istio 라우팅 수정)이 문서화되어 있는가
마무리
KServe와 Istio를 조합하면 GPU 모델도 웹 서비스처럼 점진적으로 배포할 수 있지만, GPU 워크로드는 콜드 스타트/VRAM/OOM/스케줄링 지연 같은 변수가 커서 “카나리 절차” 자체가 곧 안정성의 핵심이 됩니다.
권장하는 접근은 헤더 기반(폐쇄 실험) → 소량 비율 카나리 → 단계적 확대 → 100% 전환 후 관찰 순서입니다. 그리고 실패했을 때는 과감하게 0% 롤백하고, 이미지 풀/크래시 루프/메모리 문제를 우선순위로 처리하세요.