LLM 추론 성능은 “응답 시간이 몇 초인가” 하나로 판단하면 안 된다. 긴 프롬프트를 읽는 prefill, 토큰을 하나씩 쓰는 decode, 여러 사용자를 처리하는 스케줄러, GPU 메모리, 운영 비용이 서로 다른 병목을 만든다.
먼저 나눠야 할 지표
| 지표 | 의미 | 주로 보는 병목 |
|---|---|---|
| TTFT(Time to First Token) | 요청 시작부터 첫 출력 토큰까지의 시간 | 프롬프트 길이, prefill, 큐 대기 |
| TPOT(Time Per Output Token) | 첫 토큰 이후 토큰 간 평균 시간 | decode, 메모리 대역폭, KV 캐시 |
| 처리량(throughput) | 초당 처리 토큰·요청 수 | 배칭, 동시성, GPU 활용률 |
| tail latency | p90·p95·p99 지연 | 긴 요청, 큐 폭주, 캐시 miss |
| 메모리 사용량 | 모델 가중치와 KV 캐시가 차지하는 CPU/GPU 메모리 | 동시 사용자 수, 컨텍스트 길이 |
| 토큰당 비용 | 하드웨어·API 비용을 처리 토큰으로 나눈 값 | 모델 크기, idle capacity, 실패 요청 |
특히 prompt token과 output token을 따로 기록해야 한다. 2,000토큰 프롬프트에 20토큰 답을 쓰는 요청은 prefill이 병목이고, 20토큰 프롬프트에 2,000토큰 답을 쓰는 요청은 decode가 병목이다. 총 토큰 수가 같아도 최적화 방향이 다르다.
단일 요청 벤치마크의 기준
단일 요청에서는 generate() 전체 시간을 한 번 재는 것보다 prefill과 decode를 분리해 본다.
import time
start = time.perf_counter()
outputs = model(input_ids, use_cache=True)
prefill_end = time.perf_counter()
past_key_values = outputs.past_key_values
next_token = outputs.logits[:, -1, :].argmax(dim=-1, keepdim=True)
decode_times = []
for _ in range(max_new_tokens - 1):
step_start = time.perf_counter()
outputs = model(next_token, past_key_values=past_key_values, use_cache=True)
step_end = time.perf_counter()
decode_times.append(step_end - step_start)
past_key_values = outputs.past_key_values
next_token = outputs.logits[:, -1, :].argmax(dim=-1, keepdim=True)CUDA에서는 GPU 작업이 비동기로 실행되므로 타이머 앞뒤에 torch.cuda.synchronize()를 넣어야 wall-clock 측정이 맞는다. 커널 자체만 비교할 때는 CUDA event를 쓰고, 서비스 사용자가 체감하는 시간은 wall-clock으로 본다.
운영 벤치마크에서 빠뜨리기 쉬운 것
실제 서버에서는 단일 요청보다 분포가 중요하다.
- 요청률(request rate)과 동시 사용자 수를 고정한다.
- prompt token과 output token 분포를 실제 트래픽에 가깝게 만든다.
- 평균뿐 아니라 p90·p95·p99 TTFT와 inter-token latency를 기록한다.
- error rate, timeout rate, cancellation을 따로 본다.
- GPU 메모리 peak와 KV cache 증가량을 컨텍스트 길이별로 잰다.
- warmup 구간을 버리고 steady-state만 비교한다.
벤치마크가 모든 프롬프트 128토큰, 모든 출력 128토큰으로만 구성되면 비교는 쉽지만 제품 트래픽을 대표하지 못한다. RAG 앱, 코딩 에이전트, 채팅 앱은 길이 분포가 완전히 다르다.
비용도 성능이다
빠른 시스템이 항상 좋은 시스템은 아니다. 예를 들어 더 큰 GPU로 TPOT를 줄였지만 idle capacity가 커지면 토큰당 비용은 나빠질 수 있다.
cost_per_output_token = hardware_cost_per_second / output_tokens_per_second여기에 저장소, 네트워크, 오케스트레이션, 실패 요청, 엔지니어링 운영비까지 더해야 현실적인 비용이 나온다. 양자화, 작은 모델 라우팅, continuous batching, prompt caching은 모두 지연과 비용을 동시에 보되 품질 회귀를 별도 평가해야 한다.
관련 문서
- llm-inference — LLM 추론이 prefill과 decode로 나뉘는 기본 구조
- llm-inference-latency-tips — TTFT와 TPOT를 줄이는 최적화 방법
- llm-inference-tutorial-continuous-batching — 다중 사용자 요청을 token step 단위로 스케줄링하는 방식
- inference-caching — prefix cache, semantic cache 등 캐시 전략
- vllm — PagedAttention과 continuous batching을 제공하는 LLM 서빙 엔진
참고 자료
- Measuring Performance of Transformer Inference — MachineLearningMastery (2026-08-04)