16장. vLLM 서빙 벤치마킹과 RAG 최적화
서빙 성능 지표
택배를 기다릴 때 "주문하고 언제 출발했나(첫 소식)", "출발한 뒤 얼마나 빨리 오나", "다 받기까지 얼마나 걸렸나"를 따로 느낍니다. 택배사는 "하루에 몇 상자를 나르나"를 봅니다. LLM 서버도 사용자가 느끼는 시간과 서버가 해내는 양을 따로 잽니다.
왼쪽 셋은 사용자 한 명이 느끼는 시간입니다. TTFT 는 첫 글자가 보일 때까지, TPOT 은 글이 흘러나오는 빠르기, 지연은 다 받기까지입니다. 오른쪽 처리량은 서버 전체가 1초에 만드는 토큰 수입니다. 첫 소식이 TTFT, 배송 빠르기가 TPOT, 다 받기까지가 지연, 하루 나르는 상자 수가 처리량입니다.
지표 정리
| 지표 | 뜻 | 계산 | 좋은 방향 |
|---|---|---|---|
| TTFT | 첫 토큰까지 | 첫 조각을 받은 시각 − 보낸 시각 | 짧게 |
| TPOT | 토큰 사이 시간 | (지연 − TTFT) ÷ (출력 토큰 − 1) | 짧게 |
| 지연 | 요청 하나의 전체 시간 | 다 받은 시각 − 보낸 시각 | 짧게 |
| 처리량 | 1초에 만든 토큰 수 | 모든 출력 토큰 ÷ 걸린 시간 | 크게 |
| p50, p95 | 지연의 가운데와 느린 쪽 | 시간을 늘어놓고 50%, 95% 자리 | 짧게 |
지표 계산하기
import numpy as np
# 요청 여섯 개의 (보낸 뒤 첫 토큰까지 초, 다 받기까지 초, 출력 토큰 수) - 예시 숫자
reqs = [(0.21, 1.9, 60), (0.25, 2.1, 64), (0.19, 1.8, 58), (0.30, 2.4, 64), (1.80, 3.9, 64), (0.22, 2.0, 62)]
ttft = np.array([r[0] for r in reqs])
lat = np.array([r[1] for r in reqs])
tpot = np.array([(r[1] - r[0]) / (r[2] - 1) for r in reqs])
print(f"TTFT 평균 {ttft.mean():.2f}초 · p50 {np.percentile(ttft, 50):.2f} · p95 {np.percentile(ttft, 95):.2f}")
print(f"TPOT 평균 {tpot.mean() * 1000:.0f}ms · 지연 p50 {np.percentile(lat, 50):.2f}초 · p95 {np.percentile(lat, 95):.2f}초")다섯째 요청처럼 줄을 서느라 첫 토큰이 늦은 요청은 평균보다 p95 에 잘 드러납니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
reqs | requests(요청들)의 줄임말이다. 요청마다 (TTFT, 지연, 출력 토큰 수)를 적은 목록이다 |
np.array([...]) | 숫자 목록을 NumPy 배열로 바꾼다. 평균, 백분위수를 한 번에 계산할 수 있다 |
(r[1] - r[0]) / (r[2] - 1) | 첫 토큰 뒤의 시간을 나머지 토큰 수로 나눈다. TPOT 이다 |
ttft.mean() | 평균을 구한다 |
np.percentile(ttft, 95) | percentile(백분위수)을 구한다. 값을 작은 것부터 늘어놓았을 때 95% 자리의 값이다 |
tpot.mean() * 1000 | 초를 밀리초(ms, millisecond, 1000분의 1초)로 바꾼다 |
이 코드가 뜻하는 것
택배 여섯 상자의 기록을 모아 "보통은 얼마나 걸렸고, 늦은 쪽은 얼마나 걸렸나"를 따로 보는 일입니다. 여기서 상자 하나가 요청 하나, 출발 소식이 TTFT, 받기까지가 지연입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
ttft, lat, tpot | 요청 여섯 개에서 세 지표를 뽑는다 | 상자마다 출발까지, 다 받기까지, 이동 빠르기 |
첫 print | TTFT 평균 0.50초, p50 0.23, p95 1.43 | 보통은 0.2초대인데, 한 상자가 늦어 평균과 p95 가 끌려 올라갔다 |
둘째 print | TPOT 평균 30ms, 지연 p50 2.05초, p95 3.52초 | 출발한 뒤 빠르기는 비슷하고, 늦은 상자는 3.5초쯤 걸렸다 |
TTFT 의 p50 은 0.23초인데 평균은 0.50초입니다. 늦은 요청 하나(1.80초)가 평균을 두 배 넘게 끌어올렸습니다. 가운데 값(p50)과 느린 쪽 값(p95)을 함께 보면 이런 요청이 바로 보입니다.
작게 줄여서 눈으로 보기
숫자 다섯 개로 평균, p50, p95 가 어떻게 다른지 봅니다. 숫자는 설명용입니다.
import numpy as np
lat = np.array([1, 2, 3, 4, 10]) # 요청 다섯 개의 지연(초, 설명용 숫자)
print("평균:", lat.mean())
print("p50:", np.percentile(lat, 50))
print("p95:", round(np.percentile(lat, 95), 2))평균: 4.0
p50: 3.0
p95: 8.8| 결과 | 읽는 법 |
|---|---|
평균: 4.0 | 다섯을 더해 5로 나눴다. 하나뿐인 10초가 평균을 끌어올렸다 |
p50: 3.0 | 늘어놓은 1, 2, 3, 4, 10 의 한가운데 값이다. 10초의 영향을 받지 않는다 |
p95: 8.8 | 95% 자리가 넷째(4)와 다섯째(10) 사이라 둘 사이를 비율로 나눈 값이다. 느린 쪽을 잘 드러낸다 |
주의할 점
- 평균만 보면 가끔 아주 느린 요청이 묻힙니다. 사용자 불만은 느린 쪽에서 나오므로 p95 를 함께 적습니다.
- 처리량과 지연은 함께 움직입니다. 동시 요청을 늘리면 처리량은 늘거나 그대로지만 지연은 길어집니다. "지연 p95 가 몇 초 안일 때의 처리량"처럼 조건을 붙여 말합니다.