상태: 초안 (2026-10-27), 실측. 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0, Qwen3-Reranker-0.6B 로 2026-10-27 에 쟀습니다. 잠자기 모드 시험 중 컨테이너 하나가 메모리 한도로 죽었고, 그 일도 결과의 일부로 적었습니다. 지표의 이름이 판마다 바뀐 이야기는 「오픈소스 기여 기록」 3편에, OpenTelemetry 트레이싱은 「토스 ML/AI Infra 준비」 25편에 있습니다. 2026-10-27 추가. 같은 날 vLLM 0.32.0 으로 인증 경로, 기동 시간, 잠자기 모드를 다시 잰 결과를 각 절의 「0.32.0 재측정」에 더했습니다. 0.31.0 수치는 그대로 두었습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 잠자기 모드: 서버를 끄지 않고 GPU 비우기
- 2. 기동 시간: 컴파일 캐시가 절반을 줄였다
- 3. /metrics: 무엇이 나오나
- 4. --api-key 는 /v1 만 지킨다
- 5. 운영에서 가져갈 것
- 6. 확인하지 못한 것
- 참고
1. 잠자기 모드: 서버를 끄지 않고 GPU 비우기
GPU 한 장을 여러 모델이나 학습과 번갈아 쓰려면, 쓰지 않는 동안 GPU 메모리를 내놓아야 합니다. 서버를 끄면 다시 띄우는 데 수십 초가 듭니다(2절). 잠자기 모드는 프로세스와 CUDA 문맥, 컴파일한 그래프를 그대로 둔 채 큰 메모리만 내놓습니다.
| 단계 | 가중치 | KV 캐시 | 깨울 때 | 쓰는 때 |
|---|---|---|---|---|
| 1단계 | CPU 메모리로 옮겨 둠 | 버림 | CPU 에서 되돌림 | 같은 모델로 다시 돌아올 때 |
| 2단계 | 버림 | 버림 | 가중치를 다시 읽어야 함(reload_weights) |
다른 가중치로 바꿀 때(RLHF 가중치 갱신), CPU 메모리가 모자랄 때 |
켜는 법은 서버 옵션과 개발 모드 환경 변수 둘입니다. 잠재우기와 깨우기가 HTTP 경로로 열리기 때문에 개발 모드에서만 나옵니다.
VLLM_SERVER_DEV_MODE=1 vllm serve <모델> --enable-sleep-mode
curl -X POST 'localhost:8000/sleep?level=1'
curl -X POST 'localhost:8000/wake_up'
# 2단계: 가중치 자리만 깨우고, 가중치를 다시 읽고, KV 를 깨운다
curl -X POST 'localhost:8000/sleep?level=2'
curl -X POST 'localhost:8000/wake_up?tags=weights'
curl -X POST 'localhost:8000/collective_rpc' -H 'Content-Type: application/json' -d '{"method":"reload_weights"}'
curl -X POST 'localhost:8000/wake_up?tags=kv_cache'
--enable-sleep-mode 는 vLLM 의 CuMem 할당기를 켜서, 가중치와 KV 를 태그가 붙은 가상 메모리로 잡아 두었다가 실제 메모리만 풀고 다시 붙입니다. vLLM 블로그(2025-10-26, vLLM 0.11.0)는 A100 에서 Qwen3-0.6B 를 1단계로 깨우는 데 0.26초, 2단계는 0.85초였고 새로 띄우는 것보다 18~20배 빠르다고 보고했습니다. 같은 글에서 A4000 은 깨우기에 0.1초에서 0.8초가 걸렸습니다(업체 발표).
직접 재 보기
- 환경: 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0, Qwen3-Reranker-0.6B(가중치 1.12GiB),
VLLM_SERVER_DEV_MODE=1 --enable-sleep-mode. - 방법: 탐욕 디코딩으로 100토큰을 만드는 요청을 먼저 한 번 보내 출력 해시를 기록하고, 잠재우기 → 깨우기 → 같은 요청을 네 번 되풀이했습니다. 각 단계의 HTTP 호출 시간,
nvidia-smi의 GPU 메모리, 컨테이너 cgroup 의 익명 메모리를 기록했습니다. 2단계를 먼저, 1단계를 나중에 했습니다.
| 항목 | 2단계(4회) |
|---|---|
| 잠재우기 호출 | 42ms(42, 42, 42, 50) |
| 잠든 동안 GPU 메모리 | 6,168MiB → 606MiB |
깨우기(가중치 자리 → reload_weights → KV 자리) |
0.39초 중앙(첫 회 0.85초, 이후 0.40·0.38·0.38초) |
| 깨운 뒤 첫 요청(100토큰) | 569ms(잠들기 전 579ms) |
| 출력이 잠들기 전과 같은가 | 4회 모두 같음 |
| 컨테이너 익명 메모리 | 2,750MiB 그대로 |
| 항목 | 1단계 |
|---|---|
| 첫 시도(컨테이너 한도 4GiB, 잠들기 전 익명 메모리 2,704MiB) | 잠재우기 호출 중 컨테이너가 OOMKilled |
| 다시(새 컨테이너, 한도 4.5GiB, 익명 메모리 2,750MiB) | 다시 OOMKilled |
읽는 법은 이렇습니다.
- 2단계는 이 모델에서 거의 공짜였습니다. 가중치 1.12GiB 를 디스크(페이지 캐시)에서 다시 읽어도 0.4초 안에 깨어났고, 깨운 뒤 첫 요청은 잠들기 전과 같은 시간에 같은 출력을 냈습니다. 같은 설정의 재기동(2절, 32초)보다 80배쯤 빠릅니다. CUDA 문맥과 컴파일한 그래프가 살아 있기 때문입니다.
- 잠든 동안에도 606MiB 는 남았습니다. CUDA 문맥과 CUDA 그래프, 잠자기 할당기가 관리하지 않는 버퍼입니다. 다른 일에 줄 수 있는 GPU 메모리는 전체에서 이만큼을 뺀 값입니다.
- 1단계는 컨테이너 메모리 한도를 넘겼습니다. 1단계는 가중치를 CPU 의 고정 메모리로 옮기므로 그만큼 컨테이너 메모리가 늘어납니다. 셈으로는 2.75GiB + 1.12GiB 로 4.5GiB 안에 들어가야 했지만 두 번 모두 넘쳤습니다. 가중치 말고도 옮기는 것이 있거나 복사 중에 잠시 두 벌이 생기는 것으로 보이며, 정확한 양은 미확인입니다. 이 OOM 은 파드 하나로 끝났고 노드에는 영향이 없었지만, 운영에서 같은 일이 나면 서비스 중인 엔진이 통째로 죽습니다. 1단계를 쓰려면 컨테이너 한도에 가중치 크기의 두 배 이상을 여유로 두고 먼저 시험해야 합니다. 0.32.0 재측정(2026-10-27 0.31.0 측정 → 0.32.0 재측정). 0.32.0 릴리스 노트는 잠자기가 작업 공간과 KV 초기화 상태까지 내놓도록 고쳤다고 적습니다. 같은 모델, 같은 4.5GiB 한도로 다시 쟀습니다.
| 항목 | 0.31.0 | 0.32.0 |
|---|---|---|
| 2단계 잠재우기 호출(4회 중앙) | 42ms | 43ms |
| 잠든 동안 GPU 메모리 | 6,168 → 606MiB | 6,170 → 606MiB |
| 2단계 깨우기(4회 중앙) | 0.39초 | 0.41초 |
| 깨운 뒤 첫 요청, 출력 같음 | 569ms, 4/4 | 569ms, 4/4 |
| 1단계 | 컨테이너 OOMKilled(두 번) | 다시 OOMKilled(잠들기 전 익명 메모리 2,693MiB) |
2단계는 판이 바뀌어도 거의 같았고, 잠든 동안 남는 606MiB 도 줄지 않았습니다. 1단계의 메모리 문제도 그대로였습니다. 세 번의 OOM 은 모두 파드 하나에서 끝났고 노드에는 영향이 없었습니다.
- 문서도 같은 것을 경고합니다. 1단계는 가중치를 CPU 에 백업하므로 CPU 메모리가 충분해야 하고, CPU 메모리가 모자라면 2단계를 쓰고
reload_weights로 다시 읽으라고 적습니다. 이번 실측은 그 「충분」이 가중치 크기보다 크다는 것을 보여 줍니다.
2. 기동 시간: 컴파일 캐시가 절반을 줄였다
같은 파드에서 서버를 여러 번 띄우며 /health 가 200 을 낼 때까지의 시간을 기록했습니다.
| 경우 | 준비까지 | 그중 torch.compile |
|---|---|---|
| 새 파드에서 처음(기본 설정) | 65초, 72초(두 파드) | 19.6초 |
| 같은 설정을 다시 | 30~34초 | 0.15초(디스크의 결과를 읽음) |
| 처음 쓰는 설정(옵션을 바꿈) | 46~62초 | 약 19초 |
| 추측 디코딩을 켬 | 78초, 96초 | 19~36초 |
vLLM 은 torch.compile 결과를 ~/.cache/vllm 아래에 남기고, 모델·설정·관련 환경 변수·torch 판·GPU 가 같으면 다시 읽습니다. 로그에는 Directly load AOT compilation from path … 가 찍힙니다. 컨테이너가 새로 뜨면 이 캐시가 사라지므로, 자주 다시 뜨는 서버라면 캐시 폴더를 볼륨에 두거나 이미지에 구워 두는 것을 문서가 권합니다. 0.31.0 문서는 이 밖에 메모리 측정을 건너뛰는 --kv-cache-memory, 컴파일을 아예 안 하는 --enforce-eager(대신 디코드가 느려짐), 최적화 단계 -O0 부터 -O3 까지(기본 -O2)를 기동 시간의 손잡이로 듭니다.
기동 시간과 별개로, 서버가 「준비됨」을 낸 뒤에도 처음 보는 모양의 일에서 컴파일이 또 일어납니다. 4편에서 본 것처럼 첫 표본 추출 묶음에서 Triton 커널을 컴파일하느라 수 초가 걸렸고, 로그에 jit_monitor 경고가 남았습니다. 준비 확인은 /health 가 아니라 실제 요청 모양으로 한 번 데운 뒤에 내는 편이 안전합니다.
0.32.0 재측정. 새 컨테이너에서 처음 띄울 때 60초, 같은 설정을 다시 띄울 때 30초로 0.31.0 과 같은 모양이었습니다. 0.32.0 로그에는 「JIT kernel warmup」 단계가 새로 보였지만, 첫 표본 추출 묶음의 커널 컴파일 경고와 지연(대화 모델 6.2초, 재순위 모델 동시 16 에서 5.3초)은 그대로 남아 있었습니다.
3. /metrics: 무엇이 나오나
/metrics 는 Prometheus 형식입니다. 기본 서버에서 vllm: 로 시작하는 지표는 38종이었고, 추측 디코딩 서버는 41종, KV 오프로딩 서버는 53종이었습니다. 운영 대시보드에 먼저 둘 만한 것을 실측 서버의 실제 값과 함께 묶었습니다.
| 묻고 싶은 것 | 지표 | 이번 실측에서 본 값 |
|---|---|---|
| 지금 몇 개가 돌고 몇 개가 기다리나 | vllm:num_requests_running, vllm:num_requests_waiting, vllm:num_requests_waiting_by_reason |
기다림의 이유를 capacity(자리 없음)와 deferred(일시 제약)로 가름 |
| KV 가 얼마나 찼나 | vllm:kv_cache_usage_perc |
0~1 사이 비율 |
| 접두사 캐시가 듣나 | vllm:prefix_cache_hits_total / vllm:prefix_cache_queries_total |
기준 서버 시험 전체에서 94,592 / 330,084 토큰 |
| 프롬프트 토큰은 어디서 왔나 | vllm:prompt_tokens_by_source_total{source=...} |
local_compute, local_cache_hit, external_kv_transfer(7편의 CPU 오프로딩) |
| 지연 | vllm:time_to_first_token_seconds, vllm:inter_token_latency_seconds, vllm:e2e_request_latency_seconds, vllm:request_queue_time_seconds |
히스토그램. 0.32.0 부터 --custom-histogram-buckets 로 구간을 바꿀 수 있음 |
| 선점이 일어나나 | vllm:num_preemptions_total |
이번 시험은 0 |
| 끝난 이유 | vllm:request_success_total{finished_reason=...} |
stop 372, length 807 |
| 추측 디코딩이 맞히나 | vllm:spec_decode_num_accepted_tokens_total / ..._draft_tokens_total, 위치별 ..._per_pos_total |
3편 |
| 도구 호출 파서가 무엇을 냈나 | vllm:tool_call_parser_invocations_total{outcome=...} |
5편 |
| 잠들어 있나 | vllm:engine_sleep_state{sleep_state=...} |
awake, weights_offloaded(1단계), discard_all(2단계) |
| 모델 FLOPs 활용률 | vllm:estimated_flops_per_gpu_total 등 |
기본은 0, --enable-mfu-metrics 로 켬 |
지표 이름은 판마다 바뀌어 왔습니다. 예를 들어 V0 엔진의 vllm:gpu_prefix_cache_hit_rate 는 0.10 에서 사라졌고, 그 자리를 위의 hits·queries 두 카운터가 대신합니다. 대시보드와 경보 규칙은 판을 올릴 때마다 지금 서버가 내는 이름과 대조해야 합니다. 이번에 쓴 방법은 단순합니다. curl -s localhost:8000/metrics | grep '^# HELP vllm:' 로 이름 목록을 뽑아 규칙 파일과 비교했습니다.
4. --api-key 는 /v1 만 지킨다
vLLM 의 보안 문서는 「--api-key 는 /v1, /v2, /inference, /cohere 로 시작하는 경로만 지킨다」고 분명히 적고, 인증 없이 열리는 경로를 나열합니다. 실제로 그런지 확인했습니다. 서버를 VLLM_API_KEY(이 시험에서만 쓰고 버린 무작위 값)로 띄우고, 키 없이 각 경로를 불렀습니다.
| 경로 | 키 없이 | 뜻 |
|---|---|---|
GET /v1/models |
401 | 지켜짐(키를 주면 200) |
POST /v1/completions |
401 | 지켜짐(키를 주면 200) |
POST /invocations |
200 | SageMaker 호환 경로. /v1/completions 와 같은 생성 기능이 키 없이 돌았음 |
POST /tokenize |
200 | 토큰화. 프롬프트 처리 비용을 키 없이 쓸 수 있음 |
GET /metrics |
200 | 모델 이름, 처리량, 대기열, 캐시 상태 |
GET /version, GET /load |
200 | 판 정보, 부하 |
POST /pause, POST /sleep |
404 | 이 판, 이 설정에서는 경로가 없음(잠자기는 개발 모드에서만) |
0.32.0 재측정(2026-10-27 0.31.0 측정 → 0.32.0 재측정). 같은 시험을 0.32.0 공식 컨테이너에서 다시 돌렸더니 14개 경로의 상태 코드가 하나도 바뀌지 않았습니다. /invocations 는 0.32.0 에서도 키 없이 200 을 냈습니다.
/invocations 하나로 결론이 납니다. 이 서버를 인터넷이나 다른 팀의 망에 그대로 열면, 키를 줬어도 아무나 모델을 돌릴 수 있습니다. 문서도 --api-key 에만 기대지 말고 허용한 경로만 통과시키는 역프록시나 게이트웨이를 앞에 두라고 권합니다. 같은 문서에 따르면 풀링 모델의 /score, /rerank, /pooling, /classify 도 /v1 이 없는 판은 인증을 받지 않습니다.
개발 모드(VLLM_SERVER_DEV_MODE=1)는 더 위험합니다. 잠재우기(서비스 거부), 캐시 비우기, 그리고 엔진 안에서 임의의 메서드를 부르는 /collective_rpc 가 열립니다. 문서는 운영에서 절대 켜지 말라고 적습니다. 1절의 잠자기 모드를 운영에서 쓰려면, 그 경로가 외부에서 닿지 않도록 망 정책으로 막고 오케스트레이터만 부르게 해야 합니다.
0.32.0 은 기본 CORS 를 「모든 출처 허용」에서 「허용 없음」으로 바꿨습니다. 브라우저에서 직접 부르던 서비스라면 판을 올릴 때 --allowed-origins 를 명시해야 합니다(릴리스 노트 기준, 미실측).
5. 운영에서 가져갈 것
- 잠자기는 2단계부터 시험합니다. 1단계를 쓰려면 컨테이너 메모리 한도에 가중치 크기의 두 배 이상을 더해 잡고 먼저 시험합니다. 이번 실측에서 그 여유가 없던 컨테이너는 두 번 모두 통째로 OOM 으로 죽었습니다.
- 컴파일 캐시를 볼륨에 두면 같은 설정의 재기동이 절반이 됩니다. 판을 올리거나 옵션을 바꾸면 캐시가 맞지 않아 다시 컴파일합니다.
- 준비 확인은 실제 요청 모양으로 데운 뒤에 냅니다.
/metrics이름은 판마다 대조합니다.- vLLM 앞에는 반드시 경로를 고르는 게이트웨이를 둡니다.
--api-key는/v1만 지킵니다.
6. 확인하지 못한 것
- 잠자기 모드에서 텐서 병렬(여러 GPU)과 큰 모델의 깨우기 시간은 미실측입니다.
- 0.32.0 의 잠자기 개선(작업 공간·KV 초기화 상태 해제, CUDA 그래프 오프로딩)과 CORS 기본값 변경은 미실측입니다.
/pause·/resume이 이 판에서 404 였던 조건(필요한 옵션이 따로 있는지)은 미확인입니다.
참고
- vLLM 문서, Sleep Mode, Optimization and Tuning(Faster Startup), Production Metrics, Security(0.32.0): https://docs.vllm.ai/en/latest/
- vLLM 블로그, 「Zero-Reload Model Switching with vLLM Sleep Mode」, 2025-10-26: https://vllm.ai/blog/2025-10-26-sleep-mode
- vLLM 0.32.0 릴리스 노트(CORS, 잠자기, 히스토그램 구간): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 이전 글: 「오픈소스 기여 기록」 3편 vLLM 이 지운 메트릭 이름, 「토스 ML/AI Infra 준비」 25편 LLM 서빙을 OpenTelemetry 로 관측하기