fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/mlops/캐시를 보고 인스턴스를 고른다
mlops

캐시를 보고 인스턴스를 고른다

자체 호스팅 vLLM 을 세 대 띄우고 앞에 로드 밸런서를 두면 요청이 고르게 분산된다. 분산은 잘 되는데 응답이 예상보다 느리다. 원인은 로드 밸런서가 나쁜 것이 아니다. 일반 로드 밸런서는 각 인스턴스가 무엇을 이미 계산해 두었는지 모른다. 같은 앞부분을 가진 요청이 매번 다른 인스턴스로 가면 그 앞부분을 매번 다시 계산한다. 앞 글에서 다룬 캐시와 이...

2026.09.08·7 min read·106 views·SERIES · Model Router · 4/8

Model Router 시리즈 4번이다. 앞 글 모델을 골라주는 계층이 대신 가져오는 제약에서 위 계층의 캐시와 라우팅이 충돌하는 구간을 다뤘다. 이 글은 아래 계층, 즉 자체 호스팅 모델이 여러 대일 때 어느 인스턴스로 보낼지를 다룬다. 서빙 자체의 원리는 GPU로 LLM을 서빙한다는 것이 다룬다.

자체 호스팅 vLLM 을 세 대 띄우고 앞에 로드 밸런서를 두면 요청이 고르게 분산된다. 분산은 잘 되는데 응답이 예상보다 느리다.

원인은 로드 밸런서가 나쁜 것이 아니다. 일반 로드 밸런서는 각 인스턴스가 무엇을 이미 계산해 두었는지 모른다. 같은 앞부분을 가진 요청이 매번 다른 인스턴스로 가면 그 앞부분을 매번 다시 계산한다.

캐시라는 한 단어가 두 계층에서 다른 것을 가리킨다

앞 글에서 다룬 캐시와 이 글에서 다루는 캐시는 다른 것이다.

축위 계층. 제공자 prompt caching아래 계층. prefix cache
누가 갖는가외부 제공자내가 띄운 vLLM 인스턴스
무엇을 아끼는가입력 토큰 단가prefill 연산 시간
효과가 나타나는 곳요금TTFT
내가 제어하는가캐시를 붙일지 여부만어느 인스턴스로 보낼지까지
회계cache write 프리미엄과 cache read 할인요금과 무관하다. GPU 시간만 아낀다

둘을 섞으면 판단이 어긋난다. 제공자 prompt caching 은 비용 문제이고, prefix cache 는 지연 문제다. 외부 제공자 API 만 쓰면 아래 계층은 아예 존재하지 않는다.

prefill 과 decode 가 캐시로 갈린다

캐시가 왜 지연을 바꾸는지는 LLM 추론이 두 단계로 나뉜다는 데서 나온다.

단계하는 일계산 특성
prefill입력 프롬프트 전체를 한 번에 처리해 KV cache 를 채우고 첫 토큰을 낸다입력 토큰 수에 비례한다. 병렬 연산이 많다
decode토큰을 하나씩 생성하며 KV cache 를 이어 붙인다생성 토큰 수에 비례한다. 메모리 대역폭에 묶인다

이 둘의 차이와 배칭이 어디에 걸리는지는 배칭과 GPU 활용률이 다룬다.

prefix cache 가 아끼는 것은 prefill 이다. 같은 앞부분에 대한 KV cache 가 이미 인스턴스 메모리에 있으면 그 부분의 계산을 건너뛴다. 그래서 효과가 TTFT 에 나타나고, decode 속도에는 나타나지 않는다.

여기서 두 값이 정해진다.

  • 앞부분이 얼마나 긴가. system prompt 가 2,000 토큰이고 사용자 질문이 50 토큰이면 아낄 수 있는 것이 대부분이다
  • 앞부분이 얼마나 자주 반복되는가. 한 번만 오는 앞부분에는 캐시가 쌓일 기회가 없다

system prompt 가 길고 질문이 짧은 서비스에서 효과가 가장 크다. 반대로 매번 다른 긴 문서를 넣는 서비스에서는 라우팅을 캐시 기준으로 잡아도 얻는 것이 거의 없다.

prefix aware 와 KV cache aware 는 판단하는 것이 다르다

캐시를 보고 인스턴스를 고르는 방식이 둘 있고, 이름이 비슷해 섞이기 쉽다.

방식판단
prefix aware같은 앞부분이면 항상 같은 인스턴스로 보낸다. 그 캐시가 이미 밀려났어도
KV cache aware실제 적중률이 가장 높은 인스턴스로 보낸다

차이가 나는 지점은 캐시가 밀려난 뒤다.

KV cache 공간은 유한하다. 새 요청이 들어오면 오래된 블록이 제거된다. prefix aware 는 그 사실을 모르고 계속 같은 인스턴스로 보낸다. 그 인스턴스에 캐시가 없어도 보내고, 옆 인스턴스가 같은 앞부분을 우연히 갖고 있어도 그쪽으로 보내지 않는다.

prefix aware 는 해시 기반 분산이고, KV cache aware 는 상태를 조회하는 분산이다. 앞의 것은 판단이 싸고 상태 조회가 필요하지 않다. 뒤의 것은 정확하지만 인스턴스 상태를 알아야 한다. 앞 글에서 본 것과 같은 구조가 여기서도 반복된다. 정밀도를 올리는 판단이 요청 경로에 조회를 얹는다.

llm-d 의 구현

llm-d 는 Gateway API Inference Extension 과 External Processing Pod 로 이것을 구현한다. Kubernetes Gateway 뒤에 별도 Pod 를 두고 그 Pod 가 라우팅 판단을 내리는 구조다.

판단 순서가 둘로 나뉘어 있다.

  1. 강한 prefix 일치가 있으면 그 앞부분을 가진 replica 로 보낸다
  2. 강한 일치가 없으면 vllm:kv_cache_usage_perc 를 보고 여유가 가장 많은 replica 로 보낸다

두 번째 단계가 이 설계의 핵심이다. 캐시로 고를 근거가 없을 때 아무 곳으로 보내지 않고, 앞으로 캐시가 쌓일 여유가 가장 많은 곳으로 보낸다. 지금의 적중이 아니라 다음 요청의 적중을 만드는 판단이다.

vLLM production-stack 도 같은 문제를 다루고, 공식 문서에 KV Cache Aware Routing 사용 사례가 있다.

session affinity 로는 절반만 해결된다

캐시 기준 라우팅을 처음 만들 때 가장 먼저 떠오르는 것이 session affinity 다. 세션 식별자나 사용자 식별자를 해시해 같은 인스턴스로 보내는 방식이고, 웹 백엔드에서 오래 써온 sticky session 과 같은 발상이다.

같은 대화가 이어지는 서비스에서는 이것만으로도 상당히 맞는다. 대화 이력이 앞부분을 이루므로 같은 세션은 같은 앞부분을 갖는다.

한계가 셋 있다.

  • 여러 사용자가 같은 앞부분을 공유하는 경우를 놓친다. 긴 system prompt 는 모든 사용자에게 같다. 사용자별로 흩어 보내면 그 공통 부분의 캐시가 인스턴스마다 중복으로 쌓인다
  • 세션 길이가 고르지 않다. 긴 대화를 하는 사용자가 한 인스턴스에 붙으면 그 인스턴스만 포화된다. 캐시 적중은 오르는데 대기 시간이 늘어난다
  • 인스턴스 수가 바뀌면 해시 결과가 달라진다

인스턴스를 늘리거나 줄이면 적중이 무너진다

마지막 항목이 운영에서 가장 자주 걸린다.

인스턴스 수로 나눈 나머지를 쓰는 단순 해시는 인스턴스가 하나 늘거나 줄 때 거의 모든 키의 목적지가 바뀐다. 목적지가 바뀌면 그 앞부분의 캐시는 새 인스턴스에 없고, prefill 을 처음부터 다시 한다.

오토스케일이 캐시 적중을 깨뜨리는 구간이 여기다. 부하가 올라 인스턴스를 늘리면, 늘린 직후에 적중률이 떨어져 prefill 부하가 오히려 늘어난다. 새로 뜬 인스턴스가 캐시를 채울 시간도 필요하다.

대응을 셋으로 정리하면 이렇게 된다. 근거 문서에서 옮긴 것이 아니라 위의 사실들에서 끌어낸 것이므로, 실제 효과는 자기 트래픽에서 재야 한다.

대응동작
consistent hashing인스턴스가 바뀔 때 목적지가 바뀌는 키를 일부로 제한한다
KV cache aware 로 전환목적지를 해시로 정하지 않으므로 인스턴스 수가 바뀌어도 기존 목적지가 흔들리지 않는다
증감 속도를 늦춘다캐시가 채워질 시간을 준다. 부하 급증에 늦게 반응하는 대가를 치른다

앞의 둘은 지금 요청의 목적지를 다루고, 마지막은 캐시가 채워지는 속도를 다룬다. 어느 쪽도 새로 뜬 인스턴스에 캐시가 없다는 사실 자체를 없애지는 못한다. 증감이 잦으면 그 손실이 계속 발생한다.

인스턴스를 줄일 때도 같은 문제가 있다. 줄인 인스턴스가 갖고 있던 캐시는 사라지고, 그 앞부분을 쓰던 요청은 다른 인스턴스에서 prefill 을 다시 한다.

적중률을 어떻게 재는가

vLLM 은 prefix cache 적중률을 지표로 주지 않는다. counter 둘로 직접 구한다.

지표종류뜻
vllm:prefix_cache_queriescounterprefix cache 를 조회한 토큰 수 누적
vllm:prefix_cache_hitscounter재사용된 토큰 수 누적
promql
rate(vllm:prefix_cache_hits[5m]) / rate(vllm:prefix_cache_queries[5m])

counter 를 그대로 나누면 서버 시작 이후 전체 평균이 되어 최근 상태를 반영하지 못한다. rate() 로 구간 증가율을 먼저 구한다.

적중률이 낮을 때 의심할 것이 셋이다.

  • 요청 사이에 공통 앞부분이 거의 없는 작업이다. 이 경우 라우팅으로 해결되지 않는다
  • KV cache 공간이 작아 블록이 금방 밀려난다. vllm:kv_block_lifetime_seconds 와 vllm:kv_block_idle_before_evict_seconds 로 확인한다
  • 여러 인스턴스로 띄웠는데 라우팅이 캐시를 보지 않아 같은 앞부분을 가진 요청이 흩어진다

마지막이 이 글의 주제다. 그런데 몇 % 이하면 문제인지에 대한 공식 기준은 없다. 서비스의 요청 분포에 따라 도달 가능한 최대치가 다르므로, 라우팅을 바꾸기 전과 후를 같은 트래픽에서 비교하는 편이 낫다.

지표를 라우팅 판단에 실제로 쓰는 방법과 다른 지표들과의 관계는 vLLM 지표를 라우팅 판단에 쓰는 방법에서 다룬다.

정리

  • 위 계층의 제공자 prompt caching 은 비용 문제이고, 아래 계층의 prefix cache 는 지연 문제다. 둘을 섞으면 판단이 어긋난다.
  • prefix cache 가 아끼는 것은 prefill 이므로 효과가 TTFT 에만 나타난다. system prompt 가 길고 질문이 짧은 서비스에서 효과가 가장 크다.
  • prefix aware 는 해시로 목적지를 정하고 캐시가 밀려난 것을 모른다. KV cache aware 는 인스턴스 상태를 조회하므로 정확하지만 요청 경로에 조회가 얹힌다.
  • llm-d 는 강한 prefix 일치가 없을 때 KV cache 여유가 가장 많은 replica 로 보낸다. 지금의 적중이 아니라 다음 요청의 적중을 만드는 판단이다.
  • session affinity 는 여러 사용자가 공유하는 앞부분을 놓치고, 인스턴스 수가 바뀌면 목적지가 달라진다.
  • 오토스케일 직후에 적중률이 떨어져 prefill 부하가 오히려 늘어난다. consistent hashing 이나 KV cache aware 로 완화한다.
  • 적중률은 vLLM 이 지표로 주지 않아 counter 둘로 직접 구한다. 공식 기준값이 없으므로 같은 트래픽에서 전후를 비교한다.

다음 편

다음 글에서는 라우팅이 응답 중간에 막히는 구간을 다룬다. 첫 토큰이 사용자에게 나간 뒤에는 다른 모델로 재시도할 수 없고, 그 제약이 HTTP 와 모델 두 층에서 온다. 그때 남는 선택지가 셋이고, SSE 로 오류를 알리는 방식에 공식 규격이 없다는 것이 그 글의 주제다.

첫 토큰이 나간 뒤에는 재시도할 수 없다로 이어진다.

참고 링크

  • vLLM production-stack KV Cache Aware Routing
  • vllm-project/production-stack
  • llm-d Precise Prefix Cache Aware Routing
  • Red Hat. KV cache aware routing with llm-d
  • TrueFoundry. KV cache routing
  • DigitalOcean. cache-aware inference router
  • vLLM Metrics 설계
  • vLLM Forums. prefix cache hit rate
on this page
  • 01캐시라는 한 단어가 두 계층에서 다른 것을 가리킨다
  • 02prefill 과 decode 가 캐시로 갈린다
  • 03prefix aware 와 KV cache aware 는 판단하는 것이 다르다
  • llm-d 의 구현
  • 04session affinity 로는 절반만 해결된다
  • 05인스턴스를 늘리거나 줄이면 적중이 무너진다
  • 06적중률을 어떻게 재는가
  • 07정리
  • 08다음 편
  • 09참고 링크
tags
#Model Router#LLM 서빙#KV cache#study📚 Model Router
← PREVIOUS모델을 골라주는 계층이 대신 가져오는 제약NEXT →첫 토큰이 나간 뒤에는 재시도할 수 없다

이런 글도

  • mlops

    같은 문제를 회사마다 다른 층에서 푼다

    2026.09.08
  • mlops

    첫 토큰이 나간 뒤에는 재시도할 수 없다

    2026.09.08
  • mlops

    어느 모델로 보낼지 무엇으로 정하는가

    2026.09.08
  • mlops

    모델을 골라주는 계층이 대신 가져오는 제약

    2026.09.08

댓글 (0)