[model] support voxtral (speech-to-text) (#21635)

Co-authored-by: mengxiancheng03 <mengxiancheng03@kuaishou.com>
This commit is contained in:
Xiancheng Meng
2026-04-05 15:46:30 +08:00
committed by GitHub
co-authored by mengxiancheng03
parent 904bb476d8
commit 71544f0341
4 changed files with 777 additions and 1 deletions
@@ -1333,6 +1333,7 @@ multimodal_model_archs = [
"InternS1ForConditionalGeneration",
"InternS1ProForConditionalGeneration",
"Phi4MMForCausalLM",
"VoxtralForConditionalGeneration",
"WhisperForConditionalGeneration",
"Step3VLForConditionalGeneration",
"POINTSV15ChatModel",
+444
View File
@@ -0,0 +1,444 @@
# Adapted from:
# https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/voxtral.py
# https://huggingface.co/mistralai/Voxtral-Mini-3B-2507
#
# Copyright 2025 Mistral AI and the HuggingFace Inc. team.
# Licensed under the Apache License, Version 2.0.
"""Inference-only Voxtral (speech-to-text) model."""
import math
from typing import Any, Iterable, List, Optional, Tuple
import torch
import torch.nn as nn
from transformers import PretrainedConfig
from sglang.srt.layers.activation import get_act_fn
from sglang.srt.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.managers.mm_utils import (
MultiModalityDataPaddingPatternMultimodalTokens,
general_mm_embed_routine,
)
from sglang.srt.managers.schedule_batch import (
Modality,
MultimodalDataItem,
MultimodalInputs,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.llama import LlamaForCausalLM
class AudioLanguageAdapter(nn.Module):
"""MLP projector: Linear -> GELU -> Linear (no bias)."""
def __init__(self, hidden_size: int, dim: int) -> None:
super().__init__()
self.w_in = nn.Linear(hidden_size, dim, bias=False)
self.gelu = nn.GELU()
self.w_out = nn.Linear(dim, dim, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.w_out(self.gelu(self.w_in(x)))
class VoxtralWhisperAttention(nn.Module):
"""Multi-headed self-attention using plain SDPA (no KV cache).
Note: HF Voxtral has bias on q_proj, v_proj, out_proj but NOT on k_proj.
We use QKVParallelLinear with bias=True and create a zero bias for k_proj
during weight loading.
"""
def __init__(
self,
embed_dim: int,
num_heads: int,
quant_config: Optional[QuantizationConfig] = None,
):
super().__init__()
self.head_dim = embed_dim // num_heads
self.scaling = self.head_dim**-0.5
self.qkv_proj = QKVParallelLinear(
embed_dim, self.head_dim, num_heads, quant_config=quant_config
)
# After TP split, the local head count lives on the linear layer
self.num_heads = self.qkv_proj.num_heads
self.out_proj = RowParallelLinear(
embed_dim, embed_dim, bias=True, quant_config=quant_config
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
batch_size, seq_len, _ = hidden_states.shape
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.chunk(3, dim=-1)
q = q * self.scaling
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim).permute(
0, 2, 1, 3
)
k = k.view(batch_size, seq_len, self.num_heads, self.head_dim).permute(
0, 2, 1, 3
)
v = v.view(batch_size, seq_len, self.num_heads, self.head_dim).permute(
0, 2, 1, 3
)
attn_output = torch.nn.functional.scaled_dot_product_attention(
q, k, v, scale=1.0
)
attn_output = attn_output.permute(0, 2, 1, 3).reshape(
batch_size, seq_len, self.num_heads * self.head_dim
)
attn_output, _ = self.out_proj(attn_output)
return attn_output
class VoxtralWhisperEncoderLayer(nn.Module):
def __init__(
self,
config: PretrainedConfig,
quant_config: Optional[QuantizationConfig] = None,
):
super().__init__()
embed_dim = config.d_model
self.self_attn = VoxtralWhisperAttention(
embed_dim=embed_dim,
num_heads=config.encoder_attention_heads,
quant_config=quant_config,
)
self.self_attn_layer_norm = nn.LayerNorm(embed_dim)
self.activation_fn = get_act_fn(
getattr(config, "activation_function", "gelu"),
quant_config=quant_config,
)
self.fc1 = ColumnParallelLinear(embed_dim, config.encoder_ffn_dim)
self.fc2 = RowParallelLinear(config.encoder_ffn_dim, embed_dim)
self.final_layer_norm = nn.LayerNorm(embed_dim)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
residual = hidden_states
hidden_states = self.self_attn_layer_norm(hidden_states)
hidden_states = self.self_attn(hidden_states)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.final_layer_norm(hidden_states)
hidden_states, _ = self.fc1(hidden_states)
hidden_states = self.activation_fn(hidden_states)
hidden_states, _ = self.fc2(hidden_states)
hidden_states = residual + hidden_states
if hidden_states.dtype == torch.float16:
clamp_value = torch.finfo(hidden_states.dtype).max - 1000
hidden_states = torch.clamp(
hidden_states, min=-clamp_value, max=clamp_value
)
return hidden_states
class VoxtralWhisperEncoder(nn.Module):
"""Whisper encoder (Conv1d + positional embed + transformer + layer norm)."""
def __init__(
self,
config: PretrainedConfig,
quant_config: Optional[QuantizationConfig] = None,
):
super().__init__()
embed_dim = config.d_model
self.conv1 = nn.Conv1d(config.num_mel_bins, embed_dim, kernel_size=3, padding=1)
self.conv2 = nn.Conv1d(embed_dim, embed_dim, kernel_size=3, stride=2, padding=1)
self.embed_positions = nn.Embedding(config.max_source_positions, embed_dim)
self.layers = nn.ModuleList(
[
VoxtralWhisperEncoderLayer(config, quant_config)
for _ in range(config.encoder_layers)
]
)
self.layer_norm = nn.LayerNorm(embed_dim)
def forward(self, input_features: torch.Tensor) -> torch.Tensor:
"""
Args:
input_features: [batch, num_mel_bins, seq_len]
Returns:
[batch, seq_len // 2, d_model]
"""
inputs_embeds = torch.nn.functional.gelu(self.conv1(input_features))
inputs_embeds = torch.nn.functional.gelu(self.conv2(inputs_embeds))
inputs_embeds = inputs_embeds.permute(0, 2, 1)
seq_len = inputs_embeds.shape[1]
position_ids = torch.arange(seq_len, device=inputs_embeds.device)
hidden_states = inputs_embeds + self.embed_positions(position_ids)
for layer in self.layers:
hidden_states = layer(hidden_states)
hidden_states = self.layer_norm(hidden_states)
return hidden_states
class VoxtralForConditionalGeneration(nn.Module):
"""Voxtral: Whisper encoder + MLP projector + Llama decoder.
HF weight prefixes:
audio_tower.* -> self.audio_tower (VoxtralWhisperEncoder)
multi_modal_projector.* -> self.multi_modal_projector (AudioLanguageAdapter)
language_model.* -> self.language_model (LlamaForCausalLM)
"""
def __init__(
self,
config: PretrainedConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
audio_config = config.audio_config
text_config = config.text_config
# Ensure text_config has rope_parameters (transformers v5 compatibility)
if not hasattr(text_config, "rope_parameters"):
text_config.rope_parameters = {
"rope_type": getattr(text_config, "rope_type", "default"),
"rope_theta": getattr(text_config, "rope_theta", 10000.0),
}
if getattr(text_config, "rope_scaling", None):
text_config.rope_parameters.update(text_config.rope_scaling)
# Infer downsample_factor: intermediate_size / hidden_size for HF format
self.downsample_factor = getattr(
audio_config,
"downsample_factor",
audio_config.intermediate_size // audio_config.hidden_size,
)
# Encoder (named audio_tower to match HF weight prefix directly)
self.audio_tower = VoxtralWhisperEncoder(audio_config, quant_config)
# Projector: input = d_model * downsample_factor, output = text_hidden_size
adapter_input_dim = audio_config.d_model * self.downsample_factor
self.multi_modal_projector = AudioLanguageAdapter(
hidden_size=adapter_input_dim,
dim=text_config.hidden_size,
)
# Language model
self.language_model = LlamaForCausalLM(text_config, quant_config=quant_config)
# Mel filter bank for raw waveform -> mel spectrogram
self._init_mel_filters(audio_config)
self.pattern = MultiModalityDataPaddingPatternMultimodalTokens()
def _init_mel_filters(self, audio_config: PretrainedConfig):
"""Initialize mel filter bank for mel spectrogram computation."""
self._window_size = getattr(audio_config, "window_size", 400)
self._hop_length = getattr(audio_config, "hop_length", 160)
self._sampling_rate = getattr(audio_config, "sampling_rate", 16000)
try:
from mistral_common.audio import mel_filter_bank
except ImportError:
raise ImportError(
"mistral_common is required for Voxtral. "
"Install it with: pip install mistral_common"
)
mel_filters = mel_filter_bank(
num_frequency_bins=1 + self._window_size // 2,
num_mel_bins=audio_config.num_mel_bins,
min_frequency=0.0,
max_frequency=8000.0,
sampling_rate=self._sampling_rate,
)
self.register_buffer(
"mel_filters", torch.tensor(mel_filters, dtype=torch.float32)
)
@property
def _conv_downsample_factor(self) -> int:
return self.audio_tower.conv1.stride[0] * self.audio_tower.conv2.stride[0]
@property
def _chunk_size(self) -> int:
return (
self.config.audio_config.max_source_positions * self._conv_downsample_factor
)
def _compute_mel_spectrogram(self, audio_waveform: torch.Tensor) -> torch.Tensor:
"""Compute log-mel spectrogram from raw waveform using STFT."""
window = torch.hann_window(self._window_size, device=audio_waveform.device)
stft = torch.stft(
audio_waveform,
self._window_size,
self._hop_length,
window=window,
return_complex=True,
)
magnitudes = stft[..., :-1].abs() ** 2
mel_spec = self.mel_filters.T @ magnitudes
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
log_spec_max = log_spec.max()
log_spec = torch.maximum(log_spec, log_spec_max - 8.0)
log_spec = (log_spec + 4.0) / 4.0
return log_spec
def _encode_audio(self, audio_waveforms: List[torch.Tensor]) -> List[torch.Tensor]:
"""Encode raw audio waveforms through mel spectrogram + whisper encoder."""
dtype = self.audio_tower.conv1.weight.dtype
device = self.audio_tower.conv1.weight.device
chunked_features: List[torch.Tensor] = []
chunks_per_example: List[int] = []
chunk_size = self._chunk_size
# Pad raw audio to a multiple of chunk_samples so that silence is
# properly converted to mel features (matching HF VoxtralProcessor).
chunk_samples = chunk_size * self._hop_length
for waveform in audio_waveforms:
waveform = waveform.to(device=device, dtype=torch.float32)
n_samples = waveform.shape[-1]
target_samples = chunk_samples * math.ceil(n_samples / chunk_samples)
if target_samples > n_samples:
waveform = torch.nn.functional.pad(
waveform, (0, target_samples - n_samples)
)
mel = self._compute_mel_spectrogram(waveform)
chunks = mel.split(chunk_size, dim=-1)
chunked_features.extend(chunks)
chunks_per_example.append(len(chunks))
if not chunked_features:
return []
input_embeds = torch.stack(chunked_features).to(dtype)
encoder_out = self.audio_tower(input_embeds)
results = []
chunk_idx = 0
for n_chunks in chunks_per_example:
result = encoder_out[chunk_idx : chunk_idx + n_chunks].flatten(0, 1)
results.append(result)
chunk_idx += n_chunks
return results
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
return self.pattern.pad_input_tokens(input_ids, mm_inputs)
def get_audio_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
"""Encode audio waveforms -> downsample -> project."""
audio_waveforms = [item.feature for item in items]
audio_embeddings = self._encode_audio(audio_waveforms)
# Downsample: reshape to merge adjacent frames
for i, emb in enumerate(audio_embeddings):
seq_len, dim = emb.shape
audio_embeddings[i] = emb.reshape(
seq_len // self.downsample_factor,
dim * self.downsample_factor,
)
# Project through adapter
packed = torch.cat(audio_embeddings, dim=0)
packed = self.multi_modal_projector(packed)
return packed
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
**kwargs: Any,
) -> torch.Tensor:
hidden_states = general_mm_embed_routine(
input_ids=input_ids,
forward_batch=forward_batch,
language_model=self.language_model,
data_embedding_funcs={
Modality.AUDIO: self.get_audio_feature,
},
positions=positions,
)
return hidden_states
def get_language_model(self) -> nn.Module:
return self.language_model
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
encoder_stacked = [
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
]
encoder_dict = dict(self.audio_tower.named_parameters())
projector_dict = dict(self.multi_modal_projector.named_parameters())
# Collect all weights; synthesise missing k_proj bias as zeros.
weights_list = list(weights)
extra_weights = []
for name, w in weights_list:
if name.startswith("audio_tower.") and ".self_attn.k_proj.weight" in name:
bias_name = name.replace(".weight", ".bias")
if not any(n == bias_name for n, _ in weights_list):
extra_weights.append(
(bias_name, torch.zeros(w.shape[0], dtype=w.dtype))
)
weights_list.extend(extra_weights)
def llm_weights_generator():
for name, w in weights_list:
# Encoder weights
if name.startswith("audio_tower."):
trimmed = name[len("audio_tower.") :]
loaded = False
for param_name, weight_name, shard_id in encoder_stacked:
if f".{weight_name}." in trimmed:
stacked_name = trimmed.replace(weight_name, param_name)
if stacked_name in encoder_dict:
param = encoder_dict[stacked_name]
param.weight_loader(param, w, shard_id)
loaded = True
break
if not loaded and trimmed in encoder_dict:
param = encoder_dict[trimmed]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, w)
continue
# Projector weights
if name.startswith("multi_modal_projector."):
trimmed = name[len("multi_modal_projector.") :]
trimmed = trimmed.replace("linear_1.", "w_in.").replace(
"linear_2.", "w_out."
)
if trimmed in projector_dict:
param = projector_dict[trimmed]
default_weight_loader(param, w)
continue
# LLM weights
if name.startswith("language_model."):
name = name[len("language_model.") :]
yield (name, w)
self.language_model.load_weights(llm_weights_generator())
EntryClass = [VoxtralForConditionalGeneration]
@@ -0,0 +1,217 @@
"""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 = 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
@@ -837,9 +837,25 @@ def get_tokenizer(
)
raise RuntimeError(err_msg) from e
except ValueError as e:
# MistralCommon tokenizers reject standard HF kwargs like
# trust_remote_code, use_fast etc. Retry without them.
if "are not supported by" in str(e) and "MistralCommon" in str(e):
for k in (
"trust_remote_code",
"tokenizer_revision",
"use_fast",
"_from_auto",
"clean_up_tokenization_spaces",
):
kwargs.pop(k, None)
tokenizer = AutoTokenizer.from_pretrained(
tokenizer_name,
*args,
**kwargs,
)
# If the error pertains to the tokenizer class not existing or not
# currently being imported, suggest using the --trust-remote-code flag.
if not trust_remote_code and (
elif not trust_remote_code and (
"does not exist or is not currently imported." in str(e)
or "requires you to execute the tokenizer file" in str(e)
):
@@ -875,6 +891,7 @@ def get_tokenizer(
"slowdown. Consider using a fast tokenizer instead."
)
_patch_mistral_common_tokenizer(tokenizer)
_fix_special_tokens_pattern(tokenizer)
attach_additional_stop_token_ids(tokenizer)
tokenizer = patch_tokenizer(tokenizer)
@@ -1267,6 +1284,22 @@ def get_processor(
revision=revision,
**kwargs,
)
elif (
"are not supported by" in error_message and "MistralCommon" in error_message
):
logger.info(
"AutoProcessor for %s rejected standard kwargs, "
"retrying without trust_remote_code/use_fast",
tokenizer_name,
)
kwargs.pop("use_fast", None)
kwargs.pop("_from_auto", None)
processor = AutoProcessor.from_pretrained(
tokenizer_name,
*args,
revision=revision,
**kwargs,
)
elif "Unrecognized feature extractor" in error_message:
logger.info(
"AutoProcessor failed on feature extractor for %s, "
@@ -1315,6 +1348,7 @@ def get_processor(
)
tokenizer = get_tokenizer_from_processor(processor)
_patch_mistral_common_tokenizer(tokenizer)
if tokenizer.chat_template is None:
local_path = download_from_hf(
@@ -1341,6 +1375,86 @@ def attach_additional_stop_token_ids(tokenizer):
tokenizer.additional_stop_token_ids = None
def _patch_mistral_common_tokenizer(tokenizer):
"""Patch MistralCommonTokenizer/Backend to be compatible with HF tokenizer API.
MistralCommon tokenizers (used by Voxtral, Pixtral, etc.) reject several
standard kwargs and lack some attributes that sglang expects. We wrap the
offending methods once at load time so that the rest of the codebase does
not need any special-casing.
"""
cls_name = type(tokenizer).__name__
if "MistralCommon" not in cls_name:
return tokenizer
if getattr(tokenizer, "_mistral_common_patched", False):
return tokenizer
tokenizer._mistral_common_patched = True
# Missing attributes
if not hasattr(tokenizer, "get_added_vocab"):
tokenizer.get_added_vocab = lambda: {}
# Set a chat_template containing "audio" so that sglang's content format
# detector returns "openai" (which preserves audio_url extraction).
# The actual template rendering is done by MistralCommon's apply_chat_template.
if not hasattr(tokenizer, "chat_template") or tokenizer.chat_template is None:
tokenizer.chat_template = "<!-- audio/image multimodal -->"
# convert_tokens_to_ids asserts on multi-token strings
_orig_convert = tokenizer.convert_tokens_to_ids
def _safe_convert(val):
try:
return _orig_convert(val)
except AssertionError:
return getattr(tokenizer, "unk_token_id", None)
tokenizer.convert_tokens_to_ids = _safe_convert
# Wrap methods that reject certain kwargs
def _drop_kwargs(fn, keys):
def wrapper(*args, **kwargs):
for k in keys:
kwargs.pop(k, None)
return fn(*args, **kwargs)
return wrapper
tokenizer.decode = _drop_kwargs(tokenizer.decode, ["spaces_between_special_tokens"])
tokenizer.batch_decode = _drop_kwargs(
tokenizer.batch_decode, ["spaces_between_special_tokens"]
)
# Save original apply_chat_template for processors that need it (e.g. Voxtral)
tokenizer._orig_apply_chat_template = tokenizer.apply_chat_template
def _safe_apply_chat_template(messages, **kwargs):
"""Wrapper that strips unsupported kwargs and non-text content parts.
When sglang extracts audio/image URLs, it replaces content blocks with
{"type": "audio"} or {"type": "image"} (no URL). MistralCommon fails
on these stripped blocks. We convert them to text-only messages.
"""
kwargs.pop("add_generation_prompt", None)
cleaned = []
for msg in messages:
if isinstance(msg, dict):
content = msg.get("content", "")
if isinstance(content, list):
text_parts = [
p.get("text", "")
for p in content
if isinstance(p, dict) and p.get("type") == "text"
]
msg = {**msg, "content": " ".join(text_parts) if text_parts else ""}
cleaned.append(msg)
else:
cleaned.append(msg)
return tokenizer._orig_apply_chat_template(cleaned, **kwargs)
tokenizer.apply_chat_template = _safe_apply_chat_template
def check_gguf_file(model: Union[str, os.PathLike]) -> bool:
"""Check if the file is a GGUF model."""
model = Path(model)