
LLM 서빙 성능 지표와 튜닝
- 6서빙을 위한 양자화 가이드 FP8, AWQ, GPTQ
- 7Speculative Decoding으로 LLM 추론 속도 높이기
- 8vLLM 실전 서빙 가이드
- 9LLM 서빙 성능 지표와 튜닝읽는 중
부하 테스트에서 동시성을 올렸더니 전체 처리량이 두 배가 됐습니다. 그런데 배포하고 나니 사용자들은 오히려 답변이 느려졌다고 합니다. 모순처럼 들리지만 둘 다 사실입니다. 서버가 초당 만들어내는 토큰의 총량과 사용자 한 명이 체감하는 속도는 서로 다른 지표이고, 배치를 키우면 앞의 것은 오르고 뒤의 것은 나빠집니다.
시리즈의 마지막인 이번 글은 이 간극을 다루는 도구를 정리합니다. 요청 하나의 시간을 쪼개는 지표(TTFT, TPOT, ITL)와 지연 목표(SLO)를 정의하고, 처리량과 지연시간이 왜 반대로 움직이는지, 그 끝에 있는 두 하드웨어 천장(대역폭과 KV 용량)이 무엇인지 계산합니다. MoE에서 그 천장이 어떻게 달라지는지 짚은 뒤, 측정한 지표를 근거로 서빙 인자를 움직이는 것으로 마무리합니다. 기준은 vLLM v0.25.1입니다.
요청 하나의 시간을 쪼개는 네 가지 지표
LLM은 프롬프트 전체를 한 번에 읽어 첫 토큰을 만드는 prefill과, 그 뒤로 토큰을 하나씩 이어 만드는 decode의 두 단계로 응답을 생성합니다. 스트리밍으로 응답하는 서비스에서 요청 하나의 시간을 이 흐름대로 쪼개면 이렇게 나뉩니다.
- TTFT(Time To First Token): 요청을 보낸 순간부터 첫 토큰을 받을 때까지. 큐 대기와 prefill이 여기에 들어갑니다. 사용자가 “반응이 왔다”고 느끼는 시점입니다.
- TPOT(Time Per Output Token): 첫 토큰 이후 토큰 하나당 걸린 평균 시간. (E2E 지연시간 - TTFT) ÷ (출력 토큰 수 - 1)로, 요청 하나에 값이 하나 나옵니다.
- ITL(Inter-Token Latency): 토큰과 토큰 사이의 간격 하나하나. 요청 하나에서 출력 토큰 수보다 하나 적은 개수만큼 나옵니다.
- E2E 지연시간: 요청 전송부터 마지막 토큰까지의 전체 시간.
TPOT과 ITL은 같은 구간을 재지만 용도가 다릅니다. TPOT은 요청 단위 평균이라 “이 요청이 전반적으로 빨랐는가”를 말하고, ITL은 간격 하나하나의 분포라 중간에 한 번씩 길게 멈칫한 순간이 그대로 남습니다. TPOT P99가 가장 느린 요청을 찾는 지표라면, ITL P99는 가장 느린 순간을 찾는 지표인 셈입니다.
vLLM에는 이 지표들을 재는 부하 테스트 도구가 내장되어 있습니다.
vllm bench serve \
--model google/gemma-4-31B-it \
--dataset-name random \
--random-input-len 1024 --random-output-len 128 \
--num-prompts 1000 --max-concurrency 64실행이 끝나면 이런 표가 나옵니다(숫자는 예시입니다).
============ Serving Benchmark Result ============
Successful requests: 1000
Failed requests: 0
Maximum request concurrency: 64
Benchmark duration (s): 86.40
Total input tokens: 1024000
Total generated tokens: 128000
Request throughput (req/s): 11.57
Output token throughput (tok/s): 1481.48
Peak output token throughput (tok/s): 1650.00
Peak concurrent requests: 64
Total token throughput (tok/s): 13333.33
---------------Time to First Token----------------
Mean TTFT (ms): 214.63
Median TTFT (ms): 186.20
P99 TTFT (ms): 841.87
-----Time per Output Token (excl. 1st token)------
Mean TPOT (ms): 41.85
Median TPOT (ms): 40.12
P99 TPOT (ms): 58.90
---------------Inter-token Latency----------------
Mean ITL (ms): 41.85
Median ITL (ms): 38.75
P99 ITL (ms): 97.43
==================================================위쪽이 서버 전체의 처리량, 아래쪽이 개별 요청의 지연 분포입니다. 이 예시에서 TPOT P99는 59ms인데 ITL P99는 97ms입니다. 요청 평균으로 보면 다들 무난했지만, 토큰 간격 중에는 평소의 두 배 넘게 벌어진 순간이 있었다는 뜻입니다. vLLM은 매 스텝 진행 중인 요청들의 decode에 새 요청의 prefill을 조각내 섞습니다(chunked prefill). prefill이 decode를 통째로 멈추는 것을 막으려는 장치인데, 그래도 prefill이 많이 섞인 스텝은 무거워져 그 순간의 간격이 벌어집니다.
어디서 재느냐에 따라 숫자가 다릅니다
vllm bench serve의 TTFT는 클라이언트가 요청을 보낸 직후부터 첫 응답 청크를 받을 때까지라 네트워크 왕복이 포함됩니다. 반면 서버의 Prometheus 지표 vllm:time_to_first_token_seconds는 토큰화 시작 시점부터 잽니다. 두 값이 다르게 나오는 것이 정상입니다.
처리량 목표에 지연 조건을 붙인다
처리량 숫자에는 함정이 있습니다. 배치를 극단적으로 키우면 tok/s는 계속 오르는데, 그 사이 개별 요청의 TTFT와 토큰 간격은 계속 나빠집니다. 초당 토큰을 아무리 뽑아도 사용자가 기다리다 떠날 수준이면 의미가 없습니다. 그래서 목표를 “처리량 최대화”가 아니라 “지연 조건을 지키는 한도 안에서 처리량 최대화”로 다시 씁니다. 그 지연 조건이 SLO(Service Level Objective)입니다.
SLO는 워크로드마다 다르고, 같은 워크로드라도 모델 크기마다 다릅니다. prefill과 decode를 서로 다른 GPU로 분리하는 구조를 제안한 DistServe 논문이 실험에 쓴 기준이 그 점을 잘 보여줍니다.
| 워크로드 | 모델 | TTFT SLO | TPOT SLO |
|---|---|---|---|
| 챗봇 | OPT-13B | 250ms | 100ms |
| 챗봇 | OPT-66B | 2.5s | 150ms |
| 챗봇 | OPT-175B | 4s | 200ms |
| 코드 자동완성 | OPT-66B | 125ms | 200ms |
| 문서 요약 | OPT-66B | 15s | 150ms |
코드 자동완성은 타이핑을 끊지 않아야 해서 첫 반응 기준이 가장 빡빡하고, 문서 요약은 입력이 길어 첫 토큰은 한참 기다려주는 대신 나오기 시작하면 빨라야 합니다. 눈여겨볼 것은 챗봇 세 줄입니다. 같은 챗봇인데 13B와 175B의 TTFT 기준이 16배 차이납니다. SLO는 사용자 경험만 보고 정하는 값이 아니라 그 모델이 그 하드웨어에서 낼 수 있는 속도와 타협해 정하는 값이라, 남의 표를 그대로 가져다 쓸 수 없습니다.
SLO를 정했으면 벤치마크에 그대로 넘길 수 있습니다. vllm bench serve에 ms 단위의 키:값 쌍으로 조건을 주면, 그 조건을 지킨 요청만 따로 세어줍니다.
vllm bench serve ... --goodput ttft:250 tpot:100쓸 수 있는 키는 ttft, tpot, e2el 셋이고, 지정한 조건을 전부 만족한 요청만 세어 결과에 Request goodput (req/s) 줄로 찍어줍니다. 동시성을 올려가며 스윕할 때 전체 처리량은 계속 오르는데 이 값이 꺾이기 시작하는 지점이 보이면, 거기가 이 SLO에서 이 서버의 실질 용량입니다.
이름이 낯설 수 있는데, goodput은 네트워킹에서 온 말로 조건을 만족한 몫만 센 처리량을 뜻합니다. LLM 서빙에서는 아직 TTFT나 TPOT만큼 일상적으로 쓰이는 지표는 아니고 주로 논문 쪽 용어입니다. 용어를 외울 필요는 없고, 스윕할 때 “몇 건이 기준을 지켰는가”를 손으로 세지 않아도 된다는 점만 챙기면 됩니다.
처리량과 지연시간은 왜 함께 못 가는가
트레이드오프의 뿌리는 decode의 병목입니다. decode는 토큰 하나를 만들 때마다 모델 가중치 전체를 HBM(GPU의 온보드 고대역폭 메모리)에서 읽어야 해서, 연산 속도가 아니라 메모리 읽기 속도에 묶이는 memory-bound 단계입니다. 그런데 요청이 1개든 64개든 가중치는 한 번만 읽으면 됩니다. 배치를 키우면 같은 읽기 한 번으로 토큰을 64개 만들게 되고, 이것이 배칭이 처리량을 올리는 원리입니다.
공짜가 아닌 이유는 스텝이 무거워지기 때문입니다. 배치가 커지면 스텝당 연산량이 늘고, 각 요청이 쌓아둔 KV Cache(이전 토큰들의 attention Key, Value를 저장해 재계산을 피하는 공간) 읽기도 늘어납니다. 그렇게 길어진 한 스텝의 시간이 곧 진행 중인 모든 요청의 토큰 간격입니다. 전체 tok/s는 오르지만 개별 사용자의 토큰은 더 띄엄띄엄 나옵니다. 글 첫머리의 모순이 바로 이것입니다.
동시성을 계속 올리면 곡선은 세 구간을 지납니다.
배치에 자리가 남는 동안은 동시성을 올린 만큼 처리량이 오르고 지연은 거의 그대로입니다. GPU가 포화에 가까워지면 처리량 증가가 둔해지면서 지연이 본격적으로 오르는데, 이 꺾이는 지점을 knee라고 부릅니다. 배치 상한(max_num_seqs)이나 KV 풀, 하드웨어 중 먼저 닿는 것에서 처리량은 멈추고, 이후 들어오는 요청은 큐에서 기다리므로 TTFT만 늘어납니다. 튜닝의 목표를 한 문장으로 줄이면, 이 곡선 위에서 SLO를 아직 지키는 가장 오른쪽 지점을 찾는 일입니다.
서빙의 두 천장, 대역폭과 용량
곡선이 어디서 멈추는지는 결국 하드웨어가 정합니다. 천장은 두 개이고 성격이 다릅니다. 대역폭 천장은 토큰이 얼마나 빨리 나오는가의 상한이고, 용량 천장은 요청을 몇 개까지 동시에 올려둘 수 있는가의 상한입니다.
대역폭 천장부터 계산해보겠습니다. decode 한 스텝은 가중치 전체를 한 번 읽어야 하므로, 요청 하나만 있을 때 토큰 생성 속도의 이론상 상한은 메모리 대역폭을 모델 크기로 나눈 값입니다. Llama-3.3-70B를 H100 4장에 TP=4로 올린 서버를 예로 들겠습니다. TP에서는 GPU마다 가중치를 1/4씩 나눠 들고 동시에 읽으므로, 대역폭도 4장을 합산해서 칩니다.
Llama-3.3-70B BF16 가중치 = 70.6B × 2byte ≈ 141GB
H100 SXM 4장 합산 대역폭 (TP=4) = 4 × 3.35TB/s = 13.4TB/s
단일 요청 decode 상한 = 13,400GB/s ÷ 141GB ≈ 초당 95토큰forward pass 한 번이 토큰 하나를 만드는 한, 커널을 아무리 최적화해도 이 서버에서 요청 하나는 초당 95토큰을 넘지 못합니다. 한 번의 가중치 읽기로 여러 토큰을 확정하는 speculative decoding만이 이 선을 넘는 예외입니다. 배칭은 이 한 번의 읽기로 여러 요청의 토큰을 만들어 전체 처리량을 올리는 것이지, 요청 하나의 속도를 올리는 것이 아닙니다.
다만 이 값은 도달 목표가 아니라 넘을 수 없는 선입니다. 얼마나 근접했는지는 MBU(Model Bandwidth Utilization), 즉 (가중치 + KV Cache 크기) ÷ TPOT으로 구한 실효 대역폭을 이론 대역폭으로 나눈 값으로 잽니다. 70B급 모델을 배치 1로 돌린 공개 측정에서 2×H100 기준 60%, 4×A100 기준 55% 수준이니, 방금 계산한 95토큰짜리 서버라면 실제로는 초당 50~60토큰을 기대하는 것이 맞습니다.
지연을 줄이는 손잡이가 무엇인지도 이 식에서 나옵니다. TP를 8로 늘리면 합산 대역폭이 두 배가 되어 상한도 두 배로 오르고, 양자화로 가중치가 절반이 되면 읽을 바이트가 절반이라 역시 상한이 두 배로 오릅니다. 다만 양자화의 이득은 대역폭에 묶인 작은 배치에서 뚜렷하고, 배치가 커질수록 줄어듭니다. 다만 TP에는 스텝마다 GPU들이 부분 결과를 합치는 all-reduce가 끼고, 이 비용은 GPU 사이를 무엇으로 연결했느냐가 정합니다. NVLink로 묶인 서버용 카드에서는 대역폭 이득이 통신 손실을 넉넉히 넘지만, PCIe로만 연결된 카드에서는 손실이 이득을 잡아먹어 TP를 켠 쪽이 단일 카드보다 느려지기도 합니다. TP 확대를 지연 대책으로 꺼내기 전에 인터커넥트부터 확인해야 합니다.
배치가 커지면 읽어야 하는 것이 가중치만이 아닙니다. 스텝마다 배치에 있는 모든 요청의 KV Cache도 읽습니다.
스텝 시간 ≈ (가중치 크기 + 배치 전체의 KV 총량) ÷ 메모리 대역폭배치가 작을 때는 가중치 항이 압도적이라 요청을 더 태워도 스텝 시간이 거의 안 변합니다. 배치가 크고 컨텍스트가 길어질수록 KV 항이 자라나 스텝이 느려집니다. 앞 섹션 곡선의 knee가 이 식 안에 있습니다. 배치를 수백 단위로 더 키우면 연산이 먼저 포화해 이 식만으로는 설명되지 않지만, 실무에서 잡는 배치 범위에서는 대체로 맞습니다.
용량 천장은 KV Cache 풀의 크기입니다. Llama-3.3-70B의 토큰당 KV는 모델 config 값으로 계산됩니다.
2(K,V) × 80 레이어 × 8 KV head × 128 head_dim × 2byte = 320KB/토큰H100 80GB 4장에 TP=4로 올리면, 전체 320GB에 vLLM이 기본으로 예약하는 비율 0.92(gpu_memory_utilization 기본값)를 곱한 294GB가 예산입니다. 여기서 가중치 141GB와 vLLM이 시작할 때 재서 미리 빼두는 활성값, CUDA graph 몫을 제하면 대략 140GB대가 KV 풀로 남습니다. 320KB로 나누면 약 44만 토큰입니다. 요청마다 8K 컨텍스트를 쓰는 워크로드라면 50개 남짓한 요청이 이 서버가 동시에 올려둘 수 있는 한계입니다.
어느 천장에 먼저 닿는지는 워크로드가 정합니다. 컨텍스트가 긴 워크로드는 요청 몇 개만으로 KV 풀이 차서, 연산 자원이 놀고 있는데도 동시성을 못 올립니다. 반대로 짧은 컨텍스트에 동시 요청이 많은 워크로드는 KV 풀이 차기 전에 스텝 시간이 먼저 SLO를 넘어섭니다. 증상이 다르니 처방도 다릅니다. 용량이 병목이면 풀 자체를 키우거나(gpu_memory_utilization 상향, KV Cache FP8, TP 확대) 요청당 사용량을 줄이는 쪽(max_model_len 하향)이 답이고, 대역폭이 병목이면 양자화, 더 빠른 GPU, 아니면 인스턴스 복제가 답입니다.
MoE에서 두 천장은 어떻게 달라지는가
지금까지의 계산은 dense 모델 기준입니다. MoE(Mixture of Experts) 모델은 두 천장과 맺는 관계가 달라집니다.
MoE는 FFN 레이어를 여러 expert로 쪼개두고 토큰마다 라우터가 일부 expert만 골라 통과시키는 구조입니다. 그래서 파라미터 수가 두 종류로 나뉩니다.
| 모델 | 전체 파라미터 | 토큰당 활성 파라미터 |
|---|---|---|
| DeepSeek-V3 | 671B | 37B |
| Qwen3-235B-A22B | 235B | 22B |
| Gemma 4 26B-A4B | 25.2B | 3.8B |
연산은 활성 파라미터만큼만 하므로 MoE의 연산 비용은 작은 dense 모델급입니다. 같은 품질을 dense로 내려면 훨씬 큰 모델이 필요한데 연산은 그 일부만 쓰는 셈이라, 트래픽이 많은 서빙에서 MoE가 주류가 된 이유가 여기 있습니다. 다만 메모리 쪽 두 천장은 활성 파라미터를 그대로 따라오지 않습니다. 용량은 처음부터 전체 크기를 기준으로 잡히고, 대역폭은 배치가 커질수록 전체 크기 쪽으로 옮겨갑니다. 용량을 계산할 때 활성 파라미터로 잡으면 어긋나는 지점이 여기입니다.
용량 쪽은 단순합니다. 어떤 토큰이 어떤 expert를 고를지 미리 알 수 없으니 expert 전부가 VRAM에 올라가 있어야 합니다. DeepSeek-V3는 FP8 체크포인트로도 가중치가 671GB를 넘어 H100 80GB 8장 노드(640GB)에조차 들어가지 않고, Gemma 4 26B-A4B도 연산은 3.8B지만 올릴 때는 25.2B 모델입니다.
대역폭 쪽은 배치 크기에 따라 양상이 달라집니다. 요청 하나만 decode할 때는 그 토큰이 고른 expert의 가중치만 읽으면 되니, 활성 3.8B짜리 모델은 작은 dense처럼 가볍게 돕니다. 그런데 배치가 커지면 토큰마다 다른 expert를 고르므로, 배치 안의 토큰을 전부 합치면 결국 거의 모든 expert가 호출됩니다. 스텝당 읽는 가중치가 활성분이 아니라 전체 크기에 수렴하는 것입니다.
여기서 비교 대상을 분명히 해야 오해가 없습니다. 전체 크기가 같은 dense(25.2B)와 견주면 MoE가 불리해지는 구간은 없습니다. 배치가 작을 때는 활성분만 읽어 훨씬 빠르고, 배치가 커지면 양쪽 다 전체를 읽어 같아질 뿐입니다. 연산량까지 치면 배치 전 구간에서 MoE가 앞섭니다. 반면 활성 크기가 같은 dense(3.8B)와 견주면 대역폭 부담은 배치가 커질수록 벌어집니다. 저쪽은 배치가 아무리 커져도 3.8B만 읽지만 MoE는 25.2B를 향해 올라가기 때문입니다. 물론 3.8B dense는 애초에 같은 품질을 내지 못하니, 실제 선택지가 아니라 기준선으로 읽는 편이 맞습니다.
공개 벤치마크에도 이 구조가 드러납니다. Gemma 4 26B-A4B를 FP8로 RTX PRO 6000 Blackwell 한 장에 올린 측정에서, 1K 컨텍스트 단일 요청의 생성 속도는 초당 139토큰입니다. 매 스텝 전체 25.2B를 읽는다면 이론 상한이 초당 71토큰이니, 활성분만 읽지 않고서는 나올 수 없는 숫자입니다. 같은 조건에서 동시 요청을 10개로 올리면 전체 처리량이 초당 674토큰까지 오릅니다. GPU 한 장으로 25B급 모델을 이만큼 돌린다는 뜻이라, MoE를 고르는 이유가 이 숫자에 있습니다. 대신 사용자 한 명이 받는 속도는 그만큼 내려가니, 배치 1에서 잰 값이 아니라 목표 동시성에서 다시 재야 합니다.
그래서 MoE 서빙에는 전용 병렬화가 있습니다. vLLM의 --enable-expert-parallel(EP, Expert Parallelism)은 GPU마다 expert 일부를 통째로 맡기고, 스텝마다 각 토큰을 담당 expert가 있는 GPU로 보냈다가(all-to-all 통신) 결과를 되돌려받습니다. TP도 가중치를 나눠 들기는 하지만 expert 하나하나를 잘게 쪼개므로, 덩어리째 맡는 EP 쪽이 연산 효율이 좋습니다. 대가는 all-to-all 통신 비용과 특정 expert로 토큰이 몰리는 쏠림입니다. EP 크기를 정하는 별도 인자는 없고, TP 크기에 DP(Data Parallelism, 모델 전체를 복제해 트래픽을 나누는 방식) 크기를 곱한 값이 곧 EP 크기입니다.
KV Cache는 MoE라고 달라지지 않습니다. MoE가 바꾸는 것은 FFN이고 어텐션은 그대로라, 토큰당 KV는 dense와 같은 공식으로 계산하면 됩니다.
지표를 근거로 인자를 움직이는 법
vLLM의 auto_tune 스크립트와 클라우드 벤더 가이드들이 공통으로 쓰는 순서는 세 단계입니다.
- SLO를 먼저 고정합니다 (예: P99 TTFT 500ms, P99 TPOT 50ms)
- 동시성을 바꿔가며
vllm bench serve로 스윕합니다 - SLO를 지키는 범위에서 처리량이 최대인 지점을 고릅니다. 그 처리량이 목표 트래픽에 모자라면 인자를 조정하거나 인스턴스를 늘립니다
for c in 8 16 32 64 128; do
vllm bench serve \
--model google/gemma-4-31B-it \
--dataset-name random \
--random-input-len 1024 --random-output-len 256 \
--num-prompts $((c * 5)) --max-concurrency $c \
--goodput ttft:500 tpot:50
done--num-prompts는 공식 레시피가 권장하는 대로 동시성의 5배 이상으로 잡습니다. 짧게 돌리면 워밍업 구간이 결과를 흐립니다. 부하를 거는 방식은 두 가지인데, --max-concurrency는 동시 요청 수를 고정해 위처럼 용량 곡선을 그릴 때 쓰고, --request-rate는 초당 도착률을 고정해 예상 트래픽에서 SLO가 지켜지는지 최종 확인할 때 씁니다. 도착률을 고정하면 서버가 밀리기 시작할 때 대기 요청이 스스로 불어나, 실제 서비스의 붕괴 양상까지 재현됩니다.
스윕 결과가 SLO에 어긋날 때, 어느 지표가 나쁜지에 따라 움직일 인자가 다릅니다.
- ITL이 나쁠 때: 스텝이 무겁다는 신호입니다.
max_num_batched_tokens를 낮추면(2048 수준) 한 스텝에 끼어드는 prefill 양이 줄어 decode 간격이 고르게 유지됩니다.max_num_seqs를 낮춰 배치 자체를 줄이는 것도 같은 방향입니다. 대가는 처리량입니다. - TTFT가 나쁠 때: prefill이 밀린다는 신호입니다.
max_num_batched_tokens를 올리면(8192 이상) prefill이 한 번에 더 많이 소화됩니다. 동시성 초과로 큐에서 기다리는 것이 원인이라면 인자가 아니라 용량의 문제라, 인스턴스를 늘리는 쪽이 답입니다. - 처리량이 모자랄 때: 공식 문서는 큰 GPU에 비교적 작은 모델을 올린 경우
max_num_batched_tokens를 8192보다 크게 잡으라고 권장합니다. TP를 줄이고 인스턴스를 복제하는 선택지도 함께 봅니다. - 동시성이 목표에 못 미칠 때: 용량 천장 신호입니다.
gpu_memory_utilization을 올리고, 그래도 모자라면 KV Cache FP8이나 TP 확대로 풀을 키웁니다.max_model_len하향은 풀을 키우는 것이 아니라 요청 하나가 쓸 수 있는 상한을 막는 것이라, 실제로 긴 요청이 들어올 때만 효과가 있습니다. - MoE 모델에서 배치를 키운 만큼 처리량이 안 따라올 때: 스텝당 읽는 가중치가 활성분에서 전체 크기로 옮겨가는 구조 특성입니다.
--enable-expert-parallel로 expert를 GPU들에 나눠 스텝당 가중치 읽기를 분산합니다.
TP와 배치 크기의 큰 그림은 공식 레시피가 Llama 3.3 70B를 B200에 올려 정리한 세 프리셋을 기준점으로 삼으면 됩니다. 처리량이 목표면 TP를 최소로 두고 배치를 최대로, 지연이 목표면 TP 4에서 8에 배치를 작게, 균형이 목표면 TP 2에 배치 128입니다. 최소 TP가 몇인지는 GPU 메모리가 정하니, 카드가 작으면 그만큼 올려 잡아야 합니다.
서버 쪽에서 병목을 확인하는 창은 엔진 로그입니다. 운영 중 주기적으로 찍히는 이 줄에 신호가 다 들어 있습니다.
Avg prompt throughput: 3021.4 tokens/s, Avg generation throughput: 512.7 tokens/s, Running: 64 reqs, Waiting: 12 reqs, Preemptions: 3, GPU KV cache usage: 97.2%, Prefix cache hit rate: 41.3%Waiting이 계속 쌓이면 배치 상한이나 용량이 트래픽에 밀리는 중입니다. Preemptions가 0이 아니면 KV 풀이 모자라 진행 중인 요청을 내리고 다시 계산했다는 뜻이라, 용량 천장에 닿았다는 가장 직접적인 신호입니다. 같은 값들은 /metrics Prometheus 엔드포인트로도 노출됩니다.
스윕이 손에 익으면 자동화 도구에 맡길 수 있습니다. vllm bench sweep serve는 서버 인자 조합을 돌고, serve_workload는 동시성이나 도착률을 훑어 지연과 처리량의 트레이드오프 곡선을 뽑습니다. 그 결과를 plot_pareto로 그리면 SLO를 만족하는 구간이 눈에 들어옵니다. vLLM 저장소의 auto_tune.sh는 “P99 E2E 지연 X ms 이하”라는 제약을 걸고 max_num_seqs × max_num_batched_tokens 조합을 돌면서 제약 안에서 처리량이 최대인 조합을 찾아줍니다. 수동 스윕으로 곡선의 감을 잡은 뒤 마지막 탐색을 맡기기 좋습니다.
마치며
이 글로 시리즈를 마칩니다. prefill과 decode의 비대칭에서 출발해 KV Cache와 PagedAttention, 스케줄링, 양자화와 speculative decoding, 엔진 인자를 지나 성능 지표까지 왔습니다. 관통하는 생각은 하나였습니다. 엔진이 어떻게 동작하는지 알면 설정값은 외우는 목록이 아니라 유도하는 결론이 됩니다. 지표를 먼저 재고, 어느 천장에 닿았는지 확인하고, 그에 맞는 손잡이를 움직이면 됩니다.
함께 보면 좋은 글
- Prefill과 Decode로 이해하는 LLM 추론 과정
- KV Cache가 LLM 서빙을 바꾸는 방식
- vLLM의 핵심 원리 PagedAttention 파헤치기
- Continuous Batching과 Chunked Prefill 완전 이해
- 서빙을 위한 양자화 가이드 FP8, AWQ, GPTQ
- Speculative Decoding으로 LLM 추론 속도 높이기
- vLLM 실전 서빙 가이드
참고자료
- vLLM Benchmark CLI
- vLLM Parameter Sweeps
- vLLM Optimization and Tuning
- vLLM Expert Parallel Deployment
- LLM Inference Performance Engineering: Best Practices (MBU)
- MoE vs dense models: How do they compare in inference?
- DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving
- Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve
- NVIDIA LLM Inference Benchmarking: Fundamental Concepts
- How to Scale Your Model: All About Inference
- vLLM Recipes: Llama 3.3 70B