연속 배치에서 스케줄러가 한 스텝에 넣을 수 있는 요청 수는 결국 GPU 메모리가 정한다. 가중치를 올리고 남은 메모리 대부분은 요청마다 쌓이는 KV 캐시가 차지하고, 이 캐시의 크기는 모델 구조와 문맥 길이로 계산할 수 있다. 용량 계획에서 가장 먼저 해 볼 계산이 이것이다.
KV 캐시에 들어가는 것
트랜스포머의 어텐션은 토큰마다 쿼리(Q), 키(K), 값(V) 벡터를 만든다. 새 토큰을 생성할 때 그 토큰의 쿼리는 앞선 모든 토큰의 키와 비교되고, 그 결과로 값들을 가중합한다. 앞선 토큰의 키와 값은 한 번 계산하면 바뀌지 않으므로 저장해 두고 다시 쓴다. 저장하지 않으면 스텝마다 앞선 토큰 전부의 키와 값을 다시 계산해야 해서, 길이 n의 시퀀스를 생성하는 비용이 n²에 비례해 늘어난다.
키와 값은 층마다, KV 헤드마다 따로 있다. 그래서 KV 캐시는 요청 수 × 토큰 수 × 층 수 × KV 헤드 수만큼의 벡터를 담는다.
토큰당 크기
층마다, KV 헤드마다 키와 값 벡터가 하나씩 있으므로 토큰 하나가 차지하는 KV 캐시는 다음 식으로 구한다. 원소당 바이트는 KV 캐시를 저장하는 정밀도에 따라 정한다.
토큰당 바이트 = 2 (K, V) × 층 수 × KV 헤드 수 × 헤드 차원 × 원소당 바이트
Llama 3.1 8B Instruct는 층 32개, KV 헤드 8개이고, 은닉 차원 4,096을 어텐션 헤드 32개로 나누므로 헤드 차원은 128이다. BF16이면 원소 하나가 2바이트다.
2 × 32 × 8 × 128 × 2바이트 = 131,072바이트 = 128KiB
이 모델의 어텐션 헤드는 32개인데 KV 헤드는 8개다. 쿼리 헤드 4개가 키·값 헤드 하나를 나눠 쓰는 그룹 쿼리 어텐션(GQA) 구조라서다. 쿼리 헤드마다 키·값을 따로 두는 일반적인 멀티헤드 어텐션이었다면 토큰당 512KiB로 네 배가 된다. KV 캐시를 줄이는 가장 효과가 큰 결정은 서빙이 아니라 모델 설계 단계에서 이미 내려진다.
메모리 예산
H100 SXM 80GB에 이 모델을 올렸을 때 KV 캐시에 쓸 수 있는 메모리는 대략 다음과 같다.
| 항목 | 크기 |
|---|---|
| 전체 HBM | 80GB |
| 엔진에 할당하는 비율 | vLLM 기본값 기준 92% → 약 74GB |
| 모델 가중치 (BF16) | 약 16GB |
| 활성화 버퍼·CUDA 그래프 등 | 수 GB |
| KV 캐시 | 약 55GB |
엔진에 할당하는 비율은 vLLM의 gpu_memory_utilization 설정이고, 문서에 적힌 기본값은 0.92다. 활성화 버퍼는 한 스텝에 처리하는 최대 토큰 수에 따라 달라지므로 정확한 값은 엔진이 시작할 때 로그로 남기는 KV 캐시 크기를 확인한다. 55GB를 토큰당 128KiB로 나누면 약 42만 토큰이다.
55 × 10⁹ 바이트 ÷ 131,072바이트 ≈ 420,000토큰
GPU 한 장이 동시에 품을 수 있는 KV 캐시는 모든 요청을 합쳐 약 42만 토큰이다. 이 값이 연속 배치의 실제 상한이 된다.
동시 요청 수와 문맥 길이
42만 토큰을 요청들이 나눠 쓴다. 요청 하나가 쓰는 토큰은 프롬프트와 생성한 토큰을 합친 길이다. 요청마다 문맥을 끝까지 쓴다고 하면 동시 요청 수는 이렇게 된다.
| 요청당 문맥 길이 | 동시 요청 수 상한 |
|---|---|
| 2,048토큰 | 약 205개 |
| 4,096토큰 | 약 102개 |
| 32,768토큰 | 약 12개 |
| 131,072토큰 (모델 최대) | 약 3개 |
문맥 상한을 두 배로 열면 동시 요청 수 상한은 절반이 된다. 컨텍스트 길이와 동시 요청 수는 같은 메모리를 나눠 쓰는 값이라 따로 정할 수 없다. 모델이 128k 문맥을 지원한다고 서비스의 문맥 상한을 128k로 열면, 긴 요청 몇 개가 들어오는 순간 나머지 요청은 대기열에서 기다린다.
실제 요청은 대부분 상한보다 훨씬 짧으므로 평균 길이로 계산하면 수용량이 더 크게 나온다. 다만 평균으로 계획하면 긴 요청이 몰리는 시간대에 메모리가 바닥나 선점과 대기가 생긴다. 용량 계획에는 평균이 아니라 피크 시간대의 길이 분포 상위 구간을 쓴다.
디코드 스텝에서 읽는 양
KV 캐시는 메모리만 차지하지 않는다. 디코드 스텝마다 배치 안 모든 요청의 KV 캐시를 메모리에서 읽어야 한다. 요청 64개의 문맥이 각각 2,000토큰이라면 한 스텝에 읽는 KV 캐시는 이렇다.
64 × 2,000토큰 × 131,072바이트 ≈ 16.8GB
가중치 16GB와 거의 같은 양이다. 디코드 스텝 시간의 하한은 읽는 바이트 수가 정하므로 이 배치의 스텝 하한은 가중치만 읽을 때의 약 4.8ms에서 두 배인 약 9.8ms로 늘어난다. 배치를 키우면 가중치를 읽는 비용은 여러 요청이 나눠 내지만 KV 캐시를 읽는 비용은 요청마다 따로 낸다. 문맥이 긴 부하에서 배치를 키워도 처리량이 생각만큼 오르지 않는 이유가 이것이다.
스텝 하한 ≈ (가중치 바이트 + Σ 요청별 문맥 길이 × 토큰당 KV 바이트) ÷ 메모리 대역폭
줄이는 방법
KV 캐시의 부담을 줄이는 방법은 식의 항마다 하나씩 있다.
| 줄이는 항 | 방법 |
|---|---|
| KV 헤드 수 | GQA·MQA를 쓰는 모델을 고른다 (모델 설계 단계) |
| 원소당 바이트 | KV 캐시를 FP8 등으로 양자화한다 |
| 문맥 길이 | 서비스의 문맥 상한을 사용 사례에 맞게 정하고, 대화 이력은 잘라 내거나 요약한다 |
| 낭비되는 메모리 | 요청마다 연속 공간을 미리 잡지 않고 고정 크기 블록 단위로 할당한다 |
| 중복 | 여러 요청이 공유하는 프롬프트 앞부분의 KV 캐시를 한 번만 저장한다 |
앞의 둘과 문맥 길이는 토큰당 크기와 토큰 수를 줄이고 뒤의 둘은 같은 메모리에 실제로 담기는 토큰 수를 늘린다.