KV Cache가 LLM 서빙을 바꾸는 방식
LLM

KV Cache가 LLM 서빙을 바꾸는 방식

2026.07.07.
LLM Serving9
  1. 1Prefill과 Decode로 이해하는 LLM 추론 과정
  2. 2KV Cache가 LLM 서빙을 바꾸는 방식읽는 중
  3. 3vLLM의 핵심 원리 PagedAttention 파헤치기
  4. 4Continuous Batching과 Chunked Prefill 완전 이해
  5. 5Prefix Caching과 RadixAttention으로 보는 vLLM과 SGLang
1 / 2

LLM은 토큰을 하나씩 생성합니다. 이 decode 단계는 계산보다 메모리 대역폭에 먼저 묶이기 때문에, 여러 요청을 배치로 묶어 한 번에 처리하는 것이 처리량을 끌어올리는 핵심입니다. 그런데 여기에는 조건이 하나 붙습니다. 배치를 키우려면 요청마다 KV Cache가 GPU에 살아 있어야 합니다.

KV Cache는 흔히 “추론 속도를 올리는 캐시” 정도로 소개됩니다. 하지만 실제로는 그 이상의 역할을 합니다. 진행 중인 모든 요청이 각자의 KV Cache를 메모리에 들고 있어야 하고, 이 캐시들을 어떻게 다루느냐가 서빙 시스템 설계의 중심이 되기 때문입니다. 이 글에서는 KV Cache가 정확히 무슨 일을 하는지, 그리고 왜 이것이 서빙 시스템의 중심에 놓이는지 살펴봅니다.


KV Cache가 하는 일

먼저 KV Cache가 없다고 생각해봅시다. decode 스텝마다 모델은 지금까지의 토큰 전체를 입력으로 받아 다음 토큰을 예측합니다. 이때 attention은 현재 토큰의 Query 벡터를 앞선 모든 토큰의 Key 벡터와 곱하고, 그 결과로 Value 벡터들을 가중합해서 다음 토큰의 실마리를 만듭니다.

여기서 결정적인 사실이 하나 있습니다. 각 토큰의 Key와 Value는 한 번 계산되면 변하지 않습니다. 3번째 토큰의 K, V는 4번째 토큰을 만들 때도, 100번째 토큰을 만들 때도 똑같습니다. 그런데 캐시가 없으면 매 스텝마다 이 K, V들을 처음부터 다시 계산해야 합니다.

캐시 없이: 앞부분을 매번 다시 계산 스텝 1 K1 V1 스텝 2 K1 V1 K2 V2 스텝 3 K1 V1 K2 V2 K3 V3 새로 계산 다시 계산 (낭비)

토큰이 t개인 스텝에서 K, V를 t개 계산하고, 이걸 생성 길이만큼 반복하니 전체가 O(n²) 입니다. 그중 대부분이 이미 했던 계산의 반복이죠.

KV Cache는 이 낭비를 없앱니다. 각 토큰의 K, V를 처음 계산할 때 저장해두고, 다음 스텝부터는 새 토큰 하나의 K, V만 계산해 캐시에 덧붙입니다. attention은 저장된 캐시를 읽기만 하면 됩니다.

KV Cache: 앞부분은 캐시에서 읽기 스텝 1 K1 V1 스텝 2 K1 V1 K2 V2 스텝 3 K1 V1 K2 V2 K3 V3 새로 계산 캐시에서 읽기

이렇게 하면 스텝당 계산량이 새 토큰 하나로 고정되어 전체가 O(n) 이 됩니다. 이 차이가 있어야 자기회귀 생성이 현실적인 속도로 돌아갑니다. KV Cache가 없으면 긴 텍스트 생성은 계산량이 제곱으로 불어나 사실상 불가능합니다.

그리고 여기서 KV Cache의 진짜 정체가 드러납니다. KV Cache는 그 요청의 “상태(state)“입니다. 생성이 이어지는 한 계속 GPU에 들고 있어야 하는, 그 요청이 지금까지 쌓아온 진행 상태인 셈입니다.


요청이 끝날 때까지 자리를 차지한다

KV Cache가 요청의 상태라는 게 왜 중요한지는, 익숙한 서버와 비교해보면 분명해집니다.

흔한 이미지 분류 서버를 떠올려봅시다. 요청이 들어오면 계산 한 번 하고 결과를 돌려주면 끝입니다. 처리가 끝난 요청은 메모리에서 깨끗이 사라지고, 서버는 곧바로 다음 요청을 받습니다. 요청과 요청 사이에 서버가 붙들고 있어야 할 것이 없습니다.

LLM은 그렇지 않습니다. 한 요청이 100 토큰을 생성한다면, 그 100번의 스텝 내내 그 요청의 KV Cache가 GPU에 자리를 잡고 앉아 있어야 합니다. 토큰이 하나 늘 때마다 캐시도 조금씩 자라고요. 요청 하나가 응답을 마칠 때까지 GPU 메모리 한 자리를 계속 차지하고 앉아 있는 셈입니다.

그리고 서버는 이런 요청을 한 번에 수십 개씩 돌립니다. 그러면 GPU 안에서는 진행 중인 요청 수십 개의 캐시가 동시에 자리를 차지한 채 저마다 자라나고 있습니다.

여기서 배칭과 맞물립니다. decode가 memory-bound인 이상 처리량을 올리려면 배치를 키워야 하는데, 배치를 키운다는 건 결국 이 캐시들을 GPU에 더 많이 올린다는 뜻입니다. 커지고 길이도 제각각인 이 캐시들을 한정된 메모리에 욱여넣다가, 자리가 다 차면 더는 새 요청을 받지 못하고 뒤에 온 요청은 기다립니다.

핵심

LLM 서빙 시스템 설계의 상당 부분이 “KV Cache를 어떻게 관리하느냐”로 귀결됩니다. 앞으로 다룰 PagedAttention, continuous batching, prefix caching이 전부 이 하나의 자원을 둘러싸고 도는 이유가 여기에 있습니다.


그 대가는 메모리다

이 캐시들을 메모리에 담아내는 게 서빙의 과제라면, 자연히 따라오는 질문이 있습니다. 이 캐시는 실제로 메모리를 얼마나 쓸까요?

토큰마다 모든 레이어에 걸쳐 K, V 벡터가 쌓이니, 요청 하나의 캐시는 시퀀스가 길어질수록 커집니다. 정확한 크기는 모델 구조에 따라 다르지만, 요즘 모델에서 흔한 수천 토큰짜리 요청 하나는 대략 수백 MB에서 1~2 GiB 정도입니다. 요청 하나만 보면 큰 부담은 아닙니다.

문제는 이게 요청마다 쌓이고, 가중치를 뺀 나머지 공간을 모든 요청이 나눠 쓴다는 점입니다. 가중치는 모델을 올리는 순간 정해지는 고정분이고, 그 나머지가 KV Cache의 몫입니다. 동시 요청이 늘거나 컨텍스트가 길어지면 이 공간을 금세 채우고, 다 차면 새 요청은 기다리거나 밀려납니다(preemption). 배치가 무한정 커지지 못하는 이유가 이것입니다.

그래서 요즘 모델들은 이 한정된 공간을 덜 잡아먹도록, KV Cache를 애초에 작게 유지하는 방향으로 설계됩니다. Gemma를 포함한 최신 오픈 모델이 기본으로 다는 GQA나 슬라이딩 윈도우 어텐션이 그런 장치입니다.


마치며

KV Cache는 단순한 속도 최적화가 아닙니다. 요청이 응답을 마칠 때까지 GPU에 자리를 차지하게 만들고, 진행 중인 요청 전부의 캐시를 동시에 들고 있어야 하는 부담을 서버에 지웁니다. 그래서 서빙 시스템 설계의 상당 부분이 이 한정된 공간을 얼마나 알뜰하게 쓰느냐를 두고 씨름합니다.

그런데 vLLM 이전의 서빙 시스템들은 정반대였습니다. 이 귀한 공간의 60~80%를 단편화로 흘려버리고 있었습니다. 다음 글에서는 그 낭비가 어디서 왔는지, 그리고 vLLM의 PagedAttention이 운영체제의 가상 메모리에서 아이디어를 빌려 이 문제를 어떻게 풀었는지 살펴봅니다.


참고자료

Thank You for Visiting My Blog, Have a Good Day 😆