Documents
Home>Documents>AI>Inference

LLM 서빙 벤치마크가 현실과 어긋나는 이유: 합성 트래픽이 숨기는 세 가지 함정

12 min readAug 22, 2026Aug 22, 2026

벤치마크를 돌렸을 때 나온 2,000 tok/s가 실서비스에서 재현된 적이 한 번도 없다면, 벤치마크 도구를 잘못 쓴 게 아니라 벤치마크 입력 자체가 실서비스 부하와 구조적으로 다르기 때문이다. 도구 사용법 문제라면 파라미터 하나 바꿔서 해결된다. 하지만 입력 설계 문제라면 수치가 아예 다른 현상을 측정하고 있는 것이다.

이 글은 vLLM benchmark_serving, SGLang bench, llmperf 같은 도구의 사용법을 설명하지 않는다. 합성 트래픽이 실서비스 부하와 다른 이유를 세 가지 축으로 파고들고, 각 축에서 수치가 얼마나 달라지는지를 보인다.

전형적인 배신 패턴

스테이징 환경에서 vllm bench serve --request-rate inf 로 throughput을 측정하면 GPU 활용률이 95% 이상으로 올라가고 tok/s가 인상적으로 나온다. 같은 모델을 프로덕션에 올리면 p99 TPOT이 벤치마크보다 3배 이상 높고, 특정 시간대에는 TTFT이 수 초로 튄다.

이런 패턴이 반복되는 이유는 벤치마크가 세 가지를 동시에 틀리게 설정하기 때문이다.

함정 1: 요청 길이 분포 — Heavy-tail이 decode 큐를 막는다

대부분의 LLM 서빙 벤치마크는 --random-input-len 512 식으로 고정 길이나 좁은 범위 분포를 쓴다. ShareGPT 같은 실제 대화 데이터를 쓰더라도 "ShareGPT 분포를 사용했다"는 사실 자체가 의미 있는 게 아니다 — vLLM SOSP 2023 논문의 평가 섹션에서 ShareGPT 입력 평균은 161 토큰으로, 이미 균질한 단기 요청 위주다.

실서비스 입력 길이 분포는 다르게 생겼다. 짧은 요청(50 tok 이하)이 전체의 60–70%를 차지하고, 꼬리 쪽에 2,000–8,000 tok짜리 요청이 드문드문 나타난다. 로그파일 기반으로 추출해 보면 중앙값(p50)이 ~120 tok인데 p99은 ~2,048 tok, p99.9는 이보다 훨씬 높은 경우가 흔하다. 이 분포는 정규분포가 아니라 log-normal에 가깝고, 꼬리가 무겁다.

문제는 continuous batching 환경에서도 이 꼬리 요청이 decode 단계에서 배치 전체를 잡아당긴다는 것이다. Prefill은 병렬 처리되므로 긴 요청도 빠르게 소화된다. 하지만 decode는 토큰 하나씩 자기회귀적으로 생성되므로, 2,000 tok 출력 요청 하나가 배치에 들어가 있는 동안 다른 요청들의 TPOT이 전부 그 요청의 pace에 영향을 받는다. vLLM이나 SGLang이 이른바 preemption 메커니즘으로 일부 완화하지만, 완전히 제거되지는 않는다.

균일 분포 512 tok 벤치마크에서는 이 현상이 나타나지 않는다. 모든 요청이 비슷한 속도로 decode되므로 배치가 균형을 유지한다. 실서비스에서 p99 TPOT이 p50 대비 3–5배 높게 나오는 주된 원인이 여기에 있다.

실서비스 로그에서 분포를 추출하는 방법은 단순하다:

import json
import pandas as pd
import matplotlib.pyplot as plt

# vLLM access log는 기본적으로 JSON lines 형식
# {"prompt_tokens": 142, "completion_tokens": 89, ...}
records = []
with open("vllm_access.log") as f:
    for line in f:
        try:
            records.append(json.loads(line))
        except json.JSONDecodeError:
            pass

df = pd.DataFrame(records)
print(df["prompt_tokens"].describe(percentiles=[.5, .9, .95, .99]))

df["prompt_tokens"].hist(bins=100, log=True)
plt.xlabel("Input tokens")
plt.ylabel("Count (log scale)")
plt.title("Production request length distribution")
plt.savefig("input_dist.png", dpi=150)

이 히스토그램 모양을 보고 나서야 "벤치마크에 어떤 분포를 써야 하는가"를 제대로 물을 수 있다.

함정 2: 도착 과정 — Closed-loop는 부하를 인위적으로 평탄화한다

--request-rate inf로 동시에 수백 개 요청을 보내는 건 closed-loop다 — 모든 요청이 시작 시점에 투입되고 서버가 처리하는 속도 이상으로 새 요청이 들어오지 않는다. 실서비스는 open-loop다. 요청이 사용자로부터 랜덤하게 도착하고 서버 처리 여부와 무관하게 계속 들어온다.

Anyscale의 llmperf 설계 문서는 이 차이를 명확히 짚는다: closed-loop 생성기는 앞 요청이 완료된 뒤 다음 요청을 보내므로, 서버가 과부하 상태면 자동으로 새 요청이 적게 들어가고 tail latency가 낮아 보인다. 이건 측정 결과가 아니라 도구가 부하를 스스로 제한한 것이다.

큐잉 이론으로 차이를 수치로 보면 더 명확하다. 단순한 M/M/1 큐 모델에서 평균 대기 시간은:

E[W] = ρ / (μ(1 - ρ))

여기서 ρ = λ/μ (서버 utilization), λ = 도착률, μ = 서비스율이다. Utilization이 80%일 때와 95%일 때 대기 시간을 비교해 보면:

Utilization (ρ)E[W] (μ=1 기준)배율
50%1.0
80%4.0
90%9.0
95%19.019×

Closed-loop 벤치마크는 utilization을 인위적으로 낮게 유지한다. 동시에 투입한 N개 요청이 소화되는 동안 새 요청이 들어오지 않으니까 큐가 짧게 유지된다. 반면 open-loop에서 도착률이 서비스 용량의 90%에 달하면 TTFT은 closed-loop 대비 이론적으로 9배 높아진다.

vLLM CLI는 --request-rate에 초당 요청 수를 지정하고 --burstiness 1.0을 추가하면 Poisson 도착 과정을 시뮬레이션한다:

vllm bench serve \
  --backend vllm \
  --model meta-llama/Llama-3.1-8B \
  --dataset-name sharegpt \
  --dataset-path ShareGPT_V3.json \
  --request-rate 10 \
  --burstiness 1.0 \
  --max-concurrency 200

--burstiness 0.1로 낮추면 버스트가 심한 트래픽을 모사한다. --burstiness 5.0 이상이면 사실상 균일 도착이다.

llmperf는 설계 단계부터 open-loop를 강제한다. 동시 요청 수를 지정하면 그 수를 유지하면서 완료된 요청을 즉시 새 요청으로 교체하는 방식이라, 높은 utilization 상태를 지속시키며 tail latency를 정직하게 측정한다.

SGLang의 bench_serving.py도 동일하게 --request-rate로 open-loop Poisson 도착을 지원한다. 세 도구 모두 open-loop 모드를 지원하지만, 기본값이나 문서에서 이 구분을 강조하지 않아서 모르고 쓰는 경우가 많다.

함정 3: Prefix 재사용률 — 캐시 히트율이 throughput을 크게 부풀린다

시스템 프롬프트가 고정된 RAG 파이프라인이나 에이전트 서비스는 요청마다 수백–수천 토큰짜리 동일 prefix를 공유한다. vLLM의 prefix caching과 SGLang의 RadixAttention은 이 prefix의 KV cache를 재사용해 prefill 비용을 크게 줄인다.

문제는 벤치마크가 이 히트율을 극단적으로 설정한다는 것이다. --dataset-name random은 매 요청이 완전히 다른 prompt를 쓰므로 히트율 0%다. 반대로 --prefix-repetition-prefix-len 1024 --prefix-repetition-num-prefixes 1 같은 설정은 히트율이 사실상 100%다. 실서비스는 그 사이 어딘가다.

히트율에 따라 throughput이 얼마나 달라지는지를 직관적으로 보면:

Prefix cache hit ratePrefill 절감전체 throughput 변화
0%없음baseline
50%절반 요청 prefill skip+30–60%
100%전체 prefill skip+2–4×

수치는 prefix 길이와 decode 길이 비율에 따라 크게 달라진다. Prefix가 512 tok이고 decode가 64 tok인 요청이라면, 히트율 100% 시 prefill이 없어지므로 throughput이 수 배 뛴다. Decode가 512 tok이고 prefix가 짧다면 prefill 절감 효과가 작아진다.

실서비스에서 실제 prefix 재사용률을 모르는 상태에서 히트율 0% 벤치마크를 보고하면 throughput이 과소 평가되고, 히트율 100%를 보고하면 실서비스에서 재현 불가능한 수치가 된다. 어떤 경우든 히트율을 명시하지 않은 throughput 수치는 비교 기준이 없다.

측정값이 의미 있으려면: 재현 가능한 벤치마크 설계

위 세 가지 함정을 모두 고려한 벤치마크 설계의 최소 요건을 정리하면:

1. 입력 길이 분포: 실서비스 로그 기반

  • 위 Python 스니펫으로 프로덕션 로그에서 prompt_tokens 분포를 추출한다
  • p50, p90, p99 수치를 파악한다
  • 벤치마크에서는 ShareGPT나 실제 로그 샘플을 그대로 쓴다. --random-input-len에 단일 값을 넣는 건 지양한다

2. 도착 패턴: open-loop Poisson

  • --request-rate를 명시한다 (inf를 쓰면 closed-loop에 가까워진다)
  • --burstiness 1.0으로 Poisson 과정을 지정한다
  • 목표 utilization을 70%, 85%, 95% 등 여러 단계로 찍는다. 단일 RPS만 보고하면 의미가 제한적이다

3. Prefix 재사용률: 시나리오별로 분리

  • 히트율 0%(완전 fresh), 실서비스 추정값, 100%를 각각 별도 실험으로 돌린다
  • 보고서에 히트율을 반드시 명시한다

4. 워밍업 구간 제거

  • 처음 30–60초(또는 수백 요청)는 KV cache가 차가운 상태라 측정에서 제외한다

5. p50 / p95 / p99를 함께 보고

  • TTFT 평균 하나만 보고하면 tail latency를 숨긴다. p99가 p50의 3배 이상이라면 그 자체가 진단 신호다

도구별 핵심 파라미터 대응표:

설정 항목vLLM CLIllmperfSGLang bench
도착률(open-loop)--request-rate N--num-concurrent-requests N--request-rate N
버스트 제어--burstiness 1.0(항상 open-loop)--burstiness 1.0
입력 분포--dataset-name sharegpt--prompt-token-count 범위 지정--dataset-name sharegpt
Prefix 반복--dataset-name prefix_repetition미지원(별도 스크립트)--enable-prefix-caching 조합
워밍업 제거--warmup-duration 60초기 N 요청 drop--warmup-requests N
퍼센타일 출력p50/p90/p99 자동 출력별도 집계 필요p50/p90/p99 출력

어떤 숫자를 함께 봐야 하는가

TTFT·TPOT·throughput 세 숫자를 동시에 보고하지 않으면 해석이 불가능하다. Throughput을 극대화하는 설정은 보통 TTFT를 올리고, TTFT를 낮추려면 동시 요청 수를 줄여야 해서 throughput이 떨어진다. 두 목표가 trade-off 관계이므로 하나만 최적화한 결과를 보고하면 다른 축에서 실서비스가 어떻게 동작하는지 알 수 없다.

GPU utilization과 KV cache 사용률도 함께 기록해야 한다. Utilization이 60% 미만에서 p99 TTFT가 높게 나온다면 요청 길이 분포 문제나 prefill 큐 쏠림을 의심해야 한다. 반면 utilization 95% 이상에서 높은 p99가 나온다면 용량 문제다. 두 케이스 처방이 다르다.

처리량 최대화 실험(saturate throughput)과 지연 보장 실험(latency-constrained, 예: p99 TPOT ≤ 100ms 유지)은 반드시 분리해 돌린다. 흔히 보이는 "X tok/s at p99 TPOT 80ms" 같은 단일 포인트 보고는 trade-off 곡선에서 특정 한 점만 찍은 것이다. 어떤 utilization에서 그 수치가 나오는지, 조금만 부하가 올라가면 p99이 얼마나 불어나는지를 함께 봐야 그 수치가 실용적인 의미를 가진다.

Tags
LLMInference서빙vLLM아키텍처모니터링