TT Lab
요금제

한국어

시작하기
블로그

블로그

KV 캐시는 왜 필요한가: 다시 계산하지 않으려고 기억해 두는 것

상태: 초안 (2026-10-13), 일부 실측. 장난감 모델 측정(3절)은 필자가 노트북 CPU 에서 직접 잰 값입니다. GPU 서버 측정(4·6절)은 같은 사이트의 「토스 ML/AI Infra 준비」 시리즈에서 8GB급 GPU 한 장으로 잰 값을 다시 인용했습니다. 모델 구성은 공식 config 와 논문 표에서 확인했고(2026-10-13 열람), KV 크기는 그 값으로 계산했습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

목차

1. 한 토큰씩 만드는 모델

챗봇의 답은 한 번에 나오지 않습니다. 모델은 지금까지의 글(질문 + 지금까지 만든 답)을 보고 다음 토큰 하나를 고르고, 그 토큰을 글 끝에 붙여 다시 다음 토큰을 고릅니다. 이것을 자기회귀 디코딩이라고 합니다.

토큰 하나를 고를 때 트랜스포머의 각 층은 어텐션을 계산합니다. 토큰마다 벡터 세 개를 만듭니다.

새 토큰의 Q 를 앞 토큰들의 K 와 하나씩 견주어 점수를 내고(얼마나 관련 있나), 그 점수로 V 들을 섞습니다. 식으로 쓰면 softmax(q·Kᵀ/√d)·V 입니다.

여기서 중요한 성질이 하나 있습니다. 언어 모델의 어텐션은 뒤를 보지 못하게 가려져(causal mask) 있습니다. 3번째 토큰의 K·V 는 1~3번째 토큰만 보고 만들어지므로, 뒤에 토큰이 100개 붙어도 바뀌지 않습니다. 그렇다면 한 번 만든 K·V 를 버리고 다시 만들 이유가 없습니다. 저장해 두고 다음 걸음에서 꺼내 쓰면 됩니다. 이것이 KV 캐시입니다.

Q 는 저장하지 않습니다. 앞 토큰의 Q 는 그 토큰을 만들 때 한 번 쓰고 나면 다시 쓸 일이 없기 때문입니다. 새 걸음에 필요한 Q 는 새 토큰의 것 하나뿐입니다.

2. 캐시가 없으면 걸음마다 처음부터

토큰 5개를 만드는 동안 계산하는 K·V 캐시 없음 — 걸음마다 처음부터 다시 걸음 1 걸음 2 걸음 3 걸음 4 걸음 5 앞 토큰의 K·V 를 버리고 매번 전부 다시 만든다 계산 1+2+3+4+5 = 15칸 n 걸음이면 약 n²/2 캐시 있음 — 새 토큰만 계산, 지난 것은 읽기 걸음 1 걸음 2 걸음 3 걸음 4 걸음 5 새 토큰의 K·V 하나만 만들어 캐시에 붙이고, 어텐션은 전부 본다 계산 5칸, 읽기 10칸 n 걸음이면 계산 n칸 새로 계산한 K·V 캐시에서 읽은 K·V 한 칸 = 한 토큰의 K·V(모든 층). 어텐션 점수 계산은 두 방식 모두 지난 토큰 수만큼 든다.
그림: 캐시 없음과 있음의 디코딩 비교

캐시가 없으면 t번째 걸음에서 t개 토큰 전부의 K·V 를 다시 만듭니다. 층마다 하는 선형 투영(Q·K·V·출력)과 MLP 는 토큰 수에 비례하므로, 한 걸음의 비용이 t 에 비례하고 n 걸음을 모으면 1+2+…+n ≈ n²/2 입니다. 캐시가 있으면 걸음마다 새 토큰 하나만 투영하고 MLP 에 넣으므로 n 걸음에 n 입니다.

어텐션 점수 계산은 두 방식 모두 남습니다. 캐시가 있어도 새 토큰의 Q 는 앞의 모든 K 와 견주어야 하므로 한 걸음에 t 만큼 듭니다(n 걸음 합 n²/2). 캐시가 없으면 이것마저 걸음마다 t² 이라 합이 n³/3 으로 커집니다. 문맥이 아주 길지 않은 동안은 투영과 MLP 가 계산의 대부분이라, 실제로 체감하는 차이는 「걸음마다 t 대 1」입니다.

3. 직접 해 보기: numpy 장난감 트랜스포머

말로는 쉬운데 체감이 어려워서, 가중치를 난수로 채운 작은 트랜스포머를 numpy 로 만들어 두 방식을 재 보았습니다. 학습한 모델이 아니므로 출력 토큰에 뜻은 없지만, 계산의 모양(투영·어텐션·MLP·정규화)은 진짜와 같습니다.

캐시가 있는 쪽의 핵심은 몇 줄입니다. 이번 토큰의 K·V 를 미리 잡아 둔 배열 끝에 써 넣고, 어텐션은 지금까지 쌓인 전부를 봅니다(헤드별로 나누는 reshape 는 줄였습니다).

k = mm(h, W["k"])  # 새 K
v = mm(h, W["v"])  # 새 V
n = cache["n"]
cache["k"][:, n:n+T] = k  # 끝에 붙임
cache["v"][:, n:n+T] = v
K = cache["k"][:, :n+T]  # 지난 것 전부
V = cache["v"][:, :n+T]
a = attend(q, K, V)

결과 1. 출력은 같고, 연산량과 시간은 크게 다르다

연산량(GFLOP, 코드가 센 값):

전체 길이 캐시 없음 캐시 있음 배율
128 6.71 0.121 55배
256 31.0 0.264 118배
512 148.2 0.599 247배

시간(초, 세 번 중앙값):

전체 길이 캐시 없음 캐시 있음 배율
128 0.205 0.084 2.5배
256 1.16 0.131 8.9배
512 8.01 0.485 16.5배
한 토큰을 만드는 데 걸린 시간(ms) numpy 장난감 모델 · 노트북 CPU · 실측(64토큰 구간 중앙값) 0 10 20 30 40 0 128 256 384 512 지금까지의 문맥 길이(토큰) 캐시 없음 35ms 캐시 있음 0.4→0.6ms 캐시 없음(실선·점) 캐시 있음(점선) 512토큰 전체: 없음 8.0초, 있음 0.49초 · 연산량 247배 차이
그림: 생성 위치별 한 걸음 시간

결과 2. 캐시가 없으면 한 걸음이 문맥에 비례해 느려진다. 512토큰 실행에서 64토큰 구간마다 한 걸음 시간의 중앙값을 보면, 캐시 없는 쪽은 1.25ms 에서 35.1ms 로 늘었고 캐시 있는 쪽은 0.43ms 에서 0.60ms 로 조금만 늘었습니다. 캐시가 있어도 조금씩 느는 것은 2절에서 말한 어텐션 점수 계산(한 걸음에 t) 때문입니다.

결과 3. 캐시 배열의 크기는 공식과 바이트까지 같다. 1,024토큰을 프리필한 뒤 캐시 배열의 nbytes 를 더했습니다.

쿼리 헤드는 셋 다 8개이고, 공식 값은 2 × 층(2) × KV 헤드 × 헤드 차원(16) × 토큰(1,024) × 4바이트입니다.

방식 KV 헤드 실제 바이트 공식 값
MHA 8 2,097,152 2,097,152
GQA 2 524,288 524,288
MQA 1 262,144 262,144

세 방식 모두 캐시를 쓴 마지막 로짓이 캐시 없이 처음부터 계산한 값과 같았습니다(허용 오차 10⁻³).

4. 캐시의 값: 메모리

토큰 하나가 남기는 KV 의 크기 2 × 층 × KV 헤드 × 헤드 차원 × 바이트 K 와 V 두 벌 · 이 값에 토큰 수를 곱하면 한 요청의 KV MHA GQA MQA MLA 헤드마다 KV 그룹마다 KV KV 하나 공유 압축해 저장 8칸 (기준) KV 헤드 = 쿼리 헤드 2칸 (1/4) 헤드 4개가 한 벌 공유 1칸 (1/8) 모든 헤드가 한 벌 잠재 벡터 하나 쓸 때 K·V 로 펼침 쿼리 헤드(저장 안 함) 캐시에 남는 K·V Llama 3.1 70B(80층·헤드 차원 128·bf16): GQA 8 → 320KiB/토큰 KV 헤드가 쿼리 헤드만큼(64) 있었다면 2.5MiB/토큰 (계산)
그림: KV 메모리 공식과 MHA·GQA·MQA·MLA

캐시는 계산을 메모리로 바꾼 것입니다. 토큰 하나가 남기는 KV 는 층마다 K 와 V 한 벌씩이므로 다음과 같습니다.

KV 바이트/토큰 = 2 × 층 수 × KV 헤드 수 × 헤드 차원 × 자료형 바이트

이 식이 실제 서버와 맞는지는 다른 시리즈에서 확인했습니다. Qwen3-1.7B(28층, KV 헤드 8, 헤드 차원 128, bf16)는 식으로 토큰당 114,688바이트이고, vLLM 서버 로그의 KV 캐시 메모리(2.36GiB)를 토큰 수(22,048)로 나눈 값이 약 114,900바이트로 0.2% 안에서 맞았습니다(8GB급 GPU, vLLM 0.31.0, 실측). KV 를 8비트(fp8)로 두면 같은 메모리에 41,920토큰이 들어가 정확히 두 배 가까이 되었습니다.

잘 알려진 공개 모델에 넣어 보면 다음과 같습니다. 층·헤드 수는 공식 자료에서 확인했고, 크기는 KV 를 bf16(2바이트)으로 둔다고 보고 계산했습니다. 아래 모델은 헤드 차원이 모두 128입니다. 「32K」는 32,768토큰 요청 하나의 KV 입니다.

모델 층 × KV 헤드 토큰당 KV 32K
Qwen2.5-1.5B 28 × 2 28KiB 0.88GiB
Qwen2.5-7B 28 × 4 56KiB 1.75GiB
Llama 3.1 8B 32 × 8 128KiB 4GiB
Mistral-7B 32 × 8 128KiB 4GiB
Qwen3-8B 36 × 8 144KiB 4.5GiB
Llama 3.1 70B 80 × 8 320KiB 10GiB
Llama 3.1 405B 126 × 8 504KiB 15.75GiB
70B 가 MHA 였다면(가정) 80 × 64 2.5MiB 80GiB
DeepSeek-V2(MLA) 60층, 잠재 576 67.5KiB 2.11GiB

쿼리 헤드 수는 Qwen2.5-1.5B 12, Qwen2.5-7B 28, Llama 3.1 8B·Mistral-7B·Qwen3-8B 32, 70B 64, 405B 128 입니다. KV 헤드가 쿼리 헤드보다 적은 것이 5절의 GQA 입니다.

5. 크기를 줄이는 방법: MHA → GQA → MQA → MLA

식에서 줄일 수 있는 것은 KV 헤드 수(또는 그것을 대신하는 무엇)와 바이트입니다. 층 수와 헤드 차원은 모델의 능력과 직결되어 함부로 줄이지 못합니다.

왜 이렇게까지 줄이려 할까요. 6절이 그 답입니다.

6. 프리필과 디코드: 성격이 다른 두 단계

요청 하나는 두 단계를 지납니다.

  1. 프리필(prefill): 프롬프트 전체를 한 번에 넣어 모든 토큰의 K·V 를 만들고 캐시에 씁니다. 첫 토큰이 여기서 나옵니다. 첫 토큰까지의 시간(TTFT)을 정합니다.
  2. 디코드(decode): 그 뒤로 한 토큰씩 만듭니다. 토큰 사이 간격(ITL·TPOT)을 정합니다.

둘은 같은 모델을 쓰지만 하드웨어에서 보이는 모습이 다릅니다.

장난감 모델에서도 차이가 보였습니다(캐시 있음, 세 번 중앙값).

프롬프트 프리필 토큰당 디코드 한 걸음 배율
64 0.018ms 0.445ms 24배
256 0.034ms 0.506ms 15배
1,024 0.144ms 0.852ms 6배

프리필 전체 시간은 64토큰 1.16ms, 256토큰 8.76ms, 1,024토큰 146.9ms 였습니다.

토큰 하나를 디코드하는 데 프리필의 토큰당 시간보다 6~24배가 들었습니다. CPU 의 numpy 라 파이썬 호출 비용이 섞여 있고, 1,024토큰 프리필은 어텐션의 제곱 항이 커져 토큰당 비용이 올라갑니다. 그래서 배율 자체보다 「한 번에 몰아 하는 쪽이 토큰당 훨씬 싸다」는 방향을 보시면 됩니다.

GPU 에서는 이 차이가 더 큽니다. 다른 시리즈의 실측(8GB급 GPU, Qwen3-1.7B, vLLM)에서 입력 처리 속도는 약 8,200토큰/초였고, 요청 하나를 디코드할 때는 토큰 간격이 15ms(약 66토큰/초)였습니다. 토큰당 120배 넘게 차이가 납니다.

디코드가 메모리에 묶여 있다면 처리량을 올리는 길은 하나입니다. 가중치를 한 번 읽을 때 여러 요청을 같이 디코드하는 것, 즉 배치를 키우는 것입니다. 같은 실측에서 동시 요청을 1에서 8로 늘리자 처리량이 66에서 383토큰/초(5.8배), 32에서 831토큰/초로 늘었습니다. 그런데 배치에 든 요청마다 자기 KV 가 GPU 메모리에 있어야 합니다. 입력 512 + 출력 128 토큰이면 요청 하나가 640토큰이고, 그 GPU 의 KV 자리(22,048토큰)에는 약 34개가 들어갑니다. 동시 64개를 보내자 처리량은 늘지 않고 첫 토큰 시간이 5.7초로 뛰었습니다. 처리량의 상한을 KV 메모리가 정한 것입니다.

7. 정리: 이 시리즈가 따라갈 질문

사실 그래서 생기는 질문 다루는 편
KV 는 바뀌지 않으니 저장해 두고 쓴다 — 1편
디코드는 배치가 클수록 싸다 KV 메모리를 낭비 없이 써서 배치를 키우려면? 2편(PagedAttention, 연속 배칭)
같은 앞부분의 KV 는 같다 다른 요청의 KV 를 재사용하려면? 2편(프리픽스 캐시), 3편
GPU 메모리는 작다 KV 를 CPU·디스크·다른 서버에 두고 불러오면? 3편(LMCache)
프리필과 디코드는 성격이 다르다 둘을 다른 GPU 에 나누면? 이미 KV 가 있는 곳으로 보내면? 4편(Dynamo)

티티 한마디. 탁구 경기에서 점수를 매번 처음 서브부터 다시 세는 심판은 없어요. 점수판에 적어 두고 한 점씩 더하죠. KV 캐시가 바로 그 점수판이에요. 다만 경기장이 수백 개면, 점수판 놓을 자리부터 모자라기 시작해요.

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다