- Published on
KServe·KFServing로 LLM 추론 API 배포와 오토스케일
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 레이어를 직접 만들지 않고도 LLM 추론 API를 빠르게 배포하려면, 쿠버네티스 위에서 KServe(구 KFServing) 조합이 가장 현실적인 선택지 중 하나입니다. KServe는 모델을 “서빙 리소스”로 선언하면 네트워크 라우팅, 롤아웃, 스케일링(특히 Knative 기반), 관측 지점까지 한 번에 엮어줍니다.
다만 LLM은 일반 ML 모델과 달리 GPU 자원, 긴 초기 로딩 시간, 동시성 제어, 스트리밍 응답, 큐잉/백프레셔 같은 운영 변수가 많습니다. 이 글에서는 KServe+KFServing 생태계 관점에서 LLM 추론 API를 배포하고 오토스케일하는 실전 흐름을 정리합니다.
KServe와 KFServing 관계부터 정리
- KFServing: Kubeflow 생태계에서 시작한 모델 서빙 프로젝트 이름입니다.
- KServe: KFServing이 CNCF 쪽으로 확장되면서 프로젝트가 리브랜딩된 이름이며, 현재는 KServe가 공식 명칭입니다.
실무에서는 문서/블로그/레거시 YAML에 kfserving 표현이 섞여 있어 “KServe+KFServing”로 함께 부르는 경우가 많습니다. 하지만 최신 설치와 CRD는 보통 KServe 기준으로 맞추는 게 안전합니다.
전체 아키텍처: LLM 추론 API가 흘러가는 길
대표적인 배포 토폴로지는 아래처럼 구성됩니다.
- Ingress Gateway(또는 Knative Ingress) → KServe
InferenceService InferenceService는 내부적으로 Predictor(모델 서버)와 선택적으로 Transformer(전처리/후처리)를 구성- 오토스케일은 보통 Knative Pod Autoscaler(KPA) 를 사용(또는 HPA)
- GPU 노드는 NodePool로 분리하고, 스케줄링은
nodeSelector/tolerations/affinity로 제어
LLM은 “모델 파일을 올려두면 끝”이 아니라, 서빙 런타임 선택이 핵심입니다.
- Hugging Face
transformers기반 서버(단순) - vLLM(고성능, continuous batching)
- TGI(Text Generation Inference)
- TensorRT-LLM(최대 성능, 난이도 높음)
KServe는 이런 런타임을 ServingRuntime/ClusterServingRuntime로 정의해 재사용할 수 있습니다.
설치 전 체크리스트: 여기서 삐끗하면 하루가 날아간다
1) GPU 노드와 디바이스 플러그인
- NVIDIA GPU 노드 준비
nvidia-device-plugin설치- 런타임 컨테이너가 CUDA 드라이버와 호환되는지 확인
2) Knative Serving
KServe의 서버리스 오토스케일(KPA)을 쓰려면 Knative Serving이 필요합니다. 클러스터에 이미 Istio/Contour/Kourier 중 어떤 네트워킹이 깔려 있는지도 영향을 줍니다.
3) 이미지 풀 권한(ECR, 프라이빗 레지스트리)
LLM 서빙 이미지는 크고, 프라이빗 레지스트리를 쓰는 경우가 많아 ImagePullBackOff가 자주 납니다. EKS라면 IRSA/ECR 권한을 먼저 점검하는 것이 시간을 절약합니다.
가장 단순한 배포: InferenceService로 “LLM 서버” 띄우기
아래 예시는 커스텀 컨테이너(예: vLLM OpenAI 호환 서버)를 Predictor로 띄우고, KServe가 라우팅/스케일링을 담당하게 하는 패턴입니다.
주의: 본문에서
<>문자가 그대로 나오면 MDX 빌드 에러가 날 수 있으므로, 모든 예시는 코드 블록 안에만 둡니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm-vllm
namespace: llm
spec:
predictor:
containers:
- name: vllm
image: ghcr.io/vllm-project/vllm-openai:latest
args:
- "--model"
- "/models/llama"
- "--host"
- "0.0.0.0"
- "--port"
- "8000"
- "--gpu-memory-utilization"
- "0.90"
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
cpu: "4"
memory: "16Gi"
requests:
nvidia.com/gpu: "1"
cpu: "2"
memory: "12Gi"
env:
- name: HF_HOME
value: "/cache/hf"
volumeMounts:
- name: model-vol
mountPath: /models
- name: cache-vol
mountPath: /cache
volumes:
- name: model-vol
persistentVolumeClaim:
claimName: llm-model-pvc
- name: cache-vol
emptyDir: {}
핵심은 다음입니다.
- KServe는 “모델 서버가 HTTP로 떠 있다”는 전제만 맞으면, 내부적으로 서비스/라우팅/스케일링을 붙여줍니다.
- 모델은 PVC로 마운트하거나, 오브젝트 스토리지에서 initContainer로 내려받는 방식도 흔합니다.
오토스케일: LLM에서는 “동시성”을 먼저 정의해야 한다
Knative 기반 오토스케일(KPA)은 기본적으로 동시 요청 수(Concurrency) 또는 RPS 같은 지표로 스케일합니다. LLM은 요청당 실행 시간이 길고, GPU 메모리와 KV-cache에 민감해서 “무작정 동시성 증가”가 장애로 이어질 수 있습니다.
1) 추천 접근: containerConcurrency를 보수적으로
LLM 서버가 자체적으로 continuous batching(vLLM 등)을 지원하더라도, 쿠버네티스 레벨에서는 다음을 먼저 정합니다.
- Pod 1개가 감당할 최대 동시 요청 수
- 타임아웃/큐잉 정책
KServe는 Knative Service를 생성하므로, podSpec 템플릿에 Knative 어노테이션을 붙여 튜닝합니다. (클러스터/버전에 따라 필드 위치가 달라질 수 있어, 실제 적용은 kubectl get ksvc -o yaml로 생성물을 확인하면서 맞추는 방식이 안전합니다.)
아래는 개념 예시입니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: llm-vllm
namespace: llm
annotations:
autoscaling.knative.dev/class: "kpa.autoscaling.knative.dev"
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/target: "2"
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "10"
spec:
predictor:
containerConcurrency: 2
containers:
- name: vllm
image: ghcr.io/vllm-project/vllm-openai:latest
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
memory: "16Gi"
target를 2로 잡으면 “Pod 하나당 동시 2개를 목표”로 스케일합니다.- LLM에서는 여기서 시작해, P95 지연과 OOM 여부를 보며 올리는 게 일반적입니다.
2) scale-to-zero는 비용을 줄이지만, 콜드스타트가 크다
Knative의 장점 중 하나가 scale-to-zero인데, LLM은 콜드스타트 비용이 큽니다.
- 컨테이너 이미지 pull(수 GB)
- 모델 로딩(수십 초~수 분)
- GPU 메모리 워밍업
따라서 프로덕션에서는 보통 다음 중 하나로 타협합니다.
minScale: 1로 항상 1개는 유지- 트래픽이 확실히 없는 배치성 서비스만 scale-to-zero 허용
3) GPU 오토스케일의 현실: “Pod는 늘어도 GPU 노드는 안 늘 수 있다”
KPA/HPA는 Pod 수만 늘립니다. GPU 노드는 별도로 Cluster Autoscaler(Karpenter 포함)가 늘려줘야 합니다.
- GPU NodeGroup에 오토스케일 설정
- Pod에 GPU 리소스 요청이 있어야 스케줄러가 GPU 노드 증설을 유도
- 스케줄링 제약(taint/toleration)이 맞지 않으면 Pending으로 멈춤
트래픽 라우팅: 카나리 배포와 모델 버전 운영
KServe는 모델 버전/롤아웃에 유리합니다. 가장 흔한 패턴은 “v1, v2를 동시에 띄우고 트래픽을 나눠” 검증하는 것입니다.
- 새 모델이 더 느려졌는지
- 프롬프트 템플릿 변경이 품질/토큰 사용량에 미치는 영향
- GPU 메모리 사용량 변화
KServe는 InferenceService 업데이트로 롤링이 가능하지만, LLM은 로딩 시간이 길어 롤링 중 용량 부족이 생기기 쉽습니다. 이때는 다음이 중요합니다.
maxScale을 잠깐 올려 여유 용량 확보minScale을 늘려 롤링 동안 다운타임 방지- 노드 증설이 따라오는지(Cluster Autoscaler 이벤트) 확인
관측: LLM 서빙은 지표가 “HTTP 200”만으로는 부족하다
최소한 아래를 수집해야 운영이 편해집니다.
- P50/P95/P99 latency
- 토큰 생성 속도(token/sec)
- 입력/출력 토큰 수 분포(비용 직결)
- GPU utilization, GPU memory, OOM 이벤트
- 큐 길이/대기 시간(서버 내부 큐 또는 프록시 레벨)
vLLM/TGI는 자체 메트릭을 Prometheus로 노출하는 경우가 많으니, KServe 리소스에 ServiceMonitor를 붙이거나 사이드카/스크레이프 설정을 맞추는 방식으로 통합합니다.
실전 장애 포인트 7가지
1) ImagePullBackOff로 시작도 못 하는 케이스
프라이빗 레지스트리 권한/토큰 만료가 가장 흔합니다. 특히 EKS에서 IRSA를 쓰면 “노드 IAM”과 “서비스어카운트 IAM”이 섞여 헷갈립니다.
2) 모델 로딩이 느려서 readiness 타임아웃
Knative/KServe 기본 프로브 타임아웃이 짧으면, 모델 로딩 중에 계속 재시작 루프를 탑니다.
- readiness/liveness probe의
initialDelaySeconds와timeoutSeconds조정 - 가능하면 “모델 다운로드”와 “서버 부팅”을 분리(initContainer)
3) 동시성 과대 설정으로 GPU OOM
LLM은 동시 요청이 늘면 KV-cache가 급증합니다.
containerConcurrency를 낮추고- 서버 내부 배칭 파라미터(vLLM의 경우 max_num_batched_tokens 등)를 함께 튜닝
4) 큐잉이 없어서 스파이크 때 전부 타임아웃
오토스케일은 즉시 반응하지 않습니다. 스파이크를 흡수할 버퍼가 필요합니다.
- 요청 큐(서버 내부/프록시)
minScale유지- 클라이언트 리트라이 정책(지수 백오프)
5) Pending이 계속되는 GPU 스케줄링 문제
nvidia.com/gpu요청이 있는데 GPU 노드가 없거나tolerations가 없어 taint에 막히거나- 노드 셀렉터가 잘못된 경우
6) 스트리밍 응답이 Ingress에서 끊기는 문제
LLM 채팅은 SSE/Chunked 응답이 많습니다.
- Ingress Controller의 proxy buffering, idle timeout 확인
- HTTP/2, keep-alive 설정 점검
7) 백엔드 DB 커넥션 폭주(대화 이력/로그 저장)
추론 API 자체는 GPU가 병목인데, 주변부(로그/세션/이력 저장)가 먼저 터지는 경우가 많습니다. 특히 요청이 길고 리트라이가 겹치면 DB 커넥션이 급증합니다.
로컬에서 호출 테스트: OpenAI 호환 엔드포인트 예시
vLLM OpenAI 호환 서버를 올렸다면, 보통 아래 형태로 테스트합니다.
export BASE_URL="http://llm-vllm.llm.example.com"
curl -sS "$BASE_URL/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"model": "llama",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "KServe로 LLM 오토스케일 핵심만 요약해줘"}
],
"temperature": 0.2,
"max_tokens": 200
}'
스트리밍(SSE)을 쓴다면 "stream": true를 켜고, Ingress 타임아웃/버퍼링 설정이 맞는지까지 같이 확인해야 합니다.
운영 팁: KServe를 LLM에 맞게 쓰는 현실적인 전략
- 처음엔 단순하게: Predictor에 “검증된 LLM 서버 이미지”를 붙이고, KServe는 라우팅/스케일링만 맡깁니다.
- 동시성은 낮게 시작:
containerConcurrency를 1~2부터 시작해, 토큰 길이 분포를 보고 올립니다. - minScale로 콜드스타트 제거: 비용보다 안정성이 우선인 API라면
minScale: 1이 기본값이 되는 경우가 많습니다. - GPU 노드 오토스케일을 별도로 검증: KPA가 Pod를 늘려도 노드가 안 늘면 의미가 없습니다.
- 관측 지표를 토큰 중심으로 설계: RPS보다 “토큰/초, 토큰/요청”이 비용과 성능을 더 잘 설명합니다.
마무리
KServe(KFServing)는 “모델을 쿠버네티스에서 제품처럼 운영”하기 위한 좋은 추상화입니다. 특히 Knative 기반 오토스케일과 선언형 배포는 LLM처럼 트래픽 변동이 큰 워크로드에 잘 맞습니다.
다만 LLM은 GPU와 메모리, 동시성, 콜드스타트가 얽혀 있어서 오토스케일을 켠다고 자동으로 비용과 지연이 최적화되지는 않습니다. containerConcurrency, minScale, GPU 노드 오토스케일, 그리고 토큰 기반 관측 지표까지 한 세트로 잡으면 “추론 API를 운영 가능한 서비스”로 만들 수 있습니다.