Published on

KServe+Knative로 GPU 모델 서버리스 배포

Authors

서버리스라는 단어를 GPU 추론에 붙이면 보통 두 가지 반응이 나옵니다.

  • "GPU는 비싸서 항상 띄워둬야 하는데 무슨 서버리스?"
  • "트래픽이 들쭉날쭉한데 GPU 파드를 상시 유지하기엔 비용이 너무 크다"

KServe와 Knative를 함께 쓰면 이 간극을 꽤 현실적으로 메울 수 있습니다. KServe는 모델 서빙에 필요한 추상화(모델 아티팩트 로딩, 표준 추론 엔드포인트, 트래픽 라우팅, 관측)를 제공하고, Knative는 요청 기반 오토스케일과 scale-to-zero를 담당합니다. 이 글은 "GPU 모델을 필요할 때만 켜고, 트래픽에 맞춰 늘렸다 줄이는" 배포 구성을 실전 관점에서 정리합니다.

아키텍처 한 장으로 이해하기

구성 요소 역할을 먼저 분리하면 이후 설정이 훨씬 쉬워집니다.

  • KServe InferenceService: 모델 서빙의 최상위 CRD. predictor(추론), transformer(전처리), explainer 등을 조합 가능
  • Knative Serving: Revision 단위로 배포/롤백/트래픽 분배, 요청 기반 오토스케일, scale-to-zero
  • Istio 또는 Kourier: 인그레스/라우팅 계층(클러스터에 따라 다름)
  • GPU 디바이스 플러그인: NVIDIA GPU 리소스 스케줄링
  • 모델 스토리지: S3, GCS, PVC 등. KServe storage-initializer가 컨테이너 시작 전에 모델을 가져옴

핵심 데이터 플로우는 다음과 같습니다.

  1. 외부 요청이 Knative Ingress로 들어옴
  2. Knative가 필요 시 Revision을 스케일업(0에서 1로 포함)
  3. KServe predictor 컨테이너가 뜨고, storage-initializer가 모델을 로컬로 다운로드
  4. 추론 요청 처리

언제 KServe+Knative 조합이 특히 유리한가

다음 조건이면 "GPU 서버리스"가 비용/운영 측면에서 이득이 큽니다.

  • 트래픽이 버스트성(낮엔 많고 밤엔 거의 없음)
  • 다수 모델을 운영하지만 동시에 핫한 모델은 일부
  • 배포/롤백이 잦고, 트래픽 분배(카나리)가 필요
  • 표준화된 엔드포인트(v1/models/...)와 관측(메트릭/로그)이 중요

반대로 다음은 주의가 필요합니다.

  • 콜드스타트가 치명적인 초저지연 서비스
  • 모델 로딩이 수십 GB이고 시작 시간이 길어 scale-to-zero가 오히려 UX를 망침
  • GPU가 매우 희소해 스케줄링 대기가 길어지는 환경

이 경우에도 완전한 scale-to-zero 대신 최소 파드 1개 유지, 혹은 "웜 풀" 전략으로 타협할 수 있습니다.

사전 준비: 필수 컴포넌트 체크리스트

1) NVIDIA 디바이스 플러그인

노드에 NVIDIA 드라이버가 설치되어 있고, Kubernetes에서 nvidia.com/gpu 리소스를 인식해야 합니다.

kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.allocatable.nvidia\.com/gpu}{"\n"}{end}'

출력이 비어 있거나 0이면 디바이스 플러그인/드라이버부터 점검해야 합니다.

2) KServe와 Knative 설치 형태

  • KServe는 내부적으로 Knative 모드를 사용할 수 있습니다(환경에 따라 RawDeployment도 가능).
  • 이 글은 "Knative 기반"을 전제로 합니다.

클러스터에 Knative Serving이 설치되어 있는지 확인합니다.

kubectl get pods -n knative-serving

3) 인그레스(네트워킹)

KServe+Knative는 보통 Istio 또는 Kourier를 씁니다. 이미 Istio를 쓰고 있다면 이후 트래픽 분배까지 자연스럽게 확장할 수 있습니다.

GPU 모델의 카나리 배포까지 확장하려면 아래 글도 함께 보면 연결이 잘 됩니다.

기본 배포: GPU를 쓰는 InferenceService 예제

아래는 KServe InferenceService로 GPU predictor를 띄우는 예시입니다. 런타임은 kserveServingRuntime 또는 ClusterServingRuntime에 따라 달라질 수 있는데, 여기서는 "커스텀 컨테이너" 방식이 가장 범용적이라 그 형태로 설명합니다.

중요 포인트는 다음입니다.

  • resources.limitsnvidia.com/gpu를 지정해야 GPU가 할당됨
  • Knative 오토스케일 파라미터(어노테이션)를 InferenceService에 함께 설정
  • 모델은 S3 같은 원격 스토리지에서 받아오도록 storageUri를 사용
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-llm
  namespace: ml
  annotations:
    autoscaling.knative.dev/class: kpa.autoscaling.knative.dev
    autoscaling.knative.dev/metric: concurrency
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/minScale: "0"
    autoscaling.knative.dev/maxScale: "5"
    autoscaling.knative.dev/scaleDownDelay: "60s"
spec:
  predictor:
    containers:
      - name: predictor
        image: ghcr.io/your-org/gpu-llm-server:0.1.0
        ports:
          - containerPort: 8080
        env:
          - name: MODEL_DIR
            value: /mnt/models
        resources:
          limits:
            cpu: "4"
            memory: 16Gi
            nvidia.com/gpu: "1"
          requests:
            cpu: "2"
            memory: 8Gi
    model:
      storageUri: s3://your-bucket/models/llm

적용:

kubectl apply -f inferenceservice.yaml
kubectl get inferenceservice -n ml

KServe는 준비가 되면 URL을 제공합니다. Knative를 쓰는 경우 내부적으로 ksvc 리비전이 생성됩니다.

kubectl get ksvc -n ml
kubectl get revisions -n ml

서버리스의 핵심: Knative 오토스케일 파라미터 해설

GPU 추론에서 오토스케일은 CPU 웹앱과 다르게 튜닝해야 합니다. 특히 concurrency 기반이 직관적입니다.

  • autoscaling.knative.dev/metric: concurrency
    • 한 파드가 동시에 처리할 요청 수를 기준으로 스케일
  • autoscaling.knative.dev/target: "1"
    • 동시성 1을 목표로 스케일(즉, 요청이 겹치면 파드를 늘리는 방향)
    • GPU 추론은 보통 배치/스트리밍 여부에 따라 1~수십까지 달라짐
  • minScale: "0"
    • 진짜 서버리스처럼 0으로 내림
  • scaleDownDelay: "60s"
    • 트래픽이 잠깐 끊겼다고 바로 0으로 내리면 콜드스타트가 잦아짐

실무 팁:

  • LLM 스트리밍 응답은 연결이 오래 유지되어 동시성 계산이 예상보다 크게 작동할 수 있습니다.
  • 이미지 생성(Stable Diffusion 계열)은 요청당 GPU 점유 시간이 길어 target을 낮추는 편이 안정적입니다.

SDXL/ControlNet 같은 "무거운" 워크로드는 동시성 튜닝이 특히 중요합니다. 모델 자체 최적화가 필요하면 아래 글도 참고가 됩니다.

콜드스타트 줄이기: 모델 로딩과 이미지 풀 전략

scale-to-zero의 대가가 콜드스타트입니다. GPU 모델은 콜드스타트가 보통 다음 합으로 결정됩니다.

  • 파드 스케줄링 대기(특히 GPU 노드 희소 시)
  • 컨테이너 이미지 pull 시간
  • 모델 다운로드 시간(storage-initializer)
  • 프레임워크 초기화 및 GPU 메모리 로딩

현장에서 효과가 큰 순서대로 개선책을 정리합니다.

1) 이미지 pull 최적화

  • 이미지 크기를 줄이고(불필요한 빌드 아티팩트 제거)
  • GPU 노드에 이미지 프리풀(daemonset) 적용

2) 모델 다운로드 최적화

  • 모델을 S3에 두면 네트워크/스토리지 성능이 곧 콜드스타트
  • 자주 쓰는 모델은 PVC에 캐싱하거나, 노드 로컬 캐시 전략 고려

Kubernetes에서 큰 매니페스트/큰 설정을 다루다 보면 API 서버 제한에 걸리는 경우가 있습니다. 모델 자체와 직접 관련은 없지만, 배포 자동화 중에 413을 맞는 경우가 있어 참고로 링크합니다.

3) minScale을 0이 아닌 1로

"완전 서버리스"를 포기하는 대신, 최소 1개 파드를 유지하면 체감 레이턴시가 급격히 좋아집니다.

metadata:
  annotations:
    autoscaling.knative.dev/minScale: "1"

비용은 늘지만, GPU 서비스를 제품화할 때는 이 타협이 흔합니다.

GPU 스케줄링 실전: 노드 셀렉터, 톨러레이션, MIG

GPU 노드는 보통 테인트로 보호합니다. 예를 들어 GPU 노드에 nvidia.com/gpu=true:NoSchedule 테인트가 있다면, 파드에 톨러레이션이 필요합니다.

spec:
  predictor:
    containers:
      - name: predictor
        resources:
          limits:
            nvidia.com/gpu: "1"
    tolerations:
      - key: nvidia.com/gpu
        operator: Equal
        value: "true"
        effect: NoSchedule
    nodeSelector:
      node.kubernetes.io/instance-type: "gpu"

추가로 MIG를 쓰는 환경이라면 nvidia.com/gpu 대신 MIG 리소스 이름이 노출될 수 있습니다. 이 경우 클러스터의 allocatable 리소스 키를 확인하고 그대로 limits에 맞춰야 합니다.

kubectl describe node your-gpu-node | sed -n '1,120p'

트래픽 유입: Ingress와 호출 방법

KServe는 기본적으로 V2 또는 V1 엔드포인트 형태를 제공합니다. 환경에 따라 호스트 기반 라우팅이 걸려 있을 수 있으니, 먼저 URL을 확인합니다.

kubectl get inferenceservice gpu-llm -n ml -o jsonpath='{.status.url}{"\n"}'

테스트 호출 예시(환경에 따라 Host 헤더 필요):

SERVICE_URL=$(kubectl get inferenceservice gpu-llm -n ml -o jsonpath='{.status.url}')
curl -s -X POST "$SERVICE_URL/v1/models/gpu-llm:predict" \
  -H 'Content-Type: application/json' \
  -d '{"instances":[{"prompt":"hello"}]}'

만약 외부에서 접근이 안 되면 다음을 순서대로 확인합니다.

  • Knative ingress gateway 서비스 타입(LoadBalancer인지)
  • DNS/도메인 매핑
  • 네임스페이스별 네트워크 정책
  • Istio VirtualService 생성 여부

관측과 디버깅: 어디에서 병목이 생겼는지 찾기

GPU 서버리스에서 장애/지연 원인은 대체로 아래 중 하나입니다.

  • 스케일업이 안 됨(요청이 들어와도 0에서 안 올라감)
  • 스케줄링이 안 됨(GPU 노드 부족)
  • 모델 다운로드가 느림(S3 대역폭/권한 문제)
  • 컨테이너가 뜨지만 readiness 실패(포트/헬스체크/의존성)

1) Knative 오토스케일 이벤트 확인

kubectl get pods -n knative-serving
kubectl logs -n knative-serving deploy/autoscaler | tail -n 200

2) 해당 Revision의 상태 확인

kubectl get revision -n ml
kubectl describe revision -n ml

3) 파드 이벤트에서 스케줄링 문제 확인

kubectl get pods -n ml
kubectl describe pod -n ml -l serving.kserve.io/inferenceservice=gpu-llm

Insufficient nvidia.com/gpu가 보이면 단순히 GPU가 부족한 것입니다. 이때는

  • maxScale을 현실적으로 낮추거나
  • GPU 노드를 늘리거나
  • 요청 큐잉/레이트리밋을 걸어야 합니다.

운영 팁: 비용, 안정성, 배포 전략

비용 최적화 체크리스트

  • minScale을 0으로 두되 scaleDownDelay를 길게(예: 300s) 잡아 "짧은 공백"에 덜 민감하게
  • 트래픽 패턴이 예측 가능하면 시간대별로 minScale을 자동 조정(크론잡으로 패치)
  • 모델을 여러 개 운영한다면 "핫 모델"만 minScale=1, 나머지는 0

안정성 체크리스트

  • readinessProbe는 "모델 로딩 완료"를 기준으로 설정
  • OOM 방지를 위해 GPU 메모리 상한/배치 크기 제한을 환경변수로 노출
  • 장시간 요청(이미지 생성 등)은 타임아웃/재시도 정책을 명확히

배포 전략

KServe는 버전 전환과 트래픽 분배에 강점이 있습니다. GPU 모델은 배포 후 성능 회귀가 자주 발생하므로, 카나리로 안전하게 굴리는 편이 좋습니다.

예제: 콜드스타트 완화용 워밍 엔드포인트(간단 패턴)

minScale=0을 유지하면서도 특정 시간대에 미리 깨우고 싶다면, 외부 크론에서 주기적으로 더미 요청을 보내는 방식이 가장 단순합니다.

SERVICE_URL=$(kubectl get inferenceservice gpu-llm -n ml -o jsonpath='{.status.url}')

curl -s -X POST "$SERVICE_URL/v1/models/gpu-llm:predict" \
  -H 'Content-Type: application/json' \
  -d '{"instances":[{"prompt":"warmup"}]}' > /dev/null

이 방식은 "진짜 서버리스" 철학과는 다르지만, GPU 콜드스타트가 제품 UX를 망치는 경우 현실적인 절충안이 됩니다.

마무리: KServe+Knative로 얻는 것과 남는 과제

KServe+Knative 조합은 GPU 모델 서빙을 "요청 기반 운영"으로 바꾸는 가장 실용적인 선택지 중 하나입니다. 표준화된 서빙 인터페이스 위에서 오토스케일과 scale-to-zero를 가져가며, 운영자는 모델 자체(성능/메모리/로딩)와 스케줄링(GPU 자원) 최적화에 집중할 수 있습니다.

다만 GPU는 CPU처럼 무한히 늘릴 수 없기 때문에, 결국 다음 과제가 남습니다.

  • 콜드스타트와 비용의 균형(특히 모델 로딩)
  • GPU 부족 시의 큐잉/레이트리밋/우선순위
  • 멀티모델 운영에서 캐시 전략과 스토리지 병목

원한다면 다음 후속 글 주제로도 확장할 수 있습니다.

  • scale-to-zero 환경에서 모델 캐시를 PVC로 유지하는 패턴
  • Knative 오토스케일 파라미터를 워크로드별로 튜닝하는 실측 방법
  • Istio 기반 카나리와 결합한 "성능 지표 기반 자동 롤백" 구성