멀티모달 데이터 처리
멀티모달 데이터 처리 (Multi-Modal Data Processing)
이미지나 오디오 같은 멀티모달 입력을 넣을 때, <image> 같은 자리표시자 토큰이 실제 입력 데이터와 어떻게 대응하는지는 vLLM이 알아서 챙겨줄까요? 네, 그건 BaseMultiModalProcessor라는 친구가 담당해요. vLLM의 chunked prefill이나 prefix caching 같은 최적화가 동작하려면, 자리표시자 피처 토큰(예: <image>)과 멀티모달 입력(예: 원본 이미지) 사이의 대응 관계를 정확히 맞춰야 해요. 이 처리가 HF(Hugging Face) processor의 출력을 기준으로 이뤄진다고 보면 돼요.
그런데 여기 vLLM의 렌더링 파이프라인([BaseRenderer][vllm.renderers.base.BaseRenderer])에서는 토큰화를 멀티모달 처리보다 먼저 별도 단계로 수행해요. 그래서 BaseMultiModalProcessor는 원본 텍스트를 보지 못한 채, HF processor를 처음부터 끝까지 호출한 것과 같은 결과를 재현해야 해요. 이걸 가능하게 하는 게 바로 Dummy Input Text(더미 입력 텍스트)와 Prompt Update Detection(프롬프트 갱신 감지)이에요.
Dummy Input Text (더미 입력 텍스트)
Transformers 5.10부터는 ProcessorMixin이 멀티모달 입력만 따로 받을 수 있게 됐어요. 그런데 ChameleonProcessor 같은 일부 하위 클래스나 오래된 외부 구현들은 여전히 텍스트와 그에 맞는 자리표시자 토큰이 함께 있다고 가정하고 자체 __call__ 메서드를 정의하고 있어요. 문제는 우리가 더 이상 그 processor에 넘길 원본 텍스트가 없다는 거예요.
이걸 해결하려고 각 모델은 멀티모달 입력 개수에 따라 더미 텍스트를 어떻게 만들지, [get_dummy_text][vllm.multimodal.processing.BaseDummyInputsBuilder.get_dummy_text]로 정의해요. 그리고 [_get_hf_processor_text][vllm.multimodal.processing.BaseMultiModalProcessor._get_hf_processor_text]의 override가 이 더미 텍스트를 반환하고, [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]이 멀티모달 입력과 함께 이 텍스트를 HF processor에 넘겨 처리된 멀티모달 데이터를 얻어요.
마찬가지로, vLLM이 추출한 멀티모달 데이터가 특정 HF processor가 기대하는 형태와 다를 수 있어요. 이때 [_apply_hf_processor_main][vllm.multimodal.processing.BaseMultiModalProcessor._apply_hf_processor_main]은 각 모델이 전체 메서드를 다시 구현하지 않아도 되도록 입력은 [_preprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._preprocess_hf_mm_data]로, 출력은 [_postprocess_hf_mm_data][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_hf_mm_data]로 적응시킬 수 있게 해줘요. 예를 들면 audios 같은 키를 audio로 바꾸거나 sampling_rate 같은 추가 키워드 인자를 넣는 식이죠.
Prompt Update Detection (프롬프트 갱신 감지)
HF processor의 핵심 임무 중 하나는 자리표시자 토큰으로 프롬프트를 갱신하는 거예요. 예를 들면 이런 작업이죠.
- 문자열의 시작 부분에 피처 자리표시자 토큰을 삽입하기 (예:
<image><image>...<image>— 개수는 피처 크기에 맞춰요). - 기존 입력 자리표시자 토큰(단일 이미지용
<image>)을 피처 자리표시자 토큰(개수가 피처 크기와 같은<image><image>...<image>)으로 바꾸기.
어떤 토큰이 갱신됐는지에 대한 정보가 바로 자리표시자 피처 토큰과 멀티모달 입력 사이의 대응을 찾는 핵심이에요.
vLLM은 입력 텍스트 없이 HF processor를 호출하니까 이 갱신을 직접 수행해야 해요. 그래서 필요한 정보를 [PromptUpdate][vllm.multimodal.processing.PromptUpdate]로 표현해 [_get_prompt_updates][vllm.multimodal.processing.BaseMultiModalProcessor._get_prompt_updates]에서 만들고, [_apply_prompt_updates][vllm.multimodal.processing.BaseMultiModalProcessor._apply_prompt_updates]에서 적용해요.
일부 HF processor는 멀티모달 입력과 무관하게 프롬프트 자체를 변형하기도 해요 (ChameleonProcessor가 채팅 모드에서 sep 토큰을 붙이는 것처럼요). 프롬프트 토큰도 마찬가지로 HF processor를 거치지 않으니까, 이런 변형은 프롬프트 갱신을 찾거나 적용하기 전에 [_postprocess_prompt][vllm.multimodal.processing.BaseMultiModalProcessor._postprocess_prompt]로 재현해요.
Processor Output Caching (processor 출력 캐싱)
Qwen2-VL용 processor처럼 일부 HF processor는 정말 느려요 (관련 이슈). 이 문제를 완화하려고 같은 멀티모달 입력(예: 이미지)을 다시 처리하지 않도록 HF processor의 멀티모달 출력을 캐싱해요.
새 데이터가 들어오면 먼저 어떤 항목이 캐시에 있고 어떤 게 없는지 확인해요. 없는 항목만 한 배치로 HF processor에 넣어 캐시하고, 캐시에 있던 기존 항목과 병합해요.
멀티모달 데이터 처리 가속화
디바이스에서 융합 정규화 (Fused Normalisation on the Device)
멀티모달 데이터 파이프라인(디코딩, 리사이징, 정규화, 리스케일링)을 빠르게 하려고, 무거운 수치 전처리를 CPU에서 GPU로 옮기고 데이터 이동을 최적화해요.
GPU에서 정규화와 리스케일링 융합
전통적으로는 CPU에서 픽셀 값을 255로 나누고, 평균을 빼고, 표준편차로 나누는 과정을 거쳤어요. 우리는 이 단계들을 하나로 합쳐 전부 GPU에서 실행해요.
동작 방식: 전용 FusedInputNorm 모듈이 리스케일 계수(1/255)를 레이어의 weight와 bias 파라미터에 직접 구워 넣어요. 세 단계(스케일, 빼기, 나누기)를 하는 대신 모듈이 단일 아핀 변환 y = x * weight + bias 한 번으로 전부 처리해요.
파라미터는 이렇게 설정돼요.
weight는 표준편차와 리스케일 계수를 함께 제어해요bias는 평균과 같은 리스케일 계수로 데이터를 중앙에 맞춰요
즉 이 모듈은 원시 픽셀 값(0–255)을 받아서, 255로 나누는 별도 단계 없이 바로 정규화된 값을 출력해요.
융합 정규화를 위한 최적화된 데이터 경로
정규화를 디바이스에서 직접 수행하면 전체 전송 경로 — Entrypoint → Engine Core → GPU 메모리 — 를 uint8 로 유지할 수 있어요. 이렇게 하면 PCIe 대역폭이 절반으로 줄고 CPU 메모리 사용량도 줄어들어요.
데이터가 GPU 메모리에 도달한 뒤에야 FusedInputNorm 레이어를 위해 fp32로 캐스팅해요(수치 정확도 보장), 그리고 이후 레이어에서는 bf16으로 캐스팅해요 — 이 모든 게 GPU 안에서 이뤄져 호스트 쪽 변환은 없어요.
전체 경로: Entrypoint (uint8) → Engine Core (uint8) → GPU Memory (uint8) → GPU 내부 fp32 FusedInputNorm → bf16 출력.
토글: mm_device_do_normalize
이 GPU 쪽 융합은 mm_device_do_normalize 라는 설정 플래그로 제어돼요.
True면 정규화와 리스케일링을FusedInputNorm레이어로 GPU에서 수행하고,False면 옛 CPU 쪽 경로로 폴백해요.- 이 플래그는 지원하는 모든 모델에서 기본적으로 켜져 있어요.
- 현재 기본으로 켜져 있는 아키텍처는 이렇고요.
| name | Architecture | Example HF Models |
|---|---|---|
qwen2-vl |
Qwen2VLForConditionalGeneration |
Qwen/Qwen2-VL-2B-Instruct, etc. |
qwen2.5-vl |
Qwen2_5_VLForConditionalGeneration |
Qwen/Qwen2.5-VL-3B-Instruct, etc. |
전체적으로 얻는 이점
- CPU 오프로드: 정규화와 리스케일링을 위한 연산이 CPU에서 완전히 사라져요.
- PCIe 절약:
bf16(2바이트) 대신uint8(1바이트)을 보내면 데이터 전송량이 50% 줄어요. - GPU 오버헤드: 융합 커널은 매우 가벼워서 이후 CUDA 연산과 자주 병합되기 때문에 추가 비용이 거의 들지 않아요.