IT/Kubernets

vLLM + KServe를 Karpenter GPU NodePool에 올린 첫 삽질 회고

gfrog 2026. 5. 10. 09:12
반응형

지난 3주 동안 사내 LLM 추론 서비스를 KServe + vLLM 조합으로 K8s에 올렸다. 결과만 말하면 "어찌어찌 굴러는 가는데, 처음 일주일은 거의 매일 야근"이었다. 글로 정리해두지 않으면 또 까먹을 것 같아서 적어둔다.

배경부터 짧게 풀자면, 우리 팀은 자체 호스팅 LLM 추론을 sagemaker나 bedrock 대신 EKS 위에 올리기로 했다. 비용도 비용이지만, 모델 빈번한 교체 + 사내 RAG 데이터와의 결합 때문에 직접 운영이 불가피했다. NVIDIA L40S 노드 4대로 시작했고, 모델은 처음에 Llama 3.1 8B, 그다음 70B로 키워가는 시나리오였다.

1. 첫 번째 벽 — 이미지 풀(Pull)에 12분

vLLM 공식 이미지(vllm/vllm-openai:latest)가 거의 9GB 가까이 된다. CUDA 런타임, PyTorch, 모델 로딩 의존성이 다 묶여 있어서 어쩔 수 없는데, 문제는 Karpenter가 GPU 노드를 막 띄우면서 매번 fresh pull을 하니까 첫 추론 요청이 들어왔을 때 노드가 Ready되고 컨테이너 시작까지 12분 가까이 걸렸다.

P99 응답 시간이 SLO를 한참 넘기는 수준이라 어디부터 손대야 할지 머리가 하얘졌다. 결국 두 가지로 나눠서 해결했다.

첫째, ECR pull-through cache를 켜고, vLLM 이미지를 사내 ECR에 미러링했다. 같은 리전의 ECR에서 받으니까 9GB가 약 3분 정도로 줄어들었다.

둘째, Karpenter NodePool에 minimum nodes를 1개 두고 warm pool 비슷하게 굴렸다. 비용이 좀 더 들지만 첫 요청 응답 SLO를 맞추려면 어쩔 수 없었다. NodePool spec에 disruption.budgets로 churn을 좀 막고, GPU 노드는 consolidation을 당장 끄는 쪽으로 결정했다.

# 핵심만
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-l40s
spec:
  template:
    spec:
      requirements:
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ["g6e.2xlarge", "g6e.4xlarge"]
      taints:
        - key: nvidia.com/gpu
          effect: NoSchedule
  disruption:
    consolidationPolicy: WhenEmpty   # WhenEmptyOrUnderutilized 아님
    consolidateAfter: 30m

WhenEmptyOrUnderutilized로 두면 우리 워크로드 패턴에선 노드가 자꾸 떴다 사라지는 이슈가 났다. 인퍼런스가 한산할 때 노드가 죽고, 다음 트래픽 와서 다시 띄울 때 또 12분 걸리는 것의 반복. 그래서 일단 WhenEmpty로 보수적으로 잡았다.

2. KServe + vLLM의 InferenceService — KEDA가 의외로 발목

KServe로 vLLM을 InferenceService로 올리는 건 문서대로 하면 비교적 쉽다. 우리는 KServe 0.14에 vLLM ServingRuntime을 정의해서 썼다. 근데 오토스케일링이 문제였다.

KServe 기본 오토스케일러는 Knative 기반인데, GPU 워크로드에는 잘 안 맞는다. RPS나 concurrency 기반으로 스케일하면 GPU가 한참 노는 상황에서도 replica를 늘리거나, 반대로 OOM이 코앞인데 안 늘리거나 한다. 그래서 KServe 어노테이션으로 외부 오토스케일러를 KEDA로 바꿨다.

metadata:
  annotations:
    autoscaling.knative.dev/class: external
    serving.kserve.io/autoscalerClass: external
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 6

그리고 KEDA ScaledObject로 vLLM이 노출하는 vllm:num_requests_running이랑 vllm:gpu_cache_usage_perc 메트릭(Prometheus)을 같이 봤다. KV cache 사용률이 80%를 넘으면 스케일업, 30분 idle이면 스케일다운으로. 이게 RPS 기반보다 훨씬 안정적이었다. vLLM은 continuous batching이라 RPS만 보면 의미를 잘못 해석하기 쉽다.

3. KV cache aware routing — Gateway API Inference Extension의 등장

여기까지 오니까 두 번째 벽이 보였다. replica가 여러 개일 때 같은 user의 후속 요청이 다른 pod로 가서, prefix cache를 못 쓰는 거다. RAG 컨텍스트가 긴 우리 워크로드에선 이게 TTFT(time to first token)에 직격타였다.

마침 Gateway API Inference Extension이 2026년 2월에 v1.3.1로 GA가 됐다. 모델 이름 기반 트래픽 분기, KV cache aware scheduling이 들어왔다. 우리는 거기에 더해 llm-d를 붙여봤는데, llm-d가 cross-runtime 라우팅과 KV cache 인지를 게이트웨이 레이어에서 해주니까 vLLM pod들이 prefix-cache hit을 잘 살릴 수 있었다.

수치만 보면, prefix-cache aware routing 켠 후 TTFT가 절반 정도로 줄었다. 외부 벤치마크에선 70B 모델에서 3배 처리량까지 나왔다는데, 우리 워크로드는 8B 기준으로는 그 정도까진 아니었다. 그래도 P95 TTFT가 2.4초에서 1.1초로 떨어진 건 체감이 컸다.

다만 솔직히 말하면 llm-d 도입은 아직 검증 중이다. 우리 환경에선 한 가지 운영상 이슈가 있었다. 게이트웨이 자체가 SPOF가 되니까, 거기 health check와 graceful drain 로직을 따로 더 신경써야 했다. 그리고 Inference Extension은 게이트웨이 컨트롤러 호환성을 미리 확인해야 한다(우리는 Envoy Gateway를 쓰는데, 일부 설정이 호환 안 되는 게 있었다).

4. 모델 캐시 — PVC vs initContainer

70B 모델로 가니까 또 새 문제. 모델 weight가 130GB가 넘어가니, pod 시작 때마다 S3에서 다운받게 했더니 5분~10분이 또 추가됐다.

처음엔 EFS로 PVC 만들어서 ReadOnlyMany로 공유하는 걸 시도했다. 동작은 하는데, EFS의 throughput이 모델 로딩 같은 burst read에 안 어울려서 첫 token 나오기 전에 GPU가 일하기는커녕 디스크 I/O를 기다리는 그림이 됐다.

결국 FSx for Lustre로 갈아탔다. 비용은 더 비싸지만, 모델 로딩 시간이 EFS 대비 1/4 정도. 그리고 노드 로컬 NVMe에 캐시하는 sidecar도 같이 붙여서 cold start 후 두 번째 pod부턴 노드 로컬에서 읽도록 했다. 사실 이 부분은 아직도 더 다듬을 여지가 있다.

마무리

여기까지가 첫 3주 회고다. SRE 관점에서 정리하면:

  • 이미지 + 모델 weight는 워밍 전략이 무조건 필요하다. Cold start 12분은 SLO 협상 자체를 어렵게 한다.
  • 오토스케일링은 RPS 말고 KV cache 사용률 같은 GPU 친화 메트릭으로.
  • KServe + vLLM 조합은 좋지만, 라우팅 레이어를 Gateway API Inference Extension(또는 llm-d) 쪽으로 일찍 넘기는 게 좋다. prefix cache 효과를 안 살리면 vLLM의 강점 절반이 죽는다.

아직 운영 한 달도 안 됐고, 70B 트래픽이 본격적으로 들어오는 건 다음 주부터다. 무슨 일이 또 터질지는 모르겠다. 다음 글에선 실제 본운영 한 달치 데이터로 비용/성능 트레이드오프를 정리해보려고 한다.

혹시 비슷한 환경 운영하시는 분 있으면, KEDA 트리거 메트릭 뭐 쓰시는지 댓글로 알려주시면 감사하겠다. 우리도 아직 베스트 조합 찾는 중이라.

반응형