KV 캐시에서 이미 계산해 둔 prefix 구간을 다음 요청에서 그대로 꺼내 쓰는 아이디어 자체는 단순하다. 구현도 어렵지 않다. 그런데 활성화해 두고 지표를 보면 생각과 다른 숫자들이 나온다. TTFT는 줄었는데 처리량은 그대로다. 히트율이 80%라고 뜨는데 p99 레이턴시는 꿈쩍도 안 한다. 캐시 공간을 늘렸더니 오히려 처리량이 떨어졌다.
저장되는 것과 재사용되는 것
vLLM의 Automatic Prefix Caching은 KV 캐시를 기본 16토큰 블록 단위로 나눠 관리하고, 각 블록에 체이닝 해시(부모 블록 해시 + 현재 블록 토큰 목록의 SHA-256)를 붙여 저장한다. 다음 요청이 동일한 prefix를 가지면 그 블록들의 해시 체인이 일치하고, prefill 없이 저장된 KV 상태를 바로 꺼낸다.
히트 조건에는 단순하지만 중요한 제약이 있다. 블록이 완전히 채워져야 한다. 마지막 부분 블록은 항상 미스다. 시스템 프롬프트가 2,050토큰이라면 첫 128블록(2,048토큰)은 히트될 수 있지만 남은 2토큰 블록은 공유되지 않는다. prefix 길이가 블록 크기의 배수가 아닌 한 경계 손실이 생긴다.
히트율을 결정하는 것
히트율은 워크로드 패턴 세 가지로 거의 전부 결정된다. 시스템 설정만으로 이 상한을 넘을 방법은 없다.
시스템 프롬프트 공유율. 모든 요청이 동일한 긴 시스템 프롬프트로 시작한다면, 첫 요청 이후부터 그 구간의 prefill이 생략된다. RAG 파이프라인에서 정적 컨텍스트 6,000토큰이 고정이면 prefill 절감율이 8894%에 달한다. 일반 오픈엔드 Q&A처럼 요청마다 다른 맥락이 붙으면 히트율은 05% 수준이다.
prefix 길이 분포. 멀티턴 대화에서는 이전 대화 내용이 쌓이면서 2번째 턴부터 캐시 가능한 구간이 늘어난다. n4n.ai의 멀티턴 벤치마크에서 2,000토큰 세션의 두 번째 턴 TTFT가 120ms에서 15ms로 줄었다. 세 번째 턴 이후부터는 네트워크 왕복 수준으로 내려간다.
요청 간 간격과 eviction. 캐시 블록은 메모리 압박 시 LRU 순서로 퇴출된다. 같은 prefix를 쓰는 요청이 짧은 간격으로 들어오지 않으면 캐시가 비어 있을 수 있다. SGLang v0.4의 캐시 인지 로드 밸런서는 이 문제를 라우팅 수준에서 해결해 히트율을 20%에서 75%로 끌어올렸고, 처리량은 82,665 tok/s에서 158,596 tok/s로 늘었다.
워크로드 유형별 기대 히트율과 효과:
| 워크로드 유형 | prefix 특성 | 예상 hit rate | TTFT 개선 | throughput 개선 |
|---|---|---|---|---|
| RAG (고정 컨텍스트 6k토큰) | 매우 높은 공유율 | 88~94% | 70~85% | prefill-bound일 때만 |
| 챗봇 (멀티턴, 공유 시스템 프롬프트) | 시스템 프롬프트 + 누적 히스토리 | 60~80% | 30~55% | 미미 |
| Tool/Agent (툴 정의 반복) | 시스템 프롬프트 + 툴 스키마 | 70~85% | 30~45% | 미미 |
| 일반 Q&A (고유 사용자 쿼리) | 공유율 거의 없음 | 0~5% | ~0% | 없음 |
Tool & Agent 요청의 76%는 prefix를 50% 이상 공유한다. 시스템 프롬프트에 툴 정의가 반복해서 붙기 때문이다.
TTFT는 줄었는데 throughput은 왜 그대로인가
Prefill과 decode는 GPU를 완전히 다른 방식으로 쓴다.
Prefill은 입력 토큰 전체를 병렬로 처리하는 대형 행렬 곱셈이다. GPU 활용률이 9095%에 달하고 연산 강도가 200400 ops/byte 수준이어서 compute-bound다. Decode는 매 스텝마다 토큰 하나를 생성하면서 전체 KV 캐시를 HBM에서 읽어야 한다. Tensor core는 마이크로초 안에 계산을 끝내지만, 다음 메모리 읽기를 기다리면서 GPU 활용률이 2040%로 떨어진다. 연산 강도는 6080 ops/byte — memory-bandwidth-bound다.
Prefix Caching은 prefill 단계를 생략한다. TTFT가 줄어드는 이유는 이것이다. 그런데 decode 단계에서는 캐시 히트 여부와 무관하게 해당 요청의 전체 KV 상태를 HBM에서 읽어야 하고, 생성해야 하는 출력 토큰 수도 줄지 않는다. 서빙이 decode-bound인 상황 — 출력이 길거나, 배치가 꽉 차 있거나, 모델이 크거나 — 에서 Prefix Caching은 TTFT만 건드린다. GPU 활용률이나 req/s는 올라가지 않는다.
Spheron의 2026 벤치마크가 이를 잘 보여준다. H100 SXM5 80GB 단일 GPU, Llama 3.3 70B FP8 기준으로 캐시 없는 고유 프롬프트 처리량은 vLLM 1,850 tok/s, SGLang 1,920 tok/s로 4% 차이에 그친다. 그런데 80% 공유 prefix, 512토큰, 동시 요청 50건 조건에서는 TTFT p50과 p95가 크게 줄지만, 처리량 수치는 두 엔진 모두 큰 변화가 없다.
반대로 prefill-bound인 워크로드 — 입력이 매우 길고 출력이 짧은 문서 분류, 임베딩 생성, 요약 — 에서는 prefill 생략이 곧 GPU 사이클 절약이고 throughput도 따라 올라간다.
히트율을 올리려다 생기는 문제
캐시 공간은 KV 캐시 풀에 할당한 GPU 메모리 크기에 달려 있다. vLLM에서 --gpu-memory-utilization을 0.85에서 0.90으로 올리면 KV 캐시 풀이 늘고 이전에 퇴출됐던 블록들이 더 오래 살아있다. 대신 같은 GPU 메모리 안에서 동시에 처리할 수 있는 요청 수 — 최대 배치 크기 — 가 줄어든다.
이 트레이드오프가 역방향으로 작용하는 경우가 있다. 히트율이 올라가도 배치 크기가 줄면 스케줄러가 덜 채워진 배치를 돌리고 GPU 활용률이 오히려 낮아진다. 특히 decode-bound 워크로드에서는 배치가 크게 묶여야 GPU가 충분히 채워지는데, 캐시 공간을 늘릴수록 그 상한이 낮아져 throughput이 하락한다.
LRU eviction도 공정하지 않다. 블록 수가 많은 장문 시스템 프롬프트 항목은 여러 블록을 한꺼번에 점유하다가 메모리 압박 시 통째로 퇴출된다. 짧은 prefix를 쓰는 요청들의 블록은 LRU 기준으로 오래 살아남는다. 결과적으로 장문 시스템 프롬프트를 쓰는 요청이 반복 미스를 겪는 구조가 생긴다.
vLLM에서 히트율 모니터링은 Prometheus 메트릭 vllm:prefix_cache_hits / vllm:prefix_cache_queries로 한다. 히트율이 올라가는 동시에 throughput이 떨어지기 시작하면 최대 배치 크기 지표와 GPU 활용률을 같이 확인해야 한다.
vLLM·SGLang·TensorRT-LLM의 구현 차이
세 엔진이 히트를 판단하는 granularity가 다르다. 블록 크기 하나가 같은 워크로드에서 실효 히트율을 크게 벌린다.
vLLM은 기본 16토큰 블록, SHA-256 체이닝 해시, 풀 블록만 히트로 인정한다. V1 엔진에서 기본으로 켜져 있고, 레거시 V0는 --enable-prefix-caching 플래그가 필요하다.
# vLLM V0 (명시 활성화)
vllm serve meta-llama/Llama-3.3-70B-Instruct \
--enable-prefix-caching \
--gpu-memory-utilization 0.90 \
--block-size 16
SGLang의 RadixAttention은 radix tree(compact prefix tree)로 KV 캐시를 관리한다. 기본 page_size가 1토큰이어서 블록 경계 정렬 없이 prefix를 공유할 수 있다. 프롬프트 중간에서 분기가 일어나도 분기 전 공통 구간을 노드 분할(split)로 최대화한다. 기본 활성화 상태이며 --disable-radix-cache로 끌 수 있다. --schedule-policy lpm 옵션을 추가하면 Longest Prefix Match 우선 스케줄링으로 히트율을 더 높일 수 있다.
# SGLang (기본으로 RadixAttention 활성화)
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.3-70B-Instruct \
--mem-fraction-static 0.88 \
--schedule-policy lpm
TensorRT-LLM은 기본 블록 크기가 128토큰으로 vLLM 대비 8배 크다. 커널 효율은 높지만 재사용 가능 구간이 128의 배수로만 인정된다. 호스트 메모리 오프로드(--kv_host_cache_bytes)로 GPU에서 퇴출된 블록을 pinned memory에 보관했다가 재사용할 수 있고, Grace-Hopper 아키텍처에서 효과가 크다. x86에서 대용량 pinned memory를 할당하면 초기화에 10초 이상 걸릴 수 있다.
# TensorRT-LLM 빌드
trtllm-build --use_paged_context_fmha enable --tokens_per_block 128
# Triton 서버 (runtime에서 기본 활성화)
enable_kv_cache_reuse: "true"
# 호스트 메모리 오프로드 (Grace-Hopper 권장)
--kv_host_cache_bytes 45000000000
같은 워크로드 — 80% 공유 prefix, 512토큰, 동시 50건 — 에서 vLLM TTFT p50이 310ms, SGLang이 195ms로 37% 차이가 나는 건 블록 경계 정렬 손실이 누적된 결과다.
지표를 어떻게 읽어야 하나
히트율 하나만 보면 그림이 반쪽이다. prefix_cache_hit_rate가 80%라도 히트된 블록들이 짧은 prefix에 몰려 있으면 실제 prefill 절감량이 작다. 함께 봐야 할 지표는 effective prefill tokens 감소율 — 원래 처리했어야 할 prefill 토큰 대비 실제 처리한 비율이다.
TTFT 분포는 히트 요청과 미스 요청의 혼합이다. p50이 낮아도 p99가 꿈쩍 않는 건 미스 요청들이 테일을 끌어올리기 때문이다. 히트율이 높은데 TTFT p99가 안 줄면 두 가지를 의심한다. 장문 시스템 프롬프트 항목이 eviction 당하고 있거나, 새로운 사용자 세션처럼 항상 미스가 날 수밖에 없는 요청 유형이 테일을 지배하고 있거나.
히트율이 높고 TTFT p50도 줄었는데 throughput(req/s)이 그대로라면, 서빙이 decode-bound라는 신호다.
언제 끄거나 제한해야 하나
prefix 중복이 낮은 환경에서는 캐시가 거의 비어 있는 채로 메모리를 점유한다. 요청마다 다른 사용자 컨텍스트가 붙는 B2C 서비스, 짧은 컨텍스트 위주 배치, 히트율이 안정적으로 60%를 넘지 않는 워크로드에서는 캐시 보존 공간이 배치 크기 상한을 줄이는 역할만 한다.
메모리가 이미 빡빡한 상황 — gpu_memory_utilization 0.90 이상을 이미 쓰고 있거나, 긴 출력 때문에 KV 캐시 footprint가 큰 경우 — 도 마찬가지다. vLLM에서는 --no-enable-prefix-caching, SGLang에서는 --disable-radix-cache로 끄거나 --mem-fraction-static을 낮춰 캐시 풀 크기를 제한한다.
끄기 전에 기준선을 측정해 두는 게 낫다. Prefix Caching 켜기 전후의 TTFT p50/p99, throughput, 최대 배치 크기를 비교하면 해당 워크로드에서 트레이드오프가 어느 방향으로 작용하는지 직접 확인할 수 있다.