
운영 중인 챗봇에 “응답이 느려요”라는 문의가 쌓이면, 어디부터 손대야 할지 막막합니다. 이럴 때는 LLM이 느리다는 말을 쪼개서 첫 토큰이 나오기까지 걸리는 시간, 토큰 하나를 생성하는 데 걸리는 시간, 요청이 처리되기 전 대기열에 머무는 시간을 따로 측정해야 원인이 보입니다. 세 구간을 나누지 않고 “전체 응답 시간”만 재면, 프롬프트가 길어서 느린 건지 GPU가 포화된 건지 요청이 몰려서 밀린 건지 구별할 방법이 없습니다.
응답이 느리다는 말이 뭉뚱그리는 세 구간
사용자가 “느리다”고 느끼는 순간은 사실 세 개의 서로 다른 구간이 합쳐진 결과입니다. 요청을 서버에 보낸 뒤 화면에 글자가 처음 찍히기까지의 시간(TTFT, Time To First Token), 그 이후 토큰이 한 개씩 이어지는 속도(TPOT, Time Per Output Token), 그리고 요청이 서버에 도착했지만 GPU가 바빠서 처리를 시작하지 못하고 기다리는 큐 대기 시간(Queue Wait Time)입니다.
이 세 구간은 원인도, 해결책도 완전히 다릅니다. TTFT가 길면 프롬프트가 너무 길거나 prefill 연산이 무거운 경우이고, TPOT가 길면 GPU 연산 자체가 느리거나 배치가 비효율적인 경우이며, 큐 대기가 길면 동시 요청 수가 서버 처리 능력을 넘어선 경우입니다. 하나로 뭉쳐서 “느림”이라고만 부르면 세 가지 중 무엇을 고쳐야 할지 알 수 없습니다.
TTFT 측정법: 스트리밍 응답에서 첫 토큰까지 걸린 시간 재기
TTFT는 반드시 스트리밍(streaming) 응답으로만 잴 수 있습니다. 스트리밍을 켜지 않고 전체 응답을 한 번에 받는 방식으로 호출하면, 서버가 응답을 완성해서 통째로 돌려주기 때문에 “첫 토큰이 언제 나왔는지”는 클라이언트에서 원천적으로 알 수 없습니다. 그래서 OpenAI 호환 API나 vLLM, TGI(Text Generation Inference) 같은 추론 서버를 쓸 때는 stream=True 옵션이 TTFT 측정의 전제 조건이 됩니다.
측정 방식은 요청을 보낸 시각을 찍고, 스트리밍 청크가 처음 도착한 시각을 찍어서 그 차이를 구하는 것입니다. Python에서 OpenAI 클라이언트 라이브러리를 쓰면 다음과 같이 재현할 수 있습니다.
import time
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
def measure(prompt: str, model: str):
t0 = time.perf_counter()
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=True,
max_tokens=128,
)
ttft = None
token_times = []
for chunk in stream:
now = time.perf_counter()
delta = chunk.choices[0].delta.content
if delta:
if ttft is None:
ttft = now - t0
token_times.append(now)
tpot = None
if len(token_times) >= 2:
tpot = (token_times[-1] - token_times[0]) / (len(token_times) - 1)
return ttft, tpot, len(token_times)
ttft, tpot, n = measure("한국의 수도는 어디인가요?", "meta-llama/Llama-3.1-8B-Instruct")
print(f"TTFT: {ttft*1000:.1f} ms")
print(f"TPOT: {tpot*1000:.1f} ms/token")
print(f"생성 토큰 수: {n}")

예시 출력은 아래와 같은 형태로 나옵니다. 실제 값은 모델 크기, GPU, 동시 요청 수에 따라 크게 달라지므로 절대적인 기준으로 삼지 말고 형식만 참고하시면 됩니다.
TTFT: 312.4 ms
TPOT: 41.7 ms/token
생성 토큰 수: 47
토큰당 시간(TPOT)은 왜 따로 봐야 할까요
TTFT만 좋고 TPOT가 나쁜 경우가 실제로 자주 발생합니다. 예를 들어 짧은 프롬프트라 첫 토큰은 300ms 만에 나왔는데, 이후 토큰이 하나씩 나오는 간격이 100ms씩 벌어진다면 답변이 길어질수록 체감 속도는 급격히 나빠집니다. 128토큰짜리 답변이면 TTFT 300ms보다 TPOT가 누적되는 13초 가까운 시간이 사용자 체감에 훨씬 더 크게 작용합니다.
TPOT가 늘어나는 원인은 보통 GPU 연산 자체보다 배치 처리 방식에 있습니다. 여러 요청을 동시에 처리하는 연속 배치(continuous batching) 환경에서는 내 요청과 함께 묶인 다른 요청들의 시퀀스 길이, KV 캐시 메모리 여유 공간이 토큰 생성 속도에 직접 영향을 줍니다. 그래서 TPOT가 특정 시간대에만 나빠진다면 동시 접속자 수 그래프와 겹쳐 보는 것이 원인 파악에 도움이 될 수 있습니다.
큐 대기 시간은 클라이언트가 아니라 서버 메트릭에서 확인해야 하는 이유
TTFT와 TPOT는 클라이언트에서 스트리밍 응답만 받아도 계산할 수 있지만, 큐 대기 시간은 그렇지 않습니다. 클라이언트가 보는 TTFT 안에는 “대기열에서 기다린 시간”과 “실제로 GPU가 prefill 연산을 하는 시간”이 섞여 있기 때문에, 겉에서는 둘을 구분할 수 없습니다. 이 부분을 나누려면 요청이 서버에 도착한 시각과 스케줄러가 실제로 처리를 시작한 시각을 서버 쪽에서 직접 로깅해야 합니다.
자체 호스팅 추론 서버를 쓴다면 이 값을 이미 제공하는 경우가 있습니다. 오픈소스 추론 엔진인 vLLM은 Prometheus 형식으로 요청 단위 지표를 노출하는데, 요청이 대기열에 머문 시간과 실제 처리(prefill·decode)에 걸린 시간을 구분해서 기록합니다. 이런 지표는 동시 요청이 몰릴 때 서버 처리 능력이 부족한 건지 아니면 순수 연산이 느린 건지 구분하는 근거가 되며, 이 구분은 대기행렬 이론으로 설명되는 현상이기도 합니다. 반면 OpenAI, Anthropic 같은 상용 API를 그대로 쓰는 경우에는 이런 서버 내부 지표에 접근할 수 없어서, 큐 대기 시간은 추정만 가능하고 정확히 분리해서 잴 수는 없습니다.

세 지표를 한 번에 기록하는 코드와 원인 좁히기 표
세 지표를 동시에 로깅해두면, 이상 발생 시 어느 구간이 늘어났는지 비교만으로 원인을 좁힐 수 있습니다. 자체 서버라면 요청 수신 시각을 헤더나 로그에 남기고, 클라이언트 TTFT와의 차이로 대략적인 큐 대기를 근사할 수 있습니다.
| 지표 | 측정 시점 | 어디서 재나요 | 값이 커지면 의심할 곳 |
|---|---|---|---|
| TTFT | 요청 전송 ~ 첫 토큰 도착 | 클라이언트(스트리밍 필수) | 프롬프트 길이, prefill 연산량, 큐 대기 혼입 |
| TPOT | 첫 토큰 ~ 마지막 토큰, 토큰 간 평균 | 클라이언트(스트리밍 필수) | 동시 배치 크기, KV 캐시 부족, GPU 포화 |
| 큐 대기 시간 | 요청 도착 ~ 스케줄러 처리 시작 | 서버 내부 메트릭(Prometheus 등) | 동시 요청 수 초과, max_num_seqs 설정값 |
세 값을 같은 타임스탬프 기준으로 모아 대시보드에 쌓아두면, “느려졌다”는 문의가 들어왔을 때 로그를 뒤지지 않고 그래프만 봐도 어느 구간 문제인지 바로 나옵니다. vLLM처럼 Prometheus 지표를 노출하는 엔진을 쓴다면 Grafana로 세 값을 같은 패널에 겹쳐 그리는 것만으로 충분합니다.
같은 프롬프트인데 왜 TTFT가 매번 다르게 나올까요
동일한 프롬프트를 반복 호출해도 TTFT가 200ms에서 800ms까지 흔들리는 경우가 있습니다. 대부분은 측정 문제가 아니라 서버 상태가 매번 다르기 때문입니다. 연속 배치 서버는 내 요청 하나만 처리하는 게 아니라 그 순간 들어온 다른 요청들과 함께 스케줄링하므로, 같은 프롬프트라도 동시에 몰린 요청 수에 따라 TTFT가 달라집니다.
이 변동성을 줄이려면 평균값 하나가 아니라 p50, p95, p99 같은 퍼센타일로 봐야 합니다. 평균 TTFT가 400ms라도 p99가 3초라면, 일부 사용자는 실제로 심각하게 느린 경험을 하고 있다는 뜻입니다. vLLM의 벤치마크 스크립트(benchmark_serving.py)도 TTFT와 TPOT를 퍼센타일 단위로 집계하는 방식을 기본으로 채택하고 있는데, 이 코드는 vLLM 저장소에서 직접 확인할 수 있습니다.
이 측정 방식이 항상 통하는 것은 아닙니다. 상용 API처럼 서버 내부에 접근할 수 없는 환경에서는 큐 대기 시간을 정확히 분리할 수 없고, TTFT 안에 섞인 채로만 관찰됩니다. 또한 짧은 프롬프트·짧은 응답 테스트로 얻은 TPOT 값을 실제 서비스의 긴 대화 맥락에 그대로 적용하면 오차가 커질 수 있으므로, 실제 트래픽 패턴과 비슷한 조건에서 재측정하는 과정이 필요합니다.
LLM이 느리다는 말을 TTFT, TPOT, 큐 대기 세 지표로 쪼개서 보면, 다음번에 “느려요”라는 문의가 들어왔을 때 로그 하나만 확인해도 원인을 좁힐 수 있습니다. 자체 호스팅 환경이라면 오늘 바로 스트리밍 응답에 타임스탬프를 찍는 것부터 시작해 보시고, 추론 엔진이 Prometheus 지표를 제공한다면 세 값을 같은 대시보드에 올려두는 작업을 다음 단계로 진행해 보시기 바랍니다.
