Documents
Home>Documents>AI>Inference

LLM 서빙에서 p99 latency 꼬리가 길어지는 진짜 이유: 스케줄러 결정이 tail latency를 만드는 구조

13 min readAug 28, 2026Aug 28, 2026

서빙 엔지니어라면 이 상황이 낯설지 않을 것이다. Grafana를 열어보면 평균 TTFT는 400ms로 멀쩡한데, p99를 펼치는 순간 3초를 넘는 수치가 튀어나온다. GPU 메모리 점유율은 60% 수준이고 utilization도 이상 없다. 그래서 max_num_seqs를 줄여봤더니 throughput이 3,320 tok/s에서 2,180 tok/s로 떨어지면서 p99는 3,241ms에서 852ms로 급락했다. 직관과 반대다. 이 역설의 원인은 스케줄러가 배치를 구성하는 방식에 있다.

p99가 평균보다 수배 빠르게 나빠지는 구조

continuous batching 환경에서 스케줄러는 매 iteration마다 새 요청을 현재 배치에 추가할 수 있는지 판단한다. Orca(Yu et al., 2022)가 도입한 iteration-level scheduling은 고정 배치를 쓰는 static batching의 낮은 GPU 활용률 문제를 해결했지만, 새로운 구조적 문제를 남겼다. 배치 안에 긴 요청이 있으면 그 요청이 decode를 진행하는 동안 짧은 새 요청은 배치에 진입하지 못하고 큐에서 대기한다.

예를 들어 max_num_seqs=32로 설정했고 배치가 이미 32개 요청으로 가득 찼다고 하자. 새로 들어온 요청은 배치에서 하나가 빠질 때까지 기다린다. 배치 안에 1,024 토큰을 출력해야 하는 긴 요청이 있다면, 그 요청이 완료되기 전까지 큐가 계속 쌓인다. 이것이 Head-of-Line Blocking이다. 네트워크에서 패킷 하나가 전체 스트림을 지연시키는 현상과 구조가 같다.

p50과 p99가 수배 차이 나는 이유가 여기에 있다. 대부분의 요청은 배치에 빠르게 진입하지만, 운 나쁘게 긴 요청들이 배치를 점령한 순간에 들어온 요청은 그것들이 전부 완료될 때까지 기다린다. 트래픽이 고르면 이 확률이 낮아서 p50과 p99의 격차가 크지 않다. 트래픽이 몰리거나 요청 길이 분포의 분산이 크면, 긴 요청이 배치를 오래 점령하는 빈도가 높아지고 p99가 비선형으로 커진다. GPU 메모리를 더 확보해도 이 문제는 해결되지 않는다. 스케줄러가 배치에 수용하는 요청 수와 총 토큰 수를 바꾸지 않는 한, 메모리 여유가 늘어도 HOL Blocking의 빈도는 그대로다.

스케줄러 파라미터 세 가지가 지표를 각각 다른 방향으로 잡아당긴다

vLLM에서 배치 구성에 직접 영향을 주는 파라미터는 max_num_seqsmax_num_batched_tokens다.

max_num_seqs는 한 iteration에 처리할 최대 요청 수다. 값을 늘리면 배치 크기가 커져 GPU 행렬 연산의 병렬도가 높아지고 throughput이 오른다. 동시에 배치 슬롯 경쟁이 심해지면서 새 요청의 TTFT p99가 상승한다. 더 많은 요청이 동시에 decode를 진행하면 KV cache 메모리 압력이 커지고, preemption 발생 가능성도 올라간다.

max_num_batched_tokens는 한 iteration에 처리할 최대 총 토큰 수다. chunked prefill이 활성화된 경우 이 파라미터가 TTFT에 직접 작용한다. 값을 낮추면 prefill 청크가 작아져서 prefill이 decode 배치에 주는 inter-token latency 충격이 줄어든다. 단, 청크가 작을수록 GPU SM 활용률이 떨어진다. vLLM chunked prefill의 기본값은 2,048인데, 이 값이 ITL 최적화에 맞춰져 있어서 throughput을 최우선으로 할 경우 더 높은 값이 필요하다.

preemption은 KV cache가 부족해질 때 실행 중인 요청을 중단하고 cache를 비우는 동작이다. 발생하면 해당 요청의 TPOT이 급등한다. Sarathi-Serve 연구팀의 측정에 따르면, prefill이 배치에 끼어드는 것만으로도 inter-token latency가 decode-only 배치 대비 최대 28배까지 치솟는다. vLLM의 preemption은 recompute(중단 후 재계산)와 swap(CPU 이동) 두 방식을 지원하는데, swap은 CPU 전송 지연이 TPOT spike로 직접 나타나는 탓에 latency 측면에서는 recompute가 낫다.

세 파라미터가 세 지표에 미치는 방향:

파라미터 변경ThroughputTTFT p99TPOT p99
max_num_seqs 증가상승상승상승 (preemption 증가 시)
max_num_batched_tokens 감소하락하락하락
chunked prefill 청크 축소하락하락하락

실측: max_num_seqs 스윕으로 세 지표를 동시에 추적하다

A100 80GB, Llama-3-8B, 입력 512토큰 고정 조건에서 max_num_seqs를 8에서 64까지 올리며 측정했다. 출력 길이 균일(256토큰 고정)과 혼합(64~1,024토큰 균등분포) 두 시나리오를 함께 돌렸다.

균일 출력 (256토큰 고정)

max_num_seqsTTFT p50 (ms)TTFT p99 (ms)TPOT p50 (ms)TPOT p99 (ms)Throughput (tok/s)
811216831421,240
1622138732512,180
3241885234893,320
648363,241384873,580

max_num_seqs=32에서 64로 올릴 때 throughput은 3,320에서 3,580으로 8% 증가하는 데 그쳤다. 반면 TTFT p99는 852ms에서 3,241ms로 3.8배, TPOT p99는 89ms에서 487ms로 5.5배가 됐다. 8%의 throughput 이득을 위해 tail latency를 4~6배 희생하는 교환이다.

p50과 p99의 격차 변화가 이 문제를 더 직접적으로 보여준다. max_num_seqs=8에서 TTFT 격차는 56ms(168-112)다. max_num_seqs=64에서는 2,405ms(3,241-836)로 벌어진다. 배치가 커질수록 운 좋은 요청과 운 나쁜 요청의 경험이 극단적으로 갈린다.

임계점은 max_num_seqs=32 부근이다. 여기까지는 max_num_seqs를 두 배로 늘릴 때 throughput도 거의 두 배로 늘었다(1,240 → 2,180 → 3,320). GPU가 아직 충분히 활용되지 않아 배치 크기를 늘리는 것이 효율적인 구간이다. 32에서 64로 넘어가면 throughput 증분이 꺾이는 반면 p99는 수직 상승한다. 이 꺾이는 지점이 GPU 메모리 여유가 아니라 스케줄 큐 길이로 결정된다는 것이 측정에서 확인된다. 32→64 구간에서 GPU 메모리 사용률 변화는 3% 미만이었지만 preemption 발생 횟수는 0건에서 분당 12건으로 늘었다.

혼합 출력 트래픽 (max_num_seqs=32 고정)

트래픽 유형TTFT p50 (ms)TTFT p99 (ms)TPOT p50 (ms)TPOT p99 (ms)
균일 (256토큰)4188523489
혼합 (64~1,024토큰)5623,69851924

같은 max_num_seqs=32에서 혼합 트래픽의 TTFT p99는 균일 트래픽 대비 4.3배다. TPOT p99는 10배가 넘는다.

요청 길이 편차가 클수록 tail latency가 비선형으로 폭발하는 이유

균일 트래픽에서 max_num_seqs=32이면 배치 안의 요청들이 비슷한 속도로 decode를 마치고 빠져나간다. 새 요청은 빠르게 빈자리를 채우고, HOL Blocking이 발생해도 긴 대기 없이 해소된다.

혼합 트래픽에서는 다르다. 64토큰짜리 요청이 decode를 마치고 배치에서 빠져나가는 동안, 1,024토큰짜리 요청은 여전히 배치 슬롯을 차지하고 있다. 새로 들어온 요청은 빈 슬롯을 기다리는데, 긴 요청들이 슬롯을 오래 점유하는 탓에 교체 빈도가 낮다. 대기 큐에는 더 많은 요청이 쌓이고, 쌓인 요청들은 더 많은 긴 요청들과 마주치며 대기 시간이 늘어난다. 이 연쇄가 비선형이다. 긴 요청의 비율이 조금만 늘어도 큐 대기 시간은 그보다 훨씬 가파르게 증가한다.

실제 운영 트래픽에서 요청 길이 분포의 표준편차가 크면, 실험실 균일 트래픽으로 측정한 p99와 현실 p99의 격차는 예측보다 훨씬 커진다. 합성 트래픽 벤치마크를 신뢰하기 전에 요청 길이 분포부터 확인해야 하는 이유다.

Sarathi-Serve는 이 문제를 chunked prefill로 공략했다. prefill을 작은 청크로 쪼개어 decode 배치와 혼합하면, 긴 prefill이 한 번에 배치를 점령하는 일이 없어진다. max_num_seqs=64 조건에서 max_num_batched_tokens(청크 크기)별 TTFT p99와 GPU SM 활용률을 측정한 결과:

max_num_batched_tokensTTFT p99 (ms)GPU SM 활용률 (%)
51224558
1,02444172
2,04871584
무제한3,24195

청크를 512토큰으로 제한하면 TTFT p99가 245ms까지 내려가지만 GPU SM 활용률이 58%로 떨어진다. 무제한으로 두면 GPU는 95%를 쓰지만 p99는 3,241ms다. vLLM chunked prefill 기본값인 2,048이 이 사이 어딘가에 있는데, 이 값도 워크로드에 따라 최적이 아닐 수 있다.

SLO를 먼저 정하고 파라미터를 결정하라

TTFT < 200ms를 SLO로 잡은 서비스와 p99 TPOT < 50ms를 SLO로 잡은 서비스는 파라미터 방향이 반대다. TTFT 중심이라면 max_num_seqs를 낮추고(816) max_num_batched_tokens도 낮게(5121,024) 유지해야 한다. 배치에 진입하는 시간을 최소화하는 설정이고, GPU 활용률이 떨어지고 throughput이 줄어드는 것은 이 선택의 비용이다.

p99 TPOT 중심이라면 decode 연속성이 중요하다. max_num_seqs를 16~24 사이에 두고 preemption 정책을 recompute로 설정하는 편이 낫다. swap은 CPU 전송 지연이 TPOT spike로 직접 나타나기 때문이다.

파라미터를 조정하는 순서는 이렇다. TTFT p99가 SLO를 넘기 시작하면 max_num_batched_tokens를 절반으로 낮춰본다. TPOT p99가 SLO를 넘기 시작하면 max_num_seqs를 4씩 줄이면서 preemption 발생 횟수를 함께 모니터링한다. preemption이 0건인데도 TPOT p99가 높다면 배치 내 요청 길이 분포를 확인해야 한다. 길이 분산이 원인이라면 파라미터 조정보다 요청 길이별 라우팅이나 priority scheduling이 더 직접적인 해결책이다.

SLO 우선순위max_num_seqsmax_num_batched_tokenspreemption 정책
TTFT < 200ms8~16512~1,024recompute
p99 TPOT < 50ms16~241,024~2,048recompute
Throughput 최대화48~64무제한recompute

이 표의 수치는 Llama-3-8B / A100 80GB 기준이다. 모델이 크거나 GPU 메모리 대역폭이 다르면 임계 max_num_seqs가 달라진다. 어느 지표가 먼저 SLO를 위반하기 시작하는지를 기준으로 어느 파라미터를 어떤 방향으로 움직일지를 결정하는 로직을 가져가야 한다. 수치는 직접 스윕해야 나온다.

Tags
LLMInferencevLLM서빙아키텍처KV 캐시GPU