KV 캐시 히트율을 지표로 보지 않는 LLM 서빙은 왜 비싸지는가
긴 프롬프트를 쓰는 AI 제품에서 비용이 갑자기 튀는 순간은 대개 모델이 더 똑똑해져서가 아니다. 같은 시스템 프롬프트, 같은 도구 스키마, 같은 대화 기록을 매 요청마다 다시 읽고 있기 때문이다. 사용자는 “답변 500토큰”을 받았다고 느끼지만, 서버는 그 전에 30,000토큰짜리 입력을 다시 prefill 했을 수 있다.
KV 캐시는 이 문제를 줄이는 가장 현실적인 장치다. 이미 읽은 prefix의 key/value 텐서를 재사용하면 모델은 같은 앞부분을 반복 계산하지 않는다. 그런데 단일 vLLM 인스턴스에서 잘 보이던 이 이득은 여러 pod로 scale-out하는 순간 쉽게 사라진다. 일반 로드밸런서는 GPU가 한가한지만 보고 요청을 보낸다. “이 고객의 이전 turn이 어느 pod의 KV 캐시에 남아 있는가”는 보지 않는다.
이 글의 중심은 단순하다. LLM 서빙에서 KV 캐시 히트율은 부가 지표가 아니라 latency, throughput, 비용을 한꺼번에 설명하는 운영 지표다. 특히 에이전트, RAG, 고객별 긴 컨텍스트 SaaS에서는 p95 latency보다 먼저 봐야 할 때가 많다.
prefill과 decode를 나눠 봐야 비용이 보인다
LLM 추론은 한 덩어리 작업처럼 보이지만 운영자는 prefill과 decode를 따로 봐야 한다. prefill은 입력 토큰 전체를 읽고 attention 계산을 하면서 KV cache를 만드는 단계다. decode는 이미 만들어진 KV를 재사용하면서 다음 토큰을 하나씩 생성하는 단계다.
문제는 많은 AI 제품의 입력이 출력보다 훨씬 크다는 점이다. 사내 문서 8개, 정책 전문, 시스템 프롬프트, 도구 설명, 이전 대화가 붙으면 입력은 20,000토큰을 넘기 쉽다. 반면 실제 답변은 300~800토큰이다. 이 상황에서 “초당 출력 토큰”만 보면 병목을 놓친다. 사용자가 기다리는 첫 토큰까지의 시간, 즉 TTFT는 prefill에 크게 묶인다.
예를 들어 고객 지원 코파일럿이 매 요청마다 system_prompt, company_policy, tool_schema, conversation_history를 같은 순서로 보낸다고 하자. 새 user message는 120토큰뿐인데 앞의 18,000토큰이 동일하다면, 캐시가 맞을 때와 안 맞을 때의 체감 속도는 완전히 달라진다. GPU 사용률이 낮아 보여도 사용자는 첫 토큰을 오래 기다릴 수 있다.
prefix caching은 “비슷한 문장”이 아니라 “같은 토큰 앞부분”을 재사용한다
자동 prefix caching을 오해하면 대시보드를 잘못 읽는다. 이 기능은 의미적으로 비슷한 프롬프트를 알아서 묶어 주는 검색 캐시가 아니다. 토큰화된 앞부분이 동일한 요청에서 이미 계산된 KV 블록을 재사용하는 방식이다.
vLLM의 prefix caching 문서는 KV cache block을 토큰과 그 앞 prefix 기반으로 식별한다고 설명한다. 전체 prompt가 블록 단위로 나뉘고, 특정 블록은 자기 블록의 토큰뿐 아니라 그 이전 prefix와 함께 해시된다. 그래서 첫 10,000토큰이 같고 마지막 사용자 질문만 바뀌는 요청은 유리하다. 반대로 동일한 문서라도 순서가 매번 바뀌거나, timestamp를 system prompt 앞에 끼워 넣거나, tool schema JSON의 key 순서가 흔들리면 캐시가 깨진다.
운영자가 해야 할 일은 “캐시 켜기”에서 끝나지 않는다. 프롬프트 조립 순서를 고정해야 한다. 변하는 값은 가능하면 뒤쪽 suffix로 밀어야 한다. request_id, current_time, 실험용 trace 문구를 system prompt 최상단에 넣으면 모든 요청의 prefix가 달라진다. 이 한 줄 때문에 캐시 히트율이 떨어질 수 있다.
단일 인스턴스에서 빠르던 것이 클러스터에서 느려지는 이유
단일 vLLM 인스턴스에서는 같은 prefix를 가진 요청이 자연스럽게 같은 메모리 공간을 쓴다. 캐시가 남아 있으면 재사용된다. 하지만 replica가 8개인 클러스터에서는 상황이 달라진다. 각 pod는 자기 GPU 메모리에 자기 KV cache를 가진다. 캐시는 전역 공유 자원이 아니다.
일반적인 round-robin 또는 단순 least-connections 라우팅은 이 사실을 모른다. 고객 A의 1번째 turn이 pod-3에서 처리되고, 2번째 turn이 pod-7로 가면 pod-7은 고객 A의 긴 prefix를 다시 prefill 해야 한다. pod-3에는 쓸 수 있는 캐시가 남아 있는데도 말이다.
이 문제는 에이전트에서 더 심해진다. 에이전트는 LLM 호출 사이에 도구 호출을 끼운다. 검색 API, 브라우저, 코드 실행, DB 조회가 200ms~3초 걸릴 수 있다. 그 사이 scheduler가 다음 LLM 호출을 다른 pod로 보내면 이전 KV가 살아 있어도 재사용하지 못한다. 에이전트가 느린 이유를 “모델이 느리다”로만 해석하면 해결이 안 된다.
캐시 친화적 라우팅은 로드밸런싱 기준을 바꾼다
KV cache-aware scheduling의 핵심은 요청을 가장 빈 GPU에 보내는 것이 아니라, 가장 비싼 prefix를 이미 가진 GPU에 보내는 것이다. 물론 큐가 너무 길면 예외가 필요하다. 그래서 실제 scheduler는 적어도 세 가지 정보를 함께 봐야 한다.
첫째, 요청 prefix의 cache affinity다. 같은 tenant, 같은 conversation, 같은 agent workflow step이 어느 pod에 있었는지 추적한다. 둘째, 해당 pod의 대기열과 decode 처리량이다. 캐시가 있어도 큐가 길면 TTFT가 나빠진다. 셋째, KV 메모리 압박이다. prefix를 오래 붙잡으면 새 요청이 밀릴 수 있다.
이 기준은 API gateway 설계도 바꾼다. tenant_id, conversation_id, agent_run_id, prompt_fingerprint 같은 필드가 라우터까지 전달되어야 한다. 애플리케이션 서버 내부 로그에만 있으면 scheduler가 쓸 수 없다. 프롬프트 본문을 그대로 라우터에 넘길 필요는 없지만, 안정적인 fingerprint는 필요하다.
제품 코드에서 가장 먼저 고칠 부분은 프롬프트 조립 순서다
캐시를 살리려면 모델 서버보다 애플리케이션 코드가 먼저 바뀌어야 할 때가 많다. 흔한 실수는 변동성이 큰 값을 앞쪽에 배치하는 것이다.
나쁜 예시는 이런 구조다. 현재 시각: 2026-07-10T09:15:33Z가 system prompt 첫 줄에 있고, 그 다음에 40KB짜리 tool schema가 붙는다. 1초 뒤 요청은 첫 줄부터 달라진다. 뒤의 40KB가 같아도 prefix caching 입장에서는 같은 앞부분이 아니다.
더 나은 구조는 안정적인 덩어리를 먼저 두는 것이다. 모델 역할, 안전 정책, 도구 스키마, 회사 정책처럼 자주 변하지 않는 내용을 앞에 둔다. 현재 시각, A/B 테스트 플래그, 요청별 사용자 상태는 뒤로 보낸다. RAG 문서도 가능하면 정렬 기준을 고정한다. 검색 점수가 같은 문서의 순서가 매번 바뀌면 prefix가 흔들린다.
프롬프트 fingerprint를 남기는 방식도 중요하다. 전체 프롬프트 해시 하나만 남기면 어디서 흔들렸는지 모른다. system_hash, tools_hash, retrieval_pack_hash, history_prefix_hash, suffix_hash처럼 나눠야 한다. 그래야 캐시 miss가 tool schema 변경 때문인지, RAG 정렬 때문인지, 대화 기록 truncation 때문인지 알 수 있다.
에이전트 워크플로에서는 TTL이 필요하다
일반 챗봇에서는 사용자의 다음 입력이 언제 올지 예측하기 어렵다. 하지만 에이전트 내부 호출은 다르다. LLM이 도구를 부르고, 도구 결과를 받은 뒤 다시 LLM을 부르는 패턴은 짧은 간격으로 반복된다. 이때 이전 turn의 KV를 너무 빨리 evict하면 매 step마다 긴 prefix를 다시 읽는다.
그래서 에이전트 서빙에서는 KV cache TTL을 별도로 생각해야 한다. “tool call이 평균 800ms 안에 끝나는 워크플로라면 agent_run_id 단위로 2~5초 정도 캐시를 붙잡을 가치가 있는가” 같은 질문이 생긴다. 반대로 외부 결제 승인처럼 30초 이상 걸리는 도구라면 GPU 메모리에 계속 붙잡는 것이 손해일 수 있다.
TTL은 무조건 길수록 좋지 않다. 작은 GPU에서 긴 context를 가진 agent run을 여러 개 붙잡으면 새 요청이 대기한다. 운영자는 cache_retained_bytes, evicted_before_next_turn, tool_latency_ms, recompute_prefill_ms를 같이 봐야 한다. 이 값이 없으면 TTL 정책은 감으로 바뀐다.
대시보드에서 p95 latency보다 먼저 볼 숫자
KV 캐시 운영 대시보드는 일반 웹 서버 대시보드와 다르다. requests per second, GPU utilization, p95 latency만으로는 부족하다. 최소한 다음 값을 분리해서 봐야 한다.
prefill_tokens_total과decode_tokens_total: 입력 병목인지 출력 병목인지 나눈다.prefix_cache_hit_tokens와prefix_cache_miss_tokens: hit rate를 요청 수가 아니라 토큰 기준으로 본다.ttft_ms: 사용자가 느끼는 첫 응답 지연이다.queue_wait_ms: 모델 계산 전 대기 시간이다.prompt_fingerprint_cardinality: 캐시가 깨질 만큼 prefix 종류가 폭증하는지 본다.tenant_cache_affinity: 같은 tenant 요청이 같은 pod로 잘 가는지 확인한다.
특히 cache hit rate를 “요청 단위”로만 보면 착시가 생긴다. 짧은 요청 100개가 hit이고, 긴 요청 5개가 miss라면 요청 기준 hit rate는 높다. 하지만 비용은 긴 miss 5개가 잡아먹는다. 토큰 가중 hit rate를 따로 봐야 한다.
실무 판단 기준
KV 캐시 최적화는 모든 서비스의 1순위가 아니다. 짧은 단발 질의가 대부분이고 system prompt도 1,000토큰 이하라면 라우팅 복잡도를 늘릴 이유가 작다. 반대로 다음 조건 중 둘 이상이면 캐시를 제품 요구사항으로 다뤄야 한다.
첫째, 동일 tenant 또는 동일 conversation에서 긴 prefix가 반복된다. 둘째, agent가 한 작업에서 LLM을 여러 번 호출한다. 셋째, tool schema가 크고 자주 바뀌지 않는다. 넷째, RAG 문서 묶음이 고객별로 안정적이다. 다섯째, TTFT가 decode 속도보다 큰 불만이다.
도입 순서는 프롬프트 안정화, fingerprint 로깅, 단일 인스턴스 prefix cache 확인, replica 라우팅 개선, TTL 정책 순서가 안전하다. 처음부터 복잡한 scheduler를 만들기보다 “왜 같은 고객의 두 번째 요청이 다른 pod로 갔는가”를 설명할 로그를 남기는 것이 먼저다. KV 캐시 히트율을 모르면 LLM 비용 최적화는 대부분 추측이 된다.