상태: 초안 (2026-10-13), 일부 실측. 논문과 공식 문서는 2026-10-13 에 직접 열어 확인했습니다. 실측은 같은 사이트의 「토스 ML/AI Infra 준비」 14편에서 8GB급 GPU 한 장, Qwen3-1.7B, vLLM 0.31.0 으로 잰 값을 다시 인용했습니다. 큰 모델과 여러 GPU 에서의 값은 미실측입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 처리량은 결국 배치 크기의 문제다
- 2. PagedAttention: KV 를 블록으로 나누기
- 3. 연속 배칭: 빈 자리를 걸음마다 채우기
- 4. 프리픽스 캐시: 같은 앞부분은 한 번만 계산
- 5. KV 자리가 모자라면
- 6. 정리: 그리고 GPU 메모리의 벽
- 참고
1. 처리량은 결국 배치 크기의 문제다
1편의 결론을 다시 적습니다. 디코드는 토큰 하나를 만들 때마다 가중치 전체를 메모리에서 읽어 와야 해서, 요청 하나만 돌리면 GPU 의 계산기가 대부분 놉니다. 가중치를 한 번 읽을 때 요청 여럿을 함께 디코드하면 같은 읽기로 토큰을 여러 개 만들 수 있습니다. 그래서 배치를 키우면 처리량이 오릅니다.
배치에 든 요청은 저마다 자기 KV 를 GPU 메모리에 두어야 합니다. 가중치를 올리고 남은 메모리를 KV 가 나눠 쓰므로, 같은 메모리에 KV 를 얼마나 빽빽하게 담느냐가 배치 크기, 곧 처리량을 정합니다. 서빙 엔진의 KV 관리 기법은 대부분 이 한 문장을 위한 것입니다.
| 기법 | 아끼는 것 | 처리량에 닿는 길 |
|---|---|---|
| PagedAttention | 예약·단편화로 버리는 KV 메모리 | 같은 메모리에 더 많은 요청 → 큰 배치 |
| 연속 배칭 | 일찍 끝난 요청의 빈 자리 | 걸음마다 배치를 가득 채움 |
| 청크 프리필 | 긴 프리필이 디코드를 멈추는 시간 | 토큰 간격 제약을 지키면서 배치를 키움 |
| 프리픽스 캐시 | 같은 앞부분의 KV 계산과 메모리 | 프리필 계산을 건너뛰고 블록을 함께 씀 |
2. PagedAttention: KV 를 블록으로 나누기
옛 방식의 낭비
vLLM 이전의 서빙 시스템은 요청마다 최대 출력 길이만큼 KV 자리를 한 덩어리로 미리 잡았습니다. 출력이 몇 토큰이 될지 모르니 넉넉히 잡을 수밖에 없습니다. vLLM 논문(Kwon 외, SOSP 2023)은 여기서 생기는 낭비를 셋으로 나눕니다.
- 예약: 앞으로 쓸지도 모르는 자리를 미리 잡아 둔 것
- 내부 단편화: 끝내 쓰지 않은 채 요청이 끝난 자리
- 외부 단편화: 덩어리 사이에 남은, 아무도 못 쓰는 틈
논문의 측정에서 기존 시스템은 KV 메모리의 20.4~38.2% 만 실제 토큰 저장에 썼습니다. 같은 논문의 그림에 따르면 A100 에서 13B 모델을 서빙할 때 메모리의 약 65% 가 가중치, 30% 가까이가 KV 입니다. KV 몫이 원래 작은데 그 가운데 3분의 2 이상을 버린 셈입니다.
블록과 블록 표
PagedAttention 은 운영체제의 가상 메모리처럼 풉니다. KV 를 고정 크기 블록(기본 16토큰)으로 나누고, 요청마다 「논리 블록 → 실제 블록」의 블록 표를 둡니다. 블록은 GPU 메모리의 빈 곳 아무 데나 놓이고, 토큰이 블록 하나를 다 채우면 그때 새 블록을 하나 받습니다.
- 덩어리가 없으니 외부 단편화가 사라집니다.
- 미리 예약하지 않으니 예약 낭비도 없습니다.
- 남는 낭비는 요청마다 마지막 블록의 빈칸뿐입니다(16토큰 미만).
어텐션 커널은 블록 표를 따라가며 흩어진 블록에서 K·V 를 읽습니다. 연속된 배열이 아니어도 계산은 같습니다.
블록을 함께 쓰기
블록 단위로 관리하면 여러 요청이 같은 블록을 가리킬 수 있습니다. 한 프롬프트에서 답을 여러 개 뽑는 병렬 샘플링이나 빔 서치는 프롬프트 부분의 KV 가 똑같으므로, 블록에 참조 수를 세어 두고 함께 씁니다. 누군가 그 블록에 덧붙여 써야 할 때만 복사합니다(copy-on-write). 논문의 측정에서 이것으로 줄인 KV 메모리는 다음과 같습니다.
| 데이터셋 | 병렬 샘플링 | 빔 서치 |
|---|---|---|
| Alpaca | 6.1~9.8% | 37.6~55.2% |
| ShareGPT | 16.2~30.5% | 44.3~66.3% |
결과
논문은 같은 지연 수준에서 처리량이 FasterTransformer·Orca 보다 2~4배 높다고 보고합니다(OPT-13B·66B·175B 와 LLaMA-13B, A100 40GB 1장~A100 80GB 8장). ShareGPT 부하에서 감당하는 요청률은 FasterTransformer 보다 최대 22배였습니다. 비교 대상과 부하에 따라 배율이 크게 달라지므로 「2~4배」를 대표 값으로 읽으면 됩니다.
3. 연속 배칭: 빈 자리를 걸음마다 채우기
메모리를 아껴도 배치를 짜는 방식이 나쁘면 자리가 빕니다. 요청마다 출력 길이가 다르기 때문입니다. 정적 배치는 요청 몇 개를 묶어 시작하고 모두 끝날 때까지 새 요청을 받지 않습니다. 10토큰짜리 답은 일찍 끝나도 그 자리는 500토큰짜리 답이 끝날 때까지 놉니다.
Orca(Yu 외, OSDI 2022)는 스케줄을 모델의 한 걸음(iteration) 단위로 바꿨습니다. 걸음이 끝날 때마다 끝난 요청은 바로 돌려보내고, 기다리던 요청을 다음 걸음부터 넣습니다. 이것이 지금 대부분의 엔진이 쓰는 연속 배칭입니다. 논문은 GPT-3 175B 급 모델(A100 40GB 16장)에서 토큰당 지연 중앙값을 190ms 로 맞췄을 때 FasterTransformer 가 초당 0.185 요청, Orca 가 6.81 요청을 처리했다고 보고합니다(36.9배). 이 비교의 기준선은 정적 배치이고, 이 차이에는 연속 배칭뿐 아니라 Orca 의 다른 설계도 함께 들어 있습니다.
연속 배칭과 PagedAttention 은 서로를 필요로 합니다. 걸음마다 요청이 들고 나려면 KV 자리를 그때그때 잡고 풀어야 하는데, 덩어리 예약으로는 틈만 늘어납니다. 블록 단위 할당이 그것을 가능하게 했습니다.
새 문제: 긴 프리필이 디코드를 멈춘다
연속 배칭에는 그림 (나)의 문제가 남습니다. 새 요청이 들어오면 그 요청의 프리필을 한 걸음에 해야 하는데, 프롬프트가 길면 그 걸음이 오래 걸리고 그동안 배치 안의 다른 요청은 다음 토큰을 받지 못합니다. Sarathi-Serve 논문(Agrawal 외, OSDI 2024)은 이것을 generation stall 이라 부르고, Yi-34B·A100 2장 조건에서 이런 멈춤이 수 초까지 이어질 수 있다고 보고합니다.
해법은 그림 (다)처럼 긴 프리필을 청크로 잘라 여러 걸음에 나눠 넣고, 걸음마다 디코드 토큰과 섞는 것입니다(청크 프리필). 같은 논문의 표 4(Yi-34B, A100 2장, 한 걸음 토큰 예산 1,024)에서 P99 토큰 간격은 섞기만 할 때 0.68초, 청크와 「디코드 먼저 채우기」를 함께 쓰면 0.14초였습니다. 꼬리 지연 제약 아래 감당하는 부하는 vLLM 대비 Mistral-7B(A100 1장)에서 2.6배, Yi-34B(A100 2장)에서 최대 3.7배였습니다(논문 초록).
우리 실측. 8GB급 GPU 에서 Qwen3-1.7B 로 300토큰을 디코드하는 요청 하나가 도는 동안 3,500토큰짜리 입력 3개를 끼워 넣고, 한 걸음에 넣는 입력 토큰 수(--max-num-batched-tokens)를 바꿔 끼운 구간의 토큰 간격을 쟀습니다.
| 한 걸음 입력 토큰 상한 | 끼운 구간 최대 토큰 간격 | 긴 입력 자체의 첫 토큰 |
|---|---|---|
| 512 | 69ms | 481~494ms |
| 1,024 | 129ms | 468~493ms |
| 2,048 (이 판의 기본) | 227ms(3회 227·226·226) | 466~480ms |
| 8,192 (실효 3,500) | 445ms | 461~474ms |
- 최대 토큰 간격이 청크 크기에 거의 비례했습니다. 이 GPU 의 입력 처리 속도(약 8,200토큰/초)로 계산한 청크 하나의 시간(512토큰 약 62ms, 2,048토큰 약 250ms)과 맞습니다.
- 청크를 작게 하면 긴 입력의 첫 토큰이 3~5% 늦어지는 대신 디코드가 덜 끊겼습니다. 대화·음성처럼 토큰이 고르게 나와야 하는 서비스는 작은 청크, 긴 문서를 몰아 처리하는 작업은 큰 청크가 맞습니다.
청크 프리필은 간격을 줄일 뿐 없애지는 못합니다. 프리필과 디코드가 같은 GPU 를 나눠 쓰는 한 서로를 방해합니다. 4편의 분리 서빙이 이 문제를 다른 방식으로 풉니다.
4. 프리픽스 캐시: 같은 앞부분은 한 번만 계산
요청 사이에도 같은 앞부분이 많습니다. 모든 대화에 붙는 시스템 프롬프트, 같은 문서를 두고 여러 번 묻는 RAG, 이전 턴을 그대로 다시 보내는 다회전 대화가 그렇습니다. 1편에서 본 것처럼 앞부분이 같으면 그 KV 도 같습니다. 그러니 이미 GPU 에 있는 블록을 찾아 그대로 쓰면 그만큼의 프리필을 건너뜁니다.
vLLM 의 자동 프리픽스 캐시(설계 문서, 2026-10-13 열람)는 블록마다 열쇠를 만듭니다.
- 열쇠 = 해시(앞 블록의 열쇠, 이 블록의 토큰 ID들, 추가 정보)
- 추가 정보에는 LoRA 어댑터, 멀티모달 입력의 해시, 사용자 사이를 가르는 cache salt 가 들어갑니다.
- 앞 블록의 열쇠가 들어가므로 열쇠 하나가 처음부터 그 블록까지의 앞부분 전체를 대표합니다. 같은 토큰 16개라도 앞부분이 다르면 열쇠가 다릅니다.
- 꽉 찬 블록만 캐시합니다.
- 요청이 끝나도 블록을 바로 지우지 않고 「비어 있지만 내용이 남은」 줄에 넣어 두었다가, 자리가 필요하면 가장 오래 안 쓴 것부터(LRU) 지웁니다. 끝난 요청의 블록은 뒤쪽부터 줄에 넣어, 다시 쓰일 가능성이 낮은 꼬리가 먼저 지워지게 합니다.
- 해시 함수의 기본은 v0.11 부터 sha256 입니다.
우리 실측. 3,000토큰의 공통 접두사에 요청마다 다른 32토큰 꼬리를 붙여 하나씩 보냈습니다.
| 설정 | 첫 요청 첫 토큰 | 이후 요청 첫 토큰(중앙) |
|---|---|---|
| 프리픽스 캐시 켬(기본) | 394ms | 28ms |
| 끔 | 395ms | 393ms |
적중하면 3,000토큰의 프리필을 건너뛰어 첫 토큰이 14배 빨라졌습니다. 반대로 앞부분이 전혀 겹치지 않는 부하(입력 2,048, 동시 32)에서 켜 둔 비용은 처리량 약 1.2%, 첫 토큰 중앙값 약 8% 였습니다(해시 계산 비용으로 추정, 분해하지 않음).
측정할 때의 함정. 같은 문장을 반복해 성능을 재면 프리픽스 캐시가 적중해 GPU 가 낼 수 없는 속도가 나옵니다. 성능 시험의 입력은 요청마다 달라야 하고, 캐시 효과를 보고 싶을 때만 공통 앞부분을 일부러 만듭니다.
5. KV 자리가 모자라면
배치 크기의 상한이 KV 라는 것은 숫자로도 보입니다. 같은 실측 환경에서 KV 자리는 22,048토큰이었고, 입력 512 + 출력 128 = 640토큰짜리 요청이면 약 34개가 들어갑니다.
| 동시 요청 | 처리량 | 첫 토큰 중앙 |
|---|---|---|
| 1 | 66토큰/초 | 73ms |
| 8 | 383토큰/초 | 351ms |
| 32 | 831토큰/초 | 666ms |
| 64 | 787토큰/초 | 5.7초 |
32에서 64로 늘리자 처리량은 오히려 줄고 첫 토큰이 8.6배 늦어졌습니다. 자리가 없는 요청은 대기열에서 기다립니다. 더 모자라면 이미 돌던 요청이 KV 를 내놓고 쫓겨났다가(선점) 처음부터 다시 프리필됩니다. 입력 2,048, 동시 32 에서 선점이 7번, 입력 1,500·출력 256·동시 64 에서 22번 일어났고 첫 토큰 중앙값은 각각 12.5초, 35.4초였습니다. 같은 KV 를 두 번 계산하는 것이라 처리량은 늘지 않고 지연만 늘어납니다.
KV 자리는 서버를 띄울 때마다 22,048~27,504토큰으로 달랐고, 동시 64 처럼 한계 근처에서는 그 20% 차이가 첫 토큰을 2.7배 바꿨습니다. 운영에서는 서버 로그의 GPU KV cache size 를 기록하고, 동시성 상한을 「평균 요청 길이로 KV 가 담는 요청 수」 근처에 맞추는 것이 좋습니다.
6. 정리: 그리고 GPU 메모리의 벽
| 기법 | 근거 | 확인한 효과 |
|---|---|---|
| PagedAttention | 논문 | 낭비를 블록 하나 안으로, 같은 지연에서 처리량 2~4배 |
| 연속 배칭 | 논문(Orca) | 정적 배치 대비 같은 지연에서 36.9배(175B 급, 다른 설계 포함) |
| 청크 프리필 | 논문(Sarathi-Serve)·실측 | P99 토큰 간격 0.68→0.14초(논문), 최대 간격 445→69ms(실측) |
| 프리픽스 캐시 | 실측 | 3,000토큰 접두사 첫 토큰 394→28ms |
이 기법들은 모두 GPU 메모리 안에서 일합니다. 프리픽스 캐시는 GPU 자리가 모자라면 오래된 블록부터 지우고, 지운 앞부분이 다시 오면 처음부터 계산합니다. 엔진 인스턴스가 여럿이면 각자 따로 캐시해서, 옆 인스턴스가 이미 계산한 KV 를 쓰지 못합니다. 3편은 KV 를 GPU 밖(CPU 메모리, 디스크, 다른 서버)에 두는 LMCache 를 다룹니다.
티티 한마디. 탁구장 테이블을 「한 팀이 하루 종일」 예약받으면 빈 테이블이 많아요. 30분 단위로 쪼개고, 끝난 자리에 바로 다음 팀을 넣고, 같은 동호회는 한 테이블을 같이 쓰게 하면 같은 체육관에서 훨씬 많은 사람이 칠 수 있죠. PagedAttention 과 연속 배칭이 그 일을 해요.
참고
- Kwon 외, Efficient Memory Management for Large Language Model Serving with PagedAttention (SOSP 2023): https://arxiv.org/abs/2309.06180 (1·3.1·4.3·6.2·6.3·7.2절, 표 1)
- Yu 외, Orca: A Distributed Serving System for Transformer-Based Generative Models (OSDI 2022): https://www.usenix.org/conference/osdi22/presentation/yu (6.2절, 그림 10b)
- Agrawal 외, Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve (OSDI 2024): https://arxiv.org/abs/2403.02310 (초록, 3.2·4절, 표 4)
- vLLM 자동 프리픽스 캐시 설계: https://docs.vllm.ai/en/latest/design/prefix_caching.html (개발판 문서, 2026-10-13 열람)
- 실측 인용: 같은 사이트 「토스 ML/AI Infra 준비」 14편(8GB급 GPU, Qwen3-1.7B, vLLM 0.31.0)