Documents
Home>Documents>AI>Inference

max_new_tokens를 보수적으로 잡으면 throughput이 왜 떨어지는가: 출력 길이 예측 불확실성의 서빙 비용

10 min readSep 4, 2026Sep 4, 2026

autoregressive 생성은 EOS 토큰이 나오기 전까지 출력이 몇 토큰이 될지 알 수 없다. prefill 단계가 끝나는 시점에 decode 단계에서 얼마나 많은 KV 슬롯이 필요한지 확정할 방법이 없다. 서빙 시스템은 이 불확실성에 대해 반드시 어떤 가정을 세워야 하고, 그 가정이 max_new_tokens 설정값이다.

예약 기반 메모리 관리의 구조

vLLM의 KV 캐시 관리는 블록(page) 단위다. 기본값 block_size=16이고, 요청이 스케줄러에 들어오면 해당 요청의 max_new_tokens를 기준으로 페이지를 미리 확보한다:

num_reserved_pages = ceil(max_new_tokens / block_size) × batch_size

max_new_tokens=128이면 요청당 8페이지, max_new_tokens=2048이면 128페이지다. KV 풀이 총 1,000페이지라면 최대 동시 처리 가능 요청 수는 다음과 같이 달라진다:

max_new_tokens요청당 예약 페이지최대 동시 요청 수
1288~125
51232~31
2048128~7

예약은 실제 사용량과 무관하게 decode가 끝날 때까지 묶인다. ShareGPT 출력 길이 분포 분석에서 실제 출력 토큰의 median은 21 토큰이다. max_new_tokens=512로 설정하면 절반 이상의 요청이 예약 공간의 5% 미만만 쓰면서 32페이지를 점유한다.

PagedAttention이 해결한 문제는 내부 단편화(internal fragmentation)다. 하나의 요청 내에서 미사용 메모리가 조각조각 남는 현상을 페이지 단위 할당으로 줄였다. 그런데 over-reservation으로 발생하는 외부 단편화(external fragmentation) — 다른 요청이 쓸 수 있는 물리 메모리가 예약 때문에 잠기는 현상 — 은 여전하다. 페이지가 16토큰 단위로 잘게 쪼개져 있어도, 미래 decode에 쓰일 거라 예상해 선점한 페이지는 다른 요청에 배정할 수 없다.

max_new_tokens가 GPU 활용률을 직접 꺾는 경로

LLM 서빙에서 throughput은 배치 크기와 거의 선형 관계를 가진다. 같은 forward pass에 요청이 더 많을수록 같은 연산 시간에 더 많은 토큰이 생성된다. 최대 동시 요청 수가 줄면 배치가 작아지고, 배치가 작아지면 throughput이 같은 비율로 떨어진다.

Orca (Yu et al., OSDI 2022) 이후 iteration-level scheduling이 표준이 됐지만, 이 스케줄러도 매 iteration마다 새 요청을 배치에 넣으려면 KV 풀에 공간이 남아있어야 한다. max_new_tokens가 크면 KV 풀이 빠르게 포화되고 새 요청은 큐에서 기다린다. TTFT가 올라가는 경로가 여기서 생긴다.

Sarathi-Serve (Agrawal et al., OSDI 2024)는 chunked prefill을 통해 이 병목을 개선했다. 긴 prefill을 균등하게 분할해 decode 배치에 끼워 실행하고, 스케줄러가 stall 없이 새 요청을 추가할 수 있는 구조다. 이 방식이 vLLM 대비 Mistral-7B에서 2.6배, Yi-34B에서 3.7배, Falcon-180B에서 최대 5.6배의 처리 용량(serving capacity) 차이를 만들었다 — 모두 모델이나 GPU를 바꾼 게 아니라 스케줄러와 prefill 방식만 달리한 결과다.

ShareGPT 출력 길이 분포가 이 설정 문제를 구체화한다:

  • median: 21 tokens
  • P90: median의 약 4.6배 ≈ 97 tokens
  • P99: median의 약 10.8배 ≈ 226 tokens

P99가 226 토큰인 워크로드에 max_new_tokens=2048을 기본값으로 잡으면, 99%의 요청이 실제 필요한 공간의 9배 이상을 예약하는 셈이다.

스케줄러가 선점을 꺼내는 순간

max_new_tokens를 작게 잡으면 초반 예약은 줄지만, 실제 출력이 예약을 넘을 때 스케줄러가 선점(preemption)을 발동한다. vLLM에는 두 전략이 있다.

recompute: 해당 요청의 KV 캐시 블록을 반납하고 WAITING 큐로 돌려보낸다. 재입력 시 prefill을 처음부터 다시 실행해 KV를 재생성한다. 비용은 시퀀스 길이 s에 대해 O(s²)다.

swap-out: KV 블록을 PCIe를 통해 CPU DRAM으로 복사하고 SWAPPED 큐로 이동한다. 재입력 시 반대 방향으로 복사해 resume한다. 비용은 O(s)다.

vLLM 분석에 따르면 시퀀스 길이 4,000 토큰 아래에서는 recompute가 빠르고 그 이상에서는 swap이 유리하다. 두 전략 모두 비용이 발생하며, 선점이 일어나는 순간 해당 요청의 latency는 prefill 재실행 또는 PCIe 복사 시간만큼 늘어난다.

선점이 tail latency에 집중적으로 영향을 미치는 이유가 있다. 긴 출력을 생성하는 요청은 예약 범위를 초과할 확률이 높고, 그 요청들은 이미 대량의 KV 블록을 보유한 상태에서 preempt되므로 재실행 비용이 크다. p50 요청은 대개 예약 안에서 끝나는데, p99 요청은 선점과 재실행을 반복하면서 latency가 폭발한다.

길이 예측을 파이프라인에 끼워 넣으면

출력 길이를 prefill 전에 예측해 그 예측값 기반으로 KV 예약량을 요청별로 달리 설정하면 이 제약을 완화한다. 모든 요청에 동일한 max_new_tokens를 적용하는 대신, 프롬프트 특성으로 예상 출력 길이를 추정해 짧은 요청에는 적게, 긴 요청에는 더 많이 예약한다.

Uncertainty-Aware Output Length Prediction (arXiv 2604.00499)은 log-t 분포로 출력 길이 분포를 모델링하는 방식을 제안한다. 예측된 μ(R² = 0.82)와 σ(R² = 0.76) 파라미터를 기반으로 예약량을 요청별로 조정했을 때, FIFO 스케줄러 대비 per-token latency를 2.31배 개선하고 offline throughput을 1.42배 올렸다고 보고한다.

Sarathi-Serve의 chunked prefill은 예측 기반 예약량 조정과 독립적인 접근이다. 예약 자체를 줄이는 게 아니라 기존 예약 구조 아래서 스케줄러가 낭비 없이 배치를 채우는 방식이고, 두 기법을 결합하면 효과가 누적된다.

예측이 항상 맞지 않으므로 오차 여유를 어떻게 설계하느냐가 실제 trade-off를 결정한다. 예측값의 p75 신뢰 구간으로 예약하면 재선점 빈도가 높아지고, p95로 잡으면 over-reservation이 남는다. 어느 쪽 비용을 더 감수할 것인지는 워크로드의 출력 길이 분산과 latency SLO에 따라 다르다.

트래픽 분포에 맞춰 max_new_tokens를 잡는 방법

실용적 접근은 실제 출력 길이 분포를 먼저 측정하는 것에서 시작한다:

import numpy as np
output_lengths = [len(r.outputs[0].token_ids) for r in responses]
p50, p90, p95, p99 = np.percentile(output_lengths, [50, 90, 95, 99])
print(f"p50={p50:.0f}, p90={p90:.0f}, p95={p95:.0f}, p99={p99:.0f}")

대화형 QA나 요약처럼 출력이 짧고 분포가 가벼운 워크로드라면 max_new_tokens를 p95로 설정하는 게 합리적이다. 5%의 요청에서 선점이 생기지만 KV 활용률이 크게 오르고 동시 처리 요청 수도 늘어난다. 코드 생성이나 긴 문서 출력은 분포의 분산이 크므로 p99 이상으로 잡거나, 길이 예측 레이어를 도입해 요청별로 예약량을 다르게 설정한다.

선점 전략은 서버 시작 시 명시적으로 지정한다:

vllm serve <model> --preemption-mode recompute   # 기본값 (vLLM v1 이후)
vllm serve <model> --preemption-mode swap        # 시퀀스가 4,000 토큰 이상일 때 유리

p95에 max_new_tokens를 맞추면 처리량이 오르는 대신, 초과하는 5%를 어떻게 처리할 것인지 — recompute, swap, 요청 거절 중 어느 쪽인지 — 는 서빙 엔진이 자동으로 결정하는 영역이 아니다. 이 정책을 명시적으로 설정해두지 않으면 선점이 조용히 p99 latency를 잡아먹는다.

Tags
LLMInferenceGPU서빙vLLMKV 캐시메모리아키텍처