TT Lab
요금제

한국어

시작하기
블로그

블로그

vLLM production-stack · LMCache · llm-d: 엔진 앞단과 KV 캐시 계층의 코드 지도

대화로 듣기 · 10분두 사람이 이 글을 풀어 줍니다

상태: 초안 (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. 한 줄 요약: 주방은 하나, 홀 매니저와 냉장고는 바꿔 끼운다

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. 구조도

요청 경로: 앞단 → vLLM 엔진 ↔ LMCache 계층 앞단은 둘 중 하나 클라이언트 production-stack 라우터 Python · FastAPI 한 프로세스 roundrobin session prefixaware kvaware k8s 파드 감시 + /metrics 수집 llm-d Router 프록시(Envoy 등) + EPP(Go) 필터 점수 고르기 ext-proc 로 파드 주소만 돌려주고 전달은 프록시가 KV 이벤트(ZMQ) 구독 vLLM 엔진 파드 스케줄러 워커(GPU) KV 커넥터: LMCache 구현 GPU 안 KV 블록(16토큰) vLLM 엔진 파드 스케줄러 워커(GPU) KV 커넥터: LMCache 구현 GPU 안 KV 블록(16토큰) LMCache: 256토큰 청크 단위로 저장·조회 LocalCPU LocalDisk 원격 백엔드 P2P·PD 앞 계층부터 찾고, 원격에서 찾으면 CPU 에 올려 둔다 MP 모드는 이 계층을 엔진 밖 별도 서버(ZMQ)로 뺀다
그림: 요청 경로: 클라이언트가 production-stack 라우터 또는 llm-d Router 를 거쳐 vLLM 엔진 파드에 닿고, 엔진은 KV 커넥터로 LMCache 계층(LocalCPU·LocalDisk·원격·P2P)을 부른다

그림 읽는 법입니다.

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: 요청 하나가 엔진에 닿기까지

  1. 라우터는 시작할 때 initialize_all 에서 서비스 디스커버리, 엔진 통계 수집기, 요청 통계, 라우팅 로직을 차례로 싱글턴으로 만듭니다. 라우팅 로직이 앞의 셋이 만든 표를 읽으므로 이 순서입니다.
  2. 엔진 목록은 K8sPodIPServiceDiscovery._watch_engines 가 만듭니다. 라벨 셀렉터로 파드를 watch 하고, 준비된 파드마다 _get_model_names 로 /v1/models 를 물어 「이 IP 는 어떤 모델을 낸다」를 기록합니다. Service 가 아니라 파드 IP 로 직접 붙는 이유는, 라우터가 파드를 골라야 하는데 Service 는 그 선택을 kube-proxy 에 넘기기 때문입니다.
  3. 요청은 route_chat_completion 을 거쳐 route_general_request 로 들어갑니다. 여기서 본문의 model 로 후보 엔진을 거르고, 라우팅 로직의 route_request 를 불러 URL 하나를 받은 뒤, process_request 로 응답을 스트리밍합니다. 실패하면 그 URL 을 빼고 다시 고르는 재시도 고리도 이 함수 안에 있습니다.
  4. 라우팅 로직은 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 하나로 추상화합니다. 이 클래스는 같은 객체가 두 역할로 만들어진다는 점이 핵심입니다.

왜 둘로 나눴을까요. 스케줄러는 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 메트릭 경로: 정의 한 줄이 세 곳의 쿼리가 된다

메트릭 경로: 정의 한 줄이 세 곳의 쿼리가 된다 vLLM 엔진 · vllm/v1/metrics/loggers.py Counter(name="vllm:prefix_cache_hits") GET /metrics 에서 _total · _created 가 붙어 나간다 ① PS 라우터 engine_stats.py 30초마다 직접 긁음 이름 정확 일치 ② Prometheus ServiceMonitor service-port 포트 PromQL 로 질의 ③ llm-d EPP datalayer 소스 factories.go 이름표 파드마다 폴링 라우터 /metrics 로 같은 접두사 재노출 Grafana 대시보드 JSON 3벌 KV 사용률 점수 = 1 − 사용률 이름이 바뀌면 오류 없이 0 이나 No data 가 된다 옛 이름 v0.31.0 의 이름 gpu_cache_usage_perc → kv_cache_usage_perc gpu_prefix_cache_hits/queries → prefix_cache_hits/queries time_per_output_token_seconds → inter_token_latency_seconds 실측 /metrics(v0.31.0)에 옛 이름은 하나도 없었다
그림: 메트릭 경로: vLLM loggers.py 의 정의가 /metrics 로 나가 production-stack 라우터 파서, Prometheus·Grafana 대시보드, llm-d EPP 이름표 세 곳에서 이름 그대로 읽히고, 이름이 바뀌면 오류 없이 값이 비게 된다

그림의 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. 어디부터 읽을까

  1. vllm/v1/metrics/loggers.py 의 PrometheusStatLogger 생성자를 위에서 아래로 훑습니다. 엔진이 무엇을 세는지가 나머지 모든 것의 입력입니다.
  2. vllm_router/app.py 의 initialize_all 에서 시작해 request.py 의 route_general_request 로 요청 하나를 끝까지 따라갑니다.
  3. routing_logic.py 에서 RoutingInterface 와 구현 두셋을 비교합니다. 테스트(src/tests/test_*_router.py)를 같이 보면 기대 동작이 빨리 잡힙니다.
  4. kv_connector/v1/base.py 맨 위 주석(스케줄러 쪽·워커 쪽 함수 목록)을 읽은 뒤 LMCache vllm_v1_adapter.py 의 같은 이름 함수로 건너갑니다.
  5. llm-d 는 docs/architecture/core/router/README.md 로 용어를 잡고, 코드는 llm-d-router scheduler.go 와 factories.go 를 봅니다.

8. 첫 기여하기 좋은 곳

소스를 읽다 찾은 것입니다. 이미 열린 PR·이슈가 있는지는 확인하지 않았으니 올리기 전에 먼저 검색해야 합니다.

  1. 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).
  2. 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 절).
  3. 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 이후)에서 이름이 바뀐 것인지부터 확인해야 합니다.
  4. KvawareRouter 의 채팅 요청. route_request 는 request_json.get("prompt", "") 만 토큰화하고 TODO: Handle chat completions 를 남겨 두었습니다. /v1/chat/completions 요청은 빈 프롬프트로 조회되어 세션·QPS 라우팅으로 떨어질 것으로 보입니다. 같은 파일 PrefixAwareRouter 의 messages 처리를 참고할 수 있습니다.
  5. 기본 대시보드의 섞인 시계열(미확인). 「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. 한계

티티 한마디. 주방은 그대로인데 메뉴판 이름만 바뀌어도 홀 매니저는 「그런 요리 없어요」 대신 빈 접시를 내와요. 그래서 이름 하나 고치는 PR 이 생각보다 큰 구멍을 막아요!

참고

열람일은 모두 2026-10-17 입니다.

인용한 코드: vLLM·production-stack·llm-d-router 모두 Apache License 2.0 입니다.

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

댓글

아직 댓글이 없습니다.

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