본문 바로가기

AI·LLM 엔지니어링

vLLM KV Cache 완전정리 — 메모리 계산·Prefix Caching·FP8 튜닝법

반응형

vLLM 서버를 운영하다 보면 모델 가중치는 GPU에 충분히 들어갔는데 긴 프롬프트나 동시 요청이 늘어나는 순간 메모리가 부족해지는 경우가 있습니다. 양자화 모델로 바꿔도 같은 문제가 반복된다면 가중치가 아니라 KV Cache가 병목일 가능성이 큽니다.

KV Cache는 이미 계산한 Attention의 Key와 Value를 저장해 다음 토큰을 만들 때 같은 계산을 반복하지 않도록 합니다. 생성 속도를 위해 꼭 필요하지만, 입력 길이와 동시 요청 수에 비례해 커집니다. 결론부터 말하면 vLLM의 KV Cache 튜닝은 모델 길이 제한 → 동시성 → 캐시 정밀도 → 반복 Prefix 재사용 → 모니터링 순서로 접근하는 편이 안전합니다.

모델 가중치의 FP16·INT8·INT4 차이가 먼저 궁금하다면 LLM 양자화 완전정리를 참고하세요. 이미 CUDA OOM이 발생했다면 vLLM CUDA OOM 해결 가이드와 함께 보는 것이 좋습니다.

핵심 요약

  • KV Cache는 요청의 토큰 수, 동시 요청 수, 레이어 수, KV Head 수, 데이터 형식에 따라 증가합니다.
  • GQA·MQA 모델은 Query Head보다 KV Head가 적어 같은 조건의 MHA 모델보다 캐시가 작을 수 있습니다.
  • --max-model-len은 무조건 모델 최대값으로 두지 말고 서비스가 실제로 허용할 길이로 제한합니다.
  • 반복되는 시스템 프롬프트·긴 문서가 많다면 Automatic Prefix Caching이 Prefill 계산을 줄일 수 있습니다.
  • FP8 KV Cache는 BF16·FP16 대비 캐시 메모리를 크게 줄일 수 있지만 스케일과 품질 검증이 필요합니다.
  • vllm:kv_cache_usage_perc, 대기 요청, preemption, TTFT를 함께 봐야 병목을 구분할 수 있습니다.

1. KV Cache는 왜 필요한가

Autoregressive LLM은 한 번에 다음 토큰 하나를 예측하고, 그 토큰을 입력에 붙여 다시 다음 토큰을 예측합니다. 매 단계에서 이전 토큰 전체의 Attention Key와 Value를 다시 계산하면 생성 길이가 길어질수록 낭비가 커집니다.

KV Cache는 이전 토큰에서 계산한 Key와 Value를 저장합니다. Decode 단계에서는 새 토큰에 해당하는 값만 계산하고 기존 캐시를 재사용하므로 토큰 생성 속도를 유지할 수 있습니다.

구간주요 작업KV Cache 관점

Prefill 입력 프롬프트 전체를 처리 입력 토큰의 K·V를 한꺼번에 생성
Decode 출력 토큰을 한 개씩 생성 새 토큰의 K·V를 기존 캐시에 추가

따라서 긴 입력은 첫 토큰 지연시간(TTFT)과 초기 캐시 사용량을 키우고, 긴 출력은 요청이 진행되는 동안 캐시를 계속 증가시킵니다.

2. KV Cache 메모리 계산식

일반적인 Transformer의 요청 하나에 필요한 KV Cache는 다음 식으로 대략 계산할 수 있습니다.

요청당 KV Cache ≈
2 × 레이어 수 × KV Head 수 × Head 차원 × 전체 토큰 수 × 데이터형 바이트

전체 KV Cache ≈ 요청당 KV Cache × 동시에 유지되는 시퀀스 수

앞의 2는 Key와 Value 두 텐서를 의미합니다. FP16·BF16은 원소당 2바이트, FP8은 대략 1바이트로 계산할 수 있습니다. 실제 사용량에는 블록 단위 할당, 정렬, 메타데이터, 모델별 Attention 구조가 반영되므로 이 식은 용량 계획의 출발점입니다.

계산 예시

32개 레이어, 8개 KV Head, Head 차원 128인 GQA 모델을 BF16으로 실행한다고 가정해 보겠습니다.

토큰당 KV Cache
= 2 × 32 × 8 × 128 × 2바이트
= 131,072바이트
≈ 128KiB

8,192토큰 요청 1개
≈ 128KiB × 8,192
≈ 1GiB

같은 크기의 요청 8개가 동시에 살아 있다면 단순 계산으로 약 8GiB가 필요합니다. 최대 문맥 길이는 같아도 평균 입력·출력 길이와 동시성에 따라 실제 캐시 압력은 크게 달라집니다.

3. MHA·GQA·MQA가 메모리에 미치는 영향

모델 설정의 Attention Head 수만 보고 KV Cache를 계산하면 틀릴 수 있습니다. GQA(Grouped Query Attention)와 MQA(Multi-Query Attention)는 여러 Query Head가 더 적은 수의 KV Head를 공유합니다.

구조특징KV Cache 경향

MHA Query Head마다 K·V Head를 가짐 상대적으로 큼
GQA 여러 Query Head가 KV Head를 공유 MHA보다 작을 수 있음
MQA 모든 Query Head가 소수의 KV Head를 공유 캐시 절감 폭이 큼

Hugging Face 모델 설정에서는 보통 num_hidden_layers, num_key_value_heads, hidden_size, num_attention_heads를 확인합니다. Head 차원은 일반적으로 hidden_size ÷ num_attention_heads로 구하지만 모델 구현에 따라 별도 설정이 있을 수 있습니다.

4. PagedAttention과 블록 단위 관리

vLLM은 PagedAttention을 사용해 KV Cache를 고정 크기 블록으로 나누고 필요할 때 할당합니다. 운영체제의 가상 메모리처럼 논리 블록과 물리 블록을 분리해, 요청마다 최대 길이만큼 연속 메모리를 미리 잡는 낭비와 단편화를 줄입니다.

이 구조 덕분에 서로 길이가 다른 요청을 Continuous Batching으로 함께 처리하기 쉬워집니다. 다만 블록 단위 관리가 KV Cache 자체를 없애는 것은 아닙니다. 활성 토큰과 캐시된 Prefix가 늘어 블록 풀이 부족해지면 대기나 preemption이 발생할 수 있습니다.

5. 가장 먼저 조정할 값 — max-model-len

모델이 128K 문맥을 지원하더라도 서비스에서 실제로 128K가 필요하지 않다면 최대 길이를 제한하는 편이 좋습니다.

vllm serve YOUR_MODEL \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.90

여기서 길이는 입력과 출력에 사용되는 전체 시퀀스 길이를 고려해야 합니다. API 계층에서도 입력 토큰과 max_tokens의 합이 서비스 한도를 넘지 않도록 검사합니다.

길이 제한을 정하는 방법

  1. 최근 요청의 입력 토큰 P50·P95·P99를 측정합니다.
  2. 실제 필요한 최대 출력 길이를 업무별로 구분합니다.
  3. 긴 문서는 Chunking이나 RAG로 줄일 수 있는지 확인합니다.
  4. P99와 예외 요청 사이에 별도 라우팅이 필요한지 결정합니다.

모든 요청에 가장 긴 문맥을 허용하는 것보다 일반 요청과 장문 요청을 별도 엔드포인트·모델 인스턴스로 분리하는 편이 용량 예측에 유리합니다.

6. gpu-memory-utilization과 kv-cache-memory-bytes

--gpu-memory-utilization은 모델 실행기가 사용할 GPU 메모리 비율을 정하는 대표 옵션입니다. 너무 낮으면 KV Cache 블록이 적어져 동시 처리량이 줄고, 너무 높으면 CUDA Graph·임시 버퍼·다른 프로세스와 충돌할 여유가 줄어듭니다.

최근 vLLM 버전은 GPU별 KV Cache 크기를 바이트 단위로 지정하는 --kv-cache-memory-bytes도 제공합니다. 이 값을 지정하면 캐시 용량을 더 직접적으로 통제할 수 있지만, 버전과 하드웨어에서 지원되는지 vllm serve --help로 먼저 확인해야 합니다.

# 예시: 설치 버전에서 지원 여부를 먼저 확인
vllm serve --help | grep -E "kv-cache|gpu-memory"

# 명시적 캐시 용량 예시
vllm serve YOUR_MODEL \
  --kv-cache-memory-bytes 8G \
  --max-model-len 8192

7. 동시성과 max-num-seqs

KV Cache는 활성 요청 수에 비례해 커집니다. --max-num-seqs를 높이면 이론적인 동시 처리량은 늘 수 있지만, 요청당 확보 가능한 캐시 블록이 줄고 긴 요청이 섞일 때 대기와 preemption이 증가할 수 있습니다.

동시성은 숫자 하나로 결정하지 말고 실제 길이 분포로 부하 테스트해야 합니다.

관찰 결과가능한 원인우선 조치

KV 사용률은 낮지만 GPU가 놀음 요청 수·배치가 작음 동시성 또는 배치 토큰 검토
KV 사용률이 자주 100%에 접근 길이·동시성이 과도함 길이 제한, 동시성 축소, FP8 검토
대기 요청과 preemption 증가 활성 블록 부족 max-num-seqs와 긴 요청 분리
TTFT만 급격히 증가 Prefill 또는 큐 병목 입력 길이·Prefix hit·배치 확인

8. Automatic Prefix Caching

Automatic Prefix Caching(APC)은 이전 요청에서 계산한 Prefix의 KV 블록을 해시로 식별해 새 요청이 같은 Prefix를 가질 때 재사용합니다.

vllm serve YOUR_MODEL \
  --enable-prefix-caching

효과가 큰 경우

  • 모든 요청에 긴 시스템 프롬프트가 반복되는 챗봇
  • 같은 긴 문서를 두고 여러 질문을 하는 문서 QA
  • Few-shot 예시와 도구 정의가 반복되는 Agent 요청
  • 다중 턴 대화에서 이전 Prefix가 그대로 유지되는 경우

효과가 작은 경우

  • 매 요청의 앞부분이 서로 다를 때
  • 입력은 짧고 출력 생성이 대부분의 시간을 차지할 때
  • 공유 Prefix가 블록 경계보다 짧거나 자주 변할 때

APC는 반복 Prefix의 Prefill 계산을 줄이는 기능입니다. Decode에서 새 출력 토큰을 만드는 계산을 빠르게 해주는 기능은 아닙니다. 템플릿에 요청마다 바뀌는 ID·시간을 앞부분에 넣으면 Cache hit가 크게 줄 수 있으므로, 고정 지시문을 앞에 두고 가변 데이터는 뒤로 배치하는 것도 중요합니다.

9. FP8 KV Cache

BF16·FP16 대신 FP8로 KV Cache를 저장하면 원소당 바이트 수가 절반 수준이 되어 더 많은 토큰과 동시 요청을 수용할 수 있습니다.

vllm serve YOUR_MODEL \
  --kv-cache-dtype fp8 \
  --calculate-kv-scales \
  --max-model-len 8192

vLLM 공식 문서는 FP8 KV Cache에서 스케일을 적절히 사용하지 않으면 정확도 저하가 생길 수 있다고 안내합니다. 지원 GPU·Attention backend·모델에 따라 가능한 FP8 형식과 스케일 전략이 다릅니다.

적용 전 검증 항목

  • 설치한 vLLM 버전과 GPU가 선택한 FP8 형식을 지원하는가
  • 고정 스케일, 동적 계산, 보정된 스케일 중 어떤 방식을 사용하는가
  • 긴 문맥에서 정답률·인용 정확도·반복 문제가 증가하지 않는가
  • 메모리 절감이 실제 동시 처리량과 TTFT 개선으로 이어지는가

FP8은 가중치 양자화와 별개의 설정입니다. INT4 가중치 모델도 KV Cache는 기본값에서 BF16이나 FP16을 사용할 수 있으므로 두 영역을 따로 확인해야 합니다.

10. KV Cache Offloading

GPU 캐시가 부족할 때 일부 KV 블록을 CPU 메모리나 외부 저장 계층으로 옮기는 KV Offloading을 검토할 수 있습니다. 최신 vLLM은 별도 KV Offloading 기능과 외부 KV Connector 구성을 제공하지만, 버전별 옵션과 지원 backend가 빠르게 바뀌는 영역입니다.

오프로딩은 용량을 늘리는 대신 PCIe·네트워크·스토리지 전송 지연을 추가합니다. 지연시간이 중요한 실시간 챗봇보다 긴 Prefix 재사용, 배치 작업, GPU 메모리가 매우 제한된 환경에서 먼저 비교하는 편이 좋습니다.

11. 반드시 볼 운영 지표

vLLM의 OpenAI 호환 서버는 Prometheus 형식의 /metrics를 제공합니다. vLLM v1 기준으로 다음 지표를 함께 봅니다.

  • vllm:kv_cache_usage_perc: 사용 중인 KV Cache 블록 비율
  • vllm:num_requests_running: 실행 중인 요청 수
  • vllm:num_requests_waiting: 대기 중인 요청 수
  • vllm:num_preemptions: 캐시 압력 등으로 중단·재계산된 누적 횟수
  • vllm:prefix_cache_queries: Prefix Cache 조회 토큰 수
  • vllm:prefix_cache_hits: 재사용된 Prefix 토큰 수
curl -s http://localhost:8000/metrics \
  | grep -E "kv_cache|prefix_cache|num_requests|preemption"

메트릭 이름은 vLLM v0·v1과 버전에 따라 변경된 이력이 있습니다. 대시보드를 만들기 전에 실제 서버의 /metrics 출력을 기준으로 쿼리를 작성하세요.

12. 증상별 튜닝 순서

증상 A. 서버 시작 단계에서 KV Cache를 만들지 못한다

  1. --max-model-len을 실제 서비스 길이로 낮춥니다.
  2. 가중치가 차지하는 VRAM과 다른 GPU 프로세스를 확인합니다.
  3. 더 작은 모델 또는 가중치 양자화를 검토합니다.
  4. FP8 KV Cache 지원 여부를 확인합니다.

증상 B. 낮은 동시성에서는 되지만 부하에서 OOM·대기가 발생한다

  1. 요청 길이 P95·P99와 동시 활성 시퀀스 수를 측정합니다.
  2. --max-num-seqs를 단계적으로 낮춰 안정점을 찾습니다.
  3. 긴 요청을 별도 큐나 인스턴스로 분리합니다.
  4. FP8 KV Cache로 동일 부하를 재측정합니다.

증상 C. 긴 공통 프롬프트 때문에 TTFT가 크다

  1. 시스템 프롬프트와 도구 스키마가 완전히 동일한지 확인합니다.
  2. 가변 값을 Prefix 뒤쪽으로 이동합니다.
  3. Automatic Prefix Caching을 켜고 hit 비율을 측정합니다.
  4. 입력 길이를 줄이거나 RAG 문서 재정렬을 검토합니다.

13. 비교 실험 방법

옵션을 여러 개 동시에 바꾸면 무엇이 효과를 냈는지 알 수 없습니다. 다음 순서로 한 번에 하나씩 비교합니다.

  1. 기준 설정에서 짧은 입력·긴 입력·혼합 입력 부하를 저장합니다.
  2. TTFT, ITL, 전체 처리량, 최대 KV Cache 사용률을 기록합니다.
  3. max-model-len과 동시성 제한을 먼저 조정합니다.
  4. 반복 Prefix가 있다면 APC를 추가합니다.
  5. FP8 적용 후 같은 프롬프트의 품질과 성능을 다시 측정합니다.
  6. 최악 조건에서 30분 이상 지속 부하를 걸어 대기와 preemption을 확인합니다.

속도 지표의 의미와 측정 순서는 vLLM 속도 튜닝 가이드에서 TTFT·ITL·처리량 기준으로 정리했습니다.

14. 흔한 실수

실수 1. 모델의 최대 문맥 길이를 서비스 기본값으로 그대로 사용한다

지원 가능한 길이와 실제 필요한 길이는 다릅니다. 불필요하게 큰 길이는 캐시 용량 계획과 동시 처리량에 부담을 줍니다.

실수 2. 가중치가 INT4면 KV Cache도 자동으로 4비트라고 생각한다

가중치 형식과 KV Cache 형식은 별도입니다. 서버 시작 로그와 실행 옵션에서 캐시 dtype을 확인해야 합니다.

실수 3. Prefix Caching을 켜면 모든 요청이 빨라진다고 생각한다

앞부분 토큰이 실제로 같아야 재사용할 수 있습니다. 출력 Decode가 병목이면 효과가 제한적입니다.

실수 4. KV 사용률 하나만 본다

사용률이 높아도 처리량과 지연시간이 안정적일 수 있습니다. 대기 요청, preemption, TTFT, GPU 사용률을 함께 봐야 합니다.

실전 체크리스트

  • 모델의 레이어 수·KV Head 수·Head 차원을 확인했다.
  • 입력과 출력 토큰의 P50·P95·P99를 측정했다.
  • 서비스에 필요한 최대 문맥 길이를 별도로 정했다.
  • 동시 요청 수를 실제 길이 분포로 부하 테스트했다.
  • 반복 Prefix가 있는지와 Cache hit를 확인했다.
  • FP8 적용 전후의 품질과 처리량을 같은 요청으로 비교했다.
  • KV Cache 사용률·대기·preemption을 대시보드에 추가했다.

마치며

KV Cache는 LLM이 빠르게 다음 토큰을 만들도록 해주는 핵심 장치이면서, 긴 문맥과 높은 동시성을 제한하는 주요 메모리 항목입니다. 모델 가중치가 GPU에 들어갔다고 서버 용량 계획이 끝난 것이 아닙니다.

먼저 실제 요청 길이를 측정해 max-model-len과 동시성을 정하세요. 반복 Prefix가 많다면 APC로 Prefill을 줄이고, 캐시 용량이 병목이라면 FP8을 품질 검증과 함께 비교합니다. 마지막으로 KV 사용률뿐 아니라 대기 요청과 preemption, TTFT를 함께 보면 안정적인 운영 지점을 찾을 수 있습니다.

FAQ

Q1. KV Cache는 입력 토큰에만 필요한가요?

아닙니다. Prefill에서 입력 토큰의 캐시가 만들어지고, Decode에서 출력 토큰이 생성될 때마다 캐시가 추가됩니다. 입력과 현재까지 생성된 출력을 합친 전체 시퀀스 길이를 봐야 합니다.

Q2. FP8 KV Cache를 쓰면 정확히 메모리가 절반이 되나요?

텐서 원소의 이론적 크기는 2바이트에서 1바이트 수준으로 줄지만 스케일, 정렬, 블록 메타데이터와 다른 메모리 영역이 남습니다. 전체 GPU 메모리가 정확히 절반이 되는 것은 아닙니다.

Q3. Prefix Caching과 Prompt Caching은 같은 뜻인가요?

문맥에 따라 비슷하게 부르지만 vLLM의 Automatic Prefix Caching은 동일한 Prefix 토큰의 KV 블록을 서버 내부에서 재사용하는 기능입니다. API 제공자가 별도 과금·저장 정책으로 제공하는 Prompt Caching과 구현과 수명주기가 다를 수 있습니다.

Q4. block-size는 작을수록 좋은가요?

작은 블록은 마지막 부분의 낭비를 줄일 수 있지만 블록 관리와 커널 효율에 영향을 줄 수 있습니다. 지원 값과 backend가 다르므로 기본값을 출발점으로 실제 워크로드에서 비교해야 합니다.

참고 공식 문서

함께 읽으면 좋은 글

반응형