Skip to content

필사 모드: vLLM 완벽 가이드 — PagedAttention부터 프로덕션 최적화까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

LLM 추론 서빙은 비용과 성능의 균형이 핵심입니다. GPU 메모리의 60~80%를 차지하는 KV Cache를 효율적으로 관리하지 않으면, 비싼 GPU를 비효율적으로 사용하게 됩니다. **vLLM**은 UC Berkeley에서 개발한 오픈소스 LLM 추론 엔진으로, **PagedAttention**이라는 혁신적 메모리 관리 기법으로 이 문제를 해결합니다.

KV Cache 문제 이해

왜 KV Cache가 문제인가

Transformer 추론 시 KV Cache 크기 계산

def kv_cache_size_gb(

num_layers: int,

num_heads: int,

head_dim: int,

seq_len: int,

batch_size: int,

dtype_bytes: int = 2 # float16

) -> float:

"""

KV Cache 메모리 = 2 * L * H * D * S * B * dtype

(2 = K와 V 각각)

"""

total_bytes = 2 * num_layers * num_heads * head_dim * seq_len * batch_size * dtype_bytes

return total_bytes / (1024**3)

Llama 3 70B 예시

print(kv_cache_size_gb(

num_layers=80,

num_heads=64, # GQA: 8 KV heads

head_dim=128,

seq_len=4096,

batch_size=1

))

→ 약 5.2GB per request!

batch_size=32이면?

→ 약 166GB — A100 80GB 2장으로도 부족!

기존 방식의 낭비

기존 KV Cache 할당 (연속 메모리):

Request 1: [████████████░░░░░░░░] 실제 1024 토큰, 4096 예약

Request 2: [██████░░░░░░░░░░░░░░] 실제 512 토큰, 4096 예약

Request 3: [████████████████░░░░] 실제 3072 토큰, 4096 예약

총 예약: 4096 * 3 = 12,288 슬롯

실제 사용: 4,608 슬롯

낭비율: 62.5%!

PagedAttention

핵심 아이디어

OS의 가상 메모리(Virtual Memory) 페이징 개념을 KV Cache에 적용합니다:

PagedAttention KV Cache 관리:

논리 블록 (Logical Blocks):

Request 1: [B0] → [B1] → [B2] → [B3]

물리 블록 (Physical Blocks - GPU 메모리):

┌────┬────┬────┬────┬────┬────┬────┬────┐

│ B0 │ B2 │ B5 │ B1 │ B3 │ B6 │ B4 │ B7 │

│ R1 │ R1 │ R2 │ R1 │ R1 │ R2 │ R2 │FREE│

└────┴────┴────┴────┴────┴────┴────┴────┘

페이지 테이블:

Request 1: [0→0, 1→3, 2→1, 3→4]

Request 2: [0→2, 1→5, 2→6]

PagedAttention 핵심 구조

class PagedAttentionManager:

def __init__(self, num_blocks: int, block_size: int, num_heads: int, head_dim: int):

self.block_size = block_size # 예: 16 tokens per block

self.num_blocks = num_blocks

물리 블록 풀 (GPU 메모리에 미리 할당)

self.k_cache = torch.zeros(

num_blocks, block_size, num_heads, head_dim,

dtype=torch.float16, device='cuda'

)

self.v_cache = torch.zeros_like(self.k_cache)

프리 리스트

self.free_blocks = list(range(num_blocks))

def allocate_block(self) -> int:

"""물리 블록 하나 할당"""

if not self.free_blocks:

raise MemoryError("No free blocks")

return self.free_blocks.pop()

def free_block(self, block_id: int):

"""블록 반환"""

self.free_blocks.append(block_id)

def append_token(self, request_id: int, key: torch.Tensor, value: torch.Tensor):

"""토큰 추가 — 블록이 꽉 차면 새 블록 할당"""

page_table = self.page_tables[request_id]

last_block = page_table[-1]

slot_in_block = self.token_counts[request_id] % self.block_size

if slot_in_block == 0 and self.token_counts[request_id] > 0:

새 블록 필요

new_block = self.allocate_block()

page_table.append(new_block)

last_block = new_block

self.k_cache[last_block, slot_in_block] = key

self.v_cache[last_block, slot_in_block] = value

self.token_counts[request_id] += 1

Copy-on-Write로 Prefix 공유

여러 요청이 같은 시스템 프롬프트를 공유하는 경우

Copy-on-Write로 메모리 절약

system_prompt = "You are a helpful assistant..."

시스템 프롬프트의 KV Cache 블록을 공유

Request 1: system_prompt + "What is Python?"

Request 2: system_prompt + "Explain Docker"

Request 3: system_prompt + "How to use Git?"

공유 블록:

[System Block 0] ← 3개 요청이 공유 (ref_count=3)

[System Block 1] ← 3개 요청이 공유 (ref_count=3)

[System Block 2] ← 3개 요청이 공유 (ref_count=3)

개별 블록:

[R1 Block 3] [R2 Block 3] [R3 Block 3] ← 각자 고유

메모리 절약: 시스템 프롬프트 블록을 3번 복사하지 않음!

Continuous Batching

기존 Static Batching

모든 요청이 끝날 때까지 기다림

def static_batching(requests):

"""

R1: ████████████ (완료)

R2: ████████████████████ (완료)

R3: ████ (완료, but 대기...)

↑ 여기서야 새 배치 시작

"""

max_len = max(r.output_len for r in requests)

for step in range(max_len):

이미 끝난 요청도 GPU를 점유

outputs = model.forward(batch)

return outputs

vLLM의 Continuous Batching

def continuous_batching(scheduler):

"""

Step 1: [R1, R2, R3] → 모두 처리

Step 2: [R1, R2, R3] → R3 완료! → 빈 자리에 R4 투입

Step 3: [R1, R2, R4] → R1 완료! → R5 투입

Step 4: [R5, R2, R4] → ...

GPU 활용률: ~95% (vs Static의 ~50-60%)

"""

while requests_exist():

완료된 요청 제거, 새 요청 추가

batch = scheduler.schedule()

Prefill과 Decode를 분리하여 처리

prefill_batch = [r for r in batch if r.is_prefill]

decode_batch = [r for r in batch if r.is_decode]

if prefill_batch:

model.forward(prefill_batch, mode="prefill")

if decode_batch:

model.forward(decode_batch, mode="decode")

vLLM 설치 및 기본 사용

설치

pip 설치

pip install vllm

CUDA 12.1+ 필요, PyTorch 2.4+

GPU: Compute Capability 7.0+ (V100, A100, H100, L40S 등)

Offline Inference

from vllm import LLM, SamplingParams

모델 로드

llm = LLM(

model="meta-llama/Llama-3.1-8B-Instruct",

dtype="auto",

max_model_len=8192,

gpu_memory_utilization=0.9,

tensor_parallel_size=1,

)

샘플링 파라미터

sampling_params = SamplingParams(

temperature=0.7,

top_p=0.9,

max_tokens=1024,

repetition_penalty=1.1,

)

배치 추론

prompts = [

"Kubernetes Pod의 생명주기를 설명해주세요.",

"Docker와 Podman의 차이점은?",

"Redis 캐싱 전략에 대해 알려주세요.",

]

outputs = llm.generate(prompts, sampling_params)

for output in outputs:

print(f"Prompt: {output.prompt[:50]}...")

print(f"Generated: {output.outputs[0].text[:100]}...")

print(f"Tokens/s: {len(output.outputs[0].token_ids) / output.metrics.finished_time:.1f}")

print()

OpenAI 호환 서버

vLLM 서버 실행 (OpenAI API 호환)

vllm serve meta-llama/Llama-3.1-8B-Instruct \

--host 0.0.0.0 \

--port 8000 \

--max-model-len 8192 \

--gpu-memory-utilization 0.9 \

--tensor-parallel-size 2 \

--enable-prefix-caching \

--max-num-seqs 256

API 호출 (OpenAI SDK 호환)

curl http://localhost:8000/v1/chat/completions \

-H "Content-Type: application/json" \

-d '{

"model": "meta-llama/Llama-3.1-8B-Instruct",

"messages": [

{"role": "system", "content": "You are a helpful assistant."},

{"role": "user", "content": "vLLM이란 무엇인가요?"}

],

"temperature": 0.7,

"max_tokens": 512

}'

Python SDK

from openai import OpenAI

client = OpenAI(

base_url="http://localhost:8000/v1",

api_key="dummy" # vLLM은 인증 불필요

)

response = client.chat.completions.create(

model="meta-llama/Llama-3.1-8B-Instruct",

messages=[

{"role": "user", "content": "Hello!"}

],

stream=True

)

for chunk in response:

if chunk.choices[0].delta.content:

print(chunk.choices[0].delta.content, end="")

병렬화 전략

Tensor Parallelism

4 GPU에 모델 분산

vllm serve meta-llama/Llama-3.1-70B-Instruct \

--tensor-parallel-size 4 \

--dtype bfloat16

Pipeline Parallelism

8 GPU: 4-way TP × 2-way PP

vllm serve meta-llama/Llama-3.1-405B-Instruct \

--tensor-parallel-size 4 \

--pipeline-parallel-size 2

성능 최적화 팁

1. GPU 메모리 활용률

기본 0.9 (90%), 더 공격적으로 설정 가능

--gpu-memory-utilization 0.95

KV Cache 블록 수 확인

로그: "# GPU blocks: 12345, # CPU blocks: 0"

2. Prefix Caching

시스템 프롬프트가 동일한 요청이 많을 때 효과적

--enable-prefix-caching

3. Quantization

AWQ 양자화 모델 사용

vllm serve TheBloke/Llama-3.1-70B-AWQ \

--quantization awq \

--dtype auto

GPTQ

vllm serve TheBloke/Llama-3.1-70B-GPTQ \

--quantization gptq

FP8 (H100에서 최적)

vllm serve meta-llama/Llama-3.1-70B-Instruct \

--quantization fp8

4. Speculative Decoding

작은 모델로 추측 → 큰 모델로 검증

vllm serve meta-llama/Llama-3.1-70B-Instruct \

--speculative-model meta-llama/Llama-3.1-8B-Instruct \

--num-speculative-tokens 5

벤치마크

vLLM 벤치마크 도구

python -m vllm.entrypoints.openai.api_server \

--model meta-llama/Llama-3.1-8B-Instruct &

ShareGPT 데이터셋으로 벤치마크

python benchmarks/benchmark_serving.py \

--backend vllm \

--model meta-llama/Llama-3.1-8B-Instruct \

--dataset-name sharegpt \

--num-prompts 1000 \

--request-rate 10

벤치마크 결과 예시 (A100 80GB, Llama-3.1-8B):

| 메트릭 | vLLM | TGI | 순수 HF |

|-----------------|---------|---------|---------|

| Throughput | 2,400 | 1,800 | 400 |

| (tokens/s) | | | |

| TTFT p50 (ms) | 45 | 60 | 200 |

| TTFT p99 (ms) | 120 | 180 | 500 |

| ITL p50 (ms) | 8 | 10 | 25 |

| Max Batch Size | 256 | 128 | 16 |

| Memory Util. | 95% | 85% | 60% |

프로덕션 배포

Kubernetes 배포

apiVersion: apps/v1

kind: Deployment

metadata:

name: vllm-server

spec:

replicas: 1

selector:

matchLabels:

app: vllm

template:

metadata:

labels:

app: vllm

spec:

containers:

- name: vllm

image: vllm/vllm-openai:latest

command:

- python3

- -m

- vllm.entrypoints.openai.api_server

- --model=meta-llama/Llama-3.1-8B-Instruct

- --tensor-parallel-size=1

- --gpu-memory-utilization=0.9

- --max-model-len=8192

- --enable-prefix-caching

ports:

- containerPort: 8000

resources:

limits:

nvidia.com/gpu: 1

memory: 32Gi

requests:

nvidia.com/gpu: 1

memory: 24Gi

readinessProbe:

httpGet:

path: /health

port: 8000

initialDelaySeconds: 120

periodSeconds: 10

volumeMounts:

- name: model-cache

mountPath: /root/.cache/huggingface

volumes:

- name: model-cache

persistentVolumeClaim:

claimName: model-cache-pvc

apiVersion: v1

kind: Service

metadata:

name: vllm-service

spec:

selector:

app: vllm

ports:

- port: 80

targetPort: 8000

type: ClusterIP

퀴즈

OS의 가상 메모리 페이징 개념을 KV Cache에 적용합니다. 연속 메모리 할당 대신 작은 블록 단위로 KV Cache를 관리하여 메모리 단편화와 낭비를 해소합니다.

요청이 완료되면 즉시 배치에서 제거하고 새 요청을 추가합니다. Static Batching은 가장 긴 요청이 끝날 때까지 모든 슬롯을 점유하여 GPU가 낭비됩니다.

동일한 시스템 프롬프트를 사용하는 여러 요청이 있을 때 효과적입니다. 공통 접두사의 KV Cache를 공유하여 중복 계산과 메모리를 절약합니다.

Tensor Parallelism은 하나의 레이어를 여러 GPU에 분할하고, Pipeline Parallelism은 서로 다른 레이어를 다른 GPU에 배치합니다. TP는 레이턴시를, PP는 처리량을 최적화합니다.

작은 모델이 여러 토큰을 빠르게 생성(추측)하고, 큰 모델이 이를 한번에 검증합니다. 검증된 토큰만 채택하여, 품질을 유지하면서 속도를 높입니다.

KV Cache에 할당할 GPU 메모리의 비율입니다. 0.9이면 전체 GPU 메모리의 90%까지 KV Cache로 사용하여 더 많은 동시 요청을 처리할 수 있습니다.

FP16 기준 약 140GB이므로 최소 2장이 필요합니다. AWQ/GPTQ 4bit 양자화를 사용하면 1장으로도 가능합니다.

마무리

vLLM은 PagedAttention, Continuous Batching, 다양한 병렬화 전략을 통해 LLM 추론의 표준 도구로 자리잡았습니다. OpenAI API 호환 서버를 제공하여 기존 코드 변경 없이 도입할 수 있고, Kubernetes 환경에서의 프로덕션 배포도 용이합니다.

참고 자료

- [vLLM 공식 사이트](https://vllm.ai)

- [PagedAttention 논문](https://arxiv.org/abs/2309.06180)

- [vLLM GitHub](https://github.com/vllm-project/vllm)

- [vLLM Documentation](https://docs.vllm.ai/)

현재 단락 (1/266)

LLM 추론 서빙은 비용과 성능의 균형이 핵심입니다. GPU 메모리의 60~80%를 차지하는 KV Cache를 효율적으로 관리하지 않으면, 비싼 GPU를 비효율적으로 사용하게 됩니...

작성 글자: 0원문 글자: 8,286작성 단락: 0/266