paged-attention은 도대체 뭘 최적화 하는 것일까
이 글은 vLLM의 PagedAttention만 따로 떼어 정리한 글이다. 앞선 글인 '안녕하세요 반갑습니다'로 배우는 vLLM — KV Cache와 PagedAttention 완전 해부에서는 토크나이징, Attention, KV Cache, Prefill, Decode까지 전체 흐름을 순서대로 다뤘다. 이 글에서는 그중 PagedAttention이 왜 필요했고, 어떤 문제를 해결했으며, 실제로 메모리를 어떻게 다루는지에 집중한다.
PagedAttention은 한 문장으로 정리하면 LLM 추론 중 계속 커지는 KV Cache를 고정된 큰 덩어리로 미리 잡아두지 않고, 작은 블록 단위로 나누어 필요한 만큼만 연결해서 쓰는 메모리 관리 방식이다. 운영체제가 가상 메모리에서 페이지를 다루는 방식과 비슷한 아이디어를 LLM의 Attention KV Cache에 적용한 것이다.
이 방식은 단순한 최적화가 아니다. LLM 서빙에서 처리량을 크게 좌우하는 핵심 병목이 KV Cache 메모리이기 때문이다. vLLM 논문은 기존 시스템에서 KV Cache 메모리 낭비가 60%에서 80% 수준까지 발생할 수 있다고 설명한다. vLLM은 PagedAttention으로 이 낭비를 줄이고, 같은 GPU에서 더 많은 요청을 동시에 처리할 수 있게 만든다.

그림. PagedAttention은 GPU 메모리 위의 KV Cache 배치를 블록 단위로 관리한다. 이미지 출처: https://funktor.github.io/software-engineering/2025/06/21/the-gpu-notes-1.html
1. 먼저 문제를 단순하게 잡기
예시 문장은 다음과 같다.
안녕하세요 반갑습니다
이 문장이 다음 6개 토큰으로 나뉘었다고 가정한다.
["안녕", "하", "세요", "반갑", "습", "니다"]
LLM은 이 문장을 사람처럼 한 번에 읽고 끝내지 않는다. 각 토큰을 숫자 ID로 바꾸고, 벡터로 바꾸고, 여러 Transformer layer를 통과시키면서 다음 토큰을 예측한다. 이때 Attention은 현재 토큰이 이전 토큰들을 얼마나 참고해야 하는지 계산한다.
예를 들어 마지막 토큰인 "니다"를 처리할 때 모델은 앞의 "안녕", "하", "세요", "반갑", "습"을 함께 본다. 이 참조 관계를 매번 처음부터 다시 계산하면 비용이 너무 크다. 그래서 이전 토큰들의 Key와 Value를 저장해 둔다. 이것이 KV Cache다.
KV Cache는 말 그대로 캐시다. 한 번 계산한 Key와 Value를 다시 쓰기 위해 GPU 메모리에 저장해 둔다. 문제는 생성이 길어질수록 이 캐시도 계속 커진다는 점이다.
2. KV Cache가 왜 그렇게 큰가
LLM은 토큰 하나를 처리할 때 layer마다 Key와 Value를 만든다. 모델이 32개 layer를 가지고 있다면 토큰 하나는 32개 layer 각각에 Key와 Value를 남긴다. 토큰 1개가 단순히 작은 문자열 하나로 끝나는 것이 아니라, 여러 layer에 걸친 벡터 묶음으로 GPU 메모리에 쌓인다.
KV Cache 크기는 대략 다음 요소에 의해 결정된다.
- batch 안의 요청 수
- 각 요청의 현재 토큰 길이
- Transformer layer 수
- attention head 또는 KV head 수
- head dimension
- 데이터 타입 크기, 예를 들어 FP16 또는 BF16이면 값 하나가 2바이트
단순화하면 다음과 같은 형태다.
KV Cache 크기
= 요청 수
x 토큰 수
x layer 수
x KV head 수
x head dimension
x 2개(K와 V)
x dtype 바이트 수
여기서 중요한 부분은 토큰 수다. 사용자가 짧은 질문을 하면 KV Cache가 작다. 사용자가 긴 문서를 넣으면 KV Cache가 커진다. 답변을 길게 생성하면 생성 토큰이 늘어나는 만큼 KV Cache도 계속 커진다.
서빙 시스템 입장에서는 각 요청이 얼마나 길어질지 처음부터 정확히 알기 어렵다. 사용자가 max_tokens=2048을 줬다고 해서 실제로 2048개를 모두 생성하는 것도 아니다. 어떤 요청은 20개 토큰에서 끝나고, 어떤 요청은 1000개 토큰을 넘긴다.
이 불확실성이 PagedAttention이 등장한 배경이다.
3. 기존 방식의 핵심 문제: 미리 크게 잡거나, 옮기거나, 낭비한다
PagedAttention을 이해하려면 먼저 PagedAttention이 없는 상황을 봐야 한다.
가장 단순한 방식은 요청마다 KV Cache 공간을 연속된 메모리로 크게 잡는 것이다. 예를 들어 어떤 요청이 최대 2048개 토큰까지 생성될 수 있다면, 처음부터 2048개 토큰을 담을 수 있는 공간을 잡는다.
하지만 실제 요청은 이렇게 움직인다.
| 요청 | 예약한 최대 길이 | 실제 사용 길이 | 낭비 |
|---|---|---|---|
| A | 2048 | 120 | 1928 |
| B | 2048 | 760 | 1288 |
| C | 2048 | 35 | 2013 |
| D | 2048 | 1800 | 248 |
이 표에서 GPU 메모리는 이미 예약되어 있다. 실제로 쓰지 않은 공간도 다른 요청이 마음대로 쓸 수 없다. 결과적으로 GPU에는 빈 공간이 많아 보이지만, 서빙 시스템은 새 요청을 더 받지 못한다.
이 상황을 식당 좌석으로 비유하면 이해하기 쉽다. 손님 한 팀이 올 때마다 20인실을 하나씩 배정한다고 생각하면 된다. 실제로는 2명만 앉아도 그 방은 다른 손님이 쓸 수 없다. 손님 수가 불확실하다는 이유로 모든 팀에게 큰 방을 주면, 전체 식당의 수용 인원은 빠르게 줄어든다.
GPU 메모리에서도 같은 일이 벌어진다. 연속된 큰 공간을 미리 예약하면 관리가 단순하지만 낭비가 커진다. 반대로 필요한 만큼만 매번 새로 할당하고 옮기면 낭비는 줄어도 복사 비용과 관리 비용이 늘어난다.
PagedAttention은 이 둘 사이에서 다른 해법을 택한다. 요청마다 큰 연속 공간을 요구하지 않고, 작은 블록을 여러 개 붙여 하나의 긴 KV Cache처럼 보이게 만든다.
4. PagedAttention의 기본 아이디어
PagedAttention의 핵심 구성 요소는 세 가지다.
- logical block: 요청 내부에서 보이는 논리적 블록
- physical block: 실제 GPU 메모리에 존재하는 물리적 블록
- block table: 논리적 블록과 물리적 블록을 연결하는 표
토큰을 한 줄로 길게 놓지 않고 일정한 개수씩 잘라 블록으로 만든다. 예를 들어 블록 크기를 4토큰으로 단순화하면, "안녕하세요 반갑습니다"의 6개 토큰은 다음처럼 나뉜다.
토큰: [안녕, 하, 세요, 반갑, 습, 니다]
논리 블록 0: [안녕, 하, 세요, 반갑]
논리 블록 1: [습, 니다, 빈칸, 빈칸]
실제 vLLM의 block size는 설정과 구현에 따라 다르지만, 개념은 같다. 요청의 토큰들을 고정 크기 블록으로 나눈다. 마지막 블록은 항상 꽉 차지 않을 수 있다.
이제 중요한 변화가 생긴다. 논리 블록 0과 논리 블록 1이 실제 GPU 메모리에서 반드시 연속되어 있을 필요가 없다.
요청 A의 논리 블록
logical 0 -> physical 17
logical 1 -> physical 03
logical 2 -> physical 91
요청 A 입장에서는 0번, 1번, 2번 블록이 순서대로 이어져 있는 것처럼 보인다. 하지만 실제 GPU 메모리에서는 17번, 3번, 91번 physical block에 흩어져 있을 수 있다. 이 연결 정보를 저장하는 것이 block table이다.
이 구조는 운영체제의 가상 메모리와 비슷하다. 프로그램은 자기 메모리가 연속된 것처럼 보지만, 실제 물리 메모리에서는 여러 페이지에 흩어져 있을 수 있다. PagedAttention은 이 발상을 KV Cache에 적용한다.
5. 블록 테이블이 하는 일
block table은 요청별로 존재하는 주소록에 가깝다. 각 요청은 자신의 logical block 번호를 가지고 있고, block table은 그 logical block이 실제 어떤 physical block에 저장되어 있는지 알려준다.
예를 들어 요청 A가 10개 토큰을 가지고 있고 블록 크기가 4라고 하자.
요청 A 토큰 수: 10
블록 크기: 4
필요 블록 수: ceil(10 / 4) = 3
요청 A의 논리 블록은 다음과 같다.
logical block 0: token 0~3
logical block 1: token 4~7
logical block 2: token 8~9
실제 GPU 메모리에서는 다음처럼 배치될 수 있다.
block table for request A
logical block 0 -> physical block 42
logical block 1 -> physical block 07
logical block 2 -> physical block 18
Attention 커널은 특정 토큰의 Key와 Value를 읽을 때 이 block table을 따라간다. 요청 A의 5번째 토큰을 읽어야 한다면 먼저 이 토큰이 logical block 1에 있다는 것을 계산하고, block table에서 logical block 1이 physical block 07에 있다는 것을 찾는다. 그다음 physical block 07 안의 해당 위치에서 KV 값을 읽는다.
이 과정은 처음 보면 복잡해 보이지만, 목적은 단순하다. 요청마다 긴 연속 메모리를 잡지 않기 위해 한 번 더 주소 변환을 하는 것이다.
6. 예시로 보는 "안녕하세요 반갑습니다"의 블록 할당
블록 크기를 4토큰으로 놓고 예시를 계속 보자. 실제 시스템의 기본값을 설명하려는 것이 아니라 개념을 보기 위한 숫자다.
입력 토큰은 6개다.
0: 안녕
1: 하
2: 세요
3: 반갑
4: 습
5: 니다
필요한 블록 수는 다음과 같다.
ceil(6 / 4) = 2
논리 블록은 두 개다.
logical block 0: [안녕, 하, 세요, 반갑]
logical block 1: [습, 니다, 빈칸, 빈칸]
GPU 메모리의 free block pool에 빈 physical block들이 있다고 하자.
free physical blocks: [11, 12, 13, 14, 15, 16]
vLLM은 요청에 physical block 두 개를 할당한다.
request R1 block table
logical block 0 -> physical block 11
logical block 1 -> physical block 12
이제 Prefill 단계에서 6개 토큰의 KV가 계산되어 physical block 11과 12에 저장된다.
physical block 11: [안녕, 하, 세요, 반갑]
physical block 12: [습, 니다, 빈칸, 빈칸]
그다음 모델이 새 토큰을 하나 생성했다고 하자. 예를 들어 "." 토큰이 생성되었다고 가정한다.
0: 안녕
1: 하
2: 세요
3: 반갑
4: 습
5: 니다
6: .
logical block 1에는 아직 빈칸이 2개 남아 있다. 따라서 새 physical block을 할당하지 않고 physical block 12의 다음 칸에 "."의 KV를 저장한다.
physical block 12: [습, 니다, ., 빈칸]
토큰이 하나 더 생성되면 마지막 칸까지 채운다.
physical block 12: [습, 니다, ., 다음]
이제 다음 토큰이 생성되면 logical block 2가 필요하다. 그때 free block pool에서 새 physical block을 하나 가져온다.
request R1 block table
logical block 0 -> physical block 11
logical block 1 -> physical block 12
logical block 2 -> physical block 13
PagedAttention의 중요한 지점은 여기다. 처음부터 최대 길이만큼 잡지 않고, 토큰이 늘어날 때 블록 단위로 조금씩 늘린다.
7. 왜 낭비가 마지막 블록에만 생기는가
PagedAttention이 메모리 낭비를 줄이는 이유는 단순하다. 각 요청에서 낭비가 생길 수 있는 위치가 마지막 블록 하나로 제한되기 때문이다.
블록 크기가 16토큰이라고 하자. 어떤 요청이 100개 토큰을 사용한다면 필요한 블록은 다음과 같다.
ceil(100 / 16) = 7 blocks
앞의 6개 블록은 꽉 찬다.
6 blocks x 16 tokens = 96 tokens
마지막 1개 블록에는 4개 토큰만 들어간다.
last block = 4 tokens used, 12 tokens empty
낭비는 마지막 블록의 12칸이다. 요청이 100개 토큰을 쓰는데 2048개 토큰 공간을 미리 잡는 방식과 다르다.
일반화하면 블록 크기가 B일 때 한 요청에서 내부 단편화로 낭비될 수 있는 토큰 칸은 최대 B - 1개다. 블록 크기가 16이면 최대 15개다. 요청이 1000개 토큰을 쓰든 4000개 토큰을 쓰든, 마지막 블록 하나에서만 빈칸이 생긴다.
vLLM 논문은 PagedAttention의 메모리 낭비가 4% 미만에 가깝다고 설명한다. 큰 연속 공간을 미리 예약하는 방식과 비교하면 차이가 크다.
8. PagedAttention은 Attention 계산 자체를 바꾸는가
여기서 헷갈리기 쉬운 부분이 있다. PagedAttention은 모델의 의미를 바꾸지 않는다. Attention 수식 자체를 다른 것으로 바꾸는 것이 아니다.
기본 Attention은 여전히 Query, Key, Value를 사용한다.
Attention(Q, K, V) = softmax(QK^T / sqrt(d))V
PagedAttention이 바꾸는 것은 K와 V가 메모리에 놓이는 방식이다. 기존 방식에서는 K와 V가 요청별로 긴 연속 배열에 놓인다고 가정하기 쉽다. PagedAttention에서는 K와 V가 여러 physical block에 나뉘어 저장되고, 커널이 block table을 따라 필요한 K와 V를 읽는다.
즉, 결과적으로 모델이 보는 문맥은 같다. "니다"가 앞의 "안녕", "하", "세요", "반갑", "습"을 참고하는 구조는 유지된다. 달라지는 것은 그 참조 대상이 GPU 메모리 어디에 저장되어 있느냐다.
이 차이는 사용자가 보는 답변 품질의 문제가 아니라 서버가 같은 GPU에서 얼마나 많은 요청을 효율적으로 처리할 수 있느냐의 문제다.
9. Prefill과 Decode에서 PagedAttention이 보이는 방식
LLM 서빙은 크게 Prefill과 Decode로 나뉜다.
Prefill은 사용자가 보낸 입력 프롬프트 전체를 한 번 처리하는 단계다. "안녕하세요 반갑습니다"의 6개 토큰을 모델에 넣고, 각 layer의 KV Cache를 만든다. 이 단계에서는 입력 토큰들이 한 번에 들어오기 때문에 병렬 계산 비중이 크다.
Decode는 모델이 새 토큰을 하나씩 생성하는 단계다. 답변을 100개 토큰 생성한다면 Decode는 100번 반복된다. 매번 새 Query가 만들어지고, 지금까지 쌓인 모든 Key와 Value를 참조한다. 그래서 Decode에서는 KV Cache를 읽는 비용이 중요해진다.
PagedAttention은 두 단계 모두에 관여한다.
Prefill에서는 입력 토큰 수에 맞춰 필요한 블록을 할당하고 KV를 채운다. Decode에서는 새 토큰이 생성될 때 현재 마지막 블록에 자리가 있으면 그곳에 KV를 추가하고, 자리가 없으면 새 블록을 할당한다.
이를 흐름으로 쓰면 다음과 같다.
1. 요청 수신
2. 토크나이징
3. 필요한 초기 블록 수 계산
4. free block pool에서 physical block 할당
5. block table 생성
6. Prefill 실행 및 KV 저장
7. Decode 반복
8. 마지막 블록에 공간이 있으면 KV 추가
9. 마지막 블록이 가득 차면 새 physical block 할당
10. 요청 종료 시 physical block 반환
이 구조 덕분에 요청이 짧게 끝나면 적은 블록만 쓰고 끝난다. 요청이 길어지면 그때그때 블록을 추가한다. GPU 메모리는 요청의 실제 길이에 더 가깝게 사용된다.
10. Continuous Batching과 함께 볼 때 더 중요해진다
PagedAttention은 vLLM의 또 다른 핵심 기능인 continuous batching과 함께 볼 때 효과가 더 잘 드러난다.
일반적인 배치 처리에서는 여러 요청을 묶어서 처리한다. 문제는 요청마다 생성 길이가 다르다는 점이다. 어떤 요청은 금방 끝나고, 어떤 요청은 오래 이어진다. 고정 배치 방식에서는 짧은 요청이 끝나도 긴 요청 때문에 전체 배치가 계속 묶여 있는 상황이 생긴다.
continuous batching은 완료된 요청 자리에 새 요청을 계속 넣는다. GPU가 쉬지 않도록 배치를 동적으로 갱신하는 방식이다.
이때 KV Cache 메모리 관리가 느슨하면 문제가 생긴다. 새 요청을 넣고 싶어도 GPU 메모리에 연속된 큰 공간이 없으면 어렵다. 반대로 PagedAttention처럼 작은 physical block 단위로 메모리를 관리하면, 끝난 요청의 블록을 free block pool로 돌려보내고 새 요청이 바로 가져다 쓸 수 있다.
즉, continuous batching은 스케줄링 측면의 최적화이고, PagedAttention은 그 스케줄링이 실제 GPU 메모리 위에서 잘 돌아가게 만드는 기반 구조다.
11. Prefix Caching과 Copy-on-Write
PagedAttention은 prefix caching과도 잘 맞는다. prefix caching은 여러 요청이 같은 앞부분 프롬프트를 공유할 때, 그 앞부분의 KV Cache를 다시 계산하지 않고 공유하는 방식이다.
예를 들어 모든 요청 앞에 같은 시스템 프롬프트가 붙는다고 하자.
너는 친절한 한국어 AI 어시스턴트다.
사용자의 질문에 정확하고 간결하게 답하라.
이 프롬프트가 수천 요청에서 반복된다면 매번 KV Cache를 새로 만드는 것은 낭비다. vLLM의 automatic prefix caching은 공통 prefix의 KV Cache를 재사용할 수 있게 한다.
PagedAttention 구조에서는 공유할 수 있는 prefix가 block 단위로 관리된다. 여러 요청이 같은 physical block을 참조할 수 있고, 각 요청의 block table은 같은 physical block을 가리킨다.
request A logical block 0 -> physical block 30
request B logical block 0 -> physical block 30
request C logical block 0 -> physical block 30
이때 physical block 30은 공통 시스템 프롬프트의 KV Cache를 담고 있다. 세 요청은 같은 블록을 읽기만 한다. 메모리를 세 번 쓰지 않는다.
문제는 어떤 요청이 그 블록을 수정해야 하는 경우다. 공유 중인 블록을 그대로 수정하면 다른 요청에도 영향을 준다. 그래서 Copy-on-Write가 필요하다. 읽을 때는 공유하고, 쓰기가 필요해지는 순간 복사본을 만들어 해당 요청만 새 블록을 사용하게 한다.
이 구조는 파일 시스템이나 운영체제 메모리 관리에서도 쓰이는 방식과 비슷하다. PagedAttention이 블록 단위 주소 체계를 가지고 있기 때문에 prefix caching과 Copy-on-Write를 자연스럽게 구현할 수 있다.
12. Swap과 eviction을 이해하기
서빙 중인 요청이 너무 많아지면 GPU 메모리의 KV Cache 블록이 부족해질 수 있다. 이때 시스템은 몇 가지 선택을 해야 한다.
- 새 요청을 기다리게 한다.
- 일부 요청을 중단하거나 실패 처리한다.
- 덜 급한 KV Cache 블록을 CPU 메모리로 옮긴다.
- 이미 끝난 요청의 블록을 즉시 반환한다.
vLLM은 스케줄러와 메모리 관리자가 함께 동작하면서 어떤 요청을 계속 GPU에서 처리할지 결정한다. PagedAttention은 여기서도 유리하다. 메모리가 큰 연속 덩어리로 묶여 있으면 일부만 옮기기 어렵지만, 블록 단위라면 관리 단위가 작아진다.
다만 CPU로 swap하는 것은 공짜가 아니다. GPU 메모리와 CPU 메모리 사이의 이동은 지연 시간을 만든다. 따라서 swap은 처리량과 안정성을 위한 완충 장치에 가깝고, 이상적인 상태는 GPU 메모리 안에서 블록을 충분히 재사용하며 요청을 처리하는 것이다.
13. 숫자로 보는 간단한 비교
다음은 개념을 설명하기 위한 단순 예시다. 실제 모델과 설정에 따라 수치는 달라진다.
가정은 다음과 같다.
- 요청마다 최대 2048토큰까지 생성 가능
- 실제 평균 사용 길이는 300토큰
- 블록 크기는 16토큰
- 요청 1개가 토큰 1개당 KV Cache 단위 1을 사용한다고 단순화
기존 예약 방식에서는 요청마다 2048 단위를 잡는다.
요청 100개 x 2048 = 204,800 단위 예약
실제 사용량은 평균 300토큰이다.
요청 100개 x 300 = 30,000 단위 사용
예약된 공간의 상당 부분이 비어 있다.
PagedAttention에서는 요청마다 실제 길이에 가까운 블록 수만 잡는다.
ceil(300 / 16) = 19 blocks
19 blocks x 16 = 304 단위
요청 100개 x 304 = 30,400 단위 예약
실제 사용량 30,000 단위에 비해 예약량은 30,400 단위다. 낭비는 요청당 마지막 블록의 일부로 제한된다.
이 예시는 PagedAttention의 직관을 보여준다. 핵심은 GPU가 더 커지는 것이 아니라, 같은 GPU 메모리를 더 촘촘하게 쓴다는 점이다.
14. PagedAttention 커널은 왜 별도로 필요할까
블록 단위로 KV Cache를 저장하면 메모리 효율은 좋아진다. 대신 Attention 계산이 더 복잡해진다. K와 V가 연속된 배열에 있다고 가정하면 커널은 단순하게 순서대로 읽으면 된다. 하지만 PagedAttention에서는 토큰 위치를 block table로 변환해서 읽어야 한다.
그래서 vLLM은 PagedAttention에 맞는 Attention 커널을 사용한다. vLLM Paged Attention 문서는 block table을 이용해 KV cache block을 조회하는 구조를 설명한다.
커널 입장에서는 다음 일이 필요하다.
- 현재 요청의 block table을 읽는다.
- 필요한 토큰 위치가 어느 logical block에 속하는지 계산한다.
- logical block을 physical block으로 변환한다.
- physical block 안의 offset을 계산한다.
- 해당 위치의 Key와 Value를 읽는다.
- Attention score와 weighted sum을 계산한다.
이 주소 변환 비용은 존재한다. 하지만 큰 연속 메모리 예약으로 인한 낭비를 줄이고, 더 많은 요청을 동시에 처리할 수 있는 이점이 더 크다. vLLM의 성능 개선은 이 메모리 관리 방식과 커널 구현이 함께 맞물린 결과다.
15. PagedAttention이 해결하지 않는 문제
PagedAttention은 강력하지만 모든 문제를 해결하는 기술은 아니다. 범위를 정확히 잡아야 한다.
PagedAttention은 모델 파라미터 자체를 줄이지 않는다. 70B 모델을 올릴 GPU 메모리가 부족하다면 PagedAttention만으로 해결되지 않는다. 모델 weight를 줄이려면 양자화, tensor parallelism, CPU offload 같은 다른 기법이 필요하다.
PagedAttention은 prompt가 길 때 필요한 총 KV Cache 양을 없애지 않는다. 토큰이 100만 개라면 그에 해당하는 KV Cache는 여전히 필요하다. 다만 그 메모리를 더 효율적으로 배치하고 공유하고 회수할 수 있게 한다.
PagedAttention은 Attention의 계산량을 근본적으로 선형으로 바꾸는 기술도 아니다. 긴 문맥에서 Attention 계산이 부담스러운 문제는 여전히 존재한다. PagedAttention은 주로 KV Cache 메모리 배치와 관리 문제를 해결한다.
정리하면 PagedAttention은 LLM의 지능을 높이는 기술이 아니라, LLM 서버가 GPU 메모리를 더 잘 쓰게 만드는 기술이다.
16. 실무에서 PagedAttention이 체감되는 지점
실제 vLLM 사용자는 PagedAttention을 직접 조작하지 않는 경우가 많다. vllm serve를 실행하면 내부에서 스케줄러, KV cache manager, PagedAttention 커널이 함께 동작한다.
체감은 다음 상황에서 나타난다.
- 같은 GPU에서 동시 요청을 더 많이 받을 수 있다.
- 짧은 요청과 긴 요청이 섞여도 메모리 낭비가 줄어든다.
- 요청이 끝난 뒤 KV Cache가 블록 단위로 빠르게 회수된다.
- prefix caching과 함께 쓰면 반복 프롬프트 비용을 줄일 수 있다.
- continuous batching과 함께 쓰면 GPU 유휴 시간을 줄일 수 있다.
사용자가 직접 봐야 하는 설정은 대체로 max_model_len, gpu_memory_utilization, batch 관련 옵션이다. 이 값들은 PagedAttention이 쓸 수 있는 KV Cache 공간과 동시에 처리 가능한 요청 수에 영향을 준다.
예를 들어 max_model_len을 지나치게 크게 잡으면 각 요청이 잠재적으로 사용할 수 있는 문맥 길이가 늘어난다. PagedAttention이 낭비를 줄여도, 긴 문맥을 실제로 많이 사용하는 워크로드라면 KV Cache는 크게 필요하다.
gpu_memory_utilization은 vLLM이 GPU 메모리 중 어느 정도를 활용할지 정하는 중요한 인자다. 너무 낮게 잡으면 처리 가능한 요청 수가 줄어들고, 너무 높게 잡으면 다른 프로세스나 런타임 여유 공간이 부족해질 수 있다.
17. 아주 단순한 의사코드로 보기
PagedAttention을 실제 코드가 아니라 의사코드로 쓰면 다음과 같다.
BLOCK_SIZE = 4
class RequestState:
def __init__(self):
self.tokens = []
self.block_table = [] # logical block index -> physical block id
free_blocks = [11, 12, 13, 14, 15, 16]
kv_memory = {}
def allocate_block():
return free_blocks.pop(0)
def ensure_capacity(request, next_token_index):
logical_block = next_token_index // BLOCK_SIZE
while len(request.block_table) <= logical_block:
physical_block = allocate_block()
request.block_table.append(physical_block)
kv_memory[physical_block] = [None] * BLOCK_SIZE
def write_kv(request, token_index, kv_value):
ensure_capacity(request, token_index)
logical_block = token_index // BLOCK_SIZE
offset = token_index % BLOCK_SIZE
physical_block = request.block_table[logical_block]
kv_memory[physical_block][offset] = kv_value
def read_kv(request, token_index):
logical_block = token_index // BLOCK_SIZE
offset = token_index % BLOCK_SIZE
physical_block = request.block_table[logical_block]
return kv_memory[physical_block][offset]
이 의사코드는 핵심만 보여준다. 실제 vLLM은 layer, head, dtype, GPU kernel, scheduler, block manager, prefix caching, swap 등을 함께 다룬다. 그래도 중심 원리는 같다.
- 토큰 위치를 logical block과 offset으로 바꾼다.
- logical block을 block table로 physical block에 매핑한다.
- physical block 안에서 KV를 읽고 쓴다.
이 세 줄이 PagedAttention의 핵심이다.
18. 전체 흐름 다시 정리
"안녕하세요 반갑습니다" 예시로 전체 흐름을 다시 압축하면 다음과 같다.
1. 입력 문장 수신
"안녕하세요 반갑습니다"
2. 토크나이징
["안녕", "하", "세요", "반갑", "습", "니다"]
3. 블록 크기 기준으로 필요한 블록 계산
block size = 4라면 2개 블록 필요
4. free block pool에서 physical block 할당
logical 0 -> physical 11
logical 1 -> physical 12
5. Prefill에서 각 토큰의 KV 계산 후 저장
physical 11: 안녕, 하, 세요, 반갑
physical 12: 습, 니다, empty, empty
6. Decode에서 새 토큰 생성
마지막 블록에 공간이 있으면 이어서 저장
7. 마지막 블록이 가득 차면 새 physical block 할당
logical 2 -> physical 13
8. 요청이 끝나면 사용한 physical block 반환
11, 12, 13이 free block pool로 돌아감
이 흐름에서 PagedAttention은 모델의 답변 내용을 직접 결정하지 않는다. 대신 답변을 생성하는 동안 생기는 KV Cache를 작은 블록으로 나누어 관리한다. 이 관리 방식이 좋아지면 같은 GPU에서 더 많은 요청을 안정적으로 처리할 수 있다.
19. vLLM 사용자가 기억해야 할 핵심
PagedAttention을 처음 접할 때 세 가지만 기억하면 된다.
첫째, LLM 추론에서 KV Cache는 매우 크고 계속 늘어난다. 긴 프롬프트와 긴 답변은 GPU 메모리를 빠르게 사용한다.
둘째, 기존 방식처럼 요청마다 큰 연속 공간을 미리 잡으면 실제로 쓰지 않는 공간이 많이 생긴다. 이 낭비 때문에 GPU 메모리가 남아 보이는데도 새 요청을 받지 못하는 상황이 생긴다.
셋째, PagedAttention은 KV Cache를 고정 크기 블록으로 나누고 block table로 연결한다. 그래서 요청의 실제 길이에 맞춰 블록을 추가하고, 끝난 요청의 블록을 회수하고, 공통 prefix 블록을 공유할 수 있다.
이 구조가 vLLM의 핵심 경쟁력이다. vLLM이 vllm serve 한 줄로 OpenAI 호환 API를 제공하는 사용성도 중요하지만, 그 아래에서 많은 요청을 효율적으로 처리하게 만드는 기반에는 PagedAttention이 있다.
vLLM을 처음 설치하고 실행하는 흐름은 vLLM 완전 입문 — 설치부터 OpenAI 호환 API 서빙까지 한 번에에 정리했다. KV Cache와 Attention 전체 흐름을 토큰 예시로 다시 보고 싶다면 '안녕하세요 반갑습니다'로 배우는 vLLM — KV Cache와 PagedAttention 완전 해부를 먼저 읽는 편이 좋다. 실제 운영 옵션으로 넘어가면 vLLM serve 옵션 정리에서 max_model_len, gpu_memory_utilization, batch 관련 설정을 함께 봐야 한다.