vLLM + Modal Volume으로 콜드 스타트를 1초 이하로 줄이는 가중치 캐싱 + GPU 스냅샷 구현 레시피
서버리스 GPU 환경에서 LLM 추론 서비스를 운영해본 분이라면 콜드 스타트 문제가 얼마나 골치 아픈지 잘 아실 겁니다. 저도 처음 vLLM을 서버리스 환경에 올렸을 때 꽤 당황했거든요. 첫 배포 당시 콜드 스타트가 4분을 훌쩍 넘었고, 최종적으로 0.8초까지 줄이기까지 꽤 많은 시행착오를 거쳤습니다. HuggingFace에서 14GB짜리 가중치를 받고, torch.compile이 돌아가고, CUDA 그래프가 초기화되기까지... 사용자 입장에서는 그냥 "느린 서비스"일 뿐입니다.
2025년에 이 문제를 정말 효과적으로 해결할 수 있는 기반이 갖춰졌습니다. NVIDIA가 드라이버 570/575 브랜치에서 GPU 메모리 직렬화 API를 공식 제공하기 시작하면서, Modal 같은 서버리스 플랫폼이 이를 활용한 메모리 스냅샷 기능을 내놓을 수 있게 됐거든요. Modal Volume에 가중치와 컴파일 아티팩트를 캐싱하고, GPU 메모리 스냅샷까지 적용하면 콜드 스타트를 1초 이하로 줄이는 것이 실제로 가능합니다. 이 글에서는 단순한 "Volume에 저장하기"를 넘어, 세 가지 캐싱 계층을 어떻게 조합하는지, 각 단계에서 어떤 트레이드오프가 있는지를 실제 코드와 함께 풀어보겠습니다.
이 글을 다 읽고 나면 콜드 스타트가 왜 느린지 구조적으로 이해하고, Modal + vLLM 환경에서 각 캐싱 전략을 직접 적용해볼 수 있는 실전 코드를 손에 쥐게 될 겁니다. 예시 코드를 실행하려면 Modal 계정과 CLI 설치가 필요합니다 — pip install modal로 설치하고 modal setup으로 인증을 마치면 바로 따라해볼 수 있습니다.
핵심 개념
콜드 스타트는 사실 세 번 느려집니다
콜드 스타트를 "그냥 모델 로딩 시간"으로 뭉뚱그리면 해결책을 찾기가 어렵습니다. 실제로는 세 단계가 순차적으로 쌓이는 구조입니다.
| 단계 | 캐싱 전 소요 시간 | 캐싱 후 소요 시간 |
|---|---|---|
| 모델 가중치 다운로드 (HuggingFace) | 수 분 | ~수십 초 (Volume 로드) |
| torch.compile / CUDA 그래프 컴파일 | 1~5분 | ~10초 (아티팩트 캐시) |
| GPU 메모리 로드 | ~수십 초 | < 1초 (GPU 스냅샷) |
각 단계를 하나씩 공략하는 게 핵심입니다. 3단계를 모두 적용하면 누적 효과로 수 분짜리 콜드 스타트가 1초 안쪽으로 들어옵니다.
vLLM이 왜 초기화가 이렇게 오래 걸릴까
vLLM은 PagedAttention 알고리즘을 기반으로 KV 캐시를 OS의 가상 메모리처럼 관리합니다. GPU 메모리 효율을 극대화하는 대신, 초기화 단계에서 CUDA 그래프를 미리 캡처하고, torch.compile로 Triton 커널을 JIT 컴파일하는 작업이 필요합니다.
PagedAttention: 어텐션 연산의 KV(Key-Value) 캐시를 연속된 메모리 블록이 아닌 페이지 단위로 관리하는 기법. OS의 가상 메모리 페이징에서 착안했으며, GPU 메모리 단편화를 줄여 더 많은 요청을 동시에 처리할 수 있게 해줍니다.
Triton 커널과 CUDA 그래프를 매번 컴파일하는 이유는, GPU 아키텍처와 드라이버 버전별로 최적화된 바이너리가 달라지기 때문입니다. 즉, 환경이 동일하게 유지되는 한 이 결과물을 캐싱해두면 재사용이 가능합니다. 이 작업들이 매 콜드 스타트마다 반복되는 게 문제의 본질이고, 바꿔 말하면 결과물만 저장해두면 다음 기동부터는 건너뛸 수 있다는 뜻이기도 합니다.
Modal Volume — 쉽게 말하면 이겁니다
쉽게 말하면, 컨테이너가 꺼져도 살아남는 공유 디스크입니다. 그게 전부입니다. 읽기 속도가 12 GB/s에 달하는데, NVMe 로컬 SSD가 보통 37 GB/s 수준임을 감안하면 절반 정도의 속도입니다. "느리지 않나요?"라고 물어볼 수 있는데, HuggingFace에서 네트워크로 받는 것(수백 MB/s 이하)에 비하면 훨씬 빠르고, 여러 컨테이너가 동일 Volume을 동시에 읽을 수 있어 오토스케일링 상황에서도 효율적입니다.
GPU 메모리 스냅샷 — 게임 체인저
2025년에 등장한 Modal의 GPU Memory Snapshots는 NVIDIA의 CUDA Checkpoint/Restore API(드라이버 570/575 이상)를 활용합니다. 초기화가 완료된 직후 컨테이너의 CPU + GPU 메모리 전체를 디스크에 직렬화해두고, 이후 기동 시에는 초기화를 건너뛰고 역직렬화만 수행합니다.
CUDA Checkpoint/Restore API: NVIDIA가 드라이버 570/575 브랜치에서 공식 제공하기 시작한 GPU 메모리 직렬화/역직렬화 API. 로드된 커널, CUDA 그래프, 가중치 상태를 모두 보존합니다.
vLLM Sleep Mode — 멀티 모델 서빙의 구원자
2025년 10월 공개된 vLLM Sleep Mode는 서버 프로세스를 유지하면서 GPU 메모리만 비우는 기능입니다. 솔직히 처음 이 기능을 봤을 때 "이걸 왜 이제야 만들었지?"라는 생각이 들었습니다.
- Level 1: 가중치를 CPU RAM으로 오프로드 → 빠른 웨이크업 (0.1~0.8초)
- Level 2: 가중치 완전 폐기 → 최소 RAM 사용, 약간 느린 웨이크업
Sleep → Wake 전환 속도가 콜드 스타트 대비 얼마나 빠른지는 조건에 따라 크게 달라집니다. Level 1(CPU 오프로드)에서는 18~30배, 가중치가 이미 Volume에 캐시된 상태의 Level 2에서도 수십 배 이상 빠릅니다. 200배라는 수치는 최초 HuggingFace 다운로드를 포함한 콜드 스타트와의 비교입니다. 여러 모델을 전환하며 서빙하는 멀티 모델 아키텍처에서 특히 유용합니다.
실전 적용
예시 1: Modal Volume 3단계 캐싱 파이프라인 구성
가장 기본이 되는 구성입니다. HuggingFace 가중치 캐시와 torch.compile 아티팩트 캐시를 별도 Volume으로 분리하는 것이 핵심입니다. 처음에 하나로 묶었다가 아티팩트 무효화 관리가 꼬인 경험이 있어서, 분리를 강하게 권장합니다.
import modal
app = modal.App("vllm-cached-inference")
# 가중치용 Volume과 컴파일 아티팩트용 Volume을 분리
hf_volume = modal.Volume.from_name("hf-model-cache", create_if_missing=True)
vllm_volume = modal.Volume.from_name("vllm-compile-cache", create_if_missing=True)
image = (
modal.Image.debian_slim()
.pip_install("vllm", "hf_transfer")
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"}) # 고속 다운로드 활성화
)
@app.cls(
gpu="A100",
image=image,
volumes={
"/root/.cache/huggingface": hf_volume, # HF 가중치 캐시
"/root/.cache/vllm": vllm_volume, # torch.compile 아티팩트 캐시
},
)
class VLLMServer:
@modal.enter()
def load_model(self):
from vllm import LLM
self.llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
enable_prefix_caching=True, # Automatic Prefix Caching 활성화
)
@modal.method()
def generate(self, prompt: str) -> str:
from vllm import SamplingParams
outputs = self.llm.generate(prompt, SamplingParams(max_tokens=256))
return outputs[0].outputs[0].text| 코드 포인트 | 설명 |
|---|---|
hf_transfer + 환경변수 |
HuggingFace 다운로드 속도를 네트워크 대역폭 한계까지 끌어올림 |
volumes 딕셔너리 |
컨테이너 내 경로를 Volume에 마운트. 재기동 시 다운로드 생략 |
enable_prefix_caching=True |
동일 프롬프트 접두사의 KV 캐시를 재사용 (Automatic Prefix Caching) |
이 구성만으로도 첫 기동 이후 콜드 스타트가 수 분 → 수십 초로 단축됩니다. HuggingFace 장애에도 영향을 받지 않게 되는 건 덤이고요.
torch.compile 배치 크기 최적화가 필요하다면 예시 1을 확장해 아래 설정 파일을 함께 사용하면 좋습니다.
# compile.yaml — 정적 배치 크기에 최적화된 커널 사전 컴파일
compilation:
compile_sizes: [1, 2, 4, 8]vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-prefix-caching \
--compilation-config compile.yaml캐시 없이 컴파일하면 1~5분, Volume 캐시를 쓰면 ~10초로 단축됩니다. 오토스케일링 환경에서 여러 인스턴스가 동일 Volume을 공유하면 두 번째 인스턴스부터는 컴파일 비용이 사실상 0에 수렴합니다.
이 접근의 주요 제약: torch.compile 아티팩트는 GPU 아키텍처·드라이버·CUDA 버전이 동일한 환경에서만 재사용됩니다. 인스턴스 타입이나 드라이버 버전을 바꾸면 캐시를 통째로 무효화해야 합니다.
예시 2: GPU 메모리 스냅샷으로 1초 이하 달성
Modal의 LFM2 배포 사례에서 가장 잘 정리된 패턴입니다. enable_memory_snapshot=True와 @modal.enter(snap=True) 두 줄이 핵심입니다.
@app.cls(
gpu="H100",
image=image,
volumes={"/root/.cache/huggingface": hf_volume},
enable_memory_snapshot=True, # CPU + GPU 메모리 스냅샷 활성화
)
class SnapshotVLLMServer:
@modal.enter(snap=True) # 이 메서드 실행 결과를 스냅샷 대상으로 지정
def load_model(self):
from vllm import LLM
self.llm = LLM(
model="liquid-ai/LFM2-1B",
# Modal 레이어 옵션 — vLLM 공식 파라미터가 아닌 Modal 연동 설정
experimental_options={"enable_gpu_snapshot": True},
)
@modal.method()
def generate(self, prompt: str) -> str:
from vllm import SamplingParams
outputs = self.llm.generate(prompt, SamplingParams(max_tokens=256))
return outputs[0].outputs[0].text기동 흐름을 정리하면 다음과 같습니다.
| 기동 순서 | 수행 작업 | 소요 시간 |
|---|---|---|
| 최초 기동 | 모델 초기화 → CPU + GPU 메모리 스냅샷 저장 | 수십 초 (1회성) |
| 이후 기동 | 스냅샷 역직렬화만 수행 | < 1초 |
snap=True로 지정된 load_model은 최초 1회만 실행됩니다. 이후 컨테이너가 올라올 때는 load_model 자체를 호출하지 않고, 해당 시점의 CPU + GPU 메모리 이미지를 직접 복원합니다. torch.compile 재컴파일도, CUDA 그래프 재초기화도 필요 없습니다. 디버깅 시 "분명히 모델을 다시 로드했는데 왜 업데이트가 안 되지?"라고 당황할 수 있는데, 스냅샷이 남아있는 한 load_model이 실행되지 않기 때문입니다.
주의: GPU 스냅샷은 2025년 기준 알파 기능입니다. NVIDIA 드라이버 570/575 이상이 필요하며, 모든 모델과 환경에서 지원되지는 않습니다. 프로덕션 적용 전 충분한 테스트를 거치는 것을 권장합니다.
이 접근의 주요 제약: 모델 버전이나 vLLM 버전을 업그레이드하면 스냅샷을 반드시 재생성해야 합니다. 재생성 없이 배포하면 이전 가중치 상태로 추론이 돌아가는 미묘한 버그로 이어집니다.
예시 3: vLLM Sleep Mode로 멀티 모델 서빙 구성
여러 모델을 하나의 GPU 인스턴스에서 전환하며 서빙해야 할 때 Sleep Mode가 유용합니다. 실무에서 자주 맞닥뜨리는 상황인데, 모델마다 컨테이너를 따로 띄우면 비용이 기하급수적으로 늘어나거든요.
아래 코드는 실제 vLLM Sleep Mode API 스펙에 맞춘 의사 코드입니다. /sleep과 /wake는 모델을 파라미터로 받지 않으며, 모델 전환 로직은 별도 오케스트레이션 레이어에서 처리해야 합니다.
import requests
# 서버 실행 시 Sleep Mode 활성화
# vllm serve ... --enable-sleep-mode
# Sleep Mode API 사용 예시
# POST /sleep — 모델 파라미터 없음, 현재 인스턴스를 슬립 상태로 전환
# POST /wake — 모델 파라미터 없음, 슬립 상태에서 깨워 GPU 메모리 복원
def sleep_current_model(server_url: str):
# 현재 모델을 CPU로 오프로드 (Level 1 — 빠른 웨이크업 유지)
requests.post(f"{server_url}/sleep", json={"level": 1})
def wake_model(server_url: str):
# GPU 메모리 복원 (Level 1의 경우 0.1~0.8초)
requests.post(f"{server_url}/wake")
# Sleep → Wake 전환 특성
# - Level 1 (CPU 오프로드): 0.1~0.8초, 충분한 CPU RAM 필요
# - Level 2 (완전 폐기): RAM 최소화, 상대적으로 느린 웨이크업Level 1은 가중치를 CPU RAM으로 오프로드하므로, 해당 모델의 가중치를 담을 충분한 CPU RAM이 확보되어야 합니다. 70B 모델이라면 RAM 요구량이 상당하니 인스턴스 사양을 미리 확인해 두면 좋습니다.
장단점 분석
제가 실제로 운영하면서 가장 많이 당한 것만 솎아내면 아래 표로 정리됩니다.
장점
| 항목 | 내용 |
|---|---|
| 네트워크 독립성 | HuggingFace 다운로드를 완전히 생략. 외부 서비스 장애 영향 없음 |
| 고속 Volume 읽기 | 1~2 GB/s 읽기 속도로 대형 모델도 빠르게 로드 |
| 컴파일 비용 공유 | 오토스케일링 인스턴스 간 torch.compile 아티팩트 공유로 추가 비용 0 |
| GPU 스냅샷 복원 완전성 | CUDA 그래프, 로드된 커널, 컴파일 상태 모두 복원됨 |
| Sleep Mode 전환 속도 | Level 1 기준 0.1~0.8초 웨이크업. CUDA 컨텍스트 재초기화 불필요 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| GPU 스냅샷 알파 상태 | 모든 모델/환경에서 지원 보장 안 됨 | 스냅샷 없이도 동작하는 폴백 경로 유지 |
| 드라이버 버전 요구 | NVIDIA 드라이버 570/575 이상 필요 | 인스턴스 선택 시 드라이버 버전 사전 확인 |
| 스냅샷 무효화 | 모델 버전·vLLM 업그레이드 시 재생성 필요 | 버전 변경 시 스냅샷 재생성을 배포 파이프라인에 포함 |
| Volume 스토리지 비용 | 70B+ 모델은 수백 GB 스토리지 필요 | 사용 빈도 낮은 모델은 필요 시 다운로드 전략 검토 |
| Level 1 Sleep RAM 요구 | CPU 오프로드 방식은 충분한 CPU RAM 필요 | 인스턴스 RAM 용량을 모델 크기 기준으로 사전 계획 |
| 컴파일 캐시 환경 의존성 | GPU 아키텍처·드라이버·CUDA 버전이 동일해야 재사용 가능 | 환경 변경 시 캐시 무효화 처리 자동화 권장 |
| APC 트레이드오프 | KV 캐시 공간 소모. 짧은 프롬프트 중심 워크로드에선 비효율 가능 | 워크로드 특성에 따라 enable_prefix_caching 선택적 활성화 |
Automatic Prefix Caching(APC): 동일한 프롬프트 접두사에 대한 KV 캐시를 해시 기반 블록 매핑으로 재사용하는 기법. 긴 시스템 프롬프트가 반복되는 챗봇이나 RAG 시나리오에서 효과적이지만, 매번 완전히 다른 짧은 프롬프트를 처리하는 워크로드에서는 캐시 히트율이 낮아 KV 캐시 공간만 점유합니다.
실무에서 가장 흔한 실수
- Volume을 하나로 통합 관리: 가중치 캐시와 컴파일 아티팩트를 같은 Volume에 두면, vLLM 버전 업그레이드로 아티팩트를 무효화해야 할 때 가중치까지 날려버리는 사고가 납니다. 처음부터 분리하는 것을 권장합니다.
- 스냅샷 무효화 타이밍 놓치기: 모델 버전이나 vLLM을 업그레이드한 뒤 스냅샷을 재생성하지 않으면 이전 가중치 상태로 추론이 돌아가는 미묘한 버그가 생깁니다. 버전 변경 시 스냅샷 재생성을 배포 파이프라인에 포함해 두는 것을 권장합니다.
- APC를 무조건 활성화:
enable_prefix_caching=True가 항상 좋은 건 아닙니다. 매번 새로운 프롬프트를 처리하는 워크로드라면 KV 캐시 공간만 낭비됩니다. 워크로드 특성을 먼저 파악한 뒤 활성화 여부를 결정하는 것을 권장합니다.
마치며
모델 크기가 7B 이하라면 Volume 가중치 캐싱만으로도 수십 초 수준으로 충분히 줄어들고, 그 이상이라면 torch.compile 아티팩트 캐싱과 GPU 메모리 스냅샷을 순서대로 쌓아야 비로소 1초 이하가 됩니다. 세 계층을 모두 적용할 때 비로소 물리적 한계에 가까운 콜드 스타트가 달성됩니다.
지금 바로 시작해볼 수 있는 3단계:
- Volume 마운트부터 적용해 보시면 좋습니다.
modal.Volume.from_name("hf-model-cache", create_if_missing=True)로 Volume을 생성하고,@app.cls의volumes파라미터에/root/.cache/huggingface경로로 마운트하면 됩니다. 첫 기동 이후 HuggingFace 다운로드가 사라지는 것을 바로 확인할 수 있습니다. - torch.compile 아티팩트 캐시용 Volume을 별도로 추가해보시면 좋습니다.
/root/.cache/vllm경로를 두 번째 Volume에 마운트하고,compile_sizes를 워크로드에 맞게 설정해 두면 오토스케일링 시 컴파일 비용이 사라집니다. - GPU 스냅샷은 소형 모델로 먼저 실험해 보시는 것을 권장합니다.
enable_memory_snapshot=True와@modal.enter(snap=True)를 추가하고, LFM2-1B 같은 소형 모델로 스냅샷 생성 → 역직렬화 흐름을 검증한 뒤 대형 모델로 확장하면 안정적입니다.
다음 글: KV 캐시를 단일 인스턴스를 넘어 클러스터 레벨로 확장하는 방법 — LMCache와 llm-d로 구현하는 분산 Prefix Caching 아키텍처
참고 자료
- Run OpenAI-compatible LLM inference with Gemma and vLLM | Modal Docs
- GPU Memory Snapshots: Supercharging Sub-second Startup | Modal Blog
- Modal + Mistral 3: 10x faster cold starts with GPU snapshotting
- Low Latency, Serverless LFM2 with vLLM and Modal | Modal Docs
- Snapshot GPU memory to speed up cold starts | Modal Docs
- High-performance LLM inference | Modal Docs
- Volumes | Modal Docs
- Memory Snapshots | Modal Docs
- Sleep Mode - vLLM Official Docs
- Zero-Reload Model Switching with vLLM Sleep Mode | vLLM Blog
- torch.compile integration - vLLM
- Automatic Prefix Caching - vLLM
- Reducing GPU Cold Start Time when using vLLM - Tensorfuse
- vLLM Roadmap Q1 2026 · GitHub
- GPU Memory Snapshotting to reduce cold starts · vLLM Issue #33930
- KV-Cache Wins You Can See: From Prefix Caching in vLLM to Distributed Scheduling with llm-d
- Product updates: GPU memory snapshots, notebooks, service tokens | Modal Blog