본문으로 건너뛰기

[SGLang] Multimodal 처리 파이프라인 개요: Vision/Audio/Video 통합

들어가며

최신 LLM은 텍스트뿐 아니라 이미지, 비디오, 오디오까지 이해한다. SGLang은 30개 이상의 멀티모달 프로세서를 통합하여 다양한 VLM, 음성 모델을 단일 파이프라인에서 서빙한다.

이 글에서는 python/sglang/srt/multimodal/ 디렉토리를 중심으로 멀티모달 처리의 전체 흐름을 분석한다.

전체 파이프라인 구조도

사용자 입력 (text + image/video/audio)
    │
    ▼
┌──────────────────────────────────────────┐
│  BaseMultimodalProcessor                 │
│  ├── load_mm_data()                      │
│  │   ├── load_image() / load_video()     │
│  │   └── load_audio()                    │
│  │                                       │
│  ├── process_and_combine_mm_data()       │
│  │   ├── HF Processor 호출               │
│  │   └── MultimodalDataItem 생성         │
│  │                                       │
│  └── MultimodalProcessorOutput 반환      │
│      ├── input_ids: [1, 2, IMG, IMG, 3]  │
│      └── mm_items: [DataItem(feature)]   │
└──────────────────┬───────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────┐
│  Model Forward                           │
│  ├── Vision Encoder (CLIP, ViT, etc.)    │
│  ├── Audio Encoder (Whisper, etc.)       │
│  └── Projection Layer                    │
│      pixel_values → vision_embeddings    │
│      audio_features → audio_embeddings   │
└──────────────────┬───────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────┐
│  Embedding 결합                          │
│  input_embeddings[IMG_offsets] =         │
│      vision_embeddings                   │
│  → LLM Decoder로 전달                    │
└──────────────────────────────────────────┘

핵심 코드 분석

BaseMultimodalProcessor: 공통 기반

모든 멀티모달 프로세서의 기반 클래스는 세 가지 핵심 자료구조를 정의한다.

@dataclasses.dataclass
class BaseMultiModalProcessorOutput:
    input_text: str
    images: Optional[list[Union[Image.Image, dict]]] = ...
    videos: Optional[list[Union[torch.Tensor, dict]]] = ...
    audios: Optional[list[Union[np.ndarray, dict]]] = ...

    def organize_results(self):
        return (
            [(Modality.IMAGE, data) for data in self.images]
            + [(Modality.VIDEO, data) for data in self.videos]
            + [(Modality.AUDIO, data) for data in self.audios]
        )

세 가지 모달리티(IMAGE, VIDEO, AUDIO)를 단일 출력에서 통합한다.

MultimodalSpecialTokens: 플레이스홀더 관리

각 모달리티의 특수 토큰과 정규식 패턴을 관리한다.

@dataclasses.dataclass
class MultimodalSpecialTokens:
    image_token: Optional[Union[str, List[str]]] = None
    video_token: Optional[Union[str, List[str]]] = None
    audio_token: Optional[Union[str, List[str]]] = None

    image_token_id: Optional[int] = None
    video_token_id: Optional[int] = None
    audio_token_id: Optional[int] = None

    def build(self, processor):
        self.convert_to_strs(processor)
        self.parse_regex()
        self.get_combined_regex()
        return self

build() 호출 시 텍스트 토큰을 ID로 변환하고, 정규식 패턴을 컴파일한다. 프롬프트에서 <image>, <video>, <audio> 등의 플레이스홀더를 찾아 실제 멀티모달 데이터와 매핑한다.

mm_utils.py: 이미지 처리 유틸리티

LLaVA 계열 모델의 anyres(Any Resolution) 처리를 제공한다.

def select_best_resolution(original_size, possible_resolutions):
    original_width, original_height = original_size
    best_fit = None
    max_effective_resolution = 0
    min_wasted_resolution = float("inf")

    for width, height in possible_resolutions:
        scale = min(width / original_width, height / original_height)
        downscaled_width = int(original_width * scale)
        downscaled_height = int(original_height * scale)
        effective_resolution = min(
            downscaled_width * downscaled_height,
            original_width * original_height,
        )
        wasted_resolution = (width * height) - effective_resolution
        # 유효 해상도 최대화, 낭비 최소화

원본 이미지 비율을 보존하면서 가장 효율적인 해상도를 선택한다.

DP Sharded Vision Model

대규모 이미지 배치를 텐서 병렬로 분산 처리한다.

def run_dp_sharded_vision_model(image_input, vision_model):
    num_chunks = image_input.shape[0]
    mp_world_size = get_tensor_model_parallel_world_size()
    num_chunks_per_rank = (num_chunks + mp_world_size - 1) // mp_world_size

    rank = get_tensor_model_parallel_rank()
    image_input_per_rank = image_input_padded[
        rank * num_chunks_per_rank : (rank + 1) * num_chunks_per_rank
    ]
    vision_embeddings = vision_model(image_input_per_rank)
    vision_embeddings = tensor_model_parallel_all_gather(vision_embeddings, dim=0)

각 TP rank가 이미지의 일부를 인코딩하고, all_gather로 결과를 모은다.

MRoPE(Multi-Resolution Rotary Position Embedding) 모델에는 더 정교한 로드 밸런싱이 적용된다.

def get_dp_encoder_lb_assignment(sizes, num_gpus=2):
    large_to_small_indices = sorted(
        range(n_samples), key=lambda i: sizes[i], reverse=True
    )
    for idx in large_to_small_indices:
        min_gpu = min(range(num_gpus), key=lambda i: gpu_loads[i])
        gpu_assignments[min_gpu].append(idx)
        gpu_loads[min_gpu] += sizes[idx]

큰 이미지부터 가장 여유 있는 GPU에 할당하는 greedy 알고리즘으로, 이미지 크기 차이로 인한 GPU 간 불균형을 최소화한다.

프로세서 디렉토리 구조

processors/
├── base_processor.py     # BaseMultimodalProcessor
├── clip.py               # CLIP 모델용
├── internvl.py           # InternVL 모델용
├── llava.py              # LLaVA 계열
├── qwen_vl.py            # Qwen-VL 계열
├── qwen3_asr.py          # Qwen3-ASR 음성
├── whisper.py            # Whisper 음성
├── glmasr.py             # GLM-ASR 음성
├── pixtral.py            # Pixtral
├── gemma3.py / gemma4.py # Gemma 계열
└── ... (30+ 프로세서)

설계 근거

설계 선택 이유
BaseMultimodalProcessor 30+ 모델의 공통 로직(로드, 토큰화, 오프셋 계산) 재사용
MultimodalSpecialTokens 모델마다 다른 플레이스홀더를 통일된 인터페이스로 처리
Modality enum IMAGE/VIDEO/AUDIO를 타입 안전하게 구분
greedy 로드밸런싱 이미지 크기가 다양한 배치에서 GPU 활용도 극대화
anyres 처리 고해상도 이미지를 패치로 분할하여 ViT 입력 크기 제한 우회

관련 포스트

참고

댓글

관련 포스트

SGLang 의 다른글