개념 관점
PagedAttention, prefix caching, chunked prefill, speculative decoding, disaggregated prefill 5가지 최적화가 각각 어떤 병목을 해결하는지 1문장으로 설명한다.
개념 관점
PagedAttention, prefix caching, chunked prefill, speculative decoding, disaggregated prefill 5가지 최적화가 각각 어떤 병목을 해결하는지 1문장으로 설명한다.
설계 관점
workload(채팅·배치·RAG·에이전트) 특성을 진단하고 어떤 옵션을 어느 순서로 켤지 의사결정 표로 정리한다.
구현 관점
vllm serve 명령으로 단일/멀티 GPU/MIG 서버를 띄우고, OpenAI 호환 client에서 latency·throughput 지표를 기록하며, admission control 큐를 코드로 구현한다.
운영 관점
TTFT, TPOT, queue time, cache hit ratio, error rate 5개 핵심 지표를 Prometheus/Grafana에 노출하고, 옵션 변경 전·후 차이를 정량적으로 보고한다.
LLM을 직접 배포할 때 모델 파일만으로는 아무 것도 운영되지 않는다. 실제 서비스 품질은 추론 서버가 결정한다.
| 운영 문제 | 추론 서버가 하는 일 |
|---|---|
| GPU 메모리 부족 | KV cache 관리, quantization, tensor parallelism |
| 요청 지연 | continuous batching, chunked prefill, speculative decoding |
| 반복 프롬프트 비용 | automatic prefix caching |
| 장문 입력 폭주 | prefill/decode 분리, max context policy |
| 장애 분석 | metrics, trace, request log, token accounting |
vLLM은 이 문제를 OpenAI 호환 API로 감싸서 agent harness가 모델을 바꾸더라도 같은 인터페이스를 유지하게 한다.
전통적 LLM 추론은 KV cache를 연속 메모리로 크게 예약한다. 요청 길이가 서로 다르면 빈 공간이 생기고, 장문 요청이 섞이면 fragmentation이 심해진다. vLLM의 PagedAttention은 OS 가상 메모리처럼 KV cache를 block 단위로 나누어 필요한 만큼 연결한다.
논리 sequence는 page table을 통해 가용한 물리 블록으로 흩어져 매핑된다. 새 요청이 들어와도 free block을 할당하면 되므로 fragmentation이 사실상 사라진다.
PagedAttention은 기본기다. 2026년 운영에서 중요한 질문은 “PagedAttention을 쓰는가”가 아니라 어떤 workload에 어떤 최적화를 켤 것인가다.
Automatic Prefix Caching
반복되는 system prompt, tool schema, AGENTS.md/CLAUDE.md prefix의 KV cache를 재사용한다. Ralph loop처럼 정적 prefix가 긴 workload에서 효과가 크다.
Chunked Prefill
긴 입력 prefill을 작은 청크로 나누어 decode 요청과 섞는다. 장문 요청 하나가 전체 큐를 막는 현상을 줄인다.
Speculative Decoding
작은 draft model 또는 n-gram speculation으로 후보 토큰을 먼저 만들고 큰 모델이 검증한다. latency 절감이 목표다.
Disaggregated Prefill
prefill과 decode를 다른 worker/GPU로 분리한다. 장문 입력과 짧은 응답 요청이 섞인 production workload에 유리하다.
Structured Outputs
JSON schema, tool call, reasoning output을 serving 계층에서 강제한다. agent harness의 parser 실패율을 줄인다.
| 레버 | 주 효과 | 가장 큰 위험 | 측정 지표 |
|---|---|---|---|
| Prefix Caching | TTFT 감소, prefill 비용 절감 | 매번 prompt가 미세하게 다르면 hit 0% | cache hit ratio, TTFT |
| Chunked Prefill | 장문 입력의 head-of-line blocking 완화 | chunk 단위가 너무 작으면 오버헤드 증가 | p95 latency, queue time |
| Speculative Decoding | 평균 TPOT 감소 | draft model 품질이 낮으면 reject로 오히려 느려짐 | acceptance rate, TPOT |
| Disaggregated Prefill | 장문/단문 mix workload throughput ↑ | 추가 inter-node 통신 비용 | throughput, network util |
| Structured Outputs | parser 실패율 감소 | 일부 schema는 sampling 품질 저하 유발 | invalid JSON rate, score |
| 시나리오 | 정적 prefix | 동적 prefix | 예상 cache hit | TTFT 영향 |
|---|---|---|---|---|
| Ralph loop, 같은 PROMPT.md 반복 | 길다 (3-8K) | 작은 turn 차이 | 70-95% | 매우 큼 |
| Multi-tenant chatbot, system prompt 공통 | 중간 (500-2K) | user message가 매번 다름 | 30-60% | 보통 |
| RAG with 매번 다른 retrieved chunks | 거의 없음 | 거의 전부 | 0-15% | 거의 없음 |
| 동일 코드베이스 반복 분석 | 매우 길다 (10K+) | 파일 diff만 변함 | 80-99% | 매우 큼 |
draft model이 정확할수록 한 번에 받아들여지는 토큰이 많아져 평균 TPOT가 줄어든다. draft가 자주 틀리면 verify 비용만 늘어나서 오히려 느려질 수 있다는 점에 유의한다.
긴 입력은 prefill 워커가 처리하고 KV cache만 decode 워커로 넘기는 패턴이다. 장문 입력과 짧은 응답이 혼합된 production에서 자원 활용도를 끌어올린다.
모든 옵션을 한꺼번에 켜는 것이 최적화가 아니다. workload별 병목을 먼저 보고 하나씩 켠다.
| Workload | 주 병목 | 먼저 켤 옵션 | 측정할 지표 |
|---|---|---|---|
| 긴 repository context + 짧은 수정 | prefill 비용 | prefix caching, chunked prefill | TTFT, cache hit ratio |
| 짧은 질의가 매우 많음 | batching 효율 | continuous batching, 적절한 max_num_seqs | throughput, queue time |
| 긴 문서 요약과 짧은 코드 요청 혼합 | long prefill blocking | chunked prefill, disaggregated prefill | p95 latency, queue time |
| interactive coding assistant | 첫 토큰 지연 | speculative decoding, 작은 max_tokens | TTFT, perceived latency |
| strict JSON/tool output | parser 실패 | structured outputs, tool parser 설정 | invalid JSON rate |
| Ralph 루프 캡스톤 | prefill 반복 | prefix caching + chunked prefill | TTFT, cache hit |
실습 보고서에는 “옵션을 켰다”가 아니라 옵션을 켜기 전/후 병목 지표가 어떻게 변했는지를 기록한다.
vllm serve deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ --served-model-name local-coder \ --max-model-len 32768 \ --gpu-memory-utilization 0.90 \ --enable-prefix-caching \ --port 8000vllm serve Qwen/Qwen3-Coder-Next \ --served-model-name qwen3-coder \ --tensor-parallel-size 2 \ --max-model-len 65536 \ --enable-prefix-caching \ --enable-chunked-prefill \ --port 8000vllm serve zai-org/GLM-5.1 \ --served-model-name glm-5.1 \ --tensor-parallel-size 4 \ --enable-auto-tool-choice \ --port 8000vllm serve Qwen/Qwen3-Coder-Next \ --served-model-name qwen3-coder \ --speculative-model deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ --num-speculative-tokens 5 \ --port 8000추론 서버 튜닝에서 흔한 실수는 tokens/sec만 보는 것이다. 에이전트 시스템에서는 다음 지표를 함께 봐야 한다.
| 지표 | 의미 | 나쁜 신호 |
|---|---|---|
| TTFT(Time to First Token) | 사용자가 첫 응답을 보기까지 시간 | 장문 prefill이 queue를 막음 |
| TPOT(Time per Output Token) | 토큰 생성 간격 | decode 단계 GPU utilization 저하 |
| Throughput | 초당 처리 토큰 | batching 부족 또는 memory fragmentation |
| Queue time | 요청이 대기한 시간 | concurrency 과다, admission control 부재 |
| Cache hit ratio | prefix 재사용률 | prompt 조립 방식이 매번 달라짐 |
| Error rate | 실패/timeout 비율 | max_model_len, OOM, parser 실패 |
vLLM은 /metrics 엔드포인트를 기본 제공한다. 캡스톤 발표용 핵심 패널은 다음 PromQL로 만든다.
# Panel 1: TTFT p95 (모델별)histogram_quantile(0.95, sum by (le, model_name) (rate(vllm:time_to_first_token_seconds_bucket[5m])))
# Panel 2: 처리량 (tokens/sec)sum by (model_name) (rate(vllm:generation_tokens_total[1m]))
# Panel 3: prefix cache hit ratiosum(rate(vllm:cache_hit_tokens_total[5m])) /sum(rate(vllm:prompt_tokens_total[5m]))
# Panel 4: 큐 대기 (active vs queued)vllm:num_requests_runningvllm:num_requests_waiting발표 자료에는 옵션을 변경한 시각을 annotation으로 표시해 “켜기 전후 차이”가 한눈에 보이게 한다.
추론 서버가 느려지는 순간 무작정 더 많은 요청을 받으면 전체 팀의 실습이 무너진다. production serving은 요청을 받기 전에 다음을 판단한다.
| 통제 항목 | 정책 예시 |
|---|---|
| 최대 입력 길이 | max_model_len보다 낮은 course-level limit을 둔다 |
| 동시 요청 수 | 팀별 queue와 전역 queue를 분리한다 |
| 우선순위 | demo/replay 요청을 실험성 batch보다 우선한다 |
| timeout | prefill timeout과 decode timeout을 분리 기록한다 |
| fallback | local model 실패 시 commercial API 또는 작은 모델로 reroute한다 |
# admission.py — 팀별 queue 와 토큰 예산 관리import asyncio, timefrom dataclasses import dataclass
@dataclassclass Budget: max_concurrent: int = 4 max_prompt_tokens: int = 16000 timeout_s: float = 60.0
team_locks: dict[str, asyncio.Semaphore] = {}
async def admit(team: str, prompt_tokens: int, budget: Budget) -> bool: if prompt_tokens > budget.max_prompt_tokens: return False sem = team_locks.setdefault(team, asyncio.Semaphore(budget.max_concurrent)) try: await asyncio.wait_for(sem.acquire(), timeout=budget.timeout_s) return True except asyncio.TimeoutError: return False
async def release(team: str): team_locks[team].release()Agent OS Runtime 관점에서는 admission control도 policy gate다. “요청을 거절했다”는 것도 실패가 아니라, 시스템이 경계를 지킨 성공 이벤트일 수 있다.
DGX H100 환경에서 학생 팀별 실습을 운영할 때는 “모두가 같은 대형 모델을 공유”하는 방식보다 resource boundary를 명확히 나누는 편이 안정적이다.
# 팀 A: 1g.10gb slice, 교육용 모델CUDA_VISIBLE_DEVICES=MIG-GPU-a vllm serve deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ --served-model-name team-a-coder \ --max-model-len 16384 \ --port 8001
# 팀 B: 3g.40gb slice, 더 큰 모델CUDA_VISIBLE_DEVICES=MIG-GPU-b vllm serve Qwen/Qwen3-Coder-30B-A3B-Instruct \ --served-model-name team-b-coder \ --max-model-len 32768 \ --port 8002vLLM은 Prometheus/Grafana, OpenTelemetry 예제를 제공한다. 12주차에서 본격적으로 다루지만, 11주차 Lab에서도 최소한 다음 로그는 남겨야 한다.
{ "request_id": "run-20260512-001", "model": "local-coder", "prompt_tokens": 1842, "completion_tokens": 419, "ttft_ms": 820, "tpot_ms": 34, "cache_hit": true, "finish_reason": "stop"}이 형식은 Agent OS Runtime의 .events.jsonl과도 연결된다. serving layer는 토큰과 지연시간을 기록하고, agent runtime은 tool call, approval, test result, replay 상태를 기록한다.
기준 서버 실행
vllm serve deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ --served-model-name local-coder \ --max-model-len 32768 \ --port 8000prefix caching 전후 비교
같은 system prompt와 같은 repository summary를 포함한 요청 20개를 실행한다. --enable-prefix-caching을 끈 상태와 켠 상태의 TTFT, throughput, cache hit를 비교한다.
chunked prefill 전후 비교
20K token 이상의 긴 파일 요약 요청과 짧은 코드 생성 요청을 섞는다. 긴 요청이 짧은 요청의 대기 시간을 얼마나 늘리는지 측정한다.
OpenAI-compatible client 작성
from openai import OpenAIimport time
client = OpenAI(base_url="http://localhost:8000/v1", api_key="local")
started = time.perf_counter()response = client.chat.completions.create( model="local-coder", messages=[{"role": "user", "content": "pytest fixture 예시를 작성해줘"}], temperature=0.2,)elapsed = time.perf_counter() - startedprint(elapsed, response.choices[0].message.content[:200])admission control 큐 도입
admission.py 패턴을 적용해 팀별 동시 요청 수를 제한한다. 거절된 요청은 별도 metric(admission.rejected)으로 카운트한다.
대시보드 준비
Prometheus/Grafana 또는 간단한 CSV 로그로 ttft_ms, tokens/sec, error_rate, cache_hit을 기록한다. 12주차에서는 이 값을 OpenTelemetry trace와 연결한다.
| 항목 | 통과 기준 |
|---|---|
| 모델 로딩 | cold start와 warm restart 시간이 기록됨 |
| API 호환성 | OpenAI client에서 chat completion 성공 |
| 안정성 | 100개 요청 중 timeout/OOM 0-2개 이하 |
| 비용 | GPU 시간당 비용과 API 비용 비교표 작성 |
| 관측성 | 최소 request_id, model, tokens, latency 기록 |
| 보안 | 외부 네트워크/파일 접근 권한 분리 |
| Admission control | 팀별 queue가 분리되고 거절 이벤트가 기록됨 |
12주차에서는 이 vLLM 서버 위에 텔레메트리, event store, LLM-as-Judge 평가 게이트를 구축한다. 11주차가 “빠르게 생성하는 법”이라면, 12주차는 “생성 결과를 믿을 수 있게 운영하는 법”이다.
핵심 문서
관측성
논문 / 리포트