LLM 엔지니어링16장 · vLLM 서빙 벤치마킹과 RAG 최적화5 / 22← 이전목차다음 →MetaContents

16장. vLLM 서빙 벤치마킹과 RAG 최적화

KV 캐시와 PagedAttention

도서관 사물함을 생각해 봅니다. 이용자가 올 때마다 짐이 얼마나 될지 몰라 가장 큰 사물함을 통째로 내주면, 대부분 반쯤 빈 채로 자리만 차지합니다. 작은 칸을 여러 개 두고 짐이 늘 때마다 한 칸씩 더 내주면 같은 벽에 훨씬 많은 사람이 짐을 둘 수 있습니다.

KV 캐시를 최대 길이만큼 통째로 잡는 방식과 작은 쪽으로 나눠 필요할 때 잡는 방식을 견준 비교 그림

왼쪽은 요청마다 최대 길이만큼 KV 캐시를 미리 잡는 방식으로, 실제로 쓰지 않는 자리가 많이 남습니다. 오른쪽은 vLLM 의 PagedAttention 으로, KV 캐시를 16토큰쯤의 작은 쪽(블록)으로 나눠 글이 길어질 때마다 한 쪽씩 더 잡습니다. 큰 사물함이 통째로 잡는 방식, 작은 칸이 쪽입니다. 같은 GPU 메모리에 더 많은 요청을 동시에 올릴 수 있어 처리량이 늘어납니다.

두 방식 견주기

통째로 잡기쪽으로 나눠 잡기
잡는 크기요청마다 최대 길이쓴 만큼(쪽 단위로 올림)
남는 자리많다마지막 쪽의 빈칸뿐
동시에 올릴 요청적다많다
덤없음앞부분이 같은 요청끼리 쪽을 함께 쓴다(프리픽스 캐시)

남는 자리 계산하기

Python
import math

max_len, page = 4096, 16                       # 최대 길이와 쪽 하나의 토큰 수
used = [180, 650, 90, 1200, 300, 75, 420, 260]  # 요청 여덟 개가 실제로 쓴 토큰 수(예시)

whole = max_len * len(used)                                  # 통째로 잡으면
paged = sum(math.ceil(u / page) * page for u in used)        # 쪽으로 잡으면(쪽 단위로 올림)
print("실제로 쓴 토큰:", sum(used))
print(f"통째로 잡은 자리: {whole:,} (쓴 몫 {sum(used) / whole:.1%})")
print(f"쪽으로 잡은 자리: {paged:,} (쓴 몫 {sum(used) / paged:.1%})")

코드에 나오는 함수

코드쉬운 뜻
math.ceil(u / page)ceil 은 ceiling(천장)에서 온 이름이다. 소수점이 있으면 위로 올린다. 180 ÷ 16 = 11.25 이면 12쪽이다
math.ceil(u / page) * page쪽 수에 쪽의 크기를 곱한다. 실제로 잡히는 자리 수다
max_len * len(used)요청마다 최대 길이 4096 을 통째로 잡았을 때의 자리 수다
{whole:,}세 자리마다 쉼표를 찍어 출력한다
{... :.1%}비율을 백분율로 바꿔 소수 첫째 자리까지 적는다

이 코드가 뜻하는 것

도서관 사물함 이야기를 숫자로 옮긴 것입니다. 이용자 여덟 명에게 큰 사물함을 하나씩 주는 경우와, 작은 칸을 필요한 만큼만 주는 경우의 빈자리를 셉니다. 여기서 이용자가 요청, 짐이 토큰, 작은 칸이 16토큰짜리 쪽입니다.

줄하는 일쉽게 말하면
used = [...]요청 여덟 개가 실제로 쓴 토큰 수(예시)이용자마다 실제 짐의 양
whole = ...4096 × 8 = 32,768 자리모두에게 큰 사물함을 통째로 준다
paged = ...쪽 단위로 올려 더한 3,232 자리짐만큼 작은 칸을 준다
print(...) 세 줄쓴 토큰 3175, 쓴 몫 9.7% 와 98.2%큰 사물함은 열에 아홉이 비고, 작은 칸은 거의 꽉 찬다

같은 짐(3175토큰)을 두는 데 통째로 잡으면 열 배쯤 넓은 자리가 듭니다. 그 남는 자리에 다른 요청을 올릴 수 있으니, 쪽으로 나누면 동시에 처리할 요청 수가 늘어납니다.

작게 줄여서 눈으로 보기

쪽 단위로 올리면 빈칸이 얼마나 남는지 요청 하나씩 봅니다. 토큰 수는 설명용 숫자입니다.

Python
import math

page = 16                       # 쪽 하나에 토큰 16개
for used in [1, 16, 17, 35]:    # 요청 하나가 쓴 토큰 수(설명용 숫자)
    pages = math.ceil(used / page)
    print(f"{used:2d}토큰 → 쪽 {pages}개 = 자리 {pages * page}칸, 빈칸 {pages * page - used}")
출력
 1토큰 → 쪽 1개 = 자리 16칸, 빈칸 15
16토큰 → 쪽 1개 = 자리 16칸, 빈칸 0
17토큰 → 쪽 2개 = 자리 32칸, 빈칸 15
35토큰 → 쪽 3개 = 자리 48칸, 빈칸 13
결과읽는 법
1토큰 → 쪽 1개토큰이 하나라도 쪽 하나를 잡는다. 빈칸 15
16토큰 → 쪽 1개, 빈칸 0쪽이 딱 맞게 찼다
17토큰 → 쪽 2개한 토큰이 넘쳐 쪽을 하나 더 잡는다
35토큰 → 쪽 3개, 빈칸 13빈칸은 늘 마지막 쪽에만 생기고 15칸을 넘지 않는다

주의할 점

  • KV 캐시는 글 길이와 동시 요청 수에 비례해 커집니다(15장). 쪽으로 나눠도 GPU 메모리를 넘는 만큼은 올릴 수 없습니다.
  • vLLM 은 시작할 때 GPU 메모리의 정한 몫(--gpu-memory-utilization)을 KV 캐시용으로 미리 잡습니다. 같은 GPU 를 다른 프로그램과 나눠 쓰면 이 값을 낮춥니다.
← 이전 절16장 · 프리필과 디코드다음 절 →16장 · 연속 배치