본문으로 건너뛰기

[vLLM] Renderer & Tokenizer: 모델별 입력 파이프라인

들어가며

LLM은 모델마다 토크나이저와 입력 형식이 다르다. Mistral은 자체 토크나이저를, DeepSeek-V3는 커스텀 인코딩을, 멀티모달 모델은 이미지 전처리 파이프라인을 필요로 한다. vLLM은 RendererTokenizerRegistry를 통해 이 다양성을 추상화한다.

핵심 구조/코드 분석

TokenizerRegistry: 토크나이저 통합 관리

_VLLM_TOKENIZERS = {
    "deepseek_v32": ("deepseek_v32", "DeepseekV32Tokenizer"),
    "grok2": ("grok2", "Grok2Tokenizer"),
    "hf": ("hf", "CachedHfTokenizer"),
    "kimi_audio": ("kimi_audio", "KimiAudioTokenizer"),
    "mistral": ("mistral", "MistralTokenizer"),
    "qwen_vl": ("qwen_vl", "QwenVLTokenizer"),
}

TokenizerRegistry = _TokenizerRegistry(
    {
        mode: (f"vllm.tokenizers.{mod_relname}", cls_name)
        for mode, (mod_relname, cls_name) in _VLLM_TOKENIZERS.items()
    }
)

레지스트리 패턴으로 토크나이저를 관리한다. tokenizer_mode 문자열 하나로 적절한 토크나이저 클래스가 선택된다. 기본값은 HuggingFace 토크나이저(hf)이며, 모델별 특수 토크나이저가 필요한 경우 해당 모드가 자동 감지된다.

class _TokenizerRegistry:
    def load_tokenizer_cls(self, tokenizer_mode: str) -> type[TokenizerLike]:
        module, class_name = self.tokenizers[tokenizer_mode]
        return resolve_obj_by_qualname(f"{module}.{class_name}")

    def load_tokenizer(self, tokenizer_mode: str, *args, **kwargs) -> TokenizerLike:
        tokenizer_cls = self.load_tokenizer_cls(tokenizer_mode)
        return tokenizer_cls.from_pretrained(*args, **kwargs)

resolve_obj_by_qualname으로 지연 로딩을 구현하여, 실제로 사용할 때만 토크나이저 모듈을 임포트한다.

BaseRenderer: 입력 변환의 핵심

BaseRenderer는 사용자의 프롬프트를 엔진이 이해할 수 있는 형태로 변환하는 추상 클래스이다.

class BaseRenderer(ABC, Generic[_T]):
    def __init__(self, config: "VllmConfig", tokenizer: _T | None) -> None:
        self.config = config
        self.tokenizer = tokenizer

        pool_workers = config.model_config.renderer_num_workers
        self._executor = ThreadPoolExecutor(max_workers=pool_workers)
        self._mm_executor: Executor = self._executor
        self._async_tokenizer: AsyncMicrobatchTokenizer | None = None

두 가지 핵심 설계가 보인다:

  1. ThreadPoolExecutor: 토크나이징과 멀티모달 전처리를 스레드 풀에서 실행하여 asyncio 이벤트 루프를 블로킹하지 않는다.
  2. Generic[_T]: 토크나이저 타입을 제네릭으로 선언하여, 모델별 특수 토크나이저의 타입 안전성을 보장한다.

멀티모달 프로세서 초기화

if config.model_config.is_multimodal_model:
    mm_processor_cache = mm_registry.processor_cache_from_config(config)

    # Deep-copy the tokenizer so the multimodal processor gets its
    # own Rust tokenizer backend.
    mm_tokenizer = copy.deepcopy(tokenizer)

    self.mm_processor = mm_registry.create_processor(
        config.model_config,
        tokenizer=mm_tokenizer,
        cache=mm_processor_cache,
    )

멀티모달 프로세서에 토크나이저의 딥카피를 전달한다. 이는 HuggingFace tokenizers 라이브러리의 Rust 백엔드가 RefCell을 사용하므로, 동시 접근 시 "Already borrowed" 에러가 발생하기 때문이다. 딥카피로 별도의 Rust 인스턴스를 생성하여 동시성 문제를 해결한다.

비동기 토크나이징

self._process_multimodal_async = make_async(
    self._process_multimodal, executor=self._mm_executor
)

make_async 유틸리티로 동기 함수를 스레드 풀에서 실행하는 비동기 함수로 변환한다. 이미지 인코딩처럼 CPU 집약적인 작업이 이벤트 루프를 블로킹하지 않는다.

TokenizerLike 프로토콜

# vllm/tokenizers/protocol.py
class TokenizerLike:
    """Protocol for all tokenizer types used in vLLM."""
    ...

모든 토크나이저가 구현해야 하는 공통 인터페이스이다. from_pretrained, encode, decode 등의 메서드를 정의하며, HuggingFace 토크나이저와 커스텀 토크나이저가 모두 이 프로토콜을 따른다.

왜 이 설계인가

  1. 레지스트리 패턴: 새로운 토크나이저를 추가할 때 레지스트리에 한 줄만 추가하면 된다. 핵심 코드를 수정하지 않고 확장이 가능하다.

  2. 토크나이저 딥카피: Rust RefCell의 동시성 제약을 우회하기 위한 실용적 해결책이다. 메모리를 약간 더 사용하지만, 동시 요청 처리에서의 안정성을 보장한다.

  3. ThreadPoolExecutor 공유: 토크나이징과 멀티모달 전처리가 같은 스레드 풀을 공유한다. GIL 제약 하에서도 I/O 대기 시간을 겹칠 수 있고, 풀 크기로 동시성을 제어할 수 있다.

  4. Renderer 추상화: 토크나이징, 채팅 템플릿 적용, 멀티모달 전처리를 하나의 인터페이스로 묶는다. 모델별 특수 로직은 서브클래스에서 오버라이드한다.

정리

RendererTokenizerRegistry는 vLLM이 수백 가지 모델을 지원할 수 있게 하는 추상화 계층이다. 모델마다 다른 토크나이저, 채팅 형식, 멀티모달 파이프라인을 통일된 인터페이스 뒤에 숨기면서도, 동시성과 성능을 보장하는 설계가 인상적이다.

댓글

관련 포스트

vLLM 의 다른글