내가 LLM 지연 시간(Latency)을 잘못 측정하고 있었던 이유
요약
LLM 서비스의 성능을 단순히 하나의 지연 시간(Latency) 수치로 측정할 때 발생하는 문제점을 지적합니다. 사용자 경험을 정확히 파악하기 위해 지연 시간을 큐 대기, 첫 토큰 생성, 생성 시간, 도구 실행 등으로 세분화하여 측정해야 함을 강조합니다.
핵심 포인트
- 단일 지연 시간 측정은 애플리케이션 큐, TTFT, 도구 호출 등 다양한 병목 지점을 숨깁니다.
- 사용자 경험의 핵심은 전체 프로세스인 end-to-end 지연 시간입니다.
- 지연 시간을 queue, ttft, generation, tool, end-to-end의 5가지 항목으로 분리하여 측정해야 합니다.
- Node.js를 활용하여 세분화된 지연 시간을 추적하는 구현 예시를 제공합니다.
오랫동안 저는 모든 LLM 요청에 대해 하나의 지연 시간(Latency) 수치를 기록해 왔습니다.
latency_ms = response_finished_at - request_started_at
이 방식은 유용해 보였습니다. 평균을 계산할 수 있고, p95 차트를 추가할 수 있으며, 모델이 점점 느려지고 있는지 확인할 수 있었기 때문입니다.
하지만 사용자가 AI 기능이 느리다고 불평할 때마다, 그 수치는 무엇을 수정해야 할지 거의 알려주지 못했습니다.
때로는 요청이 제공자(Provider)에게 도달하기 전에 큐(Queue)에서 대기하기도 했습니다. 때로는 나머지 응답은 빠르게 스트리밍되었음에도 첫 번째 토큰(First token)이 느리게 나오기도 했습니다. 때로는 모델이 2초 만에 완료되었는데 도구 호출(Tool call)이 추가로 8초가 걸리기도 했습니다.
이 모든 사례가 제 대시보드에서는 모두 "LLM 지연 시간(LLM latency)"으로 나타났습니다.
그것이 문제였습니다. 저는 요청(Request)을 측정하고 있었지만, 사용자는 기능(Feature)을 기다리고 있었던 것입니다.
하나의 지연 시간 수치는 여러 가지 서로 다른 문제들을 숨깁니다
LLM 기반 기능은 보통 API 요청 자체보다 더 많은 단계를 거칩니다:
사용자 작업 (User action)
-> 애플리케이션 큐 (application queue)
-> LLM 요청 (LLM request)
...
만약 제가 요청을 보내고 마지막 토큰을 받을 때까지의 시간만 기록한다면, 그 전후에 발생하는 모든 일을 놓치게 됩니다.
이제 저는 지연 시간을 최소 다섯 가지 측정 항목으로 분리합니다:
queue_ms: 요청이 시작되기 전 대기하는 데 소비된 시간ttft_ms: 요청 시작부터 첫 번째 유용한 토큰(First useful token)까지의 시간generation_ms: 첫 번째 토큰부터 마지막 토큰까지의 시간tool_ms: 도구(Tools) 실행 또는 외부 작업에 소비된 시간end_to_end_ms: 최초 사용자 작업부터 기능이 완료될 때까지의 시간
이 수치들은 서로 다른 질문에 답을 줍니다.
높은 queue_ms는 애플리케이션 용량이나 동시성 제한(Concurrency limits) 문제를 나타냅니다. 높은 ttft_ms는 스트리밍이 얼마나 반응성 있게 느껴지는지에 영향을 미칩니다. 높은 generation_ms는 출력 길이 또는 모델 동작과 관련이 있을 수 있습니다. 높은 tool_ms는 모델 속도와는 거의 관련이 없습니다.
사용자는 오직 end_to_end_ms만을 봅니다.
작은 Node.js 지연 시간 추적기
다음은 OpenAI Node.js SDK를 사용한 최소한의 예시입니다. LLM_BASE_URL을 설정하면 OpenAI 호환 API와도 작동합니다.
의존성을 설치하세요:
npm install openai
이 내용을 latency-tracker.mjs로 저장하세요:
import OpenAI from "openai";
import { performance } from "node:perf_hooks";
...
다음 명령어로 실행하세요:
LLM_API_KEY="your-api-key" \
LLM_MODEL="your-model-name" \
node latency-tracker.mjs
로그 항목은 다음과 같이 보일 것입니다:
{
"feature": "support_reply",
"status": "success",
...
위의 값들은 로그 형식의 예시일 뿐입니다. 실제 결과는 모델, 제공자 (provider), 프롬프트 (prompt), 출력 길이, 네트워크 및 애플리케이션 워크로드 (workload)에 따라 달라집니다.
첫 번째 토큰까지의 시간 (TTFT)은 전체 지연 시간 (Latency)이 아닙니다
스트리밍 (Streaming)을 통해 "빠르다"는 말이 두 가지 서로 다른 의미를 가질 수 있다는 것을 깨달았습니다.
두 가지 가상의 요청을 가정해 봅시다:
요청 A
TTFT: 300 ms
생성 (Generation): 4700 ms
...
두 요청 모두 전체 모델 지연 시간 (total model latency)은 동일합니다. 하지만 체감되는 느낌은 다릅니다.
요청 A는 사용자에게 거의 즉각적으로 가시적인 피드백을 제공합니다. 반면 요청 B는 인터페이스가 2초 이상 멈춰 있는 것처럼 보이게 만듭니다.
만약 제가 전체 지연 시간 (total latency)만을 차트로 그린다면, 이러한 차이를 볼 수 없습니다. 대화형 애플리케이션 (interactive applications)을 위해서는 스트리밍을 단순한 구현 세부 사항 (implementation detail)으로 취급하는 대신, 별도의 TTFT 백분위수 (percentile)를 확인하고 싶습니다.
또한 저는 단순히 첫 번째 스트림 이벤트 (stream event)가 아니라, 첫 번째 유용한 토큰 (useful token)이 나오는 시간을 기록합니다. 일부 API는 콘텐츠가 시작되기 전에 메타데이터나 빈 델타 (empty delta)를 보내기도 하기 때문입니다.
평균 지연 시간은 제가 중요하게 생각하는 요청들을 숨기고 있었습니다
평균 (Averages)은 편리하지만 종종 오해를 불러일으킵니다.
몇 개의 매우 느린 요청이 합리적인 평균값 안에 숨겨질 수 있습니다. 또한 서로 다른 기능 (features)들은 완전히 다른 지연 시간 프로필 (latency profiles)을 가질 수 있습니다.
짧은 분류 (classification) 호출과 긴 보고서 생성 (report-generation) 호출은 단순히 같은 모델을 사용한다는 이유만으로 동일한 지연 시간 차트를 공유해서는 안 됩니다.
이제 저는 다음과 같은 필드별로 지연 시간을 그룹화합니다:
feature
model
provider
...
그 다음 백분위수 (percentiles)를 살펴봅니다:
- p50은 일반적인 경험을 보여줍니다.
- p95는 정기적으로 발생하는 느린 경험을 보여줍니다.
- p99는 심각한 이상치 (outliers)를 드러내는 데 도움이 됩니다.
저는 지연 시간 (latency) 로그에 사용자 프롬프트 (raw user prompts)를 그대로 넣는 것을 피합니다. 운영 메타데이터 (operational metadata)만으로도 해당 지연이 큐잉 (queuing), 모델 생성 (model generation), 도구 실행 (tool execution), 또는 다른 애플리케이션 단계에 속하는지 식별하기에 충분하기 때문입니다.
모델 지연 시간 (Model latency)과 기능 지연 시간 (feature latency)은 별도의 대시보드가 필요합니다
이러한 구분은 제가 성능 장애를 해석하는 방식을 바꾸어 놓았습니다.
모델은 빠르게 완료되었지만, 검색 서비스 (search service)를 호출하고, 데이터베이스 (database)를 업데이트하며, 다른 워크플로우 (workflow)를 기다려야 하기 때문에 기능 (feature)이 여전히 느리게 느껴지는 상황을 가정해 봅시다.
이를 "LLM 지연 시간 문제"라고 부르는 것은 저를 잘못된 해결책으로 이끕니다. 모델을 교체한다고 해서 느린 데이터베이스 쓰기 (database write) 속도가 빨라지지는 않습니다.
저는 이제 서로 연관된 두 가지 뷰 (view)를 유지합니다.
모델 뷰 (model view)는 다음을 추적합니다:
- 첫 번째 토큰 생성 시간 (time to first token)
- 생성 시간 (generation time)
- 요청 실패 (request failures)
- 출력 크기 (output size)
- 모델 및 제공자 (model and provider)
기능 뷰 (feature view)는 다음을 추적합니다:
- 큐 대기 시간 (queue time)
- 전체 모델 시간 (full model time)
- 도구 실행 시간 (tool execution time)
- 검증 시간 (validation time)
- 영속화 시간 (persistence time)
- 엔드 투 엔드 완료 시간 (end-to-end completion time)
첫 번째 뷰는 추론 계층 (inference layer)을 진단하는 데 도움이 됩니다. 두 번째 뷰는 사용자가 실제로 경험하는 것을 나타냅니다.
실패한 요청에도 타이밍 데이터가 필요합니다
처음에는 성공적인 응답이 있을 때만 지연 시간 메트릭 (latency metrics)을 내보냈습니다. 이는 가장 느린 실패 사례들이 대시보드에서 누락되었음을 의미했습니다.
30초 후의 타임아웃 (timeout)은 성공 로그 코딩 단계에 도달하지 못했기 때문에 지연 시간 차트에 반영되지 않았습니다.
이로 인해 시스템이 신뢰성이 낮을 때 오히려 더 빠르게 보이는 현상이 발생했습니다.
이제 실패 로그에는 다음이 포함됩니다:
- 실패 전 경과 시간 (elapsed time before failure)
- 콘텐츠가 도착했는지 여부
- 실패가 발생한 단계
- 재시도 횟수 (retry attempt)
- 에러 카테고리 (error category)
- 기능 이름 (feature name)
성공 및 실패 지연 시간은 서로 구분 가능하면서도 함께 분석되어야 합니다. 그렇지 않으면 대시보드는 최악의 사용자 경험을 조용히 제외함으로써 성능이 양호한 것처럼 보고할 수 있습니다.
제가 가장 중요하게 생각하는 메트릭
제공자 지연 시간 (Provider latency)은 여전히 중요합니다. 모델을 평가하거나, 성능 저하 (regressions)를 조사하거나, 타임아웃 (timeouts)을 설정할 때 이 데이터가 필요합니다.
하지만 그것은 더 이상 제가 AI 기능(feature)을 설명할 때 사용하는 주요 수치가 아닙니다.
제가 가장 중요하게 생각하는 지표는 다음과 같습니다:
사용자의 동작부터 유효하고, 눈에 보이며, 완료된 결과가 나올 때까지의 시간
이 정의를 사용하면 큐(queues), 재시도(retries), 도구(tools), 검증(validation), 그리고 영속성(persistence)을 반드시 포함하게 됩니다. 또한, 모델의 빠른 응답이 느리거나 고장 난 워크플로우(workflow)를 숨기는 것을 방지해 줍니다.
모델은 빠를 수 있지만, 제품은 느릴 수 있습니다.
전체 경로를 측정하기 시작하자, 지연 시간(latency)은 더 이상 제공업체(provider)에 대한 막연한 불만이 아니라, 제가 실제로 조사할 수 있는 구체적인 엔지니어링 문제들의 집합이 되었습니다.
저는 TokenBay에서 일하고 있어 모델과 제공업체의 동작에 대해 많은 시간을 할애합니다. 하지만 저에게 가장 유용했던 지연 시간에 대한 교훈은 더 광범위했습니다. 먼저 기능의 경계(feature boundary)를 측정하고, 그다음 모든 느린 단계에 담당자가 생길 때까지 이를 세분화하는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기