- Published on
KServe로 GPU 모델 무중단 배포 - Canary+A/B
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
모델 서빙에서 “무중단 배포”는 단순히 파드가 내려가지 않는다는 뜻을 넘어섭니다. 특히 GPU 모델은 로딩 시간이 길고(수십 초~수분), 워밍업이 필요하며, 같은 모델이라도 토크나이저/커널/드라이버 조합에 따라 지연시간 분포가 크게 달라집니다. 그래서 배포 전략은 보통 다음 요구를 동시에 만족해야 합니다.
- 기존 트래픽을 끊지 않기: 새 버전이 준비되기 전까지는 100% 구버전 응답 유지
- 점진적 노출: 새 버전으로의 트래픽을 1%→5%→20%처럼 단계적으로 늘리기
- A/B 실험: 특정 사용자/테넌트/요청 헤더에만 새 모델을 고정 라우팅
- GPU 비용 최적화: 불필요한 동시 구동 시간을 줄이고, 실패 시 빠르게 회수
이 글은 KServe에서 Canary(가중치 기반 분할) 와 A/B(헤더 기반 고정 라우팅) 를 함께 사용해 GPU 모델을 무중단으로 배포하는 패턴을 다룹니다.
KServe에서의 무중단 배포 개념 정리
KServe의 핵심 객체는 InferenceService 입니다. 하나의 InferenceService 는 보통 다음을 포함합니다.
predictor: 실제 모델 서버(예: Triton, TorchServe, vLLM, custom container)canary: 새로운 리비전(또는 별도 predictor spec)을 붙여 트래픽을 일부만 보내는 기능- (설치 옵션에 따라) 네트워크 계층: Istio 또는 KServe Gateway/Knative 기반 라우팅
무중단을 위해 중요한 포인트는 “새 모델을 먼저 띄우고 준비가 끝난 뒤 트래픽을 보낸다”는 점입니다. 즉, 준비되지 않은 파드로 트래픽이 가는 순간 5xx가 발생합니다. GPU 모델은 readiness 조건을 엄격히 잡아야 합니다.
Canary vs A/B의 역할 분담
- Canary(가중치): 전체 트래픽 중 일부 비율만 새 버전으로 보내며 안전하게 확대
- A/B(헤더/쿠키/테넌트): 특정 집단을 새 버전으로 “고정”하여 실험/검증
현업에서는 둘을 같이 씁니다.
- Canary로 1%만 열어 전체적인 안정성을 확인
- 동시에 A/B로 내부 사용자나 특정 고객에게는 100% 새 모델을 고정 노출
아키텍처: GPU 모델 배포 시 체크리스트
GPU 모델 무중단 배포는 “배포 YAML”보다 운영 요소가 더 중요합니다.
- 모델 로딩 시간 반영: readiness probe가 모델 로딩 완료 후에만
Ready되도록 - 워밍업 트래픽: 첫 요청 지연을 줄이기 위해 초기 워밍업 수행(가능하면 배포 파이프라인에서)
- 지연시간/에러율 메트릭: P50/P95/P99, 429/5xx, GPU 메모리 사용률, 큐 길이
- 동시성/배치 파라미터: 새 버전이 같은 GPU에서도 다른 메모리/레이트 특성을 가질 수 있음
- 롤백 비용: 새 버전이 OOM을 내면 노드 전체가 흔들릴 수 있으니 빠른 롤백 경로 확보
모델 컨테이너 이미지가 프라이빗 레지스트리에 있다면 배포가 ImagePullBackOff 로 멈추는 경우가 흔합니다. 사전 점검이 필요합니다.
기본: InferenceService로 GPU predictor 구성
아래는 예시로 Triton 기반 GPU predictor를 띄우는 InferenceService 입니다. 핵심은 resources.limits 에 nvidia.com/gpu 를 지정하는 것과, 모델 스토리지(예: S3/MinIO, PVC)를 안정적으로 마운트하는 것입니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm-summarizer
namespace: ml-serving
spec:
predictor:
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.01-py3
args:
- tritonserver
- --model-repository=/models
ports:
- containerPort: 8000
name: http
resources:
limits:
nvidia.com/gpu: "1"
cpu: "4"
memory: "16Gi"
requests:
cpu: "2"
memory: "8Gi"
volumeMounts:
- name: model-repo
mountPath: /models
volumes:
- name: model-repo
persistentVolumeClaim:
claimName: llm-summarizer-model-pvc
readiness를 “모델 로딩 완료”로 잡기
GPU 모델은 컨테이너가 실행 중이어도 모델 로딩이 끝나지 않으면 요청을 받으면 안 됩니다. 가능하다면 다음 중 하나를 권장합니다.
- 모델 서버가 제공하는
/v2/health/ready같은 readiness 엔드포인트 사용 - custom container라면 모델 로딩 완료 후에만 readiness가
200을 반환하도록 구현
KServe 자체가 readiness를 대신 해결해주지는 않습니다. readiness가 느슨하면 canary가 의미가 없어집니다.
Canary: 가중치 기반 점진 배포
KServe는 spec.canary 와 spec.canaryTrafficPercent 로 canary를 구성할 수 있습니다(설치/버전에 따라 필드가 다를 수 있으니 클러스터의 CRD 스키마를 확인하세요).
아래 예시는 “기존 predictor는 유지하고, canary predictor를 새 이미지로 띄운 뒤, 트래픽 10%만 canary로” 보내는 형태입니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm-summarizer
namespace: ml-serving
spec:
predictor:
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.01-py3
args: ["tritonserver", "--model-repository=/models-v1"]
resources:
limits:
nvidia.com/gpu: "1"
canary:
containers:
- name: triton
image: nvcr.io/nvidia/tritonserver:24.02-py3
args: ["tritonserver", "--model-repository=/models-v2"]
resources:
limits:
nvidia.com/gpu: "1"
canaryTrafficPercent: 10
운영 팁: GPU canary는 “비율”보다 “동시성”이 더 위험하다
CPU 서비스는 10% 트래픽이 대체로 10% 자원 소모로 이어지지만, GPU 모델은 다릅니다.
- 새 버전이 더 큰 KV cache를 쓰면 10% 트래픽에서도 OOM 가능
- 배치/동시성 정책이 바뀌면 tail latency가 급격히 악화
따라서 canary 단계는 “트래픽 비율”과 함께 다음을 같이 제한하는 게 안전합니다.
- 모델 서버 동시성 제한(예: vLLM의
--max-num-seqs, Triton의 instance group) - 큐 길이 제한 및 429 반환 정책
- HPA/스케일 정책이 있다면 canary 쪽에만 먼저 더 보수적으로 적용
A/B: 헤더 기반 고정 라우팅(테넌트/사용자 단위)
가중치 canary만으로는 “특정 고객에게만 새 버전 제공”이 어렵습니다. 이때 A/B 라우팅이 필요합니다.
구현 방법은 보통 두 가지입니다.
- KServe 네트워크 계층(Istio/Envoy)에서 헤더 기반 라우팅
- 클라이언트/게이트웨이에서 라우팅 (예: API Gateway가
x-model-variant에 따라 다른 URL 호출)
여기서는 Istio VirtualService로 “헤더가 있으면 canary로 100% 고정”시키는 패턴을 예시로 듭니다.
주의: 아래 YAML에서 부등호 기호가 들어갈 만한 표현은 쓰지 않았고, 호스트/경로도 일반 문자열만 사용했습니다.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: llm-summarizer-ab
namespace: ml-serving
spec:
hosts:
- llm-summarizer.ml-serving.svc.cluster.local
http:
- match:
- headers:
x-model-variant:
exact: canary
route:
- destination:
host: llm-summarizer-canary.ml-serving.svc.cluster.local
weight: 100
- route:
- destination:
host: llm-summarizer-predictor.ml-serving.svc.cluster.local
weight: 90
- destination:
host: llm-summarizer-canary.ml-serving.svc.cluster.local
weight: 10
이 구성의 의미는 다음과 같습니다.
x-model-variant: canary헤더가 있으면 무조건 canary- 헤더가 없으면 90:10 가중치로 분산
클라이언트 호출 예시
curl -s \
-H "x-model-variant: canary" \
-H "Content-Type: application/json" \
http://llm-summarizer.ml-serving.svc.cluster.local/v1/models/summarize:predict \
-d '{"text":"..."}'
이 방식은 내부 QA, 특정 테넌트, 일부 사용자군을 대상으로 “강제 A/B”를 할 때 특히 유용합니다.
무중단 롤아웃 절차(실전 runbook)
GPU 모델 배포는 “YAML 적용”이 아니라 “관측 가능한 단계”로 운영해야 합니다.
1) canary 0%로 먼저 올리기
처음에는 트래픽을 보내지 말고 파드가 준비되는지부터 확인합니다.
canaryTrafficPercent: 0- canary 파드가
Ready상태가 되는지 - 모델 로딩 로그에서 에러가 없는지
- GPU 메모리 사용량이 예상 범위인지
2) 워밍업 요청 수행
첫 요청이 느린 모델이라면 canary 파드에 워밍업을 넣습니다.
- 토크나이저 캐시 로드
- 커널 JIT/컴파일
- 대표 입력 1~3개로 샘플 추론
워밍업은 배포 파이프라인에서 Job 으로 수행하거나, 내부 운영자가 헤더 기반으로 canary에만 몇 건 보내도 됩니다.
3) canary 1% → 5% → 20% 단계 확대
각 단계마다 아래 지표를 비교합니다.
- P95/P99 latency
- 5xx 비율, 429 비율
- GPU 메모리/SM utilization
- OOM kill 여부, 재시작 횟수
만약 새 버전에서 지연시간이 늘어나는 게 정상(정확도 개선으로 더 무거워짐)이라면, SLO 관점에서 허용 가능한지 먼저 합의해야 합니다.
4) A/B로 특정 고객 고정 노출
실험군을 고정해 품질/정확도를 검증합니다.
x-model-variant: canary를 테넌트 라우팅 규칙에 연결- 응답에
x-model-version같은 헤더를 추가해 추적 가능하게
5) 100% 전환 및 구버전 회수
canary를 100%로 올린 뒤 일정 시간 안정성을 확인하고, 구버전 리소스를 회수합니다.
canaryTrafficPercent: 100- 이후
predictor와canary를 스왑하거나, 구버전 spec 제거
실패 시 롤백 전략: “빠르게, 비용 적게”
GPU 모델의 실패는 단순 5xx가 아니라 노드 OOM, 드라이버 리셋, 성능 급락으로 번질 수 있습니다.
- 즉시 롤백:
canaryTrafficPercent를0으로 - A/B 차단: 헤더 기반 canary 라우트를 제거하거나 weight를 0으로
- 원인 격리: canary 파드를 별도 노드풀(taint/toleration)로 격리하면 blast radius를 줄일 수 있음
만약 파드가 반복 재시작되는데 로그가 부족하면, 이벤트/상태 기반으로 먼저 원인을 좁히는 게 빠릅니다.
배포 자동화: 이미지 빌드와 캐시 최적화
GPU 서빙 이미지는 대개 크고 빌드 시간이 길어, 배포 빈도가 잦아지면 파이프라인이 병목이 됩니다. 멀티스테이지 빌드와 캐시를 제대로 잡으면 canary 실험 속도가 올라갑니다.
모델 서버 바이너리/런타임과 앱 레이어 분리
의존성 레이어 캐시 유지
GitHub Actions에서
buildx캐시를 레지스트리에 푸시
간단한 멀티스테이지 Dockerfile 예시는 아래처럼 가져갈 수 있습니다.
FROM python:3.11-slim AS builder
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install poetry && poetry export -f requirements.txt -o requirements.txt
FROM nvidia/cuda:12.3.2-runtime-ubuntu22.04
WORKDIR /app
COPY /app/requirements.txt ./
RUN apt-get update && apt-get install -y python3-pip && rm -rf /var/lib/apt/lists/*
RUN pip3 install -r requirements.txt
COPY . .
CMD ["python3", "server.py"]
관측(Observability) 없이는 Canary가 성립하지 않는다
Canary는 “조금 보내보고 괜찮으면 늘린다”인데, 괜찮은지 판단할 근거가 없으면 결국 감으로 운영하게 됩니다. 최소한 다음은 갖추는 것을 권장합니다.
- 요청 단위 메트릭: latency histogram, status code, timeouts
- 모델 단위 메트릭: queue time, batch size, tokens per second
- GPU 메트릭: DCGM exporter 기반 utilization/memory
- 로그 상관관계:
request-id와model-revision을 함께 남기기
또한 GPU 노드에서 파일 디스크립터가 부족해 소켓/메트릭 수집이 흔들리는 경우도 있습니다. 트래픽이 늘어나는 canary 단계에서 갑자기 Too many open files 가 터지면, 모델 문제가 아니라 노드/런타임 설정 문제일 수 있습니다.
정리: Canary와 A/B를 함께 쓰는 이유
- Canary만 쓰면 “특정 집단 고정 노출”이 어렵고, 품질 검증이 느립니다.
- A/B만 쓰면 “전체 안정성”을 담보하기 어렵고, 실수로 노출 범위가 커질 수 있습니다.
KServe에서는 InferenceService 의 canary로 점진 배포를 만들고, Istio/게이트웨이 라우팅으로 헤더 기반 A/B를 얹어 “안전한 확장”과 “정확한 실험”을 동시에 달성하는 구성이 실전에서 가장 재현성이 좋습니다.
다음 단계로는 (1) canary 확대를 Argo Rollouts나 Flagger 같은 컨트롤러로 자동화하고, (2) 모델 품질 지표(정확도/환각률)를 온라인 메트릭으로 만들고, (3) GPU 비용까지 포함한 승인 게이트를 두는 방식으로 성숙도를 올릴 수 있습니다.