APPENDIX · THEORY

트랜스포머 추론의 두 단계

Prefill 과 Decode

트랜스포머 추론의 두 단계: Prefill과 Decode

세션을 한참 진행하다가 컨텍스트가 길어졌을 때, 사이드바에서 같은 세션을 다시 열면 첫 응답이 유난히 늦게 시작돼요. 그런데 한 번 첫 토큰이 나오기 시작하면 이후 출력 속도는 평소와 비슷해요. 같은 모델, 같은 GPU 인데 — 왜 처음에만 그렇게 느릴까요?

답은 트랜스포머의 추론이 본질적으로 다른 두 단계로 나뉘어 있기 때문입니다. 그 두 단계가 어떤 비용 곡선을 그리는지를 정확히 알면 "1세션 1작업" 이나 "Prompt Caching 활용" 같은 실무 원칙이 단순한 노하우가 아니라 구조적 귀결로 보여요.

본문 §3에서 "컨텍스트 윈도우가 길어질수록 느려지고 비싸진다"고 했던 말의 구조적 이유를 이 부록에서 풀어 놓아요. 왜 세션 재개가 느린지, 왜 컨텍스트 오염이 단순한 "집중력 저하" 이상의 실질 비용을 갖는지를 Prefill·Decode 두 단계로 나눠 설명합니다.


토큰을 하나씩 출력하는 이유

트랜스포머는 자동회귀(autoregressive — 자기 출력을 다음 입력으로 다시 받아쓰는) 방식으로 동작합니다. 토큰 t+1을 예측하려면 [1, 2, ..., t]까지의 정보가 필요하기 때문에, 출력은 본질적으로 순차적이에요.

여기서 자연스러운 의문이 생겨요 — "왜 출력 토큰들도 한 번에 병렬로 못 뽑는가?" 답은 각 토큰의 확률 분포가 그 앞 토큰에 의존하기 때문입니다. 토큰 5의 분포는 토큰 4가 무엇으로 뽑혔는지에 따라 달라지므로, 토큰 4를 먼저 결정해야 토큰 5의 분포를 계산할 수 있어요. 입력은 이미 다 정해져 있으므로 한 번에 처리할 수 있지만 (이게 Prefill), 출력은 한 토큰씩 펼쳐가야 합니다 (이게 Decode).

그렇다고 매번 모든 토큰을 처음부터 다시 계산하지는 않아요. 여기서 PrefillDecode의 구분이 중요해집니다.


Prefill 단계 — 입력을 한 번에 소화한다

사용자가 프롬프트를 보내면, 그 안의 토큰들은 한 번에 병렬로 처리됩니다. 이 단계에서 서버는:

  • 각 토큰의 Key · Value 벡터를 계산
  • 계산 결과를 KV 캐시에 저장
  • 출력 토큰의 첫 번째를 생성

Prefill은 처리할 토큰이 많을수록 시간이 오래 걸려요. Attention 연산이 시퀀스 길이의 제곱(O(n²))에 비례하기 때문입니다. 세션의 "첫 응답이 언제 시작되느냐"를 좌우하는 단계가 바로 여기예요.

비유로 풀면 — Prefill 은 책 한 권을 받아서 처음부터 끝까지 한 번 쭉 훑으며 키워드별 색인을 만드는 작업이에요. 색인을 다 만들고 나야 첫 문장에 답을 쓸 수 있어요. 책이 두꺼울수록(컨텍스트가 길수록) 색인 작성에 시간이 더 들어요 — 그것도 비례가 아니라 제곱으로요.


Decode 단계 — 한 번에 토큰 하나씩

첫 번째 출력 토큰이 나온 이후부터는 Decode 단계예요:

  1. 새로 생성된 토큰의 Query · Key · Value만 계산
  2. 이전 토큰들의 K/V는 캐시에서 그대로 읽어옴
  3. 새 토큰의 K/V를 캐시에 추가
  4. 다음 토큰 생성 반복

이전 토큰들을 다시 forward pass(모델을 한 번 끝까지 통과시키는 계산) 할 필요가 없어요. KV 캐시 덕분에 "이미 읽은 것을 다시 읽지 않는다"가 성립합니다.

Decode 는 색인을 다 만든 뒤 한 줄씩 답을 적어 내려가는 단계에 해당해요. 답 한 줄을 쓸 때마다 색인 전체를 한 번씩 참고하지만(Attention), 색인을 다시 만들지는 않아요.

flowchart LR P["선반입(Prefill): [T1, T2, T3, T4]<br/>K/V 캐시 저장(K/V cache stored)"] --> T5["출력(output): T5"] T5 --> D1["복호화(Decode): T5 query + cached [K1..K4]"] D1 --> T6["출력(output): T6"] T6 --> D2["복호화(Decode): T6 query + cached [K1..K5]"] D2 --> T7["출력(output): T7"] T7 --> Dots["..."]

두 단계의 시간축을 함께 그리면 다음과 같아요.

flowchart LR subgraph Prefill["선반입(Prefill, one big batch, O(n²))"] P["T1 T2 T3 T4 — 병렬(parallel)"] end Prefill --> T5["첫 출력 토큰(first output token): T5"] T5 --> Decode subgraph Decode["복호화(Decode, token by token, O(n) per step)"] direction LR D1["1단계(step1)<br/>T6"] --> D2["2단계(step2)<br/>T7"] --> D3["3단계(step3)<br/>T8"] --> D4["4단계(step4)<br/>T9"] --> D5["5단계(step5)<br/>T10"] --> Stop["... 종료까지(until stop)"] end

Prefill이 "입력 한 덩어리를 씹어 삼키는" 단계라면, Decode는 "한 입씩 뱉어내는" 단계예요. 실제 응답 속도 체감은 Prefill(첫 토큰까지의 대기, TTFT — Time To First Token) + Decode(초당 토큰 수, tokens/s) 의 합으로 나타납니다. "응답이 시작되기까지 길었다"고 느끼면 Prefill 비용이 큰 것이고, "출력이 줄줄 나오는 속도가 느렸다"고 느끼면 Decode 처리량이 부족한 것이에요.


세션 재개 시 무슨 일이 일어나는가

세션이 진행 중일 때는 KV 캐시가 서버 메모리에 살아있어요. 새 메시지는 Decode 단계부터 시작할 수 있어서 빠릅니다.

세션이 종료되고 재개되면 얘기가 달라져요:

  • 이전 대화 전체를 컨텍스트로 다시 전송
  • 서버는 이 전체를 다시 Prefill
  • 컨텍스트가 길수록 첫 응답 지연이 늘어남

체감 예 — 코드베이스를 한참 다룬 사이드바 세션을 한참 뒤에 재개하면 첫 응답이 평소의 두 배 가까이 늦게 시작돼요. 서버 입장에서는 200K 토큰에 가까운 컨텍스트를 처음부터 다시 Prefill 해야 하기 때문입니다. 첫 토큰만 늦지, 그 이후 출력 속도는 평소와 같다는 점도 Prefill·Decode 비대칭에서 직접 따라 나오는 결과예요.

flowchart TD subgraph First["최초 호출(First call)"] F1["[system + user1]"] -->|"선반입(Prefill)"| F2["첫 토큰 — 시작 지연(first token, slow start)"] F2 --> F3["KV 캐시 GPU 메모리 상주(KV cache live in GPU memory)"] end subgraph Same["동일 세션 진행(Same session continuing)"] S1["[user2]"] -->|"즉시 복호화(Decode immediately)"| S2["빠름(fast)"] end subgraph Resumed["타임아웃 후 재개(Session resumed after timeout)"] R1["[system + user1 + assistant1 + user2]"] -->|"전체 선반입 재실행(full Prefill again)"| R2["첫 토큰 지연(first token, slow)"] end First --> Same Same -.->|"타임아웃(timeout)"| Resumed

Prompt Caching을 활성화하면 이 비용을 일부 회수할 수 있어요. 이전에 전송했던 동일한 prefix에 대해 서버가 KV 계산 결과를 일정 시간 보관했다가, 같은 prefix가 다시 오면 Prefill을 재실행하지 않고 저장된 KV를 그대로 씁니다. Anthropic API 기준 TTL은 기본 5분(ephemeral)이며, 요청 시 1시간 옵션으로 확장할 수 있어요. 캐시 히트가 나면 해당 prefix 구간의 Prefill 비용과 토큰 가격이 모두 크게 내려갑니다. 정확한 가격 비율은 모델별로 다르지만, 캐시 히트 토큰은 일반 입력 토큰의 1/10 수준이라고 외워두면 실무 견적에 큰 무리가 없어요. 서버측 KV 재사용 구현(vLLM APC·SGLang RadixAttention)은 추론 시스템·커널 최적화 §4 Prefix Caching 에서 다뤄요.

여기서 한 가지 깜짝 사실 — 캐시는 prefix 매칭만 인식합니다. 시스템 프롬프트가 1바이트만 달라져도 그 뒤 캐시는 전부 무효가 되고 처음부터 다시 Prefill 해요. Skill 명세를 동적으로 끼워 넣는 식의 시스템 프롬프트 변동은 캐시 히트율을 0에 가깝게 만들 수 있다는 뜻이에요. 캐시를 살리려면 변동하는 부분은 항상 뒤쪽에 두어야 합니다.


컨텍스트 오염과의 연결

컨텍스트 오염 — 세션에 무관한 내용이 뒤섞이는 현상 — 은 단순히 "기억이 뒤죽박죽"되는 문제가 아니에요. 트랜스포머 구조 수준에서 실질적인 비용이 세 가지로 발생합니다.

1. Attention 분산

Decode 단계에서 모델은 새 토큰의 Query를 캐시된 모든 K/V에 대해 attend 해요. 컨텍스트에 불필요한 정보가 많을수록 Attention Score가 분산되어, 모델이 정작 중요한 정보에 집중하지 못합니다. "집중력이 떨어진다"는 표현이 비유가 아니라 수치적 사실이에요 — softmax 분포가 평평해지면서 핵심 토큰의 가중치가 잡음 토큰들에게 깎여나갑니다.

2. Prefill 비용 증가

세션을 재개할 때 오염된 긴 컨텍스트를 다시 Prefill하면, 첫 응답이 나오기까지 더 오래 걸려요. Attention이 O(n²)이므로, 컨텍스트 길이가 2배가 되면 Prefill 비용은 4배가 됩니다. 10K 토큰 세션이 80K 토큰까지 부풀면 Prefill 비용은 64배 — 첫 응답 지연이 체감으로 바뀌는 지점이 여기예요.

3. KV 캐시 압박

컨텍스트가 길수록 KV 캐시 메모리를 더 많이 차지해요. 서버가 메모리 한도에 가까워지면 캐시를 일찍 evict 하고(밀어내고), 다음 요청에서 Prefill을 다시 해야 합니다. Prompt Caching의 TTL이 다 되기 전이라도, 메모리 압박으로 먼저 밀려날 수 있다는 뜻이에요.

본문 §3의 "1세션 = 1작업"과 "의존성 경량화" 원칙의 근거가 여기 있어요. 컨텍스트를 짧고 선명하게 유지하는 것은 실용적 습관이기 전에, 트랜스포머 추론 구조가 요구하는 사용법입니다.


비유의 한계

"책 색인" 비유는 Prefill 의 사전 처리 성격과 Decode 의 순차성을 직관적으로 잡아주지만, 한 가지 어긋나요 — 색인은 한 번 만들면 끝이지만 KV 캐시는 새 토큰이 나올 때마다 자라납니다. 또 색인은 정적인 자료 구조지만 KV 는 레이어별로 중첩된 거대한 텐서고, 한 토큰의 K/V 만 따져도 (레이어 수 × hidden_dim × 2) 만큼 메모리를 차지해요. 비유를 끌고 오되, 메모리 측면 분석을 할 때는 비유에서 빠져나와 KV 캐시의 진실 부록의 수식으로 옮겨가야 합니다.


요약

단계언제 발생비용 특성
Prefill세션 시작, 컨텍스트 재전송O(n²) — 컨텍스트 길이에 민감
Decode토큰 생성 매 스텝O(n) attention — 상대적으로 가벼움
KV 캐시 히트동일 prefix 재전송 (Prompt Caching)Prefill 생략 가능, 기본 5분 / 옵션 1시간

컨텍스트가 짧을수록 Prefill이 빠르고, Attention이 집중되고, 캐시가 오래 유지돼요. 세 효과가 같은 방향으로 합쳐지므로, "컨텍스트를 짧게" 라는 한 줄 원칙이 비용·속도·품질 세 측면에서 동시에 이득이에요.

KV 캐시가 실제로 메모리를 얼마나 먹는지, MoE와 Hybrid Attention이 이 수식을 어떻게 바꾸는지는 파라미터 착시: KV 캐시의 진실에서 이어 다뤄요.