vLLM은 어떻게 5~10배 더 높은 처리량을 달성할 수 있을까요? 그 핵심은 PagedAttention과 동적 메모리 관리에 있습니다. 이제 이 두 가지 기능이 어떻게 GPU 리소스를 최적화하는지 살펴보겠습니다.
KV 캐시를 가상 메모리처럼 사용? PagedAttention의 혁신적인 아이디어
자체 회귀 생성에서, 모델은 각 토큰을 생성할 때마다 해당 토큰의 Key와 Value를 캐시에 저장해야 합니다. 이러한 캐시를 KV 캐시라고 하며, 일반적으로 GPU 메모리의 60% 이상을 차지합니다.
기존 방식은 다음과 같습니다:
- 예를 들어 최대 컨텍스트 길이를 8192로 설정하면, 시스템은 모든 요청에 대해 8192개의 토큰을 저장할 수 있는 연속된 메모리를 미리 할당합니다.
- 이로 인해 짧은 요청도 큰 공간을 차지하고, 긴 요청이 올 경우 메모리가 부족하게 됩니다.
vLLM은 이런 문제를 해결하기 위해 PagedAttention을 도입했습니다. 이는 KV 캐시를 고정 크기의 "페이지"로 분할하여, 각 페이지가 예를 들어 512개의 토큰을 저장하도록 합니다.
각 요청은 더 이상 연속된 공간이 필요하지 않으며, 대신 "페이지 테이블"을 통해 데이터 위치를 추적합니다. 예를 들어, CUDA 커널은 이 테이블을 참조하여 다양한 물리적 위치에 저장된 데이터를 자동으로 결합합니다.
# vLLM 사용 예제
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-2-7b-chat-hf",
gpu_memory_utilization=0.9 # 최대 90%까지 GPU 메모리 사용
)
sampling_params = SamplingParams(max_tokens=256)
outputs = llm.generate(["AI를 쉽게 설명해주세요"], sampling_params)
동적 메모리 관리: 각 페이지의 메모리 활용 극대화
PagedAttention이 "어떻게 저장할 것인가"를 해결했다면, "누가 관리할 것인가"는 동적 메모리 관리의 역할입니다. 이를 통해 GPU 메모리를 효율적으로 관리할 수 있습니다.
예를 들어 BlockSpaceManager는 다음과 같은 작업을 수행합니다:
- 메모리 풀 초기화
- 새로운 요청에 대한 메모리 할당 가능 여부 확인
- 다중 요청 간 공유 가능한 부분의 관리
from vllm.core.block_manager import BlockSpaceManager
block_manager = BlockSpaceManager(
block_size=512, # 각 페이지는 512개의 토큰을 저장
num_gpu_blocks=1024, # 총 1024개의 GPU 블록 (약 512K 토큰)
num_cpu_blocks=128,
sliding_window=None
)
seq_len = 3200
blocks_needed = (seq_len + block_size - 1) // block_size # 올림 계산
if block_manager.can_allocate(blocks_needed):
block_table = block_manager.allocate(seq_len)
print(f"✅ {blocks_needed} 개의 블록 성공적으로 할당")
else:
print("❌ 메모리 부족, 서비스 거부")
연속 배치 처리: GPU의 지속적인 활용
효율적인 메모리 관리 외에도, GPU를 지속적으로 활용하는 것이 중요합니다. 이를 위해 vLLM은 연속 배치 처리를 도입했습니다.
전통적인 정적 배치 처리는 버스와 같아서, 시간에 따라 일괄 처리되지만, 연속 배치 처리는 실시간으로 요청을 처리하며 GPU를 계속 활용합니다.
import asyncio
from vllm import AsyncLLMEngine
from vllm.sampling_params import SamplingParams
engine = AsyncLLMEngine.from_engine_args(EngineArgs(model="Qwen/Qwen-7B"))
async def generate_stream(prompt: str):
results = []
async for output in engine.generate(prompt, SamplingParams(max_tokens=100), request_id=f"req-{hash(prompt)}"):
if not output.finished:
delta = output.outputs[0].text[len(''.join(results)):]
print(f"🟢 스트리밍 출력: {delta}")
results.append(delta)
else:
print(f"✅ 완료: {''.join(results)}")
return ''.join(results)
async def main():
await asyncio.gather(
generate_stream("재미있는 이야기 들려줘"),
generate_stream("양자역학 설명해줄래?"),
generate_stream("봄에 관한 시를 써줄래?")
)
asyncio.run(main())
실제 적용 사례: 기업 AI 아키텍처에 vLLM 통합
vLLM은 주로 "추론 가속 이미지" 형태로 배포되며, 다음의 아키텍처를 따릅니다:
[클라이언트]
↓ (HTTP/gRPC/OpenAI API)
[API 게이트웨이] → [로드 밸런서]
↓
[vLLM 추론 노드 클러스터]
↙ ↘
[GPU 워커: vLLM + PagedAttention] ←→ [공유 객체 스토리지 (모델 가중치)]
↓
[모니터링/로그/경고 시스템]
배포 팁 📌
- 페이지 크기: 512 또는 1024를 권장합니다.
- 메모리 예약: CUDA 컨텍스트 및 임시 버퍼를 위해 10%를 남겨두세요.
- max_num_seqs: 예상 최대 동시 요청 수의 1.2~1.5배로 설정하세요.
- 모니터링 포인트:
block_usage_ratio가 85%를 초과하면 확장을 고려하세요.