이 글은 Internal LLM Serving 시리즈의 두 번째 편이다. 내부 GPU 서빙이 필요한 이유와 vLLM 도입 조건은 첫 편에서 다룬다.
1. 모델을 올린 다음에 시작되는 문제
나는 한국 대기업의 온프레미스 환경에서 오픈소스 LLM을 내부 서비스와 Agent에 제공하는 일을 하고 있다. 국가핵심기술과 관련된 데이터의 보안 요구 때문에 외부 상용 API에 모든 요청을 보낼 수 없다. Kubernetes Pod에 모델을 올리고, vLLM이 제공하는 API를 여러 애플리케이션에서 호출하는 방식이다. 여기서는 특정 조직의 구성이나 수치를 공개하지 않고, 그 환경에서 필요한 판단 방법을 다룬다.
처음에는 모델이 GPU 메모리에 올라가고 답변이 돌아오면 큰 고비를 넘었다고 생각하기 쉽다. 하지만 서비스가 늘어나면 질문이 바뀐다. 긴 문서를 읽는 RAG와 짧은 대화를 같은 자원에서 처리해도 되는가? Agent가 계획, 도구 결과 해석, 최종 답변을 위해 여러 번 모델을 호출하면 부하가 얼마나 늘어나는가? GPU를 바로 추가하기 어려운 상황에서 어디까지 서비스할 수 있는가?
vLLM은 단순히 모델을 띄워주는 서버가 아니라, 제한된 GPU 자원을 여러 추론 요청 사이에서 어떻게 나눌지를 결정하는 스케줄러이기도 하다. 따라서 배포의 성공과 운영의 성공은 다른 문제다.
이 글의 목표함수는 명확하다. 서비스의 지연 시간·오류 SLO를 만족하면서 GPU당 지속 가능한 throughput을 최대화한다. 여기서 지속 가능하다는 말은 짧은 벤치마크의 최고점이 아니라, 대표 부하를 계속 받았을 때 대기열이 계속 증가하지 않고 안정적으로 완료할 수 있다는 뜻이다.
문서 확인일: 2026-09-20. 공식
stable문서의 V1 설명을 기준으로 작성했다.stableURL은 바뀌므로 실제 운영에서는 설치 버전·이미지 다이제스트·CLI 도움말·시작 로그·/metrics를 함께 기록해야 한다. 아래 YAML, 부하·SLO·성능 수치는 모두 설명용 가상 예시이며 실제 회사의 운영값이나 측정 결과가 아니다.
2. Kubernetes Pod 위에 모델을 올리는 구조
가장 작은 경로는 내부 애플리케이션 → Kubernetes Service → vLLM Pod → GPU다. 운영에서는 이 앞에 인증과 요청 제한을 담당하는 게이트웨이를 둘 수 있다.
- 내부 Agent · RAG · Chat
- Gateway: 인증 · 요청 제한
- Kubernetes Service
- vLLM Pod: API + Scheduler
- GPU: 모델 가중치 + KV cache
Deployment는 Pod의 실행 상태를 관리하고 Service는 Pod 교체와 무관하게 접근할 경로를 제공한다. GPU는 드라이버와 장치 플러그인이 준비된 노드에서 확장 리소스로 할당한다. GPU requests와 limits를 모두 지정하면 같아야 하며, limits만 적으면 이를 요청값으로 사용한다. CPU와 메모리도 함께 계획해야 토큰화나 프런트엔드가 병목이 되는 일을 진단할 수 있다. Kubernetes GPU scheduling
다음은 그대로 배포하기 위한 완성본이 아닌 설명용 매니페스트다. 이미지 태그는 승인된 고정 버전으로 치환하고 가능하면 다이제스트까지 고정한다. 모델 파일은 검증된 PVC에 미리 준비하며, CPU·RAM·공유 메모리와 프로브 시간은 실제 측정에 맞춘다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-example
spec:
replicas: 1
# Illustrative: avoids needing a spare GPU during replacement.
# Recreate causes downtime; choose an availability plan separately.
strategy:
type: Recreate
selector:
matchLabels:
app: inference-example
template:
metadata:
labels:
app: inference-example
spec:
containers:
- name: vllm
image: vllm/vllm-openai:REPLACE_WITH_PINNED_VERSION
command: ["vllm", "serve"]
args:
- /models/approved-model
- --host
- "0.0.0.0"
- --port
- "8000"
- --tensor-parallel-size
- "1"
- --max-model-len
- "8192"
- --gpu-memory-utilization
- "0.90"
- --max-num-seqs
- "64"
- --max-num-batched-tokens
- "8192"
ports:
- name: http
containerPort: 8000
resources:
requests:
cpu: "4"
memory: 16Gi
nvidia.com/gpu: "1"
limits:
cpu: "8"
memory: 32Gi
nvidia.com/gpu: "1"
volumeMounts:
- name: model
mountPath: /models
readOnly: true
- name: shm
mountPath: /dev/shm
startupProbe:
httpGet: {path: /health, port: http}
periodSeconds: 10
failureThreshold: 60
readinessProbe:
httpGet: {path: /health, port: http}
periodSeconds: 10
livenessProbe:
httpGet: {path: /health, port: http}
periodSeconds: 30
failureThreshold: 5
volumes:
- name: model
persistentVolumeClaim:
claimName: approved-model-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: 2Gi
---
apiVersion: v1
kind: Service
metadata:
name: inference-example
spec:
type: ClusterIP
selector:
app: inference-example
ports:
- port: 8000
targetPort: http
시작 프로브는 모델 로딩·준비 중 성급한 재시작을 막고, 준비 상태 프로브는 트래픽 수신 가능 여부, 생존 프로브는 재시작 필요 여부를 판단한다. /health 성공은 지연 시간 SLO 충족을 보장하지 않는다. 부하 중 응답 지연을 장애로 오인해 재시작이 반복되지 않도록 시간 초과와 실패 임계값도 검증한다. 메모리 기반 /dev/shm은 공짜 공간이 아니므로 컨테이너 메모리 예산에 포함한다. Kubernetes probes, vLLM Kubernetes deployment
텐서 병렬화는 모델 연산을 여러 GPU에 나누는 선택이다. 값을 늘릴 때는 GPU 할당 수, 모델의 지원 조건과 GPU 간 통신 비용을 함께 확인한다. 단순히 복제본 수를 늘리는 것과 같지 않다. 복제본은 각각 가중치와 cache를 갖는 별도 서버이고, TP는 하나의 모델 실행에 여러 GPU가 협력하는 구조다.
YAML의 0.90은 예시로 명시한 값이다. 확인 시점의 serve CLI는 기본값을 0.92로 안내한다. 이 옵션은 GPU 연산 사용률 목표가 아니라 실행기의 메모리 예산 비율이다. --kv-cache-memory-bytes를 명시하면 그 KV 설정이 우선하므로 두 옵션을 혼동하지 않는다. 최신 태그로 자동 갱신하기보다 검증된 버전을 고정하고 업그레이드마다 재측정한다.
3. 왜 추론 계층을 REST API로 분리하는가
Agent A/B · RAG · 채팅 앱 → API → vLLM → GPU 구조에서는 앱 배포와 모델 교체를 분리할 수 있다. 앱마다 가중치를 적재하거나 GPU를 소유하게 만드는 대신, 공통 추론 계층의 용량을 관리한다. Python이든 다른 언어든 같은 HTTP 계약을 사용하고, 인증·요청 제한·사용량·관측 경계를 모을 수 있다.
vLLM의 OpenAI 호환 API는 기존 클라이언트 재사용을 돕는다. 다만 모든 모델과 기능이 완전히 같다는 뜻은 아니다. 채팅 템플릿, 지원 매개변수, 도구 호출, 스트리밍 동작은 고정 버전과 모델로 검증한다. 공식 API 문서
공통 계층은 공유 병목이기도 하다. 요청 수뿐 아니라 입력·출력 토큰 상한, 시간 초과, 취소 전달, 재시도 횟수와 대기열 한계를 정해야 한 Agent의 폭주가 다른 서비스를 밀어내지 않는다. 스트리밍 응답을 프록시가 버퍼링하면 GPU가 빠르게 생성해도 사용자는 첫 토큰을 늦게 본다.
--api-key 하나를 유일한 보안 경계로 삼지 않는다. 공식 보안 문서는 API key가 모든 endpoint를 보호하지 않는다고 설명한다. Gateway/reverse proxy에서 필요한 경로만 노출하고, NetworkPolicy와 네트워크 격리로 접근을 제한하며, TLS와 사용자별 권한·quota를 별도 설계한다. /metrics도 내부 관측 경로로 관리한다. 위 YAML의 ClusterIP만으로 이 보안 구성이 완성되는 것은 아니다. vLLM Security
4. Prefill과 decode는 서로 다른 일을 한다
Prefill은 입력 프롬프트를 처리하고 첫 출력에 필요한 상태를 만드는 과정이다. 긴 RAG 문서를 넣으면 처음 읽어야 할 토큰이 많아진다. Decode는 앞서 생성한 토큰과 저장된 상태를 이용해 다음 토큰을 생성하는 과정을 반복한다.
- 요청 도착 · Queue
- Prompt 처리: Prefill
- 첫 토큰
- Decode → 다음 토큰
- Decode → 다음 토큰 …
TTFT: 요청 → 첫 토큰 · TPOT: 첫 토큰 이후 평균 간격. 도식은 비례 축이 아니다.
일반적으로 prefill은 많은 입력 토큰을 한꺼번에 처리해 상대적으로 연산 집약적이고, decode는 매 단계 가중치와 KV 상태를 읽는 부담 때문에 메모리 대역폭의 영향을 크게 받기 쉽다. 그러나 이는 진단의 출발점이지 모든 하드웨어·배치·모델에 적용되는 법칙은 아니다. vLLM tuning guide
그래서 “응답이 느리다”만으로는 다음 설정을 정할 수 없다. 첫 글자가 늦게 나오는지, 나오기 시작한 뒤 글자 사이가 느린지를 먼저 분리해야 한다. 긴 prompt와 긴 output은 서로 다른 방식으로 공유 자원을 점유한다.
5. KV cache: 동시성의 숨은 메모리 비용
Attention은 이전 token의 key와 value를 재사용한다. 이를 저장한 KV cache가 없으면 이미 처리한 문맥에 대한 계산을 반복해야 한다. 요청이 생성 중인 동안 상태를 유지하므로 가중치가 올라간 뒤 남은 공간이 곧 무한한 동시 처리 공간은 아니다. vLLM APC 설명
직관적으로는 다음과 같이 생각할 수 있다.
KV cache pressure ≈ active sequences
× sequence length
× model-dependent KV size
시퀀스 길이는 프롬프트에 지금까지 생성한 출력을 더한 길이다. 이 식은 정확한 용량 산식이 아니다. 레이어 수, KV head 수, dtype, 어텐션 구조, 블록 할당, 접두부 공유와 병렬화 방식에 따라 달라진다. 용량 계획에는 실행 환경이 산출한 KV 용량과 실제 최댓값을 사용한다.
긴 문맥, 많은 동시 요청, 긴 출력이 겹치면 처음에는 잘 시작한 요청도 생성 중 메모리 압박을 키운다. GPU 메모리가 많이 잡혀 있다는 것과 KV 풀이 실제로 얼마나 차 있는지도 다르다. vLLM이 미리 확보한 메모리 안에서 cache가 쓰이기 때문이다. 따라서 프레임 버퍼 사용량만으로 동시 처리 여유를 판단하지 않는다.
6. 연속 배치: 끝난 요청의 자리를 다시 사용한다
요청마다 프롬프트 길이와 종료 시점이 다르다. 정적 배치에서 모든 요청이 끝날 때까지 다음 요청을 기다리면 짧은 요청이 비운 자리를 활용하기 어렵다. 연속 배치는 스케줄링 단계마다 완료된 요청을 빼고 실행 가능한 요청을 구성해 빈자리를 활용하는 방식이다.
그러나 빈자리가 생겼다는 것이 즉시 모든 요청을 넣을 수 있다는 뜻은 아니다. 시퀀스 상한, 이번 단계의 토큰 예산, KV 용량과 다른 제약을 동시에 만족해야 한다. 이 세 가지를 구분하면 “배치 크기를 늘려라”라는 모호한 조언에서 벗어날 수 있다. vLLM scheduler config
7. max_num_seqs와 max_num_batched_tokens는 다른 제한이다
max_num_seqs는 한 반복 단계에 처리할 시퀀스 수의 상한이다. max_num_batched_tokens는 한 반복 단계에 처리할 토큰 수의 상한이다. 둘 다 대기열 전체 길이나 초당 요청 수를 뜻하지 않는다. CLI에서는 각각 --max-num-seqs, --max-num-batched-tokens처럼 하이픈을 쓴다. serve CLI
한 iteration에 처리할 sequence 수의 상한
동시 처리 증가 → KV 수요 증가 가능한 iteration에 처리할 token 수의 상한
Prefill과 decode가 같은 예산을 사용요청이 iteration 사이에도 유지하는 상태
누적 context와 output이 메모리를 점유설명용 예시로 일반적인 1토큰 decode 32개가 있으면 대략 32토큰의 이번 단계 예산을 쓴다. 여기에 1,000토큰 prefill 조각을 넣으면 약 1,032토큰과 33개 시퀀스 자리가 필요하다. 하지만 decode 요청들이 이미 보유한 과거 문맥의 KV는 그 32토큰보다 훨씬 클 수 있다. 이번에 계산하는 토큰 수와 메모리에 보관하는 토큰 수는 같지 않다. 추측 디코딩 등에서는 이 단순 계산도 달라진다.
max_num_seqs를 늘리면 더 많은 요청을 함께 실행할 기회가 생기는 대신 KV 수요와 단계별 시간이 커질 수 있다. 토큰 예산을 늘려도 시퀀스 상한이 막으면 요청 수는 더 늘지 않는다. 반대로 시퀀스 상한만 높여도 토큰 예산과 KV가 모자라면 기대한 동시성을 얻지 못한다.
두 옵션의 기본값을 하나의 고정 숫자로 외우지 않는다. 현재 EngineArgs 구현은 하드웨어, 사용 맥락과 설정에 따라 값을 결정한다. 벤치마크에는 명시값뿐 아니라 최종 적용 설정을 남긴다.
8. 긴 prompt와 chunked prefill
긴 prefill을 한 번에 처리하는 동안 decode가 기다리면 이미 답변을 받던 사용자의 토큰 간격이 벌어질 수 있다. 분할 prefill은 긴 입력의 계산을 여러 스케줄링 단계로 나눠 decode와 함께 다루는 기능이다. 앱에서 문서를 여러 개의 독립 프롬프트로 쪼개 보내는 것과는 다르며, 한 요청의 문맥은 유지된다.
현재 V1 문서는 가능한 경우 기본 활성화라고 설명한다. 기본 결정은 모델 지원 조건에도 의존하므로 --enable-chunked-prefill의 실효 상태를 시작 로그에서 확인한다. 과거 V0의 기본 scheduler 설명을 V1에 그대로 적용하지 않는다. Chunked prefill, 지원 조건과 기본값 결정
9. 무엇을 측정할 것인가
대시보드는 네 그룹을 같은 시간축으로 읽을 수 있어야 한다. 아래는 확인 시점의 공식 V1 지표 계열이다. 카운터는 Prometheus 엔드포인트에 통상 _total, 히스토그램은 _bucket, _sum, _count 형태로 노출된다. 실제 이름·레이블·단위는 해당 Pod의 /metrics HELP/TYPE과 표본에서 확정한다. 과거 V0 이름을 섞거나 없는 지표를 0으로 채우지 않는다. Production Metrics
| 그룹 | 볼 것 | 현재 문서의 metric family |
|---|---|---|
| 사용자 지연 시간 | TTFT / 요청별 TPOT / E2E, p50·p95·p99 | vllm:time_to_first_token_seconds, vllm:request_time_per_output_token_seconds, vllm:e2e_request_latency_seconds |
| 스케줄러 | 실행 중 / 대기 중 / 대기 시간 / 선점 | vllm:num_requests_running, vllm:num_requests_waiting, vllm:request_queue_time_seconds, vllm:num_preemptions |
| GPU / KV | KV 점유율, GPU 연산·메모리 관측 | vllm:kv_cache_usage_perc + DCGM |
| 처리량 | 입력 / 출력 tok/s, 성공 요청/s | vllm:prompt_tokens, vllm:generation_tokens, vllm:request_success의 증가율 |
토큰 간격 자체는 vllm:inter_token_latency_seconds로 별도 관찰한다. KV 게이지는 이름에 perc가 있어도 1이 100%다. 구버전의 vllm:gpu_cache_usage_perc를 현재 이름으로 가정하면 안 된다. 접두부 재사용이 있는 경우 입력 토큰 수가 cache 저장분과 계산분 중 무엇을 포함하는지도 설치 버전의 HELP와 정의로 확인한다.
NVIDIA exporter에서는 DCGM_FI_DEV_GPU_UTIL, DCGM_FI_DEV_FB_USED, DCGM_FI_DEV_FB_FREE, DCGM_FI_DEV_MEM_COPY_UTIL 등을 볼 수 있다. 마지막 항목은 framebuffer 용량 비율과 다르며 이를 곧바로 GB/s로 해석하지 않는다. 더 상세한 DRAM profiling 지표는 hardware 지원과 exporter 수집 설정을 확인한 뒤 사용한다. 기본 counter 목록에 없는 항목은 자동으로 수집된다고 가정하지 않는다. NVIDIA counter 정의
다음 PromQL은 엔드포인트에서 해당 시계열과 레이블이 확인됐다는 전제의 설명용 쿼리다. 실제로는 작업·모델·배포 레이블로 대상 범위를 제한한다.
histogram_quantile(
0.95,
sum by (le, model_name) (
rate(vllm:time_to_first_token_seconds_bucket[5m])
)
)
sum by (model_name) (rate(vllm:generation_tokens_total[5m]))
sum by (model_name) (increase(vllm:num_preemptions_total[5m]))
Pod별 p95를 평균내지 않고 히스토그램 구간을 합친 뒤 분위수를 구한다. 관측 수가 적거나 구간이 거칠면 p99는 불안정하거나 부정확할 수 있다. 전체 서버군 합계와 복제본별 부하 편중도 함께 본다. 게이트웨이의 거부·시간 초과·취소와 클라이언트 지연 시간은 서버의 성공 요청 히스토그램만으로 알 수 없으므로 별도 수집한다.
10. TTFT와 TPOT: 첫 대기와 생성 속도를 분리한다
TTFT는 요청 도착부터 첫 토큰까지의 시간이다. 클라이언트에서 잰 값에는 네트워크와 게이트웨이가 포함되며 서버 지표의 경계와 다를 수 있다. 스트리밍에서는 내용 없는 첫 이벤트를 첫 토큰으로 세지 않도록 정의한다.
TTFT ≈ 대기 + prefill + 스케줄링·프런트엔드 오버헤드
TPOT = (마지막 토큰 시각 − 첫 토큰 시각) / (출력 토큰 수 − 1)
두 번째 식은 출력이 2토큰 이상인 요청에 대한 정의다. 1토큰 요청의 처리 기준은 따로 기록한다. TPOT는 요청마다 평균을 만든 뒤 분포를 계산하고, ITL은 토큰 사이의 개별 간격을 관측한다. 따라서 TPOT p95와 ITL p95는 같은 수치가 아니다. 전자는 전체 생성 속도를, 후자는 중간에 끊기는 경험을 보완해서 설명한다. 서버와 벤치마크 클라이언트의 정의도 일치하는지 확인한다. vLLM metrics 설계
| 관측 조합 | 먼저 검증할 가설 | 다음 확인 |
|---|---|---|
| TTFT↑ + 대기열↑ | 도착률이 처리 용량을 초과하거나 수용 제어/스케줄러가 막힘 | 유입·완료 RPS, 대기열 증가 추세, 복제본 편중 |
| TTFT↑ + 대기열 짧음 + 프롬프트 김 | Prefill 비용이 지배적일 수 있음 | 프롬프트 길이별 TTFT, prefill 시간, 캐시 적중 |
| TTFT 정상 + TPOT 악화 | Decode 동시성·메모리 압박 또는 단계 간섭 가능성 | ITL, 활성 시퀀스, KV, DRAM, 선점 |
이는 원인 확정표가 아니라 가설을 고르는 표다. 예를 들어 클라이언트 TTFT만 늘었다면 프록시 버퍼링이나 네트워크도 먼저 살펴야 한다.
11. 평균 대신 p95와 p99를 함께 보는 이유
평균은 대부분의 짧은 요청에 가려진 긴 대기를 숨길 수 있다. p95는 관측 요청의 95%가 그 값 이하라는 뜻이며, p99는 더 드문 지연을 보여준다. 표본 수, 시간 구간과 작업 부하 구성이 달라지면 비교도 달라진다.
Agent 업무 흐름에서는 여러 LLM 호출이 직렬로 이어지거나 병렬 호출 중 가장 느린 결과를 기다린다. 설명용 계산으로 독립적인 10회 호출이 각각 95% 확률로 어떤 경계 안에 들어온다면, 전부 그 안에 들어올 확률은 0.95^10 ≈ 60%다. 실제 호출은 부하 때문에 상관될 수 있어 이 계산은 설명용일 뿐이다. 개별 호출의 p95를 더한 값이 업무 흐름 p95인 것도 아니다. 모델 호출과 별도로 업무 흐름 E2E를 직접 측정해야 한다.
12. 실제 숫자의 형태로 판단해 보기
아래 모든 값은 설명을 위한 예시이며 실측이 아니다. 같은 가상 작업 부하·GPU 할당·측정 구간을 놓고 읽는 비교표다. GPU/KV/대기 요청은 구간 평균을 가정하고, 실제 평가표에는 최댓값·추세·분포도 추가한다.
| 지표 | 설정 A | 설정 B | 설정 C |
|---|---|---|---|
| max_num_seqs | 32 | 64 | 128 |
| max_num_batched_tokens | 2,048 | 8,192 | 16,384 |
| GPU 사용률 | 56% | 87% | 96% |
| KV 캐시 | 48% | 76% | 94% |
| 대기 요청 | 17 | 3 | 1 |
| TTFT p95 | 2.8s | 1.2s | 0.9s |
| TPOT p95 | 38ms | 44ms | 86ms |
| Output tok/s | 1,100 | 1,950 | 2,100 |
| 선점 | 0 | 0 | 발생 |
Illustrative SLO가 TTFT p95 1.5초 미만, TPOT p95 50ms 미만이라면 A는 첫 응답이 늦고 C는 생성 속도 조건을 위반한다. C의 처리량은 B보다 약 7.7% 높지만 TPOT p95는 약 95% 길다. Queue가 짧다는 사실만으로 C를 고르기 어렵다. KV headroom도 작고 preemption 발생 원인과 지속 여부를 확인해야 한다.
B는 이 두 지연 시간 조건으로는 후보다. E2E·오류·OOM·시간에 따른 대기열 추세는 표에 없으므로 아직 최종 승자가 아니다. 그것까지 충족하고 반복 실험에서 안정적이라면 B가 더 좋은 운영점일 수 있다. 선점 한 번과 지속적인 재계산 폭주도 구분해야 한다.
또한 표에서는 두 parameter가 동시에 달라졌다. 이 표는 선택 논리를 설명할 뿐, 어느 parameter가 개선의 원인인지 증명하지 않는다. 그 질문에는 한 변수씩 바꾸는 실험이 필요하다. 고정된 입력 부하에서 지속적으로 완료되는 처리량은 결국 도착률에 제한되므로, 서로 다른 완료 tok/s를 곧바로 최대 용량으로 읽지 않는다. 처리 중 남은 요청·실패·출력 길이와 부하 sweep까지 확인한다.
13. 매개변수 변경은 무엇을 얻고 무엇을 내주는가
분할 prefill 환경에서 작은 토큰 예산은 decode의 ITL에, 큰 예산은 prefill 진행과 TTFT에 유리할 수 있다. 공식 튜닝 안내서가 제시하는 방향이지 작업 부하마다 보장되는 결과는 아니다. 공식 tuning guide
내가 실제로 묻고 싶은 것은 “큰 값이 좋은가?”가 아니라 “현재 병목에서 더 큰 step이 유효한가?”다. TTFT가 이미 충분히 좋은데 TPOT만 나빠진다면 prefill을 더 밀어 넣는 실험의 성공 기준부터 다시 정해야 한다.
시퀀스 상한을 올릴 때도 같은 방식이다. 대기열 감소가 KV 압박·선점·상위 구간 지연 악화보다 가치 있는지 측정한다. 선점으로 작업을 다시 계산하면 GPU는 바쁜데 유용한 완료량은 떨어질 수 있다. gpu_memory_utilization 증가는 여유분을 줄이는 선택이므로 OOM이나 다른 GPU 사용자를 무시한 해결책이 아니다.
마법의 숫자는 없다. 긴 문맥을 받는 RAG와 짧은 응답의 채팅이 같은 값에서 최적이라는 보장도 없다. 하나의 설정으로 SLO를 맞추기 어렵다면 작업 부하별 수용 제어나 별도 서빙 풀의 비용을 비교한다.
14. 반복되는 prompt와 prefix cache
기업용 Agent에는 긴 시스템 프롬프트, 공통 지시, RAG 상용구가 반복된다. APC는 동일한 접두부의 KV를 재사용해 입력 계산을 줄인다. 의미가 비슷한 문장을 찾는 의미 기반 cache도, 최종 답변 cache도 아니다. 공통 접두부가 실제 토큰 단위로 재사용되어야 하며 새로운 출력 생성 자체를 없애지 않는다. Automatic Prefix Caching
현재 기본 활성화 결정은 모델 지원 여부를 반영한다. 지원되는 환경에서 CLI --enable-prefix-caching과 effective config를 확인한다. 무조건 켜져 있다고 추정하지 않는다. EngineArgs
확인된 엔드포인트에서 vllm:prefix_cache_hits_total의 증가량을 vllm:prefix_cache_queries_total 증가량으로 나눈 토큰 기준 적중률과 TTFT를 함께 본다. 분모가 0이면 적중률은 관측 불가이지 0%가 아니다. 콜드 캐시와 웜 캐시, 실제 접두부 반복률을 분리하지 않으면 같은 프롬프트만 반복하는 벤치마크가 개선 폭을 과장할 수 있다. Metric 정의
권한이나 사용자별 지시를 합쳐 hit ratio를 높이면 안 된다. Prefix 공유는 데이터 접근 권한을 대신하지 않으며, tenant 간 cache 격리 정책도 유지해야 한다.
15. 실전 튜닝 순환
- Workload 분포 정의
- SLO와 baseline 측정
- Metrics로 병목 가설
- Parameter 하나 변경
- 같은 부하로 재측정 → 반복
선택 기준: TTFT · TPOT · E2E · 오류 SLO를 지키는 GPU당 지속 가능한 처리량
1단계 — 작업 부하를 먼저 적는다. 채팅·RAG·Agent별 입력/출력 토큰 p50·p95·p99, 도착 RPS와 순간 집중, 동시 요청, Agent당 평균과 상위 구간 LLM 호출 수를 기록한다. 총량이 같아도 한꺼번에 몰리는 요청과 고르게 오는 요청은 다르다.
2단계 — SLO를 정한다. 설명용 시작점은 TTFT p95 < 1.5s, TPOT p95 < 50ms, E2E p95 < 8s다. 오류 허용 범위와 시간 초과·취소·거절의 포함 기준도 정한다. 출력이 긴 작업 부하가 E2E 목표와 양립하는지 먼저 확인하고 작업 부하별로 적용한다.
3단계 — 현재 설정 그대로 기준선을 잰다. 준비 단계를 제외하고 같은 데이터셋·출력 정책·도착 추적값·cache 상태로 반복한다. 개방형 RPS 단계 시험은 응답이 느려져도 예정된 요청을 보내 밀린 작업을 드러낸다. 폐쇄형 동시성 시험은 느려질수록 도착률도 줄어 과부하를 숨길 수 있으므로 둘의 목적을 구분한다. 부하 생성기 자체의 포화도 점검한다. vLLM benchmark CLI
4단계 — 병목 가설을 세운다. GPU 사용률이 낮고 대기열이 길다면 시퀀스 상한뿐 아니라 CPU 스로틀링, 토큰화, 통신, 게이트웨이와 복제본 불균형을 본다. GPU/KV 사용률이 높고 선점이 발생하면 문맥·출력·동시성의 조합을 의심한다. GPU 사용률이 높고 대기열이 짧으며 지연 시간이 양호하면 이미 좋은 지점일 수 있어 사용률을 더 높일 이유가 없다.
5단계 — 한 변수만 바꾼다. 첫 번째 실험에서는 시퀀스 상한을 고정하고 토큰 예산을 비교한 뒤, 선택한 예산에서 시퀀스 상한을 비교한다. 상호작용은 두 번째 실험에서 탐색한다. 각 후보와 부하 단계마다 반복 수, 표본 수, 변동 폭을 남기고 SLO를 지키는 지속 가능한 한계를 찾는다.
최종 평가표에는 유입/수용/완료 RPS, 입력/출력 tok/s, 할당 GPU 수로 나눈 처리량, SLO 안에서 완료한 유효 처리량, p50·p95·p99, 오류, 대기열 추세, KV 최댓값과 선점률이 필요하다. 전체 tok/s가 늘어도 SLO를 위반한 답변이 늘면 서비스 효용은 감소할 수 있다. 되돌리기 기준과 이전 이미지/설정도 실험 전에 정한다.
16. 코딩 어시스턴트에게 무엇을 요청할 것인가
“vLLM을 최적화해 줘”라고만 요청하면 그럴듯한 설정값을 받기 쉽다. 아래 영어 프롬프트는 관측 → 재현 가능한 실험 → 근거가 있는 제안을 요구한다. 전체 복사 버튼을 사용하거나 텍스트를 직접 선택할 수 있다. 환경 점검은 읽기 전용으로 시작하며, config patch는 제안만 하고 적용하지 않도록 했다. 운영 부하 시험도 별도 승인된 범위에서만 실행하게 한다.
Act as an inference performance engineer for an on-premises Kubernetes vLLM service.
Objective: maximize sustainable throughput subject to TTFT/TPOT/E2E/error SLO and no persistent OOM/preemption.
Do not optimize for 100% GPU utilization. Optimize useful completed work per allocated GPU, with stable queues and latency headroom.
Safety and scope
- Begin with read-only inspection in the explicitly authorized environment.
- Do not apply configuration patches, restart Pods, scale workloads, change routing, install exporters, or run production load tests.
- Prepare the benchmark plan first. Execute tests only in an approved isolated environment and within an agreed load budget.
- Keep prompts, outputs, credentials, hostnames, inventory, and internal metrics inside the authorized environment. Redact report identifiers. Do not export raw production payloads.
- Label every result as measured, derived, inferred, or illustrative. Never fabricate measurements or silently substitute unavailable metrics.
1. Inspect the runtime
Record the exact vLLM version, image digest, engine, startup command, effective config, model/tokenizer revision (redacted identifiers), dtype, quantization, KV dtype, context limit, parallel layout, replica count, and allocated GPUs.
Inspect driver/runtime compatibility, resource requests/limits, CPU throttling, memory, volumes, startup/readiness/liveness probes, restarts, gateway admission control, and streaming/proxy buffering.
Read installed-version CLI help and matching official docs. Do not import defaults or metric names from another version or engine.
Record effective max_num_seqs, max_num_batched_tokens, gpu_memory_utilization, explicit KV-memory overrides, chunked prefill, and prefix caching. Capture support constraints from startup logs.
2. Establish metric definitions
Inspect the actual /metrics endpoint and exporter configuration. List exact series names, HELP/TYPE, units, labels, histogram buckets, counter suffixes, and unavailable metrics. Save a sanitized schema snapshot.
Distinguish server TTFT from client-visible TTFT; exclude empty role-only SSE events from first-content timing. Record client E2E, timeouts, cancellations, errors, and rejected requests.
Define per-request TPOT as (last-token time - first-token time)/(output tokens - 1) for outputs longer than one token; report exclusions. Do not equate TPOT percentiles with inter-token-latency percentiles.
Report TTFT, TPOT, E2E p50/p95/p99; running/waiting requests; queue-time distributions; preemption rates; KV occupancy; GPU utilization; framebuffer memory; supported DRAM activity/bandwidth counters; CPU/network constraints.
Report input/output tokens per second, successful requests per second, offered/admitted/completed RPS, SLO-compliant goodput, and per-GPU throughput. Explain whether cached input tokens count as processed input.
Use rate/increase for counters. Aggregate histogram buckets before quantiles; never average per-Pod percentiles. Keep workload/model/replica attribution and sample counts.
3. Profile the workload
For chat, long-context RAG, and agent traffic separately, measure prompt/output token p50/p95/p99, arrival RPS and burstiness, in-flight concurrency, mean and tail LLM calls per agent workflow, and repeated-prefix frequency.
Use a sanitized representative dataset with fixed sampling parameters, seeds where supported, and token-length distributions. Include long prompts and long outputs, not only short synthetic requests.
Obtain explicit TTFT/TPOT/E2E/error SLOs, duration, minimum sample count, and stop conditions. If missing, propose illustrative targets labeled as unapproved.
4. Baseline and controlled experiments
Start with unchanged config. Warm up compilation, kernels, and connections; separate cold-cache and warm-cache tests and exclude warmup from scored intervals.
Use the same dataset, arrival trace, GPU allocation, measurement window, and cache-state policy. Repeat each candidate at least three times where the approved budget permits; report variability and insufficient evidence.
Use open-loop arrival-rate sweeps to expose backlog; separately report closed-loop concurrency tests and their tendency to reduce offered load as latency grows. Verify the load generator is not the bottleneck.
First hold max_num_seqs fixed and vary only max_num_batched_tokens. Then hold the chosen token budget fixed and vary only max_num_seqs. Revisit interactions in a documented second pass.
Inspect chunked-prefill effectiveness using long-prompt/streaming traffic; test alternatives only if the installed model/engine supports them. For prefix caching, compare realistic shared prefixes against unique prefixes and cold/warm states; measure token hit ratio and TTFT together.
Stop on agreed error/OOM/latency thresholds or persistent queue growth. Do not interpret burst throughput as sustainable capacity.
5. Scorecard and diagnosis
Produce one row per candidate, load level, cache condition, and repeat. Include config diff, versions, sample count, offered/admitted/completed RPS, input/output tok/s, per-GPU goodput, latency percentiles, errors, queue trend, preemptions, KV peak/headroom, GPU/DRAM metrics, and repeat variability.
For each suspected bottleneck, state evidence, competing explanations, and the next discriminating measurement. Low GPU plus high queue is not proof that concurrency is too low; check CPU tokenization, gateway, replica imbalance, scheduling constraints, and communication.
Reject candidates violating any SLO or showing persistent OOM/preemption/backlog. If all fail, say so. Recommend the stable candidate with the highest sustainable SLO-compliant throughput and sufficient headroom, not the highest utilization or raw tokens/sec.
6. Deliverables, without applying changes
Provide a recommendation, runtime inventory summary, metric dictionary, reproducible benchmark commands for the installed version, complete scorecard, and measured-versus-inferred findings.
Propose a minimal configuration patch only. Include expected trade-offs, uncertainties, validation gates, and rollback to the previous image digest/config and routing, with explicit rollback triggers and post-rollback checks.
End with what remains unmeasured and what authorization is required before execution or deployment.
17. GPU 사용률 자체가 목적함수가 아니다
vLLM 튜닝은 작업 부하를 정하고, 벤치마크를 재현하고, 지표에서 병목 가설을 세우고, 매개변수 하나를 바꾸어 검증하는 피드백 순환이다. 설정값보다 중요한 것은 그 값을 선택한 근거다.
높은 사용률과 높은 원시 throughput은 유용한 관측값이지만, 서비스가 약속한 응답 시간과 안정성을 대신하지 못한다. 같은 GPU에서 얼마나 많은 유용한 요청을 지속적으로 완료할 수 있는지가 기준이어야 한다.
모델을 GPU에 올리는 것은 시작일 뿐이다. 실제 추론 엔지니어링은 그다음부터 시작된다.
Discussion
토론
질문, 정정, 반례, 다른 해석이 있다면 남겨 주세요.