Published on

KServe GPU 추론 배포 - Cold Start 3원인

Authors

KServe는 scale-to-zero와 요청 기반 오토스케일링을 쉽게 제공하지만, GPU 추론에서는 Cold Start가 곧바로 사용자 지연(latency)과 오류율(5xx)로 이어집니다. 특히 LLM·Diffusion·대형 비전 모델처럼 모델 파일이 크고 GPU 메모리 초기화 비용이 큰 워크로드는, “한 번 꺼졌다가 다시 켜질 때” 수십 초에서 수 분까지도 지연이 발생할 수 있습니다.

이 글에서는 KServe 기반 GPU 추론에서 Cold Start가 길어지는 3가지 핵심 원인을 (1) 스케줄링 지연, (2) 이미지/모델 로딩 지연, (3) 런타임 워밍업 지연으로 나눠 설명하고, 원인별로 바로 적용 가능한 완화책과 관측 포인트를 제시합니다.

또한 Cold Start가 길어지면 상위 레이어(예: Ingress/ALB)에서 타임아웃이 먼저 터지며 장애처럼 보일 수 있습니다. 이때는 네트워크·타임아웃도 함께 점검해야 하므로, 필요하면 AWS ALB 502/504 급증 - 타임아웃 7곳 점검도 같이 참고하세요.


Cold Start를 구성하는 시간 구간

GPU 추론의 Cold Start는 보통 아래 구간의 합으로 측정됩니다.

  1. 스케줄링 대기: GPU 노드가 없거나, 리소스가 부족하거나, 이미지 풀링 정책/노드 정책 때문에 Pod가 뜨지 못함
  2. 컨테이너 준비: 이미지 pull, 볼륨 mount, init container 실행
  3. 모델 준비: 모델 다운로드/언팩/캐시, 파일 시스템 I/O
  4. 런타임 초기화: CUDA 컨텍스트, 프레임워크 로딩, 그래프/커널 컴파일, 토큰나이저 로딩
  5. 첫 요청 처리: 첫 배치 처리에서 메모리 할당과 커널 JIT가 몰리며 p99가 폭증

KServe에서는 특히 scale-to-zero가 켜져 있을 때 1~4번이 “사용자 첫 요청”에 모두 붙습니다. 따라서 원인을 정확히 나눠서 줄여야 합니다.


원인 1) GPU 스케줄링 지연: 노드 준비와 Pod 배치가 늦다

대표 증상

  • 요청이 들어오면 Revision은 생성되는데 Pod가 한참 Pending
  • 이벤트에 0/.. nodes are available: Insufficient nvidia.com/gpu가 반복
  • Cluster Autoscaler가 GPU 노드를 띄우느라 수십 초~수 분 소요
  • GPU 노드는 있는데도 특정 노드풀 제약 때문에 배치 실패

체크 포인트

  1. kubectl describe pod에서 Events 확인
  2. 노드에 GPU 리소스가 광고되는지 확인: kubectl get nodes -o jsonpath=... (GPU 디바이스 플러그인)
  3. 노드풀/테인트/톨러레이션/어피니티로 인해 스케줄링이 막히지 않는지 확인
  4. GPU 노드 부팅 후에도 드라이버·디바이스 플러그인 준비가 느린지 확인

완화 전략

1) scale-to-zero를 완전히 끄지 말고, 최소 레플리카를 유지

KServe는 Knative 기반이면 minScale로 최소 인스턴스를 유지할 수 있습니다. GPU는 비용이 크지만, p99 지연이 중요한 서비스는 최소 1개를 상시 유지하는 편이 안정적입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-llm
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 3
    containers:
      - name: kserve-container
        image: myrepo/llm-server:latest
        resources:
          limits:
            nvidia.com/gpu: 1

minReplicas가 환경에 따라 동작 방식이 다를 수 있으니, Knative를 쓰는 경우에는 autoscaling.knative.dev/minScale 어노테이션도 함께 검토하세요.

2) GPU 노드풀에 워밍 노드(버퍼)를 둔다

Cluster Autoscaler로 GPU 노드를 0에서 1로 올리는 순간 자체가 Cold Start가 됩니다. 트래픽이 간헐적이라도 “아예 노드가 없는 상태”를 피하려면 GPU 노드풀에 1대 정도는 상시 유지하거나, 스케줄링 가능한 더 작은 GPU 인스턴스로 버퍼를 두는 방식이 효과적입니다.

3) Pod 우선순위와 선점(Preemption) 전략

공유 클러스터에서 배치 지연이 잦다면 PriorityClass를 적용해 중요한 추론 Pod가 먼저 스케줄되도록 만드세요.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: inference-high
value: 100000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
description: "High priority for GPU inference"

그리고 InferenceService의 Pod 템플릿에 priorityClassName을 연결합니다.


원인 2) 이미지 Pull + 모델 다운로드: 네트워크와 스토리지가 병목이다

GPU 추론에서 Cold Start는 “GPU가 느려서”가 아니라 “GPU가 일하기 전에 준비가 느려서” 길어지는 경우가 많습니다. 특히 컨테이너 이미지가 크고, 모델 아티팩트가 수 GB 단위면 네트워크·디스크가 병목이 됩니다.

대표 증상

  • Pod는 ContainerCreating 상태가 오래 지속
  • 이벤트에 Pulling image가 오래 걸리거나 Back-off pulling image가 발생
  • 모델 다운로드 단계에서 타임아웃 또는 재시도 반복
  • 노드 디스크 I/O가 100%에 가깝고, 압축 해제(untar)로 CPU가 튐

체크 포인트

  1. 이미지 크기와 레이어 구성(불필요한 CUDA/빌드 툴 포함 여부)
  2. 모델을 런타임에 매번 다운로드하는지, 캐시가 유지되는지
  3. 노드 로컬 디스크 vs 네트워크 파일 시스템(EFS/NFS 등) 사용 여부
  4. 사설 레지스트리 접근 지연(프록시, NAT, 방화벽)

EKS에서 특정 Pod만 외부로 나갈 때 403이 나거나, NAT/WAF 정책으로 모델 다운로드가 막히면 “Cold Start가 길다”가 아니라 사실상 “준비 실패”일 수 있습니다. 이런 경우는 EKS Pod만 외부 API 403 - NAT IP·WAF로 해결처럼 네트워크 경로를 먼저 정리해야 합니다.

완화 전략

1) 모델을 이미지에 bake-in 하거나, 사전 캐시(Preload)한다

  • 모델을 컨테이너 이미지에 포함하면 pull 시간이 늘 수 있지만, 런타임 다운로드 불확실성을 제거합니다.
  • 또는 노드 로컬 캐시를 쓰되, Pod 재시작에도 캐시가 유지되도록 hostPath 또는 로컬 PV를 검토합니다.

KServe에서 일반적인 패턴은 init container로 모델을 내려받아 공유 볼륨에 두고, 메인 컨테이너가 그 경로를 읽게 하는 것입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-sd
spec:
  predictor:
    volumes:
      - name: model-cache
        emptyDir: {}
    initContainers:
      - name: model-downloader
        image: alpine:3.19
        command: ["sh", "-c"]
        args:
          - |
            apk add --no-cache curl tar;
            curl -L "$MODEL_URL" -o /models/model.tar;
            tar -xf /models/model.tar -C /models;
        env:
          - name: MODEL_URL
            value: "https://my-model-store/model.tar"
        volumeMounts:
          - name: model-cache
            mountPath: /models
    containers:
      - name: kserve-container
        image: myrepo/sd-server:latest
        env:
          - name: MODEL_DIR
            value: /models
        volumeMounts:
          - name: model-cache
            mountPath: /models
        resources:
          limits:
            nvidia.com/gpu: 1

emptyDir는 Pod가 죽으면 캐시가 날아가므로, Cold Start를 더 줄이려면 노드 로컬 PV나 이미지 bake-in을 고려하세요.

2) 이미지 최적화: 레이어 다이어트 + 멀티스테이지 빌드

  • 빌드 툴체인, 캐시, 테스트 데이터 제거
  • CUDA 런타임만 포함한 베이스 이미지 사용
  • 모델 서버 바이너리/파이썬 의존성만 남기기

이미지 pull이 줄면 Cold Start가 선형에 가깝게 줄어듭니다.

3) 레지스트리/모델 스토어를 클러스터와 가깝게

  • 같은 리전에 레지스트리 배치
  • 프라이빗 링크/엔드포인트로 egress 병목 제거
  • 노드에 이미지 pre-pull 데몬셋을 두는 방식도 효과적

원인 3) 런타임 워밍업: CUDA 컨텍스트, JIT, 메모리 할당이 첫 요청에 몰린다

Pod가 Ready가 되었는데도 첫 요청이 유독 느리면, 대부분 런타임 워밍업 비용입니다.

대표 증상

  • 첫 요청만 p99가 수십 배 튐, 이후 안정
  • GPU 메모리 할당이 첫 요청에서 급증
  • TensorRT/torch compile, 커널 JIT, 토크나이저 로딩이 첫 요청에 수행
  • 모델이 크면 첫 로드에서 VRAM OOM이 나며 재시작 루프

VRAM이 빡빡한 모델은 워밍업 과정에서 일시적으로 메모리를 더 쓰기도 합니다. OOM이 동반된다면 Stable Diffusion VRAM 부족 OOM 해결 7단계처럼 메모리 최적화 체크리스트를 같이 적용하는 게 좋습니다.

체크 포인트

  1. 애플리케이션 로그에서 “모델 로딩 완료” 시점과 “서버 리슨 시작” 시점 분리
  2. nvidia-smi로 첫 요청 시점의 VRAM 변화를 관찰
  3. readiness probe가 “프로세스 살아있음”만 보는지, “모델 로딩 완료”까지 보는지 확인

완화 전략

1) 진짜 준비 상태를 readiness에 반영

서버 프로세스가 떠도 모델이 아직 로딩 중이면, 트래픽이 들어와 첫 요청이 타임아웃으로 터집니다. /ready 같은 엔드포인트를 만들어 “모델 로딩 완료 + GPU 준비 완료”를 반환하게 하세요.

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 2
  timeoutSeconds: 1
  failureThreshold: 30

failureThreshold를 늘려 초기 로딩을 허용하되, 로딩이 너무 길어지면 빠르게 실패로 전환해 원인 분석이 가능하게 만드는 것이 포인트입니다.

2) 워밍업 요청을 자동으로 쏴서 첫 요청 비용을 제거

가장 단순하고 효과적인 방법은 “서버가 뜨면 내부적으로 더미 추론을 1회 수행”하는 것입니다. 앱 코드에서 startup hook으로 실행하거나, 사이드카/포스트스타트 훅을 사용할 수 있습니다.

lifecycle:
  postStart:
    exec:
      command:
        - sh
        - -c
        - |
          # 서버가 뜰 때까지 대기 후 워밍업
          for i in $(seq 1 60); do
            wget -qO- http://127.0.0.1:8080/ready && break
            sleep 1
          done
          wget -qO- --post-data='{"text":"warmup"}' \
            --header='Content-Type: application/json' \
            http://127.0.0.1:8080/infer || true

주의할 점은 워밍업이 readiness 이전에 실행되면 실패할 수 있으니, 위처럼 /ready를 먼저 확인하는 흐름이 안전합니다.

3) 동적 컴파일/그래프 최적화를 빌드 타임 또는 배포 타임으로 이동

  • TensorRT 엔진 생성, torch.compile 결과 캐시, 커널 JIT 캐시를 미리 생성
  • 가능한 경우 컨테이너 이미지에 엔진/캐시를 포함

이 방식은 배포 아티팩트가 커지지만, “첫 요청 지연”을 크게 줄입니다.


실전 디버깅 절차: 10분 안에 원인 구간을 특정하기

Cold Start를 줄이려면 “어디서 시간이 쓰이는지”를 먼저 쪼개야 합니다.

  1. Pod Pending 시간 확인
    • kubectl get pod -w
    • kubectl describe pod 이벤트에서 FailedScheduling 여부 확인
  2. 이미지 pull 시간 확인
    • 이벤트에서 Pulling image부터 Pulled까지 시간
  3. 모델 다운로드/로드 로그에 타임스탬프 추가
    • download_start, download_done, load_start, load_done 같은 로그를 남겨 구간화
  4. 첫 요청 p99와 readiness 시점을 비교
    • readiness가 너무 빨리 열리면 첫 요청이 워밍업 비용을 뒤집어씀
  5. 상위 게이트웨이 타임아웃 확인
    • ALB/Ingress 타임아웃이 Cold Start보다 짧으면 502/504로 보임

Pod가 계속 재시작되면서 Cold Start가 반복되는 상황도 흔합니다. 이 경우 Cold Start 최적화보다 “재시작 원인 제거”가 먼저입니다. 크래시 루프 진단은 K8s CrashLoopBackOff 원인 12가지 10분 진단 체크리스트가 빠릅니다.


운영 팁: Cold Start를 ‘없애기’보다 ‘통제’하기

  • 비용 vs 지연 SLO를 먼저 정의: minReplicas=1은 가장 확실하지만 비용이 증가합니다.
  • 트래픽 패턴이 뚜렷하면 스케줄 기반 프리워밍: 업무 시간에만 최소 레플리카 유지
  • 모델/이미지/캐시를 가까이: 네트워크 왕복과 스토리지 I/O가 Cold Start의 숨은 1등 공범입니다.
  • readiness를 엄격히: “서버가 켜짐”이 아니라 “추론이 가능한 상태”를 기준으로 트래픽을 받으세요.

정리: KServe GPU Cold Start 3원인과 처방

  1. 스케줄링 지연: GPU 노드가 없거나 배치가 막힘
    • 처방: minReplicas 유지, GPU 노드 버퍼, 우선순위/어피니티 재설계
  2. 이미지 pull + 모델 다운로드 병목: 네트워크·스토리지·아티팩트 크기 문제
    • 처방: 모델 bake-in 또는 캐시, init container 프리로드, 이미지 다이어트, 레지스트리 근접화
  3. 런타임 워밍업 비용: CUDA/JIT/메모리 할당이 첫 요청에 집중
    • 처방: readiness 엄격화, 워밍업 요청 자동화, 컴파일/엔진 생성을 사전 수행

이 3가지를 분리해 측정하고, 가장 큰 구간부터 줄이면 KServe 기반 GPU 추론의 Cold Start는 체감적으로 크게 개선됩니다. 특히 “scale-to-zero를 유지하면서도” p99를 관리하려면, readiness 설계와 워밍업 자동화가 거의 필수에 가깝습니다.