- Published on
vLLM+KServe로 LLM 서버리스 배포와 콜드스타트 최소화
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스로 LLM을 운영하면 트래픽이 없을 때 0으로 스케일 다운해 비용을 줄일 수 있습니다. 문제는 LLM 특성상 콜드스타트가 “컨테이너 기동” 수준이 아니라 GPU 할당 + 모델 다운로드 + 가중치 로드 + KV 캐시 워밍까지 포함된다는 점입니다. 이 글은 vLLM 런타임을 KServe 위에 올려 InferenceService로 운영하면서, 콜드스타트를 현실적으로 줄이는 방법을 단계별로 정리합니다.
아래 내용은 쿠버네티스(대개 EKS/GKE/AKS), GPU 노드, KServe(대개 Knative 기반) 환경을 전제로 합니다.
왜 vLLM + KServe 조합인가
vLLM이 콜드스타트에 유리한 이유
PagedAttention기반으로 KV 캐시 메모리 사용을 효율화해 동일 GPU에서 더 많은 동시 요청을 처리할 수 있습니다.- OpenAI 호환 API 서버를 제공해 애플리케이션 변경 비용이 낮습니다.
- 텐서 병렬, 연속 배치(continuous batching)로
짧은 요청이 몰리는워크로드에서 효율이 좋습니다.
다만 콜드스타트는 결국 모델 로딩 시간이 지배합니다. vLLM이 로딩을 “없애주진” 않지만, 운영 중 처리량을 올려 필요한 최소 replica 수를 줄이기 쉬워 비용 최적화에 유리합니다.
KServe가 서버리스 운영에 유리한 이유
InferenceService로 모델 서빙을 표준 리소스로 관리합니다.- 트래픽 기반 오토스케일(특히 Knative 연동)과
scale-to-zero가 가능합니다. - 라우팅, 카나리, 관측(메트릭) 붙이기가 수월합니다.
결론적으로 vLLM = 고효율 엔진, KServe = 운영/스케일링 프레임으로 역할이 분리됩니다.
콜드스타트가 발생하는 지점 분해하기
콜드스타트 시간을 줄이려면 먼저 어디서 시간이 쓰이는지 쪼개야 합니다.
Pod 스케줄링대기: GPU 노드 부족, 리소스 프래그먼테이션, 노드 오토스케일 지연이미지 pull: 수 GB 이미지, 레지스트리 병목모델 다운로드: Hugging Face, S3, PVC, 네트워크모델 로드: 가중치 메모리 매핑/로드,safetensors처리엔진 워밍업: 토크나이저 초기화, CUDA 커널 초기화, 첫 요청 JIT 성격의 지연
이 중 3~5가 LLM에서 특히 큽니다. 따라서 전략도 모델/이미지/노드에 걸쳐 다층으로 설계해야 합니다.
아키텍처 개요: 모델을 어디에 둘 것인가
모델을 매번 원격에서 내려받으면 콜드스타트는 거의 해결 불가입니다. 보통 다음 3가지 중 하나를 택합니다.
1) 이미지에 모델을 포함
- 장점: Pod 뜨면 바로 로드 가능
- 단점: 이미지가 너무 커짐, 빌드/배포 느림, 모델 업데이트가 이미지 배포가 됨
2) PVC에 모델을 캐시
- 장점: 모델 업데이트/롤백 쉬움, 이미지 슬림
- 단점: 최초 다운로드는 필요, 스토리지 성능/공유 방식에 따라 병목
3) 노드 로컬 디스크에 프리페치
- 장점: 가장 빠름(특히 NVMe), 네트워크 의존도 낮음
- 단점: 노드 교체/오토스케일 시 재프리페치 필요, 운영 복잡도 증가
현업에선 PVC 캐시 + 노드 프리페치를 섞는 경우가 많습니다. 즉, 기본은 PVC로 안정성을 확보하고, 트래픽이 큰 모델은 노드 로컬로 프리페치해 콜드스타트를 더 줄입니다.
KServe InferenceService로 vLLM 올리기
아래는 vLLM OpenAI 호환 서버를 KServe InferenceService로 띄우는 예시입니다. 핵심은 command/args로 vLLM 서버를 기동하고, 모델 경로를 PVC로 마운트하는 구성입니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllm-llama
spec:
predictor:
containers:
- name: vllm
image: vllm/vllm-openai:latest
command: ["python", "-m", "vllm.entrypoints.openai.api_server"]
args:
- "--model=/models/llama"
- "--host=0.0.0.0"
- "--port=8000"
- "--tensor-parallel-size=1"
- "--gpu-memory-utilization=0.90"
- "--max-model-len=4096"
ports:
- containerPort: 8000
resources:
limits:
nvidia.com/gpu: "1"
cpu: "4"
memory: "16Gi"
requests:
cpu: "2"
memory: "8Gi"
volumeMounts:
- name: model-pvc
mountPath: /models
volumes:
- name: model-pvc
persistentVolumeClaim:
claimName: llama-model-pvc
이 상태로도 동작은 하지만, 콜드스타트는 여전히 큽니다. 이제부터가 핵심입니다.
콜드스타트 줄이는 핵심 전략 8가지
1) 모델 파일 형식과 로딩 경로 최적화
- 가능하면
safetensors사용 - 모델을 여러 shard로 나누되, 너무 많은 파일로 쪼개면 스토리지 메타데이터 오버헤드가 커질 수 있음
- 토크나이저 파일도 함께 캐시
PVC를 쓸 경우, 스토리지 클래스 성능이 매우 중요합니다. 네트워크 스토리지가 느리면 모델 로드가 발목을 잡습니다.
2) 이미지 pull 시간을 줄이기
- 베이스 이미지 슬림화
- 레지스트리와 클러스터를 같은 리전에 두기
- 노드에서 이미지 프리풀(daemonset)로 웜업
이미지 프리풀은 “트래픽 0에서 1로” 갈 때도 도움이 됩니다. Pod가 스케줄되면 바로 실행 단계로 넘어가기 때문입니다.
3) 모델 다운로드를 initContainer로 분리
모델을 원격에서 내려받아 PVC에 채우는 방식이라면, 메인 컨테이너가 뜨기 전에 initContainer에서 다운로드를 끝내도록 설계합니다. 실패/재시도도 분리돼 운영이 편해집니다.
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: vllm-llama
spec:
predictor:
containers:
- name: vllm
image: vllm/vllm-openai:latest
command: ["python", "-m", "vllm.entrypoints.openai.api_server"]
args: ["--model=/models/llama", "--host=0.0.0.0", "--port=8000"]
volumeMounts:
- name: model-pvc
mountPath: /models
initContainers:
- name: model-sync
image: rclone/rclone:latest
command: ["sh", "-c"]
args:
- "rclone sync s3:my-bucket/llama /models/llama --checksum --transfers=8"
volumeMounts:
- name: model-pvc
mountPath: /models
volumes:
- name: model-pvc
persistentVolumeClaim:
claimName: llama-model-pvc
포인트는 메인 컨테이너가 모델 다운로드 때문에 느려 보이는 상황을 피하는 것입니다. init 단계에서 느린 게 더 관측/제어가 쉽습니다.
4) scale-to-zero를 쓰되, 완전한 0은 피하는 하이브리드
서버리스의 목표가 비용 절감이라도, LLM은 0으로 내리는 순간 사용자 경험이 크게 흔들립니다. 보통은 아래 중 하나를 선택합니다.
minReplicas=1로 완전 콜드스타트 제거(비용 증가)- 특정 시간대만
minReplicas=1유지(업무 시간) - 모델별로 등급을 나눠 핵심 모델만
minReplicas=1
KServe/Knative 설정은 환경마다 다르지만, 핵심은 “완전 0”을 서비스 레벨로 강제하지 않는 것입니다.
5) 워밍업 요청을 넣어 첫 토큰 지연을 줄이기
모델 로드가 끝나도 첫 요청에서 CUDA 커널 초기화 등으로 지연이 튈 수 있습니다. 이를 막기 위해 readiness 이후 워밍업 프롬프트를 1회 실행합니다.
간단히는 별도 Job이나 CronJob이 주기적으로 호출합니다.
curl -s http://vllm-llama.default.example.com/v1/completions \
-H 'Content-Type: application/json' \
-d '{
"model":"/models/llama",
"prompt":"warmup",
"max_tokens":1,
"temperature":0
}' > /dev/null
워밍업은 너무 자주 하면 비용이 되고, 너무 드물면 효과가 없습니다. 실무에선 scale-to-zero를 유지하는 서비스라면 N분에 1번 정도로 “완전한 0”을 막는 용도로 쓰기도 합니다.
스트리밍 응답을 쓰는 경우, 네트워크/타임아웃 이슈로 워밍업이 실패하면 오히려 불안정해질 수 있습니다. 스트리밍 타임아웃/재시도 설계는 별도 점검이 필요합니다. 관련해서는 OpenAI Responses API 스트리밍 끊김 타임아웃 완전 복구 가이드에서 “스트리밍을 신뢰성 있게 유지하는 패턴”을 참고하면 좋습니다.
6) GPU 노드 오토스케일과 스케줄링 지연 줄이기
콜드스타트의 절반이 GPU 노드가 없어서 기다리는 시간인 경우가 흔합니다.
- GPU 노드풀을
0까지 내리지 말고 최소 1대 유지 cluster-autoscaler또는 Karpenter 사용 시, GPU 인스턴스 타입을 제한해 스핀업 예측 가능하게 만들기pod priority로 LLM 서빙 Pod가 먼저 스케줄되게 하기
또한 GPU 노드에 image와 모델을 프리페치해두면, 노드가 살아있는 한 콜드스타트가 “Pod 기동 + 로드” 수준으로 줄어듭니다.
7) 동시성 기반 오토스케일 튜닝
Knative/KServe는 보통 동시 요청 수를 기준으로 스케일링합니다. LLM은 요청당 GPU 점유 시간이 길고, vLLM은 배치로 처리량을 올릴 수 있어 “동시성 목표치”가 매우 중요합니다.
- 동시성 목표가 너무 낮으면 replica가 과도하게 늘어 비용 급증
- 너무 높으면 큐잉이 길어져 p95 지연이 폭발
권장 접근:
단일 replica에서max throughput과p95를 측정- 목표 SLO에 맞춰
containerConcurrency또는 autoscaler 목표치를 역산 - 길게는 프롬프트 길이 분포까지 반영해 조정
8) 프로브(readiness/liveness)로 “준비된 서빙”만 트래픽 받기
LLM 서버는 “프로세스는 떴는데 모델 로딩 중”인 시간이 깁니다. readiness가 부정확하면 트래픽이 먼저 들어가서 타임아웃이 나고, 이것이 재시도 폭풍으로 이어집니다.
vLLM은 헬스 엔드포인트를 제공하므로 이를 활용합니다. 예시는 아래처럼 구성할 수 있습니다.
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 60
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
failureThreshold를 충분히 크게 잡아 “모델 로딩 중에 readiness 실패로 재시작 루프”에 빠지지 않게 하는 게 중요합니다. 이런 재시작 루프는 겉보기엔 콜드스타트가 아니라 장애처럼 보이며, 원인 파악이 어려워집니다. 유사한 케이스의 진단 관점은 systemd 서비스 자동 재시작 무한루프 진단 가이드도 참고할 만합니다.
운영 팁: 관측 지표를 콜드스타트 중심으로 잡기
콜드스타트 최적화는 “좋아진 것 같은데 아닌” 경우가 많습니다. 최소한 아래를 분리해서 메트릭으로 봐야 합니다.
Pod scheduled까지 걸린 시간Container started까지 걸린 시간Model loaded시점(애플리케이션 로그로 타임스탬프)First token latency와time to first byte
특히 first token latency는 사용자 체감에 직결됩니다. 서버 로그에 request_id 기준으로 enqueue time, prefill time, decode time을 남기면 원인 분해가 쉬워집니다.
예시: 콜드스타트 단축을 위한 현실적인 조합
트래픽이 들쭉날쭉한 챗봇 서비스를 예로 들면, 다음 조합이 비용/성능 균형이 좋았습니다.
- GPU 노드풀
min=1유지(완전한 GPU 0은 피함) - 모델은 PVC에 상주 + 노드 프리페치(핵심 모델만)
- KServe는
scale-to-zero허용하되, 핵심 서비스는minReplicas=1 - readiness를
/health기반으로 보수적으로 설정 - 워밍업 호출은
배포 직후 1회 + 장시간 무트래픽 시 N분 1회
이렇게 하면 “완전한 0”을 유지하면서도 사용자 첫 응답이 수십 초로 튀는 상황을 상당 부분 제거할 수 있습니다.
트러블슈팅 체크리스트
Pod는 떴는데 응답이 느리다
- 모델이 원격에서 매번 다운로드되는지 확인
- PVC 성능(특히 IOPS/throughput) 확인
- 모델 로드 로그에서 병목 구간 확인
scale-to-zero 후 첫 요청이 타임아웃 난다
- readiness가 너무 빨리
Ready가 되는지 확인 - 클라이언트 타임아웃을
콜드스타트 최악 시간이상으로 설정 - 재시도 정책이 “동시에 몰리는” 형태인지 확인(재시도 폭풍)
GPU가 있는데도 스케줄링이 늦다
- GPU 리소스 단편화(남은 GPU가 0.5 같은 형태로 쪼개져 있지 않은지)
- 노드 셀렉터/테인트/톨러레이션 불일치
- 오토스케일러가 GPU 인스턴스를 못 올리는 제한(쿼터, 서브넷 IP, 정책)
마무리
vLLM + KServe는 LLM을 “서버리스처럼” 운영하기 위한 좋은 조합이지만, 콜드스타트는 기본 설정만으로는 해결되지 않습니다. 핵심은 콜드스타트를 스케줄링, 이미지, 모델, 엔진 워밍업으로 분해하고, 각 단계에 맞는 캐시/프리페치/스케일 정책을 겹겹이 적용하는 것입니다.
한 줄로 요약하면, scale-to-zero 자체보다 중요한 건 “0에서 1로 올라올 때의 경로를 얼마나 짧고 예측 가능하게 만들었는가”입니다.