운영자가 max_new_tokens에 손을 대는 이유는 단순하다. 긴 출력은 GPU를 오래 묶는다. TPOT(Time Per Output Token)는 동시 디코딩 중인 요청 수에 직선으로 비례하고, KV 캐시 점유 시간이 길수록 다음 요청의 대기도 늘어난다. 128로 묶어두면 배치를 빨리 순환시킬 수 있다는 생각은 합리적으로 보인다.
실제 운영에서 이 직관이 뒤집히는 경우가 있다.
KV 블록 예약과 스케줄러의 셈법
vLLM은 요청을 running 큐에 올리는 순간 KV 블록을 확보해야 한다. 이때 기준이 되는 숫자가 max_new_tokens다. 스케줄러는 입력 토큰 수 + max_new_tokens에 해당하는 블록이 여유 있는지 확인하고, 충분하면 요청을 수락한다. 기본 블록 크기가 16토큰이므로 max_new_tokens=128이면 8블록, max_new_tokens=512면 32블록이 예약 기준으로 잡힌다.
max_new_tokens를 작게 잡으면 블록 예약 요구량이 줄어드므로 스케줄러는 더 많은 요청을 동시에 수락한다. 배치가 커진다. 그런데 실제 출력이 설정값을 초과하는 요청이 생기면 vLLM은 해당 시퀀스를 preemption한다. 실행 중이던 시퀀스를 멈추고 KV 캐시를 비운 뒤 대기 큐로 돌려보낸다. 재처리 시 이전에 계산한 KV 캐시를 전부 다시 계산한다.
여러 요청이 동시에 블록 한계에 달하면 preemption이 연쇄적으로 발생한다. 스케줄러가 낙관적으로 많은 요청을 수락한 것이 연쇄 preemption의 원인이 된다. Sarathi-Serve(Agrawal et al., OSDI 2024)는 이 구조를 chunked prefill로 완화하는 접근을 제안하지만, 출력 길이 예측 오차 자체를 해결하지는 않는다.
preemption 비용
preemption이 비싼 이유는 재계산 때문이다. 8,000토큰 prefill을 처음부터 다시 돌리면 0.2~0.4초가 걸린다. swap 방식은 CPU로 KV 캐시를 옮겼다 복원하는데, PCIe 4.0 x16 대역폭(~32 GB/s) 기준으로도 긴 컨텍스트에서는 시간이 만만치 않다. vLLM v1 기본 모드는 recompute다.
실측 환경에서 GPU 캐시 사용률이 90%를 넘어 preemption이 시작되면 p99 TPOT가 200ms에서 8초까지 치솟는다. P99는 P50의 3.8배에 달하고, 그 격차의 약 70%가 preemption에서 비롯된다.
더 나쁜 경우는 "death spiral"이다. preemption된 요청이 큐로 돌아오면 새 요청과 경쟁하다 다시 preemption되는 사이클이 생긴다. GPU utilization 100%인데 처리량이 정체되는 상태가 이것이다.
세 가지 전략의 비용 구조
출력 길이를 제어하는 방법은 크게 셋이다.
| 전략 | KV 블록 점유 | Preemption 위험 | 생성 품질 |
|---|---|---|---|
| max_new_tokens 보수적 설정 | 실사용 < 예약 시 낭비 | 낮은 설정 → 동시 수락 증가 → 연쇄 preemption | 초과 시 절단 |
| EOS 기반 Early Stopping | 자연 종료 시 즉시 해제 | 상한 예측 어려워 max_model_len으로 잡히면 과다 예약 | 자연 종료 |
| Post-hoc Truncation | 전체 생성 동안 점유 | 낮음 | 생성은 완전, 결과만 절단 |
EOS 기반 early stopping이 이론상 가장 깔끔하다. EOS가 나오는 즉시 블록이 해제되므로 낭비가 없다. 단, 스케줄러가 수락 시점에 블록 예약 상한을 잡아야 하는데, 출력 길이를 모르는 상태에서 max_model_len을 상한으로 쓰면 동시 수락 수가 오히려 떨어진다.
Post-hoc truncation은 자원 낭비가 가장 크다. 2,000토큰 전체를 생성하는 동안 KV 블록이 점유된 채로 있다가, 완료 후 앞 512토큰만 반환한다. GPU 연산과 메모리를 전부 소모하고 결과만 자른다.
Reasoning 모델에서 Budget Forcing이 다른 이유
Qwen3, DeepSeek-R1 계열에서 thinking 토큰을 자르면 일반 truncation과 다른 방식으로 품질이 망가진다.
이 모델들은 <think>...</think> 블록에서 chain-of-thought 추론을 완성한 뒤 answer 토큰을 생성한다. Budget forcing은 thinking 블록이 완결되기 전에 강제로 </think> 구분자를 삽입하고 answer phase로 전환한다. 미완성 추론 위에서 답이 생성되는 구조다. 수학 문제라면 중간 계산을 포기한 채 정답을 추측하는 것과 같다.
DeepSeek-R1-32B 기준으로 thinking 토큰이 처음 500개 추가될 때마다 정확도가 약 +3.2% 오른다. 이 효과는 약 12,000토큰에서 포화된 뒤 음수로 전환된다. 이 곡선을 모른 채 512로 잘라버리면 추론이 가장 활발하게 진행되는 구간을 통째로 날리는 셈이다.
The Coupling Tax 논문은 이 현상을 고정된 총 출력 한도 안에서 thinking과 answer가 자원을 나눠 쓰는 구조 때문으로 분석한다. 둘이 같은 예산을 경쟁하면 어느 쪽도 충분히 확보하기 어렵다.
Qwen3는 thinking_budget 파라미터를 네이티브로 지원한다. /think 스위치와 함께 사용하면 모델 자체가 budget 안에서 추론을 완성하도록 학습되어 있어, 외부에서 강제로 자르는 것보다 품질 손실이 훨씬 작다. DeepSeek-R1은 네이티브 budget 제어가 없다. 외부에서 자르는 방법밖에 없고, 12,000토큰 미만에서 자를수록 추론 완성도가 선형으로 내려간다.
워크로드에 따른 선택
출력 길이 분포의 변동계수(CV = 표준편차/평균)가 0.3 이하인 워크로드라면 max_new_tokens를 평균 출력의 1.5~2배로 설정해도 preemption이 거의 발생하지 않는다. RAG 기반 요약이나 분류 레이블 추출처럼 출력 길이가 일정한 경우다. 스케줄러가 현실에 가깝게 블록을 예약할 수 있기 때문이다.
CV가 1.0을 넘는 워크로드 — reasoning, 코드 생성, 열린 대화 — 에서는 어떤 정적 값도 throughput과 품질을 동시에 만족시키기 어렵다.
한 가지 방법은 max_new_tokens를 넉넉하게 잡되(p90 출력의 1.5~2배) max_num_seqs를 줄여 동시 수락 수를 직접 제한하는 것이다. preemption 압력을 queueing latency로 전환하는 전략이고, 그러면 p99가 예측 가능해진다.
다른 방법은 앞단에 길이 예측 모델을 두고 요청마다 동적으로 max_new_tokens를 다르게 설정하는 것이다. Proxy Model-based Sequence Length Prediction처럼 작은 프록시 모델로 출력 길이를 먼저 추정하면 스케줄러가 훨씬 정확하게 블록을 예약할 수 있다. CV가 높은 워크로드에서 이 방식의 preemption 감소 효과가 가장 크게 나타난다.