Prefill과 Decode를 분리하면 LLM 서버의 병목이 보인다
LLM 서버가 느리다는 말만으로는 아무 조치도 할 수 없다. 8천 토큰 문서를 넣었을 때 첫 글자가 늦게 나오는 문제와, 첫 글자는 빨리 나오지만 이후 생성이 초당 몇 토큰밖에 안 되는 문제는 서로 다른 계산 단계에서 생긴다. 전자는 대개 prefill, 후자는 decode와 연결된다. 둘을 같은 평균 지연시간으로 합치면 GPU를 더 사도 사용자가 느끼는 병목은 그대로 남을 수 있다.
이 구분은 대규모 사업자만의 최적화가 아니다. 단일 GPU에 오픈 모델을 올린 팀도 요청 길이, 동시 사용자 수, KV 캐시 점유율을 보면 어느 단계가 막히는지 판단할 수 있다. 이 글은 분산 서빙 제품 이름을 나열하지 않고, prefill과 decode가 실제로 무엇을 소비하며 어떤 지표와 배치 정책으로 분리해서 봐야 하는지 설명한다.
한 요청 안에서 계산의 성격이 바뀌는 순간
사용자가 프롬프트를 보내면 모델은 입력 토큰 전체를 한꺼번에 통과시키며 각 층의 key와 value를 만든다. 이 구간이 prefill이다. 행렬 연산을 큰 덩어리로 실행할 수 있어 GPU 연산 유닛을 비교적 잘 채우지만, 입력이 길면 처리해야 할 토큰 수도 그대로 늘어난다. 반면 decode는 이미 만든 KV 캐시를 읽어 다음 토큰 하나를 계산하고, 새 토큰의 key와 value를 뒤에 붙이는 과정을 반복한다. 작은 연산을 수백 번 순차 실행하므로 메모리 대역폭과 스케줄링 오버헤드의 영향을 크게 받는다.
그래서 “프롬프트 1만 토큰, 출력 50토큰” 요청과 “프롬프트 200토큰, 출력 2천 토큰” 요청은 총 토큰 수가 비슷해도 서버 행동이 다르다. 첫 요청은 긴 prefill이 짧은 대화 요청을 가로막을 수 있고, 둘째 요청은 decode 슬롯과 KV 캐시를 오래 붙잡는다. 입력·출력 토큰을 하나의 usage 합계로만 저장하면 이 차이를 잃는다.
TTFT와 ITL을 같은 그래프에 섞지 말아야 하는 이유
첫 토큰까지 걸린 시간은 TTFT(time to first token), 생성 중 토큰 사이 간격은 ITL(inter-token latency)로 따로 본다. 사용자 체감도 다르다. 문서 요약에서 TTFT가 12초면 서비스가 멈춘 것처럼 보이지만 이후 100 tokens/s가 나와도 첫인상을 복구하기 어렵다. 실시간 코딩 보조에서는 TTFT 400ms라도 ITL이 들쭉날쭉하면 커서 앞 텍스트가 끊겨 보인다.
대시보드에는 최소한 input_tokens, output_tokens, queue_ms, prefill_ms, ttft_ms, decode_ms, itl_p50, itl_p95를 요청 단위로 남긴다. 엔진이 prefill 시간을 직접 제공하지 않으면 TTFT에서 큐 대기와 네트워크 시간을 분리해 근사할 수 있다. 평균만 보면 긴 요청 몇 개가 묻히므로 입력 길이 버킷(01K, 1K8K, 8K~32K 등)과 동시성 버킷으로 나눠 p95를 본다.
긴 문서 하나가 짧은 채팅을 멈추게 하는 방식
단순한 FIFO 배치에서는 먼저 들어온 긴 prefill이 GPU를 오래 점유한다. 그 사이 이미 decode 중인 채팅 요청의 다음 토큰도 기다린다. 사용자는 서버 전체 처리량이 충분한데도 스트리밍이 순간적으로 멎는 현상을 본다. 이를 head-of-line blocking이라고 이해하면 된다. 해결은 무조건 GPU 증설이 아니라 prefill을 작은 토큰 청크로 쪼개 decode 사이에 끼워 넣는 chunked prefill일 수 있다.
예를 들어 24K 토큰 계약서 분석과 300토큰 고객 상담이 같은 큐를 쓴다고 하자. 계약서를 24K 한 번에 처리하지 않고 1K 또는 2K 단위로 나누면 상담 decode가 중간에 실행될 기회를 얻는다. 다만 청크가 너무 작으면 커널 호출과 스케줄링 비용이 늘고 전체 처리량이 떨어진다. 청크 크기는 “작을수록 좋다”가 아니라 ITL p95 목표와 prefill 처리량 사이에서 정해야 한다.
분리 서빙이 의미 있는 트래픽 모양
Prefill과 decode를 서로 다른 GPU 풀에 배치하는 disaggregated serving은 두 단계의 자원 특성이 크게 다를 때 유효하다. 긴 입력이 몰리는 검색·문서 분석 제품은 prefill 풀이 순간적으로 바빠지고, 장문 생성 제품은 decode 풀이 오래 점유된다. 풀을 분리하면 각각에 맞는 배치 크기와 하드웨어를 적용할 수 있고 긴 prefill이 decode 스트림을 직접 막지 않는다.
그러나 KV 상태를 prefill 노드에서 decode 노드로 옮겨야 한다. 모델 층 수, KV 헤드 수, 시퀀스 길이가 커질수록 전송량이 커지고 네트워크 지연이 TTFT 이득을 먹는다. 단일 서버나 낮은 동시성에서는 분리로 얻는 이득보다 전송·운영 복잡성이 더 클 수 있다. 먼저 chunked prefill과 연속 배치를 적용한 뒤에도 입력 길이별 TTFT와 ITL의 목표가 충돌할 때 분리를 검토하는 편이 낫다.
시나리오 1: 사내 문서 질의와 일반 채팅이 한 클러스터를 쓸 때
사내 지식 검색은 검색 결과 20개를 붙여 입력이 18K까지 늘어나고, 일반 채팅은 대부분 1K 이하다. 한 큐에서 평균 지연만 보면 2.3초로 그럴듯하지만, 채팅 ITL p95가 문서 요청 유입 시점마다 180ms로 튄다. 이 경우 모델을 바꾸기 전에 요청 클래스를 interactive와 batch-document로 태깅하고 큐 예산을 분리한다.
interactive 요청에는 최대 입력 길이와 TTFT SLO를 두고, 문서 요청은 검색 결과를 재순위화해 실제로 넣는 토큰을 줄인다. 엔진에서 청크 prefill을 지원한다면 문서 클래스만 청크 대상으로 삼는다. 이후에도 충돌하면 prefill 전용 replica를 둔다. 중요한 검증은 전체 requests/s가 아니라 문서 유입 중에도 interactive의 itl_p95와 queue_ms_p95가 유지되는지다.
시나리오 2: 배치 요약 서비스가 GPU를 싸게 쓰지 못하는 이유
야간에 수천 개 문서를 요약하는 작업은 온라인 채팅과 다르게 첫 토큰 1초가 중요하지 않다. 그런데 온라인과 같은 스케줄러, 같은 최대 배치 지연을 사용하면 작은 요청을 급하게 실행하느라 prefill 배치를 충분히 모으지 못한다. 배치 워커는 문서를 입력 길이로 버킷팅하고 비슷한 길이끼리 묶어 패딩 낭비를 줄이는 것이 먼저다.
출력이 짧은 요약이면 prefill 비중이 높으므로 GPU utilization과 prefill tokens/s를 최적화한다. 반대로 보고서 생성까지 이어져 출력이 길다면 decode 슬롯 체류시간을 제한하고 중간 결과를 재개할 수 있게 job ID와 seed, 입력 해시를 남긴다. 온라인 SLO와 배치 완료시간을 한 autoscaling 신호로 쓰지 않는 것이 핵심이다.
벤치마크를 만들 때 고정해야 하는 것
단일 프롬프트로 tokens/s 한 줄을 얻는 벤치마크는 용량 계획에 부족하다. 입력 512/8K/32K, 출력 64/512/2K처럼 조합을 만들고 동시성 1/8/32에서 각각 TTFT와 ITL 분포를 측정해야 한다. 캐시가 따뜻한 요청과 차가운 요청, 동일 prefix가 반복되는 요청도 구분한다. 그렇지 않으면 prefix cache 효과를 모델 자체 성능으로 오인한다.
부하 생성기는 스트리밍 응답을 실제로 읽어야 한다. 클라이언트가 느리게 읽어 backpressure를 만들면 서버 decode와 네트워크 지연이 섞인다. GPU 메트릭도 사용률 하나가 아니라 HBM 사용량, KV 캐시 점유율, 배치 내 실행 시퀀스 수, preemption 횟수를 함께 기록한다. OOM이 없었다는 사실은 안정성의 충분조건이 아니다. KV 캐시가 90%를 넘을 때 재계산이나 요청 퇴출이 늘 수 있다.
운영자가 정할 수 있는 구체적인 판단 기준
첫째, 입력 길이가 늘 때 TTFT만 선형으로 악화되고 ITL은 안정적이면 prefill 용량과 입력 축약을 먼저 본다. 둘째, 입력 길이와 무관하게 동시성이 오를 때 ITL이 악화되면 decode 배치, 메모리 대역폭, KV 캐시 압력을 본다. 셋째, 긴 요청 유입 순간 짧은 요청 ITL이 튀면 chunked prefill이나 큐 격리가 후보가 된다.
분리 서빙은 prefill_ms, KV 전송 시간, decode_ms를 각각 측정할 수 있을 때만 도입한다. 전송 시간이 TTFT 예산의 큰 부분을 차지하면 토폴로지와 네트워크부터 바꿔야 한다. 최종 용량표에는 GPU당 평균 요청 수가 아니라 트래픽 클래스별 input/output 토큰 분포, TTFT p95 목표, ITL p95 목표, KV 캐시 최고 점유율을 적는다. 그래야 “GPU가 남는데 왜 느리지?”라는 질문에 단계별로 답할 수 있다.
부하 테스트 결과를 용량 계획으로 바꾸는 방법
측정값을 얻은 뒤에는 요청 하나의 평균 GPU 시간이 아니라 시간대별 토큰 도착률로 바꿔야 한다. 5분 창마다 input tokens와 output tokens를 합산하고, prefill tokens/s와 decode tokens/s의 피크를 따로 구한다. 같은 1천 requests/min도 짧은 채팅 1천 건과 32K 문서 1천 건은 전혀 다른 용량이다. 여기에 재시도 요청을 원래 요청과 구분하지 않으면 장애 시 필요한 용량을 정상 성장으로 오인한다.
Autoscaler가 GPU utilization만 보면 decode가 메모리 대역폭에 막힌 상태에서 사용률이 낮게 보일 수 있다. waiting_prefill_tokens, running_decode_sequences, KV cache 사용률, queue age를 함께 입력으로 사용한다. scale-out 시간보다 SLO가 짧다면 미리 확보한 headroom이 필요하다. 모델 로딩에 2분이 걸리는 서버를 TTFT가 무너진 뒤 늘리는 것은 늦다. 시간대별 트래픽 예측과 최소 replica를 함께 둔다.
용량 검토 문서에는 정상 p50뿐 아니라 가장 긴 허용 입력, 가장 긴 허용 출력, 동시성 급증, 한 replica 손실을 넣는다. replica 하나가 빠졌을 때 남은 노드의 KV 캐시가 연쇄 포화되는지 확인한다. 마지막으로 admission control을 정한다. 큐 대기가 이미 예산을 넘었으면 모든 요청을 받아 늦게 실패시키기보다 배치 작업을 지연시키거나 긴 입력을 명시적으로 제한하는 편이 사용자와 운영자 모두에게 낫다.
팀 회의에서 숫자로 답해야 할 질문
이 주제를 운영 회의에 올릴 때는 ‘좋아졌다’는 표현을 금지하고 TTFT p95, ITL p95, KV 캐시 점유율의 이전 값, 변경 후 값, 표본 수를 한 화면에 놓는다. 특히 긴 prefill 유입 시 짧은 decode 스트림를 재현한 부하 또는 회귀 샘플을 고정해야 다음 배포와 비교할 수 있다. 정상 요청만 모은 평균은 사고 직전의 위험을 설명하지 못한다. 실패·강등·재시도가 포함된 분모와 제외 규칙도 함께 기록한다.
담당자는 지표 변화에 대응하는 단 하나의 조치를 미리 연결한다. TTFT p95가 경계를 넘으면 어떤 배포를 되돌릴지, ITL p95가 늘면 어떤 큐나 샘플을 열어볼지, KV 캐시 점유율의 이상을 누가 확인할지를 runbook에 적는다. 알람이 다섯 개 동시에 울리는데 모두 ‘조사 필요’로 끝나면 제어면이 없는 것과 같다. 변경자는 release ID와 예상 효과를 남기고, 관찰 창이 끝난 뒤 실제 효과와 부작용을 기록한다.
마지막 검토에서는 비용과 품질을 같은 요청 ID로 연결한다. 비용이 줄었지만 실패 재시도가 늘었거나, 정확도가 올랐지만 p95가 제품 예산을 넘으면 성공이 아니다. 이 글에서 제시한 메트릭은 보고용 숫자가 아니라 다음 행동을 고르는 분기 조건이다. 임계치, 관찰 시간, rollback 조건까지 적혀 있을 때 비로소 구현 지식이 운영 정책이 된다.