Published on

KServe로 GPU 모델 무중단 배포 - Canary+A/B

Authors

모델 서빙에서 “무중단 배포”는 단순히 파드가 내려가지 않는다는 뜻을 넘어섭니다. 특히 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”보다 운영 요소가 더 중요합니다.

  1. 모델 로딩 시간 반영: readiness probe가 모델 로딩 완료 후에만 Ready 되도록
  2. 워밍업 트래픽: 첫 요청 지연을 줄이기 위해 초기 워밍업 수행(가능하면 배포 파이프라인에서)
  3. 지연시간/에러율 메트릭: P50/P95/P99, 429/5xx, GPU 메모리 사용률, 큐 길이
  4. 동시성/배치 파라미터: 새 버전이 같은 GPU에서도 다른 메모리/레이트 특성을 가질 수 있음
  5. 롤백 비용: 새 버전이 OOM을 내면 노드 전체가 흔들릴 수 있으니 빠른 롤백 경로 확보

모델 컨테이너 이미지가 프라이빗 레지스트리에 있다면 배포가 ImagePullBackOff 로 멈추는 경우가 흔합니다. 사전 점검이 필요합니다.

기본: InferenceService로 GPU predictor 구성

아래는 예시로 Triton 기반 GPU predictor를 띄우는 InferenceService 입니다. 핵심은 resources.limitsnvidia.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.canaryspec.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 라우팅이 필요합니다.

구현 방법은 보통 두 가지입니다.

  1. KServe 네트워크 계층(Istio/Envoy)에서 헤더 기반 라우팅
  2. 클라이언트/게이트웨이에서 라우팅 (예: 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
  • 이후 predictorcanary 를 스왑하거나, 구버전 spec 제거

실패 시 롤백 전략: “빠르게, 비용 적게”

GPU 모델의 실패는 단순 5xx가 아니라 노드 OOM, 드라이버 리셋, 성능 급락으로 번질 수 있습니다.

  • 즉시 롤백: canaryTrafficPercent0 으로
  • A/B 차단: 헤더 기반 canary 라우트를 제거하거나 weight를 0으로
  • 원인 격리: canary 파드를 별도 노드풀(taint/toleration)로 격리하면 blast radius를 줄일 수 있음

만약 파드가 반복 재시작되는데 로그가 부족하면, 이벤트/상태 기반으로 먼저 원인을 좁히는 게 빠릅니다.

배포 자동화: 이미지 빌드와 캐시 최적화

GPU 서빙 이미지는 대개 크고 빌드 시간이 길어, 배포 빈도가 잦아지면 파이프라인이 병목이 됩니다. 멀티스테이지 빌드와 캐시를 제대로 잡으면 canary 실험 속도가 올라갑니다.

간단한 멀티스테이지 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 --from=builder /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-idmodel-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 비용까지 포함한 승인 게이트를 두는 방식으로 성숙도를 올릴 수 있습니다.