100,000단어짜리 프롬프트를 보내는 것이 짧은 프롬프트를 보내는 것보다 왜 훨씬 더 많은 비용이 드는가? 모델과 하드웨어가 똑같은데도 말이다.
이 질문에 대한 답의 핵심에는 KV 캐시(KV cache)라 불리는 작업 메모리 블록이 있다.
이 메모리는 모델이 응답을 생성하는 동안 쌓인다. 모델 가중치(weights)에 저장된 지식과는 별개이며, 입력의 모든 토큰에 대해 계산된 키(key)와 값(value) 벡터를 담고 있다. 캐시는 토큰이 늘어날수록 커지고, 긴 컨텍스트에서는 GPU에서 상당한 공간을 차지할 수 있다.
예를 들어, 700억 파라미터 모델이 128,000토큰 컨텍스트를 처리할 때 캐시 크기는 대략 40기가바이트에 달한다. 사용자를 한 명 추가할 때마다 커지는, 심각한 수준의 GPU 메모리다.
위 수치는 자연스러운 질문을 던진다. 애초에 왜 캐시가 존재하며, 왜 이런 방식으로 커지는가? 이 글에서는 LLM이 메모리를 어떻게 사용하는지, 어떻게 비용이 커지는지, 그리고 이를 어떻게 고치는지 살펴본다.
(이 글은 여러 공개된 자료를 근거로 작성되었습니다. 참고 자료는 글 말미에 있습니다. 부정확한 부분이 있다면 코멘트로 알려주세요.)
재계산(Recomputation)
토큰 하나를 생성하기 위해 모델이 하는 작업부터 살펴보자.
다음 단어를 고르기 위해 모델은 어텐션(attention) 단계를 수행하는데, 여기서 가장 최근 토큰이 그 이전에 나온 모든 토큰과 자신을 비교한다. 이 비교에는 이전 토큰마다 두 개의 벡터, 즉 키와 값이 쓰이는데, 이는 각 층(layer) 안에서 모델이 그 토큰에 대해 계산한 숫자 요약값일 뿐이다. 매 단계마다 이전의 모든 토큰에 대한 키와 값을 다시 계산하는 모델이라면, 입력이 길어질수록 토큰당 작업량이 계속 늘어날 것이다. 이런 반복은 순수한 낭비다. 일단 처리된 토큰의 키와 값은 그대로 유지되기 때문이다.
KV 캐시는 처음 계산된 키와 값 벡터를 저장해둠으로써 이 낭비를 없앤다. 다음 단계에서 모델은 새 토큰에 대한 키와 값만 계산하고, 나머지는 캐시에서 곧바로 읽어온다.
전체적으로 이는 훌륭한 해법이다. 그러나 캐싱은 속도 문제를 해결하는 대신 새로운 문제를 만들어낸다. 캐시는 이제 매 단계마다 읽혀야 하고, 이것이 바로 비용이 계속 증가하는 진짜 원인이 된다.
여기서 짚고 넘어갈 만한 점 하나는, 캐시가 원본 텍스트가 아니라 벡터를 저장한다는 것이다. 이는 모델 자체는 여유 있게 들어가는데도 알 수 없는 메모리 부족(out-of-memory) 에러가 발생하는 이유도 설명해준다.
디코딩(Decoding)
토큰 생성은 두 단계로 진행되며, 이 둘은 하드웨어에 각기 다른 방식으로 부담을 준다.
첫 단계는 프리필(prefill)로, 모델이 입력 전체를 한 번에 읽는 단계다. 입력 토큰 전부를 병렬로 처리하며 그들의 키와 값 벡터를 한 번의 패스로 캐시에 구축한다. 프리필은 GPU의 연산 유닛을 계속 바쁘게 만들기 때문에 연산 바운드(compute-bound)라고 부른다. 즉 한계가 칩이 산술 연산을 얼마나 빠르게 처리할 수 있는지에 달려 있다는 뜻이다.
두 번째 단계는 디코딩(decoding)으로, 모델이 출력을 한 번에 토큰 하나씩 생성하는 단계다. 새 토큰마다 캐시 전체를 상대로 어텐션 단계를 수행하는데, 이는 다음 토큰을 내놓기 전에 저장된 키와 값 전체를 GPU 메모리에서 읽어와야 한다는 뜻이다. 이 읽기 작업은 생성하는 토큰마다 반복된다. 여기서의 한계는 캐시가 메모리에서 연산 유닛으로 얼마나 빠르게 이동할 수 있는지에 달려 있어, 디코딩을 메모리 바운드(memory-bound)라고 부른다.
긴 컨텍스트 생성의 비용은 캐시를 유지하는 것보다는, 토큰마다 캐시 전체를 훑는 데서 더 크게 발생한다.
캐시가 클수록 토큰당 메모리 버스를 오가는 데이터가 많아지고, 이는 곧바로 생성 속도 저하와 비용 증가로 나타난다. 이는 또한 요청이 메모리에 충분히 여유 있게 들어가는데도 왜 느리게 실행될 수 있는지를 설명해준다.
비용이 매 단계에서 캐시를 얼마나 읽는지에 좌우된다면, 다음으로 명확히 이해해야 할 것은 캐시 자체의 크기다.
스케일링(Scaling)
캐시 크기는 몇 가지 숫자의 곱으로 결정된다.
- 2라는 계수는 키와 값을 각각 반영한다.
- 층(layer) 수 — 각 층이 자신만의 캐시를 유지하기 때문이다.
- 키-값 헤드(key-value head) 수 — 각 층이 몇 개의 세트를 저장하는지를 정한다.
- 헤드 차원(head dimension) — 각 벡터의 크기다.
- 숫자당 바이트 수 — 저장된 값 하나가 차지하는 공간이다.
- 토큰 수 — 컨텍스트 길이이며, 토큰마다 항목 하나가 대응한다.
- 배치 크기(batch size) — 한 번에 처리되는 요청 수다.
정리하면, 캐시 크기는 2 × 층 수 × 키-값 헤드 수 × 헤드 차원 × 숫자당 바이트 수 × 토큰 수 × 배치 크기와 같다.
여기서 짚어야 할 두 가지가 있다.
- 캐시는 토큰 수에 정비례해서 커진다. 즉 컨텍스트를 두 배로 늘리면 캐시도 두 배가 된다.
- 배치 크기에도 같은 방식으로 커진다. 즉 한 번에 더 많은 사용자를 처리하면 캐시도 그만큼 빠르게 커진다.
대략적인 예로, Llama 3 70B 모델은 80개 층, 8개의 키-값 헤드, 128의 헤드 차원을 가지며 숫자 하나를 2바이트로 저장한다. 단일 요청에서 128,000토큰 컨텍스트를 처리할 경우, 이 숫자들을 곱하면 대략 40기가바이트가 되는데, 이것이 바로 긴 요청 하나가 80기가바이트짜리 카드의 대부분을 혼자 채워버리는 이유다.
이제 LLM이 메모리 측면을 관리하도록 돕는 최적화 기법들을 살펴보자. 각 기법은 이 방정식의 특정 항목을 겨냥한다.
어텐션(Attention)
처음 두 기법은 어텐션 자체가 구축되는 방식을 바꾸는데, 이는 모델이 학습 시점부터 이 방식에 맞춰 만들어져야 한다는 뜻이다. 둘 다 토큰 하나가 캐시에서 차지하는 공간을 줄인다.
그룹 쿼리 어텐션(Grouped-query attention)은 키-값 헤드 수를 겨냥한다. 표준 어텐션 층에서는 모든 쿼리 헤드가 자신만의 키-값 헤드를 가지므로, 쿼리 헤드가 64개인 모델은 64세트의 키와 값을 저장한다. 그룹 쿼리 어텐션은 여러 쿼리 헤드가 하나의 키-값 헤드를 공유하게 해, 저장되는 세트 수를 크게 줄인다. 예를 들어 70B급 Llama 2·3과 Mistral 7B는 8개의 키-값 헤드까지 공유해, 전체 멀티헤드 어텐션 대비 약 8배까지 캐시를 줄인다. 이 덕분에 최근의 70B 모델이 오히려 예전의 7B 모델보다 더 작은 캐시를 가질 수 있다.
더 공격적인 버전으로 멀티 쿼리 어텐션(multi-query attention)이 있는데, 여기서는 모든 쿼리 헤드가 단 하나의 키-값 헤드를 공유한다. 이는 헤드 공유 방식 중 메모리를 가장 많이 절약한다. 하지만 이 정도까지 밀어붙이면 품질이 떨어지는 경향이 있고 학습도 불안정해지기 때문에, 대부분의 설정은 절충안인 그룹 쿼리 방식에 정착한다.
두 번째 공략법은 헤드는 그대로 유지하되 각 헤드가 저장하는 내용을 압축하는 것이다.
DeepSeek 모델에서 도입된 멀티헤드 잠재 어텐션(multi-head latent attention)은 키와 값을 캐싱하기 전에 더 작은 잠재 표현(latent representation)으로 투영(project)한 뒤, 읽을 때 다시 원래 크기로 확장한다. 절감 효과는 크다. DeepSeek-V3는 토큰당 약 70킬로바이트를 유지하는데, 비교 가능한 그룹 쿼리 모델은 192에서 328킬로바이트 사이를 유지한다. 대가는 서빙(serving) 단계에서 발생한다. 압축은 읽을 때마다 추가 작업을 요구하고 일부 표준 어텐션 구현과 잘 맞지 않기 때문에, 모델과 컨텍스트가 충분히 커져서 캐시 트래픽이 지배적인 비용이 될 때 가장 효과를 낸다.
앞서 언급했듯, 헤드 공유와 잠재 어텐션 모두 아키텍처에 대한 제어권을 필요로 하므로, 이는 모델을 선택하거나 학습시킬 때 도움이 된다. 다음에 살펴볼 공략법들은 우리가 이미 손에 쥔 모델에 적용하는 방식이다.
양자화(Quantization)
양자화는 숫자당 바이트 수를 겨냥한다.
키와 값은 보통 각각 16비트로 저장되는데, 양자화는 이를 8비트나 4비트 같은 더 작은 형식으로 반올림한다. 숫자당 바이트 수 항목이 방정식에 그대로 들어 있으므로, 16비트에서 8비트로 옮기면 전체 캐시가 절반이 되고, 4비트로 가면 다시 절반이 된다. 매력적인 점은 이 방식이 이미 갖고 있는 모델에 그대로 적용되며 재학습이 전혀 필요 없다는 것이다.
품질 저하 정도는 얼마나 밀어붙이는지에 달려 있다.
8비트 저장은 대개 정확도 손실이 1% 미만으로, 대부분의 워크로드에서 오차 범위 안에 들어간다. 4비트 저장은 더 많이 절감하지만, 모델이 긴 컨텍스트에서 여러 개의 구체적인 사실을 끄집어내야 하는 다중 니들 검색(multi-needle retrieval) 같은 까다로운 작업에서 측정 가능한 손실을 보이기 시작한다.
캐시 안의 일부 숫자는 다른 숫자보다 더 높은 정밀도가 필요하기 때문에, 특화된 방법들이 단순 반올림보다 낫다. 다만 단순 반올림도 이미 대부분의 이득을 챙긴다.
축출(Eviction)
축출은 토큰 수를 겨냥해, 모델이 더 이상 필요로 하지 않을 것 같은 항목을 버린다.
일반적인 접근법은 가장 최근 토큰들의 창(window)을 유지하는 것이다. 최근 컨텍스트가 대개 가장 중요하기 때문이다. 여기에 시퀀스 맨 처음의 토큰 몇 개도 함께 유지한다. 이 첫머리 토큰들은 알고 보면 의외로 큰 역할을 한다. 실제 내용과 무관하게 상당한 양의 어텐션을 흡수하며, 모델의 출력을 안정적으로 유지시키는 앵커(anchor) 역할을 한다.
아래 다이어그램을 보라.
축출의 문제는 구조적이다.
어떤 토큰이 중요한지는 아직 도착하지 않은 질문에 달려 있다. 지금 버린 토큰이 나중에 생성 과정에서 정확히 필요해질 수도 있는데, 일단 사라지고 나면 모델은 마치 그 토큰이 처음부터 없었던 것처럼 생성을 이어간다. 이는 검색(retrieval) 작업에서 드러난다. 공격적으로 다듬어진 캐시는 가벼운 대화는 잘 처리하지만, 긴 문서 중간에 묻힌 사실은 놓치게 된다.
더 정교한 방식은 각 토큰의 중요도를 점수로 매겨 어떤 것을 안전하게 버릴 수 있는지 예측하려 한다. 이는 도움이 되지만, 근본적인 문제는 그대로 남는다.
서빙(Serving)
캐시의 내용 자체가 고정되어 있더라도, 서빙 시스템이 메모리를 관리하는 방식에는 아직 개선할 여지가 많이 남아 있으며, 두 가지 기법이 도움을 준다.
첫 번째는 페이지드 어텐션(paged attention)이다.
기존 서빙 시스템은 요청당 하나의 큰 연속된 블록을 예약했고, 그 크기는 가능한 최장 출력에 맞춰졌다. 대부분의 요청은 그보다 훨씬 짧게 끝났기 때문에, 예약된 공간이 놀고 있었고 단편화(fragmentation)가 누적됐다. 하지만 페이지드 어텐션은 운영체제에서 아이디어를 빌려온다. 메모리를 작은 고정 크기 페이지로 나누고 필요할 때마다 나눠주는 방식이다. 캐시는 어디에나 위치할 수 있는 작은 블록들로 쪼개지고, 각 요청을 자신의 블록에 매핑하는 조회 테이블(lookup table)이 이를 추적한다. 결과적으로 단편화로 캐시 메모리의 60~80%를 낭비하던 시스템이 그 수치를 4% 이하로 떨어뜨렸고, 처리량(throughput)은 같은 데이터를 더 촘촘히 채워 넣은 것만으로 2~3배 증가했다.
두 번째 기법은 첫 번째 기법에서 파생된다. 캐시가 공유 가능한 블록에 존재하므로, 같은 텍스트로 시작하는 두 요청은 각자 자신만의 개인적인 이어짐(continuation)을 유지하면서도 같은 물리 블록을 가리킬 수 있다. 이것이 프리픽스 캐싱(prefix caching)의 토대이며, 주요 API들이 프롬프트 캐싱(prompt caching)이라 부르는 상품화된 버전이다.
이는 프리픽스를 반복하는 모든 워크로드에서 큰 이득을 준다. 예를 들어 호출마다 수천 토큰짜리 동일한 시스템 프롬프트를 보내는 에이전트가 그렇다. OpenAI와 Anthropic 모두 캐시 히트(cache hit) 시 비용과 지연 시간이 50~90% 줄어든다고 보고하며, 캐시된 토큰은 새로 계산된 토큰보다 훨씬 저렴하게 과금된다.
다만 한 가지 짚어둘 점은, 캐시된 상태를 사용자 간에 공유하는 것이 타이밍 사이드채널(timing side-channel)을 열어 다른 사람의 프롬프트에 관한 정보가 새어나갈 수 있다는 것이다. 이는 현재 진행 중인 우려 사항이며 여기서는 다루지 않는다.
트레이드오프(Tradeoffs)
지금까지 살펴본 기법들은 얼마나 많은 메모리를 절약하는지는 비슷해 보이지만, 그 대가로 무엇을 요구하는지는 크게 다르다.
일부는 거의 공짜에 가깝다. 예를 들면:
- 그룹 쿼리 어텐션은 품질 손실이 매우 적어 안전한 기본값이 되었으며, 이것이 현재 거의 모든 모델이 이를 채택하는 이유다.
- 페이지드 어텐션과 프리픽스 캐싱은 캐시가 담고 있는 내용이 아니라 저장·공유되는 방식을 바꾸는 것이므로 품질에 거의 영향을 주지 않는다.
다른 기법들은 더 많은 것을 요구한다. 예를 들면:
- 양자화는 8비트에서는 저렴하지만 4비트 이하로 갈수록 위험해지므로, 적절한 설정은 작업이 얼마나 민감한지에 달려 있다.
- 잠재 어텐션은 아키텍처 선택지 대부분을 아낄 수 있지만 제대로 서빙하려면 실질적인 엔지니어링 노력을 요구한다.
- 축출은 많은 메모리를 확보해주지만 이후 생성 과정에서 필요한 정보를 잃어버릴 수도 있어, 깔끔한 승리라기보다는 진짜 도박에 가깝다.
짧은 컨텍스트에서는 캐시가 작아, 이 기법들 대부분이 아직 나타나지도 않은 문제를 해결하는 셈이 된다. 이들은 긴 컨텍스트와 높은 동시성(concurrency)에서 자신의 자리를 찾는데, 그곳에서 캐시가 지배적인 비용으로 성장하기 때문이다. 어떤 조합을 선택할지는 워크로드에 따라 달라지며, 에이전트 루프는 재사용에 기대고 긴 문서 검색은 축출을 피하는 쪽으로 기운다.
결론
긴 컨텍스트 추론의 비용은 하나의 캐시로 귀결되며, 그 크기는 하나의 짧은 방정식으로 정해진다.
디코딩이 매 토큰마다 캐시 전체를 읽기 때문에, 캐시는 저장 비용인 동시에 대역폭(bandwidth) 비용이며, 이것이 캐시를 줄이면 속도가 빨라지는 이유다. 여기서 다룬 모든 최적화는 이 전체 방정식의 특정 항목을 다루거나 그 주변의 낭비를 다듬는다.
- 그룹 쿼리와 잠재 어텐션은 토큰 하나당 드는 비용을 줄인다.
- 양자화는 숫자 하나를 더 적은 비트로 저장한다.
- 축출은 유지하는 토큰 수를 줄인다.
- 페이지드 어텐션과 프리픽스 캐싱은 캐시를 더 효율적으로 관리하고 공유한다.
핵심 요약 (20줄)
- 100,000단어짜리 긴 프롬프트가 짧은 프롬프트보다 훨씬 비싼 이유는 KV 캐시(key-value cache)라는 작업 메모리 때문이다.
- KV 캐시는 모델 가중치와 별개로, 응답 생성 중 모든 입력 토큰의 키·값 벡터를 저장하며 토큰이 늘수록 GPU 메모리를 계속 잡아먹는다.
- 700억 파라미터 모델이 128,000토큰 컨텍스트를 처리하면 캐시만 약 40GB — 사용자 한 명 추가할 때마다 늘어나는 부담이다.
- 캐시가 없으면 매 단계 이전 토큰 전체의 키·값을 다시 계산해야 하는 순수 낭비가 발생하므로, KV 캐시는 이를 저장해 재계산을 없앤다.
- 문제는 캐시를 "유지"하는 게 아니라 매 생성 단계마다 캐시 전체를 "읽어야" 한다는 것 — 이것이 비용 증가의 진짜 원인이다.
- 생성은 두 단계로 나뉜다 — 입력을 병렬로 한 번에 처리하는 연산 바운드(compute-bound) 프리필(prefill), 토큰 하나씩 캐시 전체를 읽으며 생성하는 메모리 바운드(memory-bound) 디코딩(decoding).
- 캐시 크기 = 2 × 층 수 × 키-값 헤드 수 × 헤드 차원 × 숫자당 바이트 × 토큰 수 × 배치 크기 — 컨텍스트 길이와 배치 크기 모두에 정비례해 커진다.
- Llama 3 70B(80층, 8개 키-값 헤드) 기준 128,000토큰 요청 하나가 약 40GB로, 80GB급 GPU 카드를 거의 혼자 채운다.
- 그룹 쿼리 어텐션(GQA)은 여러 쿼리 헤드가 키-값 헤드를 공유하게 해 캐시를 최대 8배까지 줄이며, 품질 손실이 적어 사실상 업계 표준이 됐다.
- 멀티 쿼리 어텐션(모든 헤드가 키-값 헤드 1개 공유)은 가장 적게 캐시를 쓰지만 품질 저하와 학습 불안정 문제로 잘 쓰이지 않는다.
- DeepSeek의 멀티헤드 잠재 어텐션(MLA)은 키·값을 더 작은 잠재 표현으로 압축해, DeepSeek-V3는 토큰당 약 70KB(경쟁 GQA 모델 192~328KB)까지 줄이지만 서빙 시 추가 연산 부담이 있다.
- 양자화는 키·값을 16비트에서 8비트·4비트로 반올림해 캐시를 절반씩 줄이며, 재학습이 필요 없다 — 8비트는 정확도 손실 1% 미만이지만 4비트는 다중 사실 검색 같은 작업에서 손실이 커진다.
- 축출(eviction)은 최근 토큰 창 + 시퀀스 맨 앞 앵커(anchor) 토큰만 남기고 나머지를 버리는 방식으로 토큰 수 자체를 줄인다.
- 축출의 근본 문제는 어떤 토큰이 나중에 필요할지 미리 알 수 없다는 것 — 긴 문서 중간에 묻힌 사실을 놓치는 식으로 정확도를 해칠 수 있다.
- 페이지드 어텐션(paged attention)은 OS의 페이징 개념을 캐시에 적용해, 단편화로 60~80% 낭비되던 메모리를 4% 이하로 줄이고 처리량을 2~3배 높였다.
- 프리픽스 캐싱(=API의 프롬프트 캐싱)은 같은 프리픽스로 시작하는 요청들이 같은 물리 블록을 공유하게 해, OpenAI·Anthropic 모두 캐시 히트 시 비용·지연 50~90% 절감을 보고한다.
- 다만 캐시 공유는 타이밍 사이드채널로 다른 사용자의 프롬프트 정보가 새어나갈 위험도 함께 열어둔다.
- 그룹 쿼리 어텐션·페이지드 어텐션·프리픽스 캐싱은 품질 손실이 거의 없는 "거의 공짜" 최적화인 반면, 양자화·잠재 어텐션·축출은 더 많은 엔지니어링 노력이나 실질적인 트레이드오프를 요구한다.
- 짧은 컨텍스트에서는 캐시가 작아 이런 기법들이 아직 나타나지도 않은 문제를 푸는 셈이며, 긴 컨텍스트·높은 동시성에서 캐시가 지배적 비용이 될 때 진짜 효과를 낸다.
- 결국 캐시는 저장 비용이자 대역폭 비용이며, 각 최적화는 그룹 쿼리·잠재 어텐션(토큰당 비용 절감), 양자화(비트 절감), 축출(토큰 수 절감), 페이지드/프리픽스 캐싱(관리·공유 효율화)처럼 방정식의 서로 다른 항목을 겨냥한다.