프리필 대 디코드: 생성은 어떻게 돌아가는가
Enter를 누르면 생성은 완전히 다른 물리를 가진 두 단계로 진행됩니다. 프리필은 프롬프트 전체를 하나의 병렬 패스로 처리합니다. 연산 집약적이고 토큰당 속도는 빠르지만, 첫 단어가 나오기 전에 전부 기다려야 합니다. 이어서 디코드가 답을 한 번에 토큰 하나씩 뱉습니다. 모든 단계는 토큰 하나를 만들기 위해 사실상 모델 가중치 전체를 메모리에서 다시 읽습니다. "첫 토큰은 느리고 그다음은 흐릅니다"는 당신 환경의 별난 특성이 아니라 이 두 단계입니다.
1단계: 프리필 — 프롬프트
- 모든 프롬프트 토큰이 동시에 모델을 통과합니다. 대형 병렬 행렬 곱 — GPU가 정확히 이것을 위해 만들어진 것. 활용률은 높고 초당 토큰도 높습니다.
- 이는 첫 토큰까지의 시간(TTFT)으로 체감됩니다. Enter를 누른 뒤의 침묵입니다.
- 이 패스는 또한 모든 프롬프트 토큰의 키/값 벡터로 KV 캐시를 채웁니다. 디코드가 읽을 스크래치패드입니다.
- 프롬프트가 길수록 TTFT는 선형으로 길어집니다. 엔진이 프리픽스를 캐싱하지 않는 한 대형 시스템 프롬프트, RAG 문서, 챗 기록은 매번 프리필됩니다.
2단계: 디코드 — 답변
- 순전파 한 번에 토큰 하나. 그 패스는 모든 가중치가 필요합니다. 토큰 하나를 만드는 것은 모델 전체를 VRAM에서 스트리밍하는 것입니다.
- 천장은 연산량이 아니라 메모리 대역폭입니다. 대략 디코드 tok/s ≈ 유효 대역폭 ÷ 가중치 바이트. ~400 GB/s 유효 대역폭 카드에서 가중치 8 GB면 ≈ 50 tok/s가 천장입니다.
- 이것이 양자화가 디코드를 빠르게 하는 이유입니다. 가중치당 비트가 적으면 단계마다 스트리밍할 바이트가 줄어듭니다(속도 참고).
비대칭의 이유: 프리필은 가중치를 한 번 읽어 수천 토큰을 처리합니다(거대 행렬, 산술 강도 높음 — 연산 병목). 디코드는 정확히 하나를 처리합니다(가중치의 모든 바이트를 토큰 하나를 위해 가져옴 — 대역폭 병목). 같은 가중치, 정반대 병목.
total_time ≈ prompt_tokens / prefill_tok_s + output_tokens / decode_tok_s
계산 예제
프롬프트: 1,000 토큰 프리필 2,000 tok/s → 0.5 s (TTFT)
출력: 300 토큰 디코드 40 tok/s → 7.5 s
합계 ≈ 8.0 s — 실제 경과 시간의 94%가 디코드
- 처음 0.5초가 토큰 1,000개를 처리했고, 이어지는 7.5초가 300개를 만듭니다. 같은 모델, 같은 GPU, 50배 차이 — 단계가 다를 뿐 노력이 다른 게 아닙니다.
- 40 tok/s는 대략 영어 초당 160자입니다(≈4자/토큰). 그 누구의 타이핑보다 빠릅니다. 그래서 스트리밍이 편하게 읽히는 것입니다(토큰).
- 같은 40 tok/s의 한국어는 대략 초당 40–60자입니다. 토큰 속도는 같은데 토큰당 텍스트가 적습니다.
- 출력 길이는 디코드 쪽 다이얼입니다. max_tokens가 생성 길이의 상한을 정하고 스톱 시퀀스가 일찍 끝냅니다. 둘 다 경과 시간을 선형으로 바꿉니다(엔진 파라미터).
- 스트리밍은 물리가 아니라 지각을 바꿉니다. 같은 40 tok/s로 디코드를 기다리는 대신 지켜보는 것입니다.
계산 예제 2: RAG 사례
프롬프트: 12,000 토큰 (시스템 + 검색 청크 20개) 프리필 1,500 tok/s → 8.0 s
출력: 500 토큰 디코드 40 tok/s → 12.5 s
합계 ≈ 20.5 s — TTFT만으로 대기의 39%
이것이 검색 중심 로컬 설정의 모양새입니다. 뚱뚱한 프롬프트는 모델이 타이핑 전에 몇 초 동안 "생각"한다는 뜻입니다. 치료책, 효과 크기 순:
- 덜 검색하세요 — 상위 3–5개 청크가 보통 상위 20개를 이깁니다(RAG).
- 시스템 프롬프트를 고정해 엔진이 캐시된 프리픽스를 재사용하게 하세요.
- 기록을 다듬으세요. 오래된 턴은 전부 재생하는 대신 요약할 수 있습니다.
KV 캐시: 자라나는 스크래치패드
캐시가 없다면 토큰 N을 생성할 때 이전 N−1개 토큰 전체에 대한 어텐션을 매번 처음부터 다시 계산해야 합니다. 매 단계가 동일한 작업을 반복하고 총 비용은 길이에 제곱입니다. 대신:
- 프리필이 모든 프롬프트 토큰의 키와 값을 계산해 한 번 저장합니다.
- 각 디코드 단계는 새 토큰의 쿼리/키/값만 계산하고 그 K/V를 캐시에 덧붙입니다.
- 새 토큰의 어텐션은 그 쿼리를 저장된 모든 키와 내적하는 것입니다.
- 디코드된 토큰 하나, 순서대로: 임베딩하고, 캐시를 읽으며 층을 돌리고, 어휘로 사영하고, 샘플링합니다 — K/V를 덧붙이고, 반복.
이제 각 디코드 단계는 컨텍스트 길이에 선형입니다(캐시를 읽습니다). 제곱(전부 재계산)이 아니라. 대가는 캐시 자체로, 토큰마다 자랍니다:
KV bytes ≈ 2 × layers × kv_heads × head_dim × bytes_per_value × context
8B-class model (32 layers, 8 KV heads, 128 head dim, FP16):
2 × 32 × 8 × 128 × 2 B ≈ 128 KiB per token
→ ~2.6 GB at 20k context, ~17 GB at 128k context
이 캐시는 VRAM을 두고 가중치와 경쟁합니다. 짧은 프롬프트로는 잘 돌던 장비가 롱 컨텍스트 챗에서 죽습니다(KV 캐시, VRAM 계산).
배칭이 경제를 바꿉니다
가중치 8 GB를 읽어 토큰 하나를 뱉는 것은 낭비입니다. 같은 읽기로 여러 시퀀스를 한꺼번에 섬길 수 있습니다. 요청 8개를 배칭하면 가중치 스트림 하나가 토큰 8개를 만듭니다. 연산이 포화될 때까지 처리량은 거의 선형으로 오릅니다. 이것이 서빙 뒤에 있는 트릭의 전부입니다(서빙 API, 동시성). 노트북에서 혼자 쓰면 토큰당 운임을 온전히 내고, 서버는 동시 사용자들에게 분산시킵니다.
계기 읽기
| 계기 | 단계 | 주요 요인 |
|---|---|---|
| TTFT | 프리필 | 프롬프트 길이, 연산량, 프리픽스 캐시 히트 |
| 프롬프트 평가 tok/s | 프리필 | 연산량, 배치 크기 |
| 생성 tok/s | 디코드 | 메모리 대역폭, 가중치 바이트, 배치 |
| 처리량 (req/s) | 양쪽 | 배치 크기, 프리픽스 캐시 재사용, 병렬 슬롯 |
| 컨텍스트 메모리 | 양쪽 | 토큰당 KV 바이트, 캐시 양자화 |
튜닝 전에 측정하세요. 제대로 된 엔진은 요청별로 프롬프트 평가와 생성 속도를 로그에 남깁니다. 로그 1분이 어느 단계가 레이턴시를 먹는지 알려줍니다. 추측은 알려주지 못합니다.
튜닝은 단계를 따릅니다. 짧은 프롬프트와 프리픽스 캐싱이 TTFT를 고치고, 양자화와 작은 가중치가 디코드를 고칩니다(엔진 파라미터, 문제 해결).