첫 글자 지연은 왜 주로 Prefill을 보나
← 전체 목차 · 이전: 추측 디코딩은 왜 빨라지면서 답은 바뀌지 않나 · 다음: 컨텍스트 윈도우는 창고가 아니라 주의력 예산이다
그럴듯해 보이는 오답: "Prefill은 모델 파라미터를 로딩하고 프롬프트를 밀어 넣는 과정이니, 첫 글자 지연은 당연히 Decode 단계를 봐야 한다. 누가 먼저 첫 token을 생성하느냐가 속도다."
틀린 이유:
- 첫 글자 지연의 90%가 Prefill 단계에서 발생한다.
- Decode가 첫 token을 생성하는 데는 행렬 곱 한 번이면 된다.
- Prefill은 프롬프트 전체의 이력에 대해 전량 주의력 계산을 해야 한다.
두 단계의 차이
| Prefill | Decode |
|---|
| 계산 방식 | 입력 시퀀스 전체를 한 번에 병렬 주의력 계산 | token 단위 직렬 생성 |
| 매 단계의 계산 | 모든 입력 token 두 쌍 사이의 주의력 | 현재 token과 이력의 주의력만 |
| 입력 길이와의 관계 | 제곱 관계 O(N²) | 선형(매 단계 token 하나만 추가) |
| 병목 자원 | GPU 행렬 곱 처리량 (연산력 제약) | GPU 메모리 대역폭 (Memory Bound) |
| 첫 글자 지연 기여 | 지배적 | 첫 token 생성은 "거의 공짜" |
결론: 프롬프트가 길수록 Prefill이 느려지고, 첫 글자 지연은 Prefill이 지배한다.
4000 Token 계산 예시
시스템 프롬프트 + 다중 턴 이력을 합쳐 4000 token이라고 하자.
Prefill 단계:
4000개 Token 두 쌍 사이에 주의력 계산을 한 번
주의력 행렬 = 4000 × 4000, 한 번에 계산
Decode 단계(첫 token 생성):
새 Token은 기존 4000개 이력 Token과 각각 한 번씩만 계산
→ 계산량이 4000배 차이
결론: 첫 글자 지연의 절대적 대부분이 Prefill에 있다
KV Cache가 첫 글자 지연을 어떻게 바꾸는가
다중 턴 대화에서는 매 턴 사용자 발언 후 이력 Token이 누적된다.
| 턴 | 이력 길이 | KV Cache 없음 | KV Cache 있음 |
|---|
| 1턴 | 4000 Token | 전부 Prefill 계산, 첫 글자 2초 | — |
| 2턴 | 4200 Token | Prefill이 처음부터 4200개를 전부 재계산 | 이전 턴의 Key/Value 행렬이 캐싱되어 있어 신규 200개만 계산 |
캐시 없음: 4200의 제곱
캐시 있음: 200 × 4200
첫 글자 지연: 2초 → 0.3초
적중률이 다중 턴 대화 체감의 핵심 지표다
- Cache 적중 조건: 프롬프트 접두사가 글자 단위로 완전히 일치할 것.
- 두 가지 전형적인 파괴 방식:
- 매 턴 대화 앞에서 시스템 프롬프트를 수정한다.
- 다중 턴 사이에 새로운 안내 텍스트를 삽입한다.
- 결과 사슬:
접두사 단절 → 적중률 0 → Prefill이 처음부터 계산으로 회귀 → 첫 글자 지연 폭증
프로덕션 현상과 근본 원인: 많은 팀이 출시 후 첫 글자 지연의 변동 폭이 크다고 하는데, 근본 원인은 프롬프트 조립 로직이 불안정해 Cache 적중률이 오르내리는 것이다.
가점 요소: Prefix Caching의 구현 디테일
"메모리에 저장했다고 적중은 아니다." 진짜 가점 요소는 세 가지 구현 디테일을 말할 수 있는 것이다.
- 최소 접두사 매칭 길이 설정
- 요청 간 Cache 공유 전략
- 긴 텍스트 시나리오에서의 Cache 축출(eviction) 전략이 적중률에 미치는 영향
전형적인 나쁜 경로
출시 후 KV Cache가 전혀 적중하지 않아 한참 원인을 찾았더니, 원인은 매 요청 앞에 밀리초 단위 타임스탬프를 붙인 것이었다 — 접두사가 영원히 정렬되지 않는다.
{"prompt": "[1692345678.123] System: You are a helpful assistant..."}
{"prompt": "[1692345680.456] System: You are a helpful assistant..."}
↑ 타임스탬프만 다름 → 접두사가 영원히 어긋난다
핵심 통찰
- "첫 글자 지연은 누가 결정하는가"의 답은 복잡도 차수가 결정하지 직관적 순서가 결정하지 않는다 — 직관적으로는 Decode가 첫 글자를 뱉으니 Decode가 결정하는 것처럼 보이지만, 소요 시간을 결정하는 것은 계산량이다. Prefill은 O(N²)의 전량 주의력이고 Decode의 첫 단계는 1×N일 뿐이다. 순서가 뒤라고 소요 시간이 뒤인 것은 아니다.
- Prefill과 Decode의 병목 자원이 다르므로 최적화 수단도 서로 바꿔 쓸 수 없다 — Prefill의 병목은 GPU 행렬 곱 처리량(연산력 제약), Decode의 병목은 GPU 메모리 대역폭(Memory Bound)이다. Decode 최적화 수단을 Prefill에 쓰거나 그 반대로 하면 효과가 없다.
- KV Cache의 진짜 가치는 "메모리 절약"이 아니라 Prefill의 계산량을 "전량 이력"에서 "증분 부분"으로 바꾸는 것이다 — 계산 예시에서 2턴이 4200²에서 200×4200으로 떨어졌다. 수익은 "이미 계산한 이력을 다시 계산하지 않는 것"에서 오며, 첫 글자 지연이 자릿수 단위로 떨어질 수 있는 이유다.
- Cache가 있다고 적중하는 것은 아니며, 적중률이야말로 관측 가능한 업무 지표다 — 적중 조건은 프롬프트 접두사의 완전 일치다. 접두사가 한 번 단절되면 Prefill은 곧바로 처음부터 계산으로 회귀하고 첫 글자 지연은 원래대로 돌아간다. 모니터링해야 할 것은 적중률이지 "Cache를 켰는가"가 아니다.
- 프로덕션 첫 글자 지연의 "변동"은 모델 문제가 아니라 프롬프트 조립 로직 문제인 경우가 많다 — "많은 팀이 출시 후 첫 글자 지연 변동이 크다"의 근본 원인은 프롬프트 조립 로직의 불안정이다. 지연 지표의 분산이 프롬프트 구성 안정성을 점검하는 입구다.
- 프롬프트 머리에 쓰이는 "매번 변하는 것"은 무엇이든 캐시를 전부 무효화한다 — 밀리초 타임스탬프는 가장 극적인 사례일 뿐이다. 자체로는 token을 거의 차지하지 않으면서 4000 token의 이력을 단 한 번도 적중하지 못하게 만든다. 접두사의 안정성이 접두사의 길이보다 중요하다.
- "외운 사람"과 "해본 사람"을 가르는 것은 구현의 경계 조건을 말할 수 있는가다 — 최소 접두사 매칭 길이, 요청 간 공유 전략, 긴 텍스트에서의 축출 전략은 Prefix Caching을 실제로 설정해 본 사람만 마주치는 파라미터이지 개념 층위의 복창이 아니다.
엔지니어링 실전 Tips
- 먼저 단계를 나누고 나서 최적화할 것: 첫 글자 지연은 Prefill과 Decode 첫 token의 합이다. 쪼개지 않으면 최적화가 어느 구간에 꽂히는지 판단할 수 없다. 경험칙은 약 90%가 Prefill이다.
- "변수"를 프롬프트 꼬리로 몰 것: 접두사가 완전히 일치해야 적중하므로 타임스탬프, 요청 ID, 랜덤 인사말처럼 매번 변하는 내용은 접두사에 있으면 안 된다.
- 프롬프트 조립 로직은 "결정론적으로 재현 가능"해야 한다: 같은 세션 상태에서 완전히 동일한 접두사 문자열이 나와야 한다. 조립 로직의 불안정이 적중률이 오르내리는 근원이다.
- "저장했다"를 "적중했다"로 착각하지 말 것: 실제로 적중을 결정하는 것은 최소 접두사 매칭 길이, 공유 전략, 축출 전략 세 가지 설정이다.
- 긴 텍스트 시나리오는 축출 전략을 따로 볼 것: 이력이 길수록 축출되기 쉽고, 축출이 일어나는 그 순간 다음 턴은 전량 Prefill이다.
- 표현 순서를 고정할 것: 계산 방식 → 각자의 병목 자원 → 첫 글자 지연 귀인 → 다중 턴에서의 KV Cache 최적화. 이 순서로 말하면 논리가 끊기지 않는다.