Skip to content

필사 모드: KV 캐시와 PagedAttention — 추론 메모리의 모든 것

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

LLM 추론에서 GPU 메모리는 두 가지가 나눠 씁니다. 하나는 모델 가중치이고, 다른 하나는 KV 캐시입니다. 가중치는 모델 크기가 정해지면 고정이지만, KV 캐시는 동시 요청 수와 시퀀스 길이에 따라 끝없이 불어납니다. 그래서 실무에서 "동시에 몇 명을 받을 수 있는가", "컨텍스트를 얼마나 길게 줄 수 있는가"를 결정하는 것은 대부분 KV 캐시입니다.

이 글은 KV 캐시라는 한 가지 주제를 끝까지 파고듭니다. 먼저 KV 캐시가 무엇이고 왜 필요한지, 그것이 왜 그렇게 많은 메모리를 먹는지 산수로 확인합니다. 그다음 전통적 메모리 관리의 단편화 문제를 보고, PagedAttention이 그것을 어떻게 해결하는지, prefix 공유와 KV 양자화로 어디까지 더 짤 수 있는지 살펴봅니다.

핵심 원리: KV 캐시란 무엇인가

트랜스포머의 어텐션은 각 토큰에 대해 query, key, value 세 가지 벡터를 만듭니다. 한 토큰을 생성할 때, 그 토큰의 query는 앞선 모든 토큰의 key, value와 어텐션을 계산합니다.

attention(Q, K, V) = softmax(Q K^T / sqrt(d_k)) V

여기서 핵심은, 새 토큰을 생성할 때마다 앞선 토큰들의 key와 value는 변하지 않는다는 점입니다. 토큰 100번째를 생성할 때 필요한 1~99번 토큰의 K, V는 이미 이전 스텝에서 계산했던 것과 똑같습니다. 그렇다면 매번 다시 계산할 이유가 없습니다.

KV 캐시는 바로 이 K, V를 저장해 재사용하는 것입니다. 캐시가 없다면 토큰 하나를 만들 때마다 지금까지의 모든 토큰에 대해 K, V를 다시 계산해야 합니다. 시퀀스가 길어질수록 이 중복 계산은 감당할 수 없게 커집니다. KV 캐시는 이 중복을 제거해 decode를 실용적인 속도로 만들어주는 필수 장치입니다.

KV 캐시는 왜 메모리를 먹는가

문제는 KV 캐시가 토큰마다, 레이어마다, 어텐션 헤드마다 쌓인다는 점입니다. 그 크기를 형태로 보면 다음과 같습니다.

KV 캐시 형태:

num_layers x 2 x batch x num_kv_heads x seq_len x head_dim

각 항목 의미:

num_layers : 트랜스포머 레이어 수 (레이어마다 캐시)

2 : key와 value 두 가지

batch : 동시 처리 요청 수

num_kv_heads : KV 헤드 수 (GQA/MQA면 query 헤드보다 적음)

seq_len : 현재까지의 토큰 수 (계속 증가)

head_dim : 헤드 하나의 차원

총 원소 수에 원소당 바이트 수(예: FP16이면 2바이트)를 곱하면 메모리 사용량이 나옵니다.

KV 캐시 바이트 = num_layers * 2 * batch * num_kv_heads * seq_len * head_dim * dtype_bytes

메모리 산수 예시

추상적인 식보다 숫자가 와닿습니다. 어떤 모델이 다음과 같다고 가정합니다(설명용 근삿값).

가정:

num_layers = 32

num_kv_heads = 8 (GQA 적용)

head_dim = 128

dtype_bytes = 2 (FP16)

토큰 1개당, 요청 1개당 KV 바이트:

= 32 * 2 * 8 * 128 * 2

= 131072 바이트 (약 128 KB)

시퀀스 4096 토큰짜리 요청 1개:

= 128 KB * 4096

= 약 512 MB

이런 요청을 동시에 32개 받으면:

= 512 MB * 32

= 약 16 GB (가중치와 별개로!)

즉 KV 캐시만으로 수십 GB를 우습게 먹습니다. 가중치가 16GB인 모델을 80GB GPU에 올렸다고 안심할 수 없습니다. 남은 공간을 KV 캐시가 얼마나 효율적으로 쓰느냐가 동시성과 컨텍스트 길이를 결정합니다. 그래서 GQA/MQA로 KV 헤드 수를 줄이는 것이 메모리 측면에서 큰 의미를 갖습니다. 위 식에서 num_kv_heads가 작아지면 그대로 캐시가 줄어들기 때문입니다.

단편화 문제

KV 캐시의 크기를 줄이는 것만큼 중요한 것이 그 공간을 낭비 없이 쓰는 것입니다. 전통적인 추론 시스템은 각 요청에 대해 최대 컨텍스트 길이만큼 연속된 메모리를 미리 잡았습니다. 그런데 실제 생성 길이는 미리 알 수 없습니다.

최대 2048 토큰을 잡았지만 실제로 100 토큰만 생성한 경우:

[■■■■... (100칸 사용) ...□□□□□□□□□□□□□□ (1948칸 낭비)]

요청마다 이런 낭비가 쌓이면, GPU 메모리는 충분한데도

"더 받을 자리가 없다"는 역설적 상황이 발생합니다.

이것을 내부 단편화(internal fragmentation)라고 합니다. 게다가 요청이 들고 나면서 메모리에 구멍이 생기는 외부 단편화(external fragmentation)도 발생합니다. 큰 연속 블록을 잡아야 하는데 흩어진 빈 공간만 남아 할당에 실패하는 것입니다. 전통적 방식에서는 GPU 메모리의 상당 부분이 이렇게 그냥 버려졌습니다.

PagedAttention: OS 가상 메모리식 관리

PagedAttention의 발상은 운영체제의 가상 메모리에서 왔습니다. OS는 프로세스에게 연속된 가상 주소 공간을 보여주지만, 실제 물리 메모리는 페이지 단위로 흩어져 있습니다. 페이지 테이블이 둘을 매핑하죠.

PagedAttention은 KV 캐시에 똑같이 합니다. KV 캐시를 작은 고정 크기 블록으로 나누고, 토큰이 쌓일 때마다 블록을 하나씩 할당합니다. 블록 테이블이 각 요청의 논리적 토큰 위치를 물리적 블록에 매핑합니다.

블록 크기 = 16 토큰이라 가정.

물리 블록 풀:

[blk0][blk1][blk2][blk3][blk4][blk5][blk6]...

요청 A (40 토큰): 블록테이블 -> [blk0, blk3, blk5]

(16+16+8 토큰, 마지막 블록만 일부 사용)

요청 B (20 토큰): 블록테이블 -> [blk1, blk2]

물리적으로 흩어져 있어도 논리적으로는 연속처럼 동작.

효과는 분명합니다. 미리 최대치를 잡지 않고 필요한 만큼만 블록을 할당하므로 내부 단편화가 거의 사라집니다. 블록 크기가 작고 균일하므로 외부 단편화도 사라집니다. 그 결과 같은 GPU 메모리로 훨씬 많은 동시 요청을 받을 수 있습니다. 낭비되던 공간이 실제 처리량으로 전환되는 것입니다.

prefix 공유와 copy-on-write

블록 단위 관리의 또 다른 큰 이점은 공유입니다. 여러 요청이 같은 앞부분(prefix)을 가질 때, 그 부분의 KV 블록을 물리적으로 한 벌만 두고 여러 요청이 가리키게 할 수 있습니다.

공통 시스템 프롬프트를 가진 세 요청:

req1 블록테이블: [공유blkS] -> [blk10] -> ...

req2 블록테이블: [공유blkS] -> [blk11] -> ...

req3 블록테이블: [공유blkS] -> [blk12] -> ...

^^^^^^^

같은 물리 블록을 셋이 공유

시스템 프롬프트, few-shot 예시, 멀티턴 대화의 공통 히스토리처럼 겹치는 부분이 많을수록 이 공유의 이득이 큽니다. 공유된 prefix는 KV 계산도 한 번만 하면 됩니다.

여기서 한 가지 주의할 점이 있습니다. 공유된 블록을 어느 한 요청이 수정해야 하는 경우입니다. 예를 들어 분기되는 시퀀스에서 한쪽만 토큰이 바뀌면, 그 블록을 복사한 뒤 수정해야 다른 요청에 영향을 주지 않습니다. 이것이 copy-on-write입니다. 읽을 때는 공유하다가, 쓰는 순간 복사본을 만드는 것이죠. OS 가상 메모리와 똑같은 패턴입니다.

KV 양자화: FP8과 INT8

KV 캐시가 메모리 병목이라면, 캐시 자체의 정밀도를 낮춰 크기를 줄이는 방법도 있습니다. KV를 FP16 대신 FP8이나 INT8로 저장하면 캐시 메모리가 대략 절반으로 줄어듭니다.

FP16 KV: 원소당 2바이트 (기준)

FP8 KV: 원소당 1바이트 (약 50% 절감)

INT8 KV: 원소당 1바이트 (약 50% 절감, 스케일/영점 필요)

decode가 메모리 바운드이므로, KV 읽기량이 줄면 속도에도 도움이 됩니다. 다만 정밀도를 낮추면 출력 품질이 미묘하게 떨어질 수 있어, 모델과 작업에 따라 영향을 검증해야 합니다. 일반적으로 KV 양자화는 가중치 양자화보다 품질 영향이 작은 편이지만, 무조건 안전하다고 단정할 수는 없습니다. 중요한 워크로드라면 실제 평가 지표로 확인하는 것이 안전합니다.

컨텍스트 길이와 메모리 트레이드오프

KV 캐시는 seq_len에 선형으로 비례합니다. 컨텍스트를 두 배로 늘리면 요청당 KV 캐시도 두 배가 됩니다. 즉 긴 컨텍스트와 높은 동시성은 같은 메모리를 두고 경쟁합니다.

같은 GPU 메모리 예산 안에서:

컨텍스트 4K -> 동시 요청 많이 가능

컨텍스트 32K -> 요청당 KV 8배 -> 동시 요청 크게 감소

컨텍스트 128K -> 요청당 KV 폭증 -> 동시성 매우 제한

서빙을 설계할 때 "우리는 최대 컨텍스트를 얼마로 지원할 것인가"는 곧 "동시에 몇 명을 받을 수 있는가"와 직결됩니다. 무작정 컨텍스트를 키우면 동시성이 무너집니다. 이 트레이드오프를 인지하고 워크로드에 맞게 상한을 정하는 것이 중요합니다.

GQA와 MQA: 헤드 수를 줄여 KV를 줄인다

KV 캐시 크기 식에서 num_kv_heads가 곱해진다는 점을 다시 봅시다. 만약 KV 헤드 수를 줄이면, 캐시가 비례해서 줄어듭니다. 이것이 GQA(Grouped-Query Attention)와 MQA(Multi-Query Attention)의 핵심 동기입니다.

MHA (Multi-Head Attention, 전통):

query 헤드 32개, key/value 헤드도 32개

-> KV 캐시 = 32 헤드분

MQA (Multi-Query Attention):

query 헤드 32개, key/value 헤드는 1개 (모든 query가 공유)

-> KV 캐시 = 1 헤드분 (32배 절감!)

GQA (Grouped-Query Attention, 절충):

query 헤드 32개를 그룹으로 묶고, 그룹마다 KV 헤드 1개

예) 8개 KV 헤드 -> KV 캐시 = 8 헤드분 (4배 절감)

MQA는 가장 공격적으로 KV를 줄이지만, 표현력이 다소 떨어져 품질에 영향이 있을 수 있습니다. GQA는 그 사이의 절충으로, 적당한 수의 KV 헤드를 두어 메모리 절감과 품질을 모두 챙깁니다. 2026년 현재 다수의 오픈 모델이 GQA를 기본으로 채택하는데, 이는 서빙 메모리 효율이 그만큼 중요해졌기 때문입니다.

KV 캐시 절감 효과 (head 차원만 비교):

MHA(32 KV헤드) : 기준 100%

GQA(8 KV헤드) : 약 25%

MQA(1 KV헤드) : 약 3%

서빙 엔진을 운영할 때, 모델이 GQA/MQA를 쓰는지 아는 것은 동시성 계획에 직접적인 영향을 줍니다. 같은 파라미터 수의 모델이라도 KV 헤드 구성에 따라 받을 수 있는 동시 요청 수가 크게 달라지기 때문입니다.

블록 크기라는 손잡이

PagedAttention에서 블록 크기(블록 하나에 담는 토큰 수)는 중요한 튜닝 손잡이입니다. 너무 크게 잡으면 마지막 블록의 낭비(내부 단편화)가 커지고, 너무 작게 잡으면 블록 테이블 관리 오버헤드와 메타데이터가 늘어납니다.

블록 크기가 큰 경우 (예: 64 토큰):

- 블록 테이블이 짧아 관리 간단

- 하지만 마지막 블록의 빈 공간 낭비가 큼

(3 토큰만 더 필요한데 64칸 블록을 통째로 할당)

블록 크기가 작은 경우 (예: 8 토큰):

- 마지막 블록 낭비가 작음

- 하지만 블록 수가 많아 테이블/메타데이터 증가

실무: 보통 16 전후의 값이 균형점으로 쓰임

대부분의 경우 기본값을 그대로 쓰는 것이 무난하지만, 아주 짧은 요청이 많은 워크로드와 아주 긴 요청이 많은 워크로드는 최적 블록 크기가 다를 수 있습니다. 이것이 "왜 블록 단위 관리가 손잡이를 제공하는가"를 보여주는 예입니다.

prefix 캐시의 수명과 무효화

prefix 공유는 강력하지만, 공유된 블록을 언제까지 살려둘지가 또 다른 문제입니다. 캐시는 무한하지 않으므로, 어느 시점에는 오래된 prefix 블록을 비워 새 요청에 공간을 내줘야 합니다.

prefix 캐시 관리의 두 축:

1) 적중(hit): 새 요청의 앞부분이 캐시된 prefix와 일치

-> KV 계산을 건너뛰고 블록 재사용 (이득)

2) 축출(eviction): 공간이 부족하면 오래된 prefix 제거

-> 보통 LRU(가장 오래 안 쓰인 것부터) 방식

트레이드오프:

prefix 캐시를 오래 유지 -> 적중률 ↑ 하지만 가용 메모리 ↓

빨리 축출 -> 가용 메모리 ↑ 하지만 적중률 ↓

워크로드가 같은 시스템 프롬프트를 계속 쓴다면 그 prefix는 거의 항상 적중하므로 유지할 가치가 큽니다. 반대로 매번 완전히 다른 prefix가 들어온다면 캐시를 유지하는 비용만 들고 적중이 없습니다. 따라서 prefix 캐시는 워크로드의 성격을 알 때 가장 잘 작동합니다.

메모리 예산을 나누는 법

지금까지의 내용을 종합하면, GPU 메모리는 결국 몇 덩어리로 나뉩니다. 이 배분을 이해하면 서빙 설정이 한눈에 들어옵니다.

GPU 메모리 예산 분해 (80 GB GPU 예시):

[모델 가중치] 예: 16 GB (FP16 8B) ~ 40 GB (FP16 20B급)

[활성화/작업 버퍼] 예: 수 GB (배치/시퀀스에 비례)

[KV 캐시 풀] 나머지 전부 -> 동시성과 컨텍스트를 결정

[안전 여유] OOM 방지용 헤드룸

핵심: 가중치를 양자화로 줄일수록 KV 캐시 풀이 커지고,

그만큼 더 많은 동시 요청 또는 더 긴 컨텍스트가 가능.

이 그림이 중요한 이유는, 서빙 설정의 거의 모든 결정이 이 예산 배분으로 귀결되기 때문입니다. gpu-memory-utilization을 올린다는 것은 KV 캐시 풀을 키운다는 뜻이고, 양자화를 켠다는 것은 가중치를 줄여 KV 풀에 공간을 더 준다는 뜻입니다. 모든 손잡이가 결국 이 한 장의 그림 위에서 움직입니다.

함정과 트러블슈팅

- **OOM이 간헐적으로 터진다**: 평균 요청이 아니라 동시에 들어온 긴 요청들이 KV 캐시를 폭증시킨 경우입니다. 최악의 동시성을 가정해 메모리 예산을 잡으세요.

- **메모리는 남는데 동시성이 안 오른다**: 단편화 또는 보수적인 사전 할당이 원인일 수 있습니다. paged 방식의 엔진을 쓰는지 확인하세요.

- **prefix 캐시가 기대만큼 안 먹힌다**: 요청들이 실제로 같은 prefix를 공유하지 않거나, prefix 길이가 블록 크기보다 짧아 공유 단위가 안 맞을 수 있습니다.

- **KV 양자화 후 품질 저하**: 작업에 따라 영향이 다릅니다. 양자화 전후를 같은 평가셋으로 비교하세요.

- **GQA인데 메모리가 안 줄었다**: 설정에서 KV 헤드 수가 실제로 줄었는지, 캐시가 KV 헤드 기준으로 잡히는지 확인하세요.

마치며

KV 캐시는 LLM 추론 메모리의 핵심이자, 동시성과 컨텍스트 길이를 좌우하는 가장 중요한 변수입니다. 그 크기는 레이어, 헤드, 시퀀스 길이, 정밀도의 곱으로 결정되며, 전통적 관리 방식은 단편화로 많은 공간을 낭비했습니다.

PagedAttention은 OS 가상 메모리의 발상을 빌려 이 낭비를 거의 제거했고, prefix 공유와 copy-on-write로 중복까지 줄였습니다. KV 양자화는 한 발 더 나아가 캐시 자체를 압축합니다. 이 모든 기법의 공통 목표는 하나입니다. 같은 GPU로 더 많은 사용자에게 더 긴 컨텍스트를 제공하는 것. KV 캐시를 이해하면 서빙 시스템의 동작과 한계가 비로소 또렷하게 보입니다.

참고 자료

- [vLLM 공식 문서](https://docs.vllm.ai/)

- [vLLM GitHub](https://github.com/vllm-project/vllm)

- [SGLang GitHub](https://github.com/sgl-project/sglang)

- [TensorRT-LLM GitHub](https://github.com/NVIDIA/TensorRT-LLM)

- [Hugging Face 문서](https://huggingface.co/docs)

- [PyTorch](https://pytorch.org/)

- [Attention Is All You Need (arXiv:1706.03762)](https://arxiv.org/abs/1706.03762)

- [FlashAttention (arXiv:2205.14135)](https://arxiv.org/abs/2205.14135)

현재 단락 (1/130)

LLM 추론에서 GPU 메모리는 두 가지가 나눠 씁니다. 하나는 모델 가중치이고, 다른 하나는 KV 캐시입니다. 가중치는 모델 크기가 정해지면 고정이지만, KV 캐시는 동시 요청 ...

작성 글자: 0원문 글자: 6,390작성 단락: 0/130