목차
- 1. LLM 추론 최적화는 “GPU가 느리다”보다 “어느 단계가 줄을 세우는가”로 봐야 합니다
- 운영에서 먼저 보는 지표
- 2. 지연은 세 갈래로 자르면 판단이 빨라집니다
- 3. 실전 진단은 시스템 30%, 애플리케이션 70%입니다
- 1단계. CPU, 런큐, 디스크, 네트워크, 소켓을 같이 봅니다
- 2단계. GPU가 진짜 계산 병목인지 구분합니다
- 3단계. API 레벨에서 연결 시간과 첫 바이트 시간을 분리합니다
- 4. 애플리케이션 계측이 없으면, 튜닝이 아니라 추측을 하게 됩니다
- 5. LLM 추론 최적화에서 우선순위 높게 손볼 포인트
- 프록시와 연결 재사용: 첫 토큰이 늦으면 여기부터 봅니다
- CPU 경합 줄이기: 토크나이저와 로그가 의외로 많이 잡아먹습니다
- 배치, 동시성, 컨텍스트 길이: 세 개를 따로 보지 마세요
- 6. 재현 가능한 트러블슈팅 시나리오: “가끔만” 느릴 때가 제일 어렵습니다
- 자주 틀리는 해석과 진짜 원인
- 7. 검증은 “얼마나 빨라졌나”보다 “어느 단계가 줄었나”가 중요합니다
- 검증 체크리스트
- 8. 상황별 추천: 이럴 땐 A, 저럴 땐 B로 가면 됩니다
- FAQ
- Q1. GPU만 더 좋은 걸로 바꾸면 해결되나요?
- Q2. 어떤 로그부터 남기면 가장 실무에 도움이 되나요?
- Q3. 배치를 키우면 무조건 효율이 좋아지지 않나요?
- Q4. 프록시 문제와 모델 문제는 가장 빠르게 어떻게 구분하나요?
- 마무리
[인프라] LLM 추론 최적화: 지연 문제 진단과 병목 해결 전략
LLM 추론 최적화는 GPU를 바꾸기 전에 먼저 해볼 일이 꽤 많습니다. 운영에서 실제로 느려지는 이유를 뜯어보면, 모델 자체보다 앞단 연결, 큐 적체, 토크나이저 CPU 경합, 스트리밍 버퍼링 같은 바깥 요인이 더 자주 문제를 만들더라고요. 저도 처음엔 “모델이 무거워서 느리겠지”라고 봤는데, 막상 들어가 보면 keep-alive 미설정, 요청별 JSON 직렬화 비용, 긴 입력이 몰릴 때 생기는 prefill 지연이 한꺼번에 겹친 경우가 많았습니다. 그래서 이 글은 막연한 튜닝 팁보다 어디서 기다리는지 먼저 분해하고, 그다음 손보는 순서에 집중합니다.
핵심은 간단합니다. TTFT(Time To First Token, 첫 토큰까지 시간), TPOT(Time Per Output Token, 출력 토큰당 시간), Queue Wait(대기열 대기 시간)를 분리해서 봐야 합니다. total latency 하나만 붙들고 있으면 프록시 문제를 GPU 문제로, CPU 병목을 모델 병목으로 잘못 읽기 쉽거든요. 운영에서 시간을 아끼는 가장 빠른 방법은 최적화 자체보다 틀린 곳을 만지지 않는 것입니다.

LLM 추론 지연 시간 병목을 한눈에 보여주는 전체 아키텍처 개요입니다.
1. LLM 추론 최적화는 “GPU가 느리다”보다 “어느 단계가 줄을 세우는가”로 봐야 합니다
추론 요청은 보통 아래 단계를 지납니다. 이걸 한 덩어리로 보면 답이 잘 안 나옵니다.
- Ingress 또는 API 프록시: 연결 수립, TLS, keep-alive, buffering
- Queue: 워커가 바쁘거나 동시성 제한에 걸려 대기
- Tokenization: 입력 전처리, 템플릿 결합, 토큰 계산
- Prefill: 긴 입력 문맥을 모델이 한 번에 읽는 구간
- Decode: 토큰을 하나씩 생성하는 구간
- Post-processing: 스트리밍 직렬화, 로그, 압축, 감사 기록
여기서 많이 헷갈리는 지점이 있습니다. GPU 사용률이 낮다고 효율적인 건 아닙니다. 오히려 GPU가 놀고 있는데 응답이 느리다면 그때가 더 골치 아픈 경우가 많습니다. 계산 장치가 아니라 요청 공급 경로가 끊기고 있다는 뜻일 수 있어서요. 반대로 GPU가 꽉 찼다고 바로 나쁜 것도 아닙니다. 처리량이 중요한 작업이라면 높은 점유율이 오히려 정상입니다.
운영에서 먼저 보는 지표
- TTFT: 사용자가 가장 먼저 체감하는 값입니다. 챗봇, 검색 보조, 문서 질의응답은 대부분 여기서 승부가 납니다.
- TPOT: 생성이 시작된 뒤 토큰이 끊기지 않고 매끄럽게 이어지는지 보여줍니다.
- P95/P99 tail latency: 평균만 보면 느린 요청이 숨어버립니다.
- Queue Wait: 워커 수와 동시성 상한이 맞는지 판단할 때 중요합니다.
- GPU utilization / memory utilization / memory used: 계산 병목인지, 메모리 압박인지 가늠하는 기본 축입니다.
- CPU user/system, run queue: 토크나이저, 로깅, 압축, 네트워크 스택 비용을 읽는 데 유용합니다.
- socket reuse, retransmission, proxy buffering: 스트리밍인데 첫 토큰이 늦을 때 꼭 봐야 합니다.
2. 지연은 세 갈래로 자르면 판단이 빨라집니다
실무에서 자주 쓰는 분류는 아래 세 가지입니다. 이렇게 나눠 놓으면 무엇부터 의심할지 훨씬 또렷해집니다.
| 구간 | 관찰되는 증상 | 근본 원인 후보 | 먼저 할 일 | 지금 하지 말 것 |
|---|---|---|---|---|
| 입장 전 | 연결은 되는데 첫 응답이 늦음 | 프록시 buffering, keep-alive 미사용, 워커 앞단 큐 적체 | 프록시와 클라이언트의 연결 재사용, 스트리밍 설정 확인 | GPU 교체부터 검토 |
| 모델 전후 | GPU는 한가한데 total latency가 큼 | 토크나이저 CPU 경합, JSON 직렬화, 감사 로그, gzip, 동기식 후처리 | 요청 단계별 계측 추가, CPU 코어 점유 패턴 확인 | 배치만 무작정 키우기 |
| 모델 내부 | TTFT도 길고 TPOT도 느림 | 긴 입력으로 인한 prefill 부담, KV cache 압박, 메모리 병목, 배치 과대 | 입력 길이 정책, 동시성 제한, 배치 전략 재조정 | 로그만 줄이고 끝내기 |
이 표에서 중요한 건 증상과 처방을 1:1로 바로 묶지 않는다는 점입니다. 예를 들어 TTFT만 길고 TPOT은 멀쩡하다면 prefill이 길어졌을 수도 있지만, 프록시가 첫 바이트를 묶고 있을 수도 있습니다. 반대로 첫 토큰은 빨리 나오는데 뒤가 끊긴다면 디코드보다 스트리밍 flush 간격, 네트워크 backpressure, 응답 직렬화 비용이 더 문제일 때도 있습니다.
3. 실전 진단은 시스템 30%, 애플리케이션 70%입니다
운영 현장에서 느낀 건 이겁니다. nvidia-smi만 봐서는 절반도 못 찾습니다. 시스템 지표로 병목 위치를 좁히고, 애플리케이션 계측으로 원인을 확정해야 합니다. 아래 명령어들은 Linux에서 많이 쓰는 조합인데, 배포판에 따라 <code>sysstat 패키지 설치가 필요하고 소켓 정보는 권한에 따라 일부만 보일 수 있습니다.
1단계. CPU, 런큐, 디스크, 네트워크, 소켓을 같이 봅니다
PID=12345
PORT=8000
pidstat -dur -h -p "$PID" 1
mpstat -P ALL 1
vmstat 1
iostat -x 1
sar -n DEV 1
ss -tinp | grep ":$PORT"
각 명령어를 보는 기준은 꽤 분명합니다.
pidstat -dur: 프로세스별 CPU, I/O, minor/major fault를 함께 봅니다. CPU가 높은데 GPU가 비면 모델 밖 병목일 가능성이 큽니다.mpstat -P ALL: 특정 코어만 과열되면 토크나이저 스레드, 로깅 스레드, 이벤트 루프가 한쪽에 몰린 상황을 의심합니다.vmstat: run queue가 길고 context switch가 튄다면 스레드 수를 늘린 게 오히려 독이 된 경우가 많습니다.iostat -x: 디스크 활용률과 await를 같이 봅니다. 모델 로드가 아니라 로그 flush나 swap 때문에 지연될 수도 있습니다.sar -n DEV: 인터페이스 오류, burst 패턴, 원격 스토리지 경유 트래픽을 파악할 때 좋습니다.ss -tinp:ESTAB연결이 재사용되는지, 요청마다 새 소켓이 생기는지 확인합니다.
이 단계에서 자주 나오는 실패 모드가 있습니다. API 프로세스 CPU는 높고 GPU는 비어 있는데, 팀은 계속 배치나 양자화만 만집니다. 그 방향은 대개 틀립니다. 토큰을 만들기 전에 이미 시간을 다 써버리고 있는데 모델 쪽만 튜닝하고 있는 셈이거든요.
2단계. GPU가 진짜 계산 병목인지 구분합니다
nvidia-smi
nvidia-smi dmon -s pucvmet -d 1
watch -n 1 'nvidia-smi --query-gpu=utilization.gpu,utilization.memory,memory.used,memory.total,power.draw --format=csv,noheader'
여기서는 평균보다 패턴을 보는 편이 낫습니다. 참고로 nvidia-smi dmon은 GPU와 드라이버 지원 여부에 따라 표시 가능한 항목이 조금 다를 수 있습니다.
- 사용률이 톱니형으로 튀고 중간에 비는 경우: 요청 공급이 끊기거나 큐에서 건네주는 속도가 불안정한 경우가 많습니다.
- 사용률은 높은데 메모리도 꽉 찬 경우: 긴 입력, 큰 배치, 동시성 과다로 prefill 또는 KV cache 압박이 강할 가능성이 큽니다.
- 메모리는 넉넉한데 TPOT만 느린 경우: 계산보다 스트리밍, 후처리, 네트워크 flush 간격을 함께 봐야 합니다.
운영 판단에서 중요한 건 이것입니다. GPU utilization 하나로 건강 상태를 판정하지 말 것. 메모리 사용 패턴, 요청 간 공백, TTFT/TPOT과 같이 봐야 의미가 생깁니다.
3단계. API 레벨에서 연결 시간과 첫 바이트 시간을 분리합니다
curl -N -sS -o /dev/null \
-w 'dns=%{time_namelookup}\nconnect=%{time_connect}\nappconnect=%{time_appconnect}\nstarttransfer=%{time_starttransfer}\ntotal=%{time_total}\n' \
-H 'Content-Type: application/json' \
-d '{"prompt":"안녕하세요. LLM 지연 진단 테스트입니다."}' \
http://127.0.0.1:8000/generate
time_starttransfer는 첫 바이트까지의 시간을 보여주므로, 스트리밍 API에서는 TTFT에 가까운 운영 힌트로 쓸 수 있습니다. 다만 엄밀히 말하면 네트워크 경로와 서버 처리 시간을 함께 포함한 값이라, 모델 내부의 첫 토큰 생성 시각과 완전히 같지는 않습니다. 저는 프록시 직통 호출과 프록시 경유 호출을 둘 다 같은 프롬프트로 반복 측정합니다. 여기서 두 값 차이가 크면 모델이 아니라 네트워크 경로와 프록시 설정부터 보는 게 맞습니다.
4. 애플리케이션 계측이 없으면, 튜닝이 아니라 추측을 하게 됩니다
시스템 지표만으로는 “느리다”는 사실까지만 알 수 있습니다. 운영에서 실제로 문제를 줄이려면 요청 하나가 어디에서 얼마나 머물렀는지가 로그에 남아 있어야 합니다. 최소 기준으로 두는 건 아래 네 가지입니다.
- 큐에 들어간 시각과 워커가 잡은 시각
- 토크나이즈 시작/종료 시각
- 모델 실행 시작 시각과 첫 토큰 시각
- 스트리밍 완료 시각과 최종 응답 바이트 수
import time
import logging
from contextvars import ContextVar
from fastapi import FastAPI, Request
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("llm_latency")
request_id_var = ContextVar("request_id", default="-")
app = FastAPI()
def now_ms() -> float:
return time.perf_counter() * 1000
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
rid = request.headers.get("x-request-id", "-")
request_id_var.set(rid)
t0 = now_ms()
response = await call_next(request)
total_ms = now_ms() - t0
log.info("request_id=%s path=%s status=%s total_ms=%.2f",
rid, request.url.path, response.status_code, total_ms)
response.headers["X-Request-Time-Ms"] = f"{total_ms:.2f}"
return response
def log_stage(stage: str, started_ms: float, **fields):
elapsed_ms = now_ms() - started_ms
extra = " ".join(f"{k}={v}" for k, v in fields.items())
log.info("request_id=%s stage=%s elapsed_ms=%.2f %s",
request_id_var.get(), stage, elapsed_ms, extra)
return now_ms()
# 예시 흐름
# t = now_ms()
# t = log_stage("queue_wait", t, queue_depth=queue_depth)
# t = log_stage("tokenize", t, prompt_chars=len(prompt), prompt_tokens=prompt_tokens)
# t = log_stage("prefill", t)
# t = log_stage("first_token", t)
# t = log_stage("decode", t, output_tokens=output_tokens)
# t = log_stage("serialize", t, bytes=response_bytes)
이 정도만 있어도 판단이 꽤 달라집니다. 전체 응답 시간이 길어도 tokenize가 길다면 CPU 쪽이고, first_token 전까지 오래 걸리면 큐나 prefill이 의심됩니다. serialize가 길면 모델이 아니라 응답 포맷팅이나 로깅 설계가 발목을 잡고 있을 수 있습니다. 이거 로그 한 번 쪼개 놓으면 생각보다 훨씬 편하더라고요.

요청 시간을 단계별로 쪼개 기록하는 계측 흐름 예시입니다.
5. LLM 추론 최적화에서 우선순위 높게 손볼 포인트
운영에서 효과가 큰 건 의외로 화려한 알고리즘보다 파이프라인의 낭비를 없애는 일입니다. 아래 항목은 체감 개선이 뚜렷했던 것들입니다.
프록시와 연결 재사용: 첫 토큰이 늦으면 여기부터 봅니다
스트리밍 응답을 프록시 뒤에 둘 때는 keep-alive, buffering, HTTP 버전이 맞물립니다. 이 셋 중 하나만 어긋나도 사용자는 “아예 멈춘 것 같다”고 느낍니다.
upstream llm_backend {
server 127.0.0.1:8000;
keepalive 64;
}
server {
listen 80;
server_name _;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass http://llm_backend;
}
}
특히 proxy_buffering off;는 스트리밍 경로에서 중요합니다. 첫 토큰이 서버에서는 나왔는데 프록시가 중간에서 모아두면, 클라이언트는 모델이 느린 줄 압니다. 이런 경우 GPU 로그를 아무리 봐도 원인이 잘 안 나옵니다. 첫 바이트를 누가 붙잡고 있는지를 봐야 합니다. 환경에 따라 애플리케이션에서 X-Accel-Buffering: no 헤더를 함께 쓰는 것도 도움이 됩니다.
CPU 경합 줄이기: 토크나이저와 로그가 의외로 많이 잡아먹습니다
GPU 서버에서 CPU를 가볍게 보는 경우가 많지만, 실제론 토크나이저와 직렬화, 로깅, 압축이 CPU를 잡아먹으면서 TTFT를 흔드는 일이 잦습니다. 특히 긴 프롬프트를 템플릿과 합치는 코드가 비효율적이면 모델을 호출하기도 전에 시간이 새기 시작합니다.
- DEBUG 로그와 요청별 전체 payload 로깅은 스트리밍 경로에서 끄는 편이 낫습니다.
- gzip은 대역폭이 정말 아쉬운 환경이 아니라면 스트리밍 응답에선 먼저 의심해 볼 만한 비용입니다.
- 토크나이저 스레드 수와 워커 수를 동시에 키우면 오히려 코어 경합이 심해질 수 있습니다.
- 한 프로세스에 너무 많은 역할을 몰아넣지 말아야 합니다. 추론, 로깅, 수집 에이전트, 배치 전처리가 같은 코어 그룹을 두드리면 tail latency가 나빠집니다.
실제로 자주 본 실패 패턴은 이렇습니다. 평균 응답 시간은 그럭저럭인데 사용자가 느끼는 건 “가끔 엄청 느리다”입니다. 이런 경우 평균보다 느린 요청 몇 건의 stage 로그를 보는 편이 훨씬 빠릅니다. tail latency는 대개 경합에서 생기고, 경합은 평균값에 잘 안 드러나거든요.
배치, 동시성, 컨텍스트 길이: 세 개를 따로 보지 마세요
이 세 가지는 묶어서 봐야 합니다. 배치만 키우면 throughput은 나아질 수 있지만 TTFT가 손해를 보고, 동시성을 올리면 GPU는 더 바빠지지만 queue wait와 메모리 압박이 같이 커질 수 있습니다. 긴 입력은 prefill을 늘리고, prefill이 늘면 첫 토큰이 늦어집니다. 결국 한 가지 값보다 서비스 목표에 맞는 균형이 더 중요합니다.
| 목표 | 우선 지표 | 추천 전략 | 피해야 할 실수 |
|---|---|---|---|
| 대화형 챗봇 | TTFT, P95 | 짧은 큐, 과도한 배치 회피, 입력 길이 상한 관리 | 처리량 욕심으로 배치 과대 설정 |
| 비동기 문서 생성 | Throughput, GPU 점유율 | 배치 확대, 워커 활용도 최적화, 후처리 비동기화 | 챗봇용 설정을 그대로 재사용 |
| 요청 길이 편차가 큰 서비스 | P99, Queue Wait | 짧은 요청과 긴 요청 분리, admission control 적용 | 모든 요청을 같은 큐에 넣기 |
| GPU 메모리 여유가 적음 | 안정성, 실패율 | 동시성 상한과 입력 길이 정책을 먼저 고정 | OOM을 배치 재시도로만 덮기 |
판단 기준은 단순합니다. 사람이 기다리는 인터랙티브 서비스면 TTFT를, 백그라운드 작업이면 throughput을 우선합니다. 둘 다 잡겠다고 한 설정으로 몰아가면 보통 둘 다 애매해집니다.
6. 재현 가능한 트러블슈팅 시나리오: “가끔만” 느릴 때가 제일 어렵습니다
현실적인 시나리오 하나를 보겠습니다. 사내 문서 요약 API가 있고, 프록시 뒤에서 스트리밍으로 응답합니다. 사용자는 “매번 느린 건 아닌데, 어떤 요청은 첫 응답이 한참 뒤에 나온다”고 합니다. 이때 평균만 보면 문제가 잘 안 보입니다. 저는 이런 순서로 갑니다.
- 프롬프트 길이가 비슷한 요청과 유난히 긴 요청을 분리합니다. 긴 입력이 섞이면 prefill 지연과 앞단 문제를 구분하기 어려워집니다.
- 프록시 직통과 프록시 경유를 각각
curl -w로 여러 번 측정합니다.starttransfer차이가 크면 프록시부터 봅니다. - ss로 연결이 재사용되는지 확인합니다. 매 요청 새 연결이면 keep-alive가 안 먹고 있을 수 있습니다.
- pidstat / mpstat로 느린 순간 CPU 특정 코어가 튀는지 봅니다. 토크나이즈, 로그 flush, 압축이 원인일 때가 많습니다.
- nvidia-smi dmon으로 GPU가 중간에 비는지 확인합니다. 비는 구간이 길면 모델이 아니라 공급 경로 문제일 가능성이 큽니다.
- 애플리케이션 로그에서 queue_wait, tokenize, first_token 단계를 비교합니다.
이런 케이스에서 자주 잡히는 원인은 두 가지였습니다. 하나는 프록시가 스트리밍을 묶고 있던 경우, 다른 하나는 입력 전처리와 로그가 같은 CPU 코어를 두드려서 TTFT가 흔들리던 경우입니다. 둘 다 공통점이 있습니다. 평균 total latency만 보면 잘 안 보인다는 점입니다.
자주 틀리는 해석과 진짜 원인
- GPU 사용률이 낮다: 좋은 신호가 아닐 수 있습니다. 요청 공급이 끊기거나 CPU가 앞단에서 병목일 수 있습니다.
- 디스크가 바쁘다: 모델을 계속 읽고 있다고 단정하면 안 됩니다. 로그 적재, 임시 파일, swap 가능성도 같이 봐야 합니다.
- 로컬 환경이라 네트워크 문제는 아니다: 로컬에서도 프록시 buffering, 소켓 재사용, flush 지연은 충분히 생깁니다.
- 배치를 키우면 무조건 효율적이다: 처리량은 좋아질 수 있어도 TTFT와 tail latency는 악화될 수 있습니다.
- 평균값만 줄면 됐다: 사용자는 평균이 아니라 느린 요청을 기억합니다. 운영 품질은 P95/P99에서 갈립니다.
7. 검증은 “얼마나 빨라졌나”보다 “어느 단계가 줄었나”가 중요합니다
튜닝 후 검증에서 봐야 할 건 단순한 전후 비교표가 아닙니다. 병목 위치가 실제로 이동했는지를 확인해야 합니다. 예를 들어 total time은 줄었는데 TTFT가 그대로면 사용자는 여전히 답답하다고 느낄 수 있습니다. 반대로 TTFT가 개선됐는데 TPOT이 흔들리면 스트리밍 경험은 여전히 좋지 않을 수 있습니다.
- TTFT: 첫 체감이 나아졌는지 확인합니다.
- TPOT: 토큰 생성이 끊기지 않고 안정적인지 봅니다.
- Queue Wait: 동시성 설정이 맞았는지 판단합니다.
- GPU/CPU 패턴: 톱니형 유휴 구간, 특정 코어 과열, 불규칙한 burst가 줄었는지 봅니다.
- P95/P99: 평균이 아닌 꼬리 지연이 얼마나 개선됐는지 확인합니다.
실무에서는 이 판단이 중요합니다. 최적화가 성공한 것처럼 보여도 사용자 불만은 그대로인 경우가 종종 있습니다. 그럴 때 로그를 뜯어보면 마지막 응답 종료 시점만 짧아졌지 첫 반응은 그대로인 경우가 많았습니다. 대화형 서비스는 특히 초반 1~2초의 인상이 전체 만족도를 크게 좌우합니다.

최적화 전후를 비교하는 검증 대시보드 예시입니다.
검증 체크리스트
- 같은 프롬프트 길이로 여러 번 호출해 편차를 봅니다.
- 짧은 입력과 긴 입력을 분리해 측정합니다.
- 동시 요청이 없을 때와 있을 때를 나눠 봅니다.
- 프록시 직통 호출과 프록시 경유 호출을 둘 다 측정합니다.
- 느린 요청 몇 건의 stage 로그를 직접 확인합니다.
- 튜닝 후에도 GPU 유휴 구간이 남는지 확인합니다.
8. 상황별 추천: 이럴 땐 A, 저럴 땐 B로 가면 됩니다
운영에서는 결국 선택을 해야 합니다. 아래처럼 우선순위를 고정해 두면 쓸데없는 우회가 줄어듭니다.
| 상황 | 우선순위 | 추천 접근 | 보류할 것 |
|---|---|---|---|
| 첫 응답이 답답한 챗봇 | TTFT | 프록시 buffering 해제, 큐 대기 축소, 입력 길이 상한 적용 | 배치 확대부터 시도 |
| 긴 문서 생성 배치 작업 | 처리량 | 배치 전략 최적화, 후처리 비동기화, 로그 비용 절감 | 챗봇과 동일한 저지연 설정 고집 |
| GPU는 한가한데 느림 | 비모델 구간 제거 | 토크나이저, 로깅, JSON 직렬화, 소켓 재사용부터 점검 | 하드웨어 증설 |
| GPU 메모리 여유가 적음 | 안정성 | 입력 길이 정책, 동시성 상한, 긴 요청 분리 | 동시성만 밀어 올리기 |
| 가끔만 매우 느린 tail latency | P95/P99 | 코어 경합, 느린 요청 로그, 긴 입력 분리, 큐 설계 재검토 | 평균값만 보고 종료 |
추천을 한 줄로 줄이면 이렇습니다. TTFT가 길면 프록시와 큐부터, TPOT이 흔들리면 스트리밍과 디코드 주변부터, GPU가 비는데도 느리면 CPU와 전처리부터 보시면 됩니다. 이 순서가 잘 먹히는 이유는 간단합니다. 비용이 적고 효과가 빠른 영역부터 건드리는 편이 증설보다 실패 확률이 훨씬 낮기 때문입니다.
또 하나, LLM 추론 최적화는 설정값 암기 게임이 아닙니다. 모델이 바뀌면 prefill 특성이 달라지고, 프롬프트 정책이 바뀌면 TTFT 분포가 달라집니다. 그래서 개별 숫자를 외우기보다 관측 → 분리 → 수정 → 재검증 흐름을 팀 습관으로 만드는 쪽이 더 오래 갑니다. 비용도 줄고, 장애 대응 시간도 확실히 짧아집니다.
관련 글이 있다면 프롬프트 길이 관리, RAG 캐시 전략, 스트리밍 API 운영 체크리스트와 내부 링크로 묶어 두는 것도 좋습니다. 검색 유입을 넓히는 데도 꽤 도움이 됩니다.

병목 진단 순서와 최적화 선택 기준을 한 장으로 요약한 인포그래픽입니다.
FAQ
Q1. GPU만 더 좋은 걸로 바꾸면 해결되나요?
항상 그렇진 않습니다. GPU 사용률이 낮은데 지연이 길다면 모델 밖에서 시간을 쓰고 있을 가능성이 큽니다. 프록시, 큐, CPU, 전처리, 응답 직렬화부터 먼저 확인하는 편이 맞습니다.
Q2. 어떤 로그부터 남기면 가장 실무에 도움이 되나요?
요청 시작 시각, 큐 진입/탈출, 토크나이즈 시작/종료, 첫 토큰 시각, 응답 종료 시각, 입력 토큰 수, 출력 토큰 수 정도는 꼭 남기는 걸 권합니다. 이 정도만 있어도 TTFT와 total latency를 분리해서 볼 수 있습니다.
Q3. 배치를 키우면 무조건 효율이 좋아지지 않나요?
처리량 관점에선 좋아질 수 있지만, 대화형 서비스에선 TTFT와 tail latency가 나빠질 수 있습니다. 사람이 기다리는 서비스인지, 백그라운드 작업인지부터 먼저 정하는 게 좋습니다.
Q4. 프록시 문제와 모델 문제는 가장 빠르게 어떻게 구분하나요?
같은 요청을 프록시 직통과 프록시 경유로 각각 curl -w 측정해 보시면 됩니다. time_starttransfer 차이가 크면 모델보다 경로 문제일 가능성이 큽니다.
마무리
여러 번 겪고 나니 패턴이 꽤 분명했습니다. 느린 LLM 서비스의 원인은 생각보다 자주 모델 바깥에 있습니다. 그래서 저는 장비보다 먼저 첫 토큰 전까지 어디서 멈추는지, 토큰 생성 중 무엇이 끊는지, GPU가 왜 놀고 있는지부터 확인합니다. 이 순서로 가면 엉뚱한 튜닝을 줄일 수 있고, 증설 없이 해결되는 문제도 꽤 많습니다.
실무 기준으로 추천을 한 줄로 압축하면 이렇습니다. 챗봇이면 TTFT 우선, 배치 작업이면 throughput 우선, GPU가 비면 CPU와 프록시부터, tail latency가 튀면 평균이 아니라 느린 요청 로그부터 보시면 됩니다. 이 기준만 잡아도 LLM 추론 최적화의 시행착오를 크게 줄일 수 있습니다.
![[AI] LLM 추론 최적화: 지연 문제 진단과 병목 해결 전략](https://blog.pswq.net/wp-content/uploads/2026/08/llm-inference-latency-troubleshooting-optimization-thumbnail.jpg)
![[OpenStack] 오픈스택 업그레이드 실패 사례 분석: Horizon 대시보드 접근 불가 문제 해결](https://blog.pswq.net/wp-content/uploads/2026/08/openstack-upgrade-failure-horizon-dashboard-troubleshooting-thumbnail.jpg)





![[Kubernetes] Karpenter 프로비저닝 실패 디버깅 실제 사례 분석](https://blog.pswq.net/wp-content/uploads/2026/08/karpenter-node-provisioning-failure-debugging-case-thumbnail.jpg)





![[보안] Nmap 오류 해결: 스캔 실패 원인부터 디버깅 팁까지](https://blog.pswq.net/wp-content/uploads/2026/06/nmap-scan-error-troubleshooting-debugging-tips-thumbnail.jpg)




