Published on

KServe로 GPU 추론 배포 - 콜드스타트 10배 줄이기

Authors

문제 정의: KServe GPU 추론의 콜드스타트는 왜 이렇게 느릴까

GPU 추론 서비스를 KServe로 올리면, 트래픽이 없을 때 scale-to-zero로 비용을 아끼는 대신 첫 요청이 매우 느려지는 문제가 자주 발생합니다. 현장에서 “콜드스타트 10배 단축”이 가능한 이유는, 콜드스타트가 단일 원인이 아니라 여러 단계의 합으로 구성되기 때문입니다.

콜드스타트를 단계별로 나누면 대략 아래처럼 쪼개집니다.

  1. 스케줄링 지연: GPU 노드가 없거나, 적절한 GPU 노드가 있어도 파드가 바로 배치되지 않음
  2. 이미지 풀링 시간: 수 GB 이미지 다운로드 및 압축 해제
  3. 컨테이너 부팅 및 런타임 초기화: Python import, CUDA 컨텍스트 생성, 모델 서버 프로세스 준비
  4. 모델 로딩 시간: 가중치 다운로드(또는 볼륨 마운트), 디시리얼라이즈, GPU 메모리 적재
  5. 첫 추론 워밍업: 커널 JIT, 텐서RT/컴파일 캐시, 프레임워크 그래프 최적화

즉, “콜드스타트 줄이기”는 KServe YAML 한두 줄이 아니라 노드 프로비저닝 + 이미지 전략 + 모델 저장/로딩 + 워밍업 + 오토스케일을 함께 다루는 문제입니다.

아래는 실제로 효과가 큰 순서대로 접근합니다.

1) 가장 큰 병목: GPU 노드 준비 시간을 먼저 줄인다

KServe가 아무리 빨라도 GPU 노드가 없으면 시작이 불가능합니다. 특히 클러스터 오토스케일(또는 Karpenter)을 쓰는 환경에서 GPU 노드 프로비저닝이 2~8분을 차지하는 경우가 흔합니다.

체크리스트

  • GPU 노드를 미리 1대 이상 유지할 수 있는가(비용 vs 지연 트레이드오프)
  • GPU 노드 그룹이 여러 AZ로 분산돼 있고, 이미지 풀/드라이버 준비가 느리진 않은가
  • 파드가 GPU 노드에 붙도록 nodeSelector/tolerations/affinity가 제대로 설정돼 있는가

KServe InferenceService에 GPU 리소스 명시

Kubernetes 스케줄러가 GPU 노드를 잡도록 리소스 요청을 명확히 합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llama2-gpu
spec:
  predictor:
    containers:
      - name: kserve-container
        image: ghcr.io/your-org/llama2-triton:1.0.0
        resources:
          requests:
            cpu: "2"
            memory: "8Gi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "4"
            memory: "16Gi"
            nvidia.com/gpu: "1"

여기서 중요한 포인트는 “GPU는 limits만”이 아니라 requests에도 동일하게 넣어 스케줄링을 확정하는 것입니다. (환경에 따라 다르지만, requests가 없으면 스케줄링이 애매해져 지연이 커질 수 있습니다.)

GPU 노드가 늦게 붙는다면

EKS에서 Karpenter를 쓴다면 NodeClaim이 NotReady로 오래 머무는 케이스가 대표적입니다. 이 경우는 KServe 문제가 아니라 노드 부팅, CNI, IAM, 드라이버 준비의 조합 문제일 가능성이 큽니다. 관련 진단 흐름은 EKS Karpenter NodeClaim NotReady 10분 진단이 도움이 됩니다.

2) 이미지 풀링 시간을 반으로: “큰 이미지”를 먼저 쪼갠다

GPU 추론 이미지는 대체로 크고(2~10GB), 콜드스타트의 상당 부분이 이미지 다운로드와 압축 해제에 쓰입니다.

실전 최적화 전략

  • 베이스 이미지 고정: CUDA, cuDNN, Python, 주요 라이브러리를 베이스 레이어에 고정해 캐시 효율을 높임
  • 모델 가중치 포함 금지: 이미지는 코드와 런타임까지만, 가중치는 별도 스토리지로
  • 멀티스테이지 빌드: 빌드 산출물만 런타임 이미지로 복사
  • BuildKit 캐시 최적화: 레이어 순서 최적화, --mount=type=cache 활용

이미지/CI 최적화는 추론 성능만큼이나 운영 성능에 직결됩니다. 빌드/캐시가 꼬이면 배포 주기마다 이미지 레이어가 바뀌어 노드 캐시를 못 타서 콜드스타트가 악화됩니다. 관련해서는 Docker 빌드 느림? BuildKit 캐시·레이어 최적화 12도 같이 보는 것을 권합니다.

예시 Dockerfile (멀티스테이지 + 캐시 친화)

아래 예시는 “런타임 레이어를 안정화”하는 데 목적이 있습니다.

# syntax=docker/dockerfile:1.6

FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 AS base
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    python3 python3-pip ca-certificates && \
    rm -rf /var/lib/apt/lists/*

FROM base AS deps
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip3 install -r requirements.txt

FROM base AS runtime
WORKDIR /app
COPY --from=deps /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
COPY . .
ENV PYTHONUNBUFFERED=1
CMD ["python3", "server.py"]

핵심은 requirements.txt를 먼저 복사해 의존성 레이어 캐시를 살리고, 코드 변경이 의존성 레이어를 깨지 않게 하는 것입니다.

3) 모델 로딩 병목 제거: “가중치 다운로드”를 요청 경로에서 빼기

콜드스타트에서 가장 치명적인 패턴은 컨테이너가 뜬 뒤에야 원격 스토리지에서 모델을 받는 구조입니다. 이 경우 첫 요청이 모델 다운로드를 기다리며 타임아웃이 나기도 합니다.

권장 패턴 3가지

  1. PVC에 모델 상주: 노드/파드 재시작에도 재사용
  2. InitContainer로 모델 프리페치: 본 컨테이너 시작 전에 다운로드 완료
  3. 노드 로컬 캐시(daemonset): GPU 노드마다 모델을 미리 받아 두기

KServe는 스토리지 이니셜라이저(Initializer)로 모델을 가져오는 패턴도 지원하지만, GPU 환경에서는 “어디에 캐시가 남는지”가 중요합니다. PVC 또는 노드 로컬 캐시로 재사용성을 확보하는 편이 콜드스타트 단축에 유리합니다.

InitContainer로 S3에서 모델 프리페치 예시

emptyDir 대신 PVC를 쓰면 파드 재시작에도 모델이 남습니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: triton-gpu
spec:
  predictor:
    volumes:
      - name: model-pvc
        persistentVolumeClaim:
          claimName: triton-model-pvc
    initContainers:
      - name: model-fetch
        image: amazon/aws-cli:2.15.0
        command: ["sh", "-c"]
        args:
          - |
            set -e
            aws s3 sync s3://your-bucket/models/triton-repo /mnt/models
        volumeMounts:
          - name: model-pvc
            mountPath: /mnt/models
    containers:
      - name: kserve-container
        image: nvcr.io/nvidia/tritonserver:24.01-py3
        args:
          - tritonserver
          - --model-repository=/mnt/models
        volumeMounts:
          - name: model-pvc
            mountPath: /mnt/models
        resources:
          requests:
            nvidia.com/gpu: "1"
            cpu: "2"
            memory: "8Gi"
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "16Gi"

이 구성의 장점은 “컨테이너가 Ready가 되기 전에 모델이 이미 존재”한다는 점입니다. 즉, 첫 요청이 모델 다운로드를 트리거하지 않습니다.

4) 워밍업으로 첫 요청 지연 제거: 커널 JIT와 그래프 캐시를 선반영

모델을 메모리에 올려도 첫 추론이 느린 경우가 있습니다. 원인은 대개 다음 중 하나입니다.

  • CUDA 커널 초기화 및 컨텍스트 생성
  • Torch compile, XLA, TensorRT 엔진 빌드
  • 메모리 풀/캐시 생성

가장 단순하고 강력한 방법: startupProbe로 “준비된 뒤에만” 트래픽 받기

KServe는 Knative 기반으로 트래픽을 라우팅하므로, 컨테이너가 Ready로 뜨는 순간부터 요청이 들어옵니다. 애플리케이션이 내부적으로 워밍업을 끝내기 전이라면 첫 요청이 느려집니다.

따라서 HTTP 서버 레벨에서 /health/ready 같은 엔드포인트를 만들고, 워밍업이 끝난 뒤에만 Ready를 true로 반환하도록 합니다.

Python 예시 (FastAPI)

아래 코드는 모델 로드 및 더미 추론까지 완료된 후에만 readiness가 통과됩니다.

import os
import time
import torch
from fastapi import FastAPI

app = FastAPI()
_ready = False
model = None

def load_and_warmup():
    global model, _ready
    device = "cuda" if torch.cuda.is_available() else "cpu"

    # 1) 모델 로드 (예시)
    model = torch.jit.load("/mnt/models/model.pt").to(device)
    model.eval()

    # 2) 워밍업 더미 추론
    x = torch.randn(1, 3, 224, 224, device=device)
    with torch.no_grad():
        for _ in range(3):
            _ = model(x)
    torch.cuda.synchronize()

    _ready = True

@app.on_event("startup")
def on_startup():
    load_and_warmup()

@app.get("/health/ready")
def ready():
    return {"ready": _ready}

@app.get("/health/live")
def live():
    return {"ok": True}

KServe에 프로브 연결

KServe InferenceService에서 컨테이너 프로브를 설정해 “워밍업 완료 전 트래픽 유입”을 차단합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: fastapi-gpu
spec:
  predictor:
    containers:
      - name: kserve-container
        image: ghcr.io/your-org/fastapi-gpu:1.0.0
        ports:
          - containerPort: 8080
        startupProbe:
          httpGet:
            path: /health/ready
            port: 8080
          failureThreshold: 120
          periodSeconds: 1
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          periodSeconds: 10
        resources:
          requests:
            nvidia.com/gpu: "1"
            cpu: "2"
            memory: "8Gi"
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "16Gi"

startupProbe는 “긴 초기화 시간”을 허용하는 데 핵심입니다. readiness만 쓰면 초기화 중에 실패로 간주되어 재시작 루프에 빠질 수 있습니다.

5) KServe 오토스케일 튜닝: scale-to-zero를 유지하면서도 빠르게

콜드스타트를 0으로 만들 수는 없지만, “0에서 1로 올라오는 시간”을 줄이거나, “0으로 내려가지 않게” 하는 구간을 설계할 수는 있습니다.

선택지 A: 최소 레플리카 유지

완전한 scale-to-zero를 포기하고, 최소 1개 레플리카를 유지하면 첫 요청 지연이 사실상 사라집니다. 비용은 증가하지만, SLA가 중요한 서비스에는 가장 확실합니다.

선택지 B: 시간대 기반 워밍(크론)

트래픽이 특정 시간대에 몰리면, 그 직전에 워밍 요청을 보내 파드를 미리 띄워두는 방식이 현실적입니다.

선택지 C: 동시성/타깃 튜닝

Knative의 동시성 설정이 지나치게 보수적이면 파드가 과도하게 늘고, 반대로 너무 공격적이면 큐잉 지연이 커집니다. 모델 특성에 맞게 “파드당 동시 요청 수”를 정해야 합니다.

6) “10배 단축”을 만드는 조합 레시피

현장에서 체감 10배까지 줄어드는 조합은 보통 아래 패턴입니다.

  1. GPU 노드 1대 상시 유지 또는 노드 프로비저닝 시간을 1분 내로 단축
  2. 이미지 크기 30~60% 감소 + 레이어 캐시 안정화
  3. 가중치 다운로드를 InitContainer 또는 PVC로 분리해 요청 경로에서 제거
  4. 워밍업 완료 전 트래픽 차단(startupProbe) + 더미 추론 워밍업

예를 들어 기존 콜드스타트가 300초였다면,

  • 노드 준비 180초 -> 30~60초
  • 이미지 풀 60초 -> 15~30초
  • 모델 다운로드 45초 -> 0초(이미 프리페치)
  • 첫 추론 워밍업 10초 -> 0~2초(부팅 중 수행)

처럼 누적 효과로 “첫 요청 대기”가 큰 폭으로 줄어듭니다.

7) 운영 팁: 관측 없이는 최적화도 없다

콜드스타트 최적화는 “추측”으로 하면 실패합니다. 최소한 아래 타임라인을 로그/메트릭으로 남기세요.

  • Pod Scheduled 시각
  • Image pulled 완료 시각
  • 앱 프로세스 시작 시각
  • 모델 로드 시작/완료 시각
  • 워밍업 시작/완료 시각
  • Ready 전환 시각

이걸 남기면, 병목이 노드인지 이미지인지 모델인지 한 번에 드러납니다.

8) 자주 하는 실수 5가지

  1. 모델을 이미지에 포함해서 배포할 때마다 이미지가 바뀌고, 풀링이 매번 발생
  2. readiness만 쓰고 startupProbe가 없어 초기화 중 재시작 루프
  3. GPU requests 누락으로 스케줄링이 불안정
  4. 워밍업을 “첫 요청에서” 하도록 만들어 사용자 요청이 워밍업 비용을 지불
  5. 노드 오토스케일 시간을 무시하고 KServe 설정만 만짐

결론

KServe로 GPU 추론을 배포할 때 콜드스타트는 단일 설정으로 해결되지 않습니다. 하지만 병목을 단계별로 분해해 노드 준비, 이미지, 모델 로딩, 워밍업, 프로브/오토스케일을 각각 최적화하면, 첫 요청 지연을 체감 10배까지 줄이는 것은 충분히 현실적입니다.

특히 “가중치를 요청 경로에서 제거(프리페치)”하고, “워밍업 완료 전 트래픽을 차단(startupProbe)”하는 두 가지는 비용 대비 효과가 매우 큽니다. 여기에 이미지 레이어 안정화와 GPU 노드 준비 시간을 더하면, 운영에서 가장 불만이 큰 콜드스타트를 눈에 띄게 줄일 수 있습니다.