2025년 9월 26일 금요일

vLLM 동작 방식, 설치 및 사용법

이 글은 vLLM 사용기를 간략히 정리한다. vLLM은 대규모 언어 모델(LLM)의 추론 및 서빙을 위한 고성능 라이브러리이다.
vLLM 메모리 사용 효율성


대규모 언어 모델을 서비스로 제공할 때 가장 큰 병목 현상은 메모리에서 발생한다. 특히 트랜스포머 아키텍처의 핵심인 어텐션 메커니즘은 이전 토큰들의 키(Key)와 값(Value)을 KV 캐시라는 메모리 공간에 저장하는데, 이 공간의 비효율적인 관리가 추론 속도를 저하시키는 주된 원인이다. 기존 방식에서는 모든 요청에 대해 최대 길이를 가정하고 메모리를 미리 할당하여 심각한 낭비를 초래했다.

vLLM은 이러한 문제를 해결하기 위해 PagedAttention이라는 알고리즘을 도입한 것이다. 이는 운영체제의 가상 메모리와 페이징 기법에서 영감을 얻은 기술이다. KV 캐시를 연속된 메모리 공간에 할당하는 대신, 페이지처럼 작은 블록 단위로 분할하여 관리한다. 이로써 메모리 단편화를 거의 없애고, 실제 필요한 만큼만 동적으로 메모리를 할당하여 낭비를 최대 90% 이상 줄일 수 있다. 결과적으로 동일한 하드웨어에서 훨씬 더 많은 요청을 동시에 처리하는 높은 처리량(Throughput)을 달성하게 된 것이다.

이 기술은 미국 UC 버클리 대학의 Sky Computing Lab 소속 연구원들이 개발했으며, 논문 발표와 함께 오픈소스로 공개되어 LLM 서빙 분야의 핵심 기술로 빠르게 자리 잡았다.


vLLM 동작 메커니즘

vLLM의 혁신은 단순히 빠른 코드를 작성한 것이 아니라, 대규모 언어 모델(LLM) 서빙의 근본적인 병목 지점인 메모리 관리 방식을 새롭게 설계한 것에서 출발한다.

1. 문제 정의: 비효율적인 KV 캐시 메모리

기존 시스템은 모든 요청에 대해 최대 출력 길이를 가정하고 거대한 메모리 공간을 미리 할당했다. 이는 아래 그림과 같이 심각한 메모리 낭비를 초래했다.


  • 내부 단편화 (Internal Fragmentation): 그림에서 'Request A'는 최대 2048개의 토큰을 생성할 수 있도록 메모리 공간을 미리 할당받았지만, 실제로는 1개의 토큰만 생성했다. 그 결과, 사용되지 않는 2038개의 슬롯이 낭비되고 있다. 'Request B' 역시 507개의 슬롯이 같은 이유로 낭비되고 있다. 이것이 바로 내부 단편화이다. vLLM은 운영체제의 '페이징(Paging)' 기법처럼 KV 캐시를 '블록(Block)'이라는 작은 단위로 나눈다. 처음부터 최대 길이에 맞춰 거대한 메모리 덩어리를 할당하는 대신, 토큰이 생성될 때마다 필요한 만큼 블록을 하나씩 동적으로 할당해 준다. 이는 C의 malloc 이 링크드 리스트 방식으로 메모리 효율적 관리하는 기법과 거의 유사하다.

  • 외부 단편화 (External Fragmentation): 'Request A'와 'Request B' 사이에는 사용되지 않는 빈 메모리 공간(회색 칸)이 존재한다. 하지만 이 공간이 새로운 요청을 처리하기에 너무 작거나 조각나 있다면, 전체적으로는 메모리가 충분해도 새로운 요청을 배치하지 못하는 외부 단편화가 발생한다. vLLM은 블록 단위로 메모리를 관리하기 때문에, 이 블록들이 물리적으로 연속될 필요가 없다. 

2. 핵심 아이디어: PagedAttention과 '메모리 링크드 리스트'

vLLM은 이 문제를 해결하기 위해 운영체제의 가상 메모리(Virtual Memory)와 페이징(Paging) 기법에서 영감을 얻은 PagedAttention 알고리즘을 도입했다. 

  • 기존 방식 (연속 할당): 한 권의 두꺼운 책처럼, 모든 페이지(토큰)가 순서대로 붙어있는 거대한 메모리 덩어리를 할당하는 것이다. 

  • vLLM 방식 (불연속 할당): 낱장의 종이(블록)에 내용을 적고, 어떤 종이가 다음 내용인지를 알려주는 별도의 '목차(Block Table)'를 관리하는 것과 같다. 이 종이들은 메모리 여기저기에 흩어져 있어도 목차만 따라가면 전체 내용을 순서대로 읽을 수 있다. 

이 '목차'가 바로 링크드 리스트의 '링크(포인터)'와 같은 역할을 하며, 메모리를 작은 블록 단위로 잘게 나누어 필요할 때마다 동적으로 할당해주기 때문에 내부 및 외부 단편화가 거의 발생하지 않는다.

3. 구현: 맞춤형 순전파(Forward) 및 역전파(Backward) 커널

PagedAttention이라는 새로운 메모리 구조를 만들었기 때문에, 이 구조를 이해하고 계산할 수 있는 새로운 연산 엔진이 필요해졌다. PyTorch의 기본 Attention 함수는 흩어져 있는 메모리 블록을 읽을 수 없기 때문이다.

이를 위해 vLLM은 순전파(Forward)와 역전파(Backward) 연산을 위한 CUDA 커널을 직접 개발하여 PyTorch 시스템에 이식했다.

1) 순전파(Forward) CUDA 커널

Attention Score를 계산하는 핵심 함수이다. block_tables라는 '목차'를 받아 흩어져 있는 Key, Value 블록의 물리적 주소를 찾아내어 Attention 연산을 수행한다.


  • 파일 위치: vllm/csrc/attention/attention_kernels.cu

C++
// 순전파(Forward Pass)를 위한 PagedAttention CUDA 커널
__global__ void paged_attention_v1_kernel(
  torch::Tensor out,              // 결과 텐서
  torch::Tensor query,            // Query 텐서
  torch::Tensor key_cache,        // Key 블록들이 저장된 전체 KV 캐시
  torch::Tensor value_cache,      // Value 블록들이 저장된 전체 KV 캐시
  torch::Tensor block_tables,     // ★★★ 흩어진 블록의 주소록 (메모리 링크드 리스트의 '링크')
  torch::Tensor context_lens,     // 각 시퀀스의 길이
  ...
) {
  // ... GPU 스레드 ID 계산 ...

  // ★★★ 블록 테이블에서 현재 시퀀스에 해당하는 블록 주소 목록을 가져온다
  const int64_t* block_table = block_tables[prompt_idx].data_ptr<int64_t>();

  // 루프를 돌며 block_table을 참조해 물리적 블록 주소를 계산하고
  // 해당 주소에서 Key, Value를 로드하여 Attention 연산을 수행한다.
  // ...
}

2) PyTorch 연동 및 역전파(Backward) 커널

이 커스텀 CUDA 커널을 PyTorch의 자동 미분 시스템과 연동시키기 위해 torch.autograd.Function을 상속받아 forward와 backward 메소드를 직접 정의한다. backward 메소드는 편미분을 통한 Gradient 계산을 위해 별도로 제작된 역전파용 CUDA 커널을 호출한다.

  • 파일 위치: vllm/model_executor/layers/attention.py

# PyTorch의 자동 미분 기능에 새로운 함수를 등록하는 부분
class PagedAttention(torch.autograd.Function):

    @staticmethod
    def forward(ctx, ...):
        # 순전파(Forward)용 CUDA 커널 (ops.paged_attention_v1)을 호출한다
        ops.paged_attention_v1(...)
        return output

    @staticmethod
    def backward(ctx, grad_output: torch.Tensor):
        # 역전파(Backward)를 위한 별도의 Gradient 계산용 CUDA 커널을 호출한다
        ops.paged_attention_v2_backward(...)
        return (d_query, d_key, d_value, ...)이

참고로, 파치토치 커널 함수는 다음과 같이 Monkey Patch 기법으로 간단히 커스텀 교체할 수 있다.

import transformers

from transformers.models.llama.modeling_llama import LlamaAttention

original_forward = LlamaAttention.forward

LlamaAttention.forward = custom_paged_attention_forward


단, 이렇게 블럭에 서브 시퀀스를 저장하면, QKV 내적 계산시 토큰의 위치별로 희소행렬 방식으로 계산해야 한다. vLLM은 고정된 블록 크기라는 규칙을 이용해 모든 토큰의 절대 위치를 즉시 계산한다. 특정 토큰의 순서를 고정된 블록 크기로 나누면 그 몫은 해당 토큰이 위치한 논리적 블록의 순서가 되고, 나머지는 그 블록 내에서의 상대적 위치가 된다. 시스템은 이 논리적 블록 순서를 블록 테이블에 대입하여 실제 물리 메모리 주소를 찾는다. 이를 통해 희소행렬처럼 표현된 시퀀스의 토큰들을 서로 계산되어야 할 위치의 토큰 임베딩벡터 끼리 내적계산한다.

vLLM은 1) PagedAttention이라는 효율적인 메모리 관리 기법을 고안하고, 2) 이 기법 위에서 동작하는 순전파 및 역전파 CUDA 커널 세트를 직접 개발하여, 3) 이를 PyTorch 시스템에 완벽하게 통합함으로써 하나의 고성능 추론 시스템을 완성한 것이다.

vLLM 설치

vLLM은 리눅스 환경을 정식으로 지원하며, 파이썬 3.8 이상과 CUDA 11.8 이상을 지원하는 NVIDIA GPU가 필요하다. 윈도우 사용자의 경우 WSL2(Windows Subsystem for Linux 2)를 통해 리눅스 환경을 구축한 후 설치해야 한다.

설치는 파이썬 패키지 관리자인 pip를 통해 간단하게 진행할 수 있다. 가상 환경을 먼저 구성하고 그 내부에 설치하는 것이 권장된다.

  1. 가상 환경 생성 및 활성화

    Bash
    python -m venv vllm-env
    source vllm-env/bin/activate
    
  2. vLLM 설치

    PyTorch가 자동으로 함께 설치되며, 시스템에 맞는 CUDA 버전과 호환되는 버전을 설치하는 것이 중요하다.

    Bash
    pip install vllm 


vLLM의 사용법은 크게 두 가지로 나뉜다. 첫째는 파이썬 코드 내에서 라이브러리로 직접 사용하는 오프라인 추론 방식이고, 둘째는 모델을 API 서버로 실행해두고 HTTP 요청으로 사용하는 온라인 서빙 방식이다.


파이썬 코드 예시 

다음은 파이썬 스크립트에서 직접 모델을 불러와 텍스트를 생성하는 예시이다. LLM 클래스로 모델을 로드하고 SamplingParams로 온도(temperature), top-p 등 생성 옵션을 조절한다.
Python
from vllm import LLM, SamplingParams

# 허깅페이스에서 모델 이름을 지정하여 LLM 객체 생성
# tensor_parallel_size는 사용할 GPU의 수를 의미한다
llm = LLM(model="meta-llama/Meta-Llama-3-8B-Instruct", tensor_parallel_size=1)

# 생성할 텍스트의 프롬프트 목록
prompts = [
    "대한민국의 수도는 어디인가?",
    "인공지능의 미래에 대해 한 문장으로 요약하면",
]

# 샘플링 파라미터 설정
sampling_params = SamplingParams(temperature=0.7, top_p=0.95, max_tokens=100)

# 텍스트 생성 실행
outputs = llm.generate(prompts, sampling_params)

# 결과 출력
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"프롬프트: {prompt}")
    print(f"생성된 텍스트: {generated_text}")
    print("---"


Ollama와 연계 (API 서버 활용)

Ollama는 모델을 로컬 환경에서 쉽게 실행하고 관리하게 해주는 도구이다. vLLM과 Ollama는 경쟁 관계이면서 상호 보완적으로 이해할 수 있다. 두 도구 모두 모델을 API 서버로 실행하는 기능을 제공하기 때문이다.

vLLM은 OpenAI의 API와 호환되는 서버를 매우 쉽게 실행할 수 있다. 터미널에서 아래와 같은 명령어를 실행하면 meta-llama/Meta-Llama-3-8B-Instruct 모델이 로드되어 8000번 포트로 API 서비스가 시작된다.

Bash
python -m vllm.entrypoints.openai.api_server \
    --model "meta-llama/Meta-Llama-3-8B-Instruct"
이렇게 실행된 vLLM 서버는 Ollama가 제공하는 API 엔드포인트와 동일한 방식으로 HTTP 요청을 통해 사용할 수 있다. 즉, Ollama를 사용하던 코드에서 API 주소만 vLLM 서버 주소(http://localhost:8000/v1)로 변경하면 vLLM의 높은 처리 성능을 그대로 활용할 수 있게 되는 것이다. 이는 vLLM을 기존 LLM 기반 애플리케이션의 백엔드 추론 엔진으로 손쉽게 교체할 수 있음을 의미한다.


간략한 챗봇 코드 예시

vLLM을 활용하여 터미널에서 간단하게 대화할 수 있는 챗봇 코드는 다음과 같다. 대화 기록을 누적하여 다음 답변 생성에 활용하는 방식이다.

Python
from vllm import LLM, SamplingParams

# 채팅 형식에 최적화된 모델을 선택하는 것이 좋다
llm = LLM(model="meta-llama/Meta-Llama-3-8B-Instruct")
sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=1024)

conversation_history = []

print("간단한 챗봇입니다. 대화를 시작하세요. (종료하려면 '종료' 입력)")

while True:
    user_input = input("나: ")
    if user_input == "종료":
        print("챗봇을 종료합니다.")
        break
    
    # 대화 기록에 사용자 입력을 추가한다
    conversation_history.append({"role": "user", "content": user_input})
    
    # 모델에 맞는 채팅 템플릿을 사용하여 프롬프트를 생성한다
    # 허깅페이스의 `apply_chat_template` 기능을 활용할 수 있다
    from transformers import AutoTokenizer
    tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")
    
    # 대화 기록을 프롬프트 문자열로 변환한다
    prompt_text = tokenizer.apply_chat_template(
        conversation_history, 
        tokenize=False, 
        add_generation_prompt=True
    )

    # vLLM으로 답변 생성
    outputs = llm.generate([prompt_text], sampling_params)
    
    bot_response = outputs[0].outputs[0].text
    print(f"챗봇: {bot_response}")

    # 대화 기록에 챗봇 답변을 추가한다
    conversation_history.append({"role": "assistant", "content": bot_response})


마무리

vLLM은 PagedAttention 기술을 통해 대규모 언어 모델 서빙의 효율성을 극대화한 핵심적인 도구이다. 메모리 관리의 혁신을 통해 이전에는 불가능했던 수준의 처리량을 달성했으며, 이는 더 많은 사용자가 더 빠르고 저렴하게 LLM 기술을 활용할 수 있는 길을 만들었다. 설치와 사용법 또한 간편하여 개발자들이 자신의 서비스에 강력한 LLM 추론 엔진을 쉽게 통합할 수 있도록 지원한다.

댓글 없음:

댓글 쓰기