1. 개요
트랜스포머는 다음 토큰을 정할 때 앞의 모든 토큰을 다시 본다(attention). 이때 쓰는 앞 토큰들의 Key·Value 벡터를 매번 새로 계산하지 않도록 GPU 메모리에 저장해 둔 것이 KV 캐시다.
| 단계 | 하는 일 | 무거운 자원 | 지표 |
|---|---|---|---|
| prefill | 입력 토큰 전부의 K·V 를 한 번에 계산해 캐시에 쓴다 | 연산(행렬 곱) | TTFT |
| decode | 새 토큰 하나의 K·V 만 계산해 붙이고, 캐시 전체와 가중치를 읽어 다음 토큰을 정한다 | 메모리 대역폭 | ITL |
캐시가 없으면 토큰 하나를 만들 때마다 앞 토큰 전부를 다시 계산해야 한다. 캐시는 그 계산을 메모리로 바꾼 것이고, 그래서 메모리가 새 한계가 된다.
2. 크기 계산
KV 캐시는 요청마다, 토큰마다 자란다.
토큰당 바이트 = 2 (K, V) × 층 수 × KV 헤드 수 × 헤드 크기 × 원소 바이트
요청 하나 = 토큰당 바이트 × (입력 + 지금까지 출력한 토큰)
| 모델 | 층 | KV 헤드 | 헤드 크기 | BF16 토큰당 | 16k 문맥 하나 |
|---|---|---|---|---|---|
| Qwen3-8B | 36 | 8 | 128 | 144KiB | 2.25GiB |
| Qwen3-30B-A3B | 48 | 4 | 128 | 96KiB | 1.5GiB |
KV 헤드 수가 Query 헤드 수보다 적은 것은 GQA(Grouped-Query Attention) 덕이다. Qwen3-8B 는 Query 헤드 32개가 KV 헤드 8개를 나눠 써서 캐시가 1/4 로 준다.
3. GPU 메모리 예산
vLLM 은 시작할 때 GPU 메모리의 --gpu-memory-utilization(기본 0.9)만큼을 쓰기로 하고, 가중치와 실행용 메모리를 뺀 나머지를 전부 KV 캐시 블록으로 미리 잡는다. 시작 로그의 GPU KV cache size: N tokens 가 그 결과다.
| 모델 (L40S 1장) | 가중치 | KV 캐시 자리 | 동시 64 에서 KV 사용률 |
|---|---|---|---|
| Qwen3.8-27B FP8 | 약 31GB | 9.3GB (약 11만 8천 토큰) | 99% |
| gpt-oss-20b MXFP4 | 약 13GB | 훨씬 넉넉 | 8% |
자리가 다 차면 새 요청은 대기 줄에서 기다리고, 실행 중인 요청이 블록을 더 못 받으면 vLLM 이 일부 요청을 내려놓았다가(preemption) 자리가 나면 다시 계산한다. 둘 다 TTFT를 늘린다.
4. PagedAttention
요청마다 최대 길이만큼 연속한 메모리를 미리 잡으면, 실제로 짧게 끝나는 요청의 자리가 낭비된다. vLLM 의 PagedAttention 은 KV 캐시를 작은 블록(기본 16토큰)으로 나눠 필요할 때마다 아무 빈 블록에나 붙인다.
| 이점 | 이유 |
|---|---|
| 낭비가 적다 | 마지막 블록의 빈칸만 남는다. 논문 기준 낭비 4% 미만 |
| 앞부분 공유 | 같은 시스템 프롬프트로 시작하는 요청은 같은 물리 블록을 함께 가리킨다(prefix caching) |
| 내려놓기·되살리기 | 블록 단위로 비우고 다시 채운다 |
5. FP8 로 저장하기
--kv-cache-dtype fp8 은 K·V 를 1바이트로 저장한다. 같은 자리에 토큰이 두 배 들어간다.
| 고려할 점 | 내용 |
|---|---|
| 품질 | 값의 정밀도가 줄어든다. 긴 문맥 과제에서 품질을 따로 확인한다 |
| 효과가 큰 경우 | KV 캐시가 먼저 차는 모델(동시 요청이 많거나 문맥이 긴 경우) |
| 효과가 작은 경우 | 가중치가 작아 KV 자리가 이미 넉넉한 모델 |
6. 관련 문서
- TTFT — KV 캐시가 차면 늘어나는 첫 토큰 시간
- Concurrency — KV 캐시가 막는 동시 요청
- MoE — 가중치를 덜 읽는 구조. KV 캐시 크기는 줄이지 않는다