94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
218 lines
8.1 KiB
Python
218 lines
8.1 KiB
Python
"""Multimodal processor for Voxtral (speech-to-text) models."""
|
|
|
|
import math
|
|
import re
|
|
from typing import Dict, List, Optional
|
|
|
|
import torch
|
|
|
|
from sglang.srt.managers.schedule_batch import (
|
|
Modality,
|
|
MultimodalDataItem,
|
|
MultimodalProcessorOutput,
|
|
)
|
|
from sglang.srt.models.voxtral import VoxtralForConditionalGeneration
|
|
from sglang.srt.multimodal.processors.base_processor import (
|
|
BaseMultimodalProcessor,
|
|
MultimodalSpecialTokens,
|
|
)
|
|
|
|
# Special token IDs for Voxtral audio (from tekken.json vocabulary)
|
|
AUDIO_TOKEN_ID = 24 # [AUDIO]
|
|
BEGIN_AUDIO_TOKEN_ID = 25 # [BEGIN_AUDIO]
|
|
INST_TOKEN_ID = 3 # [INST]
|
|
|
|
# Placeholder for load_mm_data regex matching.
|
|
# encode("[AUDIO]") does NOT produce token 24; actual token insertion
|
|
# is handled in _build_input_ids_with_audio.
|
|
AUDIO_PLACEHOLDER = "[AUDIO]"
|
|
AUDIO_PLACEHOLDER_REGEX = re.compile(r"\[AUDIO\]")
|
|
|
|
|
|
class VoxtralMultimodalProcessor(BaseMultimodalProcessor):
|
|
models = [VoxtralForConditionalGeneration]
|
|
|
|
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
|
|
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
|
|
audio_config = getattr(hf_config, "audio_config", None)
|
|
self.audio_token_id = getattr(hf_config, "audio_token_id", AUDIO_TOKEN_ID)
|
|
self.sampling_rate = getattr(audio_config, "sampling_rate", 16000)
|
|
self.hop_length = getattr(audio_config, "hop_length", 160)
|
|
self.max_source_positions = getattr(audio_config, "max_source_positions", 1500)
|
|
self.conv_downsample = 2 # conv1 stride=1 * conv2 stride=2
|
|
self.downsample_factor = getattr(
|
|
audio_config,
|
|
"downsample_factor",
|
|
getattr(audio_config, "intermediate_size", 5120)
|
|
// getattr(audio_config, "hidden_size", 1280),
|
|
)
|
|
|
|
self.mm_tokens = MultimodalSpecialTokens(
|
|
audio_token=AUDIO_PLACEHOLDER,
|
|
audio_token_regex=AUDIO_PLACEHOLDER_REGEX,
|
|
audio_token_id=self.audio_token_id,
|
|
).build(_processor)
|
|
|
|
def _compute_audio_token_count(self, n_samples: int) -> int:
|
|
"""Compute the number of [AUDIO] tokens for a given audio length."""
|
|
mel_frames = n_samples / self.hop_length
|
|
chunk_size = self.max_source_positions * self.conv_downsample
|
|
n_chunks = math.ceil(mel_frames / chunk_size) if mel_frames > 0 else 1
|
|
tokens_per_chunk = self.max_source_positions // self.downsample_factor
|
|
return n_chunks * tokens_per_chunk
|
|
|
|
async def process_mm_data_async(
|
|
self,
|
|
image_data,
|
|
audio_data,
|
|
input_text,
|
|
request_obj,
|
|
**kwargs,
|
|
) -> Optional[MultimodalProcessorOutput]:
|
|
if not audio_data:
|
|
return None
|
|
|
|
# Insert [AUDIO] placeholders into prompt for load_mm_data's regex
|
|
prompt_with_placeholders = self._insert_audio_placeholders(
|
|
input_text, len(audio_data)
|
|
)
|
|
|
|
# load_mm_data handles async loading, format detection, resampling.
|
|
# process_and_combine_mm_data cannot be used: HF VoxtralProcessor.__call__
|
|
# does not support audio (only apply_chat_template does).
|
|
base_output = await self.load_mm_data(
|
|
prompt=prompt_with_placeholders,
|
|
audio_data=audio_data,
|
|
multimodal_tokens=self.mm_tokens,
|
|
audio_sample_rate=self.sampling_rate,
|
|
)
|
|
if base_output is None:
|
|
return None
|
|
|
|
# Convert loaded audio to tensors
|
|
waveforms: List[torch.Tensor] = []
|
|
for audio in base_output.audios:
|
|
wav = torch.as_tensor(audio, dtype=torch.float32)
|
|
if wav.dim() > 1:
|
|
wav = wav.mean(dim=0)
|
|
waveforms.append(wav)
|
|
|
|
# Compute audio token counts and build input_ids with audio tokens
|
|
audio_token_counts = [
|
|
self._compute_audio_token_count(wav.shape[-1]) for wav in waveforms
|
|
]
|
|
tokenizer = getattr(self._processor, "tokenizer", self._processor)
|
|
input_ids = self._build_input_ids_with_audio(
|
|
tokenizer, input_text, audio_token_counts
|
|
)
|
|
|
|
# Find offsets of [AUDIO] token runs and build mm_items
|
|
audio_offsets = self._find_audio_offsets(input_ids, self.audio_token_id)
|
|
mm_items = []
|
|
for i, wav in enumerate(waveforms):
|
|
item = MultimodalDataItem(feature=wav, modality=Modality.AUDIO)
|
|
if i < len(audio_offsets):
|
|
item.offsets = [audio_offsets[i]]
|
|
mm_items.append(item)
|
|
|
|
return MultimodalProcessorOutput(
|
|
input_ids=input_ids,
|
|
mm_items=mm_items,
|
|
audio_token_id=self.audio_token_id,
|
|
)
|
|
|
|
@staticmethod
|
|
def _insert_audio_placeholders(prompt: str, n_audio: int) -> str:
|
|
"""Insert [AUDIO] placeholder texts into the prompt for load_mm_data."""
|
|
placeholders = AUDIO_PLACEHOLDER * n_audio
|
|
# Insert after the last [INST] marker if present
|
|
last_inst = prompt.rfind("[INST]")
|
|
if last_inst >= 0:
|
|
insert_pos = last_inst + len("[INST]")
|
|
return prompt[:insert_pos] + placeholders + prompt[insert_pos:]
|
|
return placeholders + prompt
|
|
|
|
@staticmethod
|
|
def _find_audio_offsets(input_ids: List[int], audio_token_id: int) -> List[tuple]:
|
|
"""Find consecutive runs of audio_token_id in input_ids."""
|
|
offsets = []
|
|
start = None
|
|
for i, tok_id in enumerate(input_ids):
|
|
if tok_id == audio_token_id:
|
|
if start is None:
|
|
start = i
|
|
elif start is not None:
|
|
offsets.append((start, i - 1))
|
|
start = None
|
|
if start is not None:
|
|
offsets.append((start, len(input_ids) - 1))
|
|
return offsets
|
|
|
|
def _build_input_ids_with_audio(
|
|
self,
|
|
tokenizer,
|
|
input_text: str,
|
|
audio_token_counts: List[int],
|
|
) -> List[int]:
|
|
"""Build input_ids by tokenizing text and inserting audio tokens.
|
|
|
|
The input_text is a decoded Mistral prompt (from text-only
|
|
apply_chat_template). We re-tokenize to get proper special tokens
|
|
(BOS, [INST], [/INST]), then insert [BEGIN_AUDIO] + [AUDIO]*N after
|
|
the last [INST].
|
|
"""
|
|
messages = self._parse_mistral_prompt(input_text)
|
|
try:
|
|
input_ids = tokenizer.apply_chat_template(messages, tokenize=True)
|
|
except (ValueError, KeyError):
|
|
# Fallback if prompt parsing produces malformed messages
|
|
input_ids = tokenizer.encode(input_text)
|
|
|
|
# Insert audio tokens after the last [INST]
|
|
inst_positions = [i for i, t in enumerate(input_ids) if t == INST_TOKEN_ID]
|
|
insert_pos = (inst_positions[-1] + 1) if inst_positions else 1
|
|
|
|
audio_tokens = []
|
|
for count in audio_token_counts:
|
|
audio_tokens.append(BEGIN_AUDIO_TOKEN_ID)
|
|
audio_tokens.extend([AUDIO_TOKEN_ID] * count)
|
|
|
|
return input_ids[:insert_pos] + audio_tokens + input_ids[insert_pos:]
|
|
|
|
@staticmethod
|
|
def _parse_mistral_prompt(prompt: str) -> List[Dict[str, str]]:
|
|
"""Parse a Mistral-formatted prompt into a list of messages."""
|
|
messages = []
|
|
text = prompt.strip()
|
|
|
|
for marker in ["<s>", "</s>"]:
|
|
text = text.replace(marker, "")
|
|
text = text.strip()
|
|
|
|
# Extract system prompt
|
|
system_match = re.search(
|
|
r"\[SYSTEM_PROMPT\]\s*(.*?)\s*\[/SYSTEM_PROMPT\]", text, re.DOTALL
|
|
)
|
|
if system_match:
|
|
messages.append(
|
|
{"role": "system", "content": system_match.group(1).strip()}
|
|
)
|
|
text = text[: system_match.start()] + text[system_match.end() :]
|
|
text = text.strip()
|
|
|
|
# Split by [INST] / [/INST]
|
|
parts = re.split(r"\[/?INST\]", text)
|
|
for i, part in enumerate(parts):
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
if i % 2 == 1:
|
|
messages.append({"role": "user", "content": part})
|
|
elif i > 0:
|
|
messages.append({"role": "assistant", "content": part})
|
|
|
|
if not messages:
|
|
messages.append({"role": "user", "content": text})
|
|
|
|
return messages
|