[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 코드 | 자동 감지 | 자동 감지 |
관련 포스트
- Multimodal 처리 파이프라인 개요 - 전체 멀티모달 파이프라인 구조
- Vision-Language 모델: CLIP, InternVL, LLaVA - 비전 프로세서와의 비교
- Sampler: logits에서 토큰까지 - ASR 디코딩의 샘플링 단계
참고
관련 포스트
SGLang 의 다른글
- 이전글 [SGLang] Vision-Language 모델: CLIP, InternVL, LLaVA 프로세서
- 현재글 : [SGLang] Audio 모델: Whisper, Qwen3-ASR, GLM-ASR 프로세서
- 다음글 [SGLang] ViT CUDA Graph: Vision Encoder 가속
댓글