LLM 공부 04 | Prefill과 Decode: LLM이 읽고 쓰는 두 단계
3편까지는 모델 안쪽을 봤다.
문장은 token ID가 되고, token ID는 embedding vector가 된다. 그 벡터는 Transformer block stack을 지나며 self-attention과 MLP로 계속 갱신된다. attention 안에서는 K/V가 만들어지고, 이 값들은 다음 token 생성에서 재사용될 수 있다.
이제 관점을 바꾼다.
이번 글은 모델 구조가 아니라 실행 흐름의 글이다. LLM이 사용자의 prompt를 읽고 답변을 쓰는 과정을 serving 관점에서 보면 두 단계가 보인다.
prefill = 입력을 읽는 단계
decode = 출력을 한 token씩 쓰는 단계
이 두 단계를 구분하면 LLM 비용과 속도 감각이 훨씬 선명해진다.
왜 긴 prompt를 넣으면 첫 token이 늦게 나오는지, 왜 답변은 token 단위로 흘러나오는지, 왜 KV cache가 메모리 병목이 되는지, 왜 최근 serving 시스템이 prefill과 decode를 따로 떼어 최적화하려는지 이해할 수 있다.
긴 prompt는 먼저 읽혀야 한다
Claude Code나 Codex로 작업할 때 이런 경험이 생긴다.
긴 파일을 붙여 넣고 묻는다.
이 코드 구조를 보고 문제를 찾아줘.
사용자 입장에서는 질문을 보냈으니 바로 답변이 시작되길 기대한다. 하지만 모델은 곧바로 답을 쓰지 않는다. 먼저 입력을 처리해야 한다.
이 단계가 prefill이다.
prefill은 prompt 전체 token을 Transformer block stack에 통과시키는 forward pass다. 이때 모델은 각 token의 hidden state를 만들고, attention에서 다음 단계에 재사용할 Key/Value를 layer별로 계산한다.
정확히 말하면 prefill은 "첫 token을 쓰는 단계"라기보다 "첫 token을 쓸 준비를 끝내는 단계"에 가깝다.
prompt tokens
-> embedding
-> Transformer block stack
-> layer별 K/V 생성
-> 마지막 위치의 logits 생성
-> 첫 output token 선택 준비
첫 output token 후보 logits는 prefill 마지막에서 나온다. 하지만 사용자가 체감하는 것은 "모델이 읽는 시간"이다.
이 시간이 길면 Time To First Token, 줄여서 TTFT가 길어진다.
Prefill은 병렬성이 좋지만 공짜가 아니다
prefill은 prompt token 전체를 한 번에 처리할 수 있다. decode보다 병렬성이 좋다.
하지만 병렬성이 좋다는 말이 싸다는 뜻은 아니다.
입력 token이 많으면 모든 token이 모든 layer를 지나야 한다. 각 layer에서 attention과 MLP 계산이 일어나고, attention의 K/V가 cache에 저장된다.
prefill 부담
~ prompt token 수
x layer 수
x hidden dimension / attention head 관련 차원
입력 prompt가 1,000 token이면 1,000개 위치에 대한 K/V가 생긴다. 20,000 token이면 20,000개 위치에 대한 K/V가 생긴다. layer가 많을수록 layer별 cache도 늘어난다.
그래서 긴 context는 단순히 "많이 읽는다"가 아니다. GPU memory에 실제 상태를 만든다.
특히 agentic workload에서는 prompt가 쉽게 길어진다.
system instruction
+ repository instruction
+ 이전 대화
+ tool result
+ 긴 파일
+ diff
+ 에러 로그
이 모든 것이 prefill 대상이 된다.
KV cache는 attention이 남기는 작업 메모다
3편에서 attention은 Query, Key, Value를 만든다고 했다.
decode 단계에서 새 token 하나가 들어오면, 그 token의 Query는 새로 계산해야 한다. 하지만 과거 token들의 Key와 Value는 이미 계산해 두었다.
그래서 저장한다.
이것이 KV cache다.
prefill:
prompt token들의 K/V를 layer별로 만든다
decode:
새 token의 Q를 만들고
과거 K/V를 cache에서 읽는다
새 token의 K/V를 cache에 추가한다
KV cache는 모델 지식이 아니다. parameter도 아니다. 요청 처리 중 생기는 동적 상태다.
같은 모델 weight를 써도 prompt가 다르면 KV cache도 다르다. 요청이 끝나면 cache는 버려질 수 있다. 다만 prefix caching이나 cache reuse 같은 serving 최적화에서는 비슷한 prompt의 K/V를 재사용하려고 한다.
여기서 말하는 cache reuse는 "모델이 대화를 영구히 기억한다"는 뜻이 아니다. 같은 prefix가 cache retention window 안에 다시 들어오면 prefill에서 만든 K/V 표현을 재사용할 수 있다는 뜻이다. TTL이 지나거나 다른 machine으로 routing되거나 prefix가 달라지면 같은 문장을 다시 보내도 full prefill이 필요할 수 있다.
이 구분이 중요하다.
parameter = 학습으로 만들어진 고정 weight
activation = forward pass 중 생기는 중간 계산값
KV cache = attention K/V를 다음 decode step에서 재사용하기 위해 저장한 상태
KV cache를 알면 LLM serving이 왜 memory 문제인지 보인다.
Decode는 token을 하나씩 쓴다
prefill이 끝나면 첫 output token을 고를 수 있다.
그다음부터는 decode다.
decode는 생성된 token을 context 뒤에 붙이고, 다시 다음 token을 만드는 반복이다.
context
-> next token
-> context + next token
-> next token
-> context + next token + next token
이 방식이 autoregressive generation이다.
한 step을 조금 더 코드처럼 풀면 이렇다.
selected token ID
-> embedding(selected token ID)
-> for each Transformer layer:
현재 hidden state에서 Q/K/V 계산
새 K/V를 해당 layer의 KV cache에 append
현재 Q로 과거+현재 K/V를 attention
attention output을 residual stream에 더함
MLP output을 residual stream에 더함
-> final hidden state
-> LM head
-> vocabulary logits
-> next token ID 선택
-> 다음 decode step으로 반복
Decode는 final hidden state를 그대로 다음 입력으로 넘기지 않는다. 선택된 token ID를 다시 embedding하고, layer별 K/V를 cache에 추가하며 반복한다.
여기서 cache되는 것은 각 layer의 K/V다. final hidden state를 그대로 다음 입력 vector로 넘기는 것이 아니다. 모델은 마지막 hidden state로 vocabulary 전체 점수를 만들고, 그중 선택된 discrete token ID를 다시 embedding해서 다음 step을 시작한다.
decode는 순차성이 강하다. 두 번째 output token은 첫 번째 output token이 무엇인지 알아야 만들 수 있다. 세 번째 output token은 두 번째 output token 이후의 context가 필요하다.
그래서 긴 답변은 시간이 걸린다. 출력 token 수가 많으면 decode step도 많다.
output 100 tokens
-> decode 100 steps
output 2,000 tokens
-> decode 2,000 steps
각 decode step은 같은 Transformer block stack을 지난다. prefill과 decode가 서로 다른 layer를 쓰는 것이 아니다. 일반적인 decoder-only LLM에서는 같은 model weight를 쓴다.
다만 실행 패턴이 다르다.
prefill:
긴 prompt 전체를 한 번에 처리한다
decode:
새 token 하나를 처리하고 cache를 읽고 확장한다
Decode는 memory bandwidth에 민감해진다
decode step 하나만 보면 token 하나를 처리하는 일이다. 그래서 가볍게 느껴질 수 있다.
하지만 그 token은 과거 context 전체의 K/V를 참고해야 한다. context가 길수록 읽어야 할 KV cache가 커진다.
긴 context
-> 큰 KV cache
-> 매 decode step에서 더 많은 cache read
-> inter-token latency 증가 가능
여기서 inter-token latency, 줄여서 ITL이 나온다. 사용자는 답변이 한 token씩 흘러나오는 속도로 느낀다.
TTFT는 첫 token까지 걸리는 시간이다. prefill이 크게 영향을 준다.
ITL은 이후 token 사이 간격이다. decode와 KV cache read가 크게 영향을 준다.
TTFT = 첫 token이 나오기까지의 시간
ITL = token과 token 사이의 시간
긴 prompt는 TTFT를 키운다. 긴 output은 decode step 수를 키운다. 긴 context는 decode 중 KV cache read 부담을 키운다.
Prefill은 첫 token까지의 시간인 TTFT를 크게 흔들고, decode는 token 사이 간격인 ITL과 전체 출력 시간을 만든다.
이제 "느리다"는 말이 하나가 아니라는 것을 알 수 있다.
Prefill과 Decode는 최신 serving에서 분리되고 있다
최근 LLM serving trend에서 prefill/decode 분리는 중요한 주제다.
vLLM의 disaggregated prefilling 문서는 prefill과 decode를 서로 다른 vLLM instance에 놓고, prefill instance가 만든 KV cache를 decode instance로 넘기는 구조를 설명한다. NVIDIA Dynamo도 prefill engine이 KV cache를 만들고 decode engine이 이어서 decode하는 구조를 문서화한다.
왜 이렇게 나눌까?
prefill과 decode가 원하는 자원 특성이 다르기 때문이다.
prefill:
긴 prompt를 한 번에 읽는다
compute-heavy 성격이 강하다
TTFT에 영향이 크다
decode:
token 하나씩 생성한다
KV cache를 계속 읽는다
memory bandwidth와 scheduling에 민감하다
ITL에 영향이 크다
둘을 같은 worker에서 섞어 처리하면 긴 prefill job이 decode 중인 요청의 latency를 흔들 수 있다. 반대로 decode에 맞춘 worker에 prefill을 과하게 넣으면 첫 token이 늦어진다.
그래서 serving system은 점점 더 구체적인 질문을 던진다.
prefill은 어느 GPU group에서 할 것인가
decode는 어느 GPU group에서 할 것인가
KV cache는 어떻게 옮길 것인가
같은 prefix를 가진 요청은 cache를 재사용할 수 있는가
긴 context 요청과 짧은 요청을 어떻게 섞을 것인가
LLM은 더 이상 모델 파일 하나를 띄우는 문제가 아니다. token 흐름을 scheduling하는 분산 시스템이 된다.
Cache가 있으면 항상 빠른 것은 아니다
KV cache는 과거 token을 다시 계산하지 않게 해준다. 그러면 항상 빠를 것 같다.
하지만 cache도 읽고 저장해야 한다. cache가 커지면 memory를 많이 쓰고, GPU memory bandwidth를 압박한다. cache를 다른 장치나 다른 worker로 옮기면 transfer 비용도 생긴다.
그래서 cache 최적화는 단순히 "저장하면 된다"가 아니다.
어디에 저장할 것인가
얼마나 압축할 것인가
언제 버릴 것인가
다시 쓸 가능성이 있는가
옮기는 비용보다 재계산 비용이 큰가
최근 FP8 KV cache, KV offloading, distributed KV store, cache-aware routing 같은 흐름이 나오는 이유도 여기에 있다. long-context workload가 늘어나면 KV cache가 serving 비용의 중심으로 올라온다.
이 관점에서 보면 context window는 단순한 기능이 아니다.
긴 context 지원
-> 더 긴 prompt를 넣을 수 있음
-> 더 큰 KV cache가 생김
-> 더 많은 memory와 transfer 전략이 필요함
사용자는 "긴 문서를 넣을 수 있다"고 느끼지만, serving 쪽에서는 memory system을 새로 설계해야 한다.
PagedAttention은 서로 다른 KV cache 길이를 다룬다
여러 사용자가 동시에 들어오면 요청마다 KV cache 길이가 전부 다르다.
짧은 질문: 800 tokens
긴 코드 리뷰: 40,000 tokens
긴 agent run: 180,000 tokens
이 cache를 요청마다 큰 연속 메모리로 잡으면 빈 공간과 파편화가 생긴다. 그래서 vLLM 같은 serving engine은 KV cache를 고정 크기 block/page로 나누어 관리한다. 이것을 paged KV cache라고 부를 수 있다.
request A -> block 3, block 9
request B -> block 1, block 8, block 42, ...
PagedAttention은 이 paged KV cache 위에서 attention을 계산하는 방식이다. 각 token은 자신이 속한 request의 block table을 따라가며 필요한 K/V만 읽는다. 그래서 서로 다른 길이의 sequence를 같은 batch에 태워도 context가 섞이지 않는다.
token
+ request_id
+ position_id
+ KV block table pointer
+ attention boundary
이 metadata가 없으면 여러 사용자의 token은 하나의 긴 sequence처럼 섞인다. PagedAttention의 핵심은 memory capacity를 아끼면서도, attention kernel이 각 token의 올바른 KV cache를 찾아 읽게 만드는 데 있다.
이 구조가 있어야 continuous batching, chunked prefill, prefix caching 같은 기법이 같은 serving loop 안에서 함께 돌아갈 수 있다.
정리
LLM 추론은 prefill과 decode로 나누어 보면 이해가 쉽다.
prefill은 입력 prompt를 읽는 단계다. prompt token 전체를 Transformer block stack에 통과시키고, 각 layer의 K/V를 만들어 KV cache에 저장한다. 첫 output token 후보 logits도 이 단계 끝에서 나온다. 사용자가 느끼는 TTFT에 큰 영향을 준다.
decode는 답변을 한 token씩 쓰는 단계다. 새 token의 Query를 만들고, 과거 token의 K/V를 cache에서 읽고, 새 token의 K/V를 추가한다. output token 수만큼 반복되며 ITL과 총 응답 시간에 영향을 준다.
둘은 같은 model weight와 같은 layer stack을 쓴다. 차이는 실행 패턴이다.
이제 LLM 비용은 더 구체적으로 보인다.
긴 입력 = 큰 prefill
긴 문맥 = 큰 KV cache
긴 출력 = 많은 decode step
많은 사용자 = 어려운 batch scheduling
다음 글에서는 batch와 sequence length를 같이 본다. 여러 요청을 어떻게 묶어 GPU를 효율적으로 쓰는지, 그리고 sequence length가 왜 memory와 throughput을 동시에 흔드는지 정리한다.
이어 읽기
시리즈는 순서대로, 편집 추천은 맥락대로, 비슷한 주제는 태그 기준으로 정리합니다.
시리즈 전체
LLM 공부 시리즈4/9편- 1.LLM 공부 01 | LLM은 검색기가 아니라 다음 토큰 생성기다
- 2.LLM 공부 02 | 토큰이 비용을 만든다
- 3.LLM 공부 03 | Transformer 안에서 문맥이 섞이는 방식
- 4.LLM 공부 04 | Prefill과 Decode: LLM이 읽고 쓰는 두 단계
- 5.LLM 공부 05 | Batch와 Sequence Length가 속도와 비용을 정한다
- 6.LLM 공부 06 | MoE와 GPU 클러스터: 거대 모델은 어떻게 나뉘어 도는가
- 7.LLM 공부 07 | Reasoning Effort: 더 깊게 생각한다는 말의 실제 의미
- 8.LLM 공부 08 | 학습 Batch와 Distillation: 모델은 어떻게 바뀌는가
- 9.LLM 공부 09 | RAG와 Vector DB: LLM 밖에서 근거를 찾는 방식
비슷한 주제의 글
태그가 겹치는 글입니다. 시리즈와 편집 추천에 이미 나온 글은 제외합니다.
AI 웹개발 기초: 프론트엔드 1-1 | 프론트엔드는 왜 이렇게 복잡해졌을까
프론트엔드가 React나 Next.js 같은 기술명 묶음이 아니라, 웹 문서가 구조, 표현, 동작, 상태, API 통신, 성능, 배포 책임을 차례로 떠안으며 커진 문제 해결의 축적임을 설명하는 AI 웹개발 기초 시리즈 프론트엔드 1-1.
AI 웹개발 기초: 프론트엔드 1-2 | DOM은 화면이 아니라 브라우저의 작업 모델이다
HTML source가 곧 화면이 아니라 DOM, CSSOM, render tree, layout, paint, composite, accessibility tree를 거쳐 화면과 보조기술 의미로 바뀌며, DOM 조작이 reflow, XSS, event delegation, 성능 지표와 어떻게 연결되는지 설명하는 AI 웹개발 기초 시리즈 프론트엔드 1-2.
AI 웹개발 기초: 프론트엔드 1-3 | jQuery에서 React로 넘어간 진짜 이유
jQuery의 DOM 직접 조작과 AJAX가 해결한 문제를 인정하고, UI가 커지면서 React, Vue, Angular 같은 state/data 기반 component UI가 왜 필요해졌는지 설명하는 AI 웹개발 기초 시리즈 프론트엔드 1-3.