콘텐츠로 이동

KV Cache 최적화

Continuum Router는 LLM 백엔드에서의 중복 연산을 줄이는 4단계 KV cache 최적화 시스템을 구현합니다. 각 단계는 독립적으로 설정 가능하며, backend 선택 과정에서 함께 조합되어 동작합니다.

목차


개요

최신 LLM 추론 엔진(vLLM, TensorRT-LLM, SGLang)은 요청의 token prefix에 대해 계산된 attention key-value 텐서를 GPU 메모리의 KV cache에 저장합니다. 동일한 prefix가 다시 나타나면 엔진은 해당 텐서의 재계산을 건너뛸 수 있으며, 긴 system prompt나 반복되는 컨텍스트의 경우 상당한 GPU 시간을 절약할 수 있습니다.

Continuum Router는 네 가지 상호 보완적인 메커니즘을 통해 백엔드 간 KV cache 재사용을 극대화합니다:

  1. Prefix 인식 고정 라우팅 — 동일한 prompt prefix를 공유하는 요청을 consistent hashing을 통해 같은 백엔드로 라우팅하여 GPU KV cache를 warm 상태로 유지합니다.
  2. 응답 캐시 — 반복되는 결정적(deterministic) 요청을 백엔드에 전달하지 않고 라우터 메모리 또는 Redis에서 직접 제공합니다.
  3. 공유 외부 캐시 — 응답 캐시 상태를 Redis/Valkey에 저장하여 여러 라우터 인스턴스가 동일한 캐시 항목을 공유합니다.
  4. Backend KV Cache Index — 최근 prefix에 대해 실제로 GPU에 상주하는 KV 텐서를 보유한 백엔드를 추적하여, 실제 캐시 상태에 기반한 세밀한 라우팅 결정을 가능하게 합니다.

4단계 캐싱 전략

flowchart TD
    Client([클라이언트 요청])
    RC{응답\n캐시 히트?}
    PRL{Prefix key\n사용 가능?}
    CHWBL[CHWBL Hash Ring\nPrefix → Backend]
    KVI{KV Index\noverlap ≥ threshold?}
    SCORE[복합 점수\noverlap + load + health]
    FALLBACK[기본 선택\n전략]
    BACKEND([선택된 Backend])
    STORE_RC[응답을\n캐시에 저장]

    Client --> RC
    RC -->|HIT| Client
    RC -->|MISS| PRL
    PRL -->|Yes| CHWBL
    CHWBL --> KVI
    KVI -->|Yes| SCORE
    SCORE --> BACKEND
    KVI -->|No| FALLBACK
    PRL -->|No| FALLBACK
    FALLBACK --> BACKEND
    BACKEND --> STORE_RC
    STORE_RC --> Client

각 단계는 상호 배타적이지 않습니다: Tier 1과 4는 모두 backend 선택에 참여합니다. Tier 2는 백엔드에 연결하기 전에 전체 요청을 가로챕니다. Tier 3는 Tier 2의 스토리지 기반을 제공합니다.


Tier 1: Prefix 인식 고정 라우팅

Tier 1은 공통 prompt prefix를 공유하는 요청을 동일한 백엔드로 라우팅하여, 해당 백엔드의 GPU KV cache가 이미 해당 token에 대해 warm 상태일 확률을 극대화합니다.

Prefix Key 추출

각 수신 chat completion 요청에 대해 라우터는 prefix key — 요청의 의미적 앵커를 고유하게 식별하는 32바이트 SHA256 다이제스트 — 를 추출합니다.

추출 로직은 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에서 시계 방향으로 다음 노드로 오버플로됩니다.

epsilon = 0.25  →  백엔드가 평균 부하보다 최대 25% 초과 가능

ring은 백엔드당 virtual_nodes(기본값: 150) 가상 복제본으로 채워져 key 분배 균등성을 향상시킵니다.

라우팅 결정 레이블:

  • prefix_hash — 요청이 hash ring에서 해당 prefix를 소유한 백엔드에 배치됨
  • overflow — 선호 백엔드가 부하 상한에 도달하여 요청이 다음 ring 노드로 이동
  • fallback — prefix key를 추출할 수 없어 설정된 선택 전략으로 폴백

Anthropic Cache Control 주입

anthropic_cache_control_injection: true가 설정되면, 라우터는 Anthropic API 요청의 system prompt content block에 cache_control: { type: "ephemeral" } 마커를 자동으로 추가합니다. 이는 라우터 수준의 KV cache와는 별개이지만 상호 보완적인 Anthropic의 서버 측 prompt 캐싱을 활성화합니다.


Tier 2: 응답 캐시

응답 캐시는 결정적 요청에 대한 완전한 LLM 응답을 저장하여, 라우터가 백엔드에 연결하지 않고도 반복 쿼리를 제공할 수 있게 합니다.

캐시 적격성

요청은 다음 조건이 모두 충족될 때 캐싱 대상이 됩니다:

  • temperature0이거나 지정되지 않음
  • 요청이 스트리밍을 사용하지 않음 (또는 버퍼링이 활성화된 스트리밍 사용)
  • 누적 응답 크기가 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 태그 바이트는 선택적 매개변수의 NoneSome(0) 간의 충돌을 방지합니다. tenant ID는 멀티테넌트 격리를 제공하기 위해 포함되며, 테넌트는 서로의 캐시된 응답을 읽을 수 없습니다.

구현: src/infrastructure/cache/response_cache.rs

스트리밍 캐시

스트리밍 응답의 경우, 라우터는 SSE 스트림을 max_stream_buffer_size(기본값: 10 MiB)까지 버퍼에 누적합니다. 전체 스트림이 제한 내에 들어가면 버퍼가 단일 직렬화된 blob으로 저장되고 캐시 히트 시 합성 SSE 스트림으로 재생됩니다.

캐시 제거

인메모리 백엔드는 항목 수가 capacity에 도달하면 LRU 제거를 사용합니다. Redis 백엔드는 Redis 자체에서 관리하는 TTL 기반 만료에 의존합니다.


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 네임스페이스 형식:

cr:resp:<sha256-cache-key-hex>

쓰기에는 SET EX, 읽기에는 GET을 사용하며, 설정 가능한 명령 타임아웃(기본값: 1초)을 갖습니다.

자동 폴백

Redis에 연결할 수 없는 경우, RedisCacheStore는 투명하게 인메모리 폴백 캐시를 활성화합니다. 폴백은 첫 번째 연결 실패 시 활성화되며, 백그라운드 헬스 모니터 작업(30초마다 실행)이 Redis 연결 복원을 시도합니다. 복구 시 플래그가 해제되고 이후 작업은 Redis로 돌아갑니다.

continuum_cache_fallback_active 메트릭(값 1)은 폴백 모드가 현재 활성 상태임을 나타냅니다.

연결 풀 공유

deadpool_redis::PoolAppStateArc로 저장되며 응답 캐시와 KV cache index(Tier 4) 간에 공유됩니다. 연결 수의 이중 계산을 방지하고 설정을 단순화합니다: 두 소비자가 동일한 풀 자격 증명을 재사용합니다.


Tier 4: Backend KV Cache Index

Tier 4는 특정 token prefix 해시에 대해 GPU에 상주하는 KV 텐서를 보유한 백엔드를 실시간으로 추적합니다. 이를 통해 통계적 친화성이 아닌 실제 GPU 캐시 상태에 기반한 라우팅 결정이 가능합니다.

이벤트 소비

각 vLLM 백엔드는 SSE 엔드포인트(예: http://vllm-1:8000/v1/kv_events)에서 KV cache 이벤트 스트림을 노출합니다. KvEventConsumerManager는 백엔드당 하나의 백그라운드 Tokio 작업을 생성하여 이 스트림을 구독하고 이벤트를 처리합니다.

이벤트 유형:

이벤트 의미
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: 필드를 우선 사용하며, JSON event 필드는 폴백으로 사용됩니다.
  • 버퍼는 잘못된 스트림으로부터 보호하기 위해 1 MiB(MAX_SSE_BUFFER_SIZE)로 제한됩니다.
  • 연결 실패 또는 스트림 종료 시, 소비자는 재연결 전에 지수 백오프(초기: 1초, 최대: 60초)를 적용합니다.

구현: src/infrastructure/kv_index/event_consumer.rs

인덱스 구조

이벤트는 다음 매핑을 유지하는 KvCacheIndex 구현에 전달됩니다:

prefix_hash → {backend_id → token_count}

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>
  • EXPIREZADD와 함께 단일 원자적 라운드트립으로 파이프라인 처리
  • ZREVRANGEBYSCORE +inf -inf WITHSCORES로 내림차순 점수별 백엔드 반환
  • 여러 라우터 인스턴스 간 KV index 상태 공유 가능
  • Key prefix: cr:kvidx:

구현: src/infrastructure/kv_index/index.rs

스토리지 계층 인식

storage_offloading.enabledtrue이면, 인덱스는 각 (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_createdcache_evicted 이벤트만 내보낼 때 유용합니다.

구현: src/infrastructure/kv_index/types.rs

Overlap 스코어링

KvOverlapScorerBackendScorer 트레이트를 구현하며 각 백엔드에 대한 복합 점수를 계산합니다:

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_weight
  • load_ratio = backend_in_flight / max_in_flight_across_backends (0.0 ~ 1.0)
  • health_score = backend_success_rate (0.0 ~ 1.0)

기본 가중치:

매개변수 기본값 설명
overlap_weight 0.6 캐시 overlap 신호의 가중치
load_weight 0.3 백엔드 부하 신호의 가중치
health_weight 0.1 백엔드 헬스 신호의 가중치
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 백엔드보다 낮은 점수를 받습니다.

최소 overlap 임계값: 모든 백엔드에서 가장 높은 overlap 점수가 min_overlap_threshold(기본값: 0.3) 미만이면, 스코어러는 모든 백엔드에 대해 0.0을 반환하고 풀은 기본 선택 전략으로 폴백합니다. 이는 의미 있는 캐시 커버리지를 가진 백엔드가 없을 때 비최적 백엔드로의 라우팅을 방지합니다.

비동기 준비 모델: KvCacheIndex.query_backends()는 비동기이지만 BackendScorer.score()는 동기적이어야 합니다. 스코어러는 2단계 설계를 사용합니다: prepare()가 인덱스 쿼리 결과를 가져와 캐시하고(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 CHWBL as CHWBL Ring
    participant KVI as KV Index
    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 사용 가능
            R->>CHWBL: Prefix → 선호 backend 매핑
            R->>KVI: prepare(prefix_hash)
            KVI-->>R: 인덱스로부터 backend 점수
            R->>R: Score = overlap + load + health
            alt 최고 overlap ≥ threshold
                R->>B: 요청 전달 (KV 인식 라우팅)
            else 임계값 미달
                R->>CHWBL: CHWBL 결과 사용 (prefix 라우팅)
                R->>B: 요청 전달
            end
        else Prefix key 없음
            R->>B: 요청 전달 (기본 전략)
        end
        B-->>R: 응답
        R->>RC: 응답 저장 (T=0)
        R-->>C: 200 OK (X-Cache: MISS)
    end

KV overlap 스코어러는 prefix 인식 라우팅을 대체하는 것이 아니라 함께 조합됩니다. KV index에 데이터가 있고 overlap이 임계값을 초과하면 overlap 스코어러가 선택을 주도합니다. 데이터가 없거나 불충분하면 CHWBL ring 결과가 사용됩니다.


설정 레퍼런스

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

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 이벤트를 구독할 vLLM 백엔드
  event_sources:
    - backend_name: vllm-1
      endpoint: "http://vllm-1:8000/v1/kv_events"
      reconnect_interval_ms: 5000

    - backend_name: vllm-2
      endpoint: "http://vllm-2:8000/v1/kv_events"
      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의 근사 수

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)

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 라우팅된 요청의 overlap 점수 (0.0 ~ 1.0)
continuum_kv_index_event_source_status Gauge backend, status 백엔드별 이벤트 소스 연결 상태

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 본문을 받습니다:

{ "clear_all": true }

응답 예제:

{
  "success": true,
  "action": "clear_all",
  "cleared_entries": 1243
}

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의 모든 항목을 삭제합니다. 인덱스는 수신되는 이벤트로부터 자동으로 재구축됩니다. 디버깅용입니다.

응답 예제:

{
  "success": true,
  "entries_before_clear": 618,
  "cleared_entries": 2
}

배포 가이드

Tier 1만 사용 (최소 설정)

외부 의존성 없이 GPU KV cache 지역성을 위한 prefix 라우팅을 활성화합니다:

prefix_routing:
  enabled: true
  load_factor_epsilon: 0.25
  virtual_nodes: 150

동일한 모델의 여러 인스턴스가 백엔드에서 실행되고 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://vllm-1:8000/v1/kv_events"
    - backend_name: vllm-2
      endpoint: "http://vllm-2:8000/v1/kv_events"

vLLM 요구 사항

각 vLLM 백엔드에서 kv_events SSE 엔드포인트가 활성화되어야 합니다. vLLM은 다음과 같이 실행할 때 이 엔드포인트를 노출합니다:

vllm serve <model> \
  --enable-prefix-caching \
  --kv-cache-dtype auto

이벤트 스트림 URL은 일반적으로 http://<host>:<port>/v1/kv_events입니다.

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 메모리 용량, 백엔드 설정에 따라 달라집니다.