Documents
Home>Documents>AI>Inference

Qwen3 서빙의 숨겨진 비용: /think 토큰이 출력 길이 분산을 폭발시키는 구조와 대응법

13 min readSep 6, 2026Sep 6, 2026

Qwen3를 vLLM으로 서빙하다 보면 이상한 지표가 튀기 시작한다. P99 TTFT가 P50보다 10배 넘게 느려지고, KV 캐시 점유율이 갑자기 90%를 뚫는다. 배치 크기가 절반으로 줄어드는데 요청 수가 늘어난 것도 아니다. 원인을 뜯어보면 대부분 thinking 모드 요청 하나다. 이 요청이 수만 토큰을 decode하는 동안 배치 내 다른 모든 요청이 대기해야 하는 구조가 latency 지표를 망가뜨린다.

Qwen3 기술 보고서(arXiv:2505.09388)는 thinking 체인 길이가 늘어날수록 성능이 단조 증가한다고 보고한다. 서빙 엔지니어 입장에서는 그게 바로 문제다. 모델이 더 생각할수록 출력이 길어지고, 출력이 길어질수록 서빙 스택이 압박받는다.

Thinking 모드가 출력 길이에 하는 일

Qwen3의 <think> 블록은 실제 응답이 나오기 전에 모든 사고 과정을 출력한다. 공식 배포 가이드에서 권장하는 max_tokens는 일반 태스크에서 32,768이고, 수학·코딩 경쟁 수준에서는 38,912로 올라간다. DeepSeek-R1이나 QwQ 계열처럼 reasoning에 최적화된 모델은 단일 요청에서 10,000토큰 이상의 thinking 체인을 자주 생성한다.

Qwen3-8B를 100개 프롬프트(코딩, 수학, 일반 대화, 요약 혼합)로 실행하면 thinking on/off 사이의 출력 토큰 분포가 완전히 갈린다:

조건중앙값p90p99최대
thinking off280 tok750 tok1,800 tok~3,500 tok
thinking on (혼합)2,800 tok8,500 tok19,000 tok32,768 tok
thinking on (수학·코딩)6,000 tok16,000 tok30,000 tok38,912 tok

평균이 아니라 분산을 봐야 한다. Thinking off는 거의 모든 요청이 2,000토큰 아래에서 끝난다. Thinking on에서는 p99가 p50의 7배 이상이고, 극단 요청과 일반 요청 사이에 10배 이상의 차이가 난다. 이 분산이 배치 스케줄러를 힘들게 하는 직접 원인이다. 중앙값 2배짜리 요청은 스케줄러가 처리할 수 있다. 중앙값 10배에 p99가 60배인 요청은 다른 문제다.

arXiv:2601.10274 논문은 이 문제를 M/G/1 대기열로 모델링했다. 서비스 시간은 allocated 토큰 수에 affine하게 비례하지만, 정확도는 수확 체감 곡선을 따른다. MATH-500 기준으로 thinking 길이가 0에서 5,000토큰으로 늘어날 때 정확도가 0.4에서 0.6 수준으로 올라가는 반면, 10,000토큰을 넘어가면 개선 폭이 급격히 줄어든다. 모든 요청에 최대 thinking budget을 주는 것이 throughput 관점에서도, 정확도 관점에서도 최적이 아닌 이유다.

KV 캐시 점유와 preemption

vLLM의 PagedAttention은 KV 캐시를 고정 크기 블록(기본 16토큰)으로 나눠 관리한다. Decode 단계에서 토큰이 생성될 때마다 블록이 하나씩 할당되므로, thinking 요청 하나가 10,000토큰을 생성하면 625블록을 독점한다.

배치 크기 16에 max_tokens=8,192로 설정하면 이론상 최대 16 × 8,192 = 131,072토큰 분량의 KV 캐시 블록을 동시에 예약해야 한다. GPU 메모리에서 모델 가중치를 제외하고 남은 공간이 이 예약량을 감당하지 못하는 순간 스케줄러는 일부 요청을 preemption한다.

vLLM V1에서 preemption 기본 전략은 RECOMPUTE다. SWAP(디스크 오프로드) 대신 prefill 단계를 재실행한다. 짧은 요청이라면 overhead가 낮지만, thinking 요청이 긴 prefill을 recompute해야 한다면 그 비용이 TTFT에 그대로 더해진다. gpu_cache_usage_perc가 90%를 초과하기 시작하면 preemption 빈도가 급격히 늘어난다.

단편화도 문제다. 짧게 끝난 요청이 반납한 블록 사이에 긴 thinking 요청의 블록들이 끼어들면서 연속 메모리처럼 쓰기 어려운 구멍들이 생긴다. Free 블록 수가 threshold 이하로 떨어지면 스케줄러는 새 요청을 받지 못하고 대기 큐가 쌓이기 시작한다.

스케줄러가 망가지는 구체적 시나리오

Continuous batching은 요청이 끝나는 즉시 새 요청을 배치에 합류시킨다. 하지만 thinking 요청 하나가 20,000토큰 동안 decode를 계속하면, 그 20,000 step 동안 배치 슬롯 하나가 고정된다.

TTFT와 TPOT에서 각각 다른 방식으로 문제가 나타난다. 새 요청의 TTFT는 thinking 요청이 decode 중인 동안 prefill 스케줄링 기회를 얻기 어렵다. Chunked prefill이 꺼진 환경에서는 더 심해진다 — prefill 전체가 배치 한 step을 잡아먹기 때문이다. 반면 이미 decode 중인 다른 요청들의 TPOT는 thinking 요청이 배치를 꽉 채우고 있을 때 GPU 처리량이 희석되면서 늘어난다.

LLM tail latency 분석에서 확인되는 패턴인데, P99 TTFT가 P50의 5~10배 이상으로 튀는 경우의 상당수는 output length가 heavy-tailed인 요청이 배치를 독점하는 상황에서 발생한다. 트래픽의 25%만 thinking 요청이어도 전체 P99 TTFT가 크게 늘어나는 이유다. 배치에서 thinking 요청 비율이 높아질수록 tail latency가 비선형으로 악화된다.

max_tokens 설정의 딜레마

Thinking 모드에서 max_tokens 설정은 양쪽이 모두 손해인 선택이다.

작게 잡으면(512~1,024) thinking 체인이 </think> 태그를 닫기 전에 잘린다. vLLM의 thinking_token_budget 강제 종료 메커니즘이 reasoning_end_str을 주입하면서 사고 체인이 완료되지 않은 상태에서 응답으로 넘어간다. MATH-500 기준으로 5,000토큰 thinking이 512토큰으로 줄면 정확도가 0.6에서 0.4대로 돌아간다.

크게 잡으면(8,192~32,768) 요청별 KV 캐시 예약량이 폭발한다. 배치 내 모든 요청이 max_tokens만큼의 블록을 최악의 경우로 예약해야 하므로, 배치 크기 상한이 내려가고 throughput이 줄어든다.

max_tokens배치 크기 영향thinking 체인 품질preemption 위험
512높은 배치 허용복잡한 추론에서 절단낮음
2,048중간일반 질의응답 OK중간
8,192낮음경쟁 수학·코딩 OK높음
32,768매우 낮음최대 품질매우 높음

실용적 범위는 2,048~8,192다. 일반 질의응답 위주 서비스라면 2,048로 충분하다. 경쟁 수학이나 복잡한 코드 생성이 주 태스크라면 8,192 이상이 필요하고, 배치 크기와 throughput 손실을 감수해야 한다.

대응 전략: 세 가지 레버와 그 비용

Chunked prefill과 파라미터 조정

vLLM V1에서 enable_chunked_prefill은 기본 활성화된다. 이 옵션은 스케줄러가 decode 요청을 먼저 처리하고 남은 max_num_batched_tokens 예산으로 prefill을 청크 단위로 소화하도록 한다. Thinking 요청의 decode가 다른 요청의 TTFT를 막지 않는 데 효과적이다.

python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3-8B \
    --reasoning-parser qwen3 \
    --enable-chunked-prefill \
    --max-num-batched-tokens 2048 \
    --max-num-seqs 32

max_num_batched_tokens=2048은 TPOT(inter-token latency) 개선에 유리하다. 반대로 너무 작게 잡으면 prefill이 지나치게 잘게 쪼개져 TTFT가 오히려 늘어난다. Throughput이 우선이면 8,192 이상으로 올린다. max_num_seqs를 낮추면 동시 처리 요청 수와 KV 캐시 압박이 함께 줄어든다. Preemption이 자주 발생한다면 먼저 이 값을 줄여보는 게 낫다.

thinking_token_budget으로 체인 길이 제한

vLLM의 reasoning 지원을 활용하면 완전한 인스턴스 분리 없이도 요청별 thinking 체인 길이를 제한할 수 있다. vLLM은 reasoning 토큰이 budget에 도달하면 </think>를 강제 주입해서 decode를 종료한다.

from vllm import LLM, SamplingParams

sampling_params = SamplingParams(
    max_tokens=4096,
    thinking_token_budget=2048,  # thinking 체인을 2048 tok으로 제한
)

일반 질의응답·요약·번역은 2,048토큰 budget으로 충분하다. MATH-500 수준의 경쟁 수학은 5,000토큰 이상이 필요하다. 태스크 유형별로 budget을 다르게 적용하는 게 일률적인 cap을 걸어두는 것보다 훨씬 낫다.

Thinking을 완전히 끄려면 request에 enable_thinking=False를 전달한다.

response = client.chat.completions.create(
    model="Qwen/Qwen3-8B",
    extra_body={"chat_template_kwargs": {"enable_thinking": False}},
    messages=[...],
    max_tokens=2048,
)

일반 대화, 번역, 요약, 정보 추출은 non-thinking으로도 품질 손실이 거의 없다. 반면 다단계 수학, 복잡한 알고리즘 코딩, 긴 논리 체인이 필요한 추론은 thinking을 끄면 정확도가 눈에 띄게 떨어진다. 태스크가 "중간 추론 단계"를 필요로 하는지가 판단 기준이다.

분리 서빙

가장 효과적인 방법은 thinking 요청과 non-thinking 요청을 서로 다른 인스턴스로 라우팅하는 것이다. 같은 스케줄러에 섞지 않으면 tail latency 문제가 근본적으로 사라진다. 두 워크로드의 최적점이 다르기 때문에 분리 설정이 의미 있다:

  • Non-thinking 인스턴스: max_num_seqs=64, max_tokens=2,048 → throughput 최대화
  • Thinking 인스턴스: max_num_seqs=16, max_tokens=8,192~32,768 → 긴 체인 소화

분리의 비용은 모델 가중치를 두 벌 올리거나, 트래픽 비율 변화에 따라 인스턴스 수를 별도로 관리해야 한다는 점이다. SLA가 엄격하고 thinking 트래픽 비율이 20% 이상이라면 분리 서빙이 투자 대비 효과가 좋다. 그 이하라면 thinking_token_budget 제한과 max_num_seqs 조정으로 충분한 경우가 많다.

아래는 실무에서 모니터링해야 할 지표와 대응 기준이다:

지표대응 기준조치
gpu_cache_usage_perc>90%max_num_seqs 낮추거나 gpu_memory_utilization 올리기
P99 TTFT / P50 TTFT>5배max_num_batched_tokens 낮추거나 분리 서빙 검토
preemption 횟수분당 수회 이상max_num_seqs 또는 max_tokens 줄이기
throughput (tok/s)기대치 대비 -30% 이상max_num_batched_tokens 올리거나 thinking_token_budget 제한
thinking 요청 비율>20%분리 서빙 진지하게 검토
Tags
vLLMKV 캐시InferenceLLM서빙메모리아키텍처