본문 바로가기
AI2026년 8월 14일5분 읽기

KV 캐시 — LLM이 토큰을 하나씩 뱉어도 빠른 이유

YS
김영삼
조회 5
KV 캐시 — LLM이 토큰을 하나씩 뱉어도 빠른 이유

KV 캐시(KV cache)는 LLM이 토큰을 하나씩 생성할 때, 앞서 계산해 둔 어텐션의 키(Key)·값(Value) 벡터를 메모리에 쟁여 두고 재활용하는 기법이다. 자기회귀(autoregressive) 생성은 매 스텝마다 지금까지의 문장 전체를 다시 본다. 이때 이전 토큰들의 K·V를 매번 새로 계산하면 길이의 제곱으로 연산이 폭발한다. 그걸 저장해 두면 새 토큰 하나에 대한 계산만 추가하면 되니, 이게 없으면 긴 답변은 사실상 못 만든다.

나는 처음 로컬에서 7B 모델을 돌릴 때 "왜 컨텍스트만 늘리면 VRAM이 이렇게 훅 뛰지?"에서 막혔다. 가중치는 그대로인데 말이다. 범인이 바로 이 KV 캐시였다. 캐시는 생성이 진행될수록, 그리고 동시 요청이 많을수록 계속 불어난다.

왜 필요한가 — 없을 때의 비용

트랜스포머의 셀프 어텐션은 각 토큰마다 Query·Key·Value 세 벡터를 만든다. n번째 토큰을 생성할 때, 그 Query는 1번부터 n번까지 모든 Key와 내적을 하고 Value를 가중합한다. 캐시가 없으면 스텝마다 과거 토큰의 K·V를 처음부터 다시 뽑아야 한다. 결과적으로 전체 시퀀스 생성 비용이 길이의 제곱을 넘어 세제곱 냄새까지 난다.

캐시를 쓰면 이야기가 달라진다. 이미 만들어 둔 K·V는 바뀌지 않으니(과거는 미래를 못 보므로) 새 토큰의 K·V 한 쌍만 뒤에 이어 붙인다. 스텝당 계산이 시퀀스 길이에 선형으로만 는다. 그래서 실전 추론 엔진은 예외 없이 KV 캐시를 깐다.

메모리를 얼마나 먹나

캐시 크기는 대략 이렇게 잡힌다. 감을 잡는 용도의 근사식이다.

KV 캐시 바이트
 ≈ 2 (K와 V) × 레이어 수 × KV헤드 수 × 헤드차원
   × 시퀀스 길이 × 배치 × 원소당 바이트수
# 예: 32레이어, KV헤드 8, 헤드차원 128, 4096토큰,
#     배치 1, fp16(2바이트)
# 2 × 32 × 8 × 128 × 4096 × 1 × 2 ≈ 537 MB

여기서 핵심은 시퀀스 길이와 배치(동시 요청 수)에 정비례한다는 점이다. 컨텍스트를 4K에서 32K로 늘리면 캐시도 8배가 된다. 동시 사용자 10명이면 또 10배다. 서빙에서 진짜 병목은 종종 연산이 아니라 이 캐시가 차지하는 메모리다.

PagedAttention — 캐시를 페이지로 쪼개다

초창기 엔진들은 요청 하나에 "최대 길이만큼" 연속된 메모리 덩어리를 통째로 예약했다. 문제는 실제 답변이 그보다 짧으면 예약분이 그대로 낭비된다는 것. 요청마다 길이가 제각각이라 조각난 빈틈(fragmentation)도 심했다. 나도 배치를 키우면 VRAM은 남는데 OOM이 나는 황당한 상황을 겪었는데, 원인이 이 낭비였다.

vLLM이 들고나온 PagedAttention은 운영체제의 가상 메모리에서 아이디어를 빌렸다. KV 캐시를 고정 크기 블록(페이지)으로 잘라, 논리적으로는 연속이지만 물리적으로는 흩어진 곳에 담는다. 블록 테이블이 그 매핑을 관리한다. 덕분에 낭비가 거의 사라지고, 프리픽스가 같은 요청끼리 블록을 공유하는 것도 가능해졌다.

방식메모리 낭비공유
연속 예약큼(내부·외부 단편화)어려움
PagedAttention거의 없음프리픽스 블록 공유

캐시를 줄이는 갈래들

캐시 부담이 크다 보니 이걸 줄이려는 시도가 여럿이다. 어텐션 헤드끼리 K·V를 공유하는 GQA·MQA가 대표적이고, 캐시 자체를 8비트나 4비트로 양자화하기도 한다. 아주 오래된 토큰을 버리거나 요약해 창을 밀고 가는 방식도 있다. 어느 쪽이든 "메모리를 아끼는 대신 무엇을 조금 포기하느냐"의 거래다.

KV 캐시는 속도를 위해 메모리를 쓰는 트레이드오프다. 그리고 그 메모리가 결국 동시 처리량(throughput)의 상한을 정한다.

자주 묻는 질문

프롬프트 캐싱과 KV 캐시는 같은 건가요?

뿌리는 닿아 있지만 층위가 다르다. KV 캐시는 한 번의 생성 안에서 이전 토큰들의 K·V를 재활용하는 저수준 메커니즘이다. 반면 API의 프롬프트 캐싱은 여러 요청에 걸쳐 공통 프리픽스(시스템 프롬프트, 긴 문서 등)의 계산 결과를 재사용해 비용과 지연을 줄이는 상위 기능이다. 내부적으로 프리픽스의 KV를 재활용하는 식으로 구현되는 경우가 많다.

컨텍스트를 늘리면 왜 이렇게 느려지고 무거워지나요?

KV 캐시가 시퀀스 길이에 비례해 커지기 때문이다. 메모리가 늘 뿐 아니라, 새 토큰마다 점점 더 긴 캐시를 읽어야 해서 메모리 대역폭 부담도 커진다. 긴 컨텍스트 지원 모델이 GQA나 캐시 양자화를 함께 쓰는 이유가 여기 있다.

KV 캐시를 8비트로 양자화하면 품질이 많이 떨어지나요?

8비트 정도는 대체로 체감 품질 저하가 작아서 메모리 절감 대비 이득이 크다. 다만 4비트까지 내리면 특정 작업에서 미묘한 열화가 보일 수 있으니, 반드시 자기 평가셋으로 확인하고 적용하는 편이 안전하다.

배치를 키우면 처리량이 오르는데 왜 무작정 못 올리나요?

배치를 키우면 그만큼 KV 캐시 총량이 선형으로 늘어 메모리 한계에 부딪히기 때문이다. PagedAttention 같은 기법이 낭비를 줄여 실효 배치를 키워 주지만, 물리 메모리라는 천장 자체를 없애 주지는 않는다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록