[New Model]: XiaomiMiMo/MiMo-Audio-7B-Instruct support (#750)

Signed-off-by: wangyu31577 <wangyu31577@hundsun.com>
Signed-off-by: 齐保元 <qibaoyuan@xiaomi.com>
Signed-off-by: hsliu <liuhongsheng4@huawei.com>
Signed-off-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Signed-off-by: GG-li <3226868735@qq.com>
Signed-off-by: Sihao Li <111170255+GG-li@users.noreply.github.com>
Signed-off-by: XU Mingshi <91017482+mxuax@users.noreply.github.com>
Signed-off-by: mxuax <mxuax@connect.ust.hk>
Signed-off-by: Baoyuan Qi <qibaoyuan@126.com>
Signed-off-by: gcanlin <canlinguosdu@gmail.com>
Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Signed-off-by: wuzhongjian <wuzhongjian_yewu@cmss.chinamobile.com>
Signed-off-by: dongbo910220 <1275604947@qq.com>
Signed-off-by: dongbo910220 <32610838+dongbo910220@users.noreply.github.com>
Signed-off-by: Jiangyun Zhu <riverclouds.zhu@qq.com>
Signed-off-by: Junhong Liu <98734602+LJH-LBJ@users.noreply.github.com>
Signed-off-by: tjtanaa <tunjian.tan@embeddedllm.com>
Signed-off-by: baoyuan qi <qibaoyuan@126.com>
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: Prajwal A <prajwalanagani@gmail.com>
Signed-off-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Signed-off-by: 丁宁 <nndding@gmail.com>
Signed-off-by: SHIJIN ZHANG <75300765+Dovis01@users.noreply.github.com>
Signed-off-by: dingning<dingning7@xiaomi.com>
Signed-off-by: dingning <dingning7@xiaomi.com>
Signed-off-by: dingning <dingning@xiaomi.com>
Co-authored-by: wangyu <53896905+yenuo26@users.noreply.github.com>
Co-authored-by: wangyu31577 <wangyu31577@hundsun.com>
Co-authored-by: Zhang Shijin <zhangshijin@xiaomi.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Sihao Li <111170255+GG-li@users.noreply.github.com>
Co-authored-by: XU Mingshi <91017482+mxuax@users.noreply.github.com>
Co-authored-by: Canlin Guo <canlinguosdu@gmail.com>
Co-authored-by: Isotr0py <mozf@mail2.sysu.edu.cn>
Co-authored-by: JohnJan <wuzhongjian_yewu@cmss.chinamobile.com>
Co-authored-by: WeiQing Chen <40507679+david6666666@users.noreply.github.com>
Co-authored-by: dongbo910220 <32610838+dongbo910220@users.noreply.github.com>
Co-authored-by: Jiangyun Zhu <riverclouds.zhu@qq.com>
Co-authored-by: Junhong Liu <ljh_lbj@163.com>
Co-authored-by: TJian <tunjian.tan@embeddedllm.com>
Co-authored-by: shijin zhang <zsj1364226740@gmail.com>
Co-authored-by: Zhou Taichang <tzhouam@connect.ust.hk>
Co-authored-by: root <root@hk01dgx028.cm.cluster>
Co-authored-by: Prajwal A <34590600+LawJarp-A@users.noreply.github.com>
Co-authored-by: Shijin Zhang <75300765+Dovis01@users.noreply.github.com>
Co-authored-by: dingning <dingning7@xiaomi.com>
Co-authored-by: ning ding <nndding@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
23 files changed
+8041

No files matched your search

+1
View File
@@ -43,6 +43,7 @@ th {
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` |
|`GlmImageForConditionalGeneration` | GLM-Image | `zai-org/GLM-Image` |
|`NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` |
|`MiMoAudioForConditionalGeneration` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` |
## List of Supported Models for NPU
@@ -0,0 +1,232 @@
# MiMo-Audio Offline Inference
This directory contains an offline demo for running MiMo-Audio models with vLLM Omni. It builds task-specific inputs and generates WAV files or text outputs locally.
## Model Overview
MiMo-Audio provides multiple task variants for audio understanding and generation:
- **tts_sft**: Basic text-to-speech generation from text input.
- **tts_sft_with_instruct**: TTS generation with explicit voice style instructions.
- **tts_sft_with_audio**: TTS generation with audio reference for voice cloning.
- **tts_sft_with_natural_instruction**: TTS generation from natural language descriptions embedded in text.
- **audio_trancribing_sft**: Transcribe audio to text (speech-to-text).
- **audio_understanding_sft**: Understand and analyze audio content with text queries.
- **audio_understanding_sft_with_thinking**: Audio understanding with reasoning chain.
- **spoken_dialogue_sft_multiturn**: Multi-turn spoken dialogue with audio input/output.
- **speech2text_dialogue_sft_multiturn**: Multi-turn dialogue converting speech to text.
- **text_dialogue_sft_multiturn**: Multi-turn text-only dialogue.
## Setup
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
### Environment Variables
The `MIMO_AUDIO_TOKENIZER_PATH` environment variable is mandatory due to the specialized architecture:
```bash
export MIMO_AUDIO_TOKENIZER_PATH="XiaomiMiMo/MiMo-Audio-Tokenizer"
```
## Quick Start
Run a single sample for basic TTS:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft
```
Run batch samples for basic TTS:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft \
--num-prompts {batch_size}
```
When enabling multi-batch processing, if the total number of tokens passed to the next stage exceeds the `max_model_len` value in the `mimo_audio.yaml` configuration file, you must also synchronously update the `max_position_embeddings` value in `MiMo-Audio-7B-Instruct/config.json` to match the modified value.
Generated audio files are saved to `output_audio/` by default. `--num-prompts` also can be used to all tasks below.
## Task Usage
### tts_sft (Basic Text-to-Speech)
Generate speech from text input:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft \
--text "The weather is so nice today."
```
### tts_sft_with_instruct (TTS with Voice Instructions)
Generate speech with explicit voice style instructions:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft_with_instruct \
--text "The weather is so nice today." \
--instruct "Speak happily in a child's voice"
```
### tts_sft_with_audio (TTS with Audio Reference)
Generate speech using an audio reference for voice cloning:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft_with_audio \
--text "The weather is so nice today." \
--audio-path "./spoken_dialogue_assistant_turn_1.wav"
```
### tts_sft_with_natural_instruction (Natural Language TTS)
Generate speech from text containing natural voice descriptions:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type tts_sft_with_natural_instruction \
--text "In a panting young male voice, he said: I can't run anymore, wait for me!"
```
### audio_trancribing_sft (Speech-to-Text)
Transcribe audio to text:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type audio_trancribing_sft \
--audio-path "./spoken_dialogue_assistant_turn_1.wav"
```
### audio_understanding_sft (Audio Understanding)
Understand and analyze audio content with text queries:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type audio_understanding_sft \
--text "Summarize the audio." \
--audio-path "./spoken_dialogue_assistant_turn_1.wav"
```
### audio_understanding_sft_with_thinking (Audio Understanding with Reasoning)
Audio understanding with reasoning chain:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type audio_understanding_sft_with_thinking \
--text "Summarize the audio." \
--audio-path "./spoken_dialogue_assistant_turn_1.wav"
```
### spoken_dialogue_sft_multiturn (Multi-turn Spoken Dialogue)
Multi-turn dialogue with audio input and output:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type spoken_dialogue_sft_multiturn \
--audio-path "./prompt_speech_zh_m.wav"
```
Note: This task uses hardcoded audio files in the script. The audio files used in examples are available at: https://github.com/XiaomiMiMo/MiMo-Audio/tree/main/examples
### speech2text_dialogue_sft_multiturn (Speech-to-Text Dialogue)
Multi-turn dialogue converting speech to text:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type speech2text_dialogue_sft_multiturn
```
Note: This task uses hardcoded audio files and message lists in the script.
### text_dialogue_sft_multiturn (Text Dialogue)
Multi-turn text-only dialogue:
```bash
python3 -u end2end.py \
--stage-configs-path vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--model-name XiaomiMiMo/MiMo-Audio-7B-Instruct \
--query-type text_dialogue_sft_multiturn
```
Note: This task uses hardcoded message lists in the script.
## Troubleshooting
### Audio dependencies (soundfile, librosa)
This example depends on **soundfile** (read/write WAV) and **librosa** (load audio including MP3). Install the project requirements first:
```bash
pip install -r requirements/common.txt
# or at least: pip install soundfile>=0.13.1 librosa>=0.11.0
```
- **`soundfile` / libsndfile not found**
`soundfile` uses the C library **libsndfile**. On Linux, install the system package before pip:
- Debian/Ubuntu: `sudo apt-get install libsndfile1`
- For development builds: `sudo apt-get install libsndfile1-dev`
- Then: `pip install soundfile`
- **`librosa` fails to load MP3 or reports "No backend available"**
Loading MP3 (e.g. in `spoken_dialogue_sft_multiturn` with `.mp3` files) uses **ffmpeg** as the backend. Install ffmpeg:
- Debian/Ubuntu: `sudo apt-get install ffmpeg`
- macOS: `brew install ffmpeg`
- **`ImportError: No module named 'soundfile'` or `ModuleNotFoundError: ... librosa`**
Ensure you are in the same Python environment where vLLM Omni and the example dependencies are installed, and that `requirements/common.txt` (or the packages above) are installed.
### Tokenizer path
- **`MIMO_AUDIO_TOKENIZER_PATH` not set or model fails to find tokenizer**
Export the tokenizer path before running:
```bash
export MIMO_AUDIO_TOKENIZER_PATH="XiaomiMiMo/MiMo-Audio-Tokenizer"
```
See [Environment Variables](#environment-variables) in Setup.
### Other
- If the model or stage config fails to load, check [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) for memory and GPU settings.
- For errors when reading/writing WAV (e.g. unsupported format), ensure input files are standard WAV/MP3 and that `soundfile` is linked to a working libsndfile (see above).
## Notes
- The script uses default model paths and audio files embedded in `end2end.py`. Update them if your local cache path differs.
- Use `--output-dir` to change the output folder (default: `./output_audio`).
- Use `--num-prompts` to generate multiple prompts in one run (default: 1).
- Audio files used in multi-turn dialogue examples are available at: https://github.com/XiaomiMiMo/MiMo-Audio/tree/main/examples
- The script supports various configuration options for initialization timeouts, batch timeouts, and shared memory thresholds. See `--help` for details.
@@ -0,0 +1,443 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
This example shows how to use vLLM for running offline inference
with the correct prompt format on MiMo-Audio-Omni.
"""
import copy
import json
import os
from typing import NamedTuple
import soundfile as sf
from message_convert import (
get_audio_data,
get_audio_understanding_sft_prompt,
get_s2t_dialogue_sft_multiturn_prompt,
get_spoken_dialogue_sft_multiturn_prompt,
get_text_dialogue_sft_multiturn_prompt,
get_tts_sft_prompt,
to_prompt,
)
from vllm import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniTokensPrompt
SEED = 42
MAX_CODE2WAV_TOKENS = 18192 # Maximum tokens supported by code2wav model
class QueryResult(NamedTuple):
inputs: dict
limit_mm_per_prompt: dict[str, int]
def get_codes_query_from_json(codes_path: str) -> QueryResult:
with open(codes_path, encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
code_final = data
elif isinstance(data, dict) and "code_final" in data:
code_final = data["code_final"]
else:
raise ValueError(
f"Unsupported codes json format in {codes_path}.\n"
"Expect a JSON list[int] or {{'code_final': list[int]}}."
)
if not isinstance(code_final, list) or not all(isinstance(x, int) for x in code_final):
raise ValueError("code_final must be a list[int].")
if len(code_final) > MAX_CODE2WAV_TOKENS:
print(f"[Warn] code_final len={len(code_final)} > {MAX_CODE2WAV_TOKENS}, truncating.")
code_final = code_final[:MAX_CODE2WAV_TOKENS]
return QueryResult(
inputs=OmniTokensPrompt(
prompt_token_ids=code_final,
multi_modal_data=None,
mm_processor_kwargs=None,
),
limit_mm_per_prompt={},
)
def get_tts_sft(
text="The weather is so nice today.",
instruct=None,
read_text_only=True,
prompt_speech=None,
audio_list=None,
):
res = get_tts_sft_prompt(
text,
instruct=instruct,
read_text_only=read_text_only,
prompt_speech=prompt_speech,
)
prompt = to_prompt(res)
final_prompt = {
"prompt": prompt,
}
if audio_list is not None:
final_prompt.update(
{
"multi_modal_data": {
"audio": audio_list,
},
}
)
return final_prompt
def get_audio_understanding_sft(audio_path, text="", thinking=False, use_sostm=False):
audio_list = []
audio_list.append(get_audio_data(audio_path))
res = get_audio_understanding_sft_prompt(
input_speech=audio_path, input_text=text, thinking=thinking, use_sostm=use_sostm
)
prompt = to_prompt(res)
final_prompt = {
"prompt": prompt,
"multi_modal_data": {
"audio": audio_list,
},
}
return final_prompt
def get_spoken_dialogue_sft_multiturn(message_list, system_prompt=None, ref_audio_path=None, audio_list=None):
res = get_spoken_dialogue_sft_multiturn_prompt(
message_list, system_prompt=system_prompt, prompt_speech=ref_audio_path
)
prompt = to_prompt(res)
final_prompt = {
"prompt": prompt,
"multi_modal_data": {
"audio": audio_list,
},
}
return final_prompt
def get_speech2text_dialogue_sft_multiturn(message_list, thinking=False, audio_list=None):
res = get_s2t_dialogue_sft_multiturn_prompt(
message_list,
thinking=thinking,
)
prompt = to_prompt(res)
final_prompt = {
"prompt": prompt,
"multi_modal_data": {
"audio": audio_list,
},
}
return final_prompt
def get_text_dialogue_sft_multiturn(
message_list,
):
res = get_text_dialogue_sft_multiturn_prompt(
message_list,
)
prompt = to_prompt(res)
final_prompt = {
"prompt": prompt,
}
return final_prompt
query_map = {
"tts_sft": get_tts_sft,
"tts_sft_with_instruct": get_tts_sft,
"tts_sft_with_audio": get_tts_sft,
"tts_sft_with_natural_instruction": get_tts_sft,
"audio_trancribing_sft": get_audio_understanding_sft,
"audio_understanding_sft": get_audio_understanding_sft,
"audio_understanding_sft_with_thinking": get_audio_understanding_sft,
"spoken_dialogue_sft_multiturn": get_spoken_dialogue_sft_multiturn,
"speech2text_dialogue_sft_multiturn": get_speech2text_dialogue_sft_multiturn,
"text_dialogue_sft_multiturn": get_text_dialogue_sft_multiturn,
}
def main(args):
model_name = args.model_name
# Get paths from args
text = getattr(args, "text", None)
audio_path = getattr(args, "audio_path", None)
instruct = getattr(args, "instruct", None)
# Get the query function and call it with appropriate parameters
query_func = query_map[args.query_type]
omni_llm = Omni(
model=model_name,
stage_configs_path=args.stage_configs_path,
log_stats=args.enable_stats,
log_file=("omni_llm_pipeline.log" if args.enable_stats else None),
init_sleep_seconds=args.init_sleep_seconds,
batch_timeout=args.batch_timeout,
init_timeout=args.init_timeout,
shm_threshold_bytes=args.shm_threshold_bytes,
)
thinker_sampling_params = SamplingParams(
temperature=0.0,
top_p=1.0,
top_k=-1,
max_tokens=2048,
seed=SEED,
logit_bias={},
repetition_penalty=1.1,
)
code2wav_sampling_params = SamplingParams(
temperature=0.0,
top_p=1.0,
top_k=-1,
max_tokens=4096 * 16,
seed=SEED,
detokenize=True,
repetition_penalty=1.1,
)
sampling_params_list = [
thinker_sampling_params,
code2wav_sampling_params,
]
# Build query result based on query type
# Notice: The audio files used in this example are available at: https://github.com/XiaomiMiMo/MiMo-Audio/tree/main/examples
if args.query_type == "tts_sft":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft
query_result = query_func(text=text, read_text_only=True)
elif args.query_type == "tts_sft_with_instruct":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_instruct --instruct "Speak happily in a child's voice"
query_result = query_func(text=text, instruct=instruct, read_text_only=True)
elif args.query_type == "tts_sft_with_audio":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_audio --audio_path "./spoken_dialogue_assistant_turn_1.wav"
audio_list = [get_audio_data(audio_path)]
query_result = query_func(text=text, read_text_only=True, prompt_speech=audio_path, audio_list=audio_list)
elif args.query_type == "tts_sft_with_natural_instruction":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type tts_sft_with_natural_instruction --text "In a panting young male voice, he said: I can't run anymore, wait for me!"
query_result = query_func(text=text, read_text_only=False)
elif args.query_type == "audio_trancribing_sft":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_trancribing_sft --audio_path "./spoken_dialogue_assistant_turn_1.wav"
audio_path = "spoken_dialogue_assistant_turn_1.wav"
text = "Please transcribe this audio and repeat it once."
query_result = query_func(text=text, audio_path=audio_path, use_sostm=True)
elif args.query_type == "audio_understanding_sft":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_understanding_sft --text "Summarize the audio." --audio_path "./spoken_dialogue_assistant_turn_1.wav"
query_result = query_func(text=text, audio_path=audio_path)
elif args.query_type == "audio_understanding_sft_with_thinking":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type audio_understanding_sft_with_thinking --text "Summarize the audio." --audio_path "./spoken_dialogue_assistant_turn_1.wav"
query_result = query_func(text=text, audio_path=audio_path, thinking=True)
elif args.query_type == "spoken_dialogue_sft_multiturn":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type spoken_dialogue_sft_multiturn --audio_path "./prompt_speech_zh_m.wav"
first_turn_text_response = "我没办法获取实时的天气信息。不过呢,你可以试试几个方法来查看今天的天气。首先,你可以用手机自带的天气功能,比如苹果手机的天气应用,或者直接在系统设置里查看。其次,你也可以用一些专业的天气服务,像是国外的AccuWeather、Weather.com,或者国内的中国天气网、墨迹天气等等。再有就是,你还可以在谷歌或者百度里直接搜索你所在的城市加上天气这两个字。如果你能告诉我你所在的城市,我也可以帮你分析一下历史天气趋势,不过最新的数据还是需要你通过官方渠道去获取哦。"
audio_list = []
s1_audio_path = "weather_of_today.mp3"
s2_audio_path = "spoken_dialogue_assistant_turn_1.wav"
s3_audio_path = "beijing.mp3"
audio_list.append(get_audio_data(audio_path))
audio_list.append(get_audio_data(s1_audio_path))
audio_list.append(get_audio_data(s2_audio_path))
audio_list.append(get_audio_data(s3_audio_path))
message_list = [
{"role": "user", "content": s1_audio_path},
{"role": "assistant", "content": {"text": first_turn_text_response, "audio": s2_audio_path}},
{"role": "user", "content": s3_audio_path},
]
query_result = query_func(message_list, system_prompt=None, ref_audio_path=audio_path, audio_list=audio_list)
elif args.query_type == "speech2text_dialogue_sft_multiturn":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type speech2text_dialogue_sft_multiturn
s1_audio_path = "weather_of_today.mp3"
s2_audio_path = "beijing.mp3"
audio_list = []
audio_list.append(get_audio_data(s1_audio_path))
audio_list.append(get_audio_data(s2_audio_path))
message_list = [
{"role": "user", "content": s1_audio_path},
{
"role": "assistant",
"content": "Hello, I can't get real-time weather information. If you can tell me your city, I can help you analyze historical weather trends, but for the latest data, you'll need to get it through official channels.",
},
{"role": "user", "content": s2_audio_path},
]
query_result = query_func(message_list, thinking=True, audio_list=audio_list)
elif args.query_type == "text_dialogue_sft_multiturn":
# python3 -u end2end.py --stage-configs-path ${config_file} --model ${MODEL_PATH} --query-type text_dialogue_sft_multiturn
message_list = [
{"role": "user", "content": "Could you recommend some tourist attractions in China?"},
{"role": "assistant", "content": "Hello, which city would you like to travel to?"},
{"role": "user", "content": "Beijing"},
]
query_result = query_func(message_list=message_list)
else:
raise ValueError(f"Invalid query type: {args.query_type}")
prompts = [copy.deepcopy(query_result) for _ in range(args.num_prompts)]
print("prompts", prompts)
omni_outputs = omni_llm.generate(prompts, sampling_params_list)
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
if args.query_type is not None:
output_dir = os.path.join(output_dir, args.query_type)
os.makedirs(output_dir, exist_ok=True)
for stage_outputs in omni_outputs:
if stage_outputs.final_output_type == "text":
for output in stage_outputs.request_output:
request_id = output.request_id
text_output = output.outputs[0].text
# Save aligned text file per request
prompt_text = output.prompt
out_txt = os.path.join(output_dir, f"{request_id}.txt")
lines = []
lines.append("Prompt:\n")
lines.append(str(prompt_text) + "\n")
lines.append("vllm_text_output:\n")
lines.append(str(text_output).strip() + "\n")
try:
with open(out_txt, "w", encoding="utf-8") as f:
print("lines", lines)
f.writelines(lines)
except Exception as e:
print(f"[Warn] Failed writing text file {out_txt}: {e}")
print(f"Request ID: {request_id}, Text saved to {out_txt}\n")
elif stage_outputs.final_output_type == "audio":
for output in stage_outputs.request_output:
request_id = output.request_id
audio_tensor = output.outputs[0].multimodal_output.get("audio")
if audio_tensor is None:
continue
output_wav = os.path.join(output_dir, f"{request_id}.wav")
# Convert to numpy array and ensure correct format
audio_numpy = audio_tensor.float().detach().cpu().numpy()
# Ensure audio is 1D (flatten if needed)
if audio_numpy.ndim > 1:
audio_numpy = audio_numpy.flatten()
# Save audio file with explicit WAV format
sf.write(output_wav, audio_numpy, samplerate=24000, format="WAV")
print(f"Request ID: {request_id}, Audio saved to {output_wav}")
def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument(
"--model-name",
"-m",
type=str,
default="XiaomiMiMo/MiMo-Audio-7B-Instruct",
help="Backbone LLM path.",
)
parser.add_argument(
"--text",
"-t",
type=str,
default="The weather is so nice today.",
help="input text",
)
parser.add_argument(
"--query-type",
"-q",
type=str,
default="tts_sft",
choices=query_map.keys(),
help="Query type.",
)
parser.add_argument(
"--audio-path",
"-a",
type=str,
default=None,
help="Path to local audio file. If not provided, uses default audio asset.",
)
parser.add_argument(
"--instruct",
type=str,
default=None,
help="instruct",
)
parser.add_argument(
"--enable-stats",
action="store_true",
help="Enable writing detailed statistics (default: disabled)",
)
parser.add_argument(
"--init-sleep-seconds",
type=int,
default=20,
help="Sleep seconds after starting each stage process to allow initialization (default: 20)",
)
parser.add_argument(
"--batch-timeout",
type=int,
default=5,
help="Timeout for batching in seconds (default: 5)",
)
parser.add_argument(
"--init-timeout",
type=int,
default=5000,
help="Timeout for initializing stages in seconds (default: 5000)",
)
parser.add_argument(
"--shm-threshold-bytes",
type=int,
default=65536,
help="Threshold for using shared memory in bytes (default: 65536)",
)
parser.add_argument(
"--output-dir",
default="./output_audio",
help="Output audio wav directory.",
)
parser.add_argument(
"--output-wav",
default="output_audio",
help="[Deprecated] Output wav directory (use --output-dir).",
)
parser.add_argument(
"--num-prompts",
type=int,
default=1,
help="Number of prompts to generate.",
)
parser.add_argument(
"--sampling-rate",
type=int,
default=24000,
help="Sampling rate for audio.",
)
parser.add_argument(
"--stage-configs-path",
type=str,
default="../../../model_executor/stage_configs/mimo_audio.yaml",
help="Path to a stage configs file.",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
main(args)
@@ -0,0 +1,56 @@
{
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "Your voice should be:"
},
{
"type": "audio_url",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiRKAwBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YQBKAwAFAP3/CAD7/wkAAAD9/wgA+v8JAP//AQADAP//CAD+/wEABAABAP7/BgD8//7/BgD0/wYA/f/8/wMA9f8GAP///P8DAPz/AAAFAP3//v8CAP7////+////+/8IAP//AQAAAAMABgD6/wUA+v8DAAcA+/8AAAMAAAAAAP3//v8DAAMACgD8/wQACQD9/wUA/f8BAAcABQADAAAABAADAAEA+v8AAAIAAAAFAPz//f8DAAAA/f8AAAMA/v8FAP7/9v8GAP7//f8EAAAAAQAIAPv/+/8AAP3////+//3/AAAHAPr//v8BAPz/AwD///n/AAACAPr//f/8//7/AAD//wAA+f8GAPz/9v8CAPX/BgACAPv/AQD//wQA///7/wAAAAAHAAQA/f8FAAAAAgAAAPv/BAAGAAAABgACAP7/DQD7//3/BAD//wkA//8DAPz/AwAHAPf/AwAAAAAABAD7//f/AwD8/wAA/f/5/wQA/P8AAPr//P8AAAAA//8AAP////8DAPL/AQD9//r/CwD4/wEAAgD9//7//P/9//7/AAD//wAA/v8DAPr/+v8CAPr///8CAPz///8GAPr/AAAEAP//AgADAP//AAAHAPz/BgD//wUACwD//wYAAgABAAQABQD8/wgABgACAAUA//8CAAQAAAADAP7/AQAIAPr/AwD+//v/AgACAPv/AQAJAPf/AQD+//n/AgAAAP///f8CAAAA/P8DAPf/+/8HAPv///////7//f8AAP3//P8BAPr/AAD6//z/AAD5/wIAAAD7/wIAAgD4/wAA/v/6/wQAAQAAAAAABwAAAAAAAwD+/wIABQADAAEABAADAAAAAwABAAAABAAGAAEAAgAGAAAABQD///7/BQAAAAMAAQAAAAIABAD8/wMA/////wQA/f8AAAEAAQD//wMA+/8FAPr///////P/CwDz/wQA/P/6/wIA8/8AAPH//P/+//f//v/+//n//P/8//L//v/7//r////7//3//v/5//v/AAD5/wMAAAD8/wYA/P///////////wAABwD6/wwAAQD+/wUA/P8GAP7/AwD+/wUABwD+/wUA//8FAAMAAgD//wUAAAAEAAcA+/8KAAUAAgAEAAAA//8HAAAA+v8EAAIAAQAEAPz/+/8EAP7//////wAAAQABAP//+///////AAD9/wEA/f8DAAAA+P8AAPj/AQD+//v/AQD8/wIA/v/6/wAA/////wIA/P///wMA+/8BAPj///8CAAAAAwD+/wcAAAAFAP3/AAAFAP7/AwABAP7/AAACAPv/AwD+/wMABAD9/wUA/v8AAAEA/v8AAAIAAQAAAAAAAAD8/wIA/v/8/wMAAAD///3/AAD5//7/AwD2/wgAAAD8/wQA+f8CAP7/AAAAAP//AwD//wAA/v//////AAAAAAAAAwD9/wIA+//9/wQA+/8DAAAA//8AAP///f/+/wAAAAABAP//BgD8/wIAAgD3/wwA//8AAAkA/f8EAAAA//8BAAAAAwADAP//BQD///z/AwD8/wEAAwAAAAEAAQAAAAAA/v8BAAAAAQAGAP7/AAD///3//v8AAAAAAgAGAP//AgAAAP3////9/wAAAQACAAAAAQD///3/AQD9/wAABwD//wMABAD8/wEAAwD+/wIABQD+/wEAAwD6////BAD5/wYAAAD+/wgA+f8CAP3///8DAP7/AgAAAAAA/v8EAPv/AAAHAPv/CQACAP7/BgD//wIAAAADAAAAAgAEAPz/BQD8/wQA/P/+/wMA+f8EAP3//P8AAP////8AAPv/AgD8/wAAAgDz/wgA/f/7/wQA+P8BAAAA+v8BAP7/AAACAP3/AAACAP7/AQAEAPj/BwAAAAAABgD7/wcA+v8CAP7/+/8IAP3/AQACAAAAAQACAAAA/v8CAAcA/f8HAAIA/v8IAPv/AQAAAAAABAD+/wQAAgD//wMA/P8AAAAA//8BAPr/BQD9/wAA///4/wIA+/8AAP3//v8EAPr/AAD9//r/BQD3/wEA+v/6/wYA8/8BAPz/9v8CAPn/+f/+//3//v/+//3//v///wAA/P/7/wAAAAD//wAA//8AAP7/AgD4////BAD7/wYAAAADAAAAAwAAAPr/BQD6/wUABAD//wcA/f8EAPz//P8FAPv/BQADAP//BgAAAP//AQAAAAAAAgAAAAEAAwAAAAAA//8AAAAAAAAAAP7/BAD///7/AgD8/wAAAQD8////AQD9/wQA/v///wIA//8BAPv/AQD9/wMA/v/8/wQA+f8DAP3/9/8EAAAA/P8GAPz/AwAEAP//AAD8/wQAAAAGAAAAAgAFAAEABAD8/wEAAQADAAAAAgADAAAACAD7//7/BQD9/wUAAAD+/wcAAAABAAAA/f8DAAMAAAAAAAEA/v8AAAAA+f8AAAEA//8AAP//AAD7/wUA9//6/wQA9/8BAP3//f8AAP7/AAD7//3/AwD+//v//v/7//3/BAD3//r/CAD8/wIA/v/6//3/AQD+//j/AAAAAAgAAAD///v//f8BAP7//v/9/wEA//8EAAAA//8BAAEAAAD+/////f8AAP7/AgD9/wQABQD4/wIA/f/5/wAA/P/9/wEAAQABAPf/AAD8//7/AQD5/wAA/f8BAPf/+f/7//b/AwADAPz///8BAPn//P/4/////P8AAAAA+P8JAAAA/f/8//3/AAABAAAA8v8EAAMAAAACAPn/BAAOAAcA+//t////DwAEAAIA/f8HAAYADAD7//f/BgD//wwA//8BAAEADwAEAPv/BQD1/xEA//8DAP//9/8LAPn/AgABAAMABAAEAPz//f8FAPb/BAD+//r/CwAAAP7/BQD4////BAD4/wEA/v8AAAAA/v/+//z/BAABAP//BQD+//z/AQD7/wAA/P8DAAUAAAAHAP//AQD9////AAABAP//AAACAAQACAD+/wYA//8DAAcA/v8CAAEABQADAAIAAgACAAUABQAAAAAAAgAAAAAAAwABAAAABQAAAAYABgAAAAEA/v8EAAAA/P8GAP7/BAAGAPn/AQABAAIA/f8EAPn///8GAPT/BgD5/wAAAgD8/wQA+v8BAPv/9/8AAP3/AAAAAPz/AQD9//r////6////AAD9/////f8AAPz/AAD9//z/CQD5/wUA+v/7/wIA9P8FAPn/AQADAP3//v8AAP3/+/8DAPr/AQABAAAA/f8BAAAA9/8EAP7///8GAPr///8BAPz/AQD9/wAABQD//wEAAAD9/wMAAQD7/wAAAwAAAAMAAAD+/wMAAQAAAP3/AAABAP//AgD+/wEAAQAHAPv///8EAPb/CQD//wAAAAAAAAUA9v8JAPf///8FAPr/AwD9/wIA+v8DAP//+P8GAP////8EAPv/+/8EAPv/AAD+//7/AQD//wEA+/8AAAAA/v8CAP///P8GAP////8DAPv/BgAAAAAAAgD//wEA//8CAP7/AgAAAP//BgAAAAEAAgAAAP//BwD6/wIABAD6/woA+/8EAAAAAAAEAPv/BgD//wIAAAABAAMAAAAHAPf/BwAAAPr/BwD4/wQAAAAAAAAAAAD//wAAAAD7/wcA/v8AAAAA///+/wIA///7/wkA+/8GAAAA/f8BAP//AgD9/wYA//8FAAQA//8CAPz/AQAFAPr/AwAAAP//BgD5//7//v8CAPv/AAD9//b/CAD4//z//v/6/wAAAAD+//z/AAABAP///v////3/BwABAAAAAAAAAAUA/f8DAPb/AAAIAPv/AwD9////BAAAAPf/+/8AAPv/AQD7//v/AAABAP7/+v8BAPn/AQACAPr/AQACAAEA/f8FAPv/AAAFAPj/AAABAPv//P/+//n//v////r//f/+//r/AAD0//b//f/0/wMA9//+//7//f8BAPP/BAD6////AwD8/wEAAAAKAPf/BwACAPz/DAD9//3/AwAGAPn/BwD+//3/DgD7/wAAAAD7/wYA/f/5/wQA+v8GAAgA+P8IAAMAAwAKAP//AQAFAAMABQAAAAUABQAGAAcA//8IAPr/AgAAAPX/BAD+//3/BAAAAPX/AwD5//T/AwD3//n/BgD6//z/BAD3/wAAAAD+/wAA//8AAAAA/v/+//7///8BAAAAAAABAP7//v/+//b/AAD8/wAAAAD+/wEA/f8CAPr/AQD7/wAABQD7/wcA//8FAAcAAgAIAAUABAAGAAcAAwAGAAQABAAKAAkABQAIAAUABwADAAMAAAACAAMAAgAFAP7/DAABAAgA/v/+/wYA/P8KAPj/BAAGAP7/CQAAAAEACAAEAAUA/P///wIA+/8DAP7///8FAP//AAD7/wAA/f/2/wIA9P/9/wMA8v///wAA+/8DAAEA/P8CAP7//v/5//r/CAAAAAkABgAAAAUABwD+//3/CwD//w4ABwAAAAkAAgAJAPz/BwALAAEAEgD//wAACgABAAcAAgAFAAcABwAGAAAABAAEAAoAAwAIAAsABQAMAAAAAQACAAkABwAGAA4ACwAKABMAAgAAAAoA/f8JAAAABAADAAsACQD9/wQA+/8AAPf//v/z//r/AQDv//3/8f/w//r/8f/0/+//9P/u/+j/8v/g/+7/8f/r//X/8f/x/+7/7v/m/+v/7v/3//D/8//4/+3//v/z//b/9//7//n/9f8BAPT//P8AAPj/AAADAAAAAAD+/wIA+f/8/wQA+v8DAAcA/v8AAA8AAAAFABEA//8NAA4ABwAHABAAEgAKABgAFAARABYAFAARABIAHAAXABIAIAAYABQAIgAUABEAIQAXABUAGgAVABMAFwAVAAgADQAJAAkABAAJAAkAAAAKAPz/+f/3//z/8P/2/wAA8/8CAP7/8//0//j/7v/t/Line truncated
}
}
]
},
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {
"url": "data:audio/wav;base64,UklGRgrVAABXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YebUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8AAAAAAAAAAAAA///p/9T/xv/G/8b/yf/K/8r/zP/M/83/zf/O/83/zf/P/8//z//O/87/z//P/8//z//P/8//z//P/8//z//P/8//z//Q/9D/0P/Q/9D/0P/R/9H/0f/R/9H/0f/R/9H/0f/R/9L/0v/S/9L/0v/S/9P/0//T/9P/0//T/9P/0//U/9T/1P/U/9T/1P/V/9T/1f/V/9X/1f/W/9b/1v/W/9b/1v/W/9b/1//X/9f/1//Y/9j/2P/Y/9j/2P/Z/9n/2f/Z/9n/2f/a/9r/2v/a/9v/2//b/9v/2//b/9v/3P/c/9z/3f/c/93/3f/d/93/3f/e/97/3v/e/9//3//f/9//4P/g/+D/4P/g/+D/4P/h/+H/4f/h/+H/4f/i/+L/4v/j/+P/4//j/+P/4//k/+P/5P/k/+T/5P/l/+X/5f/l/+X/5f/l/+X/5f/l/+X/5v/n/+f/5//o/+j/6P/o/+j/5v/n/+f/5//o/+j/6P/o/+n/6f/p/+j/6P/o/+j/6P/p/+n/6f/p/+n/6v/q/+r/6v/q/+r/6v/q/+r/6v/q/+r/6//r/+r/6v/r/+r/6v/p/+r/6v/r/+r/6v/r/+z/7P/s/+v/7P/r/+v/6//s/+v/6//r/+v/6//t/+z/7f/t/+3/7f/r/+z/6//s/+v/7P/s/+z/7P/s/+z/7P/t/+3/7f/s/+z/6//s/+r/6//q/+z/7P/t/+3/7v/u/+3/7f/t/+z/7P/s/+z/7P/t/+3/7f/t/+3/7f/t/+3/7f/t/+z/7P/s/+z/7f/t/+3/7f/u/+3/7f/t/+3/7f/t/+3/7f/u/+3/7f/t/+3/7f/t/+3/7f/t/+3/7P/r/+z/6//t/+z/7f/t/+7/7f/u/+7/7f/t/+z/7P/t/+z/7P/t/+3/7P/t/+7/7v/v/+7/7v/t/+z/7P/t/+3/7v/u/+7/7v/v/+//7v/u/+3/7P/u/+z/7P/s/+z/7f/u/+7/7v/v/+7/7v/s/+z/7P/t/+3/7f/u/+7/7f/u/+3/7v/t/+3/7f/t/+3/7P/t/+z/7f/u/+3/7//u/+7/7f/t/+3/7f/u/+//7v/v/+7/7v/t/+7/7f/v/+7/7v/t/+7/7f/t/+3/7v/u/+7/7v/v/+3/7v/t/+z/7P/r/+z/6//t/+r/7P/t/+7/7v/u/+7/7f/s/+z/6//s/+v/7P/t/+3/7//w//D/7v/w/+7/7v/s/+3/6//t/+v/7f/u/+//7//u/+//7v/u/+r/6//q/+v/6//s/+7/7v/w/+//7//v/+3/7f/s/+v/7P/s/+3/7f/u/+7/7v/u/+3/7P/r/+z/7f/s/+z/7P/t/+3/7P/t/+3/7v/s/+3/7f/t/+z/6//r/+z/7P/s/+z/7P/t/+z/7f/t/+z/7v/v/+7/7P/s/+z/6//u/+3/7v/u/+7/7f/u/+3/7f/t/+v/7f/r/+v/6//s/+3/7P/u/+7/7v/v/+7/7v/u/+z/7P/r/+3/6//t/+3/7v/t/+7/7v/v/+7/7v/t/+3/7P/t/+7/7v/v/+3/7v/t/+3/7f/s/+z/7f/s/+3/7f/t/+7/7v/u/+3/7//u/+7/7f/s/+z/7P/s/+v/7P/t/+3/7f/v/+7/7v/u/+3/7f/t/+3/7f/u/+7/7v/u/+7/7//u/+7/7v/t/+3/7f/t/+3/7f/u/+//7//v/+//7v/u/+7/7v/u/+7/7v/u/+7/7v/t/+7/7v/u/+7/7v/u/+7/7f/t/+3/7f/t/+7/7f/t/+3/7P/u/+3/7v/t/+7/7v/u/+7/7v/v/+7/7v/v/+7/7v/u/+7/7v/u/+3/7f/u/+3/7v/u/+7/7f/u/+3/7v/u/+7/7f/t/+7/7v/v/+7/7v/u/+3/7v/t/+3/7f/t/+3/7f/u/+7/7v/u/+3/7v/t/+3/7P/t/+3/7f/u/+3/7v/u/+//7v/u/+7/7v/u/+7/7v/u/+7/7f/u/+3/7f/u/+7/7f/u/+3/7f/t/+3/7f/t/+3/7f/s/+z/7f/t/+3/7f/t/+3/7P/t/+3/7f/t/+z/7P/s/+3/7P/t/+3/7f/u/+3/7f/t/+3/7f/t/+7/7P/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/s/+z/7f/t/+3/7f/t/+3/7f/t/+z/7P/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/t/+z/7P/s/+3/7f/t/+3/7f/t/+3/7P/s/+z/7P/t/+z/7P/s/+z/7P/s/+3/7P/t/+3/7f/t/+3/7f/t/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/t/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+v/6//s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7f/s/+z/7P/s/+z/7P/s/+v/6//r/+z/6//s/+z/7P/s/+z/7P/s/+z/7P/s/+z/6//s/+z/7P/s/+v/6//s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+3/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/s/+z/7f/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/t/+3/7f/s/+3/7P/t/+3/7P/s/+z/7P/t/+z/7f/t/+3/7f/t/+3/7f/u/+z/7f/t/+3/7f/t/+3/7f/u/+3/7f/t/+z/7P/s/+z/7P/t/+z/7v/t/+7/7f/t/+7/7f/v/+z/7f/r/+3/7P/t/+3/7f/u/+7/7v/v/+7/7v/u/+z/7f/s/+7/7f/u/+3/7f/v/+7/7//u/+7/7//t/+//7v/u/+z/7P/s/+7/7f/w/+3/7f/u/+//7f/u/+7/7//s/+//6//w/+3/7//u/+3/7v/s/+7/7P/u/+//7v/x/+z/7//t/+3/7f/u/+//7//u/+7/6//w/+3/8f/t/+//7v/v/+7/7v/u/+//7v/w/+3/7v/t/+//7//t/+7/7v/w/+3/7P/v/+3/7//t/+//7v/w/+7/7//y/+3/7v/v/+3/7//t/+7/7P/v/+3/9P/w/+3/6//r//D/7//w/+3/8v/t/+7/7f/v/+3/7f/v//D/7f/t//D/7v/u/+3/8P/v/+7/7v/w/+z/7//r//L/7v/x/+3/7v/u/+7/7//t/+//7f/v/+7/7f/v/+7/7f/u/+7/7f/v/+//8P/u/+3/7//u/+//7f/v/+z/7f/v/+3/7v/p//D/7f/x/+7/7v/s/+3/8P/v/+//7f/r/+7/7P/t/+3/7v/t/+3/7f/u/+7/7//t/+7/7f/u/+3/6//s/+z/7f/t/+3/7//s/+//7f/y/+z/7v/s/+3/7P/v/+3/6//r/+3/7P/u/+//7f/t/+7/7v/v/+z/7f/r/+//7f/w/+z/7f/v/+v/7f/s/+7/7v/v/+z/7P/u/+z/7//s/+3/7f/u/+7/7f/w/+z/7v/t/+3/7v/t/+3/7P/u/+7/7v/s/+z/7f/v/+3/7v/t/+3/7f/t/+7/7P/t/+3/7v/v/+z/7f/u/+3/7v/v/+z/7v/t/+//7f/u/Line truncated
}
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "我没办法获取实时的天气信息。不过呢,你可以试试几个方法来查看今天的天气。首先,你可以用手机自带的天气功能,比如苹果手机的天气应用,或者直接在系统设置里查看。其次,你也可以用一些专业的天气服务,像是国外的AccuWeather、Weather.com,或者国内的中国天气网、墨迹天气等等。再有就是,你还可以在谷歌或者百度里直接搜索你所在的城市加上天气这两个字。如果你能告诉我你所在的城市,我也可以帮你分析一下历史天气趋势,不过最新的数据还是需要你通过官方渠道去获取哦。"
},
{
"type": "audio_url",
"audio_url": {
"url": "data:audio/wav;base64,UklGRiQiKQBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YQAiKQAGAAcABwAIAAkACQAJAAoACwAMAAwADQAOAA4ADwAPABAAEAAQABAAEQAQABAAEQARABAAEQARABEAEAAQABEAEQAQABAAEAAPAA4ADwAOAA0ADQANAAwACwAJAAYABQAEAAIAAQAAAP7//f/7//r/+v/5//b/9v/0//X/9f/0//X/9P/0//L/9P/y//D/9P/z//T/8v/x//L/8v/x//H/8P/v/+//7v/u/+3/7P/t/+3/7P/t/+7/7v/u//D/8f/z//X/9f/3//n/+//7//3///8AAAEABAAGAAgACgAMAA4AEAASABMAFAATABQAFAAUABUAFQATABIAEgARABEAEAAOAAwACwAJAAcABgAEAAMAAgABAAEAAAAAAAAA//////7//f/8//v/+f/4//j/9//3//f/9v/2//b/9f/1//b/9f/1//b/9v/2//b/9f/1//X/9P/z//P/9P/z//P/8//0//T/9P/0//X/9v/2//f/+P/4//n/+v/7//z//f/+//7///8AAAEAAgACAAMABAAFAAYABwAJAAoADAANAA4ADwAPAA8AEAAQABEAEgASABMAFAAVABYAFwAXABYAFgAWABYAFgAVABQAEwATABMAEgASABIAEQARABAADwAOAA4ADQANAAsACgAIAAcABAACAAEA///9//v/+f/4//b/9f/z//H/8f/v/+3/7P/r/+n/5//m/+X/5f/k/+T/5P/l/+X/5//o/+r/7P/u//D/8v/1//f/+f/7//z//v8AAAEAAgAEAAQABQAHAAUABQAGAAQAAwACAAEAAgAAAAEAAQD+/wAA/v8AAP7//P////7//f/9/wAA/v/9//7//v///wAA//////////8AAAEAAwACAAIAAwADAAMAAgACAAIAAwACAAEAAAABAAEAAAD////////////////+/////v/9//3//P/8//z//f/9//3//v/+//7///8AAP////////////////////////7//v/9//3//f/8//r/+P/3//j/9//3//b/9f/2//b/9f/1//b/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//4//j/+P/5//n/+f/5//n/+f/5//n/+f/6//r/+//7//z//P/+////AAACAAMABQAGAAcACAAIAAkACQAJAAkACQAKAAoACQAJAAcABgAEAAMAAQD///3/+//6//n/+P/3//b/9v/1//b/9v/2//b/9v/2//b/9//4//n/+v/8//7//v///wEAAwAEAAUABwAJAAoACgALAA4ADAANAA0ADwAOAA0ADwAPAA8ADgARABEAEQARABEAEAAQABEAEQARABAAEAAQABEAEQAQABAADwAPAA4ADQAOAAwADQANAAwADQANAAsADAALAAsACgAJAAkACAAHAAcABQAFAAUAAgABAAEAAAD///7//P/6//n/9v/z//P/8v/w/+//7f/r/+v/6v/p/+r/6f/o/+j/6P/o/+j/6f/p/+n/6v/r/+z/7P/t/+7/7//w//H/8//1//b/+P/5//r//P/9////AAABAAIAAwAFAAYABwAIAAkACwALAAwADQAOAA4ADwAQABEAEgASABMAFAAVABUAFQAVABYAFQAVABUAFAATABIAEQAPAA4ADQAMAAsACgAJAAcABQADAAAA///8//r/+P/2//X/8//x/+//7v/t/+z/6//r/+r/6v/q/+v/6//s/+z/7f/t/+7/7//w//H/8v/z//T/9f/2//f/9//4//j/+f/6//r/+//7//z//P/9//7//v///wAAAAAAAAEAAQACAAMABAAGAAcACQAKAAwADQAPABAAEQASABIAEgATABUAFwAYABkAGgAbAB0AHgAfAB4AHgAdABwAGwAaABkAGAAWABQAEgAQAA0ACwAJAAcABQADAAEA/v/8//r/+P/2//T/8//x//D/7//t/+z/6//r/+v/6//r/+v/6//s/+3/7f/t/+7/7v/v/+//8P/w//D/8f/w//D/7//w//H/8f/w//D/8f/y//L/8v/y//P/8//1//f/+P/5//z//f/8/wAA//8DAAEAAAAEAAIAAQADAAMAAQAAAAEAAAABAAEA/////////v/9//7////9//z//P/9//3//P/8//z//f/+//7//v////////8AAAAAAQACAAIAAgACAAIAAgABAAEAAQABAAIAAQAAAAAAAAD///7//v/9//3/+//6//r/+v/6//r/+f/6//r/+v/5//n/+v/7//v/+v/6//v//f/+//7//v//////AAD///////////7//f/8//v/+//6//r/+f/5//n/+v/6//r/+v/6//z//P/9//3///8AAAEAAgADAAUABQAGAAYABwAIAAkACQAKAAsADAAMAAwADAAMAA0ADAAMAAsACwAKAAkACAAHAAUAAwACAAEAAAD+//3/+//6//j/9//1//T/8//y//L/8//z//T/9f/1//b/9v/2//f/+P/4//n/+f/5//r//P/9////AAACAAMABAAFAAYACAAJAAoACwANAA4ADQAOABEAEAARABEAFAATABMAFQAVABYAFQAXABYAFgAWABUAFAATABQAEwASABIAEQAQABAADwAPAA4ADQAMAAsACwAMAAoACwAKAAkACgAJAAgABwAFAAUAAwACAAEA///9//3/+v/5//j/9P/y//D/7v/s/+v/6P/m/+X/4v/h/+H/4f/g/+H/4P/g/+H/4v/j/+T/5f/m/+f/6P/q/+v/7f/u/+//8f/y//T/9f/3//j/+v/7//z//v///wEAAgADAAQABQAGAAgACQAKAAsADAAOAA8AEAASABMAFAAVABYAFgAXABgAGAAZABkAGgAaABoAGQAZABgAFgAVABMAEgAQAA8ADgAMAAoACQAHAAUABAACAAAA///9//v/+f/3//X/8//x//D/7//v/+7/7v/t/+3/7f/t/+7/7v/v//D/8P/x//L/8//0//X/9f/2//f/9//4//j/+f/5//r/+v/6//r/+v/6//r/+f/6//r/+v/7//v//P/8//3//v///wEAAgADAAUABgAIAAsADQAPABEAEwAUABYAFwAYABgAGQAZABkAGAAZABkAGQAZABgAFwAVABQAEgARAA4ACwAIAAUAAwABAP///v/8//v/+v/4//b/9f/0//P/8v/y//H/8P/w/+//7v/u/+3/7f/t/+3/7f/s/+z/7P/t/+3/7v/u/+7/7v/u/+//8P/v//D/8P/x//H/8f/y//P/9P/0//X/9f/1//f/+P/5//n/+//8//z//f/9//7//f///wAAAAAAAAEAAQD//wAA/f8AAPz/+//+//v/+v/8//z/+//7//z//f/+/////v///wEAAQAAAAIABAADAAIAAwADAAQAAwADAAMAAwADAAIAAgACAAEAAAAAAAAAAAD//////v/+//7//v/9//7//v/+//////////////////////8AAAAA////////AAAAAAAAAAAAAAEAAAD///7//////////v/9//3//////////v/+//7//v/+//7//v////////8AAAAAAQACAAQABQAGAAcACAAKAAoACgALAAwADAAMAAwADAAMAAwADAAMAAwACwAKAAkACQAJAAgACAAIAAgABwAHAAYABQAEAAMAAgAAAP///v/9//z/+v/5//f/9f/0//P/8v/x//D/7//u/+3/7f/s/+z/7P/s/+7/7//w//L/8//0//X/9v/3//j/+v/8//3///8BAAMABgAIAAoADAAOABAAEQARABIAEwAUABQAFQAVABYAFAAUABYAFAAUABQAFgAVABMAFQAVABUAFAAVABUAFQAVABQAFAATABQAFAAUABQAFAATABQAFAAUABMAEgASABAAEAAQAA4ADgAMAAkACAAGAAIAAAD8//r/9//0//L/7//t/+v/6f/n/+b/4//h/+D/3v/e/9//3v/e/97/3v/e/+H/4v/j/+b/5v/n/+n/6v/s/+7/7//w//L/8//0//b/9//4//n/+v/7//z//f/+////AAABAAIABAAFAAcACAAJAAoACwANAA4ADwARABIAEwAUABUAFgAXABcAFwAXABcAFgAWABUAFAATABIAEAAPAA0ACwAJAAYABAABAP///f/7//n/9//1//T/8v/w/+//7f/s/+r/6f/o/+f/5v/l/+T/5P/j/+T/5P/l/+X/5v/n/+j/6P/p/+r/6//s/+z/7f/u/+7/7v/v/+//7//v/+//8P/w//H/8f/y//P/9P/1//f/+P/5//v//P/+/wEAAwAFAAcACgALAA4AEAATABUAFwAZABsAHQAfACEAIwAkACUAJgAmACUAJQAkACMAIQAgAB0AGwAaABkAGAAWABQAEgAPAA4ADAALAAgABgAEAAIAAQAAAAAA///+//7//f/8//v/+f/5//j/+P/4//f/9//2//X/9f/0//T/9P/0//X/9f/1//X/9f/2//f/9//4//j/+f/6//v//P/8//3//v///wAAAAABAAIAAwAEAAUABQAGAAcACAAIAAcACAAJAAgACAAIAAgABwAIAAkACQAIAAoACgAIAAkABwAKAAcABgAKAAgABwAKAAoACQAJAAoACwALAAwACgAKAAsACQAIAAgACAAGAAQAAwACAAEA///+//3//P/7//r/+f/4//j/9//3//f/9//3//f/9//3//j/+P/4//j/+P/5//n/+f/5//n/+f/5//j/+f/5//n/+f/4//n/+f/6//r/+f/6//v/+//6//r/+//8//3//P/8//7/AAABAAIAAgADAAQABQAGAAcACAAJALine truncated
}
}
]
},
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {
"url": "data:audio/wav;base64,UklGRkqYAABXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAAZGF0YSaYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAP//AAAAAAAA///o/9T/xv/G/8b/yP/K/8r/y//M/8z/zf/O/83/zf/P/87/zv/O/87/zv/P/8//z//P/8//z//P/8//z//P/8//z//P/8//0P/Q/8//z//Q/9D/0f/R/9D/0f/R/9H/0f/R/9H/0f/R/9H/0f/S/9L/0v/S/9L/0v/S/9L/0v/T/9P/0//T/9P/0//T/9P/0//T/9P/0//T/9P/0//U/9T/1f/V/9T/1P/U/9X/1P/V/9X/1f/V/9b/1v/W/9b/1v/W/9b/1v/W/9f/1//X/9f/1//X/9f/1//X/9j/2P/Y/9j/2P/Y/9j/2P/Y/9j/2f/Z/9r/2v/a/9r/2v/b/9v/2//b/9v/2//c/9v/3P/c/9z/3P/d/9z/3P/c/93/3f/e/93/3f/e/97/3v/e/97/3//f/+D/4P/g/9//3//g/+D/4P/g/+H/4f/g/+H/4P/h/+H/4f/h/+L/4v/i/+H/4f/i/+L/4v/i/+L/4v/j/+L/4//j/+P/4//j/+P/4//j/+P/5P/k/+T/4//l/+T/5P/k/+T/5P/l/+T/5f/l/+X/5f/m/+X/5v/m/+b/5v/m/+b/5v/m/+b/5v/n/+b/5//n/+b/5v/o/+f/5//n/+j/5//p/+f/5//o/+j/6P/n/+j/6P/p/+n/6f/o/+j/6P/p/+j/6P/o/+n/6P/p/+j/6P/p/+n/6f/q/+n/6v/p/+n/6f/q/+n/6f/p/+n/6f/p/+n/6f/p/+n/6v/p/+n/6f/p/+n/6f/p/+n/6f/r/+r/6v/q/+r/6v/r/+n/6v/p/+r/6v/r/+n/6v/q/+r/6v/q/+r/6v/q/+r/6v/q/+r/6//q/+v/6//r/+r/6//r/+r/6v/q/+r/6v/q/+r/6v/q/+r/6//q/+v/6//s/+r/6//q/+r/6v/s/+n/6v/q/+v/6v/r/+v/6//s/+v/6//s/+v/7P/r/+r/6//s/+z/6//r/+v/7P/r/+z/6//r/+v/6//r/+r/6//r/+v/6//r/+v/6//s/+v/6//r/+z/7P/r/+v/6//s/+v/7P/r/+v/6//r/+v/6//s/+v/6//r/+z/7P/s/+z/7P/r/+z/6//s/+v/6//r/+v/6//s/+v/7P/r/+z/7P/s/+z/6//t/+v/7P/s/+v/7P/r/+z/6//s/+z/7P/r/+z/6//t/+v/7P/s/+v/7P/s/+z/6//s/+z/7P/s/+z/7P/s/+v/6//s/+z/7P/r/+z/7P/r/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/t/+z/7P/s/+z/7P/s/+z/7P/s/+v/6//s/+z/7P/s/+z/6//r/+z/7P/s/+v/7P/s/+v/7P/s/+v/6//s/+v/7P/s/+z/6//s/+v/7P/q/+v/6//s/+v/6//r/+z/6//r/+z/7P/r/+z/6//s/+z/7P/r/+z/7P/s/+v/6//t/+r/7P/r/+v/6//s/+v/7P/s/+v/7P/r/+z/6//s/+v/6//r/+z/6//r/+v/6//r/+v/6//t/+v/7P/r/+z/6//r/+z/7P/s/+v/6//r/+v/6//s/+v/7P/r/+z/7P/r/+v/6//s/+v/6//r/+z/6//s/+v/6//s/+v/7P/s/+z/7P/r/+v/6//s/+v/7P/r/+z/6//r/+v/6//s/+v/6//r/+z/7P/r/+z/6//s/+v/6//r/+v/6//s/+v/6//s/+r/6//r/+v/6//r/+v/6//r/+v/7P/r/+v/6//s/+v/6//r/+z/6//s/+v/6//q/+v/6//s/+v/6//s/+v/6//s/+v/6//r/+v/6//s/+v/6//r/+z/6//s/+v/6//s/+v/7P/q/+z/6//s/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/7P/r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//s/+r/6//r/+v/6//r/+r/6//r/+v/6//s/+v/6//q/+v/6v/s/+r/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+r/6//q/+v/6//r/+v/6//r/+v/6//r/+r/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/6//r/+z/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//s/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/6//s/+v/6//r/+z/6//r/+v/7P/r/+v/6//s/+v/6//q/+v/6//s/+r/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/6//s/+v/7P/r/+z/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//r/+v/6//r/+z/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6//s/+v/6//s/+v/7P/r/+v/6//r/+v/6//s/+v/6//r/+v/6//r/+v/6v/s/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+v/6//s/+z/7P/r/+v/7P/s/+v/6//s/+v/6//r/+v/6//r/+z/7P/s/+v/6//r/+z/6//s/+v/7P/r/+z/6//s/+v/6//s/+v/6//r/+z/6//s/+v/6//s/+v/7P/r/+v/6//s/+v/7P/r/+v/6//s/+v/7P/s/+v/7P/r/+z/7P/r/+z/6//s/+v/6//r/+z/6//s/+z/7P/s/+v/6//r/+v/6//r/+z/6//s/+v/6//r/+v/6//r/+v/6//s/+v/6//r/+v/6//s/+z/6//s/+v/6//s/+v/7P/s/+z/6//s/+v/6//s/+v/7P/s/+z/7P/s/+z/6//r/+v/6//s/+v/7P/s/+z/6//s/+v/6//r/+z/6//s/+v/6//r/+z/6//r/+v/6//s/+v/6//s/+v/6//r/+v/6//s/+v/7P/r/+z/7P/r/+v/7P/s/+z/7P/s/+v/7P/s/+z/6//s/+v/7P/s/+v/7P/s/+z/7P/s/+v/6//r/+v/6//s/+v/7P/r/+z/6//r/+z/6//s/+v/7P/r/+v/7P/r/+z/6//s/+v/6//s/+v/7P/r/+z/6//s/+z/7P/s/+v/7P/r/+z/7P/s/+v/6//r/+z/7P/s/+z/6//s/+v/6//s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/7P/s/+z/6//t/+v/7P/s/+z/7P/s/Line truncated
}
}
]
}
]
}
@@ -0,0 +1,714 @@
import base64
import io
import os
import random
import re
from collections.abc import Callable
import librosa
import numpy as np
import torch
import torchaudio
from process_speechdata import InputSegment, StreamingInputSegment
from torchaudio.transforms import MelSpectrogram
speech_zeroemb_idx = 151667
empty_token = "<|empty|>"
mimo_audio_tokenizer = None
device = "cpu"
asr_zh_templates = [
"请将这段语音转换为文字",
"帮我识别这个音频文件中的内容",
"把这段录音转成文本",
"请转录这段语音",
"将音频内容转换成文字格式",
"识别并转写这段语音",
"把语音内容写成文字",
"转录这个音频片段",
"将这段对话转换为文本",
"麻烦帮我把这段录音整理成详细的文字记录",
]
asr_en_templates = [
"Please transcribe this audio file",
"Convert this speech recording to text",
"Transcribe the following voice message",
"Turn this audio into readable text",
"Please convert the recording to written format",
"Transcribe what you hear in this audio",
"Convert this spoken content to text",
"Please write down what is said in this recording",
"Transcribe this voice recording",
"Could you please help me transcribe this important recording?",
"Would you mind converting this voice message into a readable text format?",
"I'd really appreciate it if you could turn this audio file into a written document",
]
tts_zh_templates = [
"请将这段文字转换为语音",
"帮我把这个文本读出来",
"将这些文字生成音频",
"请朗读这段内容",
"把这段话转换成语音文件",
"生成这段文字的语音版本",
"请用语音播报这些内容",
"将文本转换为可听的音频",
"帮我朗读这段文字",
"把这些内容念出来",
]
tts_en_templates = [
"Please convert this text to speech",
"Turn this writing into audio",
"Generate speech from this text",
"Read this content out loud",
"Convert these words to voice",
"Create an audio version of this text",
"Please vocalize this content",
"Turn this text into audible format",
"Help me convert this writing to speech",
"Make this text into spoken audio",
]
def detect_language(text):
if re.search(r"[\u4e00-\u9fff]", text):
return "zh"
else:
return "en"
# ============================================
# Common Helper Functions - InputSegment Creation
# ============================================
def create_segment(text: str = "", audio=None) -> InputSegment:
"""Create a standard InputSegment with default zeroemb parameters"""
return InputSegment(
text=text,
audio=audio,
speech_zeroemb_idx=speech_zeroemb_idx,
text_zeroemb_idx=empty_token,
)
def create_streaming_segment(
text: str, audio, tokenizer, group_size: int, audio_channels: int
) -> StreamingInputSegment:
"""Create a StreamingInputSegment"""
return StreamingInputSegment(
text=text,
audio=audio,
tokenizer=tokenizer,
group_size=group_size,
audio_channels=audio_channels,
speech_zeroemb_idx=speech_zeroemb_idx,
text_zeroemb_idx=empty_token,
)
# ============================================
# Common Helper Functions - Common Token Segment Creation
# ============================================
def create_user_start() -> InputSegment:
"""Create user role start token"""
return create_segment(text="<|im_start|>user\n")
def create_user_end() -> InputSegment:
"""Create role end token"""
return create_segment(text="<|im_end|>\n")
def create_assistant_start() -> InputSegment:
"""Create assistant role start token"""
return create_segment(text="<|im_start|>assistant\n")
def create_system_start() -> InputSegment:
"""Create system role start token"""
return create_segment(text="<|im_start|>system\n")
def create_thinking_segment(thinking: bool = False) -> InputSegment:
"""Create thinking token, closed or open based on thinking parameter"""
if thinking:
return create_segment(text="<think>\n")
else:
return create_segment(text="<think>\n\n</think>\n")
def create_sostm_segment() -> InputSegment:
"""Create streaming output start token"""
return create_segment(text="<|sostm|>")
def create_assistant_start_with_sostm() -> InputSegment:
"""Create assistant start token with sostm"""
return create_segment(text="<|im_start|>assistant\n<|sostm|>")
def create_assistant_start_with_think() -> InputSegment:
"""Create assistant start token with think opening"""
return create_segment(text="<|im_start|>assistant\n<think>\n")
# ============================================
# Common Helper Functions - Composite Segment Creation
# ============================================
def create_user_turn_with_audio(audio_tokenized, extra_text: str = None) -> list[InputSegment]:
"""Create a user turn containing audio"""
segments = [
create_user_start(),
create_segment(audio=audio_tokenized),
]
if extra_text:
segments.append(create_segment(text=extra_text))
segments.append(create_user_end())
return segments
def create_user_turn_with_text(text: str) -> list[InputSegment]:
"""Create a text-only user turn"""
return [
create_user_start(),
create_segment(text=text),
create_user_end(),
]
def create_system_turn_with_voice_prompt(prompt_text: str, audio_token) -> list[InputSegment]:
"""Create a system turn with voice prompt"""
return [
create_system_start(),
create_segment(text=prompt_text),
create_segment(text="", audio=audio_token),
create_user_end(),
]
def create_system_turn_text_only(system_text: str) -> list[InputSegment]:
"""Create a text-only system turn"""
return [
create_system_start(),
create_segment(text=system_text),
create_user_end(),
]
# ============================================
# Common Helper Functions - Multi-turn Dialogue Processing
# ============================================
def process_multiturn_messages(
message_list: list[dict],
user_processor: Callable[[dict], list[InputSegment]],
assistant_processor: Callable[[dict], list[InputSegment]],
) -> list[InputSegment]:
"""
Generic multi-turn dialogue message processing function
Args:
message_list: List of messages, each containing 'role' and 'content'
user_processor: Function to process user messages
assistant_processor: Function to process assistant messages
Returns:
Processed list of InputSegments
"""
lm_prompt = []
for message in message_list:
role = message["role"]
if role == "user":
lm_prompt.extend(user_processor(message))
elif role == "assistant":
lm_prompt.extend(assistant_processor(message))
else:
raise ValueError(f"Invalid role: {role}")
return lm_prompt
def create_text_user_message(message: dict) -> list[InputSegment]:
"""Process a text-only user message"""
return [
create_user_start(),
create_segment(text=message["content"]),
create_user_end(),
]
def create_text_assistant_message(message: dict) -> list[InputSegment]:
"""Process a text-only assistant message"""
return [
create_assistant_start(),
create_segment(text=message["content"]),
create_user_end(),
]
def create_audio_user_message(message: dict) -> list[InputSegment]:
"""Process an audio user message"""
return [
create_user_start(),
create_segment(audio=preprocess_input(message["content"])),
create_user_end(),
]
def append_assistant_ending(
lm_prompt: list[InputSegment], thinking: bool = False, use_sostm: bool = False
) -> list[InputSegment]:
"""
Append assistant ending to the prompt
Args:
lm_prompt: Existing prompt list
thinking: Whether to use open thinking token
use_sostm: Whether to use sostm token (for speech output)
"""
if use_sostm:
lm_prompt.append(create_assistant_start_with_sostm())
else:
lm_prompt.append(create_assistant_start())
lm_prompt.append(create_thinking_segment(thinking))
return lm_prompt
def get_asr_sft_prompt(
input: None | str = None,
):
"""Build prompt for ASR (Automatic Speech Recognition) task"""
audio_tokenized = preprocess_input(input)
template = random.choice(asr_zh_templates + asr_en_templates)
lm_prompt = create_user_turn_with_audio(audio_tokenized, extra_text=template)
lm_prompt = append_assistant_ending(lm_prompt, thinking=False)
return lm_prompt
def resample_audio_if_needed(wav_tensor: torch.Tensor, original_sr: int):
target_sr = 24000
if original_sr != target_sr:
wav_tensor = torchaudio.functional.resample(wav_tensor, original_sr, target_sr)
return wav_tensor
def wav2mel(wav, device="cpu"):
mel_transform = MelSpectrogram(
sample_rate=mimo_audio_tokenizer.config.sampling_rate,
n_fft=mimo_audio_tokenizer.config.nfft,
hop_length=mimo_audio_tokenizer.config.hop_length,
win_length=mimo_audio_tokenizer.config.window_size,
f_min=mimo_audio_tokenizer.config.fmin,
f_max=mimo_audio_tokenizer.config.fmax,
n_mels=mimo_audio_tokenizer.config.n_mels,
power=1.0,
center=True,
).to(device)
spec = mel_transform(wav[None, :])
return torch.log(torch.clip(spec, min=1e-7)).squeeze()
def group_by_length(features: torch.Tensor, lengths: torch.Tensor, max_length: int):
if features.size(0) != lengths.sum().item():
raise ValueError(f"Feature size mismatch: {features.size(0)} vs {lengths.sum().item()}")
split_points = []
current_sum = 0
for i, seq_len in enumerate(lengths):
if current_sum + seq_len > max_length and current_sum > 0:
split_points.append(i)
current_sum = seq_len.item()
else:
current_sum += seq_len.item()
# Convert split points to group sizes
group_sizes = []
prev = 0
for point in split_points:
group_sizes.append(point - prev)
prev = point
if prev < len(lengths):
group_sizes.append(len(lengths) - prev)
len_groups = torch.split(lengths, group_sizes)
feature_sizes = [group.sum().item() for group in len_groups]
feature_groups = torch.split(features, feature_sizes)
return feature_groups, len_groups
def encode_batch(input_features: torch.Tensor, input_lens: torch.Tensor, max_length: int = 256000):
feature_groups, len_groups = group_by_length(input_features, input_lens, max_length)
encoded_parts = []
for features, lengths in zip(feature_groups, len_groups):
with torch.no_grad():
codes, _ = mimo_audio_tokenizer.encoder.encode(
input_features=features.to(device), input_lens=lengths.to(device), return_codes_only=True
)
encoded_parts.append(codes)
return torch.cat(encoded_parts, dim=-1)
def preprocess_input(input: None | str | torch.Tensor = None, device="cpu", audio_channels=4, group_size=8):
if isinstance(input, torch.Tensor) or (isinstance(input, str) and os.path.isfile(input)):
return "<|sosp|><|empty|><|eosp|>"
else:
text = input
if (
text.isupper() or text.islower()
): # If the text only contains upper-case or lower-case letters, capitalize it.
text = text.capitalize()
return text
def _build_tts_system_prompt(has_voice_prompt: bool, voice_audio_token=None) -> list[InputSegment]:
"""Build system prompt for TTS task"""
if has_voice_prompt and voice_audio_token is not None:
return [
create_system_start(),
create_segment(
# text="You need to generate a speech with the same timbre as the speech prompt, based on the specified style instructions and text content. Your timbre should be: "
text="你需要根据指定的风格指令和文本内容来生成和语音prompt具有相同音色的语音。你的音色应该是:"
),
create_segment(text="", audio=voice_audio_token),
create_user_end(),
]
else:
return create_system_turn_text_only("你需要根据指定的风格指令和文本内容来生成语音。")
# return create_system_turn_text_only(
# "You need to generate speech based on the specified style instructions and text content."
# )
def _build_tts_system_prompt_no_instruct(has_voice_prompt: bool, voice_audio_token=None) -> list[InputSegment]:
"""Build system prompt for TTS task"""
if has_voice_prompt and voice_audio_token is not None:
return [
create_system_start(),
create_segment(
# text="You need to generate a speech with the same timbre as the speech prompt, based on the specified style instructions and text content. Your timbre should be:"
text="你需要根据指定的风格指令和文本内容来生成和语音prompt具有相同音色的语音。你的音色应该是:"
),
create_segment(text="", audio=voice_audio_token),
create_user_end(),
]
else:
return []
def get_tts_sft_prompt(
input: None | str = None,
instruct=None,
read_text_only=True,
prompt_speech=None,
):
"""
Build prompt for TTS (Text-to-Speech) task
Args:
input: Input text
instruct: Style instruction (e.g., "speak happily in a child's voice")
read_text_only: Whether to read only plain text (False means text contains template)
prompt_speech: Reference audio (for voice cloning)
"""
assistant_prompt_audio_token = preprocess_input(prompt_speech) if prompt_speech is not None else None
if not read_text_only:
# Not just reading text, text contains template (template:text format)
text = preprocess_input(input)
lm_prompt = _build_tts_system_prompt(
has_voice_prompt=assistant_prompt_audio_token is not None,
voice_audio_token=assistant_prompt_audio_token,
)
lm_prompt.append(create_segment(text=f"<|im_start|>user\n{text}<|im_end|>\n"))
lm_prompt.append(create_assistant_start_with_think())
else:
# Plain text (no instruction inside)
language = detect_language(input)
template = random.choice(tts_zh_templates if language == "zh" else tts_en_templates)
text = preprocess_input(input)
if instruct is None:
# No instruct instruction
lm_prompt = _build_tts_system_prompt_no_instruct(
has_voice_prompt=assistant_prompt_audio_token is not None,
voice_audio_token=assistant_prompt_audio_token,
)
lm_prompt.extend(
[
create_segment(text=f"<|im_start|>user\n{template}: {text}<|im_end|>\n"),
create_assistant_start_with_sostm(),
]
)
else:
# Has instruct instruction
lm_prompt = _build_tts_system_prompt(
has_voice_prompt=assistant_prompt_audio_token is not None,
voice_audio_token=assistant_prompt_audio_token,
)
lm_prompt.append(create_segment(text=f"<|im_start|>user\n{template}: {text}({instruct})<|im_end|>\n"))
lm_prompt.append(create_assistant_start_with_think())
return lm_prompt
def get_audio_understanding_sft_prompt(
input_speech,
input_text,
thinking=False,
use_sostm=False,
):
"""Build prompt for audio understanding task"""
audio_tokenized = preprocess_input(input_speech)
lm_prompt = create_user_turn_with_audio(audio_tokenized, extra_text=input_text)
lm_prompt = append_assistant_ending(lm_prompt, thinking=thinking, use_sostm=use_sostm)
return lm_prompt
def _build_voice_prompt_system(prompt_speech) -> list[InputSegment]:
"""Build system prompt with voice prompt"""
return create_system_turn_with_voice_prompt(
prompt_text="Your voice should be:", audio_token=preprocess_input(prompt_speech)
)
def get_spoken_dialogue_sft_prompt(
input_speech,
system_prompt=None,
prompt_speech=None,
add_history=False,
):
"""
Build prompt for spoken dialogue task
Args:
input_speech: Input speech
system_prompt: System prompt text
prompt_speech: Reference audio (for voice cloning)
add_history: Whether to add history (Note: history variable is undefined in original code)
"""
audio_tokenized = preprocess_input(input_speech)
lm_prompt = []
# Note: history variable is undefined in original code, this branch may never execute
# To use history feature, history should be passed as a parameter
if add_history:
# Simplified form of adding history
lm_prompt = create_user_turn_with_audio(audio_tokenized)
lm_prompt.append(create_assistant_start_with_sostm())
else:
# Add voice prompt (if available)
if prompt_speech:
lm_prompt.extend(_build_voice_prompt_system(prompt_speech))
# Add user turn
lm_prompt.append(create_user_start())
if system_prompt:
lm_prompt.append(create_segment(text=system_prompt))
lm_prompt.append(create_segment(audio=audio_tokenized))
lm_prompt.append(create_user_end())
lm_prompt.append(create_assistant_start_with_sostm())
return lm_prompt
def get_spoken_dialogue_sft_multiturn_prompt(
message_list,
system_prompt=None,
prompt_speech=None,
tokenizer=None,
group_size=8,
audio_channels=4,
):
"""
Build prompt for multi-turn spoken dialogue task
Args:
message_list: List of messages containing role and content
system_prompt: System prompt text
prompt_speech: Reference audio (for voice cloning)
tokenizer: Tokenizer
group_size: Group size
audio_channels: Number of audio channels
"""
lm_prompt = []
# Add voice prompt (if available)
if prompt_speech:
lm_prompt.extend(
create_system_turn_with_voice_prompt(
prompt_text="Your voice should be:", audio_token=preprocess_input(prompt_speech)
)
)
# Define message processors
def user_processor(msg):
segments = [create_user_start()]
if system_prompt:
segments.append(create_segment(text=system_prompt))
segments.append(create_segment(audio=preprocess_input(msg["content"])))
segments.append(create_user_end())
return segments
def assistant_processor(msg):
return [
create_assistant_start(),
create_streaming_segment(
text=msg["content"]["text"],
audio=preprocess_input(msg["content"]["audio"]),
tokenizer=tokenizer,
group_size=group_size,
audio_channels=audio_channels,
),
create_user_end(),
]
# Process message list
lm_prompt.extend(process_multiturn_messages(message_list, user_processor, assistant_processor))
lm_prompt.append(create_assistant_start_with_sostm())
return lm_prompt
def get_s2t_dialogue_sft_prompt(
input_speech,
thinking=False,
):
"""Build prompt for speech-to-text dialogue task"""
audio_tokenized = preprocess_input(input_speech)
lm_prompt = create_user_turn_with_audio(audio_tokenized)
lm_prompt = append_assistant_ending(lm_prompt, thinking=thinking)
return lm_prompt
def get_s2t_dialogue_sft_multiturn_prompt(message_list, thinking=False):
"""Build prompt for multi-turn speech-to-text dialogue task"""
lm_prompt = process_multiturn_messages(
message_list, user_processor=create_audio_user_message, assistant_processor=create_text_assistant_message
)
lm_prompt = append_assistant_ending(lm_prompt, thinking=thinking)
return lm_prompt
def get_text_dialogue_sft_prompt(
input_text,
thinking=False,
):
"""Build prompt for text-only dialogue task"""
lm_prompt = create_user_turn_with_text(input_text)
lm_prompt = append_assistant_ending(lm_prompt, thinking=thinking)
return lm_prompt
def get_text_dialogue_sft_multiturn_prompt(
message_list,
thinking=False,
):
"""Build prompt for multi-turn text-only dialogue task"""
lm_prompt = process_multiturn_messages(
message_list, user_processor=create_text_user_message, assistant_processor=create_text_assistant_message
)
lm_prompt = append_assistant_ending(lm_prompt, thinking=thinking)
return lm_prompt
def get_in_context_learning_s2s_prompt(
instruction,
prompt_examples,
audio,
tokenizer=None,
group_size=8,
audio_channels=4,
):
"""
Build prompt for In-Context Learning speech-to-speech task
Args:
instruction: Instruction text
prompt_examples: List of examples, each containing input_audio, output_transcription, output_audio
audio: Input audio to be processed
tokenizer: Tokenizer
group_size: Group size
audio_channels: Number of audio channels
"""
prompt = [create_segment(text=f"[Int]:{instruction}\n")]
# Add examples
for example in prompt_examples:
prompt.extend(
[
create_segment(audio=preprocess_input(example["input_audio"])),
create_segment(text="\n"),
create_streaming_segment(
text=example["output_transcription"],
audio=preprocess_input(example["output_audio"]),
tokenizer=tokenizer,
group_size=group_size,
audio_channels=audio_channels,
),
create_segment(text=" \n\n"),
]
)
# Add input audio to be processed
prompt.extend(
[
create_segment(audio=preprocess_input(audio)),
create_segment(text="\n"),
create_sostm_segment(),
]
)
return prompt
def get_audio_data(audio_url):
if audio_url.startswith("data:"):
header, b64_data = audio_url.split(",", 1)
audio_bytes = base64.b64decode(b64_data.strip())
audio_file = io.BytesIO(audio_bytes)
else:
# File path
audio_file = audio_url
audio_signal, sr = librosa.load(audio_file, sr=24000)
audio_data = (audio_signal.astype(np.float32), sr)
return audio_data
def to_prompt(input_segs):
out_put = []
for input_seg in input_segs:
if isinstance(input_seg, StreamingInputSegment) and input_seg.text:
out_put.append("<|sostm|>")
if input_seg.audio is not None and isinstance(input_seg.audio, str):
out_put.append(input_seg.text)
out_put.append("<|eot|>")
out_put.append("<|empty|>")
else:
out_put.append(input_seg.text)
out_put.append("<|eot|>")
out_put.append("<|eostm|>")
else:
out_put.append(input_seg.text)
if input_seg.audio is not None:
out_put.append(input_seg.audio)
prompt = "".join(out_put)
return prompt
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# Copyright 2025 Xiaomi Corporation.
# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
import torch
class InputSegment:
def __init__(
self,
text: str = "",
audio: torch.Tensor = None,
tokenized_text: torch.Tensor = None,
speech_zeroemb_idx: int | list[int] = 1024,
text_zeroemb_idx: int = 152067,
add_sosp_eosp=True,
) -> None:
has_text = text is not None
has_tokenized_text = tokenized_text is not None
assert has_text or has_tokenized_text, "Text or tokenized text must be provided"
self.audio = audio
self.text = text
self.tokenized_text = tokenized_text
self.speech_zeroemb_idx = speech_zeroemb_idx
self.text_zeroemb_idx = text_zeroemb_idx
self.add_sosp_eosp = add_sosp_eosp
@staticmethod
def insert_between(tensor, i, value=-1):
return torch.scatter(
torch.full(
(1, tensor.shape[1] + (tensor.shape[1] - 1) * i + i),
value,
dtype=tensor.dtype,
),
1,
torch.arange(0, tensor.shape[1], dtype=torch.int64)[None] * (i + 1),
tensor,
)
def to_input_id(
self,
tokenizer,
group_size: int,
audio_channels: int = 8,
) -> tuple[torch.Tensor, torch.Tensor]:
if (
self.audio is None
): ## If no audio is provided, tokenize the text with (group_size-1) tokens inserted between each, fill with -100; and set audio to empty indices
if self.tokenized_text is None:
tokenized_text = tokenizer(
self.text,
return_tensors="pt",
truncation=True,
max_length=999999,
padding=False,
add_special_tokens=False,
)["input_ids"].int()
else:
tokenized_text = self.tokenized_text.unsqueeze(0)
if group_size > 1:
tokenized_text = self.insert_between(tokenized_text, group_size - 1, value=-100)
if isinstance(self.speech_zeroemb_idx, list):
audio_part_input_id = torch.zeros((audio_channels, tokenized_text.shape[1]), dtype=torch.int)
for i, idx in enumerate(self.speech_zeroemb_idx):
audio_part_input_id[i, :] = idx
else:
audio_part_input_id = torch.full(
(audio_channels, tokenized_text.shape[1]), self.speech_zeroemb_idx, dtype=torch.int
)
else: # If audio is provided, add sosp/eosp markers and convert audio: fill with empty tokens based on audio length
sosp_token = tokenizer.convert_tokens_to_ids("<|sosp|>") if self.add_sosp_eosp else None
eosp_token = tokenizer.convert_tokens_to_ids("<|eosp|>") if self.add_sosp_eosp else None
audio_part = self.audio.reshape(-1, audio_channels).T # [audio_channels, seqlen]
assert audio_part.shape[1] % group_size == 0, (
f"Audio shape {audio_part.shape} is not divisible by group_size {group_size}"
)
text_len = audio_part.shape[1] // group_size
empty_token = self.text_zeroemb_idx
if empty_token is None:
empty_token = tokenizer.eod
tokenized_text = torch.full((1, text_len), empty_token, dtype=torch.int)
tokenized_text = (
torch.cat(
[
torch.tensor([[sosp_token]], dtype=torch.int),
tokenized_text,
torch.tensor([[eosp_token]], dtype=torch.int),
],
dim=1,
)
if self.add_sosp_eosp
else tokenized_text
)
tokenized_text = self.insert_between(tokenized_text, group_size - 1, value=-100)
if self.add_sosp_eosp:
if isinstance(self.speech_zeroemb_idx, list):
sosp_part = torch.zeros((audio_channels, group_size), dtype=torch.int)
eosp_part = torch.zeros((audio_channels, group_size), dtype=torch.int)
for i, idx in enumerate(self.speech_zeroemb_idx):
sosp_part[i, :] = idx
eosp_part[i, :] = idx
audio_part_input_id = torch.cat([sosp_part, audio_part, eosp_part], dim=1)
else:
audio_part_input_id = torch.cat(
[
torch.full((audio_channels, group_size), self.speech_zeroemb_idx, dtype=torch.int),
audio_part,
torch.full((audio_channels, group_size), self.speech_zeroemb_idx, dtype=torch.int),
],
dim=1,
)
else:
audio_part_input_id = audio_part
input_ids = torch.cat([tokenized_text, audio_part_input_id], dim=0) # [n_rvq + 1, seqlen]
return input_ids
class StreamingInputSegment:
def __init__(
self,
text: str = "",
audio: torch.Tensor = None,
tokenized_text: torch.Tensor = None,
speech_zeroemb_idx: int | list[int] = 1024,
text_zeroemb_idx: int = 152067,
text_segment_size: int = 5,
audio_segment_size: int = 5,
tokenizer=None,
group_size=None,
audio_channels=None,
) -> None:
has_text = text is not None
has_tokenized_text = tokenized_text is not None
assert has_text or has_tokenized_text, "Text or tokenized text must be provided"
self.audio = audio
self.text = text
self.tokenized_text = tokenized_text
self.speech_zeroemb_idx = speech_zeroemb_idx
self.text_zeroemb_idx = text_zeroemb_idx
self.text_segment_size = text_segment_size
self.audio_segment_size = audio_segment_size
self.tokenizer = tokenizer
self.group_size = group_size
self.audio_channels = audio_channels
def to_input_id(
self,
tokenizer,
group_size: int,
audio_channels: int = 8,
):
if self.tokenized_text is None:
tokenized_text = tokenizer(
self.text,
return_tensors="pt",
truncation=True,
max_length=999999,
padding=False,
add_special_tokens=False,
)["input_ids"].int() # [1, seqlen]
else:
tokenized_text = self.tokenized_text.unsqueeze(0)
tokenized_text = tokenized_text.squeeze(0)
text_segments = tokenized_text.split(self.text_segment_size, dim=0)
audio_segments = self.audio.split(self.audio_segment_size * group_size * audio_channels, dim=0)
tokenized_segments = []
tokenized_segments.append(
InputSegment(
text="<|sostm|>",
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
eot_tokens = tokenizer(
"<|eot|>",
return_tensors="pt",
truncation=True,
max_length=999999,
padding=False,
add_special_tokens=False,
)["input_ids"][0].to(text_segments[-1])
text_segments = text_segments[:-1] + (torch.cat([text_segments[-1], eot_tokens], dim=0),)
length = min(len(text_segments), len(audio_segments)) # The number of empty tokens here may vary
for i in range(length):
text_segment = text_segments[i]
audio_segment = audio_segments[i]
tokenized_segments.append(
InputSegment(
tokenized_text=text_segment,
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
tokenized_segments.append(
InputSegment(
audio=audio_segment,
add_sosp_eosp=False,
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
for j in range(length, len(text_segments)):
tokenized_segments.append(
InputSegment(
tokenized_text=text_segments[j],
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
for j in range(length, len(audio_segments)):
tokenized_segments.append(
InputSegment(
audio=audio_segments[j],
add_sosp_eosp=False,
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
tokenized_segments.append(
InputSegment(
text="<|eostm|>",
speech_zeroemb_idx=self.speech_zeroemb_idx,
text_zeroemb_idx=self.text_zeroemb_idx,
),
)
input_ids = [
seg.to_input_id(
self.tokenizer,
self.group_size,
self.audio_channels,
)
for seg in tokenized_segments
]
input_ids = torch.cat(input_ids, dim=1).type(torch.int64) # [n_rvq + 1, seqlen]
return input_ids
@@ -0,0 +1,58 @@
# Online serving Example of vLLM-Omni for MiMo-Audio
## 🛠️ Installation
Please refer to [README.md](../../../README.md)
## Run examples (MiMo-Audio)
### Launch the Server
```bash
export MIMO_AUDIO_TOKENIZER_PATH="XiaomiMiMo/MiMo-Audio-Tokenizer"
vllm-omni serve XiaomiMiMo/MiMo-Audio-7B-Instruct --omni \
--served-model-name "MiMo-Audio-7B-Instruct" \
--port 18091 --stage-configs-path ./vllm_omni/model_executor/stage_configs/mimo_audio.yaml \
--chat-template ./examples/online_serving/mimo_audio/chat_template.jinja
```
> ⚠️ **Important**
> **MiMo-Audio is not compatible with the default chat template.**
> The provided `chat_template.jinja` implements MiMo-specific role, audio token, and instruction formatting and **must be used for all inference**.
### Send Multi-modal Request
Get into the example folder
```bash
cd examples/online_serving/mimo_audio
```
#### Send request via python
```bash
# Audio dialogue task
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type multi_audios \
--message-json ../../offline_inference/mimo_audio/message_base64_wav.json
```
The Python client supports the following command-line arguments:
- `--query-type` (or `-q`): Query type (default: `multi_audios`)
- Options: `multi_audios`, `text`
- `--message-json` (or `-m`): Path to `base64` multi rounds audio messages json file
- Do not pass any value for "text" query type
- Supports local file paths (automatically encoded to base64) or HTTP/HTTPS URLs, only for "Are these two audio clips the same?" task
- Example: `---message-json ./examples/offline_inference/mimo_audio/message_base64_wav.json`
- `--prompt` (or `-p`): Custom text prompt/question, only for query type is "text"(TTS task)
- Attention! Do not pass any value for "multi_audios" query type
- Example: `--prompt "What are the main activities shown in this video?"`
For example, to use multi rounds audios with local files:
```bash
python openai_chat_completion_client_for_multimodal_generation.py \
--query-type multi_audios \
--message-json ../../offline_inference/mimo_audio/message_base64_wav.json
```
@@ -0,0 +1,86 @@
{%- if tools %}
{{- '<|im_start|>system\n' }}
{%- if messages[0]['role'] == 'system' %}
{{- messages[0]['content'] }}
{%- else %}
{{- 'You are a helpful assistant.' }}
{%- endif %}
{{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
{%- for tool in tools %}
{{- "\n" }}
{{- tool | tojson }}
{%- endfor %}
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
{%- else %}
{%- if messages[0]['role'] == 'system' %}
{%- set _m = '<|sosp|><|empty|><|eosp|>' -%}
{%- set _raw0 = messages[0]['content'] if messages[0]['content'] is string else '' -%}
{%- if _m in _raw0 %}
{%- set _t0 = (_raw0 | replace(_m ~ '\n', '') | replace(_m, '') | trim) -%}
{{- '<|im_start|>system\n' + (_t0 ~ _m if _t0 else _m) + '<|im_end|>\n' }}
{%- else %}
{{- '<|im_start|>system\n' + _raw0 + '<|im_end|>\n' }}
{%- endif %}
{%- else %}
{{- '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- for message in messages %}
{%- if message['role'] == 'assistant' %}
{{- '<|im_start|>assistant' }}
{%- set _sosp = '<|sosp|><|empty|><|eosp|>' -%}
{%- set _text = message['content'] if message['content'] is string else '' -%}
{%- if _sosp in _text %}
{%- set _clean = _text | replace(_sosp, '') -%}
{%- set _body = _clean[1:] if (_clean and _clean[0] == '\n') else _clean -%}
{{- '\n<|sostm|>' + _body + '<|eot|><|empty|><|eostm|>' }}
{%- else %}
{%- set _body = _text[1:] if (_text and _text[0] == '\n') else _text -%}
{{- '\n<|sostm|>' + _body + '<|eot|><|eostm|>' }}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- elif message['role'] == 'user' %}
{%- set _m = '<|sosp|><|empty|><|eosp|>' -%}
{%- set _raw = message['content'] if message['content'] is string else '' -%}
{%- if _m in _raw %}
{%- set _t = (_raw | replace(_m ~ '\n', '') | replace(_m, '') | trim) -%}
{{- '<|im_start|>user\n' + (_t ~ _m if _t else _m) + '<|im_end|>\n' }}
{%- else %}
{{- '<|im_start|>user\n' + _raw + '<|im_end|>\n' }}
{%- endif %}
{%- elif message['role'] == 'system' %}
{%- if not loop.first %}
{%- set _m = '<|sosp|><|empty|><|eosp|>' -%}
{%- set _raw = message['content'] if message['content'] is string else '' -%}
{%- if _m in _raw %}
{%- set _t = (_raw | replace(_m ~ '\n', '') | replace(_m, '') | trim) -%}
{{- '<|im_start|>system\n' + (_t ~ _m if _t else _m) + '<|im_end|>\n' }}
{%- else %}
{{- '<|im_start|>system\n' + _raw + '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- elif message['role'] == 'tool' %}
{%- if (loop.index0 == 0) or (messages[loop.index0 - 1]['role'] != 'tool') %}
{{- '<|im_start|>user' }}
{%- endif %}
{%- set _m = '<|sosp|><|empty|><|eosp|>' -%}
{%- set _raw = message['content'] if message['content'] is string else '' -%}
{%- if _m in _raw %}
{%- set _t = (_raw | replace(_m ~ '\n', '') | replace(_m, '') | trim) -%}
{{- '\n<tool_response>\n' + (_t ~ _m if _t else _m) + '\n</tool_response>' }}
{%- else %}
{{- '\n<tool_response>\n' + _raw + '\n</tool_response>' }}
{%- endif %}
{%- if loop.last or (messages[loop.index0 + 1]['role'] != 'tool') %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\n<|sostm|>' }}
{%- endif %}
@@ -0,0 +1,545 @@
import base64
import concurrent.futures
import datetime
import json
import os
import queue
import sys
import threading
import time
from typing import Any
import requests
from openai import OpenAI
from vllm.assets.audio import AudioAsset
from vllm.utils.argparse_utils import FlexibleArgumentParser
# Modify OpenAI's API key and API base to use vLLM's API server.
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:18091/v1"
# Modify OpenAI's API key and API base to use vLLM's API server.
client = OpenAI(
# defaults to os.environ.get("OPENAI_API_KEY")
api_key=openai_api_key,
base_url=openai_api_base,
)
SEED = 42
def encode_base64_content_from_url(content_url: str) -> str:
"""Encode a content retrieved from a remote url to base64 format."""
with requests.get(content_url) as response:
response.raise_for_status()
result = base64.b64encode(response.content).decode("utf-8")
return result
def encode_base64_content_from_file(file_path: str) -> str:
"""Encode a local file to base64 format."""
with open(file_path, "rb") as f:
content = f.read()
result = base64.b64encode(content).decode("utf-8")
return result
def get_video_url_from_path(video_path: str | None) -> str:
"""Convert a video path (local file or URL) to a video URL format for the API.
If video_path is None or empty, returns the default URL.
If video_path is a local file path, encodes it to base64 data URL.
If video_path is a URL, returns it as-is.
"""
if not video_path:
# Default video URL
return "https://huggingface.co/datasets/raushan-testing-hf/videos-test/resolve/main/sample_demo_1.mp4"
# Check if it's a URL (starts with http:// or https://)
if video_path.startswith(("http://", "https://")):
return video_path
# Otherwise, treat it as a local file path
if not os.path.exists(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
# Detect video MIME type from file extension
video_path_lower = video_path.lower()
if video_path_lower.endswith(".mp4"):
mime_type = "video/mp4"
elif video_path_lower.endswith(".webm"):
mime_type = "video/webm"
elif video_path_lower.endswith(".mov"):
mime_type = "video/quicktime"
elif video_path_lower.endswith(".avi"):
mime_type = "video/x-msvideo"
elif video_path_lower.endswith(".mkv"):
mime_type = "video/x-matroska"
else:
# Default to mp4 if extension is unknown
mime_type = "video/mp4"
video_base64 = encode_base64_content_from_file(video_path)
return f"data:{mime_type};base64,{video_base64}"
def get_image_url_from_path(image_path: str | None) -> str:
"""Convert an image path (local file or URL) to an image URL format for the API.
If image_path is None or empty, returns the default URL.
If image_path is a local file path, encodes it to base64 data URL.
If image_path is a URL, returns it as-is.
"""
if not image_path:
# Default image URL
return "https://vllm-public-assets.s3.us-west-2.amazonaws.com/vision_model_images/cherry_blossom.jpg"
# Check if it's a URL (starts with http:// or https://)
if image_path.startswith(("http://", "https://")):
return image_path
# Otherwise, treat it as a local file path
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image file not found: {image_path}")
# Detect image MIME type from file extension
image_path_lower = image_path.lower()
if image_path_lower.endswith((".jpg", ".jpeg")):
mime_type = "image/jpeg"
elif image_path_lower.endswith(".png"):
mime_type = "image/png"
elif image_path_lower.endswith(".gif"):
mime_type = "image/gif"
elif image_path_lower.endswith(".webp"):
mime_type = "image/webp"
else:
# Default to jpeg if extension is unknown
mime_type = "image/jpeg"
image_base64 = encode_base64_content_from_file(image_path)
return f"data:{mime_type};base64,{image_base64}"
def get_audio_url_from_path(audio_path: str | None) -> str:
"""Convert an audio path (local file or URL) to an audio URL format for the API.
If audio_path is None or empty, returns the default URL.
If audio_path is already a base64 data URL (starts with "data:"), returns it as-is.
If audio_path is a URL (starts with http:// or https://), returns it as-is.
If audio_path is a local file path, encodes it to base64 data URL.
"""
if not audio_path:
# Default audio URL
return AudioAsset("mary_had_lamb").url
# Check if it's already a base64 data URL
if audio_path.startswith("data:"):
return audio_path
# Check if it's a URL (starts with http:// or https://)
if audio_path.startswith(("http://", "https://")):
return audio_path
# Otherwise, treat it as a local file path
if not os.path.exists(audio_path):
raise FileNotFoundError(f"Audio file not found: {audio_path}")
# Detect audio MIME type from file extension
audio_path_lower = audio_path.lower()
if audio_path_lower.endswith((".mp3", ".mpeg")):
mime_type = "audio/mpeg"
elif audio_path_lower.endswith(".wav"):
mime_type = "audio/wav"
elif audio_path_lower.endswith(".ogg"):
mime_type = "audio/ogg"
elif audio_path_lower.endswith(".flac"):
mime_type = "audio/flac"
elif audio_path_lower.endswith(".m4a"):
mime_type = "audio/mp4"
else:
# Default to wav if extension is unknown
mime_type = "audio/wav"
audio_base64 = encode_base64_content_from_file(audio_path)
return f"data:{mime_type};base64,{audio_base64}"
def load_messages_from_json(message_json_path: str) -> dict[str, Any]:
"""Load messages from a JSON file."""
with open(message_json_path, encoding="utf-8") as f:
data = json.load(f)
return data
def process_audio_url_in_content(content: dict[str, Any]) -> dict[str, Any]:
"""Process audio_url in content, handling both file paths and base64 URLs."""
if content.get("type") == "audio_url":
audio_url = content.get("audio_url", {}).get("url")
if audio_url:
# Process the audio URL (handles base64, file paths, and URLs)
processed_url = get_audio_url_from_path(audio_url)
content = content.copy()
content["audio_url"] = {"url": processed_url}
return content
def get_system_prompt(message_json_path: str | None = None):
"""Get system prompt, optionally from message.json file."""
if message_json_path and os.path.exists(message_json_path):
data = load_messages_from_json(message_json_path)
messages = data.get("messages", [])
# Find the first system message
for msg in messages:
if msg.get("role") == "system":
# Process audio URLs in the content
processed_content = []
for content_item in msg.get("content", []):
processed_item = process_audio_url_in_content(content_item)
processed_content.append(processed_item)
return {
"role": "system",
"content": processed_content,
}
def get_text_query(custom_prompt: str | None = None):
question = f"Please convert this text to speech: {custom_prompt}"
prompt = {
"role": "user",
"content": [
{
"type": "text",
"text": f"{question}",
}
],
}
return prompt
def get_multi_audios_query(
audio_path: str | None = None,
custom_prompt: str | None = None,
message_json_path: str | None = None,
) -> list[dict[str, Any]] | dict[str, Any]:
"""Get multi-audios query, optionally from message.json file."""
if message_json_path and os.path.exists(message_json_path):
data = load_messages_from_json(message_json_path)
messages = data.get("messages", [])
# Skip the first system role and return all other messages
rebuild_prompt_messages = []
skipped_first_system = False
for msg in messages:
if msg.get("role") == "system" and not skipped_first_system:
skipped_first_system = True
continue
# Process all content items in the message
processed_content = []
for content_item in msg.get("content", []):
processed_item = process_audio_url_in_content(content_item)
processed_content.append(processed_item)
rebuild_prompt_messages.append(
{
"role": msg.get("role"),
"content": processed_content,
}
)
return rebuild_prompt_messages
# Original behavior when message_json_path is not provided
question = custom_prompt or "Are these two audio clips the same?"
audio_url = get_audio_url_from_path(audio_path)
prompt = {
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {"url": audio_url},
},
{
"type": "audio_url",
"audio_url": {"url": AudioAsset("winning_call").url},
},
{
"type": "text",
"text": f"{question}",
},
],
}
return prompt
query_map = {
"multi_audios": get_multi_audios_query,
"text": get_text_query,
}
def run_multimodal_generation(args) -> None:
model_name = "MiMo-Audio-7B-Instruct"
thinker_sampling_params = {
"temperature": 0.0, # Deterministic - no randomness
"top_p": 1.0, # Disable nucleus sampling
"top_k": -1, # Disable top-k sampling
"max_tokens": 3200,
"seed": SEED, # Fixed seed for sampling
"detokenize": True,
"repetition_penalty": 1.1,
}
code2wav_sampling_params = {
"temperature": 0.0, # Deterministic - no randomness
"top_p": 1.0, # Disable nucleus sampling
"top_k": -1, # Disable top-k sampling
"max_tokens": 4096 * 16,
"seed": SEED, # Fixed seed for sampling
"detokenize": True,
"repetition_penalty": 1.1,
}
sampling_params_list = [
thinker_sampling_params,
code2wav_sampling_params,
]
# Get paths and custom prompt from args
audio_path = getattr(args, "audio_path", None)
custom_prompt = getattr(args, "prompt", None)
message_json_path = getattr(args, "message_json", None)
output_audio_path = getattr(args, "output_audio_path", None)
# Get the query function and call it with appropriate parameters
query_func = query_map[args.query_type]
if args.query_type == "multi_audios":
prompt = query_func(audio_path=audio_path, custom_prompt=custom_prompt, message_json_path=message_json_path)
elif args.query_type == "text":
prompt = query_func(custom_prompt=custom_prompt)
else:
prompt = query_func()
extra_body = {
"sampling_params_list": sampling_params_list
# Optional, it has a default setting in stage_configs of the corresponding model.
}
# Build messages list (filter None so concurrent tasks get valid structure)
system = get_system_prompt(message_json_path=message_json_path)
if args.query_type == "multi_audios" and isinstance(prompt, list):
messages = ([system] if system else []) + prompt
elif args.query_type == "text":
messages = [prompt]
num_concurrent_requests = args.num_concurrent_requests
start_time = time.perf_counter()
def run_one_request(req_id: int):
"""Submit one request and return (req_id, response) to preserve request order."""
return req_id, client.chat.completions.create(
messages=messages,
model=model_name,
extra_body=extra_body,
stream=args.stream,
)
with concurrent.futures.ThreadPoolExecutor(max_workers=num_concurrent_requests) as executor:
futures = [executor.submit(run_one_request, req_id) for req_id in range(num_concurrent_requests)]
if not args.stream:
# Collect by req_id so chat_completions[i] = response for request i
results_by_req = [None] * num_concurrent_requests
for future in concurrent.futures.as_completed(futures):
req_id, completion = future.result()
results_by_req[req_id] = completion
chat_completions = results_by_req
assert len(chat_completions) == num_concurrent_requests
count = 0
for req_id, chat_completion in enumerate(chat_completions):
chat_completion_id = getattr(chat_completion, "id", "")
for choice in chat_completion.choices:
if choice.message.audio:
audio_data = base64.b64decode(choice.message.audio.data)
audio_file_path = f"{output_audio_path}/{args.query_type}/audio_{count}.wav"
os.makedirs(os.path.dirname(audio_file_path), exist_ok=True)
with open(audio_file_path, "wb") as f:
f.write(audio_data)
print(f"[req {req_id}_{chat_completion_id}] Audio saved to {audio_file_path}")
count += 1
elif choice.message.content:
print(
f"[req {req_id}_{chat_completion_id}] Chat completion output from text:",
choice.message.content,
)
else:
# Stream mode: process chunks from all requests in real-time,
# displaying them in the order they arrive.
chunk_queue = queue.Queue()
audio_counters = {}
chat_completion_id_by_req = {}
chat_completion_id_lock = threading.Lock()
last_text_req_id = None
def _stream_reader(req_id: int, stream):
"""Read one stream and relay every chunk to the shared queue."""
try:
for chunk in stream:
chunk_queue.put((req_id, time.perf_counter(), chunk))
chunk_queue.put((req_id, time.perf_counter(), None))
except Exception as e:
print(f"\n[Request {req_id}] stream error: {e}", file=sys.stderr, flush=True)
chunk_queue.put((req_id, time.time(), None))
# Kick off a reader thread per request
reader_threads = []
for req_id, future in enumerate(futures):
req_id_from_future, stream = future.result()
assert req_id == req_id_from_future, f"Request ID mismatch: {req_id} != {req_id_from_future}"
audio_counters[req_id] = 0
t = threading.Thread(target=_stream_reader, args=(req_id, stream), daemon=True)
t.start()
reader_threads.append(t)
# Main loop – consume chunks in arrival order
active_streams = set(range(num_concurrent_requests))
while active_streams:
try:
request_id, ts, chunk = chunk_queue.get(timeout=2.0)
except queue.Empty:
if all(not t.is_alive() for t in reader_threads):
break
continue
if chunk is None:
elapsed = ts - start_time
print(f" ({elapsed:.2f}s)", flush=True)
with chat_completion_id_lock:
chat_completion_id = chat_completion_id_by_req.get(request_id, "")
print(
f" [req {request_id}_{chat_completion_id}] Time finished for streaming: ",
datetime.datetime.now(),
file=sys.stderr,
flush=True,
)
active_streams.discard(request_id)
continue
with chat_completion_id_lock:
if request_id not in chat_completion_id_by_req:
chat_completion_id_by_req[request_id] = getattr(chunk, "id", "")
chat_completion_id = chat_completion_id_by_req[request_id]
modality = getattr(chunk, "modality", None)
elapsed = ts - start_time
for choice in chunk.choices:
content = getattr(choice.delta, "content", None) if hasattr(choice, "delta") else None
if modality == "audio" and content:
audio_data = base64.b64decode(content)
audio_dir = (
f"{output_audio_path}/{args.query_type}/{num_concurrent_requests}/{chat_completion_id}"
)
os.makedirs(audio_dir, exist_ok=True)
audio_file_path = f"{audio_dir}/audio_{audio_counters[request_id]}.wav"
with open(audio_file_path, "wb") as f:
f.write(audio_data)
print(
f"\n[{elapsed:7.2f}s][req {request_id}_{chat_completion_id}] Audio saved to {audio_file_path}",
file=sys.stderr,
flush=True,
)
audio_counters[request_id] += 1
elif modality == "text" and content:
if last_text_req_id != request_id:
if last_text_req_id is not None:
print(flush=True)
print(
f"\n[{elapsed:7.2f}s][req {request_id}_{chat_completion_id}] content:",
end="",
flush=True,
)
last_text_req_id = request_id
print(
f"\n[{elapsed:7.2f}s][req {request_id}_{chat_completion_id}] {content}", end="", flush=True
)
# Final newline if the last output was streaming text
if last_text_req_id is not None:
print(flush=True)
for t in reader_threads:
t.join(timeout=1.0)
elapsed = time.perf_counter() - start_time
print(
f"num_concurrent_requests_{num_concurrent_requests}>>>>>>>>>Time finished for streaming<<<<<<<<: ",
elapsed,
file=sys.stderr,
flush=True,
)
timing_audio_folder = f"{output_audio_path}/{args.query_type}/{num_concurrent_requests}"
os.makedirs(timing_audio_folder, exist_ok=True)
timing_file = os.path.join(timing_audio_folder, "streaming_finish_time.txt")
with open(timing_file, "w") as f:
f.write(f"num_concurrent_requests_{num_concurrent_requests} elapsed_seconds: {elapsed}\n")
def parse_args():
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument(
"--query-type",
"-q",
type=str,
default="multi_audios",
choices=query_map.keys(),
help="Query type.",
)
parser.add_argument(
"--prompt",
"-p",
type=str,
default="Who are you?",
help="Custom text prompt/question to use instead of the default prompt for the selected query type.",
)
parser.add_argument(
"--message-json",
"-m",
type=str,
# default="../../offline_inference/mimo_audio/message_base64_wav.json",
default="../../offline_inference/mimo_audio/message_base64_wav_tts.json",
help="Path to message.json file containing conversation history. When provided, "
"system prompt and multi_audios query will be loaded from this file.",
)
parser.add_argument(
"--output-audio-path",
"-o",
type=str,
default="./",
help="Path to save the generated audio files.",
)
parser.add_argument(
"--stream",
action="store_true",
help="Stream the response.",
)
parser.add_argument(
"--num-concurrent-requests",
type=int,
default=1,
help="Number of concurrent requests to send. Default is 1.",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
run_multimodal_generation(args)
@@ -29,6 +29,15 @@ class DummyReqState:
pass
class MiMoAudioForConditionalGeneration(torch.nn.Module):
"""Dummy model whose class name must exactly match the production check."""
def __init__(self):
super().__init__()
# No real forward needed for these tests.
class DummyTalkerMTP(torch.nn.Module):
"""A fake talker_mtp module for deterministic CPU testing."""
@@ -82,6 +91,30 @@ def _make_runner(req_ids=("r1", "r2"), hidden_size=4):
return runner
def _make_runner_for_mimo(req_id="r_mimo"):
"""Create a minimal runner with MiMoAudio-like model and request state."""
runner = object.__new__(OmniGPUModelRunner)
runner.model = MiMoAudioForConditionalGeneration()
# Minimal vllm_config / model_config used by helper.
class _DummyModelConfig:
async_chunk = False
class _DummyVllmConfig:
model_config = _DummyModelConfig()
runner.vllm_config = _DummyVllmConfig()
# Attach a single request state with mm_features and additional_information_cpu.
req_state = DummyReqState()
req_state.mm_features = ["mm_feature_obj"]
req_state.additional_information_cpu = {"some_key": "some_value"}
runner.requests = {req_id: req_state}
return runner
def test_talker_mtp_forward_cpu_updates_inputs_and_info(monkeypatch):
# Patch the module-level `set_forward_context` symbol used inside
# OmniGPUModelRunner._talker_mtp_forward.
@@ -132,3 +165,32 @@ def test_talker_mtp_forward_cpu_empty_batch_noop(monkeypatch):
# Ensure no changes were made
assert torch.allclose(inputs_embeds, before)
def test_maybe_attach_mimo_audio_req_infos_enriches_dict():
runner = _make_runner_for_mimo()
req_id = "r_mimo"
req_state = runner.requests[req_id]
# Existing req_infos should be copied and enriched, not mutated in place.
original_req_infos = {"existing": 1}
enriched = OmniGPUModelRunner._maybe_attach_mimo_audio_req_infos(runner, req_state, original_req_infos, req_id)
assert enriched is not original_req_infos
assert enriched["existing"] == 1
# mm_features should be filled from req_state when missing
assert enriched["mm_features"] == req_state.mm_features
# req_id should always be attached
assert enriched["req_id"] == req_id
def test_maybe_attach_mimo_audio_req_infos_no_req_state_returns_input():
runner = _make_runner_for_mimo()
req_id = "missing"
req_state = None
req_infos = {"k": "v"}
result = OmniGPUModelRunner._maybe_attach_mimo_audio_req_infos(runner, req_state, req_infos, req_id)
# When no req_state, helper should be a no-op.
assert result is req_infos
Whitespace-only changes.
@@ -0,0 +1,206 @@
# Copyright 2025 Xiaomi Corporation.
import copy
from dataclasses import dataclass
from transformers import PretrainedConfig, Qwen2Config
SPAN_CODEC_START_TOKEN_ID = 151670
SPAN_CODEC_END_TOKEN_ID = 151672
TALKER_CODEC_PAD_TOKEN_ID = 151667
TEXT_GROUP_SIZE = 5
PAD_GROUP_SIZE = 5
NO_INTERLEAVE_NEXT_TOKEN_ID = 151671
@dataclass
class MiMoAudioConfig(Qwen2Config):
def __init__(
self,
*,
speech_vocab_size: str | int = "1025-1025-129-129-129-129-129-129",
speech_zeroemb_idx: str | int = "1024-1024-128-128-128-128-128-128",
delay_pattern: str = "0-1-2-3-4-5-6-7",
head_dim: int = 128,
group_size: int = 4,
audio_channels: int = 8,
local_dim: int = 1024,
local_layers: int = 16,
local_attn_heads: int = 64,
local_ffn_dim: int = 4096,
local_attn_dropout: float = 0.1,
input_local_layers: int = 6,
input_local_dim: int | None = None,
input_full_attention: bool | None = None,
**kwargs,
):
super().__init__(
**kwargs,
)
self.speech_vocab_size = speech_vocab_size
self.speech_zeroemb_idx = speech_zeroemb_idx
self.delay_pattern = delay_pattern
self.head_dim = head_dim
self.group_size = group_size
self.audio_channels = audio_channels
self.local_dim = local_dim
self.local_layers = local_layers
self.local_attn_heads = local_attn_heads
self.local_ffn_dim = local_ffn_dim
self.local_attn_dropout = local_attn_dropout
self.input_local_layers = input_local_layers
self.input_local_dim = input_local_dim or local_dim
self.input_full_attention = input_full_attention
def _parse_maybe_list(self, value: str | int, length: int) -> list[int]:
if isinstance(value, str) and "-" in value:
return [int(s) for s in value.split("-")]
return [int(value)] * length
def parsed_speech_empty_ids(self):
return self._parse_maybe_list(self.speech_zeroemb_idx, self.audio_channels)
def parsed_speech_vocab_sizes(self):
return self._parse_maybe_list(self.speech_vocab_size, self.audio_channels)
def parsed_delay_pattern(self):
return self._parse_maybe_list(self.delay_pattern, self.audio_channels)
def local_config(self):
config = copy.deepcopy(self)
config.hidden_size = self.local_dim
config.num_hidden_layers = self.local_layers
config.num_attention_heads = self.local_attn_heads
config.num_key_value_heads = self.local_attn_heads
config.head_dim = config.hidden_size // self.local_attn_heads
config.intermediate_size = self.local_ffn_dim
config.attention_dropout = self.local_attn_dropout
return config
def input_local_config(self):
config = copy.deepcopy(self)
config.hidden_size = self.input_local_dim
config.num_hidden_layers = self.input_local_layers
config.num_attention_heads = self.local_attn_heads
config.num_key_value_heads = self.local_attn_heads
config.head_dim = config.hidden_size // self.local_attn_heads
config.intermediate_size = config.hidden_size * 4
config.attention_dropout = self.local_attn_dropout
return config
def __repr__(self):
return (
f"{self.__class__.__name__}("
f"speech_vocab_size={self.speech_vocab_size!r}, "
f"speech_zeroemb_idx={self.speech_zeroemb_idx!r}, "
f"delay_pattern={self.delay_pattern!r}, "
f"head_dim={self.head_dim}, "
f"group_size={self.group_size}, "
f"audio_channels={self.audio_channels}, "
f"local_dim={self.local_dim}, "
f"local_layers={self.local_layers}, "
f"local_attn_heads={self.local_attn_heads}, "
f"local_ffn_dim={self.local_ffn_dim}, "
f"local_attn_dropout={self.local_attn_dropout}, "
f"input_local_layers={self.input_local_layers}, "
f"input_local_dim={self.input_local_dim}, "
f"input_full_attention={self.input_full_attention})"
)
class MiMoAudioTokenizerConfig(PretrainedConfig):
model_type = "mimo_audio_tokenizer"
def __init__(
self,
max_audio_seconds: int = 1800,
stride_size: int = 2,
avg_pooler: int = 1,
d_model: int = 768,
scale_embedding: bool = True,
kernel_size: int = 3,
activation_function: str = "gelu",
encoder_layers: int = 8,
encoder_skip_layer_id: int = None,
encoder_attention_heads: int = 12,
encoder_ffn_dim: int = 3072,
encoder_causal: bool = False,
encoder_attn_window_size: list[int] = None,
decoder_layers: int = 8,
decoder_attention_heads: int = 12,
decoder_ffn_dim: int = 3072,
decoder_kernel_size: int = 3,
decoder_stride_size: int = 2,
decoder_causal: bool = True,
decoder_attn_window_size: list[int] = None,
nfft: int = 1024,
vocoder_dim: int = 512,
vocoder_intermediate_dim: int = 4096,
vocoder_num_layers: int = 30,
n_mels: int = 80,
sampling_rate: int = 24000,
hop_length: int = 240,
window_size: int = 1024,
vocoder_padding: str = "same",
fmin: int = 0,
fmax: int = None,
num_quantizers: int = 12,
codebook_size: list[int] = None,
threshold_ema_dead_code: int = 10,
position_embedding_type: str = "rope",
rope_theta: int = 10000,
rope_type: str = "default",
ln_type: str = "LayerNorm",
vocoder_attention_heads: int = 4,
vocoder_attn_window_size: list[int] = None,
**kwargs,
):
super().__init__(**kwargs)
self.max_audio_seconds = max_audio_seconds
self.stride_size = stride_size
self.avg_pooler = avg_pooler
self.d_model = d_model
self.scale_embedding = scale_embedding
self.kernel_size = kernel_size
self.activation_function = activation_function
self.encoder_layers = encoder_layers
self.encoder_skip_layer_id = encoder_skip_layer_id
self.encoder_attention_heads = encoder_attention_heads
self.encoder_ffn_dim = encoder_ffn_dim
self.encoder_causal = encoder_causal
self.encoder_attn_window_size = encoder_attn_window_size if encoder_attn_window_size is not None else [-1, -1]
self.decoder_layers = decoder_layers
self.decoder_attention_heads = decoder_attention_heads
self.decoder_ffn_dim = decoder_ffn_dim
self.decoder_kernel_size = decoder_kernel_size
self.decoder_stride_size = decoder_stride_size
self.decoder_causal = decoder_causal
self.decoder_attn_window_size = decoder_attn_window_size if decoder_attn_window_size is not None else [-1, -1]
self.nfft = nfft
self.vocoder_dim = vocoder_dim
self.vocoder_intermediate_dim = vocoder_intermediate_dim
self.vocoder_num_layers = vocoder_num_layers
self.n_mels = n_mels
self.sampling_rate = sampling_rate
self.hop_length = hop_length
self.window_size = window_size
self.vocoder_padding = vocoder_padding
self.fmin = fmin
self.fmax = fmax
self.num_quantizers = num_quantizers
self.codebook_size = codebook_size if codebook_size is not None else [1024]
self.threshold_ema_dead_code = threshold_ema_dead_code
self.position_embedding_type = position_embedding_type
self.rope_theta = rope_theta
self.rope_type = rope_type
self.ln_type = ln_type
self.vocoder_attention_heads = vocoder_attention_heads
self.vocoder_attn_window_size = vocoder_attn_window_size if vocoder_attn_window_size is not None else [40, 10]
@@ -0,0 +1,914 @@
# Copyright 2025 Xiaomi Corporation.
import os
from collections.abc import Iterable, Mapping, Sequence
from functools import cached_property
from typing import Any
import numpy as np
import torch
from torch import nn
from transformers import BatchFeature, Qwen2Config
from vllm.config import VllmConfig
from vllm.config.multimodal import BaseDummyOptions
from vllm.logger import init_logger
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.models import SupportsPP
from vllm.model_executor.models.interfaces import SupportsMultiModal
from vllm.model_executor.models.utils import init_vllm_registered_model
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import (
AudioItem,
ModalityData,
MultiModalBatchedField,
MultiModalDataDict,
MultiModalFieldConfig,
MultiModalFieldElem,
MultiModalKwargsItem,
)
from vllm.multimodal.parse import AudioProcessorItems, MultiModalDataItems, MultiModalDataParser
from vllm.multimodal.processing import (
BaseDummyInputsBuilder,
BaseMultiModalProcessor,
BaseProcessingInfo,
ProcessorInputs,
PromptReplacement,
PromptUpdate,
PromptUpdateDetails,
)
from vllm.multimodal.utils import group_mm_kwargs_by_modality
from vllm.sequence import IntermediateTensors
from vllm.utils.cache import LRUCache
from vllm.utils.collection_utils import is_list_of
from vllm.utils.platform_utils import is_pin_memory_available
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import (
NO_INTERLEAVE_NEXT_TOKEN_ID,
PAD_GROUP_SIZE,
SPAN_CODEC_END_TOKEN_ID,
SPAN_CODEC_START_TOKEN_ID,
TALKER_CODEC_PAD_TOKEN_ID,
TEXT_GROUP_SIZE,
MiMoAudioConfig,
)
from vllm_omni.model_executor.models.mimo_audio.mimo_audio_code2wav import get_tokenizer_worker
from vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni import OmniOutput
logger = init_logger(__name__)
def interleave_5_and_5_in_span(
input_ids: list[int],
*,
span_start_token: int = SPAN_CODEC_START_TOKEN_ID,
span_end_token: int = SPAN_CODEC_END_TOKEN_ID,
pad_token_id: int = TALKER_CODEC_PAD_TOKEN_ID,
text_group_size: int = TEXT_GROUP_SIZE,
pad_group_size: int = PAD_GROUP_SIZE,
no_interleave_next_token: int = NO_INTERLEAVE_NEXT_TOKEN_ID,
) -> list[int]:
"""
Interleave text tokens and padding tokens within spans marked by special tokens.
Finds spans delimited by span_start_token and span_end_token, then reorganizes
tokens inside each span by grouping text tokens (excluding padding) into groups
of text_group_size, followed by pad_group_size padding tokens. Interleaving is
skipped if the token after span_end_token is no_interleave_next_token.
Args:
input_ids: Input token ID list
span_start_token: Token marking span start (default: 151670)
span_end_token: Token marking span end (default: 151672)
pad_token_id: Padding token ID (default: 151667)
text_group_size: Number of text tokens per group (default: 5)
pad_group_size: Number of padding tokens per group (default: 5)
no_interleave_next_token: Skip interleaving if this token follows span_end (default: 151671)
Returns:
Processed token list with same length as input
"""
if not input_ids:
return input_ids
original_len = len(input_ids)
output_ids: list[int] = []
cursor = 0
while cursor < original_len:
# Non-span starting point: Copy as is
if input_ids[cursor] != span_start_token:
output_ids.append(input_ids[cursor])
cursor += 1
continue
# ===== Enter span processing =====
output_ids.append(input_ids[cursor])
cursor += 1
span_end_pos = cursor
while span_end_pos < original_len and input_ids[span_end_pos] != span_end_token:
span_end_pos += 1
# If span_end_token is not found: Output the remaining as is and end
if span_end_pos >= original_len:
output_ids.extend(input_ids[cursor:])
break
next_pos = span_end_pos + 1
next_token = input_ids[next_pos] if next_pos < original_len else None
# Rule:
# After "-end" is 151667(PAD) -> for interlacing
# After "-end" is 151671 -> Do not interlace
do_interleave = next_token == pad_token_id
if next_token == no_interleave_next_token:
do_interleave = False
if not do_interleave:
output_ids.extend(input_ids[cursor:span_end_pos])
output_ids.append(span_end_token)
cursor = span_end_pos + 1
continue
span_content = input_ids[cursor:span_end_pos]
span_original_len = len(span_content)
span_text_tokens = [t for t in span_content if t != pad_token_id]
rebuilt_span: list[int] = []
text_cursor = 0
total_text = len(span_text_tokens)
while text_cursor < total_text:
take_text = min(text_group_size, total_text - text_cursor)
rebuilt_span.extend(span_text_tokens[text_cursor : text_cursor + take_text])
text_cursor += take_text
if text_cursor < total_text:
rebuilt_span.extend([pad_token_id] * pad_group_size)
output_ids.extend(rebuilt_span)
output_ids.append(span_end_token)
extra_tokens_added = len(rebuilt_span) - span_original_len
cursor = span_end_pos + 1
while extra_tokens_added > 0 and cursor < original_len:
if input_ids[cursor] == pad_token_id:
extra_tokens_added -= 1
cursor += 1
else:
output_ids.append(input_ids[cursor])
cursor += 1
if extra_tokens_added > 0:
return output_ids[:original_len]
if len(output_ids) > original_len:
output_ids = output_ids[:original_len]
elif len(output_ids) < original_len:
output_ids.extend([pad_token_id] * (original_len - len(output_ids)))
return output_ids
class MiMoAudioLLMProcessingInfo(
BaseProcessingInfo,
):
def get_hf_config(self):
return self.ctx.get_hf_config(Qwen2Config)
def get_hf_processor(
self,
*,
# Ignored in initialization
sampling_rate: int | None = None,
**kwargs: object,
):
# MiMoAudio doesn't use a standard HF processor like Qwen2Audio
# Instead it uses MiMoAudioTokenizer directly
# This method may return None or a dummy processor
# depending on the actual implementation requirements
return None
def get_data_parser(self) -> MultiModalDataParser:
sampling_rate = 24000
return MiMoAudioDataParser(target_sr=sampling_rate)
def get_feature_extractor(
self,
*,
# Ignored in initialization
sampling_rate: int | None = None,
):
# MiMoAudio uses MiMoAudioTokenizer instead of WhisperFeatureExtractor
# This method may need to return the tokenizer or None
# depending on the actual implementation requirements
return None
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
return {"audio": None}
def get_mm_max_tokens_per_item(
self,
seq_len: int,
mm_counts: Mapping[str, int],
) -> Mapping[str, int]:
# MiMoAudio has different tokenization than Qwen2Audio
# This is a placeholder - adjust based on actual MiMoAudio tokenizer limits
# For now, return a large value or None
# This should be adjusted based on MiMoAudio's actual constraints
return {"audio": 1}
class MiMoAudioLLMDummyInputsBuilder(BaseDummyInputsBuilder[MiMoAudioLLMProcessingInfo]):
_processor_inputs_cache: LRUCache = LRUCache(capacity=1024)
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
num_audios = mm_counts.get("audio", 0)
return (" <|empty|>" * num_audios).strip()
def get_dummy_mm_data(
self, seq_len: int, mm_counts: Mapping[str, int], mm_options: Mapping[str, BaseDummyOptions] | None = None
) -> MultiModalDataDict:
num_audios = mm_counts.get("audio", 0)
if num_audios == 0:
return {}
# Return dummy raw audio data (not encoded codes)
# This will be processed by _parse_audio_data like real audio
# Use 1 second of audio at target_sr (24000 Hz)
dummy_audio_length = mm_options.get("audio").length if mm_options else 24000 # 1 second at 24kHz
dummy_audio = np.zeros((dummy_audio_length,), dtype=np.float32)
return {"audio": [(dummy_audio, 24000)] * num_audios}
def get_dummy_processor_inputs(
self,
seq_len: int,
mm_counts: Mapping[str, int],
mm_options: Mapping[str, BaseDummyOptions] | None = None,
) -> ProcessorInputs:
dummy_text = self.get_dummy_text(mm_counts)
dummy_mm_data = self.get_dummy_mm_data(seq_len, mm_counts, mm_options)
dummy_mm_items = self.info.parse_mm_data(dummy_mm_data)
dummy_processor_inputs = ProcessorInputs(
prompt=dummy_text,
mm_items=dummy_mm_items,
)
return dummy_processor_inputs
class MiMoAudioDataParser(MultiModalDataParser):
def __init__(self, target_sr: int, use_mono_channel: bool = True):
super().__init__(target_sr=target_sr)
self.use_mono_channel = use_mono_channel
self.target_sr = target_sr
tokenizer_device = os.environ.get("MIMO_AUDIO_TOKENIZER_DEVICE", None)
if tokenizer_device:
if tokenizer_device.lower() == "cpu":
self.device = torch.device("cpu")
else:
# Support formats like "cuda", "cuda:0", "cuda:6", etc.
self.device = torch.device(tokenizer_device)
else:
# Default to cuda (will use current GPU)
self.device = torch.device(f"cuda:{torch.cuda.current_device()}" if torch.cuda.is_available() else "cpu")
self.audio_tokenizer_path = os.environ.get("MIMO_AUDIO_TOKENIZER_PATH", None)
if not self.audio_tokenizer_path:
raise ValueError(
"Audio tokenizer path is not set. Provide "
"`model_config.audio_tokenizer_path` in the stage config "
"or export MIMO_AUDIO_TOKENIZER_PATH."
)
if not os.path.exists(self.audio_tokenizer_path):
raise ValueError(
"Audio tokenizer not exists. Provide "
"`model_config.audio_tokenizer_path` in the stage config "
"or export MIMO_AUDIO_TOKENIZER_PATH."
)
self.tokenizer_config_path = os.environ.get("MIMO_AUDIO_TOKENIZER_PATH", None)
self.mimo_tokenizer = get_tokenizer_worker(
device=self.device,
config_path=self.tokenizer_config_path,
audio_tokenizer_path=self.audio_tokenizer_path,
)
def _parse_audio_data(
self,
data: ModalityData[AudioItem],
):
if data is None:
return AudioProcessorItems(None)
# also check single audio item with sampling rate
if self._is_empty(data) or (isinstance(data, tuple) and self._is_empty(data[0])):
return None
if (
is_list_of(data, float)
or isinstance(data, (np.ndarray, torch.Tensor))
and data.ndim == 1
or isinstance(data, tuple)
):
data_items = [data]
elif isinstance(data, (np.ndarray, torch.Tensor)):
data_items = [elem for elem in data]
else:
data_items = data
new_audios = list[np.ndarray]()
for data_item in data_items:
if not isinstance(data_item, tuple) or not isinstance(data_item[0], np.ndarray):
new_audios.append(data_item)
continue
audio, orig_sr = self._get_audio_with_sr(data_item)
if orig_sr is None:
new_audio = audio
else:
new_audio = self.audio_resampler.resample(audio, orig_sr=orig_sr)
wav_tensor = torch.from_numpy(new_audio.astype(np.float32))
if wav_tensor.dim() == 1:
wav_tensor = wav_tensor.unsqueeze(0)
if self.use_mono_channel:
if wav_tensor.size(0) > 1:
wav_mono = wav_tensor.mean(dim=0) # [samples]
else:
wav_mono = wav_tensor[0]
audio_codes = self.mimo_tokenizer.encode(audio=(wav_mono, self.target_sr))
new_audio = audio_codes.detach().cpu().numpy().astype(np.int64).tolist()
new_audios.append(new_audio)
return AudioProcessorItems(new_audios)
class MiMoAudioLLMMultiModalProcessor(BaseMultiModalProcessor[MiMoAudioLLMProcessingInfo]):
def _call_hf_processor(
self,
prompt: str,
mm_data: Mapping[str, object],
mm_kwargs: Mapping[str, Any],
tok_kwargs: Mapping[str, object],
) -> BatchFeature:
sampling_rate = 24000
audios = mm_data.pop("audios", [])
tokenizer = self.info.get_tokenizer()
if audios:
mm_data["audio"] = audios
if isinstance(prompt, str):
prompt_ids = tokenizer.encode(prompt)
else:
prompt_ids = prompt
# Text-only input not supported in composite processor
if not mm_data.get("audio", []):
prompt_ids = self._apply_hf_processor_tokens_only(prompt_ids)
return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
# feature_extractor = self.info.get_feature_extractor(**mm_kwargs)
mm_kwargs = dict(
**mm_kwargs,
sampling_rate=sampling_rate,
)
audio_lengths = [len(x) for x in audios]
if all(audio_length == 8 for audio_length in audio_lengths):
audio_lengths = [len(x[0]) for x in audios]
return BatchFeature(
data={
"input_ids": torch.tensor([prompt_ids], dtype=torch.int64),
"audio_embeds": audios,
"audio_lengths": audio_lengths,
"mm_data": mm_data,
"mm_kwargs": mm_kwargs,
"tok_kwargs": tok_kwargs,
},
tensor_type=None,
)
def _get_mm_fields_config(
self,
hf_inputs: BatchFeature,
hf_processor_mm_kwargs: Mapping[str, object],
) -> Mapping[str, MultiModalFieldConfig]:
# MiMoAudio may have different field names than Qwen2Audio
# Adjust based on actual MiMoAudio model requirements
# For now, return empty dict or adjust based on actual needs
return {
"audio_embeds": MultiModalFieldConfig.batched("audio"),
"audio_lengths": MultiModalFieldConfig.batched("audio"),
}
def _get_prompt_updates(
self,
mm_items: MultiModalDataItems,
hf_processor_mm_kwargs: Mapping[str, object],
out_mm_kwargs,
) -> Sequence[PromptUpdate]:
# MiMoAudio doesn't use a standard HF processor like Qwen2Audio
# Get tokenizer and vocabulary directly
tokenizer = self.info.get_tokenizer()
vocab = tokenizer.get_vocab()
# MiMoAudio may use different audio tokens
# Adjust these based on actual tokenizer vocabulary
# Common tokens in MiMoAudio might be different from Qwen2Audio
audio_token = "<|empty|>"
audio_bos_id = None
audio_eos_id = None
audio_token_id = vocab.get(audio_token, None)
# If tokens don't exist, try alternative tokens or use None
if audio_token_id is None:
# Try alternative token names or skip audio token replacement
return []
audio_kwargs = out_mm_kwargs.get("audio", [])
# feature_attention_mask = out_mm_kwargs.get("feature_attention_mask", None)
if audio_kwargs:
audio_output_lengths = [item["audio_lengths"].data for item in audio_kwargs]
else:
audio_output_lengths = []
if mm_items and "audio" in mm_items:
try:
audios = mm_items.get_items("audio", AudioProcessorItems)
if audios is not None:
num_audios = audios.get_count()
audio_output_lengths = [audios.get_audio_length(i) for i in range(num_audios)]
except Exception as e:
logger.error(f"_get_prompt_updates failed: {e}")
def get_replacement_mimo_audio(item_idx: int):
if item_idx >= len(audio_output_lengths):
# If no length info, use a default or skip
return PromptUpdateDetails.select_token_id(
[audio_token_id],
embed_token_id=audio_token_id,
)
num_features = audio_output_lengths[item_idx] // 4
if num_features == 0:
try:
audios = mm_items.get_items("audio", AudioProcessorItems)
audio_len = audios.get_audio_length(item_idx)
raise ValueError(f"The audio (len={audio_len}) is too short to be represented inside the model")
except (AttributeError, KeyError):
# If AudioProcessorItems is not available, use default
num_features = 1
audio_tokens = [audio_token_id] * num_features
# Build replacement tokens following Qwen2Audio pattern
# [audio_bos_id] + audio_tokens + [audio_eos_id]
replacement_tokens = []
if audio_bos_id is not None:
replacement_tokens.append(audio_bos_id)
replacement_tokens.extend(audio_tokens)
if audio_eos_id is not None:
replacement_tokens.append(audio_eos_id)
# If no bos/eos tokens, just use audio tokens
if not replacement_tokens:
replacement_tokens = audio_tokens
return PromptUpdateDetails.select_token_id(
replacement_tokens,
embed_token_id=audio_token_id,
)
return [
PromptReplacement(
modality="audio",
target=audio_token,
replacement=get_replacement_mimo_audio,
)
]
@MULTIMODAL_REGISTRY.register_processor(
MiMoAudioLLMMultiModalProcessor,
info=MiMoAudioLLMProcessingInfo,
dummy_inputs=MiMoAudioLLMDummyInputsBuilder,
)
class MiMoAudioForConditionalGeneration(
nn.Module,
SupportsPP,
SupportsMultiModal,
CustomProcessMixin,
):
packed_modules_mapping = {
"qkv_proj": [
"q_proj",
"k_proj",
"v_proj",
],
"gate_up_proj": [
"gate_proj",
"up_proj",
],
}
@classmethod
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
if modality.startswith("image"):
return "<|vision_start|><|IMAGE|><|vision_end|>"
if modality.startswith("video"):
return "<|vision_start|><|VIDEO|><|vision_end|>"
if modality.startswith("audio"):
return "<|sosp|><|empty|><|eosp|>"
def __init__(
self,
vllm_config: VllmConfig,
prefix: str = "",
):
super().__init__()
self.has_preprocess = False
self.have_multimodal_outputs = True
self.requires_raw_input_tokens = True
config = vllm_config.model_config.hf_config
config = MiMoAudioConfig(**vars(config)) if isinstance(config, Qwen2Config) else config
quant_config = vllm_config.quant_config
lora_config = vllm_config.lora_config
self.logits_processor = LogitsProcessor(config.vocab_size)
self.config = config
self.lora_config = lora_config
self.quant_config = quant_config
self.vllm_config = vllm_config
self.llm_prev_hidden_state = None
self.device = vllm_config.device_config.device
self.pin_memory = is_pin_memory_available()
if "model_stage" in os.environ:
self.model_stage = os.environ["model_stage"]
else:
self.model_stage = vllm_config.model_config.model_stage
if self.model_stage == "fused_thinker_talker":
self.has_preprocess = True
self.set_custom_preprocess(self.fused_thinker_talker_preprocess)
# Initialize fused_thinker_talker llm model (multimodal processing)
self.fused_thinker_talker = init_vllm_registered_model(
vllm_config=vllm_config,
# prefix=maybe_prefix(prefix, "model"),
hf_config=vllm_config.model_config.hf_config,
# Use registry architecture key
architectures=["MiMoAudioLLMModel"],
)
self.token2wav = None
self.model = self.fused_thinker_talker
elif self.model_stage == "code2wav":
self.fused_thinker_talker = None
# Initialize token2wav (code->mel->wav) like thinker/talker
self.token2wav = init_vllm_registered_model(
vllm_config=vllm_config,
# prefix=maybe_prefix(prefix, "token2wav"),
# hf_config=self.token2wav_config,
architectures=["MiMoAudioToken2WavModel"],
)
self.model = self.token2wav
else:
raise ValueError("Invalid model stage")
# Set up intermediate tensors
self.make_empty_intermediate_tensors = (
(self.fused_thinker_talker.make_empty_intermediate_tensors)
if self.model_stage == "fused_thinker_talker"
else lambda: None
)
def fused_thinker_talker_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
# Mixed-mode support: In a single step, both Prefill*n and Decode*n are supported.
# Ensure we have base embeddings when only ids are provided
if input_embeds is None and input_ids is not None:
input_embeds = self.fused_thinker_talker.embed_input_ids(input_ids)
span_len = input_ids.shape[0]
if span_len > 1:
# prefill
return self.fused_thinker_talker_prefill(input_ids, input_embeds, **info_dict)
else:
# decode
return input_ids, input_embeds, info_dict
def fused_thinker_talker_prefill(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: dict):
empty_token_id = self.fused_thinker_talker.empty_token_id
mm_kwargs = list[tuple[str, MultiModalKwargsItem]]()
mm_features = info_dict.get("mm_features", [])
mm_embeddings = []
prompt_ids = torch.tensor(
interleave_5_and_5_in_span(input_ids.tolist()),
dtype=torch.int64,
device=input_ids.device,
)
for mm_feature in mm_features:
mm_item = mm_feature.data
if mm_item is not None:
mm_item["mm_offset"] = MultiModalFieldElem(
data=mm_feature.mm_position.offset,
field=MultiModalBatchedField(),
)
mm_kwargs.append((mm_feature.modality, mm_item))
for modality, num_items, mm_kwargs_group in group_mm_kwargs_by_modality(
mm_kwargs,
device=self.device,
pin_memory=self.pin_memory,
):
# MiMoAudio is only supported audio modality
mm_kwargs_group["prompt_ids"] = prompt_ids
mm_kwargs_group["modality_preprocess"] = True
mm_kwargs_group["mm_offset"] = torch.tensor(
mm_kwargs_group["mm_offset"], dtype=torch.long, device=self.device
)
mm_embeddings = self.fused_thinker_talker.embed_multimodal(**mm_kwargs_group)
input_embeds = self.fused_thinker_talker.embed_input_ids(
prompt_ids, multimodal_embeddings=mm_embeddings, is_multimodal=(prompt_ids == empty_token_id)
)
return prompt_ids, input_embeds, info_dict
@staticmethod
def _module_device(module: nn.Module) -> torch.device:
try:
return next(module.parameters()).device
except StopIteration:
# No parameters; fall back to buffers or cpu
for _, buf in module.named_buffers(recurse=True):
return buf.device
return torch.device("cpu")
def move_submodules_to_devices(
self,
*,
llm_device: str | torch.device | None = None,
token2wav_device: str | torch.device | None = None,
) -> None:
"""Optionally move thinker/talker/token2wav to different devices.
Example:
model.move_submodules_to_devices(
thinker_device='cuda:0',
talker_device='cuda:1',
token2wav_device='cpu',
)
"""
if llm_device is not None and self.fused_thinker_talker is not None:
self.fused_thinker_talker.to(llm_device)
if token2wav_device is not None and self.token2wav is not None:
self.token2wav.to(token2wav_device)
@cached_property
def sampler(self):
if hasattr(self.model, "sampler"):
return self.model.sampler
return Sampler()
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal: bool = False,
) -> torch.Tensor:
if self.model_stage == "code2wav":
return torch.zeros_like(input_ids).reshape(-1, 1).repeat(1, self.vllm_config.model_config.get_hidden_size())
return self.model.embed_input_ids(input_ids, multimodal_embeddings, is_multimodal=is_multimodal)
def embed_multimodal(self, **kwargs):
# Delegate to thinker model for multimodal processing
return self.model.embed_multimodal(**kwargs)
def last_index_of(self, list, value):
return len(list) - 1 - list[::-1].index(value)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> None:
"""Load weights for all components of the omni model."""
loaded_weights = set()
llm_weights = []
token2wav_weights = []
for k, v in weights:
if (
k.startswith("model.")
or k.startswith("lm_head.")
or k.startswith("input_local_transformer.")
or k.startswith("hidden_states_downcast.")
or k.startswith("local_transformer")
or k.startswith("speech_embeddings")
or k.startswith("speech_group_downcast")
):
if self.fused_thinker_talker:
llm_weights.append((k, v))
elif k.startswith("mel_transform."):
if self.token2wav:
token2wav_weights.append((k, v))
else:
pass
# Load fused_thinker_talker llm weights
if self.fused_thinker_talker:
if llm_weights:
llm_loaded = self.fused_thinker_talker.load_weights(llm_weights)
else:
llm_loaded = set([k for k, v in llm_weights])
# thinker_loaded = add_prefix_to_loaded_weights(thinker_loaded, "thinker")
loaded_weights.update(llm_loaded)
## load code2wav
if self.token2wav:
token2wav_loaded = self.token2wav.load_weights(
token2wav_weights,
os.environ["MIMO_AUDIO_TOKENIZER_PATH"],
)
loaded_weights.update(token2wav_loaded)
@staticmethod
def insert_between(input_ids: torch.Tensor, group_size: int, value: int = -100):
if group_size < 0:
raise ValueError("group_size must be non-negative")
if not isinstance(input_ids, torch.Tensor):
input_ids = torch.tensor(input_ids)
if input_ids.dim() != 1:
raise ValueError("input_ids must be 1-D tensor or list")
L = input_ids.numel()
if group_size == 0 or L == 0:
return input_ids.clone()
new_len = L * (group_size + 1)
# Create output tensor filled with value, dtype & device consistent with input_ids
out = input_ids.new_full((new_len,), value)
# Place original elements at the first position of each block: 0, group_size+1, 2*(group_size+1), ...
positions = torch.arange(L, dtype=torch.long, device=input_ids.device) * (group_size + 1)
out[positions] = input_ids
return out
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
generate_audio: bool = True,
codec: torch.Tensor | None = None,
sampling_metadata: SamplingMetadata | None = None,
logits_index: int | None = None,
sampler=None,
additional_information: dict[str, object] | None = None,
**kwargs: object,
) -> torch.Tensor | IntermediateTensors | OmniOutput:
"""
Workflow:
1) llm: multimodal understanding → text hidden states.
2) If audio requested and codec not provided, use talker to derive codec.
3) If audio requested (or codec provided), use token2wav to synthesize waveform.
4) Return text hidden states (and audio when applicable).
"""
if self.model_stage == "fused_thinker_talker":
next_speech_tokens, text_hidden_states = self.generate_codes(
input_ids=input_ids,
positions=positions,
inputs_embeds=inputs_embeds,
intermediate_tensors=intermediate_tensors,
**kwargs,
)
return OmniOutput(
text_hidden_states=text_hidden_states.reshape(-1, text_hidden_states.shape[-1]),
multimodal_outputs={"code_predictor_codes": next_speech_tokens},
)
if self.model_stage == "code2wav":
code = (
input_ids # tensor [seq_len]
if input_ids is not None
else torch.zeros(
inputs_embeds.shape[0],
dtype=torch.long,
device=inputs_embeds.device,
)
)
audio_tensor = self.generate_audio(code)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"model_outputs": audio_tensor.reshape(1, -1) if audio_tensor is not None else audio_tensor
},
)
def generate_codes(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
**kwargs: object,
):
llm_dev = self._module_device(self.fused_thinker_talker)
# if input_ids is None, set it to a zero tensor, in the length of the
# same as the embedding seq length
if input_ids is None:
input_ids = torch.zeros(inputs_embeds.shape[1], dtype=torch.long, device=llm_dev).unsqueeze(0) # (1, 0)
# 1) Thinker (ensure inputs on thinker's device)
if input_ids is not None and input_ids.device != llm_dev:
input_ids = input_ids.to(llm_dev)
if positions is not None and positions.device != llm_dev:
positions = positions.to(llm_dev)
if inputs_embeds is not None and inputs_embeds.device != llm_dev:
inputs_embeds = inputs_embeds.to(llm_dev)
# Run llm
llm_output = self.fused_thinker_talker(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**kwargs,
)
next_speech_tokens = None
if isinstance(llm_output, tuple):
if len(llm_output) == 2:
next_speech_tokens, text_hidden_states = llm_output
elif len(llm_output) == 3:
ids, next_speech_tokens, text_hidden_states = llm_output
else:
text_hidden_states = llm_output
return next_speech_tokens, text_hidden_states
def generate_audio(self, code: torch.Tensor):
token2wav_dev = self._module_device(self.token2wav)
# Check if in CUDA graph capture phase
is_capturing = torch.cuda.is_current_stream_capturing()
if isinstance(code, torch.Tensor):
if is_capturing:
# During CUDA graph capture, avoid device movement operations
# Assume tensor is already on the correct device, only convert dtype
code_tensor = code.to(dtype=torch.long)
else:
# During non-capture phase, normally perform device movement and type conversion
code_tensor = code.to(dtype=torch.long, device=token2wav_dev)
else:
# If code is not a Tensor, should avoid creating new tensors during capture
# This case should be rare during capture, as code usually comes from input_ids
if is_capturing:
# During capture, if code is not a Tensor, try to use current stream device
# But this case should theoretically not happen
code_tensor = torch.as_tensor(code, dtype=torch.long)
else:
code_tensor = torch.as_tensor(code, dtype=torch.long, device=token2wav_dev)
if code_tensor.ndim == 2 and code_tensor.shape[0] == 1:
code_tensor = code_tensor.squeeze(0)
with torch.inference_mode():
audio_tensor = self.token2wav(codes=code_tensor) # code_tensor tensor [seq_len]
return audio_tensor
def compute_logits(
self,
hidden_states: torch.Tensor,
sampling_metadata: SamplingMetadata | None = None,
) -> torch.Tensor | None:
# logits = self.logits_processor(self.lm_head, hidden_states,
# sampling_metadata)
# Handle OmniOutput type
if isinstance(hidden_states, OmniOutput):
hidden_states = hidden_states.text_hidden_states
# Use thinker model for logits computation
return self.model.compute_logits(hidden_states)
@@ -0,0 +1,647 @@
# Copyright 2025 Xiaomi Corporation.
import logging
import os
import time
from collections.abc import Iterable
from dataclasses import dataclass
import torch
import torchaudio
from torch import nn
from torchaudio.transforms import MelSpectrogram
from transformers import AutoTokenizer, Qwen2Config
from vllm.config import VllmConfig
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.models import SupportsPP
from vllm.sequence import IntermediateTensors
from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID, MiMoAudioConfig
from vllm_omni.model_executor.models.mimo_audio.modeling_audio_tokenizer import MiMoAudioTokenizer
logger = logging.getLogger(__name__)
DUMMY_CODE_SHAPE = 36
class MiMoAudioTokenizerWorker:
def __init__(
self,
device_str: str,
config_path: str,
audio_tokenizer_path: str,
):
self.device = device_str
logger.info("[tokenizer worker] Loading MiMoAudioConfig from %s", config_path)
load_cfg_start = time.monotonic()
self.config: MiMoAudioConfig = MiMoAudioConfig.from_pretrained(config_path)
logger.info(
"[tokenizer worker] Config loaded in %.2fs",
time.monotonic() - load_cfg_start,
)
if device_str == "cpu":
model_dtype = torch.float32 # CPU must use float32
else:
model_dtype = torch.bfloat16 # GPU uses bfloat16
logger.info(
"[tokenizer worker] Loading MiMo-Audio Tokenizer from %s (device=%s, dtype=%s)",
audio_tokenizer_path,
self.device,
model_dtype,
)
start_loading_slm_tokenizer_time = time.monotonic()
logger.info(f"MiMoAudioTokenizer,device:{self.device}")
self.audio_tokenizer = MiMoAudioTokenizer.from_pretrained(
audio_tokenizer_path,
dtype=model_dtype,
device_map={"": self.device},
)
# Move to target device and only cast to bfloat16 on non-CPU devices.
self.audio_tokenizer.to(self.device)
if self.device != "cpu" and model_dtype == torch.bfloat16:
self.audio_tokenizer.to(dtype=torch.bfloat16)
self.audio_tokenizer.eval()
logger.info(
f"Audio Tokenizers loaded in "
f"{time.monotonic() - start_loading_slm_tokenizer_time:.2f} seconds, "
f"device: {self.device}"
)
logger.info(
"[tokenizer worker] Building MelSpectrogram transform (sr=%s, n_fft=%s)",
self.audio_tokenizer.config.sampling_rate,
self.audio_tokenizer.config.nfft,
)
mel_start = time.monotonic()
self.mel_transform = (
MelSpectrogram(
sample_rate=self.audio_tokenizer.config.sampling_rate,
n_fft=self.audio_tokenizer.config.nfft,
hop_length=self.audio_tokenizer.config.hop_length,
win_length=self.audio_tokenizer.config.window_size,
f_min=self.audio_tokenizer.config.fmin,
f_max=self.audio_tokenizer.config.fmax,
n_mels=self.audio_tokenizer.config.n_mels,
power=1.0,
center=True,
)
.to(self.device)
.to(torch.float32)
)
logger.info(
"[tokenizer worker] MelSpectrogram ready in %.2fs",
time.monotonic() - mel_start,
)
self.group_size = self.config.group_size
self.audio_channels = self.config.audio_channels
self.sample_rate = self.audio_tokenizer.config.sampling_rate
# Warmup (skip for CPU due to potential shape mismatch issues)
if device_str != "cpu":
logger.info("[tokenizer worker] Running warmup encode/decode...")
warmup_start = time.monotonic()
warmup_dtype = torch.float32 if device_str == "cpu" else torch.bfloat16
try:
self.encode_wav_base(torch.zeros(self.sample_rate, dtype=warmup_dtype))
self.decode(torch.zeros(self.audio_channels, self.group_size, dtype=torch.long))
logger.info(
"[tokenizer worker] Warmup finished in %.2fs",
time.monotonic() - warmup_start,
)
except Exception as e:
logger.warning("[tokenizer worker] Warmup failed (non-critical): %s", str(e))
else:
logger.info("[tokenizer worker] Skipping warmup for CPU device")
def resample_audio_if_needed(self, wav_tensor: torch.Tensor, original_sr: int):
"""Resample audio if sample rate doesn't match config"""
target_sr = self.sample_rate
if original_sr != target_sr:
wav_tensor = torchaudio.functional.resample(wav_tensor, original_sr, target_sr)
return wav_tensor
def wav2mel(self, wav: torch.Tensor):
"""Convert waveform to mel spectrogram using consistent processing"""
wav = wav.to(torch.float32)
spec = self.mel_transform(wav[None, :])
return torch.log(torch.clip(spec, min=1e-7)).squeeze()
def group_by_length(self, features: torch.Tensor, lengths: torch.Tensor, max_length: int):
if features.size(0) != lengths.sum().item():
raise ValueError(f"Feature size mismatch: {features.size(0)} vs {lengths.sum().item()}")
split_points = []
current_sum = 0
for i, seq_len in enumerate(lengths):
if current_sum + seq_len > max_length and current_sum > 0:
split_points.append(i)
current_sum = seq_len.item()
else:
current_sum += seq_len.item()
# Convert split points to group sizes
group_sizes = []
prev = 0
for point in split_points:
group_sizes.append(point - prev)
prev = point
if prev < len(lengths):
group_sizes.append(len(lengths) - prev)
len_groups = torch.split(lengths, group_sizes)
feature_sizes = [group.sum().item() for group in len_groups]
feature_groups = torch.split(features, feature_sizes)
return feature_groups, len_groups
@torch.inference_mode()
def encode_batch_base(
self,
feature_groups: list[torch.Tensor],
len_groups: list[torch.Tensor],
) -> torch.Tensor:
"""Run this in cuda stream if available"""
encoded_parts = []
for features, lengths in zip(feature_groups, len_groups):
codes, _ = self.audio_tokenizer.encoder.encode(
input_features=features.to(self.device),
input_lens=lengths.to(self.device),
return_codes_only=True,
)
encoded_parts.append(codes)
codes_packed = torch.cat(encoded_parts, dim=1)
codes = codes_packed.transpose(0, 1).detach()
audio_codes = codes[:, : self.audio_channels]
# Pad the sequence to be a multiple of group_size by repeating the last frame
T = audio_codes.shape[0]
if T % self.group_size != 0:
pad = self.group_size - (T % self.group_size)
last_tokens = audio_codes[-1, :] # Keep dim for repeat
padding_tokens = last_tokens.expand(pad, -1)
audio_codes = torch.cat([audio_codes, padding_tokens], dim=0)
audio_codes = audio_codes.transpose(0, 1).cpu()
return audio_codes # [audio_channels, T] cpu
@torch.inference_mode()
def encode(self, audio: tuple[torch.Tensor, int], max_length: float | None = 256000) -> torch.Tensor:
"""wav: [samples] cpu, sample_rate = 24000"""
wav, original_sr = audio
wav = self.resample_audio_if_needed(wav, original_sr)
wav = wav.to(self.device)
mel = self.wav2mel(wav).transpose(0, 1) # (seq_len, n_mels)
input_len = mel.size(0)
input_features = mel
segment_size = 6000
input_len_seg = [segment_size] * (input_len // segment_size)
if input_len % segment_size > 0:
input_len_seg.append(input_len % segment_size)
input_lens = torch.tensor(input_len_seg, device=self.device)
feature_groups, len_groups = self.group_by_length(input_features, input_lens, max_length)
audio_codes = self.encode_batch_base(feature_groups, len_groups)
return audio_codes # [audio_channels, T] cpu
@torch.inference_mode()
def encode_wav_base(
self,
wav: torch.Tensor, # [samples] cpu
) -> torch.Tensor:
"""Run this in cuda stream if available"""
wav = wav.to(self.device)
mel = self.wav2mel(wav).transpose(0, 1) # (seq_len, n_mels)
input_features = mel # [seq_len, n_mels]
input_lens = torch.tensor([mel.shape[0]], device=self.device)
codes_packed, _ = self.audio_tokenizer.encoder.encode(
input_features=input_features,
input_lens=input_lens,
return_codes_only=True,
)
codes = codes_packed.transpose(0, 1).detach()
audio_codes = codes[:, : self.audio_channels]
# Pad the sequence to be a multiple of group_size by repeating the last frame
T = audio_codes.shape[0]
if T % self.group_size != 0:
pad = self.group_size - (T % self.group_size)
last_tokens = audio_codes[-1, :] # Keep dim for repeat
padding_tokens = last_tokens.expand(pad, -1)
audio_codes = torch.cat([audio_codes, padding_tokens], dim=0)
audio_codes = audio_codes.transpose(0, 1).cpu()
return audio_codes # [audio_channels, T] cpu
@torch.inference_mode()
def decode(
self,
tokens: torch.Tensor, # [audio_channels, T] cpu
) -> torch.Tensor:
"""Decode audio tokens to waveform using the tokenizer's decoder"""
tokens = tokens.to(self.device)
with torch.no_grad():
decoded_audio: torch.Tensor = self.audio_tokenizer.decode(tokens)
decoded_audio = decoded_audio.float().reshape(-1).detach().cpu()
return decoded_audio # [samples] cpu
@dataclass
class AudioStreamerConfig:
group_size: int
audio_channels: int
@dataclass
class MiMoAudioCodes:
sosp: int
eosp: int
sostm: int
eostm: int
im_end: int
pad: int
eot: int
empty: int
@classmethod
def from_tokenizer(cls, tokenizer: AutoTokenizer) -> "MiMoAudioCodes":
def idx(token: str) -> int:
token_id = tokenizer.convert_tokens_to_ids(token)
if not isinstance(token_id, int):
raise ValueError(f"Token {token} is not mapped to a single id.")
return token_id
return cls(
sosp=idx("<|sosp|>"),
eosp=idx("<|eosp|>"),
sostm=idx("<|sostm|>"),
eostm=idx("<|eostm|>"),
im_end=idx("<|im_end|>"),
pad=idx("<|endoftext|>"),
eot=idx("<|eot|>"),
empty=idx("<|empty|>"),
)
def extract_audio_code_tensor(
flat_codes: torch.Tensor,
group_size: int,
audio_channels: int,
codes: MiMoAudioCodes,
) -> torch.Tensor | None:
"""Convert flattened talker output into [audio_channels, T] codes."""
if flat_codes.numel() == 0:
return None
group_width = group_size * (audio_channels + 1)
usable = (flat_codes.numel() // group_width) * group_width
if usable == 0:
return None
groups = flat_codes[:usable].view(-1, group_size, audio_channels + 1)
audio_buffer: list[torch.Tensor] = []
for group in groups:
text_token = int(group[0, 0].item())
if text_token == codes.empty:
audio_buffer.append(group[:, 1:])
elif text_token == codes.eostm:
break
if not audio_buffer:
return None
audio_tokens = torch.cat(audio_buffer, dim=0).transpose(0, 1).contiguous()
return audio_tokens # [audio_channels, T]
def _normalize_tokenizer_worker_cache_key(
device: torch.device,
config_path: str | None,
audio_tokenizer_path: str,
) -> tuple[str, str, str]:
"""Normalize cache key so that same tokenizer always hits the same cache entry."""
device_type = device.type if isinstance(device, torch.device) else str(device).split(":")[0]
# Use realpath so symlinks / trailing slash don't create duplicate entries
ap = audio_tokenizer_path or ""
if ap and os.path.exists(ap):
ap = os.path.realpath(ap)
cp = config_path or ""
if cp and os.path.exists(cp):
cp = os.path.realpath(cp)
if not cp and ap:
cp = os.path.dirname(ap)
return (device_type, cp, ap)
_TOKENIZER_WORKER_CACHE: dict[tuple[str, str, str], MiMoAudioTokenizerWorker] = {}
def get_tokenizer_worker(
device: torch.device,
config_path: str,
audio_tokenizer_path: str,
) -> MiMoAudioTokenizerWorker:
key = _normalize_tokenizer_worker_cache_key(device, config_path, audio_tokenizer_path)
if key not in _TOKENIZER_WORKER_CACHE:
device_type = key[0]
_TOKENIZER_WORKER_CACHE[key] = MiMoAudioTokenizerWorker(
device_str=device_type,
config_path=config_path,
audio_tokenizer_path=audio_tokenizer_path,
)
logger.info(
"[tokenizer cache worker] cache size=%s pid=%s",
len(_TOKENIZER_WORKER_CACHE),
os.getpid(),
)
return _TOKENIZER_WORKER_CACHE[key]
class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
"""Decode MiMo audio codes to waveform for the code2wav stage."""
def __init__(self, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
config = MiMoAudioConfig(**vars(config)) if isinstance(config, Qwen2Config) else config
self.config = config
self.vllm_config = vllm_config
self.quant_config = vllm_config.quant_config
self.lora_config = vllm_config.lora_config
self.logits_processor = LogitsProcessor(config.vocab_size)
device_str = "cuda" if torch.cuda.is_available() else "cpu"
self.device = torch.device(device_str)
self.sample_rate = getattr(config, "audio_sample_rate", 24000)
self.tokenizer = AutoTokenizer.from_pretrained(self.config.name_or_path, trust_remote_code=True)
self.codes = MiMoAudioCodes.from_tokenizer(self.tokenizer)
self.streamer_config = AudioStreamerConfig(
group_size=self.config.group_size,
audio_channels=self.config.audio_channels,
)
self.audio_tokenizer_path = getattr(vllm_config.model_config, "audio_tokenizer_path", None) or os.environ.get(
"MIMO_AUDIO_TOKENIZER_PATH"
)
if not self.audio_tokenizer_path:
raise ValueError(
"Audio tokenizer path is not set. Provide "
"`model_config.audio_tokenizer_path` in the stage config "
"or export MIMO_AUDIO_TOKENIZER_PATH."
)
self.tokenizer_config_path = (
getattr(vllm_config.model_config, "audio_tokenizer_config_path", None)
or os.environ.get("MIMO_AUDIO_TOKENIZER_PATH")
or self.config.name_or_path
)
self._tokenizer_service: MiMoAudioTokenizerWorker | None = get_tokenizer_worker(
device=self.device,
config_path=self.tokenizer_config_path,
audio_tokenizer_path=self.audio_tokenizer_path,
)
# samples per codec frame for streaming context strip (same as tokenizer frames_per_token)
audio_tokenizer_config = self._tokenizer_service.audio_tokenizer.config
self.total_upsample = (
getattr(audio_tokenizer_config, "avg_pooler", 2)
* getattr(audio_tokenizer_config, "stride_size", 2)
* getattr(audio_tokenizer_config, "hop_length", 240)
) * self.config.group_size
def load_weights(
self,
weights: Iterable[tuple[str, torch.Tensor]],
audio__dict_path: str | None = None,
**kwargs,
) -> set[str]:
# Decoder has no trainable weights to load in this stage.
return set()
def chunked_decode_streaming(
self,
codes: torch.Tensor,
chunk_size: int = 10,
left_context_size: int = 10,
) -> torch.Tensor:
"""
Decode one chunk of codes and return waveform with left context removed.
Used when async_chunk is True: each chunk is decoded and we strip
context_size * total_upsample samples from the start (left context).
"""
wav_chunk = self._decode_waveform_from_codes(codes)
if wav_chunk.numel() == 0:
return wav_chunk
# Infer number of codec frames: flat format has 36 elements per frame (9*4 with prepend)
num_flat = codes.numel() if codes.ndim == 1 else codes.shape[-1]
num_chunks = num_flat // 36
if num_chunks <= chunk_size:
context_size = 0
else:
context_size = left_context_size
drop = context_size * self.total_upsample
if drop >= wav_chunk.numel():
return wav_chunk[:0].clone()
return wav_chunk[drop:].contiguous()
def forward(
self,
input_ids: torch.Tensor | None = None,
codes: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor | IntermediateTensors:
# Support both input_ids (from vLLM) and codes (from mimo_audio.py)
code_tensor = codes if codes is not None else input_ids
if code_tensor is None:
raise ValueError("Either input_ids or codes must be provided")
if code_tensor.numel() == 0:
raise ValueError("code_tensor is empty.")
if getattr(self.vllm_config.model_config, "async_chunk", False):
waveform = self.chunked_decode_streaming(code_tensor, chunk_size=10, left_context_size=10)
else:
waveform = self._decode_waveform_from_codes(code_tensor)
# For warmup/dummy run: if waveform is empty, return dummy hidden_states
# _dummy_run expects a tensor that can be processed by extract_multimodal_outputs
if waveform.numel() == 0:
# Return dummy hidden_states with shape [seq_len, hidden_size]
# This is needed for warmup to avoid NoneType errors
hidden_size = getattr(self.config, "hidden_size", None)
if hidden_size is None:
# Fallback: try to get from vllm_config.model_config
hidden_size = getattr(
self.vllm_config.model_config,
"hidden_size",
4096, # Default fallback
)
# Return [seq_len=1, hidden_size] tensor for warmup
dummy_hidden = torch.zeros(
(1, hidden_size),
dtype=torch.bfloat16,
device=self.device,
)
return dummy_hidden
if waveform.numel() == 1 and waveform.item() == 0:
# Designed for Text output only
return None
return waveform
def _get_full_code_sequence(
self,
code_tensor: torch.Tensor | None,
kwargs: dict,
) -> torch.Tensor | None:
"""Gather the full prompt token ids (code sequence) if available."""
if code_tensor is not None and code_tensor.numel() > 1:
return code_tensor
sampling_metadata = kwargs.get("sampling_metadata")
prompt_token_ids = getattr(sampling_metadata, "prompt_token_ids", None) if sampling_metadata else None
if prompt_token_ids is None:
return code_tensor
if isinstance(prompt_token_ids, torch.Tensor):
return prompt_token_ids.detach().to(torch.long).view(-1)
if isinstance(prompt_token_ids, list):
if len(prompt_token_ids) == 0:
return code_tensor
if isinstance(prompt_token_ids[0], list):
flat = prompt_token_ids[0]
else:
flat = prompt_token_ids
if len(flat) == 0:
return code_tensor
return torch.tensor(flat, dtype=torch.long)
return code_tensor
def _check_dummy_code_tensor(self, code_tensor: torch.Tensor) -> bool:
if code_tensor is not None and code_tensor.numel() == DUMMY_CODE_SHAPE:
code_groups = code_tensor.view(self.config.group_size, self.config.audio_channels + 1)
return (
(code_groups[:, 0] == TALKER_CODEC_PAD_TOKEN_ID).all() and (code_groups[:, 1:].sum() == 0).all()
).item()
return False
def _decode_waveform_from_codes(self, code_tensor: torch.Tensor) -> torch.Tensor:
# Check if in CUDA graph capture phase
is_capturing = torch.cuda.is_current_stream_capturing()
# During CUDA graph capture, return dummy tensor to avoid operations like .cpu() which are not allowed
if is_capturing:
# Return an empty dummy tensor with shape and dtype consistent with normal output
return torch.zeros(0, dtype=torch.float32, device=self.device)
if code_tensor is None or code_tensor.numel() == 0:
return torch.zeros(0, dtype=torch.float32, device=self.device)
if self._check_dummy_code_tensor(code_tensor):
return torch.zeros(1, dtype=torch.float32, device=self.device)
if code_tensor.ndim > 1:
if code_tensor.shape[0] == 1:
code_tensor = code_tensor.squeeze(0)
elif code_tensor.shape[0] == self.streamer_config.audio_channels + 1:
code_tensor = code_tensor.transpose(0, 1).contiguous().view(-1)
else:
raise ValueError(
f"code2wav expects shape [L], [1, L] or [audio_channels+1, T], got {code_tensor.shape}"
)
flat_codes = code_tensor.detach().to(torch.long).cpu()
audio_codes = extract_audio_code_tensor(
flat_codes,
self.streamer_config.group_size,
self.streamer_config.audio_channels,
self.codes,
)
if audio_codes is None or audio_codes.numel() == 0:
return torch.zeros(0, dtype=torch.float32, device=self.device)
decoded_audio = self._tokenizer_service.decode(audio_codes)
result = decoded_audio.to(self.device)
return result
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal: bool = False,
) -> torch.Tensor:
return torch.zeros_like(input_ids).reshape(-1, 1).repeat(1, self.vllm_config.model_config.get_hidden_size())
def compute_logits(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor | None:
# code2wav stage does not need actual logits, but needs to return a valid tensor
# Return a dummy logits tensor with shape [batch_size, vocab_size]
if hidden_states.numel() == 0:
return None
batch_size = hidden_states.shape[0] if hidden_states.ndim > 0 else 1
vocab_size = self.config.vocab_size
# Create an all-zero logits tensor (sampler will choose the first token, usually pad token)
# This is safe for the code2wav stage as we do not rely on sampling results
dummy_logits = torch.zeros(
(batch_size, vocab_size),
dtype=hidden_states.dtype,
device=self.device,
)
return dummy_logits
def sample(
self,
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> SamplerOutput | None:
# code2wav stage does not need sampling, but needs to return a valid SamplerOutput to avoid CUDA errors
# Use Sampler to handle sampling, even though the result will not be used
if logits is None or logits.numel() == 0:
return None
# Use Sampler for sampling (although the result will not be used)
# This ensures the sampling process can complete normally and avoids CUDA errors
sampler = Sampler()
sampler_output = sampler(
logits=logits,
sampling_metadata=sampling_metadata,
)
return sampler_output
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,824 @@
# Copyright 2025 Xiaomi Corporation.
import math
from dataclasses import dataclass, field
import torch
import torch.nn as nn
from torch.nn import functional as F
from transformers.activations import ACT2FN
from transformers.modeling_utils import PreTrainedModel
from vllm.logger import init_logger
from .config_mimo_audio import MiMoAudioTokenizerConfig
from .modeling_rope_utils import ROPE_INIT_FUNCTIONS, apply_rotary_pos_emb, dynamic_rope_update
from .quantization import ResidualVectorQuantizer
logger = init_logger(__name__)
is_flash_atth_available = False
try:
from flash_attn import flash_attn_varlen_func
is_flash_atth_available = True
except Exception:
logger.warning("flash_attn not installed")
def get_sequence_mask(inputs, inputs_length):
if inputs.dim() == 3:
bsz, tgt_len, _ = inputs.size()
else:
bsz, tgt_len = inputs_length.shape[0], torch.max(inputs_length)
sequence_mask = torch.arange(0, tgt_len).to(inputs.device)
sequence_mask = torch.lt(sequence_mask, inputs_length.reshape(bsz, 1)).view(bsz, tgt_len, 1)
unpacking_index = torch.cumsum(sequence_mask.to(torch.int64).view(-1), dim=0) - 1
return sequence_mask, unpacking_index
def unpack_hidden_states(hidden_states, lengths, sequence_mask=None, unpacking_index=None):
bsz = lengths.shape[0]
if sequence_mask is None or unpacking_index is None:
sequence_mask, unpacking_index = get_sequence_mask(hidden_states, lengths)
hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
bsz, torch.max(lengths), hidden_states.shape[-1]
)
hidden_states = torch.where(sequence_mask, hidden_states, 0)
return hidden_states
def get_position_ids(lengths):
total_len = lengths.sum()
offset = torch.cat([torch.zeros(1).to(lengths), lengths[:-1].cumsum(dim=0)])
offset = torch.repeat_interleave(offset, lengths)
position_ids = torch.arange(0, total_len).to(offset) - offset
return position_ids
@dataclass
class StreamingConfig:
seg_point: int = field(default=60 * 25)
process_seg_point: bool = field(default=True)
left_overlap: int = field(default=10 * 25)
right_overlap: int = field(default=40)
seg_point_left_overlap: int = field(default=0)
@dataclass
class StreamingCache:
hidden_states: list[torch.Tensor] = field(default=None)
processed_lengths: list[int] = field(default=None)
class ISTFT(nn.Module):
"""
Custom implementation of ISTFT since torch.istft doesn't allow custom padding (other than `center=True`) with
windowing. This is because the NOLA (Nonzero Overlap Add) check fails at the edges.
See issue: https://github.com/pytorch/pytorch/issues/62323
Specifically, in the context of neural vocoding we are interested in "same" padding analogous to CNNs.
The NOLA constraint is met as we trim padded samples anyway.
Args:
n_fft (int): Size of Fourier transform.
hop_length (int): The distance between neighboring sliding window frames.
win_length (int): The size of window frame and STFT filter.
padding (str, optional): Type of padding. Options are "center" or "same". Defaults to "same".
"""
def __init__(self, n_fft: int, hop_length: int, win_length: int, padding: str = "same"):
super().__init__()
if padding not in ["center", "same"]:
raise ValueError("Padding must be 'center' or 'same'.")
self.padding = padding
self.n_fft = n_fft
self.hop_length = hop_length
self.win_length = win_length
window = torch.hann_window(win_length)
self.register_buffer("window", window)
def forward(self, spec: torch.Tensor) -> torch.Tensor:
"""
Compute the Inverse Short Time Fourier Transform (ISTFT) of a complex spectrogram.
Args:
spec (Tensor): Input complex spectrogram of shape (B, N, T), where B is the batch size,
N is the number of frequency bins, and T is the number of time frames.
Returns:
Tensor: Reconstructed time-domain signal of shape (B, L), where L is the length of the output signal.
"""
if self.padding == "center":
# Fallback to pytorch native implementation
return torch.istft(
spec,
self.n_fft,
self.hop_length,
self.win_length,
self.window,
center=True,
)
elif self.padding == "same":
pad = (self.win_length - self.hop_length) // 2
else:
raise ValueError("Padding must be 'center' or 'same'.")
assert spec.dim() == 3, "Expected a 3D tensor as input"
B, N, T = spec.shape
# Inverse FFT
ifft = torch.fft.irfft(spec, self.n_fft, dim=1, norm="backward")
ifft = ifft * self.window[None, :, None]
# Overlap and Add
output_size = (T - 1) * self.hop_length + self.win_length
y = torch.nn.functional.fold(
ifft,
output_size=(1, output_size),
kernel_size=(1, self.win_length),
stride=(1, self.hop_length),
)[:, 0, 0, pad:-pad]
# Window envelope
window_sq = self.window.square().expand(1, T, -1).transpose(1, 2)
window_envelope = torch.nn.functional.fold(
window_sq,
output_size=(1, output_size),
kernel_size=(1, self.win_length),
stride=(1, self.hop_length),
).squeeze()[pad:-pad]
# Normalize
assert (window_envelope > 1e-11).all()
y = y / window_envelope
return y
class ISTFTHead(nn.Module):
"""
ISTFT Head module for predicting STFT complex coefficients.
Args:
dim (int): Hidden dimension of the model.
n_fft (int): Size of Fourier transform.
hop_length (int): The distance between neighboring sliding window frames, which should align with
the resolution of the input features.
padding (str, optional): Type of padding. Options are "center" or "same". Defaults to "same".
"""
def __init__(self, dim: int, n_fft: int, hop_length: int, padding: str = "same"):
super().__init__()
out_dim = n_fft + 2
self.out = torch.nn.Linear(dim, out_dim)
self.istft = ISTFT(n_fft=n_fft, hop_length=hop_length, win_length=n_fft, padding=padding)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
Forward pass of the ISTFTHead module.
Args:
x (Tensor): Input tensor of shape (B, L, H), where B is the batch size,
L is the sequence length, and H denotes the model dimension.
Returns:
Tensor: Reconstructed time-domain audio signal of shape (B, T), where T is the length of the output signal.
"""
x = self.out(x).transpose(1, 2)
mag, p = x.chunk(2, dim=1)
mag = torch.exp(mag)
mag = torch.clip(mag, max=1e2) # safeguard to prevent excessively large magnitudes
# wrapping happens here. These two lines produce real and imaginary value
x = torch.cos(p)
y = torch.sin(p)
# recalculating phase here does not produce anything new
# only costs time
# phase = torch.atan2(y, x)
# S = mag * torch.exp(phase * 1j)
# better directly produce the complex value
original_dtype = x.dtype
S = mag.float() * (x.float() + 1j * y.float())
audio = self.istft(S)
audio = audio.to(original_dtype)
return audio
class RotaryEmbedding(nn.Module):
def __init__(self, base, dim, max_seq_len, rope_type="default", device=None):
super().__init__()
self.max_seq_len = max_seq_len
self.rope_type = rope_type
self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
inv_freq, self.attention_scaling = self.rope_init_fn(device=device, base=base, dim=dim)
self.register_buffer("inv_freq", inv_freq, persistent=False)
self.original_inv_freq = self.inv_freq
@torch.no_grad()
@dynamic_rope_update
def forward(self, x, position_ids):
inv_freq_expanded = self.inv_freq[:, None].float().expand(-1, 1).to(x.device)
position_ids_expanded = position_ids[None, :].float()
device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
with torch.autocast(device_type=device_type, enabled=False): # Force float32
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(0, 1)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos() * self.attention_scaling
sin = emb.sin() * self.attention_scaling
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
class RMSNorm(nn.Module):
def __init__(self, hidden_size, eps=1e-6):
"""
RMSNorm is equivalent to T5LayerNorm
"""
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden_size))
self.variance_epsilon = eps
def forward(self, hidden_states):
variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
if self.weight.dtype in [torch.float16, torch.bfloat16]:
hidden_states = hidden_states.to(self.weight.dtype)
return self.weight * hidden_states
LAYER_NORM = {"LayerNorm": nn.LayerNorm, "RMSNorm": RMSNorm}
class Attention(nn.Module):
def __init__(self, embed_dim, num_heads, window_size=(-1, -1), causal=False):
super().__init__()
self.embed_dim = embed_dim
self.num_heads = num_heads
self.head_dim = embed_dim // num_heads
self.window_size = window_size
self.k_proj = nn.Linear(embed_dim, embed_dim, bias=False)
self.v_proj = nn.Linear(embed_dim, embed_dim, bias=True)
self.q_proj = nn.Linear(embed_dim, embed_dim, bias=True)
self.out_proj = nn.Linear(embed_dim, embed_dim, bias=True)
self.causal = causal
def forward(
self,
hidden_states: torch.Tensor,
seq_len: torch.Tensor,
rope_position_embeddings=None,
):
total_seq_len, _ = hidden_states.size()
query_states = self.q_proj(hidden_states).view(total_seq_len, self.num_heads, self.head_dim)
key_states = self.k_proj(hidden_states).view(total_seq_len, self.num_heads, self.head_dim)
value_states = self.v_proj(hidden_states).view(total_seq_len, self.num_heads, self.head_dim)
if rope_position_embeddings is not None:
cos, sin = rope_position_embeddings
query_states = apply_rotary_pos_emb(query_states, cos, sin)
key_states = apply_rotary_pos_emb(key_states, cos, sin)
if hidden_states.is_cuda and is_flash_atth_available is True:
# === Use flash-attn in GPU mode ===
cu_len = F.pad(torch.cumsum(seq_len, dim=0), (1, 0), "constant", 0).to(torch.int32)
max_seqlen = torch.max(seq_len).to(torch.int32).detach()
attn_output = flash_attn_varlen_func(
query_states,
key_states,
value_states,
cu_len,
cu_len,
max_seqlen,
max_seqlen,
causal=self.causal,
window_size=self.window_size,
)
attn_output = attn_output.reshape(total_seq_len, self.embed_dim)
else:
# === Fallback implementation in CPU / Eager mode ===
cu_len = F.pad(torch.cumsum(seq_len, dim=0), (1, 0), "constant", 0).to(torch.long)
attn_output = torch.zeros_like(hidden_states)
for i, slen in enumerate(seq_len.tolist()):
start_idx = cu_len[i].item()
end_idx = cu_len[i + 1].item()
q = query_states[start_idx:end_idx] # [slen, num_heads, head_dim]
k = key_states[start_idx:end_idx]
v = value_states[start_idx:end_idx]
q = q.transpose(0, 1) # [num_heads, slen, head_dim]
k = k.transpose(0, 1)
v = v.transpose(0, 1)
attn_scores = torch.matmul(q, k.transpose(-1, -2)) / (self.head_dim**0.5)
if self.causal:
mask = torch.tril(torch.ones_like(attn_scores))
attn_scores = attn_scores.masked_fill(mask == 0, float("-inf"))
attn_probs = F.softmax(attn_scores, dim=-1)
attn_out = torch.matmul(attn_probs, v) # [num_heads, slen, head_dim]
attn_out = attn_out.transpose(0, 1).reshape(slen, self.embed_dim)
attn_output[start_idx:end_idx] = attn_out
attn_output = self.out_proj(attn_output)
return attn_output
class TransformerLayer(nn.Module):
def __init__(
self,
act,
d_model,
encoder_attention_heads,
encoder_ffn_dim,
causal,
ln_type="LayerNorm",
attn_window_size=(-1, -1),
):
super().__init__()
self.embed_dim = d_model
self.self_attn = Attention(self.embed_dim, encoder_attention_heads, attn_window_size, causal)
self.self_attn_layer_norm = LAYER_NORM[ln_type](self.embed_dim)
self.activation_fn = act
self.fc1 = nn.Linear(self.embed_dim, encoder_ffn_dim)
self.fc2 = nn.Linear(encoder_ffn_dim, self.embed_dim)
self.final_layer_norm = LAYER_NORM[ln_type](self.embed_dim)
def forward(
self,
hidden_states: torch.Tensor,
seq_len: torch.Tensor,
rope_position_embeddings: torch.Tensor,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.self_attn_layer_norm(hidden_states)
hidden_states = self.self_attn(hidden_states, seq_len, rope_position_embeddings=rope_position_embeddings)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.final_layer_norm(hidden_states)
hidden_states = self.activation_fn(self.fc1(hidden_states))
hidden_states = self.fc2(hidden_states)
hidden_states = residual + hidden_states
if (hidden_states.dtype == torch.float16 or hidden_states.dtype == torch.bfloat16) and (
torch.isinf(hidden_states).any() or torch.isnan(hidden_states).any()
):
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 TransformerVocos(nn.Module):
def __init__(self, config: MiMoAudioTokenizerConfig):
super().__init__()
self.config = config
self.max_source_positions = self.config.max_audio_seconds * self.config.sampling_rate // self.config.hop_length
self.embeddings = nn.Linear(config.n_mels, config.vocoder_dim, bias=False)
self.position_embedding = RotaryEmbedding(
config.rope_theta,
config.vocoder_dim // config.vocoder_attention_heads,
self.max_source_positions,
self.config.rope_type,
)
self.layers = nn.ModuleList(
[
TransformerLayer(
ACT2FN[self.config.activation_function],
self.config.vocoder_dim,
self.config.vocoder_attention_heads,
self.config.vocoder_intermediate_dim,
causal=False,
ln_type=self.config.ln_type,
attn_window_size=self.config.vocoder_attn_window_size,
)
for _ in range(self.config.vocoder_num_layers)
]
)
self.layer_norm = LAYER_NORM[self.config.ln_type](self.config.vocoder_dim)
self.hop_size = self.config.hop_length
self.head = ISTFTHead(
self.config.vocoder_dim,
self.config.nfft,
self.config.hop_length,
self.config.vocoder_padding,
)
def forward(self, x: torch.Tensor, input_length):
x = x.transpose(1, 2)
attention_mask, unpacking_index = get_sequence_mask(x, input_length)
x = torch.masked_select(x, attention_mask).view(torch.sum(input_length), self.config.n_mels)
x = self.embeddings(x)
position_ids = torch.arange(0, x.size(0), device=x.device, dtype=torch.long)
rope_position_embeddings = self.position_embedding(x, position_ids)
for idx, layer in enumerate(self.layers):
x = layer(x, input_length, rope_position_embeddings=rope_position_embeddings)
x = self.layer_norm(x)
x = unpack_hidden_states(x, input_length, attention_mask, unpacking_index)
x = self.head(x)
output_length = input_length * self.hop_size
return x[:, None, :], output_length
class AudioEncoder(nn.Module):
def __init__(self, config: MiMoAudioTokenizerConfig):
super().__init__()
config._attn_implementation = "flash_attention_2"
self.config = config
self.max_source_positions = (
config.max_audio_seconds * config.sampling_rate // config.hop_length
) // config.stride_size
self.embed_scale = math.sqrt(config.d_model) if config.scale_embedding else 1.0
self.skip_layer_idx = config.encoder_skip_layer_id
self.conv1 = nn.Conv1d(config.n_mels, config.d_model, kernel_size=config.kernel_size, padding=1)
self.conv2 = nn.Conv1d(
config.d_model,
config.d_model,
kernel_size=config.kernel_size,
stride=config.stride_size,
padding=1,
)
self.position_embedding = RotaryEmbedding(
config.rope_theta,
config.d_model // config.encoder_attention_heads,
self.max_source_positions,
config.rope_type,
)
self.layers = nn.ModuleList(
[
TransformerLayer(
ACT2FN[config.activation_function],
config.d_model,
config.encoder_attention_heads,
config.encoder_ffn_dim,
causal=self.config.encoder_causal,
ln_type=self.config.ln_type,
attn_window_size=self.config.encoder_attn_window_size,
)
for _ in range(config.encoder_layers)
]
)
self.layer_norm = LAYER_NORM[config.ln_type](config.d_model)
if self.config.avg_pooler != 1:
self.down_sample_layer = nn.Sequential(
nn.Conv1d(
config.d_model,
config.d_model,
config.avg_pooler,
config.avg_pooler,
bias=False,
),
nn.GELU(),
)
self.down_sample_norm = LAYER_NORM[config.ln_type](config.d_model)
else:
self.down_sample_layer = None
if self.config.num_quantizers != 0:
self.quantizer = ResidualVectorQuantizer(
dimension=self.config.d_model,
n_q=self.config.num_quantizers,
bins=self.config.codebook_size,
threshold_ema_dead_code=self.config.threshold_ema_dead_code,
)
else:
self.quantizer = None
def get_features(self, input_features, output_length):
input_features = input_features.to(self.conv1.weight)
inputs_embeds = nn.functional.gelu(self.conv1(input_features))
inputs_embeds = nn.functional.gelu(self.conv2(inputs_embeds))
inputs_embeds = inputs_embeds.permute(0, 2, 1)
bsz, tgt_len, _ = inputs_embeds.size()
hidden_states = inputs_embeds
position_ids = get_position_ids(output_length).long().to(input_features.device)
rope_position_embeddings = self.position_embedding(input_features, position_ids)
attention_mask, unpacking_index = get_sequence_mask(hidden_states, output_length)
hidden_states = torch.masked_select(hidden_states, attention_mask).view(
torch.sum(output_length), self.config.d_model
)
skip_connect_hidden_states = 0.0
for idx, encoder_layer in enumerate(self.layers):
hidden_states = encoder_layer(
hidden_states,
output_length,
rope_position_embeddings=rope_position_embeddings,
)
if (self.skip_layer_idx is not None) and idx == self.skip_layer_idx - 1:
skip_connect_hidden_states = hidden_states.clone()
hidden_states += skip_connect_hidden_states
hidden_states = self.layer_norm(hidden_states)
if self.down_sample_layer is not None:
hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
bsz, tgt_len, self.config.d_model
)
if hidden_states.size(1) % self.config.avg_pooler:
pad_len = self.config.avg_pooler - hidden_states.size(1) % self.config.avg_pooler
hidden_states = torch.nn.functional.pad(hidden_states, (0, 0, 0, pad_len), mode="constant", value=0.0)
tgt_len += pad_len
tgt_len = tgt_len // self.config.avg_pooler
hidden_states = self.down_sample_layer(hidden_states.transpose(1, 2))
output_length = (
output_length // self.config.avg_pooler + (output_length % self.config.avg_pooler != 0).int()
)
hidden_states = hidden_states.transpose(1, 2)
attention_mask, unpacking_index = get_sequence_mask(hidden_states, output_length)
hidden_states = torch.masked_select(hidden_states, attention_mask).view(
torch.sum(output_length), self.config.d_model
)
hidden_states = self.down_sample_norm(hidden_states)
return (
hidden_states,
output_length,
attention_mask,
unpacking_index,
tgt_len,
bsz,
)
def get_output_length(self, mel_len):
tgt_len = mel_len + 3 - self.config.kernel_size
return (tgt_len + 2 - self.config.kernel_size) // self.config.stride_size + 1
@torch.no_grad()
def encode(
self,
input_features,
input_lens=None,
output_length=None,
return_codes_only=False,
n_q=None,
use_quantizer=True,
):
if output_length is None:
output_length = self.get_output_length(input_lens)
input_features = unpack_hidden_states(input_features, input_lens)
hidden_states, output_length, attention_mask, unpacking_index, tgt_len, bsz = self.get_features(
input_features=input_features.transpose(1, 2),
output_length=output_length,
)
dtype = hidden_states.dtype
if use_quantizer and self.quantizer is not None:
self.quantizer.float()
codes = self.quantizer.encode(hidden_states.float(), n_q=n_q)
if return_codes_only:
return codes, output_length
hidden_states = self.quantizer.decode(codes)
hidden_states = hidden_states.to(dtype)
else:
codes = None
hidden_states_packed = hidden_states.clone()
# unpacking
hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(bsz, tgt_len, self.config.d_model)
hidden_states = torch.where(attention_mask, hidden_states, 0)
return hidden_states, hidden_states_packed, output_length, codes
@torch.no_grad()
def decode_vq(self, codes):
self.quantizer.float()
hidden_states = self.quantizer.decode(codes)
return hidden_states
class CausalConvTranspose1d(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride):
super().__init__()
self.conv = nn.ConvTranspose1d(in_channels, out_channels, kernel_size, stride)
self.norm = nn.GroupNorm(1, out_channels)
self.in_channels = in_channels
self.out_channels = out_channels
def forward(self, hidden_states, input_length, output_dim=None):
kernel_size = self.conv.kernel_size[0]
stride = self.conv.stride[0]
bsz = input_length.shape[0]
if output_dim is None:
output_dim = hidden_states.dim()
if hidden_states.dim() <= 2: # unpack sequence to 3d
sequence_mask, unpacking_index = get_sequence_mask(hidden_states, input_length)
hidden_states = torch.index_select(hidden_states, 0, unpacking_index).view(
bsz, torch.max(input_length), self.in_channels
)
hidden_states = torch.where(sequence_mask, hidden_states, 0)
hidden_states = hidden_states.transpose(2, 1) # (N, L, C) -> (N, C, L)
hidden_states = self.conv(hidden_states)
hidden_states = self.norm(hidden_states)
hidden_states = hidden_states.transpose(2, 1) # (N, C, L) -> (N, L, C)
casual_padding_right = max(0, kernel_size - stride)
hidden_states = hidden_states[:, : hidden_states.shape[1] - casual_padding_right, :]
output_length = (input_length - 1) * stride + kernel_size - casual_padding_right
sequence_mask, _ = get_sequence_mask(hidden_states, output_length)
if output_dim <= 2:
hidden_states = torch.masked_select(hidden_states, sequence_mask).view(-1, self.out_channels)
else:
hidden_states = torch.where(sequence_mask, hidden_states, 0)
hidden_states = hidden_states[:, : torch.max(output_length), :]
return hidden_states, output_length
class AudioDecoder(nn.Module):
def __init__(self, config: MiMoAudioTokenizerConfig):
super().__init__()
self.config = config
self.max_source_positions = self.config.max_audio_seconds * self.config.sampling_rate // self.config.hop_length
if self.config.avg_pooler != 1:
self.dconv1 = CausalConvTranspose1d(
self.config.d_model,
self.config.d_model,
self.config.avg_pooler,
self.config.avg_pooler,
)
else:
self.dconv1 = None
self.position_embedding = RotaryEmbedding(
config.rope_theta,
config.d_model // config.decoder_attention_heads,
self.max_source_positions,
config.rope_type,
)
self.layers = nn.ModuleList(
[
TransformerLayer(
ACT2FN[self.config.activation_function],
self.config.d_model,
self.config.decoder_attention_heads,
self.config.decoder_ffn_dim,
causal=self.config.decoder_causal,
ln_type=self.config.ln_type,
attn_window_size=self.config.decoder_attn_window_size,
)
for _ in range(self.config.decoder_layers)
]
)
self.layer_norm = LAYER_NORM[config.ln_type](self.config.d_model)
self.dconv2 = CausalConvTranspose1d(
self.config.d_model,
self.config.n_mels,
self.config.decoder_kernel_size,
self.config.decoder_stride_size,
)
self.vocoder = TransformerVocos(config)
def forward(
self,
audio_embed,
input_length,
):
assert audio_embed.shape[-1] == self.config.d_model
audio_embed = audio_embed.to(self.layer_norm.weight)
if self.dconv1 is not None:
audio_embed, output_length = self.dconv1(audio_embed, input_length, output_dim=3)
else:
output_length = input_length
hidden_states = audio_embed
position_ids = get_position_ids(output_length).long().to(hidden_states.device)
rope_position_embeddings = self.position_embedding(hidden_states, position_ids)
# packing hidden states
attention_mask, _ = get_sequence_mask(hidden_states, output_length)
hidden_states = torch.masked_select(hidden_states, attention_mask).view(
torch.sum(output_length), self.config.d_model
)
for idx, encoder_layer in enumerate(self.layers):
hidden_states = encoder_layer(
hidden_states,
output_length,
rope_position_embeddings=rope_position_embeddings,
)
hidden_states = self.layer_norm(hidden_states)
coarse_mel, output_length = self.dconv2(hidden_states, output_length, output_dim=3)
recon_wav, wav_length = self.vocoder(
x=coarse_mel.transpose(1, 2),
input_length=output_length,
)
return recon_wav
class MiMoAudioTokenizer(PreTrainedModel):
config_class = MiMoAudioTokenizerConfig
def __init__(self, config: MiMoAudioTokenizerConfig):
super().__init__(config)
self.config = config
self.sampling_rate = config.sampling_rate
self.encoder = AudioEncoder(config=config)
self.decoder = AudioDecoder(config=config)
self.downsample_rate = int(self.config.hop_length * 2 * self.config.avg_pooler)
def get_output_length(self, mel_len):
tgt_len = mel_len + 3 - self.config.kernel_size
return (tgt_len + 2 - self.config.kernel_size) // self.config.stride_size + 1
@torch.no_grad()
def encode(self, mels, input_lens, use_quantizer=True):
input_features = mels
hidden_states, hidden_states_packed, encoder_output_length, codes = self.encoder.encode(
input_features, input_lens=input_lens, use_quantizer=use_quantizer
)
return hidden_states, hidden_states_packed, encoder_output_length, codes
@torch.no_grad()
def decode(self, codes):
hidden_states = self.encoder.decode_vq(codes)
output = self.decoder(
hidden_states,
torch.tensor([hidden_states.size(0)], device=hidden_states.device),
)
return output
@torch.no_grad()
def streaming_decode(
self,
codes_chunks,
chunk_input_lengths,
history_cache=StreamingCache(),
streaming_config=StreamingConfig(),
last_chunk=False,
):
hidden_states = self.encoder.decode_vq(codes_chunks)
input_lengths = []
input_hidden_states = []
start_idx = 0
cache_hidden_states = []
for i, input_length in enumerate(chunk_input_lengths):
sample_hidden_states = hidden_states[start_idx : start_idx + input_length]
start_idx += input_length
if history_cache.hidden_states is not None:
sample_hidden_states = torch.cat([history_cache.hidden_states[i], sample_hidden_states], dim=0)
input_length += history_cache.hidden_states[i].size(0)
input_hidden_states.append(sample_hidden_states)
cache_hidden_states.append(sample_hidden_states.clone())
input_lengths.append(input_length)
input_hidden_states = torch.cat(input_hidden_states, dim=0)
input_lengths = torch.tensor(input_lengths, device=hidden_states.device)
output = self.decoder(input_hidden_states, input_lengths)
return_wavs = []
frames_per_token = self.config.avg_pooler * self.config.stride_size * self.config.hop_length
processed_lengths = []
for i, wav in enumerate(output):
wav = wav.float().detach().cpu()
start_idx = history_cache.processed_lengths[i] if history_cache.processed_lengths is not None else 0
if last_chunk:
return_wavs.append(wav[:, start_idx * frames_per_token :])
new_processed_length = input_lengths[i].item()
elif input_lengths[i].item() <= streaming_config.right_overlap:
return_wavs.append(None)
new_processed_length = 0
else:
end_idx = input_lengths[i].item() - streaming_config.right_overlap
wav = wav[:, start_idx * frames_per_token : end_idx * frames_per_token]
return_wavs.append(wav)
new_processed_length = end_idx
if input_lengths[i].item() > streaming_config.left_overlap:
cache_hidden_states[i] = cache_hidden_states[i][-streaming_config.left_overlap :]
new_processed_length -= input_lengths[i].item() - streaming_config.left_overlap
processed_lengths.append(new_processed_length)
history_cache.hidden_states = cache_hidden_states
history_cache.processed_lengths = processed_lengths
return return_wavs, history_cache
@@ -0,0 +1,703 @@
# Copyright 2025 Xiaomi Corporation.
# Copyright 2024 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
from collections.abc import Callable
from functools import wraps
from typing import Optional
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import is_torch_available, logging
logger = logging.get_logger(__name__)
if is_torch_available():
import torch
def dynamic_rope_update(rope_forward: Callable) -> Callable:
"""
Decorator function to update the RoPE parameters in the forward pass, if the model is using a dynamic RoPE
(i.e. a RoPE implementation that may recompute its frequencies in the forward pass).
Args:
rope_forward (Callable):
The forward pass of the RoPE implementation.
Returns:
The decorated forward pass.
"""
def longrope_frequency_update(self, position_ids, device):
"""Longrope uses long factor if sequence is larger than original pretraining length, short otherwise."""
seq_len = torch.max(position_ids) + 1
if hasattr(self.config, "original_max_position_embeddings"):
original_max_position_embeddings = self.config.original_max_position_embeddings
else:
original_max_position_embeddings = self.config.max_position_embeddings
if seq_len > original_max_position_embeddings:
if not hasattr(self, "long_inv_freq"):
self.long_inv_freq, _ = self.rope_init_fn(
self.config, device, seq_len=original_max_position_embeddings + 1
)
self.register_buffer("inv_freq", self.long_inv_freq, persistent=False)
else:
# This .to() is needed if the model has been moved to a device after being initialized (because
# the buffer is automatically moved, but not the original copy)
self.original_inv_freq = self.original_inv_freq.to(device)
self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
def dynamic_frequency_update(self, position_ids, device):
"""
dynamic RoPE layers should recompute `inv_freq` in the following situations:
1 - growing beyond the cached sequence length (allow scaling)
2 - the current sequence length is in the original scale (avoid losing precision with small sequences)
"""
seq_len = torch.max(position_ids) + 1
if seq_len > self.max_seq_len_cached: # growth
inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device, seq_len=seq_len)
self.register_buffer("inv_freq", inv_freq, persistent=False) # TODO joao: may break with compilation
self.max_seq_len_cached = seq_len
if seq_len < self.original_max_seq_len and self.max_seq_len_cached > self.original_max_seq_len: # reset
# This .to() is needed if the model has been moved to a device after being initialized (because
# the buffer is automatically moved, but not the original copy)
self.original_inv_freq = self.original_inv_freq.to(device)
self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
self.max_seq_len_cached = self.original_max_seq_len
@wraps(rope_forward)
def wrapper(self, x, position_ids):
if "dynamic" in self.rope_type:
dynamic_frequency_update(self, position_ids, device=x.device)
elif self.rope_type == "longrope":
longrope_frequency_update(self, position_ids, device=x.device)
return rope_forward(self, x, position_ids)
return wrapper
def _compute_default_rope_parameters(
config: PretrainedConfig | None = None,
device: Optional["torch.device"] = None,
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies according to the original RoPE implementation
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length. Unused for this type of RoPE.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE).
"""
if config is not None and len(rope_kwargs) > 0:
raise ValueError(
"Unexpected arguments: `**rope_kwargs` and `config` are mutually exclusive in "
f"`_compute_default_rope_parameters`, got `rope_kwargs`={rope_kwargs} and `config`={config}"
)
if len(rope_kwargs) > 0:
base = rope_kwargs["base"]
dim = rope_kwargs["dim"]
elif config is not None:
base = config.rope_theta
partial_rotary_factor = config.partial_rotary_factor if hasattr(config, "partial_rotary_factor") else 1.0
head_dim = getattr(config, "head_dim", None) or config.hidden_size // config.num_attention_heads
dim = int(head_dim * partial_rotary_factor)
attention_factor = 1.0 # Unused in this type of RoPE
# Compute the inverse frequencies
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim))
return inv_freq, attention_factor
def _compute_linear_scaling_rope_parameters(
config: PretrainedConfig | None = None,
device: Optional["torch.device"] = None,
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies with linear scaling. Credits to the Reddit user /u/kaiokendev
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length. Unused for this type of RoPE.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE).
"""
if config is not None and len(rope_kwargs) > 0:
raise ValueError(
"Unexpected arguments: `**rope_kwargs` and `config` are mutually exclusive in "
f"`_compute_linear_scaling_rope_parameters`, got `rope_kwargs`={rope_kwargs} and `config`={config}"
)
if len(rope_kwargs) > 0:
factor = rope_kwargs["factor"]
elif config is not None:
factor = config.rope_scaling["factor"]
# Gets the default RoPE parameters
inv_freq, attention_factor = _compute_default_rope_parameters(config, device, seq_len, **rope_kwargs)
# Then applies linear scaling to the frequencies.
# NOTE: originally, scaling was applied to the position_ids. However, we get `embs = inv_freq @ position_ids`, so
# applying scaling to the inverse frequencies is equivalent.
inv_freq /= factor
return inv_freq, attention_factor
def _compute_dynamic_ntk_parameters(
config: PretrainedConfig | None = None,
device: Optional["torch.device"] = None,
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies with NTK scaling. Credits to the Reddit users /u/bloc97 and /u/emozilla
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length, used to update the dynamic RoPE at inference time.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin (unused in this type of RoPE).
"""
# TODO (joao): use the new `original_max_position_embeddings` from rope_scaling
if config is not None and len(rope_kwargs) > 0:
raise ValueError(
"Unexpected arguments: `**rope_kwargs` and `config` are mutually exclusive in "
f"`_compute_dynamic_ntk_parameters`, got `rope_kwargs`={rope_kwargs} and `config`={config}"
)
if len(rope_kwargs) > 0:
base = rope_kwargs["base"]
dim = rope_kwargs["dim"]
max_position_embeddings = rope_kwargs["max_position_embeddings"]
factor = rope_kwargs["factor"]
elif config is not None:
base = config.rope_theta
partial_rotary_factor = config.partial_rotary_factor if hasattr(config, "partial_rotary_factor") else 1.0
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
dim = int(head_dim * partial_rotary_factor)
max_position_embeddings = config.max_position_embeddings
factor = config.rope_scaling["factor"]
attention_factor = 1.0 # Unused in this type of RoPE
# seq_len: default to max_position_embeddings, e.g. at init time
seq_len = seq_len if seq_len is not None and seq_len > max_position_embeddings else max_position_embeddings
# Compute the inverse frequencies
base = base * ((factor * seq_len / max_position_embeddings) - (factor - 1)) ** (dim / (dim - 2))
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).to(device=device, dtype=torch.float) / dim))
return inv_freq, attention_factor
def _compute_yarn_parameters(
config: PretrainedConfig,
device: "torch.device",
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies with NTK scaling. Please refer to the
[original paper](https://huggingface.co/papers/2309.00071)
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length. Unused for this type of RoPE.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin.
"""
# No need to keep BC with yarn, unreleased when this new pattern was created.
if len(rope_kwargs) > 0:
raise ValueError(
f"Unexpected arguments: `**rope_kwargs` should be unset in `_compute_yarn_parameters`, got {rope_kwargs}"
)
base = config.rope_theta
partial_rotary_factor = config.partial_rotary_factor if hasattr(config, "partial_rotary_factor") else 1.0
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
dim = int(head_dim * partial_rotary_factor)
factor = config.rope_scaling["factor"]
attention_factor = config.rope_scaling.get("attention_factor")
mscale = config.rope_scaling.get("mscale")
mscale_all_dim = config.rope_scaling.get("mscale_all_dim")
# NOTE: DeekSeek-V3 (and potentially other models) modify `max_position_embeddings` and have a
# `original_max_position_embeddings` field containing the pretrained value. They use the ratio between these two
# values to compute the default attention scaling factor, instead of using `factor`.
if "original_max_position_embeddings" in config.rope_scaling:
original_max_position_embeddings = config.rope_scaling["original_max_position_embeddings"]
factor = config.max_position_embeddings / original_max_position_embeddings
else:
original_max_position_embeddings = config.max_position_embeddings
def get_mscale(scale, mscale=1):
if scale <= 1:
return 1.0
return 0.1 * mscale * math.log(scale) + 1.0
# Sets the attention factor as suggested in the paper
if attention_factor is None:
if mscale and mscale_all_dim:
attention_factor = float(get_mscale(factor, mscale) / get_mscale(factor, mscale_all_dim))
else:
attention_factor = get_mscale(factor)
# Optional config options
# beta_fast/beta_slow: as suggested in the paper, default to 32/1 (correspondingly)
beta_fast = config.rope_scaling.get("beta_fast") or 32
beta_slow = config.rope_scaling.get("beta_slow") or 1
# Compute the inverse frequencies
def find_correction_dim(num_rotations, dim, base, max_position_embeddings):
"""Inverse dimension formula to find the dimension based on the number of rotations"""
return (dim * math.log(max_position_embeddings / (num_rotations * 2 * math.pi))) / (2 * math.log(base))
def find_correction_range(low_rot, high_rot, dim, base, max_position_embeddings):
"""Find dimension range bounds based on rotations"""
low = math.floor(find_correction_dim(low_rot, dim, base, max_position_embeddings))
high = math.ceil(find_correction_dim(high_rot, dim, base, max_position_embeddings))
return max(low, 0), min(high, dim - 1)
def linear_ramp_factor(min, max, dim):
if min == max:
max += 0.001 # Prevent singularity
linear_func = (torch.arange(dim, dtype=torch.float32) - min) / (max - min)
ramp_func = torch.clamp(linear_func, 0, 1)
return ramp_func
# Note on variable naming: "interpolation" comes from the original technique, where we interpolate the position IDs
# to expand the possible context length. In other words, interpolation = apply scaling factor.
pos_freqs = base ** (torch.arange(0, dim, 2).to(device=device, dtype=torch.float) / dim)
inv_freq_extrapolation = 1.0 / pos_freqs
inv_freq_interpolation = 1.0 / (factor * pos_freqs)
low, high = find_correction_range(beta_fast, beta_slow, dim, base, original_max_position_embeddings)
# Get n-dimensional rotational scaling corrected for extrapolation
inv_freq_extrapolation_factor = 1 - linear_ramp_factor(low, high, dim // 2).to(device=device, dtype=torch.float)
inv_freq = (
inv_freq_interpolation * (1 - inv_freq_extrapolation_factor)
+ inv_freq_extrapolation * inv_freq_extrapolation_factor
)
return inv_freq, attention_factor
def _compute_longrope_parameters(
config: PretrainedConfig,
device: "torch.device",
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies with LongRoPE scaling. Please refer to the
[original implementation](https://github.com/microsoft/LongRoPE)
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin.
"""
# TODO (joao): use the new `original_max_position_embeddings` from rope_scaling
# No need to keep BC with longrope, unreleased when this new pattern was created.
if len(rope_kwargs) > 0:
raise ValueError(
"Unexpected arguments: `**rope_kwargs` should be unset in `_compute_longrope_parameters`, got "
f"{rope_kwargs}"
)
base = config.rope_theta
partial_rotary_factor = config.partial_rotary_factor if hasattr(config, "partial_rotary_factor") else 1.0
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
dim = int(head_dim * partial_rotary_factor)
long_factor = config.rope_scaling["long_factor"]
short_factor = config.rope_scaling["short_factor"]
factor = config.rope_scaling.get("factor")
attention_factor = config.rope_scaling.get("attention_factor")
# NOTE: Phi3 (and potentially other models) modify `max_position_embeddings` and have a
# `original_max_position_embeddings` field containing the pretrained value. They use the ratio between these two
# values to compute the default attention scaling factor, instead of using `factor`.
if hasattr(config, "original_max_position_embeddings"):
original_max_position_embeddings = config.original_max_position_embeddings
factor = config.max_position_embeddings / config.original_max_position_embeddings
else:
original_max_position_embeddings = config.max_position_embeddings
# Sets the attention factor as suggested in the paper
if attention_factor is None:
if factor <= 1.0:
attention_factor = 1.0
else:
attention_factor = math.sqrt(1 + math.log(factor) / math.log(original_max_position_embeddings))
# Compute the inverse frequencies -- scaled based on the target sequence length
if seq_len and seq_len > original_max_position_embeddings:
ext_factors = torch.tensor(long_factor, dtype=torch.float32, device=device)
else:
ext_factors = torch.tensor(short_factor, dtype=torch.float32, device=device)
inv_freq_shape = torch.arange(0, dim, 2, dtype=torch.int64, device=device).float() / dim
inv_freq = 1.0 / (ext_factors * base**inv_freq_shape)
return inv_freq, attention_factor
def _compute_llama3_parameters(
config: PretrainedConfig,
device: "torch.device",
seq_len: int | None = None,
**rope_kwargs,
) -> tuple["torch.Tensor", float]:
"""
Computes the inverse frequencies for llama 3.1.
Args:
config ([`~transformers.PretrainedConfig`]):
The model configuration.
device (`torch.device`):
The device to use for initialization of the inverse frequencies.
seq_len (`int`, *optional*):
The current sequence length. Unused for this type of RoPE.
rope_kwargs (`Dict`, *optional*):
BC compatibility with the previous RoPE class instantiation, will be removed in v4.45.
Returns:
Tuple of (`torch.Tensor`, `float`), containing the inverse frequencies for the RoPE embeddings and the
post-processing scaling factor applied to the computed cos/sin.
"""
# Gets the default RoPE parameters
inv_freq, attention_factor = _compute_default_rope_parameters(config, device, seq_len, **rope_kwargs)
factor = config.rope_scaling["factor"] # `8` in the original implementation
low_freq_factor = config.rope_scaling["low_freq_factor"] # `1` in the original implementation
high_freq_factor = config.rope_scaling["high_freq_factor"] # `4` in the original implementation
old_context_len = config.rope_scaling["original_max_position_embeddings"] # `8192` in the original implementation
low_freq_wavelen = old_context_len / low_freq_factor
high_freq_wavelen = old_context_len / high_freq_factor
wavelen = 2 * math.pi / inv_freq
# wavelen < high_freq_wavelen: do nothing
# wavelen > low_freq_wavelen: divide by factor
inv_freq_llama = torch.where(wavelen > low_freq_wavelen, inv_freq / factor, inv_freq)
# otherwise: interpolate between the two, using a smooth factor
smooth_factor = (old_context_len / wavelen - low_freq_factor) / (high_freq_factor - low_freq_factor)
smoothed_inv_freq = (1 - smooth_factor) * inv_freq_llama / factor + smooth_factor * inv_freq_llama
is_medium_freq = ~(wavelen < high_freq_wavelen) * ~(wavelen > low_freq_wavelen)
inv_freq_llama = torch.where(is_medium_freq, smoothed_inv_freq, inv_freq_llama)
return inv_freq_llama, attention_factor
# This maps the "rope_type" string field in rope config to the corresponding function to compute the RoPE parameters
# from the model config. You can append new {'rope_type': callable} pairs to this dictionary to enable custom RoPE
# parameterizations, as long as the callable has the same signature.
ROPE_INIT_FUNCTIONS = {
"default": _compute_default_rope_parameters,
"linear": _compute_linear_scaling_rope_parameters,
"dynamic": _compute_dynamic_ntk_parameters,
"yarn": _compute_yarn_parameters,
"longrope": _compute_longrope_parameters,
"llama3": _compute_llama3_parameters,
}
def _check_received_keys(
rope_type: str,
received_keys: set,
required_keys: set,
optional_keys: set | None = None,
ignore_keys: set | None = None,
):
"""Compare the received keys in `config.rope_scaling` against the expected and optional keys"""
# BC: "rope_type" was originally "type" -- let's check for "rope_type" when "type" is present
if "type" in received_keys:
received_keys -= {"type"}
required_keys.add("rope_type")
# Some models need to store model-specific keys, and we don't want to throw warning at them
if ignore_keys is not None:
received_keys -= ignore_keys
missing_keys = required_keys - received_keys
if missing_keys:
raise KeyError(f"Missing required keys in `rope_scaling` for 'rope_type'='{rope_type}': {missing_keys}")
if optional_keys is not None:
unused_keys = received_keys - required_keys - optional_keys
else:
unused_keys = received_keys - required_keys
if unused_keys:
logger.warning(f"Unrecognized keys in `rope_scaling` for 'rope_type'='{rope_type}': {unused_keys}")
def _validate_default_rope_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {"rope_type"}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, ignore_keys=ignore_keys)
def _validate_linear_scaling_rope_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {"rope_type", "factor"}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, ignore_keys=ignore_keys)
factor = rope_scaling["factor"]
if factor is None or not isinstance(factor, float) or factor < 1.0:
logger.warning(f"`rope_scaling`'s factor field must be a float >= 1, got {factor}")
def _validate_dynamic_scaling_rope_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {"rope_type", "factor"}
# TODO (joao): update logic for the inclusion of `original_max_position_embeddings`
optional_keys = {"original_max_position_embeddings"}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, optional_keys, ignore_keys=ignore_keys)
factor = rope_scaling["factor"]
if factor is None or not isinstance(factor, float) or factor < 1.0:
logger.warning(f"`rope_scaling`'s factor field must be a float >= 1, got {factor}")
def _validate_yarn_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {"rope_type", "factor"}
optional_keys = {
"attention_factor",
"beta_fast",
"beta_slow",
"original_max_position_embeddings",
"mscale",
"mscale_all_dim",
}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, optional_keys, ignore_keys=ignore_keys)
factor = rope_scaling["factor"]
if factor is None or not isinstance(factor, float) or factor < 1.0:
logger.warning(f"`rope_scaling`'s factor field must be a float >= 1, got {factor}")
attention_factor = rope_scaling.get("attention_factor")
if attention_factor is not None and (not isinstance(attention_factor, float) or attention_factor < 0):
logger.warning(
f"`rope_scaling`'s attention_factor field must be a float greater than 0, got {attention_factor}"
)
beta_fast = rope_scaling.get("beta_fast")
if beta_fast is not None and not isinstance(beta_fast, float):
logger.warning(f"`rope_scaling`'s beta_fast field must be a float, got {beta_fast}")
beta_slow = rope_scaling.get("beta_slow")
if beta_slow is not None and not isinstance(beta_slow, float):
logger.warning(f"`rope_scaling`'s beta_slow field must be a float, got {beta_slow}")
if (beta_fast or 32) < (beta_slow or 1):
logger.warning(
f"`rope_scaling`'s beta_fast field must be greater than beta_slow, got beta_fast={beta_fast} "
f"(defaults to 32 if None) and beta_slow={beta_slow} (defaults to 1 if None)"
)
def _validate_longrope_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {"rope_type", "short_factor", "long_factor"}
# TODO (joao): update logic for the inclusion of `original_max_position_embeddings`
optional_keys = {"attention_factor", "factor", "original_max_position_embeddings"}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, optional_keys, ignore_keys=ignore_keys)
partial_rotary_factor = config.partial_rotary_factor if hasattr(config, "partial_rotary_factor") else 1.0
head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
dim = int(head_dim * partial_rotary_factor)
short_factor = rope_scaling.get("short_factor")
if not isinstance(short_factor, list) and all(isinstance(x, (int, float)) for x in short_factor):
logger.warning(f"`rope_scaling`'s short_factor field must be a list of numbers, got {short_factor}")
if not len(short_factor) == dim // 2:
logger.warning(f"`rope_scaling`'s short_factor field must have length {dim // 2}, got {len(short_factor)}")
long_factor = rope_scaling.get("long_factor")
if not isinstance(long_factor, list) and all(isinstance(x, (int, float)) for x in long_factor):
logger.warning(f"`rope_scaling`'s long_factor field must be a list of numbers, got {long_factor}")
if not len(long_factor) == dim // 2:
logger.warning(f"`rope_scaling`'s long_factor field must have length {dim // 2}, got {len(long_factor)}")
# Handle Phi3 divergence: prefer the use of `attention_factor` and/or `factor` over
# `original_max_position_embeddings` to compute internal variables. The latter lives outside `rope_scaling` and is
# unique to longrope (= undesirable)
if hasattr(config, "original_max_position_embeddings"):
logger.warning_once(
"This model has set a `original_max_position_embeddings` field, to be used together with "
"`max_position_embeddings` to determine a scaling factor. Please set the `factor` field of `rope_scaling`"
"with this ratio instead -- we recommend the use of this field over `original_max_position_embeddings`, "
"as it is compatible with most model architectures."
)
else:
factor = rope_scaling.get("factor")
if factor is None:
logger.warning("Missing required keys in `rope_scaling`: 'factor'")
elif not isinstance(factor, float) or factor < 1.0:
logger.warning(f"`rope_scaling`'s factor field must be a float >= 1, got {factor}")
attention_factor = rope_scaling.get("attention_factor")
if attention_factor is not None:
if not isinstance(attention_factor, float) or attention_factor < 0.0:
logger.warning(
f"`rope_scaling`'s attention_factor field must be a float greater than 0, got {attention_factor}"
)
def _validate_llama3_parameters(config: PretrainedConfig, ignore_keys: set | None = None):
rope_scaling = config.rope_scaling
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None)) # BC: "rope_type" was originally "type"
required_keys = {
"rope_type",
"factor",
"original_max_position_embeddings",
"low_freq_factor",
"high_freq_factor",
}
received_keys = set(rope_scaling.keys())
_check_received_keys(rope_type, received_keys, required_keys, ignore_keys=ignore_keys)
factor = rope_scaling["factor"]
if factor is None or not isinstance(factor, float) or factor < 1.0:
logger.warning(f"`rope_scaling`'s factor field must be a float >= 1, got {factor}")
low_freq_factor = rope_scaling["low_freq_factor"]
high_freq_factor = rope_scaling["high_freq_factor"]
if low_freq_factor is None or not isinstance(low_freq_factor, float):
logger.warning(f"`rope_scaling`'s low_freq_factor field must be a float, got {low_freq_factor}")
if high_freq_factor is None or not isinstance(high_freq_factor, float):
logger.warning(f"`rope_scaling`'s high_freq_factor field must be a float, got {high_freq_factor}")
if high_freq_factor <= low_freq_factor:
logger.warning(
"`rope_scaling`'s high_freq_factor field must be greater than low_freq_factor, got high_freq_factor="
f"{high_freq_factor} and low_freq_factor={low_freq_factor}"
)
original_max_position_embeddings = rope_scaling["original_max_position_embeddings"]
if original_max_position_embeddings is None or not isinstance(original_max_position_embeddings, int):
logger.warning(
"`rope_scaling`'s original_max_position_embeddings field must be an integer, got "
f"{original_max_position_embeddings}"
)
if original_max_position_embeddings >= config.max_position_embeddings:
logger.warning(
"`rope_scaling`'s original_max_position_embeddings field must be less than max_position_embeddings, got "
f"{original_max_position_embeddings} and max_position_embeddings={config.max_position_embeddings}"
)
# Like `ROPE_INIT_FUNCTIONS`, this validation function mapping can be dynamically updated for custom RoPE types.
ROPE_VALIDATION_FUNCTIONS = {
"default": _validate_default_rope_parameters,
"linear": _validate_linear_scaling_rope_parameters,
"dynamic": _validate_dynamic_scaling_rope_parameters,
"yarn": _validate_yarn_parameters,
"longrope": _validate_longrope_parameters,
"llama3": _validate_llama3_parameters,
}
def rope_config_validation(config: PretrainedConfig, ignore_keys: set | None = None):
"""
Validate the RoPE config arguments, given a `PretrainedConfig` object
"""
rope_scaling = getattr(config, "rope_scaling", None) # not a default parameter in `PretrainedConfig`
if rope_scaling is None:
return
# BC: "rope_type" was originally "type"
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", "default"))
validation_fn = ROPE_VALIDATION_FUNCTIONS.get(rope_type)
if validation_fn is not None:
validation_fn(config, ignore_keys=ignore_keys)
else:
logger.warning(
f"Missing validation function mapping in `ROPE_VALIDATION_FUNCTIONS` for 'rope_type'='{rope_type}'"
)
def rotate_half(x):
"""Rotates half the hidden dims of the input."""
x1 = x[..., : x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2 :]
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(x, cos, sin, position_ids=None, unsqueeze_dim=1):
"""Applies Rotary Position Embedding to the query and key tensors.
Args:
x (`torch.Tensor`): The input tensor.
cos (`torch.Tensor`): The cosine part of the rotary embedding.
sin (`torch.Tensor`): The sine part of the rotary embedding.
position_ids (`torch.Tensor`, *optional*):
Deprecated and unused.
unsqueeze_dim (`int`, *optional*, defaults to 1):
The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
Returns:
`tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
"""
cos = cos.unsqueeze(unsqueeze_dim)
sin = sin.unsqueeze(unsqueeze_dim)
x_embed = (x * cos) + (rotate_half(x) * sin)
return x_embed
@@ -0,0 +1,450 @@
# Copyright 2025 Xiaomi Corporation.
import typing as tp
import torch
import torch.distributed as dist
import torch.nn.functional as F
from einops import rearrange, repeat
from torch import nn
def rank():
if dist.is_initialized():
return dist.get_rank()
else:
return 0
def world_size():
if dist.is_initialized():
return dist.get_world_size()
else:
return 1
def default(val: tp.Any, d: tp.Any) -> tp.Any:
return val if val is not None else d
def ema_inplace(moving_avg, new, decay: float):
if dist.is_initialized():
dist.all_reduce(new, op=dist.ReduceOp.SUM)
moving_avg.data.mul_(decay).add_(new, alpha=(1 - decay))
def laplace_smoothing(x, n_categories: int, epsilon: float = 1e-5):
return (x + epsilon) / (x.sum() + n_categories * epsilon)
def uniform_init(*shape: int):
t = torch.empty(shape)
nn.init.kaiming_uniform_(t)
return t
def sample_vectors(samples, num: int):
num_samples, device = samples.shape[0], samples.device
if num_samples >= num:
indices = torch.randperm(num_samples, device=device)[:num]
else:
indices = torch.randint(0, num_samples, (num,), device=device)
selected_samples = samples[indices]
if dist.is_initialized():
dist.broadcast(selected_samples, src=0)
return selected_samples
def kmeans(samples, num_clusters: int, num_iters: int = 10):
dim, dtype = samples.shape[-1], samples.dtype
means = sample_vectors(samples, num_clusters)
for _ in range(num_iters):
dists = -(samples.pow(2).sum(1, keepdim=True) - 2 * samples @ means.t() + means.t().pow(2).sum(0, keepdim=True))
buckets = dists.max(dim=-1).indices
bins = torch.bincount(buckets, minlength=num_clusters)
new_means = buckets.new_zeros(num_clusters, dim, dtype=dtype)
new_means = new_means.scatter_add_(0, repeat(buckets, "n -> n d", d=dim), samples)
if dist.is_initialized():
dist.all_reduce(bins, op=dist.ReduceOp.SUM)
dist.all_reduce(new_means, op=dist.ReduceOp.SUM)
zero_mask = bins == 0
bins_min_clamped = bins.masked_fill(zero_mask, 1)
new_means = new_means / bins_min_clamped[..., None]
means = torch.where(zero_mask[..., None], means, new_means)
return means, bins
class EuclideanCodebook(nn.Module):
"""Codebook with Euclidean distance.
Args:
dim (int): Dimension.
codebook_size (int): Codebook size.
kmeans_init (bool): Whether to use k-means to initialize the codebooks.
If set to true, run the k-means algorithm on the first training batch and use
the learned centroids as initialization.
kmeans_iters (int): Number of iterations used for k-means algorithm at initialization.
decay (float): Decay for exponential moving average over the codebooks.
epsilon (float): Epsilon value for numerical stability.
threshold_ema_dead_code (int): Threshold for dead code expiration. Replace any codes
that have an exponential moving average cluster size less than the specified threshold with
randomly selected vector from the current batch.
"""
def __init__(
self,
dim: int,
codebook_size: int,
kmeans_init: int = False,
kmeans_iters: int = 10,
decay: float = 0.99,
epsilon: float = 1e-5,
threshold_ema_dead_code: int = 2,
):
super().__init__()
self.decay = decay
init_fn: tp.Callable[..., torch.Tensor] | tp.Any = uniform_init if not kmeans_init else torch.zeros
embed = init_fn(codebook_size, dim)
self.codebook_size = codebook_size
self.kmeans_iters = kmeans_iters
self.epsilon = epsilon
self.threshold_ema_dead_code = threshold_ema_dead_code
self.register_buffer("inited", torch.Tensor([not kmeans_init]))
self.register_buffer("cluster_size", torch.zeros(codebook_size))
self.register_buffer("embed", embed)
self.register_buffer("embed_avg", embed.clone())
@torch.jit.ignore
def init_embed_(self, data):
if self.inited:
return
embed, cluster_size = kmeans(data, self.codebook_size, self.kmeans_iters)
self.embed.data.copy_(embed)
self.embed_avg.data.copy_(embed.clone())
self.cluster_size.data.copy_(cluster_size)
self.inited.data.copy_(torch.Tensor([True]))
def replace_(self, samples, mask):
# modified_codebook = torch.where(
# mask[..., None], sample_vectors(samples, self.codebook_size), self.embed
# )
replace_num = mask.sum()
modified_codebook = self.embed.clone()
modified_codebook[mask] = sample_vectors(samples, replace_num)
self.embed.data.copy_(modified_codebook)
def expire_codes_(self, batch_samples):
if self.threshold_ema_dead_code == 0:
return
expired_codes = self.cluster_size < self.threshold_ema_dead_code
if not torch.any(expired_codes):
return
batch_samples = rearrange(batch_samples, "... d -> (...) d")
self.replace_(batch_samples, mask=expired_codes)
def preprocess(self, x):
x = rearrange(x, "... d -> (...) d")
return x
def quantize(self, x):
embed = self.embed.t()
dist = -(x.pow(2).sum(1, keepdim=True) - 2 * x @ embed + embed.pow(2).sum(0, keepdim=True))
embed_ind = dist.max(dim=-1).indices
return embed_ind
def postprocess_emb(self, embed_ind, shape):
return embed_ind.view(*shape[:-1])
def dequantize(self, embed_ind):
quantize = F.embedding(embed_ind, self.embed)
return quantize
def encode(self, x):
shape = x.shape
# pre-process
x = self.preprocess(x)
# quantize
embed_ind = self.quantize(x)
# post-process
embed_ind = self.postprocess_emb(embed_ind, shape)
return embed_ind
def decode(self, embed_ind):
quantize = self.dequantize(embed_ind)
return quantize
def forward(self, x):
shape, dtype = x.shape, x.dtype
x = self.preprocess(x)
self.init_embed_(x)
embed_ind = self.quantize(x)
embed_onehot = F.one_hot(embed_ind, self.codebook_size).type(dtype)
embed_ind = self.postprocess_emb(embed_ind, shape)
quantize = self.dequantize(embed_ind)
if self.training:
# We do the expiry of code at that point as buffers are in sync
# and all the workers will take the same decision.
self.expire_codes_(x)
ema_inplace(self.cluster_size, embed_onehot.sum(0), self.decay)
embed_sum = x.t() @ embed_onehot
ema_inplace(self.embed_avg, embed_sum.t().contiguous(), self.decay)
cluster_size = (
laplace_smoothing(self.cluster_size, self.codebook_size, self.epsilon) * self.cluster_size.sum()
)
embed_normalized = self.embed_avg / cluster_size.unsqueeze(1)
self.embed.data.copy_(embed_normalized)
return quantize, embed_ind
class VectorQuantization(nn.Module):
"""Vector quantization implementation.
Currently supports only euclidean distance.
Args:
dim (int): Dimension
codebook_size (int): Codebook size
codebook_dim (int): Codebook dimension. If not defined, uses the specified dimension in dim.
decay (float): Decay for exponential moving average over the codebooks.
epsilon (float): Epsilon value for numerical stability.
kmeans_init (bool): Whether to use kmeans to initialize the codebooks.
kmeans_iters (int): Number of iterations used for kmeans initialization.
threshold_ema_dead_code (int): Threshold for dead code expiration. Replace any codes
that have an exponential moving average cluster size less than the specified threshold with
randomly selected vector from the current batch.
commitment_weight (float): Weight for commitment loss.
"""
def __init__(
self,
dim: int,
codebook_size: int,
codebook_dim: int | None = None,
decay: float = 0.99,
epsilon: float = 1e-5,
kmeans_init: bool = True,
kmeans_iters: int = 50,
threshold_ema_dead_code: int = 2,
commitment_weight: float = 1.0,
):
super().__init__()
_codebook_dim: int = default(codebook_dim, dim)
requires_projection = _codebook_dim != dim
self.project_in = nn.Linear(dim, _codebook_dim) if requires_projection else nn.Identity()
self.project_out = nn.Linear(_codebook_dim, dim) if requires_projection else nn.Identity()
self.epsilon = epsilon
self.commitment_weight = commitment_weight
self._codebook = EuclideanCodebook(
dim=_codebook_dim,
codebook_size=codebook_size,
kmeans_init=kmeans_init,
kmeans_iters=kmeans_iters,
decay=decay,
epsilon=epsilon,
threshold_ema_dead_code=threshold_ema_dead_code,
)
self.codebook_size = codebook_size
@property
def codebook(self):
return self._codebook.embed
def encode(self, x):
# x = rearrange(x, "b d n -> b n d")
x = self.project_in(x)
embed_in = self._codebook.encode(x)
return embed_in
def decode(self, embed_ind):
quantize = self._codebook.decode(embed_ind)
quantize = self.project_out(quantize)
# quantize = rearrange(quantize, "b n d -> b d n")
return quantize
def forward(self, x):
device = x.device
x = self.project_in(x)
quantize, embed_ind = self._codebook(x)
if self.training:
quantize = x + (quantize - x).detach()
loss = torch.tensor([0.0], device=device, requires_grad=self.training)
if self.training:
if self.commitment_weight > 0:
commit_loss = F.mse_loss(quantize.detach(), x)
loss = loss + commit_loss * self.commitment_weight
quantize = self.project_out(quantize)
# quantize = rearrange(quantize, "b n d -> b d n")
return quantize, embed_ind, loss
class ResidualVectorQuantization(nn.Module):
"""Residual vector quantization implementation.
Follows Algorithm 1. in https://arxiv.org/pdf/2107.03312.pdf
"""
def __init__(self, *, num_quantizers, codebook_size, **kwargs):
super().__init__()
if isinstance(codebook_size, int):
codebook_size = [codebook_size] * num_quantizers
elif len(codebook_size) < num_quantizers:
codebook_size += [codebook_size[-1]] * (num_quantizers - len(codebook_size))
self.layers = nn.ModuleList(
[VectorQuantization(codebook_size=codebook_size[i], **kwargs) for i in range(num_quantizers)]
)
def forward(self, x, n_q: int | None = None, layers: list | None = None):
quantized_out = 0.0
residual = x
all_losses = []
all_indices = []
out_quantized = []
n_q = n_q or len(self.layers)
for i, layer in enumerate(self.layers[:n_q]):
quantized, indices, loss = layer(residual)
residual = residual - quantized
quantized_out = quantized_out + quantized
all_indices.append(indices)
all_losses.append(loss)
if layers and i in layers:
out_quantized.append(quantized_out)
out_losses, out_indices = map(torch.stack, (all_losses, all_indices))
return quantized_out, out_indices, out_losses, out_quantized
def encode(self, x: torch.Tensor, n_q: int | None = None, st: int | None = None) -> torch.Tensor:
residual = x
all_indices = []
n_q = len(self.layers) if n_q is None else n_q
st = 0 if st is None else st
for layer in self.layers[st:n_q]:
indices = layer.encode(residual)
quantized = layer.decode(indices)
residual = residual - quantized
all_indices.append(indices)
out_indices = torch.stack(all_indices)
return out_indices
def decode(self, q_indices: torch.Tensor, st: int = 0) -> torch.Tensor:
quantized_out = torch.tensor(0.0, device=q_indices.device)
for i, indices in enumerate(q_indices):
layer = self.layers[st + i]
quantized = layer.decode(indices)
quantized_out = quantized_out + quantized
return quantized_out
class ResidualVectorQuantizer(nn.Module):
"""Residual Vector Quantizer.
Args:
dimension (int): Dimension of the codebooks.
n_q (int): Number of residual vector quantizers used.
bins (int): Codebook size.
decay (float): Decay for exponential moving average over the codebooks.
kmeans_init (bool): Whether to use kmeans to initialize the codebooks.
kmeans_iters (int): Number of iterations used for kmeans initialization.
threshold_ema_dead_code (int): Threshold for dead code expiration. Replace any codes
that have an exponential moving average cluster size less than the specified threshold with
randomly selected vector from the current batch.
"""
def __init__(
self,
dimension: int = 256,
n_q: int = 8,
bins: int | list = 1024,
decay: float = 0.99,
kmeans_init: bool = True,
kmeans_iters: int = 50,
threshold_ema_dead_code: int = 2,
):
super().__init__()
self.n_q = n_q
self.dimension = dimension
self.bins = bins
self.decay = decay
self.kmeans_init = kmeans_init
self.kmeans_iters = kmeans_iters
self.threshold_ema_dead_code = threshold_ema_dead_code
self.vq = ResidualVectorQuantization(
dim=self.dimension,
codebook_size=self.bins,
num_quantizers=self.n_q,
decay=self.decay,
kmeans_init=self.kmeans_init,
kmeans_iters=self.kmeans_iters,
threshold_ema_dead_code=self.threshold_ema_dead_code,
)
def forward(
self,
x: torch.Tensor,
n_q: int | None = None,
layers: list | None = None,
):
"""Residual vector quantization on the given input tensor.
Args:
x (torch.Tensor): Input tensor.
n_q (int): Number of quantizer used to quantize. Default: All quantizers.
layers (list): Layer that need to return quantized. Default: None.
Returns:
QuantizedResult:
The quantized (or approximately quantized) representation with
the associated number quantizers and layer quantized required to return.
"""
n_q = n_q if n_q else self.n_q
quantized, codes, commit_loss, quantized_list = self.vq(x, n_q=n_q, layers=layers)
return quantized, codes, torch.mean(commit_loss), quantized_list
def encode(self, x: torch.Tensor, n_q: int | None = None, st: int | None = None) -> torch.Tensor:
"""Encode a given input tensor with the specified sample rate at the given bandwidth.
The RVQ encode method sets the appropriate number of quantizer to use
and returns indices for each quantizer.
Args:
x (torch.Tensor): Input tensor.
n_q (int): Number of quantizer used to quantize. Default: All quantizers.
st (int): Start to encode input from which layers. Default: 0.
"""
n_q = n_q if n_q else self.n_q
st = st or 0
codes = self.vq.encode(x, n_q=n_q, st=st)
return codes
def decode(self, codes: torch.Tensor, st: int = 0) -> torch.Tensor:
"""Decode the given codes to the quantized representation.
Args:
codes (torch.Tensor): Input indices for each quantizer.
st (int): Start to decode input codes from which layers. Default: 0.
"""
quantized = self.vq.decode(codes, st=st)
return quantized
@@ -63,6 +63,23 @@ _OMNI_MODELS = {
"qwen3_tts_code2wav",
"Qwen3TTSCode2Wav",
),
## mimo_audio
"MiMoAudioModel": (
"mimo_audio",
"mimo_audio",
"MiMoAudioForConditionalGeneration",
),
"MiMoAudioLLMModel": (
"mimo_audio",
"mimo_audio_llm",
"MiMoAudioLLMForConditionalGeneration",
),
"MiMoAudioToken2WavModel": (
"mimo_audio",
"mimo_audio_code2wav",
"MiMoAudioToken2WavForConditionalGenerationVLLM",
),
## glm_image
"GlmImageForConditionalGeneration": (
"glm_image",
"glm_image_ar",
@@ -0,0 +1,67 @@
# stage config for running mimo-audio with architecture of OmniLLM.
# The following config has been verified on 1x H20-96G GPU.
async_chunk: false
stage_args:
- stage_id: 0
stage_type: llm
runtime:
process: true # Run this stage in a separate process
devices: "0" # Visible devices for this stage (CUDA_VISIBLE_DEVICES/torch.cuda.set_device)
max_batch_size: 1
engine_args:
dtype: bfloat16
model_stage: fused_thinker_talker
model_arch: MiMoAudioForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.3
enforce_eager: true # need to discuss
trust_remote_code: true
enable_prefix_caching: false
engine_output_type: latent # change the param name,such as pooling_output
max_model_len: 8192
max_num_batched_tokens: 8192
is_comprehension: true
final_output: true
final_output_type: text
default_sampling_params:
temperature: 0.6
top_p: 0.95
top_k: 50
max_tokens: 18192
seed: 42
detokenize: True
repetition_penalty: 1.1
- stage_id: 1
stage_type: llm
runtime:
process: true # Run this stage in a separate process
devices: "0" # Visible devices for this stage (CUDA_VISIBLE_DEVICES/torch.cuda.set_device)
max_batch_size: 1
engine_args:
model_stage: code2wav
model_arch: MiMoAudioForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
gpu_memory_utilization: 0.2
enforce_eager: true
trust_remote_code: true
enable_prefix_caching: false
engine_output_type: audio
max_model_len: 18192
max_num_batched_tokens: 18192
async_scheduling: false
engine_input_source: [ 0 ]
is_comprehension: false
custom_process_input_func: vllm_omni.model_executor.stage_input_processors.mimo_audio.llm2code2wav
final_output: true
final_output_type: audio
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 18192
seed: 42
detokenize: false
@@ -0,0 +1,67 @@
# stage config for running mimo-audio with architecture of OmniLLM.
# The following config has been verified on 1x H20-96G GPU.
async_chunk: true
stage_args:
- stage_id: 0
stage_type: llm
runtime:
process: true # Run this stage in a separate process
devices: "0" # Visible devices for this stage (CUDA_VISIBLE_DEVICES/torch.cuda.set_device)
max_batch_size: 1
engine_args:
dtype: bfloat16
model_stage: fused_thinker_talker
model_arch: MiMoAudioForConditionalGeneration
worker_type: ar
scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
gpu_memory_utilization: 0.3
enforce_eager: true # need to discuss
trust_remote_code: true
enable_prefix_caching: false
engine_output_type: latent # change the param name,such as pooling_output
max_model_len: 8192
max_num_batched_tokens: 8192
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.mimo_audio.llm2code2wav_async_chunk
is_comprehension: true
final_output: true
final_output_type: text
default_sampling_params:
temperature: 0.6
top_p: 0.95
top_k: 50
max_tokens: 18192
seed: 42
detokenize: True
repetition_penalty: 1.1
- stage_id: 1
stage_type: llm
runtime:
process: true # Run this stage in a separate process
devices: "0" # Visible devices for this stage (CUDA_VISIBLE_DEVICES/torch.cuda.set_device)
max_batch_size: 1
engine_args:
model_stage: code2wav
model_arch: MiMoAudioForConditionalGeneration
worker_type: generation
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
gpu_memory_utilization: 0.2
enforce_eager: true
trust_remote_code: true
async_scheduling: false
enable_prefix_caching: false
engine_output_type: audio
max_model_len: 8192
max_num_batched_tokens: 8192
engine_input_source: [ 0 ]
is_comprehension: false
final_output: true
final_output_type: audio
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 18192
seed: 42
detokenize: false
@@ -0,0 +1,206 @@
from typing import Any
import torch
from vllm.inputs import TextPrompt
from vllm.logger import init_logger
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID
logger = init_logger(__name__)
def prepend_and_flatten_colmajor(x: torch.Tensor, pad_vec: torch.Tensor) -> torch.Tensor:
"""
Prepend a padding vector to the input tensor and flatten in column-major order.
This function expands the padding vector to match the batch dimensions of the input
tensor, prepends it to the row dimension, and then flattens the result in column-major
order (transposing before flattening).
Args:
x: Input tensor with shape (..., R, C) where R is the row dimension and C is
the column dimension. Example: (B, 1, 8, 4) where B is batch size.
pad_vec: Padding vector with shape (C,) to be prepended to x. The vector will
be expanded to match the batch dimensions of x.
Returns:
A flattened 1D tensor in column-major order with shape (-1,). The result
contains the padded row followed by all rows of x, flattened column by column.
"""
pad_row = pad_vec.view(1, -1)
# Expand pad_row to the front of x, keeping other batch dimensions consistent
# Example: x shape = (B,1,R,C) → pad shape = (B,1,1,C)
pad_expand = pad_row.view(*([1] * (x.dim() - 2)), 1, x.size(-1)).expand(*x.shape[:-2], 1, x.size(-1))
# Prepend to the row dimension
y = torch.cat([pad_expand, x], dim=-2) # (..., R+1, C)
# Flatten in column-major order:
# First transpose (..., R+1, C) → (..., C, R+1)
# Then flatten
y_col_major = y.permute(*range(y.dim() - 2), -1, -2).reshape(-1)
return y_col_major
def _make_finished_sentinel() -> dict[str, Any]:
"""Return a minimal payload with finished=True so Stage-1 can end the request."""
return {"code_predictor_codes": [], "finished": torch.tensor(True, dtype=torch.bool)}
def llm2code2wav_async_chunk(
transfer_manager: Any, pooling_output: dict[str, Any], request: Any
) -> dict[str, Any] | None:
"""
Async chunk version: convert stage-0 pooling_output to code2wav payload (pooling / connector accumulation).
Accumulates codes in connector per request_id,
returns payload only when chunk_size is full or request is finished; returns None when waiting.
"""
if "code_predictor_codes" not in pooling_output:
if request.is_finished():
return _make_finished_sentinel()
return None
code_predictor_codes = pooling_output["code_predictor_codes"]
if code_predictor_codes is None:
if request.is_finished():
return _make_finished_sentinel()
return None
if isinstance(code_predictor_codes, torch.Tensor):
if code_predictor_codes.numel() == 0:
if request.is_finished():
return _make_finished_sentinel()
return None
elif hasattr(code_predictor_codes, "__len__"):
if len(code_predictor_codes) == 0:
return None
if isinstance(code_predictor_codes, torch.Tensor):
if not code_predictor_codes.any():
if request.is_finished():
return _make_finished_sentinel()
return None
code_tensor = code_predictor_codes.to(torch.long)
else:
code_tensor = torch.tensor(code_predictor_codes, dtype=torch.long)
if not code_tensor.any():
if request.is_finished():
return _make_finished_sentinel()
return None
if code_tensor.ndim == 3:
code_tensor = code_tensor.unsqueeze(0)
if code_tensor.ndim != 4 or code_tensor.shape[-2:] != (8, 4):
return None
pad_vec = torch.tensor([TALKER_CODEC_PAD_TOKEN_ID] * 4, device=code_tensor.device, dtype=code_tensor.dtype)
code_final = prepend_and_flatten_colmajor(code_tensor, pad_vec)
code_list = code_final.tolist()
if sum(code_list) == 0:
if request.is_finished():
return _make_finished_sentinel()
return None
request_id = getattr(request, "external_req_id", None)
if request_id is None:
return None
chunk_size = left_context_size = 10
transfer_manager.code_prompt_token_ids[request_id].append(code_list)
length = len(transfer_manager.code_prompt_token_ids[request_id])
chunk_length = length % chunk_size
if chunk_length != 0 and not request.is_finished():
return None
context_length = chunk_length if chunk_length != 0 else chunk_size
end_index = min(length, left_context_size + context_length)
info = {
"code_predictor_codes": (
torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:]).reshape(-1).tolist()
),
"finished": torch.tensor(request.is_finished(), dtype=torch.bool),
}
return info
def llm2code2wav(
stage_list: list[Any],
engine_input_source: list[int],
prompt: OmniTokensPrompt | TextPrompt | None = None,
requires_multimodal_data: bool = False,
) -> list[OmniTokensPrompt]:
"""
Process talker outputs to create code2wav inputs.
Workflow:
1. Extract talker's codec code outputs (8-layer RVQ codes)
2. Flatten codes for code2wav input
3. Package for code2wav stage
Args:
stage_list: List of stage objects
engine_input_source: Source stage IDs (typically [1] for talker)
prompt: Original prompt data
requires_multimodal_data: Whether multimodal data is required
Returns:
List of OmniTokensPrompt for code2wav stage
"""
if not engine_input_source:
raise ValueError("engine_input_source cannot be empty")
source_stage_id = engine_input_source[0]
if source_stage_id >= len(stage_list):
raise IndexError(f"Invalid stage_id: {source_stage_id}")
if stage_list[source_stage_id].engine_outputs is None:
raise RuntimeError(f"Stage {source_stage_id} has no outputs yet")
talker_outputs = stage_list[source_stage_id].engine_outputs
code2wav_inputs = []
# Process each talker output
for i, talker_output in enumerate(talker_outputs):
output = talker_output.outputs[0]
# Extract codec codes from talker output
# Expected shape: [8, seq_len] (8-layer RVQ codes)
if "code_predictor_codes" in output.multimodal_output:
codec_codes = output.multimodal_output["code_predictor_codes"].to(torch.long) # [seq_batch_size, 1, 8, 4]
is_all_zero = (codec_codes == 0).all(dim=(1, 2, 3))
non_zero_indices = (~is_all_zero).nonzero(as_tuple=True)[0]
if len(non_zero_indices) == 0:
# All codec codes are zero - skip this request with a warning
request_id = getattr(talker_output, "request_id", f"unknown_{i}")
logger.warning(
"Skipping request %s: all codec codes are zero (empty output from Stage-0). "
"This may indicate the model failed to generate valid audio codes.",
request_id,
)
else:
if len(non_zero_indices) < codec_codes.shape[0]:
codec_codes = codec_codes[non_zero_indices]
elif "latent" in output.multimodal_output and "code_predictor_codes" not in output.multimodal_output:
codec_codes = torch.zeros(1, 1, 8, 4, dtype=torch.long)
else:
raise ValueError(f"Invalid multimodal_output: {output.multimodal_output}")
pad_vec = torch.tensor([TALKER_CODEC_PAD_TOKEN_ID] * 4)
code_final = prepend_and_flatten_colmajor(codec_codes, pad_vec)
code_final = code_final.tolist()
code2wav_inputs.append(
OmniTokensPrompt(
prompt_token_ids=code_final,
multi_modal_data=None,
mm_processor_kwargs=None,
)
)
return code2wav_inputs
+27
View File
@@ -1013,6 +1013,29 @@ class OmniGPUModelRunner(GPUModelRunner):
for req_id, req_infos in cached_infos.items():
self._update_request_information(req_id, req_infos)
def _maybe_attach_mimo_audio_req_infos(
self,
req_state: CachedRequestState | None,
req_infos: dict | None,
req_id: str,
) -> dict | None:
"""Attach MiMoAudio-specific fields into req_infos if applicable.
This helper is intentionally small and self-contained so that it can be
unit-tested to prevent regressions when updating MiMoAudio handling.
"""
if req_state is None or self.model.__class__.__name__ != "MiMoAudioForConditionalGeneration":
return req_infos
# Always operate on a dict copy to avoid mutating shared instances.
req_infos = dict(req_infos) if isinstance(req_infos, dict) else {}
mm_features = getattr(req_state, "mm_features", None)
if mm_features and (not req_infos.get("mm_features")):
req_infos["mm_features"] = mm_features
req_infos["req_id"] = req_id
return req_infos
def _preprocess(
self,
scheduler_output: "SchedulerOutput",
@@ -1137,6 +1160,10 @@ class OmniGPUModelRunner(GPUModelRunner):
for req_index, req_id in enumerate(self.input_batch.req_ids):
req_state = self.requests.get(req_id)
req_infos = getattr(req_state, "additional_information_cpu", None) if req_state is not None else None
# mimo-audio check
req_infos = self._maybe_attach_mimo_audio_req_infos(req_state, req_infos, req_id)
start_offset = int(self.query_start_loc.cpu[req_index])
sched_tokens = int(num_scheduled_tokens_np[req_index])
s, e = start_offset, start_offset + sched_tokens