feel free to

Contact us

The IMC의 사업과 솔루션, 기술지원, 채용정보에
관련한 여러분의 문의사항을 알려주세요.

  • Tell 053-744-0707
  • E-mail theimc@theimc.co.kr
프로필 정보
프로필 이미지

by Jin

AI 혁신팀 팀장

상용API 및 오픈소스 LLM 모델을 활용한 서비스 개발을 담당하고 있습니다.

AI 엔지니어링

00
리스트가 없습니다.

vLLM을 이용한 LLM 추론 코드 소개 및 스타일 제안

2025-12-11

대규모 언어모델(LLM)을 실제 서비스 환경에 배포할 때 가장 먼저 직면하는 문제는 추론 속도, GPU 메모리 관리, 동시 요청 처리입니다. 이러한 문제를 해결을 좀더 쉽게 처리하기 위해 다양한 추론 최적화 도구들이 소개 되고 있습니다. 다만, 다른 분야 패키지에 비해 지배적으로 사용되고 있는 추론 최적화 도구가 없을 뿐만 아니라 이에 대한 파이썬 코드 작성 스타일 또한 관련 파이썬 코드 작성 방식도 개발자마다 상이해 일관된 스타일에 기반한 협업이 쉽지 않은 상황입니다. 이러한 환경을 고려하여, 추론 최적화 도구인 vLLM을 추천하고 추천하는 이유를 본 포스팅에서 설명합니다. 아울러, 통일된 스타일을 제안하기 위한 코드 예시와 vLLM의 한계에 따른 주의점까지 본포스팅에서 다루고자 합니다.

vLLM 활용 이유

  • PagedAttention 기반의 고효율 메모리 관리
  • TensorRT-LLM 등 대체 솔루션에 비해, Python 기반 API가 단순하고 일관적임
  • SSE 기반 스트리밍에 최적화된 구조 제공
  • Hugging Face 생태계와 완전한 호환성

vLLM 코드 스타일 제안

아래 코드는 vllm을 활용하여 sse 통신이 가능하도록 작성해둔 코드 이며, 객체 지향으로 작성하여 효율성을 높이고자 노력하였습니다. 아래 코드 스타일을 참고하여 주세요. 단 출력 형태는 SSE(Server-Sent Events) 출력시 출력 표준제안 를 참고하여 코드를 완성하였습니다.

import asyncio
import json
import traceback
import uuid
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, Dict, Optional

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from transformers import AutoTokenizer
from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.engine.async_llm_engine import AsyncLLMEngine
from vllm.sampling_params import SamplingParams

# --- 1. 설정 관리 클래스 (Configuration) ---
@dataclass
class VLLMConfig:
    """vLLM 엔진 및 모델 설정을 관리하는 데이터 클래스"""
    model_path: str = "{model_path}"
    dtype: str = "bfloat16"
    gpu_memory_utilization: float = 0.75
    tensor_parallel_size: int = 2
    max_num_seqs: int = 16
    max_model_len: int = 4096
    limit_mm_per_prompt: Dict[str, int] = field(default_factory=lambda: {"image": 0})
    trust_remote_code: bool = True

# --- 2. 엔진 매니저 클래스 (Engine Manager) ---
class VLLMEngineManager:
    """vLLM 엔진의 생명주기(초기화, 접근)를 담당하는 클래스"""
    def __init__(self, config: VLLMConfig):
        self.config = config
        self._engine: Optional[AsyncLLMEngine] = None
        self._tokenizer = None
        self._lock = asyncio.Lock()

    async def get_engine(self) -> AsyncLLMEngine:
        """싱글톤 패턴을 사용하여 엔진 인스턴스를 반환 (Double-checked locking)"""
        if self._engine is not None:
            return self._engine

        async with self._lock:
            if self._engine is not None:
                return self._engine
            
            print(">>> Initializing vLLM Engine...")
            try:
                engine_args = AsyncEngineArgs(
                    model=self.config.model_path,
                    dtype=self.config.dtype,
                    gpu_memory_utilization=self.config.gpu_memory_utilization,
                    tensor_parallel_size=self.config.tensor_parallel_size,
                    max_num_seqs=self.config.max_num_seqs,
                    max_model_len=self.config.max_model_len,
                    limit_mm_per_prompt=self.config.limit_mm_per_prompt,
                    trust_remote_code=self.config.trust_remote_code,
                    disable_log_stats=False
                )
                self._engine = AsyncLLMEngine.from_engine_args(engine_args)
                print(">>> vLLM Engine is ready.")
            except Exception as e:
                print(f"!!! Engine initialization failed: {e}")
                raise e
            
        return self._engine

    def get_tokenizer(self):
        """토크나이저 로드 (Lazy Loading)"""
        if self._tokenizer is None:
            try:
                self._tokenizer = AutoTokenizer.from_pretrained(self.config.model_path)
            except Exception as e:
                print(f"!!! Tokenizer loading failed: {e}")
                raise e
        return self._tokenizer

# --- 3. 채팅 서비스 클래스 (Service Logic) ---
class ChatCompletionService:
    """사용자 요청을 처리하고 스트리밍 응답을 생성하는 서비스 클래스"""
    def __init__(self, engine_manager: VLLMEngineManager):
        self.engine_manager = engine_manager

    def _build_prompt(self, question: str) -> str:
        """프롬프트 템플릿 구성"""
        tokenizer = self.engine_manager.get_tokenizer()
        
        # System Prompt를 User 메시지에 병합 (Gemma 호환성)
        system_instruction = (
            "You are Gemma-3, a helpful, honest, and concise AI assistant. "
            "Answer clearly, avoid hallucinations, and think step-by-step internally."
            "Ask for clarification when needed and follow the user’s instructions precisely."
        )
        
        conversation = [
            {"role": "user", "content": f"{system_instruction}\n\nQuestion: {question}"}
        ]

        try:
            #return tokenizer.apply_chat_template(
            #    conversation,
            #    tokenize=False,
            #    add_generation_prompt=True
            #)
            return (
                f"<bos><start_of_turn>system\n{system_instruction}<end_of_turn>\n"
                f"<start_of_turn>user\n{question}<end_of_turn>\n"
                f"<start_of_turn>model\n"
                )
        except Exception as e:
            print(f"Template apply failed, using raw string: {e}")
            return f"<start_of_turn>user\n{question}<end_of_turn>\n<start_of_turn>model\n"

    async def generate_response_sse(self, question: str, request_id: str = None) -> AsyncGenerator[str, None]:
        """SSE 포맷으로 답변 스트리밍"""
        if request_id is None:
            request_id = str(uuid.uuid4())

        try:
            engine = await self.engine_manager.get_engine()
            prompt = self._build_prompt(question)
            print("QUESTION")
            print(prompt)

            sampling_params = SamplingParams(
                max_tokens=2048,
                temperature=0.5,
                top_p=0.9,
                repetition_penalty=1,
                skip_special_tokens=True
            )

            results_generator = engine.generate(
                prompt,
                sampling_params,
                request_id=request_id
            )

            generated_text = ""
            
            async for request_output in results_generator:
                for output in request_output.outputs:
                    new_text = output.text
                    if len(new_text) > len(generated_text):
                        delta = new_text[len(generated_text):]
                        generated_text = new_text
                        
                        # SSE Event: message
                        yield "event: message\n"
                        yield f"data: {json.dumps({'text': delta}, ensure_ascii=False)}\n\n"
            
            # SSE Event: DONE
            yield "event: done\n"
            yield "data: [DONE]\n\n"

        except Exception as e:
            traceback.print_exc()
            yield "event: error\n"
            yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"

# 1. 설정 로드
config = VLLMConfig() # 필요 시 인자로 설정 변경 가능

# 2. 매니저 및 서비스 인스턴스화 (Dependency Injection)
engine_manager = VLLMEngineManager(config)
chat_service = ChatCompletionService(engine_manager)

print(">>> Starting Stream Process...")
app = FastAPI(title="Gemma-3 Chat API", version="1.0.0")
class ChatRequest(BaseModel):
    question: str

@app.post("/chat")
async def chat(request: ChatRequest):
    
    # 3. 서비스 실행
    return StreamingResponse(
        chat_service.generate_response_sse(request.question),
        media_type="text/event-stream",
    )

vLLM 활용시 주의점

vLLM은 서버에 장착된 GPU 자원을 최대한 활용해 추론 엔진을 초기화합니다. 이 과정에서 지정된 GPU를 독점적으로 점유하기 때문에, 동일한 GPU를 사용하는 다른 프로세스나 서비스와 자원 충돌이 발생할 수 있습니다. 따라서 운영 환경에서 vLLM을 사용할 때는 GPU에 상주하는 모델 수를 최소화하는 것이 중요합니다. 기본적으로 GPU 개수만큼만 모델 엔진이 생성되므로, 불필요한 모델 로딩을 피해야 합니다.

프롬프트 엔지니어링 기반의 파이프라인을 사용하는 경우라면 이미 로딩된 vanilla 모델을 재사용해 불필요한 모델 초기화를 방지할 수 있습니다. 반면, 파인튜닝이 필요한 경우에는 LoRA 방식을 적용하는 것이 효율적입니다. 예를 들어 저는 Gemma 모델에 LoRA 가중치 두 개를 동시에 로드해두고, 조건문을 통해 필요한 가중치만 선택해 사용하는 방식으로 운영합니다.

Release Note

2025-12-10 - ver 0.1.0

  • 초안 업로드

대구본부 : (42250) 대구광역시 수성구 알파시티1로 35길 17(텍스톰 베이스, 1층)| 서울본부 : (04534) 서울특별시 중구 을지로 50(을지한국빌딩, 20층)|상주 스마트팜 연구소 : 경상북도 상주시 사벌국면상풍로 604-61(빅데이터센터, 2층)|대표이사 : 전채남|Tel(개발문의) : 053-744-0707|Email : theimc@theimc.co.kr

©COPYRIGHT 2018 The IMC Inc. ALL RIGHTS RESERVED.