Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

317 lines
12 KiB
Python

"""Stateful audio preprocessing pipeline shared by MiMo multimodal and ASR processors."""
import io
import math
import os
import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Optional
import numpy as np
import pybase64
import torch
from sglang.srt.utils import common
from sglang.utils import logger
try:
from torchcodec.decoders import AudioDecoder
except ImportError:
logger.warning(
"torchcodec is not installed; audio inputs will fail at request time"
)
AudioDecoder = None
try:
import torchaudio
from torchaudio.transforms import MelSpectrogram
except ImportError:
logger.warning(
"torchaudio is not installed; audio inputs will fail at request time"
)
torchaudio = None
MelSpectrogram = None
@dataclass
class AudioInput:
"""
if audio is str or bytes, only load it as mel spectrogram.
if audio is tuple, it is (waveform, original_sr)
if audio is torch.Tensor, it is tokenized input ids with shape (T, n_vq+).
if audio is np.ndarray, it is a pre-loaded waveform (1D, already resampled).
"""
audio: str | bytes | tuple | torch.Tensor | np.ndarray
def __post_init__(self):
if not isinstance(self.audio, (str, bytes, tuple, torch.Tensor, np.ndarray)):
raise ValueError(
f"audio must be a str, bytes, tuple, torch.Tensor, or np.ndarray, but got {type(self.audio)}"
)
if isinstance(self.audio, tuple):
if (
len(self.audio) != 2
or not isinstance(self.audio[0], torch.Tensor)
or not isinstance(self.audio[1], (int, float))
):
raise ValueError(
f"audio must be a tuple of (waveform-T, original_sr-int/float), but got {len(self.audio)} elements and {type(self.audio[0])} and {type(self.audio[1])}"
)
if self.audio[0].ndim != 1:
raise ValueError(
f"waveform must be a 1D tensor, but got {self.audio[0].ndim}D tensor"
)
if self.audio[1] <= 0:
raise ValueError(
f"original_sr must be a positive number, but got {self.audio[1]}"
)
if isinstance(self.audio, torch.Tensor) and self.audio.ndim != 2:
raise ValueError(
f"audio must be a 2D tensor, but got {self.audio.ndim}D tensor"
)
class MiMoAudioPipeline:
"""Stateful audio preprocessing pipeline.
Composable: held by both MiMoProcessor (multimodal) and MiMoV2ASRProcessor.
Owns the mel spectrogram, resampler cache, http session, and the special
token ids for ``<|sosp|> <|empty|>* <|eosp|>`` placeholders.
"""
def __init__(
self,
*,
audio_token_id: int,
audio_start_token_id: int,
audio_end_token_id: int,
audio_kernel_size: int = 3,
audio_stride_size: int = 2,
audio_avg_pooler: int = 2,
audio_group_size: int = 4,
audio_channels: int = 8,
audio_sampling_rate: int = 24000,
audio_nfft: int = 960,
audio_hop_length: int = 240,
audio_window_size: int = 960,
audio_fmin: int = 0,
audio_fmax: Optional[int] = None,
audio_n_mels: int = 128,
audio_input_id_per_second: int = 25,
max_resamplers: int = 16,
) -> None:
self.audio_token_id = audio_token_id
self.audio_start_token_id = audio_start_token_id
self.audio_end_token_id = audio_end_token_id
self.audio_kernel_size = audio_kernel_size
self.audio_stride_size = audio_stride_size
self.audio_avg_pooler = audio_avg_pooler
self.audio_group_size = audio_group_size
self.audio_channels = audio_channels
self.audio_sampling_rate = audio_sampling_rate
self.audio_nfft = audio_nfft
self.audio_hop_length = audio_hop_length
self.audio_window_size = audio_window_size
self.audio_fmin = audio_fmin
self.audio_fmax = audio_fmax
self.audio_n_mels = audio_n_mels
self.audio_input_id_per_second = audio_input_id_per_second
self.mel_spectrogram_kwargs = dict(
sample_rate=audio_sampling_rate,
n_fft=audio_nfft,
hop_length=audio_hop_length,
win_length=audio_window_size,
f_min=audio_fmin,
f_max=audio_fmax,
n_mels=audio_n_mels,
power=1.0,
center=True,
)
self._mel_spectrogram = None
self._resamplers: OrderedDict[int, torchaudio.transforms.Resample] = (
OrderedDict()
)
self._resamplers_max = max_resamplers
@property
def audio_token_per_second(self) -> float:
return self.audio_input_id_per_second / self.audio_group_size
@staticmethod
def _ensure_audio_dependencies() -> None:
if torchaudio is None or MelSpectrogram is None:
raise RuntimeError(
"torchaudio is required for audio inputs; install torchaudio"
)
@property
def mel_spectrogram(self):
self._ensure_audio_dependencies()
if self._mel_spectrogram is None:
self._mel_spectrogram = MelSpectrogram(**self.mel_spectrogram_kwargs)
return self._mel_spectrogram
def compute_audio_token_len(self, mel_len: int) -> int:
n = mel_len + 3 - self.audio_kernel_size
n = (n + 2 - self.audio_kernel_size) // self.audio_stride_size + 1
n = n // self.audio_avg_pooler + int(n % self.audio_avg_pooler != 0)
return math.ceil(n / self.audio_group_size)
def preprocess_audio(self, audio):
"""Load audio source → log-mel spectrogram + token length.
Input: filename string, bytes, or tuple of (waveform, original_sr).
Output: (mel-spectrogram tensor [T, n_mels], audio_token_len int).
"""
self._ensure_audio_dependencies()
assert isinstance(
audio, (str, bytes, tuple)
), f"audio must be a str, bytes or tuple, but got {type(audio)}"
if isinstance(audio, tuple):
waveform, original_sr = audio
else:
if isinstance(audio, bytes):
file = io.BytesIO(audio)
elif isinstance(audio, str):
if audio.startswith("data:"):
file = io.BytesIO(
pybase64.b64decode(audio.split(",")[1], validate=True)
)
elif audio.startswith("http://") or audio.startswith("https://"):
dl_start = time.perf_counter()
timeout = int(os.getenv("REQUEST_TIMEOUT", "5"))
try:
with common.get_mm_http_session().get(
audio, stream=True, timeout=timeout
) as response:
response.raise_for_status()
dl_elapsed_ms = (time.perf_counter() - dl_start) * 1000
if dl_elapsed_ms > 1000.0:
content_len = len(response.content)
logger.warning(
f"Slow audio download: {dl_elapsed_ms:.2f}ms, "
f"size={content_len / 1024:.1f}KB, url={audio}"
)
file = io.BytesIO(response.content)
except Exception as e:
dl_elapsed_ms = (time.perf_counter() - dl_start) * 1000
logger.error(
f"Failed to download audio: {dl_elapsed_ms:.2f}ms, "
f"error={type(e).__name__}: {e}, url={audio}"
)
raise
else:
file = audio
if AudioDecoder is None:
raise RuntimeError(
"torchcodec is required for audio decoding; install with `pip install torchcodec`."
)
try:
samples = AudioDecoder(file).get_all_samples()
except RuntimeError as e:
audio_source = (
audio
if isinstance(audio, str)
and (audio.startswith("http://") or audio.startswith("https://"))
else "<bytes or base64>"
)
logger.error(f"Failed to decode audio: {e}, source={audio_source}")
raise ValueError(
f"Invalid audio format: source={audio_source}, detail={e}"
) from e
waveform = samples.data
original_sr = samples.sample_rate
if original_sr != self.audio_sampling_rate:
if original_sr in self._resamplers:
self._resamplers.move_to_end(original_sr)
else:
if len(self._resamplers) >= self._resamplers_max:
self._resamplers.popitem(last=False)
self._resamplers[original_sr] = torchaudio.transforms.Resample(
orig_freq=original_sr, new_freq=self.audio_sampling_rate
)
waveform = self._resamplers[original_sr](waveform)
if waveform.ndim == 2:
waveform = waveform.mean(dim=0)
spec = self.mel_spectrogram(waveform[None, :])
spec = torch.log(torch.clip(spec, min=1e-7)).squeeze()
spec = spec.transpose(0, 1)
audio_token_len = self.compute_audio_token_len(spec.shape[0])
return spec, audio_token_len
def process_audio(self, audio_input: AudioInput):
"""Dispatch on the underlying audio payload.
- str/bytes/tuple/np.ndarray waveform → returns (mel-spec, token_len) tuple
- 2D tensor of pre-tokenized audio codes → returns padded codes tensor
shaped [T//group, group, channels]
"""
audio = audio_input.audio
if isinstance(audio, np.ndarray):
waveform = torch.from_numpy(audio).float()
audio = (waveform, self.audio_sampling_rate)
if isinstance(audio, (str, bytes, tuple)):
return self.preprocess_audio(audio)
assert (
audio.shape[1] >= self.audio_channels
), f"audio must have at least {self.audio_channels} channels, but got {audio.shape[1]}"
T = audio.shape[0]
audio = audio[:, : self.audio_channels].to(torch.long)
padded_T = (
(T + self.audio_group_size - 1)
// self.audio_group_size
* self.audio_group_size
)
padded_audio = torch.cat(
[
audio,
torch.zeros(padded_T - T, self.audio_channels, dtype=torch.long)
+ audio[-1, :],
],
dim=0,
)
padded_audio = padded_audio.reshape(
padded_T // self.audio_group_size,
self.audio_group_size,
self.audio_channels,
)
return padded_audio
def build_audio_placeholder_ids(self, audio_token_len: int) -> list[int]:
return (
[self.audio_start_token_id]
+ [self.audio_token_id] * audio_token_len
+ [self.audio_end_token_id]
)
def process_audio_input(self, audio_input: AudioInput) -> dict:
"""Run process_audio and produce the placeholder input_ids.
Replaces the duplicated _process_audio_content bodies in both processors.
Returns dict with input_ids, audio_input (mel or codes), and is_tokenized.
"""
processed = self.process_audio(audio_input)
if isinstance(processed, tuple):
is_tokenized = False
audio_spec, audio_token_len = processed
payload = audio_spec
else:
is_tokenized = True
audio_token_len = processed.shape[0]
payload = processed
return {
"input_ids": self.build_audio_placeholder_ids(audio_token_len),
"audio_input": payload,
"audio_token_len": audio_token_len,
"is_tokenized": is_tokenized,
}