TT Lab
블로그

한국어

시작하기
블로그

블로그

운영 기능: 잠자기 모드, 기동 시간, /metrics, 그리고 --api-key 가 지키지 않는 길

상태: 초안 (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 비우기

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초가 걸렸습니다(업체 발표).

직접 재 보기

항목 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

읽는 법은 이렇습니다.

  1. 2단계는 이 모델에서 거의 공짜였습니다. 가중치 1.12GiB 를 디스크(페이지 캐시)에서 다시 읽어도 0.4초 안에 깨어났고, 깨운 뒤 첫 요청은 잠들기 전과 같은 시간에 같은 출력을 냈습니다. 같은 설정의 재기동(2절, 32초)보다 80배쯤 빠릅니다. CUDA 문맥과 컴파일한 그래프가 살아 있기 때문입니다.
  2. 잠든 동안에도 606MiB 는 남았습니다. CUDA 문맥과 CUDA 그래프, 잠자기 할당기가 관리하지 않는 버퍼입니다. 다른 일에 줄 수 있는 GPU 메모리는 전체에서 이만큼을 뺀 값입니다.
  3. 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. 문서도 같은 것을 경고합니다. 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. 운영에서 가져갈 것

6. 확인하지 못한 것

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다