EKS에서 vLLM 콜드 스타트 428초를 226초로: 구간을 나눠 하나씩 걷어내기¶
- 출처: Amazon EKS에서 vLLM으로 Gemma 4 서빙 최적화하기 (1부: 콜드 스타트 최적화)
- 저자: WooHyoung Choi
- 발행: 2026-09-10, AWS 기술 블로그
- 구성: 2부작 중 1부. 2부는 Ready 이후의 처리량과 지연 SLO(양자화 정밀도, speculative decoding, prefix caching, 스케줄러 설정)
- 환경: ap-northeast-2, EKS 1.36, Karpenter v1.14, g7e.2xlarge Spot (RTX PRO 6000 Blackwell 96GB, GPU 1개), vLLM v0.26.0
- 모델: Gemma-4-31B-IT NVFP4, 로드 시 31.22GiB
- 읽은 날짜: 2026-09-10
- 태그: #vLLM #EKS #Kubernetes #ColdStart #Karpenter #Spot #SleepMode #GPU서빙
한 줄 요약¶
"시작에 7분 걸린다"를 네 구간으로 쪼개 각 구간의 원인을 따로 잡은 기록. 콜드 428초에서 226초, 웜 재시작 314초에서 133초, 유휴 상태 복귀는 314초에서 0.97초가 됐다.
방법론이 이 글의 핵심이다¶
수치보다 먼저 볼 것이 측정 설계다. 저자가 첫 문장에서 못을 박는다. "시작까지 7분 걸린다와 같은 단순 시간 측정으로는 무엇을 개선할지를 정할 수 없다."
지킨 원칙이 셋이다.
- 구간을 먼저 나눈다. 노드 프로비저닝, 이미지 pull, 가중치 로드, 엔진 초기화 네 단계로 쪼개고 각각 측정 근거를 붙였다. Kubernetes 구간은 파드 conditions와 events로, vLLM 구간은 로그 마커(
Loading weights took,init engine took)로 잡는다. - 한 번에 하나만 바꾼다. Step 0부터 Step 5까지 나누고 각 Step은 직전과 정확히 기술 하나만 다르다. Deployment 이름을 같게 두고 Recreate 전략을 써서 Step 전환이
kubectl apply한 번이다. - 캐시는 적재 회차와 히트 회차를 나눈다. 캐시를 채우는 회차는 효과가 없는 것이 정상이므로 개선 수치는 항상 히트 회차 기준이다.
마지막 문장이 글 전체의 태도를 요약한다. "분석과 시험이 없이 넣은 기술은 서빙 최적화 효과를 주장할 수 없습니다."
기준 구성 428초의 분해¶
| 구간 | 시간 | 측정 근거 |
|---|---|---|
| 1단계 노드 프로비저닝 | 30초 | Karpenter NodeClaim 생성에서 노드 Ready까지 |
| 2단계 이미지 pull | 65.1초 | kubelet Pulled 이벤트, 8.9GB |
| 3단계 가중치 로드 (S3에서 GPU로) | 127.6초 | vLLM 로그 Loading weights took |
| 4단계 엔진 초기화 | 93.9초 | vLLM 로그 init engine took (torch.compile 50.8초 포함) |
| 기타 | 약 111초 | 전체에서 위 네 단계를 뺀 나머지 |
| 합계 | 428초 | 파드 conditions |
가중치 로드와 엔진 초기화가 합쳐 221.5초로 절반이 넘는다. 그리고 이 둘은 같은 노드에서 파드만 다시 만들어도 사라지지 않는다.
"기타 111초"의 내역도 밝혀뒀다. 가중치 로드 전 vLLM 프로세스 초기화 약 39초, 엔진 초기화 뒤 API 서버 시작 약 46초, 파드 스케줄링 대기 약 15초, 컨테이너 시작 전 준비 약 4초, probe 주기 오차 약 7초다.
웜 재시작이 안 줄어드는 이유¶
같은 노드에서 파드만 다시 만든 웜 재시작이 314초다. 콜드와의 차이 114초는 노드 프로비저닝과 스케줄링 45초에 이미지 pull 65초를 더한 값과 거의 같다. 가중치 127.8초와 컴파일 51.0초는 그대로 반복된다.
원인이 둘이다.
- Mountpoint for Amazon S3는 FUSE 클라이언트다. vLLM은 마운트를 로컬 파일로 인식하므로 시작할 때마다 가중치를 S3에서 파일 읽기 방식으로 다시 읽는다.
- torch.compile 캐시가 컨테이너 안
/root/.cache에 있다. 파드가 죽으면 같이 사라진다. 기본 구성에서는 재시작이 곧 재컴파일이다.
권고 기술 네 가지¶
1. 스트리밍 로더로 S3 직결¶
가장 배울 게 많은 대목이다. 가중치 로드 127.6초는 대역폭 부족이 아니었다. 33.5GB를 127.6초에 읽으면 실사용 대역폭이 약 2.1Gbps인데, 노드의 네트워크 대역폭은 50Gbps다.
병목은 FUSE 경로 자체다. vLLM의 순차 파일 읽기가 그대로 S3 요청으로 옮겨가 단일 스트림에 가깝게 동작하고, 요청마다 커널과 사용자 공간을 오가는 비용이 붙는다.
vLLM에 들어 있는 Run:ai Model Streamer(--load-format=runai_streamer)는 s3:// 경로를 직접 받아 병렬 range read로 읽는다. 같은 버킷, 같은 가중치에서 127.6초가 19.8초로 6.5배 빨라졌고 실사용 대역폭은 약 13.6Gbps가 됐다.
이 Step에서 초기화 94.4초는 그대로라 로더 효과만 분리해 확인된다. 측정 설계가 잘 작동한 예다.
2와 3. 컴파일 캐시를 살려두기¶
같은 노드에서는 hostPath로 영속화하고, 새 노드에서는 S3에 게시해둔 캐시를 initContainer로 미리 내려받는다. 캐시 크기는 218MB다.
엔진 초기화가 캐시 히트 시 94.0초에서 29.6초로 약 68% 줄었다.
Spot 인스턴스 전략과 잘 맞는 구성이다. 노드가 회수돼도 같은 GPU 아키텍처면 컴파일 비용을 다시 내지 않는다.
4. sleep/wake¶
요청이 없을 때 replicas를 0으로 내리면 복귀가 콜드 경로라 226초가 든다. vLLM sleep mode는 프로세스와 컴파일 상태를 유지한 채 GPU 메모리만 반납한다. level 1은 가중치를 CPU RAM에 보존하고 KV 캐시만 버린다.
| 동작 | 시간 |
|---|---|
POST /sleep?level=1 |
12.8초 |
POST /wake_up |
0.97초 |
| 참고: 웜 재시작 | 133초 |
wake 뒤에도 컴파일 캐시가 유지돼 추론 지연이 sleep 전과 같다(0.28초에서 0.30초).
결과¶
| 시나리오 | 기준 | 최적화 후 | 개선 |
|---|---|---|---|
| 콜드 (노드 프로비저닝부터) | 428초 | 226초 (캐시 히트, 이미지 사전 pull 포함) | -47% |
| 웜 재시작 (같은 노드) | 314초 | 133초 | -58% |
| 유휴 상태에서 서빙 복귀 | 314초 (재시작) | 0.97초 (wake) | 약 320배 |
조건부와 제외로 분류한 것들¶
권고와 별개로 검토만 한 기술을 따로 표시한 점이 좋다.
조건부 1. 이미지 사전 pull은 절반만 먹힌다. userData로 노드 부팅 직후 백그라운드 pull을 걸면 64~65초가 32.2초와 39.8초로 줄었다. 그런데 pull 0초를 뜻하는 already present 이벤트는 두 번 다 나오지 않았다. Karpenter가 노드 Ready 즉시 파드를 스케줄해 userData의 pull과 kubelet의 pull이 같은 이미지를 놓고 경합하기 때문이다. containerd가 레이어를 공유해 절반쯤만 절감된다. pull 0초는 노드가 워크로드보다 먼저 존재하는 패턴에서만 가능하다.
AL2023 기준이고 Bottlerocket은 셸 userData를 지원하지 않아 이 방식을 못 쓴다. userData의 이미지 태그와 Deployment 태그가 어긋나면 사전 pull이 무효가 되어 65초로 되돌아간다.
조건부 2. 노드 보존과 warm pool은 수요 데이터로 정한다. 판단 공식을 제시한다. 히트율에 지연 절감(226초에서 133초를 뺀 93초)을 곱해 SLO 페널티로 환산한 금액이 노드 보존 비용을 넘는 지점을 찾으라는 것이다. 단일 GPU 안에서는 sleep/wake가 더 싸므로 warm pool은 다중 레플리카 스케일아웃 대비일 때 성립한다.
제외 세 가지는 시험 없이 근거를 대고 뺐다.
| 항목 | 제외 근거 |
|---|---|
| SOCI lazy loading | LLM 서빙은 시작 시 대부분의 레이어를 실제로 읽어 lazy 효과가 제한적이다. pull 비용이 가중치 로드 구간으로 이연돼 겉으로만 빨라질 위험이 있다 |
| 가중치 EBS pre-bake | g7e.2xlarge의 EBS 대역폭 최대 5Gbps가 스트리밍 로더의 13.6Gbps보다 낮아 오히려 느려질 수 있다. S3 단일 진실 원천의 이점도 잃는다 |
| 컴파일 캐시 EBS pre-bake | S3 사전 로드(218MB)가 같은 목표를 훨씬 가볍게 달성한다 |
각각 재검토 조건까지 붙여뒀다. "지금은 안 한다"와 "언제 다시 본다"를 함께 적은 형태라 나중에 판단을 되짚기 좋다.
걸려 넘어질 지점¶
- sleep mode와
gpu-memory-utilization=0.90을 같이 켜면 시작에 실패한다. sleep mode의 전용 메모리 할당자(cumem allocator)가 non-torch 메모리를 음수(-4.04 GiB)로 계산하는 오류 때문이다. 0.85에서는 여유 안에 있지만 0.90에서는 KV 캐시 산정이 예산을 약 1GiB 초과해(56.77GiB 대 55.68GiB) 크래시루프에 빠진다. 0.85로 두거나 vLLM이 로그로 제안하는--kv-cache-memory고정값을 쓴다. - sleep/wake 제어 엔드포인트에 인증이 없다.
VLLM_SERVER_DEV_MODE=1이 필요하고, NetworkPolicy나 사이드카로 파드 외부 접근을 막고 호출은 CronJob이나 KEDA 같은 컨트롤러로 한정하라고 권한다. - sleep 시 KV 캐시는 비워진다. prefix cache 의존이 큰 워크로드는 wake 직후 첫 요청들의 TTFT가 늘어난다.
/health만으로는 sleep 상태를 판단할 수 없다. 시작 시간 측정용 기준이지 운영용 준비 상태 판정 기준이 아니다.- Hugging Face에서 직접 받는 구성은 피한다. 콜드마다 수십 GB를 인터넷에서 다시 받고, 프라이빗 서브넷이면 NAT 게이트웨이 데이터 처리 요금이 붙으며, 캐시가 컨테이너 안이라 파드 재생성 때 사라진다. 온보딩 때
s5cmd sync로 한 번만 S3에 올리는 방식을 쓴다.
읽을 때 감안할 것¶
- 단일 환경, 소수 시행이다. g7e.2xlarge Spot 한 종류이고 이미지 사전 pull 시험은 n=2다. 저자가 첫머리에 "개선율과 단계별 수치는 모두 g7e.2xlarge Spot 단일 환경에서 나온 결과값"이라고 밝힌다.
- 226초는 순수 권고 4개의 값이 아니다. 이미지 사전 pull(노드 계층 변경)의 32초가 포함돼 있다. 권고 4개만 적용한 콜드는 따로 시험하지 않았고 약 258초로 추정한다고 밝힌다. 인용할 때 구분해야 한다.
- 측정에 최대 10초 오차가 있다. readinessProbe 주기가 10초라 시작 시간이 10초 단위로 기록된다. 개별 구간 수치와 전체 합계를 같은 정밀도로 다루면 안 된다.
- "기타 111초"가 최적화 대상에서 빠져 있다. 전체의 26%다. API 서버 시작 46초와 가중치 로드 전 초기화 39초는 vLLM 내부라 손대기 어렵겠지만, 적지 않은 몫이 미개척으로 남았다.
- 2부가 아직 없다. Ready 이후의 처리량과 지연은 다음 글이다. 이 글만으로는 서빙 성능을 판단할 수 없다.
가져갈 지점¶
- 서빙 구조 노트와 정확히 상보적이다
서빙 구조가 LLM 처리량을 바꾸는 방식이 Ready 이후의 처리량을 다뤘다면 이 글은 Ready까지 걸리는 시간을 다룬다. 두 글을 붙이면 GPU 서빙의 시작부터 정상 운영까지가 이어진다. 이 글의 2부가 바로 그 처리량 주제이므로 나오면 같이 읽을 것.
- 병목을 대역폭으로 착각하지 않기
가중치 로드가 느린 이유가 대역폭이 아니라 FUSE 경로였다는 발견이 이 글에서 가장 값어치 있다. 50Gbps 노드에서 2.1Gbps만 쓰고 있었다. 느리면 자원을 늘리기 전에 실사용률부터 재봐야 한다는 사례다. 서빙 구조 노트에서 "설정을 바꿔도 안 빨라지면 계층 없이 돌려보라"고 한 것과 같은 태도다.
- 재시작이 잦은 개발 단계에 바로 쓸 수 있다
튜닝을 반복하면 재시작이 곧 비용이다. hostPath 컴파일 캐시만 붙여도 재컴파일 50초가 사라진다. 매니페스트 몇 줄이라 시도 비용이 거의 없다.
- 제외 근거를 적어두는 습관
"안 한 것"에 근거와 재검토 조건을 함께 적는 형식은 저장소 정리에도 가져올 만하다. 나중에 같은 선택지를 다시 만났을 때 처음부터 따지지 않아도 된다.
결론¶
구간 분해와 단일 변수 변경이라는 기본기를 제대로 지킨 글이다. 네 구간으로 나눠 각각의 원인을 다르게 진단했고, Step마다 기술 하나씩만 바꿔 개선 폭을 귀속시켰다. 조건부와 제외를 따로 분류하고 재검토 조건까지 붙인 것도 드물다.
단일 환경 소수 시행이라 수치 자체의 일반화는 조심해야 한다. 다만 구간을 나눠 재고 하나씩 바꾼다는 절차와 FUSE 경로가 대역폭보다 먼저 병목이 된다는 관찰은 환경이 달라도 남는다.