첫 글자 지연은 왜 주로 Prefill을 보나

← 전체 목차 · 이전: 추측 디코딩은 왜 빨라지면서 답은 바뀌지 않나 · 다음: 컨텍스트 윈도우는 창고가 아니라 주의력 예산이다

그럴듯해 보이는 오답: "Prefill은 모델 파라미터를 로딩하고 프롬프트를 밀어 넣는 과정이니, 첫 글자 지연은 당연히 Decode 단계를 봐야 한다. 누가 먼저 첫 token을 생성하느냐가 속도다."

틀린 이유:

  • 첫 글자 지연의 90%가 Prefill 단계에서 발생한다.
  • Decode가 첫 token을 생성하는 데는 행렬 곱 한 번이면 된다.
  • Prefill은 프롬프트 전체의 이력에 대해 전량 주의력 계산을 해야 한다.

두 단계의 차이

PrefillDecode
계산 방식입력 시퀀스 전체를 한 번에 병렬 주의력 계산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 TokenPrefill이 처음부터 4200개를 전부 재계산이전 턴의 Key/Value 행렬이 캐싱되어 있어 신규 200개만 계산
캐시 없음: 4200의 제곱
캐시 있음: 200 × 4200
첫 글자 지연: 2초  →  0.3초

적중률이 다중 턴 대화 체감의 핵심 지표다

  • Cache 적중 조건: 프롬프트 접두사가 글자 단위로 완전히 일치할 것.
  • 두 가지 전형적인 파괴 방식:
    1. 매 턴 대화 앞에서 시스템 프롬프트를 수정한다.
    2. 다중 턴 사이에 새로운 안내 텍스트를 삽입한다.
  • 결과 사슬:
접두사 단절 → 적중률 0 → Prefill이 처음부터 계산으로 회귀 → 첫 글자 지연 폭증

프로덕션 현상과 근본 원인: 많은 팀이 출시 후 첫 글자 지연의 변동 폭이 크다고 하는데, 근본 원인은 프롬프트 조립 로직이 불안정Cache 적중률이 오르내리는 것이다.

가점 요소: Prefix Caching의 구현 디테일

"메모리에 저장했다고 적중은 아니다." 진짜 가점 요소는 세 가지 구현 디테일을 말할 수 있는 것이다.

  1. 최소 접두사 매칭 길이 설정
  2. 요청 간 Cache 공유 전략
  3. 긴 텍스트 시나리오에서의 Cache 축출(eviction) 전략이 적중률에 미치는 영향

전형적인 나쁜 경로

출시 후 KV Cache가 전혀 적중하지 않아 한참 원인을 찾았더니, 원인은 매 요청 앞에 밀리초 단위 타임스탬프를 붙인 것이었다 — 접두사가 영원히 정렬되지 않는다.

{"prompt": "[1692345678.123] System: You are a helpful assistant..."}
{"prompt": "[1692345680.456] System: You are a helpful assistant..."}
                ↑ 타임스탬프만 다름 → 접두사가 영원히 어긋난다

핵심 통찰

  1. "첫 글자 지연은 누가 결정하는가"의 답은 복잡도 차수가 결정하지 직관적 순서가 결정하지 않는다 — 직관적으로는 Decode가 첫 글자를 뱉으니 Decode가 결정하는 것처럼 보이지만, 소요 시간을 결정하는 것은 계산량이다. Prefill은 O(N²)의 전량 주의력이고 Decode의 첫 단계는 1×N일 뿐이다. 순서가 뒤라고 소요 시간이 뒤인 것은 아니다.
  2. Prefill과 Decode의 병목 자원이 다르므로 최적화 수단도 서로 바꿔 쓸 수 없다 — Prefill의 병목은 GPU 행렬 곱 처리량(연산력 제약), Decode의 병목은 GPU 메모리 대역폭(Memory Bound)이다. Decode 최적화 수단을 Prefill에 쓰거나 그 반대로 하면 효과가 없다.
  3. KV Cache의 진짜 가치는 "메모리 절약"이 아니라 Prefill의 계산량을 "전량 이력"에서 "증분 부분"으로 바꾸는 것이다 — 계산 예시에서 2턴이 4200²에서 200×4200으로 떨어졌다. 수익은 "이미 계산한 이력을 다시 계산하지 않는 것"에서 오며, 첫 글자 지연이 자릿수 단위로 떨어질 수 있는 이유다.
  4. Cache가 있다고 적중하는 것은 아니며, 적중률이야말로 관측 가능한 업무 지표다 — 적중 조건은 프롬프트 접두사의 완전 일치다. 접두사가 한 번 단절되면 Prefill은 곧바로 처음부터 계산으로 회귀하고 첫 글자 지연은 원래대로 돌아간다. 모니터링해야 할 것은 적중률이지 "Cache를 켰는가"가 아니다.
  5. 프로덕션 첫 글자 지연의 "변동"은 모델 문제가 아니라 프롬프트 조립 로직 문제인 경우가 많다 — "많은 팀이 출시 후 첫 글자 지연 변동이 크다"의 근본 원인은 프롬프트 조립 로직의 불안정이다. 지연 지표의 분산이 프롬프트 구성 안정성을 점검하는 입구다.
  6. 프롬프트 머리에 쓰이는 "매번 변하는 것"은 무엇이든 캐시를 전부 무효화한다 — 밀리초 타임스탬프는 가장 극적인 사례일 뿐이다. 자체로는 token을 거의 차지하지 않으면서 4000 token의 이력을 단 한 번도 적중하지 못하게 만든다. 접두사의 안정성이 접두사의 길이보다 중요하다.
  7. "외운 사람"과 "해본 사람"을 가르는 것은 구현의 경계 조건을 말할 수 있는가다 — 최소 접두사 매칭 길이, 요청 간 공유 전략, 긴 텍스트에서의 축출 전략은 Prefix Caching을 실제로 설정해 본 사람만 마주치는 파라미터이지 개념 층위의 복창이 아니다.

엔지니어링 실전 Tips

  • 먼저 단계를 나누고 나서 최적화할 것: 첫 글자 지연은 Prefill과 Decode 첫 token의 합이다. 쪼개지 않으면 최적화가 어느 구간에 꽂히는지 판단할 수 없다. 경험칙은 약 90%가 Prefill이다.
  • "변수"를 프롬프트 꼬리로 몰 것: 접두사가 완전히 일치해야 적중하므로 타임스탬프, 요청 ID, 랜덤 인사말처럼 매번 변하는 내용은 접두사에 있으면 안 된다.
  • 프롬프트 조립 로직은 "결정론적으로 재현 가능"해야 한다: 같은 세션 상태에서 완전히 동일한 접두사 문자열이 나와야 한다. 조립 로직의 불안정이 적중률이 오르내리는 근원이다.
  • "저장했다"를 "적중했다"로 착각하지 말 것: 실제로 적중을 결정하는 것은 최소 접두사 매칭 길이, 공유 전략, 축출 전략 세 가지 설정이다.
  • 긴 텍스트 시나리오는 축출 전략을 따로 볼 것: 이력이 길수록 축출되기 쉽고, 축출이 일어나는 그 순간 다음 턴은 전량 Prefill이다.
  • 표현 순서를 고정할 것: 계산 방식 → 각자의 병목 자원 → 첫 글자 지연 귀인 → 다중 턴에서의 KV Cache 최적화. 이 순서로 말하면 논리가 끊기지 않는다.