Published on

KServe+Knative로 GPU 추론 자동스케일 구축

Authors

서빙 인프라에서 GPU는 비용이 크고, 트래픽은 들쭉날쭉합니다. 그래서 "필요할 때만 GPU 파드를 늘리고, 유휴 시에는 0까지 줄이는" 자동스케일이 매력적입니다. KServe는 모델 서빙을 표준화하고, Knative는 요청 기반 오토스케일(Scale-to-Zero 포함)을 제공합니다. 둘을 결합하면 GPU 추론 서비스의 운영 난이도를 낮추면서 비용 효율을 크게 올릴 수 있습니다.

이 글은 다음을 목표로 합니다.

  • KServe InferenceService를 Knative 기반으로 배포
  • GPU 노드에서만 동작하도록 스케줄링
  • 트래픽에 따라 자동 스케일(0 포함)
  • 콜드스타트와 지연, 타임아웃, 동시성 같은 현실적인 운영 포인트 정리

운영 중 지연이 튀는 원인을 더 깊게 파고들고 싶다면, 모델 서버 관점의 체크리스트로 Triton 배포 후 지연 폭증 8가지 원인도 같이 참고하면 좋습니다.

전체 아키텍처 한 장 요약

구성 요소를 역할 중심으로 정리하면 아래 흐름입니다.

  • KServe: InferenceService CRD로 모델/서버/스토리지를 선언적으로 관리
  • Knative Serving: 요청 기반 오토스케일, 리비전 관리, 트래픽 분배
  • Istio 또는 Kourier: Knative의 인그레스(클러스터 구성에 따라 다름)
  • GPU 노드 풀: NVIDIA 디바이스 플러그인으로 nvidia.com/gpu 리소스를 제공

핵심은 KServe가 내부적으로 Knative Service를 생성하고, Knative의 Autoscaler(KPA)가 동시 요청 수(concurrency) 등을 기준으로 파드를 늘렸다 줄였다 한다는 점입니다.

사전 준비 체크리스트

환경에 따라 설치 방식은 다르지만, 최소 조건은 다음입니다.

1) GPU 노드 및 디바이스 플러그인

  • GPU 노드에 NVIDIA 드라이버 설치
  • Kubernetes에 NVIDIA device plugin 설치
  • 파드에서 resources.limitsnvidia.com/gpu: 1 같은 형태로 요청 가능해야 함

확인은 아래로 합니다.

kubectl describe node | grep -A3 -i nvidia

노드 Allocatablenvidia.com/gpu 가 보여야 정상입니다.

2) Knative Serving

  • Knative Serving 설치
  • 네트워킹 레이어(예: Kourier, Istio) 준비
  • 외부에서 접근할 도메인(또는 로컬 테스트용 포트포워딩) 준비

3) KServe

  • KServe 설치
  • (선택) 모델 아티팩트를 둘 스토리지(S3, GCS, PVC 등)

왜 "GPU 추론"은 일반 웹 오토스케일과 다르게 봐야 하나

Knative는 기본적으로 요청 기반 스케일링에 강합니다. 하지만 GPU 추론은 다음 특성이 있어서 튜닝이 필요합니다.

  • 콜드스타트 비용이 큼: 컨테이너 시작 + 모델 로딩 + GPU 워밍업
  • 동시성 설계가 중요: 동시 요청을 늘리면 처리량은 오르지만 지연이 튈 수 있음
  • GPU는 공유 비용이 큼: 파드 하나가 GPU 1장을 물면, 다른 파드가 그 GPU를 못 씀
  • Scale-to-Zero의 양날의 검: 비용은 절감되지만 첫 요청 지연이 커짐

따라서 "무조건 0으로 줄이기" 보다는, 서비스 SLO에 따라 최소 파드 수(minScale) 를 0 또는 1 이상으로 결정하는 것이 실전적입니다.

KServe InferenceService로 GPU 추론 서비스 배포

여기서는 예시로 TorchServe 기반 예제를 들되, KServe의 predictor 컨테이너에 GPU 리소스 요청과 노드 선택을 넣는 패턴을 보여드리겠습니다.

아래 YAML은 핵심 옵션을 담은 예시입니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: gpu-infer
  namespace: model-serving
  annotations:
    autoscaling.knative.dev/minScale: "0"
    autoscaling.knative.dev/maxScale: "5"
    autoscaling.knative.dev/target: "1"
    autoscaling.knative.dev/metric: "concurrency"
    serving.knative.dev/timeoutSeconds: "300"
spec:
  predictor:
    containers:
      - name: kserve-container
        image: your-registry/your-gpu-model-server:latest
        ports:
          - containerPort: 8080
        resources:
          limits:
            nvidia.com/gpu: 1
            cpu: "2"
            memory: 8Gi
          requests:
            cpu: "1"
            memory: 4Gi
        env:
          - name: MODEL_NAME
            value: "gpu-infer"
    nodeSelector:
      nvidia.com/gpu.present: "true"
    tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"

주요 포인트 해설

  • autoscaling.knative.dev/minScale: 0이면 Scale-to-Zero 허용
  • autoscaling.knative.dev/target: 동시성 목표치. 1이면 파드당 동시 요청 1개를 목표로 스케일
  • autoscaling.knative.dev/metric: concurrency 기준 권장(추론은 CPU 사용률보다 요청 동시성이 직관적)
  • serving.knative.dev/timeoutSeconds: 추론이 길어질 수 있으니 타임아웃을 충분히 크게
  • nvidia.com/gpu: 1: GPU 1장 점유(멀티 GPU 파드도 가능하지만 운영 난이도 상승)
  • nodeSelectortolerations: GPU 노드로만 스케줄

nodeSelector 의 라벨 키는 클러스터마다 다릅니다. EKS라면 노드그룹 라벨을 쓰거나, GPU 노드에 커스텀 라벨을 붙여 관리하는 방식이 흔합니다.

kubectl label node <your-gpu-node> nvidia.com/gpu.present=true

위 명령의 <your-gpu-node> 같은 부등호 표기는 MDX에서 빌드 에러를 유발할 수 있으니, 실제 문서/위키에서는 인라인 코드로 감싸는 습관을 권장합니다.

오토스케일 튜닝: concurrency, maxScale, 그리고 현실적인 상한선

GPU 추론에서 오토스케일 설정은 단순히 "잘 늘어난다"가 아니라 GPU가 고갈되지 않게 하는 것이 핵심입니다.

1) target concurrency는 어떻게 잡나

  • 지연이 민감하고 배치가 없다면: target=1 또는 2
  • 모델이 가볍고 GPU 여유가 많다면: target=4 이상도 가능
  • 배치 추론(서버 내부 batching)이 있다면: target 을 너무 낮게 잡으면 오히려 효율이 떨어짐

권장 접근은 다음입니다.

  1. 단일 파드에서 동시 요청을 1, 2, 4...로 올려가며 p95 지연과 오류율을 측정
  2. p95가 급격히 꺾이는 지점 직전 값을 target 으로 설정

2) maxScale은 "GPU 수"와 함께 결정

maxScale 을 크게 잡아도, 실제로는 GPU 노드 수만큼만 뜰 수 있습니다. 하지만 문제는 스케줄링 대기 파드가 쌓이면서 큐잉 지연이 길어지는 것 입니다.

  • GPU가 4장뿐이면 maxScale=4 또는 약간 여유를 둔 값 권장
  • GPU 오토프로비저닝(Cluster Autoscaler, Karpenter)을 쓴다면 maxScale 을 더 크게 잡을 수 있음

3) Scale-to-Zero를 쓸 때의 첫 요청 보호

Scale-to-Zero는 비용을 줄이지만, 첫 요청이 "모델 로딩 시간"을 그대로 맞습니다. 완화 전략은 다음 중 하나입니다.

  • minScale=1 로 항상 1개는 유지
  • 주기적 워밍업 트래픽(크론잡, 외부 헬스체크)으로 0으로 내려가지 않게 유도
  • 모델 로딩을 빠르게: 이미지 경량화, 모델 파일 로컬 캐시, 초기화 코드 최적화

콜드스타트 줄이기: 이미지, 모델, 그리고 초기화 코드

GPU 추론의 콜드스타트는 대개 3단계입니다.

  1. 컨테이너 이미지 pull
  2. 모델 아티팩트 다운로드
  3. 프레임워크 초기화 + GPU 커널 워밍업

1) 이미지 pull 최적화

  • 베이스 이미지 최소화
  • 불필요한 패키지 제거
  • 레이어 캐시가 잘 먹도록 Dockerfile 정리
  • GPU 노드에 이미지 프리풀(daemonset로 미리 pull)

2) 모델 다운로드 최적화

  • 가능한 한 모델을 이미지에 포함(모델 업데이트가 잦으면 트레이드오프)
  • 오브젝트 스토리지 사용 시 같은 AZ/리전 유지
  • 모델 파일을 여러 조각으로 쪼개지 말고(너무 많은 small files) 단일 아카이브도 고려

3) 첫 요청에서만 느린 "워밍업" 제거

모델 서버 시작 시 더미 입력으로 1회 추론을 돌려 GPU 커널을 워밍업하면, 첫 실요청 지연을 줄일 수 있습니다.

예: 서버 엔트리포인트에서 워밍업 수행(의사 코드)

import torch

def warmup(model, device="cuda"):
    model.eval().to(device)
    x = torch.randn(1, 3, 224, 224, device=device)
    with torch.no_grad():
        for _ in range(3):
            _ = model(x)

네트워킹/타임아웃/로드밸런서 이슈

Knative 경로는 보통 "외부 LB -> Ingress -> Knative activator -> revision pod" 같은 홉을 탑니다. 추론이 길거나 응답이 크면 타임아웃/버퍼링 문제가 자주 나옵니다.

  • Knative timeoutSeconds 조정
  • Ingress(LB) 타임아웃 조정
  • 클라이언트 타임아웃 조정

특히 AWS 환경에서 ALB를 쓴다면 408이나 idle timeout 문제로 증상이 비슷하게 나타날 수 있습니다. 인그레스 타임아웃 트러블슈팅은 EKS에서 ALB Ingress 408 Request Timeout 해결 가이드가 도움이 됩니다.

관측과 디버깅: "왜 스케일이 안 늘지?"를 빨리 푸는 법

GPU 오토스케일 문제는 대개 아래 범주 중 하나입니다.

  • Knative가 스케일 결정을 못 함(메트릭 수집/설정 문제)
  • 파드는 늘리려는데 스케줄링이 안 됨(GPU 부족, taint/toleration 불일치)
  • 늘었는데도 요청이 실패(타임아웃, OOM, 모델 서버 오류)

1) InferenceService와 생성된 Knative 리소스 확인

kubectl get inferenceservice -n model-serving
kubectl describe inferenceservice gpu-infer -n model-serving

KServe가 만든 Knative Service/Revision을 봅니다.

kubectl get ksvc -n model-serving
kubectl get revision -n model-serving

2) 오토스케일러 상태 확인

kubectl describe ksvc gpu-infer -n model-serving

이벤트에 "스케일 업/다운" 관련 로그가 남습니다.

3) 스케줄링 실패 원인 확인

GPU가 부족하거나 taint/toleration이 안 맞으면 파드가 Pending에 머뭅니다.

kubectl get pod -n model-serving
kubectl describe pod <pod-name> -n model-serving

Events 에서 0/.. nodes are available 같은 메시지를 확인하세요.

운영 팁: 비용과 안정성 사이의 절충안

1) minScale은 서비스 성격에 따라 다르게

  • 내부 배치/비동기 작업: minScale=0 로 비용 최적화
  • 사용자-facing 실시간 API: minScale=1 이상 권장(콜드스타트 숨기기)

2) GPU 자원 단편화 방지

  • 가능하면 "파드당 GPU 1"로 단순화
  • 모델을 여러 개 띄우기보다, 트래픽이 낮은 모델은 멀티 모델 서버 또는 경량 모델로 통합을 검토

3) 지연이 튀면 "스케일"보다 먼저 볼 것

스케일이 늘어도 지연이 줄지 않는 경우가 많습니다.

  • 배치 설정(서버 내부 dynamic batching)
  • 토크나이저/전처리 CPU 병목
  • 모델이 GPU 메모리 스로틀링을 일으키는지

이런 체크리스트는 Triton 배포 후 지연 폭증 8가지 원인에서 더 체계적으로 다룹니다.

(선택) HPA 대신 Knative KPA를 쓰는 이유

GPU 워크로드에서 HPA를 쓰는 경우도 있지만, Knative의 강점은 다음입니다.

  • 요청 기반 스케일링(큐 길이, 동시성)에 더 직접적
  • Scale-to-Zero가 기본 철학
  • 리비전 단위 트래픽 분할로 카나리/롤백이 쉬움

반대로, GPU 사용률 기반으로 세밀하게 스케일하고 싶다면 Prometheus 어댑터로 커스텀 메트릭을 붙이는 접근도 가능합니다. 다만 운영 복잡도가 올라가므로, 먼저 KPA의 concurrency 기반으로 안정적인 기준선을 만든 뒤 확장하는 것을 권합니다.

마무리: 최소 구성으로 시작해, SLO 기반으로 조정하기

KServe+Knative 조합은 GPU 추론을 "선언적으로 배포"하고 "요청 기반으로 자동 스케일"하게 만들어줍니다. 하지만 GPU 추론은 콜드스타트, 동시성, 타임아웃, GPU 부족에 따른 스케줄링 지연 등 변수가 많아서, 아래 순서로 접근하는 것이 안전합니다.

  1. 단일 파드 성능(처리량, p95) 측정
  2. target concurrency 를 성능 꺾이는 지점 직전으로 설정
  3. GPU 수에 맞춰 maxScale 상한 설정
  4. 사용자-facing이면 minScale=1로 콜드스타트 숨기기
  5. 지연/오류가 나오면 스케일보다 먼저 모델 서버 병목을 의심

원하시면 사용 중인 환경(EKS인지, 온프렘인지), 인그레스(Kourier/Istio), 모델 서버(Triton/TorchServe/vLLM), 목표 트래픽과 SLO(p95 지연, QPS)를 알려주시면 그 조건에 맞는 InferenceService YAML과 오토스케일 파라미터를 더 구체적으로 제안해드릴 수 있습니다.