1. vLLM을 사용해야 하는가? LLM 서빙의 새로운 기준
최근 발표된 Llama3.1 모델이 최대 405b개의 파라미터를 가지는 등, 거대 언어 모델(LLM)의 성능은 기하급수적으로 발전하고 있습니다. 하지만 이러한 발전은 모델을 구동하기 위한 컴퓨팅 자원의 막대한 요구로 이어집니다. 고가의 GPU 자원을 효율적으로 사용하지 못한다면, 아무리 뛰어난 모델이라도 실제 서비스에 적용하기는 어렵습니다. 바로 이 지점에서 효율적인 LLM 서빙의 중요성이 대두되며, vLLM은 이 문제에 대한 가장 강력한 해결책 중 하나로 주목받고 있습니다.
단순히 PyTorch와 같은 딥러닝 프레임워크를 사용하여 LLM을 로드하고 서빙할 수도 있지만, 이는 GPU 메모리 관리, 처리 속도, 그리고 여러 사용자의 동시 요청을 처리하는 스케일링 측면에서 심각한 비효율을 초래합니다. vLLM은 이러한 문제들을 해결하기 위해 탄생한 전문 서빙 프레임워크로, LLM 서빙의 새로운 기준을 제시합니다.
LLM 서빙 프레임워크를 사용하는 이유
LLM을 서비스로 제공하기 위해서는 단순히 모델을 실행하는 것을 넘어, 한정된 자원 내에서 최대한 많은 사용자의 요청을 빠르고 안정적으로 처리해야 합니다. 이를 위해 vLLM, ollama, llama.cpp, OpenLLM 등 다양한 전문 프레임워크가 개발되었습니다. 이러한 프레임워크가 필요한 핵심적인 이유는 다음과 같습니다.
- GPU 메모리 최적화: LLM 추론 과정에서 가장 큰 병목 중 하나는 GPU 메모리입니다. 전문 프레임워크는 KV 캐시와 같은 중간 결과물을 효율적으로 관리하여 메모리 사용량을 최소화하고, 더 큰 모델을 서빙하거나 더 많은 동시 사용자를 수용할 수 있게 합니다.
- 처리 시간 단축: 다중 요청을 효과적으로 묶어 처리하는 배치(Batching) 기술이나, 추론 과정을 최적화하는 다양한 기법을 통해 개별 요청의 지연 시간(Latency)을 줄이고 전체 처리량(Throughput)을 극대화합니다.
- 다중 사용자 스케일링: 여러 사용자의 요청이 동시에 들어왔을 때, 이를 공정하고 효율적으로 처리하는 스케줄링 기능이 내장되어 있어 안정적인 서비스 운영이 가능합니다.
vLLM의 핵심 특징
vLLM은 여러 서빙 프레임워크 중에서도 특히 짧은 지연 시간과 뛰어난 메모리 효율성으로 높은 평가를 받고 있습니다. vLLM의 성능을 뒷받침하는 핵심 특징은 다음과 같습니다.
- PagedAttention을 통한 KV 캐시 최적화: 운영체제의 페이징(Paging) 기법에서 영감을 얻은 PagedAttention을 통해 어텐션 메커니즘에서 생성되는 KV 캐시를 물리적으로 분리된 메모리 블록에 저장합니다. 이를 통해 메모리 단편화를 방지하고, 활용률을 극대화합니다.
- 지속적인 배치(Continuous Batching) 처리: 기존의 정적 배치(Static Batching) 방식과 달리, 요청이 완료될 때마다 즉시 새로운 요청을 배치에 추가하여 GPU의 유휴 시간을 최소화하고 처리량을 높입니다.
- Chunk Prefill: 긴 프롬프트 처리 시, 이를 여러 청크로 나누어 처리함으로써 메모리 사용량을 최적화하고 응답 시작 시간을 단축합니다.
- 폭넓은 하드웨어 및 기술 지원:
- NVIDIA CUDA / AMD HIP: 대부분의 AI 가속기에 사용되는 NVIDIA CUDA는 물론, AMD의 HIP 환경까지 지원하여 폭넓은 하드웨어 호환성을 제공합니다.
- 다양한 양자화(Quantization) 지원: GPTO, AWQ, INT4, INT8, FP8 등 다양한 양자화 기법을 지원하여 모델의 성능 저하를 최소화하면서 메모리 사용량과 계산 비용을 크게 절감할 수 있습니다.
vLLM 필수 요구사항
글을 작성하는 시점을 기준으로, vLLM을 사용하기 위해서는 다음의 환경 요건이 충족되어야 합니다.
- 운영체제: Linux 환경
- Python 버전: 3.10 이상
- GPU: NVIDIA GPU (Compute Capability 7.0 이상: V100, T4, RTX 20xx 시리즈, A100, L4, H100 등)
환경 사양 예시
| 항목 | 사양 |
| 운영체제 | Ubuntu 20.04 LTS |
| CPU | Intel Xeon Silver 4314 (16코어 32스레드 @ 2.40GHz) |
| 메모리 | 256GB |
| GPU | NVIDIA RTX A6000 48GB x 2 |
이 사양을 기준으로, 양자화하지 않은 Llama3.1 8B 모델은 매우 여유롭게 운영할 수 있습니다.
--------------------------------------------------------------------------------
2. 사전 준비: Hugging Face 가입 및 Llama3 모델 다운로드
vLLM으로 LLM을 서빙하기 위한 첫 번째 단계는 당연하게도 서빙할 모델을 확보하는 것입니다. 이 섹션에서는 세계 최대의 AI 모델 허브인 Hugging Face에 가입하고, 실습에 사용할 meta-llama/Llama-3.1-8B-Instruct 모델의 사용 허가를 받은 뒤, 실제 리눅스 서버 환경으로 다운로드하는 전체 과정을 안내합니다.
Hugging Face 가입 및 모델 사용 신청
Meta에서 배포하는 Llama 시리즈와 같은 일부 고성능 모델은 사용자의 정보와 동의를 요구하는 Gated Model 정책을 따릅니다. 따라서 모델을 다운로드하려면 먼저 Hugging Face 계정이 필요합니다.
- Hugging Face 가입: Hugging Face 웹사이트에 접속하여 계정을 생성합니다. 공개된 LLM을 다운로드하고 커뮤니티에 참여하기 위해 필수적인 과정입니다.
- 모델 선택: 실습에서는 준수한 성능과 비교적 낮은 리소스 요구사항을 가진 meta-llama/Llama-3.1-8B-Instruct 모델을 사용합니다. 이 모델은 지시사항(Instruction)을 따르도록 파인튜닝되어 있어 챗봇이나 질의응답 서비스에 적합합니다.
- 모델 접근 요청:
- 로그인 후 Llama-3.1-8B-Instruct 모델 페이지로 이동합니다.
- 'Expand to review and access' 버튼을 클릭하여 라이선스 동의 및 사용자 정보 입력 양식을 펼칩니다.
- 이름, 소속 등 필요한 개인정보를 입력하고 라이선스 약관에 동의한 후 Submit 버튼을 클릭합니다.
- 승인 대기: 요청을 제출하면 Request Status가 PENDING으로 표시됩니다. 일반적으로 10분 이상 소요될 수 있으며, 승인이 완료되면 상태가 ACCEPT로 변경됩니다. 이메일로도 알림이 전송됩니다.
Hugging Face 접근 토큰(Access Token) 발급
리눅스 서버에서 git clone 명령어로 비공개(Gated) 모델을 다운로드하려면 인증이 필요합니다. 이때 계정 비밀번호 대신, 보안을 위해 발급받은 접근 토큰을 사용합니다.
- Hugging Face 우측 상단의 프로필 아이콘을 클릭한 뒤 Settings로 이동합니다.
- 왼쪽 메뉴에서 Access Tokens를 선택합니다.
- + Create new token 버튼을 클릭하여 새 토큰을 생성합니다.
- Token type은 모델을 읽기만 할 것이므로 Read로 설정하고, 적절한 이름을 지정한 뒤 Create token을 클릭합니다.
- 생성된 토큰은 한 번만 표시되므로, 안전한 곳에 즉시 복사하여 보관해야 합니다.
리눅스 환경에서 모델 다운로드
이제 모든 준비가 끝났습니다. 실제 서버 환경에서 모델을 다운로드해 보겠습니다.
- git-lfs 설치: LLM 모델 파일은 수십 기가바이트에 달하는 대용량 파일이므로, 이를 처리하기 위한 Git 확장 프로그램인 git-lfs(Large File Storage)를 반드시 설치해야 합니다.
- 모델 다운로드: git clone 명령어를 사용하여 모델을 다운로드합니다.
- 명령어 실행 후 사용자 이름(Username)과 비밀번호(Password)를 입력하라는 프롬프트가 나타납니다.
- Username: 본인의 Hugging Face 아이디를 입력합니다.
- Password: 계정 비밀번호가 아닌, 위에서 발급받은 접근 토큰(Access Token)을 붙여넣기 합니다.
다운로드가 완료되면 Meta-Llama-3.1-8B-Instruct 디렉토리 내에 .safetensors 확장자를 가진 모델 가중치 파일들이 생성된 것을 확인할 수 있습니다.
--------------------------------------------------------------------------------
3. vLLM 설치 및 기본 테스트
모델 다운로드를 완료했다면, 이제 LLM 서빙의 핵심 프레임워크인 vLLM을 설치할 차례입니다. 이 섹션에서는 vLLM을 설치하고, 다운로드한 Llama3.1 모델을 로드하여 간단한 질의응답을 수행하는 코드를 실행해 봅니다.
vLLM 설치
vLLM은 pip를 통해 간단하게 설치할 수 있습니다. 먼저 pip3가 설치되어 있는지 확인하고, 없다면 설치합니다.
# pip3 설치
sudo apt update
sudo apt install python3-pip
이제 pip3를 사용하여 vLLM을 설치합니다.
pip3 install vllm
설치 완료 테스트
vLLM이 성공적으로 설치되었는지 확인하기 위해, 간단한 Python 스크립트를 작성하여 모델을 로드하고 질문에 대한 답변을 생성해 보겠습니다.
from vllm import LLM, SamplingParams
# 1. 모델 경로 지정
# 이전 단계에서 다운로드한 모델의 절대 경로를 지정합니다.
model_path = "/home/xxxjjhhh/my_model/Meta-Llama-3.1-8B-Instruct/"
# 2. vLLM 엔진 초기화: LLM
# vLLM의 핵심 클래스인 LLM을 초기화합니다.
# - model: 로드할 모델의 경로
# - gpu_memory_utilization: GPU 메모리의 최대 사용률 (0.9 = 90%)
# - tensor_parallel_size: 모델 병렬 처리에 사용할 GPU 개수
llm = LLM(model=model_path, gpu_memory_utilization=0.9, tensor_parallel_size=2)
# 3. 답변 생성 전략 설정: SamplingParams
# - temperature: 생성의 창의성. 낮을수록 결정적, 높을수록 무작위적입니다. (0.5)
# - top_p: 누적 확률이 p 이상인 단어 후보군에서만 샘플링합니다. (0.7)
# - repetition_penalty: 반복적인 단어 등장을 억제합니다. 1.0 이상으로 설정합니다. (1.1)
# - max_tokens: 생성할 최대 토큰 수.
sampling_params = SamplingParams(temperature=0.5, top_p=0.7, repetition_penalty=1.1, max_tokens=1024)
# 4. 질의 및 응답 생성
query = "해리포터의 줄거리를 한글로 간략히 설명해 주세요."
response = llm.generate(query, sampling_params)
# 5. 결과 출력
# response는 리스트 형태이며, 각 요청에 대한 결과가 담겨 있습니다.
print(response[0].outputs[0].text)
이 코드를 실행했을 때, 해리포터 줄거리에 대한 그럴듯한 한국어 답변이 출력된다면 vLLM 설치와 기본 모델 구동이 성공적으로 완료된 것입니다.
주요 클래스 파라미터 상세 분석
vLLM의 동작을 세밀하게 제어하기 위해 LLM 클래스와 SamplingParams 클래스의 주요 파라미터를 이해하는 것이 중요합니다.
LLM 클래스 주요 파라미터
- model: Hugging Face 모델 이름 또는 로컬 경로.
- tokenizer: 토크나이저의 경로 또는 이름. 지정하지 않으면 model 경로에서 찾습니다.
- tensor_parallel_size: 모델의 텐서를 여러 GPU에 분산하여 병렬 처리할 때 사용할 GPU의 개수.
- quantization: 모델 가중치를 양자화하는 방법 (awq, gptq 등)을 지정하여 메모리 사용량을 줄입니다.
- gpu_memory_utilization: vLLM이 사용할 전체 GPU 메모리의 최대 비율 (0.0 ~ 1.0).
- trust_remote_code: True로 설정 시, Hugging Face Hub에서 모델 코드를 다운로드하고 실행하는 것을 허용합니다.
- dtype: 모델 가중치 및 계산에 사용할 데이터 타입 (auto, float16, bfloat16 등).
- seed: 난수 생성을 위한 시드 값으로, 재현 가능한 결과를 얻기 위해 사용됩니다.
- enforce_eager: True로 설정하면 CUDA 그래프를 비활성화하고 Eager 모드로 실행합니다. 디버깅에 유용할 수 있습니다.
SamplingParams 클래스 주요 파라미터
- n: 각 프롬프트에 대해 생성할 독립적인 출력 시퀀스의 개수.
- best_of: n개보다 많은 후보를 생성한 후, 가장 높은 로그 확률을 가진 n개의 결과를 반환합니다. best_of는 n보다 크거나 같아야 합니다.
- temperature: 샘플링의 무작위성을 제어. 0에 가까울수록 결정적이고, 높을수록 다양성이 증가합니다. (0.0으로 설정 시 Greedy Decoding)
- top_p: 누적 확률이 top_p를 초과하는 가장 가능성 높은 토큰들만 샘플링 후보로 고려합니다 (Nucleus Sampling).
- top_k: 가장 확률이 높은 k개의 토큰만 샘플링 후보로 고려합니다. -1은 비활성화를 의미합니다.
- presence_penalty: 텍스트에 이미 등장한 토큰에 패널티를 부여하여 새로운 토픽을 유도합니다. 양수 값은 모델이 새로운 토큰을 생성하도록 장려합니다.
- frequency_penalty: 텍스트에 자주 등장한 토큰에 패널티를 부여하여 반복을 줄입니다. 양수 값은 모델이 이미 사용된 토큰을 반복할 가능성을 줄입니다.
- repetition_penalty: 반복적인 단어 등장을 억제하는 패널티. 1.0은 패널티 없음을 의미합니다.
- stop: 지정된 문자열이 생성되면 텍스트 생성을 중단합니다. (예: ["\n", "User:"])
- max_tokens: 생성할 최대 토큰 수를 제한합니다.
- logits_processors: 토큰 생성 시 각 토큰의 확률(logit)을 동적으로 수정하는 함수 리스트를 전달하여 출력을 세밀하게 제어할 수 있습니다.
기타: 설치 오류 해결
vLLM 설치 과정에서 tokenizers 라이브러리와 관련된 의존성 문제로 오류가 발생하는 경우가 있습니다. 이는 주로 Rust 컴파일러가 시스템에 설치되어 있지 않아 발생하는 문제입니다. 이 경우, 아래 명령어를 통해 Rust와 관련 환경을 설치하면 문제를 해결할 수 있습니다.
# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 환경 변수 적용
source $HOME/.cargo/env
이제 로컬 환경에서 vLLM을 성공적으로 실행했습니다.
--------------------------------------------------------------------------------
4. FastAPI를 이용한 API 서버 구축
지금까지는 로컬 Python 스크립트를 통해 vLLM을 직접 실행했습니다. 하지만 실제 서비스에서는 다른 애플리케이션이나 원격 사용자가 HTTP 요청을 통해 LLM의 기능을 호출할 수 있어야 합니다. 이를 위해 LLM을 웹 API 형태로 감싸는 과정이 필수적입니다.
이 섹션에서는 현대적이고 빠른 Python 웹 프레임워크인 FastAPI를 사용하여, 우리가 만든 vLLM 모델 구동 로직을 외부에서 접근 가능한 RESTful API 서비스로 전환하는 방법을 단계별로 안내합니다.
의존성 설치
FastAPI 서버를 구동하기 위해서는 몇 가지 추가 라이브러리가 필요합니다. uvicorn은 고성능 ASGI(Asynchronous Server Gateway Interface) 서버이며, fastapi는 웹 프레임워크 자체, pydantic은 데이터 유효성 검사를 위해 FastAPI가 사용하는 라이브러리입니다.
pip3 install uvicorn
pip3 install fastapi
pip3 install pydantic
FastAPI 서버 스크립트 작성 (server.py)
이제 이전 섹션에서 작성했던 vLLM 구동 코드를 FastAPI 애플리케이션에 통합해 보겠습니다. server.py라는 이름으로 파일을 생성하고 아래 코드를 작성합니다.
import uvicorn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from vllm import LLM, SamplingParams
# 1. FastAPI 앱 초기화
app = FastAPI()
# 2. vLLM 모델 및 샘플링 파라미터 초기화 (이전 코드와 동일)
model_path = "/home/xxxjjhhh/my_model/Meta-Llama-3.1-8B-Instruct/"
llm = LLM(model=model_path, gpu_memory_utilization=0.9, tensor_parallel_size=2)
sampling_params = SamplingParams(temperature=0.5, top_p=0.7, repetition_penalty=1.1, max_tokens=1024)
# 3. API 요청 본문(Request Body) 구조 정의
# Pydantic의 BaseModel을 상속받아 클라이언트가 보내야 할 데이터 형식을 정의합니다.
class QueryRequest(BaseModel):
query: str
# 4. API 엔드포인트 생성
# @app.post("/generate/") 데코레이터는 이 함수가 HTTP POST 요청을
# '/generate/' 경로로 처리하도록 지정합니다.
@app.post("/generate/")
async def generate_response(request: QueryRequest):
try:
# Pydantic 모델로 정의된 요청 본문에서 'query'를 추출합니다.
prompt = request.query
# vLLM을 사용하여 응답 생성
response = llm.generate(prompt, sampling_params)
result_text = response[0].outputs[0].text
# 결과를 JSON 형태로 반환
return {"response": result_text}
except Exception as e:
# 오류 발생 시 500 상태 코드와 함께 오류 메시지를 반환합니다.
raise HTTPException(status_code=500, detail=str(e))
서버 실행 및 관리
작성된 server.py 파일을 uvicorn을 통해 실행하여 API 서버를 구동할 수 있습니다.
서버 실행
터미널에서 아래 명령어를 실행하여 서버를 백그라운드에서 실행합니다.
uvicorn server:app --host 0.0.0.0 --port 8080 --reload &
- server:app: server.py 파일의 app 객체를 실행하라는 의미입니다.
- --host 0.0.0.0: 모든 네트워크 인터페이스에서 접속을 허용합니다 (외부 접속 가능).
- --port 8080: 8080번 포트를 사용합니다.
- --reload: 코드 변경 시 서버를 자동으로 재시작합니다 (개발 시 유용).
- &: 프로세스를 백그라운드에서 실행합니다.
서버 종료
백그라운드에서 실행 중인 uvicorn 프로세스를 찾아 종료해야 합니다.
# 'uvicorn'이라는 이름의 프로세스를 찾습니다.
ps aux | grep uvicorn
# 출력된 목록에서 PID(프로세스 ID)를 확인하고 kill 명령어로 종료합니다.
sudo kill [PID번호]
때로는 서버를 종료한 후에도 Python 프로세스가 GPU 메모리를 계속 점유하는 경우가 있습니다. nvidia-smi 명령어로 확인 후, 남아있는 프로세스가 있다면 해당 PID를 kill 명령어로 직접 정리해주는 것이 좋습니다.
Pro-Tip: 좀비 프로세스 강제 종료
일반 kill 명령어로도 종료되지 않는 완고한 프로세스가 남아있을 수 있습니다. 이런 경우, 다음의 더 강력한 명령어들을 사용할 수 있습니다.
# 1. 특정 포트를 사용 중인 프로세스의 PID를 찾습니다. (예: 8080 포트)
sudo lsof -i :8080
# 2. 확인된 PID를 강제로 종료합니다.
sudo kill -9 [PID번호]
# 3. 또는, 프로세스 이름의 일부(예: uvicorn)를 포함하는 모든 프로세스를 강제로 종료합니다.
sudo pkill -9 -f uvicorn
API 테스트
서버가 정상적으로 실행 중이라면, curl과 같은 도구를 사용하여 API를 테스트할 수 있습니다. 새 터미널을 열고 아래 명령어를 실행해 보세요.
curl -X POST "http://localhost:8080/generate/" \
-H "Content-Type: application/json" \
-d '{"query": "인공지능의 미래에 대해 한 문단으로 설명해줘."}'
이 명령어는 localhost:8080/generate/ 엔드포인트로 JSON 형식의 데이터를 담아 POST 요청을 보냅니다. 성공적으로 실행되면, vLLM이 생성한 답변이 JSON 형태로 반환될 것입니다.
이제 단일 요청을 처리하는 API 서버를 구축했습니다. 하지만 실제 서비스 환경에서는 여러 사용자의 요청을 동시에, 그리고 지연 없이 처리하는 고도화된 아키텍처가 필요합니다. 다음 장에서는 다중 사용자 지원과 Docker를 이용한 배포 방법을 알아보겠습니다.
--------------------------------------------------------------------------------
5. 프로덕션 환경을 위한 고도화: 다중 사용자 지원 및 Docker 배포
실제 서비스 환경에서는 수많은 사용자의 요청이 예측 불가능한 간격으로 들어옵니다. 이런 상황에서 요청을 하나씩 순차적으로 처리하는 방식은 사용자 경험을 심각하게 저하시킵니다. 따라서 여러 요청을 동시에, 지연 없이 처리하는 것이 프로덕션 레벨 LLM 서빙의 핵심 과제입니다.
이 섹션에서는 이를 위해 vLLM의 비동기 엔진을 활용하여 다중 사용자 요청을 효율적으로 처리하고 스트리밍 응답을 구현하는 방법을 알아봅니다. 나아가, 안정적이고 재현 가능한 배포를 위해 모든 환경을 Docker 컨테이너로 패키징하는 심층적인 과정을 다룹니다.
5.1. 다중 사용자 지원 및 스트리밍 응답 구현
vLLM 엔진 클래스 이해
vLLM은 사용 목적에 따라 두 가지 상위 엔진 클래스를 제공합니다.
- LLM 클래스: 이전 섹션에서 사용한 클래스로, 오프라인 환경이나 단일 요청 처리에 최적화되어 있습니다. 사용법이 간단하고 직관적입니다.
- AsyncLLMEngine 클래스: 온라인 서빙 환경을 위해 설계된 클래스입니다. 비동기 I/O를 통해 여러 사용자의 요청을 동시에 처리하는 데 특화되어 있습니다.
다중 사용자 환경에서는 여러 요청이 블로킹 없이 동시에 처리되어야 하므로, AsyncLLMEngine을 사용하는 것이 이상적입니다.
스트리밍 응답 구현 (LLMEngine 활용)
LLM이 긴 답변을 생성할 때, 모든 텍스트가 완성될 때까지 기다렸다가 한 번에 반환하는 것은 사용자가 긴 지연을 느끼게 합니다. ChatGPT와 같은 서비스처럼, 토큰이 생성될 때마다 실시간으로 클라이언트에게 전송하는 스트리밍(Streaming) 방식은 사용자 경험을 크게 향상시킵니다.
vLLM의 AsyncLLMEngine이 온라인 서빙에 가장 적합한 선택이지만, 초기 버전에서는 구현상의 어려움이 있었습니다. 따라서 여기서는 교육적인 목적으로, 저수준 API인 LLMEngine을 직접 활용하여 스트리밍을 구현하는 견고한 방법을 먼저 살펴보겠습니다. 이 방식을 통해 vLLM 엔진의 내부 동작을 더 깊이 이해할 수 있습니다.
import asyncio
import uuid
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from vllm import EngineArgs, LLMEngine, SamplingParams
# LLMEngine을 직접 초기화합니다.
engine_args = EngineArgs(
model="/home/xxxjjhhh/my_model/Meta-Llama-3.1-8B-Instruct/",
gpu_memory_utilization=0.9,
tensor_parallel_size=2
)
llm = LLMEngine.from_engine_args(engine_args)
app = FastAPI()
class QueryRequest(BaseModel):
query: str
@app.post("/generate")
async def generate_post(request: QueryRequest):
request_id = str(uuid.uuid4())
sampling_params = SamplingParams(temperature=0.5, top_p=0.7, repetition_penalty=1.1, max_tokens=1024)
# 1. 생성 요청을 엔진에 추가
llm.add_request(request_id, request.query, sampling_params)
sent_text = ""
# 2. 비동기 스트리밍 함수 정의
async def stream_response():
nonlocal sent_text
while True:
# 3. 엔진의 다음 스텝을 실행하여 결과 확인
request_outputs = llm.step()
for output in request_outputs:
if output.request_id == request_id:
# 새로 생성된 텍스트만 추출
new_text = output.outputs[0].text[len(sent_text):]
sent_text = output.outputs[0].text
# 공백 단위로 분리하여 클라이언트에 전송 (yield)
for word in new_text.split(" "):
if word:
yield word + " "
# 생성 완료 시 루프 종료
if output.finished:
return
# CPU 부하를 줄이기 위해 짧은 대기 시간 추가
await asyncio.sleep(0.01)
return StreamingResponse(stream_response(), media_type="text/plain")
5.2. Docker를 활용한 배포 자동화
도커 배포 아키텍처
로컬 환경에서 개발한 애플리케이션을 서버에 배포할 때, 의존성 충돌이나 환경 차이로 인한 문제가 자주 발생합니다. 도커는 애플리케이션과 그 의존성을 컨테이너라는 격리된 환경에 패키징하여, 어디서든 동일하게 실행될 수 있도록 보장합니다.
vLLM 서버 배포 시에는 다음과 같은 아키텍처를 구성하는 것이 효율적입니다.
- vLLM/FastAPI 코드 및 의존성: 도커 이미지 내부에 포함시켜 환경의 일관성을 유지합니다.
- LLM 모델 파일: 모델 파일은 크기가 매우 크므로 이미지에 포함시키지 않습니다. 대신, 호스트 서버의 특정 경로에 모델을 위치시키고, 이 경로를 도커 컨테이너 내부로 **볼륨 마운트(Volume Mount)**하여 사용합니다.
스크립트 수정 (async_server.py)
이제 우리의 최종 목표인 프로덕션용 서비스를 위해, 현재는 안정화된 AsyncLLMEngine을 사용하여 도커 환경에 최적화된 스크립트를 작성하겠습니다. 이 방식은 더 깔끔하고 관용적인 비동기 코드를 제공합니다. 모델 경로는 하드코딩 대신 환경 변수에서 읽어오고, SamplingParams의 주요 값들을 API 요청 시 동적으로 받을 수 있도록 QueryRequest 모델을 확장합니다.
import os
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from vllm import AsyncEngineArgs, AsyncLLMEngine, SamplingParams
# 모델 경로를 환경 변수 'MODEL_PATH'에서 읽어옵니다.
model_path = os.getenv('MODEL_PATH')
engine_args = AsyncEngineArgs(
model=model_path,
gpu_memory_utilization=0.95,
tensor_parallel_size=2
)
llm = AsyncLLMEngine.from_engine_args(engine_args)
app = FastAPI()
# API 요청 시 다양한 샘플링 파라미터를 받을 수 있도록 확장
class QueryRequest(BaseModel):
request_id: str
query: str
n: int = Field(default=1)
top_p: float = Field(default=0.7)
temperature: float = Field(default=0.0)
max_tokens: int = Field(default=1024)
seed: int = Field(default=42)
@app.post("/generate")
async def generate_post(request: QueryRequest):
sent_text = ""
async def stream_response():
nonlocal sent_text
sampling_params = SamplingParams(
n=request.n,
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens,
seed=request.seed
)
# AsyncLLMEngine의 generate 메서드는 비동기 제너레이터를 반환합니다.
results_generator = llm.generate(request.query, sampling_params, request_id=request.request_id)
async for output in results_generator:
text = output.outputs[0].text
new_text = text[len(sent_text):]
sent_text = text
for word in new_text.split(" "):
if word:
yield word + " "
if output.finished:
return
return StreamingResponse(stream_response(), media_type="text/plain")
Dockerfile 작성
프로젝트 루트 디렉토리에 Dockerfile이라는 이름의 파일을 생성하고 아래 내용을 작성합니다.
# 베이스 이미지: NVIDIA CUDA를 지원하는 Ubuntu 20.04 환경을 사용합니다.
FROM nvidia/cuda:12.6.2-cudnn-devel-ubuntu20.04
# 작업 디렉토리 설정
WORKDIR /app
# 시스템 패키지 설치: python3, pip, ray (멀티 GPU 환경을 위해 요구됨)
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y python3 python3-pip ray
# Python 의존성 설치를 위해 requirements.txt 복사
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
# 모델 파일을 마운트할 볼륨 지정
VOLUME ["/app/models"]
# 현재 디렉토리의 모든 파일을 컨테이너의 작업 디렉토리로 복사
COPY . .
# 컨테이너 실행 시 실행될 기본 명령어 (uvicorn 서버 구동)
CMD ["python3", "-m", "uvicorn", "async_server:app", "--host", "0.0.0.0", "--port", "8080"]
requirements.txt 파일에는 필요한 Python 라이브러리를 명시합니다.
vllm
uvicorn
fastapi
pydantic
이제 docker build 명령어로 이미지를 생성합니다.
docker build --no-cache -t test_vllm_1:latest .
Docker 컨테이너 실행
생성된 이미지를 docker run 명령어로 실행하여 vLLM 서버를 컨테이너로 구동합니다.
docker run -d \
--gpus all \
--shm-size=16G \
-v /home/xxxjjhhh/my_model:/app/models \
-e MODEL_PATH="/app/models/Meta-Llama-3.1-8B-Instruct" \
-p 8080:8080 \
test_vllm_1:latest
- -d: 컨테이너를 백그라운드에서 실행합니다.
- --gpus all: 호스트의 모든 GPU를 컨테이너에서 사용할 수 있도록 설정합니다.
- --shm-size=16G: 공유 메모리 크기를 설정합니다. LLM 구동 시 필수적이며, 시스템 메모리에 맞게 적절히 조절해야 합니다.
- -v ...: 호스트의 모델 경로(/home/xxxjjhhh/my_model)를 컨테이너 내부의 /app/models 경로로 마운트합니다.
- -e MODEL_PATH=...: async_server.py가 참조할 모델 경로를 환경 변수로 전달합니다.
- -p 8080:8080: 호스트의 8080 포트를 컨테이너의 8080 포트로 포워딩합니다.
이제 vLLM 서버는 격리되고 재현 가능한 Docker 컨테이너 환경에서 안정적으로 운영될 준비를 마쳤습니다. 다음 섹션에서는 vLLM의 놀라운 성능을 가능하게 하는 핵심 기술들의 내부 동작 원리를 깊이 탐구해 보겠습니다.
--------------------------------------------------------------------------------
6. vLLM 핵심 기술 탐구
지금까지 우리는 vLLM을 '설치'하고 '사용하는 방법'에 초점을 맞추었습니다. 이제는 vLLM이 '어떻게' 그렇게 높은 성능을 낼 수 있는지, 그 내부의 핵심 원리를 파헤쳐 볼 시간입니다. 이 섹션에서는 vLLM 성능의 두 기둥인 PagedAttention과 Speculative Decoding이라는 핵심 기술을 통해, vLLM의 내부 동작을 깊이 이해하고 성능을 최대로 끌어올릴 수 있는 통찰력을 얻게 될 것입니다.
6.1. PagedAttention: 메모리 효율성의 비밀
가장 먼저 명확히 해야 할 점은, PagedAttention이 새로운 종류의 어텐션 메커니즘(예: Self-Attention)이 아니라는 것입니다. PagedAttention은 어텐션 계산 과정에서 생성되는 KV 캐시를 효율적으로 관리하는 메모리 관리 기법입니다. 이 기법이야말로 vLLM이 다른 서빙 프레임워크 대비 압도적인 메모리 효율성을 달성할 수 있는 비결입니다.
기존 방식 vs PagedAttention
기존 Hugging Face 라이브러리와 같은 방식과 PagedAttention의 KV 캐시 관리 방식을 비교하면 그 차이가 명확해집니다.
- 기존 방식 (연속적인 메모리 할당):
- 하나의 요청이 들어오면, 이 요청이 생성할 수 있는 최대 토큰 길이만큼의 GPU 메모리 공간을 통째로, 연속적으로 예약합니다.
- 예를 들어, max_tokens가 2048로 설정되었다면, 실제로는 100개의 토큰만 생성하더라도 2048개 분량의 메모리 공간은 다른 요청이 사용할 수 없게 됩니다.
- 이로 인해 사용되지 않고 낭비되는 메모리 공간, 즉 내부 단편화(Internal Fragmentation) 문제가 심각하게 발생합니다. 연구에 따르면, 이 방식으로 인한 실제 메모리 활용률은 30% 수준에 불과할 수 있습니다.
- PagedAttention 방식 (비연속적인 블록 할당):
- PagedAttention은 운영체제(OS)의 가상 메모리 관리 기법인 **페이징(Paging)**에서 아이디어를 얻었습니다.
- GPU 메모리를 미리 잘게 나뉜 고정된 크기의 **'블록(Block)'**으로 관리합니다.
- KV 캐시를 저장할 때, 연속된 큰 메모리 덩어리 대신 필요한 만큼의 블록을 할당하여 비연속적으로 저장합니다.
- 논리적으로는 연속된 데이터처럼 보이지만, 물리적으로는 흩어져 있는 블록들에 저장되는 방식입니다. 이를 통해 내부 단편화를 거의 완벽하게 해결합니다.
PagedAttention의 장점
PagedAttention은 단순히 메모리 낭비를 줄이는 것 이상의 장점을 제공합니다.
- 메모리 활용률 극대화: 낭비되는 공간이 거의 없어지므로, 동일한 GPU 메모리로 더 많은 요청을 동시에 처리하거나(처리량 증가), 더 큰 모델을 서빙할 수 있게 됩니다.
- 블록 공유를 통한 재사용성 향상:
- 다중 사용자: 여러 사용자가 유사하거나 동일한 프롬프트로 요청을 보낼 경우, 해당 프롬프트에 대한 KV 캐시 블록을 공유하여 중복 계산과 메모리 할당을 피할 수 있습니다.
- 멀티턴 대화: 대화 기록을 포함하여 요청을 보내는 멀티턴 시나리오에서, 이전 대화 내용에 해당하는 KV 캐시 블록을 그대로 재사용하여 효율성을 극대화합니다.
6.2. Speculative Decoding: 추론 속도 향상 기법
PagedAttention이 메모리 효율성을 통해 처리량을 높인다면, Speculative Decoding은 추론 속도 자체를 가속화하여 처리량을 늘리는 또 다른 고급 기법입니다.
이 기법의 기본 원리는 '분업'에 있습니다.
- 예측 (Draft Model): 작고 매우 빠른 '드래프트 모델(Draft Model)'이 앞으로 생성될 여러 개의 토큰(예: 5개)을 한 번에 예측하여 제안합니다.
- 검증 (Base Model): 크고 정확하지만 느린 원래의 '베이스 모델(Base Model)'은 드래프트 모델이 제안한 토큰들을 한 번의 추론 단계로 병렬 검증합니다.
- 수용 또는 수정: 베이스 모델이 드래프트 모델의 예측이 맞다고 판단하면, 여러 개의 토큰이 한 번에 확정되므로 속도가 크게 향상됩니다. 만약 예측이 틀렸다면, 틀린 부분부터 베이스 모델이 직접 토큰을 생성하고 다시 드래프트 모델이 예측을 시작합니다.
vLLM에서는 CLI(Command-Line Interface)를 통해 Speculative Decoding을 간단하게 설정할 수 있습니다.
vllm serve [BASE_MODEL_NAME] \
--speculative-model [DRAFT_MODEL_NAME] \
--num-speculative-tokens [NUMBER]
사용 시 주의점
vLLM에서 Speculative Decoding을 활용할 때는 몇 가지 고려사항이 있습니다.
- 추가 VRAM 필요: 드래프트 모델을 로드하기 위한 추가적인 GPU VRAM이 필요합니다. vLLM의 gpu-memory-utilization 설정은 베이스 모델과 KV 캐시에만 적용되므로, 드래프트 모델이 OOM(Out of Memory) 오류를 일으키지 않도록 해당 설정을 평소보다 낮게 조절해야 합니다.
- 데모 버전 상태: 현재 vLLM에서 이 기능은 최적화가 진행 중인 데모 버전으로 간주됩니다.
- 적절한 드래프트 토큰 수: 한 번에 예측할 토큰 수(num-speculative-tokens)는 모델의 특성에 따라 다르므로, 최적의 성능을 내는 값을 실험적으로 찾아야 합니다.
vLLM을 지탱하는 핵심 이론까지 이해했으니, 이제 마지막으로 실제 활용 시 유용한 추가 팁과 다른 모델 적용 사례를 살펴보며 이 긴 가이드를 마무리하겠습니다.
--------------------------------------------------------------------------------
7. 추가 활용팁 및 다른 모델 적용 사례
vLLM의 기본 기능과 배포, 그리고 핵심 기술까지 마스터했다면 이제는 실제 서비스에 적용할 때 유용한 고급 기능과 다양한 모델 활용 사례를 살펴볼 차례입니다. 이 섹션에서는 Logit Processor를 통해 LLM의 출력을 섬세하게 제어하는 방법과, Llama3 외에 최근 주목받는 국산 LLM인 LG EXAONE Deep 모델을 vLLM으로 서빙하는 구체적인 사례를 다룹니다.
Logit Processor를 이용한 출력 제어
Logit Processor는 LLM이 다음 토큰을 선택하기 직전, 각 토큰의 출력 확률(logit) 분포를 동적으로 조작하여 결과물을 제어하는 강력한 기능입니다. 예를 들어, 특정 단어의 등장을 금지하거나, 특정 형식의 문장만 생성하도록 유도하는 등의 세밀한 제어가 가능합니다.
vLLM에서는 SamplingParams의 logits_processors 인자에 커스텀 함수 리스트를 전달하여 이 기능을 사용할 수 있습니다. 아래는 특정 토큰들의 등장을 막는 간단한 Logit Processor를 적용하는 구체적인 코드 예시입니다.
from typing import List
import torch
# Logit Processor 함수 정의
# 이 함수는 토큰 ID 리스트와 logit 텐서를 인자로 받습니다.
def block_specific_tokens_processor(token_ids: List[int], logits: torch.Tensor) -> torch.Tensor:
# 예시: 100번, 200번 토큰 ID의 생성을 막습니다.
# 해당 토큰들의 logit 값을 음의 무한대로 설정하여 선택될 확률을 0으로 만듭니다.
banned_token_ids = [100, 200]
for token_id in banned_token_ids:
logits[token_id] = -float('inf')
return logits
# SamplingParams에 Logit Processor 적용
sampling_params = SamplingParams(
n=request.n,
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens,
seed=request.seed,
# logits_processors 리스트에 정의한 제어 함수를 추가합니다.
logits_processors=[block_specific_tokens_processor]
)
이처럼 직접 로직을 구현하여 LLM의 출력을 비즈니스 요구사항에 맞게 정교하게 제어할 수 있습니다.
(사례) LG EXAONE Deep 모델 서빙하기
vLLM은 Llama3와 같은 글로벌 모델뿐만 아니라, 다양한 아키텍처의 모델을 지원합니다. 국내 기업 LG AI 연구원에서 개발한 EXAONE Deep 모델 역시 vLLM을 통해 효율적으로 서빙할 수 있습니다.
vLLM CLI를 이용한 서빙
vLLM serve CLI 명령어를 사용하면 코드 작성 없이 간편하게 모델을 서빙할 수 있습니다.
# LGAI-EXAONE/EXAONE-Deep-32B 모델을 8080 포트로, 최대 4096 토큰 길이로 서빙합니다.
# NCCL_P2P_DISABLE=1은 특정 멀티 GPU 환경에서 통신 오류를 방지하기 위한 옵션입니다.
NCCL_P2P_DISABLE=1 vllm serve LGAI-EXAONE/EXAONE-Deep-32B --port 8080 --max-model-len 4096
메모리가 부족할 경우, AWQ로 양자화된 LGAI-EXAONE/EXAONE-Deep-32B-AWQ 모델을 사용하는 것을 고려할 수 있습니다.
LGAI-EXAONE/EXAONE-Deep-32B은 80GB 이상 VRAM 권장, AWQ 양자화 버전은 40GB 이상 VRAM 권장
아니면, 가장 저사양인 EXAONE 4.0 1.2B (VRAM 2.4GB이상)
API 요청 예시
vLLM CLI로 서버를 실행하면 OpenAI와 호환되는 API 엔드포인트가 자동으로 생성됩니다. 아래와 같이 curl을 사용하여 요청을 보낼 수 있습니다.
- API 엔드포인트: POST http://[서버_IP]:8080/v1/chat/completions
- JSON 요청 본문:
라이선스 확인
모델을 사용하기 전에는 반드시 라이선스를 확인해야 합니다. EXAONE Deep 모델은 EXAONE AI Model License Agreement 1.1 - NC 라이선스를 따르며, 비상업적(Non-Commercial) 용도로 제한되는 등 다소 까다로운 조건이 포함될 수 있으므로 상업적 이용 시에는 세심한 검토가 필요합니다.
--------------------------------------------------------------------------------
8. 결론: vLLM으로 나만의 고성능 LLM 서비스 구축하기
우리는 vLLM의 필요성부터 시작하여, 모델 준비, 설치, 기본 테스트, FastAPI를 이용한 API 서버 구축, 그리고 Docker를 활용한 프로덕션 배포에 이르기까지 LLM 서빙의 전 과정을 단계별로 학습했습니다. 나아가 PagedAttention과 Speculative Decoding과 같은 vLLM의 핵심 기술을 탐구하며 그 성능의 비밀을 파헤쳐 보았습니다.
vLLM은 단순히 LLM을 실행시켜주는 도구를 넘어, 고비용의 AI 인프라를 효율적으로 운영하고 더 많은 사용자에게 안정적인 서비스를 제공할 수 있게 하는 핵심 기술입니다. PagedAttention을 통한 혁신적인 메모리 관리와 지속적인 배치를 통한 처리량 극대화는 개인 개발자부터 대규모 기업에 이르기까지 누구나 고성능 LLM 서비스를 구축할 수 있는 문을 열어주었습니다.
Llama3, EXAONE, 또는 여러분이 선택한 어떤 모델이든 자신만의 아이디어를 담아 고성능 LLM 애플리케이션으로 구현할 수 있습니다.
'AI agent' 카테고리의 다른 글
| Google Antigravity 설치 (0) | 2026.01.15 |
|---|---|
| 온톨로지(Ontology) (0) | 2026.01.14 |
| 엘라스틱서치(Elasticsearch)란? (1) | 2026.01.12 |
| MCP server (0) | 2026.01.10 |
| LangChain v1 & LangGraph (0) | 2026.01.09 |