상태: 초안 (2026-10-17), 일부 실측. 기준 판 production-stack vllm-stack-0.1.13(커밋 e8cb4959ebfa), vLLM v0.31.0(커밋 db9527a46873), LMCache v0.5.6(커밋 3e11e10812b4), llm-d v0.10.0(커밋 f22bbaf3173a), llm-d-router v0.11.0(커밋 a5cbe600ebad). 작은 클러스터에는 vLLM 이 상시 떠 있지 않아 오늘 새 파드는 띄우지 않았고, 실측은 2026-10-16 에 GPU 노드 하나(8GB급 노트북 GPU)에서 1회용 파드로 잰 원자료를 2026-10-17 에 다시 세어 인용합니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 한 줄 요약: 주방은 하나, 홀 매니저와 냉장고는 바꿔 끼운다
- 2. 구조도
- 3. 코드 지도
- 4. 핵심 경로 따라가기
- 5. 사용자 기여와의 연결
- 6. 실측
- 7. 어디부터 읽을까
- 8. 첫 기여하기 좋은 곳
- 9. 한계
- 참고
1. 한 줄 요약: 주방은 하나, 홀 매니저와 냉장고는 바꿔 끼운다
vLLM 은 GPU 한 장(또는 몇 장)에서 모델을 빠르게 돌리는 주방입니다. 주방을 여러 개 차리면 주문을 어느 주방에 넣을지 정하는 홀 매니저, 주방마다 따로 노는 밑준비(KV 캐시)를 함께 쓰는 냉장고, 주방이 얼마나 바쁜지 알리는 주문판(메트릭) 이 필요합니다. 세 프로젝트는 주방(vLLM)은 그대로 두고 이 둘레를 각자 다른 방식으로 채웁니다.
| 구성요소 | 하는 일 | 어디서 도나 |
|---|---|---|
production-stack 라우터(vllm_router) |
OpenAI 호환 요청을 받아 엔진 파드 하나를 골라 그대로 흘려 보냄 | Deployment 하나, Python·FastAPI 프로세스 |
| production-stack 헬름 차트 | 엔진 Deployment·라우터·ServiceMonitor·Grafana 대시보드를 한 번에 설치 | helm install 한 번 |
vLLM KV 커넥터(KVConnectorBase_V1) |
엔진 스케줄러·워커가 외부 KV 저장소를 부르는 구멍 | 엔진 프로세스 안 |
| LMCache | 커넥터 구현. KV 를 256토큰 청크로 잘라 CPU·디스크·원격에 저장·조회 | 엔진 프로세스 안, 또는 별도 서버(MP 모드) |
| llm-d Router(프록시 + EPP) | 프록시는 연결만, EPP 가 파드를 골라 주소를 돌려줌 | Envoy 계열 게이트웨이 + EPP Deployment(Go) |
vLLM /metrics |
큐 길이·KV 사용률·프리픽스 캐시 적중 등을 Prometheus 형식으로 냄 | 엔진 API 서버 |
KV 캐시가 왜 필요하고 LMCache 가 무엇을 아껴 주는지는 LLM KV 캐시 해부 시리즈가 다룹니다. 이 편은 원리 대신 어느 파일의 어느 함수가 그 일을 하는지에 집중합니다.
2. 구조도
그림 읽는 법입니다.
- 앞단은 둘 중 하나입니다. 왼쪽 production-stack 라우터는 요청 본문을 직접 받아 엔진으로 프록시합니다. 오른쪽 llm-d 는 Envoy 같은 프록시가 본문을 들고 있고, EPP 는 ext-proc 콜백으로 「이 파드로 보내라」는 주소만 돌려줍니다.
- 두 앞단 모두 엔진의
/metrics를 주기적으로 읽어 파드의 바쁨을 압니다. llm-d EPP 는 여기에 더해 엔진이 ZMQ 로 쏘는 KV 이벤트(점선)를 구독해 「어느 파드가 어느 블록을 가졌나」를 실시간으로 압니다. - 엔진 안에서는 GPU 의 16토큰 블록이 1차 캐시입니다. 모자라면 KV 커넥터가 LMCache 를 부르고, LMCache 는 앞 계층부터 차례로 찾습니다.
- LMCache 를 엔진 밖 별도 서버로 띄우는 MP 모드에서는 맨 아래 상자가 통째로 다른 프로세스가 됩니다.
3. 코드 지도
먼저 「어느 저장소에 코드가 있나」부터 정리합니다. llm-d/llm-d 저장소에는 서비스 코드가 없습니다. v0.10.0 태그의 파일 1,315개 중 YAML 이 834개, Markdown 이 249개이고 Go 파일은 0개, Python 은 CI·도구·예제 스크립트 33개뿐입니다. 배포 가이드·헬름 값·설계 문서를 모은 곳이고, 실제 구성요소는 COMPONENTS.md 의 Roster 표가 가리키는 별도 저장소들에 있습니다. EPP 는 llm-d-router 입니다. 예전 이름 llm-d-inference-scheduler 주소는 지금 llm-d-router 로 넘어갑니다(2026-10-17 HTTP 301 확인). llm-d v0.10.0 의 guides/env.sh 가 EPP 이미지 v0.11.0 과 Gateway API Inference Extension(GAIE) v1.5.0 CRD 를 고정하므로, 이 글은 llm-d-router v0.11.0 을 따로 받아 읽었습니다. InferencePool 같은 API 타입은 GAIE 모듈에서 가져옵니다.
| 경로 | 무엇 | 처음 읽을 파일 |
|---|---|---|
production-stack src/vllm_router/ |
라우터 본체. 요청 받기·서비스 디스커버리·라우팅·통계 | app.py 의 initialize_all |
production-stack helm/ |
차트. 엔진 Deployment, 라우터, ServiceMonitor, 대시보드 ConfigMap | templates/deployment-vllm-multi.yaml |
production-stack helm/dashboards/ |
Grafana 대시보드 JSON 세 벌(vLLM·모델별·LMCache) | vllm-dashboard.json |
vLLM vllm/v1/metrics/ |
Prometheus 지표 정의와 기록 | loggers.py 의 PrometheusStatLogger |
vLLM vllm/distributed/kv_transfer/kv_connector/v1/ |
KV 커넥터 인터페이스와 구현들(LMCache·NIXL·Mooncake 등) | base.py |
LMCache lmcache/integration/vllm/ |
vLLM 커넥터의 실제 구현 | vllm_v1_adapter.py |
LMCache lmcache/v1/ |
캐시 엔진·토큰 청크·저장 백엔드·MP 서버 | cache_engine.py, storage_backend/__init__.py |
llm-d guides/, docs/architecture/ |
배포 레시피와 설계 문서. 코드 없음 | docs/architecture/core/router/README.md |
llm-d-router pkg/epp/ |
EPP: ext-proc 서버, 스케줄러, 필터·스코어러·피커 플러그인 | scheduling/scheduler.go |
llm-d-router pkg/kvevents/, pkg/kvcache/, pkg/sidecar/ |
KV 이벤트 구독·블록 색인, P/D 라우팅 사이드카 | kvevents/README.md |
4. 핵심 경로 따라가기
4.1 production-stack: 요청 하나가 엔진에 닿기까지
- 라우터는 시작할 때
initialize_all에서 서비스 디스커버리, 엔진 통계 수집기, 요청 통계, 라우팅 로직을 차례로 싱글턴으로 만듭니다. 라우팅 로직이 앞의 셋이 만든 표를 읽으므로 이 순서입니다. - 엔진 목록은
K8sPodIPServiceDiscovery._watch_engines가 만듭니다. 라벨 셀렉터로 파드를 watch 하고, 준비된 파드마다_get_model_names로/v1/models를 물어 「이 IP 는 어떤 모델을 낸다」를 기록합니다. Service 가 아니라 파드 IP 로 직접 붙는 이유는, 라우터가 파드를 골라야 하는데 Service 는 그 선택을 kube-proxy 에 넘기기 때문입니다. - 요청은
route_chat_completion을 거쳐route_general_request로 들어갑니다. 여기서 본문의model로 후보 엔진을 거르고, 라우팅 로직의route_request를 불러 URL 하나를 받은 뒤,process_request로 응답을 스트리밍합니다. 실패하면 그 URL 을 빼고 다시 고르는 재시도 고리도 이 함수 안에 있습니다. - 라우팅 로직은
RoutingLogic열거형의 여덟 가지입니다. 기본 축은RoundRobinRouter, 세션 키를 해시 링에 올리는SessionRouter, 프롬프트 문자열을 128자 청크로 해시해 트라이에 넣는PrefixAwareRouter(HashTrie), 그리고 LMCache 컨트롤러에 「이 토큰을 가장 길게 가진 인스턴스」를 묻는KvawareRouter입니다.
두 프리픽스 라우터의 차이가 이 프로젝트의 설계 고민을 잘 보여 줍니다. PrefixAwareRouter 는 라우터가 자기가 보낸 기록만으로 추측합니다. 클래스 주석이 스스로 「프리픽스 캐시 축출은 없다고 가정한다」고 밝히듯, 엔진이 블록을 지워도 라우터는 모릅니다. 대신 외부 의존이 없습니다. KvawareRouter 는 LMCache 컨트롤러에게 실제로 저장된 위치를 묻기 때문에 정확하지만, 라우터 이미지에 LMCache 패키지가 들어가야 합니다. 기본 라우터 이미지는 pyproject.toml 의 선택 의존 lmcache==0.3.11 을 깔아 엔진 쪽 0.5대와 판이 다른데, 둘의 컨트롤러 메시지가 맞는지는 확인하지 못했습니다(미확인).
헬름 차트는 이 라우터 인자를 그대로 펼칩니다. deployment-router.yaml 이 routerSpec.routingLogic 을 --routing-logic 으로 넘기고, deployment-vllm-multi.yaml 은 modelSpec 마다 vllm serve 명령을 조립합니다. LMCache 를 켜면 --kv-transfer-config 에 LMCacheConnectorV1 을, 크기는 LMCACHE_* 환경 변수로 줍니다. 엔진 인자가 문자열로 조립되므로, vLLM 이 플래그를 지우면 차트와 튜토리얼은 조용히 낡아 있다가 실행 순간 깨집니다. 5절의 기여가 이 지점입니다.
4.2 vLLM ↔ LMCache: 커넥터라는 구멍
vLLM v1 은 외부 KV 저장소를 KVConnectorBase_V1 하나로 추상화합니다. 이 클래스는 같은 객체가 두 역할로 만들어진다는 점이 핵심입니다.
- 스케줄러 쪽:
get_num_new_matched_tokens가 「GPU 에는 없지만 바깥에서 불러올 수 있는 토큰 수」를 답합니다. 스케줄러는schedule안에서(949줄) 이 값을 보고 그만큼은 계산하지 않기로 정한 뒤,update_state_after_alloc와build_connector_meta로 「무엇을 불러오고 무엇을 저장할지」 메타데이터를 만듭니다. - 워커 쪽: 그 메타데이터를 받아
start_load_kv로 불러오기를 시작하고, 층마다wait_for_layer_load·save_kv_layer를 부르며, 끝에wait_for_save로 저장을 맞춥니다.
왜 둘로 나눴을까요. 스케줄러는 GPU 텐서를 만지지 않고 배치만 짜며, 워커는 GPU 텐서를 직접 다룹니다. 둘은 서로 다른 프로세스에 있을 수도 있습니다. 「얼마나 불러올 수 있나」는 배치를 짜기 전에 알아야 하고, 실제 복사는 순전파와 겹쳐 돌려야 하므로, 판단과 실행을 메타데이터 한 덩이로 이어 붙인 구조입니다. 층 단위 함수는 한 층을 계산하는 동안 다음 층 KV 를 불러와 복사 시간을 가리려는 것입니다.
vLLM 트리 안의 LMCacheConnectorV1 은 얇은 껍데기입니다.
use_native = vllm_config.kv_transfer_config.get_from_extra_config(
"use_native", False
)
if use_native:
from vllm.distributed.kv_transfer.kv_connector.v1 import lmcache_integration
_adapter = lmcache_integration.vllm_v1_adapter
cls = _adapter.LMCacheConnectorV1Impl
else:
from lmcache.integration.vllm.vllm_v1_adapter import (
LMCacheConnectorV1Impl as LMCacheConnectorLatestImpl,
)
cls = LMCacheConnectorLatestImpl
self._lmcache_engine = cls(vllm_config, role, self)
출처: vllm v0.31.0 vllm/distributed/kv_transfer/kv_connector/v1/lmcache_connector.py(88–108줄, 로그 줄 생략), Apache-2.0
기본값은 설치된 LMCache 패키지 쪽 구현입니다. 구현을 vLLM 에 박아 두면 LMCache 가 고칠 때마다 vLLM 릴리스를 기다려야 하므로, 인터페이스는 vLLM 이, 구현은 LMCache 가 갖습니다. MP 커넥터도 lmcache_mp_connector.py 에서 LMCache 쪽 구현을 먼저 찾고, 없을 때만 트리 안 사본을 씁니다.
LMCache 쪽 실제 구현은 LMCacheConnectorV1Impl 입니다. 조회는 get_num_new_matched_tokens 에서 시작해 lookup 클라이언트를 거치고, 토큰은 ChunkedTokenDatabase 가 _chunk_tokens 로 자르고 _prefix_hash 로 앞 청크 해시를 이어 가며 키를 만듭니다. 청크 기본값은 config.py 의 256토큰으로 vLLM GPU 블록(실측 16토큰)의 16배입니다. 원격 저장소의 객체 수와 왕복을 줄이려면 덩어리가 커야 하기 때문이고, 그 대가로 256토큰보다 짧게 겹치는 앞부분은 LMCache 에서 적중하지 않습니다.
저장 계층은 CreateStorageBackends 가 OrderedDict 에 넣는 순서가 곧 조회 순서입니다(PD, LocalCPU, P2P, NIXL, LocalDisk, GDS, Maru, 원격 중 켜진 것). StorageManager.batched_get 은 앞에서부터 찾고, 원격에서 찾은 것은 LocalCPU 에 다시 써 둡니다. 원격 쪽은 storage_backend/connector/ 의 어댑터 열일곱 개(Redis·Valkey·S3·파일시스템·Mooncake 등)로 갈립니다.
MP 모드는 이 계층 전체를 엔진 밖으로 꺼낸 것입니다. MPCacheServer 가 여러 엔진 모듈을 조립하고, MPServerConfig 의 기본 전송은 ZMQ(포트 5555), 청크는 역시 256, 해시는 blake3 입니다. 엔진이 재시작해도 캐시가 남고 엔진 여럿이 나눠 쓰게 하려는 것입니다.
4.3 llm-d: EPP 가 파드를 고르는 법
llm-d Router 는 프록시와 EPP 로 나뉩니다. 프록시가 요청을 붙잡아 두고 ext-proc 스트림으로 EPP 에 묻으면, EPP 의 StreamingServer.Process 가 받아 스케줄러를 돌리고, generateHeaders 가 x-gateway-destination-endpoint 헤더에 고른 파드 주소를 실어 돌려보냅니다. 본문 전달·TLS·재시도는 검증된 프록시에 맡기고 LLM 특화 판단만 떼어 낸 구조로, 둘을 한 Python 프로세스에서 다 하는 production-stack 라우터와 정반대입니다.
스케줄러 Scheduler.Schedule 은 「프로필 핸들러가 고른 프로필을 돌리고, 결과를 보고 또 고르는」 고리입니다. 프로필 하나는 SchedulerProfile.Run 안에서 필터 → 가중 스코어러(runScorerPlugins) → 피커를 거칩니다. 프로필을 여러 번 돌릴 수 있게 한 이유가 P/D 분리입니다. disagg 핸들러의 Pick 은 디코드 프로필로 D 파드를 먼저 고르고, 필요하면 프리필 프로필로 P 파드를 고릅니다. 실제 P→D 흐름은 디코드 파드 옆의 라우팅 사이드카(llm-d-router pkg/sidecar/proxy/)가 맡아, P 에 max_tokens=1 로 먼저 보내고 받은 KV 전송 정보를 붙여 D 로 넘깁니다. P/D 분리 자체의 원리는 NVIDIA Dynamo 편에 있습니다.
스코어러가 읽는 숫자는 두 통로로 들어옵니다. 하나는 엔진 /metrics 를 긁는 datalayer 소스로, parseMetrics 가 Prometheus 텍스트를 파싱하고, 엔진 종류별 이름표가 어떤 이름을 어떤 속성으로 옮길지 정합니다.
var defaultEngineConfigs = []engineConfigParams{
{
Name: "vllm",
QueuedRequestsSpec: "vllm:num_requests_waiting",
RunningRequestsSpec: "vllm:num_requests_running",
KVUsageSpec: "vllm:kv_cache_usage_perc",
LoRASpec: "vllm:lora_requests_info",
CacheInfoSpec: "vllm:cache_config_info",
},
출처: llm-d-router v0.11.0 pkg/epp/framework/plugins/datalayer/extractor/metrics/factories.go(91–99줄), Apache-2.0
같은 표에 SGLang·TensorRT-LLM·Triton 이름이 나란히 있고, 파드 라벨 llm-d.ai/engine-type 으로 표를 고릅니다. 엔진이 이름을 바꿔도 이 표 한 곳만 고치면 되게 한 설계입니다. 이 값을 받아 KVCacheUtilizationScorer.Score 는 1 − KV 사용률 을 점수로 줍니다.
다른 하나는 KV 이벤트입니다. vLLM 은 블록을 저장·축출할 때 BlockStored 같은 이벤트를 ZmqEventPublisher 로 발행하고, EPP 의 precise-prefix-cache 플러그인은 ensureSubscriber 로 파드마다 구독을 엽니다. 받은 메시지는 VLLMAdapter.ParseMessage 가 풀어 블록 색인에 더하거나 뺍니다. production-stack PrefixAwareRouter 의 「보낸 기록으로 추측」을 엔진이 알려 준 사실로 바꾼 것이고, 대가로 엔진에 --kv-events-config 를 켜고 ZMQ 포트를 열어야 합니다.
4.4 메트릭 경로: 정의 한 줄이 세 곳의 쿼리가 된다
그림의 PS 는 production-stack 입니다. 출발점은 PrometheusStatLogger 생성자로, vllm: 지표가 한 줄씩 정의돼 있습니다. KV 사용률은 vllm:kv_cache_usage_perc, 프리픽스 캐시는 vllm:prefix_cache_queries·vllm:prefix_cache_hits, 토큰 사이 지연은 vllm:inter_token_latency_seconds 입니다. 소스의 카운터 이름에는 _total 이 없지만 prometheus_client 가 내보낼 때 _total 과 _created 를 붙이므로, 쿼리를 쓰는 쪽은 vllm:prefix_cache_hits_total 로 읽어야 합니다. 이 차이가 이름 혼선의 한 원인입니다. 이 레지스트리는 attach_router 가 /metrics 경로에 붙입니다.
vLLM 은 프리픽스 캐시를 「적중률 게이지」 대신 「조회·적중 카운터 둘」로 냅니다. 사용자가 rate(hits[5m]) / rate(queries[5m]) 처럼 원하는 구간으로 비율을 내게 하려는 것입니다(docs/design/metrics.md). 지표를 없앨 때는 X.Y 폐기 예고, X.Y+1 숨김(--show-hidden-metrics-for-version 로 되살림), X.Y+2 삭제 규칙(docs/usage/metrics.md)을 따릅니다. 엔진 쪽 규칙은 분명한데, 이 이름을 읽는 쪽이 다른 저장소에 있다는 것이 문제입니다.
첫째 소비자, production-stack 라우터의 수집기 EngineStatsScraper 는 기본 30초마다 엔진마다 /metrics 를 GET 하고 from_vllm_scrape 로 이름을 정확히 맞춰 꺼냅니다.
for family in text_string_to_metric_families(vllm_scrape):
for sample in family.samples:
if sample.name == "vllm:num_requests_running":
num_running_reqs = sample.value
elif sample.name == "vllm:num_requests_waiting":
num_queuing_reqs = sample.value
elif sample.name == "vllm:gpu_prefix_cache_hit_rate":
gpu_prefix_cache_hit_rate = sample.value
elif sample.name == "vllm:gpu_prefix_cache_hits_total":
gpu_prefix_cache_hits_total = sample.value
elif sample.name == "vllm:gpu_prefix_cache_queries_total":
gpu_prefix_cache_queries_total = sample.value
elif sample.name == "vllm:gpu_cache_usage_perc":
gpu_cache_usage_perc = sample.value
출처: production-stack vllm-stack-0.1.13 src/vllm_router/stats/engine_stats.py(63–76줄), Apache-2.0
맞는 이름이 없으면 예외도 로그도 없이 0 이 남습니다. 라우터는 이 값을 metrics_router.py 의 /metrics 로 같은 vllm: 접두사를 단 게이지(server 라벨)로 다시 냅니다. 그래서 같은 이름이 엔진·라우터 두 출처에서 라벨만 다르게 섞이고, 기본 대시보드의 프리픽스 캐시 패널은 endpoint="service-port" 로 거릅니다. service-port 는 차트가 엔진 Service 포트에 붙이는 이름이고, 라우터 Service 포트 이름은 router-sport 이므로 엔진에서 온 시계열만 남깁니다.
둘째 소비자는 Prometheus 와 Grafana 입니다. 차트의 ServiceMonitor 가 엔진과 라우터를 긁고, 대시보드 JSON 의 PromQL 이 이름을 문자열로 들고 있습니다. 셋째 소비자는 4.3 절의 EPP 이름표입니다. 셋 다 이름이 틀리면 조용히 0 이나 「No data」가 될 뿐 실패하지 않습니다.
5. 사용자 기여와의 연결
글쓴이의 PR 네 건은 모두 「vLLM 이 지운 이름을 다른 저장소가 아직 부른다」는 같은 모양입니다(상태는 2026-10-17 기준). 이 글에서는 v0.31.0 태그만 다시 열어 「지금은 없고, 대신 이것이 있다」를 확인했습니다.
| PR | 고친 곳 | 이어지는 vLLM v0.31.0 줄 | 상태 |
|---|---|---|---|
| vllm-project/production-stack#1147 | helm/dashboards/vllm-dashboard.json:826 「GPU KV Cache Hit Rate」 패널의 vllm:gpu_prefix_cache_* |
loggers.py:600 vllm:prefix_cache_queries, loggers.py:611 vllm:prefix_cache_hits |
리뷰 대기(자동 리뷰는 「고칠 점 없음」) |
| vllm-project/production-stack#1148 | tutorials/23-whisper-api-transcription.md:24 의 --task transcription |
--task 는 없고 그 자리를 arg_utils.py:930 의 --runner·--convert 가 이어받음 |
리뷰 대기 |
| LMCache/LMCache#5814 | 문서·벤치마크·예제 12개 파일(v0.5.6 태그에서 20곳)의 --disable-log-requests |
arg_utils.py:3141 에는 --enable-log-requests 만 있고, BooleanOptionalAction 이라 반대말은 --no-enable-log-requests |
리뷰 대기(base dev) |
| llm-d/llm-d#2835 | 문서의 vllm:time_per_output_token_seconds, vllm:gpu_prefix_cache_hit_rate |
loggers.py:832 vllm:inter_token_latency_seconds, 적중률은 600·611줄 카운터 둘의 비율 |
리뷰 대기 |
#1147 은 대시보드만 고쳤고, 라우터 파서(engine_stats.py:69–76)의 같은 문제는 PR 본문에 적었듯 #1140·#1146 이 따로 다룹니다. llm-d#2835 는 main 기준이라, v0.10.0 태그에서는 같은 문장이 docs/architecture/advanced/autoscaling/wva.md:144·210줄 과 DigitalOcean 대시보드 1544줄 근처에 있고, 세 번째로 고친 가이드(turn-priority-fairness)는 v0.10.0 에 아직 없습니다. 경로가 다른 까닭은 확인하지 않았습니다(미확인).
6. 실측
6.1 무엇을 어떻게 쟀나
2026-10-16 에 작은 클러스터의 GPU 노드 하나(8GB급 노트북 GPU)에 1회용 파드를 띄워 쟀습니다. 측정 스크립트(pod.sh)는 노드를 고정하고, 이미 있는 이미지만 쓰고(imagePullPolicy: Never), Hugging Face 접속을 막고, 메모리 한도 3.5Gi·90분 마감을 줍니다. 모델은 설정 파일만 만든 무작위 가중치 Qwen2 구조(2층, hidden 256, --load-format dummy)입니다. 이미지는 vllm/vllm-openai v0.31.0(PyTorch 2.13, CUDA 13.0)이고, 앞부분 160토큰이 같은 요청 5개를 보낸 뒤 /metrics 를 받았습니다. 라우터·LMCache·EPP 는 띄우지 않았으므로 6.3 절은 각 태그 소스에서 뽑은 이름을 이 출력과 맞춘 정적 대조입니다.
6.2 /metrics 출력 세기
| 항목 | 값 |
|---|---|
| 전체 줄 | 587 |
# TYPE 패밀리 |
88 (vllm 68, http 10, process 6, python 4) |
vllm: 패밀리 중 _created 보조 게이지 |
31 |
vllm: 실제 지표 |
37 (카운터 15, 히스토그램 16, 게이지 6) |
vllm:prefix_cache_queries_total / _hits_total |
805 / 640 → 적중률 0.795 |
vllm:inter_token_latency_seconds_count |
75 |
vllm:cache_config_info |
block_size="16", num_gpu_blocks="144272", gpu_memory_utilization="0.3" |
옛 이름(gpu_prefix_cache*, gpu_cache_usage_perc, time_per_output_token_seconds) |
0개 |
숫자는 맞아떨어집니다. 요청 5개 × 161토큰 = 805 조회이고, 첫 요청은 다 놓치고 나머지 넷이 160토큰(블록 10개)씩 적중해 640 입니다. 16토큰씩 만든 요청 5개의 토큰 사이 간격은 15 × 5 = 75 입니다. 블록 144,272개 × 16 = 2,308,352 도 같은 줄의 kv_cache_size_tokens 와 같습니다(모델이 아주 작아 블록이 많습니다).
6.3 소비자가 찾는 이름이 출력에 있나
| 소비자(태그 소스) | 찾는 vllm: 이름 |
출력에 있음 | 없음과 그 이유 |
|---|---|---|---|
production-stack 라우터 파서 engine_stats.py |
6 | 2 | 4: gpu_prefix_cache_hit_rate·_hits_total·_queries_total·gpu_cache_usage_perc, v0.31.0 소스에 정의 없음 |
production-stack vllm-dashboard.json |
13 | 9 | 4: current_qps·healthy_pods_total 은 라우터 자신의 지표(정상), gpu_prefix_cache_* 2개는 사라진 이름(#1147) |
production-stack vllm-model-metrics-dashboard.json |
21 | 21 | 0 |
| llm-d-router EPP 기본 vLLM 이름표 | 5 | 4 | 1: lora_requests_info, LoRA 를 켜야 나오는 조건부 지표(loggers.py:993) |
| llm-d v0.10.0 문서·가이드 전체 | 71 | 40 | 31: 27개는 오프로드·NIXL 커넥터를 켜야 나오는 조건부 지표, 4개는 v0.31.0 소스에 정의 없음 |
llm-d 문서의 「정의 없음」 4개는 time_per_output_token_seconds 계열 3개(#2835 가 고친 것)와 vllm:kv_offload_tiering_block_hits_total 하나입니다. 마지막 것은 8절의 기여 후보로 남겼습니다.
6.4 사라진 플래그
| 명령 | 결과 |
|---|---|
vllm serve <작은 모델> --disable-log-requests |
unrecognized arguments, 종료 코드 2 |
vllm serve <작은 모델> --swap-space 0 |
unrecognized arguments, 종료 코드 2 |
vllm serve 전체 도움말에서 뽑은 플래그 목록(약 400개) |
--task·--disable-log-requests·--swap-space 없음, --enable-log-requests·--no-enable-log-requests·--runner·--kv-transfer-config·--kv-events-config·--show-hidden-metrics-for-version 있음 |
--task 없이 무작위 가중치 Whisper 구조로 vllm serve |
로그 Supported tasks: ['transcription'], /v1/audio/transcriptions HTTP 200 |
같은 날 읽은 차트 기본 엔진 이미지(lmcache/vllm-openai:latest)는 vLLM 0.29.0·LMCache 0.5.5 였고, 그 안 loggers.py 에도 gpu_prefix_cache 는 없었습니다. 차트 기본값 그대로여도 라우터 파서와 프리픽스 캐시 패널은 0 이나 No data 를 볼 것입니다. 차트를 실제 설치해 Grafana 를 본 것은 아닙니다(미실측).
7. 어디부터 읽을까
vllm/v1/metrics/loggers.py의PrometheusStatLogger생성자를 위에서 아래로 훑습니다. 엔진이 무엇을 세는지가 나머지 모든 것의 입력입니다.vllm_router/app.py의initialize_all에서 시작해request.py의route_general_request로 요청 하나를 끝까지 따라갑니다.routing_logic.py에서RoutingInterface와 구현 두셋을 비교합니다. 테스트(src/tests/test_*_router.py)를 같이 보면 기대 동작이 빨리 잡힙니다.kv_connector/v1/base.py맨 위 주석(스케줄러 쪽·워커 쪽 함수 목록)을 읽은 뒤 LMCachevllm_v1_adapter.py의 같은 이름 함수로 건너갑니다.- llm-d 는
docs/architecture/core/router/README.md로 용어를 잡고, 코드는 llm-d-routerscheduler.go와factories.go를 봅니다.
8. 첫 기여하기 좋은 곳
소스를 읽다 찾은 것입니다. 이미 열린 PR·이슈가 있는지는 확인하지 않았으니 올리기 전에 먼저 검색해야 합니다.
- production-stack 클라우드별 대시보드 사본. 헬름 차트 대시보드는 #989 와 #1147 로 고쳐 가는 중이지만,
tutorials/terraform/{coreweave,eks,nebius}/config/의vllm-dashboard*.json네 벌에는vllm:gpu_cache_usage_perc와vllm:gpu_prefix_cache_*가 모두 10곳 남아 있습니다(예:eks/config/vllm-dashboard.json:732). - production-stack 벤치마크 튜토리얼의
--swap-space 0.07-benchmark-multi-round-qa-single-gpu.md:51등 07·08번 튜토리얼 네 곳이extraArgs: ["--swap-space", 0]를 줍니다. v0.31.0 은 이 플래그에서 종료 코드 2 로 멈춥니다(6.4 절). - llm-d P2P 가이드의 지표 이름.
guides/p2p-kv-cache-sharing/benchmarking/README.md:129는vllm:kv_offload_tiering_block_hits_total을 쓰는데, v0.31.0 에는vllm:kv_offload_tiering_chunk_hits만 정의돼 있습니다. 가이드가 기대는 vLLM 판(vllm#48798 이후)에서 이름이 바뀐 것인지부터 확인해야 합니다. KvawareRouter의 채팅 요청.route_request는request_json.get("prompt", "")만 토큰화하고TODO: Handle chat completions를 남겨 두었습니다./v1/chat/completions요청은 빈 프롬프트로 조회되어 세션·QPS 라우팅으로 떨어질 것으로 보입니다. 같은 파일PrefixAwareRouter의messages처리를 참고할 수 있습니다.- 기본 대시보드의 섞인 시계열(미확인). 「Number of Running Requests」 패널은 라벨 없이
vllm:num_requests_running을 그립니다. 라우터도 같은 이름을 내므로(4.4 절) 두 시계열이 함께 그려질 수 있습니다. 실제 화면을 먼저 봐야 합니다.
프로젝트 안내: production-stack 은 큰 변경이면 proposals/ 에 제안서부터, 커밋에는 DCO Signed-off-by 를 요구합니다. LMCache 는 기본 브랜치가 dev, DCO 필수이고 입문용 이슈 모음(LMCache#627)이 있으며, AGENTS.md 와 코딩 기준에 「AI 가 만든 코드 신호」 리뷰 항목을 둡니다. llm-d 는 AI 를 쓴 기여도 같은 기준으로 받되 제출자가 내용을 직접 설명할 수 있어야 한다고 적습니다. 글쓴이의 PR 은 모두 본문 끝에 AI 도움 사실을 밝혔습니다.
9. 한계
- 라우터·LMCache·EPP 를 실제로 띄워 요청을 흘려 본 측정은 없습니다(미실측). 6.3 절 대조는 태그 소스의 문자열과 엔진 출력 하나를 맞춘 정적 대조입니다.
- 실측 엔진은 무작위 가중치의 아주 작은 모델 하나, GPU 하나입니다. LoRA·스펙 디코딩·KV 커넥터를 켜야 나오는 지표와 다중 API 서버의 멀티프로세스 지표 모드는 보지 않았습니다.
- production-stack 의 Go 오퍼레이터, 시맨틱 캐시·PII 같은 실험 기능, LMCache 의 CacheBlend·컨트롤러 내부, llm-d 의 오토스케일러(WVA)와 배치 게이트웨이는 다루지 않았습니다.
- 옛 이름·플래그가 어느 판에서 숨겨지고 지워졌는지는 v0.31.0 이전 태그를 열지 않고 PR 본문의 조사를 옮겼습니다.
- 성능 수치(TTFT·처리량)는 이 편의 범위가 아닙니다. LMCache 실측은 LMCache 편, vLLM 부하 실험은 vLLM 부하 시험에 있습니다.
티티 한마디. 주방은 그대로인데 메뉴판 이름만 바뀌어도 홀 매니저는 「그런 요리 없어요」 대신 빈 접시를 내와요. 그래서 이름 하나 고치는 PR 이 생각보다 큰 구멍을 막아요!
참고
열람일은 모두 2026-10-17 입니다.
- production-stack vllm-stack-0.1.13:
src/vllm_router/,helm/,CONTRIBUTING.md,pyproject.toml - vLLM v0.31.0:
vllm/v1/metrics/loggers.py,kv_connector/v1/base.py,kv_connector/v1/lmcache_connector.py,kv_events.py,engine/arg_utils.py,docs/design/metrics.md,docs/usage/metrics.md - LMCache v0.5.6:
lmcache/integration/vllm/vllm_v1_adapter.py,lmcache/v1/token_database.py,lmcache/v1/storage_backend/,lmcache/v1/multiprocess/,CONTRIBUTING.md,AGENTS.md - llm-d v0.10.0:
COMPONENTS.md,docs/architecture/,docs/architecture/advanced/disaggregation/README.md,guides/env.sh,CONTRIBUTING.md - llm-d-router v0.11.0:
pkg/epp/,pkg/kvevents/,pkg/sidecar/ - 같은 사이트: LLM KV 캐시 해부 2편 서빙 엔진 안의 KV 캐시, 3편 LMCache, 4편 NVIDIA Dynamo
인용한 코드: vLLM·production-stack·llm-d-router 모두 Apache License 2.0 입니다.