본문으로 건너뛰기

[SGLang] Audio 모델: Whisper, Qwen3-ASR, GLM-ASR 프로세서

들어가며

음성 인식(ASR)은 멀티모달 LLM의 중요한 축이다. SGLang은 Whisper, Qwen3-ASR, GLM-ASR 세 가지 음성 모델을 지원하며, OpenAI 호환 Transcription API를 제공한다. 각 모델은 전용 프로세서(전처리)와 어댑터(후처리)로 구성된다.

이 글에서는 프로세서(processors/whisper.py, processors/qwen3_asr.py, processors/glmasr.py)와 어댑터(transcription_adapters/)를 함께 분석한다.

전체 구조도

Audio 입력 (wav/mp3/base64)
    │
    ▼
┌────────────────────────────────────────┐
│ Multimodal Processor (전처리)          │
│ ├── WhisperProcessor                   │
│ │   └── feature_extractor → mel spec   │
│ ├── Qwen3ASRMultimodalProcessor        │
│ │   └── load_mm_data → audio tokens    │
│ └── GlmAsrProcessor                   │
│     └── load_mm_data → audio tokens    │
└──────────────┬─────────────────────────┘
               │ MultimodalProcessorOutput
               ▼
┌────────────────────────────────────────┐
│ Model Forward (인코딩 + 디코딩)         │
│ audio_features → encoder → decoder     │
│ → output_ids                           │
└──────────────┬─────────────────────────┘
               │
               ▼
┌────────────────────────────────────────┐
│ Transcription Adapter (후처리)         │
│ ├── WhisperAdapter                     │
│ │   └── timestamp 파싱 → segments      │
│ ├── Qwen3ASRAdapter                    │
│ │   └── <asr_text> 태그 제거           │
│ └── (다른 어댑터 확장 가능)             │
└────────────────────────────────────────┘

Whisper Processor: 고전적 ASR

오디오 전처리

Whisper는 mel 스펙트로그램을 입력으로 받는다. 30초 고정 컨텍스트로 패딩한다.

class WhisperProcessor(BaseMultimodalProcessor):
    models = [WhisperForConditionalGeneration]

    async def process_mm_data_async(self, image_data, audio_data, input_text, ...):
        audios = [load_audio(audio) for audio in audio_data]

        input_features = self._processor.feature_extractor(
            audios[0],
            sampling_rate=16000,
            padding="max_length",  # 3000 frames = 30초로 패딩
            return_tensors="pt",
        )["input_features"][0]

디코더 입력 토큰 구성

Whisper의 디코더 입력은 고정된 토큰 시퀀스다.

language = normalize_language_to_code(
    self._pop_sampling_param(request_obj, "language")
)
language_token_id = self._get_language_token_id(language)

input_ids = [
    decoder_start_token_id,    # <|startoftranscript|>
    language_token_id,          # <|en|>, <|ko|> 등
    transcribe_token_id,        # <|transcribe|>
    timestamp_token_id,         # <|0.00|> 또는 <|notimestamps|>
]

타임스탬프 생성 여부에 따라 <|0.00|> 또는 <|notimestamps|>를 선택한다.

WhisperAdapter: 타임스탬프 파싱

출력 토큰에서 타임스탬프 토큰을 파싱하여 세그먼트를 생성한다.

@register_transcription_adapter("Whisper")
class WhisperAdapter(TranscriptionAdapter):
    TIMESTAMP_BASE_TOKEN_ID = 50365  # <|0.00|>
    TIMESTAMP_BASE_OFFSET = 0.02     # 토큰당 0.02초

    @staticmethod
    def _parse_segments(output_ids, tokenizer):
        for token_id in output_ids:
            if token_id >= ts_base:
                timestamp = (token_id - ts_base) * ts_step
                if current_text_tokens:
                    seg_text = tokenizer.decode(current_text_tokens, ...)
                    segments.append(TranscriptionSegment(
                        id=seg_id,
                        start=round(current_start, 2),
                        end=round(timestamp, 2),
                        text=seg_text,
                    ))
                current_start = timestamp
            else:
                current_text_tokens.append(token_id)

토큰 ID 50365 이상은 타임스탬프 토큰이며, (token_id - 50365) * 0.02로 시간을 계산한다.

Qwen3-ASR Processor: 청크 스트리밍 지원

Qwen3-ASR은 오디오를 토큰 시퀀스로 변환하고, MRoPE를 지원한다.

class Qwen3ASRMultimodalProcessor(BaseMultimodalProcessor):
    models = [Qwen3ASRForConditionalGeneration]

    def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
        super().__init__(hf_config, server_args, _processor, *args, **kwargs)
        self.AUDIO_TOKEN = "<|audio_start|><|audio_pad|><|audio_end|>"
        tokenizer = self._processor.tokenizer
        self.audio_start_id = tokenizer.convert_tokens_to_ids("<|audio_start|>")
        self.audio_token_id = tokenizer.convert_tokens_to_ids("<|audio_pad|>")
        self.audio_end_id = tokenizer.convert_tokens_to_ids("<|audio_end|>")

기본 프롬프트

DEFAULT_ASR_PROMPT = (
    "<|im_start|>user\n"
    "<|audio_start|><|audio_pad|><|audio_end|>"
    "<|im_end|>\n"
    "<|im_start|>assistant\n"
)

ChatML 형식의 프롬프트에 오디오 플레이스홀더를 삽입한다.

MRoPE 위치 계산

Qwen3-ASR은 3차원 MRoPE를 사용한다.

def compute_mrope_positions(self, input_ids, mm_items):
    seq_len = len(input_ids)
    positions = torch.arange(seq_len, dtype=torch.long)
    mrope_positions = positions.unsqueeze(0).expand(3, -1).clone()
    return mrope_positions, torch.tensor([0], dtype=torch.long)

Qwen3ASRAdapter: 청크 스트리밍

@register_transcription_adapter("Qwen3ASR")
class Qwen3ASRAdapter(TranscriptionAdapter):
    @property
    def supports_chunked_streaming(self):
        return True

    @property
    def chunked_streaming_config(self):
        return {
            "chunk_size_sec": 2.0,
            "unfixed_chunk_num": 2,
            "unfixed_token_num": 5,
        }

    def postprocess_text(self, text):
        if self.ASR_TEXT_TAG in text:
            return text.split(self.ASR_TEXT_TAG, 1)[-1]
        return text

2초 단위 청크로 스트리밍 처리하며, 출력에서 <asr_text> 태그 이후의 실제 텍스트만 추출한다.

GLM-ASR Processor: 최소 구현

GLM-ASR은 Qwen3-ASR과 유사한 구조지만 더 단순하다.

class GlmAsrProcessor(BaseMultimodalProcessor):
    models = [GlmAsrForConditionalGeneration]

    def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
        super().__init__(hf_config, server_args, _processor, *args, **kwargs)
        self.AUDIO_TOKEN = "<|begin_of_audio|><|pad|><|end_of_audio|>"
        tokenizer = self._processor.tokenizer
        self.audio_start_id = tokenizer.convert_tokens_to_ids("<|begin_of_audio|>")
        self.audio_token_id = tokenizer.convert_tokens_to_ids("<|pad|>")
        self.audio_end_id = tokenizer.convert_tokens_to_ids("<|end_of_audio|>")

Transcription Adapter 등록 패턴

모든 어댑터는 아키텍처 이름으로 자동 매칭된다.

def resolve_adapter(architectures):
    for arch in architectures or []:
        for key, adapter_cls in _ADAPTER_REGISTRY.items():
            if key in arch:
                return adapter_cls()
    return _ADAPTER_REGISTRY[_DEFAULT_ADAPTER_KEY]()

"Whisper""WhisperForConditionalGeneration"에 매칭되고, "Qwen3ASR""Qwen3ASRForConditionalGeneration"에 매칭된다.

세 모델 비교

특성 Whisper Qwen3-ASR GLM-ASR
입력 형식 mel 스펙트로그램 오디오 토큰 오디오 토큰
고정 컨텍스트 30초 (3000 frames) 가변 가변
타임스탬프 토큰 ID 기반 ForcedAligner 필요 미지원
스트리밍 토큰 레벨 청크 레벨 (2초) 토큰 레벨
프롬프트 고정 토큰 시퀀스 ChatML + 오디오 ChatML + 오디오
언어 지원 ISO 639-1 코드 자동 감지 자동 감지

관련 포스트

참고

댓글

관련 포스트

SGLang 의 다른글