AWS 기술 블로그

Amazon EKS에서 vLLM으로 Gemma 4 서빙 최적화하기 [1부: 콜드 스타트 최적화]

Gemma 4는 Google이 공개한 오픈 웨이트 모델 패밀리로, E2B부터 31B까지 5가지 크기에 텍스트와 이미지 입력(E2B, E4B, 12B는 오디오도 지원), 최대 256K 토큰 컨텍스트(E2B, E4B는 128K), 추론(thinking) 모드를 제공합니다. 그중 31B는 밀집(dense) 구조의 최상위 모델로, NVFP4(4비트 부동소수점)로 양자화한 가중치 약 31GiB가 96GB GPU 1개에 올라가는 대신 시작할 때마다 수십 GB를 S3에서 GPU로 옮겨야 합니다.

Amazon EKS에 이 모델을 오픈소스 서빙 엔진 vLLM으로 배포하면, 기본 구성에서는 GPU 노드가 없는 상태에서 시작해서 첫 요청을 처리할 수 있게 되기까지 7분 이상 소요됩니다. 이 시간은 하나의 병목이 아니라 노드 프로비저닝, 이미지 pull, 가중치 로드, 엔진 초기화가 순서대로 쌓인 합계입니다. 이 콜드 스타트(노드 프로비저닝부터 첫 요청 처리까지 걸리는 시간)를 4단계로 나누어 분석하고 단계별 최적화 기술을 하나씩 구현합니다. 그 결과 콜드 스타트는 428초에서 226초(이미지 사전 pull 포함)로 최적화되었고 웜(Warm) 재시작과 유휴 상태에서 서빙으로 복귀하는 시간도 함께 단축됐습니다.

이 글은 2부로 구성됩니다.

  • 1부(현재글): 파드가 Ready가 되기까지의 시작 시간의 최적화
  • 2부: Ready 이후에 같은 GPU 한 장의 처리량과 지연 SLO (양자화 정밀도, speculative decoding, prefix caching, 스케줄러 설정)

1부는 Amazon EKS에서 vLLM과 같은 LLM 서빙 엔진을 운영하는 엔지니어를 위한 글입니다. 시작 시간과 GPU 비용을 줄이기 위해 Gemma 4 서빙에 적용한 vLLM 최적화 구현(로더와 캐시 설정, 매니페스트, sleep mode)을 정리합니다. 개선율과 단계별 수치는 모두 g7e.2xlarge Spot 단일 환경에서 나온 결과값입니다.

1. 서빙 환경

이 글의 모든 수치는 아래 환경으로 구성되었습니다. 기술 적용과 시험은 같은 클러스터, 같은 인스턴스 계열에서 진행되었습니다. 모델은 NVIDIA가 공개한 NVFP4 체크포인트로, 30.7B 파라미터가 모두 4비트라면 약 15GiB이지만 MLP 계층만 NVFP4로 양자화하고 어텐션과 임베딩 계층은 BF16을 유지하는 구성이라 저장소 크기는 32.7GB(약 30.5GiB), vLLM이 로드 시 보고하는 GPU 메모리 기준으로는 31.22GiB입니다.

항목
리전, 클러스터 ap-northeast-2, Amazon EKS 1.36, Karpenter v1.14
GPU 노드 g7e.2xlarge Spot (NVIDIA RTX PRO 6000 Blackwell 96GB, GPU 1개). 전 시험 동일 계열, 웜 시험은 동일 노드
서빙 vLLM v0.26.0 (vllm/vllm-openai:v0.26.0, 이미지 8.9GB)
모델 Gemma-4-31B-IT NVFP4 (nvidia/Gemma-4-31B-IT-NVFP4, NVIDIA Model Optimizer로 양자화한 체크포인트), safetensors, 로드 시 31.22GiB. 리전 내 S3 버킷에 사전 스테이징
모델 접근 자격 EKS Pod Identity로 S3 읽기 권한을 파드의 서비스 계정에 연결
시험 도구 파드 conditions와 events로 Kubernetes 구간을, vLLM 로그 마커로 가중치와 컴파일 구간을 분해하는 시험 스크립트(셸)
전체 시작 시간 정의 파드 생성 → readinessProbe(/health) 첫 통과. probe 주기 10초 단위 오차 포함

표 1. Gemma 4 on EKS 서빙 환경 (2026년 8월 30일 기준)

그림 1. Gemma 4 on EKS 서빙 환경. Karpenter가 만든 g7e.2xlarge Spot 노드 위의 vLLM 파드가 S3 모델 버킷의 가중치를 EKS Pod Identity 자격 증명으로 읽고, 시험 스크립트는 kubectl로 파드 상태와 이벤트, vLLM 로그 마커를 수집합니다.

2. 콜드 스타트 상세 분석

“시작까지 7분 걸린다”와 같은 단순 시간 측정으로는 무엇을 개선할지를 정할 수 없습니다. 서빙 환경의 시작 과정을 구간으로 나누고 각 구간마다 무엇이 시간을 점유하는지 확인하는 것이 최적화의 출발점입니다.

2.1 4단계와 이 글의 용어

vLLM 파드(Pod)는 1단계 노드 프로비저닝, 2단계 이미지 pull, 3단계 가중치 로드, 4단계 엔진 초기화를 순서대로 지나 readinessProbe(컨테이너가 트래픽을 받을 준비가 됐는지 kubelet이 주기적으로 확인하는 검사)를 통과하면 서빙을 시작합니다. 시작 시간은 파드 생성 시점부터 readinessProbe 첫 통과까지입니다. probe 주기가 10초이기 때문에 전체 시작 시간은 10초 단위로 기록되고 최대 10초의 오차를 포함합니다. 이 기준은 시작 시간 측정용입니다. sleep mode를 운영에 적용할 때는 /health만으로 파드의 준비 상태를 판단할 수 없는데, 그 이유와 대안은 sleep/wake 기술에서 다룹니다.

콜드(Cold)는 GPU 노드가 없는 상태에서 Karpenter(파드 요구에 맞춰 노드를 자동으로 프로비저닝하는 오픈소스 노드 오토스케일러) 프로비저닝부터 시작하는 경우로, 4단계를 모두 지납니다. 웜은 이미지와 캐시를 이미 가진 같은 노드에서 파드만 재생성하는 경우로, 3단계 가중치 로드부터 시작합니다.

캐시를 쓰는 기술은 캐시를 채우는 적재 회차와 캐시를 사용하는 히트 회차로 나누어 기록했습니다. 적재 회차는 효과가 없는 것이 정상이므로 개선 수치는 항상 히트 회차 기준입니다.

그림 2. 콜드 스타트의 4단계와 이 글의 용어. 콜드는 1단계 노드 프로비저닝부터 4단계 전체를, 웜은 같은 노드에서 3단계 가중치 로드부터를 밟습니다. 캐시 기술이 작용하는 엔진 초기화 구간은 캐시를 채우는 적재 회차와 캐시를 사용하는 히트 회차로 나누어 기록했습니다.

2.2 기준 구성의 구간 분해

기준 구성(Mountpoint FUSE 마운트 + vLLM 기본 로더, 캐시 없음)의 콜드 428초를 단계별로 나누면 아래 표와 같습니다. 가중치 로드와 엔진 초기화가 합쳐서 221.5초로 전체의 절반을 넘습니다. 노드 프로비저닝과 이미지 pull은 같은 노드에서 파드만 다시 만들면 사라지지만 이 2개 구간은 그대로 반복됩니다.

구간 시간 시험 근거 대응 기술
1단계 / 노드 프로비저닝 30초 Karpenter NodeClaim 생성 → 노드 Ready 노드 선행 패턴, warm pool (조건부)
2단계 / 이미지 pull 65.1초 kubelet Pulled 이벤트, 8.9GB 이미지 사전 pull → 32~40초 (조건부)
3단계 / 가중치 로드 (S3 → GPU) 127.6초 vLLM 로그 Loading weights took 스트리밍 로더 S3 직결 → 19.8초
4단계 / 엔진 초기화 (torch.compile 50.8초 포함) 93.9초 vLLM 로그 init engine took 컴파일 캐시 영속화와 S3 사전 로드 → 29.6~30.4초
기타 (스케줄링, probe 간격 등) 약 111초 전체 시작 시간에서 위 4단계를 제외한 나머지
합계 (파드 생성 → Ready) 428초 파드 conditions

표 2. 기준 구성 콜드 428초의 구간 분해

4단계에 포함되지 않는 약 111초는 vLLM 프로세스가 가중치 로드에 들어가기 전 초기화(약 39초), 엔진 초기화 뒤 API 서버 시작(약 46초), 파드 스케줄링 대기(약 15초), 컨테이너 시작 전 준비(약 4초), probe 주기 단위 오차(약 7초)의 합입니다.

2.3 웜 재시작 시간이 줄어들지 않는 이유

같은 노드에서 파드만 다시 만들면 노드 프로비저닝, 스케줄링 대기, 이미지 pull이 사라집니다. 기준 구성의 웜 재시작은 314초입니다. 콜드 428초와의 차이 114초는 노드 프로비저닝과 스케줄링 대기 45초, 이미지 pull 65초를 제외한 값과 거의 일치합니다. 가중치 127.8초와 컴파일 51.0초는 단축되지 않습니다.

원인은 2가지입니다. 첫째, Mountpoint for Amazon S3는 S3 버킷을 파일 시스템처럼 마운트하는 FUSE 클라이언트입니다. FUSE(Filesystem in Userspace)는 커널이 아닌 사용자 공간 프로세스(User Space Process)가 파일 시스템을 구현하도록 하는 Linux 인터페이스입니다. 애플리케이션의 파일 읽기 요청은 커널을 거쳐 사용자 공간(User Space)의 Mountpoint 프로세스로 전달되고, Mountpoint가 S3 API로 데이터를 받아 다시 커널을 통해 애플리케이션에 돌려줍니다. vLLM은 이 마운트를 로컬 파일로 인식하므로 가중치를 파일 읽기 방식으로 매 시작마다 S3에서 다시 읽습니다. 둘째, torch.compile(PyTorch 모델 그래프를 미리 컴파일해 실행 속도를 높이는 기능)의 캐시는 기본 위치가 컨테이너 안의 /root/.cache이므로 파드가 종료될 때 함께 사라집니다. 기본 구성에서는 재시작이 곧 재컴파일입니다.

그림 3. 기본 구성에서 웜 재시작 시간이 줄어들지 않는 2가지 원인. 왼쪽은 FUSE 경로로 가중치를 읽을 때 요청과 데이터가 커널과 사용자 공간(User Space)을 오가는 흐름이고, 오른쪽은 컴파일 캐시가 컨테이너 파일 시스템에 있어 파드 재생성 때 함께 사라지는 구조입니다.

3. 최적화 기술과 결과 요약

단계마다 원인이 다르므로 기술도 단계별로 나뉩니다. 본문에서는 기술을 그대로 표현하고, 그림처럼 지면이 좁은 곳에서만 아래 표의 식별자를 씁니다. 권고 기술은 ‘권고 N’, 조건부 기술은 ‘조건부 N’, 검토 후 제외한 기술은 ‘제외 N’으로 부르고 번호는 적용 순서입니다.

식별자 기술 줄이는 구간 분류
권고 1 스트리밍 로더로 가중치를 S3에서 직접 로드 3단계 / 가중치 로드 권고
권고 2 컴파일 캐시를 hostPath에 영속화 4단계 / 엔진 초기화 (같은 노드) 권고
권고 3 컴파일 캐시를 S3에 게시하고 initContainer로 사전 로드 4단계 / 엔진 초기화 (신규 노드) 권고
권고 4 sleep/wake로 유휴 상태와 서빙 상태 전환 재시작 자체 권고
조건부 1 노드 부팅 시 이미지 사전 pull 2단계 / 이미지 pull 조건부
조건부 2 캐시 노드 보존, warm pool 1단계 / 노드 프로비저닝 조건부
제외 1~3 SOCI lazy loading, 가중치 EBS pre-bake, 컴파일 캐시 EBS pre-bake 제외

표 3. 이 글에서 쓰는 기술 식별자

그림 4. 콜드 스타트 4단계와 기술 매핑. 실선은 시작 순서, 점선은 그 구간을 줄이는 기술입니다. 각 기술은 자기 구간에만 작용하므로 독립적으로 적용하고 독립적으로 시험할 수 있습니다.

권고 기술을 모두 적용한 결과는 아래 표와 같습니다. 콜드 행의 226초에는 이미지 사전 pull의 부분 효과 약 32초가 포함되어 있습니다. 표준 NodeClass에서 권고 기술 4개만 적용한 콜드는 따로 시험하지 않았고, 226초에 이미지 pull 차이(63.9초 – 32.2초)를 되돌리면 약 258초로 추정됩니다. 웜 재시작 행과 유휴 상태 복귀 행은 권고 기술 4개만으로 나온 값입니다.

시나리오 기준 구성 스트리밍 로더 + 노드 로컬 캐시 + sleep/wake S3 캐시 사전 로드 + 이미지 사전 pull 개선
콜드 (노드 프로비저닝부터) 428초 (7분 8초) 306초 (신규 노드, 캐시 미스) 226초 (3분 46초, 캐시 히트) -47%
웜 재시작 (같은 노드) 314초 133초 (2분 13초) 133초 -58%
유휴 상태 → 서빙 복귀 314초 (재시작) 0.97초 (wake) 0.97초 약 320배

표 4. 시나리오별 시작 시간 변화 (g7e.2xlarge Spot, 2026년 8월 30일 기준)

단계별로 보면 스트리밍 로더로 가중치 로드는 127.6초에서 19.8초로 6.5배 빨라졌고, 컴파일 캐시 영속화와 S3 사전 로드로 엔진 초기화는 캐시 히트 시 94.0초에서 29.6초로 약 68% 감소합니다. 유휴 상태에서 복귀하는 0.97초는 sleep/wake가 재시작 자체를 대체한 결과입니다.

4. 구현 순서

개선 폭을 기술 단위로 귀속하려면 한 번에 하나만 바꿔야 합니다. 구현 순서는 Step 0(Baseline)부터 Step 5까지로 나누고 매니페스트를 이 원칙으로 적용했습니다. 콜드 스타트의 1~4단계 구간과 구분하기 위해 구현 순서는 Step으로 부릅니다. 각 Step은 직전 Step과 정확히 기술 하나만 다릅니다. Step 0에서 Step 3까지는 순서대로 이어지고 Step 4는 노드 계층 기술의 별도 A/B, Step 5는 Step 2에서 갈라진 분기입니다. Deployment 이름을 모두 같게 두고 Recreate 전략(기존 파드를 모두 종료한 뒤 새 파드를 만드는 배포 전략)을 쓰므로 Step 전환은 kubectl apply 1회입니다.

Step 추가 기술 (직전 대비 1개) 공략 구간 결과
Step 0 (Baseline) Mountpoint FUSE 마운트 + 기본 로더, 캐시 없음 기준점 콜드 428초, 웜 314초
Step 1 runai_streamer S3 직결 + env AWS_REGION 3단계 / 가중치 로드 127.8초 가중치 19.8초, 웜 204초
Step 2 VLLM_CACHE_ROOT를 hostPath로 영속화 4단계 / 초기화 94초 (compile 51초 포함) 히트 회차 compile 6.6초, 초기화 29.6초, 웜 133초. 신규 노드 콜드는 306초(미스)
Step 3 sleep mode (--enable-sleep-mode, gpu-memory-utilization 0.85) 재시작 자체 sleep 12.8초, wake 0.97초. 시작 시간은 133초로 캐시 히트 유지
Step 4 (별도 A/B) EC2NodeClass userData에서 이미지 사전 pull 2단계 / 이미지 pull 65초 pull 32.2초, 39.8초 (n=2). already present는 불발
Step 5 (Step 2 분기) initContainer가 S3에서 compile 캐시를 hostPath로 사전 로드 4단계 / 신규 노드의 캐시 미스 콜드에서 compile 6.98초 히트, 총 226초

표 5. 구현 Step 순서와 각 Step의 결과 (공략 구간 값은 웜 재시작 기준)

5. 권고 기술 4가지 구현

권고 기술 4개는 적용한 순서대로 설명합니다. 각 기술은 어느 단계에서 왜 시간이 걸리는지, 기술이 이를 어떻게 해결하는지, 결과 수치가 얼마인지, 매니페스트를 어떻게 바꾸는지, 어떤 조건에서 성립하는지를 같은 순서로 정리했습니다.

모든 권고는 모델 가중치가 리전 내 S3 버킷에 미리 올라가 있다는 전제에서 출발합니다. 가중치는 온보딩할 때 1회만 Hugging Face에서 내려받아 s5cmd(S3 대용량 전송에 특화된 오픈소스 CLI)의 sync로 S3에 올리고 이후 서빙 파드는 S3에서만 가중치를 읽습니다. 서빙 파드가 Hugging Face에서 직접 내려받는 구성은 다음 3가지 이유로 권장하지 않습니다.

  • 콜드마다 수십 GB를 인터넷에서 다시 내려받습니다.
  • 프라이빗 서브넷에서는 이 트래픽이 NAT 게이트웨이를 거치므로 데이터 처리 요금이 붙습니다.
  • 다운로드 캐시가 컨테이너 안에 있어 파드를 재생성하면 사라집니다.

이 글의 가중치 로드 수치는 모두 S3에서 GPU 메모리까지 걸린 시간입니다.

그림 5. 권고 기술 3개(스트리밍 로더, hostPath 캐시, S3 사전 로드)를 적용한 구성. 가중치는 S3 게이트웨이 엔드포인트를 거쳐 vLLM 컨테이너로 직결되고, 컴파일 캐시는 hostPath에 남으며 S3 캐시 prefix에서 initContainer가 사전 로드합니다. EKS Pod Identity가 파드에 S3 읽기 자격 증명을 공급합니다. Mountpoint 마운트는 보조 파일용으로만 남기므로 그림에서는 생략했습니다.

5.1 권고 1 – 가중치는 FUSE 마운트 대신 스트리밍 로더로 S3 직접 연결

가중치 로드 127.6초는 대역폭 부족 때문이 아닙니다. 31.22GiB(약 33.5GB)를 127.6초에 읽으면 실제 사용 대역폭은 약 2.1Gbps로, g7e.2xlarge의 네트워크 대역폭 50Gbps에 크게 못 미칩니다. 병목은 FUSE 경로 자체입니다. FUSE 경로는 vLLM의 순차 파일 읽기를 그대로 S3 요청으로 옮기므로 단일 스트림에 가깝게 동작하고, 요청마다 커널과 사용자 공간(User Space)을 오가는 비용이 더해집니다.

vLLM에 포함된 오픈소스 로더 Run:ai Model Streamer(--load-format=runai_streamer)는 s3:// 경로를 직접 받아 병렬 range read로 가중치를 읽습니다. 같은 버킷, 같은 가중치에서 127.6초가 19.8초로 6.5배 빨라졌고 실제 사용 대역폭은 약 13.6Gbps였습니다. 이 Step에서 초기화 94.4초(compile 51.1초 포함)는 바뀌지 않아 스트리밍 로더의 효과만 분리해 확인할 수 있습니다.

containers:
  - name: vllm
    image: vllm/vllm-openai:v0.26.0
    args:
      - --model=s3://<model-bucket>/Gemma-4-31B-IT-NVFP4/   # FUSE 경로(/data/...) 대신 s3:// 직접 지정
      - --quantization=modelopt                              # 체크포인트 설정에서 자동 감지되지만 재현성을 위해 명시
      - --load-format=runai_streamer
      - '--model-loader-extra-config={"concurrency":64,"memory_limit":25769803776}'  # 2xlarge(64GiB RAM) 기준 버퍼 24GiB
      - --served-model-name=gemma4-31b
      - --max-model-len=8192
    env:
      - name: AWS_REGION            # s3:// 직결의 필수 종속 env (리전 명시)
        value: ap-northeast-2

concurrencymemory_limit은 노드 RAM을 CPU 스테이징 버퍼로 사용하기 때문에 인스턴스 타입이 바뀌면 함께 조정해야 합니다. 매니페스트에 인스턴스 타입별 권장값을 주석으로 남겨 두면 타입 변경 시 누락을 막을 수 있습니다.

스트리밍 로더를 적용하면 EKS 구성에서 4가지가 함께 바뀝니다.

  • 자격 증명EKS Pod Identity(IAM 역할을 Kubernetes 서비스 계정에 연결해 파드에 임시 자격 증명을 제공하는 기능)로 부여합니다. S3 읽기 권한을 파드의 서비스 계정에 직접 연결하고 노드 IAM 역할에는 권한을 추가하지 않으므로 파드 단위 최소 권한이 유지됩니다.
  • 네트워크 대역폭 – 새로운 한계가 됩니다. 스트리밍 로더의 실제 사용 대역폭(약 13.6Gbps)은 인스턴스의 네트워크 대역폭에 좌우됩니다. GPU 메모리와 연산 성능만 보고 인스턴스를 고르면 같은 GPU라도 네트워크 대역폭이 낮은 인스턴스에서는 가중치 로드 시간이 그만큼 길어집니다. 50Gbps 인스턴스에서 19.8초였던 로드가 네트워크 대역폭이 절반인 인스턴스에서는 2배 가까이 늘어날 수 있으므로, 인스턴스를 고를 때 GPU 사양과 함께 네트워크 대역폭을 확인해야 합니다.
  • S3 게이트웨이 엔드포인트 경로를 확보합니다. (VPC에서 인터넷 게이트웨이나 NAT 장치 없이 S3로 라우팅하는 엔드포인트, 추가 요금 없음) 재시작마다 가중치 크기만큼 NAT 게이트웨이 데이터 처리 요금이 발생하는 것을 막습니다.
  • Mountpoint for Amazon S3 CSI 드라이버는 제거하지 않고 보조 파일(draft 모델, 토크나이저) 접근용으로 역할을 낮춥니다.

5.2 권고 2 – torch.compile 캐시를 컨테이너 밖 노드에 영속화

vLLM은 시작할 때 torch.compile로 그래프를 컴파일하고 CUDA graph(GPU 커널 실행 순서를 미리 기록해 재생하는 기법)를 캡처합니다. 이 산출물은 VLLM_CACHE_ROOT 아래에 캐시되는데, 기본값이 컨테이너 안이라 파드를 다시 만들 때마다 사라집니다. 스트리밍 로더까지 적용한 웜 재시작 204초 중 초기화 94초(그중 컴파일 51초)가 이런 이유로 매번 반복됩니다.

VLLM_CACHE_ROOT를 hostPath(노드 디스크의 디렉터리를 파드에 직접 마운트하는 볼륨)로 옮기면 2번째 시작부터 캐시가 히트합니다. 히트 회차에서 compile은 50.9초에서 6.6초로, 초기화 전체는 94.0초에서 29.6초로 약 68% 감소합니다. 웜 재시작 전체는 204초에서 133초가 됩니다. 적재 회차(첫 시작)는 204초로 효과가 없는 것이 정상입니다. 기준 웜 재시작 314초에서 스트리밍 로더가 110초, 캐시 영속화가 71초를 줄였고 두 값은 서로 다른 구간에서 나옵니다.

containers:
  - name: vllm
    env:
      - name: VLLM_CACHE_ROOT       # 기본값 /root/.cache 대신 마운트된 경로로
        value: /vllm-cache
    volumeMounts:
      - name: vllm-cache
        mountPath: /vllm-cache
volumes:
  - name: vllm-cache
    hostPath:                        # 캐시 수명 = 노드 수명. 노드가 회수되면 함께 사라짐
      path: /var/cache/vllm
      type: DirectoryOrCreate

구성 비용이 0에 가까운 기술이지만 한계가 분명합니다. 캐시가 노드 로컬에 있으므로 Karpenter consolidation(비어 있거나 활용도가 낮은 노드를 회수하는 기능)이나 Spot 중단으로 노드가 바뀌면 사라집니다. 신규 노드 콜드는 두 기술을 적용해도 306초로, compile 51.1초가 그대로 돌아옵니다. 이 한계는 다음 기술인 S3 사전 로드가 해결합니다.

캐시 키는 구성 조합에 묶여 있습니다. vLLM 문서에 따르면 캐시는 모델, 컴파일 관련 구성, 관련 VLLM_* 환경 변수, torch 빌드, GPU 모델 중 하나라도 바뀌면 무효화됩니다. 이번 시험에서는 --enable-sleep-mode 추가와 gpu-memory-utilization 변경에도 히트가 유지됐습니다. 키에는 컴파일에 영향을 주는 구성만 들어가고 그 밖의 플래그는 포함되지 않는다는 뜻입니다. 모델 경로나 vLLM 버전 변경이 콜드 컴파일을 동반한다는 사실은 릴리스 절차에 적어 두어야 합니다. 컴파일과 무관한 플래그 조정은 캐시 히트를 유지한 채 배포됩니다.

5.3 권고 3 – 컴파일 캐시를 S3로 배포해 신규 노드에서도 히트

hostPath 캐시는 노드가 바뀌면 사라지지만 크기는 218MB, 1,204개 객체에 그칩니다. 리전 내 S3에서 내려받는 데 수 초가 걸리므로 컴파일 51초와 비교하면 캐시를 옮기는 비용이 훨씬 저렴합니다.

S3 사전 로드는 initContainer(본 컨테이너보다 먼저 실행되는 준비용 컨테이너)가 S3에서 캐시를 hostPath로 내려받은 뒤 vLLM을 시작하는 구성입니다. 신규 노드 콜드에서 compile 6.98초 히트가 나왔고 초기화 전체는 30.4초, 총 콜드는 226초였습니다. 같은 사전 pull NodeClass 조건의 다른 신규 노드에서 캐시 미스였던 회차가 297초였으므로 전체 시작 시간 차이는 71초입니다. 이 71초가 전부 사전 로드의 효과는 아닙니다. 사전 로드에 귀속되는 초기화 구간은 94.3초에서 30.4초로 약 64초 줄었습니다. 나머지 약 8초는 이미지 pull이 39.8초에서 32.2초로 달라진 회차 간 편차입니다. 표준 NodeClass의 콜드 306초와 비교한 80초에는 이미지 사전 pull의 부분 효과가 더해져 있습니다.

initContainers:
  - name: cache-preload
    image: amazon/aws-cli:<version>   # latest 대신 고정 태그 사용
    command: ["/bin/sh", "-c"]
    args:
      - |
        PFX="s3://<model-bucket>/vllm-cache/v0.26.0-nvfp4/"   # vLLM 버전과 구성 식별자를 prefix에
        N=$(aws s3 ls "$PFX" --recursive 2>/dev/null | wc -l)
        echo "[cache-preload] source objects: $N"           # 0이면 무증상 미스. 반드시 로그로 남김
        aws s3 sync "$PFX" /vllm-cache/ --only-show-errors || true   # 실패해도 시작 진행(콜드 컴파일 폴백)
        du -sh /vllm-cache || true
    env:
      - name: AWS_REGION
        value: ap-northeast-2
    volumeMounts:
      - name: vllm-cache
        mountPath: /vllm-cache

캐시를 S3에 올리는 게시 절차는 별도이며 게시 prefix 문자열은 initContainer가 읽는 값과 정확히 같아야 합니다. 해당 구성의 첫 시작(적재 회차)이 끝난 뒤 1회만 실행합니다.

kubectl -n serving exec deploy/gemma4-31b -- \
  aws s3 sync /vllm-cache s3://<model-bucket>/vllm-cache/v0.26.0-nvfp4/

# vllm-openai 이미지에 aws cli가 없으면 hostPath를 마운트한 별도 파드에서 실행
# 결과: 218M /vllm-cache, uploaded objects 1204

그림 6. 캐시 수명을 컨테이너에서 노드로, 노드에서 S3로 끌어올리는 구조. hostPath 캐시는 파드 재생성을 넘기고(웜 133초), S3 사전 로드는 노드 교체를 넘깁니다(콜드 226초). 사전 로드가 실패하면 콜드 컴파일로 돌아가므로 시작은 막히지 않습니다.

이 기술이 있어야 Spot 우선 NodePool 전략과 캐시 전략이 양립합니다. 같은 아키텍처 노드로 교체되는 경우에 한해 Spot 중단 뒤에도 컴파일 비용이 돌아오지 않을 것으로 추정합니다. 이 추정은 신규 노드 히트 결과(콜드 226초)에서 외삽한 값이고 Spot 중단 폴백 경로 자체는 확인하지 않았습니다. 성립 조건은 4가지입니다.

  • S3 prefix에 vLLM 버전과 구성 식별자를 넣고 구성이 바뀌면 다시 게시합니다.
  • 게시용 쓰기 권한은 해당 prefix로 한정한 s3:PutObject로 분리합니다. 서빙 자격 증명은 읽기 전용을 유지합니다.
  • initContainer는 실패를 허용하되 소스 객체 수를 로그로 남깁니다. 빈 prefix를 동기화하는 s3 sync는 아무것도 전송하지 않고 exit 0으로 끝나므로 로그가 없으면 미스를 알아챌 방법이 없습니다. 스테이징에서 게시 prefix를 검증할 때는 VLLM_FORCE_AOT_LOAD=1을 함께 켜면 캐시 미스 시 조용히 재컴파일하는 대신 시작이 실패하므로 미스를 바로 발견할 수 있습니다. 운영에서는 콜드 컴파일 폴백을 유지하기 위해 이 값을 끕니다.
  • vLLM 문서는 GPU 모델 변경도 캐시 무효화 요인으로 명시하므로, Spot 폴백으로 g6e(Ada)가 섞이는 NodePool이면 아키텍처별 prefix를 나눠 두는 것이 안전합니다.

5.4 권고 4 – 유휴와 활성 사이의 전환은 sleep/wake 적용

요청이 없는 시간대에 GPU 메모리를 비우고 싶을 때 일반적인 선택은 Deployment의 replicas를 0으로 내리는 것입니다. 복귀는 콜드 경로라 226초가 듭니다. vLLM sleep mode는 프로세스와 컴파일 상태를 유지한 채 GPU 메모리만 반납합니다. level 1은 가중치를 CPU RAM에 보존하고 KV 캐시(어텐션 계산에 쓰는 키와 값을 저장하는 GPU 메모리 영역)만 버립니다. 깨울 때는 S3 재로드나 재컴파일 없이 가중치를 GPU로 되돌리기만 합니다.

시험 결과 POST /sleep?level=1이 12.8초, POST /wake_up이 0.97초였습니다. 웜 재시작 133초와 비교하면 약 137배 빠릅니다. wake 뒤에도 torch.compile 캐시가 유지되므로 추론 지연은 sleep 전과 같습니다(sleep 전 0.28초 → wake 직후 0.30초, wake 직후 짧은 요청도 0.06~0.08초 수준). 단 KV 캐시는 sleep 시 비워지므로 프리픽스 캐시 의존이 큰 워크로드는 wake 직후 첫 요청들의 TTFT가 늘어날 수 있습니다.

containers:
  - name: vllm
    args:
      - --enable-sleep-mode               # /sleep, /wake_up 허용
      - --gpu-memory-utilization=0.85     # sleep 병행 시 0.90은 시작 실패
    env:
      - name: VLLM_SERVER_DEV_MODE        # sleep/wake 제어 엔드포인트 활성화
        value: "1"
kubectl -n serving port-forward svc/gemma4-31b 8000:8000 &
time curl -sX POST 'localhost:8000/sleep?level=1'   # GPU 메모리 반납, 가중치는 CPU RAM 보존 (12.8초)
time curl -sX POST 'localhost:8000/wake_up'         # 복귀 (0.97초)
curl -s localhost:8000/v1/models                    # 추론 가능 확인

sleep을 운영에 적용할 때 확인할 조건은 4가지입니다.

  • 제어 엔드포인트 접근 – sleep/wake 엔드포인트는 VLLM_SERVER_DEV_MODE=1이 필요합니다. 인증이 없는 제어 엔드포인트이므로 NetworkPolicy나 사이드카(sidecar)로 파드 외부 접근을 차단하고, 호출은 컨트롤러(CronJob, KEDA 등)로 한정하는 것을 권합니다.
  • readinessProbe/health는 엔진 프로세스가 살아 있는지만 확인하므로 sleep 상태에서도 200을 반환합니다. readinessProbe를 /health로 두면 실제로는 동작하지 않는 파드가 Ready로 남아 트래픽을 계속 받고, 요청은 오류 없이 wake까지 대기합니다. 시험에서는 실제로는 동작하지 않는 파드로 보낸 요청이 30초 타임아웃까지 응답이 없었고, 그 사이 wake를 보내면 밀린 요청이 처리되어 10.87초가 걸렸습니다. 5xx도 알람도 없이 지연만 늘어나므로 발견이 가장 늦은 장애 유형입니다. readinessProbe가 /is_sleeping(읽기 전용 엔드포인트)까지 확인하도록 exec 프로브로 바꾸거나, vLLM production-stack의 라우터처럼 잠든 파드를 라우팅에서 제외해야 합니다. /health 첫 통과는 시작 시간 측정 기준으로만 씁니다.
  • gpu-memory-utilization – 0.90과 함께 켜면 시작에 실패하므로 0.85로 두거나 vLLM이 시작 로그로 제안하는 고정 --kv-cache-memory 값을 씁니다. sleep mode의 전용 메모리 할당자(cumem allocator)가 non-torch 메모리를 음수(-4.04 GiB)로 계산하는 오류 때문인데, 0.85에서는 여유 안에 머물지만 0.90에서는 KV 캐시 산정이 예산을 약 1GiB 초과해(56.77GiB vs 55.68GiB) 크래시루프에 빠집니다.
  • 호스트 RAM – level 1은 잠든 모델의 가중치를 호스트 RAM에 올려 둡니다. 이 글처럼 모델 하나만 재우는 구성은 약 31GiB라 g7e.2xlarge의 64GiB 안에 들어갑니다. 반면 한 GPU에서 두 모델을 sleep/wake로 교대시키는 구성(A 서빙 중 B는 잠들어 있고, 전환 때 A가 잠들고 B가 깨는 방식)은 전환 순간 두 모델 가중치가 호스트 RAM에 동시에 올라갑니다. 31B와 26B 조합이면 49GB에 vLLM 프로세스 두 개의 기본 메모리가 더해져 64GiB를 넘기고, 시험에서는 노드가 NotReady까지 갔습니다. 이 구성은 호스트 RAM 128GiB 이상인 사이즈에서만 안전합니다. 스트리밍 로더의 스테이징 버퍼(memory_limit 24GiB)도 같은 호스트 RAM을 쓰므로 인스턴스를 바꿀 때 함께 계산합니다.

그림 7. 유휴 상태 대응은 2계층으로 나눕니다. 짧은 유휴 상태는 sleep(복귀 0.97초, 노드 과금 지속), 야간처럼 긴 유휴 상태는 scale-to-zero(복귀 226초, 노드 비용 0)로 처리합니다.

sleep 상태에서도 GPU 인스턴스 과금은 계속됩니다. 유휴 상태가 이어지는 기대 시간을 T, 노드 시간당 단가를 C라고 하면 sleep은 T×C의 유휴 비용을 내는 대신 0.97초에 복귀하고 scale-to-zero는 유휴 비용 없이 226초에 복귀합니다. 복귀 지연의 SLO 페널티가 이 차액을 넘는지가 판단 기준입니다. 수요 패턴 데이터를 먼저 확보한 뒤 결정합니다.

6. 조건부 권고와 제외 기술

권고 기술 4개 밖에도 검토한 기술이 있습니다. 2개는 특정 운영 패턴에서만 성립하기 때문에 조건부이고, 3개는 검토 후 제외했습니다.

6.1 조건부 1 – 이미지 사전 pull은 부분 효과 확인

EC2NodeClass의 userData에서 노드 부팅 직후 vLLM 이미지를 백그라운드로 pull하면 kubelet의 pull이 짧아집니다. 효과는 부분적입니다. 표준 NodeClass의 64~65초가 사전 pull NodeClass에서는 32.2초와 39.8초(n=2)로 줄었습니다.

pull 0초를 뜻하는 already present 이벤트는 2번 다 나오지 않았습니다. Karpenter는 노드가 Ready가 되는 즉시 파드를 스케줄하므로, userData의 사전 pull과 kubelet의 pull이 같은 이미지를 놓고 경합합니다. containerd가 레이어를 공유해 절반 정도만 절감되는 구조입니다. pull 0초는 노드 보존이나 warm pool처럼 노드가 워크로드보다 먼저 존재하는 패턴에서만 가능합니다.

아래 userData는 AL2023 AMI family 기준입니다. Karpenter가 셸 스크립트를 nodeadm의 MIME multipart userData에 병합해 실행하며, Bottlerocket은 셸 userData를 지원하지 않으므로 이 방식을 그대로 쓸 수 없습니다.

#!/bin/bash
# AL2023 AMI family 기준. containerd 소켓이 열릴 때까지 기다린 뒤 백그라운드로 pull. 실패해도 노드 조인은 진행
(
  for i in $(seq 1 60); do
    [ -S /run/containerd/containerd.sock ] && break
    sleep 5
  done
  ctr -n k8s.io images pull docker.io/vllm/vllm-openai:v0.26.0 >>/var/log/vllm-prepull.log 2>&1
) &

약 30초를 더 줄여야 하는 환경이면 활성화해도 손해는 없습니다. 다만 userData의 이미지 태그와 Deployment의 태그가 어긋나면 사전 pull이 무효가 되고 pull 65초로 회귀합니다. 따라서 태그 동기화를 배포 절차에 넣는 것을 권장합니다.

6.2 조건부 2 – 캐시 노드 보존과 warm pool은 수요 데이터로 결정

콜드를 웜으로 바꾸는 구조적 방법은 노드를 워크로드보다 먼저 확보하는 것뿐입니다. 방법은 2가지로, NodePool의 consolidateAfter(비어 있는 노드를 회수하기 전에 기다리는 시간)를 연장해 비어 있는 GPU 노드를 보존하거나, 낮은 priorityClass의 pause 파드로 노드를 상시 확보하는 warm pool을 두는 것입니다. 트레이드오프는 보존 시간에 노드 단가를 곱한 유휴 비용입니다.

재시작 간격 분포를 확보한 뒤 결정하는 것이 이 글의 권고입니다. 히트율에 지연 절감(226초 – 133초 = 93초)을 곱해 SLO 페널티로 환산한 금액이 보존 비용을 넘는 지점을 데이터로 찾아야 합니다. 단일 GPU 안에서 같은 목적을 더 저렴하게 달성하는 sleep/wake가 있으므로, warm pool은 다중 레플리카 스케일아웃에 대비할 때 성립합니다.

6.3 제외 기술

3개 항목은 적용 전 검토 단계에서 제외했습니다. 시험 없이 판단한 이유는 각각 대체 기술이 같은 목표를 더 가볍게 달성했기 때문입니다. 조건이 바뀌면 재검토 대상입니다.

항목 제외 근거 재검토 조건
SOCI lazy loading LLM 서빙은 시작 시 대부분의 레이어를 실제로 읽어 lazy 효과가 제한적입니다. pull 비용이 가중치 로드 구간으로 이연되어 겉으로만 빨라질 위험이 있고, 비표준 snapshotter 운영 부담이 절감폭보다 큽니다. 이미지 구간이 주 병목으로 남을 때
가중치 EBS pre-bake g7e.2xlarge의 EBS 대역폭은 최대 5Gbps로 스트리밍 로더가 낸 13.6Gbps보다 낮아 오히려 느려질 수 있습니다. 모델 갱신마다 스냅샷을 다시 만들어야 해서 S3 단일 진실 원천의 이점도 잃습니다. 가중치 구간에 초 단위 SLO가 붙을 때
컴파일 캐시 EBS pre-bake S3 사전 로드(218MB)가 같은 목표를 훨씬 가볍게 달성합니다. 스냅샷 신선도 관리와 EBS fast snapshot restore를 쓰지 않을 때의 lazy restore 지연 검토가 추가됩니다. initContainer 단계조차 없는 초 단위 시작이 필요할 때

표 6. 제외 기술과 근거

7. 적용 의사결정표

어떤 기술을 어디까지 적용할지는 운영 패턴이 정합니다. 자주 만나는 4가지 상황의 권고 조합과 기대효과를 정리했습니다.

운영 상황 권고 조합 기대효과 (시험결과)
재시작이 잦다 (배포, 튜닝 반복) 스트리밍 로더 + hostPath 캐시 웜 재시작 314초 → 133초
Spot 위주라 노드가 자주 바뀐다 스트리밍 로더 + hostPath 캐시 + S3 사전 로드 (+ 이미지 사전 pull) 노드 교체 콜드 428초 → 226초 (사전 pull 포함)
수요가 간헐적이다 (유휴 상태가 자주 생김) + sleep/wake 복귀 0.97초. 장시간 유휴 상태는 scale-to-zero와 계층화
콜드 SLO가 3분대도 부족하다 노드 보존, warm pool 검토 노드 선행 시 콜드가 웜(133초)으로 수렴. 이미지 사전 pull의 절감은 226초에 이미 포함

표 7. 운영 상황별 권고 조합과 기대효과

적용 순서는 Step별 결과가 정합니다. 가장 오래 처리되는 시간을 가장 낮은 난이도로 줄이는 스트리밍 로더가 먼저이고 구성 비용이 없는 hostPath 캐시, Spot 전략과 양립시키는 S3 사전 로드, 수요 패턴을 확인한 뒤의 sleep/wake 순서입니다. 원칙은 하나입니다. 어느 단계가 오래 처리되는지를 먼저 측정하고, 그 단계의 최적화 기술만 하나씩 적용해서 같은 환경에서 다시 분석합니다. 분석과 시험이 없이 넣은 기술은 서빙 최적화 효과를 주장할 수 없습니다.

8. 전체 시험 결과표

본문의 수치를 재현하거나 다른 환경에 같은 방법을 적용하려는 분들을 위해 전체 시험 결과표를 정리합니다. 본문의 Step별 시작 시간과 sleep/wake 수치가 나온 결과표입니다. 단위는 초입니다. 가중치 로드는 S3에서 GPU 메모리까지 모델 가중치 31.22GiB를 옮기는 데 걸린 시간(vLLM 로그 Loading weights took)이고, 초기화 전체는 compile 시간을 포함한 vLLM init engine took 마커 값입니다. 대역폭은 이 표의 가중치 시간에서 십진 Gbps(31.22GiB는 약 33.5GB)로 계산했습니다.

시험 시나리오 시작
시간
이미지
pull
가중치
로드
compile 초기화
전체
비고
stage0-cold 콜드 428 65.1 127.6 50.8 93.9 Mountpoint, 캐시 없음. 노드 30초
stage0-warm 314 캐시 127.8 51.0 94.0 가중치, 컴파일 절감 0
stage1-warm 204 캐시 19.8 51.1 94.4 +스트리밍 로더
stage2-warm-1st 웜 (적재 회차) 204 캐시 19.3 50.9 94.0 +hostPath 캐시, 캐시를 채우는 회차
stage2-warm-2nd 웜 (히트 회차) 133 캐시 19.1 6.6 29.6 +hostPath 캐시 히트
stage2-cold 콜드 306 63.9 19.9 51.1 94.3 신규 노드 = 캐시 미스. 노드 29초
stage3-warm 웜 (히트) 133 캐시 19.8 6.4 30.2 +sleep mode, gpu-memory-utilization 0.85
stage5-cold-miss 콜드 (사전 pull 노드) 297 39.8 20.9 51.2 94.3 S3 사전 로드 대조군(미스). 노드 30초
stage5-cold-hit 콜드 (사전 pull 노드, 캐시 히트) 226 32.2 20.1 6.98 30.4 +S3 사전 로드 히트, 이미지 사전 pull 부분 효과. 노드 30초
stage3-sleepwake sleep → wake sleep 12.8초, wake 0.97초, 추론 0.28초 → 0.30초

표 8. g7e.2xlarge Spot 전 시험 결과 (2026년 8월 30일 기준, 단위: 초)

GPU 사용은 Spot g7e 합계 약 1.5시간이었습니다. 오후 작업(Step 0~3, 크래시루프 진단 약 15분 포함)이 약 1시간, 저녁 작업(Step 4, 5)이 약 25분입니다.

마치며

Amazon EKS의 GPU 노드에서 Gemma 4를 vLLM으로 서빙하면서 시작 속도와 비용을 함께 최적화하려는 고객에게 이 글의 결론은 3가지입니다. 첫째, 속도 측면에서 콜드 스타트는 428초에서 226초(이미지 사전 pull 포함)로, 같은 노드의 웜 재시작은 314초에서 133초로 줄어듭니다. 배포와 튜닝을 반복하는 팀은 재시작마다 약 3분을 절약하고 새 노드가 필요할 때도 이미지 사전 pull을 포함하면 4분 안에 서빙을 시작할 수 있습니다. 둘째, 비용 측면에서 콜드 스타트가 4분 안으로 들어오면 Spot 인스턴스를 GPU 서빙의 기본 선택지로 삼을 수 있습니다. 노드가 회수되어도 서빙이 빠르게 복구되고 S3에 게시한 컴파일 캐시 덕분에 같은 GPU 아키텍처의 새 노드에서는 컴파일 비용을 다시 내지 않기 때문입니다. 요청이 없는 시간은 sleep/wake로 GPU 메모리만 반납해 0.97초에 복귀하고 야간처럼 긴 유휴 상태는 scale-to-zero로 노드 비용을 0으로 만들 수 있습니다. 수요 패턴에 맞춰 두 방식을 계층화하는 것이 GPU 비용을 줄이는 가장 직접적인 방법입니다. 셋째, 권고 기술 4개는 모두 vLLM과 EKS의 기존 기능(스트리밍 로더, hostPath, initContainer, sleep mode)을 조합한 것이라 별도 컴포넌트를 들이지 않고 매니페스트 수정만으로 속도와 비용 두 효과를 함께 얻을 수 있습니다. 226초에 포함된 이미지 사전 pull만 EC2NodeClass userData를 바꾸는 노드 계층 변경이고, 이를 빼면 콜드는 약 258초로 추정됩니다.

이미지 pull 단계는 리전 내 미러인 Amazon ECR pull through cache를 함께 적용하면 효과를 더 얻을 수 있습니다.

Amazon EKS에서 vLLM과 같은 LLM 서빙 엔진을 운영하면서 시작 시간과 GPU 비용을 최적화하려는 엔지니어가 시작 시간을 단계별로 측정하고 기술을 하나씩 검증하는 데 이 글의 구현 기록이 실질적인 참고가 되기를 바랍니다.

참고 자료

WooHyoung Choi

WooHyoung Choi

최우형 (Woohyung Choi) 님은 AWS Korea의 Principal Solutions Architect로, 국내 주요 엔터프라이즈 및 디지털 기업의 클라우드 전환과 아키텍처 현대화를 지원하고 있습니다. 특히 클라우드 네이티브 아키텍처와 Agentic AI 분야에 전문성을 가지고 있으며, 고객이 Amazon Bedrock을 비롯한 AWS의 생성형 AI 및 Agentic AI 기술을 안정적이고 비용 효율적으로 도입하고 확장할 수 있도록 아키텍처 설계와 기술 자문을 제공하고 있습니다. 컨테이너, 네트워킹, 비용 최적화를 비롯한 클라우드 핵심 기술부터 Agentic AI를 활용한 업무 혁신과 지능형 시스템 구축까지 폭넓은 영역에서 AWS 아키텍처 모범 사례를 제시하고 있습니다. 이를 통해 고객의 복잡한 기술적 과제를 해결하고, AI와 클라우드를 기반으로 지속 가능한 기술 전략을 수립하고 실행할 수 있도록 기술 리더십을 발휘하고 있습니다.

Jinhyun Park

Jinhyun Park

Jinhyun Park helps customers navigate their cloud journey, leveraging my experience in web application development and operations. I collaborate with Digital Native Business (DNB) customers to optimize architectures and implement efficient solutions that drive business value. I provide technical guidance focused on maximizing the potential of data and AI initiatives, supporting customers in their successful cloud transformation.