SGLang RadixAttention KV Cache Hit Rate: Prometheus 모니터링과 운영 튜닝으로 히트율을 3%에서 78%로 올린 방법 — 심화편
LLM 서빙 인프라를 운영하다 보면 어느 순간 GPU 비용이 감당하기 어려운 수준에 달합니다. 저도 멀티턴 챗봇 서비스를 운영하던 시절, 요청마다 동일한 시스템 프롬프트를 전부 다시 계산하고 있다는 걸 뒤늦게 깨달았습니다. 알고 보니 SGLang의 RadixAttention이 이미 그 문제를 해결해두었는데, 설정 하나를 빠뜨려서 혜택을 전혀 못 받고 있었던 거죠. --enable-metrics를 켜고 실제 히트율을 처음 봤을 때 충격이었습니다. 3%였거든요. 프롬프트 구조를 고치고 캐시 인식 라우팅을 적용하고 나서는 78%까지 올라갔습니다.
대상은 SGLang을 이미 어느 정도 써본 분들입니다. "KV 캐시가 뭔지는 아는데, 히트율이 낮게 나오는 이유를 모르겠다"거나 "HiCache나 Cache-Aware Load Balancer 같은 용어를 들어봤지만 언제 쓰는지 감이 안 잡힌다"는 분들께 특히 도움이 됩니다. RadixAttention의 prefix 재사용률을 Prometheus로 모니터링하고, KV cache hit rate를 운영 환경에서 실제로 끌어올리는 튜닝 방법을 이 글에서 풀어봅니다.
핵심 개념
RadixAttention이 KV 캐시를 재사용하는 방식
기존 LLM 서빙 프레임워크는 요청마다 프롬프트 전체를 처음부터 prefill합니다. 동일한 시스템 프롬프트를 100번 보낸다면, GPU는 그걸 100번 고스란히 계산하죠. RadixAttention은 이 비효율을 Radix Tree(기수 트리) 자료구조로 해결합니다.
트리의 각 노드는 토큰 시퀀스를 엣지 레이블로 보유하고, GPU 메모리 내 KV 캐시 텐서를 가리키는 **포인터(참조)**를 값으로 관리합니다. 노드 안에 텐서 전체가 내장된 게 아니라, GPU 메모리 어디에 그 텐서가 있는지를 알고 있는 구조입니다. 새 요청이 들어오면 루트부터 순회하며 최장 공통 prefix를 찾아냅니다. 일치하는 구간은 GPU 메모리에 이미 있으니 prefill 연산을 건너뜁니다. 캐시가 꽉 차면 LRU(기본값) 정책으로 가장 최근에 쓰이지 않은 리프 노드부터 축출합니다.
[루트]
└── "당신은 친절한 AI 어시스턴트입니다. [1024 tokens]" ← 시스템 프롬프트 노드 (캐시됨)
├── "안녕하세요, 오늘 날씨..." ← 대화 A
└── "파이썬 코드 리뷰 부탁..." ← 대화 B동일한 시스템 프롬프트로 시작하는 모든 요청은 루트 → 시스템 프롬프트 노드까지 히트가 발생하고, 그 이후의 대화 분기만 새로 계산합니다.
KV 캐시 히트율(cache hit rate): 전체 prefill 토큰 중 RadixAttention으로 재사용된 토큰의 비율입니다. 이 값이 높을수록 prefill 연산이 줄어들고, TTFT(Time To First Token) — 요청 전송 시점부터 모델이 첫 토큰을 출력하기까지 걸리는 시간 — 가 단축됩니다.
prefix 재사용이 효과적인 워크로드 패턴
솔직히 모든 워크로드에 RadixAttention이 도움이 되는 건 아닙니다. 완전히 새로운 질문이 매번 들어오는 검색형 서비스라면 히트율이 0에 가깝게 나올 수 있습니다. 반면 다음 패턴에서는 효과가 극적입니다.
| 워크로드 | prefix 중복 형태 | 기대 히트율 |
|---|---|---|
| 멀티턴 대화 | 대화 히스토리 누적 | 75~95% |
| 동일 시스템 프롬프트 공유 에이전트 | 긴 시스템 프롬프트 | 60~90% |
| RAG 파이프라인 | 동일 컨텍스트 문서 반복 | 50~80% |
| Few-shot 프롬프트 | 예제 블록 공유 | 40~70% |
| 완전 랜덤 단발성 질문 | 없음 | ~0% |
Prometheus 메트릭으로 히트율 읽기
SGLang은 --enable-metrics 플래그를 활성화하면 Prometheus 형식으로 메트릭을 노출합니다. KV 캐시 히트율 게이지 이름이 버전마다 다른데, SGLang v0.3.x 이하에서는 sglang:cache_hit_rate(콜론 구분자), **v0.4.x 이상에서는 sglang_cache_hit_rate(언더스코어)**를 사용합니다. grep 결과가 비어 있다면 두 형식을 모두 확인해보시면 좋습니다.
# 서버 실행 시 메트릭 엔드포인트 활성화
python -m sglang.launch_server \
--model-path meta-llama/Llama-3-70B-Instruct \
--enable-metrics \
--port 30000# 현재 히트율 즉시 확인 (두 형식 모두 검색)
curl http://localhost:30000/metrics | grep -E "cache_hit_rate"Grafana를 쓰고 있다면 SGLang 레포의 examples/monitoring/grafana.json을 그대로 임포트할 수 있습니다. 기본 대시보드가 이미 잘 구성되어 있어서 처음부터 만들 필요가 없습니다.
실전 적용
Prometheus + Grafana 모니터링 파이프라인 구성
가장 먼저 할 일은 히트율이 눈에 보이는 상태를 만드는 겁니다. 어두운 곳에서 운전할 수는 없으니까요.
# prometheus.yml — SGLang 스크레이프 설정
scrape_configs:
- job_name: 'sglang'
static_configs:
- targets: ['localhost:30000']
metrics_path: '/metrics'
scrape_interval: 15s아래는 히트율 저하 알람을 위한 Prometheus Alertmanager 규칙입니다. Grafana Unified Alerting(v8+)은 이 YAML 형식을 직접 지원하지 않으며, Grafana에서 알람을 설정하려면 Grafana Provisioning API나 UI를 통해 별도로 구성해야 합니다.
# prometheus/alerting_rules.yml — Prometheus Alertmanager 규칙
groups:
- name: sglang_cache
rules:
- alert: LowCacheHitRate
expr: sglang_cache_hit_rate < 0.1
for: 15m
labels:
severity: warning
annotations:
summary: "SGLang KV 캐시 히트율이 10% 미만 (현재: {{ $value | humanizePercentage }})"| 설정 항목 | 값 | 설명 |
|---|---|---|
scrape_interval |
15s | 15초마다 메트릭 수집 |
for: 15m |
15분 | 일시적 급락과 지속적 저하 구분 |
| 알람 임계값 | 0.1 (10%) | prefix 중복 워크로드 기준 최소선 |
히트율이 0.3 미만으로 15분 이상 지속된다면 뭔가 잘못된 겁니다. 아래에서 진단 방법을 다룹니다.
멀티턴 챗봇 — 히트율을 최대로 끌어올리는 서버 설정
히트율 최적화에서 가장 중요한 포인트는 프롬프트 구조입니다. 저도 처음엔 헷갈렸는데, RadixAttention이 자동으로 공통 prefix를 찾아주긴 하지만 그 구조를 어떻게 배치하느냐에 따라 효과가 크게 달라집니다.
# 권장하는 프롬프트 구조
# 1. 시스템 프롬프트 (불변, 항상 맨 앞에)
# 2. 대화 히스토리 (prefix로 누적)
# 3. 새 사용자 메시지 (맨 뒤)
messages = [
{"role": "system", "content": SYSTEM_PROMPT}, # 절대 변경 금지
{"role": "user", "content": turn_1_user},
{"role": "assistant", "content": turn_1_assistant},
{"role": "user", "content": turn_2_user},
# ...
{"role": "user", "content": new_message}, # 새 입력
]# 최적화된 서버 기동 파라미터
python -m sglang.launch_server \
--model-path meta-llama/Llama-3-70B-Instruct \
--enable-metrics \
--chunked-prefill-size 4096 \
--mem-fraction-static 0.85 \
--port 30000| 파라미터 | 값 | 역할 |
|---|---|---|
--enable-metrics |
— | Prometheus 메트릭 노출 |
--chunked-prefill-size |
4096 | 긴 prefill을 청크로 나눠 메모리 효율 향상 |
--mem-fraction-static |
0.85 | KV 캐시에 GPU VRAM의 85% 할당 |
시스템 프롬프트에 공백 한 자, 개행 하나만 달라져도 캐시 전체가 미스됩니다. 타임스탬프, 사용자 ID, 날짜처럼 요청마다 달라지는 값이 시스템 프롬프트 앞부분에 들어가는 순간, 그 뒤 내용 전체가 캐시되지 않습니다. 변동 값은 반드시 대화 마지막 사용자 메시지에 배치하는 것을 권장합니다.
히트율 저하 진단 흐름
히트율이 갑자기 떨어졌을 때 어디서부터 봐야 하는지, 실무에서 자주 맞닥뜨리는 상황인데 아래 순서로 확인해보시면 좋습니다.
히트율 < 0.3 감지
│
▼
[1단계] 시스템 프롬프트 드리프트 확인
→ 최근 배포에서 프롬프트 변경 있었는지?
→ whitespace, 개행, 인코딩 차이 없는지?
│
▼ 문제 없으면
[2단계] 라우팅 확인 (다중 노드 배포)
→ Cache-Aware Load Balancer 설정되어 있는지?
→ 동일 prefix가 다른 노드로 분산되고 있는지?
│
▼ 문제 없으면
[3단계] 워크로드 자체의 prefix 중복도 분석
→ 실제로 중복 prefix가 있는 트래픽인지?
→ 완전히 랜덤한 단발성 요청 비중이 높아진 건 아닌지?# 메트릭에서 추가 지표 함께 확인
curl -s http://localhost:30000/metrics | grep -E "(cache_hit|num_cached|num_running)"
# 주요 지표
# sglang_cache_hit_rate — 현재 히트율
# sglang_num_cached_tokens — 캐시에 올라있는 토큰 수
# sglang_num_running_reqs — 현재 처리 중인 요청 수다중 노드 배포에서 Cache-Aware Load Balancer 적용
저는 단일 노드에서는 히트율이 잘 나오다가 수평 스케일아웃 이후 히트율이 반 토막 났던 경험이 있습니다. 각 노드가 독립적인 KV 캐시를 보유하기 때문에, 라운드로빈으로 트래픽을 분산하면 동일한 prefix가 매번 다른 노드에 떨어져 히트가 발생하지 않습니다.
SGLang v0.4부터는 Cache-Aware Load Balancer가 내장되어 있습니다. prefix의 해시 값을 기준으로 동일한 prefix를 가진 요청을 동일 워커로 라우팅하는 방식입니다.
아래 예시는 단일 호스트에서 GPU 2개를 Data Parallelism으로 묶는 경우입니다. --dp-size 2는 두 개의 데이터 병렬 워커를 같은 프로세스 내에서 생성하며, --load-balance-method cache_aware가 요청을 prefix 해시 기준으로 두 워커 중 하나에 분배합니다.
# 단일 호스트, GPU 2개 — Cache-Aware DP 설정
python -m sglang.launch_server \
--model-path meta-llama/Llama-3-70B-Instruct \
--enable-metrics \
--dp-size 2 \
--load-balance-method cache_aware \
--port 30000별도의 물리 노드 2대를 묶는 경우라면 각 노드에서 SGLang 서버를 개별 기동하고, 앞단에 SGLang Router(python -m sglang.srt.router)를 두어 cache_aware 방식으로 트래픽을 분산시킵니다. 외부에서는 Router 엔드포인트 하나만 바라보면 되고, 각 노드의 주소는 Router 설정에 등록합니다.
공식 벤치마크(LMSYS 블로그 기준)에 따르면 이 설정 하나로 처리량 1.9배, 캐시 히트율 3.8배 향상을 달성한 사례가 있습니다. 설정 대비 효과가 상당히 좋은 편입니다.
HiCache로 캐시 용량 계층 확장
GPU VRAM만으로 감당이 안 되는 규모로 성장했다면 HiCache를 고려해볼 수 있습니다. GPU VRAM(L1) → Host RAM(L2) → 분산 스토리지(L3)의 3단 계층으로 캐시를 확장합니다.
# HiCache 활성화 (Host RAM 계층 추가)
python -m sglang.launch_server \
--model-path meta-llama/Llama-3-70B-Instruct \
--enable-metrics \
--enable-hierarchical-cache \
--hicache-ratio 4.0 \
--mem-fraction-static 0.80 \
--port 30000| 파라미터 | 값 | 의미 |
|---|---|---|
--enable-hierarchical-cache |
— | HiCache 계층 캐싱 활성화 |
--hicache-ratio |
4.0 | Host RAM KV 캐시 크기 = GPU VRAM KV 캐시 × 4 |
--mem-fraction-static |
0.80 | HiCache 사용 시 GPU 할당을 약간 줄임 |
--hicache-ratio 4.0이 실제로 어떤 의미인지 구체적으로 보면, GPU VRAM 80GB 서버에서 --mem-fraction-static 0.80을 적용하면 KV 캐시에 약 64GB가 할당됩니다. 여기에 --hicache-ratio 4.0을 적용하면 Host RAM에 추가로 **256GB(64GB × 4)**를 L2 캐시로 사용하게 됩니다. 서버 Host RAM이 GPU VRAM 대비 충분히 클수록 — 보통 4배 이상 — 효과를 제대로 볼 수 있습니다.
Alibaba Cloud Tair와의 협업 사례(공식 블로그 기준)에서는 코딩 에이전트 워크로드(세션당 평균 8턴, 25K+ 토큰)에 HiCache를 적용해 TTFT 56% 감소, 처리량 2배, 히트율 40% → 80% 향상을 기록했습니다.
# KV 캐시 FP8 양자화로 같은 VRAM에 더 많은 엔트리 유지
python -m sglang.launch_server \
--model-path meta-llama/Llama-3-70B-Instruct \
--kv-cache-dtype fp8 \
--enable-metrics \
--port 30000장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 자동 최적화 | 수동 프롬프트 캐싱 코드 없이 런타임에 자동으로 캐시 기회 탐지 |
| 다양한 패턴 지원 | Few-shot, 브랜칭 추론 트리, 멀티턴 대화, RAG 모두 커버 |
| 계층 확장성 | HiCache로 GPU VRAM 한계를 Host RAM, 분산 스토리지까지 확장 |
| 측정 가능한 성능 향상 | prefix 중복 60% 이상 워크로드에서 TTFT 최대 80% 감소, 처리량 최대 6배 |
| 기존 인프라 통합 용이 | Prometheus/Grafana 표준 스택으로 바로 연결 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 캐시 무효화 민감성 | 시스템 프롬프트 공백 하나 변경으로 전체 캐시 미스 | 프롬프트를 코드처럼 버전 관리, 배포 시 diff 확인 |
| 랜덤 워크로드 비효과 | prefix 중복이 낮은 요청에서는 효과 없음 | 워크로드 특성 분석 후 적용 여부 결정 |
| 다중 노드 라우팅 필수 | 라운드로빈 분산 시 히트율 급락 | Cache-Aware Load Balancer 설정 |
| 메모리 튜닝 필요 | --mem-fraction-static과 --hicache-ratio 조합이 부정확하면 OOM |
워크로드에 맞게 점진적으로 조정 |
| 투기적 디코딩 버그 | Speculative Decoding 사용 시 cached_tokens가 0으로 오보고 |
GitHub Issue #20451 (2025년 5월 기준 오픈 상태), 해당 조합 일시 회피 권장 |
투기적 디코딩(Speculative Decoding): 작은 드래프트 모델로 여러 토큰을 빠르게 추측하고 원본 모델로 검증하는 추론 가속 기법입니다. SGLang에서 RadixAttention과 함께 쓸 때
cached_tokens메트릭이 0으로 잘못 보고되는 버그가 알려져 있습니다(GitHub Issue #20451). 직접 이슈를 확인해봤는데 글 작성 시점(2025년 5월)에 아직 열려 있는 상태라, 히트율 모니터링이 중요한 환경에서는 이 조합을 일시적으로 피하는 것을 권장합니다.
실무에서 가장 흔한 실수
-
프롬프트 템플릿에 동적 값을 앞부분에 넣는 경우 — 날짜, 사용자 ID 등 변동 값이 시스템 프롬프트 앞에 들어가면 그 뒤 내용 전체가 캐시되지 않습니다. 변동 값은 대화 마지막 사용자 메시지에 배치하는 것을 권장합니다.
-
--enable-metrics없이 히트율을 짐작으로 판단하는 경우 — "잘 되고 있겠지"라고 생각했다가 오랫동안 0%로 운영되는 경우가 생각보다 많습니다. 수치를 보는 것만으로도 다음 행동이 명확해집니다. -
다중 노드 배포 후 캐시 인식 라우팅을 적용하지 않는 경우 — 수평 스케일아웃을 했는데 히트율이 오히려 떨어졌다면 이 경우가 대부분입니다. Cache-Aware Load Balancer는 다중 노드에서 선택이 아닌 필수입니다.
마치며
RadixAttention의 효과는 "설정"이 아니라 "측정 → 진단 → 조정"의 사이클에서 나옵니다. 히트율이 눈에 보이는 상태를 먼저 만들어야 나머지 최적화가 의미 있어집니다. 세 가지 중 하나만 한다면 메트릭부터 켜는 것이 맞습니다. 그 수치 하나가 다음 행동을 결정합니다.
지금 바로 시작해볼 수 있는 3단계:
- 지금 바로
--enable-metrics를 켜보세요.curl http://localhost:30000/metrics | grep cache_hit_rate로 현재 히트율을 확인해보시면 됩니다. 수치를 보는 것만으로도 다음 행동이 명확해집니다. - 히트율이 기대보다 낮다면 프롬프트 구조를 점검해볼 수 있습니다. 시스템 프롬프트가 고정되어 있는지, 타임스탬프나 랜덤 값이 앞부분에 섞여 있진 않은지 확인해보시면 좋습니다.
- 다중 노드 환경이라면
--load-balance-method cache_aware옵션 적용을 권장합니다. 설정 한 줄로 히트율이 눈에 띄게 달라지는 걸 확인하실 수 있습니다. 그다음 단계로 HiCache나 FP8 양자화를 검토해보시면 됩니다.
참고 자료
- Fast and Expressive LLM Inference with RadixAttention and SGLang | LMSYS Blog
- RadixAttention | SGLang 공식 문서
- SGLang HiCache: Fast Hierarchical KV Caching | LMSYS Blog
- Production Metrics | SGLang 공식 문서
- HiCache System Design and Optimization | SGLang 공식 문서
- SGLang v0.4: Cache-Aware Load Balancer | LMSYS Blog
- SGLang Prometheus Metrics: A Guide for Production Monitoring | kuncoro.io
- SGLang Production Deployment Guide: RadixAttention | Spheron Blog
- SGLang Prometheus Metrics | NVIDIA Dynamo 문서
- Alibaba Cloud Tair Partners with SGLang to Build HiCache | Alibaba Cloud
- SGLang: Efficient Execution of Structured Language Model Programs | arXiv