[vLLM] Renderer & Tokenizer: 모델별 입력 파이프라인
들어가며
LLM은 모델마다 토크나이저와 입력 형식이 다르다. Mistral은 자체 토크나이저를, DeepSeek-V3는 커스텀 인코딩을, 멀티모달 모델은 이미지 전처리 파이프라인을 필요로 한다. vLLM은 Renderer와 TokenizerRegistry를 통해 이 다양성을 추상화한다.
핵심 구조/코드 분석
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
두 가지 핵심 설계가 보인다:
- ThreadPoolExecutor: 토크나이징과 멀티모달 전처리를 스레드 풀에서 실행하여 asyncio 이벤트 루프를 블로킹하지 않는다.
- 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 토크나이저와 커스텀 토크나이저가 모두 이 프로토콜을 따른다.
왜 이 설계인가
-
레지스트리 패턴: 새로운 토크나이저를 추가할 때 레지스트리에 한 줄만 추가하면 된다. 핵심 코드를 수정하지 않고 확장이 가능하다.
-
토크나이저 딥카피: Rust
RefCell의 동시성 제약을 우회하기 위한 실용적 해결책이다. 메모리를 약간 더 사용하지만, 동시 요청 처리에서의 안정성을 보장한다. -
ThreadPoolExecutor 공유: 토크나이징과 멀티모달 전처리가 같은 스레드 풀을 공유한다. GIL 제약 하에서도 I/O 대기 시간을 겹칠 수 있고, 풀 크기로 동시성을 제어할 수 있다.
-
Renderer 추상화: 토크나이징, 채팅 템플릿 적용, 멀티모달 전처리를 하나의 인터페이스로 묶는다. 모델별 특수 로직은 서브클래스에서 오버라이드한다.
정리
Renderer와 TokenizerRegistry는 vLLM이 수백 가지 모델을 지원할 수 있게 하는 추상화 계층이다. 모델마다 다른 토크나이저, 채팅 형식, 멀티모달 파이프라인을 통일된 인터페이스 뒤에 숨기면서도, 동시성과 성능을 보장하는 설계가 인상적이다.
관련 포스트
vLLM 의 다른글
- 이전글 [vLLM] OutputProcessor: 출력 후처리 및 디토크나이징
- 현재글 : [vLLM] Renderer & Tokenizer: 모델별 입력 파이프라인
- 다음글 [vLLM] Executor 아키텍처: UniProc, Multiproc, Ray
댓글