KV Cache 최적화¶
Continuum Router는 LLM 백엔드의 중복 연산을 줄이는 4단계 KV 캐시 최적화 시스템을 제공합니다. 네 단계는 서로 종속된 계층이 아니라 개별 설정 섹션으로 제어하는 독립적인 기능입니다. 활성화된 단계는 정해진 순서로 실행됩니다: Tier 2(응답 캐시), Tier 4(KV 캐시 인덱스 스코어러), 그리고 설정된 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)은 요청의 토큰 프리픽스에 대해 계산된 attention key-value 텐서를 GPU 메모리의 KV 캐시에 보관합니다. 동일한 프리픽스가 다시 들어오면 엔진은 해당 텐서의 재계산을 건너뛰어, 긴 시스템 프롬프트나 반복되는 컨텍스트에서 GPU 연산 시간을 크게 절약합니다.
Continuum Router는 네 가지 상호 보완적인 메커니즘으로 백엔드 전반의 KV 캐시 재사용률을 극대화합니다.
- Prefix 인식 고정 라우팅: 동일한 프롬프트 프리픽스를 공유하는 요청을 일관 해싱(consistent hashing)으로 같은 백엔드에 라우팅하여 GPU KV 캐시를 웜 상태로 유지합니다.
- 응답 캐시: 반복되는 결정론적(deterministic) 요청을 백엔드 호출 없이 라우터 메모리나 Redis에서 즉시 반환합니다.
- 공유 외부 캐시: 응답 캐시 상태를 Redis/Valkey에 저장해 여러 라우터 인스턴스가 동일한 캐시 항목을 공유합니다.
- Backend KV Cache Index: 최근 프리픽스에 대해 실제로 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(Redis/Valkey)는 그 하단에서 공유 스토리지 역할을 합니다.
- Tier 4는 백엔드 선택 단계에서 가장 먼저 실행됩니다. 활성화 조건은
selection_strategy와 무관하며prefix_routing.enabled(프리픽스 키 생성)와kv_cache_index.enabled(인덱스 유지)가 모두 켜져 있을 때 동작합니다. - Tier 1은 Tier 4 스코어러가 백엔드를 선택하지 않았고
selection_strategy가PrefixAwareHash로 설정된 경우에 실행됩니다.
Tier 1: Prefix 인식 고정 라우팅¶
Tier 1은 공통 프롬프트 프리픽스를 공유하는 요청을 동일한 백엔드로 라우팅하여 해당 백엔드의 GPU KV 캐시가 이미 웜 상태일 확률을 높입니다.
Prefix Key 추출¶
수신된 각 chat completion 요청에서 라우터는 요청의 의미적 앵커를 고유하게 식별하는 32바이트 SHA-256 다이제스트인 **프리픽스 키(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은 동일한 텍스트가 시스템 프롬프트에 위치할 때와 첫 번째 사용자 메시지에 위치할 때 서로 다른 해시값을 갖도록 구분합니다.
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)**을 사용해 프리픽스 키를 백엔드에 매핑합니다. 단순 일관 해싱은 특정 프리픽스에 요청이 집중될 때 부하 불균형을 초래할 수 있으므로, Continuum Router는 Consistent Hashing with Bounded Loads(CHWBL) 알고리즘을 적용합니다.
CHWBL은 백엔드당 동시 처리 요청 수를 최대 (1 + epsilon) * average_load로 제한하는 부하 상한을 둡니다. 특정 백엔드가 부하 상한에 도달하면 요청은 링에서 시계 방향의 다음 노드로 오버플로됩니다.
키 분배의 균등성을 높이기 위해 링에는 백엔드당 virtual_nodes(기본값: 150)개의 가상 노드가 배치됩니다. virtual_nodes는 핫 리로드를 지원하므로 재시작 없이 새 크기로 해시 링을 재구성할 수 있습니다.
prefix key가 없을 때¶
CHWBL 링은 요청에 프리픽스 키가 존재할 때만 사용됩니다. 다음 두 경우에는 프리픽스 키가 생성되지 않습니다.
prefix_routing.enabled가 기본값인false인 경우: 추출 함수request_prefix_key와typed_request_prefix_key가 즉시None을 반환합니다.prefix_routing.enabled는true이지만 요청 본문에 시스템 프롬프트와 메시지 배열이 모두 비어 있어 해싱할 기준 텍스트가 없는 경우.
이 두 경우에 PrefixAwareHash 전략은 프리픽스 대신 모델 이름을 해싱하여 일반 일관 해시 경로로 링을 탐색합니다. 이 경로에는 부하 상한이 적용되지 않으므로, 모델 해시 위치에서 시계 방향으로 처음 만나는 노드를 항상 선택하며 풀 구성이 바뀌지 않는 한 동일 모델의 모든 요청이 한 백엔드로 고정됩니다.
따라서 PrefixAwareHash가 프리픽스 기반 분산으로 동작하려면 반드시 prefix_routing.enabled: true를 함께 설정해야 합니다. 프리픽스 라우팅이 꺼진 상태에서 PrefixAwareHash를 설정하면 continuum-router config validate가 selection_strategy 경로에 경고를 출력하며, 라우터 시작 시점과 핫 리로드 시점에도 동일한 경고 로그를 남깁니다.
라우팅 결정 레이블¶
continuum_prefix_routing_requests_total{strategy=...} 메트릭은 PrefixAwareHash 분기가 실행될 때만 선택당 한 번 기록됩니다.
prefix_hash: 프리픽스 해시가 지정한 소유 백엔드가 요청을 처리한 경우입니다. 소유 백엔드가 부하 상한 이내였던 일반 케이스와, 모든 백엔드가 상한에 도달해 프리픽스 지역성 유지를 위해 원래 소유 백엔드를 선택한 케이스를 포함합니다.overflow: 소유 백엔드가 부하 상한에 도달해 링에서 시계 방향의 다음 노드로 요청이 넘어간 경우입니다.fallback: 요청에 프리픽스 키가 없어 모델 이름을 해싱하고 부하 상한 없이 링을 탐색한 경우입니다. 위의 prefix key가 없을 때를 참고하세요.
Anthropic Cache Control 주입¶
anthropic_cache_control_injection: true로 설정하면 라우터는 네이티브 Anthropic 요청의 시스템 프롬프트에 cache_control: { type: "ephemeral" } 마커를 자동으로 주입합니다. 이를 통해 라우터 수준의 KV 캐시와 상호 보완적으로 동작하는 Anthropic 서버 측 프롬프트 캐싱을 활성화할 수 있습니다.
이 플래그는 플릿 전체에 적용되는 마스터 스위치이며 요청마다 참조됩니다(핫 리로드 지원). 백엔드별 anthropic_auto_cache_control 설정과의 우선순위는 다음과 같습니다.
- 기본 상태에서는 백엔드별
anthropic_auto_cache_control(미설정 시 기본값true)이 동작을 결정하므로 네이티브 Anthropic 백엔드는 별도 설정 없이도 자동 주입됩니다. prefix_routing.anthropic_cache_control_injection: true로 설정하면anthropic_auto_cache_control: false로 지정된 백엔드에도 주입을 강제합니다.- 기본값(
false)일 때는 백엔드별 설정만 따릅니다. - 마커 주입은 네이티브 Anthropic 요청의 마지막 시스템 블록에만 적용되며, 요청(시스템, 메시지, 도구 정의)에 이미
cache_control이 포함되어 있으면 호출자의 의도를 보존하기 위해 주입을 건너뜁니다.
Tier 2: 응답 캐시¶
응답 캐시는 결정론적 요청에 대한 전체 LLM 응답을 저장하여 백엔드를 호출하지 않고도 반복 쿼리에 즉시 응답합니다.
캐시 적격성¶
다음 조건을 모두 만족하는 요청만 응답 캐시 대상이 됩니다.
temperature가0이거나 생략됨- 스트리밍을 사용하지 않거나, 스트리밍 버퍼링이 활성화됨
- 누적 응답 크기가
max_response_size이하임
temperature가 0이 아닌 확률적 요청은 캐싱되지 않습니다. 캐시 처리 결과는 응답 헤더 X-Cache: HIT, X-Cache: MISS, X-Cache: BYPASS로 표시됩니다.
Cache Key 계산¶
캐시 키는 LLM 출력에 영향을 미치는 모든 요청 파라미터에 대한 SHA-256 해시입니다.
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,
)
선택적 파라미터의 None과 Some(0) 간 해시 충돌을 방지하기 위해 SOME/NONE 태그 바이트를 함께 해싱합니다. 또한 멀티테넌트 격리를 위해 tenant_id를 포함하므로 서로 다른 테넌트 간에는 캐시된 응답이 공유되지 않습니다.
구현: src/infrastructure/cache/response_cache.rs
스트리밍 캐시¶
스트리밍 응답의 경우 라우터는 SSE 스트림을 최대 max_stream_buffer_size(기본값: 10 MiB)까지 버퍼에 누적합니다. 전체 스트림이 한도 내에 들어오면 단일 직렬화 블롭으로 저장하고, 이후 캐시 히트 시 합성 SSE 스트림으로 재전송합니다.
캐시 제거¶
인메모리 백엔드는 항목 수가 capacity에 도달하면 LRU 방식으로 항목을 제거하며, Redis 백엔드는 Redis 자체의 TTL 만료 기능을 사용합니다.
시맨틱 캐시¶
정확 일치(exact) 및 프리픽스 조회가 모두 미스일 때, 결정론적 요청의 마지막 사용자 메시지가 이전 요청과 의미적으로 유사하면 캐시된 응답을 반환하는 선택적 시맨틱 캐시 경로를 사용할 수 있습니다. 시맨틱 캐시는 다음 조건이 모두 충족될 때만 실행됩니다.
response_cache.semantic.enabled: true이며backend와embedding_model이 지정되어 있음- 라우터의
control_plane.optimization.semantic_cache_enabled: true가 활성화되어 있음 - 조직의 Hub 최적화 정책에서
semantic_cache와exact_cache가 모두 켜져 있고, 해당 티어의cache_enabled가 활성화되어 있음 temperature: 0인 비스트리밍/v1/chat/completions요청이며, 마지막 메시지가max_input_bytes이하의 텍스트를 담은user턴임
임베딩 조회 및 백엔드 가시성 규칙¶
시맨틱 캐시 조회는 가드레일 입력 게이트 이후에 실행됩니다. 따라서 가드레일이 차단한 프롬프트는 임베딩 백엔드로 전송되지 않으며, transform 판정이 내려진 경우 변환된 텍스트로 임베딩을 계산합니다.
조건을 통과한 요청은 지정된 백엔드로 POST /v1/embeddings를 한 번 호출하며 타임아웃은 embedding_timeout_ms로 제한됩니다. 캐시 미스 시에는 계산된 벡터를 보관했다가 백엔드 응답이 도착하면 해당 벡터 아래에 응답을 색인합니다. 임베딩 호출이 실패하거나 시간 초과되면 채팅 요청을 실패시키지 않고 일반 캐시 미스로 처리합니다.
임베딩 백엔드 지정에는 LLM 분류기 및 가드레일과 동일한 가시성 규칙이 적용되어, 운영자가 명시한 internal: true 백엔드는 호출할 수 있지만 enabled: false 백엔드는 거부됩니다.
파티셔닝 및 격리 경계¶
시맨틱 캐시의 격리는 파티션 다이제스트로 보장됩니다. 코사인 유사도는 동일한 파티션 다이제스트를 가진 항목 사이에서만 계산되며, 다이제스트에는 다음 요소가 모두 포함됩니다.
- 호출자의 캐시 식별자 (경로 범위 로컬 식별자 및 Hub 키 ID)
- 대상 모델 이름 및 임베딩 모델 이름
- 출력 형태에 영향을 주는 모든 요청 필드 (
tools,response_format, 샘플링 파라미터,seed) - 시스템 프롬프트를 포함한 마지막
user턴 이전의 전체 대화 이력
따라서 API 키, 시스템 프롬프트, 또는 도구 정의가 하나라도 다르면 시맨틱 캐시 항목을 절대 공유하지 않습니다.
응답 제공, 저장소 구조 및 TTL¶
후보 항목의 코사인 유사도가 threshold(생략 시 Hub 정책의 semantic_similarity_bps) 이상이면 캐시 히트로 응답합니다. 응답에는 X-Cache: HIT와 X-Cache-Mode: semantic 헤더가 추가되고 Hub 사용량 레코드에는 cache_hit_type = semantic이 기록되며, 다른 로컬 캐시 히트와 마찬가지로 토큰 한도나 예산을 차감하지 않습니다.
응답 본문은 응답 캐시 저장소의 sem: 키 공간에 저장되고, 벡터 인덱스는 외부 의존성 없이 라우터별 인메모리 구조(max_entries 상한, 오래된 항목부터 제거)로 관리됩니다. 항목 TTL은 Hub 정책의 cache_ttl_secs, semantic.ttl, response_cache.ttl 순으로 적용됩니다. 라우터 간 공유 벡터 인덱스, 외부 벡터 데이터베이스, 스트리밍 시맨틱 캐시는 지원하지 않습니다.
시맨틱 캐시 본문은 정확 일치 및 프리픽스 캐시와 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를 사용합니다. 동일한 Redis 인스턴스를 공유하는 다른 애플리케이션과의 충돌을 방지하기 위해 모든 키에 설정 가능한 프리픽스(기본값: cr:resp:)를 붙입니다.
키 네임스페이스 형식:
쓰기에는 SET EX, 읽기에는 GET을 사용하며 명령 타임아웃(기본값: 1초)을 설정할 수 있습니다.
자동 폴백¶
Redis에 연결할 수 없는 경우 RedisCacheStore는 첫 연결 실패 시 자동으로 인메모리 폴백 캐시를 활성화합니다. 이후 백그라운드 헬스 모니터 작업이 30초마다 Redis 연결 복구를 시도하며, 연결이 복구되면 폴백 플래그를 해제하고 다시 Redis를 사용합니다.
현재 폴백 모드가 활성화된 상태인지는 continuum_cache_fallback_active 메트릭(값 1)으로 확인할 수 있습니다.
연결 풀 공유¶
deadpool_redis::Pool은 AppState 내에 Arc로 저장되어 응답 캐시와 KV 캐시 인덱스(Tier 4)가 함께 공유합니다. 따라서 두 기능이 동일한 연결 풀과 자격 증명 설정을 재사용하며 불필요한 중복 연결을 만들지 않습니다.
Tier 4: Backend KV Cache Index¶
Tier 4는 특정 토큰 프리픽스 해시에 대해 실제로 GPU에 상주하는 KV 텐서를 보유한 백엔드를 실시간으로 추적합니다. 이를 통해 단순 해시 친화도가 아니라 실제 GPU 캐시 상태에 기반해 라우팅을 결정할 수 있습니다.
이벤트 소비¶
이벤트 수집 브리지로는 continuum-kv-listener 바이너리를 사용합니다. 리스너는 vLLM 및 SGLang의 ZMQ msgpack 레코드(vLLM <= 0.28 및 SGLang의 배열 인코딩 시퀀스와 vLLM 0.29+의 맵 인코딩 tagged-struct 객체 모두 지원)를 수신하거나 네이티브 trtllm-serve의 JSON 이벤트를 HTTP로 폴링합니다.
이후 블록 체인을 라우터와 동일한 프리픽스 해시로 변환하여 백엔드별 SSE 스트림(예: http://kv-listener.internal:7817/events/vllm-1)으로 노출합니다. 라우터의 KvEventConsumerManager는 백엔드마다 백그라운드 Tokio 태스크를 띄워 이 스트림을 구독하고 이벤트를 처리합니다.
| 엔진 소스 | 수집 방식 | Prefix 식별 | 기본 medium |
|---|---|---|---|
| vLLM | ZMQ publisher (vLLM <= 0.28 배열 인코딩 msgpack; vLLM 0.29+ 맵 인코딩 tagged-struct msgpack) | 가능한 경우 라우터가 발급한 cache salt, 그 외에는 엔진 /detokenize | 이벤트가 제공한 medium |
| SGLang | ZMQ publisher (배열 인코딩 msgpack) | 가능한 경우 라우터가 발급한 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를 유지 |
블록 체인 추적 및 메모리 한도¶
리스너는 모든 블록에 전체 토큰 체인을 복사하는 대신 각 블록의 토큰 세그먼트와 부모 블록 참조만 저장합니다.
max_chain_tokens(기본값:16384):/detokenize호출을 위해 재구성하는 토큰 체인 길이를 제한합니다. 체인이 이 한도 안에 들어올 때만 토큰 세그먼트를 보관하며, 더 긴 체인은 메타데이터 한도 내에서 시퀀스만 유지한 채 미귀속 상태로 추적합니다.max_tracked_blocks(기본값:100000) 및max_retained_tokens(기본값:16777216, 4바이트 ID 기준 64 MiB): 각각 블록 메타데이터 수와 보관 토큰 ID 수를 제한하며, 한도 도달 시 가장 오래된 항목부터 개별 정리합니다.- 두 한도 중 하나에 도달하더라도 백엔드의 전체 프리픽스를 초기화하지 않고, 마지막 참조가 정리된 프리픽스만 제거합니다. 따라서 소스당 보관되는 토큰 ID 메모리는 항상
min(max_retained_tokens, max_tracked_blocks * max_chain_tokens)(기본값 기준 64 MiB) 이하로 유지됩니다. - Salt 에코 및 네이티브 TensorRT-LLM 소스는 토큰을 재구성하지 않고 검증된 프리픽스 식별자를 바로 유지합니다.
엔진별 block_size 처리¶
정상적인 단일 블록 이벤트는 block_size개의 토큰을 담지만, block_size는 엔진이 전달하는 무제한 값이므로 리스너는 엔진별 특성에 맞춰 토큰 수를 검증합니다.
- SGLang (
8055ccd2cd커밋의python/sglang/srt/mem_cache/events.py기준): 페이지마다 이벤트를 발행하며block_size를 페이지 길이로 설정하고 동일한block_size의 이벤트만 병합하므로len(token_ids) == block_size * len(block_hashes)가 성립합니다. - vLLM (
713ec07655커밋의vllm/v1/core/block_pool.py기준): 전체 블록 경로에서는 해시마다 KV 블록 하나의 구간을 보내지만, null 또는 마스킹된 블록은block_hashes에서 제외하면서 토큰 구간은 연속으로 유지하므로 슬라이딩 윈도나 하이브리드 어텐션에서는 해시 하나가 여러 블록 구간을 담을 수 있습니다. 부분 블록 경로에서는 정확히hash_block_size크기의 세그먼트 하나를 보냅니다. - 불일치 블록 처리: 비어 있지 않은
token_ids길이가 0이 아닌block_size와 일치하지 않는 단일 블록 이벤트는 토큰 없이 체인 길이만 추적하며(하위 블록은 프리픽스 미귀속 처리), 이벤트마다 카운터를 올리고 소스당 한 번 경고 로그를 남깁니다.
네이티브 TensorRT-LLM 이벤트 처리¶
TensorRT-LLM 소스에서는 created 이벤트가 리스너의 블록 추적 상태를 초기화하고, stored 이벤트가 체인을 생성하거나 연장하며, removed 이벤트가 해석된 프리픽스를 제거합니다. 폴러는 이벤트 ID를 기준으로 중복 응답을 걸러내고 시퀀스 누락이 발생하면 이전 상태를 비웁니다. 이미 vLLM 호환 ZMQ 이벤트를 발행하는 Dynamo 배포에서는 별도의 네이티브 HTTP 소스를 중복 설정하지 말고 기존 ZMQ 소스를 그대로 사용하세요.
이벤트 유형:
| 이벤트 | 의미 |
|---|---|
cache_created | 해당 백엔드에서 토큰 프리픽스에 대한 KV 블록이 생성됨 (GPU VRAM 적재) |
cache_evicted | 해당 백엔드의 GPU 메모리에서 KV 블록이 축출됨 |
cache_offloaded | KV 블록이 GPU에서 외부 스토리지(예: S3 호환 스토리지)로 오프로드됨 |
cache_reloaded | KV 블록이 외부 스토리지에서 GPU 메모리로 다시 로드됨 |
cache_purged | KV 블록이 모든 스토리지 계층에서 영구 삭제됨 |
각 이벤트에는 prefix_hash(16진수 문자열)와 해당 백엔드에 캐시된 토큰 수를 나타내는 선택적 token_count가 포함됩니다.
SSE 파싱 세부사항:
- 컨슈머는 이벤트 유형을 판별할 때 SSE
event:필드를 우선 사용하며, 없을 때만 JSON 본문의event필드를 사용합니다. - 잘못된 스트림으로부터 메모리를 보호하기 위해 줄 버퍼와 단일 레코드의 누적
data:크기는 모두 1 MiB(MAX_SSE_BUFFER_SIZE)로 제한됩니다. 레코드 종료를 나타내는 빈 줄 없이data:줄만 계속 도착해 한도를 초과하면 컨슈머는 즉시 스트림을 끊고 백오프 후 재연결하여 마지막으로 전달받은 이벤트 ID부터 이어받습니다. - 연결 실패나 스트림 종료 시 지수 백오프(초기 1초, 최대 60초) 후 재연결합니다.
구현: src/infrastructure/kv_index/event_consumer.rs
Producer bridge 구현: crates/continuum-kv-listener/
푸시 모드: 리스너가 라우터에 접속¶
기본 설정에서는 라우터가 각 백엔드의 이벤트를 리스너의 SSE 엔드포인트에서 풀(pull) 방식으로 가져오므로 라우터에서 리스너로의 네트워크 연결이 가능해야 합니다. 만약 리스너가 사설 GPU 클러스터 내부에서 실행되고 라우터가 클러스터 외부에 있다면, 이벤트 소스에 transport: event_bus를 설정하고 리스너에 event_bus 섹션을 추가하세요.
푸시 모드에서는 리스너가 TLS 및 HTTP/2를 통해 라우터의 이벤트 버스로 아웃바운드 연결을 맺고 백엔드별 데이터 스트림으로 동일한 이벤트를 푸시합니다. 이때도 리스너의 로컬 SSE 서버는 그대로 유지되며, transport를 지정하지 않은 소스는 기존 SSE 풀 방식을 사용합니다. 푸시 모드를 사용하려면 dynamo-event-bus 기능이 포함된 라우터 바이너리(공식 릴리스 바이너리에 기본 포함)가 필요합니다.
sequenceDiagram
participant L as continuum-kv-listener
participant R as Router event bus
participant C as kv_cache_index consumer (vllm-1)
L->>R: POST /bus/v1/session, hello (role kv_listener, sources vllm-1, vllm-2)
R-->>L: welcome
C->>R: open_announced_stream(vllm-1, last_event_id)
R->>L: subscribe (announced_source vllm-1, last_event_id)
L->>R: POST /bus/v1/streams/{id}, exact SSE bytes
R->>C: SSE bytes, parsed and tagged vllm-1 양쪽 설정은 client_id와 backend_name으로 매칭됩니다. 라우터에는 dynamo.event_bus.brokers[] 아래에 role: kv_listener 항목을 추가하고, 각 백엔드 이벤트 소스에 transport: event_bus를 설정합니다.
# 라우터
dynamo:
event_bus:
enabled: true
listen: "0.0.0.0:9443"
tls:
cert: "/etc/continuum-router/event-bus/tls.crt"
key: "/etc/continuum-router/event-bus/tls.key"
brokers:
- id: "kv-listener-a" # 리스너의 event_bus.client_id와 같아야 함
role: kv_listener
token_hash: "sha256:<64 lowercase hex>" # printf %s "$TOKEN" | sha256sum
backends: ["vllm-1", "vllm-2"] # 이 자격 증명이 알릴 수 있는 유일한 백엔드
kv_cache_index:
enabled: true
event_sources:
- backend_name: vllm-1
transport: event_bus # endpoint 없음
reconnect_interval_ms: 5000
- backend_name: vllm-2
transport: event_bus
# continuum-kv-listener (config.kv-listener.yaml)
listen: "127.0.0.1:7817"
event_bus:
url: "https://router.example.com:9443" # https만 허용; host[:port], 기본 포트 443
ca_file: "/etc/continuum/bus-ca.pem" # 공개 웹 루트를 쓰려면 생략
client_id: "kv-listener-a"
credential_file: "/etc/continuum/bus-token" # 또는 credential_env; 인라인 금지
sources:
- backend_name: "vllm-1" # 유효한 소스 id여야 함: [A-Za-z0-9_-] 1~64자
engine: "vllm"
zmq_endpoint: "tcp://127.0.0.1:5557"
zmq_mode: "bind"
detokenize_endpoint: "http://vllm-1:8000/detokenize"
model: "meta-llama/Llama-3.1-8B-Instruct"
allowed_publishers: ["127.0.0.1"]
# ... vllm-2도 마찬가지
전체 리스너 설정 옵션(tls_server_name, mTLS용 client_cert_file 및 client_key_file, credential_env, reconnect_backoff, drain_deadline, connect_timeout)은 config.kv-listener.yaml.example과 continuum-kv-listener(1) 매뉴얼 페이지를 참고하세요.
스트림 동작 및 재연결¶
- 이벤트 ID 기반 이어받기: 백엔드
X의 컨슈머는 소스X만 열고 SSE 전송과 동일한 파서로 바이트를 파싱해 백엔드X를 태깅합니다. 스트림이 끊어졌다가 다시 연결될 때는 마지막으로 처리한 이벤트 ID부터 이어받으므로 리스너 히스토리에 남아 있는 이벤트를 중복 재생하거나 건너뛰지 않습니다. - 백오프 및 세션 대기: 스트림 오류 시
reconnect_interval_ms부터 시작해max(reconnect_interval_ms, 60s)까지 두 배씩 대기 시간을 늘려 재연결합니다. 단, 활성 리스너 세션이 아직 해당 소스를 광고하지 않은 상태(no_session)에서는 대기 시간을 늘리지 않고reconnect_interval_ms간격으로 재시도하여 리스너 연결 직후 빠르게 스트림을 엽니다. - 백프레셔 및 드레인: 컨슈머가 실시간 피드 속도를 따라가지 못하면 리스너는 이벤트를 임의로 버리는 대신
buffer_overflow로 스트림을 닫고 라우터가 마지막 이벤트 ID부터 재구독하도록 합니다. 리스너가SIGTERM으로 종료될 때는draining을 전송하며, 라우터는 즉시 스트림을 닫고 다른 활성 세션에 소스를 재요청합니다.kv_cache_index핫 리로드 시에도 버스 소스는 일반 소스와 함께 재시작됩니다.
전송 동등성¶
푸시 스트림의 바이트는 리스너의 GET /events/{backend}와 동일한 코드 경로에서 생성되며 라우터에서도 같은 SSE 파서로 처리됩니다. 골든 픽스처(crates/continuum-kv-listener/tests/fixtures/push_stream_golden.sse)를 통해 리스너 테스트와 라우터 동등성 테스트 양쪽에서 동일한 결과를 검증합니다.
보안 및 권한 제어¶
- TLS 및 자격 증명 격리: 인증이 없는 SSE 포트와 달리 이벤트 버스는 TLS 전용이며 Bearer 토큰 해시(
SHA-256) 또는 mTLS 클라이언트 인증서(항목id와 일치하는 DNS SAN)로 인증합니다. - 역할 및 백엔드 권한:
broker자격 증명으로는kv_listener세션을 열 수 없으며 그 반대도 불가능합니다. 리스너는 자신의 항목에 나열된backends만 광고할 수 있고, 핫 리로드로 허용 목록이 줄어들면 기존 세션은forbidden으로 즉시 종료됩니다. - Hub 설정 동기화 제외: 버스 소스는 Control Plane Hub 스키마에
transport필드가 없으므로 Hub 설정 동기화 대상에서 제외됩니다.
인덱스 구조¶
수신된 이벤트는 다음 매핑을 유지하는 KvCacheIndex 구현체에 반영됩니다.
토큰 수가 곧 점수로 사용되므로 특정 프리픽스에 대해 2048개의 캐시 토큰을 보유한 백엔드가 512개를 보유한 백엔드보다 높은 순위를 얻습니다.
두 가지 구현체가 제공됩니다.
InMemoryKvIndex¶
- 락프리 동시 읽기를 위한
DashMap<String, PrefixEntry>사용 - 항목 수가
max_entries에 도달하면 LRU 방식으로 가장 오래된 10% 항목 제거 query_backends()호출 시 지연(lazy) 확인되는 TTL 기반 만료- 주기적인
cleanup_expired()작업으로 만료 항목 사전 정리 - 기본값: 최대 100,000개 항목, 300초 TTL
RedisKvIndex¶
- 각 프리픽스를 Redis Sorted Set으로 저장:
ZADD cr:kvidx:<prefix_hash> <token_count> <backend_id> EXPIRE를ZADD와 단일 원자적 파이프라인으로 함께 실행ZREVRANGEBYSCORE +inf -inf WITHSCORES로 점수 내림차순 백엔드 목록 조회- 여러 라우터 인스턴스 간 KV 인덱스 상태 공유 지원
- 키 프리픽스:
cr:kvidx:
구현: src/infrastructure/kv_index/index.rs
스토리지 계층 인식¶
storage_offloading.enabled가 true이면 인덱스는 각 (prefix, backend) 항목에 대해 두 가지 스토리지 계층을 추적합니다.
| 계층 | 이름 | 설명 |
|---|---|---|
| 핫 | GpuHot | KV 데이터가 GPU VRAM에 상주하며 재로드 지연 없이 즉시 히트 처리됩니다. |
| 웜 | StorageWarm | KV 데이터가 외부 스토리지(예: S3 호환 스토리지)로 오프로드된 상태이며 재로드 지연이 추가됩니다. |
이벤트에 따른 계층 전환:
| 이벤트 | 결과 계층 |
|---|---|
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 트레이트를 구현하며, 요청의 프리픽스에 대해 캐시 데이터를 실제로 보유한 백엔드마다 복합 점수를 계산합니다.
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)
해당 프리픽스에 대해 인덱스에 항목이 없는 백엔드는 0.0점을 받고 순위 계산에서 제외됩니다. 따라서 부하와 상태 점수는 캐시 데이터를 실제로 보유한 백엔드들 사이에서만 우열을 가립니다. 이 규칙 덕분에 캐시가 전혀 없는 유휴 백엔드가 load_weight + health_weight(기본값 0.4)만으로 풀 임계값(0.3)을 넘어 기본 라우팅 전략을 덮어쓰는 일이 발생하지 않습니다.
가중치 설정 및 적용 시점¶
6개 스코어링 파라미터는 시작 시 스코어러가 등록될 때 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)는 가중 합산을 구하기 전에 raw_overlap에 곱해집니다. 따라서 동일한 토큰 수를 캐시하고 있더라도 StorageWarm 백엔드는 GpuHot 백엔드보다 낮은 점수를 받습니다.
부하 및 상태 입력¶
부하 항목은 CHWBL 부하 상한과 동일한 실시간 게이지인 백엔드 풀의 in-flight 트래커에서 가져옵니다. prepare() 단계에서 인덱스 쿼리와 함께 트래커 스냅샷을 한 번 캡처해 100ms 동안 캐시하므로, 한 번의 스코어링 패스 안에서 모든 후보 백엔드가 동일한 시점의 부하 기준으로 비교됩니다.
상태 항목은 공유 통계 맵의 백엔드 성공률을 동기적으로 읽으며, 잠금 경합이 발생하면 블로킹하지 않고 중립값 1.0을 사용합니다.
최소 overlap 임계값¶
min_overlap_threshold(기본값: 0.3)는 각 보유 백엔드의 계층 반영 정규화 점수(raw_overlap * tier_multiplier)에 적용됩니다. 이 임계값에 미달하는 백엔드는 캐시가 없는 백엔드와 동일하게 0.0점을 받습니다.
raw_overlap이 최다 토큰 보유 백엔드를 기준으로 정규화되므로 다음과 같이 동작합니다.
- 가장 많은 토큰을 보유한
GpuHot백엔드는raw_overlap = 1.0이므로 계층 반영 점수가gpu_tier_weight가 되며,gpu_tier_weight >= min_overlap_threshold(기본값1.0 >= 0.3)이면 통과합니다. StorageWarm사본만 보유한 백엔드는storage_tier_weight >= min_overlap_threshold(기본값0.6 >= 0.3)일 때만 통과합니다. 만약storage_tier_weight를 임계값보다 낮게 설정하면 웜 스토리지 사본만 가진 백엔드는 스코어링 후보에서 제외됩니다.
만약 gpu_tier_weight를 min_overlap_threshold보다 낮게 설정하면 어떤 백엔드도 임계값을 통과하지 못해 항상 기본 selection_strategy로 폴백됩니다. prefix_routing.enabled와 kv_cache_index.enabled가 모두 켜진 상태에서 이 조합이 감지되면 continuum-router config validate와 라우터 시작 로그가 kv_cache_index.scoring.gpu_tier_weight 경로에 경고를 출력합니다.
후보 백엔드 중 임계값을 통과한 백엔드가 하나도 없으면 모든 점수가 0.0이 됩니다. 풀이 요구하는 복합 점수 임계값은 0.3 초과(DEFAULT_SCORER_THRESHOLD, 초과 비교)이므로, 이 경우 설정된 selection_strategy가 백엔드를 선택합니다.
비동기 준비 모델 및 보유 계층 기록¶
KvCacheIndex.query_backends()는 비동기 함수이지만 BackendScorer.score()는 동기 함수여야 하므로 스코어러는 2단계로 동작합니다. 먼저 prepare()가 인덱스 조회 결과와 in-flight 스냅샷을 가져와 100ms 내부 TTL로 캐시하고, 이어서 score()가 캐시된 데이터를 동기적으로 읽습니다.
구현: src/infrastructure/kv_index/scorer.rs
또한 스코어러는 BackendScorer::score_with_tier를 통해 점수와 보유 계층(gpu_hot, storage_warm, none)을 한 번의 읽기에서 함께 반환합니다. 백엔드 풀은 스코어링 루프에서 선택된 백엔드의 보유 계층을 요청 상태에 함께 보관했다가 continuum_kv_index_routing_decisions_total 메트릭의 tier 레이블로 기록합니다. 점수와 계층을 한 번에 읽어 요청 상태에 보관하므로 동시 요청의 prepare() 호출로 인해 기록 전에 캐시 슬롯이 덮어써지더라도 계층 레이블이 누락되지 않습니다.
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 스코어러는 selection_strategy보다 먼저 실행되며 프리픽스 인식 라우팅과 상호 보완적으로 동작합니다.
- 후보 백엔드 중 해당 프리픽스 데이터를 보유한 백엔드가 있고 최고 복합 점수가 풀 임계값(
0.3)을 초과하면 스코어러가 백엔드를 결정합니다. - 그렇지 않으면 설정된
selection_strategy가 선택을 수행합니다(PrefixAwareHash인 경우 CHWBL 링을 사용하며,RoundRobin,WeightedRoundRobin,LeastLatency,Random,ConsistentHash는 각자의 알고리즘을 실행).
Tier 4는 prefix_routing.enabled와 kv_cache_index.enabled만으로 활성화되므로, RoundRobin 전략에서도 두 옵션을 켜면 평시에는 KV 인식 라우팅을 수행하고 캐시 미스 시에만 라운드로빈으로 폴백합니다.
Gemini Context Cache¶
Tier 1부터 Tier 4까지는 GPU에 KV 캐시가 상주하는 백엔드(vLLM, TensorRT-LLM, SGLang)를 대상으로 합니다. Google Generative Language API는 외부에서 구독할 수 있는 KV 이벤트 스트림을 제공하지 않으므로, type: gemini 백엔드에는 라우터가 Google 서버 측 cachedContents 리소스를 대신 관리하는 별도의 옵트인 메커니즘을 제공합니다.
개요¶
이 기능을 활성화하면 라우터는 type: gemini 백엔드로 전송되는 요청 중 크고 반복적인 시스템 프롬프트 프리픽스를 cachedContents 리소스로 생성하고 후속 요청에서 재사용합니다. OpenAI 호환 클라이언트는 기존과 동일한 요청을 그대로 보내면 되며, 라우터가 백엔드 전달 전에 요청 형식을 자동으로 변환합니다.
- OpenAI 호환 요청 형식:
extra_body.google.cached_content = "cachedContents/..."필드로 캐시를 참조합니다. - 네이티브
generateContent형식: 최상위cachedContent필드를 설정하고, 이미 캐시 리소스에 포함된systemInstruction필드는 제거합니다.
Google은 캐시된 토큰에 할인된 입력 요율을 적용하는 대신 토큰-시간당 스토리지 요금을 부과하며, 모델별 최소 캐싱 가능 토큰 수를 강제합니다(Gemini 2.5 계열은 약 2,048 토큰, 3.x 계열은 약 4,096 토큰). 따라서 프리픽스가 충분히 크고 자주 재사용될 때만 비용 절감 효과가 발생하므로 기본값은 enabled: false이며 짧은 ttl을 사용합니다.
라우터는 직접 토크나이징을 수행하지 않고 바이트 기반 기준값(min_prefix_bytes)으로 최소 토큰 수를 근사합니다. 만약 Google이 토큰 수 부족으로 생성을 거부하면 해당 프리픽스를 negative_ttl_seconds 동안 네거티브 캐시에 등록해 불필요한 재시도를 막습니다. gemini_context_cache.enabled: false(기본값)일 때는 맵 할당이나 백그라운드 작업이 전혀 실행되지 않습니다.
캐시 토큰 과금 및 사용량 집계¶
라우터가 관리하는 cachedContents 리소스에서 히트가 발생하든, Gemini 2.5+의 암묵적 캐싱(별도 설정 없이 usageMetadata.cachedContentTokenCount에 보고됨)에서 히트가 발생하든 동일하게 캐시된 입력 버킷으로 계측됩니다. 각 API 엔드포인트별 응답 형식은 다음과 같습니다.
/v1/chat/completions:prompt_tokens는 캐시 토큰을 포함한 전체 프롬프트 토큰 수를 유지하고,usage.prompt_tokens_details.cached_tokens에 캐시된 토큰 수를 함께 기록합니다(일반 응답 본문과 네이티브 스트림의 마지막 usage 청크 모두 동일)./v1/responses:usage.input_tokens_details.cached_tokens에 캐시된 토큰 수를 기록합니다./anthropic/v1/messages: 캐시 읽기를 일반 입력 토큰 수에서 제외하는 Anthropic 관례에 따라input_tokens와cache_read_input_tokens를 분리해 기록합니다.
사용량 레코드와 라우터 로컬 한도 제어는 input_tokens = prompt_tokens - cache_read만 input_tpm 및 월간 예산에 차감하고, 캐시 히트 물량은 cache_hit: true와 함께 cached_input_tokens로 집계합니다.
요청 라이프사이클¶
type: gemini 백엔드를 대상으로 하고 시스템 프롬프트 크기가 min_prefix_bytes 이상인 요청은 다음 순서로 처리됩니다.
- 프리픽스 다이제스트 계산:
src/core/prefix_key.rs와 동일한 해싱 구조로 모델명과 전체 시스템 프롬프트의 SHA-256 다이제스트를 계산합니다. Tier 1 프리픽스 키와 달리 시스템 프롬프트 전체를 잘라내지 않고 해싱합니다. - 맵 히트(만료 전): 라우터가
cachedContents참조를 주입하고 중복된 시스템 프롬프트를 요청에서 제거합니다.extend_on_hit: true이고 남은 TTL이 전체의 20% 이하이면 백그라운드에서PATCH요청으로 TTL을 연장합니다. - 맵 미스(네거티브 캐시 미포함): 첫 요청이 캐시 생성 지연을 겪지 않도록 원본 요청을 즉시 전달하고, 백그라운드에서 single-flight 방식으로 생성 작업(
POST /v1beta/cachedContents,model·systemInstruction·ttl포함)을 한 번만 실행합니다. - 생성 거부(
HTTP 400): 최소 캐싱 가능 토큰 수 미달 등으로 거절되면 해당 프리픽스를negative_ttl_seconds동안 네거티브 캐시에 등록합니다. - 항목 제거(Eviction):
max_entries를 초과하는 항목은 LRU로 제거됩니다. LRU 제거와 관리자 캐시 초기화 엔드포인트는 모두 스토리지 과금을 조기에 종료하기 위해 Google API로 best-effortDELETE를 호출합니다. 네거티브 캐시도 동일한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 Completions 요청에서 OpenAI 호환
# /v1beta/openai/chat/completions 대신 Gemini Native API(generateContent /
# streamGenerateContent)를 기본으로 사용할지 여부 (기본값: false)
prefer_native_api: false
적용 범위: type: gemini 백엔드의 chat completion 요청에만 적용됩니다. /v1/responses Gemini 컨버터, 이미지 생성, 임베딩 경로는 대상이 아닙니다. Generative Language API를 모방하지만 cachedContents를 지원하지 않는 프록시나 자체 호스팅 엔드포인트는 생성 시도가 실패한 뒤 네거티브 캐시에 등록되므로 항목이 누적되지 않습니다.
메트릭¶
| 메트릭 | 유형 | 레이블 | 설명 |
|---|---|---|---|
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 | 없음 | 캐시에서 제공된 토큰 수. 호환 응답 형식에서는 usage.prompt_tokens_details.cached_tokens, 네이티브 형식에서는 usageMetadata.cachedContentTokenCount에서 읽음 |
관리자 엔드포인트¶
GET /admin/gemini-context-cache/stats¶
enabled 플래그, 적용 중인 설정 값, 현재 항목 수, negative_entries, in_flight 생성 작업 수, 백엔드별 통계, 그리고 hit/miss/ineligible/negative/creates/create_failures/evictions/cached_tokens 카운터를 반환합니다.
POST /admin/gemini-context-cache/clear¶
라우터 내부의 항목 맵을 비우고 추적 중인 모든 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입니다. 또는 endpoint를 생략하고 transport: event_bus(기본값 sse)를 설정할 수 있습니다(푸시 모드 참고).
메트릭¶
모든 KV 캐시 메트릭은 continuum_ 프리픽스를 사용하며, 카디널리티 급증을 막기 위해 레이블 값은 허용 목록 기준으로만 기록됩니다.
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으로 유지됩니다. 각 레이블의 의미는 라우팅 결정 레이블을 참고하세요.
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, tier | 승자의 보유 계층(gpu_hot, storage_warm, none)별 KV 인식 라우팅 결정 (kv_aware, fallback) |
continuum_kv_index_overlap_score | Histogram | 없음 | 스코어러가 결정한 선택의 최고 복합 점수 (0.0 ~ 1.0) |
continuum_kv_index_event_source_status | Gauge | backend, status | 백엔드별 이벤트 소스 연결 상태 |
continuum_kv_index_routing_decisions_total은 스코어러가 실행된 선택마다 한 번 기록됩니다. 스코어러가 백엔드를 선택한 경우 decision="kv_aware"가 기록되고, 프리픽스 키로 스코어러를 실행했지만 복합 점수 임계값(0.3)을 넘은 백엔드가 없어 기본 전략으로 넘어간 경우 decision="fallback"이 기록됩니다. 스코어러가 등록되지 않았거나 프리픽스 키가 없어 스코어러가 실행되지 않은 요청은 집계하지 않습니다. continuum_kv_index_overlap_score는 kv_aware 결정의 최고 복합 점수(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 라우팅¶
GET /admin/prefix-routing/stats¶
라우팅 결정 횟수, overflow 비율, 백엔드 부하 분배, CHWBL 설정을 포함한 프리픽스 라우팅 통계를 반환합니다.
응답 예제:
{
"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¶
항목 수, 라우팅 결정 내역, 쿼리 지연 카운터, overlap 점수 카운트를 포함한 KV 캐시 인덱스 통계를 반환합니다.
응답 예제:
{
"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 캐시 이벤트 통계(수신/처리/드롭된 이벤트 수, 연결 상태, 인덱스 생성/제거 이벤트 수)를 반환합니다.
응답 예제:
{
"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 캐시 인덱스의 모든 항목을 비웁니다. 인덱스는 이후 수신되는 이벤트로부터 자동 재구축되며 주로 디버깅 용도로 사용합니다.
응답 예제:
배포 가이드¶
Tier 1만 사용 (최소 설정)¶
외부 의존성 없이 GPU KV 캐시 지역성을 높이기 위해 프리픽스 라우팅만 활성화합니다:
동일 모델의 여러 인스턴스가 백엔드에서 실행되고 시스템 프롬프트가 긴 경우(>128 토큰)에 효과적입니다.
Tier 1 + 2 (응답 캐시)¶
반복되는 결정론적 요청을 백엔드 호출 없이 처리하도록 응답 캐시를 추가합니다:
prefix_routing:
enabled: true
response_cache:
enabled: true
backend: memory
capacity: 5000
ttl: "10m"
고정된 시스템 프롬프트와 동일한 질문을 반복 전송하는 워크로드(예: 문서 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
모든 라우터 Pod에서 Redis/Valkey에 접근할 수 있어야 하며, 암호화 연결에는 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를 실행하고 각 서빙 엔진이 리스너의 ZMQ 엔드포인트로 KV 이벤트를 발행하도록 구성합니다. 라우터는 kv_cache_index.event_sources[]를 통해 리스너의 SSE 엔드포인트만 구독하므로 엔진 컨테이너에 별도의 HTTP 포트를 열 필요가 없습니다.
- vLLM 및 SGLang: vLLM은
/v1/kv_events같은 HTTP/SSE 경로를 제공하지 않으며,--kv-events-config(예:{"publisher":"zmq","endpoint":"tcp://kv-listener.internal:5557","topic":""})를 지정했을 때 ZMQ PUB 소켓으로 msgpackKVEventBatch프레임을 발행합니다. SGLang도 동일한 ZMQ 이벤트 형식을 사용합니다. - 프리픽스 키 복원 및 보안: 엔진 블록 해시는 라우터의 SHA-256 프리픽스 키와 다르므로,
continuum-kv-listener가 저장된 블록에서 프롬프트를 복원하고extract_prefix_key로 프리픽스 키를 계산합니다. 토큰 이벤트 데이터가 이동하므로 리스너 바인드 주소는 내부망으로 제한하고, 라우터가 직접 접근할 수 없으면 푸시 모드를 사용하세요. - 네이티브 TensorRT-LLM: 리스너 소스에
engine: trtllm을 설정하고trtllm_endpoint에trtllm-serve의/kv_cache_eventsURL을 지정합니다. TensorRT-LLM에서는 블록 재사용(block reuse)을 켜고 이벤트 버퍼 크기를 양수로 설정해야 합니다. URL 호스트는 반드시allowed_publishers에 포함되어야 하며 리다이렉트는 거부됩니다. - TensorRT-LLM cache salt 연동:
trtllm-serve는 공개/detokenize엔드포인트를 제공하지 않으므로 라우터가 발급한 cache salt 연동이 필수입니다. 체인의 첫 유효stored블록에 해당 백엔드와 일치하는 salt가 있어야 하며, salt가 없거나 검증에 실패한 체인은 폐기됩니다. 리스너는 salt, 원본 응답 본문, 토큰 ID를 로그에 남기거나 외부로 노출하지 않습니다.
Redis/Valkey 사이징¶
응답 캐시 용량 산정 기준:
- 평균 직렬화 응답 크기: 항목당 2–10 KB
capacity = (target_hit_rate * rps * avg_unique_rate) / eviction_frequency
KV 인덱스 용량 산정 기준:
- 각
(prefix, backend)항목은 메모리에서 약 200바이트 소비 - 여유분을 위해
max_entries를 최소num_unique_prefixes * num_backends * 2이상으로 설정
고가용성 고려 사항¶
- 응답 캐시와 KV 인덱스는 자동 인메모리 폴백(Tier 3)을 통해 Redis 장애 시에도 중단 없이 동작합니다.
- KV 인덱스는 라우터 재시작 시 SSE 스트림으로부터 자동 재구축되므로 별도의 영속 스토리지가 필요하지 않습니다.
- 프리픽스 라우팅(Tier 1)은 외부 의존성이 없으며 항상 가용합니다.
- Redis 재시작 시에도 응답 캐시를 유지하려면 Sentinel 또는 Cluster 복제 구성을 권장합니다.
성능 특성¶
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
- 캐시 히트 시 백엔드 호출 지연 없이 전체 응답 즉시 반환
- 시맨틱 조회: 고정 백엔드로의 임베딩 왕복 1회 + 최대
max_entries개 정규화 벡터에 대한 파티션 내 스캔
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 윈도당 고유 프리픽스마다query_backends()1회 호출KvOverlapScorer.score(): < 1 us (사전 조회된 캐시에서 동기 읽기)- 1,000회 스코어링 호출: 총 < 100 ms (
scorer.rs단위 벤치마크 기준)
기대 효과¶
다음은 일반적인 LLM 워크로드 패턴을 기준으로 한 추정치입니다:
| 시나리오 | 지표 | 기대 개선 |
|---|---|---|
| 긴 system prompt (>512 token), 요청 간 반복 | Time-to-first-token | KV 캐시 재사용으로 20–40% 단축 |
| 고정 문서 QA (동일 문서 + 동일 질문) | 백엔드 요청 수 | 응답 캐시로 최대 100% 절감 |
| 다중 복제본 vLLM, 핫 prefix | 캐시 히트율 (Tier 4) | 요청의 60–80%를 웜 캐시 보유 백엔드로 라우팅 |
| Redis 장애 | 서비스 가용성 | 성능 저하 없음; 단일 요청 내에 인메모리로 폴백 |
실제 개선 폭은 워크로드의 프리픽스 중복도, GPU 메모리 용량, 백엔드 구성에 따라 달라집니다.