같은 모델 파드 여러 개 중
누구에게 보낼 것인가.
vLLM 파드를 두 개로 늘리면 쿠버네티스 Service가 알아서 나눠 줍니다. 그런데 Service는 어느 파드의 KV 캐시에 방금 그 프롬프트가 들어 있는지, 어느 파드 앞에 긴 요청이 줄 서 있는지 모릅니다.
llm-d는 그걸 아는 라우터입니다. 부품 셋과 EPP의 안쪽을 열고, GPU 한 장을 반씩 나눈 실습 클러스터에서 실제로 무엇이 달라졌는지 봅니다.
들어가며
[7W-1] 11장은 EPP라는 상자 앞에서 멈췄습니다. "EPP가 파드별 큐 길이·캐시 상태를 점수화해 하나를 고른다" — 이 한 줄이 무엇을 뜻하는지가 이 페이지의 주제입니다. 원문 스터디 자료도 llm-d 공식 문서를 한 페이지씩 정리한 뒤, GPU 한 장짜리 K3s 클러스터를 새로 세워 llm-d를 올리고 직접 비교합니다.
그 비교 결과가 흥미롭습니다. 파드 2개 + 작은 모델에서는 캐시 적중률이 4.9%p 오르는 대신 TTFT가 오히려 소폭 늘었습니다. "지능형 라우팅이 무조건 빠르다"가 아니라 언제 이득이 나는가를 가르는 조건이 있다는 뜻이고, 그 조건을 그림으로 찾아보는 게 이 페이지의 목표입니다.
- llm-d Router = Proxy(Envoy 등) + EPP. Proxy는 요청을 붙잡고 ext_proc로 EPP에게 묻고, EPP는 InferencePool의 파드들을 Filter → Score → Pick으로 골라 주소를 돌려줍니다. 모델 서버는 vLLM 그대로입니다.
- 점수의 근거는 모델 서버가 내놓는 신호입니다. 대기열·KV 캐시 사용률 메트릭, 그리고 vLLM이 ZMQ로 흘리는 KV 블록 이벤트. 신호가 없으면(llama.cpp) 라우팅은 되지만 판단은 못 합니다.
- 이득은 규모가 커야 날 것으로 보입니다. 원문 실습(파드 2개, Qwen3-0.6B-FP8)에서 precise prefix 라우팅은 캐시 적중률 63.9% → 68.75%, TTFT는 59~66 → 67~69 ms. 원문은 파드 수가 많고 캐시 미스가 비쌀수록 차이가 커질 것으로 예상합니다. EPP 자체 비용은 트레이스상 요청의 0.54%였습니다.
접는 상자(▸)는 안 열어도 됩니다. 안에는 설정 YAML, 플러그인 표, 원문 명령과 출력이 있습니다.
그림은 만져보라고 넣었습니다. 특히 9장의 그림은 원문의 "파드 2개에선 이득이 작다"는 결론을 재현한 뒤 파드를 늘려 보며 반대 상황을 만들 수 있습니다.
"[7W-1] N장"·"[6W-2] N장"은 이전 주차 페이지의 장이고, 그냥 "N장"은 이 페이지의 장입니다.
원문 실습은 GPU가 반드시 필요하고, 가정용 카드가 없으면 VRAM 24 GiB 이상 AWS GPU EC2 한 대를 권합니다. GPU 없이 게이트웨이 부분만 보고 싶다면 [7W-1] 11장(CPU llama.cpp)과 원문이 추천한 멤버 글 "GPU 없이 llm-d 게이트웨이 뜯어보기"가 출발점입니다. 버전은 vLLM v0.28.0, llm-d-router EPP v0.10.0, Envoy Gateway v1.9.1, Agent Router v1.1.0, GAIE v1.6.1입니다.
먼저 질문부터 — 파드를 늘렸는데 왜 빨라지지 않는가.
01왜 LLM 전용 라우터인가
한 줄 요약: LLM 서빙에서 파드를 늘리는 건(Scaling) 절반이고, 요청을 어느 파드에 보내느냐(Routing)가 나머지 절반입니다. 같은 prefix를 이미 캐시한 파드, 줄이 짧은 파드를 고르면 같은 GPU로 더 빨라집니다.
원문이 인용한 Red Hat 발표(Beyond vLLM)는 AI 추론을 Scaling vs Routing으로 나눕니다. 백엔드가 많아질수록 LLM을 아는 부하 분산이 필요해진다는 것이 요지입니다. 일반 round robin은 첫 요청 이후에도 캐시를 모른 채 파드를 돌리니, 방금 계산해 둔 KV 캐시를 다른 파드가 처음부터 다시 계산합니다.
원문의 비교 캡처는 한 가지를 짚습니다. 최초 캐시 생성 요청 이후부터 성능 수치가 갈린다는 것. 캐시가 쌓이기 시작하면 "어디에 쌓였는지 아는 라우터"와 "모르는 라우터"의 차이가 벌어집니다.
- KV 캐시 · Prefix caching
- 모델이 앞 토큰들을 읽으며 만든 중간 계산값(Key·Value)을 GPU 메모리에 남긴 것. 다음 요청이 같은 앞부분(prefix)으로 시작하면 그 부분 계산을 건너뜁니다. vLLM에서는
--enable-prefix-caching. - TTFT · TPOT
- Time To First Token(첫 토큰까지 걸린 시간)과 Time Per Output Token(토큰 하나 더 만드는 데 걸린 시간). prefix 캐시는 주로 TTFT를 줄입니다.
- Prefill · Decode
- 프롬프트를 한 번에 읽는 단계(prefill, TTFT를 결정)와 토큰을 하나씩 만드는 단계(decode).
- llm-d
- 쿠버네티스 네이티브 오픈소스 LLM 추론 서빙(라우팅) 스택. CNCF 샌드박스 프로젝트이고, vLLM·SGLang 같은 엔진을 대체하지 않고 감싸서 확장합니다.
- Well-Lit Paths
- llm-d가 제공하는 검증된 배포 레시피. Foundations(기초 기능)와 Workloads(워크로드별 패턴)로 나뉩니다.
- IGW (Inference Gateway)
- Gateway API Inference Extension을 구현한 게이트웨이. llm-d는 GKE Gateway, Istio, Agentgateway, Envoy AI Gateway를 지원 목록에 올립니다.
llm-d가 내세우는 것 (공식 문서 기준)
| 항목 | 내용 |
|---|---|
| 해결하는 문제 | 단일 GPU·노드에서 잘 도는 vLLM을 여러 노드·GPU에서 라우팅·오토스케일링·모니터링까지 되는 프로덕션 서비스로 만드는 표준 패턴을 미리 제공 |
| 하드웨어 | NVIDIA·AMD GPU, Google TPU, Intel XPU, x86 CPU, 그리고 Rebellions NPU까지 같은 방식으로 배포 |
| 성능 주장 | 자체 벤치마크 기준 처리량 최대 3배, 응답 속도 2배, 토큰/초 최대 70% 개선 (llm-d 발표 수치, 이 페이지에서 검증하지 않음) |
| 운영 기능 | 지능형 라우팅, 라우터 고가용성, 오토스케일링, 엔드투엔드 관찰성 |
| 도구 | llm-d Prism — 분산 추론 성능 데이터를 모아 비교·분석하는 대시보드 웹 앱 |
요청 한 건마다 일어나는 일 (per-call flow)
- IGW의 Envoy가 요청을 받는다[7W-1] 9장의 ext_proc 필터가 붙은 그 Envoy입니다.
- Envoy가 EPP에게 넘긴다EPP는 KV 캐시 인식과 부하 인식을 맡습니다.
- Scrapers모든 추론 풀 파드에서 대기열·사용 가능한 KV 캐시·현재 부하를 읽어 옵니다.
- Filters → Scorers스케줄링에서 뺄 파드를 거르고, 남은 후보에 점수를 매깁니다.
- Envoy가 선택된 파드로 보낸다EPP가 고른 주소로 요청이 갑니다.
비유 — 반찬을 미리 해 둔 주방
체인 식당 주방 네 곳이 있습니다. 주방 B에 "김치찌개 육수"가 이미 끓고 있다면, 김치찌개 주문은 B로 보내는 게 빠릅니다. 다른 주방은 육수부터 다시 끓여야 하니까요.
그런데 김치찌개 주문이 몰려 B 앞에 줄이 20개면? 육수를 새로 끓이더라도 비어 있는 주방이 낫습니다.
round robin 배정원은 육수도 줄도 모르고 차례대로 넘깁니다. "육수만 보는" 배정원은 B를 터뜨리고, "육수와 줄을 같이 보는" 배정원이 llm-d의 EPP입니다.
작은 숫자로 한 번
round robin · prefix만 보기 · prefix와 대기열 같이 보기
버튼 · 슬라이더prefix 1,500 토큰 + 질문 200 토큰, 캐시 안 된 토큰당 prefill 0.15 ms, 출력 100 토큰 × 15 ms, 파드당 동시 처리 8, 파드당 prefix 4종까지 캐시(LRU), 인기 prefix에 요청이 몰리는 분포로 60초를 흘린 모형입니다. 실측이 아니며 방식 간 관계를 보이기 위한 것입니다.
원문 블로그 타임라인 — 굵게 표시된 글
2025 — Announcing the llm-d community · Intelligent Inference Scheduling with llm-d · KV-Cache Wins You Can See(vLLM prefix caching에서 llm-d 분산 스케줄링까지, KVEvents로 전역 캐시 뷰) · llm-d 0.3(IGW GA)
2026 — llm-d 0.5 · Native KV Cache Offloading to Any Filesystem · Predicted-Latency Based Scheduling · KServe + llm-d + vLLM · v0.7(프로덕션 강화) · No Kubernetes? No Problem · BLIS(시뮬레이션) · 3개 GPU 벤더 이기종 서빙 · Hybrid 모델 · 분산 추론 네트워킹 · v0.8(Platform → Control Plane) · GLM-5.2 agentic 워크로드 · Kimi-VL E/PD · P2P KV 캐시 공유 · Sticky Until Saturated(token-aware routing) · End-to-End Tracing · v0.9(Hardened for Scale) · 멀티모달
마일스톤: llm-d 본체 v0.10.0은 9월 24일 예정(실습에 쓴 llm-d-router 이미지 v0.10.0과는 별개 번호), v1.0.0은 아직 계획 없음(원문 작성 시점).
- 정의 — llm-d는 vLLM 같은 엔진 앞에서 KV 캐시와 부하를 보고 파드를 고르는 쿠버네티스 네이티브 추론 라우팅 스택입니다(CNCF 샌드박스).
- 성질 — round robin은 캐시를 모르고, prefix만 보면 인기 파드가 터집니다. 둘을 같이 봐야 적중률과 TTFT를 함께 잡습니다. prefix 종류가 적으면 round robin과 prefix+대기열의 차이는 작아지지만, prefix만 보는 방식은 오히려 크게 무너집니다.
- 쓰임 — 요청마다 Envoy → EPP(Scrape → Filter → Score → Pick) → 선택 파드. 이 흐름의 부품이 무엇인지가 다음 장입니다.
"Envoy가 EPP에게 묻는다"를 부품 단위로 나눠 봅니다. 공식 문서는 이걸 Router, InferencePool, Model Server 셋으로 정리합니다.
02세 부품 — Router, InferencePool, Model Server
한 줄 요약: InferencePool은 "누가 후보인가", EPP는 "그중 누구를 고를까", Proxy는 "고른 곳으로 실제로 보내기", Model Server는 "추론하고 신호를 내놓기"를 맡습니다. 결정과 전달이 분리돼 있어서 요청마다 목적지가 새로 정해집니다.
Service(kube-proxy)는 TCP 연결을 맺을 때 파드를 고릅니다. HTTP keep-alive로 연결을 재사용하면 그 연결의 요청은 계속 같은 파드로 갑니다. 원문은 이걸 "연결 단위 무작위 배정 후 keep-alive로 고착"이라고 부르고, llm-d의 구조에서는 이 문제가 구조적으로 생길 수 없다고 정리합니다.
이유는 [7W-1] 9장의 두 트릭입니다. Proxy가 요청을 붙잡고 ext_proc로 EPP에 물은 뒤, EPP가 헤더에 적어 준 파드 주소로 ORIGINAL_DST 클러스터가 직행합니다. 매 요청이 새 질문입니다.
- Router · Proxy · EPP
- Router는 llm-d의 라우팅 부품 전체 이름이고, 그 안이 Proxy(Envoy·Istio 등 L7 프록시, 데이터 플레인)와 EPP(Endpoint Picker, 결정만 하는 두뇌)로 나뉩니다.
- InferencePool
- 같은 모델을 서빙하는 파드를 라벨로 묶은 리소스. Router·EPP·모델 서버 사이의 source of truth(단일 정보원)입니다.
- Variant
- InferencePool 안의 부분 집합 개념. prefill/decode 역할이나 성능·비용별로 파드를 나눕니다.
- Standalone · Gateway mode
- Proxy와 EPP를 같은 파드에 사이드카로 두는 방식과, 쿠버네티스 Gateway API(Gateway·HTTPRoute)로 정식 연결하는 방식.
- kube-proxy · keep-alive
- kube-proxy는 Service IP로 온 연결을 iptables 등으로 파드에 나눕니다. keep-alive는 HTTP 연결 하나로 여러 요청을 보내는 것.
- LoRA 어댑터
- 기본 모델에 얹는 작은 추가 가중치. 요청의
model필드로 어떤 어댑터를 쓸지 고릅니다.
부품별 역할
| 부품 | 맡는 일 | 핵심 |
|---|---|---|
| Proxy | OpenAI 호환 요청을 받아 EPP가 고른 파드로 전달하고 응답을 스트리밍 | 자체 관리형: Istio, agentgateway, Envoy AI Gateway · 클라우드 관리형: Google Cloud Application LB |
| EPP | InferencePool 상태를 평가해 최적 파드 선택 — 결정만 | KV 캐시 위치성, 현재 부하(대기열), 요청 우선순위 · Filters → Scorers → Pickers |
| InferencePool | ① EPP를 위한 파드 디스커버리(selector, Ready 변화 자동 반영, targetPorts) ② 게이트웨이 연동(HTTPRoute의 backendRef, EPP 주소 제공, failureMode) | 보통 풀 1개 = 모델의 논리적 배포 1개 = EPP 1개. 모든 참조는 같은 네임스페이스로 제한 |
| Model Server | vLLM·SGLang·TensorRT-LLM(trtllm-serve). OpenAI 호환 API, 메트릭 노출, KV 캐시·prefix 캐시 관리, TP·DP·EP 병렬화 | 나머지 스택과 완전히 독립 배포. 라벨로 풀에 자동 등록, /health Ready여야 트래픽 대상 |
EPP가 모델 서버에게 요구하는 메트릭
| 메트릭 | 의미 | vLLM에서의 이름(예) |
|---|---|---|
TotalQueuedRequests | 대기 중 요청 수 | vllm:num_requests_waiting |
TotalRunningRequests | 처리 중 요청 수 | vllm:num_requests_running |
KVCacheUtilization | KV 캐시 사용률(%) | vllm:kv_cache_usage_perc |
BlockSize · NumGPUBlocks (선택) | 블록당 토큰 수 · GPU 메모리의 총 블록 수 | — |
SGLang은 sglang_num_queue_reqs·sglang_token_usage, TensorRT-LLM은 trtllm_*(v1.3.0rc12+, 성능 메트릭 옵션 필요)을 씁니다. 엔진 종류는 파드 라벨 llm-d.ai/engine-type으로 알려 줍니다.
그리고 EPP의 prefix 인식 라우팅이 의미 있으려면 모델 서버가 prefix 캐시 재사용을 지원해야 합니다(vLLM --enable-prefix-caching). [7W-1] 11장의 llama.cpp가 이 메트릭들을 안 줘서 501 로그가 났던 것입니다.
두 가지 배포 모드
| Standalone | Gateway (Inference Gateway) | |
|---|---|---|
| 배치 | Proxy와 EPP가 같은 파드에 사이드카. ext_proc가 localhost(네트워크 hop 없음) | Gateway → HTTPRoute → InferencePool(또는 일반 Service) |
| 용도 | 테스트, 레거시 Ingress, 배치 추론, RL 파이프라인. Gateway API 인프라 불필요 | 프로덕션. 한 Gateway가 여러 HTTPRoute, 가중치 분할·미러링·멀티클러스터 |
| EPP 관여 | 모든 요청 | InferencePool을 backendRef로 둔 경로만. 일반 Service 경로는 표준 k8s 라우팅 |
비유 — 인력 사무소와 현장 반장
인력 사무소 명단(InferencePool)에는 오늘 나올 수 있는 기술자가 적혀 있고, 출근·퇴근하면 명단이 바뀝니다. 현장 반장(EPP)은 명단을 보며 "이 일은 어제 비슷한 걸 한 김 기술자에게, 단 김 기술자가 너무 바쁘면 이 기술자에게"를 정합니다. 배송 기사(Proxy)는 반장이 적어 준 주소로 실어 나를 뿐입니다.
kube-proxy는 아침에 차 한 대를 기술자 한 명에게 배정하고 하루 종일 그 차로 일을 나르는 방식입니다. 차가 세 대뿐이면 한 명은 종일 놀 수도 있습니다.
작은 숫자로 한 번
연결 단위 배정 vs 요청 단위 배정
버튼 · 슬라이더연결마다 요청 부하가 같다고 두고(1초에 1건), 60초 동안 연결이 끊기지 않는다고 가정했습니다. 실제 kube-proxy(iptables)는 연결마다 확률적으로 파드를 고르고, 클라이언트의 연결 풀 동작에 따라 연결이 다시 맺어지기도 합니다.
InferencePool 핵심 필드와 Gateway 모드 HTTPRoute 예시 (공식 문서 요약)
spec: selector: # 파드 검색용 라벨 셀렉터 — 같은 네임스페이스만 targetPorts: # 실제 추론 트래픽 포트 (예: vLLM 8000) endpointPickerRef: # EPP 서비스 참조 — Envoy ext_proc 클러스터가 가리킬 주소 failureMode: # EPP 실패 시 FailOpen / FailClose
# "이 경로는 InferencePool로 보낼 요청"이라고 표시해 두면 그 경로만 EPP 상담 절차를 거친다
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
spec:
parentRefs:
- kind: Gateway
name: my-gateway
rules:
- backendRefs:
- kind: InferencePool
name: my-infpool
matches:
- path: {type: PathPrefix, value: /}
멀티 InferencePool — Prefill/Decode를 서로 다른 풀로 나눠 독립 스케일링하거나, 모델·버전별로 풀을 나눕니다. 파드·EPP 서비스·풀 사이의 참조가 같은 네임스페이스로 엄격히 제한돼 멀티테넌트 경계가 분명합니다.
LoRA — 요청의 model 필드로 어댑터를 동적으로 고르고, vllm:lora_requests_info가 max_lora·running·waiting 어댑터를 노출합니다.
- 정의 — llm-d = Router(Proxy + EPP) + InferencePool + Model Server. InferencePool은 후보 목록, EPP는 선택, Proxy는 전달, 모델 서버는 추론과 메트릭입니다.
- 성질 — 결정(EPP)과 전달(Proxy)이 분리돼 요청마다 목적지가 정해집니다. kube-proxy의 연결 단위 고착이 생기지 않습니다. 대신 EPP 판단은 모델 서버 메트릭에 의존합니다.
- 쓰임 — 프로덕션은 Gateway 모드(Gateway → HTTPRoute → InferencePool), 테스트·배치는 Standalone. EPP 안에서 선택이 어떻게 일어나는지가 다음 두 장입니다.
EPP를 열어 봅니다. 공식 문서는 요청 처리를 6단계 파이프라인으로 그립니다 — 먼저 앞쪽 절반, 요청을 받아 줄 세우는 부분부터.
03EPP 안쪽 ① — Request Handler와 Flow Control
한 줄 요약: EPP는 요청을 파싱해 스케줄링에 필요한 데이터를 만들고(Request Handler), 모델 서버가 포화되면 바로 보내지 않고 우선순위·공정성 규칙으로 줄을 세웁니다(Flow Control). 한 번 보내면 되돌릴 수 없기 때문입니다.
원문이 정리한 EPP의 두 목표 중 두 번째가 이것입니다. 여러 워크로드를 같은 모델 서버 풀에서 돌릴 때 우선순위를 조율하는 것.
일반적인 RPS 제한은 LLM에 안 맞습니다. 긴 컨텍스트와 자기회귀 decode 때문에 자원 소비가 요청마다 비선형적이기 때문입니다([7W-1] 8장).
Flow Control 문서가 꼽는 네 문제가 그대로 이 장의 질문입니다. Noisy neighbor(큰 프롬프트 테넌트가 KV 캐시 독점), 스케줄링 후회(디스패치 후 재라우팅 불가), 우선순위 역전(낮은 배치 작업이 중요 요청을 막음), 자원 비대칭(컨텍스트 크기 차이).
- Parser · DataProducer · Admitter
- Request Handler의 3대 부품. 요청을 내부 구조로 파싱, 스케줄링용 상태(토큰화·캐시 매칭·지연 예측)를 생성, SLO를 못 지킬 요청을 즉시 반려합니다.
- SLO
- Service Level Objective. "TTFT 200 ms 이내" 같은 서비스 목표치.
- FlowKey · Priority band · Fairness · Ordering
- Flow Control의 3단 구조. FlowKey = FairnessID + Priority. 먼저 가장 높은 우선순위 대역을 고르고, 대역 안에서 테넌트(플로우)를 공정하게 고르고, 플로우 안에서 요청 순서를 정합니다.
- Sheddable
- "버려도 되는" 요청. 음수 우선순위로 표시하고, 포화 시 가장 먼저 거절됩니다. 헤더가 없으면 우선순위 0.
- SaturationDetector
- 풀이 포화됐는지 판정하는 문지기. 실시간 텔레메트리를 보는 폐루프(utilization)와 활성 요청을 세는 개루프(concurrency) 방식이 있습니다.
- TTL · graceful drain
- 큐에서 기다릴 수 있는 최대 시간, 그리고 종료 중인 EPP가 새 요청을 받지 않고 비우는 상태.
요청 처리 6단계 파이프라인
(llm-d 문서는 [7W-1] 9장의 ext_proc를 ext-proc으로 적습니다.) 요청 도착 → ext-proc → Request Handling → Flow Control → Request Scheduling → 요청 프록시. 별도로 백그라운드의 Data Layer가 k8s API와 메트릭을 비동기로 모읍니다(4장).
Proxy ↔ EPP는 반드시 FULL_DUPLEX_STREAMED ext-proc이고, 백엔드 선택 규격은 Gateway API Inference Extension의 Endpoint Picker Protocol을 따릅니다.
Request Handler — 요청의 생명주기 관리자
| 부품 | 기본 제공 플러그인 | 하는 일 |
|---|---|---|
| Parser | openai-parser(/chat/completions, /completions, /embeddings) · vllmgrpc-parser(Generate, Embed) · passthrough-parser | 원시 요청 → 내부 구조. 응답에서 usage 추출. passthrough는 본문을 안 읽어 페이로드 기반 스케줄링 불가 |
| DataProducer | predicted-latency-producer(XGBoost로 TTFT·TPOT 예측) · inflight-load-producer · approx-prefix-cache-producer | 스케줄링 근거 데이터 생성 |
| Admitter | latency-slo-admitter | SLO 미충족 시 낮은 우선순위 요청 거부 |
확장 훅은 PreRequest(진행 중 요청 수 증가 등), ResponseHeaderProcessor, ResponseBodyProcessor(스트리밍 중·완료 시 사용량 추적)이고, 주요 메트릭은 llm_d_epp_request_total·_running·_ttft_seconds·_streaming_tpot_seconds·_input_tokens·_cached_tokens입니다.
Flow Control — 풀을 지키는 중앙 큐
- Tier 1 · Priority band가장 높은 우선순위 대역을 먼저 고릅니다(고정 규칙).
- Tier 2 · Fairness대역 안에서 테넌트를 고릅니다.
round-robin-fairness-policy(활성 플로우 순회) 또는global-strict-fairness-policy(플로우 격리 무시, 전역 순서). - Tier 3 · Ordering플로우 안의 순서.
fcfs(도착 순),edf(데드라인 빠른 순),slo-deadline(x-llm-d-slo-ttft-ms헤더 기반).
포화 판정은 utilization-detector(정확하지만 지연 존재) 또는 concurrency-detector(즉각적이지만 실제 메모리 압력은 모름). Flow Control이 켜져 있으면 포화 시 디스패치를 멈추고 중앙 큐에 버퍼링하고, 꺼져 있으면 음수 우선순위 요청만 즉시 429로 거부합니다.
실패는 큐 용량 초과 429, TTL 만료 503, 클라이언트 연결 끊김 503, graceful drain 503으로 매핑됩니다.
비유 — 응급실 접수
진료실(모델 서버)이 다 차면 접수(EPP)는 환자를 억지로 들여보내지 않고 대기실에 앉힙니다. 응급(높은 우선순위)부터 부르고, 같은 응급도 안에서는 병동별로 번갈아 부르고(fairness), 같은 병동 안에서는 온 순서대로 부릅니다.
일단 진료실에 들어간 환자는 다른 방으로 옮길 수 없습니다 — 그래서 들여보내는 순간을 신중하게 고르는 것이 Flow Control의 이유입니다. 대기실이 없는 접수(Flow Control 끔)는 "나중에 오셔도 되는 분"(sheddable)만 돌려보내고 나머지는 전부 들여보내 진료실을 미어터지게 합니다.
작은 숫자로 한 번
채팅과 배치를 한 풀에서 — Flow Control 켜고 끄기
버튼 · 슬라이더채팅: 우선순위 100, 초당 3건, 처리 1.5초. 배치: 20~40초 구간에만 몰려 들어오고 처리 5초. 한도를 넘겨 들여보내면 처리 시간이 동시 처리 수가 한도를 넘은 비율만큼 늘어난다고 가정했습니다.
큐 TTL은 차이가 보이도록 12초로 줄였습니다(문서 예시는 60초). 모두 설명용 추정치입니다.
우선순위 요청 헤더와 Flow Control 설정 예시 (공식 문서)
# 테넌트·목표를 헤더로 지정. 헤더가 없으면 우선순위 0
curl -X POST http://${IP}:${PORT}/v1/completions \
-H 'Content-Type: application/json' \
-H 'x-llm-d-inference-fairness-id: tenant-a' \
-H 'x-llm-d-inference-objective: premium-traffic' \
-d '{"model": "default-model", "prompt": "Say hello"}'
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
featureGates: [flowControl] # 명시적으로 켜야 한다
plugins:
- type: round-robin-fairness-policy
- type: fcfs-ordering-policy
- type: concurrency-detector
parameters: {maxConcurrency: 15, concurrencyMode: requests}
saturationDetector:
pluginRef: concurrency-detector
flowControl:
maxBytes: "10Gi" # 전역 큐 한도
maxRequests: "1k"
defaultRequestTTL: "60s" # 큐에서 기다릴 수 있는 최대 시간
priorityBands:
- priority: 100
maxRequests: "500"
fairnessPolicyRef: round-robin-fairness-policy
orderingPolicyRef: fcfs-ordering-policy
파이프라인 위치: Request Handler(파싱·헤더 추출) → Flow Control(큐잉·포화 게이팅) → Request Scheduler(엔드포인트 선택) → Model Servers.
- 정의 — Request Handler는 Parser·DataProducer·Admitter로 요청을 해석하고 근거 데이터를 만들며, Flow Control은 Priority band → Fairness → Ordering 3단 규칙의 중앙 큐입니다.
- 성질 — 포화 시 켜져 있으면 대기(TTL 넘으면 503, 큐 넘치면 429), 꺼져 있으면 sheddable만 429이고 나머지는 모델 서버로 밀려 들어갑니다. 디스패치는 되돌릴 수 없어 신중하게 합니다.
- 쓰임 — 실시간 채팅과 배치를 한 풀에서 섞어 돌릴 때. 헤더
x-llm-d-inference-fairness-id·objective로 테넌트와 우선순위를 표시하고,featureGates: [flowControl]로 켭니다. 원문 실습의 default-plugins.yaml에는 featureGates가 없어 꺼져 있던 것으로 보입니다(4장 접는 상자).
줄을 통과한 요청은 이제 파드 하나를 배정받습니다. 파이프라인의 뒤쪽 절반 — 스케줄러와, 그 판단 근거를 모으는 데이터 계층입니다.
04EPP 안쪽 ② — Scheduler, Data Layer, 그리고 설정 파일
한 줄 요약: 스케줄러는 후보 파드를 걸러내고(Filter), 여러 기준으로 0~1 점수를 매겨 가중합하고(Score), 하나를 고릅니다(Pick). 점수의 원재료는 Data Layer가 모델 서버에서 긁어 온 메트릭이고, 이 모든 조합은 시작할 때 한 번 읽는 YAML 하나로 정해집니다.
11장 트레이스에서 실제로 이 질문이 나옵니다. 두 파드가 2점 대 2점 동점이었고, 원문은 점수표를 한 줄씩 풀어 이유를 찾습니다. 그러려면 어떤 scorer가 무엇을 보고 몇 점을 주는지, 가중치가 어떻게 곱해지는지 알아야 합니다.
그리고 운영에서 가장 먼저 부딪히는 사실 하나. EPP 설정은 핫 리로드가 없습니다. 가중치 하나를 바꾸려 해도 EPP를 재시작해야 합니다.
- Filter · Scorer · Picker
- 스케줄링 3단계 플러그인. Filter는 부적합 후보 제거, Scorer는 0.0~1.0 점수, Picker는 최종 1개 선택입니다.
- 가중합 (weighted sum)
- scorer마다 가중치를 곱해 더한 값. 예: A(가중치 2.0) 0.8점, B(가중치 1.0) 0.5점 → 각 점수에 가중치를 곱해 더하면 2.1.
- SchedulingProfile · ProfileHandler
- 프로필 = Filter 목록 + 가중 Scorer 목록 + Picker 1개. ProfileHandler가 어떤 프로필을 돌릴지 정합니다(
single-profile-handler, P/D용disagg-profile-handler). - P/D disaggregation
- 한 요청의 Prefill과 Decode를 서로 다른 워커가 처리하는 구성. 라우터가 두 endpoint를 각각 고르고 KV 캐시 전송까지 조율합니다.
- Data Layer
- EPP의 실시간 상태 수집·저장 계층. Source → Extractor → Endpoint Attributes 순으로 파드마다 속성 맵을 채웁니다.
- EndpointPickerConfig
- CRD처럼 생겼지만 CRD가 아닌, EPP가 시작할 때 한 번 읽는 정적 설정 파일(
--config-file또는--config-text).
Filter → Score → Pick
| 단계 | 대표 플러그인 | 기준 |
|---|---|---|
| Filter | prefix-cache-affinity-filter · slo-headroom-tier-filter · label-selector-filter · prefill/decode-endpoints-filter | prefix 점수 높은 "sticky" 파드 우선(느리면 제외) · SLO 여유 · 라벨 · P/D 역할 |
| Score | kv-cache-utilization-scorer · queue-depth-scorer · prefix-scorer · latency-scorer · lora-affinity-scorer · running-requests-size-scorer · token-load-scorer · session-affinity-scorer · no-hit-lru-scorer | KV 사용률 낮을수록 · 대기열 짧을수록 · prefix 일치 길이 · 예측 지연과 SLO 여유 · LoRA 로드 여부 · 활성 요청 수 · 처리 중 토큰량 · 같은 세션 · 캐시 미스를 아직 안 받은 파드(prefill 균등화) |
| Pick | max-score-picker(기본) · random-picker · weighted-random-picker | 최고 점수 1개 · 무작위 · 점수를 확률로 쓰는 가중 추첨 |
표의 queue-depth-scorer·prefix-scorer는 원문 실습 설정에서 queue-scorer·prefix-cache-scorer라는 이름으로 나옵니다. running-requests-size-scorer도 payload-agnostic 설정에서는 active-request-scorer로 나옵니다.
P/D 분리일 때는 ProfileHandler가 Prefill 프로필(prefix 친화도·대기열 중시)과 Decode 프로필(메모리 대역폭 바운드 최적화)을 각각 돌리고, decode 파드가 Proxy의 실제 목적지가 되며 prefill 파드 주소는 헤더로 주입돼 decode 쪽 사이드카가 원격 prefill을 조율합니다.
주요 메트릭은 llm_d_epp_ready_endpoints, average_kv_cache_utilization, average_queue_size, scheduler_e2e_duration_seconds, disagg_decision_total입니다.
Data Layer — 판단 근거를 모으는 곳
| 소스 유형 | 방식 | 예 |
|---|---|---|
| Polling | 설정 주기로 폴링 — 새 endpoint마다 Collector 하나 | metrics-data-source → core-metrics-extractor: KVCacheUsagePercent, WaitingQueueSize 등 |
| Notification | k8s 리소스 변경 이벤트 | Pod·Service 변경 |
| Endpoint Event | endpoint 생명주기 이벤트 | 파드 추가·삭제·업데이트 |
Data Layer는 수집만 하고, 판단은 Scheduler와 Flow Control이 합니다. core-metrics-extractor가 엔진마다 다른 메트릭 이름(2장 표)을 표준 키로 바꿔 줍니다. 새 소스는 PollingDataSource·NotificationSource·EndpointSource 중 하나를 구현해 붙입니다.
EndpointPickerConfig — 그래프 한 장
노드는 plugins에 정의한 플러그인 인스턴스, 엣지는 schedulingProfiles·flowControl에서 pluginRef로 가리키는 연결입니다. 같은 인스턴스를 여러 프로필·대역에서 재사용합니다. name을 생략하면 type이 이름이 되고, Admitter·DataProducer는 참조 없이 자동 연결됩니다.
- 기본값 규칙profiles 생략 → 모든 Filter·Scorer·Picker를 담은 default 프로필 자동 생성 · plugins 빈 목록 → Picker만 주입 · Picker 미지정 →
max-score-picker(1개). - 검증은 시작 시점에만없는 pluginRef, 이름 중복, 프로필당 Picker 2개 이상, DataProducer 순환 의존 → 초기화·런타임 에러. 실행 중 감지·리로드 없음.
- 재시작이 필요한 이유"configuration is not reconciled by a controller" — 시작할 때 플러그인 그래프를 한 번 만들고 끝입니다. HA는 replicas > 1 + leader 선출로 롤링 재시작 무중단을 확보합니다.
비유 — 입사 면접 채점표
서류 탈락(Filter) 뒤 면접관 셋이 0~1점을 줍니다. 경력(prefix 일치) 가중치 3, 체력(KV 여유) 2, 일정(대기열) 2. 가중합이 가장 높은 사람을 뽑거나(max), 점수 비율대로 추첨합니다(weighted-random).
두 사람의 총점이 같으면 동점이고, 그땐 둘 중 한 명이 뽑힙니다(목록 순서인지 무작위인지는 구현에 따라 다릅니다). 채점 기준표(설정)를 바꾸려면 면접을 다시 열어야(재시작) 합니다.
작은 숫자로 한 번 — 원문 실습의 기본 가중치
채점표 — 신호를 끌어 올리고 가중치를 바꿔 보기
드래그 · 슬라이더 · 버튼점수 변환은 설명용입니다 — KV 점수 = 1 − 사용률, 대기열 점수 = 파드들 중 최소·최대로 정규화(모두 같으면 1). 실제 플러그인의 정규화 방식은 구현에 따라 다릅니다. 이 그림의 affinity filter에는 위 표에 적은 "느리면 제외" 조건을 넣지 않아, 대기열이 길어도 계속 고정됩니다.
원문 실습 EPP의 기본 설정 (default-plugins.yaml)과 스키마 골격
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: queue-scorer
- type: kv-cache-utilization-scorer
- type: prefix-cache-scorer
- type: metrics-data-source
parameters: { scheme: "http", path: "/metrics", insecureSkipVerify: true }
- type: core-metrics-extractor
schedulingProfiles:
- name: default
plugins:
- pluginRef: queue-scorer
weight: 2
- pluginRef: kv-cache-utilization-scorer
weight: 2
- pluginRef: prefix-cache-scorer
weight: 3
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins: [...] # 노드
featureGates: [...] # 현재 flowControl 만 명시 필요
parser: {...} # 기본 openai-parser
flowControl: {...} # 기본 fairness 는 global-strict-fairness-policy
saturationDetector: {...} # 미지정 시 utilization-detector 자동 생성
schedulingProfiles: [...] # 프로필당 Picker 정확히 1개
dataLayer: {...} # metrics-data-source + core-metrics-extractor 자동 주입
같은 ConfigMap에 payload-agnostic.yaml도 있습니다. 요청 형식을 모를 때 본문을 읽지 않는 passthrough-parser와 active-request-scorer(덜 바쁜 곳)·session-affinity-scorer(세션 고정)만으로 라우팅합니다 — prefix scorer는 본문이 필요해서 빠집니다.
운영 권장: 배포 전 로컬에서 스키마·pluginRef 검증, :9090/metrics 모니터링과 upstream 대시보드, P99 지연·에러율·큐 깊이·KV 사용률 알림, 기능 롤백은 Feature Gate 비활성화로.
- 정의 — Request Scheduler는 Filter → Score(0~1, 가중합) → Pick 플러그인 파이프라인이고, Data Layer는 Polling·Notification·Endpoint 이벤트로 파드 속성을 채우는 수집 계층입니다.
- 성질 — 선택은 가중치와 신호의 곱으로 정해지고, 신호가 같으면 동점이 납니다. 설정(EndpointPickerConfig)은 CRD가 아닌 정적 파일이라 바꾸면 재시작이 필요합니다.
- 쓰임 — 원문 실습 기본값(default-plugins.yaml)은 queue 2 · kv 2 · prefix 3입니다. 11장 트레이스는 precise-plugins.yaml로 돌아 가중치가 다르고,
top_scores역산은 11장에서 합니다. prefix 점수를 정확히 만드는 방법이 다음 장입니다.
점수표에서 가장 무거운 칸은 prefix였습니다. 그런데 EPP는 파드 안 GPU 메모리를 들여다볼 수 없습니다. 어떤 파드가 어떤 KV 블록을 가졌는지 어떻게 알까요.
05KV 캐시 관리 — 어디에 무엇이 캐시됐는지 아는 법
한 줄 요약: EPP가 파드별 캐시를 아는 방법은 둘입니다. 자기가 보낸 기록으로 추측하는 근사(Approximate), 모델 서버가 흘리는 KV 블록 이벤트로 추적하는 정밀(Precise). 점수는 "끊기지 않은 가장 긴 prefix 블록 수"이고, 캐시가 GPU에 안 들어가면 CPU·스토리지로 내립니다(Offloading).
GPU 메모리가 차면 vLLM은 오래된 KV 블록을 축출합니다. EPP가 자기 기록만 믿으면 없는 캐시를 보고 라우팅하게 됩니다. 원문이 근사 방식의 단점으로 정확히 이 "상태 불일치"를 꼽습니다.
9장 실습의 Phase 5가 정밀 방식을 켜는 단계입니다. vLLM에 --kv-events-config로 ZMQ 이벤트를 켜고, EPP가 그걸 구독합니다. 이 장은 그 설정 한 줄의 의미를 풉니다.
- KV 블록 · block key
- KV 캐시는 고정 크기 토큰 묶음(예: 16토큰) 단위로 저장됩니다. 블록 키는 그 블록 내용(과 앞 블록들)의 해시입니다.
- 의존성 체인
- 블록 B2는 B0·B1의 계산 결과 위에서만 의미가 있습니다. 그래서 끊기지 않은 prefix만 재사용됩니다. B0·B1은 있는데 B2가 없으면 B3가 있어도 쓸 수 없습니다.
- KVEvents · ZMQ
- 모델 서버가 캐시 변화를 알리는 이벤트(
BlockStored,BlockRemoved,AllBlocksCleared). ZeroMQ라는 메시징 라이브러리로 발행합니다. - HBM · Offloading
- HBM은 GPU의 고대역폭 메모리. Offloading은 KV 블록을 CPU DRAM·로컬 디스크·공유 스토리지로 옮겨 용량을 늘리는 것입니다.
- Speculative indexing
- 라우팅 직후 "이 파드가 곧 이 블록을 갖게 된다"고 인덱스에 미리 적어 두는 것. 확인 이벤트가 오기 전의 공백을 메웁니다(기본 TTL 2초).
- IPP (Inference Payload Processor)
- 라우팅 전후로 요청·응답 페이로드를 검사·수정하는 플러그형 프레임워크. 풀(pool) 레벨에서 동작합니다.
KV 캐시 관리 3계층
- Prefix-Cache Aware Routing (지능)Router/EPP가 캐시 인덱스를 보고 적중률이 최대가 되게 보냅니다. 근사(
approx-prefix-cache-producer)와 정밀(precise-prefix-cache-producer) 두 구현. - KV-Cache Indexing (관찰)풀 전체의 KV 캐시 상태를 이벤트로 지속 추적. 정밀 라우팅의 source of truth.
- KV Offloading (용량)HBM → CPU RAM → 로컬 디스크 다중 계층으로 캐시를 확장.
근사 vs 정밀
| 근사 (Approximate) | 정밀 (Precise) | |
|---|---|---|
| 토큰화 | EPP엔 토크나이저가 없어 문자-토큰 비율로 근사 | vLLM의 /v1/completions/render로 정확한 토큰 ID 획득(요청이 chat 형식이면 /v1/chat/completions/render)(사이드카 또는 공유 render 서비스) |
| 인덱스 | "최근 어느 prefix 해시를 어느 파드로 보냈나"를 메모리 내 LRU로 자체 관리. 보낸 뒤 "이제 갖고 있다"고 가정 | 모델 서버가 ZMQ로 흘리는 KVEvents를 KV-Cache Indexer가 구독해 파드별 블록 위치를 전역 추적 |
| 장점 | 외부 의존성 없음, 가벼움, 토크나이저·ZMQ 불필요 | 토큰 기반 100% 정밀, 복잡한 축출 정책도 반영, P/D 분리 네이티브 지원 |
| 단점 | 파드가 캐시를 지워도 EPP는 모름(상태 불일치), 문자 기반이라 정밀도 낮음 | render 엔드포인트 + ZMQ 연결 필요, 약간의 리소스 오버헤드, 모델 서버의 이벤트 지원 필요 |
| 선택 | 가볍게 시작하려면 근사, 정확도와 대규모 운영이 중요하면 정밀 | |
KV-Cache Indexer — 점수는 "끊기지 않은 가장 긴 prefix"
구성은 Index(블록 키 → 파드 위치, 이벤트로 갱신) · Data Producer(요청당 한 번 토큰화, 멀티모달 피처 추출) · Scorer(파드별 가장 긴 연속 prefix 매치) 셋입니다. 원문 예시 그대로 — B0~B3를 가진 파드는 4점, B0·B1은 있지만 B2가 없는 파드는 2점. 블록은 저장 계층에 따라 가중치를 받습니다(GPU 1.0, CPU 0.8).
이벤트 전달은 중앙집중형(단일 EPP)과 파드 디스커버리(라벨로 여러 EPP active-active) 두 방식이고, 인덱스 저장소는 In-Memory(기본, 최저 지연), Cost-Aware Memory(바이트 예산), Redis/Valkey(영속, 네트워크 hop) 중에서 고릅니다. 멀티모달 해시, 블록 단위 LoRA 스코핑, 하이브리드 어텐션 레이어 그룹(진행 중)도 지원합니다.
비유 — 도서관 대출 장부
사서(EPP)가 "이 책 시리즈 1~4권은 A 분관에 있다"고 알려 줍니다. 근사 방식의 사서는 자기가 A로 보낸 기록만 믿습니다. A가 공간이 모자라 3·4권을 폐기해도 모릅니다.
정밀 방식은 분관이 입고·폐기 때마다 문자(이벤트)를 보내 장부를 고칩니다. 그리고 시리즈는 1권부터 끊기지 않고 있어야 이어 읽을 수 있습니다 — 1·2·4권만 있으면 2권까지만 쓸모 있습니다.
작은 숫자로 한 번
블록 체인 점수 — 정밀 인덱스와 근사 인덱스가 갈리는 순간
클릭 · 버튼초록 = GPU에 있음(1.0), 보라 = CPU로 내려감(0.8), 회색 = 없음. 점선 테두리는 근사 인덱스가 "있다고 믿는" 블록입니다.
KV-Cache Offloading과 IPP
Offloading은 GPU HBM 용량 제약과 인스턴스마다 캐시가 고립되는 문제를 풉니다. 두 갈래입니다.
네이티브(vLLM OffloadingConnector, --kv-offloading-backend native --kv-offloading-size <GB>)는 CPU 계층이나 공유 스토리지로 블록을 보내고,
Out-of-tree 커넥터는 LMCache·Mooncake·NVIDIA KVBM 같은 외부 엔진을 KV-cache 커넥터 API로 붙입니다.
- CPU Offloading — pinned CPU 메모리를 스테이징 버퍼로 GPU DMA 비동기 전송. 원문 기준 vLLM 0.12.0+의 연속 메모리 레이아웃으로 처리량 4~5배 향상.
- llm-d FS Backend — POSIX 파일시스템에 블록을 파일로 저장. 인스턴스 간 공유, 재시작 후에도 유지.
- MooncakeStoreConnector — 중앙 Master가 메타데이터·축출 관리. Embedded(프로세스 내 DRAM 풀) / Standalone-store(SSD 가능) 모드.
- 성능 가이드 — CPU DRAM이 GPU HBM보다 크면 CPU offloading은 항상 켜라. 스토리지 offloading은 캐시가 단일 노드를 넘거나 노드 간 공유 가치가 있을 때.
IPP는 Envoy ext-proc 위에서 Plugin → Profile → Profile Picker 구조로 동작하며, 라우팅 전(헤더·본문·트레일러 검사·수정)과 응답이 돌아온 뒤의 처리를 맡습니다. 흐름은 Proxy → IPP(변경 반환) → Proxy가 적용해 InferencePool로 → EPP가 풀 안에서 파드 선택 → 모델 서버.
IPP는 독립 Deployment로 배포되고, Istio에선 EnvoyFilter로, GKE에선 GCPRoutingExtension으로 끼웁니다. 가드레일·콘텐츠 필터링·모델 인지 라우팅의 자리입니다.
- 정의 — prefix 인식 라우팅은 근사(보낸 기록으로 추측)와 정밀(ZMQ KVEvents로 추적) 두 방식이고, 점수는 끊기지 않은 가장 긴 prefix 블록 수(GPU 1.0, CPU 0.8)입니다.
- 성질 — 근사는 가볍지만 축출을 모르고, 정밀은 정확하지만 render 엔드포인트·ZMQ·이벤트 지원이 필요합니다. 라우팅 직후 공백은 speculative indexing(TTL 2초)이 메웁니다.
- 쓰임 — 9장 Phase 5가
--enable-prefix-caching --kv-events-config로 정밀 방식을 켭니다(계획에 있던--block-size 16은 실제 적용되지 않았습니다). 용량이 모자라면 offloading, 페이로드 정책은 IPP.
여기까지가 "풀 안에서 파드 고르기"입니다. 그런데 [7W-1]의 Agent Router도 스스로를 라우터라고 불렀습니다. 둘은 같은 걸까요.
06두 개의 라우터 — Agent Router와 llm-d Router
한 줄 요약: 이름은 둘 다 라우터지만 층이 다릅니다. Agent Router(Tier 1)는 앱 앞에서 여러 provider를 하나로 묶는 API 게이트웨이이고, llm-d Router(Tier 2)는 같은 모델을 서빙하는 vLLM 파드들 안에서 한 곳을 고르는 추론 스케줄러입니다. 경쟁이 아니라 앞뒤로 이어 씁니다.
원문은 소개 구획 끝에서 이 비교를 따로 정리합니다. 원문에 실린 Two-Tier Gateway Design 그림이 답입니다. 앞단(edge)은 Agent Router, 그 뒤 vLLM 클러스터 내부 라우팅은 llm-d Router.
9장 실습 구성 역시 사실상 이 구조의 절반입니다. Envoy Gateway + Agent Router 컨트롤러를 깔았지만 AIGatewayRoute는 쓰지 않고 HTTPRoute + InferencePool 경로만 썼습니다. 원문의 설치 계획에 "Agent Router 자체 기능은 이번 테스트에서 미사용"이라고 적혀 있습니다.
- Tier 1 · Tier 2
- 요청이 지나는 앞단 게이트웨이 층과 모델 서빙 클러스터 내부 층. 3장 Flow Control의 Tier 1·2·3(우선순위 대역·공정성·순서)과는 다른 뜻입니다.
- Model virtualization
- 앱이 부르는 모델 이름과 실제 provider·모델을 분리하는 것. 앱은 "chat-default"를 부르고, 게이트웨이가 그 이름을 어디로 보낼지 정합니다.
- MCP · tool catalog
- Model Context Protocol. 에이전트가 쓰는 도구 서버 규약. Agent Router는 여러 MCP 서버의 도구를 하나의 카탈로그로 모읍니다.
- Homogeneous serving
- 같은 모델을 같은 방식으로 서빙하는 파드 묶음. llm-d Router가 최적화하는 대상입니다.
- AAIF
- Agentic AI Foundation. Agent Router가 소속된 Linux Foundation 산하 재단([7W-1] 9장).
나란히 놓으면
| Tier 1 · Agent Router (구 Envoy AI Gateway) | Tier 2 · llm-d Router | |
|---|---|---|
| 질문 | "이 요청을 어느 provider·모델로?" | "이 모델을 서빙하는 N개 파드 중 어디로?" |
| 구조 | 설정 계층(Agent Router: provider·자격증명·규칙·도구·한도) + 트래픽 계층(Envoy). "Agent Router가 설정하고 Envoy가 처리한다" | Proxy(데이터 플레인, 요청을 park하고 ext-proc 콜백) + EPP(Filter → Scorer → Picker) |
| 핵심 기능 | 16개 provider 통합(OpenAI 호환 → Anthropic·Bedrock·Vertex·Azure·자체 vLLM 등), fallback·failover, 모델명 가상화, 팀·앱·모델별 토큰 한도, 키 중앙 관리·로테이션, MCP 도구 통합, OpenTelemetry GenAI 관측 | KV·prefix 캐시 인지, 부하 인지, 요청 우선순위·flow control — 동종 모델 서빙 인프라 내부 최적화 |
| 배포 | 로컬 aigw run 한 줄 → Kubernetes까지 설정 변경 없이 | 경량: Envoy가 EPP와 같은 파드 · 프로덕션 권장: Gateway API(InferencePool backend + 공유 Gateway의 HTTPRoute) |
| 연결점 | Agent Router가 self-hosted 모델에 대해 InferencePool을 지원 → 그 풀의 endpoint 선택을 llm-d EPP가 맡는다([7W-1] 11장에서 본 llm-d-router-endpoint-picker 이미지) | |
원문 기준 연혁: Agent Router는 2026년 6월 v1.0, 8월 v1.1을 내고 Envoy 프로젝트에서 분리돼 AAIF 소속이 되었습니다.
비유 — 공항 환승 안내와 게이트 배정
공항 안내 데스크(Tier 1)는 "런던행이면 A 항공사, 파리행이면 B 항공사, 국내선이면 우리 자체 셔틀"을 정합니다. 여권 확인(인증), 수하물 한도(토큰 한도), 결항 시 대체편(failover)도 여기서 합니다.
자체 셔틀(self-hosted)을 고른 승객에게는 셔틀 터미널의 배차원(Tier 2)이 어느 차가 비었고 누가 이미 같은 방향 짐을 실었는지 보고 차를 고릅니다. 안내 데스크는 차 한 대 한 대를 모르고, 배차원은 항공사를 모릅니다.
작은 숫자로 한 번
요청 한 건이 두 층을 지나는 길
버튼- 정의 — Agent Router는 provider 통합·키·한도·failover·MCP를 맡는 앞단 게이트웨이, llm-d Router는 동종 vLLM 파드 중 하나를 고르는 클러스터 내부 스케줄러입니다.
- 성질 — 층이 달라 겹치지 않고, InferencePool을 매개로 이어집니다. Tier 1은 파드를 모르고, Tier 2는 provider를 모릅니다.
- 쓰임 — 외부 provider와 자체 모델을 함께 쓰면 Two-Tier. 원문 실습은 Tier 2 경로(HTTPRoute + InferencePool + llm-d EPP)를 검증했습니다(9장).
개념 정리는 여기까지입니다. 이제 실제 GPU 한 장짜리 클러스터를 세우고, llm-d를 붙이기 전의 기준선을 만듭니다.
07실습 환경 — GPU 한 장, vLLM 파드 둘
한 줄 요약: K3s 단일 노드에 모니터링을 깔고, NVIDIA device plugin 대신 HAMi를 올려 GPU 한 장을 코어·메모리 50%씩 나눕니다. 모델은 MinIO(S3)에 올려 두고 vLLM이 스트리밍 로더로 바로 읽습니다. 이 "파드 2개"가 9장의 비교 대상입니다.
원문 실습의 해법은 [6W-2] 9장에서 본 HAMi입니다. nvidia.com/gpucores: "50", nvidia.com/gpumem-percentage: "50"으로 한 장을 둘로 나눠 vLLM 파드 2개를 띄웁니다.
작은 모델(Qwen3-0.6B-FP8)을 쓰는 것도 같은 이유입니다.
다만 이 선택이 9장 결론에 그대로 영향을 줍니다. 파드가 2개뿐이고 모델이 작아 토큰 생성(TPOT 약 15 ms)이 빠르다는 조건이 "precise 라우팅의 이득이 작았다"는 결과의 배경입니다. 환경을 알아야 결과를 제대로 읽습니다.
- K3s
- 가벼운 쿠버네티스 배포판. 실습은 기본 Ingress(traefik)와 LB(servicelb)를 끄고 MetalLB를 따로 씁니다.
- HAMi · hami-scheduler
- GPU를 코어 비율·메모리 비율로 쪼개 여러 파드에 나눠 주는 가상화 미들웨어와 그 스케줄러. 파드에
schedulerName: hami-scheduler를 적습니다. - gpucores · gpumem-percentage
- HAMi 리소스 이름. 파드가 쓸 GPU 연산 비율과 메모리 비율(%).
- runai model streamer
- vLLM이 S3 같은 오브젝트 스토리지에서 가중치를 스트리밍으로 바로 로드하는 로더(
--load-format runai_streamer). FUSE 마운트가 필요 없습니다. - gpu-memory-utilization
- vLLM이 보이는 GPU 메모리 중 몇 %를 쓸지(가중치 + 작업 공간 + KV 캐시). 실습 값 0.8.
- DCGM Exporter
- NVIDIA GPU 지표를 Prometheus 형식으로 내보내는 exporter([6W-2] 6장).
쌓는 순서
- K3s + NVIDIA device plugin + MetalLB노드
gpupc(v1.36.4+k3s1, Ubuntu 24.04.4). device plugin은runtimeClassName=nvidia. MetalLB는 Helm 0.16.1. - kube-prometheus-stack + DCGM ExporterPrometheus NodePort 30001, Grafana 30002. DCGM도 NVML 조회를 위해
runtimeClassName=nvidia. 대시보드 12239·23382. - HAMi 2.10.0 (device plugin 제거)HAMi도
nvidia.com/gpu를 관리하므로 기존 nvdp를 uninstall. 노드에gpu=on라벨, monitor 메트릭 9394, HAMi vGPU 대시보드 추가. - MinIO + model-loader JobinitContainer가 HF에서
Qwen/Qwen3-0.6B-FP810개 파일을 익명 다운로드 →mc mirror로 1.00 GiB를 3초(288 MiB/s)에 업로드. Job 41초. - vLLM v0.28.0 Deployment
vllm serve s3://models/Qwen3-0.6B-FP8 --load-format runai_streamer --max-model-len 8192 --enforce-eager --kv-cache-dtype fp8, 컴파일 캐시를 hostPath(/var/cache/vllm)에 영속화(원문 매니페스트 그대로입니다), NodePort 30004. - replicas: 2 + HAMi 50/50
--gpu-memory-utilization 0.8,nvidia.com/gpu: 1·gpucores: 50·gpumem-percentage: 50,strategy: Recreate. per-Pod vLLM 대시보드를 추가해 파드별로 봅니다.
호출 확인: /v1/models에 "id": "Qwen3-0.6B-FP8", "root": "s3://models/Qwen3-0.6B-FP8". 한국의 수도를 물으면 <think>로 시작하는 답이 옵니다(Qwen3의 reasoning 출력).
원문 MinIO Secret에는 예시용 계정·비밀번호가 평문으로 들어 있습니다. 이 페이지에는 옮기지 않았습니다. 실제 환경에 쓸 때는 반드시 다른 값으로 바꾸고, 공개 저장소에 올리지 않는 편이 안전합니다.
비유 — 원룸을 가벽으로 나누기
GPU 한 장은 원룸 하나입니다. HAMi는 가벽을 세워 두 사람이 반씩 쓰게 합니다. 각자는 자기 쪽이 방 전체인 줄 압니다(vLLM은 절반 메모리를 "GPU 전체"로 봅니다).
그 절반에서 vLLM은 다시 80%만 쓰고, 그중 가구(가중치)와 작업대(활성화 텐서·연산 버퍼)를 놓고 남은 바닥이 KV 캐시입니다. 바닥이 좁으면 동시에 펼칠 수 있는 대화(시퀀스)가 줄어듭니다.
작은 숫자로 한 번 — 24 GiB 카드라고 가정
GPU 한 장을 나누면 KV 캐시는 얼마나 남나
버튼 · 슬라이더원문에는 GPU 모델과 vLLM의 KV 캐시 할당 로그가 없어 카드 크기를 선택지로 두었습니다. 가중치 1.0 GiB(원문의 model.safetensors 1.06 GB), 작업 공간 1.2 GiB는 추정이고, KV는 원문 설정(--kv-cache-dtype fp8)대로 값당 1바이트로 계산했습니다. 실제 값은 vLLM 기동 로그의 KV cache 크기로 확인하세요.
원문 매니페스트 발췌 — HAMi values, vLLM 2 replica
# hami-values.yaml
devicePlugin:
runtimeClassName: nvidia
monitor:
extraArgs: ["-v=4", "--metrics-bind-address=:9394"]
prometheus:
enabled: true # GPU Utilization Metrics
scheduler:
kubeScheduler:
imageTag: v1.36.4
helm uninstall nvdp -n nvidia-device-plugin
kubectl label nodes ${NODE_NAME} gpu=on
helm install hami hami-charts/hami -n kube-system --version 2.10.0 -f hami-values.yaml
# vLLM Deployment (발췌)
spec:
replicas: 2
strategy: { type: Recreate }
template:
spec:
schedulerName: hami-scheduler
runtimeClassName: nvidia
containers:
- name: vllm
image: docker.io/vllm/vllm-openai:v0.28.0
args:
- |
python3 -c "import runai_model_streamer" 2>/dev/null || pip install --no-cache-dir vllm[runai]
exec vllm serve s3://models/Qwen3-0.6B-FP8 \
--served-model-name Qwen3-0.6B-FP8 \
--load-format runai_streamer \
--host 0.0.0.0 --port 8000 \
--max-model-len 8192 \
--enforce-eager \
--gpu-memory-utilization 0.8 \
--kv-cache-dtype fp8
env:
- { name: AWS_ENDPOINT_URL, value: "http://minio.vllm.svc.cluster.local:9000" }
- { name: VLLM_CACHE_ROOT, value: /vllm-cache } # torch.compile 캐시 영속화
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY 는 Secret 참조
resources:
limits:
nvidia.com/gpu: "1"
nvidia.com/gpucores: "50"
nvidia.com/gpumem-percentage: "50"
- 정의 — 실습 클러스터는 K3s 단일 노드 + 모니터링 + HAMi(GPU 50/50) + MinIO + vLLM v0.28.0 2 replica(Qwen3-0.6B-FP8, S3 스트리밍 로드)입니다.
- 성질 — 파드는 절반 메모리를 GPU 전체로 보고, 그중
gpu-memory-utilization만큼에서 가중치·작업 공간을 뺀 나머지가 KV 캐시입니다. 비율 합이 100%를 넘으면 파드가 뜨지 않습니다. - 쓰임 — GPU 한 장으로 "파드 여럿" 라우팅 실험을 가능하게 합니다. 대신 파드 2개·작은 모델이라는 조건이 9장의 결과 해석에 따라붙습니다.
파드 둘이 떴습니다. llm-d를 붙이기 전에, 이 상태가 얼마나 빠른지부터 반복해서 잴 수 있어야 비교가 됩니다.
08벤치마크 반복 — 같은 부하를 여러 번, 그리고 seed의 함정
한 줄 요약: vllm bench serve를 CronJob으로 만들어 2분마다 같은 조건으로 돌립니다. 그런데 seed를 고정하면 매번 똑같은 프롬프트가 나가 prefix 캐시가 "공짜로" 적중해 버립니다. 원문은 seed를 실행 시각으로 바꿔 매번 다른 요청을 보냅니다.
원문 표현을 그대로 빌리면, ShareGPT 샘플링은 seed로 초기화된 의사난수로 94,145개 대화를 섞은 뒤 앞에서부터 num_prompts개를 뽑는 방식입니다. seed가 같으면 언제 실행하든 같은 대화가 같은 순서로 나갑니다.
vLLM은 prefix 캐시가 기본으로 켜져 있어(9장 Phase 5의 --enable-prefix-caching은 명시적으로 못 박는 것입니다), 같은 프롬프트를 다시 보내면 캐시 적중이 치솟습니다. 9장에서 "라우팅 방식에 따른 캐시 적중률"을 비교하려면 이 착시를 먼저 걷어내야 합니다. 그래서 원문은 --seed $(date +%s)로 매번 다른 요청을 보냅니다.
- vllm bench serve
- vLLM 이미지에 들어 있는 온라인 서빙 벤치마크 CLI. 처리량·TTFT·TPOT·ITL·E2E 지연을 잽니다. GPU가 필요 없는 순수 HTTP 부하 생성기입니다.
- ShareGPT · prefix_repetition
- ShareGPT는 실제 대화 모음 데이터셋(약 642 MB). prefix_repetition은 공통 prefix 몇 개에 서로 다른 suffix를 붙인 합성 프롬프트를 만들어 prefix 캐시 효과를 재는 데이터셋입니다.
- seed
- 의사난수의 출발값. 같으면 매번 같은 "무작위" 결과가 나옵니다.
- ITL · E2EL
- Inter-Token Latency(토큰 사이 간격)와 End-to-End Latency(요청 전체 소요).
- CronJob · suspend
- 정해진 주기로 Job을 만드는 쿠버네티스 리소스.
suspend: true면 멈춰 두고kubectl create job --from=cronjob/…으로 수동 실행합니다. - OOMKilled
- 컨테이너가 메모리 limit을 넘어 커널에 의해 종료된 상태.
원문이 만든 벤치마크 구조
- CronJob 템플릿매번 파드를 손으로 띄우지 않도록. 처음엔
suspend: true로 두고 수동 실행, 검증 뒤*/2 * * * *(2분마다) +startingDeadlineSeconds: 60(밀린 스케줄 무한 누적 방지). - 데이터셋·결과를 노드에 캐시642 MB ShareGPT를 hostPath에 한 번만 받고, 두 번째 실행부터 initContainer가 다운로드를 건너뜁니다. 결과 JSON·타임라인 HTML·데이터셋 통계 PNG도 hostPath에.
- 파라미터
--backend vllm --endpoint /v1/completions --host qwen3-0-6b-fp8.vllm.svc.cluster.local --model Qwen3-0.6B-FP8 --tokenizer Qwen/Qwen3-0.6B-FP8 --dataset-name sharegpt --num-prompts 50 --max-concurrency 8. 기본값(1000개, 동시성 무제한)은 GPU 50% 공유 환경엔 과합니다. - 트러블슈팅첫 실행이 memory limit 2 Gi에서 OOMKilled — vLLM CLI 임포트 + 642 MB JSON 파싱. requests 2 Gi / limits 6 Gi로 올려 해결.
- seed를 실행 시각으로
--seed $(date +%s). 두 CronJob 모두 워커마다RUN_SEED_BASE + i.
기준선 결과 (원문, llm-d 붙이기 전 · 직접 Service 호출)
| 지표 | 값 | 지표 | 값 |
|---|---|---|---|
| 성공 요청 | 50 / 50 | Mean TTFT | 49.56 ms (P99 72.43) |
| Benchmark duration | 26.44 s | Mean TPOT | 15.18 ms (P99 15.35) |
| 입력 / 생성 토큰 | 12,298 / 11,676 | Mean ITL | 15.19 ms (P99 16.59) |
| Request throughput | 1.89 req/s | Mean E2EL | 3,581 ms (P99 12,028) |
| Output token throughput | 441.66 tok/s (peak 536) | Peak concurrent | 14 |
두 번 반복해도 1.88 · 1.90 req/s, TTFT 50.26 · 49.23 ms로 재현됐고, Grafana 파드별 대시보드에서 두 파드가 트래픽을 나눠 처리하는 것도 확인했습니다. 이 두 번은 seed 고정(42)이었지만 TTFT 차이는 1 ms 안팎이었습니다 — seed 효과는 지연보다 캐시 적중률에서 봐야 합니다.
prefix caching 벤치마크는 --dataset-name prefix_repetition, 기본 num_prefixes 5 · prefix_len 512 · suffix_len 128 · output_len 128 · NUM_PROMPTS 100을 병렬 워커로 나눠 돌립니다. 원문은 seed 고정/변경에 따른 대시보드 변화를 캡처로 남겼습니다(수치 표는 없음).
비유 — 같은 모의고사를 다시 보기
모의고사 성적이 두 번째에 확 오르면 실력이 는 걸까요, 같은 시험지를 받은 걸까요. seed 고정은 같은 시험지입니다.
캐시는 "지난번에 푼 문제"를 기억하는 능력이라, 같은 시험지면 점수가 부풀려집니다. 라우터의 실력(어느 파드로 보내야 캐시를 잘 쓰는가)을 재려면 매번 새 시험지를 줘야 합니다.
작은 숫자로 한 번
반복 실행 10회 — seed 고정과 변경의 캐시 적중률
버튼 · 슬라이더원문에는 이 비교의 수치 표가 없고 대시보드 캡처만 있어, 모형으로 다시 만들었습니다. 16토큰 블록, 파드 2개에 요청을 무작위 배정, 파드마다 LRU 블록 캐시. ShareGPT는 공통 템플릿 1블록 + 대화별 고유 블록(평균 약 15블록), prefix_repetition은 원문 기본값(prefix 512 · suffix 128 · prefix 5종 · 100개)으로 흉내 냈습니다.
파드당 KV 용량은 7장 계산값(약 138k 토큰)보다 좁은 범위로 두어 축출이 보이게 했습니다.
원문 실행 명령과 결과 파일
# CronJob 적용 (최초 1회) → 반복 실행은 이 세 줄 kubectl apply -f /root/manifests/vllm-bench/vllm-bench-cronjob.yaml JOB_NAME="vllm-bench-manual-$(date +%s)" kubectl create job --from=cronjob/vllm-bench-sharegpt "$JOB_NAME" -n vllm kubectl logs -f job/"$JOB_NAME" -n vllm -c bench # 조건 바꾸기 kubectl set env cronjob/vllm-bench-sharegpt -n vllm NUM_PROMPTS=200 MAX_CONCURRENCY=16 # 결과 (hostPath) ls -la /var/cache/vllm-bench/results/ # vllm-bench-infqps-concurrency8-Qwen3-0.6B-FP8-20260913-013518.json # ...timeline.html (--plot-timeline) # ...dataset_stats.png (--plot-dataset-stats) # 2분 주기 자동 실행으로 전환 # schedule: "0 0 31 2 *"(존재하지 않는 날짜) → "*/2 * * * *", suspend: true → false kubectl get cronjobs.batch -n vllm
- 정의 — 기준선은 vllm bench serve를 CronJob으로 반복 실행해 만듭니다. ShareGPT 50개·동시성 8에서 1.89 req/s, TTFT 49.56 ms, TPOT 15.18 ms였습니다(원문).
- 성질 — seed가 같으면 매번 같은 프롬프트라 prefix 캐시 적중이 부풀려집니다. prefix_repetition은 한 회차 안에서도 공통 prefix가 반복돼 seed와 무관하게 적중이 높습니다.
- 쓰임 — 라우팅 방식을 비교하려면 seed를 매번 바꾸고, 같은 파라미터로 짝지어 실행합니다. 9장 Phase 6이 정확히 그렇게 비교합니다.
기준선이 준비됐습니다. 이제 같은 파드 둘 앞에 llm-d를 세우고, 같은 벤치마크를 게이트웨이 경유로 다시 돌립니다.
098단계 설치와 A/B 비교 — 파드 2개에서 얻은 것과 잃은 것
한 줄 요약: Envoy Gateway → Agent Router → Inference Extension → InferencePool·llm-d EPP → 정밀 prefix 라우팅(ZMQ) → 비교 → 메트릭 → 대시보드 순으로 올렸습니다. 같은 파드 둘에 게이트웨이를 거치자 캐시 적중률은 63.9% → 68.75%로 올랐지만 TTFT는 59~66 → 67~69 ms로 소폭 늘었습니다.
잘못 설치한 게 아닙니다. 원문의 결론은 정직합니다. "파드 2개 + 작은 모델" 규모에서는 precise 라우팅의 캐시 이득이 라우팅 오버헤드를 상쇄할 만큼 크지 않을 수 있다.
그렇게 보이는 이유는 이렇습니다. Qwen3-0.6B의 TPOT가 이미 약 15 ms로 빨라서, Envoy → EPP gRPC 스코어링 한 번의 고정 비용이 상대적으로 커 보였다는 해석입니다.
그리고 원문은 반대 조건도 짚습니다. precise 라우팅의 진짜 이점은 파드 수가 많아 라운드로빈이 우연히 캐시를 못 맞출 확률이 높고, 큰 모델이라 캐시 미스(재계산)가 비싼 환경(예: replica 8개 이상)에서 더 뚜렷할 것으로 예상합니다. 이 장의 그림이 그 경계를 찾아봅니다.
- Gateway API 표준 CRD
- GatewayClass·Gateway·HTTPRoute·GRPCRoute·ReferenceGrant. 실습은 v1.3.0.
- GAIE
- Gateway API Inference Extension. InferencePool CRD(실습 v1.6.1)와 Envoy Gateway의 InferencePool 지원을 켭니다.
- llm-d-router-gateway Helm 차트
- InferencePool·EPP·EPP Service·RBAC·HTTPRoute를 한 번에 만드는 공식 차트(v0.10.0).
- --kv-events-config
- vLLM이 KV 블록 이벤트를 ZMQ로 발행하게 하는 인자. 5장의 정밀 인덱스가 이걸 구독합니다.
- token-producer · render
- EPP의 정밀 토큰화 플러그인. 별도 토크나이저 사이드카 없이 vLLM의
/v1/completions/render(요청이 chat 형식이면/v1/chat/completions/render)를 재사용했습니다. - PodMonitor · ServiceMonitor
- Prometheus Operator에게 어떤 파드·서비스를 스크랩할지 알려 주는 리소스.
설치 아키텍처
Client (curl / bench) │ ▼ Envoy Gateway (데이터 플레인, envoy-gateway-system) ── Agent Router 가 설정을 얹음 (envoy-ai-gateway-system) │ InferencePool 기능 활성화 │ ext-proc (gRPC) ▼ EPP (llm-d 커스텀 이미지 + 플러그인) ── InferencePool ── [Qwen3-0.6B-FP8 pod A] │ prefix-cache indexer (KV 이벤트 ZMQ 구독) [Qwen3-0.6B-FP8 pod B] ▼ 선택된 vLLM 파드로 포워딩
여덟 단계
- Phase 1 · Envoy GatewayGateway API v1.3.0 CRD, Envoy Gateway v1.9.1 Helm →
envoy-gateway1/1. - Phase 2 · Agent Router CRD + Controllerv1.1.0. CRD: aigatewayroutes · aiservicebackends · backendsecuritypolicies · gatewayconfigs · mcproutes · quotapolicies.
- Phase 3 · GAIE 활성화InferencePool CRD(v1.6.1 — 이 버전엔 InferenceObjective가 릴리스에서 빠져 설치하지 않음), agent-router 예제 config로 InferencePool watch를 켜고 envoy-gateway 재기동.
- Phase 4 · 기존 vLLM 파드 연동새로 배포하지 않고
app: qwen3-0-6b-fp8라벨을 selector로 재사용. llm-d-router-gateway 차트 v0.10.0으로 InferencePool(failureMode: FailOpen) + EPP(llm-d-router-endpoint-picker:v0.10.0) + HTTPRoute. GAIE 기본 EPP는 prefix 플러그인이 없어 llm-d EPP로 교체. MetalLB가 Gateway에192.168.254.240. - Phase 5 · 정밀 prefix 라우팅vLLM에
--enable-prefix-caching --kv-events-config(ZMQ publisher, 포트 5557, topickv@$(POD_NAME)@Qwen3-0.6B-FP8). EPP를 precise 모드(token-producer → precise-prefix-cache-producer)로. EPP 로그에 두 파드 모두Connected subscriber socket: tcp://<PodIP>:5557. 같은 긴 prompt 6회 → 100% 같은 파드. - Phase 6 · Prefix-Aware Scheduling 비교기존 prefix caching CronJob(직접 Service)과 같은 파라미터로 게이트웨이 경유 Job을 짝지어 실행, 캐시 메트릭 스냅샷으로 비교.
- Phase 7 · 메트릭 수집EPP ServiceMonitor의
router.monitoring.*위치 버그를router.monitoring.provider.name: prometheusoperator로 수정. vLLM은 기존 ServiceMonitor와 중복 스크랩되던 새 PodMonitor를 지워 일원화(더블 카운팅 방지). - Phase 8 · Grafana공식 llm-d 대시보드 3종(vllm-overview, performance-kv-cache, inference-gateway). KV Cache Hit Rate 패널 65.1%. 요청 단위 EPP 메트릭·flow control 패널은 해당 기능이 꺼져 있어 "No data".
Phase 6 비교 결과 (원문 표)
| 지표 | 직접 호출 (baseline, kube-proxy) | 게이트웨이 경유 (precise prefix-cache) |
|---|---|---|
| Prefix cache 적중률 | 63.9% (40,896 / 64,005) | 68.75% (44,000 / 64,005) |
| 파드 간 쿼리 분산 | 42% / 58% (거의 균등) | 62% / 38% (캐시 어피니티로 한쪽에 더 몰림) |
| Mean TTFT | 59~66 ms | 67~69 ms |
| P99 TTFT | 113~151 ms | 148~173 ms |
| Throughput | 약 3.9~4.1 req/s | 약 4.0~4.1 req/s (동일) |
원문 해석 세 줄: 캐시 적중률은 명확히 개선(+약 4.9%p, 파드 분산이 42/58에서 62/38로 치우친 것 자체가 캐시 어피니티의 증거) · TTFT는 소폭 증가(Envoy → EPP 홉의 고정 오버헤드, 작은 모델이라 상대적으로 두드러짐) · 작은 규모에선 이득이 오버헤드를 상쇄하지 못할 수 있음.
비교 후엔 prefix caching CronJob의 대상을 192.168.254.240:80(게이트웨이)으로 바꿔 이후 실행이 llm-d를 거치게 했습니다.
비유 — 동네 편의점 두 곳에 배달 앱 붙이기
편의점이 두 곳뿐이고 둘 다 인기 상품을 늘 갖춰 두면, 아무 데나 가도 대부분 있습니다.
여기에 "재고를 조회해 알맞은 곳으로 안내"하는 앱을 붙이면 조회 시간만큼 조금 늦어집니다. 편의점이 스무 곳이고 상품이 수백 가지라면 이야기가 다릅니다. 아무 데나 가면 허탕이 잦고, 안내 앱의 몇 초가 싸게 느껴집니다.
작은 숫자로 한 번
파드 수와 prefix 종류에 따라 precise 라우팅이 이득이 되는 지점
슬라이더prefix 512 + suffix 128 토큰, 요청 800건, TTFT는 기본 지연 45 ms에 캐시 안 된 토큰 수에 비례한 시간(토큰당 0.06 ms)을 더하고, precise면 오버헤드를 얹은 모형입니다. 파라미터는 원문 기준선(TTFT 60 ms 안팎, 오버헤드 수 ms)과 비슷한 크기가 되도록 정했을 뿐 실측이 아닙니다. precise는 prefix를 가진 파드 중 덜 바쁜 곳, 너무 몰리면 다른 파드로 보냅니다.
원문 Phase 4·5·7 설정 발췌
# kubectl get inferencepools -n vllm -o yaml (발췌)
spec:
appProtocol: http
endpointPickerRef:
failureMode: FailOpen # EPP 가 죽어도 요청은 흘린다
kind: Service
name: qwen3-router-epp
port: { number: 9002 }
selector:
matchLabels: { app: qwen3-0-6b-fp8 }
targetPorts:
- number: 8000
# endpoints/qwen3-0-6b-fp8 10.42.0.15:8000,10.42.0.16:8000
# EPP 컨테이너 (발췌)
image: ghcr.io/llm-d/llm-d-router-endpoint-picker:v0.10.0
ports: 9002 (grpc), 9003 (grpc-health), 9090 (metrics)
args: --pool-name qwen3-router --pool-namespace vllm --pool-group inference.networking.k8s.io
--config-file /config/default-plugins.yaml --grpc-health-port 9003 --tracing=false
# Phase 5 — vLLM 인자 추가
--enable-prefix-caching # 계획엔 --block-size 16도 있었으나 실제 describe 출력엔 없음
--kv-events-config '{"enable_kv_cache_events": true, "publisher": "zmq", "endpoint": "tcp://*:5557", "topic": "kv@$(POD_NAME)@Qwen3-0.6B-FP8"}'
# EPP 설정 파일은 이후 /config/precise-plugins.yaml 로 전환
# 재기동 전 prefix caching CronJob 을 suspend → patch → 재기동 확인 → resume
# 경로 비교 — 같은 파드, 다른 입구 직접 호출 Client → k8s Service(ClusterIP) → 파드 kube-proxy(iptables), TCP 연결 단위 무작위 게이트웨이 Client → Envoy Gateway → EPP(ext-proc gRPC 스코어링) → 파드 요청마다 prefix/kv-util/queue 점수
- 정의 — 설치는 Envoy Gateway → Agent Router → GAIE → InferencePool + llm-d EPP → 정밀 prefix(ZMQ KV 이벤트) → 비교 → 메트릭·대시보드 8단계였고, 기존 vLLM 파드를 라벨로 그대로 재사용했습니다.
- 성질 — 원문 비교에서 적중률 +4.9%p, TTFT +수 ms, 처리량 동일. 캐시 이득은 파드 수·prefix 다양성·미스 비용이 클수록 커지고, EPP 홉의 고정 비용은 작은 모델에서 두드러집니다.
- 쓰임 — 도입 효과를 판단할 때 "적중률"과 "TTFT"를 같이 보고, 규모를 키운 조건에서 다시 재야 합니다. EPP 오버헤드가 정말 몇 ms인지는 다음 장의 트레이스가 답합니다.
"게이트웨이 홉 때문에 느려졌다"는 해석이 맞는지, 요청 한 건을 시간 단위로 쪼개 확인해 봅니다.
10트레이싱으로 본 450 ms — EPP는 정말 비쌌나
한 줄 요약: EPP와 vLLM에 OpenTelemetry를 켜서 Jaeger로 보니, 요청 한 건 450.44 ms 중 vLLM 추론이 448.01 ms(99.46%)였고 EPP 쪽 스팬은 모두 합쳐 약 2.43 ms(0.54%), 그중 루트 스팬 self는 1.28 ms였습니다. 라우팅 단계는 토큰화 → prefix 캐시 조회 → 스코어러 셋 → 선택으로 전부 한 트레이스에 이어져 보였습니다.
평균 지표만으로는 "어느 구간"인지 모릅니다. 원문은 선택 단계로 분산 트레이싱(Jaeger + OTel Collector)을 붙여 한 요청을 스팬 단위로 쪼갭니다. 결과는 9장의 해석을 정량적으로 받쳐 줍니다 — EPP 내부 처리는 마이크로초~밀리초 단위였습니다.
예상 밖의 발견도 하나 있었습니다. Envoy Gateway에는 트레이싱을 따로 켜지 않아 EPP 스팬과 vLLM 스팬이 따로 떨어질 줄 알았는데, Envoy가 trace context 헤더를 그대로 통과시켜 하나의 traceID로 이어졌습니다.
- Trace · Span
- Trace는 요청 한 건의 전체 여정, Span은 그 안의 작업 한 구간(시작·끝·태그). 스팬은 부모-자식 트리를 이룹니다.
- Self · Total
- 스팬의 자기 자신만의 시간과 자식 스팬까지 포함한 누적 시간.
- OTLP · OTel Collector · Jaeger
- OTLP는 OpenTelemetry 전송 프로토콜(gRPC 4317), Collector는 받은 스팬을 거르고 묶어 내보내는 중계기, Jaeger는 트레이스 저장·조회 UI(16686)입니다.
- traceparent
- 서비스 사이에 trace ID를 이어 주는 HTTP 헤더(W3C Trace Context). 중간 프록시가 이걸 지우지 않으면 트레이스가 이어집니다.
- Flamegraph · Waterfall
- 스팬을 시간 폭만큼 가로로 쌓은 그림과, 시작 시각 순서로 계단식으로 늘어놓은 그림.
- gen_ai.* 속성
- OpenTelemetry GenAI 시맨틱 컨벤션. 모델명, e2e 지연, decode 시간 같은 LLM 전용 태그 이름 규약입니다.
구성
vLLM (vllm-decode) ────┐ EPP (llm-d-router) ──┼── OTLP/gRPC :4317 ──▶ OTel Collector ──▶ Jaeger (all-in-one, in-memory) (P/D Routing Proxy 는 │ /metrics 스크랩 스팬 제거 UI :16686 → NodePort 30005 이번 구성에 없음) ┘ batch 처리
- Jaeger 2.15.0 + OTel Collector contrib 0.120.0vllm 네임스페이스. Collector 파이프라인: otlp 수신 →
/metrics관련 스팬 필터 → batch → Jaeger. - EPPhelm values
router.tracing.enabled: true,otelExporterEndpoint, samplerparentbased_traceidratio· 1.0(실습 트래픽이 적어 100%, 운영은 0.1 권장). EPP가--tracing=true로 재기동. - vLLM
--otlp-traces-endpoint,--collect-detailed-traces all,OTEL_SERVICE_NAME=vllm-decode등 env 4종. Phase 5와 같은 패턴(CronJob suspend → patch → 재기동 → resume).
원문 트레이스 한 건 해부
| 스팬 | 서비스 | 시간 | 읽을 점 |
|---|---|---|---|
gateway.request | llm-d-router/epp | Total 450.44 ms · Self 1.28 ms | 루트. 자기 몫(self)은 1.28 ms이고, 하위 스팬 orchestration과 vLLM 대기가 나머지 |
tokenize_render | epp | 1.04 ms | vLLM /render로 정확한 토큰화 — EPP 안에서 가장 긴 단계 |
produce_precise_prefix_cache | epp | 0.02 ms | 5장의 정밀 인덱스 조회 |
| scorer 3종 (prefix-cache · kv-cache-utilization · queue) | epp | 각 <0.01 ms | 4장의 가중합 |
vllm-decode: llm_request | vllm-decode | 448.01 ms (99.46%) | gen_ai.latency.e2e=0.4478s, time_in_model_decode=0.4161s, time_in_model_inference=0.4463s |
그래프 뷰에선 gateway.request → gateway.request_orchestration과 llm_request로 갈라지고, orchestration 아래로 tokenize_render · produce_precise_prefix_cache · run_scheduler_profile → filter_endpoints · llm_d.epp.scoring(스코어러 3개) · pick_endpoints가 이어집니다. 원문 캡처 기준 서비스 2개, 스팬 13개입니다(아래 그림과 이 문단에는 이름이 확인되는 12개만 옮겼습니다).
pick_endpoints 태그에는 gen_ai.request.model=Qwen3-0.6B-FP8, llm_d.epp.picker.candidate_endpoints=2가 찍힙니다.
다만 이 트레이스가 답한 건 절반입니다. 2.43 ms는 EPP가 소유한 스팬의 합이고, 그 안의 루트 self 1.28 ms에는 Envoy를 거쳐 vLLM 응답을 기다린 시간도 섞여 있습니다.
9장에서 늘어난 5 ms 남짓 중 나머지는 트레이싱을 켜지 않은 Envoy 홉과 네트워크 구간으로 보이지만, 이번 구성에서는 재지 못했습니다.
비유 — 택시 호출 앱의 배차 시간
강남에서 인천공항까지 택시로 45분. 그중 앱이 기사를 배차하는 데 쓴 시간이 15초라면 비중은 약 0.56%입니다 — 이번 트레이스의 EPP(0.54%)와 거의 같은 비중입니다.
다만 가까운 편의점까지 3분 거리라면 같은 15초가 8%가 됩니다 — 9장의 "작은 모델에서 오버헤드가 두드러진다"는 해석이 이 구도입니다.
작은 숫자로 한 번
flamegraph — 확대해야 보이는 EPP
슬라이더 · 클릭원문 값: 전체 450.44, llm_request 448.01, EPP self 1.28, tokenize_render 1.04, prefix 조회 0.02, 스코어러 각 <0.01 ms. 추정: orchestration 1.15 ms(역산), run_scheduler_profile·filter·pick의 개별 시간과 모든 스팬의 시작 시각. "근사" 버튼은 render 호출을 뺐을 때를 가정한 가상 트레이스입니다.
원문 설정 발췌 — EPP helm values와 vLLM 인자
router:
tracing:
enabled: true
otelExporterEndpoint: "http://otel-collector.vllm.svc.cluster.local:4317"
sampling:
sampler: "parentbased_traceidratio"
samplerArg: "1.0" # 실습은 100%, 운영은 0.1 권장
# vLLM
--otlp-traces-endpoint http://otel-collector.vllm.svc.cluster.local:4317
--collect-detailed-traces all
env:
- { name: OTEL_SERVICE_NAME, value: "vllm-decode" }
- { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: "http://otel-collector.vllm.svc.cluster.local:4317" }
- { name: OTEL_TRACES_SAMPLER, value: "parentbased_traceidratio" }
- { name: OTEL_TRACES_SAMPLER_ARG, value: "1.0" }
# 검증
curl http://<node>:30005/api/services # vllm-decode, llm-d-router/epp
- 정의 — EPP(
router.tracing)와 vLLM(--otlp-traces-endpoint)이 OTLP로 Collector → Jaeger에 스팬을 보내고, Envoy가 trace 헤더를 통과시켜 한 traceID로 이어집니다. - 성질 — 원문 트레이스에서 EPP 쪽 합계 약 2.43 ms(0.54%, 루트 self 1.28 ms), vLLM 448.01 ms(99.46%). EPP 안에서는 정밀 토큰화(
tokenize_render1.04 ms)가 가장 크고, 스코어링은 마이크로초 단위입니다. - 쓰임 — "라우터 오버헤드"를 숫자로 확인하고, 어떤 단계(토큰화·조회·스코어링)를 줄일지 고릅니다.
pick_endpoints태그로 후보 수와 선택 근거를 봅니다(11장).
마지막으로 설치된 것들을 하나씩 열어 확인합니다. 로그 두 줄이 서로 다른 파드에 찍힌 이유, 그리고 2점 대 2점 동점의 정체.
11동작 확인 — 로그 두 줄과 2점 대 2점
한 줄 요약: 설치된 Router(Envoy 파드)·HTTPRoute·InferencePool·EPP·모델 서버를 차례로 열어 봅니다. vLLM 로그에 요청마다 두 줄(EPP의 토큰화 호출, Envoy의 실제 추론 호출)이 찍히고, 처음 보는 prompt는 두 파드가 동점이라 목록 첫 번째로 갔을 가능성이 높고, 같은 prompt를 반복하면 prefix 점수가 붙어 거의 한 파드로 몰립니다.
원문이 실제로 멈춰서 따져 본 장면입니다. 결론은 서로 다른 두 흐름입니다. 하나는 EPP가 정밀 prefix 스코어링을 위해 vLLM Service에 보낸 /render(토큰화), 다른 하나는 Envoy가 EPP가 고른 파드로 보낸 진짜 추론입니다.
이 장은 1~10장의 부품을 실물 리소스로 확인하는 점검표이자, "왜 이 파드로 갔나"를 로그·트레이스 태그로 역산하는 연습입니다.
- access log 필드
- Envoy가 요청마다 남기는 JSON 한 줄.
downstream_local_address(Envoy가 받은 주소),upstream_host(실제로 보낸 파드),response_flags(재시도·타임아웃 등 이상 표시). - /v1/chat/completions/render
- vLLM이 프롬프트를 추론 없이 토큰화만 해 주는 엔드포인트. 정밀 prefix 스코어링의 재료입니다.
- top_endpoints · top_scores
pick_endpoints스팬 태그. 최고 점수를 받은 파드 목록과 점수. 여러 개면 동점입니다.- kubectl tail · get-all
kubectl tail은 여러 파드 로그를 한 번에 따라가는 도구,kubectl get-all은 네임스페이스의 모든 리소스를 한 번에 보여 주는 도구입니다. 둘 다 kubectl 플러그인 관리자 krew로 설치합니다.
부품별 점검표 (원문에서 확인한 것)
| 부품 | 확인 결과 | 눈여겨볼 점 |
|---|---|---|
| Router — Envoy 파드 | envoy-vllm-inference-gateway-42784ead, 2/2(envoy + shutdown-manager), IP 10.42.0.225, LB IP 192.168.254.240 | [7W-1] 10장의 3/3과 달리 extproc 컨테이너가 없습니다 — AIGatewayRoute를 쓰지 않고 HTTPRoute + InferencePool 경로만 써서, ext_proc 대상이 별도 파드의 EPP이기 때문으로 보입니다 |
| access log | downstream_local_address 10.42.0.225:10080, x-envoy-origin-path /v1/completions, user-agent aiohttp(벤치 CronJob), upstream_host가 10.42.0.19 ↔ 10.42.0.20으로 번갈아 | 10080은 비루트 컨테이너라서(서비스에선 80). response_code_details: via_upstream, response_flags: - — 정상 |
HTTPRoute qwen3-router | parentRef Gateway inference-gateway, path /, backendRef InferencePool, timeouts.request: 300s | LLM 생성이 길어질 수 있어 넉넉하게(차트 기본값). Phase 4에서 차트가 자동 생성 |
| InferencePool + EPP | InferencePool qwen3-router → endpointPickerRef EPP Service :9002. EPP 10.42.0.17, --config-file /config/precise-plugins.yaml | 트래픽 순서: 클라이언트 → Envoy(별도 파드) → EPP(gRPC) → Envoy → vLLM. EPP 로그: "EPP received request", "EPP sent response body back to proxy" |
| Model Server | qwen3-0-6b-fp8 2/2, vLLM v0.28.0, NodePort 30004, 로그에 prompt·generation throughput과 GPU KV cache usage | 요청마다 /render(출발지 EPP)와 실제 추론 호출(출발지 Envoy — 벤치는 /v1/completions, 수동 curl은 /v1/chat/completions) 두 줄 |
로그 두 줄의 정체
vkk2k 10.42.0.17:33942 - "POST /v1/chat/completions/render HTTP/1.1" 200 ← EPP 파드 (token-producer 토큰화, vLLM Service 경유) zhsb7 10.42.0.225:39450 - "POST /v1/chat/completions HTTP/1.1" 200 ← Envoy 파드 (EPP가 고른 파드로 실제 추론)
토큰화는 어느 파드든 같은 토크나이저라 결과가 같으니 다른 파드에 떨어져도 문제없고, 실제 추론 파드는 EPP의 prefix·kv·queue 점수가 정합니다. 원문은 이어서 같은 prompt를 반복 호출해 "거의 대부분 한쪽 파드(vkk2k)로 전달"되는 것을 확인합니다.
2점 대 2점 — pick_endpoints 트레이스
| 태그 | 값 | 의미 |
|---|---|---|
llm_d.epp.picker.candidate_endpoints | 2 | 풀의 두 파드(vkk2k, zhsb7)가 모두 후보 |
llm_d.epp.picker.top_endpoints | […vkk2k-rank-0, …zhsb7-rank-0] | 최고 점수 파드가 둘 — 동점 |
llm_d.epp.picker.top_scores | [2, 2] | 각 최종 점수 |
원문 해석: 처음 보는 prompt라 prefix-cache-scorer는 두 파드 모두 0, 두 파드가 유휴라 kv-cache-utilization·queue가 각각 1 → 0 + 1 + 1 = 2 동점. 즉 "캐시가 변별력을 못 낸 순수 부하 분산 상황"이고, max-score-picker는 목록 첫 번째(vkk2k)를 골랐을 가능성이 높습니다. 스팬 시간은 prefix-cache-scorer 84 µs, kv-cache-utilization-scorer 75 µs, queue-scorer 1 µs, pick_endpoints 5 µs였습니다. (10장 트레이스의 "스코어러 각 <0.01 ms"와 달라 다른 요청의 트레이스로 보이지만, 원문에 trace ID가 없어 확인하지 못했습니다.)
비유 — 처음 온 손님과 단골
처음 온 손님은 어느 직원에게 가도 똑같습니다(동점). 매니저는 명단 맨 위 직원에게 보냅니다. 그 직원이 손님 취향을 기억하게 되면(prefix 캐시), 다음부터는 그 직원 점수만 올라가 자연스럽게 단골 담당이 됩니다.
작은 숫자로 한 번
같은 prompt를 반복하면 동점이 깨진다
버튼/render와 /chat/completions가 다른 파드에 찍힐 수 있습니다. ② 이어서 같은 prompt 보내기를 몇 번 누르면 선택된 파드의 prefix 점수가 올라 4 대 2로 고정됩니다. ③ 동점 규칙을 무작위로 바꾸고 새 prompt를 여러 번 보내면 가끔 첫 배정이 다른 파드로 갑니다(연달아 같은 파드가 나올 수도 있습니다). ④ 같은 prompt 보내기로 한 파드에 prefix 점수가 붙은 상태에서(초기화를 눌렀다면 이것부터), pod 부하 올리기를 두 번 누르고 다시 같은 prompt 보내기를 눌러 보세요. queue 점수가 0으로 떨어져도(합계 3 대 2) prefix 가중치가 더 커서 파드가 바뀌지 않습니다.가중치(prefix 2 · kv 1 · queue 1)는 원문 해석 문장("가중치 2.0", "1+1=2")을 따른 값입니다. [2, 2] 동점으로 확인되는 건 kv·queue 가중치 합이 2라는 것뿐이고, prefix 가중치는 precise-plugins.yaml이 없어 확인하지 못했습니다(4장의 3·2·2였다면 [4, 4]).
kv 사용률은 두 파드 모두 거의 0으로 고정했고, /render는 vLLM Service를 거쳐 무작위 파드로 간다고 두었습니다. 부하는 queue 점수를 0.5씩 깎는 것으로 흉내 냈습니다.
원문 점검 명령 모음
# Router
kubectl get deploy,pod -n envoy-gateway-system -l app.kubernetes.io/component=proxy -owide
kubectl logs -n envoy-gateway-system -l app.kubernetes.io/component=proxy -c envoy -f
# HTTPRoute → InferencePool
kubectl get httproute -n vllm qwen3-router -oyaml
kubectl get inferencepool -n vllm qwen3-router -oyaml
# EPP
kubectl logs -n vllm -l llm-d-router-gateway=qwen3-router-epp -f
# Model Server — 두 파드 로그를 한 번에
kubectl tail -n vllm -l app=qwen3-0-6b-fp8 --since=3m | grep -vE 'metrics|health'
# 반복 호출 (부하 CronJob 은 먼저 멈춘다)
kubectl patch cronjob -n vllm vllm-bench-prefix-caching -p '{"spec":{"suspend":true}}'
curl -s -X POST http://192.168.254.240/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"Qwen3-0.6B-FP8","messages":[{"role":"user","content":"What can you do?"}],"max_tokens":100}' | jq
- 정의 — 동작 확인은 Router(Envoy 2/2) → HTTPRoute(backend InferencePool, 300s) → InferencePool·EPP(precise-plugins) → Model Server 순으로, 로그와 트레이스 태그로 합니다.
- 성질 — vLLM 로그의 두 줄은 EPP의
/render와 Envoy의 실제 추론으로 서로 다른 흐름입니다. 새 prompt는 prefix 0점이라 부하 점수만으로 동점이 나고, 반복되면 prefix 점수로 한 파드에 고정됩니다. - 쓰임 — "왜 이 파드로 갔나"는
top_endpoints·top_scores와 가중치로 역산합니다. 원문의 다음 과제는 Load-aware·Predicted Latency·Tiered Prefix Cache·P/D·Flow Control·Autoscaling을 직접 켜 보는 것입니다.
EPP의 상자를 열고, 파드 둘에 붙여 재고, 트레이스로 쪼갰습니다. 정리합니다.
마치며
처음 질문으로 돌아갑니다. 같은 모델 파드 여러 개 중 누구에게 보낼 것인가.
llm-d의 답은 "InferencePool이 후보를 대고, EPP가 Filter → Score → Pick으로 고르고, Proxy가 그 주소로 보낸다"였습니다. 점수의 기준은 모델 서버의 대기열·KV 사용률 메트릭과, vLLM이 ZMQ로 흘리는 KV 블록 이벤트였습니다.
즉, 같은 모델 파드 여러 개 중 누구에게 보낼지는 InferencePool이 후보를 정하고, EPP가 점수로 고른 뒤 Proxy가 그 주소로 보내는 구조로 결정된다고 할 수 있습니다.
이 구조 덕분에 llm-d는 모델 서버의 상태와 vLLM의 캐시 상황을 반영하여 효율적으로 요청을 분배할 수 있다는 것을 알게 되었습니다.
이로써 7주차 LLMSO 스터디의 마지막 주차가 끝났네요, 후반부의 실습 내용은 사정으로 인해 직접 해보지 못했지만, 가시다님의 설명을 통해 개념적으로 이해할 수 있었습니다. 연말에 직접 실습을 해보며 경험을 쌓아, 회사에서 실제로 활용할 수 있기를 기대합니다.
어려운 내용을 최대한 쉽게 나누어 이해될 수 있게 스터디를 준비해 주신 CloudNet@에 감사드립니다.
이상으로 7주차 LLMSO 스터디의 llm-d 학습 자료 정리를 마칩니다. 감사합니다.
제보 부탁드립니다
이 페이지는 [7W-2] llm-d 스터디 자료의 정리본입니다. 참고한 것은 llm-d 공식 문서(Architecture · Core · Advanced)와 블로그, 원문에 링크된 Red Hat 발표(Beyond vLLM), Gateway API Inference Extension, 그리고 멤버 추천 글(GPU 없이 llm-d 게이트웨이 뜯어보기, PagedAttention에서 llm-d까지, vLLM 라우팅 6주차 정리, llm-d 기반 EKS 분산 추론 가이드)입니다.
설명이 틀렸거나 최신 버전에서 다르게 동작하는 부분이 있으면 알려주세요.
'AI > [LLMSO] Hands-On LLM 서빙과 최적화 스터디' 카테고리의 다른 글
| [7주차-LLMSO] Envoy: Agent Router (2026.09.13) (0) | 2026.09.20 |
|---|---|
| [6주차-LLMSO] Scaling LLM Inference with vLLM and AWS Trainium (2026.09.06) (1) | 2026.09.13 |
| [5주차-LLMSO]CH10. Advancements in LLM Serving (2026.08.30) (1) | 2026.09.05 |
| [5주차-LLMSO]CH9. LLM Optimization in Practice (2026.08.30) (0) | 2026.09.05 |
| [4주차-LLMSO]CH8. LLM Serving Frameworks (2026.08.23) (0) | 2026.08.30 |