본문 바로가기
일반IT/AI

LLM은 이전 토큰을 어떻게 참고할까 — Q·K·V와 KV Cache의 동작 원리

by gasbugs 2026. 7. 30.
  •  

LLM이 문장을 생성하는 모습을 보면 이미 읽은 내용을 기억하면서 자연스럽게 다음 단어를 고르는 것처럼 보입니다. 하지만 Decoder-only Transformer 안에는 사람이 문장을 기억하는 것과 같은 기억 장치가 들어 있지 않습니다.

 

대신 각 토큰은 Query, Key, Value, 줄여서 Q·K·V라는 벡터로 변환됩니다. 현재 위치의 Q가 이전 토큰들의 K와 관련도를 계산하고, 관련도가 높은 토큰의 V를 더 많이 가져오면서 문맥을 구성합니다.

 

여기에 추론 속도를 높이기 위한 KV Cache가 결합됩니다. 이미 처리한 토큰의 K와 V를 저장해 두었다가 다음 토큰을 생성할 때 재사용하는 방식입니다.

 

이 글에서는 Q·K·V가 만들어지는 과정부터 Self-Attention 계산, Prefill과 Causal Mask, 토큰별 디코딩, KV Cache가 성능에 미치는 영향까지 하나의 흐름으로 살펴보겠습니다.

그림: 하나의 토큰 표현에서 Q·K·V가 만들어지는 세 갈래 투영 과정

Q·K·V는 어디에서 등장할까

Decoder-only Transformer는 여러 개의 Transformer Layer를 쌓은 구조입니다. 각 레이어에는 Self-Attention과 Feed Forward Network가 들어 있습니다.

 

Q·K·V는 이 가운데 Self-Attention에서 등장합니다. 중요한 점은 입력 문장 전체에 대해 Q·K·V를 한 번만 만드는 것이 아니라, 각 레이어에서 레이어의 현재 입력을 다시 투영해 새로운 Q·K·V를 만든다는 것입니다.

 

한 레이어에 들어온 토큰 표현을 X라고 하면 다음처럼 표현할 수 있습니다.

Q = XWQ
K = XWK
V = XWV

WQ, WK, WV는 학습을 통해 결정되는 서로 다른 가중치 행렬입니다. 같은 토큰 표현 X를 사용하더라도 어떤 관점으로 비교하고 어떤 내용을 전달할지에 따라 서로 다른 벡터가 만들어집니다.

토큰의 현재 표현 X
 ├─ WQ와 곱함 → Q
 ├─ WK와 곱함 → K
 └─ WV와 곱함 → V

세 벡터를 검색 시스템에 비유하면 이해하기 쉽습니다.

벡터 Attention에서의 역할 검색 시스템 비유
Q, Query 현재 위치가 찾고 싶은 정보 검색어
K, Key 각 위치가 어떤 정보와 관련되는지 나타내는 표식 문서의 색인·키워드
V, Value 선택됐을 때 실제로 전달할 정보 문서의 본문

다만 이 비유를 너무 문자 그대로 받아들이면 안 됩니다. K가 사람이 읽을 수 있는 키워드를 저장하고 V가 문장을 그대로 보관하는 것은 아닙니다. 모두 학습된 고차원 벡터이며, 모델이 작업을 수행하기 좋은 내부 표현입니다.

Q는 K를 비교하고 V를 가져온다

Self-Attention의 핵심은 한 문장으로 정리할 수 있습니다.

현재 위치의 Q로 모든 허용된 위치의 K를 비교한 다음, 그 점수에 따라 V를 가중합한다.

Q와 K는 관련도를 계산하기 위한 쌍이고, V는 관련도가 계산된 뒤 실제로 섞이는 정보입니다. 이 역할을 구분하면 왜 K와 V가 캐시되고 Q는 보통 캐시되지 않는지도 자연스럽게 이해할 수 있습니다.

그림: Q와 K의 비교 강도가 달라지면서 V의 반영 비중이 결정되는 과정

예를 들어 다음 문장을 처리한다고 하겠습니다.

대한민국의 수도는 서울이다

Prefill이나 학습 과정에서 서울 위치의 표현을 계산한다면 Q서울은 자신과 앞쪽 토큰의 K를 비교할 수 있습니다.

Q서울 · K대한민국 → 국가 문맥과의 관련도
Q서울 · K수도     → 수도 개념과의 관련도
Q서울 · K는       → 문법적 관계와의 관련도
Q서울 · K서울     → 현재 위치 자체와의 관련도

관련도 점수가 다음과 같이 정규화됐다고 가정해 보겠습니다.

대한민국의 K → 0.30
수도의 K     → 0.50
는의 K       → 0.10
서울의 K     → 0.10

그러면 서울 위치의 Attention 출력은 다음과 같은 가중합으로 생각할 수 있습니다.

Attention 출력
= V대한민국 × 0.30
 + V수도    × 0.50
 + V는      × 0.10
 + V서울    × 0.10

이 결과는 Residual Connection, 정규화, Feed Forward Network 등 다음 계산으로 전달됩니다. 여러 레이어를 지나면서 토큰 표현은 주변 문맥을 점점 더 반영하게 됩니다.

 

여기서 한 가지를 구분해야 합니다. 모델이 아직 서울을 생성하기 전이라면 미래 토큰인 서울의 Q는 존재하지 않습니다. 그때는 프롬프트 마지막 토큰 위치의 표현으로 다음 토큰 서울의 확률을 예측합니다. Q서울이라는 설명은 서울 토큰이 입력에 이미 포함됐거나, 생성된 뒤 그 위치의 문맥 표현을 계산하는 상황에 해당합니다.

Scaled Dot-Product Attention은 어떻게 계산될까

Attention의 대표 공식은 다음과 같습니다.

Attention(Q, K, V)
= softmax(QKᵀ / √dₖ + M) V

여기서 dₖ는 한 Attention Head에서 사용하는 Key 벡터의 차원이고, M은 Attention Mask입니다. Decoder-only 모델의 Causal Mask는 미래 위치의 점수에 매우 큰 음수, 개념적으로는 -∞를 더해 Softmax 결과가 0이 되게 만듭니다.

그림: 점수 행렬 생성, 스케일링, Causal Mask, Softmax, V 가중합으로 이어지는 Attention 파이프라인

계산을 단계별로 나누면 다음과 같습니다.

1. Q와 K의 내적으로 관련도 점수를 만든다

scores = QKᵀ

Q 한 행과 K 한 행의 내적이 두 벡터의 관련도 점수가 됩니다. 여러 토큰을 한꺼번에 계산하면 토큰과 토큰 사이의 점수가 들어 있는 행렬이 만들어집니다.

2. √dₖ로 나눈다

벡터 차원이 커지면 내적값의 크기도 커질 수 있습니다. 점수가 지나치게 커지면 Softmax가 한쪽으로 포화돼 학습이 불안정해질 수 있으므로 √dₖ로 나눠 크기를 조절합니다.

scaled_scores = scores / √dₖ

3. Mask를 적용한다

Decoder-only 모델은 다음 토큰을 예측해야 하므로 현재 위치보다 뒤에 있는 미래 토큰을 참고하면 안 됩니다.

allowed position → 점수 유지
future position  → -∞를 더함

4. Softmax로 가중치를 만든다

weights = softmax(masked_scores)

각 행의 점수는 합이 1인 가중치로 바뀝니다. 미래 위치의 가중치는 0이 되고, 관련도가 높은 과거·현재 위치의 가중치는 커집니다.

5. V를 가중합한다

context = weights × V

최종 결과는 관련도가 높은 Value를 더 많이 반영한 새로운 문맥 벡터입니다.

Multi-Head Attention에서는 무엇이 달라질까

실제 Transformer는 Q·K·V를 한 번의 관점으로만 비교하지 않습니다. 여러 Attention Head가 서로 다른 하위 공간에서 관계를 학습합니다.

 

어떤 Head는 문법적 의존성을 강하게 볼 수 있고, 다른 Head는 멀리 떨어진 주어와 서술어의 관계나 특정 개체의 반복을 더 잘 포착할 수 있습니다.

Head 1 → Q1, K1, V1 → Attention 결과 1
Head 2 → Q2, K2, V2 → Attention 결과 2
Head 3 → Q3, K3, V3 → Attention 결과 3
...

각 Head의 결과를 이어 붙이고 출력 투영 행렬 WO를 통과시키면 레이어의 Multi-Head Attention 출력이 됩니다.

MultiHead(Q, K, V)
= Concat(head1, head2, ..., headh) WO

현대 LLM에서는 모든 Query Head가 독립된 K·V Head를 갖는 MHA뿐 아니라, 여러 Query Head가 K·V를 공유하는 GQA, Grouped Query Attention이나 MQA, Multi-Query Attention도 많이 사용합니다. 이 구조는 KV Cache의 크기와 메모리 대역폭을 줄이는 데 중요한 역할을 합니다.

Prefill 단계: 프롬프트 전체를 한 번에 처리한다

입력이 다음과 같다고 하겠습니다.

생성형 AI는 문장을

토큰화 결과를 단순하게 표현하면 다음과 같습니다.

생성형 / AI / 는 / 문장 / 을

Prefill 단계에서는 프롬프트의 모든 토큰을 GPU가 병렬적으로 처리합니다. 각 레이어에서 모든 위치의 Q·K·V를 만들고, Causal Mask를 적용한 Self-Attention을 계산합니다.

그림: 모든 프롬프트 위치의 Q·K·V를 병렬 계산하되 미래 위치는 삼각형 Mask로 차단하는 Prefill

생성형 → Q1, K1, V1
AI     → Q2, K2, V2
는     → Q3, K3, V3
문장   → Q4, K4, V4
을     → Q5, K5, V5

각 위치에서 참고할 수 있는 범위는 다음과 같습니다.

Q1 → K1
Q2 → K1, K2
Q3 → K1, K2, K3
Q4 → K1, K2, K3, K4
Q5 → K1, K2, K3, K4, K5

행렬로 보면 아래쪽 삼각형만 허용되는 형태입니다.

        K1  K2  K3  K4  K5
Q1      O   X   X   X   X
Q2      O   O   X   X   X
Q3      O   O   O   X   X
Q4      O   O   O   O   X
Q5      O   O   O   O   O

Prefill이 끝나면 각 레이어에서 프롬프트 토큰의 K와 V가 KV Cache에 저장됩니다. 다음 토큰의 확률을 계산할 때는 일반적으로 마지막 입력 위치의 최종 Hidden State에서 나온 Logits를 사용합니다.

생성형 AI는 문장을
→ 다음 토큰 후보의 확률
→ "처리" 선택

Prefill은 많은 토큰을 병렬 계산할 수 있어 GPU 연산량을 적극 활용하는 구간입니다. 긴 프롬프트를 읽을 때 첫 토큰이 늦게 나오는 이유인 TTFT, Time To First Token과 밀접한 관련이 있습니다.

Decode 단계: 새 토큰을 하나씩 생성한다

첫 출력 토큰으로 처리가 선택됐다고 하겠습니다. 다음 Decode Step에서는 전체 프롬프트를 다시 모델에 넣는 대신 새 토큰 처리만 입력합니다.

 

각 레이어는 새 토큰의 Q·K·V를 계산합니다.

"처리"
→ q6, k6, v6

그림: 현재 토큰 하나의 Q가 과거 K를 조회하고 V를 가중합해 다음 토큰을 만드는 직렬 Decode 과정

현재 레이어에서 q6은 캐시에 저장된 과거 K와 새 k6을 함께 비교합니다.

q6
→ [k1, k2, k3, k4, k5, k6]과 비교

그 점수로 과거와 현재의 V를 가중합합니다.

[v1, v2, v3, v4, v5, v6]
→ Attention 가중합
→ "처리" 위치의 문맥 벡터

이 계산이 모든 Transformer Layer를 통과한 뒤 마지막 Hidden State가 Vocabulary 크기의 Logits로 변환됩니다.

문맥 벡터
→ LM Head
→ 다음 토큰별 Logit
→ Softmax 또는 Sampling
→ "한다" 선택

이제 한다를 입력으로 같은 과정을 반복합니다. 이처럼 이전에 생성된 토큰을 다시 입력으로 사용해 다음 토큰을 하나씩 만드는 방식을 Autoregressive Decoding이라고 합니다.

 

Prefill과 달리 Decode는 다음 단계가 이전 단계의 결과를 기다려야 합니다. 토큰을 완전히 병렬로 생성할 수 없고, 매 단계마다 KV Cache를 읽어야 하므로 메모리 대역폭과 토큰별 지연시간의 영향을 크게 받습니다.

KV Cache는 무엇을 저장할까

KV Cache는 이미 처리한 토큰의 Key와 Value를 레이어별로 저장하는 메모리입니다.

Layer 1 KV Cache
K1, K2, K3, K4, K5
V1, V2, V3, V4, V5

Layer 2 KV Cache
K1, K2, K3, K4, K5
V1, V2, V3, V4, V5

...

그림: 레이어마다 저장된 과거 K·V를 다시 읽고 현재 토큰의 새 K·V만 끝에 추가하는 KV Cache

새 토큰이 들어오면 각 레이어는 해당 토큰의 K와 V만 캐시에 추가합니다.

Kcache ← concat(Kpast, k현재)
Vcache ← concat(Vpast, v현재)

현재 토큰의 Q는 Attention 계산에 바로 사용되지만 다음 토큰 단계에서 다시 쓸 필요가 없습니다.

벡터 현재 Decode Step 이후 Step에서의 처리
현재 Q 캐시된 K와 관련도 계산 일반적으로 저장하지 않음
현재 K 현재 Q와 비교 KV Cache에 추가
현재 V 현재 출력의 가중합에 참여 KV Cache에 추가
과거 K·V 현재 Q가 조회 캐시에서 반복 재사용
과거 Q 사용하지 않음 다시 계산하거나 저장할 필요 없음

전체 흐름은 다음과 같습니다.

현재 토큰
  ↓
q현재, k현재, v현재 계산
  ↓
q현재 × [K과거 + k현재]ᵀ
  ↓
Attention 가중치
  ↓
[V과거 + v현재] 가중합
  ↓
현재 위치의 문맥 벡터
  ↓
다음 토큰 확률 계산

KV Cache가 없으면 어떤 계산이 반복될까

KV Cache가 없다면 토큰을 하나 생성할 때마다 지금까지의 전체 시퀀스를 다시 처리해야 합니다.

첫 번째 생성
프롬프트 전체 계산

두 번째 생성
프롬프트 + 첫 출력 전체 계산

세 번째 생성
프롬프트 + 첫 출력 + 두 번째 출력 전체 계산

KV Cache를 사용하면 이미 처리한 위치의 K와 V 투영 결과를 재사용할 수 있습니다.

첫 번째 생성
프롬프트의 K·V 저장

두 번째 생성
새 토큰의 q·k·v만 계산
+ 과거 K·V 재사용

세 번째 생성
다시 새 토큰의 q·k·v만 계산
+ 더 길어진 과거 K·V 재사용

하지만 KV Cache가 Attention 계산을 완전히 일정한 비용으로 만드는 것은 아닙니다. 새 Query는 여전히 시퀀스가 길어진 만큼 더 많은 과거 Key와 비교하고 Value를 읽어야 합니다.

없어지는 비용
→ 과거 토큰의 K·V를 다시 투영하는 계산

남아 있는 비용
→ 현재 Q와 전체 캐시 K의 비교
→ 전체 캐시 V의 가중합
→ 길어진 Cache를 읽는 메모리 비용

즉, KV Cache는 중복 계산을 크게 줄이지만 컨텍스트가 길어질수록 Attention 조회량과 메모리 사용량이 증가하는 문제까지 없애지는 않습니다.

KV Cache의 메모리는 왜 커질까

기본적인 KV Cache 크기는 다음 요소에 비례합니다.

KV Cache 크기
≈ 2
× Transformer Layer 수
× KV Head 수
× Head 차원
× 저장 토큰 수
× 배치 크기
× 원소당 바이트

앞의 2는 K와 V를 모두 저장하기 때문입니다. 컨텍스트가 길어지거나 동시에 처리하는 요청이 많아지면 Cache가 빠르게 커질 수 있습니다.

 

그래서 실제 LLM Serving에서는 다음과 같은 최적화가 사용됩니다.

  • GQA·MQA로 KV Head 수 줄이기
  • KV Cache를 더 낮은 정밀도로 양자화하기
  • 일부 레이어 Cache를 CPU로 Offload하기
  • Sliding Window Attention으로 오래된 토큰 범위 제한하기
  • Paged Attention으로 Cache 메모리를 페이지 단위로 관리하기
  • 공통 Prefix의 KV Cache를 여러 요청에서 재사용하기

각 방법에는 속도, 메모리, 정확도, 구현 복잡도 사이의 Trade-off가 있습니다.

KV Cache를 이해할 때 자주 생기는 오해

KV Cache는 모델의 장기기억이 아니다

KV Cache는 현재 요청에서 이미 처리한 토큰의 Attention용 중간 결과입니다. 대화가 끝나 Cache가 폐기되면 모델 파라미터에 새로운 지식이 남는 것이 아닙니다.

Cache에는 토큰 원문만 저장되는 것이 아니다

토큰 ID나 문자열을 보관하는 단순 기록이 아니라, 각 Transformer Layer에서 계산된 K·V Tensor가 저장됩니다. 따라서 모델 크기와 레이어 구조, KV Head 수가 메모리 사용량에 직접 영향을 줍니다.

과거 문장이 바뀌면 Cache를 그대로 사용할 수 없다

KV는 이전 토큰과 위치 정보, 앞선 레이어 계산에 의존합니다. 앞부분의 토큰이 바뀌면 그 뒤의 Cache도 더 이상 같은 문맥을 나타내지 않으므로 변경 지점 이후를 다시 계산해야 합니다.

KV Cache는 주로 추론용이다

학습에서는 전체 시퀀스의 Gradient를 계산해야 하므로 일반적인 생성용 KV Cache를 같은 방식으로 사용하지 않습니다. Hugging Face 문서도 Cache는 추론에서 사용해야 한다고 안내합니다.

한 장면으로 다시 정리하기

Q·K·V와 KV Cache를 시간 순서로 연결하면 다음과 같습니다.

1. 프롬프트 토큰화
2. Prefill에서 모든 프롬프트 위치의 Q·K·V 계산
3. Causal Mask를 적용해 미래 토큰 참조 차단
4. 각 레이어의 프롬프트 K·V를 Cache에 저장
5. 마지막 위치의 Logits로 첫 출력 토큰 선택
6. 새 토큰의 q·k·v만 계산
7. q로 캐시된 K를 조회하고 V를 가중합
8. 새 k·v를 Cache 끝에 추가
9. 다음 토큰을 선택하고 6번부터 반복

핵심만 압축하면 다음과 같습니다.

Q는 현재 위치가 무엇을 참고할지 묻는 벡터이고, K는 각 위치가 어떤 정보와 관련되는지를 나타내며, V는 선택됐을 때 실제로 전달할 정보다.

그리고 추론 성능 관점의 핵심은 다음과 같습니다.

과거 토큰의 K와 V는 레이어별 KV Cache에 저장해 재사용하고, 새 토큰이 생길 때마다 현재 토큰의 K와 V만 추가한다. 다만 새 Q는 여전히 전체 Cache의 K와 V를 조회해야 하므로 긴 컨텍스트의 계산량과 메모리 비용이 완전히 사라지는 것은 아니다.

Q·K·V는 LLM이 문맥을 선택하는 원리이고, KV Cache는 그 선택을 매 토큰마다 현실적인 속도로 반복하게 만드는 추론 최적화라고 이해하면 됩니다.

참고 자료