KV Cache 최적화¶
Continuum Router는 LLM 백엔드의 중복 연산을 줄이는 4단계 KV cache 최적화 시스템을 구현합니다. 네 단계는 서로 쌓아 올린 계층이 아니라 각각 독립적으로 켜고 끄는 스위치입니다. 저마다 자기 설정 섹션을 가지며, 켜진 단계가 정해진 순서로 참조됩니다. 순서는 Tier 2 응답 캐시, Tier 4 스코어러, 그다음 설정된 selection_strategy이고, 이 전략이 PrefixAwareHash일 때만 Tier 1이 됩니다.
목차¶
- 개요
- 4단계 캐싱 전략
- Tier 1: Prefix 인식 고정 라우팅
- Tier 2: 응답 캐시
- Tier 3: 공유 외부 캐시
- Tier 4: Backend KV Cache Index
- Backend 선택 파이프라인
- Gemini Context Cache
- 설정 레퍼런스
- 메트릭
- 관리자 엔드포인트
- 배포 가이드
- 성능 특성
개요¶
최신 LLM 추론 엔진(vLLM, TensorRT-LLM, SGLang)은 요청의 token prefix에 대해 계산된 attention key-value 텐서를 GPU 메모리의 KV cache에 저장합니다. 동일한 prefix가 다시 나타나면 엔진은 해당 텐서의 재계산을 건너뛸 수 있으며, 긴 system prompt나 반복되는 컨텍스트의 경우 상당한 GPU 시간을 절약할 수 있습니다.
Continuum Router는 네 가지 상호 보완적인 메커니즘을 통해 백엔드 간 KV cache 재사용을 극대화합니다:
- Prefix 인식 고정 라우팅 — 동일한 prompt prefix를 공유하는 요청을 consistent hashing을 통해 같은 백엔드로 라우팅하여 GPU KV cache를 warm 상태로 유지합니다.
- 응답 캐시 — 반복되는 결정적(deterministic) 요청을 백엔드에 전달하지 않고 라우터 메모리 또는 Redis에서 직접 제공합니다.
- 공유 외부 캐시 — 응답 캐시 상태를 Redis/Valkey에 저장하여 여러 라우터 인스턴스가 동일한 캐시 항목을 공유합니다.
- Backend KV Cache Index — 최근 prefix에 대해 실제로 GPU에 상주하는 KV 텐서를 보유한 백엔드를 추적하여, 실제 캐시 상태에 기반한 세밀한 라우팅 결정을 가능하게 합니다.
4단계 캐싱 전략¶
flowchart TD
Client([클라이언트 요청])
RC{응답\n캐시 히트?}
PRL{Prefix key\n사용 가능?}
REG{KV index 스코어러\n등록됨?}
SCORE[복합 점수\noverlap + load + health]
THRESH{최고 복합 점수\n> 0.3?}
STRATEGY[설정된 selection_strategy]
PAH{PrefixAwareHash:\nprefix key 있음?}
CHWBL[CHWBL Hash Ring\nPrefix → Backend, 부하 상한]
MHASH[모델명 일관 해싱\n부하 상한 없음]
BACKEND([선택된 Backend])
STORE_RC[응답을\n캐시에 저장]
Client --> RC
RC -->|HIT| Client
RC -->|MISS| PRL
PRL -->|Yes| REG
REG -->|Yes| SCORE
SCORE --> THRESH
THRESH -->|Yes| BACKEND
THRESH -->|No| STRATEGY
REG -->|No| STRATEGY
PRL -->|No| STRATEGY
STRATEGY -->|PrefixAwareHash| PAH
STRATEGY -->|그 외 전략| BACKEND
PAH -->|Yes| CHWBL
PAH -->|No| MHASH
CHWBL --> BACKEND
MHASH --> BACKEND
BACKEND --> STORE_RC
STORE_RC --> Client 각 단계는 상호 배타적이지도 않고 서로 쌓여 있지도 않습니다. Tier 2는 백엔드에 연결하기 전에 요청 전체를 가로채고, Tier 3는 그 아래에서 스토리지 기반 역할만 합니다. Tier 4는 backend 선택 안에서 가장 먼저 실행되는데, 켜지는 조건은 prefix_routing.enabled(prefix key를 만듭니다)와 kv_cache_index.enabled(인덱스를 만듭니다)이지 selection_strategy가 아닙니다. 스코어러가 정하지 못하면 설정된 전략이 정하며, 그 전략이 PrefixAwareHash일 때만 Tier 1이 됩니다.
Tier 1: Prefix 인식 고정 라우팅¶
Tier 1은 공통 prompt prefix를 공유하는 요청을 동일한 백엔드로 라우팅하여, 해당 백엔드의 GPU KV cache가 이미 해당 token에 대해 warm 상태일 확률을 극대화합니다.
Prefix Key 추출¶
각 수신 chat completion 요청에 대해 라우터는 요청의 의미적 앵커를 고유하게 식별하는 32바이트 SHA256 다이제스트인 prefix key를 추출합니다.
추출 로직은 OpenAI와 Anthropic 요청 형식을 모두 처리합니다:
| 형식 | 기본 앵커 | 폴백 앵커 |
|---|---|---|
| OpenAI | messages[].role == "system" 콘텐츠 | 첫 번째 비시스템 메시지 |
| Anthropic | 최상위 system 문자열 또는 content-block 배열 | 첫 번째 비시스템 메시지 |
해시는 다음과 같이 계산됩니다:
# System prompt가 있는 경우:
SHA256(model_bytes ++ "\x00" ++ "S" ++ system_bytes[:max_prefix_length])
# System prompt가 없는 경우 (첫 번째 메시지 폴백):
SHA256(model_bytes ++ "\x00" ++ "M" ++ first_msg_bytes[:max_prefix_length])
\x00 구분자는 모델명과 콘텐츠 간의 length-extension 충돌을 방지합니다. 태그 바이트 S/M은 동일한 텍스트가 system prompt로 나타나는 경우와 첫 번째 사용자 메시지로 나타나는 경우 같은 해시값이 되는 것을 방지합니다.
max_prefix_length 매개변수(기본값: 1024바이트)는 해싱 전에 콘텐츠를 잘라내며, 멀티바이트 문자를 분할하지 않도록 UTF-8 경계를 인식합니다.
구현: src/core/prefix_key.rs, src/core/hashing.rs
Consistent Hashing with Bounded Loads (CHWBL)¶
PrefixAwareHash 선택 전략은 consistent hash ring을 사용하여 prefix key를 백엔드에 매핑합니다. 단순 consistent hashing은 특정 prefix가 다른 것보다 훨씬 인기 있을 때 불균등한 부하 분배를 야기할 수 있습니다. Continuum Router는 Consistent Hashing with Bounded Loads (CHWBL) 알고리즘으로 이를 해결합니다.
CHWBL은 부하 상한을 추가합니다: 각 백엔드는 최대 (1 + epsilon) * average_load 요청을 동시에 처리할 수 있습니다. 백엔드가 부하 상한에 도달하면 요청은 ring에서 시계 방향으로 다음 노드로 오버플로됩니다.
ring은 백엔드당 virtual_nodes(기본값: 150) 가상 복제본으로 채워져 key 분배 균등성을 향상시킵니다. virtual_nodes는 핫 리로드로 실행 중인 풀에 적용됩니다. 값을 바꾸면 재시작 없이 새 크기로 consistent hash ring을 다시 만듭니다.
prefix key가 없을 때¶
위의 CHWBL ring은 요청이 실제로 prefix key를 들고 있을 때만 도달합니다. key가 없는 경우는 두 가지입니다.
prefix_routing.enabled가 기본값인false입니다. 두 추출 함수request_prefix_key와typed_request_prefix_key가 곧바로None을 돌려주므로 그 배포에서는 어떤 요청도 key를 갖지 못합니다.prefix_routing.enabled는true이지만 요청 본문에 system prompt도 messages도 없어서 해싱할 기준이 없습니다.
두 경우 모두 PrefixAwareHash 전략은 대신 모델 이름을 해싱해 일반 consistent hash 경로로 ring을 걷습니다. 이 경로에는 부하 상한이 없습니다. 모델 해시값에서 시계 방향으로 처음 만나는 노드를 그대로 쓰고, 덜 바쁜 백엔드로 넘기지 않습니다. 따라서 풀 구성이 바뀌지 않는 한 한 모델의 모든 요청이 같은 백엔드로 몰리며, 그 백엔드가 아무리 바빠져도 마찬가지입니다.
결국 PrefixAwareHash가 prefix와 관련된 일을 하려면 prefix_routing.enabled: true가 필요합니다. prefix 라우팅을 끈 상태의 이 전략은 이름만 다른 모델명 일관 해싱이고, 이 문서에서 겉보기보다 적게 동작하는 유일한 조합입니다. 라우터는 이를 알려 줍니다. continuum-router config validate는 selection_strategy 경로에 경고를 하나 남기고(설정 자체는 유효한 상태로 둡니다), 같은 메시지를 시작 시점과 이 조합을 적용하는 모든 핫 리로드에서 로그로 남깁니다.
라우팅 결정 레이블¶
continuum_prefix_routing_requests_total{strategy=...}은 선택마다 한 번, 그리고 PrefixAwareHash 분기가 실제로 실행됐을 때만 기록됩니다. 다른 전략에서는 이 메트릭 계열에 아무것도 남지 않습니다.
prefix_hash: prefix 해시가 지목한 소유 백엔드가 요청을 받았습니다. 소유 백엔드가 부하 상한 아래였던 일반적인 경우와, 모든 백엔드가 상한에 걸려 ring을 한 바퀴 돈 뒤 prefix 지역성을 지키려고 소유 백엔드를 그대로 쓴 경우를 함께 포함합니다.overflow: 소유 백엔드가 부하 상한에 도달했거나 넘어서, ring에서 시계 방향으로 뒤에 있는 노드가 요청을 받았습니다.fallback: 요청에 prefix key가 없어 모델 이름을 해싱했고, 부하 상한 없이 ring을 걸었습니다. 위의 prefix key가 없을 때를 참고하세요.
Anthropic Cache Control 주입¶
anthropic_cache_control_injection: true가 설정되면, 라우터는 네이티브 Anthropic 요청의 system prompt에 cache_control: { type: "ephemeral" } 마커를 자동으로 추가합니다. 이는 라우터 수준의 KV cache와는 별개이지만 상호 보완적인 Anthropic의 서버 측 prompt 캐싱을 활성화합니다.
이 플래그는 fleet 전체 마스터 활성화 스위치이며 요청마다 읽힙니다(핫 리로드 가능). 백엔드별 anthropic_auto_cache_control과의 우선순위는 다음과 같습니다.
- 백엔드별
anthropic_auto_cache_control이 기본 결정 주체이며, 설정하지 않으면true입니다. 따라서 네이티브 Anthropic 백엔드는 기본적으로 자동 주입됩니다. prefix_routing.anthropic_cache_control_injection: true는anthropic_auto_cache_control: false로 설정한 백엔드에도 주입을 강제합니다.- 기본값
false는 엄격한 no-op입니다. 즉, 플래그를 배선하기 전과 똑같이 백엔드별 설정만으로 결정됩니다. - 주입은 네이티브 Anthropic 요청에만 적용되며 다른 provider는 절대 변형하지 않습니다. 마지막 system block에만 적용하고, 요청에 이미
cache_control(system, message, tool 중 어디든)이 있으면 호출자의 명시적 캐싱 의도를 보존하기 위해 주입을 건너뜁니다.
Tier 2: 응답 캐시¶
응답 캐시는 결정적 요청에 대한 완전한 LLM 응답을 저장하여, 라우터가 백엔드에 연결하지 않고도 반복 쿼리를 제공할 수 있게 합니다.
캐시 적격성¶
요청은 다음 조건이 모두 충족될 때 캐싱 대상이 됩니다:
temperature가0이거나 지정되지 않음- 요청이 스트리밍을 사용하지 않음 (또는 버퍼링이 활성화된 스트리밍 사용)
- 누적 응답 크기가
max_response_size이내
temperature가 0이 아닌 요청은 확률적이므로 캐싱되지 않습니다. 응답 헤더 X-Cache: HIT, X-Cache: MISS, 또는 X-Cache: BYPASS가 캐시 상태를 나타냅니다.
Cache Key 계산¶
cache key는 LLM 출력에 영향을 미치는 모든 매개변수에 대한 SHA256 해시입니다:
SHA256(
model,
"\x00",
SHA256(messages), // 사전 해싱된 messages 배열
"\x00",
temperature_bytes,
"\x00",
SOME/NONE_tag + max_tokens_bytes,
"\x00",
SOME/NONE_tag + top_p_bytes,
"\x00",
SOME/NONE_tag + tenant_id_bytes,
)
SOME/NONE 태그 바이트는 선택적 매개변수의 None과 Some(0) 간의 충돌을 방지합니다. tenant ID는 멀티테넌트 격리를 제공하기 위해 포함되며, 테넌트는 서로의 캐시된 응답을 읽을 수 없습니다.
구현: src/infrastructure/cache/response_cache.rs
스트리밍 캐시¶
스트리밍 응답의 경우, 라우터는 SSE 스트림을 max_stream_buffer_size(기본값: 10 MiB)까지 버퍼에 누적합니다. 전체 스트림이 제한 내에 들어가면 버퍼가 단일 직렬화된 blob으로 저장되고 캐시 히트 시 합성 SSE 스트림으로 재생됩니다.
캐시 제거¶
인메모리 백엔드는 항목 수가 capacity에 도달하면 LRU 제거를 사용합니다. Redis 백엔드는 Redis 자체에서 관리하는 TTL 기반 만료에 의존합니다.
시맨틱 캐시¶
정확 일치 경로와 prefix 경로 뒤에는 세 번째 옵트인 경로(이슈 #1571)가 있습니다. 결정적 요청의 마지막 user 메시지가 이전 응답의 마지막 user 메시지와 의미적으로 가까우면 그 응답을 그대로 제공합니다. 정확 일치와 prefix 조회가 모두 미스일 때만 참조되며, 다음 조건이 모두 성립해야 동작합니다.
response_cache.semantic.enabled: true이고backend와embedding_model이 고정되어 있음- 라우터 자체의
control_plane.optimization.semantic_cache_enabled: true - 허브 최적화 정책의
semantic_cache(그리고exact_cache)가 조직에 대해 켜져 있고, 티어의cache_enabled가 참 - 비스트리밍
/v1/chat/completions요청이며temperature: 0이고, 마지막 메시지가max_input_bytes이하의 텍스트를 담은 user 턴
조회는 가드레일 입력 게이트 뒤에서 실행되므로, 게이트가 차단한 프롬프트는 임베더로 전송되지 않고 transform 판정은 임베딩 전에 적용됩니다. 그 뒤 적격 요청 하나당 고정된 백엔드로 POST /v1/embeddings를 정확히 한 번 호출하며, embedding_timeout_ms로 제한됩니다. 미스일 때는 그 벡터를 보관했다가 백엔드 응답이 도착하면 같은 벡터 아래에 색인합니다. 임베딩 호출이 실패하거나 시간 초과되면 단순 미스로 처리되고 채팅 요청 자체는 실패하지 않습니다. 백엔드 고정은 LLM 분류기와 가드레일 전송이 공유하는 고정 백엔드 가시성 규칙을 따릅니다. internal: true 백엔드는 운영자가 명시했으므로 도달 가능하고, enabled: false 백엔드는 프롬프트 텍스트를 보내게 되므로 거부됩니다.
파티셔닝이 이 경로의 보안 경계입니다. 유사도는 같은 파티션 다이제스트를 공유하는 항목 사이에서만 계산되며, 다이제스트에는 호출자의 캐시 아이덴티티(경로 범위의 로컬 아이덴티티와 허브 키 ID), 모델, 임베딩 모델, 요청의 모든 응답 형성 필드(tools, response format, 샘플링 범위, seed), 그리고 system prompt를 포함해 마지막 user 턴 앞의 대화 전체가 들어갑니다. 따라서 서로 다른 API 키, 서로 다른 system prompt, 서로 다른 tool 집합은 절대 항목을 공유하지 않습니다.
후보의 코사인 유사도가 threshold(미설정 시 허브 정책의 semantic_similarity_bps)에 도달하면 제공됩니다. 응답에는 X-Cache: HIT와 X-Cache-Mode: semantic이 실리고, 허브 사용량 레코드에는 cache_hit_type = semantic이 찍힙니다. 다른 로컬 히트와 마찬가지로 메타데이터만 담기며 토큰 윈도우나 예산을 소비하지 않습니다. 본문은 응답 캐시 저장소의 sem: 키 공간에 저장되므로 저장소의 용량과 TTL이 본문을 제한하고, 벡터 인덱스는 외부 의존성 없는 라우터별 유한 구조(max_entries, 가장 오래된 항목부터 제거)입니다. 항목 TTL은 허브 정책의 cache_ttl_secs가 있으면 그것, 없으면 semantic.ttl, 그것도 없으면 response_cache.ttl입니다. 공유 인덱스나 라우터 간 인덱스, 외부 벡터 데이터베이스, 스트리밍 시맨틱 히트는 범위 밖입니다.
시맨틱 본문은 정확 일치·prefix 키 공간과 함께 response_cache.capacity를 공유하므로, 시맨틱 경로가 바쁘면 두 키 공간에도 실질적인 축출 압력이 더해집니다. 합산 물량에 맞게 capacity를 늘리거나, 시맨틱 본문이 저장소에서 작은 비중을 유지하도록 semantic.max_entries를 충분히 낮게 잡으십시오.
구현: src/infrastructure/cache/semantic/(파티션, 인덱스, 임베딩 형식)와 src/proxy/semantic_cache.rs(제공 경로).
Tier 3: 공유 외부 캐시¶
공유 외부 캐시는 Redis/Valkey에 대한 CacheStore 트레이트 추상화를 제공하여, 여러 라우터 인스턴스가 응답 캐시 상태를 공유할 수 있게 합니다.
CacheStore 트레이트¶
pub trait CacheStore: Send + Sync + 'static {
async fn get(&self, key: &str) -> CacheStoreResult<Option<Vec<u8>>>;
async fn set(&self, key: &str, value: &[u8], ttl: Duration) -> CacheStoreResult<()>;
async fn delete(&self, key: &str) -> CacheStoreResult<()>;
async fn clear(&self) -> CacheStoreResult<()>;
async fn stats(&self) -> CacheStoreStats;
}
구현체: InMemoryCacheStore (기본값, LRU + TTL), RedisCacheStore (Redis/Valkey, 연결 풀링 포함).
구현: src/infrastructure/cache/store.rs
Redis 백엔드¶
RedisCacheStore는 연결 풀링을 위해 deadpool-redis를 사용합니다. 모든 key는 동일한 Redis 인스턴스를 공유하는 다른 애플리케이션과의 충돌을 방지하기 위해 설정 가능한 prefix(기본값: cr:resp:)로 네임스페이스가 지정됩니다.
Key 네임스페이스 형식:
쓰기에는 SET EX, 읽기에는 GET을 사용하며, 설정 가능한 명령 타임아웃(기본값: 1초)을 갖습니다.
자동 폴백¶
Redis에 연결할 수 없는 경우, RedisCacheStore는 투명하게 인메모리 폴백 캐시를 활성화합니다. 폴백은 첫 번째 연결 실패 시 활성화되며, 백그라운드 헬스 모니터 작업(30초마다 실행)이 Redis 연결 복원을 시도합니다. 복구 시 플래그가 해제되고 이후 작업은 Redis로 돌아갑니다.
continuum_cache_fallback_active 메트릭(값 1)은 폴백 모드가 현재 활성 상태임을 나타냅니다.
연결 풀 공유¶
deadpool_redis::Pool은 AppState에 Arc로 저장되며 응답 캐시와 KV cache index(Tier 4) 간에 공유됩니다. 연결 수의 이중 계산을 방지하고 설정을 단순화합니다: 두 소비자가 동일한 풀 자격 증명을 재사용합니다.
Tier 4: Backend KV Cache Index¶
Tier 4는 특정 token prefix 해시에 대해 GPU에 상주하는 KV 텐서를 보유한 백엔드를 실시간으로 추적합니다. 그래서 통계적 친화성 대신 실제 GPU 캐시 상태에 기반해 라우팅을 결정할 수 있습니다.
이벤트 소비¶
지원되는 producer bridge는 continuum-kv-listener 바이너리입니다. 이 listener는 vLLM과 SGLang의 msgpack record를 ZMQ로 받거나 네이티브 trtllm-serve JSON 이벤트를 HTTP로 폴링하고, block chain을 라우터와 같은 prefix hash로 해석한 뒤 백엔드별 router-compatible SSE 스트림(예: http://kv-listener.internal:7817/events/vllm-1)을 노출합니다. KvEventConsumerManager는 백엔드당 하나의 백그라운드 Tokio 작업을 생성하여 listener 스트림을 구독하고 이벤트를 처리합니다.
| 엔진 소스 | 수집 방식 | Prefix 식별 | 기본 medium |
|---|---|---|---|
| vLLM | ZMQ publisher | 가능한 경우 라우터가 발급한 cache salt, 그 외에는 엔진 /detokenize | 이벤트가 제공한 medium |
| SGLang | ZMQ publisher | 가능한 경우 라우터가 발급한 cache salt, 그 외에는 엔진 /detokenize | 이벤트가 제공한 medium |
| 네이티브 TensorRT-LLM | 제한된 POST /kv_cache_events 폴링 | 라우터가 발급한 cr1:<backend>:<prefix-hash> salt, salt가 없거나 다른 백엔드용인 chain은 무시 | cache_level: 0은 GPU, 그보다 큰 level은 storage이며 저장 tier가 없으면 GPU, 제거 시에는 추적된 tier를 유지 |
TensorRT-LLM created 이벤트는 listener가 추적하는 block 상태를 초기화하고, stored 이벤트는 공유 source processor를 통해 chain을 생성하거나 연장하며, removed 이벤트는 해석된 prefix를 제거합니다. Poller는 event ID로 중복 응답을 거부하고 누락이 생기면 오래된 상태를 비웁니다. 이미 vLLM 호환 ZMQ 이벤트 형식을 게시하는 Dynamo 배포에서는 별도의 네이티브 HTTP 소스를 중복 구성하지 말고 ZMQ 소스를 계속 사용해야 합니다.
이벤트 유형:
| 이벤트 | 의미 |
|---|---|
cache_created | 해당 백엔드에서 token prefix에 대한 KV block이 생성됨 (데이터가 GPU VRAM에 진입) |
cache_evicted | 해당 백엔드의 GPU 메모리에서 KV block이 제거됨 |
cache_offloaded | KV block이 GPU에서 외부 스토리지(예: S3 호환 스토리지)로 명시적으로 오프로드됨 |
cache_reloaded | KV block이 외부 스토리지에서 GPU 메모리로 다시 로드됨 |
cache_purged | KV block이 모든 스토리지 계층에서 영구적으로 제거됨 |
각 이벤트는 prefix_hash(hex 문자열)와 해당 백엔드에서 해당 prefix에 대해 캐시된 token 수를 나타내는 선택적 token_count를 포함합니다.
SSE 파싱 세부사항:
- 소비자는 이벤트 유형 결정에 SSE
event:필드를 우선 사용하며, JSONevent필드는 폴백으로 사용됩니다. - 버퍼는 잘못된 스트림으로부터 보호하기 위해 1 MiB(
MAX_SSE_BUFFER_SIZE)로 제한됩니다. - 연결 실패 또는 스트림 종료 시, 소비자는 재연결 전에 지수 백오프(초기: 1초, 최대: 60초)를 적용합니다.
구현: src/infrastructure/kv_index/event_consumer.rs
Producer bridge 구현: crates/continuum-kv-listener/
인덱스 구조¶
이벤트는 다음 매핑을 유지하는 KvCacheIndex 구현에 전달됩니다:
token count가 점수로 사용됩니다: 특정 prefix에 대해 2048개의 캐시된 token을 보유한 백엔드가 512개를 보유한 것보다 높은 순위를 갖습니다.
두 가지 구현이 제공됩니다:
InMemoryKvIndex¶
- 락프리 동시 읽기를 위한
DashMap<String, PrefixEntry> - 항목 수가
max_entries에 도달하면 LRU 제거 (가장 오래된 10%의 항목 제거) query_backends()시 지연 확인되는 TTL 기반 만료- 주기적
cleanup_expired()로 오래된 항목을 사전 제거 - 기본값: 최대 100,000 항목, 300초 TTL
RedisKvIndex¶
- 각 prefix를 Redis sorted set으로 저장:
ZADD cr:kvidx:<prefix_hash> <token_count> <backend_id> EXPIRE가ZADD와 함께 단일 원자적 라운드트립으로 파이프라인 처리ZREVRANGEBYSCORE +inf -inf WITHSCORES로 내림차순 점수별 백엔드 반환- 여러 라우터 인스턴스 간 KV index 상태 공유 가능
- Key prefix:
cr:kvidx:
구현: src/infrastructure/kv_index/index.rs
스토리지 계층 인식¶
storage_offloading.enabled가 true이면, 인덱스는 각 (prefix, backend) 항목에 대해 두 가지 스토리지 계층을 추적합니다:
| 계층 | 이름 | 설명 |
|---|---|---|
| 핫 | GpuHot | KV 데이터가 GPU VRAM에 상주. 최소 지연으로 즉시 접근 가능. |
| 웜 | StorageWarm | KV 데이터가 외부 스토리지(예: S3 호환 스토리지)로 오프로드됨. GPU 상주 데이터에 비해 재로드 지연이 발생하지만 여전히 사용 가능. |
이벤트에 따른 계층 전환:
| 이벤트 | 결과 계층 |
|---|---|
cache_created | GpuHot |
cache_offloaded | StorageWarm |
cache_reloaded | GpuHot |
cache_evicted (treat_eviction_as_offload: true 시) | StorageWarm |
cache_evicted (treat_eviction_as_offload: false 시) | 항목 제거 |
cache_purged | 항목 제거 |
treat_eviction_as_offload 옵션은 계층 정보를 포함하지 않는 일반적인 cache_evicted 이벤트를 웜 스토리지로의 오프로드로 처리할지, 아니면 영구 제거로 처리할지를 제어합니다. vLLM 백엔드가 명시적인 cache_offloaded 이벤트 유형 없이 cache_created와 cache_evicted 이벤트만 내보낼 때 유용합니다.
구현: src/infrastructure/kv_index/types.rs
Overlap 스코어링¶
KvOverlapScorer는 BackendScorer 트레이트를 구현하며, 요청의 prefix에 대해 캐시 데이터를 실제로 들고 있는 백엔드마다 복합 점수를 계산합니다.
final_score = overlap_weight * (raw_overlap * tier_multiplier)
+ load_weight * (1.0 - load_ratio)
+ health_weight * health_score
각 항목:
raw_overlap = backend_token_count / max_token_count_across_backends(0.0 ~ 1.0)tier_multiplier=GpuHot데이터는gpu_tier_weight,StorageWarm데이터는storage_tier_weightload_ratio = backend_in_flight / max_in_flight_across_backends(0.0 ~ 1.0)health_score = backend_success_rate(0.0 ~ 1.0)
해당 prefix에 대해 인덱스에 항목이 없는 백엔드는 0.0을 받고 순위 경쟁에 아예 들어가지 않습니다. 부하와 헬스는 데이터를 실제로 들고 있는 백엔드끼리만 순위를 가릅니다. 이 규칙이 없으면 아무것도 캐시하지 않은 한가하고 건강한 백엔드가 기본값 기준 load_weight + health_weight 즉 0.4를 받아 풀의 복합 점수 임계값 0.3을 넘고, 그 prefix의 모든 요청에서 설정된 전략을 덮어쓰게 됩니다.
가중치의 출처¶
여섯 개 값은 kv_cache_index.scoring 블록에서 읽어 시작 시점의 스코어러 등록 과정에 그대로 전달됩니다. 그 시점에 유효 가중치를 INFO 로그로 한 번 남기므로, 기본값이 아닌 블록이 실제로 반영됐는지 운영자가 확인할 수 있습니다. kv_cache_index는 재시작이 필요한 섹션이라, scoring만 고쳐서 핫 리로드해도 스코어러는 다시 만들어지지 않습니다.
기본 가중치:
| 매개변수 | 기본값 | 설명 |
|---|---|---|
overlap_weight | 0.6 | 캐시 overlap 신호의 가중치 |
load_weight | 0.3 | 백엔드 부하 신호의 가중치 |
health_weight | 0.1 | 백엔드 헬스 신호의 가중치 |
min_overlap_threshold | 0.3 | 보유 백엔드가 후보로 남기 위한 계층 반영 overlap 하한 |
gpu_tier_weight | 1.0 | GPU 상주(GpuHot) 데이터에 대한 계층 배수 |
storage_tier_weight | 0.6 | 스토리지 오프로드(StorageWarm) 데이터에 대한 계층 배수 |
주요 가중치 세 개(overlap_weight, load_weight, health_weight)의 합은 정확히 1.0이어야 하며 설정 로드 시점에 검증됩니다.
계층 가중치 배수(gpu_tier_weight, storage_tier_weight)는 독립적으로 적용됩니다. 가중 합산 계산 전에 원시 overlap 점수를 스케일링합니다. 동일한 token 수를 가진 StorageWarm 백엔드는 계층 배수가 유효 overlap 신호를 감소시키기 때문에 GpuHot 백엔드보다 낮은 점수를 받습니다.
부하와 헬스 입력¶
부하 항목은 백엔드 풀의 in-flight 트래커에서 옵니다. CHWBL 부하 상한이 읽는 것과 같은 실시간 게이지이고, 공유 stats 맵의 BackendStats.in_flight_requests 필드가 아닙니다(그 필드는 더 이상 갱신되지 않습니다). prepare()가 인덱스 쿼리와 함께 트래커 스냅샷을 한 번 떠서 캐시 결과에 담아 두므로, 한 번의 스코어링에서 모든 백엔드가 같은 부하 화면을 기준으로 비교됩니다. 백엔드마다 다시 읽으면서 값이 흔들리는 일이 없습니다. 스냅샷의 낡음은 아래에 설명한 100ms 결과 TTL로 제한됩니다.
헬스 항목은 공유 stats 맵의 성공률을 동기적으로 읽습니다. 그 잠금이 잠시 경합하면 스코어러는 대기하지 않고 중립값 1.0을 씁니다.
최소 overlap 임계값¶
min_overlap_threshold(기본값: 0.3)는 보유 백엔드마다 계층 반영 정규화 overlap(raw_overlap * tier_multiplier)을 기준으로 걸러 냅니다. 임계값에 못 미치는 보유 백엔드는 아무것도 들고 있지 않은 백엔드와 똑같이 0.0을 받습니다.
정규화 때문에 결과가 두 가지로 갈립니다. 가장 많이 들고 있는 GPU 상주 백엔드는 원시 overlap이 정의상 1.0이라 계층 반영값이 정확히 gpu_tier_weight가 되고, gpu_tier_weight >= min_overlap_threshold이면 통과합니다. 기본값에서는 1.0 대 0.3이라 여유롭게 통과합니다. 유일한 사본이 웜 스토리지로 내려간 백엔드는 storage_tier_weight >= min_overlap_threshold일 때만 통과합니다. 계층 배수를 둔 이유가 바로 이것입니다. 기본값에서는 0.6이 0.3을 넘지만, storage_tier_weight를 임계값 아래로 낮추면 웜 사본만 가진 백엔드는 비보유 백엔드로 취급됩니다.
로더는 계층 가중치와 임계값의 관계를 확인하지 않습니다(두 가중치 모두 음수만 아니면 통과합니다). 그래서 gpu_tier_weight를 min_overlap_threshold보다 작게 두어도 설정은 여전히 정상으로 로드되고, 어떤 백엔드도 관문을 통과하지 못해 언제나 설정된 전략이 결정하는 스코어러가 만들어집니다. 이 조합은 런타임에서 정상 동작과 구별되지 않기 때문에, continuum-router config validate가 kv_cache_index.scoring.gpu_tier_weight 경로에 경고를 보고하고 기동 시에도 같은 메시지를 한 번 남깁니다. 이 경고는 스코어러가 실제로 등록되는 경우, 즉 prefix_routing.enabled와 kv_cache_index.enabled가 모두 참일 때만 발생합니다. 스코어링 선택을 사실상 끄려는 의도가 아니라면 gpu_tier_weight >= min_overlap_threshold를 유지하세요.
후보 중 아무도 이 관문을 통과하지 못하면 모든 점수가 0.0이 되어 풀의 복합 점수 임계값을 넘지 못하고, 설정된 selection_strategy가 선택을 맡습니다. 이 복합 점수 임계값은 0.3이며 DEFAULT_SCORER_THRESHOLD로 풀에 컴파일되어 있습니다. 설정 필드로 바꿀 수 없고, 비교는 초과 비교라서 0.3과 같은 점수로는 선택되지 않습니다.
비동기 준비 모델¶
KvCacheIndex.query_backends()는 비동기이지만 BackendScorer.score()는 동기적이어야 합니다. 스코어러는 2단계 설계를 씁니다. prepare()가 인덱스 쿼리 결과와 in-flight 스냅샷을 가져와 100ms 내부 TTL로 캐시하고, score()는 그 캐시에서 동기적으로 읽습니다.
구현: src/infrastructure/kv_index/scorer.rs
Backend 선택 파이프라인¶
chat completion 요청이 도착하면, 라우터는 다음 파이프라인을 실행합니다:
sequenceDiagram
participant C as Client
participant R as Router
participant RC as Response Cache
participant PK as Prefix Key
participant KVI as KV Index
participant CHWBL as CHWBL Ring
participant B as Backend
C->>R: POST /v1/chat/completions
R->>RC: Cache key 조회 (T=0만)
alt Cache HIT
RC-->>R: 캐시된 응답
R-->>C: 200 OK (X-Cache: HIT)
else Cache MISS
R->>PK: Prefix key 추출
alt Prefix key 있고 KV index 스코어러 등록됨
R->>KVI: prepare(prefix_hash)
KVI-->>R: 인덱스로부터 backend 점수
R->>R: Score = overlap + load + health (보유 백엔드만)
alt 최고 복합 점수 > 0.3
R->>B: 요청 전달 (KV 인식 라우팅)
else 임계값 이하
R->>R: 설정된 selection_strategy 실행
R->>CHWBL: PrefixAwareHash: prefix → backend 매핑 (부하 상한)
R->>B: 요청 전달
end
else Prefix key 있고 스코어러 미등록
R->>R: 설정된 selection_strategy 실행
R->>CHWBL: PrefixAwareHash: prefix → backend 매핑 (부하 상한)
R->>B: 요청 전달
else Prefix key 없음
R->>R: 설정된 selection_strategy 실행
R->>CHWBL: prefix key 없는 PrefixAwareHash: 모델명 해싱 (부하 상한 없음)
R->>B: 요청 전달
end
B-->>R: 응답
R->>RC: 응답 저장 (T=0)
R-->>C: 200 OK (X-Cache: MISS)
end KV overlap 스코어러는 전략보다 먼저 실행되며, prefix 인식 라우팅을 대체하지 않고 함께 조합됩니다. 후보 백엔드 중 하나라도 해당 prefix의 데이터를 들고 있고 최고 복합 점수가 풀에 컴파일된 임계값 0.3을 넘으면 스코어러가 선택합니다. 그렇지 않으면 설정된 selection_strategy가 선택하는데, CHWBL ring이 그 전략인 경우는 selection_strategy가 PrefixAwareHash일 때뿐입니다. RoundRobin, WeightedRoundRobin, LeastLatency, Random, ConsistentHash에서는 임계값 미달 경로가 각각 그 전략을 실행합니다.
Tier 4가 켜지는 조건은 prefix_routing.enabled(스코어러가 쓸 prefix key를 만듭니다)와 kv_cache_index.enabled(인덱스를 만듭니다)이며 selection_strategy는 조건이 아닙니다. 따라서 두 스위치를 켠 RoundRobin 배포도 완전한 KV 인식 선택을 하고, 라운드로빈은 임계값 미달일 때의 폴백 역할만 합니다.
Gemini Context Cache¶
Tier 1부터 4까지는 GPU에 KV cache가 상주하는 백엔드(vLLM, TensorRT-LLM, SGLang)를 대상으로 한다. Google Generative Language API에는 이런 이벤트 스트림이 없어서 type: gemini 백엔드에는 별도의 옵트인 방식을 적용한다. 라우터가 Google의 서버 측 리소스인 cachedContents를 대신 관리해 주는 방식이다.
개요¶
이 기능을 활성화하면 라우터는 type: gemini 백엔드로 보내는 요청 중 크고 안정적인 system prompt prefix를 cachedContents 리소스로 캐싱하고 이후 요청에서 재사용한다. OpenAI 호환 클라이언트 입장에서는 변화가 전혀 없어서 클라이언트는 기존과 동일한 OpenAI 호환 요청을 그대로 보내면 되고 라우터가 전달 전에 요청을 다시 써 준다.
compat 요청 형식은 extra_body.google.cached_content = "cachedContents/..."로 캐시를 참조하며 native generateContent 형식은 최상위 cachedContent 필드를 대신 사용하고 systemInstruction은 아예 제거하는데 이제 해당 지시문이 캐시된 리소스 안에 들어있기 때문이다.
Google은 캐시된 token을 낮은 input 요율로 과금하는 대신 token-시간당 스토리지 요금을 별도로 부과하고 모델별로 캐싱 가능한 최소 token 수도 강제한다(Gemini 2.5 계열은 대략 2,048 token, 3.x 계열은 4,096 token). prefix가 충분히 크고 스토리지 요금을 상쇄할 만큼 자주 재사용될 때만 캐시가 이득이 되므로 이 기능은 기본값이 enabled: false이고 ttl도 짧게 잡혀 있다. 라우터는 요청을 tokenize하지 않으며 대신 바이트 기반 휴리스틱(min_prefix_bytes)으로 token 수 하한을 근사하고 Google이 거절한 prefix는 negative cache에 등록해 두므로 너무 작은 prefix라도 negative_ttl_seconds 윈도우당 한 번 이상 재시도되지 않는다.
gemini_context_cache.enabled: false(기본값)일 때는 완전한 no-op이다. map을 할당하지도 않고 백그라운드 작업도 돌지 않으며 요청당 지연도 전혀 추가되지 않는다.
요청 라이프사이클¶
type: gemini 백엔드를 대상으로 하면서 system prompt가 min_prefix_bytes 이상인 요청이 캐싱 대상이다. 대상 요청은 다음 경로를 거친다.
- Prefix 다이제스트 계산. 라우터는 모델명과 전체 system prompt에 대한 다이제스트를 계산하는데
src/core/prefix_key.rs의 해싱 로직을 그대로 따라서 prefix 라우팅의 고정성과 context cache 조회가 동일한 식별자를 공유하게 한다. Tier 1의 prefix key와 달리 이 다이제스트는 system 텍스트 전체를 대상으로 하며 잘라내지 않는다. - Map hit (항목이 만료되지 않은 경우): 라우터는 cached-content 참조를 주입하고 중복된 system prompt를 나가는 요청에서 제거한다.
extend_on_hit이 설정되어 있고 항목의 남은 TTL이 20% 이내로 줄었으면 백그라운드에서 TTL을 PATCH로 연장한다. - Map miss (negative cache에 없는 경우): 라우터는 요청을 그대로 즉시 전달하고 백그라운드에서 single-flight 방식의 생성 작업을 하나만 띄운다(
POST /v1beta/cachedContents에model,systemInstruction,ttl을 담아 호출). 첫 요청은 생성 지연을 전혀 부담하지 않으며 같은 prefix를 공유하는 이후 요청부터 주입 혜택을 받는다. - 생성 거부 (HTTP 400, 대개 모델의 최소 캐싱 가능 token 수에 못 미칠 때): 해당 prefix를
negative_ttl_seconds동안 negative cache에 등록해서 윈도우당 정확히 한 번만 생성을 시도하게 한다. - 제거.
max_entries를 넘는 항목은 LRU로 제거된다. 제거와 admin clear 엔드포인트 모두 Google 쪽에 best-effortDELETE를 보내 스토리지 과금을 조기에 멈추며 Google 자체의 TTL 만료는 최후의 보루로 남는다. negative cache도 같은max_entries상한을 공유하는데 가득 차면 만료된 거부 윈도우부터 먼저 정리하고 그래도 자리가 없으면 새 거부 항목은 등록하지 않는다.
설정 레퍼런스¶
gemini_context_cache:
enabled: false
# 캐싱을 시도할 최소 system prompt 크기 (UTF-8 바이트, 기본값: 16384)
# 1024 이상이어야 함. 라우터는 tokenize하지 않으므로 이는 Google의
# 모델별 최소 캐싱 가능 token 수를 바이트 기준으로 근사한 값임
min_prefix_bytes: 16384
# cachedContents 리소스 생성 시 적용할 TTL (기본값: "10m")
# duration으로 파싱 가능해야 하고 60초 이상이어야 함
ttl: "10m"
# 히트가 만료 20% 이내로 근접하면 백그라운드에서 TTL 연장 (기본값: false)
extend_on_hit: false
# LRU 제거 전 최대 추적 항목 수 (기본값: 1000)
# 범위: 10 ~ 1,000,000
max_entries: 1000
# cachedContents 생성 호출 타임아웃, 밀리초 (기본값: 5000)
# 범위: 500 ~ 60000
create_timeout_ms: 5000
# Google이 거절한 prefix를 negative cache에 두는 시간, 초 (기본값: 600)
# 1 이상이어야 함
negative_ttl_seconds: 600
적용 범위: type: gemini 백엔드의 chat completion만 대상이다. /v1/responses의 Gemini 컨버터, 이미지 생성, embeddings 경로는 대상이 아니다. Generative Language API를 흉내 내지만 cachedContents를 지원하지 않는 프록시나 자체 호스팅 엔드포인트는 생성 시도가 매번 실패해서 negative cache에 등록되므로 항목이 아예 쌓이지 않는다.
메트릭¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
continuum_gemini_context_cache_requests_total | Counter | result | 결과별 조회 수 (hit, miss, ineligible, negative) |
continuum_gemini_context_cache_creates_total | Counter | — | cachedContents 생성 시도 횟수 |
continuum_gemini_context_cache_create_failures_total | Counter | reason | 생성 실패 (min_tokens, auth, timeout, other) |
continuum_gemini_context_cache_entries | Gauge | — | 현재 추적 중인 항목 수 |
continuum_gemini_context_cache_evictions_total | Counter | — | LRU 제거 횟수 |
continuum_gemini_context_cache_cached_tokens_total | Counter | — | 캐시에서 제공된 token 수. compat 응답 형식에서는 usage.prompt_tokens_details.cached_tokens, native 형식에서는 usageMetadata.cachedContentTokenCount에서 읽음 |
관리자 엔드포인트¶
GET /admin/gemini-context-cache/stats¶
enabled 플래그, 유효 설정 echo, 현재 항목 수, negative_entries, in_flight 생성 건수, 백엔드별 내역, 그리고 hit/miss/ineligible/negative/creates/create_failures/evictions/cached_tokens 카운터를 반환한다.
POST /admin/gemini-context-cache/clear¶
라우터 측 항목 map을 비우고 추적 중인 모든 cachedContents 리소스에 대해 best-effort 원격 DELETE를 보낸다.
응답 예제:
기능이 비활성화된 경우:
설정 레퍼런스¶
Tier 1: Prefix 인식 라우팅¶
prefix_routing:
enabled: true
# Prefix 해시에 사용되는 prompt 콘텐츠의 최대 바이트 수 (기본값: 1024)
max_prefix_length: 1024
# CHWBL 부하 상한 epsilon: 백엔드가 (1 + epsilon) * avg_load 처리 가능
# 범위: 0.01 ~ 10.0 (기본값: 0.25)
load_factor_epsilon: 0.25
# Consistent hash ring에서 백엔드당 가상 노드 수 (기본값: 150)
# 높은 값은 분배 균등성을 향상시킴
virtual_nodes: 150
# System prompt에 Anthropic cache_control 마커 주입 (기본값: false)
anthropic_cache_control_injection: false
Tier 2: 응답 캐시¶
response_cache:
enabled: true
# 캐시 백엔드: "memory" (기본값) 또는 "redis"
# 백엔드 변경은 재시작 필요; 다른 필드는 핫 리로드 지원
backend: memory
# LRU 제거 전 최대 캐시 항목 수 (기본값: 1000)
capacity: 1000
# 캐시 항목의 TTL (기본값: "5m")
ttl: "5m"
# 캐싱 대상 최대 응답 본문 크기 (기본값: 1 MiB)
max_response_size: 1048576
# 캐싱 대상 최대 스트리밍 버퍼 크기 (기본값: 10 MiB)
max_stream_buffer_size: 10485760
# 시맨틱(유사도) 캐시, 기본값은 꺼짐. 실제 제공에는
# control_plane.optimization.semantic_cache_enabled: true와
# 허브 정책의 semantic_cache 플래그도 필요
semantic:
enabled: true
backend: embedder # 임베딩 모델을 제공하는 설정된 백엔드 (필수)
embedding_model: bge-m3 # 그 백엔드에 요청할 모델 (필수)
threshold: 0.92 # 코사인 유사도; 생략 시 허브의 semantic_similarity_bps
max_entries: 1000 # 라우터별 인덱스에 보관할 벡터 수, 1..=1000000
ttl: "5m" # 항목 TTL; 생략 시 response_cache.ttl
embedding_timeout_ms: 2000 # 임베딩 호출 제한, 100..=60000; 초과 시 미스로 처리
max_input_bytes: 8192 # 이보다 긴 마지막 user 메시지는 이 경로를 건너뜀, 1..=1048576
Tier 3: 공유 외부 캐시 (Redis 백엔드)¶
response_cache:
enabled: true
backend: redis
redis:
# Redis/Valkey 연결 URL
url: "redis://redis:6379"
# TLS를 위해 rediss:// 사용; 또는 redis:// URL과 함께 tls: true 설정
# url: "rediss://redis:6380"
# 연결 풀 크기 (기본값: 8)
pool_size: 8
# Key 네임스페이스 prefix (glob 문자를 포함하지 않아야 함)
key_prefix: "cr:resp:"
# 연결 타임아웃 (밀리초, 기본값: 3000)
connect_timeout_ms: 3000
# 명령당 타임아웃 (밀리초, 기본값: 1000)
command_timeout_ms: 1000
# Redis 접근 불가 시 인메모리 캐시 폴백 용량 (기본값: 1000)
fallback_capacity: 1000
# 폴백 인메모리 항목의 TTL (초, 기본값: 300)
fallback_ttl_seconds: 300
# Redis 실패 시 인메모리 캐시로 폴백 (기본값: true)
fallback_to_memory: true
Tier 4: KV Cache Index¶
kv_cache_index:
enabled: true
# 인덱스 백엔드: "memory" (기본값) 또는 "redis"
# "redis" 사용 시 response_cache.redis의 연결 풀을 재사용
backend: memory
# 추적할 최대 prefix hash 항목 수 (기본값: 100000)
# 범위: 100 ~ 10,000,000
max_entries: 100000
# 인덱스 항목의 TTL (초, 기본값: 600)
# 범위: 1 ~ 86400
entry_ttl_seconds: 600
# Backend 스코어링 가중치 (overlap + load + health의 합이 1.0이어야 함)
scoring:
overlap_weight: 0.6
load_weight: 0.3
health_weight: 0.1
# KV 인식 라우팅을 활성화하는 최소 best-overlap (기본값: 0.3)
# 이를 초과하는 백엔드가 없으면 설정된 전략으로 폴백
min_overlap_threshold: 0.3
# 계층 가중치 배수 (주요 세 가중치와 독립적)
# 가중 합산 계산 전에 원시 overlap 점수에 적용
gpu_tier_weight: 1.0 # GpuHot (GPU 상주) 데이터에 대한 배수
storage_tier_weight: 0.6 # StorageWarm (오프로드) 데이터에 대한 배수
# 계층형 스토리지 인식 (GPU 핫 vs. 외부 스토리지 웜)
storage_offloading:
enabled: false # 스토리지 계층 추적 활성화 (기본값: false)
treat_eviction_as_offload: true # cache_evicted를 웜으로의 오프로드로 처리 (기본값: true)
# KV cache 이벤트를 구독할 continuum-kv-listener SSE 스트림
event_sources:
- backend_name: vllm-1
endpoint: "http://kv-listener.internal:7817/events/vllm-1"
reconnect_interval_ms: 5000
- backend_name: vllm-2
endpoint: "http://kv-listener.internal:7817/events/vllm-2"
reconnect_interval_ms: 5000
event_sources[].endpoint에 지원되는 엔드포인트 스킴: http, https, ws, wss.
메트릭¶
모든 KV cache 메트릭은 continuum_ prefix를 사용합니다. 레이블 값은 카디널리티 폭발을 방지하기 위해 허용 목록에 대해 정제됩니다.
Tier 1: Prefix 라우팅¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
continuum_prefix_routing_requests_total | Counter | strategy | 전략별 라우팅 결정 (prefix_hash, overflow, fallback) |
continuum_prefix_routing_backend_distribution | Gauge | backend | Prefix 라우팅 하의 백엔드별 진행 중 요청 |
continuum_prefix_routing_prefix_cardinality | Gauge | — | 관측된 고유 prefix key의 근사 수 |
continuum_prefix_routing_requests_total은 선택마다 한 번, PrefixAwareHash 전략 분기가 실행됐을 때만 기록됩니다. 다른 selection_strategy에서는 설계상 0으로 남습니다. 각 레이블이 ring 탐색의 어떤 상황을 뜻하는지는 라우팅 결정 레이블을 참고하세요.
PromQL 예제:
# Overflow 비율 (CHWBL 부하 상한 활성화)
rate(continuum_prefix_routing_requests_total{strategy="overflow"}[5m])
/ rate(continuum_prefix_routing_requests_total[5m])
# 백엔드별 부하 분배
continuum_prefix_routing_backend_distribution
Tier 2: 응답 캐시¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
continuum_response_cache_requests_total | Counter | result | 결과별 캐시 조회 (hit, miss, skip) |
continuum_response_cache_entries | Gauge | — | 현재 캐시된 항목 수 |
continuum_response_cache_size_bytes | Gauge | — | 대략적인 메모리 사용량 (바이트) |
continuum_response_cache_evictions_total | Counter | — | 응답 캐시에서의 LRU 제거 |
continuum_response_cache_hit_rate | Gauge | — | 롤링 히트율 (0.0 ~ 1.0) |
continuum_response_cache_semantic_total | Counter | result | 시맨틱 캐시 결과 (hit, miss, below_threshold, skip, embed_error, store) |
continuum_response_cache_semantic_entries | Gauge | — | 시맨틱 캐시 인덱스에 현재 보관된 벡터 수 |
PromQL 예제:
# 5분 동안의 캐시 히트율
rate(continuum_response_cache_requests_total{result="hit"}[5m])
/ rate(continuum_response_cache_requests_total{result=~"hit|miss"}[5m])
# 캐시 바이패스율 (비결정적 요청)
rate(continuum_response_cache_requests_total{result="skip"}[5m])
Tier 3: Redis 백엔드¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
continuum_cache_backend_type | Gauge | backend | 활성 백엔드 유형 (memory=1 또는 redis=1) |
continuum_cache_redis_connections_active | Gauge | — | 활성(사용 중) Redis 연결 |
continuum_cache_redis_connections_idle | Gauge | — | 풀의 유휴 Redis 연결 |
continuum_cache_redis_latency_seconds | Histogram | operation | Redis 작업 지연 시간 (get, set, delete) |
continuum_cache_redis_errors_total | Counter | type | Redis 오류 (connection, timeout, other) |
continuum_cache_fallback_active | Gauge | — | 인메모리 폴백 활성 시 1 |
PromQL 예제:
# Redis P99 GET 지연 시간
histogram_quantile(0.99,
rate(continuum_cache_redis_latency_seconds_bucket{operation="get"}[5m])
)
# Redis 오류율
rate(continuum_cache_redis_errors_total[5m])
# 알림: 폴백 활성
continuum_cache_fallback_active == 1
Tier 4: KV Cache Index¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
continuum_kv_event_received_total | Counter | backend | 백엔드별 수신된 KV 이벤트 |
continuum_kv_event_processed_total | Counter | backend | 백엔드별 성공적으로 처리된 KV 이벤트 |
continuum_kv_event_dropped_total | Counter | backend | 채널 백프레셔로 인해 드롭된 KV 이벤트 |
continuum_kv_consumer_connected | Gauge | backend | 소비자가 SSE 스트림에 연결되면 1 |
continuum_kv_consumer_reconnects_total | Counter | backend | 백엔드별 SSE 재연결 시도 |
continuum_kv_index_entries | Gauge | — | 현재 추적 중인 (prefix, backend) 쌍 수 |
continuum_kv_index_events_total | Counter | backend, type | 인덱스 변경 (created, evicted) |
continuum_kv_index_query_latency_seconds | Histogram | — | KV index 쿼리 지연 시간 (초) |
continuum_kv_index_routing_decisions_total | Counter | decision | KV 인식 라우팅 결정 (kv_aware, fallback) |
continuum_kv_index_overlap_score | Histogram | — | 스코어러가 결정한 선택의 최고 복합 점수 (0.0 ~ 1.0) |
continuum_kv_index_event_source_status | Gauge | backend, status | 백엔드별 이벤트 소스 연결 상태 |
라우팅 레이블 두 개는 같은 지점에서 선택마다 한 번 기록됩니다. kv_aware는 스코어러가 이겨서 그 백엔드로 전달한 경우이고, fallback은 스코어러가 prefix key를 가지고 실행됐지만 복합 점수 임계값을 넘은 백엔드가 없어 설정된 전략이 대신 결정한 경우입니다. 스코어러가 아예 시도하지 않은 선택(스코어러 미등록, prefix key 없음)은 둘 다 기록하지 않습니다. continuum_kv_index_overlap_score는 kv_aware 결정의 최고 복합 점수를 관측하며, 이 값은 overlap 항목만이 아니라 overlap, load, health를 모두 반영한 가중 합입니다.
PromQL 예제:
# KV 인식 라우팅 활성화율
rate(continuum_kv_index_routing_decisions_total{decision="kv_aware"}[5m])
/ rate(continuum_kv_index_routing_decisions_total[5m])
# 백엔드별 이벤트 드롭율 (백프레셔 표시)
rate(continuum_kv_event_dropped_total[5m])
# 라우팅된 요청의 P50 overlap 점수
histogram_quantile(0.50, rate(continuum_kv_index_overlap_score_bucket[5m]))
# 연결 끊긴 이벤트 소비자
continuum_kv_consumer_connected == 0
관리자 엔드포인트¶
모든 관리자 엔드포인트는 /admin prefix 아래에 있으며, 설정된 경우 인증이 필요합니다.
Prefix 라우팅¶
GET /admin/prefix-routing/stats¶
라우팅 결정 횟수, overflow 비율, 백엔드 부하 분배, CHWBL 설정을 포함한 prefix 라우팅 통계를 반환합니다.
응답 예제:
{
"enabled": true,
"config": {
"max_prefix_length": 1024,
"load_factor_epsilon": 0.25,
"virtual_nodes": 150,
"anthropic_cache_control_injection": false
},
"routing_decisions": {
"total": 4926,
"prefix_hash": 4821,
"overflow": 93,
"fallback": 12,
"overflow_rate": "0.0189"
},
"backend_distribution": [
{ "backend": "vllm-1", "in_flight_requests": 4 },
{ "backend": "vllm-2", "in_flight_requests": 3 }
],
"unique_prefixes": 247
}
응답 캐시¶
GET /admin/response-cache/stats¶
응답 캐시 통계를 반환합니다: 히트/미스/스킵 횟수, 히트율, 항목 수, 메모리 사용량, Redis 연결 정보(해당하는 경우).
응답 예제:
{
"enabled": true,
"backend_type": "redis",
"entries": 1243,
"capacity": 5000,
"requests": {
"hit": 8912,
"miss": 2341,
"skip": 441,
"total": 11694
},
"hit_rate": "0.7924",
"evictions": 0,
"size_bytes": 0,
"config": {
"backend": "redis",
"ttl": "30m",
"capacity": 5000,
"max_response_size": 1048576,
"max_stream_buffer_size": 10485760
},
"redis": {
"connections": {
"active": 3,
"idle": 5
},
"fallback_active": false,
"errors": {
"connection": 0,
"timeout": 2,
"other": 0
}
}
}
POST /admin/response-cache/invalidate¶
캐시된 응답을 무효화합니다. JSON 본문을 받습니다:
응답 예제:
KV Cache Index¶
GET /admin/kv-index/stats¶
KV cache index 통계를 반환합니다: 항목 수, 라우팅 결정 내역, 쿼리 지연 시간 카운터, overlap 점수 카운트.
응답 예제:
{
"enabled": true,
"config": {
"backend": "memory",
"max_entries": 100000,
"entry_ttl_seconds": 600,
"event_sources_count": 2,
"scoring": {
"overlap_weight": 0.6,
"load_weight": 0.3,
"health_weight": 0.1,
"min_overlap_threshold": 0.3
}
},
"index": {
"prefix_count": 312,
"entry_count": 618,
"total_hits": 12490,
"total_evictions": 83
},
"event_sources": [
{
"backend_name": "vllm-1",
"connected": true,
"events_received": 8412,
"events_dropped": 0,
"last_event_at": "2026-03-13T10:24:17Z",
"reconnect_count": 0
}
],
"routing_decisions": {
"kv_aware": 9841,
"fallback": 2649,
"total": 12490
},
"query_latency_count": 12490,
"overlap_score_count": 9841
}
GET /admin/kv-index/backends¶
백엔드별 KV cache 이벤트 통계를 반환합니다: 수신/처리/드롭된 이벤트, 연결 상태, 인덱스 이벤트 수(생성/제거).
응답 예제:
{
"enabled": true,
"backends": [
{
"backend_name": "vllm-1",
"connection": {
"connected": true,
"reconnect_count": 0,
"last_event_at": "2026-03-13T10:24:17Z"
},
"events": {
"received": 8412,
"dropped": 0,
"index_created": 7981,
"index_evicted": 431
}
}
]
}
POST /admin/kv-index/clear¶
KV cache index의 모든 항목을 삭제합니다. 인덱스는 수신되는 이벤트로부터 자동으로 재구축됩니다. 디버깅용입니다.
응답 예제:
배포 가이드¶
Tier 1만 사용 (최소 설정)¶
외부 의존성 없이 GPU KV cache 지역성을 위한 prefix 라우팅을 활성화합니다:
동일한 모델의 여러 인스턴스가 백엔드에서 실행되고 system prompt가 길 때(>128 token) 효과적입니다.
Tier 1 + 2 (응답 캐시)¶
반복되는 결정적 요청을 완전히 제거하기 위한 응답 캐싱을 추가합니다:
prefix_routing:
enabled: true
response_cache:
enabled: true
backend: memory
capacity: 5000
ttl: "10m"
애플리케이션이 반복적으로 동일한 요청을 하는 경우(예: 고정 system prompt와 고정 쿼리를 사용하는 문서 QA) 효과적입니다.
Tier 1 + 2 + 3 (분산 응답 캐시)¶
다중 인스턴스 배포 시, 모든 라우터 인스턴스 간에 응답 캐시를 공유합니다:
prefix_routing:
enabled: true
response_cache:
enabled: true
backend: redis
ttl: "30m"
redis:
url: "redis://redis-service:6379"
pool_size: 16
key_prefix: "cr:resp:"
fallback_to_memory: true
fallback_capacity: 2000
Redis/Valkey는 모든 라우터 pod에서 접근 가능해야 합니다. 암호화된 연결을 위해 rediss:// URL 또는 tls: true를 사용하세요.
모든 Tier 사용 (전체 KV 인식 라우팅)¶
GPU 캐시 재사용을 극대화하기 위해 모든 단계를 활성화합니다:
prefix_routing:
enabled: true
load_factor_epsilon: 0.20
response_cache:
enabled: true
backend: redis
ttl: "30m"
redis:
url: "redis://redis-service:6379"
pool_size: 16
kv_cache_index:
enabled: true
backend: redis # response_cache.redis의 풀을 공유
max_entries: 500000
entry_ttl_seconds: 900
scoring:
overlap_weight: 0.6
load_weight: 0.3
health_weight: 0.1
min_overlap_threshold: 0.25
gpu_tier_weight: 1.0 # GPU 상주 데이터는 전체 overlap 점수 부여
storage_tier_weight: 0.6 # 오프로드 데이터도 유용하지만 할인 적용
storage_offloading:
enabled: true # GPU 핫 vs. 스토리지 웜 계층 추적
treat_eviction_as_offload: true
event_sources:
- backend_name: vllm-1
endpoint: "http://kv-listener.internal:7817/events/vllm-1"
- backend_name: vllm-2
endpoint: "http://kv-listener.internal:7817/events/vllm-2"
엔진 이벤트 소스 요구 사항¶
내부 네트워크에서 continuum-kv-listener를 실행하고, 각 엔진이 해당 listener의 ZMQ 엔드포인트로 KV 이벤트를 게시하도록 설정합니다. 라우터는 kv_cache_index.event_sources[]를 통해 listener의 SSE 엔드포인트만 소비합니다. 엔진 컨테이너에는 별도의 HTTP producer port가 필요하지 않습니다.
vLLM에서는 kv_events_config 또는 --kv-events-config를 백엔드에 대응하는 listener 엔드포인트로 설정합니다. 예시는 {"publisher":"zmq","endpoint":"tcp://kv-listener.internal:5557","topic":""}입니다. SGLang도 같은 ZMQ event class를 사용하므로 sibling source entry로 게시할 수 있습니다. Detokenize 전 token-derived event data가 이 포트를 지나가므로 listener bind address와 network policy를 내부 경계로 제한하세요.
네이티브 TensorRT-LLM에서는 listener source를 engine: trtllm으로 설정하고 trtllm_endpoint가 정확한 trtllm-serve /kv_cache_events URL을 가리키도록 합니다. TensorRT-LLM에서 block reuse를 활성화하고 event buffer size를 양수로 설정해야 합니다. Poll interval, request timeout, response byte 상한은 각각 제한됩니다. URL host는 allowed_publishers에 있어야 하고 redirect는 거부되며, host는 배포 내부 네트워크에서만 해석되도록 구성해야 합니다.
trtllm-serve에는 공개 detokenize endpoint가 없으므로 네이티브 TensorRT-LLM 소스는 라우터 발급 cache salt 연동에 의존합니다. 사용 가능한 첫 stored block은 source backend와 일치하는 salt를 가져야 하며 이후 block은 해석된 chain identity를 상속할 수 있습니다. Listener는 salt, 원시 response body, token ID를 기록하거나 게시하지 않습니다. Salt가 없거나 검증할 수 없으면 prefix를 임의로 귀속하지 않고 해당 chain을 버립니다.
Redis/Valkey 사이징¶
응답 캐시 사이징 추정:
- 평균 직렬화된 응답 크기: 항목당 2-10 KB
capacity = (target_hit_rate * rps * avg_unique_rate) / eviction_frequency
KV index의 경우:
- 각
(prefix, backend)항목은 메모리에서 약 200바이트를 소비 max_entries를 최소num_unique_prefixes * num_backends * 2로 설정하여 여유 확보
고가용성 고려 사항¶
- 응답 캐시와 KV index는 자동 인메모리 폴백(Tier 3)을 통해 Redis 장애를 허용합니다.
- KV index는 재시작 시 SSE 스트림에서 재구축됩니다; 영속성은 필요하지 않습니다.
- Prefix 라우팅(Tier 1)은 외부 의존성이 없으며 항상 사용 가능합니다.
- Redis 재시작 시 캐시 영속성이 필요한 경우 replication(Sentinel 또는 Cluster)으로 Redis를 배포하세요.
성능 특성¶
Tier 1: Prefix 라우팅¶
- Prefix key 추출 (SHA256): 요청당 < 10 us
- CHWBL ring 조회: O(log N), N =
virtual_nodes * num_backends; 일반적인 배포에서 < 5 us - 네트워크 I/O 없음; 완전히 프로세스 내에서 동작
Tier 2: 응답 캐시¶
- 인메모리 캐시 조회: < 1 us
- Cache key 계산 (SHA256): < 5 us
- 캐시 히트 시 백엔드 지연 시간 없이 전체 응답 제공
Tier 3: Redis 백엔드¶
- Redis GET 지연 시간 (LAN): 일반적으로 0.1-2 ms; P99 < 5 ms
- Redis SET 지연 시간: GET과 유사
- 명령 타임아웃 기본값: 1초; 이를 초과하는 작업은 폴백을 활성화
Tier 4: KV Cache Index¶
InMemoryKvIndex.query_backends(): < 100 us (DashMap 읽기, 빈 결과 시 할당 없음)RedisKvIndex.query_backends(): Redis GET 지연 시간과 동일 (0.1-2 ms)KvOverlapScorer.prepare(): 100 ms 윈도우당 고유 prefix마다query_backends()한 번 호출KvOverlapScorer.score(): < 1 us (사전 가져온 캐시에서의 동기 읽기)- 1000회 스코어링 호출: 총 < 100 ms (
scorer.rs의 단위 벤치마크에서 검증)
기대 효과¶
다음은 일반적인 LLM 워크로드 패턴에 기반한 예시적 추정치입니다:
| 시나리오 | 지표 | 기대 개선 |
|---|---|---|
| 긴 system prompt (>512 token), 요청 간 반복 | Time-to-first-token | KV cache 재사용으로 20-40% 감소 |
| 고정 문서 QA (동일 문서 + 동일 질문) | 백엔드 요청 | 응답 캐시로 최대 100% 제거 |
| 다중 복제본 vLLM, 핫 prefix | 캐시 히트율 (Tier 4) | 60-80% 요청이 warm 캐시를 가진 백엔드로 라우팅 |
| Redis 장애 | 서비스 가용성 | 성능 저하 없음; 한 요청 내에 인메모리로 폴백 |
실제 효과는 워크로드의 prefix overlap, GPU 메모리 용량, 백엔드 설정에 따라 달라집니다.