LLM 추론 서빙 엔진 SGLang 완전 정복
이 글을 읽기전에 여러가지 질문들을 던지고 시작하겟다
- DeepSeek는 왜 KV 캐시를 압축하는데 그렇게 집작했을까요?
- MTP(다중 토큰 예측)로 학습하면 실제로 어디서 이득이 날까용?
- GLM-5.2의 수용길이 4.56 -> 5.47은 사용자 입장에서 몇 밀리초 차이일까?
- 1M 컨텍스트가 광고용이 아니라 실용적이라는 말은 무엇을 측정한 것일까?
이 질문들의 답은 전부 추론 서빙 inference serving 층에 있다.
모델 아키텍처는 서빙의 제약 조건에 맞춰 역설계된것이고, 서빙 엔진은 그 아키텍처를 하드웨어 위에서 실현하는 층이다.
두 층을 같이 봐야 비로소 왜 그렇게 만들었는지가 이해할 수 있다.
실습 - 이 글에서 실습은 가상의 시나리오를 가정하고 H200 8장 짜리 노드를 지금 당장 만질 수 있는 사람들은 드물겠지만, 그 환경에서 일하는 것이 목표라면 그 환경에서 무엇을 결정하는가를 미리 훈련해두는 것이 맞다. 명령어와 설정값은 실제 그대로 적되, 결과는 공개 벤치마크와 벤더 리포트의 실측치를 인용한다.
| 사례 | 상황 | 하드웨어 | 핵심 고민 |
|---|---|---|---|
| A. 사내 코딩 어시스턴트 | 엔지니어 200명, 대화형 | 8×H200 (1노드) | 지연 시간 (TTFT) |
| B. 문서 QA / RAG 서비스 | 동일 문서를 반복 질의 | 4×H100 | 접두사 재사용 |
| C. 대형 MoE 대규모 서빙 | DeepSeek V4급, 트래픽 폭주 | 12노드 / 96 GPU | 처리량과 원가 |
서빙 엔진 #
왜 별도의 엔진이 필요한가??
Hugging Face transformers로도 모델은 돌릴 수 있다.
그렇지만 이 방식을 실제로 돌려보면 사용자가 여덟명만 동시에 붙어도 무너지게 되는것을 확인할 수 있다.
왜냐면 transformers의 기본 생성 루트는 이렇게 생겼다.
요청을 하나 받는다 -> 처음부터 끝까지 생성한다 -> 다음 요청을 받는다
GPU 관점에서 보면 8차선 고속도로에서 차 한대만 달리는 상태인 것인데, GPU는 수천개의 코어로 병렬 계산을 하도록 만들어졌지만, 요청 하나만 처리하면 그 병렬성을 거의 못쓴다.
실측으로 GPU 활용률이 30~40%에 머무른다.
서빙엔진은 이 고속도로를 꽉 채우는 일을 한다. 그게 전부이고, 그게 전부인데도 어렵다.
2026. 8 기준 현재의 지형도 #
[ 사용 규모 ]
개인 ──────────────────────────────────────→ 초대형
llama.cpp vLLM SGLang TensorRT-LLM
Ollama (범용 기본값) (프리픽스/에이전트) (NVIDIA 극한)
│ │ │ │
단일 사용자 가장 넓은 하드웨어 RadixAttention 컴파일 필요
CPU 가능 pip 한 줄 설치 PD분리 + 대규모EP Blackwell 최적
| 엔진 | 개발 주체 | 성격 | 2026.6 기준 버전 |
|---|---|---|---|
| vLLM | UC 버클리 Sky Computing Lab 출신 | 범용 기본값. PagedAttention 원조 | 0.23.0 |
| SGLang | LMSYS / xAI 계열 연구자 | 구조화 실행 특화. RadixAttention | 0.5.13 |
| TensorRT-LLM | NVIDIA | NVIDIA 전용 극한 최적화. 엔진 사전 컴파일 | 1.2.1 |
| LMDeploy | 상하이 AI Lab | 중국계 모델 지원이 빠름 | — |
| llama.cpp / Ollama | 커뮤니티 | 개인용, CPU/Mac 가능 | — |
| TGI | Hugging Face | 2025년 12월 유지보수 모드 진입 | (신규 도입 비권장) |
들어가기 앞서 알아둘것. vLLM, SGLang, TensorRT-LLM은 연속 배칭, Paged KV Cache, FP8 양자화를 모두 구현하고 있어서 표면적인 기능 격차는 완전히 사라졌다. 2024에는 vLLM에선 이게 되고 SGLang에서는 이게 됨. 등 비교 의미가 있었지만 지금은 선택 기준이 기능이 아니라 아키텍처 철학과 하드웨어다.
최적화 스택을 고를때 하드웨어가 첫 번째 필터가 되는 것인데 SGLang은 NVIDIA, AMD, Intel Xeon, Google TPU, Ascend NPU 등을 공식 지원한다고 밝히고 있다. TensorRT-LLM은 설계상 NVIDIA전용으로 CUDA엔진을 컴파일해 blackwell급 (GB200, GB300)에 최적화 되어있다.
AMD MI300이나 Ascend를 쓸 가능성이 조금이라도 있다면 TensorRT-LLM은 제외하고 보도록하자.
워크로드가 두 번째 필터가 된다. 본 글은 SGLang을 중점적으로 팔건데 여기서 SGLang의 존재 이유가 나온다. 워크로드.
SGLang은 무엇이 다른가? #
대부분의 서빙 스택은 모든 요청이 서로 독립적이다를 전제하고 단순 챗 API에게는 맞는 가정이지만 2026년의 실제 워크로드는 그렇지 않다.
- 멀티턴 대화 -> 앞턴 전체가 다음 요청의 접두사라면?
- RAG -> 같은 문서가 수십 개 질문에 반복 등장
- 에이전트 -> 동일한 시스템 프롬프트 + 도구 정의가 매 호출마다 반복
- 자기일관성 샘플링 / 트리 탐색 -> 같은 프롬프트에서 여러 갈래로 분기
SGLang은 이 중복 1급을 1급 자원으로 취급한다. 요청이 끝나도 KV 캐시를 버리지않고 Radix Tree에 보관해서 다음 요청이 같은 접두사를 쓰면 계산을 통째로 건너뛰게 된다. 이게 밑에서 알아볼 RadixAttetnion이다.
공개 벤치마크에서 SGLang은 접두사 공유가 많은 워크로드 (RAG, 멀티턴 챗)에서 H100 Llama 3.1 8B 기준 vLLM 대비 약 29% 높은 처리량을 보였고 반대로 모든 요청이 완전히 고유한 워크로드에서는 이 이점이 0이라 이 조건부성이 SGLang의 가치를 이해하는 핵심이다.
규모 감각 #
SGLang은 xAI, NVIDIA, AMD, Google Cloud, Oracle Cloud, LinkedIn, Cursor등이 채택하고있고 매일 수조개의 토큰을 프로덕션에서 생성하고있을정도로 체급이 크다.
성능의 물리학 #
핵심 사실 세 가지를 먼저 보자
- LLM 디코딩은 계산이 부족해서 느린 게 아니라 메모리에서 가중치를 읽어오느라 느리다
- 따라서 배치를 키우면 거의 공짜로 처리량이 올라간다.
- 배치를 키우는 걸 막는것은 KV캐시가 먹고있는 메모리다.
이 세 문장에서 정의한 문제가 서빙 엔진 전체 존재 이유인 것이다.
PD (Prefill, Decode) #
LLM이 답변을 생성하는 과정은 하나의 균질한 작업처럼 보이지만, 실제로는 성격이 완전히 다른 두 단계로 나뉜다.
[프리필 Prefill]
입력: "다음 코드를 리뷰해줘: ..." (2,000 토큰)
↓
2,000개 토큰을 한꺼번에 모델에 통과시킴
↓
2,000개 토큰 각각의 K, V를 계산해서 KV 캐시에 저장
↓
첫 번째 출력 토큰 하나 생성
[디코드 Decode]
1번째 출력 토큰 → 모델 통과 → 2번째 토큰
2번째 출력 토큰 → 모델 통과 → 3번째 토큰
... (500번 반복)
핵심 차이는 한 번의 모델 통과당 처리하는 토큰 수이다.
- Prefill은 한 번에 2,000토큰 -> 병렬 처리가 가능하다.
- Decode는 한 번에 1토큰씩 -> 순차, 병렬 처리 불가
여기서 GPU의 본성이 들어나는데 GPU는 행렬 곱셈을 병렬로 하는 기계다.
프리필에서는 2000 x 5120 크기의 행렬이 가중치와 행렬곱되고 큰행렬 x 큰행렬이라 GPU가 좋아하는 일이다.
디코드는 1 x 5120 벡터가 가중치 행렬과 곱해지고 가중치는 똑같이 전부 읽어야하는데 그 가중치로 하는 계산량은 2,000분의 1인 것이다.
거대한 사전을 통째로 책상위에 올려놓는데 10분이 걸린다고 치자 Prefill은 사전에 한번 올려놓고 2000개의 단어를 한번에 찾는일이고 단어당 0.3초. Decode는 사전을 올려놓고 단어 하나만 찾고 치우고 다음 단어를 위해 또 올리는 일이다. 단어당 10분.
- Prefill은: compute-bound GPU의 텐서 코어가 포화되는 계산 집약적 작업
- Decode은: 메모리 대역폭 집약적 memory-bound고 배치 1의 디코드 산술 강도는 H100 루프라인상에서 1FLOP/Byte 근처이며 선형 어텐션 계열 아키텍처는 그보다 더 낮다
산술강도 arithmetic intensity는 메모리에서 1바이트를 읽어와서 몇 번의 연산을 하는가이다.
H100의 텐서코어는 대략 바이트당 수백 번의 연산을 소화할 수 있는데, 디코드는 1번밖에 안한다. GPU성능의 99% 이상이 놀고있다는 뜻이다.
이 사실에서 세 가지가 따라 나오는데
- 디코드 속도를 올리려면 계산을 줄이는 게 아니라 메모리 읽기를 줄여야한다 -> 양자화 FP8, INT4가 왜 그렇게 효과적인지 이유이다. 가중치를 절반 크기로 만들면 읽는 시간이 절반이 된다.
- 놀고있는 계산 자원을 공짜로 쓸 방법이 있다. -> 배칭과 투기적 디코딩을 아래에서 배울건데 여기서 나온다.
- 프리필과 디코드는 다른 자원을 소모하므로 분리하면 이득이다 -> PD 분리에서 다룬다.
KV 캐시 #
이 글에서 가장 중요한 단일 개념으로
디코드 단계에서 500번째 토큰을 생성할 때 어텐션은 앞선 2,499개의 토큰 K, V를 참조해야한다. 매번 다시 계산하면 같은 계산 500번을 반복하게 된다.
한번 계산한 K, V를 GPU 메모리에 저장해두고 재사용하는것이 KV 캐시이다.
크기 공식
KV 캐시 크기 = 2 × L × H_kv × D × S × B × (바이트/원소)
2 : Key와 Value 두 개
L : 레이어 수
H_kv : KV 헤드 수 (GQA면 쿼리 헤드보다 적음)
D : 헤드 차원
S : 시퀀스 길이 (컨텍스트)
B : 동시 처리 요청 수 (배치)
바이트 : BF16이면 2, FP8이면 1
직접 계산해보자 8 x H200 노드에 70B급 모델을 올렸다. 레이어 80개 KV 헤드 8개 헤드 차원 128, BF16 에서 사용자 한 명이 32K 컨텍스트를 쓸때 쓸때
2 x 80 x 8 x 128 x 32,768(32k) x 1 x 2 byte
= 2 x 80 x 8 x 128 x 32,768 x 2
= 10,737,418,240 바이트
≈ 10.7GB
사용자 한명당 10.7GB의 KV 캐시 크기 공간이 필요하게 된다 (띠용
H200 한장의 메모리는 141GB이고 8장이면 1,128GB고 모델 가중치가 BF16 70B는 140GB라고 할때, 988GB가 남게된다. 이를 아까구한 10.7GB로 나누면
988 GB ÷ 10.7 GB = 약 92명
엔지니어 200명이 동시에 32K 컨텍스트를 쓰면 절반은 대기해야한다.
이 계산에서 보이는 것들은 이 한 번의 계산이 여러 설계 설정을 한 번에 필요성을 느끼게 할수있다.
| 앞 노트의 기법 | 위 공식의 어느 항을 건드리나 | 효과 |
|---|---|---|
| GQA | Hkv를 줄임 | 헤드 8개 공유 → 캐시 1/8 |
| MLA (DeepSeek) | Hkv × D를 압축된 잠재 차원으로 대체 | 캐시 대폭 축소 |
| 선형 어텐션 / GDN (Qwen, Kimi) | S 의존성을 제거 (고정 크기 상태) | 캐시가 길이와 무관 |
| FP8 KV 캐시 | 바이트/원소를 2 → 1 | 캐시 절반 |
| DSA (희소) | 캐시 자체는 유지하되 읽는 양을 줄임 | 대역폭 절약 |
DeepSeek가 MLA에 집착한 이유가 이제 명확해진다. 위 계산에서 92명 수용량이 184명이 되면 같은 GPU로 두 배의 매출이 난다. 그게 SGLang이 96개의 H100으로 DeepSeek를 서빙했을때 출력 100만 토큰당 $0.20라는 원가 즉 공식 DeepSeek Chat API의 약 5분의 1을 만들어낸 배경이다.
KV캐시로 인한 대가로 KV 캐시를 없앨 수 없다 이다. 없애면 매 토큰마다 전체 재계산이라 훨 느려지기 때문에 관리를 잘 하는것만으로 유일한 길이고 그 관리기법이 필요하게 된다.
메모리 계층 - 왜 오프로딩만으로 안되는걸까 #
KV캐시가 GPU메모리에 다 안들어가면 CPU 메모리에 두면 되지 않을까? 라는 발상이 자연스럽게 나올 수 있고 실제로 쓰이는 기법이다. 하지만 대역폭 차이를 알고 써야한다.
계층은 세 단계로 활성 생성중인 뜨거운 KV 블록은 GPU HBM에 약 3.35TB/s 대역폭으로 두고, 완료되었지만 재사용 가능성이 있는 따뜻한 블록은 PCIe 5.0을 통해 약 63 GB/s의 CPU DRAM으로 차가운 블록은 7 GB/s의 NVMe로 보낸다.
GPU HBM ████████████████████████████████████ 3,350 GB/s (기준 100%)
CPU DRAM ▌ 63 GB/s (1.9%)
NVMe SSD ▏ 7 GB/s (0.2%)
PCIe 5.0은 HBM 대역폭에 2%에 불과한다 50GB짜리 KV캐시를 옮길때 HBM에서는 15밀리초면 되지만 CPU DRAM에서 가져오면 800밀리초가 걸린다.
캐시된 토큰이 새 토큰보다 압도적으로 많은 워크로드에서 이 병목은 치명적이다. 65K 토큰 문서에서 32 토큰 질문을 던지는 QA 시나리오에서 Llama-3.1-405B는 33GB를 PCIe로 전송해야하고, 여기에 500밀리초가 든다.
즉 KV 캐시를 오프로딩해서 재사용하면 프리필을 아낄 수 있다는 아이디어가 맡긴한데 전송시긴아 개느려서 재계산 시간보다 길면 손해다. 이 손익분기점을 아는것이 실무적 사고인 것
판단 기준은
재계산 비용 ≈ (토큰 수 × 모델 FLOPs) / GPU 연산 성능
전송 비용 ≈ (KV 캐시 바이트) / 링크 대역폭
전송이 더 싸면 → 오프로딩 이득
재계산이 더 싸면 → 그냥 다시 계산 (vLLM의 preemption 기본 동작이 이것)
실제로 vLLM은 선점된 시퀀스에 대해 재계산과 스왑 두 모드를 모두 지원하며, 컨텍스트가 짧을 땐 재계산이 싸고 길 땐 스왑이 유리해서 기본값이 컨텍스트 크기에 따라 전환된다.
계층 캐시를 도입하면 시스템 복잡도가 크게 늘고 캐시 미스시 지연 시간의 꼬리 p99가 튀는 현상이 나오고 접두사 재사용률이 낮은 워크로드에는 순손해다.
측정 지표 #
서빙 성능을 하나의 숫자로 말할 수는 없고, 최소 네 개를 같이 고려해야한다.
TTFT (Time To First Token, 첫 토큰까지의 시간) #
요청을 보낸 순간부터 첫 글자가 화면에 나타날때 까지의 지연 시간이다.
큐 대기시간 + 프리필 시간, 즉 프리필 단계의 성능이다.
사용자가 느리다 라고 느끼는 지점은 대부분 여기이기 때문이다. 스트리밍 UI에서 첫 글자가 나오는 순간 체감 대기가 끝난다.
목표치 감각: 대화형이면 300ms이하가 최적이고, 1초를 넘으면 불편, 3초를 넘으면 짜증 이탈이 발생한다. (뭐 애니메이션을 추가해서 3초 뻐기게 해도 되긴하지만)
TPOT / ITL (Time Per Output Token / Inter-Token Latency) #
토큰과 토큰 사이의 간격, 두 용어는 사실상 같은 것을 가리키는데
디코드의 단계 성능, 배치 크기, 모델 크기, 양자화 여부가 결정한다.
읽는 속도와 비교되는데 사람이 읽는속도는 대략 초당 10~15토큰 정도로 ITL이 50ms = 초당 20토큰 이하면 인간이 읽는속도보다는 빠르다는 체감이 든다.
Goodput - 자주 놓치는 지표 #
SLO를 만족한 요청만 세었을때의 처리량
처리량만 최적화하면 배치를 무한정 키우고 그러면 개별 사용자의 TTFT, ITL이 무너진다.
쉽게 말해 초당 10,000토큰을 처리했지만 그중 절반이 사용자가 기다리다 창을 닫은 요청이면 의미가 없다
식당의 시간당 조리한 접시수가 처리량이면 goodput은 손님이 화내기 전에 나간 접시랄까
처리량과 지연의 근본적 긴장
배치 크기 ↑
→ GPU 활용률 ↑ → 처리량 ↑ ✅
→ 한 스텝의 계산량 ↑ → 개별 요청의 ITL ↑ ❌
→ 큐 대기 ↑ → TTFT ↑ ❌
서빙 튜닝의 본질은 이 곡선 위에서 자기 SLO에 맞는 지점을 찾는 일이고 최고 성능 설정이라는 것은 존재하지 않는다. 상황마다 다름
그래서 앞으로 나올 기법들은 아래와 같은 문제들을 해결한다
- 디코드가 메모리 바운드라 GPU가 노는 상황
- 프리필이 디코드를 막아서 지연이 튀는 상황
- KV 캐시의 메모리 단편화
- 같은 접두사를 반복 계산
- GPU 메모리가 부족
- 커널이 하드웨어를 못 살림
- 모델이 GPU 한장에 안 들어감
- 프리필과 디코드가 서로 방해
- 디코드의 노는 계산 자원 활용
- 가중치 읽기가 느림
- JSON을 안정적으로 뽑아야 함
Batching - GPU를 채우는 기술 #
계산 자원이 남으니 여러개를 겹쳐서 처리하자
정적 배칭 #
가장 소박한 방법부터 보자. 요청 8개를 모아서 한 배치로 묶고 다 끝나면 다음 8개를 받는다
시간 →
요청1 ████████████████████████ (500토큰)
요청2 ████ (50토큰) ← 끝났는데도 자리를 차지
요청3 ██████ (80토큰) ← 끝났는데도 자리를 차지
요청4 ████████ (120토큰) ← 끝났는데도 자리를 차지
└──────────────────────┘
배치 전체가 요청1을 기다림
요청 5~8은 이 시간 내내 큐에서 대기
LLM의 출력 길이는 극단적으로 불균형 하다는 특징이 있다. "네" 라고 답할 요청과 코드 300줄을 쓰는 요청이 같은 배치에 있다면? 정적 배칭은 가장 느린 요청에 전체가 인질로 잡힌다.
게다가 끝난 요청의 슬롯은 빈 채로 계산된다. GPU는 패딩된 자리도 똑같이 연산한다.
정적 배칭에서 GPU 활용률은 보통 30~40%에 머문다.
결론적으로 2026년에 정적 배칭을 쓰는 프로덕션 서빙 엔진은 없고, 개념 이해용으로 알아두자 문제를 알았으니
연속 배칭 Continuous Batching #
배치가 전체 끝날때까지 대기하는게 문자였다면, 자리가 나는 즉시 새요청을 갈아끼워 처리하자로 시작한다.
스케줄링 단위를 배치가 아니라 이터레이션(모델 통과 한번)으로 바꾸고 그래서 이터레이션 수준 스케줄링 Iteration-level Scheduling 이라고도 부른다.
매 이터레이션마다:
1. 완료된 시퀀스를 배치에서 제거하고 결과 반환
2. 비워진 슬롯에 대기 큐의 새 요청을 삽입
3. 배치 전체에 대해 모델을 한 번 통과
4. 반복
vLLM의 스케줄러는 매 포워드 패스 이후 세 개의 큐를 평가한다.
프리필을 아직 시작하지 않은 waiting, 활발히 디코딩중인 running 그리고 더 높은 우선순위 작업을 위해 GPU 공간을 비우려고 KV 블록이 CPU 메모리로 축출된 swapped
시간 →
슬롯1 ███(요청A)██████████████(요청E)███████
슬롯2 ████████(요청B)████(요청F)████(요청I)█
슬롯3 ██(요청C)███████████████████(요청G)███
슬롯4 ██████(요청D)█████(요청H)█████████████
↑ 빈 자리가 생기는 즉시 다음 요청이 들어감
핵심 통찰은 한이터레이션 안에서는 모든시퀀스가 정확히 토큰 하나씩 진행한다는 것이다.
시퀀스마다 총 길이는 달라도 한 스텝의 크기는 같다. 그러니 매 스탭마다 참가자를 갈아치워도 문제가 없다.
단순 정적배칭에서 PagedAttention 기반 연속 배칭으로 전환한다면 GPU활용률이 3040%대에서 7590%까지 올라가고 GPU 시간당 출력 토큰 수 2~4배로 이어진다.
- 스케줄러가 매 이터레이션마다 돌아야하므로 CPU 오버헤드가 대신 생긴다 이게 SGLang에서 이따 언급될 제로 오버헤드 스케줄러의 배경이 된다.
- 배치 구성이 계속 바뀌므로 CUDA Graph Capture가 까다롭다.
- 개별요청의 ITL이 다른 요청의 상황에 따라 변동되기도 한다. 지연시간 예측이 어려워진다
vLLM, SGLang, TensorRT-LLM 전부 사실상 지원하는 표준이다.
Chunked Prefill - 긴 프롬프트가 만드는 지연 폭발 방지 #
연속 배칭을 켰는데도 지연이 튀는 현상이 남는다. 원인은 프리필과 디코드의 크기 차이다.
사용자 한명이 32K 토큰짜리 문서를 붙여 넣었다고 하자. 이 요청은 프리필은 32,000개의 토큰을 한 번에 처리하는 거대한 작업이다. 그동안 GPU는 이 하나에 매달린다.
[32K 프리필 = 400ms]
디코드 중인 다른 20명 → ████████ (400ms 동안 아무 토큰도 못 받음)
이걸 헤드-오브 라인 블로킹 head-of-line blocking이라고 하며 한명 때문에 전체 사용자의 ITL이 느려지는 (여기선 400ms까지 튀는) 상황이다.
이를 해결하기 위해 청크드 프리필을 하는거고 방식은 프리필을 통째로하지 않고 고정 크기 조각으로 쪼개서 각 조각을 다른 요청들의 디코드 스텝과 같은 배치에 섞는다.
이터레이션 1: [프리필 조각 1 (8K)] + [디코드 20개]
이터레이션 2: [프리필 조각 2 (8K)] + [디코드 20개]
이터레이션 3: [프리필 조각 3 (8K)] + [디코드 20개]
이터레이션 4: [프리필 조각 4 (8K)] + [디코드 20개]
두 가지가 동시에 해결된다
- 지연 평준화: 400ms 대신 100ms 네 번으로 나뉘면 그 사이사이에 다른 사용자들이 토큰을 받는다.
- 공짜 계산 활용: 디코드에만 있는 배치는 메모리 바운드라 계산 자원이 남는다. (1장 사실 1.) 거기에 프리필 조각을 끼워넣어 남는 계산 자원을 공짜로 쓴다 프리필과 디코드는 병목자원이 다르기 때문에 섞으면 서로를 채워준다.
vLLM v0.18.0(V1)에서 온라인 서빙의 기본 청크 크기는 8,192 토큰이며 (--max-num-batched-tokens로 제어), 구버전의 512에서 크게 올린 값이다. 청크 프리필이 없으면 32K 토큰 프리필 하나가 수백 밀리초동안 GPU를 막아 동시 요청 전체에 지연 스파이크를 일으킨다.
튜닝 사다리의 실측 예시로 보면, Llama 3.1 8B Instruct / A100 80GB / ShareGPT 트레이스 기준으로 기본 설정 4,200 tok/s에서 청크 프리필 추가로 4,800 tok/s가 되었다.
대가
- 청크가 너무 작으면 프리필 자체에 효율이 떨어진다 작은 행렬 곱셈은 GPU에게 비효율적이니
- 청크가 너무 크면 블로킹이 다시 나타난다.
- 오프라인 배치 작업에는 오히려 손해일 수 있다. 지연시간을 신경쓸 필요없는데 프리필 효율만 깎기 때문이다.
Example 사내 코딩 어시스턴트 #
엔지니어들이 파일 전체를 붙여넣고 프롬프트 길이가 500토큰에서 40,000 토큰까지 극단적으로 다양하다. 청크 프리필은 선택이 아니라 필수다 없으면 짧은 질문을 던진 사람이 남은 40K 붙여넣기 때문에 몇초 씩 멈춘다.
# vLLM
--enable-chunekd-prefill --max-num-batched-tokens 8192
# SGLang (기본 활성, 청크 크기 조정)
--chunekd-prefill-size 8192
배칭 관련 핵심 설정값 정리 실무에서 만질일이 많을 파라미터들은 아래다
| 설정 | 무엇을 제어하나 | 올리면 | 내리면 |
|---|---|---|---|
--gpu-memory-utilization | KV 캐시 풀이 쓸 VRAM 비율 (vLLM 기본 0.90) | 동시 사용자 ↑ | OOM 여유 ↑ |
--max-num-batched-tokens | 한 이터레이션의 최대 토큰 수 (vLLM V1 기본 8192) | 처리량 ↑ | ITL ↓ (좋아짐) |
--max-num-seqs | 최대 동시 시퀀스 수 (vLLM V1 기본 1024) | 처리량 ↑ | 개별 지연 ↓ (좋아짐) |
전용 인스턴스에서는 --gpu-memory-utilization을 0.95까지 올릴 수 있고, 고처리량 배치 워크로드라면 --max-num-batched-tokens를 8192나 16384로 올리는것이 권장된다.
주의! -
--gpu-memory-utilization을 0.98 같은 값으로 올리면 대부분 OOM으로 죽는다. 활성화 메모리, CUDA Graph Buffer, 단편화 여유분등 필요하기 때문에 0.90 -> 0.95가 안전한 범위이고 그 이상은 실측하며 조금씩 올려야한다.
KV Cache 관리 #
1장에서 KV캐시가 배치 크기를 제한한다.라고 했었는데, 이 장의 기법들이 동시 사용자 수를 몇배로 늘린다
PagedAttention - OS 가상 메모리처럼 KV 캐시를 쓰는것 #
vLLM에서 만든 기법이고 지금의 모든 엔진이 이를 기반한다.
문제 #
KV 캐시를 어떻게 할당할까? 소박한 방법은 요청마다 최대 컨텍스트의 길이 만큼 연속된 메모리를 미리 잡아두는 것이다.
하지만 문제는 대부분의 요청이 그 길이를 다 쓰지 않는다는 것이다.
요청 A에 128K 길이만큼 예약 ████████████████████████████████
실제 사용 ████ ← 나머지 전부 낭비
전통적인 추론 엔진은 모든 요청에 대해 최대 컨텍스트 길이 전체를 미리할당하는데, 대부분의 요청이 전체 컨텍스트 윈도우를 쓰지않기 때문에 할당된 VRAM의 60~80%가 낭비도니다.
여기에 외부 단편화까지 겹친다. 요청들이 끝나면서 메모리 중간중간 구멍이 생기는데, 새 요청은 연속된 큰 덩어리를 요구하므로 총 여유공간이 충분해도 할당에 실패한다.
메커니즘 #
운영체제의 가상 메모리 페이징을 그대로 가져온다
프로세스가 논리적 페이지 테이블 통해 비연속 물리 프레임에 접근하듯, vLLM의 각 활성 시퀀스는 논리적 블록 테이블을 통해 GPU 메모리의 비연속 물리 블록에 있는 KV 캐시에 접근한다.
vLLM KV 캐시 풀의 물리 블록이 하나는 연속된 16개의 토큰의 키,값 텐서에 담는다. (--block-size로 조절 가능한 기본 값.)
논리 뷰 (시퀀스가 보는 것) 물리 뷰 (실제 GPU 메모리)
시퀀스 A: [블록0][블록1][블록2] → [#7][#3][#12] ← 흩어져 있어도 무방
시퀀스 B: [블록0][블록1] → [#1][#9]
↑ 빈 블록은 즉시 재사용
왜 통하는 건가?
- 필요한 순간에만 블록을 할당하므로 미리 예약하는 낭비가 사라진다.
- 블록 크기가 고정 16토큰이라 외부 단편화는 원리적으로 발생하지 않고 내부 단편화는 최대 15토큰 뿐이다.
- 요청이 끝나면 블록을 즉시 풀에 반납한다.
PagedAttention은 토큰이 생성됨에 따라 필요한 블록만 요청 시점에 할당하고 요청 완료 즉시 해체하여 같은 GPU에서 2~4배 더 많은 동시 요청을 처리할 수 있게 한다.
Trade off
- 어텐션 커널이 블록 테이블을 따라가며 KV를 읽어야해서 전용 커널이 필요하다. 순진하게 구현하면 오히려 느리다. - 5장 커널 얘기해서 더 자세히 다루겠다
- 블록 크기가 작으면 관리 오버헤드가 높고 크면 내부 단편화 문제가 생긴다.
vLLM이 원조이지만 SGLang도 Paged Layout을 기반으로 쓰고 사실상 모든 현대 엔진의 기본 전제다.
접두사 캐싱 Prefix Caching #
같은 것을 두번 계산하지 않는 것이다. PagedAttetnion은 메모리 낭비를 없앴지만, 계산 낭비는 그대로다
요청 1: [시스템 프롬프트 2000토큰][질문 A]
요청 2: [시스템 프롬프트 2000토큰][질문 B]
요청 3: [시스템 프롬프트 2000토큰][질문 C]
↑ 완전히 동일한데 세 번 프리필
에이전트 워크로드에서는 시스템 프롬프트 + 도구 정의가 5,000 토큰을 넘는 경우가 흔하다. 매 호출마다 5,000 토큰을 재계산하고 있는 것이다.
블록 단위로 토큰 내용의 해시를 키로 삼아서 KV 블록을 캐싱한다. 새요청이 오면 앞에서부터 블록 해시를 비교해 일치하는 만큼 재사용하고, 갈라지는 지점부터만 계산한다.
왜 통하는 것인가?:
KV는 인과적 (casual)이라서 즉 어떤 토큰의 K, V는 그 앞의 토큰들에게만 의존하고 그 뒤의 토큰들과는 무관하다. 따라서 접두사가 같으면 그 부분의 KV는 비트 단위로 동일하고 재사용해도 결과가 달라지지 않는다
이 성질이 접두사 캐싱을 가능하게 만드는 전부다. 뒤에 무엇이 오든 앞부분 KV는 변하지 않는다.
대부분의 워크로드에서 가장 가치가 큰 플래그를 하나 꼽으라면 --enable-prefix-caching 으로 모든 요청이 공유하는 시스템 프롬프트가 있다면 이것만으로 30% 처리량 향상이 나온다. 앞서 본 튜닝 사다리에서는 4,800 -> 5,500 tok/s 구간이 이 항목이다.
Trade off
- 접두사가 정확히 일치해야한다. 시스템 프롬프트에 타임스탬프타 사용자 이름을 넣는 순간 적중률이 0이 된다.
- 캐시가 메모리를 차지하므로 축출 정책이 필요하다 (eviction 넘칠때 안쓰이는거 걷어내는거)
Tips - 프롬프트를 설계할 때 변하지 않는 부분을 앞에, 변하는 부분을 뒤에 두어라, 이 한가지 원칙만으로 TTFT가 수 배 개선되는 경우가 흔하다 ex.
"현재시각 now 당신은 도우미 입니다.."에서당신은 도우미입니다 ... 현재시각 now..로 바꿔도 앞부분 전체가 캐시된다. mutable한 값으로 시작하지말자.
RadixAttention - SGLang 핵심 #
위에서 언급한 접두사 캐싱을 한 단계 더 밀어붙인것이 RadixAttention이고 이것이 SGLang의 존재 이유다.
일반적인 접두사 캐싱은 평평(flat)한 해시테이블로 구현된다. 그런데 실제 워크로드의 재사용 패턴은 평평하지 않고 나무 모양이다.
[시스템 프롬프트]
│
┌─────────────────┼─────────────────┐
[대화 1턴] [대화 1턴'] [대화 1턴'']
│ │
┌────┴────┐ ┌────┴────┐
[2턴 A] [2턴 B] [2턴 C] [2턴 D] ← 멀티턴 대화의 분기
- 멀티턴 대화: 턴이 쌓일수록 트리가 깊어진다
- 자기 일관성 샘플링: 같은 프롬프트에서 답을 8개 뽑는다 -> 한 노드에서 8갈래
- 에이전트 트리 탐색: 여러 경로를 시도하고 되돌아온다.
- RAG: 같은 문서 노드에서 질문마다 분기
평평한 캐시 구조로는 이 구조를 효율적으로 관리하기가 어렵다.
메커니즘 #
생성요청이 끝나도 KV 캐시를 버리지않고 프롬프트와 생성 결과 양쪽에 KV 캐시를 Radix Tree에 보관한다. 이 자료구조가 효율적인 접두사 탐색, 삽입, 축출을 가능하게 한다. 여기에 LRU eviction 정책과 캐시 인지 스케줄링 정책을 결합해서 캐시 적중률을 높인다.
radix tree는 트라이 (접두사 트리)의 공간 효율적인 변형이다. 일반적인 트리와 달리 라딕스 트리의 간선은 단일 원소가 아니라 가변 길이의 원소 시퀀스로 레이블링 될 수 있고 이 특성이 효율을 크게 높인다.
트라이 (간선 = 토큰 1개) 라딕스 트리 (간선 = 토큰 시퀀스)
root root
└─"당" └─"당신은 도움이 되는 어시스턴트입니다"
└─"신" ├─"오늘 날씨는?"
└─"은" └─"파이썬 코드를 짜줘"
└─ ... (수천 노드) ← 노드 수가 극적으로 줄어듦
실제 자료구조는 아래처럼 생겼다.
class TreeNode:
children: dict[TreeNode] # 자식 노드들
parent: TreeNode # 부모
key: RadixKey # 이 간선의 토큰 시퀀스
value: torch.Tensor # 대응하는 KV 캐시 인덱스
lock_ref: int # 참조 카운트 (사용 중이면 축출 금지)
lock_ref가 실무적으로 중요하다 현재 어떤 요청이 사용중인 노드는 축출되면 안되니까 참조 카운트로 보호한다.
동작 흐름은 아래와 같다
새 요청 도착
↓
라딕스 트리에서 접두사 매칭 (가장 긴 공통 접두사 탐색)
↓
매칭된 부분 → KV 캐시 그대로 재사용 (프리필 건너뜀)
매칭 안 된 부분 → 새로 계산하고 트리에 노드 삽입
↓
GPU 메모리 부족 시 → LRU로 리프 노드부터 재귀적 축출
축출은 리프 노드를 재귀적으로 제거하는 LRU 정책으로 구현되고 리프부터 제거하는게 중요하다 중간 노드를 지우면 그 아래 자식들이 전부 무효화되기 때문이다
Why Tree??
- 부분 매칭이 자연스럽다 얼마나 겹치는가를 트리 탐색 한 번으로 알 수 있다.
- 분기가 공짜 대화 분기를 우아하게 처리해서, 에이전트가 과거 어텐션 가중치를 재계산하지 않고 대안 경로를 탐색할 수 있다.
- 축출 단위가 의미론적이다. 리프(가장 최근의 세부 분기)부터 버리는게 히트율에 유리하다.
캐시 인지 스케줄링 #
RadixAttetion의 성능은 자료구조만으로 나오는것이 아니고 스케줄러가 같이 협력해야 성능이 나온다.
단일 워커의 경우 큐에 있는 요청들을 매칭시킨 접두사 길이 순으로 정렬한다.
왜 이게 이득인가? 접두사를 많이 공유하는 요청들은 시간적으로 붙여서 실행하면, 그 공유 노드가 축출되기 전에 전부 사용된다. 반대로 뒤섞어서 실행하면 노드가 축출되고난 뒤 다시 필요해져 재계산이 발생한다. 같은 캐시로도 적중률이 달라진다.
여러 서버로 확장하면 캐시 인지 로드밸런서 SGL Router가 된다.
라운드로빈으로 요청을 뿌리면 같은 접두사가 다른 서버로 흩어져 캐시가 무의미해진다. 접두사를 보고 같은 서버로 보내야한다.
수치 개선 지표
- KV 캐시 재사용과 병렬성으로 5배 높은 처리량
- 접두사 중첩이 60% 이상인 워크로드는 75~95% 캐시의 적중률과 함께 TTFT가 크게 낮아져 완전히 고유한 프롬프트만 있는 워크로드는 이득이 없다.
- 멀티모달 모델의 경우 입력 이미지의 해시를 라딕스 트리의 키로 써서 같은 이미지의 KV cache를 재사용할 수 있고 이런 벤치마크에서 최대 6배 높은 처리량이 나왔다.
Trade off
- 공유가 없으면 순손해임: 공유가 없을때 같은 라딕스가 없을때 RadixAttetnion은 작은 오버헤드를 가진다. 반복 접두사가 없는 단순 워크로드에는 vLLM을 쓰는것이 낫다.
- 토큰 시퀀스가 특수 토큰까지 포함해 정확히 일치해야하고, 트리 구조가 평평한 배열 대비 약간의 메모리 오버헤드를 추가하며, 축출 후보를 찾는 데 힙연산 비용이 든다.
- 시퀀스 끝의 부분 페이지는 캐싱되지 않는다. 페이지 단위 공유와 효율적 메모리 할당을 보장하기 위한 설계다.
Exmaple #
# 축출 정책 선택 (기본 LRU)
--radix-eviction-policy lru # lru lfu fifo mru filo priority
# 페이지 정렬 (기본 1 = 정렬 x)
--page-size 16
# 디버깅/콜드스타트 벤치마크용으로끄기
--disable-radix-cache
왜
disable-radix-cache가 필요할까? 벤치마크할 때 캐시가 켜져있으면 두번째 실행부터 비현실적으로 빠른숫자가 나온다. 콜드 스타트 성능을 재기위해 끄는거다.
사례 적용 - 문서 QA / RAG 서비스 #
같은 계약서 문서 20K 토큰짜리에 사용자들이 각기 다른 질문을 던지는 서비스라 쳐보자
요청 구조: [시스템 프롬프트 500][계약서 본문 20,000][질문 30]
└──────── 20,500 토큰 공유 ────────┘ └ 30토큰만 다름
**공유율 99.85%**이다. RadixAttetnion이 존재하는 이유 그 자체인 워크로드인데,
첫 질문만에 20,500 토큰 프리필을 하고 이후 질문들은 30토큰만 계산한다 TTFT가 수백 밀리초에서 수십 밀리초로 떨어진다.
여기에 SGL Router를 붙여, 같은 문서 ID의 요청은 같은 워커로 라우팅하면 다중 서버에서도 적중률이 유지된다.
계층 캐시와 분산 KV 저장소 #
GPU 밖으로 확장하는건데 라딕스 트리도 결국 GPU 메모리 크기가 상한이다. 위 적용 사례에서 문서가 1,000 종이라면 전부 GPU에 못 올린다. 서버가 또 여러대면 각자 자기 캐시만 알고있어 중복이 생긴다.
그래서 메커니즘은 KV 캐시를 GPU 밖의 계층으로 밀어내는 것이다.
GPU HBM -> CPU RAM -> SSD -> Redis, S3, Mooncake 등 순으로 KV 블록을 이동시키고, 필요할 때 고처리랑 커넥터로 관련 블록만 가져온다. 여러 vLLM 인스턴스가 하나의 KV 캐시풀을 공유할 수 있고, LMCache 서버가 독립 데몬으로 돌기 때문에 엔진이 죽었다 살아나도 캐시 상태가 유지된다.
- LMCache: 계층형 KV 오프로딩 레이어로 vLLM과 통합
- Mooncake: KVCache 중심 분산 아키텍처, Transfer Engine + Store(저장소)
- NIXL / RDMA: 노드간 KV 고속 전송
Mooncake Transfer Engine은 2024년 12월 vLLM에, 2025년 4월 SGLang에 공식 통합되었고, FAST는 2025 최우수 논문상을 받았다.
특히 중요한것은 비접두사 재사용 (CacheBlend)인데.
접두사 캐싱은 근본적 한계가 존재했다. RAG에서는 검색된 순간 청크 순서가 매번 달라진다 라는 특징이 있었다.
요청 1: [시스템][청크 A][청크 B][질문]
요청 2: [시스템][청크 C][청크 A][질문] ← 청크 A가 두 번째 위치
청크 A의 KV는 앞에 뭐가 오느냐에 따라 달라지므로, 위치가 바뀌면 접두사 캐싱으로 못쓴다.
CacheBlend는 그래서 소수의 토큰만 선택적으로 재계산하여 품질을 복구하는 방식으로 임의의 위치에서 KV 블록을 재사용한다. 이것이 매번 검색 청크를 새로 이어붙이는 RAG의 해법이다.
왜 통하나 (직관) - 청크 A의 KV 위치에 따라 완전히 달라지는 건 아니다. 대부분은 비슷하고 경계 부근의 소수 토큰만 크게 달라진다 그 소수만 다시 계산하면 근사적으로 복원된다.
이 3계층 구조는 단일 H100 80GB가 GPU 메모리만으로 가능한 것은 훨씬 많은 동시 사용자를 처리할 수 있게 하지만 콜드 블록 접근시 지연이 늘어나는 대가가 있다.
쫌 위에서 숫자로 계산한 대역폭 절벽이 그대로 적용되는데 재계산이 전송보다 싸면 오프로딩은 손해다 도입전에 자기 워크로드의 손익분기를 재야한다.
SGLang의 방향 #
SGLang은 2026년 2분기 로드맵에는 계층 캐시와 하이브리드 어텐션을 네이티브로 기능으로 만들었고. 에이전틱 워크로드를 위한 유연한 세션 제어를 지원하는 목표가 포함되어있다. 에이전트 워크로드가 접두사 캐싱을 무너뜨리는 문제(긴 세션 및 잦은 분기, 도구 결과 삽입)을 정면으로 다루겠다는 뜻이다.
SGLang #
RadixAttention등을 봤으니 SGLang 나머지 절반을 좀 더 보자.
SGLang은 Structured Generation Language에서 왔다.
원래 논문 제목이 Efficient Execution of Strucutered Language Model Programs 인데, 여기서 프로그램(program) 이라는 단어가 핵심이다.
요처잉 아니라 프로그램 ?? #
기존 서빙 엔진은 요청 하나 = 프롬프트 하나 = 완결된 단위로 보았었다.
그런데 실제 LLM 애플리케이션은 이렇게 생겼다.
# 실제 애플리케이션의 모습
문서 = 읽기(파일)
요약 = LLM(f"{문서}를 요약해줘")
질문들 = LLM(f"{요약}에서 나올 만한 질문 5개")
for 질문 in 질문들: # ← 여기서 5갈래로 분기
답 = LLM(f"{문서}\n{질문}에 답해줘") # ← 문서를 5번 다시 프리필!
평가 = LLM(f"{답들} 중 최선은?")
위에 예시에서 엔진 입장은, 이건 서로 무관한 요청 8개가 된다. 그래서 문서를 6번이나 프리필하게 된다. 하지만 사람 입장에서 보면 명백히 하나의 프로그램이고 구조가 있다.
SGLang의 출발점은 이 인식이다. 각 프롬프트를 고립된 요청으로 취급하는 대신 실제 사용자 패턴에 맞춰서 추론 실행을 정렬한다.
┌────────────────────────────────────────┐
│ 프론트엔드 (DSL) │
│ - 프로그램의 구조를 엔진에 알려줌 │
│ - gen, fork, join, select 같은 프리미티브 │
└────────────────┬───────────────────────┘
│ 구조 정보
┌────────────────▼───────────────────────┐
│ 런타임 │
│ - RadixAttention (구조를 캐시 재사용으로) │
│ - 압축 FSM (구조를 디코딩 가속으로) │
│ - 제로 오버헤드 스케줄러 │
└────────────────────────────────────────┘
핵심은 프론트엔드에서 구조를 알려주면 런타임이 그 구조를 최적화의 기회로 바꾸고 이게 co-design이라고 하는 공동 설계 접근이다.
실무적인 현실은, 솔직하게 대부분의 팀은 SGLang DSL을 안쓴다. OpenAI 호환 API로 붙여 RadixAttention과 런타임 최적화만 취한다. 그래도 되긴한다.
프론트엔드는 항상 전체 프롬프트 런타임을 보고 런타임이 자동으로 접두사 매칭과 재사용 캐싱등을 수행한다. DSL을 안써도 RadixAttention은 자동으로 작동하기에 DSL은 명시적 분기(fork) 같은걸 쓸때 추가 이득이 있는 선택지다.
압축 FSM, 점프-포워드 디코딩 구조를 만들어보자 (공짜로) #
SGLang의 두 번째 대표 기법으로 9장에서 구조화 출력을 자세히 다루지만, 이 기법 자체는 SGLang의 고유이므로 여기서 한번 더 본다.
문제 #
JSON을 강제로 출력시키고 싶다고 해보자. 기존 방식은 **FSM(유한 상태 기계)**를 써서, 매 스텝마다 현재 상태에서 허용되는 토큰만 남기고 나머지 확률을 0으로 만든다.
이 토큰 단위 방식은 여러 토큰을 한 번에 디코딩할 수 있는 기회가 있을때 비효율적이다 예를들어 {"summary": " 같은 상수 시퀀스는 일반 디코딩에서 여러 토큰에 걸치므로 여러번의 디코딩 단계가 필요한데, 사실 디코딩할 떄 유효한 다음 토큰이 하나 뿐이다.
{ " summary " : " ← 7번의 모델 통과
↑ 각 위치에서 선택지가 사실상 1개
답이 정해져있는데 모델에게 7번 물어보고 있는거다.
SGLang의 압축 FSM으로 이 한계를 극복할 수 있는데, 런타임이 FSM을 분석해서 인접한 단일 전이 간선들을 하나의 간선으로 압축하고, 여러 토큰을 함께 디코딩할 수 있는 시점을 인식한다.
압축된 전이 간선 위의 여러 토큰을 한 번의 포워드 패스로 디코딩할 수 있어 디코딩이 크게 가속된다.
일반 FSM: (s0)-{→(s1)-"→(s2)-s→(s3)-u→(s4)-m→ ... (7단계)
↓ 단일 전이 간선을 압축
압축 FSM: (s0)---{"summary": "--→(s7) (1단계)
**점프-포워드 디코딩(jump-forward decoding)**은 이 압축을 실행 시점에 활용하는 것이다.
현재 입력을 기준으로 다음 출력이 문법으로부터 결정론적으로 추론가능할 때 LLM 디코딩과 샘플링을 건너뛰고 그 출력을 직접 토큰화해서 컨텍스트에 이어붙인다.
왜 이게 되는거지
- 정보 이론적으로 자명한데, 다음 토큰이 유일하게 결정되어 있으면 그 토큰의 엔트로피는 0으로 엔트로피가 0인것을 모델에게 물어보는 것은 시간낭비다. 문법이 이미 답을 알고있으니 그냥 쓰면 된다
비유하면 서식이 정해진 서류를 쓸때, 성명, 생년월일 같은 항목명이 이미 인쇄되어 있다면 그걸 매번 성명 김땡땡 쓰라고 시키는게 기존방식이고 인쇄된 서식을 쓰는게 점프-포워드 인 것이다.
문법상 다음 몇 개 토큰이 필연적인 상황일 때 개선이 얼마나 되었는가, 닫는 중괄호, 필드명, JSON의 고정 구두점등은 엔진이 모델 포워드 패스 없이 한 번에 방출하므로 구조가 무거운 출력은 토큰 수보다 적은 모델 호출로 생성이 될 수 있다.
여기서 놀라운 결과가 나오느데, 구조가 많은 출력은 제약 없는 생성보다 빠를 수 있다. 보통 제약 = 오버헤드라고 생각하지만 정 반대가 된다.
Trade Off
- 정규 표현식/문법으로 표현 가능한 구조에만 적용된다. 자유 텍스트 필드 내부에는 이득이 없다.
- 압축 FSM 구축에 컴파일 비용이 든다 (스키마가 자주 바뀌면 부담)
- 문법 제약 적용 자체는 토큰당 오버헤드를 추가하며, 그 영향은 문법 복잡도에 따라 다르다.
제로 오버헤드 CPU Scheduler #
아까 위에서 연속 배칭의 대가로 CPU 오버헤드를 언급했었다.
매 이터레이션마다 파이썬 스케줄러가 돌면서 큐를 정리하고 배치를 구성하고 Radix Tree를 갱신하기 때문이다.
문제는 GPU가 그 동안 놀고 있을 수 있다는 것이다.
전통적 방식 (직렬):
[CPU 스케줄링 3ms][GPU 포워드 8ms][CPU 스케줄링 3ms][GPU 포워드 8ms]...
↑ GPU 유휴 3ms ↑ GPU 유휴 3ms
GPU 활용률 = 8/11 = 73%
작은 모델일 수록, 배치가 작을 수록 이 비율은 나빠진다. 8B 모델에서는 포워드가 3ms인데, 스케줄링이 3ms이면 GPU가 절반만 일을 한다.
메커니즘 #
스케줄링과 GPU 실행을 겹친다 overlap. CPU가 다음 배치를 준비하는 동안, GPU는 현재 배치를 계산한다.
겹침 방식:
GPU: [포워드 N][포워드 N+1][포워드 N+2]... ← 쉬지 않음
CPU: [준비 N+1] [준비 N+2] [준비 N+3]... ← 한 스텝 앞서서 준비
GPU 활용률 ≈ 100%
다음 배치의 구성은 현재 배치의 출력을 전부 몰라도 대부분 결정할 수 있다. 어떤 요청이 끝날지는 확정적이지 않지만, 대기큐에 누굴 넣을지 어떤 KV 블록을 쓸지는 미리 계산이 가능하기에 추측성으로 준비하고 어긋나면 보정하는 구조다.
SGLang은 접두사 캐싱을 위한 RadixAttention, 제로오버헤드 CPU Scheduler, Prefill-Decode 분리, 투기적 디코딩, 연속배칭, PagedAttention, 텐서/파이프라인/전문가/데이터 병렬, 구조화 출력, 청크 프리필, 양자화(FP4/FP8/INT4/AWQ/GPTQ), 멀티-LoRA 배칭을 제공한다.
제로 오버헤드 스케줄러는 v0.4에서 도입되었다.
TradeOff
- 구현 복잡도가 크다. 실제로 2026 2분기에는 SGLang로드맵에 Stage->Worker->Executor->Engine 구조를 Stage->Engine으로 접어 약 33,000줄을 10,000줄로 줄이고 요청 경로 깊이를 8~10에서 6으로 낮추되 정확도와 성능 저하 없이 수행하겠다는 리팩터링 목표가 있었다.
- 성능을 위해 쌓인 복잡도를 다시 걷어내는 작업이 진행중이라는 뜻이다.
SGLang의 현재 위치 2026. 8 기준 #
2025년 10월 SGLang-Jax 백엔드로 TPU 네이티브 실행이 추가되었고, 2025년 11월에는 SGLang Diffusion으로 비디오 이미지 생성 가속까지 범위를 넓혔다.
2026년 7월 릴리즈를 기준으로 GB300/B300, RTX PRO 6000 Blackwell 서버 에디션, DGX Spark, Jetson Thor를 지원하고, Hopper이상에서 FP8 정밀도를 지원한다.
P/D 분리, 모든 병렬화 방식, 투기적 디코딩 전반의 완전한 호환성과 프로덕션 수준의 신뢰성등을 목표로 이번글에서 잡고있다.
여기에 흥미로운 항목이 있는데 RL 프레임워크의 롤아웃을 위한 DP 스케줄러로 SGLang Gateway를 지원하고 gRPC모드에서 게이트웨이가 SGLang 서버의 KV Cache를 능동적으로 수선해 라우팅 결정을 개선하도록 하는 작업이다.
GLM slime이 비동기 롤아웃으로 장기 에이전트 RL을 가능하게 했는데, 그 롤아웃을 실제로 돌리는 것은 엔진이 SGLang이다. GLM-4.5의 핛브은 THUDM이 오픈소스로 공개한 slime 프레임워크로 구동되며 slime은 SGLang 네이티브고 추론 엔진 학습 인프라의 일부가 된 것이 중요한 변화라고 볼 수 있다.
Attention Kernel #
MLA, DSL, Gated DeltaNet같은 아키텍처를 실제로 빨라지게 하려면
GPU 커널이 필요하다. 왜 그런지 이 장에서 설명해보겠다.
FlashAttention - 어텐션을 메모리에서 구출 #
어텐션은 왜 느린가? 순진한 어텐션 구현은 이렇다.
S = Q @ K.T # [N, N] 행렬을 만들어 HBM에 씀
P = softmax(S) # [N, N] 을 다시 읽고, 다시 씀
O = P @ V # [N, N] 을 또 읽음
N, N 행렬이 문제다 N=8,192면 원소가 6,700만개이고 이걸 HBM에 쓰고 읽기를 반복한다. HBM접근이 가장 큰 병목인데 어텐션이 그걸 세 번씩 하는것이 문제다.
계산은 GPU가 금방하지만, 중간 결과를 주고받는 메모리의 이동과정이 병목이 되는 것이다.
타일링과 온라인 소프트맥스 #
FlashAttention은 타일링된 어텐션과 온라인 소프트맥스로 HBM 읽기, 쓰기를 시퀀스 길이에 대해 제곱하여 선형으로 줄일 수 있음을 보였고, 실측 2~4배 속도 향상을 달성햇다.
두 가지 아이디어가 결합되었는데
- 타일링 tiling: [N, N] 행렬을 통째로 만들지 않고, Q, K를 작은 블록으로 쪼개 GPU SRAM(온칩 초고속 메모리) 안에서 처리한다. SRAM은 HBM보다 훨 빠르지만 아주작은 수십KB 대라 블록 단위로 처리해야한다.
- 온라인 소프트맥스: 소프트맥스는 원래 전체 행을 다 봐야 정규화가 가능하다. 그런데 누적 최댓값과 누적합을 들고 다니면서 점진적으로 갱신한다면? 블록 단위로 처리하면서도 정확한 결과를 낼 수 있다.
블록 1 처리 → (부분합, 부분최댓값) 갱신
블록 2 처리 → 이전 결과를 새 최댓값 기준으로 재스케일하며 누적
...
전체를 한 번도 메모리에 안 올리고 정확한 소프트맥스 완성
왜 이게 되냐면 메모리를 아끼려고 계산을 조금 더한다는 트레이드 오프가 성립하는데
GPU는 compute가 여유고 메모리가 부족하기에 계산을 더하고 메모리 접근을 줄이는 트레이드 오프를 해야 이득이다.
FlashAttention-2는 A100에서 이론 최대 성능 50~73%에도달했고 FlashAttention-3은 Hopper의 비동기 기능 TMA, WGMMA 와 FP8 양자화를 활용해 H100에서 840 TFLOPS(85% 활용률)을 달성했다.
하드웨어마다 다시 짜야하는 문제는 있다 FA2, FA3, FA4의 변화는 알고리즘이 아니라 **새하드웨어의 비동기 명령어와 저정밀 텐서 코어를 쓰기 위한 재작성이다.**이게 커널 개발이 어려운 이유다.
FlashInfer #
FlashAttention과 헷갈리기 쉬운데 목적이 다르다. 서빙을 위한 커널 라이브러리가 FlashInfer이다.
어텐션만 빨라서는 부족한 것 #
FlashAttention은 어텐션 계산을 빠르게 한다. 그런데 서빙엔진에서는 그것만으로 안 되는 요구사항이 있다.
- kV 캐시가 페이지드 레이아웃이라 블록 테이블을 따라가며 읽어야한다.
- 배치 안의 시퀀스 길이가 제각각이라도 로드 밸런싱이 필요하다
- MLA처럼 압축된 KV를 다루는 변형이 필요하다
- 접두사를 공유하는 요청들을 한꺼번에 처리하면 이득이다.
FlashInfer는 더 넓혀 추론 연산자 라이브러리로 블록 희소 KV 어텐션 (vLLM의 PagedAttention이 사용), 다중헤드 잠재 어텐션(MLA, DeepSeek 계열의 압축 KV 변형)등을 처리한다.
FlashInfer는 어텐션 계산의 plan/run 단계를 분리해 가변 길이 입력 계산을 plan 단위에서 스케줄링해 부하 불균형 문제를 완화한다.
메모리 효율 측면에서는 계층적 KV캐시를 위한 캐스케이드 어텐션을 제공하고 GQA 가속을 위한 Head-Query 융합을 구현한다.
**캐스케이드 어텐션 (Cascasde Attention)**이 흥미롭다.
ex. #
요청 32개가 같은 시스템 프롬프트 2000토큰 정도를 공유하고 각자 자기 부분 200토큰이 뒤에 붙는다고 가정해보자
순진하게는 2000토큰에 대한 어텐션 -> 공유부분을 32번 읽게 한다.
*캐스케이드: 공유 부분은 한 번만 읽어 32개 쿼리에 처리하고 고유 부분만 따로 처리한 뒤 합친다.
이렇게 되면 온라인 소프트맥스의 결합 법칙덕에 두 결과를 정확하게 병합할 수 있다.
이게 RadixAttention의 커널 짝이고 캐시로 계산을 아끼고 캐스캐이드로 메모리 읽기를 아끼는 것이다.
SGLang은 Hopper, Blackwell 양쪽에서 기본적으로 FlashInfer을 통해 어텐션을 라우팅한다 vLLM에서는 Blackwell(B200/B300)에서 기본값이고 Hopper(H100,H200)에서는 FlashAttention이기본이며 FlashInfer는 선택이다.
새 아키텍처는 왜 새 커널이 필요한가? #
| 아키텍처 | 왜 표준 커널로 안 되나 | 필요한 것 |
|---|---|---|
| MLA (DeepSeek) | KV가 압축된 잠재 벡터라 표준 어텐션의 데이터 레이아웃과 다름 | FlashMLA / FlashInfer MLA 경로 |
| DSA (DeepSeek, GLM) | 인덱서가 고른 비연속 top-k 토큰만 모아 어텐션해야 함 | 희소 수집(gather) 커널 |
| Gated DeltaNet (Qwen) | 어텐션이 아니라 순환 상태 갱신. 청크 단위 병렬 알고리즘 필요 | Flash Linear Attention의 Triton 커널 |
| KDA (Kimi) | GDN + 채널별 게이팅 + MLA 혼합 | 전용 커널 |
실제로 vLLM은 Qwen3-Next를 지원하기 위해 Flash Linear Attention의 Triton 커널을 통합하고 선형 어텐션 층과 전체 어텐션 층 양쪽을 관리하는 하이브리드 KV Cache 매니저를 도입해 단편화를 피하고 GPU 활용률을 극대화한다.
여기서 중요한 교훈은 새 모델이 나왔을때 day-0 지원이라는 말의 무게인데, 아키텍처가 새로우면 커널과 메모리 매니저를 새로 짜야한다. SGLang, DeepSeek-V3.2의 희소 어텐션을 day-0에 지원했다는 것은 상당한 엔지니어링 협업의 결과다.
커널 라이브러리 의존성이 늘어나면 설치와 버전 관리가 복잡해진다. 하드웨어별로 지원 상태가 다르고, 이 조합에서는 이 커널이 안된다는 케이스가 실무에서 자주 발생한다.
CUDA 그래프 - 커널 실행 오버헤드 제거 #
문제 #
커널을 부르는 비용은 모델 한 번 통과에는 수백 수천개의 CUDA 커널 순차 실행이 된다.
각 커널을 실행하려고 CPU, GPU에 지시하는 것 자체가 마이크로초 단위 비용이 드는데, 커널이 1,000개라면 그것만으로 수 밀리초가 된다.
디코드 한 스탭이 8ms인데 커널런치오버헤드가 3ms면 약 37%가 낭비인 것이다.
해법 #
커널 호출 시퀀스를 한 번 녹화(capture)해서 그래프로 만들고 이후에는 그래프 전체를 한 번의 명령으로 재생해서 최적화 한다.
녹화 없이 (매 스텝)
CPU: [커널1 지시][커널2 지시][커널3 지시] ... [커널1000 지시] ← 1,000번의 지시
GPU: └실행┘ └실행┘ └실행┘ └실행┘
녹화 후
CPU: [그래프 재생 지시] ← 1번의 지시
GPU: [커널1][커널2][커널3] ... [커널1000] ← GPU가 알아서 순차 실행
왜 이 해법이 통하는가? #
커널의 순서와 종류가 매 스텝 똑같기 때문이다.
모델 구조는 고정되어 있으므로 디코드 1,000번째 스탭에서 호출되는 커널 목록은 1번째 스텝과 완전히 동일하다.
달라지는 건 데이터가 담긴 메모리 주소의 내용뿐이지 호출 순서 자체가 아니다
같인 지시를 1,000번 반복해서 내리는 것이 낭비였던 이유가 이것이고, 한 번 기록해두면 되는 이유도 이것이다.
매일 똑같은 순서로 진행되는 조립 공정 라인에서 감독이 매번 1번 2번 3번공정 하고 천번 외치고 있었던건데 순서가 늘 같으니 작업지시서를 남기고 이대로 하라고 한 번만 말하면 된다. 물론 작업지시서를 쓰는 비용은 존재하겠지만.
- CUDA 그래프는 모양이 고정되어야한다. 그런데 연속 배칭은 매 이터레이션마다 배치크기가 바뀐다. 위에서 해법이 통하는 부분에 대한 모순이다.
그래서 우회방법이 자주 쓰이는 배치크기들 1, 2, 4, 8, 16, 32 ... 를 미리 여러개의 그래프를 캡처해두어 가장 가까운것을 쓰되 남는 자리를 패딩하는 방식으로 해결한다.
즉 모양이 고정되어야 한다는 제약을 쓸 만한 모양을 미리 다 만들어 둔다는 것으로 해결한다.
Tradeoff #
- 서버 기동시 그래프 캡처는 수십 초 ~ 수분이 걸린다
- 캡처된 그래프가 GPU 메모리를 차지하고 -> KV 캐시 몫이 줄어든다
- 디버깅시
--enforce-eager(vLLM) /--disable-cuda-graph(SGLang)로 끄면 원인 파악이 쉬워지지만 느려진다.
FlashInfer 커널은 CUDAGraph와 torch.compile로 캡처할 수 있게 설계되어 저지연 추론을 지원한다.
커널 라이브러리가 이 호환성을 명시적으로 보장해야 하는 이유다
병렬화 전략 - 모델을 여러 GPU에 나누기 #
앞선 내용들은 한 장의 GPU에 안들어 간다. DeepSeek V4-Prox는 1.6T, Kimi K3는 2.8T 인데, 어떻게 쪼갤 것인가가 이 장의 주제고 쪼개는 방식마다 통신 패턴과 병목이 다르다.
TP, Tensor Parallelism #
모델의 가중치 행렬 하나가 GPU 메모리보다 클 수 있다.
그리고 여러 GPU의 계산력을 한 요청에 동시에 쓰고 싶다.
해법 - 행렬을 잘라서 나눈다 #
행렬을 잘라서 각 GPU가 일부만 계산하고, 결과를 합친다.
원래: Y = X @ W (W는 [5120 × 20480])
TP=4: W를 열 방향으로 4등분 → W1, W2, W3, W4 (각 [5120 × 5120])
GPU0: Y1 = X @ W1
GPU1: Y2 = X @ W2
GPU2: Y3 = X @ W3
GPU3: Y4 = X @ W4
→ all-gather로 [Y1|Y2|Y3|Y4] 결합
어텐션은 헤드 단위로 자른다. 헤드 64개를 4장에 16개씩 나누는 식이다.
효과와 대가 #
장점으론 지연 시간이 줄어든다, 한 요청의 계산이 4장에서 분산되므로 개별 응답이 빨라진다.
그렇지만 통신이 매 층마다 발생하므로 all-reduce, all-gather가 필요하고, 이건 GPU간 링크 대역폭에 직결된다.
NVLink (노드 내부) : 수백 GB/s ~ TB/s → TP 가능
InfiniBand (노드 간): 수십~수백 Gb/s → TP 하면 통신에 잡아먹힘
TP는 노드 안에서만, 노드를 넘어가는 TP는 항상 나쁜 선택이다. 8 x H200 한 노드면 TP=8까지가 상한이다.
PP, Pipeline Parallelism #
층을 깊이 방향으로 나눈다는 해법이다.
80층 모델을 4노드에 20층씩 배치하는 기법이다.
노드0: 층 1-20 → 노드1: 층 21-40 → 노드2: 층 41-60 → 노드3: 층 61-80
└─ 활성값만 전달 (작음) ─┘
효과와 대가 #
노드간 통신량이 TP보다 훨씬 작다, 층 경계에서 활성값 텐서 하나만 넘기면 된다.
노드 간 확장에 적합하다.
대가로는 파이프라인 버블. 노드0이 일할때 노드 1~3은 놀게 된다.
시간 →
노드0: [배치1][배치2][배치3][배치4]
노드1: [배치1][배치2][배치3][배치4]
노드2: [배치1][배치2][배치3]
노드3: [배치1][배치2]
└버블─┘ └버블┘
마이크로배치를 잘게 쪼개면 버블이 줄지만 완전히 없앨 수는 없다. 추론에서는 TP보다 덜 쓰이며 주로 메모리가 정말 부족할 때 쓰인다.
(TP가 모델 가중치 여러개로 쪼개는거면 PP는 요청 처리를 병렬로 쪼개는 것이다.)
EP, Expert Parallelism - MoE 서빙 핵심 #
Qwen, Kimi, GLM, DeepSeek 이런애들은 전부 MoE라 실무에서 가장 중요한 병렬화다.
문제 MoE를 TP로 왜 나누면 안될까? #
DeepSeek-V3 계열의 MoE 층은 전문가가 256개이고 토큰당 8개를 켠다.
이 256개를 TP로 나누면 어떻게 될까?
TP는 각 전문가의 가중치 행렬을 잘라서 모든 GPU에 조각을 둔다.
그러면 어떤 전문가가 선택되든 모든 GPU가 참여해야하고 매번 전체 통신이 발생한다. 비효율적이다.
전문가를 통째로 배정하는 해법 #
전문가를 통째로 GPU에 배정하는 해법이 존재한다.
EP=32일 때: GPU 하나당 전문가 8개씩 담당
토큰이 전문가 #17, #93, #201, ... 을 선택
↓
[all-to-all 통신] 각 토큰을 담당 GPU로 보냄
↓
각 GPU가 자기 전문가로 계산
↓
[all-to-all 통신] 결과를 원래 자리로 되돌림
전문가가 하나의 계산은 독립적인데 통째로 한 GPU에 두개 되면 그 안에서 완결된다. 통신은 토큰을 라우팅하는 것 뿐이고 이건 가중치를 옮기는 것 보다 훨씬 싸다.
또 하나 큰 이점은 GPU당 담당 전문가가 적으니 GPU당 메모리 부담이 줄고 그만큼 KV 캐시에서 쓸 공간이 늘어난다. 이게 곧 배치크기이고 처리량이다.
TradeOff #
all to all 지옥과 부하의 불균형인데 all-to-all 자체가 너무 비싸서 모든 GPU가 모든 GPU와 통신하는 것이라. 이게 MoE 서빙의 최대 병목이고 그래서 DeepEP같은 전용 통신 라이브러리가 나왔다.
전문가 부하가 불균형하다는 특징도 문제다 실제 트래픽에서 특정 전문가가 훨씬 자주 선택된다. 그 전문가를 담당한 gpu만 과부화가 걸리고 나머지는 논다.
해법이 EPLB(Expert Parallelism Load Balancer)로 인기 전문가를 여러 GPU에 복제하거나 배치를 재조정한다.
Effect #
SGLang은 Atlas Cloud의 12개 노드 (각 8 x H100)에서 프리필-디코드 분리와 대규모 전문가 병렬을 사용해 2,000토큰 입력 시퀀스 기준 노드당 초당 52.3k 입력토큰, 22.3k 출력 토큰을 달서앻ㅆ다.
로컬 배포시 출력 100만 토큰당 $0.20으로 공식 딥시크 chat api의 약 5분의 1비용이다 같은 자원으로 순수 텐서 병렬을 쓴 경우와 비교하면 출력 처리량이 최대 5배가 된다.
같은 하드웨어에서 TP만 쓸 때 대비 5배로 병렬화 전략 선택이 얼마나 큰 차이를 만드는지 보여주는 숫자다.
SGLang은 DeepEP, DeepGEMM, EPLB 전체 기능을 포함해 PD 분리와 대규모 EP를 지원한다.
결과적으로 EP는 가중치를 분할하는 기준과 이로 발생하는 통신 방식의 차이로 최적화한건데
MoE는 구조적으로 공통 네트워크 Attention + 수많은 전문가 네트워크 FFN으로 이루어져있다.
일반 병렬화 TP는 전체 레이어를 다 쪼개고 Attention, FFN 다 쪼갠뒤에 모든 GPU가 하나의 거대한 신경망 행렬을 조각내서 나누어 가진것이고
EP는 FFN 단위만 독립적으로 쪼개서 예를들어 전문가가 8개, GPU가 8대라면 1대당 전문가 1개씩 통째로 저장해 공통 영역인 Attention은 쪼개지 않고 DP로 복제하여 처리한다.
DP Attention, Data Parallel Attention - MLA 전용 최적화 #
MLA는 KV를 압축해서 저장하는데 그런데 TP로 어텐션을 나누게 되면 이 압축된 KV를 각 GPU가 중복 보관하게 되는 경우가 발생한다. 압축해서 아낀 메모리를 복제로 다시 날리는 샘인 것이다.
그래서 어텐션만 따로 취급하기로 했다. 어텐션 층은 TP대신 DP(데이터 병렬)로 처리한다. 즉 GPU마다 다른 요청들을 맡아 어텐션을 온전히 수행하고 MoE층에서만 EP로 협력한다.
어텐션 층: GPU0=요청 1~16, GPU1=요청 17~32, ... (각자 자기 KV만 보유)
MoE 층: all-to-all로 전문가 병렬
여기서 발생하는 트레이드오프는 소배치에서는 손해라는 것이다.
데이터 병렬 어텐션은 저지연, 소배치 용도에서는 권장되지 않는데 대배치의 고처리량 시나리오에 최적화 되어있기 때문이다
왜냐면 DP는 GPU마다 독립적인 요청 묶음으로 처리하므로 요청이 적으면 GPU가 논다. TP는 요청 하나를 여러 GPU가 나눠 처리하므로 소배치에서도 전부 일한다. 트래픽 규모에 따라 정답이 갈린다.
# SGLang에서 DeepSeek 계열 서빙 시
--enable-dp-attention --dp-size 8
병렬화 선택 가이드 #
| 전략 | 나누는 축 | 통신량 | 적합한 범위 | 주의 |
|---|---|---|---|---|
| TP | 행렬(폭) | 큼 (층마다) | 노드 내부 | 노드 넘기면 재앙 |
| PP | 층(깊이) | 작음 | 노드 간 | 파이프라인 버블 |
| EP | 전문가 | all-to-all | MoE 필수 | 부하 불균형 |
| DP 어텐션 | 요청 | 없음 (어텐션 층) | MLA + 대배치 | 소배치에 부적합 |
Example #
사례1, 8 x H200 1노드, 70B 급 #
--tp-size 8 # 노드 내부이므로 NVLink로 TP 8까지 안전하다.
사례2 (12노드 96 GPU, DeepSeek V4급 MoE) #
프리필 인스턴스: TP=4 정도의 작은 랭크 여러 개
디코드 인스턴스: 대규모 EP (수십 랭크) + DP 어텐션
그 사이를 PD 분리로 연결 <- 밑에서 더 언급함
GB200 NVL72 실험에서는 디코드에 48랭크의 대규모 EP를, 프리필에서는 고정밀 설정 인스턴스당 4랭크 저정밀시 2랭크를 사용했다. 프리필은 작은 랭크, 디코드는 큰 랭크라는 비대칭이 핵심이다 왜 그런지는 아래에서 더 알아보자.
PD 분리 - Prefill Decode 갈라놓기 #
성격이 다른 두 작업을 같은 GPU에서 돌리는 것이 애초에 이상하지 않은가? 라는 질문으로시작해보자
왜 분리하는가 #
청크 프리필로도 안풀리는 것에 대해서 보자.
청크 프리필을 큰 프롬프트가 들어왔을때 디코드 작업의 간섭을 줄일 수 있었지만, 근본적으로 프리필과 디코드는 최적 설정이 정 반대다.
- Prefill: compute-bound이며, 최적 배치가 작아도 된다. 이미 토큰이 많고 TP 소규모가 최적의 병렬화며 TTFT가 지연지표고 양자화 이득은 중간이다.
- Deocde: memory-bound이며, 최적배치는 크게 (GPU를 채워야한다.) EP 대규모가 최적의 배치이며 지연지표는 ITL, 양자화 이득은 크다.
같은 GPU에 두면 어느 쪾에도 최적이 아닌 타협점에만 머무르게 된다.
해법으로는 GPU 풀을 역할별로 나눈다. #
[요청]
↓
┌────────────────┐
│ 프리필 인스턴스 │ TP=4, 작은 배치
│ (2~6 노드) │ 계산 집약
└────────┬───────┘
│ KV 캐시 전송 ★
↓
┌────────────────┐
│ 디코드 인스턴스 │ EP=48, 큰 배치
│ (10~12 노드) │ 메모리 집약
└────────┬───────┘
↓
[스트리밍 출력]
핵심 난재는 KV 캐시 전송이다. 프리필이 만든 수 GB의 KV를 디코드 노드로 옮겨야하고 이게 느리면 분리한 의미가 없어진다.
여기서 Mooncake Transfer Engine이 등장한다. Mooncake는 프리필-디코드 분리를 위한 KV Cache시 전송에 사용되는 전송 엔진이며 NVLink를 지원하기 위해 DeepEP와 유사한 기법을 사용한다.
각 풀을 자기 성격에 맞게 독립적으로 튜닝을 할 수 있어 이 해법이 통하게 된다.
- 프리필 풀: TTFT SLO에 맞게 규모 조정, 배치 작게, TP wkrrp
- 디코드 풀: 처리량 극대화, 배치 크게, EP 크게, 양자화 공격적으로
- 비율을 트래픽에 맞춰 조절할 수 있다: 입력이 긴 워크로드면 프리필 비중을 늘리고, 입력이 짧고 요청이 많으면 디코드 비중을 높인다.
GB200 NVL72에서 제공받은 14개 노드 중 12개를 디코드에, 나머지를 프리필에 사용했는데, 이는 18개의 노드가 있을때 프리필 6개 디코드 12개를 쓰는 실제 사례를 대략 모사한 것이다.
프리필 1: 디코드 2 정도가 일반적 출발점이라는 감각을 얻을 수 있다.
효과 - 이장에서 가장 인상적인 값 예시 #
| 환경 | 성능 | 배수 |
|---|---|---|
| H100 96장, PD + 대규모 EP | 노드당 입력 52.3k / 출력 22.3k tok/s | TP 대비 출력 5배 |
| GB200 NVL72, PD + 대규모 EP | GPU당 디코드 7,583 tok/s | H100 대비 2.7배 |
| GB200 + FP8 어텐션 + NVFP4 MoE | GPU당 입력 26,156 / 출력 13,386 tok/s | H100 대비 프리필 3.8배, 디코드 4.8배 |
FP8 어텐션과 NVFP4 MoE를 사용할 때, SGLang은 2000토큰 입력 시퀀스에 대해 DeepSeek V3/R1에서 GPU당 프리필 26,156, 디코드 13,386 토큰/초를 달성했으며 이는 H100 설정 대비 각각 3.8배와 48배 빠른것이다. 전통적인 BF16 어텐션과 FP8 MoE로도 18,471과 9,087 토큰/초를 달성한다.
마지막 두줄을 주목해보자 정밀도를 낮추는 것 만으로는 프리필 18,471 -> 26.156 (약 1.4배) 인데, 다음장에서 양자화가 왜 보여주는지 보여 주는 숫자다
TradeOff
- 시스템 복잡도가 크게 증가함: 두 종류의 클러스터, 전송 계층, 라우팅 로직이 필요하다.
- KV 전송이 새로운 병목이자 새로운 장애 지점이 된다. RDMA/NVLink 급 네트워크가 없으면 이득이 사라진다.
- 소규모에서는 순손해다. GPU가 8장 뿐이라면 프리필 3장, 디코드 5장으로 나눠 봐야 양쪽 다 비효율이다.
경험칙: 최소 수십 장 규모, 안정적 고트래픽, 고속 인터커넥트 세 조건이 모두 맞을대만 검토하라
투기적 디코딩 Speculative Decoding #
Decode는 memory bounded라 계산 자원이 남는다.
투기적 디코딩은 거대 언어 모델 LLM의 텍스트 생성 속도를 높이는 추론 최적화 기술로 빠르고 가벼운 초안 모델이 여러 개의 다음 토큰을 먼저 예측하도록 하고 느리지만 정확한 대상 모델 (target)이 한 번의 병렬 연산으로 이를 검증해 속도를 극대화한다.
디코드는 한 번에 토큰 하나씩만 만든다. 그런데 그 한 토큰을 만들려고 모델 가중치 수백 GB를 모두 메모리에서 읽는다. 어차피 읽을 거면 여러 토큰을 한번에 확정할 수 없나?
초소형 모델의 가중치만 로딩해서 만든뒤에 마지막에 대상모델이 한 번의 병렬 연산으로 이를 검증하는 것이다.
수백gb, 수백gb, 수백 gb, 수백 gb로 만들기보단 수gb, 수gb, 수gb, 수gb, 수백gb로 하자는 얘기
"제안하고 한꺼번에 검증한다." 로 해법이 시작된다. 드래프트(초안) γ개의 토큰을 제안해놓고 타깃 모델이 한 번의 패스로 검증하고, 일치하는 접두사 α개가 수락되어 α+1개 토큰이 방출되며, 첫 거부 토큰부터 다시 드래프팅이 시작되도록 하는 아이디어가 나왔다.
드래프트(작고 빠름): "the cat sat on the mat" 6개 제안
↓
타깃(크고 정확함): 6개를 한 번의 포워드 패스로 동시 검증
↓
"the cat sat on" 까지 일치 → 4개 수락 + 타깃이 만든 1개 = 5개 방출
"the" 부터 불일치 → 여기서부터 다시 드래프팅
결정적으로 모든 드래프트 토큰은 방출 전에 타깃 모델 검증을 거치므로 투기적 디코딩은 최종 출력 품질을 바꾸지 않으면서 추론을 가속하는 것이다.
이건 근사가 아니라 정확하게 무손실이다. 이 성질이 투기적 디코딩을 특별하게 만든다.
타깃 모델이 한 번의 포워드 패스에서 6개 위치를 동시에 검증하는 비용은 1개를 검증하는 비용과 거의 같다.
왜냐하면 둘 다 가중치 전체를 한 번 읽기 때문이다. 계산량은 6배지만 계산은 남아돌기 때문에 시간이 거의 안늘어난다.
사전을 책상에 올리는데 10분이걸리고 이왕 올린김에 단어 6개를 찾는것과 1개를 찾는것의 차이는 미미하다.
해법의 변형 - 드래프트를 만드는 방법들 #
흔한 접근으로 작은 드래프트 모델, 다중 토큰 예측 MTP, Medusa식 다중 헤드 예측 그리고 EAGLE-3, DFlash, DSpark 같은 특징 수준 드래프팅 방법등이 더 있다.
| 방법 | 메커니즘 | 특징 |
|---|---|---|
| 작은 드래프트 모델 | 같은 계열의 소형 모델을 따로 돌림 | 타깃이 클수록 유리 |
| MTP | 학습 때 붙인 다중 토큰 예측 헤드를 재활용 | 별도 모델 불필요 |
| Medusa | 여러 예측 헤드를 병렬로 붙임 | 구현 간단 |
| EAGLE-3 | 타깃의 은닉 상태를 입력으로 받는 경량 드래프터 | 2026년 프로덕션 표준 |
| n-gram | 컨텍스트에서 반복 패턴을 찾아 복사 | 코드 편집에 강함 |
DeepSeek-V3가 MTP 학습을 목표로 넣은 것, GLM-5.2가 MTP 층을 개선해 수용 길이를 4.56 -> 5.47로 올린 것이 전부 이 절의 이야기다. 학습 때 붙인 헤드가 서빙 때 드래프터가 된다. 모델 설계와 서빙 설계가 하나의 결정이었던 것이다.
효과의 함정 - 배치가 커지면 이득이 사라진다. #
vLLM에서 5가지 투기적 디코딩 변형 (n-gram, EAGLE, EAGLE-3, 드래프트 모델, MTP)을 4개 모델과 6개의 워크로드에 체계적으로 평가한 결과 배치크기 1에서 EAGLE은 Llama-3-70B에 대해 최대 1.96배를 달성했지만, 배치 128에서는 1.21배로 떨어졌다 높은 동시성에는 시스템이 계산 바운드가 되어 GPU가 투기에 쓸 유휴 용량이 줄어든다.
이것이 투기적 디코딩의 핵심 트레이드 오프가 되는 것 이다.
배치 1 : 계산 자원 99% 유휴 → 투기가 공짜 → 2배 가속 ✅
배치 128 : 계산 자원 이미 포화 → 투기가 자리를 뺏음 → 1.2배 ⚠️
배치 256+ : 투기가 순손해가 될 수 있음 ❌
배치크기 32를 넘으면 EAGLE-3을 건너뛰는것을 추천한다. 높은 동시성에는 드래프트 헤드 수락률이 떨어지고 스텝당 두 번의 포워드 패스 비용을 치르게 된다.
32K 토큰을 넘는 롱 컨텍스트 디코드도 드래프트 헤드의 컨텍스트 에측이 어려워져 수락률이 저하된다.
검증이 실행시간의 42~95%를 차지하며, 타깃 모델의 포워드 패스가 병목이다.
EAGLE-3, 3.1 #
EAGLE 계열이 표준인데 보통, 그 이유는 드래프터가 타깃의 내부 상태를 본다는 점이다.
단순히 작은 모델을 따로 돌리는게 아니라 타깃 모델의 은닉 상태를 입력받아 다음을 에측하므로 타깃과 훨씬 잘 일치한다.
2026년 5월에 나온 EAGLE 3.1은 흥미로운 문제를 고쳤다. EAGLE 팀은 이 취약성의 원인을 어텐션 드리프트로 규명했다.
투기 깊이가 늘어날수록 드래프터가 싱크 토큰에서 자기가 생성한 토큰 쪽으로 어텐션을 점차 옮기는 현상이다.
# SGLang
--speculative-algorithm EAGLE3 \
--speculative-draft-model-path <드래프터 경로> \
--speculative-num-steps 5 \
--speculative-eagle-topk 8
speculative_num_steps를 5로 speculative_eagle_topk를 8로 설정하라 수락률 0.82에 N-5라면 스텝당 약 3.5~4개의 수락 토큰이 나온다.
SGLang의 RadixAttention KV Cache는 EAGLE-3 드래프트 검증과 완전히 호환된다.
주의! - 드래프터는 모델별로 따로 학습되어야한다. 2026년 기준 사전 학습된 EAGLE-3 헤드는 Llama 4 Scout/Maverick, Qwen 2.5 75B, DeepSeek V3 671B, GLM-5.1 등에 존재하며 공식 EAGLE 학습 저장소를 쓰면 4 x H100으로 약 2~4시간만에 커스텀 헤드를 학습할 수 있다. 쓰려는 모델의 드래프터가 있는지 먼저 확인하라.
- 대화형 동시 사용자 적은 사례: 강력 추천, 배치가 작아서 이득이 최대다.
- RAG, 중간 동시성: 배치 크기를 재보고 32 이하면 키자
- 대규모 배치 서빙: 대체로 손해다 처리량 최적화 구간에서는 끄는 게 낫다.
양자화 #
디코드가 느린 이유는 가중치를 메모리에서 읽는 시간때문이다.
그렇다면 가중치를 작게 만들면? 그만큼 빨라질 것이다.
| 방식 | 대상 | 비트 | 특징 |
|---|---|---|---|
| FP8 | 가중치 + 활성값 | 8 | Hopper 이상 하드웨어 가속. 품질 손실 작음 |
| NVFP4 / MXFP4 | 가중치 (주로 MoE) | 4 | Blackwell 세대. 블록 단위 스케일 공유 |
| AWQ | 가중치 | 4 | 활성값 크기를 보고 중요한 채널을 보호 |
| GPTQ | 가중치 | 4 | 층별로 오차를 최소화하며 순차 양자화 |
| KV 캐시 FP8 | KV 캐시 | 8 | 1장 공식의 바이트 항을 절반으로 |
| QAT | 학습 단계부터 | 4 | 압축 후 손실이 가장 적음 |
이 해법이 통하는 이유는 두 가지가 있다.
- LLM 가중치는 중복이 많다. 대부분의 값이 좁은 범위에 몰려 있어, 비트를 줄여도 표현력 손실이 생각보다 적다.
- 양자화는 메모리와 계산을 동시에 줄인다. 4비트 가중치는 읽는 시간이 1/4, blackwell 같은 하드웨어는 저정밀 텐서 코어로 계산도 빠르다.
AWQ가 GPTQ보다 종종 나을때가 있는데, AWQ는 활성값이 큰 채널에 대응하는 가중치는 중요하다라는 관찰에서 출발해 그 채널을 보호한다. 모든 가중치를 균등하게 취급하지 않는 기저라서. 그런 스노우볼이 잘 작용이 될때 시너지가 좋다.
- GB200에서 BF16 어텐션 + FP8 MoE 대비 FP8 어텐션 + NVFP4 MoE로 바꾸면 프리필 18,471 -> 26,156, 디코드 9,087 -> 13,386 토큰/초 약 1.4배정도가 개선된다.
- 튜닝 사다리에서 FP8 KV 캐시 추가는 6,200 -> 7,100 tok/s (약 15%)
- Kimi K2.6의 INT4는 QAT 기반이라 추론 속도 약 2배, GPU 메모리 50% 절감에 품질 손실이 미미하다고 보고했다.
TradeOff #
- 품질 저하가 워크로드마다 다르다. 일반 챗에서는 안 보이던 저하가 수학, 코딩에서는 드러나는 경우가 흔하니까 반드시 자기 평가셋으로 재야한다.
- 서빙 제공자가 몰래 양자화를 하는데, 서버리스 호스팅은 대개 활성값을 FP8로 양자화하고 공개 가중치를 직접 돌린 결과와 미묘하게 달라진다. 벤치마크가 재현되지 않을 때 첫 번째로 의심할 항목이다.
- 하드웨어 지원이 필수다 FP8은 Hopper 이상, NVFP4는 Blackwell 이라서 지원 없는 GPU에서 지원한다고 나와도 소프트웨어 애뮬레이션이라 오히려 느릴 수 있다.
- 실제로 릴리즈 노트에서도 특정 하드웨어에서 FP8 모델이 실패하는 알려진 이슈가 기재된다.
구조화 출력 - JSON을 확실하게 받기 #
에이전트와 도구 호출이 표준이 되면서 모델이 유효한 json을 뱉는가가 곧 시스템의 안정성이 되었다.
그로인해 생기는 문제가 있는데 프롬프트로 "JSON만 출력해" 라고 부탁하는 방식은 확률적이다. 모델이 앞에 설명을 붙이거나 마지막에 중괄호를 빠트리거나 스키마에 없는 필드를 만든다면? 파싱 오류와 불안정한 애플리케이션이 될 수 있다.
99% 성공률이라도 하루 100만 요청이면 1만건이 실패한다. 에이전트가 20단계를 도는데 각 단계가 99%라면 전체 성공률은 82% 이다.
해결법 #
제약 디코딩을 사용하는 것이다. 모델의 출력 확률 분포에서 문법상 불가능한 토큰의 확률을 0으로 만드는 것이다.
현재 상태: {"name": "김
다음에 올 수 있는 것: 임의의 문자, 또는 닫는 따옴표
불가능: [ { } , : 등 구조 토큰
→ 로짓 벡터에서 불가능한 토큰들을 -inf로 마스킹
→ 샘플링하면 반드시 유효한 토큰만 나옴
확률이 부탁이 아니라 구조적 보장이 되어 100%이다.
구현 방식 #
1. FSM 방식 - Outlines #
주어진 정규 표현식이나 문법에 대해 오토마톤의 각 상태를 허용 토큰 집합으로 매핑하는 인덱스를 미리 계산해서, 스텝별 검사 조회를 한 번으로 바꾼다.
tradeoff로는 outlines는 FSM 접근을 개척했지만 복잡한 스키마 컴파일 시간이 40초에서 10분이상까지 걸리는 문제가 있었다. JSONSchemaBench 벤치마크에서 테스트된 엔진 중 준수율이 가장 낮았는데, 주로 이 타임아웃 문제 때문이다.
근본적인 한계로 FSM 엔진은 토큰 유효성을 완전히 사전 계산하지만 재귀를 처리하지 못하고 컴파일 시간이 길 수 있다. 중첩된 객체가 임의 깊이로 반복되는 스키마를 FSM으로 표현할 수 없다. 유한 상태이기 때문이다.
CFG 방식 - XGrammer, llguidance #
문맥 자유 문법 CFG(Classifier-Free Guidance)와 푸시다운 오토마톤을 써서 재귀를 다루는 방식이다
XGrammer의 핵심 아이디어는 토큰을 두 부류로 나누는 것이다.
- 문맥 독립 context-independent 토큰: 현재 파싱 스택이 어떻든 유효성이 이미 정해진 토큰이다. 예를 들어 JSON 문법에
"안쪽의 일반 문자열 토큰은, 지금 객체가 3중으로 중첩되어있든 배열안이든 상관없이 항상 유효하다 미리 계산해둘 수 있다. - 문맥 의존 context-dependent: 토큰 스택을 봐야 아는 토큰이다 닫는 중괄호가 대표적이고 지금 열려있는 객체가 있어야만 유효하고 없으면 문법 위반이다. 실행 시점에 검사할 수 밖에 없다.
현재 상태: {"user": {"name": "김
스택: [ 객체 → 객체 → 문자열 ]
토큰 "철" → 문맥 독립. 문자열 안이면 항상 OK. (사전 계산 대상)
토큰 "}" → 문맥 의존. 문자열을 먼저 닫아야 하므로 지금은 불가.
토큰 "\"" → 문맥 의존. 문자열 닫기이므로 지금은 가능.
어휘 사전이 15만개라면 그중 압도적 다수가 문맥 독립인데 구조를 만드는 토큰 괄호나 따옴표같은건 수십개뿐이고 나머지는 전부 내용물이다. 그래서 15만개중 실시간으로 판단해야할 것은 수십 수백개에 불과하다.
XGrammer는 마스크의 문맥 독립 부분을 사전 계산하고 문맥 의존 검사를 GPU 실행과 겹쳐서 JSON 스키마와 문맥 자유 문법에 대해 토큰당 40마이크로초 미만의 오버헤드를 보고한다.
후속작언 XGrammer2는 전작의 PDA/스택 기반 파서를 Earley 파서로 바꾸고 JIT 컴파일등 최적화를 추가해 더 빠른 CFG 컴파일과 마스크 생성을 달성했다.
점프 포워드 - SGLang #
위 방식과 직교 orthogonal인데, 이 기법은 XGrammer가 채택한 제약 디코딩과 직교하여 XGrammer는 점프 포워드 디코딩을 추가 지원해서 둘을 결합하면 효율이 더 개선됨을 보였다.
제약 디코딩: 불가능한 토큰을 막는다 (정확성)
점프-포워드: 확실한 토큰은 건너뛴다 (속도)
↓
결합하면 정확하면서도 빠름
에이전트 시대의 새 요구 - 동적 구조화 생성 #
문제는 스키마가 매번 달라진다는 것이다.
도구 호출은 스키마가 매 요청마다 달라진다. 사용자가 어떤 도구를 활성화했느냐에 대한 유효한 JSON 구조가 바뀐다. 미리 컴파일해 둘 수 있다.
현황 - 백엔드별 성숙도 #
XGrammer-2 평가에서 SGLang v0.5.3.post3 + Outlines v0.2.11 조합은 도구 호출 같은 동적 구조화 생성을 아예 지원하지 못했는데 llguidance v1.2.0은 지원하지만 Qwen3-0.6B에서 빈 출력이 나오거나 영어에서 다른 언어로 드리프트 하는 현상이 발생했다.
실무 교훈 - 구조화 출력 지원 이라는 체크박스만 보고 고르면 안되고 정적 스키마와 동적 스키마는 완전히 다른 요구사항이기에 백엔드마다 성숙도가 다르다 에이전트를 만든다면 반드시 동적 케이스로 테스트하라.
- 고정된 JSON 스키마 몇 개 XGrammar (기본값)
- 재귀적/복잡한 스키마 XGrammar 또는 llguidance (CFG 필수)
- 도구 호출 (동적 스키마) XGrammar 계열, 반드시 실측 검증
- 구조 비중이 매우 큰 출력 SGLang + 점프-포워드
TradeOff
- 문법 컴파일 비용 (스키마가 자주 바뀌면 부담)
- 토큰당 오버헤드가 문법 복잡도에 따라 변동
- 품질 영향: 제약이 모델의 자연스러운 분포를 왜곡할 수 있다. 특히 모델이 원래 쓰고싶어 하던 표현이 막히면 이상한 우회로를 택한다. 스키마를 지나치게 촘촘히 걸면 내용 품질이 떨어질 수 있다.
실전 Example #
가상 사례로 튜닝을 배워보자 위에서 배운 내용들을 종합해서 실제 의사결정 순서로 재조립하자.
GPU 실습 환경은 음 돈이 많으면 가능할듯 그래도 걍 일단 가정하고 해보는거
튜닝 순서표 #
성능 문제를 만났을때 아무 설정이나 만지면 안된다 효과 크기 순서가 있다.
0단계 워크로드를 측정한다 ← 이걸 건너뛰면 나머지가 전부 추측
↓
1단계 엔진과 병렬화를 고른다 ← 나중에 바꾸려면 재설계
↓
2단계 연속 배칭 + 청크 프리필 ← 기본값. 안 켜져 있으면 켠다
↓
3단계 접두사 캐싱 / RadixAttention ← 워크로드가 맞으면 단일 최대 이득
↓
4단계 양자화 (FP8부터) ← 하드웨어가 되면 거의 공짜
↓
5단계 배치 파라미터 튜닝 ← SLO에 맞춰 곡선 위 지점 선택
↓
6단계 투기적 디코딩 ← 저동시성일 때만
↓
7단계 PD 분리 / 대규모 EP ← 수십 GPU 이상에서만
↓
8단계 계층 KV 캐시 ← 재사용률이 높고 GPU가 부족할 때만
실제 튜닝 예시 Llama 3.1 8B, A100 80GB, ShareGPT 는 이렇다.
기본 4,200 -> 청크 프리필 4,800 -> 접두사 캐싱 5,500 -> max-num-batched-tokens 튜닝 6,200 -> FP8 KV Cache 7,100 tok/s
기본 설정에서 시작해 네 단계로 1.7배이다. 그리고 이 네단계 전부가 설정 파일 수정 수준이고 코드 작성이 아니다.
0단계 - 워크로드 측정 (중요) #
튜닝 전에 자기 트래픽의 네 가지 숫자를 알아야한다
- 입력 길이 분포 (중앙값 P95), 프리필 부담 결정을 위해 알아야하고 이 값이 크면 청크 프리필 필수, PD 분리 검토를 한다.
- 출력 길이 분포, 디코드 부담 결정을 위해 필요하고 배치와 양자화가 중요하다.
- 접두사 공유율, 캐싱 이득 결정을 위해 필요하며 SGLang 선택 근거가 된다 (Radix)
- 동시성 (피크 기준) 배치 크기를 결정하기 위해 필요하며 투기적 디코드 손익이 갈린다.
접두사 공유율을 재는 법
# 프로덕션 로그에서 프롬프트를 샘플링해 공통 접두사 길이를 측정
def 공유율_측정(프롬프트들, 토크나이저):
토큰들 = [토크나이저.encode(p) for p in 프롬프트들]
총_토큰 = sum(len(t) for t in 토큰들)
# 정렬하면 인접한 것끼리 접두사가 겹침
토큰들.sort()
공유_토큰 = 0
for i in range(1, len(토큰들)):
공통 = 0
for a, b in zip(토큰들[i-1], 토큰들[i]):
if a != b: break
공통 += 1
공유_토큰 += 공통
return 공유_토큰 / 총_토큰
판단 기준은 접두사 중첩이 60퍼 이상인 워크로드는 75~95% 의 캐시 적중률을 본다
공유율 0~20% → vLLM으로 충분. RadixAttention 이득 거의 없음
공유율 20~60% → 둘 다 시도해 볼 가치 있음
공유율 60%+ → SGLang 강력 권장
Example - 사내 코딩 어시스턴트 #
상황
- 엔지니어 200명, 피크 동시 접속 25명
- 프롬프트 500 ~ 40,000 토큰 (파일 붙여넣기)
- 시스템 프롬프트 + 사내 코딩 규약 = 3,000토큰 (전원 공유)
- 하드웨어 8 x H200 (141GB x 8 = 1,128GB)
- SLO: TTFT p95 < 1초, ITL < 50ms
0단계 측정 결과 #
입력 중앙값 2,100 / p95 28,000 토큰
출력 중앙값 400 / p95 1,800 토큰
접두사 공유율 약 35% (시스템 프롬프트 + 자주 열리는 파일)
피크 동시성 25
1단계 엔진과 병렬화 #
- 공유율은 35%로 애매하지만, 에이전트형 도구 호출이 늘어날 예정이라면 공유율이 올라갈 것이라 SGLang을 선택할 수도 있을거같다
- 노드 내부이므로
--tp-size 8
2단계 배칭 #
- p59 입력이 28,000토큰 -> 청크 프리필이 필수, 짧으면 질문자가 몇 초씩 멈춘다.
3단계 캐싱 #
- RadixAttention은 SGLang 기본 활성화
- 프롬프트 재설계: 변하는 값 (타임스탬프, 사용자명)을 시스템 프롬프트 뒤로 이동 -> 3,000 토큰 전체가 다 캐시 대상이 됨
4단계 양자화 #
- H200은 Hopper -> FP8 지원, 가중치와 KV 캐시가 모두 FP8이다.
- KV 캐시 FP8로 1장의 공식 바이트항이 2-> 1 -> 동시 사용자 2배
5단계 배치 파라미터 #
- ITL 50ms SLO가 있으므로 배치를 무한정 키워서는 안된다.
- 동시성 25 기준으로
--max-running-requests를 여유있게 잡되,--chunked-prefill-size로 지연을 평준화한다.
6단계 투기적 디코딩 #
- 동시성 25 -> 32 이하이므로 이득 구간이다. EAGLE-3 적용
- 코드 편집 워크로드라 n-gram도 후보 (반복 패턴이 많음)
7~8단계 - GPU 8장은 PD 분리 규모가 아니고, 건너뜀 #
최종 설정은 아래와 같다.
python -m sglang.launch_server \
--model-path <모델> \
--tp-size 8 \
--chunked-prefill-size 8192 \
--kv-cache-dtype fp8_e5m2 \
--quantization fp8 \
--speculative-algorithm EAGLE3 \
--speculative-draft-model-path <드래프터> \
--speculative-num-steps 5 \
--speculative-eagle-topk 8 \
--mem-fraction-static 0.90 \
--enable-metrics \
--host 0.0.0.0 --port 30000
기준선 1.0×
+ 청크 프리필 (지연 안정화) TTFT p95 대폭 개선
+ 접두사 캐싱 (프롬프트 재설계) 프리필 3,000토큰 절약
+ FP8 가중치 + KV 동시 사용자 ~2배
+ EAGLE3 (저동시성) ITL ~1.5-2×
사례2 - 문서 QA / RAG #
상황
- 계약서 20K 토큰에 여러 사용자가 각기 다른 질문
- 문서 종류 1,000개, 인기 문서 50개에 트래픽 80% 급증
- 하드웨어 4 x H100
핵심 결정 #
공유율이 압도적으로 높다. 같은 문서에 대한 질문은 99% 공유한다. -> SGLang
그런데 문제가 있다 문서가 1,000종이면 GPU메모리 위에 전부 못 올리고 인기 문서 50개는 캐시에 살아남아서 계속 축출되도록 하게해보자
이 해법을 기반으로 세 가지 순서대로 검토를 한다.
- 캐시 인지 라우팅 - 문서 ID를 해싱해서 항상 같은 워커러 보낸다. 4대 서버라면 각 서버가 250종만 담당하면 되므로 4배가 개선된다.
- 계층 캐시 - 인기없는 문서의 KV를 CPU DRAM으로 내려서 손익 분기 계산은 필수지만, 20K토큰 KV가 몇GB이고 PCIe로 몇 ms인지 재서, 재계산보다 싼지확인한다.
- CacheBlend - 검색 청크 순서가 매번 바뀌는 진짜 RAG라면 접두사 캐싱이 무력화 된다. 비접두사 재사용이 필요하다.
주의할점은 이 워크로드는 프리필이 압도적이고 입력 20,000 출력 300. 그래서
- 투기적 디코딩 이득이 적다 (디코드가 짧음)
- 프리필 최적화에 자원을 집중해야한다.
- 극단적으로 프리필 전용 인스턴스를 늘리는 PD 분리도 검토 대상이다.
사례3 - 대형 MoE 대규모 서빙 #
DeepSeek V4 1.6T / 49B 활성 또는 GLM-5.2 744B / 40B 활성
12 노드 x 8 GPU = 96장 NVLink / InfiniBand
목표는 원가 최소화, 개별 지연보다 처리량
투기적 디코딩은 대배치기 때문에 손해
PD 분리는 필수
대규모 EP도 필수
DP 어텐션은 대배치에 적합함
최적화 목표는 TTFT보단 처리량 / 원가
구성요소
프리필 인스턴스: 4노드, TP=4 랭크 다수
디코드 인스턴스: 8노드, EP=48급 + DP 어텐션
KV 전송: Mooncake Transfer Engine (RDMA/NVLink)
부하 분산: EPLB로 전문가 불균형 보정
양자화: FP8 어텐션 + NVFP4/FP8 MoE
기대 성능 #
H100 96장 구성에서 노드당 52.3k 입력 / 22.3k 출력 토큰, 출력 100만 토큰당 $0.20 원가로 순수 TP 대비 출력 처리량 최대 5배다.
원가 관점에서 결론: 공식 api가 출력 100만 토큰당, 1.10 수준이라면 자체호스팅 원가 $0.20은 약 4~5배 차이다. 다만 이건 GPU가 항상 포화 상태일때고 트래픽이 절반이되면 원가는 두 배가 된다.
자체 호스팅의 손익 분기의 진짜 변수는 가동률로 96장을 24시간 놀리지 않고 채울 수 있는 트래픽이 있는가. 없으면 API가 싸다.
벤치마킹 방법론 - 자기 숫자를 만든느법 #
흔한 실수 다섯가지를 짚어보겠다.
- 워밍업 안함, 첫 요청은 CUDA 그래프, JIT 컴파일, 커널 오토튜닝때문에 비정상적으로 느려서 최소 수십 요청을 버리고 시작하는걸 추천한다.
- 캐시를 안끄거나 안켬, 같은 프롬프트를 반복 전송하면 RadixAttention이 100퍼센트 적중해 환상적인 숫자가 나온다 콜드 스타트를 재려면
--disable-radix-cache, 실제 운영을 모사하려면 실제 트래픽 분포를 재현해야한다. - 평균만 보는 것, 평균 TTFT 400ms인데, p99가 8초인 시스템은 실패한 시스템이다. 반드시 p50 / p95 / p99랑 함께 보라
- 부하를 고정 동시성으로만 준다. 현실 트래픽은 포아송 분포에 가깝게 몰려소 온다. 고정 동시성 테스트는 큐 폭주상황을 재현하지 못한다.
- 총 처리량만 최적화, 1.4절의 goodput을 기억하라 SLO를 위반한 요청은 세면 안된다.
측정해야할 지표 메트릭은 SGLang은 Prometheus와 호환이라 /metrics 엔드포인트를 제공한다.
sglang_num_queue_reqes큐 깊이sglang_token_usagekv cache 사용률sglang_cache_hit_rate(radix attention 효과)sglang_time_to_first_token_secondsTTFT- 여기에
nvidia-smi dmon -s pum -d 10을 병행해 GPU 활용률과 메모리 압력을 본다.
대시보드 읽는법 진단표 몇개 예시 #
- GPU 활용률이 낮고 큐가 긴 증상에서 가능한 원인은 스케줄링/메모리 문제다 GPU부족이 아니고, token_usage가 적은지 확인하라.
token_usage가 100%이고 큐가 길다면 KV캐시가 부족한 것이고 양자화, mem-fraction 상향, TP 확대를 권장한다.cache_hit_rate낮음, 이는 프롬프트 설계문제라 변하는 값이 앞에있는지 확인하자.- TTFT p99 튀는 현상, 긴 프리필이 블로킹될때가 있는지 확인하고 청크 프리필 켰는지, 청크 크기도 확인하자/
- ITL 불규칙, 배치 구성 변동,
max-num-seqs를 조정하라
진단 가능한 원칙 하나를 기억하자.
"GPU 활용률이 낮은데 요청 큐가 길면 원인은 자동으로 더 약한 모델이나 작은 GPU가 아니라 먼저 스케줄링, 캐시할당, 워크로드 형태를 의심해야한다."
GPU 용량 산정 - 몇 장 사야하징 #
가상 계산을 해볼거다 이런 계산을 할 줄 아는것이 좀 중요한데
피크 시간에 초당 20건의 요청, 평균 입력 3,000 / 출력 500 토큰 70B급 모델 TTFT p95 1초를 목표로 한다면
[1] 필요한 처리량
입력: 20 req/s × 3,000 = 60,000 tok/s
출력: 20 req/s × 500 = 10,000 tok/s
[2] 동시에 살아 있는 요청 수 (Little's Law)
평균 요청 지속 시간 ≈ TTFT + (출력 500 × ITL 40ms) = 0.5 + 20 = 20.5초
동시 요청 = 20 req/s × 20.5s ≈ 410건
[3] KV 캐시 소요 (1.2절 공식, 평균 컨텍스트 3,500토큰, FP8)
요청당 ≈ 2 × 80 × 8 × 128 × 3,500 × 1바이트 ≈ 0.57 GB
410건 × 0.57 GB ≈ 234 GB
[4] 모델 가중치 (FP8 70B) ≈ 70 GB
[5] 총 필요 메모리 ≈ 234 + 70 + 여유 20% ≈ 365 GB
→ H200(141GB) 3장이면 메모리는 충분
[6] 처리량 검증 ← 여기서 대부분 부족해집니다
출력 10,000 tok/s를 3장으로? → GPU당 3,333 tok/s 필요
70B FP8 디코드로 이 수치는 빠듯 → 양자화 권장
메모리와 처리량은 결국 별개의 제약이고, 대개 처리량이 먼저 걸린다.
메모리만 보고 KV 캐시 문제네 하려다가 알고보면 양자화가 필요한 경우도 있고
또한 Little'Law 동시요청 수 = 도착률 x 평균 체류 시간이다. 출력이 길수록 체류시간이 길어지고 체류시간이 길수록 KV캐시가 더 필요하다 출력 길이가 KV 메모리를 결정하는 숨은 변수다.
| 용어 | 정의와 메커니즘 |
|---|---|
| goodput | SLO를 만족한 요청만 세는 처리량. 총 처리량만 보면 배치를 과도하게 키워 개별 사용자를 희생시키게 되므로, 실제 사업 가치에 대응하는 지표는 goodput |
| CUDA 그래프 | 수백~수천 개의 커널 실행 시퀀스를 한 번 녹화해 이후 한 명령으로 재생. 커널 런치 오버헤드 제거. 모양이 고정돼야 해서 배치 크기별로 여러 개를 미리 캡처하고 패딩. 기동 시간과 GPU 메모리를 소비 |
| CacheBlend | 접두사가 아닌 위치의 KV 블록을 재사용하는 기법. 소수 토큰만 선택적으로 재계산해 품질을 복구. 검색 청크 순서가 매번 바뀌는 RAG의 해법 |
| DP 어텐션 | 어텐션 층을 TP 대신 데이터 병렬로 처리. GPU마다 다른 요청을 맡아 자기 KV만 보유하므로 MLA의 압축 이점이 복제로 상쇄되지 않음. 대배치 전용, 소배치엔 부적합 |
| DeepEP / DeepGEMM / EPLB | 각각 MoE의 all-to-all 통신 라이브러리, 저정밀 GEMM 커널, 전문가 부하 분산기. 대규모 EP를 실용화하는 3종 세트 |
| EAGLE-3 / 3.1 | 타깃 모델의 은닉 상태를 입력받는 경량 드래프터. 타깃과 잘 일치해 수락률이 높음. 3.1은 어텐션 드리프트를 FC 정규화로 교정해 롱컨텍스트 수락 길이를 최대 2배로 |
| EP (전문가 병렬) | MoE 전문가를 통째로 GPU에 배정하고 토큰을 all-to-all로 라우팅. GPU당 메모리 부담이 줄어 KV 캐시 여유가 생김. 병목은 all-to-all 통신과 전문가 부하 불균형 |
| FlashAttention | 타일링 + 온라인 소프트맥스로 [N,N] 중간 행렬을 만들지 않고 SRAM 안에서 어텐션 수행. HBM 접근을 제곱에서 선형으로. 하드웨어 세대마다 재작성 필요(FA2/3/4) |
| FlashInfer | 서빙 특화 커널 라이브러리. 블록 희소 KV 어텐션, MLA, 캐스케이드 어텐션, plan/run 분리 로드밸런싱 등을 제공. FlashAttention의 상위 버전이 아니라 다른 범주 |
| ITL / TPOT | 토큰 간 간격. 디코드 성능 지표. 50ms 이하면 사람의 읽기 속도를 앞섬 |
| KV 캐시 | 생성 중 재사용하려고 저장하는 K·V 값. 크기 = 2 × 층수 × KV헤드 × 헤드차원 × 길이 × 배치 × 바이트. 동시 사용자 수와 서비스 원가를 직접 결정 |
| LMCache | 계층형 KV 오프로딩 레이어(HBM→DRAM→SSD→원격). 독립 데몬이라 엔진이 죽어도 캐시가 살아남고, 여러 인스턴스가 캐시 풀을 공유 |
| Little's Law | 동시 요청 수 = 도착률 × 평균 체류 시간. 용량 산정의 기본 도구. 출력이 길수록 체류가 길고 KV 메모리가 더 필요 |
| Mooncake | KV Cache 중심 분산 아키텍처. Transfer Engine(노드 간 KV 전송)과 Store(분산 저장소)로 구성. PD 분리의 전송 계층 표준 중 하나 |
| PagedAttention | OS 가상 메모리를 KV 캐시에 적용. 16토큰 단위 고정 블록을 필요할 때만 할당하고 논리 블록 테이블로 매핑. 외부 단편화 원천 차단, 동시 요청 2~4배 |
| PD 분리 | 프리필과 디코드를 별도 GPU 풀에 배치. 각자 최적 병렬화와 배치 설정을 가짐. KV 전송이 새 병목. 수십 GPU + 고속 인터커넥트 조건에서만 이득 |
| RadixAttention | KV 캐시를 라딕스 트리에 보관해 요청 간 접두사를 자동 재사용. LRU로 리프부터 축출, lock_ref로 사용 중 노드 보호, 캐시 인지 스케줄링으로 적중률 강화. 공유가 없으면 순손해 |
| SGL Router | 캐시 인지 로드 밸런서. 접두사가 같은 요청을 같은 워커로 보내 다중 서버에서도 적중률 유지. 라운드로빈은 캐시를 무의미하게 만듦 |
| TP / PP | 텐서 병렬(행렬을 잘라 나눔, 통신 많음, 노드 내부 전용) / 파이프라인 병렬(층을 나눔, 통신 적음, 버블 발생) |
| TTFT | 첫 토큰까지의 시간. 큐 대기 + 프리필로 결정. 사용자 체감 지연의 대부분 |
| XGrammar | CFG 기반 제약 디코딩 엔진. 토큰을 문맥 독립/의존으로 나눠 전자는 사전 계산, 후자는 GPU 실행과 겹쳐 처리해 토큰당 40µs 미만. 2026년 3월 기준 주요 엔진들의 기본 백엔드 |
| 거부 샘플링 | 투기적 디코딩에서 드래프트 토큰을 타깃 분포에 맞게 수락/거부하는 절차. 이 덕에 최종 출력이 정확히 무손실 |
| 계층 캐시 | KV를 HBM/DRAM/NVMe 3계층에 배치. 대역폭이 3,350 / 63 / 7 GB/s로 급락하므로 재계산보다 싼지 반드시 계산 |
| 메모리 바운드 | 계산이 아니라 메모리 대역폭이 병목인 상태. LLM 디코드가 여기 해당. 양자화·배칭·투기가 통하는 근본 이유 |
| 산술 강도 | 메모리 1바이트당 수행 연산 수. 디코드는 1 FLOP/Byte 근처로, GPU 성능의 대부분이 유휴 |
| 선점 (preemption) | 메모리 부족 시 실행 중 시퀀스를 물러나게 하는 것. 재계산 방식과 CPU 스왑 방식이 있고, 컨텍스트 길이에 따라 유리한 쪽이 다름 |
| 연속 배칭 | 이터레이션 단위 스케줄링. 자리가 나는 즉시 새 요청을 배치에 투입. GPU 활용률 30 |
| 점프-포워드 디코딩 | 문법상 다음 토큰이 유일하게 결정되면 모델 호출 없이 바로 방출. 구조가 많은 출력은 토큰 수보다 적은 모델 호출로 완성 |
| 접두사 캐싱 | 동일 접두사의 KV를 재사용. KV의 인과성(앞 토큰에만 의존) 때문에 성립. 정확히 일치해야 하므로 프롬프트 설계가 중요 |
| 제로 오버헤드 스케줄러 | CPU 스케줄링과 GPU 실행을 겹쳐 GPU 유휴를 제거. 작은 모델·작은 배치일수록 효과가 큼 |
| 청크 프리필 | 긴 프리필을 조각내 디코드 배치에 섞음. 헤드-오브-라인 블로킹 제거 + 메모리 바운드 디코드의 남는 계산 자원 활용 |
| 캐스케이드 어텐션 | 공유 접두사에 대한 어텐션을 한 번만 계산해 여러 쿼리에 적용하고 고유 부분과 병합. 온라인 소프트맥스의 결합성 덕에 정확 |
| 투기적 디코딩 | 드래프트가 여러 토큰 제안 → 타깃이 한 번에 검증. 무손실. 배치가 커지면 이득이 사라짐(계산 바운드가 되므로) |
| 프리필 / 디코드 | 입력 일괄 처리(계산 바운드) / 출력 순차 생성(메모리 바운드). 최적 설정이 정반대라 분리 유인이 생김 |
오해와 함정 #
GPU를 더 사면 해결된다. #
틀린말은 아니지만,, GPU 대시보드가 60퍼 활용률을 보이는데 요청큐가 길다면 병목은 모델도 GPU도 아닌 추론 엔진인 것
설정 하나로 2~4배가 나오는 영역에서 하드웨어를 두배로 늘리는것은 가장 비싼 해법이다.
처리량이 높으면 사용자가 만족한다. #
배치를 키우면 처리량은 계속 올라간다.
그런데 각 사용자는 ITL이 계속 나빠진다.
처리량 최적화와 사용자 만족은 반대 방향이다. goodput으로 재자.
SGLang이 vLLM보다 빠르다? #
조건부로만 맞다. 공유가 없을때 RadixAttention은 오버헤드를 가지며, 반복 접두사가 없는 단순 생성 워크로드에서는 vLLM이 낫다.
어느 엔진이 빠른가는 잘못된 질문이고 내 워크로드의 접두사 공유율이 얼마인가가 옳은 질문이다.
1M 컨텍스트를 지원하니 1M을 넣으면 된다. #
앞 노트에서도 했던 경고지만, 서빙 관점에서 더 구체적으로 말하면
- KV 캐시가 폭발한다. 공식에 S = 1,000,000를 넣으면 사용자 한명이 GPU여러장을 독점하게 된다.
- 프리필이 수초에서 수십초 TTFT SLO 뒤진다
- 다른 모든 사용자의 지연이 튄다 청크 프리필로도 완전히 못막는다.
대부분의 경우 RAG로 줄이는게 더 싸고 빠르고 정확하다.
투기적 디코딩은 항상 켜야 좋다? #
배치 1에서 1.96배였던 가속이 배치 128에서 1.21배로 떨어진다.
대배치에서는 드래프트 계산이 정직한 계산의 자리를 뺏는다.
배치가 32를 넘으면 끄는것을 검토하라.
양자화하면 품질이 조금 떨어진다 #
조금이 워크로드마다 다른데, 일반 대화에서는 안보이던 저하가
수학 코딩 긴 추론 사슬에서 크게 드러난다.
그리고 api 제공하는 서빙 제공자들이 걍 말없이 양자화하는 경우도 있어서 공개 가중치와 출력이 다른 결과가 나올때가 그래서 종종있는거
PD 분리는 최신기술이니 도입해야한다 #
PD분리의 인상적인 숫자는 96장 H100규모에서 나온것이고 GPU 8장에서 이걸 하면 양쪽 풀 모두 비효율이 되어 오히려 느려진다.
규모가 조건이다
벤치마크 재현이 안되는 것 다섯가지 #
- 워밍업 부족 - CUDA 그래프 캡처, JIT 컴파일
- 캐시 상태 차이 - 두번째 실행은 항상 빠름
- 양자화 차이 - 서빙 제공자의 몰래 양자화
- 커널 백엔드 차이 - FlashInfer, FlashAttention이냐로 수치가 달라짐
- 입력 분포 차이 - 2,000토큰 고정 입력 벤치마크는 현실과 다를 수 ㅣㅇㅆ음
day-0 지원의 실제 의미 #
새 모델이 나온날 지원한다는 발표는 돌아간다는 뜻이지 최적화 되었다는 뜻은 아니다
새 아키텍처에는 새 커널이 필요하고 커널 최적화는 몇 주에서 몇 달이 걸린다 초기 성능 수치를 최종 성능으로 오해하면 안된다
설정 하나만 바꾸고 전체를 판단하기 #
성능 튜닝은 상호작용이 있다.
청크 프리필 크기를 바꾸면 최적 배치 크기를 바꿔야하고 양자화를 켜면 KV캐시 여유가 늘어 배치를 더 키울 수 있다.
튜닝 사다리의 숫자들이 누적으로 제시되는 이유가 이것이다 한 번에 하나씩 바꾸되, 매번 다른 파라미터도 재조정 해야한다.