[Model] Add Ming-omni-tts dense 0.5B pipeline (#2906)
Signed-off-by: akshatvishu <akshatnayak197@gmail.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
39ae6e12f8
commit
2fa4c7a4a9
55 files changed
+6263
-549
No files matched your search
@@ -228,6 +228,27 @@ steps:
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "TTS · Ming-Omni-TTS Test"
|
||||
timeout_in_minutes: 20
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c "
|
||||
export VLLM_LOGGING_LEVEL=DEBUG
|
||||
pytest -s -v tests/e2e/online_serving/test_ming_tts.py tests/e2e/offline_inference/test_ming_tts.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
"
|
||||
agents:
|
||||
queue: "gpu_1_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
shm-size: "8gb"
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "TTS · Voxtral-TTS Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/offline_inference/test_voxtral_tts.py
|
||||
|
||||
@@ -63,6 +63,7 @@ th {
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-CustomVoice | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-VoiceDesign | `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
|
||||
| `MingTTSForConditionalGeneration` | Ming-omni-tts dense 0.5B | `inclusionAI/Ming-omni-tts-0.5B` | ✅︎ | | | |
|
||||
| `GLMTTSForConditionalGeneration` | GLM-TTS | `zai-org/GLM-TTS` | ✅︎ | | | |
|
||||
| `MossTTSNanoForCausalLM` | MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✅︎ | | | |
|
||||
| `MossTTSDelayModel` | MOSS-TTS, MOSS-TTSD, MOSS-SoundEffect, MOSS-VoiceGenerator | `OpenMOSS-Team/MOSS-VoiceGenerator` | ✅︎ | | | |
|
||||
|
||||
@@ -17,6 +17,7 @@ list of supported architectures across all modalities, see
|
||||
| CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | 2 (talker + code2wav) | ✓ | ✓ | — | 24 kHz |
|
||||
| Fish Speech S2 Pro | `fishaudio/s2-pro` | dual-AR | ✓ | ✓ | — | 44.1 kHz |
|
||||
| GLM-TTS | `zai-org/GLM-TTS` | 2 (AR + DiT) | ✓ (required) | ✓ | — | 24 kHz |
|
||||
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | 2 (AR + audio VAE) | ✓ | ✓ | style / IP / dialect / TTA / podcast | 44.1 kHz |
|
||||
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | single (talker only) | — (caption-controlled) | — | style / IP / basic captions | 44.1 kHz |
|
||||
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | single (AR + codec) | ✓ (required) | ✓ | voice_clone, continuation | 48 kHz |
|
||||
| OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design, language hint | 24 kHz |
|
||||
@@ -159,6 +160,46 @@ Streaming requires `async_chunk: true` in the stage config.
|
||||
|
||||
---
|
||||
|
||||
## Ming-omni-tts
|
||||
|
||||
Dense 0.5B two-stage TTS pipeline (`AR + flow` + audio VAE) at 44.1 kHz. The example covers style, IP voice, music-only generation, text-to-audio events, emotion, dialect, zero-shot cloning, podcast, speech+BGM, and speech+environment-sound cases.
|
||||
|
||||
### Quick start
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case style \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
### Voice cloning
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case zero_shot \
|
||||
--ref-audio /path/to/reference.wav \
|
||||
--ref-text "在此奉劝大家别乱打美白针。" \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
### Streaming
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case basic \
|
||||
--ref-audio /path/to/reference.wav \
|
||||
--streaming \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
### Notes
|
||||
- `style`, `ip`, `bgm`, and `tta` do not require reference audio.
|
||||
- Reference-audio cases use `--ref-audio`; `zero_shot` also requires `--ref-text`.
|
||||
- `podcast` uses multiple references via `--ref-audio-paths`.
|
||||
- Full case details live in [`ming_tts/README.md`](ming_tts/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Ming-flash-omni-TTS
|
||||
|
||||
Standalone talker-only deployment of Ming-flash-omni-2.0 at 44.1 kHz. Voice is controlled through caption fields (`风格` / `IP` / `语速`/`基频`/`音量`) rather than reference audio.
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# Ming-omni-tts Offline Inference
|
||||
|
||||
`end2end.py` runs Ming dense 0.5B end to end with vLLM-Omni. It uses the in-repo Ming prompt assembly helper directly, so the example request shape matches the real integration instead of a simplified wrapper.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `end2end.py` | Driver: CLI, case loading, prompt construction, orchestration (~150 lines) |
|
||||
| `cases.yaml` | All 11 built-in case definitions (prompt, text, instruction, ref-audio flags, flow controls) |
|
||||
| `runner.py` | Engine management and audio output (streaming + blocking paths) |
|
||||
|
||||
## Model Overview
|
||||
|
||||
Ming dense 0.5B is exposed here as a two-stage offline pipeline:
|
||||
|
||||
- **Stage 0**: Qwen2-based AR generation with Ming prompt formatting and inline flow controls
|
||||
- **Stage 1**: audio VAE decode to mono 44.1 kHz waveform
|
||||
|
||||
`config_ming_tts.py` adapts the checkpoint's HuggingFace config fields
|
||||
(LLM, DiT, aggregator, AudioVAE, token ids). `vllm_omni/deploy/ming_tts.yaml`
|
||||
selects the vLLM-Omni pipeline and stage runtime topology, including
|
||||
connectors, async chunking, memory limits, and sampling defaults.
|
||||
|
||||
The example supports both:
|
||||
|
||||
- **Blocking eager** via `vllm_omni/deploy/ming_tts.yaml`
|
||||
- **Async chunk eager** via `vllm_omni/deploy/ming_tts.yaml` (default `async_chunk: true`)
|
||||
|
||||
## Setup
|
||||
|
||||
Install vLLM-Omni with the platform requirements for your accelerator:
|
||||
|
||||
```bash
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
The Ming offline example does not require a separate upstream Ming package.
|
||||
Reference-audio cases use the repo dependencies for audio loading,
|
||||
resampling, and CampPlus speaker extraction, including `soundfile`,
|
||||
`torchaudio`, and `onnxruntime-rocm` in the documented ROCm environment.
|
||||
|
||||
The tested ROCm environment is summarized in the
|
||||
[Ming recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/inclusionAI/Ming-omni-tts-0.5B.md).
|
||||
|
||||
## Supported Cases
|
||||
|
||||
These cases cover the upstream dense 0.5B cookbook surface that maps cleanly onto the current vLLM-Omni example:
|
||||
|
||||
- `style`: zero-speaker style-conditioned speech
|
||||
- `ip`: zero-speaker IP voice generation
|
||||
- `bgm`: music-only generation
|
||||
- `tta`: text-to-audio event generation with FlowLoss controls
|
||||
- `emotion`: reference-audio speech with emotion control
|
||||
- `basic`: reference-audio speech with speed / pitch / volume control
|
||||
- `dialect`: reference-audio speech with dialect control
|
||||
- `zero_shot`: reference-audio cloning with explicit transcript
|
||||
- `podcast`: multi-reference dialogue generation with automatic speaker embedding extraction
|
||||
- `speech_bgm`: speech with background music conditioning
|
||||
- `speech_sound`: speech with environmental sound conditioning
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run the zero-speaker style example:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case style \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
Run zero-shot cloning with a transcript:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case zero_shot \
|
||||
--ref-audio /path/to/10002287-00000094.wav \
|
||||
--ref-text "在此奉劝大家别乱打美白针。" \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
Run emotion-controlled speech:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case emotion \
|
||||
--ref-audio /path/to/emotion_prompt.wav \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
Run podcast generation with two reference clips:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case podcast \
|
||||
--ref-audio-paths /path/to/CTS-CN-F2F-2019-11-11-423-012-A.wav /path/to/CTS-CN-F2F-2019-11-11-423-012-B.wav \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
The script automatically extracts one 192-d speaker embedding per reference WAV using the Ming model's `campplus.onnx`.
|
||||
|
||||
If you already have precomputed multi-speaker embeddings, you can override extraction with:
|
||||
|
||||
```bash
|
||||
--speaker-embedding /path/to/podcast_speaker_embeddings.json
|
||||
```
|
||||
|
||||
where the JSON is a list of speaker embeddings, one 192-d vector per speaker.
|
||||
|
||||
Run text-to-audio event generation:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case tta \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
Use async_chunk streaming:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case basic \
|
||||
--ref-audio /path/to/10002287-00000095.wav \
|
||||
--streaming \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
`--streaming` uses `AsyncOmni` and the async_chunk stage config. It currently
|
||||
supports one prompt per process invocation; use blocking mode for
|
||||
`--num-prompts > 1`.
|
||||
|
||||
Collect runtime stats and a manifest:
|
||||
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/ming_tts/end2end.py \
|
||||
--case style \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--enforce-eager \
|
||||
--enable-stats \
|
||||
--stats-log-file output_audio/ming_style_pipeline.log \
|
||||
--metadata-json output_audio/ming_style_manifest.json
|
||||
```
|
||||
|
||||
## Reference Fixtures
|
||||
|
||||
The upstream Ming cookbook uses these public audio fixtures from `inclusionAI/Ming-omni-tts/data/wavs`:
|
||||
|
||||
- `10002287-00000094.wav` for zero-shot cloning
|
||||
- `10002287-00000095.wav` for `basic`
|
||||
- `emotion_prompt.wav` for `emotion`
|
||||
- `yue_prompt.wav` for `dialect`
|
||||
- `00000309-00000300.wav` for `speech_bgm` and `speech_sound`
|
||||
- `CTS-CN-F2F-2019-11-11-423-012-A.wav` and `CTS-CN-F2F-2019-11-11-423-012-B.wav` for `podcast`
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
The repo-facing example is intended to cover the same dense TTS workflows used
|
||||
by the local Ming validation script:
|
||||
|
||||
| Case | Blocking `deploy/ming_tts.yaml` | Async chunk `deploy/ming_tts.yaml` | Extra inputs |
|
||||
|---|---:|---:|---|
|
||||
| `style` | Yes | Optional smoke test | none |
|
||||
| `ip` | Yes | Optional smoke test | none |
|
||||
| `bgm` | Yes | Optional smoke test | none |
|
||||
| `tta` | Yes | Optional smoke test | none |
|
||||
| `emotion` | Yes | Yes | `--ref-audio emotion_prompt.wav` |
|
||||
| `basic` | Yes | Yes | `--ref-audio 10002287-00000095.wav` |
|
||||
| `dialect` | Yes | Yes | `--ref-audio yue_prompt.wav` |
|
||||
| `zero_shot` | Yes | Yes | `--ref-audio 10002287-00000094.wav --ref-text ...` |
|
||||
| `podcast` | Yes | Yes | two `--ref-audio-paths` |
|
||||
| `speech_bgm` | Yes | Yes | `--ref-audio 00000309-00000300.wav` |
|
||||
| `speech_sound` | Yes | Yes | `--ref-audio 00000309-00000300.wav` |
|
||||
|
||||
## Validated Outputs
|
||||
|
||||
The following measurements are retained from an earlier L4 CUDA validation;
|
||||
they are not ROCm benchmark results. Default async_chunk matched blocking
|
||||
output frame counts and Stage-1 patch counts for every case:
|
||||
|
||||
| Case | Blocking frames / patches / sec | Async chunk frames / patches / sec |
|
||||
|---|---:|---:|
|
||||
| `style` | 409248 / 29 / 9.28 | 409248 / 29 / 9.28 |
|
||||
| `ip` | 183456 / 13 / 4.16 | 183456 / 13 / 4.16 |
|
||||
| `bgm` | 1326528 / 94 / 30.08 | 1326528 / 94 / 30.08 |
|
||||
| `tta` | 465696 / 33 / 10.56 | 465696 / 33 / 10.56 |
|
||||
| `emotion` | 324576 / 23 / 7.36 | 324576 / 23 / 7.36 |
|
||||
| `basic` | 211680 / 15 / 4.80 | 211680 / 15 / 4.80 |
|
||||
| `dialect` | 239904 / 17 / 5.44 | 239904 / 17 / 5.44 |
|
||||
| `zero_shot` | 409248 / 29 / 9.28 | 409248 / 29 / 9.28 |
|
||||
| `podcast` | 437472 / 31 / 9.92 | 437472 / 31 / 9.92 |
|
||||
| `speech_bgm` | 296352 / 21 / 6.72 | 296352 / 21 / 6.72 |
|
||||
| `speech_sound` | 352800 / 25 / 8.00 | 352800 / 25 / 8.00 |
|
||||
|
||||
## Key Arguments
|
||||
|
||||
| Argument | Description |
|
||||
|---|---|
|
||||
| `--model` | Hugging Face repo or local Ming checkpoint path |
|
||||
| `--deploy-config` | Deploy config YAML. Use `vllm_omni/deploy/ming_tts.yaml` |
|
||||
| `--case` | Built-in demo case |
|
||||
| `--ref-audio` | Single reference wav path for cloning-style cases |
|
||||
| `--ref-audio-paths` | Multiple reference wav paths, used by `podcast` |
|
||||
| `--ref-text` | Reference transcript. Required for `zero_shot` |
|
||||
| `--instructions` | Free-form Ming instruction string |
|
||||
| `--instruction-json` | Structured Ming instruction JSON |
|
||||
| `--speaker-embedding` | JSON file containing a 192-d speaker embedding |
|
||||
| `--extract-speaker-embeddings` | Force CampPlus speaker extraction from the provided reference audio paths |
|
||||
| `--max-decode-steps` | Override `ming_max_decode_steps` |
|
||||
| `--num-prompts` | Repeat the same case N times. Outputs are indexed when `N > 1` |
|
||||
| `--streaming` | Use `AsyncOmni` and async_chunk transport |
|
||||
| `--enforce-eager` | Recommended for Ming dense; non-eager is out of scope |
|
||||
| `--enable-stats` / `--log-stats` | Enable vLLM-Omni per-request stats logging |
|
||||
| `--stats-log-file` | Optional path for the stats log |
|
||||
| `--metadata-json` | Optional path for the run manifest JSON |
|
||||
| `--stage-init-timeout` | Per-stage initialization timeout in seconds |
|
||||
| `--init-timeout` | Total initialization timeout in seconds |
|
||||
| `--batch-timeout` | Batch timeout in seconds |
|
||||
| `--worker-backend` | `multi_process` or `ray` |
|
||||
| `--ray-address` | Ray cluster address when using `--worker-backend ray` |
|
||||
|
||||
## Output
|
||||
|
||||
- The script writes one mono 44.1 kHz WAV file per run
|
||||
- Default output directory: `output_audio/`
|
||||
- Default filename: `ming_<case>.wav`
|
||||
- When `--num-prompts > 1`, outputs are indexed as `ming_<case>_00000.wav`, `..._00001.wav`, etc.
|
||||
- When stats are enabled, the script can also write:
|
||||
- a stats log file such as `ming_style_pipeline.log`
|
||||
- a manifest JSON with per-output metadata, stage durations, peak memory info,
|
||||
and streaming client latency metrics when `--streaming` is used
|
||||
|
||||
## Notes
|
||||
|
||||
- `style` and `ip` are zero-speaker paths and do not require a reference clip
|
||||
- `emotion`, `basic`, `dialect`, `speech_bgm`, and `speech_sound` require one reference clip
|
||||
- `zero_shot` requires both `--ref-audio` and `--ref-text`
|
||||
- `podcast` requires at least two reference clips via `--ref-audio-paths`
|
||||
- `podcast` automatically extracts one speaker embedding per reference clip
|
||||
- `--speaker-embedding` may contain either one 192-d vector or a list of 192-d vectors
|
||||
- `--enforce-eager` was used for the validated runs
|
||||
- The earlier L4 validation used SDPA for the Ming audio VAE instead of
|
||||
FlashAttention2, which is the preferred default when available.
|
||||
@@ -0,0 +1,110 @@
|
||||
style:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "我会一直在这里陪着你,直到你慢慢、慢慢地沉入那个最温柔的梦里……好吗?"
|
||||
instruction:
|
||||
风格: >-
|
||||
这是一种ASMR耳语,属于一种旨在引发特殊感官体验的创意风格。这个女性使用轻柔的普通话进行耳语,声音气音成分重。音量极低,紧贴麦克风,语速极慢,旨在制造触发听者颅内快感的声学刺激。
|
||||
use_zero_spk_emb: true
|
||||
max_decode_steps: 200
|
||||
|
||||
ip:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "这款产品的名字,叫变态坑爹牛肉丸。"
|
||||
instruction:
|
||||
IP: "灵小甄"
|
||||
use_zero_spk_emb: true
|
||||
max_decode_steps: 200
|
||||
|
||||
bgm:
|
||||
prompt: "Please generate music based on the following description.\n"
|
||||
text: " Genre: 电子舞曲. Mood: 自信 / 坚定. Instrument: 架子鼓. Theme: 节日. Duration: 30s."
|
||||
instruction: null
|
||||
use_zero_spk_emb: false
|
||||
max_decode_steps: 400
|
||||
|
||||
tta:
|
||||
prompt: "Please generate audio events based on given text.\n"
|
||||
text: "Thunder and a gentle rain"
|
||||
instruction: null
|
||||
use_zero_spk_emb: false
|
||||
max_decode_steps: 200
|
||||
cfg: 4.5
|
||||
sigma: 0.3
|
||||
temperature: 2.5
|
||||
|
||||
emotion:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "我竟然抢到了陈奕迅的演唱会门票!太棒了!终于可以现场听一听他的歌声了!"
|
||||
instruction:
|
||||
情感: "高兴"
|
||||
requires_ref_audio: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
basic:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "简单地说,这相当于惠普把消费领域市场拱手相让了。"
|
||||
instruction:
|
||||
语速: "快速"
|
||||
基频: "中"
|
||||
音量: "中"
|
||||
requires_ref_audio: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
dialect:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "我觉得社会企业同个人都有责任"
|
||||
instruction:
|
||||
方言: "广粤话"
|
||||
requires_ref_audio: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
zero_shot:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "我们的愿景是构建未来服务业的数字化基础设施,为世界带来更多微小而美好的改变。"
|
||||
instruction: null
|
||||
requires_ref_audio: true
|
||||
requires_ref_text: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
podcast:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: " speaker_1:你可以说一下,就大概说一下,可能虽然我也不知道,我看过那部电影没有。\n speaker_2:就是那个叫什么,变相一节课的嘛。\n speaker_1:嗯。\n speaker_2:一部搞笑的电影。\n speaker_1:一部搞笑的。\n"
|
||||
instruction: null
|
||||
prompt_text: " speaker_1:并且我们还要进行每个月还要考核 笔试的话还要进行笔试,做个,当服务员还要去笔试了\n speaker_2:对啊,这真的很奇怪,就是 单纯的因,单纯自己工资不高,只是因为可能人家那个店比较出名一点,就对你苛刻要求\n"
|
||||
requires_ref_audio_count: 2
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
speech_bgm:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "此次业绩下滑原因,可归结为企业停止服务某些品牌,而带来的负面影响。"
|
||||
instruction:
|
||||
BGM:
|
||||
Genre: "当代古典音乐."
|
||||
Mood: "温暖 / 友善."
|
||||
Instrument: "电吉他"
|
||||
Theme: "节日."
|
||||
SNR: 10.0
|
||||
ENV: null
|
||||
requires_ref_audio: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
|
||||
speech_sound:
|
||||
prompt: "Please generate speech based on the following description.\n"
|
||||
text: "此次业绩下滑原因,可归结为企业停止服务某些品牌,而带来的负面影响。"
|
||||
instruction:
|
||||
BGM:
|
||||
ENV: "Birds chirping"
|
||||
SNR: 10.0
|
||||
Genre: null
|
||||
Mood: null
|
||||
Instrument: null
|
||||
Theme: null
|
||||
requires_ref_audio: true
|
||||
auto_extract_speaker_embeddings: true
|
||||
max_decode_steps: 200
|
||||
@@ -0,0 +1,200 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Offline inference demo for Ming-omni-tts via vLLM Omni."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
import torchaudio
|
||||
import yaml
|
||||
from transformers import AutoTokenizer
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
KEY_CFG,
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
KEY_SIGMA,
|
||||
KEY_TEMPERATURE,
|
||||
SAMPLE_RATE,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_tts.prompt_assembly import build_ming_dense_prompt
|
||||
from vllm_omni.model_executor.models.ming_tts.speaker_extractor import MingSpeakerEmbeddingExtractor
|
||||
|
||||
try:
|
||||
from .runner import (
|
||||
build_manifest,
|
||||
build_sampling_params,
|
||||
resolve_metadata_json,
|
||||
resolve_stats_log_file,
|
||||
run_generation,
|
||||
)
|
||||
except ImportError:
|
||||
from runner import (
|
||||
build_manifest,
|
||||
build_sampling_params,
|
||||
resolve_metadata_json,
|
||||
resolve_stats_log_file,
|
||||
run_generation,
|
||||
)
|
||||
|
||||
_DEFAULT_MODEL = "inclusionAI/Ming-omni-tts-0.5B"
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||
_DEFAULT_DEPLOY_CONFIG = str(_REPO_ROOT / "vllm_omni/deploy/ming_tts.yaml")
|
||||
_CASES_FILE = Path(__file__).with_name("cases.yaml")
|
||||
|
||||
CASE_DEFAULTS = yaml.safe_load(_CASES_FILE.read_text(encoding="utf-8")) or {}
|
||||
if not CASE_DEFAULTS:
|
||||
raise RuntimeError(f"Empty or missing case definitions in {_CASES_FILE}")
|
||||
|
||||
|
||||
def _build_parser() -> FlexibleArgumentParser:
|
||||
p = FlexibleArgumentParser(description="Offline Ming-omni-tts example")
|
||||
p.add_argument("--model", default=_DEFAULT_MODEL, help="Model name or local path")
|
||||
p.add_argument("--deploy-config", default=None, help="Deploy config YAML; auto-selected when omitted")
|
||||
p.add_argument("--case", choices=sorted(CASE_DEFAULTS), default="style", help="Built-in demo case")
|
||||
p.add_argument("--text", default=None, help="Override case text")
|
||||
p.add_argument("--prompt", default=None, help="Override the system prompt prefix")
|
||||
p.add_argument("--instructions", default=None, help="Free-form Ming instruction string")
|
||||
p.add_argument(
|
||||
"--instruction-json", default=None, help='Structured Ming instruction JSON, e.g. \'{"方言":"广粤话"}\''
|
||||
)
|
||||
p.add_argument("--ref-audio", default=None, help="Single reference audio path for cloning-style cases")
|
||||
p.add_argument("--ref-audio-paths", nargs="+", default=None, help="Multiple reference audio paths (podcast)")
|
||||
p.add_argument("--ref-text", default=None, help="Reference transcript; required for zero_shot")
|
||||
p.add_argument("--speaker-embedding", default=None, help="Path to a JSON speaker embedding file")
|
||||
p.add_argument(
|
||||
"--extract-speaker-embeddings",
|
||||
action="store_true",
|
||||
help="Extract speaker embeddings from ref audio via campplus.onnx",
|
||||
)
|
||||
p.add_argument("--max-decode-steps", type=int, default=None, help="Override ming_max_decode_steps")
|
||||
p.add_argument("--output-dir", default="output_audio", help="Directory for output wav files")
|
||||
p.add_argument("--output-name", default=None, help="Output wav filename")
|
||||
p.add_argument("--num-prompts", type=int, default=1, help="Repeat the same prompt N times")
|
||||
p.add_argument("--streaming", action="store_true", help="Use AsyncOmni with async_chunk streaming")
|
||||
p.add_argument("--trust-remote-code", action="store_true")
|
||||
p.add_argument("--enforce-eager", action="store_true")
|
||||
p.add_argument(
|
||||
"--log-stats", "--enable-stats", dest="log_stats", action="store_true", help="Enable Omni stats logging"
|
||||
)
|
||||
p.add_argument("--stats-log-file", default=None, help="Path for the Omni stats log file")
|
||||
p.add_argument("--metadata-json", default=None, help="Path for the run manifest JSON")
|
||||
p.add_argument("--stage-init-timeout", type=int, default=300, help="Per-stage init timeout (s)")
|
||||
p.add_argument("--init-timeout", type=int, default=600, help="Total init timeout (s)")
|
||||
p.add_argument("--batch-timeout", type=int, default=5, help="Batch timeout (s)")
|
||||
p.add_argument("--shm-threshold-bytes", type=int, default=65536)
|
||||
p.add_argument("--worker-backend", default="multi_process", choices=["multi_process", "ray"])
|
||||
p.add_argument("--ray-address", default=None, help="Ray cluster address (--worker-backend ray)")
|
||||
return p
|
||||
|
||||
|
||||
def _finalize_args(args) -> None:
|
||||
if args.instructions is not None and args.instruction_json is not None:
|
||||
raise RuntimeError("Use either --instructions or --instruction-json, not both")
|
||||
if args.num_prompts < 1:
|
||||
raise RuntimeError("--num-prompts must be at least 1")
|
||||
if args.streaming and args.num_prompts != 1:
|
||||
raise RuntimeError("--streaming currently supports exactly one prompt")
|
||||
if args.deploy_config is None:
|
||||
args.deploy_config = _DEFAULT_DEPLOY_CONFIG
|
||||
|
||||
|
||||
def _load_waveform(path: str) -> torch.Tensor:
|
||||
samples, sr = sf.read(path, dtype="float32")
|
||||
wav = torch.as_tensor(samples, dtype=torch.float32)
|
||||
if wav.ndim == 2:
|
||||
wav = wav.mean(dim=1)
|
||||
wav = wav.reshape(1, -1)
|
||||
if int(sr) != SAMPLE_RATE:
|
||||
wav = torchaudio.functional.resample(wav, int(sr), SAMPLE_RATE)
|
||||
return wav
|
||||
|
||||
|
||||
def _load_speaker_embedding(path: str) -> torch.Tensor:
|
||||
return torch.as_tensor(json.loads(Path(path).read_text(encoding="utf-8")), dtype=torch.float32)
|
||||
|
||||
|
||||
def _ref_audio_paths(args) -> list[str]:
|
||||
if args.ref_audio is not None and args.ref_audio_paths is not None:
|
||||
raise RuntimeError("Use either --ref-audio or --ref-audio-paths, not both")
|
||||
if args.ref_audio_paths is not None:
|
||||
return list(args.ref_audio_paths)
|
||||
return [args.ref_audio] if args.ref_audio else []
|
||||
|
||||
|
||||
def _resolve_reference_inputs(args, case: dict, paths: list[str]):
|
||||
required = int(case.get("requires_ref_audio_count", 0))
|
||||
if required > 0 and len(paths) < required:
|
||||
raise RuntimeError(f"Case '{args.case}' needs {required} ref audio paths via --ref-audio-paths")
|
||||
if required <= 0 and case.get("requires_ref_audio") and not paths:
|
||||
raise RuntimeError(f"--ref-audio required for case '{args.case}'")
|
||||
if not paths:
|
||||
return None
|
||||
wavs = [_load_waveform(p) for p in paths]
|
||||
return wavs[0] if len(wavs) == 1 else wavs
|
||||
|
||||
|
||||
def _resolve_speaker_embedding(args, case: dict, paths: list[str]):
|
||||
if args.speaker_embedding:
|
||||
return _load_speaker_embedding(args.speaker_embedding)
|
||||
if not (case.get("auto_extract_speaker_embeddings") or args.extract_speaker_embeddings) or not paths:
|
||||
return None
|
||||
embs = MingSpeakerEmbeddingExtractor(args.model).extract_many(paths)
|
||||
if not embs:
|
||||
raise RuntimeError("Speaker extraction produced no embeddings")
|
||||
return embs[0] if len(embs) == 1 else torch.stack(embs, dim=0)
|
||||
|
||||
|
||||
def _build_prompt_payload(tokenizer, args):
|
||||
case = CASE_DEFAULTS[args.case]
|
||||
paths = _ref_audio_paths(args)
|
||||
prompt_text = args.ref_text if args.ref_text is not None else case.get("prompt_text")
|
||||
if case.get("requires_ref_text") and not prompt_text:
|
||||
raise RuntimeError(f"--ref-text required for case '{args.case}'")
|
||||
prompt_waveform = _resolve_reference_inputs(args, case, paths) if prompt_text is not None else None
|
||||
speaker_embedding = _resolve_speaker_embedding(args, case, paths)
|
||||
use_zero_spk_emb = bool(case.get("use_zero_spk_emb")) and prompt_waveform is None and speaker_embedding is None
|
||||
runtime_controls = {KEY_MAX_DECODE_STEPS: args.max_decode_steps or case["max_decode_steps"]}
|
||||
for key, field in [(KEY_CFG, "cfg"), (KEY_SIGMA, "sigma"), (KEY_TEMPERATURE, "temperature")]:
|
||||
if field in case:
|
||||
runtime_controls[key] = case[field]
|
||||
instruction = (
|
||||
json.loads(args.instruction_json) if args.instruction_json else (args.instructions or case.get("instruction"))
|
||||
)
|
||||
return build_ming_dense_prompt(
|
||||
tokenizer,
|
||||
prompt=args.prompt or case["prompt"],
|
||||
text=args.text or case["text"],
|
||||
runtime_controls=runtime_controls,
|
||||
instruction=instruction,
|
||||
prompt_text=prompt_text,
|
||||
prompt_waveform=prompt_waveform,
|
||||
speaker_embedding=speaker_embedding,
|
||||
use_zero_spk_emb=use_zero_spk_emb,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = _build_parser().parse_args()
|
||||
_finalize_args(args)
|
||||
tokenizer = AutoTokenizer.from_pretrained(args.model, trust_remote_code=False)
|
||||
prompt_payload = _build_prompt_payload(tokenizer, args)
|
||||
case = CASE_DEFAULTS[args.case]
|
||||
sampling_params_list = build_sampling_params(args.max_decode_steps or case["max_decode_steps"])
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
stats_log_file = resolve_stats_log_file(args)
|
||||
summaries = run_generation(args, prompt_payload, sampling_params_list, output_dir, stats_log_file)
|
||||
metadata_json = resolve_metadata_json(args)
|
||||
manifest = build_manifest(args, prompt_payload, stats_log_file, summaries)
|
||||
if metadata_json is not None:
|
||||
Path(metadata_json).write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"Saved run manifest to {metadata_json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
main()
|
||||
@@ -0,0 +1,277 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
|
||||
from vllm_omni import AsyncOmni, Omni
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
KEY_SPEAKER_EMBEDDING,
|
||||
SAMPLE_RATE,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
)
|
||||
|
||||
|
||||
def coerce_audio_tensor(audio, *, async_chunk: bool) -> torch.Tensor:
|
||||
if isinstance(audio, list):
|
||||
parts = []
|
||||
for item in audio:
|
||||
tensor = torch.as_tensor(item, dtype=torch.float32).reshape(-1)
|
||||
if tensor.numel() > 0:
|
||||
parts.append(tensor)
|
||||
if not parts:
|
||||
return torch.zeros((0,), dtype=torch.float32)
|
||||
return torch.cat(parts, dim=0)
|
||||
|
||||
return torch.as_tensor(audio, dtype=torch.float32).reshape(-1)
|
||||
|
||||
|
||||
def resolve_sr(sr) -> int:
|
||||
if isinstance(sr, list):
|
||||
sr = sr[-1]
|
||||
if hasattr(sr, "item"):
|
||||
return int(sr.item())
|
||||
return int(sr)
|
||||
|
||||
|
||||
def extract_sample_rate(multimodal_output: dict) -> int:
|
||||
sr = multimodal_output.get("sr")
|
||||
if sr is None:
|
||||
raise RuntimeError("Expected multimodal_output['sr']")
|
||||
return resolve_sr(sr)
|
||||
|
||||
|
||||
def write_wav(path: str, audio: torch.Tensor, sample_rate: int) -> None:
|
||||
audio = audio.clamp(-1.0, 1.0)
|
||||
pcm16 = (audio * 32767.0).round().to(torch.int16).cpu().numpy()
|
||||
with wave.open(path, "wb") as wav_file:
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(int(sample_rate))
|
||||
wav_file.writeframes(pcm16.tobytes())
|
||||
|
||||
|
||||
def request_index(request_id: str | None, fallback: int) -> int:
|
||||
try:
|
||||
return int(request_id)
|
||||
except (TypeError, ValueError):
|
||||
if isinstance(request_id, str):
|
||||
head = request_id.split("_", 1)[0]
|
||||
if head.isdigit():
|
||||
return int(head)
|
||||
return fallback
|
||||
|
||||
|
||||
def audio_summary(audio: torch.Tensor, sample_rate: int) -> dict:
|
||||
waveform = audio.detach().cpu().reshape(-1).to(torch.float32)
|
||||
return {
|
||||
"sample_rate": int(sample_rate),
|
||||
"num_samples": int(waveform.numel()),
|
||||
"duration_seconds": float(waveform.numel()) / float(sample_rate),
|
||||
"max_abs_amplitude": float(waveform.abs().max().item()) if waveform.numel() > 0 else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def resolve_output_name(output_name: str | None, case: str, index: int, total: int) -> str:
|
||||
if total == 1:
|
||||
return output_name or f"ming_{case}.wav"
|
||||
base = Path(output_name or f"ming_{case}.wav")
|
||||
return f"{base.stem}_{index:05d}{base.suffix or '.wav'}"
|
||||
|
||||
|
||||
def resolve_stats_log_file(args) -> str | None:
|
||||
if not args.log_stats:
|
||||
return None
|
||||
if args.stats_log_file:
|
||||
return args.stats_log_file
|
||||
base = Path(args.output_name or f"ming_{args.case}.wav").stem
|
||||
return str(Path(args.output_dir) / f"{base}_pipeline.log")
|
||||
|
||||
|
||||
def resolve_metadata_json(args) -> str | None:
|
||||
if args.metadata_json:
|
||||
return args.metadata_json
|
||||
if args.log_stats:
|
||||
base = Path(args.output_name or f"ming_{args.case}.wav").stem
|
||||
return str(Path(args.output_dir) / f"{base}_manifest.json")
|
||||
return None
|
||||
|
||||
|
||||
def build_manifest(args, prompt_payload, stats_log_file: str | None, outputs: list[dict]) -> dict:
|
||||
additional_information = {}
|
||||
if isinstance(prompt_payload, dict):
|
||||
additional_information = dict(prompt_payload.get("additional_information", {}))
|
||||
return {
|
||||
"model": args.model,
|
||||
"case": args.case,
|
||||
"streaming": bool(args.streaming),
|
||||
"deploy_config": args.deploy_config,
|
||||
"enforce_eager": bool(args.enforce_eager),
|
||||
"num_prompts": int(args.num_prompts),
|
||||
"log_stats": bool(args.log_stats),
|
||||
"stats_log_file": stats_log_file,
|
||||
"prompt_text": additional_information.get("prompt_text"),
|
||||
"instruction": additional_information.get("instruction"),
|
||||
"speaker_embedding_shape": (
|
||||
list(additional_information[KEY_SPEAKER_EMBEDDING].shape)
|
||||
if KEY_SPEAKER_EMBEDDING in additional_information
|
||||
and hasattr(additional_information[KEY_SPEAKER_EMBEDDING], "shape")
|
||||
else None
|
||||
),
|
||||
"outputs": outputs,
|
||||
"generated_at_unix": time.time(),
|
||||
}
|
||||
|
||||
|
||||
def build_engine_kwargs(args, stats_log_file: str | None) -> dict:
|
||||
kwargs = {
|
||||
"model": args.model,
|
||||
"deploy_config": args.deploy_config,
|
||||
"enforce_eager": args.enforce_eager,
|
||||
"trust_remote_code": args.trust_remote_code,
|
||||
"log_stats": args.log_stats,
|
||||
"stage_init_timeout": args.stage_init_timeout,
|
||||
"init_timeout": args.init_timeout,
|
||||
"batch_timeout": args.batch_timeout,
|
||||
"shm_threshold_bytes": args.shm_threshold_bytes,
|
||||
"worker_backend": args.worker_backend,
|
||||
}
|
||||
if stats_log_file is not None:
|
||||
kwargs["log_file"] = stats_log_file
|
||||
if args.ray_address is not None:
|
||||
kwargs["ray_address"] = args.ray_address
|
||||
return kwargs
|
||||
|
||||
|
||||
def build_sampling_params(max_decode_steps: int) -> list[SamplingParams]:
|
||||
return [
|
||||
SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=max_decode_steps + 1,
|
||||
stop_token_ids=[int(TEXT_EOS_TOKEN_ID)],
|
||||
),
|
||||
SamplingParams(temperature=0.0, max_tokens=1),
|
||||
]
|
||||
|
||||
|
||||
async def run_streaming(args, prompt_payload, sampling_params_list, output_dir: Path, stats_log_file: str | None):
|
||||
engine = AsyncOmni(**build_engine_kwargs(args, stats_log_file))
|
||||
try:
|
||||
all_audio_chunks = []
|
||||
accumulated_samples = 0
|
||||
chunk_idx = 0
|
||||
start_time = time.time()
|
||||
chunk_times = []
|
||||
ttfp_seconds = None
|
||||
final_stage_output = None
|
||||
async for stage_output in engine.generate(
|
||||
prompt=prompt_payload,
|
||||
request_id=str(uuid.uuid4()),
|
||||
sampling_params_list=sampling_params_list,
|
||||
):
|
||||
final_stage_output = stage_output
|
||||
multimodal_output = stage_output.multimodal_output or {}
|
||||
audio = multimodal_output.get("audio")
|
||||
if audio is None:
|
||||
continue
|
||||
|
||||
finished = stage_output.finished
|
||||
if isinstance(audio, torch.Tensor):
|
||||
if finished:
|
||||
audio_chunk = audio[accumulated_samples:].float().detach().cpu()
|
||||
else:
|
||||
audio_chunk = audio.float().detach().cpu()
|
||||
elif isinstance(audio, list):
|
||||
audio_chunk = torch.as_tensor(audio[chunk_idx], dtype=torch.float32).reshape(-1).cpu()
|
||||
else:
|
||||
audio_chunk = torch.as_tensor(audio, dtype=torch.float32).reshape(-1).cpu()
|
||||
|
||||
accumulated_samples += int(audio_chunk.numel())
|
||||
chunk_idx += 1
|
||||
if audio_chunk.numel() > 0:
|
||||
now = time.time()
|
||||
if ttfp_seconds is None:
|
||||
ttfp_seconds = now - start_time
|
||||
chunk_times.append(now)
|
||||
all_audio_chunks.append(audio_chunk)
|
||||
|
||||
if not all_audio_chunks:
|
||||
raise RuntimeError("Streaming Ming example produced no audio chunks")
|
||||
|
||||
waveform = torch.cat(all_audio_chunks, dim=0)
|
||||
output_name = resolve_output_name(args.output_name, args.case, 0, 1)
|
||||
output_path = str(output_dir / output_name)
|
||||
write_wav(output_path, waveform, SAMPLE_RATE)
|
||||
summary = {
|
||||
"request_id": getattr(final_stage_output, "request_id", None),
|
||||
"stage_id": getattr(final_stage_output, "stage_id", None),
|
||||
"output_path": output_path,
|
||||
"stage_durations": getattr(final_stage_output, "stage_durations", {}),
|
||||
"peak_memory_mb": getattr(final_stage_output, "peak_memory_mb", 0.0),
|
||||
"ttfp_seconds": ttfp_seconds,
|
||||
"mean_inter_chunk_seconds": (
|
||||
sum(t1 - t0 for t0, t1 in zip(chunk_times, chunk_times[1:])) / (len(chunk_times) - 1)
|
||||
if len(chunk_times) > 1
|
||||
else None
|
||||
),
|
||||
}
|
||||
summary.update(audio_summary(waveform, SAMPLE_RATE))
|
||||
print(f"Saved streaming output to {output_path}")
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
return [summary]
|
||||
finally:
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
def run_non_streaming(args, prompt_payload, sampling_params_list, output_dir: Path, stats_log_file: str | None):
|
||||
engine = Omni(**build_engine_kwargs(args, stats_log_file))
|
||||
try:
|
||||
outputs = engine.generate(
|
||||
prompts=[prompt_payload for _ in range(args.num_prompts)],
|
||||
sampling_params_list=sampling_params_list,
|
||||
py_generator=False,
|
||||
)
|
||||
summaries = []
|
||||
for fallback_index, output in enumerate(outputs):
|
||||
if output.final_output_type != "audio":
|
||||
continue
|
||||
multimodal_output = output.multimodal_output or {}
|
||||
waveform = coerce_audio_tensor(multimodal_output.get("audio"), async_chunk=False)
|
||||
sample_rate = extract_sample_rate(multimodal_output)
|
||||
output_name = resolve_output_name(
|
||||
args.output_name,
|
||||
args.case,
|
||||
request_index(output.request_id, fallback_index),
|
||||
args.num_prompts,
|
||||
)
|
||||
output_path = str(output_dir / output_name)
|
||||
write_wav(output_path, waveform, sample_rate)
|
||||
summary = {
|
||||
"request_id": output.request_id,
|
||||
"stage_id": output.stage_id,
|
||||
"output_path": output_path,
|
||||
"stage_durations": output.stage_durations,
|
||||
"peak_memory_mb": output.peak_memory_mb,
|
||||
}
|
||||
summary.update(audio_summary(waveform, sample_rate))
|
||||
summaries.append(summary)
|
||||
print(f"Saved output to {output_path}")
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
if not summaries:
|
||||
raise RuntimeError("Non-streaming Ming example produced no audio outputs")
|
||||
return summaries
|
||||
finally:
|
||||
engine.close()
|
||||
|
||||
|
||||
def run_generation(args, prompt_payload, sampling_params_list, output_dir: Path, stats_log_file: str | None):
|
||||
if args.streaming:
|
||||
return asyncio.run(run_streaming(args, prompt_payload, sampling_params_list, output_dir, stats_log_file))
|
||||
return run_non_streaming(args, prompt_payload, sampling_params_list, output_dir, stats_log_file)
|
||||
@@ -16,6 +16,7 @@ For the full list of supported architectures across all modalities, see
|
||||
|---|---|---|---|---|---|
|
||||
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
|
||||
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
|
||||
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | ✓ (`ref_audio` / `speaker_embedding`) | ✓ (PCM stream) | IP labels + structured `instructions` | — |
|
||||
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
|
||||
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
|
||||
| OmniVoice | `k2-fsa/OmniVoice` | ✓ | — | — | — |
|
||||
@@ -202,6 +203,51 @@ python fish_speech/gradio_demo.py --api-base http://localhost:8091 # if server
|
||||
|
||||
---
|
||||
|
||||
## Ming-omni-tts
|
||||
|
||||
Dense 0.5B two-stage TTS served through `/v1/audio/speech`. Ming uses the standard speech endpoint plus structured controls in `instructions`, `voice`, `language`, `ref_audio`, `ref_text`, and `speaker_embedding`.
|
||||
|
||||
### Launch
|
||||
```bash
|
||||
bash examples/online_serving/text_to_speech/ming_tts/run_server.sh
|
||||
```
|
||||
Equivalent manual command:
|
||||
```bash
|
||||
vllm-omni serve inclusionAI/Ming-omni-tts-0.5B \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--host 0.0.0.0 --port 8091 \
|
||||
--enforce-eager --omni
|
||||
```
|
||||
|
||||
### Sending requests
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/ming_tts/openai_speech_client.py \
|
||||
--text "你好,这是 Ming 在线语音合成测试。"
|
||||
```
|
||||
|
||||
Structured dialect control:
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/ming_tts/openai_speech_client.py \
|
||||
--text "我觉得社会企业同个人都有责任" \
|
||||
--instruction-json '{"方言":"广粤话"}' \
|
||||
--ref-audio /path/to/yue_prompt.wav
|
||||
```
|
||||
|
||||
Zero-shot cloning:
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/ming_tts/openai_speech_client.py \
|
||||
--text "我们的愿景是构建未来服务业的数字化基础设施,为世界带来更多微小而美好的改变。" \
|
||||
--ref-audio /path/to/10002287-00000094.wav \
|
||||
--ref-text "在此奉劝大家别乱打美白针。"
|
||||
```
|
||||
|
||||
### Notes
|
||||
- `run_curl.sh` keeps a small sanity subset; use the Ming README for the broader request cookbook.
|
||||
- Online serving is speech-shaped today; music-only `bgm` and text-to-audio `tta` remain offline examples.
|
||||
- Full request details live in [`ming_tts/README.md`](ming_tts/README.md).
|
||||
|
||||
---
|
||||
|
||||
## Ming-flash-omni-TTS
|
||||
|
||||
Standalone talker-only deployment of Ming-flash-omni-2.0. Voice is controlled through caption text passed via `instructions`.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Ming-omni-tts Online Serving
|
||||
|
||||
Serve the dense `inclusionAI/Ming-omni-tts-0.5B` two-stage TTS model through
|
||||
the OpenAI-compatible `/v1/audio/speech` endpoint.
|
||||
|
||||
## Start Server
|
||||
|
||||
```bash
|
||||
vllm-omni serve inclusionAI/Ming-omni-tts-0.5B \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```bash
|
||||
cd examples/online_serving/text_to_speech/ming_tts
|
||||
./run_server.sh
|
||||
```
|
||||
|
||||
The tested ROCm environment is summarized in the
|
||||
[Ming recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/inclusionAI/Ming-omni-tts-0.5B.md).
|
||||
|
||||
## Send Requests
|
||||
|
||||
The Python client targets `http://localhost:8091/v1` with `api_key=EMPTY`; it
|
||||
does not call OpenAI's hosted API.
|
||||
|
||||
```bash
|
||||
python openai_speech_client.py \
|
||||
--text "你好,这是 Ming 在线语音合成测试。" \
|
||||
--max-new-tokens 200
|
||||
```
|
||||
|
||||
Style or dialect controls can be plain text or Ming JSON. The upstream
|
||||
dialect example also uses `yue_prompt.wav` for speaker conditioning:
|
||||
|
||||
```bash
|
||||
python openai_speech_client.py \
|
||||
--text "我觉得社会企业同个人都有责任" \
|
||||
--instruction-json '{"方言":"广粤话"}' \
|
||||
--ref-audio /path/to/yue_prompt.wav \
|
||||
--max-new-tokens 200
|
||||
```
|
||||
|
||||
When `--ref-audio` is supplied without `--ref-text`, the server extracts the
|
||||
Ming speaker embedding, matching upstream `use_spk_emb=True`, without using the
|
||||
audio as a zero-shot prompt.
|
||||
|
||||
Reference-audio cloning:
|
||||
|
||||
```bash
|
||||
python openai_speech_client.py \
|
||||
--text "我们的愿景是构建未来服务业的数字化基础设施,为世界带来更多微小而美好的改变。" \
|
||||
--ref-audio /path/to/10002287-00000094.wav \
|
||||
--ref-text "在此奉劝大家别乱打美白针。" \
|
||||
--max-new-tokens 200
|
||||
```
|
||||
|
||||
Podcast-style multi-speaker prompt:
|
||||
|
||||
```bash
|
||||
python openai_speech_client.py \
|
||||
--text " speaker_1:你可以说一下,就大概说一下,可能虽然我也不知道,我看过那部电影没有。
|
||||
speaker_2:就是那个叫什么,变相一节课的嘛。
|
||||
speaker_1:嗯。
|
||||
speaker_2:一部搞笑的电影。
|
||||
speaker_1:一部搞笑的。" \
|
||||
--ref-audio /path/to/CTS-CN-F2F-2019-11-11-423-012-A.wav \
|
||||
--ref-audio /path/to/CTS-CN-F2F-2019-11-11-423-012-B.wav \
|
||||
--ref-text " speaker_1:并且我们还要进行每个月还要考核 笔试的话还要进行笔试,做个,当服务员还要去笔试了
|
||||
speaker_2:对啊,这真的很奇怪,就是 单纯的因,单纯自己工资不高,只是因为可能人家那个店比较出名一点,就对你苛刻要求"
|
||||
```
|
||||
|
||||
Streaming PCM:
|
||||
|
||||
```bash
|
||||
python openai_speech_client.py \
|
||||
--text "你好,这是流式输出测试。" \
|
||||
--stream \
|
||||
--output ming_output.pcm
|
||||
```
|
||||
|
||||
`run_curl.sh` keeps small smoke checks:
|
||||
|
||||
```bash
|
||||
./run_curl.sh basic
|
||||
REF_AUDIO=/path/to/reference.wav REF_TEXT="在此奉劝大家别乱打美白针。" ./run_curl.sh zero_shot
|
||||
./run_curl.sh stream
|
||||
```
|
||||
|
||||
## Request Fields
|
||||
|
||||
| Field | Ming meaning |
|
||||
|-------|--------------|
|
||||
| `input` | target text |
|
||||
| `instructions` | plain style text, or JSON object for structured Ming controls |
|
||||
| `voice` | Ming IP voice label unless it resolves to an uploaded speaker |
|
||||
| `language` | Ming `方言` control |
|
||||
| `ref_audio` | speaker reference; with `ref_text`, also supplies the prompt waveform |
|
||||
| `ref_text` | transcript enabling zero-shot or podcast prompt-latent conditioning |
|
||||
| `speaker_embedding` | 192-d Ming speaker embedding |
|
||||
| `max_new_tokens` | Ming `max_decode_steps` |
|
||||
|
||||
## Notes
|
||||
|
||||
- `ref_audio` accepts local paths through the client, remote URLs, `file://`,
|
||||
or `data:` URLs.
|
||||
- Non-streaming responses return WAV bytes; streaming responses return PCM.
|
||||
- Music-only `bgm` generation is offline-only until the API exposes Ming
|
||||
prompt-mode selection.
|
||||
@@ -0,0 +1,225 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""OpenAI-compatible client for Ming-omni-tts via /v1/audio/speech.
|
||||
|
||||
Examples:
|
||||
python openai_speech_client.py --text "你好,世界"
|
||||
python openai_speech_client.py --text "我觉得社会企业同个人都有责任" \
|
||||
--instruction-json '{"方言":"广粤话"}' --ref-audio yue_prompt.wav --max-new-tokens 200
|
||||
python openai_speech_client.py \
|
||||
--text "我们的愿景是构建未来服务业的数字化基础设施,为世界带来更多微小而美好的改变。" \
|
||||
--ref-audio 10002287-00000094.wav --ref-text "在此奉劝大家别乱打美白针。" \
|
||||
--max-new-tokens 200
|
||||
python openai_speech_client.py --text "speaker_1:你好。 speaker_2:你好。" \
|
||||
--ref-audio speaker_1.wav --ref-audio speaker_2.wav --ref-text "speaker_1:你好。 speaker_2:你好。"
|
||||
python openai_speech_client.py --text "你好,这是流式输出测试。" \
|
||||
--stream --output ming_output.pcm
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_API_BASE = "http://localhost:8091"
|
||||
DEFAULT_API_KEY = "EMPTY"
|
||||
DEFAULT_MODEL = "inclusionAI/Ming-omni-tts-0.5B"
|
||||
EXPECTED_SPEAKER_EMBEDDING_DIM = 192
|
||||
|
||||
|
||||
def encode_audio_to_base64(audio_path: str) -> str:
|
||||
"""Encode a local audio file to a base64 data URL."""
|
||||
if not os.path.exists(audio_path):
|
||||
raise FileNotFoundError(f"Audio file not found: {audio_path}")
|
||||
ext = audio_path.lower().rsplit(".", 1)[-1]
|
||||
mime_map = {
|
||||
"wav": "audio/wav",
|
||||
"mp3": "audio/mpeg",
|
||||
"flac": "audio/flac",
|
||||
"ogg": "audio/ogg",
|
||||
"aac": "audio/aac",
|
||||
}
|
||||
mime_type = mime_map.get(ext, "audio/wav")
|
||||
with open(audio_path, "rb") as f:
|
||||
audio_b64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
return f"data:{mime_type};base64,{audio_b64}"
|
||||
|
||||
|
||||
def load_speaker_embedding(path: str) -> list[float]:
|
||||
"""Load and validate a 192-d Ming speaker embedding JSON file."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, list):
|
||||
raise ValueError("speaker_embedding file must contain a JSON list")
|
||||
if len(data) != EXPECTED_SPEAKER_EMBEDDING_DIM:
|
||||
raise ValueError(
|
||||
f"Ming dense speaker_embedding must have {EXPECTED_SPEAKER_EMBEDDING_DIM} values, got {len(data)}"
|
||||
)
|
||||
|
||||
values = []
|
||||
for index, value in enumerate(data):
|
||||
try:
|
||||
values.append(float(value))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"speaker_embedding[{index}] must be a number, got {value!r}") from exc
|
||||
return values
|
||||
|
||||
|
||||
def build_instruction_payload(args) -> str | None:
|
||||
"""Return a string payload for the API `instructions` field."""
|
||||
if args.instructions and args.instruction_json:
|
||||
raise ValueError("Use either --instructions or --instruction-json, not both")
|
||||
if args.instruction_json:
|
||||
parsed = json.loads(args.instruction_json)
|
||||
return json.dumps(parsed, ensure_ascii=False)
|
||||
return args.instructions
|
||||
|
||||
|
||||
def validate_args(args) -> None:
|
||||
"""Fail fast on invalid combinations before hitting the server."""
|
||||
if args.ref_text and not args.ref_audio:
|
||||
raise ValueError("--ref-audio is required when --ref-text is provided")
|
||||
if args.speaker_embedding and args.ref_audio and len(args.ref_audio) > 1:
|
||||
raise ValueError("--speaker-embedding cannot be combined with multiple --ref-audio values")
|
||||
|
||||
|
||||
def run_tts(args) -> None:
|
||||
"""Generate speech via the OpenAI-compatible /v1/audio/speech API."""
|
||||
validate_args(args)
|
||||
|
||||
payload = {
|
||||
"model": args.model,
|
||||
"input": args.text,
|
||||
"response_format": args.response_format,
|
||||
}
|
||||
|
||||
if args.voice:
|
||||
payload["voice"] = args.voice
|
||||
if args.task_type:
|
||||
payload["task_type"] = args.task_type
|
||||
if args.dialect:
|
||||
payload["language"] = args.dialect
|
||||
|
||||
instructions = build_instruction_payload(args)
|
||||
if instructions:
|
||||
payload["instructions"] = instructions
|
||||
|
||||
if args.ref_audio:
|
||||
ref_audio = []
|
||||
for audio in args.ref_audio:
|
||||
if audio.startswith(("http://", "https://", "data:")):
|
||||
ref_audio.append(audio)
|
||||
else:
|
||||
ref_audio.append(encode_audio_to_base64(audio))
|
||||
payload["ref_audio"] = ref_audio[0] if len(ref_audio) == 1 else ref_audio
|
||||
if args.ref_text:
|
||||
payload["ref_text"] = args.ref_text
|
||||
if args.speaker_embedding:
|
||||
payload["speaker_embedding"] = load_speaker_embedding(args.speaker_embedding)
|
||||
if args.max_new_tokens:
|
||||
payload["max_new_tokens"] = args.max_new_tokens
|
||||
if args.stream:
|
||||
payload["stream"] = True
|
||||
payload["response_format"] = "pcm"
|
||||
|
||||
api_url = f"{args.api_base}/v1/audio/speech"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {args.api_key}",
|
||||
}
|
||||
|
||||
print(f"Model: {args.model}")
|
||||
print(f"Text: {args.text}")
|
||||
print(f"Payload keys: {sorted(payload)}")
|
||||
|
||||
if args.stream:
|
||||
output_path = args.output or "ming_output.pcm"
|
||||
with httpx.Client(timeout=300.0) as client:
|
||||
with client.stream("POST", api_url, json=payload, headers=headers) as response:
|
||||
if response.status_code != 200:
|
||||
print(f"Error: {response.status_code}")
|
||||
print(response.read().decode())
|
||||
return
|
||||
with open(output_path, "wb") as f:
|
||||
for chunk in response.iter_bytes():
|
||||
f.write(chunk)
|
||||
print(f"Streamed PCM audio to: {output_path}")
|
||||
return
|
||||
|
||||
with httpx.Client(timeout=300.0) as client:
|
||||
response = client.post(api_url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Error: {response.status_code}")
|
||||
print(response.text)
|
||||
return
|
||||
|
||||
try:
|
||||
text = response.content.decode("utf-8")
|
||||
if text.startswith('{"error"'):
|
||||
print(f"Error: {text}")
|
||||
return
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
output_path = args.output or "ming_output.wav"
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
print(f"Audio saved to: {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="OpenAI-compatible client for Ming-omni-tts via /v1/audio/speech")
|
||||
parser.add_argument("--api-base", default=DEFAULT_API_BASE, help="API base URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="API key")
|
||||
parser.add_argument("--model", "-m", default=DEFAULT_MODEL, help="Model name or path")
|
||||
parser.add_argument("--text", required=True, help="Text to synthesize")
|
||||
parser.add_argument(
|
||||
"--task-type",
|
||||
default=None,
|
||||
choices=["CustomVoice", "VoiceDesign", "Base"],
|
||||
help="Optional compatibility task type. Ming accepts the same field but primarily uses prompt metadata.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--voice",
|
||||
default=None,
|
||||
help="Maps to Ming `IP` when using built-in character voices, or to an uploaded voice sample name",
|
||||
)
|
||||
parser.add_argument("--dialect", default=None, help="Maps to Ming `方言`")
|
||||
parser.add_argument("--instructions", default=None, help="Free-form Ming instruction string")
|
||||
parser.add_argument(
|
||||
"--instruction-json",
|
||||
default=None,
|
||||
help='Structured Ming instruction JSON, for example \'{"情感":"高兴"}\'',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ref-audio",
|
||||
action="append",
|
||||
default=None,
|
||||
help="Reference audio path, URL, or data URL. Repeat for podcast-style multi-speaker prompts.",
|
||||
)
|
||||
parser.add_argument("--ref-text", default=None, help="Reference transcript for cloning")
|
||||
parser.add_argument(
|
||||
"--speaker-embedding", default=None, help="Path to a JSON file containing a 192-d speaker embedding"
|
||||
)
|
||||
parser.add_argument("--max-new-tokens", type=int, default=None, help="Override ming_max_decode_steps")
|
||||
parser.add_argument("--stream", action="store_true", help="Enable streaming PCM output")
|
||||
parser.add_argument(
|
||||
"--response-format",
|
||||
default="wav",
|
||||
choices=["wav", "mp3", "flac", "pcm", "aac", "opus"],
|
||||
help="Audio format when not streaming",
|
||||
)
|
||||
parser.add_argument("--output", "-o", default=None, help="Output file path")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
run_tts(args)
|
||||
except (FileNotFoundError, ValueError, json.JSONDecodeError) as exc:
|
||||
raise SystemExit(f"Error: {exc}") from exc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-basic}"
|
||||
HOST="${HOST:-localhost}"
|
||||
PORT="${PORT:-8091}"
|
||||
MODEL="${MODEL:-inclusionAI/Ming-omni-tts-0.5B}"
|
||||
API_URL="http://${HOST}:${PORT}/v1/audio/speech"
|
||||
TEXT="${TEXT:-你好,这是 Ming 在线语音合成测试。}"
|
||||
OUTPUT="${OUTPUT:-ming_output.wav}"
|
||||
STREAM_OUTPUT="${STREAM_OUTPUT:-ming_output.pcm}"
|
||||
REF_AUDIO="${REF_AUDIO:-}"
|
||||
REF_TEXT="${REF_TEXT:-}"
|
||||
|
||||
post_json() {
|
||||
local payload="$1"
|
||||
local output_path="$2"
|
||||
curl -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer EMPTY" \
|
||||
-d "$payload" \
|
||||
--output "$output_path"
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
basic)
|
||||
post_json "{
|
||||
\"model\": \"${MODEL}\",
|
||||
\"input\": \"${TEXT}\",
|
||||
\"response_format\": \"wav\"
|
||||
}" "$OUTPUT"
|
||||
;;
|
||||
zero_shot)
|
||||
if [ -z "$REF_AUDIO" ] || [ -z "$REF_TEXT" ]; then
|
||||
echo "zero_shot requires REF_AUDIO and REF_TEXT" >&2
|
||||
exit 1
|
||||
fi
|
||||
python - <<'PY' > /tmp/ming_zero_shot_payload.json
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(os.environ["REF_AUDIO"])
|
||||
mime_type = mimetypes.guess_type(path.name)[0] or "audio/wav"
|
||||
payload = {
|
||||
"model": os.environ["MODEL"],
|
||||
"input": os.environ["TEXT"],
|
||||
"ref_audio": f"data:{mime_type};base64,{base64.b64encode(path.read_bytes()).decode('utf-8')}",
|
||||
"ref_text": os.environ["REF_TEXT"],
|
||||
"response_format": "wav",
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False))
|
||||
PY
|
||||
curl -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer EMPTY" \
|
||||
--data-binary @/tmp/ming_zero_shot_payload.json \
|
||||
--output "$OUTPUT"
|
||||
rm -f /tmp/ming_zero_shot_payload.json
|
||||
;;
|
||||
stream)
|
||||
post_json "{
|
||||
\"model\": \"${MODEL}\",
|
||||
\"input\": \"${TEXT}\",
|
||||
\"stream\": true,
|
||||
\"response_format\": \"pcm\"
|
||||
}" "$STREAM_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown mode: $MODE" >&2
|
||||
echo "Supported sanity checks: basic, zero_shot, stream" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Launch vLLM-Omni server for Ming-omni-tts.
|
||||
#
|
||||
# Usage:
|
||||
# ./run_server.sh
|
||||
# PORT=8000 ./run_server.sh
|
||||
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$DIR/../../../.." && pwd)"
|
||||
|
||||
MODEL="${MODEL:-inclusionAI/Ming-omni-tts-0.5B}"
|
||||
PORT="${PORT:-8091}"
|
||||
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$ROOT/vllm_omni/deploy/ming_tts.yaml}"
|
||||
|
||||
echo "Starting Ming-omni-tts server with model: $MODEL"
|
||||
echo "Deploy config: $DEPLOY_CONFIG"
|
||||
|
||||
vllm-omni serve "$MODEL" \
|
||||
--deploy-config "$DEPLOY_CONFIG" \
|
||||
--host 0.0.0.0 \
|
||||
--port "$PORT" \
|
||||
--enforce-eager \
|
||||
--omni
|
||||
@@ -33,6 +33,7 @@ recipes/
|
||||
| [`fishaudio/Fish-Speech-S2-Pro.md`](./fishaudio/Fish-Speech-S2-Pro.md) | Online serving for TTS | 1x A800 80GB |
|
||||
| [`Helios/Helios.md`](./Helios/Helios.md) | Text-to-video, image-to-video, and video-to-video generation | 1x NVIDIA H20 |
|
||||
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
|
||||
| [`inclusionAI/Ming-omni-tts-0.5B.md`](./inclusionAI/Ming-omni-tts-0.5B.md) | Offline + online dense Ming TTS/audio generation | 1x AMD `gfx942` (ROCm 7.2) |
|
||||
| [`LTX/LTX-2.md`](./LTX/LTX-2.md) | Text-to-video and image-to-video serving | 1x H200 141GB |
|
||||
| [`LTX/LTX-2.3.md`](./LTX/LTX-2.3.md) | Text-to-video with audio generation (22B) | 1x GPU (96GB VRAM) |
|
||||
| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB |
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Ming-omni-tts 0.5B
|
||||
|
||||
> Offline and online dense Ming text-to-speech on ROCm
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: inclusionAI
|
||||
- Model: `inclusionAI/Ming-omni-tts-0.5B`
|
||||
- Task: Text-to-speech with style, dialect, cloning, and multi-speaker controls
|
||||
- Mode: Online serving via the OpenAI-compatible `/v1/audio/speech` API; offline inference
|
||||
- Maintainer: Community
|
||||
|
||||
## References
|
||||
|
||||
- [Model card](https://huggingface.co/inclusionAI/Ming-omni-tts-0.5B)
|
||||
- [Upstream repository](https://github.com/inclusionAI/Ming-omni-tts)
|
||||
- [Offline example](../../examples/offline_inference/text_to_speech/ming_tts/)
|
||||
- [Online example](../../examples/online_serving/text_to_speech/ming_tts/)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
## ROCm
|
||||
|
||||
### 1x AMD `gfx942`
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Ubuntu 22.04.5 LTS, x86_64
|
||||
- Python: 3.12.13
|
||||
- ROCm / HIP: 7.2.53211
|
||||
- vLLM version: 0.22.0
|
||||
- vLLM-Omni version or commit: 0.1.dev1873 / `99c35c410`
|
||||
- Docker image: `vllm/vllm-omni-rocm:v0.22.0`
|
||||
|
||||
#### Command
|
||||
|
||||
From the vLLM-Omni repository root:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
--group-add=video \
|
||||
--ipc=host \
|
||||
--cap-add=SYS_PTRACE \
|
||||
--security-opt seccomp=unconfined \
|
||||
--device /dev/kfd \
|
||||
--device /dev/dri \
|
||||
-v ~/.cache/huggingface:/root/.cache/huggingface \
|
||||
-v "$PWD":/app/vllm-omni \
|
||||
-w /app/vllm-omni \
|
||||
-e VLLM_ROCM_USE_AITER=0 \
|
||||
-p 8091:8091 \
|
||||
vllm/vllm-omni-rocm:v0.22.0 \
|
||||
--model inclusionAI/Ming-omni-tts-0.5B \
|
||||
--deploy-config vllm_omni/deploy/ming_tts.yaml \
|
||||
--omni \
|
||||
--port 8091 \
|
||||
--enforce-eager
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/ming_tts/openai_speech_client.py \
|
||||
--text "我觉得社会企业同个人都有责任" \
|
||||
--instruction-json '{"方言":"广粤话"}' \
|
||||
--ref-audio /path/to/yue_prompt.wav \
|
||||
--max-new-tokens 200 \
|
||||
--output dialect.wav
|
||||
```
|
||||
|
||||
`--ref-audio` matches upstream `use_spk_emb=True`; do not add `--ref-text`
|
||||
for the dialect case.
|
||||
|
||||
## Notes
|
||||
|
||||
- The official ROCm image includes the platform dependencies.
|
||||
- See the [ROCm installation guide](../../docs/getting_started/installation/gpu.md)
|
||||
for interactive and source-build workflows.
|
||||
- The [offline example](../../examples/offline_inference/text_to_speech/ming_tts/)
|
||||
can be run from an interactive container.
|
||||
- The tested environment uses `--enforce-eager`.
|
||||
- Output is mono 44.1 kHz audio.
|
||||
@@ -51,7 +51,7 @@ test_params = [(m, c) for m in models for c in [get_eager_config()]]
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
|
||||
def test_text_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -67,7 +67,7 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
|
||||
def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -84,7 +84,7 @@ def test_image_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
|
||||
def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -103,7 +103,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
|
||||
def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -120,7 +120,7 @@ def test_video_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.core_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True)
|
||||
def test_mixed_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -140,7 +140,7 @@ def test_mixed_to_text(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||
def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
@@ -156,7 +156,7 @@ def test_text_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "H100"}, num_cards=4)
|
||||
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4)
|
||||
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
|
||||
def test_image_to_audio(omni_runner, omni_runner_handler) -> None:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
"""End-to-end offline inference tests for Ming-omni-tts."""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from transformers import AutoTokenizer
|
||||
from vllm import SamplingParams
|
||||
from vllm.inputs import tokens_input
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni import AsyncOmni
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
SAMPLE_RATE,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_tts.prompt_assembly import DEFAULT_PROMPT, build_ming_dense_prompt
|
||||
|
||||
MODEL = "inclusionAI/Ming-omni-tts-0.5B"
|
||||
DEPLOY_CONFIG = get_deploy_config_path("ming_tts.yaml")
|
||||
TEST_TEXT = "我会一直在这里陪着你,直到你慢慢地沉入那个最温柔的梦里。"
|
||||
TEST_INSTRUCTION = "轻柔的ASMR耳语,慢速,贴近麦克风"
|
||||
MIN_AUDIO_SAMPLES = 1000
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ming_tokenizer():
|
||||
return AutoTokenizer.from_pretrained(MODEL, trust_remote_code=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ming_engine():
|
||||
with OmniRunner(
|
||||
MODEL,
|
||||
deploy_config=DEPLOY_CONFIG,
|
||||
stage_init_timeout=300,
|
||||
enforce_eager=True,
|
||||
) as runner:
|
||||
yield runner.omni
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def async_omni_engine():
|
||||
engine = AsyncOmni(
|
||||
model=MODEL,
|
||||
deploy_config=DEPLOY_CONFIG,
|
||||
stage_init_timeout=300,
|
||||
enforce_eager=True,
|
||||
)
|
||||
yield engine
|
||||
engine.shutdown()
|
||||
|
||||
|
||||
def _build_prompt(
|
||||
tokenizer,
|
||||
*,
|
||||
text: str = TEST_TEXT,
|
||||
instruction=TEST_INSTRUCTION,
|
||||
use_zero_spk_emb: bool = True,
|
||||
) -> dict:
|
||||
prompt_dict = build_ming_dense_prompt(
|
||||
tokenizer,
|
||||
prompt=DEFAULT_PROMPT,
|
||||
text=text,
|
||||
instruction=instruction,
|
||||
runtime_controls={KEY_MAX_DECODE_STEPS: 200},
|
||||
use_zero_spk_emb=use_zero_spk_emb,
|
||||
)
|
||||
prompt = tokens_input(prompt_token_ids=prompt_dict["prompt_token_ids"])
|
||||
prompt["prompt"] = prompt_dict["prompt"]
|
||||
prompt["text"] = prompt_dict["text"]
|
||||
prompt["additional_information"] = prompt_dict["additional_information"]
|
||||
return prompt
|
||||
|
||||
|
||||
def _sampling_params_list() -> list[SamplingParams]:
|
||||
return [
|
||||
SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=201,
|
||||
stop_token_ids=[int(TEXT_EOS_TOKEN_ID)],
|
||||
),
|
||||
SamplingParams(temperature=0.0, max_tokens=1),
|
||||
]
|
||||
|
||||
|
||||
def _flatten_audio(audio) -> torch.Tensor:
|
||||
if isinstance(audio, list):
|
||||
parts = [torch.as_tensor(item, dtype=torch.float32).reshape(-1).cpu() for item in audio]
|
||||
parts = [item for item in parts if item.numel() > 0]
|
||||
if not parts:
|
||||
return torch.zeros((0,), dtype=torch.float32)
|
||||
return torch.cat(parts, dim=0)
|
||||
return torch.as_tensor(audio, dtype=torch.float32).reshape(-1).cpu()
|
||||
|
||||
|
||||
def _extract_audio(multimodal_output: dict) -> torch.Tensor:
|
||||
assert isinstance(multimodal_output, dict), f"Expected dict, got {type(multimodal_output)}"
|
||||
audio = multimodal_output.get("audio", multimodal_output.get("model_outputs"))
|
||||
assert audio is not None, f"No audio output found, keys={list(multimodal_output.keys())}"
|
||||
waveform = _flatten_audio(audio)
|
||||
if waveform.numel() == 0:
|
||||
raise RuntimeError("Generated audio waveform is empty")
|
||||
return waveform
|
||||
|
||||
|
||||
def _extract_sample_rate(multimodal_output: dict) -> int:
|
||||
sample_rate = multimodal_output.get("sr")
|
||||
if sample_rate is None:
|
||||
raise RuntimeError("Expected multimodal_output['sr']")
|
||||
if isinstance(sample_rate, list):
|
||||
sample_rate = sample_rate[-1]
|
||||
if hasattr(sample_rate, "item"):
|
||||
sample_rate = sample_rate.item()
|
||||
return int(sample_rate)
|
||||
|
||||
|
||||
def _extract_final_audio_outputs(outputs):
|
||||
final_outputs = []
|
||||
for item in outputs:
|
||||
if getattr(item, "final_output_type", None) == "audio":
|
||||
final_outputs.append(item)
|
||||
continue
|
||||
request_output = getattr(item, "request_output", None)
|
||||
if request_output is None:
|
||||
continue
|
||||
multimodal_output = getattr(request_output, "multimodal_output", None)
|
||||
if isinstance(multimodal_output, dict):
|
||||
final_outputs.append(item)
|
||||
continue
|
||||
completions = getattr(request_output, "outputs", None) or []
|
||||
if any(isinstance(getattr(completion, "multimodal_output", None), dict) for completion in completions):
|
||||
final_outputs.append(item)
|
||||
return final_outputs
|
||||
|
||||
|
||||
def _extract_multimodal_output(output) -> dict:
|
||||
multimodal_output = getattr(output, "multimodal_output", None)
|
||||
if isinstance(multimodal_output, dict):
|
||||
return multimodal_output
|
||||
|
||||
request_output = getattr(output, "request_output", None)
|
||||
if request_output is not None:
|
||||
multimodal_output = getattr(request_output, "multimodal_output", None)
|
||||
if isinstance(multimodal_output, dict):
|
||||
return multimodal_output
|
||||
completions = getattr(request_output, "outputs", None) or []
|
||||
for completion in completions:
|
||||
multimodal_output = getattr(completion, "multimodal_output", None)
|
||||
if isinstance(multimodal_output, dict):
|
||||
return multimodal_output
|
||||
|
||||
raise AssertionError("No multimodal audio output found in Ming generate results")
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_ming_tts_offline_basic(ming_engine, ming_tokenizer) -> None:
|
||||
"""Test blocking Ming generation through Omni."""
|
||||
outputs = ming_engine.generate(
|
||||
prompts=[_build_prompt(ming_tokenizer)],
|
||||
sampling_params_list=_sampling_params_list(),
|
||||
py_generator=False,
|
||||
)
|
||||
final_outputs = _extract_final_audio_outputs(outputs)
|
||||
assert len(final_outputs) == 1, f"Expected one final audio output, got {len(final_outputs)}"
|
||||
multimodal_output = _extract_multimodal_output(final_outputs[0])
|
||||
waveform = _extract_audio(multimodal_output)
|
||||
sample_rate = _extract_sample_rate(multimodal_output)
|
||||
assert waveform.ndim == 1
|
||||
assert waveform.shape[0] == waveform.numel()
|
||||
assert waveform.numel() > MIN_AUDIO_SAMPLES
|
||||
assert np.max(np.abs(waveform.numpy())) > 0.01, "Audio appears silent"
|
||||
assert sample_rate == SAMPLE_RATE, f"Expected Ming output sample rate {SAMPLE_RATE}, got {sample_rate}"
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_ming_tts_speaker_conditioning_differs(ming_engine, ming_tokenizer) -> None:
|
||||
"""Test that different Ming speaker controls produce different waveform outputs."""
|
||||
style_outputs = ming_engine.generate(
|
||||
prompts=[_build_prompt(ming_tokenizer)],
|
||||
sampling_params_list=_sampling_params_list(),
|
||||
py_generator=False,
|
||||
)
|
||||
ip_outputs = ming_engine.generate(
|
||||
prompts=[_build_prompt(ming_tokenizer, text=TEST_TEXT, instruction={"IP": "灵小甄"}, use_zero_spk_emb=True)],
|
||||
sampling_params_list=_sampling_params_list(),
|
||||
py_generator=False,
|
||||
)
|
||||
|
||||
style_final_outputs = _extract_final_audio_outputs(style_outputs)
|
||||
ip_final_outputs = _extract_final_audio_outputs(ip_outputs)
|
||||
assert len(style_final_outputs) == 1, "No style audio output produced"
|
||||
assert len(ip_final_outputs) == 1, "No IP audio output produced"
|
||||
|
||||
style_waveform = _extract_audio(_extract_multimodal_output(style_final_outputs[0]))
|
||||
ip_waveform = _extract_audio(_extract_multimodal_output(ip_final_outputs[0]))
|
||||
assert style_waveform.numel() > MIN_AUDIO_SAMPLES
|
||||
assert ip_waveform.numel() > MIN_AUDIO_SAMPLES
|
||||
assert np.max(np.abs(style_waveform.numpy())) > 0.01, "Style audio appears silent"
|
||||
assert np.max(np.abs(ip_waveform.numpy())) > 0.01, "IP audio appears silent"
|
||||
|
||||
overlap = min(int(style_waveform.numel()), int(ip_waveform.numel()))
|
||||
mean_abs_diff = torch.mean(torch.abs(style_waveform[:overlap] - ip_waveform[:overlap])).item()
|
||||
assert style_waveform.shape != ip_waveform.shape or mean_abs_diff > 1e-4, (
|
||||
"Speaker-conditioned outputs should differ, but style and IP waveforms were effectively identical"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_ming_tts_multiple_prompts_queued(ming_engine, ming_tokenizer) -> None:
|
||||
"""Regression: supported max_num_seqs=1 config must still drain queued prompts."""
|
||||
prompts = [
|
||||
_build_prompt(
|
||||
ming_tokenizer,
|
||||
text="第一条语音用于验证 Ming dense 队列中的长请求可以完成。",
|
||||
instruction="平静自然的旁白,语速中等",
|
||||
),
|
||||
_build_prompt(
|
||||
ming_tokenizer,
|
||||
text="第二条短语音也必须生成完成。",
|
||||
instruction="清晰明亮的女声",
|
||||
),
|
||||
]
|
||||
outputs = ming_engine.generate(
|
||||
prompts=prompts,
|
||||
sampling_params_list=_sampling_params_list(),
|
||||
py_generator=False,
|
||||
)
|
||||
final_outputs = _extract_final_audio_outputs(outputs)
|
||||
assert len(final_outputs) == len(prompts), f"Expected {len(prompts)} audio outputs, got {len(final_outputs)}"
|
||||
for i, output in enumerate(final_outputs):
|
||||
waveform = _extract_audio(_extract_multimodal_output(output))
|
||||
duration_s = waveform.numel() / SAMPLE_RATE
|
||||
assert 0.1 < duration_s < 30.0, f"Request {i} audio duration out of range: {duration_s:.2f}s"
|
||||
assert np.max(np.abs(waveform.numpy())) > 0.01, f"Request {i} audio appears silent"
|
||||
|
||||
|
||||
@pytest.mark.advanced_model
|
||||
@pytest.mark.omni
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
def test_ming_tts_offline_streaming(async_omni_engine, ming_tokenizer) -> None:
|
||||
"""Test async_chunk streaming Ming generation through AsyncOmni."""
|
||||
|
||||
async def _run() -> None:
|
||||
all_audio_chunks = []
|
||||
accumulated_samples = 0
|
||||
chunk_idx = 0
|
||||
sample_rate = None
|
||||
async for stage_output in async_omni_engine.generate(
|
||||
prompt=_build_prompt(ming_tokenizer),
|
||||
request_id=str(uuid.uuid4()),
|
||||
sampling_params_list=_sampling_params_list(),
|
||||
):
|
||||
multimodal_output = stage_output.multimodal_output or {}
|
||||
audio = multimodal_output.get("audio")
|
||||
if "sr" in multimodal_output:
|
||||
sample_rate = _extract_sample_rate(multimodal_output)
|
||||
if audio is None:
|
||||
continue
|
||||
finished = stage_output.finished
|
||||
if isinstance(audio, torch.Tensor):
|
||||
if finished:
|
||||
audio_chunk = audio[accumulated_samples:].float().detach().cpu()
|
||||
else:
|
||||
audio_chunk = audio.float().detach().cpu()
|
||||
elif isinstance(audio, list):
|
||||
audio_chunk = _flatten_audio(audio)
|
||||
else:
|
||||
audio_chunk = torch.as_tensor(audio, dtype=torch.float32).reshape(-1).cpu()
|
||||
accumulated_samples += int(audio_chunk.numel())
|
||||
chunk_idx += 1
|
||||
if audio_chunk.numel() > 0:
|
||||
all_audio_chunks.append(audio_chunk)
|
||||
assert all_audio_chunks, "No streaming audio chunks received"
|
||||
waveform = torch.cat(all_audio_chunks, dim=0)
|
||||
assert waveform.numel() > MIN_AUDIO_SAMPLES
|
||||
assert np.max(np.abs(waveform.numpy())) > 0.01, "Audio appears silent"
|
||||
assert sample_rate is not None, "Streaming path did not return a sample rate"
|
||||
assert sample_rate == SAMPLE_RATE, f"Expected Ming output sample rate {SAMPLE_RATE}, got {sample_rate}"
|
||||
|
||||
asyncio.run(_run())
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""
|
||||
E2E online serving tests for Ming-omni-tts (dense) model.
|
||||
Tests text-to-audio via /v1/audio/speech endpoint.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniServerParams
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
pytestmark = [pytest.mark.advanced_model, pytest.mark.omni]
|
||||
|
||||
MODEL = "inclusionAI/Ming-omni-tts-0.5B"
|
||||
DEPLOY_CONFIG = get_deploy_config_path("ming_tts.yaml")
|
||||
|
||||
no_async_chunk_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=DEPLOY_CONFIG,
|
||||
server_args=["--enforce-eager", "--no-async-chunk"],
|
||||
),
|
||||
id="no_async_chunk",
|
||||
)
|
||||
]
|
||||
|
||||
async_chunk_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=DEPLOY_CONFIG,
|
||||
server_args=["--enforce-eager"],
|
||||
),
|
||||
id="async_chunk",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def get_prompt(prompt_type="zh"):
|
||||
prompts = {
|
||||
"zh": "今天天气真不错,适合出去散散步。",
|
||||
"zh_short": "这款产品的名字,叫变态坑爹牛肉丸。",
|
||||
}
|
||||
return prompts.get(prompt_type, prompts["zh"])
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", no_async_chunk_params, indirect=True)
|
||||
def test_text_to_audio_non_streaming_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Deploy Setting: ming_tts.yaml with --no-async-chunk
|
||||
Input Modal: text
|
||||
Output Modal: audio
|
||||
Input Setting: stream=False
|
||||
Datasets: two concurrent requests
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("zh"),
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"timeout": 300.0,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config, request_num=2)
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", async_chunk_params, indirect=True)
|
||||
def test_text_to_audio_streaming_001(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Deploy Setting: ming_tts.yaml (async_chunk=true)
|
||||
Input Modal: text + voice
|
||||
Output Modal: audio (streamed)
|
||||
Input Setting: stream=True
|
||||
Datasets: single request
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": get_prompt("zh_short"),
|
||||
"voice": "灵小甄",
|
||||
"stream": True,
|
||||
"response_format": "wav",
|
||||
"timeout": 300.0,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -0,0 +1,185 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.entrypoints.openai.serving_speech import OmniOpenAIServingSpeech
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAEConfig
|
||||
from vllm_omni.model_executor.models.common.ming.fm import Solver
|
||||
from vllm_omni.model_executor.models.ming_tts.constants import (
|
||||
AGGREGATOR_HIDDEN_SIZE,
|
||||
HISTORY_PATCH_SIZE,
|
||||
LATENT_DIM,
|
||||
LLM_HIDDEN_SIZE,
|
||||
LLM_VOCAB_SIZE,
|
||||
PATCH_SIZE,
|
||||
SAMPLE_RATE,
|
||||
VAE_PATCH_SIZE,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_tts.flowloss_head import FlowLoss
|
||||
from vllm_omni.model_executor.models.ming_tts.ming_tts_llm import MingLLMModel
|
||||
from vllm_omni.model_executor.models.ming_tts.validation import validate_ming_tts_config
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.tts]
|
||||
|
||||
|
||||
def test_ming_tts_audio_vae_uses_common_config():
|
||||
"""AudioVAEConfig is shared by Ming dense and Ming flash modules."""
|
||||
cfg = AudioVAEConfig(sample_rate=16000, patch_size=-1)
|
||||
|
||||
assert cfg.sample_rate == 16000
|
||||
assert cfg.patch_size == -1
|
||||
|
||||
|
||||
def test_ming_tts_cfm_solver_uses_common_implementation():
|
||||
"""Ming dense imports the shared solver implementation directly."""
|
||||
assert Solver.__module__ == "vllm_omni.model_executor.models.common.ming.fm"
|
||||
|
||||
|
||||
def test_ming_tts_flowloss_preserves_checkpoint_prefix():
|
||||
flowloss = FlowLoss(z_channels=4, llm_cond_dim=8, hidden_size=16, depth=1, num_heads=2)
|
||||
|
||||
assert any(name.startswith("cfm.model.") for name in flowloss.state_dict())
|
||||
|
||||
|
||||
def test_ming_dense_validation_rejects_semantic_audio_vae_config():
|
||||
"""Dense 0.5B validation rejects semantic AudioVAE configs."""
|
||||
cfg = SimpleNamespace(
|
||||
audio_dummy_token_id=151705,
|
||||
audio_eos_token_id=151704,
|
||||
text_eos_token_id=151669,
|
||||
audio_tokenizer_config=AudioVAEConfig(
|
||||
sample_rate=SAMPLE_RATE,
|
||||
patch_size=VAE_PATCH_SIZE,
|
||||
semantic_module_kwargs={"whisper_encoder": {}},
|
||||
enc_kwargs={"latent_dim": LATENT_DIM, "input_dim": 882, "hop_size": 882},
|
||||
dec_kwargs={"latent_dim": LATENT_DIM, "output_dim": 882},
|
||||
),
|
||||
latent_dim=LATENT_DIM,
|
||||
patch_size=PATCH_SIZE,
|
||||
history_patch_size=HISTORY_PATCH_SIZE,
|
||||
llm_hidden_size=LLM_HIDDEN_SIZE,
|
||||
llm_vocab_size=LLM_VOCAB_SIZE,
|
||||
sample_rate=SAMPLE_RATE,
|
||||
vae_patch_size=VAE_PATCH_SIZE,
|
||||
llm_config={"hidden_size": LLM_HIDDEN_SIZE},
|
||||
aggregator_config={"hidden_size": AGGREGATOR_HIDDEN_SIZE},
|
||||
ditar_config={"hidden_size": AGGREGATOR_HIDDEN_SIZE},
|
||||
latent_chunk_size=1,
|
||||
latent_left_context=0,
|
||||
max_decode_steps=1,
|
||||
stop_head_threshold=0.5,
|
||||
stop_head_min_steps=0,
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="semantic_module_kwargs"):
|
||||
validate_ming_tts_config(cfg)
|
||||
|
||||
|
||||
def test_ming_instruction_parser_preserves_dense_and_flash_defaults():
|
||||
"""Ming dense and Ming flash keep distinct instruction defaults."""
|
||||
serving = object.__new__(OmniOpenAIServingSpeech)
|
||||
serving.uploaded_speakers = {"uploaded": {}}
|
||||
|
||||
dense_plain = serving._parse_ming_instruction(SimpleNamespace(instructions="calm", language=None, voice=None))
|
||||
assert dense_plain == "calm"
|
||||
|
||||
dense_with_fields = serving._parse_ming_instruction(
|
||||
SimpleNamespace(instructions="calm", language="Auto", voice="灵小甄")
|
||||
)
|
||||
assert dense_with_fields == {"IP": "灵小甄", "风格": "calm"}
|
||||
|
||||
flash_fields = serving._parse_ming_instruction_fields(
|
||||
SimpleNamespace(instructions="calm", language="粤语", voice="灵小甄")
|
||||
)
|
||||
assert flash_fields == {"风格": "calm"}
|
||||
|
||||
|
||||
def _make_ming_logits_model(vocab_size=8):
|
||||
model = object.__new__(MingLLMModel)
|
||||
model.ming_config = SimpleNamespace(
|
||||
llm_vocab_size=vocab_size,
|
||||
max_decode_steps=1,
|
||||
stop_head_min_steps=0,
|
||||
text_eos_token_id=7,
|
||||
)
|
||||
model._last_text_mode = False
|
||||
model._last_ming_next_token_ids = None
|
||||
return model
|
||||
|
||||
|
||||
def test_ming_compute_logits_uses_cached_forced_next_token_ids():
|
||||
model = _make_ming_logits_model()
|
||||
model._last_ming_next_token_ids = [2, 5]
|
||||
|
||||
logits = MingLLMModel.compute_logits(model, torch.zeros((2, 4)), SimpleNamespace())
|
||||
|
||||
assert logits.shape == (2, 8)
|
||||
assert logits[0, 2].item() == 0.0
|
||||
assert logits[1, 5].item() == 0.0
|
||||
assert torch.isneginf(logits[0, [0, 1, 3, 4, 5, 6, 7]]).all()
|
||||
assert torch.isneginf(logits[1, [0, 1, 2, 3, 4, 6, 7]]).all()
|
||||
assert model._last_ming_next_token_ids is None
|
||||
|
||||
|
||||
def test_ming_compute_logits_falls_back_to_dummy_token_id():
|
||||
model = _make_ming_logits_model()
|
||||
|
||||
logits = MingLLMModel.compute_logits(model, torch.zeros((1, 4)), SimpleNamespace())
|
||||
assert logits.shape == (1, 8)
|
||||
assert logits[0, 7].item() == 0.0
|
||||
assert torch.isneginf(logits[0, [0, 1, 2, 3, 4, 5, 6]]).all()
|
||||
|
||||
|
||||
def test_ming_forward_non_decode_return_clears_cached_forced_next_token_ids():
|
||||
class FakeBackbone:
|
||||
def __call__(self, **kwargs):
|
||||
return kwargs["inputs_embeds"]
|
||||
|
||||
model = _make_ming_logits_model()
|
||||
model.model = FakeBackbone()
|
||||
model._last_ming_next_token_ids = [2]
|
||||
|
||||
output = MingLLMModel.forward(
|
||||
model,
|
||||
input_ids=torch.tensor([1]),
|
||||
positions=torch.tensor([0]),
|
||||
inputs_embeds=torch.zeros((1, 4)),
|
||||
model_intermediate_buffer=[],
|
||||
)
|
||||
|
||||
assert isinstance(output, OmniOutput)
|
||||
assert output.multimodal_outputs is None
|
||||
assert model._last_ming_next_token_ids is None
|
||||
|
||||
|
||||
def test_ming_compute_logits_rejects_forced_token_batch_mismatch():
|
||||
model = _make_ming_logits_model()
|
||||
model._last_ming_next_token_ids = [2]
|
||||
|
||||
with pytest.raises(RuntimeError, match="batch mismatch"):
|
||||
MingLLMModel.compute_logits(model, torch.zeros((2, 4)), SimpleNamespace())
|
||||
|
||||
|
||||
def test_ming_compute_logits_text_mode_delegates_to_backbone():
|
||||
class FakeBackbone:
|
||||
def __init__(self):
|
||||
self.hidden_states = None
|
||||
|
||||
def compute_logits(self, hidden_states):
|
||||
self.hidden_states = hidden_states
|
||||
return torch.ones((hidden_states.shape[0], 3))
|
||||
|
||||
model = _make_ming_logits_model(vocab_size=3)
|
||||
model._last_text_mode = True
|
||||
model.model = FakeBackbone()
|
||||
hidden_states = torch.zeros((2, 4))
|
||||
|
||||
logits = MingLLMModel.compute_logits(model, hidden_states, SimpleNamespace())
|
||||
|
||||
assert torch.equal(model.model.hidden_states, hidden_states)
|
||||
assert torch.equal(logits, torch.ones((2, 3)))
|
||||
@@ -921,6 +921,7 @@ def test_cosyvoice3_full_payload_replace_keys_present() -> None:
|
||||
def test_ming_flash_omni_thinker2talker_token_only_smoke() -> None:
|
||||
"""Smoke: ming_flash_omni token-only carries voice metadata."""
|
||||
from vllm_omni.model_executor.stage_input_processors.ming_flash_omni import (
|
||||
thinker2talker,
|
||||
thinker2talker_token_only,
|
||||
)
|
||||
|
||||
@@ -938,14 +939,15 @@ def test_ming_flash_omni_thinker2talker_token_only_smoke() -> None:
|
||||
|
||||
src = [_Wrap("hello world")]
|
||||
prompt = _Prompt({"voice_name": "ZH_FEMALE", "prompt_text": "ref text"})
|
||||
out = thinker2talker_token_only(src, prompt=prompt)
|
||||
assert len(out) == 1
|
||||
assert out[0]["prompt_token_ids"] == [0] # talker self-tokenizes; dummy id
|
||||
info = out[0]["additional_information"]
|
||||
assert info["text"] == "hello world"
|
||||
assert info["voice_name"] == "ZH_FEMALE"
|
||||
assert info["prompt_text"] == "ref text"
|
||||
assert info["ming_task"] == "omni"
|
||||
for func in (thinker2talker, thinker2talker_token_only):
|
||||
out = func(src, prompt=prompt)
|
||||
assert len(out) == 1
|
||||
assert out[0]["prompt_token_ids"] == [0]
|
||||
info = out[0]["additional_information"]
|
||||
assert info["text"] == "hello world"
|
||||
assert info["voice_name"] == "ZH_FEMALE"
|
||||
assert info["prompt_text"] == "ref text"
|
||||
assert info["ming_task"] == "omni"
|
||||
|
||||
|
||||
def test_qwen2_5_omni_thinker2talker_token_only_smoke() -> None:
|
||||
|
||||
@@ -101,6 +101,10 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = {
|
||||
"vllm_omni.model_executor.models.mimo_audio.pipeline",
|
||||
"MIMO_AUDIO_PIPELINE",
|
||||
),
|
||||
"ming_tts": (
|
||||
"vllm_omni.model_executor.models.ming_tts.pipeline",
|
||||
"MING_TTS_PIPELINE",
|
||||
),
|
||||
"voxtral_tts": (
|
||||
"vllm_omni.model_executor.models.voxtral_tts.pipeline",
|
||||
"VOXTRAL_TTS_PIPELINE",
|
||||
|
||||
@@ -1128,6 +1128,19 @@ class StageConfigFactory:
|
||||
continue
|
||||
return cls._create_from_registry(registered.model_type, cli_overrides, deploy_config_path)
|
||||
|
||||
# An explicit deploy config can select a pipeline when the model's HF
|
||||
# metadata is too generic for registry auto-detection.
|
||||
if deploy_config_path is not None:
|
||||
deploy_path = Path(deploy_config_path)
|
||||
if deploy_path.exists():
|
||||
deploy_config = load_deploy_config(deploy_path)
|
||||
if deploy_config.pipeline in _PIPELINE_REGISTRY:
|
||||
return cls._create_from_registry(
|
||||
deploy_config.pipeline,
|
||||
cli_overrides,
|
||||
deploy_config_path,
|
||||
)
|
||||
|
||||
# Not in the pipeline registry — let the caller fall back to the
|
||||
# legacy ``stage_configs/*.yaml`` path (resolve_model_config_path).
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Ming-omni-tts deploy: stage-0 LLM+flow -> stage-1 audio VAE.
|
||||
# Verified legacy settings migrated from ming_tts.yaml and ming_tts_async_chunk.yaml.
|
||||
# Default mode is async-chunk streaming on a single GPU.
|
||||
# pipeline: required — HF config reports model_type="dense" with no architectures,
|
||||
# so auto-detection falls through. This key routes the deploy-config probe in
|
||||
# StageConfigFactory.create_from_model to the correct registry entry.
|
||||
pipeline: ming_tts
|
||||
async_chunk: true
|
||||
trust_remote_code: false
|
||||
dtype: bfloat16
|
||||
enable_prefix_caching: false
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
latent_chunk_size: 25
|
||||
latent_left_context: 0
|
||||
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 1
|
||||
gpu_memory_utilization: 0.45
|
||||
enforce_eager: true
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 8192
|
||||
devices: "0"
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 512
|
||||
detokenize: true
|
||||
|
||||
- stage_id: 1
|
||||
max_num_seqs: 1
|
||||
gpu_memory_utilization: 0.25
|
||||
enforce_eager: true
|
||||
enable_prefix_caching: false
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 8192
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
top_p: 1.0
|
||||
top_k: -1
|
||||
max_tokens: 1
|
||||
detokenize: false
|
||||
@@ -35,6 +35,7 @@ def _register_omni_hf_configs() -> None:
|
||||
try:
|
||||
from transformers import AutoConfig
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import MingDenseConfig
|
||||
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import (
|
||||
Qwen3TTSConfig,
|
||||
)
|
||||
@@ -55,6 +56,7 @@ def _register_omni_hf_configs() -> None:
|
||||
_CONFIG_REGISTRY = None
|
||||
|
||||
for model_type, config_cls in [
|
||||
("dense", MingDenseConfig),
|
||||
("qwen3_tts", Qwen3TTSConfig),
|
||||
("cosyvoice3", CosyVoice3Config),
|
||||
("glm_tts", GLMTTSConfig),
|
||||
|
||||
@@ -7,6 +7,43 @@ from pydantic import AliasChoices, BaseModel, Field, field_validator, model_vali
|
||||
_MAX_EMBEDDING_DIM = 8192
|
||||
|
||||
|
||||
def _normalize_ref_audio_value(value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
items = []
|
||||
for item in value:
|
||||
if not isinstance(item, str):
|
||||
raise TypeError("'ref_audio' list entries must be strings")
|
||||
items.append(item)
|
||||
if not items:
|
||||
raise ValueError("'ref_audio' list cannot be empty")
|
||||
return items
|
||||
raise TypeError("'ref_audio' must be a string or list of strings")
|
||||
|
||||
|
||||
def _normalize_speaker_embedding_value(value):
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise TypeError("'speaker_embedding' must be a list of numbers or list of embedding vectors")
|
||||
if not value:
|
||||
return []
|
||||
|
||||
first = value[0]
|
||||
if isinstance(first, (list, tuple)):
|
||||
embeddings = []
|
||||
for item in value:
|
||||
if not isinstance(item, (list, tuple)):
|
||||
raise TypeError("'speaker_embedding' must not mix flat and nested values")
|
||||
embeddings.append([float(x) for x in item])
|
||||
return embeddings
|
||||
|
||||
return [float(x) for x in value]
|
||||
|
||||
|
||||
class OpenAICreateSpeechRequest(BaseModel):
|
||||
input: str
|
||||
model: str | None = None
|
||||
@@ -46,7 +83,7 @@ class OpenAICreateSpeechRequest(BaseModel):
|
||||
default=None,
|
||||
description="Language code (e.g., 'Chinese', 'English', 'Auto')",
|
||||
)
|
||||
ref_audio: str | None = Field(
|
||||
ref_audio: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Reference audio for voice cloning (Base task). URL, base64, or file URI.",
|
||||
)
|
||||
@@ -73,7 +110,7 @@ class OpenAICreateSpeechRequest(BaseModel):
|
||||
default=None,
|
||||
description="Use speaker embedding only without in-context learning (Base task)",
|
||||
)
|
||||
speaker_embedding: list[float] | None = Field(
|
||||
speaker_embedding: list[float] | list[list[float]] | None = Field(
|
||||
default=None,
|
||||
max_length=_MAX_EMBEDDING_DIM,
|
||||
description="Pre-computed speaker embedding vector (1024-dim for 0.6B, "
|
||||
@@ -108,10 +145,29 @@ class OpenAICreateSpeechRequest(BaseModel):
|
||||
raise ValueError("'sse' is not a supported stream_format yet. Please use 'audio'.")
|
||||
return v
|
||||
|
||||
@field_validator("ref_audio", mode="before")
|
||||
@classmethod
|
||||
def normalize_ref_audio(cls, v):
|
||||
return _normalize_ref_audio_value(v)
|
||||
|
||||
@field_validator("speaker_embedding")
|
||||
@classmethod
|
||||
def validate_speaker_embedding(cls, v: list[float] | None) -> list[float] | None:
|
||||
if v is not None and not all(math.isfinite(x) for x in v):
|
||||
def validate_speaker_embedding(
|
||||
cls, v: list[float] | list[list[float]] | None
|
||||
) -> list[float] | list[list[float]] | None:
|
||||
v = _normalize_speaker_embedding_value(v)
|
||||
if v is None:
|
||||
return None
|
||||
if not v:
|
||||
return []
|
||||
if isinstance(v[0], list):
|
||||
for item in v:
|
||||
if not item:
|
||||
raise ValueError("'speaker_embedding' nested vectors must be non-empty")
|
||||
if not all(math.isfinite(x) for x in item):
|
||||
raise ValueError("'speaker_embedding' values must be finite (no NaN or Inf)")
|
||||
return v
|
||||
if not all(math.isfinite(x) for x in v):
|
||||
raise ValueError("'speaker_embedding' values must be finite (no NaN or Inf)")
|
||||
return v
|
||||
|
||||
@@ -210,7 +266,7 @@ class OpenAICreateSpeechRequest(BaseModel):
|
||||
@model_validator(mode="after")
|
||||
def validate_embedding_constraints(self) -> "OpenAICreateSpeechRequest":
|
||||
if self.speaker_embedding is not None:
|
||||
if self.ref_audio is not None:
|
||||
if self.ref_audio is not None and not isinstance(self.ref_audio, list):
|
||||
raise ValueError("'speaker_embedding' and 'ref_audio' are mutually exclusive")
|
||||
return self
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ from vllm_omni.utils.speaker_cache import (
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# TTS Configuration
|
||||
_MING_TTS_MODEL_ARCHS = {"MingTTSForConditionalGeneration"}
|
||||
_VOXTRAL_TTS_MODEL_STAGES = {"audio_generation"}
|
||||
_QWEN3_TTS_MODEL_STAGES = {"qwen3_tts"}
|
||||
_FISH_TTS_MODEL_STAGES = {"fish_speech_slow_ar"}
|
||||
@@ -126,6 +127,7 @@ _QWEN3_TTS_REF_AUDIO_CACHE_KEY = "_qwen3_tts_ref_audio_cache_key"
|
||||
_TTS_MAX_INSTRUCTIONS_LENGTH = 500
|
||||
_TTS_MAX_NEW_TOKENS_MIN = 1
|
||||
_TTS_MAX_NEW_TOKENS_MAX = 4096
|
||||
_MING_DEFAULT_PROMPT = MING_DEFAULT_PROMPT
|
||||
|
||||
|
||||
def _create_wav_header(sample_rate: int, num_channels: int = 1, bits_per_sample: int = 16) -> bytes:
|
||||
@@ -528,6 +530,32 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
def _load_codec_frame_rate(self) -> float | None:
|
||||
"""Load codec frame rate from speech tokenizer config for prompt length estimation."""
|
||||
if self._tts_model_type == "ming_tts":
|
||||
try:
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import MingTTSConfig
|
||||
|
||||
hf_config = self.engine_client.model_config.hf_config
|
||||
ming_cfg = MingTTSConfig.from_hf_config(hf_config)
|
||||
patch_size = int(ming_cfg.patch_size)
|
||||
audio_frame_hop = int(ming_cfg.audio_frame_hop)
|
||||
sample_rate = int(ming_cfg.sample_rate)
|
||||
if patch_size <= 0 or audio_frame_hop <= 0 or sample_rate <= 0:
|
||||
raise ValueError(
|
||||
"Ming config has invalid tokenizer timing values: "
|
||||
f"patch_size={patch_size}, audio_frame_hop={audio_frame_hop}, sample_rate={sample_rate}"
|
||||
)
|
||||
rate = float(sample_rate) / float(audio_frame_hop * patch_size)
|
||||
logger.info(
|
||||
"Derived Ming codec frame rate: %.1f Hz (sample_rate=%s, audio_frame_hop=%s, patch_size=%s)",
|
||||
rate,
|
||||
sample_rate,
|
||||
audio_frame_hop,
|
||||
patch_size,
|
||||
)
|
||||
return rate
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to derive Ming codec frame rate from hf_config: {e}")
|
||||
|
||||
try:
|
||||
model_path = self.engine_client.model_config.model
|
||||
st_config_path = os.path.join(model_path, "speech_tokenizer", "config.json")
|
||||
@@ -572,7 +600,15 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
def _find_tts_stage(self):
|
||||
"""Find and return the TTS stage config, or None if not found."""
|
||||
for stage in self.engine_client.stage_configs:
|
||||
if stage.engine_args.model_stage in _TTS_MODEL_STAGES:
|
||||
engine_args = stage.engine_args
|
||||
model_stage = engine_args.model_stage
|
||||
model_arch = getattr(engine_args, "model_arch", None)
|
||||
worker_type = getattr(engine_args, "worker_type", None)
|
||||
if model_stage in _TTS_MODEL_STAGES:
|
||||
return stage
|
||||
# Ming dense identifies its AR entry stage by architecture because
|
||||
# it does not use a dedicated TTS model_stage value.
|
||||
if model_arch in _MING_TTS_MODEL_ARCHS and worker_type == "ar":
|
||||
return stage
|
||||
return None
|
||||
|
||||
@@ -602,6 +638,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
return "voxcpm2"
|
||||
if model_stage in _MING_TTS_MODEL_STAGES:
|
||||
return "ming_flash_omni_tts"
|
||||
if model_arch in _MING_TTS_MODEL_ARCHS:
|
||||
return "ming_tts"
|
||||
if model_stage in _MOSS_TTS_MODEL_STAGES:
|
||||
return "moss_tts_nano"
|
||||
if model_stage in _MOSS_TTS_FULL_MODEL_STAGES:
|
||||
@@ -682,6 +720,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
try:
|
||||
if self._tts_model_type == "glm_tts":
|
||||
return set()
|
||||
if self._tts_model_type == "ming_tts":
|
||||
return set()
|
||||
if self._tts_model_type == "voxcpm2":
|
||||
return {"default"}
|
||||
if self._tts_model_type == "voxtral_tts":
|
||||
@@ -1214,9 +1254,13 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
raise ValueError("'speaker_embedding' values must be finite (no NaN or Inf)")
|
||||
|
||||
emb_dim = len(embedding)
|
||||
dim_err = self._validate_qwen_tts_speaker_embedding_dim(emb_dim)
|
||||
if dim_err is not None:
|
||||
raise ValueError(dim_err)
|
||||
if self._tts_model_type == "ming_tts":
|
||||
if emb_dim != 192:
|
||||
logger.warning("speaker_embedding has %d dimensions; Ming dense expects 192", emb_dim)
|
||||
else:
|
||||
dim_err = self._validate_qwen_tts_speaker_embedding_dim(emb_dim)
|
||||
if dim_err is not None:
|
||||
raise ValueError(dim_err)
|
||||
|
||||
async with self._upload_lock:
|
||||
voice_name_lower = name.lower()
|
||||
@@ -1303,7 +1347,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
def _is_tts_model(self) -> bool:
|
||||
"""Check if the current model is a supported TTS model."""
|
||||
return any(stage.engine_args.model_stage in _TTS_MODEL_STAGES for stage in self.engine_client.stage_configs)
|
||||
return self._find_tts_stage() is not None
|
||||
|
||||
def _validate_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
"""Validate TTS request parameters. Returns error message or None."""
|
||||
@@ -1316,6 +1360,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
if self._tts_model_type == "voxcpm2":
|
||||
return self._validate_voxcpm2_request(request)
|
||||
if self._tts_model_type == "ming_flash_omni_tts":
|
||||
return self._validate_ming_flash_omni_tts_request(request)
|
||||
if self._tts_model_type == "ming_tts":
|
||||
return self._validate_ming_tts_request(request)
|
||||
if self._tts_model_type in ("moss_tts_nano", "moss_tts"):
|
||||
return self._validate_moss_tts_request(request)
|
||||
@@ -1362,7 +1408,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
ids = self._voxcpm2_tokenizer.encode(text, add_special_tokens=True)
|
||||
return split_multichar_chinese(ids, self._voxcpm2_split_map)
|
||||
|
||||
def _validate_ming_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
def _validate_ming_flash_omni_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
"""Validate Ming-flash-omni standalone-talker request parameters."""
|
||||
if not request.input or not request.input.strip():
|
||||
return "Input text cannot be empty"
|
||||
@@ -1399,6 +1445,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
def _validate_ref_audio_format(self, ref_audio: str) -> str | None:
|
||||
"""Validate ref_audio is a supported URI format. Returns error or None."""
|
||||
if not isinstance(ref_audio, str):
|
||||
return "ref_audio must be a URL (http/https), base64 data URL (data:...), or file URI (file://...)"
|
||||
if not (
|
||||
ref_audio.startswith(("http://", "https://"))
|
||||
or ref_audio.startswith("data:")
|
||||
@@ -2066,6 +2114,99 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
return None
|
||||
|
||||
def _validate_ming_tts_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
"""Validate Ming TTS request parameters. Returns error message or None."""
|
||||
if not request.input or not request.input.strip():
|
||||
return "Input text cannot be empty"
|
||||
|
||||
if isinstance(request.ref_audio, list):
|
||||
return self._validate_ming_tts_podcast_request(request)
|
||||
return self._validate_ming_tts_single_speaker_request(request)
|
||||
|
||||
def _validate_ming_tts_single_speaker_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
if request.ref_audio is not None:
|
||||
fmt_err = self._validate_ref_audio_format(request.ref_audio)
|
||||
if fmt_err:
|
||||
return fmt_err
|
||||
|
||||
if request.speaker_embedding is not None:
|
||||
if not request.speaker_embedding:
|
||||
return "'speaker_embedding' must be a non-empty list of floats"
|
||||
emb_len = len(request.speaker_embedding)
|
||||
if emb_len != 192:
|
||||
logger.warning(
|
||||
"speaker_embedding has %d dimensions; Ming dense expects 192. "
|
||||
"Wrong dimensions will likely fail or degrade output.",
|
||||
emb_len,
|
||||
)
|
||||
|
||||
voice_lower = request.voice.lower() if isinstance(request.voice, str) else None
|
||||
uploaded_voice = bool(voice_lower and voice_lower in self.uploaded_speakers)
|
||||
clone_source_present = request.ref_audio is not None or request.speaker_embedding is not None or uploaded_voice
|
||||
|
||||
if request.task_type == "Base" and not clone_source_present:
|
||||
return "Base task requires 'ref_audio', 'speaker_embedding', or an uploaded voice sample"
|
||||
|
||||
if request.ref_audio is not None and request.ref_text is not None and not request.ref_text.strip():
|
||||
return "'ref_text' must be non-empty when provided with 'ref_audio'"
|
||||
|
||||
# Ming offline ref-audio cases use prompt_waveform without prompt_text;
|
||||
# keep the transcript requirement for other TTS models.
|
||||
if request.ref_audio is not None and request.speaker_embedding is None and not self._is_ming_tts_model():
|
||||
uploaded_ref_text = self.uploaded_speakers[voice_lower].get("ref_text") if uploaded_voice else None
|
||||
if not (request.ref_text and request.ref_text.strip()) and not uploaded_ref_text:
|
||||
return "Reference-audio cloning requires non-empty 'ref_text'"
|
||||
|
||||
if request.ref_text is not None and request.ref_audio is None and not uploaded_voice:
|
||||
return "'ref_text' requires 'ref_audio' or an uploaded voice sample"
|
||||
|
||||
if request.instructions and len(request.instructions) > self._max_instructions_length:
|
||||
return f"Instructions too long (max {self._max_instructions_length} characters)"
|
||||
|
||||
if request.max_new_tokens is not None:
|
||||
if request.max_new_tokens < _TTS_MAX_NEW_TOKENS_MIN:
|
||||
return f"max_new_tokens must be at least {_TTS_MAX_NEW_TOKENS_MIN}"
|
||||
if request.max_new_tokens > _TTS_MAX_NEW_TOKENS_MAX:
|
||||
return f"max_new_tokens cannot exceed {_TTS_MAX_NEW_TOKENS_MAX}"
|
||||
|
||||
return None
|
||||
|
||||
def _validate_ming_tts_podcast_request(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
if len(request.ref_audio) < 2:
|
||||
return "Podcast-style Ming requests require at least two 'ref_audio' clips"
|
||||
|
||||
for ref_audio in request.ref_audio:
|
||||
fmt_err = self._validate_ref_audio_format(ref_audio)
|
||||
if fmt_err:
|
||||
return fmt_err
|
||||
|
||||
if not request.ref_text or not request.ref_text.strip():
|
||||
return "Podcast-style Ming requests require non-empty 'ref_text'"
|
||||
|
||||
if request.speaker_embedding is not None:
|
||||
embeddings = request.speaker_embedding
|
||||
embedding_count = len(embeddings) if embeddings and isinstance(embeddings[0], list) else 1
|
||||
if embedding_count != len(request.ref_audio):
|
||||
return (
|
||||
"Podcast-style Ming requests require one speaker embedding per ref_audio clip; "
|
||||
f"got {embedding_count} embeddings for {len(request.ref_audio)} clips"
|
||||
)
|
||||
if embeddings and isinstance(embeddings[0], list):
|
||||
for item in embeddings:
|
||||
if len(item) != 192:
|
||||
return "Podcast-style Ming speaker embeddings must each have 192 dimensions"
|
||||
|
||||
if request.instructions and len(request.instructions) > self._max_instructions_length:
|
||||
return f"Instructions too long (max {self._max_instructions_length} characters)"
|
||||
|
||||
if request.max_new_tokens is not None:
|
||||
if request.max_new_tokens < _TTS_MAX_NEW_TOKENS_MIN:
|
||||
return f"max_new_tokens must be at least {_TTS_MAX_NEW_TOKENS_MIN}"
|
||||
if request.max_new_tokens > _TTS_MAX_NEW_TOKENS_MAX:
|
||||
return f"max_new_tokens cannot exceed {_TTS_MAX_NEW_TOKENS_MAX}"
|
||||
|
||||
return None
|
||||
|
||||
async def _resolve_ref_audio(self, ref_audio_str: str) -> tuple[list[float], int]:
|
||||
"""Resolve ref_audio to (wav_samples, sample_rate).
|
||||
|
||||
@@ -2192,6 +2333,142 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
def _discard_ref_audio_artifact_warmup(self, request_id: str) -> None:
|
||||
self._request_ref_audio_artifact_keys.pop(request_id, None)
|
||||
|
||||
async def _resolve_ref_audio_many(self, ref_audio_list: list[str]) -> list[tuple[list[float], int]]:
|
||||
resolved = []
|
||||
for ref_audio in ref_audio_list:
|
||||
resolved.append(await self._resolve_ref_audio(ref_audio))
|
||||
return resolved
|
||||
|
||||
# ---- Ming TTS helpers ----
|
||||
|
||||
def _is_ming_tts_model(self) -> bool:
|
||||
return self._tts_model_type == "ming_tts"
|
||||
|
||||
def _coerce_ming_prompt_waveform(self, wav_samples, sample_rate):
|
||||
from torchaudio.functional import resample as resample_audio
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import SAMPLE_RATE
|
||||
|
||||
waveform = torch.as_tensor(wav_samples, dtype=torch.float32).reshape(1, -1)
|
||||
if int(sample_rate) != SAMPLE_RATE:
|
||||
waveform = resample_audio(waveform, int(sample_rate), SAMPLE_RATE)
|
||||
return waveform
|
||||
|
||||
def _build_ming_prompt_waveform(
|
||||
self,
|
||||
ref_audio_data: tuple[list[float], int] | list[tuple[list[float], int]] | None,
|
||||
):
|
||||
if isinstance(ref_audio_data, list):
|
||||
return torch.cat(
|
||||
[self._coerce_ming_prompt_waveform(item[0], item[1]) for item in ref_audio_data],
|
||||
dim=-1,
|
||||
)
|
||||
if ref_audio_data is not None:
|
||||
return self._coerce_ming_prompt_waveform(ref_audio_data[0], ref_audio_data[1])
|
||||
return None
|
||||
|
||||
def _extract_ming_speaker_embeddings_from_ref_audio(
|
||||
self,
|
||||
ref_audio_data_list: list[tuple[list[float], int]],
|
||||
) -> list[list[float]]:
|
||||
from vllm_omni.model_executor.models.ming_tts.speaker_extractor import MingSpeakerEmbeddingExtractor
|
||||
|
||||
extractor = MingSpeakerEmbeddingExtractor(self.engine_client.model_config.model, target_sr=16000)
|
||||
embeddings = []
|
||||
for wav_samples, sr in ref_audio_data_list:
|
||||
waveform = torch.as_tensor(wav_samples, dtype=torch.float32).reshape(1, -1)
|
||||
embedding = extractor.extract_from_waveform(waveform, int(sr))
|
||||
flat = embedding.detach().reshape(-1).to(torch.float32).cpu()
|
||||
if int(flat.numel()) != 192:
|
||||
raise ValueError(f"Ming speaker extractor returned {int(flat.numel())} dims; expected 192")
|
||||
embeddings.append(flat.tolist())
|
||||
return embeddings
|
||||
|
||||
def _parse_ming_instruction_fields(
|
||||
self,
|
||||
request,
|
||||
*,
|
||||
include_language=False,
|
||||
include_voice=False,
|
||||
plain_text_passthrough=False,
|
||||
):
|
||||
instruction_text = request.instructions.strip() if isinstance(request.instructions, str) else None
|
||||
instruction_dict: dict[str, Any] = {}
|
||||
|
||||
if include_language and request.language not in (None, "", "Auto"):
|
||||
instruction_dict["方言"] = request.language
|
||||
|
||||
voice_lower = request.voice.lower() if isinstance(request.voice, str) else None
|
||||
if include_voice and request.voice and not (voice_lower and voice_lower in self.uploaded_speakers):
|
||||
instruction_dict["IP"] = request.voice
|
||||
|
||||
if instruction_text:
|
||||
try:
|
||||
parsed = json.loads(instruction_text)
|
||||
except json.JSONDecodeError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
instruction_dict.update(parsed)
|
||||
elif instruction_dict or not plain_text_passthrough:
|
||||
instruction_dict["风格"] = instruction_text
|
||||
else:
|
||||
return instruction_text
|
||||
|
||||
return instruction_dict or None
|
||||
|
||||
def _parse_ming_instruction(self, request: OpenAICreateSpeechRequest) -> Any:
|
||||
"""Build a Ming instruction payload from OpenAI speech fields."""
|
||||
return self._parse_ming_instruction_fields(
|
||||
request,
|
||||
include_language=True,
|
||||
include_voice=True,
|
||||
plain_text_passthrough=True,
|
||||
)
|
||||
|
||||
def _build_ming_dense_prompt(
|
||||
self,
|
||||
request: OpenAICreateSpeechRequest,
|
||||
*,
|
||||
ref_audio_data: tuple[list[float], int] | list[tuple[list[float], int]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a Ming dense prompt directly from the OpenAI speech request."""
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import KEY_MAX_DECODE_STEPS
|
||||
from vllm_omni.model_executor.models.ming_tts.prompt_assembly import build_ming_dense_prompt
|
||||
|
||||
if self._tts_tokenizer is None:
|
||||
model_name = self.engine_client.model_config.model
|
||||
self._tts_tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=False)
|
||||
|
||||
ref_text = request.ref_text
|
||||
prompt_waveform = self._build_ming_prompt_waveform(ref_audio_data) if ref_text is not None else None
|
||||
speaker_embedding = request.speaker_embedding
|
||||
use_zero_spk_emb = prompt_waveform is None and speaker_embedding is None
|
||||
|
||||
runtime_controls = {}
|
||||
if request.max_new_tokens is not None:
|
||||
runtime_controls[KEY_MAX_DECODE_STEPS] = request.max_new_tokens
|
||||
|
||||
prompt_dict = build_ming_dense_prompt(
|
||||
self._tts_tokenizer,
|
||||
# bgm / music-prompt mode not supported online;
|
||||
# requires prompt_mode API extension (deferred).
|
||||
prompt=_MING_DEFAULT_PROMPT,
|
||||
text=request.input,
|
||||
runtime_controls=runtime_controls or None,
|
||||
instruction=self._parse_ming_instruction(request),
|
||||
prompt_text=ref_text,
|
||||
prompt_waveform=prompt_waveform,
|
||||
speaker_embedding=speaker_embedding,
|
||||
use_zero_spk_emb=use_zero_spk_emb,
|
||||
)
|
||||
prompt = tokens_input(prompt_token_ids=prompt_dict["prompt_token_ids"])
|
||||
prompt["prompt"] = prompt_dict["prompt"]
|
||||
prompt["text"] = prompt_dict["text"]
|
||||
prompt["additional_information"] = prompt_dict["additional_information"]
|
||||
return prompt
|
||||
|
||||
async def _generate_audio_chunks(
|
||||
self,
|
||||
generator,
|
||||
@@ -2796,25 +3073,12 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
|
||||
# ---- Ming-flash-omni standalone-talker (TTS) helpers ----
|
||||
|
||||
def _build_ming_prompt(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]:
|
||||
def _build_ming_flash_omni_prompt(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]:
|
||||
# request.instructions accepts two forms:
|
||||
# 1. Plain text: mapped to the caption's 风格 (style) field
|
||||
# 2. JSON object: parsed and splatted into the caption. Unlocks
|
||||
# Unknown keys are dropped by `ming_create_instruction`.
|
||||
caption_fields: dict[str, Any] = {}
|
||||
if request.instructions:
|
||||
stripped = request.instructions.strip()
|
||||
if stripped.startswith("{"):
|
||||
try:
|
||||
parsed = json.loads(stripped)
|
||||
except json.JSONDecodeError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
caption_fields.update(parsed)
|
||||
else:
|
||||
caption_fields["风格"] = request.instructions
|
||||
else:
|
||||
caption_fields["风格"] = request.instructions
|
||||
caption_fields = self._parse_ming_instruction_fields(request) or {}
|
||||
|
||||
has_spk_emb = request.speaker_embedding is not None
|
||||
|
||||
@@ -2982,8 +3246,29 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
prompt = await self._build_glm_tts_prompt(request, has_inline_ref_audio=has_inline_ref_audio)
|
||||
tts_params = {}
|
||||
elif self._tts_model_type == "ming_flash_omni_tts":
|
||||
prompt = self._build_ming_prompt(request)
|
||||
prompt = self._build_ming_flash_omni_prompt(request)
|
||||
tts_params = {}
|
||||
elif self._tts_model_type == "ming_tts":
|
||||
ref_audio_source = request.ref_audio
|
||||
voice_lower = request.voice.lower() if isinstance(request.voice, str) else None
|
||||
if ref_audio_source is None and voice_lower in self.uploaded_speakers:
|
||||
ref_audio_source = self._get_uploaded_audio_data(request.voice)
|
||||
if request.ref_text is None:
|
||||
request.ref_text = self.uploaded_speakers[voice_lower].get("ref_text")
|
||||
ref_audio_data = None
|
||||
if isinstance(ref_audio_source, list):
|
||||
ref_audio_data = await self._resolve_ref_audio_many(ref_audio_source)
|
||||
if request.speaker_embedding is None:
|
||||
request.speaker_embedding = self._extract_ming_speaker_embeddings_from_ref_audio(ref_audio_data)
|
||||
elif ref_audio_source is not None and isinstance(ref_audio_source, str):
|
||||
wav_list, sr = await self._resolve_ref_audio(ref_audio_source)
|
||||
ref_audio_data = (wav_list, sr)
|
||||
if request.speaker_embedding is None:
|
||||
request.speaker_embedding = self._extract_ming_speaker_embeddings_from_ref_audio(
|
||||
[ref_audio_data]
|
||||
)[0]
|
||||
prompt = self._build_ming_dense_prompt(request, ref_audio_data=ref_audio_data)
|
||||
tts_params = prompt.get("additional_information", {})
|
||||
elif self._tts_model_type in ("moss_tts_nano", "moss_tts"):
|
||||
tts_params = await self._build_moss_tts_params(request)
|
||||
if request.voice:
|
||||
@@ -3069,6 +3354,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
model_type = "voxcpm2"
|
||||
elif self._tts_model_type == "ming_flash_omni_tts":
|
||||
model_type = "ming_flash_omni_tts"
|
||||
elif self._tts_model_type == "ming_tts":
|
||||
model_type = "ming_tts"
|
||||
elif self._tts_model_type == "moss_tts_nano":
|
||||
model_type = "moss_tts_nano"
|
||||
elif self._tts_model_type == "moss_tts":
|
||||
@@ -3177,6 +3464,17 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
getattr(sampling_params_list[0], "min_tokens", 0),
|
||||
request.max_new_tokens,
|
||||
)
|
||||
elif self._tts_model_type == "ming_tts" and sampling_params_list:
|
||||
import copy
|
||||
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import TEXT_EOS_TOKEN_ID
|
||||
|
||||
sampling_params_list = copy.deepcopy(sampling_params_list)
|
||||
sampling_params_list[0].stop_token_ids = [int(TEXT_EOS_TOKEN_ID)]
|
||||
if request.max_new_tokens is not None:
|
||||
# Ming emits TEXT_EOS after the latent decode budget is exhausted, so
|
||||
# Stage-0 needs one extra token beyond ming_max_decode_steps.
|
||||
sampling_params_list[0].max_tokens = int(request.max_new_tokens) + 1
|
||||
|
||||
if request.seed is not None and sampling_params_list:
|
||||
if sampling_params_list is self.engine_client.default_sampling_params_list:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,209 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/audio_tokenizer/istft.py
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.buffer_len = self.win_length - self.hop_length
|
||||
|
||||
def _buffer_process(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
buffer: torch.Tensor | None,
|
||||
pad: int,
|
||||
last_chunk: bool = False,
|
||||
streaming: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
if streaming:
|
||||
if buffer is None:
|
||||
# first chunk
|
||||
x = x[:, pad:]
|
||||
if buffer is not None:
|
||||
# next chunk
|
||||
x[:, : self.buffer_len] += buffer
|
||||
buffer = x[:, -self.buffer_len :]
|
||||
if not last_chunk:
|
||||
x = x[:, : -self.buffer_len]
|
||||
else:
|
||||
x = x[:, :-pad]
|
||||
else:
|
||||
x = x[:, pad:-pad]
|
||||
|
||||
return x, buffer
|
||||
|
||||
def forward(
|
||||
self,
|
||||
spec: torch.Tensor,
|
||||
audio_buffer: torch.Tensor | None = None,
|
||||
window_buffer: torch.Tensor | None = None,
|
||||
streaming: bool = False,
|
||||
last_chunk: bool = False,
|
||||
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
||||
"""
|
||||
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.
|
||||
audio_buffer (Tensor): [Streaming Input/State] The audio overlap buffer from the previous chunk.
|
||||
Shape: (B, win_length - hop_length)
|
||||
window_buffer (Tensor): [Streaming Input/State] The window overlap buffer from the previous chunk.
|
||||
streaming: If `True`, the function operates in streaming mode, processing `spec` as a single chunk.
|
||||
last_chunk: When `streaming=True` and `last_chunk=True`, the function can perform final "flush" operations
|
||||
|
||||
Returns:
|
||||
Reconstructed signal, plus streaming buffers when `padding="same"`.
|
||||
"""
|
||||
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'.")
|
||||
|
||||
if spec.dim() != 3:
|
||||
raise ValueError(f"Expected spec rank-3 [Batch, Freq, Time], got {tuple(spec.shape)}")
|
||||
_, _, 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, :]
|
||||
|
||||
y, audio_buffer = self._buffer_process(y, audio_buffer, pad, last_chunk=last_chunk, streaming=streaming)
|
||||
|
||||
# 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(0)
|
||||
.squeeze(0)
|
||||
)
|
||||
|
||||
window_envelope, window_buffer = self._buffer_process(
|
||||
window_envelope, window_buffer, pad, last_chunk=last_chunk, streaming=streaming
|
||||
)
|
||||
window_envelope = window_envelope.squeeze()
|
||||
|
||||
# Normalize
|
||||
if not (window_envelope > 1e-11).all():
|
||||
raise RuntimeError("ISTFT window envelope underflowed; invalid overlap-add state.")
|
||||
y = y / window_envelope
|
||||
|
||||
return y, audio_buffer, window_buffer
|
||||
|
||||
|
||||
class FourierHead(nn.Module):
|
||||
"""Base class for inverse fourier modules."""
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement the forward method.")
|
||||
|
||||
|
||||
class ISTFTHead(FourierHead):
|
||||
"""
|
||||
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,
|
||||
audio_buffer: torch.Tensor | None = None,
|
||||
window_buffer: torch.Tensor | None = None,
|
||||
streaming: bool = False,
|
||||
last_chunk: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None]:
|
||||
"""
|
||||
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:
|
||||
Audio, predicted spectrogram coefficients, and streaming buffers.
|
||||
"""
|
||||
x_pred = self.out(x)
|
||||
x_pred = x_pred.transpose(1, 2)
|
||||
mag, p = x_pred.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
|
||||
S = mag * (x + 1j * y)
|
||||
audio, audio_buffer, window_buffer = self.istft(
|
||||
S, audio_buffer=audio_buffer, window_buffer=window_buffer, streaming=streaming, last_chunk=last_chunk
|
||||
)
|
||||
return audio.unsqueeze(1), x_pred, audio_buffer, window_buffer
|
||||
|
||||
|
||||
__all__ = ["FourierHead", "ISTFT", "ISTFTHead"]
|
||||
+175
-188
@@ -1,10 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025 The vLLM-Omni team.
|
||||
# Copyright (c) Ant Group. All rights reserved.
|
||||
# Adapted from:
|
||||
# https://github.com/inclusionAI/Ming/tree/e58533db227031990c5a6864dcf5f08fb53ed0d2/AudioVAE
|
||||
|
||||
from __future__ import annotations
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
@@ -13,6 +8,8 @@ from transformers import PretrainedConfig, PreTrainedModel, Qwen2Config, Qwen2Mo
|
||||
from transformers.utils import is_flash_attn_2_available
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from .audio_dsp import ISTFTHead
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@@ -21,114 +18,50 @@ class AudioVAEConfig(PretrainedConfig):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sample_rate: int = 44100,
|
||||
enc_kwargs: dict | None = None,
|
||||
dec_kwargs: dict | None = None,
|
||||
init_method: str = "kaiming",
|
||||
patch_size: int = 4,
|
||||
sample_rate=44100,
|
||||
enc_kwargs=None,
|
||||
semantic_module_kwargs=None,
|
||||
dec_kwargs=None,
|
||||
hifi_gan_disc_kwargs=None,
|
||||
spec_disc_kwargs=None,
|
||||
lambda_disc=1.0,
|
||||
lambda_mel_loss=15,
|
||||
lambda_adv=1.0,
|
||||
lambda_feat_match_loss=1.0,
|
||||
lambda_semantic=5.0,
|
||||
init_method="kaiming",
|
||||
patch_size=4,
|
||||
**kwargs,
|
||||
):
|
||||
self.sample_rate = sample_rate
|
||||
self.enc_kwargs = enc_kwargs or {}
|
||||
self.semantic_module_kwargs = semantic_module_kwargs
|
||||
self.dec_kwargs = dec_kwargs or {}
|
||||
self.hifi_gan_disc_kwargs = hifi_gan_disc_kwargs
|
||||
self.spec_disc_kwargs = spec_disc_kwargs
|
||||
self.lambda_disc = lambda_disc
|
||||
self.lambda_mel_loss = lambda_mel_loss
|
||||
self.lambda_adv = lambda_adv
|
||||
self.lambda_feat_match_loss = lambda_feat_match_loss
|
||||
self.lambda_semantic = lambda_semantic
|
||||
self.init_method = init_method
|
||||
self.patch_size = patch_size
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class ISTFT(nn.Module):
|
||||
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)
|
||||
self.buffer_len = self.win_length - self.hop_length
|
||||
|
||||
def _buffer_process(self, x, buffer, pad, last_chunk=False, streaming=False):
|
||||
if streaming:
|
||||
if buffer is None:
|
||||
x = x[:, pad:]
|
||||
if buffer is not None:
|
||||
x[:, : self.buffer_len] += buffer
|
||||
buffer = x[:, -self.buffer_len :]
|
||||
if not last_chunk:
|
||||
x = x[:, : -self.buffer_len]
|
||||
else:
|
||||
x = x[:, :-pad]
|
||||
else:
|
||||
x = x[:, pad:-pad]
|
||||
return x, buffer
|
||||
|
||||
def forward(self, spec, audio_buffer=None, window_buffer=None, streaming=False, last_chunk=False):
|
||||
if self.padding == "center":
|
||||
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'.")
|
||||
|
||||
B, N, T = spec.shape
|
||||
ifft = torch.fft.irfft(spec, self.n_fft, dim=1, norm="backward")
|
||||
ifft = ifft * self.window[None, :, None]
|
||||
|
||||
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, :]
|
||||
|
||||
y, audio_buffer = self._buffer_process(y, audio_buffer, pad, last_chunk=last_chunk, streaming=streaming)
|
||||
|
||||
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(0)
|
||||
.squeeze(0)
|
||||
)
|
||||
|
||||
window_envelope, window_buffer = self._buffer_process(
|
||||
window_envelope, window_buffer, pad, last_chunk=last_chunk, streaming=streaming
|
||||
)
|
||||
window_envelope = window_envelope.squeeze()
|
||||
|
||||
assert (window_envelope > 1e-11).all()
|
||||
y = y / window_envelope
|
||||
|
||||
return y, audio_buffer, window_buffer
|
||||
|
||||
|
||||
class ISTFTHead(nn.Module):
|
||||
def __init__(self, dim: int, n_fft: int, hop_length: int, padding: str = "same"):
|
||||
super().__init__()
|
||||
out_dim = n_fft + 2
|
||||
self.out = 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, audio_buffer=None, window_buffer=None, streaming=False, last_chunk=False):
|
||||
x_pred = self.out(x)
|
||||
x_pred = x_pred.transpose(1, 2)
|
||||
mag, p = x_pred.chunk(2, dim=1)
|
||||
mag = torch.exp(mag)
|
||||
mag = torch.clip(mag, max=1e2)
|
||||
x = torch.cos(p)
|
||||
y = torch.sin(p)
|
||||
S = mag * (x + 1j * y)
|
||||
audio, audio_buffer, window_buffer = self.istft(
|
||||
S, audio_buffer=audio_buffer, window_buffer=window_buffer, streaming=streaming, last_chunk=last_chunk
|
||||
)
|
||||
return audio.unsqueeze(1), x_pred, audio_buffer, window_buffer
|
||||
def _qwen2_config(backbone):
|
||||
config = Qwen2Config.from_dict(config_dict=backbone)
|
||||
if (getattr(config, "_attn_implementation", None) or getattr(config, "attn_implementation", None)) not in (
|
||||
None,
|
||||
"flash_attention_2",
|
||||
):
|
||||
return config
|
||||
if is_flash_attn_2_available():
|
||||
config._attn_implementation_autoset = True
|
||||
config._attn_implementation = "flash_attention_2"
|
||||
else:
|
||||
config._attn_implementation = "sdpa"
|
||||
return config
|
||||
|
||||
|
||||
class StreamingLinearUpsample(nn.Module):
|
||||
@@ -138,6 +71,8 @@ class StreamingLinearUpsample(nn.Module):
|
||||
self.upsampler = nn.Upsample(scale_factor=scale_factor, mode="linear", align_corners=False)
|
||||
|
||||
def forward(self, x, state=None, is_last=False):
|
||||
if x is None and is_last and (state is None or state.get("prev_chunk") is None):
|
||||
raise ValueError("Received end-of-stream without any latent chunk to upsample.")
|
||||
if state is None:
|
||||
state = {"prev_chunk": None, "history_last": None, "is_first": True}
|
||||
|
||||
@@ -188,16 +123,72 @@ class StreamingLinearUpsample(nn.Module):
|
||||
return final_out, state
|
||||
|
||||
|
||||
class Encoder(nn.Module):
|
||||
def __init__(self, encoder_args, input_dim=320, hop_size=320, latent_dim=64, patch_size=-1):
|
||||
super().__init__()
|
||||
config = _qwen2_config(encoder_args)
|
||||
logger.info("AudioVAE Encoder: using attn_implementation=%r", config._attn_implementation)
|
||||
self.encoder = Qwen2Model(config)
|
||||
self.input_dim = input_dim
|
||||
self.hop_size = hop_size
|
||||
self.latent_dim = latent_dim
|
||||
self.fc1 = nn.Linear(input_dim, config.hidden_size, bias=False)
|
||||
self.fc2 = nn.Linear(config.hidden_size, config.hidden_size)
|
||||
self.fc3 = nn.Linear(config.hidden_size, latent_dim * 2)
|
||||
self.norm = nn.LayerNorm(config.hidden_size)
|
||||
self.patch_size = patch_size
|
||||
if patch_size != -1:
|
||||
aggregator_config = _qwen2_config({**encoder_args, "num_hidden_layers": 4})
|
||||
self.aggregator = Qwen2Model(aggregator_config)
|
||||
self.cls_embed = nn.Parameter(torch.rand(1, 1, config.hidden_size))
|
||||
self.cls_embed.data.normal_(0, 0.02)
|
||||
|
||||
def get_frames(self, x):
|
||||
num_frames_total = (x.size(-1) + self.hop_size - 1) // self.hop_size
|
||||
expected_len = (num_frames_total - 1) * self.hop_size + self.input_dim
|
||||
padding_needed = expected_len - x.size(-1)
|
||||
waveform = F.pad(x, (0, padding_needed), value=0.0)
|
||||
frames = waveform.unfold(dimension=-1, size=self.input_dim, step=self.hop_size)
|
||||
return frames
|
||||
|
||||
def pad_patch_insert_cls(self, x):
|
||||
bsz, _, dim = x.size()
|
||||
num_frame = x.size(1)
|
||||
r = num_frame % self.patch_size
|
||||
pad_num = self.patch_size - r if r else 0
|
||||
x = F.pad(x, (0, 0, 0, pad_num), value=0.0)
|
||||
# [Batch, Time, Dimension] -> [Batch*PatchGroups, Patch, Dimension].
|
||||
x = x.reshape(-1, self.patch_size, dim)
|
||||
x = torch.cat((x, self.cls_embed.expand(x.size(0), -1, -1)), dim=1)
|
||||
# [Batch*PatchGroups, Patch+1, Dimension] -> [Batch, Time, Dimension].
|
||||
x = x.reshape(bsz, -1, dim)
|
||||
return x
|
||||
|
||||
def forward(self, waveform):
|
||||
x = self.get_frames(waveform)
|
||||
x = self.fc1(x)
|
||||
x = self.fc2(x)
|
||||
x = self.encoder(inputs_embeds=x)
|
||||
x = x.last_hidden_state
|
||||
|
||||
if self.patch_size != -1:
|
||||
x = self.pad_patch_insert_cls(x)
|
||||
x = self.aggregator(inputs_embeds=x)
|
||||
x = x.last_hidden_state
|
||||
bsz, _, dim = x.size()
|
||||
# [Batch, Time, Dimension] -> [Batch*PatchGroups, Patch+1, Dimension].
|
||||
x = x.reshape(-1, self.patch_size + 1, dim)
|
||||
# [Batch*PatchGroups, 1, Dimension] -> [Batch, PatchGroups, Dimension].
|
||||
x = x[:, -1:, :].reshape(bsz, -1, dim)
|
||||
|
||||
x = self.fc3(x)
|
||||
return x, waveform.unsqueeze(1)
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
def __init__(self, decoder_args, output_dim=320, latent_dim=64, patch_size=-1):
|
||||
super().__init__()
|
||||
config = Qwen2Config.from_dict(config_dict=decoder_args)
|
||||
if is_flash_attn_2_available():
|
||||
config._attn_implementation_autoset = True
|
||||
config._attn_implementation = "flash_attention_2"
|
||||
else:
|
||||
config._attn_implementation = "sdpa"
|
||||
|
||||
config = _qwen2_config(decoder_args)
|
||||
logger.info("AudioVAE Decoder: using attn_implementation=%r", config._attn_implementation)
|
||||
self.decoder = Qwen2Model(config)
|
||||
self.output_dim = output_dim
|
||||
@@ -211,7 +202,21 @@ class Decoder(nn.Module):
|
||||
if self.patch_size != -1:
|
||||
self.upsampling = StreamingLinearUpsample(scale_factor=patch_size)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.fc1(x)
|
||||
|
||||
if self.patch_size != -1:
|
||||
# [Batch, Time, Dimension] -> [Batch, Dimension, Time] -> [Batch, Time, Dimension].
|
||||
x = self.upsampling.upsampler(x.transpose(1, 2)).transpose(1, 2)
|
||||
|
||||
x = self.decoder(inputs_embeds=x)
|
||||
x = x.last_hidden_state
|
||||
x, _ = self.head(x)
|
||||
return x, None
|
||||
|
||||
def low_level_reconstruct(self, x, past_key_values=None, use_cache=False, stream_state=None, last_chunk=False):
|
||||
if stream_state is None:
|
||||
stream_state = (None, None, None)
|
||||
upsample_state, audio_buffer, window_buffer = stream_state
|
||||
bsz, device, dtype = x.size(0), x.device, x.dtype
|
||||
x = self.fc1(x)
|
||||
@@ -222,6 +227,7 @@ class Decoder(nn.Module):
|
||||
stream_state = (upsample_state, audio_buffer, window_buffer)
|
||||
return torch.empty(bsz, 1, 0, device=device, dtype=dtype), stream_state, past_key_values
|
||||
else:
|
||||
# [Batch, Time, Dimension] -> [Batch, Dimension, Time] -> [Batch, Time, Dimension].
|
||||
x = self.upsampling.upsampler(x.transpose(1, 2)).transpose(1, 2)
|
||||
|
||||
hidden_states_list = []
|
||||
@@ -239,7 +245,6 @@ class Decoder(nn.Module):
|
||||
past_len = 0
|
||||
|
||||
curr_len = x.shape[1]
|
||||
|
||||
if past_len < target_len and (past_len + curr_len) >= sw_size:
|
||||
fill_len = target_len - past_len
|
||||
x_fill = x[:, :fill_len, :]
|
||||
@@ -269,86 +274,39 @@ class Decoder(nn.Module):
|
||||
return x_out, stream_state, past_key_values
|
||||
|
||||
|
||||
class Encoder(nn.Module):
|
||||
def __init__(self, encoder_args, input_dim=320, hop_size=320, latent_dim=64, patch_size=-1):
|
||||
super().__init__()
|
||||
config = Qwen2Config.from_dict(config_dict=encoder_args)
|
||||
if is_flash_attn_2_available():
|
||||
config._attn_implementation_autoset = True
|
||||
config._attn_implementation = "flash_attention_2"
|
||||
else:
|
||||
config._attn_implementation = "sdpa"
|
||||
|
||||
logger.info("AudioVAE Encoder: using attn_implementation=%r", config._attn_implementation)
|
||||
self.encoder = Qwen2Model(config)
|
||||
self.input_dim = input_dim
|
||||
self.hop_size = hop_size
|
||||
self.latent_dim = latent_dim
|
||||
self.fc1 = nn.Linear(input_dim, config.hidden_size, bias=False)
|
||||
self.fc2 = nn.Linear(config.hidden_size, config.hidden_size)
|
||||
self.fc3 = nn.Linear(config.hidden_size, latent_dim * 2)
|
||||
self.norm = nn.LayerNorm(config.hidden_size)
|
||||
self.patch_size = patch_size
|
||||
if patch_size != -1:
|
||||
config.num_hidden_layers = 4
|
||||
self.aggregator = Qwen2Model(config)
|
||||
self.cls_embed = nn.Parameter(torch.rand(1, 1, config.hidden_size))
|
||||
self.cls_embed.data.normal_(0, 0.02)
|
||||
|
||||
def get_frames(self, x):
|
||||
num_frames_total = (x.size(-1) + self.hop_size - 1) // self.hop_size
|
||||
expected_len = (num_frames_total - 1) * self.hop_size + self.input_dim
|
||||
padding_needed = expected_len - x.size(-1)
|
||||
waveform = F.pad(x, (0, padding_needed), value=0.0)
|
||||
frames = waveform.unfold(dimension=-1, size=self.input_dim, step=self.hop_size)
|
||||
return frames
|
||||
|
||||
def pad_patch_insert_cls(self, x):
|
||||
bsz, _, dim = x.size()
|
||||
num_frame = x.size(1)
|
||||
r = num_frame % self.patch_size
|
||||
pad_num = self.patch_size - r if r else 0
|
||||
x = F.pad(x, (0, 0, 0, pad_num), value=0.0)
|
||||
x = x.reshape(-1, self.patch_size, dim)
|
||||
x = torch.cat((x, self.cls_embed.expand(x.size(0), -1, -1)), dim=1)
|
||||
x = x.reshape(bsz, -1, dim)
|
||||
return x
|
||||
|
||||
def forward(self, waveform):
|
||||
x = self.get_frames(waveform)
|
||||
x = self.fc1(x)
|
||||
x = self.fc2(x)
|
||||
x = self.encoder(inputs_embeds=x)
|
||||
x = x.last_hidden_state
|
||||
|
||||
if self.patch_size != -1:
|
||||
x = self.pad_patch_insert_cls(x)
|
||||
x = self.aggregator(inputs_embeds=x)
|
||||
x = x.last_hidden_state
|
||||
bsz, _, dim = x.size()
|
||||
x = x.reshape(-1, self.patch_size + 1, dim)
|
||||
x = x[:, -1:, :].reshape(bsz, -1, dim)
|
||||
|
||||
x = self.fc3(x)
|
||||
return x, waveform.unsqueeze(1)
|
||||
|
||||
|
||||
class AudioVAE(PreTrainedModel):
|
||||
config_class = AudioVAEConfig
|
||||
|
||||
def __init__(self, config: AudioVAEConfig):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
enc_kwargs = config.enc_kwargs
|
||||
dec_kwargs = config.dec_kwargs
|
||||
for key in ("backbone", "input_dim", "latent_dim"):
|
||||
if key not in enc_kwargs:
|
||||
raise ValueError(f"AudioVAE.enc_kwargs missing required key: {key}")
|
||||
for key in ("backbone", "output_dim", "latent_dim"):
|
||||
if key not in dec_kwargs:
|
||||
raise ValueError(f"AudioVAE.dec_kwargs missing required key: {key}")
|
||||
|
||||
hop_size = enc_kwargs.get("hop_size", enc_kwargs["input_dim"])
|
||||
if enc_kwargs["input_dim"] != hop_size:
|
||||
raise ValueError(f"AudioVAE encoder input_dim ({enc_kwargs['input_dim']}) != hop_size ({hop_size}).")
|
||||
if hop_size != dec_kwargs["output_dim"]:
|
||||
raise ValueError(
|
||||
f"AudioVAE encoder hop_size ({hop_size}) != decoder output_dim ({dec_kwargs['output_dim']})."
|
||||
)
|
||||
self.encoder = Encoder(
|
||||
encoder_args=config.enc_kwargs["backbone"],
|
||||
input_dim=config.enc_kwargs["input_dim"],
|
||||
hop_size=config.enc_kwargs.get("hop_size", 320),
|
||||
latent_dim=config.enc_kwargs["latent_dim"],
|
||||
encoder_args=enc_kwargs["backbone"],
|
||||
input_dim=enc_kwargs["input_dim"],
|
||||
hop_size=hop_size,
|
||||
latent_dim=enc_kwargs["latent_dim"],
|
||||
patch_size=config.patch_size,
|
||||
)
|
||||
self.decoder = Decoder(
|
||||
decoder_args=config.dec_kwargs["backbone"],
|
||||
output_dim=config.dec_kwargs["output_dim"],
|
||||
latent_dim=config.dec_kwargs["latent_dim"],
|
||||
decoder_args=dec_kwargs["backbone"],
|
||||
output_dim=dec_kwargs["output_dim"],
|
||||
latent_dim=dec_kwargs["latent_dim"],
|
||||
patch_size=config.patch_size,
|
||||
)
|
||||
self.post_init()
|
||||
@@ -367,21 +325,50 @@ class AudioVAE(PreTrainedModel):
|
||||
if module.padding_idx is not None:
|
||||
module.weight.data[module.padding_idx].zero_()
|
||||
|
||||
@torch.inference_mode()
|
||||
def encode_latent(self, waveform, waveform_length):
|
||||
from diffusers.models.autoencoders.autoencoder_oobleck import OobleckDiagonalGaussianDistribution
|
||||
if waveform.ndim != 2:
|
||||
raise ValueError(f"Expected waveform rank-2 [Batch, Time], got {tuple(waveform.shape)}")
|
||||
if waveform_length.ndim != 1:
|
||||
raise ValueError(f"Expected waveform_length rank-1 [Batch], got {tuple(waveform_length.shape)}")
|
||||
if waveform.shape[0] != waveform_length.shape[0]:
|
||||
raise ValueError(
|
||||
"Batch mismatch: "
|
||||
f"waveform batch={waveform.shape[0]} vs "
|
||||
f"waveform_length batch={waveform_length.shape[0]}"
|
||||
)
|
||||
if torch.any(waveform_length <= 0):
|
||||
raise ValueError("waveform_length must be strictly positive.")
|
||||
|
||||
frame_num = torch.ceil(waveform_length / self.config.enc_kwargs["input_dim"]).to(torch.int32)
|
||||
if self.config.patch_size != -1:
|
||||
frame_num = torch.ceil(frame_num / self.config.patch_size)
|
||||
h, y = self.encoder(waveform)
|
||||
h, _ = self.encoder(waveform)
|
||||
h = h.transpose(1, 2)
|
||||
|
||||
posterior = OobleckDiagonalGaussianDistribution(h)
|
||||
latent = posterior.sample()
|
||||
mean, logvar = torch.chunk(h, 2, dim=1)
|
||||
logvar = torch.clamp(logvar, -30.0, 20.0)
|
||||
std = torch.exp(0.5 * logvar)
|
||||
latent = mean + std * torch.randn_like(mean)
|
||||
latent = latent.transpose(1, 2)
|
||||
return latent, frame_num
|
||||
|
||||
@torch.inference_mode()
|
||||
def decode(self, latent, past_key_values=None, use_cache=False, stream_state=(None, None, None), last_chunk=False):
|
||||
if latent.dim() != 3:
|
||||
raise ValueError(f"Expected latent rank-3 [B,T,D], got shape={tuple(latent.shape)}")
|
||||
if latent.shape[0] <= 0:
|
||||
raise ValueError("latent batch size must be positive.")
|
||||
|
||||
target_dtype = next(self.decoder.parameters()).dtype
|
||||
target_device = next(self.decoder.parameters()).device
|
||||
if latent.dtype != target_dtype or latent.device != target_device:
|
||||
latent = latent.to(device=target_device, dtype=target_dtype)
|
||||
|
||||
expected_latent_dim = self.config.dec_kwargs["latent_dim"]
|
||||
if latent.shape[-1] != expected_latent_dim:
|
||||
raise ValueError(f"Latent dim mismatch in decode(): got {latent.shape[-1]}, expected {expected_latent_dim}")
|
||||
|
||||
waveform, stream_state, past_key_values = self.decoder.low_level_reconstruct(
|
||||
latent,
|
||||
past_key_values=past_key_values,
|
||||
@@ -0,0 +1,193 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
from x_transformers.x_transformers import apply_rotary_pos_emb
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, dim, eps=1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = nn.Parameter(torch.ones(dim))
|
||||
|
||||
def forward(self, x):
|
||||
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
||||
x = x.to(self.weight.dtype)
|
||||
x = F.rms_norm(x, normalized_shape=(x.shape[-1],), weight=self.weight, eps=self.eps)
|
||||
return x
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, dim, dim_out=None, mult=4, dropout=0.0, approximate="none"):
|
||||
super().__init__()
|
||||
inner_dim = int(dim * mult)
|
||||
dim_out = dim_out if dim_out is not None else dim
|
||||
|
||||
activation = nn.GELU(approximate=approximate)
|
||||
project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation)
|
||||
self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out))
|
||||
|
||||
def forward(self, x):
|
||||
return self.ff(x)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
heads=8,
|
||||
dim_head=64,
|
||||
dropout=0.0,
|
||||
qk_norm=None,
|
||||
pe_attn_head=None,
|
||||
attn_mask_enabled=True,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.heads = heads
|
||||
self.inner_dim = dim_head * heads
|
||||
self.dropout = dropout
|
||||
self.to_q = nn.Linear(dim, self.inner_dim)
|
||||
self.to_k = nn.Linear(dim, self.inner_dim)
|
||||
self.to_v = nn.Linear(dim, self.inner_dim)
|
||||
if qk_norm is None:
|
||||
self.q_norm = None
|
||||
self.k_norm = None
|
||||
elif qk_norm == "rms_norm":
|
||||
self.q_norm = RMSNorm(dim_head)
|
||||
self.k_norm = RMSNorm(dim_head)
|
||||
else:
|
||||
raise ValueError(f"Unimplemented qk_norm: {qk_norm}")
|
||||
|
||||
self.to_out = nn.ModuleList([])
|
||||
self.to_out.append(nn.Linear(self.inner_dim, dim))
|
||||
self.to_out.append(nn.Dropout(dropout))
|
||||
self.pe_attn_head = pe_attn_head
|
||||
self.attn_mask_enabled = attn_mask_enabled
|
||||
|
||||
def forward(self, x, mask=None, rope=None):
|
||||
batch_size = x.shape[0]
|
||||
query = self.to_q(x)
|
||||
key = self.to_k(x)
|
||||
value = self.to_v(x)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // self.heads
|
||||
query = query.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
key = key.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
|
||||
if self.q_norm is not None:
|
||||
query = self.q_norm(query)
|
||||
if self.k_norm is not None:
|
||||
key = self.k_norm(key)
|
||||
|
||||
if rope is not None:
|
||||
freqs, xpos_scale = rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
if self.pe_attn_head is not None:
|
||||
on = self.pe_attn_head
|
||||
query[:, :on, :, :] = apply_rotary_pos_emb(query[:, :on, :, :], freqs, q_xpos_scale)
|
||||
key[:, :on, :, :] = apply_rotary_pos_emb(key[:, :on, :, :], freqs, k_xpos_scale)
|
||||
else:
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
|
||||
if self.attn_mask_enabled and mask is not None:
|
||||
valid_sample_indices = mask.any(dim=1)
|
||||
final_output = torch.zeros_like(query).to(query.device)
|
||||
attn_mask = mask[valid_sample_indices]
|
||||
query = query[valid_sample_indices]
|
||||
key = key[valid_sample_indices]
|
||||
value = value[valid_sample_indices]
|
||||
attn_mask = attn_mask.unsqueeze(1).unsqueeze(1)
|
||||
attn_mask = attn_mask.expand(valid_sample_indices.sum().item(), self.heads, query.shape[-2], key.shape[-2])
|
||||
else:
|
||||
attn_mask = None
|
||||
|
||||
x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
|
||||
if self.attn_mask_enabled and mask is not None:
|
||||
final_output[valid_sample_indices] = x
|
||||
x = final_output
|
||||
|
||||
x = x.transpose(1, 2).reshape(batch_size, -1, self.heads * head_dim)
|
||||
x = x.to(query.dtype)
|
||||
x = self.to_out[0](x)
|
||||
x = self.to_out[1](x)
|
||||
|
||||
if mask is not None:
|
||||
mask = mask.unsqueeze(-1)
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class DiTBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size,
|
||||
num_heads,
|
||||
mlp_ratio=4.0,
|
||||
dropout=0.1,
|
||||
qk_norm=None,
|
||||
pe_attn_head=None,
|
||||
attn_mask_enabled=True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
self.norm1 = RMSNorm(hidden_size)
|
||||
self.attn = Attention(
|
||||
dim=hidden_size,
|
||||
heads=num_heads,
|
||||
dim_head=hidden_size // num_heads,
|
||||
dropout=dropout,
|
||||
qk_norm=qk_norm,
|
||||
pe_attn_head=pe_attn_head,
|
||||
attn_mask_enabled=attn_mask_enabled,
|
||||
)
|
||||
self.norm2 = RMSNorm(hidden_size)
|
||||
self.mlp = FeedForward(dim=hidden_size, mult=mlp_ratio, dropout=dropout, approximate="tanh")
|
||||
|
||||
def forward(self, x, mask, rope):
|
||||
x = x + self.attn(self.norm1(x), mask=mask, rope=rope)
|
||||
x = x + self.mlp(self.norm2(x))
|
||||
return x
|
||||
|
||||
|
||||
class FinalLayer(nn.Module):
|
||||
def __init__(self, hidden_size, out_channels):
|
||||
super().__init__()
|
||||
self.norm_final = RMSNorm(hidden_size)
|
||||
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.norm_final(x)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class CondEmbedder(nn.Module):
|
||||
def __init__(self, input_feature_size, hidden_size):
|
||||
super().__init__()
|
||||
self.cond_embedder = nn.Linear(input_feature_size, hidden_size)
|
||||
|
||||
def forward(self, llm_cond):
|
||||
return self.cond_embedder(llm_cond)
|
||||
|
||||
|
||||
def get_epss_timesteps(n, device, dtype):
|
||||
dt = 1 / 32
|
||||
predefined_timesteps = {
|
||||
5: [0, 2, 4, 8, 16, 32],
|
||||
6: [0, 2, 4, 6, 8, 16, 32],
|
||||
7: [0, 2, 4, 6, 8, 16, 24, 32],
|
||||
10: [0, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
|
||||
12: [0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32],
|
||||
16: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32],
|
||||
}
|
||||
t = predefined_timesteps.get(n, [])
|
||||
if not t:
|
||||
return torch.linspace(0, 1, n + 1, device=device, dtype=dtype)
|
||||
return dt * torch.tensor(t, device=device, dtype=dtype)
|
||||
@@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
|
||||
from .dit import get_epss_timesteps
|
||||
|
||||
|
||||
def apply_sway_sampling(t, sway_sampling_coef):
|
||||
if sway_sampling_coef is None:
|
||||
return t
|
||||
return t + sway_sampling_coef * (torch.cos(torch.pi / 2 * t) - 1 + t)
|
||||
|
||||
|
||||
def build_timesteps(steps, device, dtype, use_epss=True, sway_sampling_coef=-1.0):
|
||||
if steps <= 0:
|
||||
raise ValueError(f"steps must be positive, got {steps}")
|
||||
if use_epss:
|
||||
t = get_epss_timesteps(steps, device=device, dtype=dtype)
|
||||
else:
|
||||
t = torch.linspace(0, 1, steps + 1, device=device, dtype=dtype)
|
||||
return apply_sway_sampling(t, sway_sampling_coef)
|
||||
|
||||
|
||||
class Solver:
|
||||
def __init__(self, func, y0, sigma=0.25, temperature=1.5) -> None:
|
||||
self.func = func
|
||||
self.y0 = y0
|
||||
self.sigma = sigma
|
||||
self.temperature = temperature
|
||||
|
||||
def integrate(self, t):
|
||||
solution = torch.empty(len(t), *self.y0.shape, dtype=self.y0.dtype, device=self.y0.device)
|
||||
solution[0] = self.y0
|
||||
|
||||
j = 1
|
||||
y0 = self.y0
|
||||
for t0, t1 in zip(t[:-1], t[1:]):
|
||||
dt = t1 - t0
|
||||
f0 = self.func(t0, y0)
|
||||
dy = dt * f0
|
||||
y1 = y0 + dy
|
||||
|
||||
while j < len(t) and t1 >= t[j]:
|
||||
solution[j] = self._linear_interp(t0, t1, y0, y1, t[j])
|
||||
j += 1
|
||||
|
||||
noise = torch.randn_like(y0)
|
||||
shift = self.sigma * (self.temperature**0.5) * (abs(dt) ** 0.5) * noise
|
||||
y0 = y1 + shift
|
||||
|
||||
return solution
|
||||
|
||||
def _linear_interp(self, t0, t1, y0, y1, t):
|
||||
if t == t0:
|
||||
return y0
|
||||
if t == t1:
|
||||
return y1
|
||||
slope = (t - t0) / (t1 - t0)
|
||||
return y0 + slope * (y1 - y0)
|
||||
|
||||
|
||||
def integrate_cfm_steps(fn, y0, t, sde_args, sde_rnd, steps):
|
||||
for step in range(steps):
|
||||
dt = t[step + 1] - t[step]
|
||||
y0 = y0 + fn(t[step], y0) * dt
|
||||
y0 = y0 + sde_args[1] * (sde_args[2] ** 0.5) * (dt.abs() ** 0.5) * sde_rnd[step]
|
||||
return y0
|
||||
@@ -26,10 +26,10 @@ from vllm.sequence import IntermediateTensors
|
||||
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAE, AudioVAEConfig
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.transformers_utils.configs.ming_flash_omni import MingFlashOmniTalkerConfig
|
||||
|
||||
from .audio_vae import AudioVAE, AudioVAEConfig
|
||||
from .prompt_utils import DEFAULT_PROMPT as MING_DEFAULT_PROMPT
|
||||
from .talker_module import CFM, Aggregator, DiT, MingAudioGenerator, build_tts_input
|
||||
from .text_processing import segment_and_normalize
|
||||
|
||||
@@ -697,6 +697,9 @@ class MingFlashOmniThinkerForConditionalGeneration(
|
||||
f"image_gen_scales={list(self.query_tokens_dict.keys())}"
|
||||
)
|
||||
|
||||
def get_language_model(self):
|
||||
return self.language_model
|
||||
|
||||
def _load_image_gen_query_tokens(self, model_path: str) -> None:
|
||||
"""Load learnable query tokens used for image generation.
|
||||
|
||||
|
||||
@@ -110,12 +110,18 @@ BASE_CAPTION_TEMPLATE: dict[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
def create_instruction(user_input: dict[str, Any]) -> str:
|
||||
def create_instruction(user_input: Any) -> str | None:
|
||||
"""Return a JSON caption string for ``audio_sequence[0]``.
|
||||
|
||||
Only keys already present on the base template are merged in; unknown
|
||||
keys are silently ignored to keep the output schema stable.
|
||||
"""
|
||||
if user_input is None:
|
||||
return None
|
||||
if isinstance(user_input, str):
|
||||
return user_input
|
||||
if not isinstance(user_input, dict):
|
||||
raise ValueError(f"Ming instruction must be str, dict, or None; got {type(user_input).__name__}")
|
||||
caption = copy.deepcopy(BASE_CAPTION_TEMPLATE)
|
||||
item = caption["audio_sequence"][0]
|
||||
for key, value in user_input.items():
|
||||
|
||||
@@ -27,223 +27,19 @@ from threading import Lock
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from transformers import PreTrainedTokenizerBase, Qwen2Config, Qwen2Model, StaticCache
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
from x_transformers.x_transformers import RotaryEmbedding, apply_rotary_pos_emb
|
||||
from x_transformers.x_transformers import RotaryEmbedding
|
||||
|
||||
from vllm_omni.model_executor.layers.timestep_embedding import DiTTimestepEmbedding
|
||||
|
||||
from .audio_vae import AudioVAE
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAE
|
||||
from vllm_omni.model_executor.models.common.ming.dit import CondEmbedder, DiTBlock, FinalLayer, get_epss_timesteps
|
||||
from vllm_omni.model_executor.models.common.ming.fm import apply_sway_sampling, integrate_cfm_steps
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
########################################################################
|
||||
# DiT Modules
|
||||
# Ported from:
|
||||
# https://github.com/inclusionAI/Ming/blob/e58533db227031990c5a6864dcf5f08fb53ed0d2/talker_module/modules.py
|
||||
# Ported from:
|
||||
# https://github.com/inclusionAI/Ming/blob/e58533db227031990c5a6864dcf5f08fb53ed0d2/talker_module/dit.py
|
||||
########################################################################
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, dim: int, eps: float = 1e-6):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = nn.Parameter(torch.ones(dim))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if self.weight.dtype in [torch.float16, torch.bfloat16]:
|
||||
x = x.to(self.weight.dtype)
|
||||
x = F.rms_norm(x, normalized_shape=(x.shape[-1],), weight=self.weight, eps=self.eps)
|
||||
return x
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(
|
||||
self, dim: int, dim_out: int | None = None, mult: float = 4, dropout: float = 0.0, approximate: str = "none"
|
||||
):
|
||||
super().__init__()
|
||||
inner_dim = int(dim * mult)
|
||||
dim_out = dim_out if dim_out is not None else dim
|
||||
|
||||
activation = nn.GELU(approximate=approximate)
|
||||
project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation)
|
||||
self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out))
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.ff(x)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
heads: int = 8,
|
||||
dim_head: int = 64,
|
||||
dropout: float = 0.0,
|
||||
qk_norm: str | None = None,
|
||||
pe_attn_head: int | None = None,
|
||||
attn_mask_enabled: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.heads = heads
|
||||
self.inner_dim = dim_head * heads
|
||||
self.dropout = dropout
|
||||
|
||||
self.to_q = nn.Linear(dim, self.inner_dim)
|
||||
self.to_k = nn.Linear(dim, self.inner_dim)
|
||||
self.to_v = nn.Linear(dim, self.inner_dim)
|
||||
if qk_norm is None:
|
||||
self.q_norm = None
|
||||
self.k_norm = None
|
||||
elif qk_norm == "rms_norm":
|
||||
self.q_norm = RMSNorm(dim_head)
|
||||
self.k_norm = RMSNorm(dim_head)
|
||||
else:
|
||||
raise ValueError(f"Unimplemented qk_norm: {qk_norm}")
|
||||
|
||||
self.to_out = nn.ModuleList([])
|
||||
self.to_out.append(nn.Linear(self.inner_dim, dim))
|
||||
self.to_out.append(nn.Dropout(dropout))
|
||||
|
||||
self.pe_attn_head = pe_attn_head
|
||||
self.attn_mask_enabled = attn_mask_enabled
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor | None = None,
|
||||
rope: tuple[torch.Tensor, torch.Tensor | None] | None = None,
|
||||
) -> torch.Tensor:
|
||||
batch_size = x.shape[0]
|
||||
|
||||
query = self.to_q(x)
|
||||
key = self.to_k(x)
|
||||
value = self.to_v(x)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // self.heads
|
||||
query = query.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
key = key.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
|
||||
|
||||
if self.q_norm is not None:
|
||||
query = self.q_norm(query)
|
||||
if self.k_norm is not None:
|
||||
key = self.k_norm(key)
|
||||
|
||||
if rope is not None:
|
||||
freqs, xpos_scale = rope
|
||||
q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
|
||||
|
||||
if self.pe_attn_head is not None:
|
||||
on = self.pe_attn_head
|
||||
query[:, :on, :, :] = apply_rotary_pos_emb(query[:, :on, :, :], freqs, q_xpos_scale)
|
||||
key[:, :on, :, :] = apply_rotary_pos_emb(key[:, :on, :, :], freqs, k_xpos_scale)
|
||||
else:
|
||||
query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
|
||||
key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
|
||||
|
||||
if self.attn_mask_enabled and mask is not None:
|
||||
valid_sample_indices = mask.any(dim=1)
|
||||
final_output = torch.zeros_like(query).to(query.device)
|
||||
|
||||
attn_mask = mask[valid_sample_indices]
|
||||
query = query[valid_sample_indices]
|
||||
key = key[valid_sample_indices]
|
||||
value = value[valid_sample_indices]
|
||||
attn_mask = attn_mask.unsqueeze(1).unsqueeze(1)
|
||||
attn_mask = attn_mask.expand(valid_sample_indices.sum().item(), self.heads, query.shape[-2], key.shape[-2])
|
||||
else:
|
||||
attn_mask = None
|
||||
|
||||
x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
|
||||
if self.attn_mask_enabled and mask is not None:
|
||||
final_output[valid_sample_indices] = x
|
||||
x = final_output
|
||||
|
||||
x = x.transpose(1, 2).reshape(batch_size, -1, self.heads * head_dim)
|
||||
x = x.to(query.dtype)
|
||||
|
||||
x = self.to_out[0](x)
|
||||
x = self.to_out[1](x)
|
||||
|
||||
if mask is not None:
|
||||
mask = mask.unsqueeze(-1)
|
||||
x = x.masked_fill(~mask, 0.0)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class DiTBlock(nn.Module):
|
||||
"""A DiT block with pre-norm and residual connections."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hidden_size: int,
|
||||
num_heads: int,
|
||||
mlp_ratio: float = 4.0,
|
||||
dropout: float = 0.1,
|
||||
qk_norm: str | None = None,
|
||||
pe_attn_head: int | None = None,
|
||||
attn_mask_enabled: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
self.norm1 = RMSNorm(hidden_size)
|
||||
self.attn = Attention(
|
||||
dim=hidden_size,
|
||||
heads=num_heads,
|
||||
dim_head=hidden_size // num_heads,
|
||||
dropout=dropout,
|
||||
qk_norm=qk_norm,
|
||||
pe_attn_head=pe_attn_head,
|
||||
attn_mask_enabled=attn_mask_enabled,
|
||||
)
|
||||
self.norm2 = RMSNorm(hidden_size)
|
||||
self.mlp = FeedForward(dim=hidden_size, mult=mlp_ratio, dropout=dropout, approximate="tanh")
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor | None,
|
||||
rope: tuple[torch.Tensor, torch.Tensor | None] | None,
|
||||
) -> torch.Tensor:
|
||||
x = x + self.attn(self.norm1(x), mask=mask, rope=rope)
|
||||
x = x + self.mlp(self.norm2(x))
|
||||
return x
|
||||
|
||||
|
||||
class FinalLayer(nn.Module):
|
||||
"""The final layer of DiT."""
|
||||
|
||||
def __init__(self, hidden_size: int, out_channels: int):
|
||||
super().__init__()
|
||||
self.norm_final = RMSNorm(hidden_size)
|
||||
self.linear = nn.Linear(hidden_size, out_channels, bias=True)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = self.norm_final(x)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class CondEmbedder(nn.Module):
|
||||
"""Embeds LLM hidden states with optional CFG dropout."""
|
||||
|
||||
def __init__(self, input_feature_size: int, hidden_size: int):
|
||||
super().__init__()
|
||||
self.cond_embedder = nn.Linear(input_feature_size, hidden_size)
|
||||
|
||||
def forward(self, llm_cond: torch.Tensor) -> torch.Tensor:
|
||||
return self.cond_embedder(llm_cond)
|
||||
|
||||
|
||||
class DiT(nn.Module):
|
||||
"""Diffusion model with a Transformer backbone for audio latent generation."""
|
||||
|
||||
@@ -324,29 +120,6 @@ class DiT(nn.Module):
|
||||
return model_out[:, -x.shape[1] :, :]
|
||||
|
||||
|
||||
#########################################################################################
|
||||
# CFM
|
||||
# Ported from:
|
||||
# https://github.com/inclusionAI/Ming/blob/e58533db227031990c5a6864dcf5f08fb53ed0d2/talker_module/cfm.py
|
||||
#########################################################################################
|
||||
|
||||
|
||||
def get_epss_timesteps(n, device, dtype):
|
||||
dt = 1 / 32
|
||||
predefined_timesteps = {
|
||||
5: [0, 2, 4, 8, 16, 32],
|
||||
6: [0, 2, 4, 6, 8, 16, 32],
|
||||
7: [0, 2, 4, 6, 8, 16, 24, 32],
|
||||
10: [0, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32],
|
||||
12: [0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32],
|
||||
16: [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32],
|
||||
}
|
||||
t = predefined_timesteps.get(n, [])
|
||||
if not t:
|
||||
return torch.linspace(0, 1, n + 1, device=device, dtype=dtype)
|
||||
return dt * torch.tensor(t, device=device, dtype=dtype)
|
||||
|
||||
|
||||
class CFM(nn.Module):
|
||||
"""Conditional Flow Matching module for audio latent generation."""
|
||||
|
||||
@@ -391,15 +164,8 @@ class CFM(nn.Module):
|
||||
pred, null_pred = torch.chunk(pred_cfg, 2, dim=0)
|
||||
return pred + (pred - null_pred) * sde_args[0]
|
||||
|
||||
if self.sway_sampling_coef is not None:
|
||||
t = t + self.sway_sampling_coef * (torch.cos(torch.pi / 2 * t) - 1 + t)
|
||||
|
||||
for step in range(self.steps):
|
||||
dt = t[step + 1] - t[step]
|
||||
y0 = y0 + fn(t[step], y0) * dt
|
||||
y0 = y0 + sde_args[1] * (sde_args[2] ** 0.5) * (dt.abs() ** 0.5) * sde_rnd[step]
|
||||
|
||||
return y0
|
||||
t = apply_sway_sampling(t, self.sway_sampling_coef)
|
||||
return integrate_cfm_steps(fn, y0, t, sde_args, sde_rnd, self.steps)
|
||||
|
||||
|
||||
class CFMGraphExecutor:
|
||||
|
||||
@@ -22,7 +22,8 @@ from .spk_embedding import SpkembExtractor
|
||||
from .talker_module import resample
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .audio_vae import AudioVAE
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAE
|
||||
|
||||
from .talker_module import Aggregator
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from .config_ming_tts import MingDenseConfig
|
||||
from .ming_tts import MingTTSForConditionalGeneration
|
||||
from .ming_tts_audio_vae import MingAudioVAEModel
|
||||
from .ming_tts_llm import MingLLMModel
|
||||
|
||||
__all__ = [
|
||||
"MingDenseConfig",
|
||||
"MingTTSForConditionalGeneration",
|
||||
"MingLLMModel",
|
||||
"MingAudioVAEModel",
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/fm/dit.py
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from x_transformers.x_transformers import RotaryEmbedding
|
||||
|
||||
from vllm_omni.model_executor.models.common.ming.dit import DiTBlock, FinalLayer
|
||||
|
||||
|
||||
class Aggregator(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels=4,
|
||||
hidden_size=1152,
|
||||
depth=28,
|
||||
num_heads=16,
|
||||
mlp_ratio=4.0,
|
||||
llm_input_dim=896,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
self.out_channels = in_channels
|
||||
self.num_heads = num_heads
|
||||
|
||||
self.word_embedder = nn.Embedding(1, hidden_size)
|
||||
self.x_embedder = nn.Linear(in_channels, hidden_size)
|
||||
self.hidden_size = hidden_size
|
||||
self.rotary_embed = RotaryEmbedding(hidden_size // num_heads)
|
||||
self.blocks = nn.ModuleList(
|
||||
[DiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, **kwargs) for _ in range(depth)]
|
||||
)
|
||||
self.final_layer = FinalLayer(hidden_size, llm_input_dim)
|
||||
|
||||
def forward(self, x, mask=None):
|
||||
if x.ndim != 3:
|
||||
raise ValueError(f"Expected x rank-3 [Batch, Time, Dimension], got {tuple(x.shape)}")
|
||||
if x.shape[-1] != self.in_channels:
|
||||
raise ValueError(f"x feature dim mismatch: got {x.shape[-1]}, expected {self.in_channels}")
|
||||
|
||||
# [Batch, Time, Dimension] -> [Batch, Time, Hidden].
|
||||
x = self.x_embedder(x)
|
||||
cls_embed = self.word_embedder(torch.zeros((x.shape[0], 1), dtype=torch.long, device=x.device))
|
||||
# Prepend a learned CLS token: [Batch, Time, Hidden] -> [Batch, Time + 1, Hidden].
|
||||
x = torch.cat([cls_embed, x], dim=1)
|
||||
|
||||
rope = self.rotary_embed.forward_from_seq_len(x.shape[1])
|
||||
if mask is not None:
|
||||
if mask.ndim != 2:
|
||||
raise ValueError(f"Expected mask rank-2 [Batch, Time], got {tuple(mask.shape)}")
|
||||
if mask.shape[0] != x.shape[0] or mask.shape[1] != x.shape[1] - 1:
|
||||
raise ValueError(
|
||||
f"Mask shape mismatch: got {tuple(mask.shape)}, expected {(x.shape[0], x.shape[1] - 1)}"
|
||||
)
|
||||
mask_pad = mask.clone().detach()[:, :1]
|
||||
mask = torch.cat([mask_pad, mask], dim=-1)
|
||||
for block in self.blocks:
|
||||
x = block(x, mask, rope)
|
||||
x = self.final_layer(x)
|
||||
# Keep the CLS projection only: [Batch, Time + 1, Hidden] -> [Batch, 1, Hidden].
|
||||
return x[:, :1, :]
|
||||
|
||||
|
||||
__all__ = ["Aggregator"]
|
||||
@@ -0,0 +1,261 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from .config_ming_tts import (
|
||||
AUDIO_FRAME_HOP,
|
||||
LATENT_DIM,
|
||||
PATCH_SIZE,
|
||||
SAMPLE_RATE,
|
||||
VAE_PATCH_SIZE,
|
||||
VISION_START_TOKEN_ID,
|
||||
MingTTSConfig,
|
||||
)
|
||||
|
||||
|
||||
def pad_prompt_waveform(
|
||||
waveform: Any,
|
||||
*,
|
||||
patch_size: int = PATCH_SIZE,
|
||||
sample_rate: int = SAMPLE_RATE,
|
||||
) -> torch.Tensor:
|
||||
tensor = coerce_prompt_waveform(waveform)
|
||||
pad_align = int((float(sample_rate) / 12.5) * int(patch_size))
|
||||
new_len = ((int(tensor.shape[-1]) + pad_align - 1) // pad_align) * pad_align
|
||||
if new_len == int(tensor.shape[-1]):
|
||||
return tensor
|
||||
padded = torch.zeros((1, new_len), dtype=tensor.dtype, device=tensor.device)
|
||||
padded[:, : tensor.shape[-1]] = tensor
|
||||
return padded
|
||||
|
||||
|
||||
def coerce_prompt_waveform(value: Any) -> torch.Tensor:
|
||||
if value is None:
|
||||
raise ValueError("prompt waveform cannot be None")
|
||||
if isinstance(value, torch.Tensor):
|
||||
tensor = value.detach()
|
||||
if tensor.ndim == 1:
|
||||
return tensor.unsqueeze(0).to(torch.float32)
|
||||
if tensor.ndim == 2:
|
||||
if tensor.shape[0] != 1:
|
||||
return tensor.reshape(1, -1).to(torch.float32)
|
||||
return tensor.to(torch.float32)
|
||||
raise ValueError(f"Unsupported Ming prompt waveform rank: {tuple(tensor.shape)}")
|
||||
if isinstance(value, (list, tuple)):
|
||||
parts = [coerce_prompt_waveform(item) for item in value if item is not None]
|
||||
if not parts:
|
||||
raise ValueError("prompt waveform list was empty")
|
||||
return torch.cat(parts, dim=-1)
|
||||
return coerce_prompt_waveform(torch.as_tensor(value))
|
||||
|
||||
|
||||
def coerce_speaker_embeddings(value: Any, *, use_zero_spk_emb: bool = False) -> list[torch.Tensor] | None:
|
||||
if value is None:
|
||||
return [torch.zeros((192,), dtype=torch.float32)] if use_zero_spk_emb else None
|
||||
if isinstance(value, torch.Tensor):
|
||||
tensor = value.detach()
|
||||
if tensor.ndim == 1:
|
||||
tensor = tensor.unsqueeze(0)
|
||||
if tensor.ndim != 2:
|
||||
raise ValueError(f"Unsupported Ming speaker embedding shape: {tuple(tensor.shape)}")
|
||||
items = [row.reshape(-1).to(torch.float32).cpu() for row in tensor]
|
||||
elif isinstance(value, (list, tuple)):
|
||||
if value and all(not isinstance(item, (list, tuple, torch.Tensor)) for item in value):
|
||||
items = [torch.as_tensor(value).detach().reshape(-1).to(torch.float32).cpu()]
|
||||
else:
|
||||
items = []
|
||||
for item in value:
|
||||
if item is None:
|
||||
continue
|
||||
if not isinstance(item, torch.Tensor):
|
||||
item = torch.as_tensor(item)
|
||||
items.append(item.detach().reshape(-1).to(torch.float32).cpu())
|
||||
else:
|
||||
return coerce_speaker_embeddings(torch.as_tensor(value), use_zero_spk_emb=use_zero_spk_emb)
|
||||
if not items:
|
||||
return [torch.zeros((192,), dtype=torch.float32)] if use_zero_spk_emb else None
|
||||
for item in items:
|
||||
if int(item.numel()) != 192:
|
||||
raise ValueError(f"Ming speaker embedding must have 192 dims, got {int(item.numel())}")
|
||||
return items
|
||||
|
||||
|
||||
def count_prompt_latent_patches(
|
||||
value: Any,
|
||||
*,
|
||||
patch_size: int = PATCH_SIZE,
|
||||
latent_dim: int = LATENT_DIM,
|
||||
) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
if not isinstance(value, torch.Tensor):
|
||||
value = torch.as_tensor(value)
|
||||
latents = value.detach()
|
||||
if latents.ndim == 3 and latents.shape[0] == 1:
|
||||
latents = latents.squeeze(0)
|
||||
if latents.ndim == 3 and latents.shape[-2:] == (patch_size, latent_dim):
|
||||
return int(latents.shape[0])
|
||||
if latents.ndim != 2 or latents.shape[-1] != latent_dim:
|
||||
raise ValueError(f"Unsupported Ming prompt_latents shape: {tuple(latents.shape)}")
|
||||
if latents.shape[0] % patch_size != 0:
|
||||
raise ValueError(
|
||||
f"Ming prompt_latents frame count must be divisible by patch_size={patch_size}, "
|
||||
f"got frames={int(latents.shape[0])}"
|
||||
)
|
||||
return int(latents.shape[0] // patch_size)
|
||||
|
||||
|
||||
def count_prompt_waveform_patches(
|
||||
value: Any,
|
||||
*,
|
||||
patch_size: int = PATCH_SIZE,
|
||||
frame_hop: int = AUDIO_FRAME_HOP,
|
||||
vae_patch_size: int = VAE_PATCH_SIZE,
|
||||
) -> int:
|
||||
if value is None:
|
||||
return 0
|
||||
waveform = pad_prompt_waveform(value, patch_size=patch_size)
|
||||
frame_count = int(math.ceil(float(waveform.shape[-1]) / float(frame_hop)))
|
||||
latent_frames = int(math.ceil(float(frame_count) / float(vae_patch_size)))
|
||||
if latent_frames % int(patch_size) != 0:
|
||||
raise ValueError(
|
||||
f"Ming prompt waveform produced latent frame count not divisible by patch_size={patch_size}: "
|
||||
f"frames={latent_frames}"
|
||||
)
|
||||
return int(latent_frames // int(patch_size))
|
||||
|
||||
|
||||
def _normalize_prompt_waveform(value: Any, *, target_sr: int) -> torch.Tensor:
|
||||
if isinstance(value, bytes):
|
||||
import torchaudio
|
||||
|
||||
waveform, sr = torchaudio.load(BytesIO(value))
|
||||
waveform = waveform[:1].to(torch.float32)
|
||||
if int(sr) != int(target_sr):
|
||||
from torchaudio.functional import resample as resample_audio
|
||||
|
||||
waveform = resample_audio(waveform, int(sr), int(target_sr))
|
||||
return waveform
|
||||
|
||||
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[1], int):
|
||||
waveform = coerce_prompt_waveform(value[0])
|
||||
if int(value[1]) != int(target_sr):
|
||||
from torchaudio.functional import resample as resample_audio
|
||||
|
||||
waveform = resample_audio(waveform, int(value[1]), int(target_sr))
|
||||
return waveform
|
||||
|
||||
if isinstance(value, dict):
|
||||
samples = value.get("samples", value.get("array", value.get("waveform")))
|
||||
sr = value.get("sample_rate", value.get("sr", target_sr))
|
||||
return _normalize_prompt_waveform((samples, int(sr)), target_sr=target_sr)
|
||||
|
||||
return coerce_prompt_waveform(value)
|
||||
|
||||
|
||||
def _coerce_prompt_latents(
|
||||
value: Any,
|
||||
*,
|
||||
patch_size: int,
|
||||
latent_dim: int,
|
||||
) -> dict[str, torch.Tensor] | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, torch.Tensor):
|
||||
value = torch.as_tensor(value)
|
||||
|
||||
latents = value.detach()
|
||||
if latents.ndim == 3 and latents.shape[0] == 1:
|
||||
latents = latents.squeeze(0)
|
||||
|
||||
if latents.ndim == 3 and latents.shape[-2:] == (patch_size, latent_dim):
|
||||
patches = latents
|
||||
# [Patch, Time, Dimension] -> [Frame, Dimension] for history seeding.
|
||||
frames = patches.reshape(-1, latent_dim)
|
||||
return {"patches": patches, "frames": frames}
|
||||
|
||||
if latents.ndim != 2 or latents.shape[-1] != latent_dim:
|
||||
raise ValueError(f"Unsupported prompt latent shape: {tuple(latents.shape)}")
|
||||
if latents.shape[0] % patch_size != 0:
|
||||
raise ValueError(
|
||||
f"Prompt latent frame count must be divisible by patch_size={patch_size}, "
|
||||
f"got frames={int(latents.shape[0])}"
|
||||
)
|
||||
# [Frame, Dimension] -> [Patch, Time, Dimension] for Aggregator prompt slots.
|
||||
patches = latents.reshape(-1, patch_size, latent_dim) if latents.shape[0] > 0 else None
|
||||
return {"patches": patches, "frames": latents}
|
||||
|
||||
|
||||
def _initial_history(
|
||||
frames: torch.Tensor | None,
|
||||
*,
|
||||
history_size: int,
|
||||
latent_dim: int,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
history = torch.zeros((history_size, latent_dim), device=device, dtype=dtype)
|
||||
if frames is None or frames.numel() == 0:
|
||||
return history
|
||||
frames = frames.to(device=device, dtype=dtype)
|
||||
take = min(history_size, int(frames.shape[0]))
|
||||
history[-take:] = frames[-take:]
|
||||
return history
|
||||
|
||||
|
||||
def _take_scalar(value: Any, idx: int) -> float | None:
|
||||
if not isinstance(value, torch.Tensor) or value.numel() == 0:
|
||||
return None
|
||||
return float(value.reshape(-1)[idx].item())
|
||||
|
||||
|
||||
def _find_audio_placeholder_positions(input_ids: torch.Tensor, cfg: MingTTSConfig) -> torch.Tensor:
|
||||
dummy_pos = (input_ids == cfg.audio_dummy_token_id).nonzero(as_tuple=True)[0]
|
||||
if dummy_pos.numel() == 0:
|
||||
return dummy_pos
|
||||
|
||||
audio_start_pos = (input_ids == cfg.audio_start_token_id).nonzero(as_tuple=True)[0]
|
||||
audio_end_pos = (input_ids == cfg.audio_end_token_id).nonzero(as_tuple=True)[0]
|
||||
if audio_start_pos.numel() == 0:
|
||||
return dummy_pos
|
||||
|
||||
start = int(audio_start_pos[0].item())
|
||||
end = int(audio_end_pos[0].item()) if audio_end_pos.numel() > 0 else int(input_ids.shape[0])
|
||||
keep = (dummy_pos > start) & (dummy_pos < end)
|
||||
filtered = dummy_pos[keep]
|
||||
return filtered if filtered.numel() > 0 else dummy_pos
|
||||
|
||||
|
||||
def _find_speaker_placeholder_positions(input_ids: torch.Tensor, hf_config: Any) -> list[int]:
|
||||
vision_start_token_id = getattr(hf_config, "vision_start_token_id", VISION_START_TOKEN_ID)
|
||||
vision_start_pos = (input_ids == int(vision_start_token_id)).nonzero(as_tuple=True)[0]
|
||||
if vision_start_pos.numel() == 0:
|
||||
return []
|
||||
|
||||
slots = []
|
||||
for pos in vision_start_pos:
|
||||
slot = int(pos.item()) + 1
|
||||
if slot < int(input_ids.shape[0]):
|
||||
slots.append(slot)
|
||||
return slots
|
||||
|
||||
|
||||
__all__ = [
|
||||
"coerce_prompt_waveform",
|
||||
"coerce_speaker_embeddings",
|
||||
"count_prompt_latent_patches",
|
||||
"count_prompt_waveform_patches",
|
||||
"pad_prompt_waveform",
|
||||
"_coerce_prompt_latents",
|
||||
"_find_audio_placeholder_positions",
|
||||
"_find_speaker_placeholder_positions",
|
||||
"_initial_history",
|
||||
"_normalize_prompt_waveform",
|
||||
"_take_scalar",
|
||||
]
|
||||
@@ -0,0 +1,209 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Ming dense checkpoint config adapters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from transformers import PretrainedConfig, Qwen2Config
|
||||
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAEConfig
|
||||
|
||||
from .constants import (
|
||||
AGGREGATOR_HIDDEN_SIZE,
|
||||
AUDIO_DUMMY_TOKEN_ID,
|
||||
AUDIO_END_TOKEN_ID,
|
||||
AUDIO_EOS_TOKEN_ID,
|
||||
AUDIO_FRAME_HOP,
|
||||
AUDIO_START_TOKEN_ID,
|
||||
DEFAULT_CFG,
|
||||
DEFAULT_SIGMA,
|
||||
DEFAULT_TEMPERATURE,
|
||||
HISTORY_PATCH_SIZE,
|
||||
KEY_CFG,
|
||||
KEY_CHUNK_ID,
|
||||
KEY_DECODE_STEP,
|
||||
KEY_LAST_STOP_PROB,
|
||||
KEY_LATENT_HISTORY,
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
KEY_MIN_DECODE_STEPS,
|
||||
KEY_NEXT_EMBEDS,
|
||||
KEY_PROMPT_LATENT_TAIL,
|
||||
KEY_PROMPT_LATENTS,
|
||||
KEY_REQUEST_ID,
|
||||
KEY_SIGMA,
|
||||
KEY_SPEAKER_EMBEDDING,
|
||||
KEY_TEMPERATURE,
|
||||
KEY_TEXT_MODE,
|
||||
LATENT_CHUNK_SIZE,
|
||||
LATENT_DIM,
|
||||
LATENT_LEFT_CONTEXT,
|
||||
LLM_HIDDEN_SIZE,
|
||||
LLM_VOCAB_SIZE,
|
||||
MAX_DECODE_STEPS,
|
||||
PATCH_SIZE,
|
||||
SAMPLE_RATE,
|
||||
STOP_HEAD_MIN_STEPS,
|
||||
STOP_HEAD_THRESHOLD,
|
||||
TEXT_EOS_TOKEN_ID,
|
||||
VAE_PATCH_SIZE,
|
||||
VISION_START_TOKEN_ID,
|
||||
)
|
||||
from .validation import _coerce_audio_vae_config, _nested_get, _to_plain_dict, validate_ming_tts_config
|
||||
|
||||
|
||||
def _coerce_qwen2_config(value: Any) -> Qwen2Config:
|
||||
if isinstance(value, Qwen2Config):
|
||||
return value
|
||||
if isinstance(value, PretrainedConfig):
|
||||
return Qwen2Config.from_dict(value.to_dict())
|
||||
if isinstance(value, dict):
|
||||
return Qwen2Config.from_dict(dict(value))
|
||||
raise TypeError(f"Unsupported llm_config type for Ming dense config: {type(value)!r}")
|
||||
|
||||
|
||||
class MingDenseConfig(PretrainedConfig):
|
||||
# The upstream checkpoint declares model_type="dense". Keep it for HF
|
||||
# config compatibility; deploy/ming_tts.yaml selects the vLLM-Omni
|
||||
# pipeline via pipeline: ming_tts.
|
||||
model_type = "dense"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
llm_config: Qwen2Config | dict[str, Any] | None = None,
|
||||
ditar_config: dict[str, Any] | None = None,
|
||||
aggregator_config: dict[str, Any] | None = None,
|
||||
audio_tokenizer_config: AudioVAEConfig | dict[str, Any] | None = None,
|
||||
architectures: list[str] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self.llm_config = _coerce_qwen2_config(llm_config or {})
|
||||
self.ditar_config = dict(ditar_config or {})
|
||||
self.aggregator_config = dict(aggregator_config or {})
|
||||
self.audio_tokenizer_config = _coerce_audio_vae_config(audio_tokenizer_config)
|
||||
super().__init__(architectures=architectures, **kwargs)
|
||||
|
||||
def get_text_config(self, decoder: bool = False, **kwargs: Any) -> Qwen2Config:
|
||||
del decoder, kwargs
|
||||
return self.llm_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class MingTTSConfig:
|
||||
"""Flat config object shared by Stage-1 and Stage-2. Build via from_hf_config()."""
|
||||
|
||||
llm_hidden_size: int = LLM_HIDDEN_SIZE
|
||||
llm_vocab_size: int = LLM_VOCAB_SIZE
|
||||
llm_config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
latent_dim: int = LATENT_DIM
|
||||
patch_size: int = PATCH_SIZE
|
||||
history_patch_size: int = HISTORY_PATCH_SIZE
|
||||
|
||||
ditar_config: dict[str, Any] = field(default_factory=dict)
|
||||
aggregator_config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
audio_tokenizer_config: AudioVAEConfig | None = None
|
||||
vae_patch_size: int = VAE_PATCH_SIZE
|
||||
sample_rate: int = SAMPLE_RATE
|
||||
audio_frame_hop: int = AUDIO_FRAME_HOP
|
||||
|
||||
cfg: float = DEFAULT_CFG
|
||||
sigma: float = DEFAULT_SIGMA
|
||||
temperature: float = DEFAULT_TEMPERATURE
|
||||
stop_head_min_steps: int = STOP_HEAD_MIN_STEPS
|
||||
stop_head_threshold: float = STOP_HEAD_THRESHOLD
|
||||
max_decode_steps: int = MAX_DECODE_STEPS
|
||||
|
||||
latent_chunk_size: int = LATENT_CHUNK_SIZE
|
||||
latent_left_context: int = LATENT_LEFT_CONTEXT
|
||||
|
||||
text_eos_token_id: int = TEXT_EOS_TOKEN_ID
|
||||
audio_dummy_token_id: int = AUDIO_DUMMY_TOKEN_ID
|
||||
audio_start_token_id: int = AUDIO_START_TOKEN_ID
|
||||
audio_end_token_id: int = AUDIO_END_TOKEN_ID
|
||||
audio_eos_token_id: int = AUDIO_EOS_TOKEN_ID
|
||||
|
||||
@classmethod
|
||||
def from_hf_config(cls, hf_config: PretrainedConfig) -> MingTTSConfig:
|
||||
llm_raw = getattr(hf_config, "llm_config", {}) or {}
|
||||
ditar_raw = getattr(hf_config, "ditar_config", {}) or {}
|
||||
agg_raw = getattr(hf_config, "aggregator_config", {}) or {}
|
||||
atc_raw = getattr(hf_config, "audio_tokenizer_config", None)
|
||||
|
||||
llm_dict = _to_plain_dict(llm_raw)
|
||||
ditar = _to_plain_dict(ditar_raw)
|
||||
agg = _to_plain_dict(agg_raw)
|
||||
ditar.setdefault("attn_backend", "torch")
|
||||
|
||||
atc = _coerce_audio_vae_config(atc_raw)
|
||||
atc_enc_latent_dim = _nested_get(atc, "enc_kwargs", "latent_dim", default=LATENT_DIM)
|
||||
atc_patch_size = _nested_get(atc, "patch_size", default=VAE_PATCH_SIZE)
|
||||
atc_sample_rate = _nested_get(atc, "sample_rate", default=SAMPLE_RATE)
|
||||
|
||||
enc_hop_size = _nested_get(atc, "enc_kwargs", "hop_size", default=AUDIO_FRAME_HOP)
|
||||
|
||||
cfg = cls(
|
||||
llm_hidden_size=llm_dict.get("hidden_size", LLM_HIDDEN_SIZE),
|
||||
llm_vocab_size=llm_dict.get("vocab_size", LLM_VOCAB_SIZE),
|
||||
llm_config=llm_dict,
|
||||
latent_dim=atc_enc_latent_dim,
|
||||
patch_size=ditar.get("patch_size", PATCH_SIZE),
|
||||
history_patch_size=ditar.get("history_patch_size", HISTORY_PATCH_SIZE),
|
||||
ditar_config=ditar,
|
||||
aggregator_config=agg,
|
||||
audio_tokenizer_config=atc,
|
||||
vae_patch_size=atc_patch_size,
|
||||
sample_rate=atc_sample_rate,
|
||||
audio_frame_hop=enc_hop_size if enc_hop_size is not None else AUDIO_FRAME_HOP,
|
||||
)
|
||||
return cfg
|
||||
|
||||
def validate(self) -> None:
|
||||
validate_ming_tts_config(self)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AGGREGATOR_HIDDEN_SIZE",
|
||||
"AUDIO_DUMMY_TOKEN_ID",
|
||||
"AUDIO_END_TOKEN_ID",
|
||||
"AUDIO_EOS_TOKEN_ID",
|
||||
"AUDIO_FRAME_HOP",
|
||||
"AUDIO_START_TOKEN_ID",
|
||||
"DEFAULT_CFG",
|
||||
"DEFAULT_SIGMA",
|
||||
"DEFAULT_TEMPERATURE",
|
||||
"HISTORY_PATCH_SIZE",
|
||||
"KEY_CFG",
|
||||
"KEY_CHUNK_ID",
|
||||
"KEY_DECODE_STEP",
|
||||
"KEY_LAST_STOP_PROB",
|
||||
"KEY_LATENT_HISTORY",
|
||||
"KEY_MAX_DECODE_STEPS",
|
||||
"KEY_MIN_DECODE_STEPS",
|
||||
"KEY_NEXT_EMBEDS",
|
||||
"KEY_PROMPT_LATENT_TAIL",
|
||||
"KEY_PROMPT_LATENTS",
|
||||
"KEY_REQUEST_ID",
|
||||
"KEY_SIGMA",
|
||||
"KEY_SPEAKER_EMBEDDING",
|
||||
"KEY_TEMPERATURE",
|
||||
"KEY_TEXT_MODE",
|
||||
"LATENT_CHUNK_SIZE",
|
||||
"LATENT_DIM",
|
||||
"LATENT_LEFT_CONTEXT",
|
||||
"LLM_HIDDEN_SIZE",
|
||||
"LLM_VOCAB_SIZE",
|
||||
"MAX_DECODE_STEPS",
|
||||
"MingDenseConfig",
|
||||
"MingTTSConfig",
|
||||
"PATCH_SIZE",
|
||||
"SAMPLE_RATE",
|
||||
"STOP_HEAD_MIN_STEPS",
|
||||
"STOP_HEAD_THRESHOLD",
|
||||
"TEXT_EOS_TOKEN_ID",
|
||||
"VAE_PATCH_SIZE",
|
||||
"VISION_START_TOKEN_ID",
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token IDs (confirmed from tokenizer_config.json)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
AUDIO_DUMMY_TOKEN_ID = 151705 # <audioPatch>
|
||||
AUDIO_START_TOKEN_ID = 151706 # <audio>
|
||||
AUDIO_END_TOKEN_ID = 151707 # </audio>
|
||||
AUDIO_EOS_TOKEN_ID = 151704 # <end_of_audio>
|
||||
VISION_START_TOKEN_ID = 151652 # <|vision_start|>
|
||||
|
||||
TEXT_EOS_TOKEN_ID = 151669 # <text_eos>
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Architectural constants (confirmed from original config.json)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
LATENT_DIM = 64
|
||||
PATCH_SIZE = 4
|
||||
HISTORY_PATCH_SIZE = 32
|
||||
LLM_HIDDEN_SIZE = 896
|
||||
LLM_VOCAB_SIZE = 151936
|
||||
AGGREGATOR_HIDDEN_SIZE = 1024
|
||||
VAE_PATCH_SIZE = 4
|
||||
SAMPLE_RATE = 44100
|
||||
|
||||
# AudioVAE frame/hop geometry (confirmed)
|
||||
AUDIO_FRAME_HOP = 882 # enc input_dim / hop_size / dec output_dim
|
||||
|
||||
# stop_head defaults
|
||||
STOP_HEAD_MIN_STEPS = 3
|
||||
STOP_HEAD_THRESHOLD = 0.5
|
||||
|
||||
# FlowLoss sampling defaults
|
||||
DEFAULT_CFG = 2.0
|
||||
DEFAULT_SIGMA = 0.25
|
||||
DEFAULT_TEMPERATURE = 0.0
|
||||
|
||||
# Connector / Stage-2 streaming defaults (runtime tuning)
|
||||
LATENT_CHUNK_SIZE = 25
|
||||
LATENT_LEFT_CONTEXT = 0
|
||||
MAX_DECODE_STEPS = 200
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# seq_data.extra_data keys
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
KEY_LATENT_HISTORY = "ming_latent_history"
|
||||
KEY_DECODE_STEP = "ming_decode_step"
|
||||
KEY_LAST_STOP_PROB = "ming_last_stop_prob"
|
||||
KEY_NEXT_EMBEDS = "ming_next_embeds"
|
||||
KEY_PROMPT_LATENTS = "ming_prompt_latents"
|
||||
KEY_PROMPT_LATENT_TAIL = "ming_prompt_latent_tail"
|
||||
KEY_SPEAKER_EMBEDDING = "ming_speaker_embedding"
|
||||
KEY_REQUEST_ID = "ming_request_id"
|
||||
KEY_CHUNK_ID = "ming_chunk_id"
|
||||
KEY_CFG = "ming_cfg"
|
||||
KEY_SIGMA = "ming_sigma"
|
||||
KEY_TEMPERATURE = "ming_temperature"
|
||||
KEY_MAX_DECODE_STEPS = "ming_max_decode_steps"
|
||||
KEY_MIN_DECODE_STEPS = "ming_min_decode_steps"
|
||||
KEY_TEXT_MODE = "ming_text_mode"
|
||||
@@ -0,0 +1,63 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/fm/flowloss.py
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.model_executor.models.common.ming.fm import Solver, build_timesteps
|
||||
|
||||
from .fm.dit import DiT
|
||||
|
||||
|
||||
class FlowLoss(nn.Module):
|
||||
"""Diffusion Loss"""
|
||||
|
||||
def __init__(self, z_channels, llm_cond_dim, **kwargs):
|
||||
super().__init__()
|
||||
self.z_channels = z_channels
|
||||
# Preserve checkpoint parameter names under flowloss.cfm.model.* while
|
||||
# keeping the inference-only CFM sampling logic local to FlowLoss.
|
||||
self.cfm = nn.Module()
|
||||
self.cfm.add_module("model", DiT(in_channels=z_channels, llm_cond_dim=llm_cond_dim, **kwargs))
|
||||
|
||||
@torch.no_grad()
|
||||
def sample(self, z, latent_history, cfg=2.0, patch_size=1, sigma=0.25, temperature=0):
|
||||
# z: [Batch, Time=1, Dimension]
|
||||
# latent_history: [Batch, History_Time, Dimension]
|
||||
if z.ndim != 3:
|
||||
raise ValueError(f"Expected conditioning rank-3 [Batch, Time, Dimension], got {tuple(z.shape)}")
|
||||
if latent_history.ndim != 3:
|
||||
raise ValueError(
|
||||
f"Expected latent_history rank-3 [Batch, Time, Dimension], got {tuple(latent_history.shape)}"
|
||||
)
|
||||
if z.shape[0] != latent_history.shape[0]:
|
||||
raise ValueError(
|
||||
f"Batch mismatch across conditioning and latent_history: {z.shape[0]}, {latent_history.shape[0]}"
|
||||
)
|
||||
dtype = z.dtype
|
||||
noise = torch.randn(z.shape[0], self.z_channels, patch_size, device=z.device, dtype=torch.float32)
|
||||
|
||||
def fn(t, x):
|
||||
x = x.to(dtype)
|
||||
t = t.to(dtype)
|
||||
if cfg < 1e-5:
|
||||
if t.ndim == 0:
|
||||
t = t.repeat(x.shape[0])
|
||||
pred = self.cfm.model(x=x, t=t, c=torch.zeros_like(z), latent_history=latent_history)
|
||||
return pred[:, -patch_size:, :].float()
|
||||
|
||||
pred_cfg = self.cfm.model.forward_with_cfg(
|
||||
x=x, t=t, c=z, latent_history=latent_history, cfg_scale=cfg, patch_size=patch_size
|
||||
)
|
||||
pred, null_pred = torch.chunk(pred_cfg, 2, dim=0)
|
||||
return (pred + (pred - null_pred) * cfg).float()
|
||||
|
||||
t = build_timesteps(
|
||||
10,
|
||||
device=noise.device,
|
||||
dtype=noise.dtype,
|
||||
use_epss=True,
|
||||
sway_sampling_coef=-1.0,
|
||||
)
|
||||
return Solver(fn, noise.transpose(1, 2), sigma=sigma, temperature=temperature).integrate(t)[-1].to(dtype)
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,98 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/fm/dit.py
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from x_transformers.x_transformers import RotaryEmbedding
|
||||
|
||||
from vllm_omni.model_executor.layers.timestep_embedding import DiTTimestepEmbedding
|
||||
from vllm_omni.model_executor.models.common.ming.dit import CondEmbedder, DiTBlock, FinalLayer
|
||||
|
||||
|
||||
class DiT(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels=4,
|
||||
hidden_size=1024,
|
||||
depth=28,
|
||||
num_heads=16,
|
||||
mlp_ratio=4.0,
|
||||
llm_cond_dim=896,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.in_channels = in_channels
|
||||
self.out_channels = in_channels
|
||||
self.num_heads = num_heads
|
||||
self.t_embedder = DiTTimestepEmbedding(hidden_size)
|
||||
self.x_embedder = nn.Linear(in_channels, hidden_size)
|
||||
self.c_embedder = CondEmbedder(llm_cond_dim, hidden_size)
|
||||
self.hidden_size = hidden_size
|
||||
self.rotary_embed = RotaryEmbedding(hidden_size // num_heads)
|
||||
self.blocks = nn.ModuleList(
|
||||
[DiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, **kwargs) for _ in range(depth)]
|
||||
)
|
||||
self.final_layer = FinalLayer(hidden_size, self.out_channels)
|
||||
|
||||
def forward(self, x, t, c, latent_history, mask=None):
|
||||
if x.ndim != 3:
|
||||
raise ValueError(f"Expected x rank-3 [Batch, Time, Dimension], got {tuple(x.shape)}")
|
||||
if latent_history.ndim != 3:
|
||||
raise ValueError(
|
||||
f"Expected latent_history rank-3 [Batch, Time, Dimension], got {tuple(latent_history.shape)}"
|
||||
)
|
||||
if c.ndim != 3:
|
||||
raise ValueError(f"Expected conditioning rank-3 [Batch, Time, Dimension], got {tuple(c.shape)}")
|
||||
if x.shape[0] != latent_history.shape[0] or x.shape[0] != c.shape[0]:
|
||||
raise ValueError(
|
||||
"Batch mismatch across x, conditioning, and latent_history: "
|
||||
f"{x.shape[0]}, {c.shape[0]}, {latent_history.shape[0]}"
|
||||
)
|
||||
if x.shape[-1] != self.in_channels:
|
||||
raise ValueError(f"x feature dim mismatch: got {x.shape[-1]}, expected {self.in_channels}")
|
||||
if latent_history.shape[-1] != self.in_channels:
|
||||
raise ValueError(
|
||||
f"latent_history feature dim mismatch: got {latent_history.shape[-1]}, expected {self.in_channels}"
|
||||
)
|
||||
if t.ndim == 0:
|
||||
t = t.reshape(1)
|
||||
if t.ndim != 1:
|
||||
raise ValueError(f"Expected timestep rank-1 [Batch], got {tuple(t.shape)}")
|
||||
if t.shape[0] != x.shape[0]:
|
||||
raise ValueError(f"Timestep batch mismatch: got {t.shape[0]}, expected {x.shape[0]}")
|
||||
|
||||
t = self.t_embedder(t).unsqueeze(1)
|
||||
x_now = self.x_embedder(x)
|
||||
x_history = self.x_embedder(latent_history)
|
||||
x = torch.cat([x_history, x_now], dim=1)
|
||||
c = self.c_embedder(c)
|
||||
y = t + c
|
||||
x = torch.cat([y, x], dim=1)
|
||||
rope = self.rotary_embed.forward_from_seq_len(x.shape[1])
|
||||
|
||||
if mask is not None:
|
||||
if mask.ndim != 2:
|
||||
raise ValueError(f"Expected mask rank-2 [Batch, Time], got {tuple(mask.shape)}")
|
||||
if mask.shape[0] != x_now.shape[0] or mask.shape[1] != x_now.shape[1]:
|
||||
raise ValueError(
|
||||
f"Mask shape mismatch: got {tuple(mask.shape)}, expected {(x_now.shape[0], x_now.shape[1])}"
|
||||
)
|
||||
mask_pad = mask.clone().detach()[:, :1].expand(-1, x_history.shape[1] + c.shape[1])
|
||||
mask = torch.cat([mask_pad, mask], dim=-1)
|
||||
for block in self.blocks:
|
||||
x = block(x, mask, rope)
|
||||
x = self.final_layer(x)
|
||||
return x
|
||||
|
||||
def forward_with_cfg(self, x, t, c, cfg_scale, latent_history, patch_size):
|
||||
if not cfg_scale == 1:
|
||||
x = torch.cat([x, x], dim=0)
|
||||
latent_history = torch.cat([latent_history, latent_history], dim=0)
|
||||
fake_latent = torch.zeros_like(c)
|
||||
c = torch.cat([c, fake_latent], dim=0)
|
||||
if t.ndim == 0:
|
||||
t = t.repeat(x.shape[0])
|
||||
model_out = self.forward(x, t, c, latent_history)
|
||||
return model_out[:, -patch_size:, :]
|
||||
@@ -0,0 +1,269 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.model_executor.models import SupportsPP
|
||||
from vllm.model_executor.models.utils import init_vllm_registered_model
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
|
||||
|
||||
from .audio_prep import (
|
||||
_coerce_prompt_latents,
|
||||
_find_audio_placeholder_positions,
|
||||
_find_speaker_placeholder_positions,
|
||||
_initial_history,
|
||||
_take_scalar,
|
||||
coerce_speaker_embeddings,
|
||||
)
|
||||
from .config_ming_tts import (
|
||||
AUDIO_START_TOKEN_ID,
|
||||
KEY_CFG,
|
||||
KEY_DECODE_STEP,
|
||||
KEY_LAST_STOP_PROB,
|
||||
KEY_LATENT_HISTORY,
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
KEY_MIN_DECODE_STEPS,
|
||||
KEY_NEXT_EMBEDS,
|
||||
KEY_PROMPT_LATENT_TAIL,
|
||||
KEY_REQUEST_ID,
|
||||
KEY_SIGMA,
|
||||
KEY_SPEAKER_EMBEDDING,
|
||||
KEY_TEMPERATURE,
|
||||
KEY_TEXT_MODE,
|
||||
MingTTSConfig,
|
||||
)
|
||||
from .patch_emission import MING_STOP_REASON_KEY
|
||||
from .prompt_encoder import _resolve_prompt_latents
|
||||
|
||||
|
||||
class _ModelSampleAdapter(nn.Module):
|
||||
def __init__(self, model: nn.Module):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
|
||||
def forward(self, logits, sampling_metadata):
|
||||
return self.model.sample(logits, sampling_metadata)
|
||||
|
||||
|
||||
class MingTTSForConditionalGeneration(nn.Module, SupportsPP, CustomProcessMixin):
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
del prefix
|
||||
self.vllm_config = vllm_config
|
||||
self.ming_config = MingTTSConfig.from_hf_config(vllm_config.model_config.hf_config)
|
||||
self.ming_config.validate()
|
||||
self.have_multimodal_outputs = True
|
||||
self.has_preprocess = False
|
||||
self.has_postprocess = False
|
||||
self.requires_raw_input_tokens = False
|
||||
self.model_stage = vllm_config.model_config.model_stage
|
||||
self._prompt_encoder = None
|
||||
|
||||
if self.model_stage == "llm":
|
||||
self.model = init_vllm_registered_model(vllm_config=vllm_config, architectures=["MingLLMModel"])
|
||||
self.has_preprocess = True
|
||||
self.has_postprocess = True
|
||||
self.set_custom_preprocess(self.preprocess)
|
||||
self.set_custom_postprocess(self.postprocess)
|
||||
elif self.model_stage == "audio_vae":
|
||||
self.model = init_vllm_registered_model(vllm_config=vllm_config, architectures=["MingAudioVAEModel"])
|
||||
self.requires_raw_input_tokens = True
|
||||
else:
|
||||
raise ValueError(f"Invalid Ming model_stage={self.model_stage}")
|
||||
self.make_empty_intermediate_tensors = getattr(self.model, "make_empty_intermediate_tensors", lambda: None)
|
||||
|
||||
@cached_property
|
||||
def sampler(self):
|
||||
if hasattr(self.model, "sample"):
|
||||
return _ModelSampleAdapter(self.model)
|
||||
if hasattr(self.model, "sampler"):
|
||||
return self.model.sampler
|
||||
return Sampler()
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor, **kwargs: Any) -> torch.Tensor:
|
||||
return self.model.embed_input_ids(input_ids=input_ids, **kwargs)
|
||||
|
||||
def forward(self, *args: Any, **kwargs: Any):
|
||||
return self.model(*args, **kwargs)
|
||||
|
||||
def compute_logits(self, hidden_states, sampling_metadata=None):
|
||||
return self.model.compute_logits(hidden_states, sampling_metadata=sampling_metadata)
|
||||
|
||||
def sample(self, logits, sampling_metadata):
|
||||
return self.model.sample(logits, sampling_metadata) if hasattr(self.model, "sample") else None
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
weights = list(weights)
|
||||
if self.model_stage == "llm":
|
||||
allowed = ("model.", "linear_proj_audio.", "flowloss.", "stop_head.", "spk_head.")
|
||||
llm_weights = [(k, v) for k, v in weights if k.startswith(allowed)]
|
||||
if not llm_weights:
|
||||
raise RuntimeError(
|
||||
"Ming Stage-0 received no loadable checkpoint weights. "
|
||||
"Expected prefixes: model.*, linear_proj_audio.*, flowloss.*, stop_head.*, spk_head.*"
|
||||
)
|
||||
loaded = self.model.load_weights(llm_weights)
|
||||
return {f"model.{name}" for name in loaded}
|
||||
|
||||
audio_weights = [(k, v) for k, v in weights if k.startswith("audio.")]
|
||||
if not audio_weights:
|
||||
raise RuntimeError("Ming Stage-1 received no loadable checkpoint weights. Expected prefix: audio.*")
|
||||
loaded = self.model.load_weights(audio_weights)
|
||||
return {f"model.{name}" for name in loaded}
|
||||
|
||||
def _resolve_prompt_latents(self, info_dict: dict[str, Any]):
|
||||
return _resolve_prompt_latents(self, info_dict)
|
||||
|
||||
def preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor | None, **info_dict: Any):
|
||||
if self.model_stage != "llm":
|
||||
return input_ids, input_embeds, {}
|
||||
input_embeds = self.model.embed_input_ids(input_ids).clone()
|
||||
return (
|
||||
self._prefill_preprocess(input_ids, input_embeds, **info_dict)
|
||||
if int(input_ids.shape[0]) > 1
|
||||
else self._decode_preprocess(input_ids, input_embeds, **info_dict)
|
||||
)
|
||||
|
||||
def postprocess(self, hidden_states: torch.Tensor, **info_dict: Any) -> dict[str, Any]:
|
||||
if self.model_stage != "llm" or hidden_states.numel() == 0:
|
||||
return {}
|
||||
req_id = info_dict.get(KEY_REQUEST_ID, info_dict.get("request_id"))
|
||||
pending = self.model.pop_postprocess_update(req_id)
|
||||
if not pending or not isinstance(pending.get("ming_latent_patch"), torch.Tensor):
|
||||
return {}
|
||||
|
||||
update = {
|
||||
KEY_LATENT_HISTORY: pending[KEY_LATENT_HISTORY].detach().to("cpu").contiguous(),
|
||||
KEY_NEXT_EMBEDS: pending[KEY_NEXT_EMBEDS].detach().to("cpu").contiguous(),
|
||||
KEY_DECODE_STEP: int(info_dict.get(KEY_DECODE_STEP, 0)) + 1,
|
||||
}
|
||||
stop_prob = _take_scalar(pending.get("ming_stop_prob"), 0)
|
||||
if stop_prob is not None:
|
||||
update[KEY_LAST_STOP_PROB] = stop_prob
|
||||
stop_reason = pending.get(MING_STOP_REASON_KEY)
|
||||
if isinstance(stop_reason, str):
|
||||
update[MING_STOP_REASON_KEY] = stop_reason
|
||||
if isinstance(req_id, str):
|
||||
update[KEY_REQUEST_ID] = req_id
|
||||
return update
|
||||
|
||||
def _prefill_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: Any):
|
||||
if bool(info_dict.get(KEY_TEXT_MODE, False)):
|
||||
update: dict[str, Any] = {KEY_TEXT_MODE: True}
|
||||
request_id = info_dict.get(KEY_REQUEST_ID, info_dict.get("request_id"))
|
||||
if request_id is not None:
|
||||
update[KEY_REQUEST_ID] = request_id
|
||||
if int(input_ids.shape[0]) > 1 and int(input_ids[-1].item()) == AUDIO_START_TOKEN_ID:
|
||||
return input_ids[:-1], input_embeds[:-1], update
|
||||
return input_ids, input_embeds, update
|
||||
|
||||
update: dict[str, Any] = {KEY_DECODE_STEP: int(info_dict.get(KEY_DECODE_STEP, 0))}
|
||||
prompt_latents = _resolve_prompt_latents(self, info_dict)
|
||||
history = _initial_history(
|
||||
prompt_latents["frames"] if prompt_latents is not None else None,
|
||||
history_size=self.ming_config.history_patch_size,
|
||||
latent_dim=self.ming_config.latent_dim,
|
||||
device=input_embeds.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
update[KEY_LATENT_HISTORY] = history.detach().to("cpu").contiguous()
|
||||
update[KEY_PROMPT_LATENT_TAIL] = update[KEY_LATENT_HISTORY]
|
||||
|
||||
speaker_embedding = info_dict.get(KEY_SPEAKER_EMBEDDING, info_dict.get("speaker_embedding"))
|
||||
speaker_embeddings = None
|
||||
if speaker_embedding is not None:
|
||||
speaker_embeddings = coerce_speaker_embeddings(
|
||||
speaker_embedding,
|
||||
use_zero_spk_emb=bool(info_dict.get("use_zero_spk_emb", False)),
|
||||
)
|
||||
if speaker_embeddings is not None and len(speaker_embeddings) > 0:
|
||||
speaker_slots = _find_speaker_placeholder_positions(input_ids, self.vllm_config.model_config.hf_config)
|
||||
if len(speaker_slots) < len(speaker_embeddings):
|
||||
raise RuntimeError(
|
||||
"Could not locate enough speaker placeholder slots: "
|
||||
f"found {len(speaker_slots)}, need {len(speaker_embeddings)}"
|
||||
)
|
||||
for speaker_slot, spk in zip(speaker_slots, speaker_embeddings):
|
||||
spk_proj = self.model.project_speaker_embedding(
|
||||
spk.to(device=input_embeds.device, dtype=input_embeds.dtype).unsqueeze(0)
|
||||
).squeeze(0)
|
||||
input_embeds[speaker_slot] = spk_proj
|
||||
|
||||
if prompt_latents is not None and prompt_latents["patches"] is not None:
|
||||
prompt_patches = prompt_latents["patches"].to(dtype=getattr(self.model, "fm_dtype", torch.float32))
|
||||
prompt_embeds = self.model.linear_proj_audio(prompt_patches).squeeze(1)
|
||||
placeholder_pos = _find_audio_placeholder_positions(input_ids, self.ming_config)
|
||||
take = min(int(placeholder_pos.numel()), int(prompt_embeds.shape[0]))
|
||||
if take > 0:
|
||||
input_embeds[placeholder_pos[:take]] = prompt_embeds[:take].to(dtype=input_embeds.dtype)
|
||||
|
||||
request_id = info_dict.get(KEY_REQUEST_ID, info_dict.get("request_id"))
|
||||
if request_id is not None:
|
||||
update[KEY_REQUEST_ID] = request_id
|
||||
_copy_runtime_controls(update, info_dict)
|
||||
return input_ids, input_embeds, update
|
||||
|
||||
def _decode_preprocess(self, input_ids: torch.Tensor, input_embeds: torch.Tensor, **info_dict: Any):
|
||||
if bool(info_dict.get(KEY_TEXT_MODE, False)):
|
||||
update: dict[str, Any] = {KEY_TEXT_MODE: True}
|
||||
request_id = info_dict.get(KEY_REQUEST_ID, info_dict.get("request_id"))
|
||||
if request_id is not None:
|
||||
update[KEY_REQUEST_ID] = request_id
|
||||
return input_ids, input_embeds, update
|
||||
|
||||
update: dict[str, Any] = {KEY_DECODE_STEP: int(info_dict.get(KEY_DECODE_STEP, 0))}
|
||||
history = info_dict.get(KEY_LATENT_HISTORY)
|
||||
if isinstance(history, torch.Tensor):
|
||||
update[KEY_LATENT_HISTORY] = history.detach().to("cpu").contiguous()
|
||||
else:
|
||||
zero_history = torch.zeros(
|
||||
(self.ming_config.history_patch_size, self.ming_config.latent_dim),
|
||||
device=input_embeds.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
update[KEY_LATENT_HISTORY] = zero_history.detach().to("cpu").contiguous()
|
||||
|
||||
next_embeds = info_dict.get(KEY_NEXT_EMBEDS)
|
||||
if isinstance(next_embeds, torch.Tensor) and input_ids.numel() == 1:
|
||||
if not torch.isfinite(next_embeds).all():
|
||||
raise RuntimeError("Non-finite next_embeds before decode preprocess write.")
|
||||
input_embeds[0] = (
|
||||
next_embeds.detach()
|
||||
.reshape(-1, self.ming_config.llm_hidden_size)[0]
|
||||
.to(
|
||||
device=input_embeds.device,
|
||||
dtype=input_embeds.dtype,
|
||||
)
|
||||
)
|
||||
if not torch.isfinite(input_embeds[0]).all():
|
||||
raise RuntimeError("Non-finite backbone input_embeds after decode preprocess write.")
|
||||
|
||||
request_id = info_dict.get(KEY_REQUEST_ID, info_dict.get("request_id"))
|
||||
if request_id is not None:
|
||||
update[KEY_REQUEST_ID] = request_id
|
||||
_copy_runtime_controls(update, info_dict)
|
||||
return input_ids, input_embeds, update
|
||||
|
||||
|
||||
def _copy_runtime_controls(update: dict[str, Any], info_dict: dict[str, Any]) -> None:
|
||||
for key in (KEY_CFG, KEY_SIGMA, KEY_TEMPERATURE, KEY_MAX_DECODE_STEPS, KEY_MIN_DECODE_STEPS):
|
||||
if key in info_dict:
|
||||
update[key] = info_dict[key]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MingTTSForConditionalGeneration",
|
||||
"_ModelSampleAdapter",
|
||||
"_coerce_prompt_latents",
|
||||
"_find_audio_placeholder_positions",
|
||||
"_find_speaker_placeholder_positions",
|
||||
"_initial_history",
|
||||
]
|
||||
@@ -0,0 +1,389 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/audio_tokenizer/modeling_audio_vae.py
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAE
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
from .config_ming_tts import KEY_CHUNK_ID, KEY_REQUEST_ID, MingTTSConfig
|
||||
from .patch_emission import MING_STOP_REASON_KEY
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
MING_FINAL_DECODE_STEP_KEY = "ming_final_decode_step"
|
||||
_STREAM_STATE_TTL_SECONDS = 15 * 60
|
||||
_MAX_STREAM_STATES = 1024
|
||||
|
||||
|
||||
class MingAudioVAEModel(nn.Module):
|
||||
input_modalities = "audio"
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
del prefix
|
||||
self.vllm_config = vllm_config
|
||||
self.ming_config = MingTTSConfig.from_hf_config(vllm_config.model_config.hf_config)
|
||||
if self.ming_config.audio_tokenizer_config is None:
|
||||
raise ValueError("MingAudioVAEModel requires audio_tokenizer_config")
|
||||
self.ming_config.validate()
|
||||
|
||||
self.audio = AudioVAE(self.ming_config.audio_tokenizer_config)
|
||||
self.have_multimodal_outputs = True
|
||||
self.has_preprocess = False
|
||||
self.has_postprocess = False
|
||||
self.enable_update_additional_information = True
|
||||
self.requires_raw_input_tokens = True
|
||||
|
||||
self._past_key_values: dict[str, Any] = {}
|
||||
self._stream_state: dict[str, tuple[Any, Any, Any]] = {}
|
||||
self._patch_totals: dict[str, int] = {}
|
||||
self._sample_totals: dict[str, int] = {}
|
||||
self._state_access_times: OrderedDict[str, float] = OrderedDict()
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
|
||||
hidden_size = int(self.ming_config.llm_hidden_size)
|
||||
if input_ids is None or input_ids.numel() == 0:
|
||||
return torch.empty((0, hidden_size), device=self.vllm_config.device_config.device, dtype=torch.float32)
|
||||
return torch.zeros((input_ids.shape[0], hidden_size), device=input_ids.device, dtype=torch.float32)
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor | OmniOutput, sampling_metadata: Any = None) -> None:
|
||||
del hidden_states, sampling_metadata
|
||||
return None
|
||||
|
||||
def chunked_decode_streaming(
|
||||
self,
|
||||
latent_chunk: torch.Tensor,
|
||||
*,
|
||||
request_id: str,
|
||||
finished: bool,
|
||||
) -> tuple[torch.Tensor, Any, Any, bool, bool]:
|
||||
had_past_key_values = request_id in self._past_key_values
|
||||
had_stream_state = _has_stream_state(self._stream_state.get(request_id))
|
||||
stream_state = self._stream_state.get(request_id, (None, None, None))
|
||||
past_key_values = self._past_key_values.get(request_id)
|
||||
waveform_parts: list[torch.Tensor] = []
|
||||
|
||||
patch_count = int(latent_chunk.shape[0])
|
||||
for patch_idx in range(patch_count):
|
||||
# [Batch, Time, Dimension] = [1, patch_size, latent_dim]
|
||||
latent_patch = latent_chunk[patch_idx : patch_idx + 1]
|
||||
is_last_patch = finished and patch_idx == patch_count - 1
|
||||
waveform, stream_state, past_key_values = self.audio.decode(
|
||||
latent_patch,
|
||||
past_key_values=past_key_values,
|
||||
use_cache=True,
|
||||
stream_state=stream_state,
|
||||
last_chunk=is_last_patch,
|
||||
)
|
||||
waveform_parts.append(waveform.reshape(-1).to(torch.float32))
|
||||
|
||||
waveform_flat = (
|
||||
torch.cat(waveform_parts, dim=0)
|
||||
if waveform_parts
|
||||
else torch.zeros((0,), dtype=torch.float32, device=latent_chunk.device)
|
||||
)
|
||||
return waveform_flat, stream_state, past_key_values, had_past_key_values, had_stream_state
|
||||
|
||||
@torch.inference_mode()
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
positions: torch.Tensor | None = None,
|
||||
intermediate_tensors: Any = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
model_intermediate_buffer: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> OmniOutput:
|
||||
del input_ids, positions, intermediate_tensors, inputs_embeds
|
||||
|
||||
info_list = model_intermediate_buffer if isinstance(model_intermediate_buffer, list) else None
|
||||
if info_list is None:
|
||||
runtime_infos = kwargs.get("runtime_additional_information")
|
||||
info_list = runtime_infos if isinstance(runtime_infos, list) else None
|
||||
info_list = info_list or [{}]
|
||||
num_reqs = max(len(info_list), 1)
|
||||
sr_tensor = torch.tensor(int(self.ming_config.sample_rate), dtype=torch.int32)
|
||||
empty = torch.zeros((0,), dtype=torch.float32, device=self.vllm_config.device_config.device)
|
||||
|
||||
outputs: list[torch.Tensor] = []
|
||||
srs: list[torch.Tensor] = []
|
||||
|
||||
for idx in range(num_reqs):
|
||||
info = info_list[idx] if idx < len(info_list) and isinstance(info_list[idx], dict) else {}
|
||||
meta = info.get("meta") or {}
|
||||
kv_meta = info.get("kv_metadata") or {}
|
||||
has_ming_context = _has_ming_context(info)
|
||||
if has_ming_context and KEY_REQUEST_ID not in info and KEY_REQUEST_ID not in kv_meta:
|
||||
raise RuntimeError(
|
||||
f"Ming Stage-2 received a payload without {KEY_REQUEST_ID}. keys={sorted(info.keys())}"
|
||||
)
|
||||
request_id = _resolve_request_id(info, idx)
|
||||
self._touch_request_state(request_id)
|
||||
chunk_id = _coerce_optional_int(_first_present(info.get(KEY_CHUNK_ID), kv_meta.get(KEY_CHUNK_ID)))
|
||||
finished = _coerce_finished(
|
||||
_first_present(
|
||||
meta.get("stream_finished"),
|
||||
meta.get("finished"),
|
||||
info.get("stream_finished"),
|
||||
kv_meta.get("stream_finished"),
|
||||
torch.tensor(True),
|
||||
)
|
||||
)
|
||||
latent = _first_present(
|
||||
info.get("latent"),
|
||||
info.get("ming_latent_patches"),
|
||||
kv_meta.get("ming_latent_patches"),
|
||||
)
|
||||
stripped = bool(
|
||||
_first_present(
|
||||
meta.get("_ming_payload_stripped"),
|
||||
info.get("_ming_payload_stripped"),
|
||||
kv_meta.get("_ming_payload_stripped"),
|
||||
False,
|
||||
)
|
||||
)
|
||||
if stripped:
|
||||
raise RuntimeError(
|
||||
"Ming Stage-2 payload was stripped before model entry. "
|
||||
f"request_id={request_id} chunk_id={chunk_id} keys={sorted(info.keys())}"
|
||||
)
|
||||
if latent is None:
|
||||
if has_ming_context and not finished:
|
||||
raise RuntimeError(
|
||||
"Ming Stage-2 received no latent chunk for an unfinished request. "
|
||||
f"request_id={request_id} chunk_id={chunk_id} keys={sorted(info.keys())}"
|
||||
)
|
||||
if finished:
|
||||
self._clear_request_state(request_id)
|
||||
outputs.append(empty)
|
||||
srs.append(sr_tensor)
|
||||
continue
|
||||
|
||||
latent_tensor = _coerce_latent_chunk(
|
||||
latent,
|
||||
device=self.vllm_config.device_config.device,
|
||||
dtype=next(self.audio.parameters()).dtype,
|
||||
latent_dim=self.ming_config.latent_dim,
|
||||
patch_size=self.ming_config.patch_size,
|
||||
)
|
||||
if latent_tensor is None or latent_tensor.numel() == 0:
|
||||
if not finished:
|
||||
raise RuntimeError(
|
||||
"Ming Stage-2 received an empty latent chunk before final flush. "
|
||||
f"request_id={request_id} chunk_id={chunk_id} latent_shape={_shape_of(latent_tensor)}"
|
||||
)
|
||||
if finished:
|
||||
self._clear_request_state(request_id)
|
||||
outputs.append(empty)
|
||||
srs.append(sr_tensor)
|
||||
continue
|
||||
|
||||
patch_count = int(latent_tensor.shape[0])
|
||||
waveform_flat, stream_state, past_key_values, had_past_key_values, had_stream_state = (
|
||||
self.chunked_decode_streaming(
|
||||
latent_tensor,
|
||||
request_id=request_id,
|
||||
finished=finished,
|
||||
)
|
||||
)
|
||||
total_patch_count = self._patch_totals.get(request_id, 0) + patch_count
|
||||
total_waveform_numel = self._sample_totals.get(request_id, 0) + int(waveform_flat.numel())
|
||||
self._patch_totals[request_id] = total_patch_count
|
||||
self._sample_totals[request_id] = total_waveform_numel
|
||||
if (had_past_key_values or had_stream_state) and waveform_flat.numel() == 0 and not finished:
|
||||
raise RuntimeError(
|
||||
"Ming Stage-2 produced an empty waveform after cached streaming state already existed. "
|
||||
f"request_id={request_id} chunk_id={chunk_id} latent_shape={tuple(latent_tensor.shape)} "
|
||||
f"had_past_key_values={had_past_key_values} had_stream_state={had_stream_state}"
|
||||
)
|
||||
|
||||
if finished:
|
||||
if os.environ.get("MING_TTS_STAGE1_FINAL_LOG") == "1":
|
||||
logger.info(
|
||||
"MING_STAGE1_FINAL %s",
|
||||
{
|
||||
"request_id": request_id,
|
||||
"chunk_id": chunk_id,
|
||||
"stop_reason": _first_present(
|
||||
info.get(MING_STOP_REASON_KEY),
|
||||
kv_meta.get(MING_STOP_REASON_KEY),
|
||||
),
|
||||
"final_decode_step": _coerce_optional_int(
|
||||
_first_present(
|
||||
info.get(MING_FINAL_DECODE_STEP_KEY),
|
||||
kv_meta.get(MING_FINAL_DECODE_STEP_KEY),
|
||||
)
|
||||
),
|
||||
"final_chunk_patch_count": patch_count,
|
||||
"total_patch_count": total_patch_count,
|
||||
"final_chunk_waveform_numel": int(waveform_flat.numel()),
|
||||
"total_waveform_numel": total_waveform_numel,
|
||||
},
|
||||
)
|
||||
self._clear_request_state(request_id)
|
||||
else:
|
||||
self._past_key_values[request_id] = past_key_values
|
||||
self._stream_state[request_id] = stream_state
|
||||
|
||||
outputs.append(waveform_flat)
|
||||
srs.append(sr_tensor)
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={
|
||||
"model_outputs": outputs,
|
||||
"sr": srs,
|
||||
},
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
weights = list(weights)
|
||||
if not weights:
|
||||
raise RuntimeError("MingAudioVAEModel received no checkpoint weights.")
|
||||
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
loaded: set[str] = set()
|
||||
skipped: list[str] = []
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if name not in params_dict:
|
||||
skipped.append(name)
|
||||
continue
|
||||
param = params_dict[name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded.add(name)
|
||||
|
||||
missing = {name for name in params_dict if name.startswith("audio.")} - loaded
|
||||
if missing:
|
||||
raise RuntimeError(f"MingAudioVAEModel: {len(missing)} params not loaded. First few: {sorted(missing)[:5]}")
|
||||
if skipped:
|
||||
warnings.warn(
|
||||
f"MingAudioVAEModel: skipped {len(skipped)} checkpoint keys during load. First few: {skipped[:8]}",
|
||||
stacklevel=2,
|
||||
)
|
||||
return loaded
|
||||
|
||||
def _clear_request_state(self, request_id: str) -> None:
|
||||
self._past_key_values.pop(request_id, None)
|
||||
self._stream_state.pop(request_id, None)
|
||||
self._patch_totals.pop(request_id, None)
|
||||
self._sample_totals.pop(request_id, None)
|
||||
self._state_access_times.pop(request_id, None)
|
||||
|
||||
def _touch_request_state(self, request_id: str) -> None:
|
||||
now = time.monotonic()
|
||||
self._evict_expired_request_states(now)
|
||||
self._state_access_times[request_id] = now
|
||||
self._state_access_times.move_to_end(request_id)
|
||||
while len(self._state_access_times) > _MAX_STREAM_STATES:
|
||||
evicted_request_id, _ = self._state_access_times.popitem(last=False)
|
||||
self._clear_request_state(evicted_request_id)
|
||||
|
||||
def _evict_expired_request_states(self, now: float) -> None:
|
||||
cutoff = now - _STREAM_STATE_TTL_SECONDS
|
||||
while self._state_access_times:
|
||||
request_id, last_access = next(iter(self._state_access_times.items()))
|
||||
if last_access >= cutoff:
|
||||
break
|
||||
self._clear_request_state(request_id)
|
||||
|
||||
|
||||
def _coerce_finished(value: Any) -> bool:
|
||||
if isinstance(value, torch.Tensor):
|
||||
if value.numel() == 0:
|
||||
return False
|
||||
return bool(value.reshape(-1)[0].item())
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _first_present(*values: Any) -> Any:
|
||||
for value in values:
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_optional_int(value: Any, default: int | None = None) -> int | None:
|
||||
if value is None:
|
||||
return default
|
||||
if isinstance(value, list):
|
||||
value = value[0] if value else default
|
||||
if isinstance(value, torch.Tensor):
|
||||
if value.numel() == 0:
|
||||
return default
|
||||
return int(value.reshape(-1)[0].item())
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _resolve_request_id(info: dict[str, Any], idx: int) -> str:
|
||||
kv_meta = info.get("kv_metadata") or {}
|
||||
request_id = info.get(KEY_REQUEST_ID) or kv_meta.get(KEY_REQUEST_ID)
|
||||
if request_id is None:
|
||||
return str(idx)
|
||||
if not isinstance(request_id, str) or not request_id:
|
||||
raise RuntimeError(f"Ming Stage-2 received invalid request id: {request_id!r}")
|
||||
return request_id
|
||||
|
||||
|
||||
def _has_ming_context(info: dict[str, Any]) -> bool:
|
||||
kv_meta = info.get("kv_metadata") or {}
|
||||
keys = (
|
||||
KEY_REQUEST_ID,
|
||||
KEY_CHUNK_ID,
|
||||
"ming_latent_patches",
|
||||
"_ming_payload_stripped",
|
||||
)
|
||||
return any(key in info or key in kv_meta for key in keys) or "latent" in info
|
||||
|
||||
|
||||
def _shape_of(value: Any) -> tuple[int, ...] | None:
|
||||
if isinstance(value, torch.Tensor):
|
||||
return tuple(value.shape)
|
||||
return None
|
||||
|
||||
|
||||
def _has_stream_state(value: Any) -> bool:
|
||||
if not isinstance(value, tuple):
|
||||
return False
|
||||
return any(item is not None for item in value)
|
||||
|
||||
|
||||
def _coerce_latent_chunk(
|
||||
value: Any,
|
||||
*,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
latent_dim: int,
|
||||
patch_size: int,
|
||||
) -> torch.Tensor | None:
|
||||
if not isinstance(value, torch.Tensor):
|
||||
value = torch.as_tensor(value)
|
||||
|
||||
latent = value.detach()
|
||||
if latent.ndim == 2:
|
||||
latent = latent.unsqueeze(0)
|
||||
if latent.ndim != 3:
|
||||
raise ValueError(f"Expected latent chunk rank-3 [Batch, Time, Dimension], got {tuple(latent.shape)}")
|
||||
if latent.shape[-2] != patch_size:
|
||||
raise ValueError(f"Latent patch size mismatch: got {latent.shape[-2]}, expected {patch_size}")
|
||||
if latent.shape[-1] != latent_dim:
|
||||
raise ValueError(f"Latent dim mismatch: got {latent.shape[-1]}, expected {latent_dim}")
|
||||
return latent.to(device=device, dtype=dtype)
|
||||
@@ -0,0 +1,414 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/modeling_bailingmm.py
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.models.qwen2 import Qwen2Model
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader, WeightsMapper, maybe_prefix
|
||||
from vllm.sequence import IntermediateTensors
|
||||
from vllm.v1.outputs import SamplerOutput
|
||||
from vllm.v1.sample.metadata import SamplingMetadata
|
||||
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
from .aggregator import Aggregator
|
||||
from .config_ming_tts import (
|
||||
KEY_CFG,
|
||||
KEY_DECODE_STEP,
|
||||
KEY_LATENT_HISTORY,
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
KEY_MIN_DECODE_STEPS,
|
||||
KEY_NEXT_EMBEDS,
|
||||
KEY_REQUEST_ID,
|
||||
KEY_SIGMA,
|
||||
KEY_TEMPERATURE,
|
||||
KEY_TEXT_MODE,
|
||||
MingTTSConfig,
|
||||
)
|
||||
from .flowloss_head import FlowLoss
|
||||
from .patch_emission import (
|
||||
MING_STOP_REASON_CODES,
|
||||
MING_STOP_REASON_CONTINUE,
|
||||
MING_STOP_REASON_KEY,
|
||||
MING_STOP_REASON_MAX_DECODE_STEPS,
|
||||
MING_STOP_REASON_STOP_HEAD,
|
||||
_coerce_latent_history,
|
||||
_get_request_token_counts,
|
||||
_normalize_request_infos,
|
||||
_resolve_ming_stop_decision,
|
||||
_resolve_optional_runtime_int,
|
||||
_resolve_runtime_float,
|
||||
_resolve_runtime_int,
|
||||
_validate_ming_decode_window,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class MingLLMModel(nn.Module):
|
||||
hf_to_vllm_mapper = WeightsMapper(
|
||||
orig_to_new_prefix={
|
||||
"model.model.": "model.",
|
||||
}
|
||||
)
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.ming_config = MingTTSConfig.from_hf_config(vllm_config.model_config.hf_config)
|
||||
self.ming_config.validate()
|
||||
self.vllm_config = vllm_config
|
||||
self.prefix = prefix
|
||||
self.fm_dtype = _resolve_ming_runtime_dtype(vllm_config)
|
||||
self.model = Qwen2Model(vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model"))
|
||||
self.linear_proj_audio = Aggregator(
|
||||
in_channels=self.ming_config.latent_dim,
|
||||
llm_input_dim=self.ming_config.llm_hidden_size,
|
||||
**self.ming_config.aggregator_config,
|
||||
)
|
||||
self.flowloss = FlowLoss(
|
||||
z_channels=self.ming_config.latent_dim,
|
||||
llm_cond_dim=self.ming_config.llm_hidden_size,
|
||||
**self.ming_config.ditar_config,
|
||||
)
|
||||
self.stop_head = nn.Linear(self.ming_config.llm_hidden_size, 2, bias=True)
|
||||
self.spk_head = nn.Linear(192, self.ming_config.llm_hidden_size, bias=True)
|
||||
self.flowloss.to(dtype=self.fm_dtype)
|
||||
self.linear_proj_audio.to(dtype=self.fm_dtype)
|
||||
self.stop_head.to(dtype=self.fm_dtype)
|
||||
self.spk_head.to(dtype=self.fm_dtype)
|
||||
self._pending_postprocess_updates: dict[str, dict[str, Any]] = {}
|
||||
self._last_ming_next_token_ids = None
|
||||
self._last_text_mode = False
|
||||
|
||||
def embed_input_ids(
|
||||
self, input_ids: torch.Tensor, inputs_embeds: torch.Tensor | None = None, **_: Any
|
||||
) -> torch.Tensor:
|
||||
if inputs_embeds is not None:
|
||||
return inputs_embeds
|
||||
return self.model.embed_input_ids(input_ids)
|
||||
|
||||
def project_speaker_embedding(self, spk_emb: torch.Tensor) -> torch.Tensor:
|
||||
return self.spk_head(spk_emb)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
latent_history: torch.Tensor | None = None,
|
||||
model_intermediate_buffer: list[dict[str, Any]] | None = None,
|
||||
seq_token_counts: list[int] | None = None,
|
||||
**kwargs: object,
|
||||
) -> OmniOutput | IntermediateTensors | torch.Tensor:
|
||||
if inputs_embeds is None:
|
||||
inputs_embeds = self.embed_input_ids(input_ids)
|
||||
if model_intermediate_buffer is None:
|
||||
model_intermediate_buffer = kwargs.get("runtime_additional_information")
|
||||
request_infos = _normalize_request_infos(model_intermediate_buffer)
|
||||
_validate_ming_decode_window(
|
||||
request_infos,
|
||||
min_stop_step=int(self.ming_config.stop_head_min_steps),
|
||||
default_max_decode_steps=self.ming_config.max_decode_steps,
|
||||
)
|
||||
backbone_out = self.model(
|
||||
input_ids=input_ids,
|
||||
positions=positions,
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
inputs_embeds=inputs_embeds,
|
||||
)
|
||||
if isinstance(backbone_out, IntermediateTensors):
|
||||
self._last_ming_next_token_ids = None
|
||||
return backbone_out
|
||||
hidden_states = _extract_hidden_states(backbone_out)
|
||||
token_counts = _get_request_token_counts(hidden_states, request_infos, seq_token_counts)
|
||||
text_mode = bool(request_infos) and all(bool(info.get(KEY_TEXT_MODE, False)) for info in request_infos)
|
||||
if request_infos and any(bool(info.get(KEY_TEXT_MODE, False)) for info in request_infos) and not text_mode:
|
||||
raise RuntimeError("Mixed Ming text/audio modes in one Stage-0 batch are unsupported.")
|
||||
if text_mode:
|
||||
self._last_ming_next_token_ids = None
|
||||
self._last_text_mode = True
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states,
|
||||
multimodal_outputs={KEY_TEXT_MODE: True},
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
)
|
||||
self._last_text_mode = False
|
||||
if latent_history is None and not token_counts:
|
||||
self._last_ming_next_token_ids = None
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states, multimodal_outputs=None, intermediate_tensors=intermediate_tensors
|
||||
)
|
||||
if latent_history is not None and not token_counts:
|
||||
token_counts, request_infos = [hidden_states.shape[0]], [{KEY_LATENT_HISTORY: latent_history}]
|
||||
|
||||
total_tokens = hidden_states.shape[0]
|
||||
latent_patch_tokens = next_embed_tokens = new_history_tokens = None
|
||||
decode_step_tokens = has_patch = stop_reason_code_tokens = None
|
||||
next_token_ids = []
|
||||
pending_updates: dict[str, dict[str, Any]] = {}
|
||||
cursor = 0
|
||||
any_decode = False
|
||||
for req_idx, token_count in enumerate(token_counts):
|
||||
end = min(cursor + token_count, total_tokens)
|
||||
if end <= cursor:
|
||||
continue
|
||||
req_info = request_infos[req_idx] if req_idx < len(request_infos) else {}
|
||||
req_id = req_info.get(KEY_REQUEST_ID)
|
||||
req_history = _coerce_latent_history(
|
||||
req_info.get(KEY_LATENT_HISTORY), device=hidden_states.device, dtype=self.fm_dtype, cfg=self.ming_config
|
||||
)
|
||||
if req_history is None:
|
||||
cursor = end
|
||||
continue
|
||||
decode_step = int(req_info.get(KEY_DECODE_STEP, req_info.get("generated_len", 0)))
|
||||
decode_hidden, output_index = (
|
||||
(hidden_states[cursor:end], cursor) if token_count == 1 else (hidden_states[end - 1 : end], end - 1)
|
||||
)
|
||||
sampled_token_latent, next_embeds, new_history, stop_probs = self._decode_one_step(
|
||||
hidden_states=decode_hidden,
|
||||
latent_history=req_history,
|
||||
cfg_scale=_resolve_runtime_float(req_info, KEY_CFG, self.ming_config.cfg),
|
||||
sigma=_resolve_runtime_float(req_info, KEY_SIGMA, self.ming_config.sigma),
|
||||
temperature=_resolve_runtime_float(req_info, KEY_TEMPERATURE, self.ming_config.temperature),
|
||||
)
|
||||
req_max_decode_steps = _resolve_runtime_int(
|
||||
req_info, KEY_MAX_DECODE_STEPS, self.ming_config.max_decode_steps
|
||||
)
|
||||
req_min_decode_steps = _resolve_optional_runtime_int(req_info, KEY_MIN_DECODE_STEPS, 0)
|
||||
if latent_patch_tokens is None:
|
||||
latent_patch_tokens = sampled_token_latent.new_zeros(
|
||||
(total_tokens, self.ming_config.patch_size, self.ming_config.latent_dim)
|
||||
)
|
||||
next_embed_tokens = next_embeds.new_zeros((total_tokens, 1, self.ming_config.llm_hidden_size))
|
||||
new_history_tokens = new_history.new_zeros(
|
||||
(total_tokens, self.ming_config.history_patch_size, self.ming_config.latent_dim)
|
||||
)
|
||||
decode_step_tokens = torch.zeros((total_tokens,), dtype=torch.int32, device=hidden_states.device)
|
||||
has_patch = torch.zeros((total_tokens,), dtype=torch.bool, device=hidden_states.device)
|
||||
stop_reason_code_tokens = torch.zeros((total_tokens,), dtype=torch.int32, device=hidden_states.device)
|
||||
latent_patch_tokens[output_index : output_index + 1] = sampled_token_latent
|
||||
next_embed_tokens[output_index : output_index + 1] = next_embeds
|
||||
new_history_tokens[output_index : output_index + 1] = new_history
|
||||
decode_step_tokens[output_index : output_index + 1] = decode_step
|
||||
has_patch[output_index : output_index + 1] = True
|
||||
stop_reason, _, _, _, next_token_id = _resolve_ming_stop_decision(
|
||||
step=decode_step,
|
||||
stop_prob=float(stop_probs.reshape(-1)[0].item()),
|
||||
stop_threshold=float(self.ming_config.stop_head_threshold),
|
||||
min_stop_step=int(self.ming_config.stop_head_min_steps),
|
||||
min_decode_steps=req_min_decode_steps,
|
||||
max_decode_steps=req_max_decode_steps,
|
||||
audio_dummy_token_id=int(self.ming_config.audio_dummy_token_id),
|
||||
text_eos_token_id=int(self.ming_config.text_eos_token_id),
|
||||
)
|
||||
stop_reason_code_tokens[output_index : output_index + 1] = MING_STOP_REASON_CODES[stop_reason]
|
||||
next_token_ids.append(int(next_token_id))
|
||||
if isinstance(req_id, str):
|
||||
pending_updates[req_id] = {
|
||||
KEY_LATENT_HISTORY: new_history,
|
||||
KEY_NEXT_EMBEDS: next_embeds,
|
||||
"ming_latent_patch": sampled_token_latent,
|
||||
"ming_stop_prob": stop_probs,
|
||||
MING_STOP_REASON_KEY: stop_reason,
|
||||
}
|
||||
any_decode = True
|
||||
cursor = end
|
||||
|
||||
self._pending_postprocess_updates = pending_updates
|
||||
if not any_decode:
|
||||
self._last_ming_next_token_ids = None
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states, multimodal_outputs=None, intermediate_tensors=intermediate_tensors
|
||||
)
|
||||
self._last_ming_next_token_ids = next_token_ids
|
||||
return OmniOutput(
|
||||
text_hidden_states=hidden_states,
|
||||
multimodal_outputs={
|
||||
"ming_latent_patch": latent_patch_tokens,
|
||||
"ming_next_embeds": next_embed_tokens,
|
||||
"ming_new_history": new_history_tokens,
|
||||
"ming_decode_step": decode_step_tokens,
|
||||
"ming_has_patch": has_patch,
|
||||
MING_STOP_REASON_KEY: stop_reason_code_tokens,
|
||||
},
|
||||
intermediate_tensors=intermediate_tensors,
|
||||
)
|
||||
|
||||
def pop_postprocess_update(self, req_id: str) -> dict[str, Any]:
|
||||
return self._pending_postprocess_updates.pop(req_id, {}) if isinstance(req_id, str) else {}
|
||||
|
||||
def compute_logits(self, hidden_states: torch.Tensor, sampling_metadata: SamplingMetadata) -> torch.Tensor | None:
|
||||
del sampling_metadata
|
||||
if self._last_text_mode:
|
||||
return (
|
||||
None
|
||||
if hidden_states is None or hidden_states.numel() == 0
|
||||
else self.model.compute_logits(hidden_states)
|
||||
)
|
||||
if hidden_states is None or hidden_states.numel() == 0:
|
||||
return None
|
||||
if hidden_states.dim() != 2:
|
||||
raise RuntimeError(
|
||||
f"Expected hidden_states rank-2 [B,H] in compute_logits, got {tuple(hidden_states.shape)}"
|
||||
)
|
||||
batch_size = hidden_states.shape[0]
|
||||
next_token_ids = self._last_ming_next_token_ids
|
||||
self._last_ming_next_token_ids = None
|
||||
if next_token_ids is None:
|
||||
logger.debug(
|
||||
"Missing Ming forced next-token ids before compute_logits. "
|
||||
"Using dummy next-token IDs (likely during a profiling or dummy run)."
|
||||
)
|
||||
next_token_ids = [int(self.ming_config.text_eos_token_id)] * batch_size
|
||||
if len(next_token_ids) != batch_size:
|
||||
raise RuntimeError(
|
||||
"Ming forced next-token batch mismatch: "
|
||||
f"got {len(next_token_ids)} ids for hidden_states batch {batch_size}."
|
||||
)
|
||||
logits = torch.full(
|
||||
(batch_size, self.ming_config.llm_vocab_size),
|
||||
float("-inf"),
|
||||
device=hidden_states.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
for i, next_token_id in enumerate(next_token_ids):
|
||||
logits[i, int(next_token_id)] = 0.0
|
||||
return logits
|
||||
|
||||
def sample(self, logits, sampling_metadata):
|
||||
if logits is None:
|
||||
return None
|
||||
if self._last_text_mode:
|
||||
return self.model.sample(logits, sampling_metadata)
|
||||
del sampling_metadata
|
||||
return SamplerOutput(
|
||||
sampled_token_ids=logits.argmax(dim=-1, keepdim=True).to(dtype=torch.int32),
|
||||
logprobs_tensors=None,
|
||||
)
|
||||
|
||||
def _decode_one_step(
|
||||
self,
|
||||
*,
|
||||
hidden_states: torch.Tensor,
|
||||
latent_history: torch.Tensor,
|
||||
cfg_scale: float,
|
||||
sigma: float,
|
||||
temperature: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
if hidden_states.dim() != 2:
|
||||
raise RuntimeError(f"Expected decode hidden_states rank-2 [B,H], got {tuple(hidden_states.shape)}")
|
||||
if latent_history.dim() != 3:
|
||||
raise RuntimeError(f"Expected latent_history rank-3 [B,T,D], got {tuple(latent_history.shape)}")
|
||||
if hidden_states.shape[0] != latent_history.shape[0]:
|
||||
raise RuntimeError(
|
||||
"Batch mismatch: "
|
||||
f"hidden_states B={hidden_states.shape[0]} "
|
||||
f"vs latent_history B={latent_history.shape[0]}"
|
||||
)
|
||||
# [Batch, Hidden] -> [Batch, Time, Hidden] = [B, 1, H] for FlowLoss conditioning.
|
||||
z_diff_cond = hidden_states.to(dtype=self.fm_dtype).unsqueeze(1)
|
||||
if not torch.isfinite(z_diff_cond).all():
|
||||
raise RuntimeError("Non-finite z_diff_cond before FlowLoss.sample().")
|
||||
sampled_token_latent = self.flowloss.sample(
|
||||
z=z_diff_cond,
|
||||
latent_history=latent_history,
|
||||
cfg=cfg_scale,
|
||||
patch_size=self.ming_config.patch_size,
|
||||
sigma=sigma,
|
||||
temperature=temperature,
|
||||
)
|
||||
expected_shape = (hidden_states.shape[0], self.ming_config.patch_size, self.ming_config.latent_dim)
|
||||
if tuple(sampled_token_latent.shape) != expected_shape:
|
||||
raise RuntimeError(
|
||||
f"FlowLoss output shape mismatch: got {tuple(sampled_token_latent.shape)}, expected {expected_shape}"
|
||||
)
|
||||
new_history = torch.cat([latent_history[:, self.ming_config.patch_size :, :], sampled_token_latent], dim=1)
|
||||
# Aggregator expects [Batch, Time, Dimension] = [B, 4, 64] and returns [B, 1, H].
|
||||
next_embeds = self.linear_proj_audio(sampled_token_latent)
|
||||
stop_probs = self.stop_head(hidden_states.to(dtype=self.fm_dtype)).softmax(dim=-1)[:, 1]
|
||||
if not torch.isfinite(sampled_token_latent).all():
|
||||
raise RuntimeError("Non-finite sampled_token_latent in Ming decode step.")
|
||||
if not torch.isfinite(next_embeds).all():
|
||||
raise RuntimeError("Non-finite next_embeds in Ming decode step.")
|
||||
if not torch.isfinite(stop_probs).all():
|
||||
raise RuntimeError("Non-finite stop_probs in Ming decode step.")
|
||||
return sampled_token_latent, next_embeds, new_history, stop_probs
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
loaded_params = AutoWeightsLoader(self).load_weights(weights, mapper=self.hf_to_vllm_mapper)
|
||||
_warn_missing_prefix("flowloss", params_dict, loaded_params, prefix="flowloss.", fatal=True)
|
||||
_warn_missing_prefix("linear_proj_audio", params_dict, loaded_params, prefix="linear_proj_audio.", fatal=True)
|
||||
_warn_missing_prefix("stop_head", params_dict, loaded_params, prefix="stop_head.", fatal=True)
|
||||
_warn_missing_prefix("spk_head", params_dict, loaded_params, prefix="spk_head.", fatal=True)
|
||||
return loaded_params
|
||||
|
||||
|
||||
def _extract_hidden_states(backbone_out: object) -> torch.Tensor:
|
||||
if isinstance(backbone_out, torch.Tensor):
|
||||
return backbone_out
|
||||
if hasattr(backbone_out, "last_hidden_state"):
|
||||
return backbone_out.last_hidden_state
|
||||
if isinstance(backbone_out, (tuple, list)) and backbone_out and isinstance(backbone_out[0], torch.Tensor):
|
||||
return backbone_out[0]
|
||||
raise TypeError(f"Unsupported backbone forward output type: {type(backbone_out)}")
|
||||
|
||||
|
||||
def _resolve_ming_runtime_dtype(vllm_config: VllmConfig) -> torch.dtype:
|
||||
dtype = getattr(vllm_config.model_config, "dtype", None)
|
||||
if isinstance(dtype, torch.dtype):
|
||||
return dtype
|
||||
if isinstance(dtype, str):
|
||||
normalized = dtype.strip().lower()
|
||||
if normalized in ("float16", "half", "torch.float16"):
|
||||
return torch.float16
|
||||
if normalized in ("bfloat16", "bf16", "torch.bfloat16"):
|
||||
return torch.bfloat16
|
||||
if normalized in ("float32", "fp32", "torch.float32"):
|
||||
return torch.float32
|
||||
return torch.float32
|
||||
|
||||
|
||||
def _warn_missing_prefix(
|
||||
module_name: str,
|
||||
params_dict: dict[str, nn.Parameter],
|
||||
loaded_params: set[str],
|
||||
prefix: str,
|
||||
fatal: bool = False,
|
||||
) -> None:
|
||||
missing = {key for key in params_dict if key.startswith(prefix)} - loaded_params
|
||||
if not missing:
|
||||
return
|
||||
msg = (
|
||||
f"MingLLMModel: {len(missing)} {module_name} params not loaded "
|
||||
f"(prefix={prefix}). First few: {sorted(missing)[:5]}"
|
||||
)
|
||||
if fatal:
|
||||
raise RuntimeError(msg)
|
||||
warnings.warn(msg, stacklevel=3)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Aggregator",
|
||||
"FlowLoss",
|
||||
"MING_STOP_REASON_CODES",
|
||||
"MING_STOP_REASON_CONTINUE",
|
||||
"MING_STOP_REASON_KEY",
|
||||
"MING_STOP_REASON_MAX_DECODE_STEPS",
|
||||
"MING_STOP_REASON_STOP_HEAD",
|
||||
"MingLLMModel",
|
||||
"_coerce_latent_history",
|
||||
"_extract_hidden_states",
|
||||
"_resolve_ming_runtime_dtype",
|
||||
"_resolve_ming_stop_decision",
|
||||
"_warn_missing_prefix",
|
||||
]
|
||||
@@ -0,0 +1,183 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/modeling_bailingmm.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from vllm.forward_context import get_forward_context, is_forward_context_available
|
||||
|
||||
from .config_ming_tts import KEY_MAX_DECODE_STEPS, KEY_MIN_DECODE_STEPS, KEY_REQUEST_ID, KEY_TEXT_MODE, MingTTSConfig
|
||||
|
||||
MING_STOP_REASON_CONTINUE = "continue"
|
||||
MING_STOP_REASON_STOP_HEAD = "stop_head"
|
||||
MING_STOP_REASON_MAX_DECODE_STEPS = "max_decode_steps"
|
||||
MING_STOP_REASON_KEY = "ming_stop_reason"
|
||||
MING_STOP_REASON_CODES = {
|
||||
MING_STOP_REASON_CONTINUE: 0,
|
||||
MING_STOP_REASON_STOP_HEAD: 1,
|
||||
MING_STOP_REASON_MAX_DECODE_STEPS: 2,
|
||||
}
|
||||
|
||||
|
||||
def _normalize_request_infos(model_intermediate_buffer: object) -> list[dict[str, Any]]:
|
||||
if not isinstance(model_intermediate_buffer, list):
|
||||
return []
|
||||
infos: list[dict[str, Any]] = []
|
||||
for item in model_intermediate_buffer:
|
||||
infos.append(item if isinstance(item, dict) else {})
|
||||
return infos
|
||||
|
||||
|
||||
def _get_request_token_counts(
|
||||
hidden_states: torch.Tensor,
|
||||
request_infos: list[dict[str, Any]],
|
||||
seq_token_counts: list[int] | None,
|
||||
) -> list[int]:
|
||||
if seq_token_counts:
|
||||
return [int(x) for x in seq_token_counts]
|
||||
|
||||
if is_forward_context_available():
|
||||
slices = getattr(get_forward_context(), "ubatch_slices", None)
|
||||
if slices is not None and len(slices) > 0:
|
||||
counts: list[int] = []
|
||||
for item in slices:
|
||||
if isinstance(item, int):
|
||||
counts.append(int(item))
|
||||
elif hasattr(item, "stop") and hasattr(item, "start"):
|
||||
counts.append(int(item.stop) - int(item.start))
|
||||
if counts:
|
||||
return counts
|
||||
|
||||
if request_infos:
|
||||
if len(request_infos) == hidden_states.shape[0]:
|
||||
return [1] * hidden_states.shape[0]
|
||||
return [hidden_states.shape[0]]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _coerce_latent_history(
|
||||
value: object,
|
||||
*,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
cfg: MingTTSConfig,
|
||||
) -> torch.Tensor | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, torch.Tensor):
|
||||
value = torch.as_tensor(value)
|
||||
|
||||
history = value.detach()
|
||||
if history.ndim == 2:
|
||||
history = history.unsqueeze(0)
|
||||
if history.ndim != 3:
|
||||
raise RuntimeError(f"Expected latent_history rank-3 [B,T,D], got {tuple(history.shape)}")
|
||||
if history.shape[1] != cfg.history_patch_size or history.shape[2] != cfg.latent_dim:
|
||||
raise RuntimeError(
|
||||
f"latent_history shape mismatch: got {tuple(history.shape)}, "
|
||||
f"expected [B,{cfg.history_patch_size},{cfg.latent_dim}]"
|
||||
)
|
||||
return history.to(device=device, dtype=dtype)
|
||||
|
||||
|
||||
def _resolve_runtime_float(req_info: dict[str, Any], key: str, default_value: float) -> float:
|
||||
raw = req_info.get(key, default_value)
|
||||
try:
|
||||
value = float(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError(f"Invalid {key}: expected float-like value, got {raw!r}") from exc
|
||||
if not value >= 0.0:
|
||||
raise RuntimeError(f"Invalid {key}: expected non-negative value, got {value}")
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_runtime_int(req_info: dict[str, Any], key: str, default_value: int) -> int:
|
||||
raw = req_info.get(key, default_value)
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError(f"Invalid {key}: expected int-like value, got {raw!r}") from exc
|
||||
if value <= 0:
|
||||
raise RuntimeError(f"Invalid {key}: expected positive value, got {value}")
|
||||
return value
|
||||
|
||||
|
||||
def _resolve_optional_runtime_int(req_info: dict[str, Any], key: str, default_value: int) -> int:
|
||||
raw = req_info.get(key, default_value)
|
||||
try:
|
||||
value = int(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RuntimeError(f"Invalid {key}: expected int-like value, got {raw!r}") from exc
|
||||
if value < 0:
|
||||
raise RuntimeError(f"Invalid {key}: expected non-negative value, got {value}")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_ming_decode_window(
|
||||
request_infos: list[dict[str, Any]],
|
||||
*,
|
||||
min_stop_step: int,
|
||||
default_max_decode_steps: int,
|
||||
) -> None:
|
||||
for i, info in enumerate(request_infos):
|
||||
if info.get(KEY_TEXT_MODE):
|
||||
continue
|
||||
max_steps = _resolve_runtime_int(info, KEY_MAX_DECODE_STEPS, default_max_decode_steps)
|
||||
min_steps = _resolve_optional_runtime_int(info, KEY_MIN_DECODE_STEPS, 0)
|
||||
min_required = max(min_stop_step + 1, min_steps)
|
||||
if max_steps < min_required:
|
||||
req_id = info.get(KEY_REQUEST_ID, f"idx={i}")
|
||||
raise ValueError(
|
||||
f"Ming request {req_id!r}: max_decode_steps={max_steps} < "
|
||||
f"min_required_decode_steps={min_required} "
|
||||
f"(min_stop_step={min_stop_step}, min_decode_steps={min_steps})"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_ming_stop_decision(
|
||||
*,
|
||||
step: int,
|
||||
stop_prob: float,
|
||||
stop_threshold: float,
|
||||
min_stop_step: int,
|
||||
min_decode_steps: int,
|
||||
max_decode_steps: int,
|
||||
audio_dummy_token_id: int,
|
||||
text_eos_token_id: int,
|
||||
) -> tuple[str, bool, bool, int, int]:
|
||||
min_required_decode_steps = max(min_stop_step + 1, min_decode_steps)
|
||||
if max_decode_steps < min_required_decode_steps:
|
||||
raise RuntimeError(
|
||||
"Invalid Ming decode window: "
|
||||
f"max_decode_steps={max_decode_steps} is smaller than "
|
||||
f"min_required_decode_steps={min_required_decode_steps}"
|
||||
)
|
||||
should_force_stop = (step + 1) >= max_decode_steps
|
||||
should_stop_head = ((step + 1) >= min_required_decode_steps) and stop_prob > stop_threshold
|
||||
|
||||
if should_force_stop:
|
||||
return (
|
||||
MING_STOP_REASON_MAX_DECODE_STEPS,
|
||||
True,
|
||||
True,
|
||||
min_required_decode_steps,
|
||||
text_eos_token_id,
|
||||
)
|
||||
if should_stop_head:
|
||||
return (
|
||||
MING_STOP_REASON_STOP_HEAD,
|
||||
True,
|
||||
False,
|
||||
min_required_decode_steps,
|
||||
text_eos_token_id,
|
||||
)
|
||||
return (
|
||||
MING_STOP_REASON_CONTINUE,
|
||||
False,
|
||||
False,
|
||||
min_required_decode_steps,
|
||||
audio_dummy_token_id,
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Ming TTS pipeline: Stage-0 LLM+flow -> Stage-1 audio VAE."""
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
PipelineConfig,
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.ming_tts"
|
||||
|
||||
MING_TTS_PIPELINE = PipelineConfig(
|
||||
model_type="ming_tts",
|
||||
model_arch="MingTTSForConditionalGeneration",
|
||||
hf_architectures=("MingTTSForConditionalGeneration",),
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="llm",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
hf_config_name="llm_config",
|
||||
engine_output_type="latent",
|
||||
async_chunk_process_next_stage_input_func=(f"{_PROC}.llm2audio_vae_async_chunk"),
|
||||
sampling_constraints={
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"top_k": -1,
|
||||
"max_tokens": 512,
|
||||
"detokenize": True,
|
||||
},
|
||||
),
|
||||
StagePipelineConfig(
|
||||
stage_id=1,
|
||||
model_stage="audio_vae",
|
||||
execution_type=StageExecutionType.LLM_GENERATION,
|
||||
input_sources=(0,),
|
||||
final_output=True,
|
||||
final_output_type="audio",
|
||||
hf_config_name="llm_config",
|
||||
engine_output_type="audio",
|
||||
sync_process_input_func=f"{_PROC}.llm2audio_vae",
|
||||
sampling_constraints={
|
||||
"temperature": 0.0,
|
||||
"top_p": 1.0,
|
||||
"top_k": -1,
|
||||
"max_tokens": 1,
|
||||
"detokenize": False,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from .audio_prep import (
|
||||
coerce_speaker_embeddings,
|
||||
count_prompt_latent_patches,
|
||||
count_prompt_waveform_patches,
|
||||
pad_prompt_waveform,
|
||||
)
|
||||
from .config_ming_tts import (
|
||||
AUDIO_FRAME_HOP,
|
||||
KEY_MAX_DECODE_STEPS,
|
||||
KEY_MIN_DECODE_STEPS,
|
||||
KEY_PROMPT_LATENTS,
|
||||
KEY_REQUEST_ID,
|
||||
KEY_SPEAKER_EMBEDDING,
|
||||
LATENT_DIM,
|
||||
PATCH_SIZE,
|
||||
SAMPLE_RATE,
|
||||
VAE_PATCH_SIZE,
|
||||
)
|
||||
|
||||
DEFAULT_PROMPT = "Please generate speech based on the following description.\n"
|
||||
BASE_CAPTION_TEMPLATE: dict[str, Any] = {
|
||||
"audio_sequence": [
|
||||
{
|
||||
"序号": 1,
|
||||
"说话人": "speaker_1",
|
||||
"方言": None,
|
||||
"风格": None,
|
||||
"语速": None,
|
||||
"基频": None,
|
||||
"音量": None,
|
||||
"情感": None,
|
||||
"BGM": {
|
||||
"Genre": None,
|
||||
"Mood": None,
|
||||
"Instrument": None,
|
||||
"Theme": None,
|
||||
"ENV": None,
|
||||
"SNR": None,
|
||||
},
|
||||
"IP": None,
|
||||
}
|
||||
]
|
||||
}
|
||||
_DURATION_SECONDS_RE = re.compile(r"Duration:\s*([0-9]+(?:\.[0-9]+)?)\s*s\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def create_instruction(user_input: Any) -> str | None:
|
||||
if user_input is None:
|
||||
return None
|
||||
if isinstance(user_input, str):
|
||||
return user_input
|
||||
if not isinstance(user_input, dict):
|
||||
raise ValueError(f"Ming instruction must be str, dict, or None; got {type(user_input).__name__}")
|
||||
caption = copy.deepcopy(BASE_CAPTION_TEMPLATE)
|
||||
item = caption["audio_sequence"][0]
|
||||
for key, value in user_input.items():
|
||||
if key in item:
|
||||
item[key] = value
|
||||
return json.dumps(caption, ensure_ascii=False)
|
||||
|
||||
|
||||
def parse_duration_seconds(text: str | None) -> float | None:
|
||||
if not isinstance(text, str):
|
||||
return None
|
||||
match = _DURATION_SECONDS_RE.search(text)
|
||||
if match is None:
|
||||
return None
|
||||
try:
|
||||
value = float(match.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
if value <= 0.0:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def estimate_decode_steps_for_duration(
|
||||
duration_seconds: float,
|
||||
*,
|
||||
sample_rate: int = SAMPLE_RATE,
|
||||
frame_hop: int = AUDIO_FRAME_HOP,
|
||||
patch_size: int = PATCH_SIZE,
|
||||
vae_patch_size: int = VAE_PATCH_SIZE,
|
||||
) -> int:
|
||||
if duration_seconds <= 0.0:
|
||||
return 0
|
||||
samples_per_decode_step = int(frame_hop) * int(patch_size) * int(vae_patch_size)
|
||||
required_samples = float(duration_seconds) * float(sample_rate)
|
||||
return max(1, int(math.ceil(required_samples / float(samples_per_decode_step))))
|
||||
|
||||
|
||||
def estimate_decode_step_window_for_duration(duration_seconds: float) -> tuple[int, int]:
|
||||
target_steps = estimate_decode_steps_for_duration(duration_seconds)
|
||||
min_steps = max(1, target_steps - 3)
|
||||
max_steps = max(min_steps, target_steps + 3)
|
||||
return min_steps, max_steps
|
||||
|
||||
|
||||
def resolve_effective_runtime_controls(
|
||||
*,
|
||||
text: str,
|
||||
runtime_controls: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
controls = {} if runtime_controls is None else dict(runtime_controls)
|
||||
has_explicit_min = KEY_MIN_DECODE_STEPS in controls and controls[KEY_MIN_DECODE_STEPS] is not None
|
||||
has_explicit_max = KEY_MAX_DECODE_STEPS in controls and controls[KEY_MAX_DECODE_STEPS] is not None
|
||||
if has_explicit_min or has_explicit_max:
|
||||
return controls
|
||||
duration_seconds = parse_duration_seconds(text)
|
||||
if duration_seconds is None:
|
||||
return controls
|
||||
min_decode_steps, max_decode_steps = estimate_decode_step_window_for_duration(duration_seconds)
|
||||
controls[KEY_MIN_DECODE_STEPS] = min_decode_steps
|
||||
controls[KEY_MAX_DECODE_STEPS] = max_decode_steps
|
||||
return controls
|
||||
|
||||
|
||||
def build_dense_prompt_token_ids(
|
||||
tokenizer: Any,
|
||||
*,
|
||||
prompt: str,
|
||||
text: str,
|
||||
instruction: str | None = None,
|
||||
prompt_text: str | None = None,
|
||||
speaker_count: int = 0,
|
||||
prompt_patch_count: int = 0,
|
||||
) -> list[int]:
|
||||
speaker_prompt = []
|
||||
for idx in range(int(speaker_count)):
|
||||
speaker_prompt.extend(
|
||||
tokenizer.encode(f" speaker_{idx + 1}:")
|
||||
+ tokenizer.encode("<|vision_start|>")
|
||||
+ tokenizer.encode("<|vision_pad|>")
|
||||
+ tokenizer.encode("<|vision_end|>\n")
|
||||
)
|
||||
instruction_prompt = (
|
||||
tokenizer.encode(instruction) + tokenizer.encode("<|endoftext|>") if instruction is not None else []
|
||||
)
|
||||
prompt_text_tokens = (
|
||||
tokenizer.encode(prompt_text) if int(prompt_patch_count) > 0 and prompt_text is not None else []
|
||||
)
|
||||
audio_patch_token_id = tokenizer.convert_tokens_to_ids("<audioPatch>")
|
||||
if audio_patch_token_id == tokenizer.unk_token_id:
|
||||
raise ValueError("Ming tokenizer is missing required <audioPatch> token.")
|
||||
prompt_latent_tokens = [audio_patch_token_id] * int(prompt_patch_count)
|
||||
text_input_prefix = (
|
||||
[]
|
||||
if all(token in text for token in ("Genre: ", "Mood: ", "Instrument: ", "Theme: ", "Duration: "))
|
||||
else tokenizer.encode(" Text input:\n")
|
||||
)
|
||||
return (
|
||||
tokenizer.encode("<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n")
|
||||
+ tokenizer.encode("<|im_start|>user\n")
|
||||
+ tokenizer.encode(prompt)
|
||||
+ speaker_prompt
|
||||
+ text_input_prefix
|
||||
+ prompt_text_tokens
|
||||
+ tokenizer.encode(text)
|
||||
+ tokenizer.encode("<|im_end|>\n")
|
||||
+ tokenizer.encode("<|im_start|>assistant\n")
|
||||
+ instruction_prompt
|
||||
+ tokenizer.encode("<audio>")
|
||||
+ prompt_latent_tokens
|
||||
)
|
||||
|
||||
|
||||
def build_ming_dense_prompt(
|
||||
tokenizer: Any,
|
||||
*,
|
||||
prompt: str,
|
||||
text: str,
|
||||
runtime_controls: dict[str, Any] | None = None,
|
||||
instruction: Any = None,
|
||||
prompt_text: str | None = None,
|
||||
prompt_waveform: Any = None,
|
||||
prompt_latents: Any = None,
|
||||
speaker_embedding: Any = None,
|
||||
use_zero_spk_emb: bool = False,
|
||||
request_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
instruction_text = create_instruction(instruction)
|
||||
speaker_embeddings = coerce_speaker_embeddings(speaker_embedding, use_zero_spk_emb=use_zero_spk_emb)
|
||||
effective_runtime_controls = resolve_effective_runtime_controls(text=text, runtime_controls=runtime_controls)
|
||||
|
||||
prompt_waveform_tensor = None
|
||||
prompt_patch_count = 0
|
||||
if prompt_waveform is not None:
|
||||
prompt_waveform_tensor = pad_prompt_waveform(prompt_waveform)
|
||||
prompt_patch_count = count_prompt_waveform_patches(prompt_waveform_tensor)
|
||||
if prompt_waveform_tensor is not None and prompt_latents is not None:
|
||||
raise ValueError(
|
||||
"Ming waveform cloning request provided both raw prompt_waveform and explicit prompt_latents. "
|
||||
"Choose exactly one source of truth."
|
||||
)
|
||||
|
||||
prompt_latent_value = None
|
||||
if prompt_waveform_tensor is not None and prompt_text is None:
|
||||
raise ValueError(
|
||||
"Ming prompt_waveform requires prompt_text for prompt-latent conditioning. "
|
||||
"Use speaker_embedding for reference-audio-only speaker conditioning."
|
||||
)
|
||||
if prompt_latents is not None:
|
||||
prompt_latent_value = torch.as_tensor(prompt_latents)
|
||||
prompt_patch_count = count_prompt_latent_patches(
|
||||
prompt_latent_value, patch_size=PATCH_SIZE, latent_dim=LATENT_DIM
|
||||
)
|
||||
|
||||
prompt_token_ids = build_dense_prompt_token_ids(
|
||||
tokenizer,
|
||||
prompt=prompt,
|
||||
text=text,
|
||||
instruction=instruction_text,
|
||||
prompt_text=prompt_text if prompt_patch_count > 0 else None,
|
||||
speaker_count=0 if speaker_embeddings is None else len(speaker_embeddings),
|
||||
prompt_patch_count=prompt_patch_count,
|
||||
)
|
||||
|
||||
additional_information = {}
|
||||
for key, value in effective_runtime_controls.items():
|
||||
if isinstance(value, torch.Tensor):
|
||||
additional_information[key] = value
|
||||
elif key in (KEY_MIN_DECODE_STEPS, KEY_MAX_DECODE_STEPS):
|
||||
additional_information[key] = torch.tensor(int(value), dtype=torch.int32)
|
||||
else:
|
||||
additional_information[key] = torch.tensor(float(value), dtype=torch.float32)
|
||||
if request_id is not None:
|
||||
additional_information[KEY_REQUEST_ID] = request_id
|
||||
if instruction_text is not None:
|
||||
additional_information["instruction"] = instruction_text
|
||||
if prompt_text is not None:
|
||||
additional_information["prompt_text"] = prompt_text
|
||||
if prompt_waveform_tensor is not None:
|
||||
additional_information["prompt_waveform"] = prompt_waveform_tensor
|
||||
additional_information["prompt_waveform_length"] = torch.tensor(
|
||||
[int(prompt_waveform_tensor.shape[-1])], dtype=torch.int32
|
||||
)
|
||||
if prompt_latent_value is not None:
|
||||
additional_information[KEY_PROMPT_LATENTS] = prompt_latent_value
|
||||
if speaker_embeddings is not None:
|
||||
additional_information[KEY_SPEAKER_EMBEDDING] = (
|
||||
speaker_embeddings[0] if len(speaker_embeddings) == 1 else torch.stack(speaker_embeddings, dim=0)
|
||||
)
|
||||
if use_zero_spk_emb:
|
||||
additional_information["use_zero_spk_emb"] = True
|
||||
return {
|
||||
"prompt": prompt,
|
||||
"text": text,
|
||||
"prompt_token_ids": prompt_token_ids,
|
||||
"additional_information": additional_information,
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_PROMPT",
|
||||
"build_dense_prompt_token_ids",
|
||||
"build_ming_dense_prompt",
|
||||
"create_instruction",
|
||||
"estimate_decode_step_window_for_duration",
|
||||
"estimate_decode_steps_for_duration",
|
||||
"parse_duration_seconds",
|
||||
"resolve_effective_runtime_controls",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from safetensors import safe_open
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.engine.stage_init_utils import _resolve_model_to_local_path
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAE
|
||||
|
||||
from .audio_prep import (
|
||||
_coerce_prompt_latents,
|
||||
_normalize_prompt_waveform,
|
||||
count_prompt_latent_patches,
|
||||
pad_prompt_waveform,
|
||||
)
|
||||
from .config_ming_tts import KEY_PROMPT_LATENTS
|
||||
|
||||
logger = init_logger(__name__)
|
||||
_PROMPT_ENCODER_LOAD_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _resolve_prompt_latents(wrapper: Any, info_dict: dict[str, Any]) -> dict[str, torch.Tensor] | None:
|
||||
raw_latents = info_dict.get(KEY_PROMPT_LATENTS, info_dict.get("prompt_latents"))
|
||||
raw_waveform = info_dict.get("prompt_waveform", info_dict.get("prompt_waveforms"))
|
||||
if raw_latents is not None and raw_waveform is not None:
|
||||
raise ValueError(
|
||||
"Ming waveform cloning request provided both raw prompt_waveform and explicit prompt_latents. "
|
||||
"Choose exactly one source of truth."
|
||||
)
|
||||
|
||||
direct_latents = _coerce_prompt_latents(
|
||||
raw_latents,
|
||||
patch_size=wrapper.ming_config.patch_size,
|
||||
latent_dim=wrapper.ming_config.latent_dim,
|
||||
)
|
||||
if direct_latents is not None:
|
||||
return direct_latents
|
||||
if raw_waveform is None:
|
||||
return None
|
||||
|
||||
encode_fn = getattr(wrapper, "_encode_prompt_waveform_to_latents", None)
|
||||
if callable(encode_fn):
|
||||
latents = encode_fn(raw_waveform, info_dict.get("prompt_waveform_length"))
|
||||
else:
|
||||
latents = _encode_prompt_waveform_to_latents(
|
||||
wrapper,
|
||||
raw_waveform,
|
||||
info_dict.get("prompt_waveform_length"),
|
||||
)
|
||||
return _coerce_prompt_latents(
|
||||
latents,
|
||||
patch_size=wrapper.ming_config.patch_size,
|
||||
latent_dim=wrapper.ming_config.latent_dim,
|
||||
)
|
||||
|
||||
|
||||
def _load_prompt_encoder(wrapper: Any) -> AudioVAE:
|
||||
if wrapper._prompt_encoder is not None:
|
||||
return wrapper._prompt_encoder
|
||||
with _PROMPT_ENCODER_LOAD_LOCK:
|
||||
if wrapper._prompt_encoder is not None:
|
||||
return wrapper._prompt_encoder
|
||||
if wrapper.ming_config.audio_tokenizer_config is None:
|
||||
raise RuntimeError("Ming Stage-0 requires audio_tokenizer_config to encode prompt audio.")
|
||||
|
||||
load_start = time.perf_counter()
|
||||
encoder = AudioVAE(wrapper.ming_config.audio_tokenizer_config).eval()
|
||||
state_dict = encoder.state_dict()
|
||||
loaded = 0
|
||||
loaded_encoder_params = set()
|
||||
with torch.no_grad():
|
||||
for shard_path in _iter_model_safetensors(
|
||||
_resolve_model_to_local_path(str(wrapper.vllm_config.model_config.model))
|
||||
):
|
||||
with safe_open(str(shard_path), framework="pt", device="cpu") as handle:
|
||||
for key in handle.keys():
|
||||
if not key.startswith("audio.encoder."):
|
||||
continue
|
||||
name = key[len("audio.") :]
|
||||
if name not in state_dict:
|
||||
continue
|
||||
target = state_dict[name]
|
||||
target.copy_(handle.get_tensor(key).to(device=target.device, dtype=target.dtype))
|
||||
loaded += 1
|
||||
loaded_encoder_params.add(name)
|
||||
if loaded == 0:
|
||||
raise RuntimeError("Ming prompt encoder received no audio.encoder.* weights from checkpoint.")
|
||||
|
||||
expected_encoder_params = {f"encoder.{name}" for name, _ in encoder.encoder.named_parameters()}
|
||||
missing = expected_encoder_params - loaded_encoder_params
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"Ming prompt encoder: {len(missing)} params not loaded. First few: {sorted(missing)[:5]}"
|
||||
)
|
||||
|
||||
dev = next(wrapper.parameters()).device
|
||||
try:
|
||||
del encoder.decoder
|
||||
encoder.decoder = None
|
||||
if dev.type != "cpu":
|
||||
encoder.encoder.to(dev, dtype=getattr(wrapper.model, "fm_dtype", torch.bfloat16))
|
||||
else:
|
||||
encoder.encoder.to(dev)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Failed to move Ming prompt encoder to {dev}: {exc}") from exc
|
||||
wrapper._prompt_encoder = encoder
|
||||
logger.info("Ming prompt encoder cold-loaded in %.3f ms", (time.perf_counter() - load_start) * 1000.0)
|
||||
return encoder
|
||||
|
||||
|
||||
@torch.inference_mode()
|
||||
def _encode_prompt_waveform_to_latents(wrapper: Any, waveform: Any, waveform_length: Any = None) -> torch.Tensor:
|
||||
encoder = _load_prompt_encoder(wrapper)
|
||||
waveform = _normalize_prompt_waveform(waveform, target_sr=wrapper.ming_config.sample_rate)
|
||||
waveform = pad_prompt_waveform(
|
||||
waveform,
|
||||
patch_size=wrapper.ming_config.patch_size,
|
||||
sample_rate=wrapper.ming_config.sample_rate,
|
||||
)
|
||||
dev = next(encoder.encoder.parameters()).device
|
||||
waveform = waveform.to(device=dev, dtype=next(encoder.encoder.parameters()).dtype)
|
||||
if waveform_length is None:
|
||||
waveform_length = torch.full((waveform.shape[0],), waveform.shape[-1], dtype=torch.int32, device=dev)
|
||||
elif not isinstance(waveform_length, torch.Tensor):
|
||||
waveform_length = torch.as_tensor(waveform_length, dtype=torch.int32, device=dev)
|
||||
else:
|
||||
waveform_length = waveform_length.to(device=dev, dtype=torch.int32)
|
||||
|
||||
latents, _ = encoder.encode_latent(waveform, waveform_length)
|
||||
if latents.ndim == 3 and latents.shape[0] == 1:
|
||||
latents = latents.squeeze(0)
|
||||
count_prompt_latent_patches(
|
||||
latents,
|
||||
patch_size=wrapper.ming_config.patch_size,
|
||||
latent_dim=wrapper.ming_config.latent_dim,
|
||||
)
|
||||
return latents.detach().to(dtype=torch.float32).contiguous()
|
||||
|
||||
|
||||
def _iter_model_safetensors(local_model_path: str) -> list[Path]:
|
||||
model_root = Path(local_model_path)
|
||||
index_path = model_root / "model.safetensors.index.json"
|
||||
if index_path.exists():
|
||||
with index_path.open("r", encoding="utf-8") as handle:
|
||||
index_data = json.load(handle)
|
||||
filenames = sorted(set(index_data.get("weight_map", {}).values()))
|
||||
if not filenames:
|
||||
raise RuntimeError(f"No checkpoint shards listed in {index_path}")
|
||||
return [model_root / filename for filename in filenames]
|
||||
|
||||
single_file = model_root / "model.safetensors"
|
||||
if single_file.exists():
|
||||
return [single_file]
|
||||
|
||||
files = sorted(model_root.glob("*.safetensors"))
|
||||
if not files:
|
||||
raise RuntimeError(f"No .safetensors checkpoint found under {local_model_path}")
|
||||
return files
|
||||
|
||||
|
||||
__all__ = [
|
||||
"_encode_prompt_waveform_to_latents",
|
||||
"_iter_model_safetensors",
|
||||
"_load_prompt_encoder",
|
||||
"_resolve_prompt_latents",
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/inclusionAI/Ming-omni-tts/blob/main/spkemb_extractor.py
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torchaudio
|
||||
from vllm.multimodal.media.audio import load_audio
|
||||
|
||||
from vllm_omni.model_executor.models.ming_flash_omni.spk_embedding import SpkembExtractor
|
||||
|
||||
|
||||
def resolve_model_to_local_path(model):
|
||||
if os.path.isdir(model):
|
||||
return model
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
return snapshot_download(model)
|
||||
|
||||
|
||||
class MingSpeakerEmbeddingExtractor:
|
||||
def __init__(self, model, target_sr=16000):
|
||||
local_model_path = resolve_model_to_local_path(model)
|
||||
campplus_path = os.path.join(local_model_path, "campplus.onnx")
|
||||
if not os.path.exists(campplus_path):
|
||||
raise RuntimeError(f"Missing Ming speaker extractor model: {campplus_path}")
|
||||
|
||||
self.target_sr = int(target_sr)
|
||||
self._core = SpkembExtractor(campplus_path, target_sr=self.target_sr)
|
||||
|
||||
def extract_from_waveform(self, waveform, sample_rate):
|
||||
if not isinstance(waveform, torch.Tensor):
|
||||
waveform = torch.as_tensor(waveform)
|
||||
|
||||
tensor = waveform.detach().to(torch.float32)
|
||||
if tensor.ndim == 1:
|
||||
tensor = tensor.unsqueeze(0)
|
||||
if int(sample_rate) != self.target_sr:
|
||||
tensor = torchaudio.transforms.Resample(orig_freq=int(sample_rate), new_freq=self.target_sr)(tensor)
|
||||
|
||||
embedding = self._core._extract_spk_embedding(tensor)
|
||||
return embedding.squeeze(0).to(dtype=torch.float32)
|
||||
|
||||
def extract_from_file(self, audio_path):
|
||||
audio, sample_rate = load_audio(audio_path, sr=None, mono=False)
|
||||
waveform = torch.from_numpy(audio).contiguous()
|
||||
return self.extract_from_waveform(waveform, sample_rate)
|
||||
|
||||
def extract_many(self, audio_paths):
|
||||
return [self.extract_from_file(path) for path in audio_paths]
|
||||
@@ -0,0 +1,172 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
from vllm_omni.model_executor.models.common.ming.audio_vae import AudioVAEConfig
|
||||
|
||||
from .constants import (
|
||||
AGGREGATOR_HIDDEN_SIZE,
|
||||
HISTORY_PATCH_SIZE,
|
||||
LATENT_DIM,
|
||||
LLM_HIDDEN_SIZE,
|
||||
LLM_VOCAB_SIZE,
|
||||
PATCH_SIZE,
|
||||
SAMPLE_RATE,
|
||||
)
|
||||
|
||||
|
||||
def _to_plain_dict(obj: Any) -> dict[str, Any]:
|
||||
"""Normalize nested config objects into plain dicts when possible."""
|
||||
if obj is None:
|
||||
return {}
|
||||
if isinstance(obj, dict):
|
||||
return dict(obj)
|
||||
if isinstance(obj, PretrainedConfig):
|
||||
return obj.to_dict()
|
||||
if hasattr(obj, "to_dict") and callable(obj.to_dict):
|
||||
try:
|
||||
return dict(obj.to_dict())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return dict(vars(obj))
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _coerce_audio_vae_config(atc_raw: Any) -> AudioVAEConfig | None:
|
||||
if atc_raw is None:
|
||||
return None
|
||||
if isinstance(atc_raw, AudioVAEConfig):
|
||||
return atc_raw
|
||||
if isinstance(atc_raw, PretrainedConfig):
|
||||
atc_dict = atc_raw.to_dict()
|
||||
elif isinstance(atc_raw, dict):
|
||||
atc_dict = dict(atc_raw)
|
||||
elif hasattr(atc_raw, "to_dict") and callable(atc_raw.to_dict):
|
||||
atc_dict = dict(atc_raw.to_dict())
|
||||
else:
|
||||
raise TypeError(f"Unsupported audio_tokenizer_config type for Ming dense config: {type(atc_raw)!r}")
|
||||
|
||||
return AudioVAEConfig(**atc_dict)
|
||||
|
||||
|
||||
def _nested_get(obj: Any, *keys: str, default: Any = None) -> Any:
|
||||
"""Safe nested attribute/key access for dicts and config-like objects."""
|
||||
cur = obj
|
||||
for key in keys:
|
||||
if cur is None:
|
||||
return default
|
||||
if isinstance(cur, dict):
|
||||
cur = cur.get(key)
|
||||
else:
|
||||
cur = getattr(cur, key, None)
|
||||
return cur if cur is not None else default
|
||||
|
||||
|
||||
def validate_ming_tts_config(cfg: Any) -> None:
|
||||
"""Run before GPU allocation/weight loading. Raises ValueError on mismatches."""
|
||||
if cfg.audio_dummy_token_id != 151705:
|
||||
raise ValueError(
|
||||
f"audio_dummy_token_id={cfg.audio_dummy_token_id}, expected 151705 (<audioPatch>). "
|
||||
"Wrong tokenizer/checkpoint?"
|
||||
)
|
||||
if cfg.audio_eos_token_id != 151704:
|
||||
raise ValueError(
|
||||
f"audio_eos_token_id={cfg.audio_eos_token_id}, expected 151704 (<end_of_audio>). "
|
||||
"Wrong tokenizer/checkpoint?"
|
||||
)
|
||||
if cfg.text_eos_token_id != 151669:
|
||||
raise ValueError(
|
||||
f"text_eos_token_id={cfg.text_eos_token_id}, expected 151669 (<text_eos>). Wrong tokenizer/checkpoint?"
|
||||
)
|
||||
|
||||
if cfg.audio_tokenizer_config is None:
|
||||
raise ValueError("audio_tokenizer_config is None. Nested AudioVAE config was not deserialized correctly.")
|
||||
|
||||
if cfg.latent_dim != LATENT_DIM:
|
||||
raise ValueError(
|
||||
f"latent_dim mismatch: got {cfg.latent_dim}, expected {LATENT_DIM}. "
|
||||
"Check audio_tokenizer_config.enc_kwargs.latent_dim."
|
||||
)
|
||||
if cfg.patch_size != PATCH_SIZE:
|
||||
raise ValueError(
|
||||
f"patch_size mismatch: got {cfg.patch_size}, expected {PATCH_SIZE}. Check ditar_config.patch_size."
|
||||
)
|
||||
if cfg.history_patch_size != HISTORY_PATCH_SIZE:
|
||||
raise ValueError(
|
||||
f"history_patch_size mismatch: got {cfg.history_patch_size}, expected {HISTORY_PATCH_SIZE}. "
|
||||
"Check ditar_config.history_patch_size."
|
||||
)
|
||||
if cfg.llm_hidden_size != LLM_HIDDEN_SIZE:
|
||||
raise ValueError(
|
||||
f"llm_hidden_size mismatch: got {cfg.llm_hidden_size}, expected {LLM_HIDDEN_SIZE}. "
|
||||
"Check llm_config.hidden_size."
|
||||
)
|
||||
if cfg.llm_vocab_size != LLM_VOCAB_SIZE:
|
||||
raise ValueError(f"llm_vocab_size mismatch: got {cfg.llm_vocab_size}, expected {LLM_VOCAB_SIZE}.")
|
||||
if cfg.sample_rate != SAMPLE_RATE:
|
||||
raise ValueError(f"sample_rate mismatch: got {cfg.sample_rate}, expected {SAMPLE_RATE}.")
|
||||
|
||||
if cfg.vae_patch_size != cfg.patch_size:
|
||||
raise ValueError(f"VAE patch size ({cfg.vae_patch_size}) != flow/DiT patch size ({cfg.patch_size}).")
|
||||
|
||||
llm_hidden_from_cfg = cfg.llm_config.get("hidden_size")
|
||||
if llm_hidden_from_cfg is not None and llm_hidden_from_cfg != cfg.llm_hidden_size:
|
||||
raise ValueError(f"llm_hidden_size ({cfg.llm_hidden_size}) != llm_config.hidden_size ({llm_hidden_from_cfg}).")
|
||||
|
||||
agg_h = cfg.aggregator_config.get("hidden_size")
|
||||
dit_h = cfg.ditar_config.get("hidden_size")
|
||||
if agg_h is not None and dit_h is not None and agg_h != dit_h:
|
||||
raise ValueError(f"aggregator_config.hidden_size ({agg_h}) != ditar_config.hidden_size ({dit_h}).")
|
||||
if agg_h is not None and agg_h != AGGREGATOR_HIDDEN_SIZE:
|
||||
raise ValueError(f"aggregator hidden_size mismatch: got {agg_h}, expected {AGGREGATOR_HIDDEN_SIZE}.")
|
||||
if dit_h is not None and dit_h != AGGREGATOR_HIDDEN_SIZE:
|
||||
raise ValueError(f"ditar hidden_size mismatch: got {dit_h}, expected {AGGREGATOR_HIDDEN_SIZE}.")
|
||||
|
||||
atc = cfg.audio_tokenizer_config
|
||||
semantic_module_kwargs = _nested_get(atc, "semantic_module_kwargs", default=None)
|
||||
if semantic_module_kwargs is not None:
|
||||
raise ValueError("Ming dense 0.5B expects audio_tokenizer_config.semantic_module_kwargs to be null.")
|
||||
|
||||
enc_latent = _nested_get(atc, "enc_kwargs", "latent_dim", default=None)
|
||||
dec_latent = _nested_get(atc, "dec_kwargs", "latent_dim", default=None)
|
||||
if enc_latent is not None and enc_latent != cfg.latent_dim:
|
||||
raise ValueError(f"audio enc latent_dim ({enc_latent}) != Ming latent_dim ({cfg.latent_dim}).")
|
||||
if dec_latent is not None and dec_latent != cfg.latent_dim:
|
||||
raise ValueError(f"audio dec latent_dim ({dec_latent}) != Ming latent_dim ({cfg.latent_dim}).")
|
||||
|
||||
atc_patch = _nested_get(atc, "patch_size", default=None)
|
||||
if atc_patch is not None and atc_patch != cfg.vae_patch_size:
|
||||
raise ValueError(f"audio_tokenizer_config.patch_size ({atc_patch}) != vae_patch_size ({cfg.vae_patch_size}).")
|
||||
|
||||
atc_sr = _nested_get(atc, "sample_rate", default=None)
|
||||
if atc_sr is not None and atc_sr != cfg.sample_rate:
|
||||
raise ValueError(f"audio_tokenizer_config.sample_rate ({atc_sr}) != sample_rate ({cfg.sample_rate}).")
|
||||
|
||||
enc_input_dim = _nested_get(atc, "enc_kwargs", "input_dim", default=None)
|
||||
enc_hop_size = _nested_get(atc, "enc_kwargs", "hop_size", default=None)
|
||||
dec_output_dim = _nested_get(atc, "dec_kwargs", "output_dim", default=None)
|
||||
|
||||
if enc_input_dim is not None and enc_hop_size is not None and enc_input_dim != enc_hop_size:
|
||||
raise ValueError(f"AudioVAE encoder input_dim ({enc_input_dim}) != hop_size ({enc_hop_size}).")
|
||||
if enc_hop_size is not None and dec_output_dim is not None and enc_hop_size != dec_output_dim:
|
||||
raise ValueError(
|
||||
f"AudioVAE encoder hop_size ({enc_hop_size}) != decoder output_dim ({dec_output_dim}). "
|
||||
"Expected 882 in this checkpoint family."
|
||||
)
|
||||
|
||||
if cfg.latent_chunk_size <= 0:
|
||||
raise ValueError(f"latent_chunk_size must be > 0, got {cfg.latent_chunk_size}.")
|
||||
if cfg.latent_left_context < 0:
|
||||
raise ValueError(f"latent_left_context must be >= 0, got {cfg.latent_left_context}.")
|
||||
if cfg.max_decode_steps <= 0:
|
||||
raise ValueError(f"max_decode_steps must be > 0, got {cfg.max_decode_steps}.")
|
||||
if not (0.0 <= cfg.stop_head_threshold <= 1.0):
|
||||
raise ValueError(f"stop_head_threshold must be in [0,1], got {cfg.stop_head_threshold}.")
|
||||
if cfg.stop_head_min_steps < 0:
|
||||
raise ValueError(f"stop_head_min_steps must be >= 0, got {cfg.stop_head_min_steps}.")
|
||||
@@ -156,6 +156,22 @@ _OMNI_MODELS = {
|
||||
"mimo_audio_code2wav",
|
||||
"MiMoAudioToken2WavForConditionalGenerationVLLM",
|
||||
),
|
||||
## ming-tts
|
||||
"MingTTSForConditionalGeneration": (
|
||||
"ming_tts",
|
||||
"ming_tts",
|
||||
"MingTTSForConditionalGeneration",
|
||||
),
|
||||
"MingLLMModel": (
|
||||
"ming_tts",
|
||||
"ming_tts_llm",
|
||||
"MingLLMModel",
|
||||
),
|
||||
"MingAudioVAEModel": (
|
||||
"ming_tts",
|
||||
"ming_tts_audio_vae",
|
||||
"MingAudioVAEModel",
|
||||
),
|
||||
## glm_image
|
||||
"GlmImageForConditionalGeneration": (
|
||||
"glm_image",
|
||||
|
||||
@@ -205,18 +205,6 @@ def _resolve_image_patch_token_id(stage: Any) -> int:
|
||||
return token_id
|
||||
|
||||
|
||||
def _validate_stage_inputs(stage_list, engine_input_source):
|
||||
if not engine_input_source:
|
||||
raise ValueError("engine_input_source cannot be empty")
|
||||
stage_id = engine_input_source[0]
|
||||
if stage_id >= len(stage_list):
|
||||
raise IndexError(f"Invalid stage_id: {stage_id}")
|
||||
stage = stage_list[stage_id]
|
||||
if stage.engine_outputs is None:
|
||||
raise RuntimeError(f"Stage {stage_id} has no outputs yet")
|
||||
return stage, stage.engine_outputs
|
||||
|
||||
|
||||
def _ensure_list(x) -> list[int]:
|
||||
"""Convert ConstantList / tensor-like to plain list."""
|
||||
if hasattr(x, "_x"):
|
||||
@@ -458,20 +446,10 @@ def thinker2imagegen(
|
||||
return [{"prompt": "", "extra": extra}]
|
||||
|
||||
|
||||
def thinker2talker(
|
||||
stage_list: list[Any],
|
||||
engine_input_source: list[int],
|
||||
def _build_talker_inputs(
|
||||
source_outputs: list[Any],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""Build talker stage inputs from thinker stage outputs.
|
||||
|
||||
Extracts the generated text from thinker output and constructs
|
||||
a talker input prompt with the text and any speaker/instruction info
|
||||
from the original request.
|
||||
"""
|
||||
_, source_outputs = _validate_stage_inputs(stage_list, engine_input_source)
|
||||
|
||||
if not isinstance(prompt, list):
|
||||
prompt = [prompt]
|
||||
|
||||
@@ -492,8 +470,6 @@ def thinker2talker(
|
||||
# the talker's spk_head wants a torch tensor.
|
||||
spk_emb = additional_info.get("spk_emb", None)
|
||||
if isinstance(spk_emb, list) and spk_emb and not hasattr(spk_emb[0], "device"):
|
||||
import torch
|
||||
|
||||
spk_emb = torch.tensor(spk_emb, dtype=torch.float32).unsqueeze(0)
|
||||
|
||||
# Omni speech path mirrors upstream `omni_audio_generation`:
|
||||
@@ -535,12 +511,22 @@ def thinker2talker(
|
||||
return talker_inputs
|
||||
|
||||
|
||||
def thinker2talker(
|
||||
source_outputs: list[Any],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
_requires_multimodal_data: bool = False,
|
||||
_streaming_context: Any | None = None,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""Build talker stage inputs from thinker stage outputs."""
|
||||
return _build_talker_inputs(source_outputs, prompt)
|
||||
|
||||
|
||||
# ming_flash_omni is not in ``_OMNI_CONNECTOR_INIT_ARCHS`` or
|
||||
# ``_FULL_PAYLOAD_INPUT_STAGES``, so the worker connector is not
|
||||
# initialised for this arch and the consumer never waits on a connector
|
||||
# payload. Data flows through ``additional_information`` written by
|
||||
# ``thinker2talker_token_only`` (wired as ``sync_process_input_func``
|
||||
# in the pipeline) or the legacy ``thinker2talker`` (wired as
|
||||
# in the pipeline) or ``thinker2talker`` (wired as
|
||||
# ``custom_process_input_func``).
|
||||
|
||||
|
||||
@@ -549,55 +535,8 @@ def thinker2talker_token_only(
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
_requires_multimodal_data: bool = False,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
"""Sync-side builder for the non-async-chunk thinker→talker path.
|
||||
|
||||
Ports the legacy ``thinker2talker`` body to the new stage-input-processor signature
|
||||
(``source_outputs`` instead of ``stage_list, engine_input_source``).
|
||||
Body is otherwise identical: extracts the
|
||||
generated text from each thinker output and packages it with the
|
||||
request's voice/speaker additional_information for the talker.
|
||||
"""
|
||||
if not isinstance(prompt, list):
|
||||
prompt = [prompt]
|
||||
|
||||
talker_inputs: list[OmniTokensPrompt] = []
|
||||
for i, source_output in enumerate(source_outputs):
|
||||
output = source_output.outputs[0]
|
||||
|
||||
generated_text = output.text if hasattr(output, "text") and output.text else ""
|
||||
|
||||
original_prompt = prompt[i] if i < len(prompt) else None
|
||||
additional_info: dict[str, Any] = {}
|
||||
if original_prompt is not None and hasattr(original_prompt, "additional_information"):
|
||||
additional_info = original_prompt.additional_information or {}
|
||||
|
||||
spk_emb = additional_info.get("spk_emb", None)
|
||||
if isinstance(spk_emb, list) and spk_emb and not hasattr(spk_emb[0], "device"):
|
||||
import torch
|
||||
|
||||
spk_emb = torch.tensor(spk_emb, dtype=torch.float32).unsqueeze(0)
|
||||
|
||||
talker_info = {
|
||||
"ming_task": "omni",
|
||||
"text": generated_text,
|
||||
"spk_emb": spk_emb,
|
||||
"voice_name": additional_info.get("voice_name", "DB30"),
|
||||
"prompt_text": additional_info.get("prompt_text", None),
|
||||
"prompt_wav_lat": additional_info.get("prompt_wav_lat", None),
|
||||
"prompt_wav_emb": additional_info.get("prompt_wav_emb", None),
|
||||
"max_text_length": additional_info.get("max_text_length", 50),
|
||||
}
|
||||
|
||||
talker_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0],
|
||||
additional_information=talker_info,
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
)
|
||||
)
|
||||
|
||||
return talker_inputs
|
||||
"""Sync-side builder for the non-async-chunk thinker→talker path."""
|
||||
return _build_talker_inputs(source_outputs, prompt)
|
||||
|
||||
|
||||
thinker2talker_token_only._is_sync_input = True
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from vllm.inputs import TextPrompt
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.data_entry_keys import CodesStruct, MetaStruct, OmniPayloadStruct
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import (
|
||||
KEY_CHUNK_ID,
|
||||
KEY_REQUEST_ID,
|
||||
LATENT_CHUNK_SIZE,
|
||||
LATENT_DIM,
|
||||
LATENT_LEFT_CONTEXT,
|
||||
PATCH_SIZE,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_tts.patch_emission import (
|
||||
MING_STOP_REASON_CODES,
|
||||
MING_STOP_REASON_KEY,
|
||||
)
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
MING_EMIT_PATCH_COUNT_KEY = "ming_emit_patch_count"
|
||||
MING_LATENT_SHAPE_KEY = "ming_latent_shape"
|
||||
MING_ESTIMATED_BYTES_KEY = "ming_estimated_bytes"
|
||||
MING_FINAL_FLUSH_KEY = "ming_final_flush"
|
||||
MING_FINAL_DECODE_STEP_KEY = "ming_final_decode_step"
|
||||
MING_STOP_REASON_BY_CODE = {code: reason for reason, code in MING_STOP_REASON_CODES.items()}
|
||||
|
||||
|
||||
def _extract_last_patch(pooling_output: dict[str, Any] | None) -> torch.Tensor | None:
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
has_patch = pooling_output.get("ming_has_patch")
|
||||
patch = pooling_output.get("ming_latent_patch")
|
||||
if not isinstance(patch, torch.Tensor) or patch.numel() == 0:
|
||||
return None
|
||||
|
||||
if isinstance(has_patch, torch.Tensor) and has_patch.numel() > 0:
|
||||
active = (has_patch.reshape(-1) > 0).nonzero(as_tuple=True)[0]
|
||||
if active.numel() == 0:
|
||||
return None
|
||||
patch = patch[int(active[-1].item())]
|
||||
elif patch.ndim == 3:
|
||||
patch = patch[-1]
|
||||
|
||||
if patch.ndim != 2:
|
||||
raise ValueError(f"Invalid Ming latent patch shape: {tuple(patch.shape)}")
|
||||
return patch.to(torch.float32).cpu()
|
||||
|
||||
|
||||
def _extract_all_patches(pooling_output: dict[str, Any] | None) -> torch.Tensor | None:
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
has_patch = pooling_output.get("ming_has_patch")
|
||||
patch = pooling_output.get("ming_latent_patch")
|
||||
if not isinstance(patch, torch.Tensor) or patch.numel() == 0:
|
||||
return None
|
||||
|
||||
if patch.ndim == 2:
|
||||
patch = patch.unsqueeze(0)
|
||||
if patch.ndim != 3:
|
||||
raise ValueError(f"Invalid Ming latent patch tensor shape: {tuple(patch.shape)}")
|
||||
|
||||
if isinstance(has_patch, torch.Tensor) and has_patch.numel() > 0:
|
||||
active = (has_patch.reshape(-1) > 0).nonzero(as_tuple=True)[0]
|
||||
if active.numel() == 0:
|
||||
return None
|
||||
patch = patch.index_select(0, active.to(device=patch.device))
|
||||
|
||||
if patch.numel() == 0:
|
||||
return None
|
||||
return patch.to(torch.float32).cpu()
|
||||
|
||||
|
||||
def _extract_last_value(pooling_output: dict[str, Any] | None, key: str) -> Any:
|
||||
if not isinstance(pooling_output, dict):
|
||||
return None
|
||||
value = pooling_output.get(key)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
has_patch = pooling_output.get("ming_has_patch")
|
||||
selected_index = -1
|
||||
if isinstance(has_patch, torch.Tensor) and has_patch.numel() > 0:
|
||||
active = (has_patch.reshape(-1) > 0).nonzero(as_tuple=True)[0]
|
||||
if active.numel() == 0:
|
||||
return None
|
||||
selected_index = int(active[-1].item())
|
||||
|
||||
if isinstance(value, torch.Tensor):
|
||||
flat = value.reshape(-1)
|
||||
if flat.numel() == 0:
|
||||
return None
|
||||
return flat[min(selected_index, flat.numel() - 1)].item()
|
||||
if isinstance(value, (list, tuple)):
|
||||
if not value:
|
||||
return None
|
||||
if selected_index < 0:
|
||||
return value[-1]
|
||||
return value[min(selected_index, len(value) - 1)]
|
||||
return value
|
||||
|
||||
|
||||
def _decode_stop_reason(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return MING_STOP_REASON_BY_CODE.get(int(value))
|
||||
|
||||
|
||||
def _get_async_chunk_config(transfer_manager: Any) -> tuple[int, int]:
|
||||
connector = getattr(transfer_manager, "connector", None)
|
||||
raw_cfg = getattr(connector, "config", {}) or {}
|
||||
cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {}
|
||||
if "latent_chunk_size" not in cfg:
|
||||
logger.warning(
|
||||
"Ming async chunk config missing latent_chunk_size, using fallback value %s",
|
||||
LATENT_CHUNK_SIZE,
|
||||
)
|
||||
if "latent_left_context" not in cfg:
|
||||
logger.warning(
|
||||
"Ming async chunk config missing latent_left_context, using fallback value %s",
|
||||
LATENT_LEFT_CONTEXT,
|
||||
)
|
||||
chunk_size = int(cfg.get("latent_chunk_size", LATENT_CHUNK_SIZE))
|
||||
left_context = int(cfg.get("latent_left_context", LATENT_LEFT_CONTEXT))
|
||||
if chunk_size <= 0:
|
||||
raise ValueError(f"Invalid Ming latent_chunk_size={chunk_size}")
|
||||
# Stage-2 VAE caches past_key_values and stream_state by request_id.
|
||||
# Replaying left-context latents would double-feed cached decoder state.
|
||||
if left_context != 0:
|
||||
raise ValueError(
|
||||
"Ming async chunk transport does not support latent_left_context replay. "
|
||||
"Ming boundary continuity is handled by per-request decoder state cache, not "
|
||||
f"latent replay. Got latent_left_context={left_context}."
|
||||
)
|
||||
return chunk_size, left_context
|
||||
|
||||
|
||||
def _build_chunk_observability(
|
||||
latent_patches: torch.Tensor | None,
|
||||
*,
|
||||
final_flush: bool,
|
||||
) -> dict[str, Any]:
|
||||
if latent_patches is None:
|
||||
emit_patch_count = 0
|
||||
latent_shape = None
|
||||
estimated_bytes = 0
|
||||
else:
|
||||
emit_patch_count = int(latent_patches.shape[0])
|
||||
latent_shape = tuple(latent_patches.shape)
|
||||
estimated_bytes = int(latent_patches.numel() * latent_patches.element_size())
|
||||
return {
|
||||
MING_EMIT_PATCH_COUNT_KEY: emit_patch_count,
|
||||
MING_LATENT_SHAPE_KEY: latent_shape,
|
||||
MING_ESTIMATED_BYTES_KEY: estimated_bytes,
|
||||
MING_FINAL_FLUSH_KEY: bool(final_flush),
|
||||
}
|
||||
|
||||
|
||||
def llm2audio_vae_async_chunk(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any] | None,
|
||||
request: Any,
|
||||
is_finished: bool = False,
|
||||
) -> OmniPayloadStruct | None:
|
||||
request_id = request.external_req_id
|
||||
chunk_id = int(transfer_manager.put_req_chunk[request_id])
|
||||
finished = bool(is_finished or request.is_finished())
|
||||
final_decode_step = _extract_last_value(pooling_output, "ming_decode_step")
|
||||
stop_reason = _decode_stop_reason(_extract_last_value(pooling_output, MING_STOP_REASON_KEY))
|
||||
request_payload = transfer_manager.request_payload
|
||||
request_state = request_payload.get(request_id)
|
||||
if not isinstance(request_state, dict) or "_ming_async_state" not in request_state:
|
||||
request_state = {
|
||||
"_ming_async_state": {
|
||||
"seen_patch_len": 0,
|
||||
"terminal_sent": False,
|
||||
}
|
||||
}
|
||||
request_payload[request_id] = request_state
|
||||
state = request_state["_ming_async_state"]
|
||||
if bool(state.get("terminal_sent", False)):
|
||||
return None
|
||||
|
||||
patch = _extract_last_patch(pooling_output)
|
||||
if patch is not None:
|
||||
transfer_manager.code_prompt_token_ids[request_id].append(patch)
|
||||
|
||||
chunk_size, _ = _get_async_chunk_config(transfer_manager)
|
||||
|
||||
patches = transfer_manager.code_prompt_token_ids[request_id]
|
||||
seen_patch_len = int(state.get("seen_patch_len", 0))
|
||||
new_patches = patches[seen_patch_len:] if seen_patch_len < len(patches) else []
|
||||
length = len(new_patches)
|
||||
if length <= 0:
|
||||
if finished and not bool(state.get("terminal_sent", False)):
|
||||
observability = _build_chunk_observability(None, final_flush=True)
|
||||
kv_meta = {
|
||||
KEY_CHUNK_ID: chunk_id,
|
||||
KEY_REQUEST_ID: request_id,
|
||||
**observability,
|
||||
}
|
||||
if final_decode_step is not None:
|
||||
kv_meta[MING_FINAL_DECODE_STEP_KEY] = int(final_decode_step)
|
||||
if stop_reason is not None:
|
||||
kv_meta[MING_STOP_REASON_KEY] = stop_reason
|
||||
state["terminal_sent"] = True
|
||||
return OmniPayloadStruct(
|
||||
codes=CodesStruct(audio=torch.empty(0, dtype=torch.long)),
|
||||
meta=MetaStruct(
|
||||
finished=torch.tensor(True, dtype=torch.bool),
|
||||
stream_finished=torch.tensor(True, dtype=torch.bool),
|
||||
),
|
||||
kv_metadata=kv_meta,
|
||||
)
|
||||
return None
|
||||
|
||||
chunk_length = length % chunk_size
|
||||
if chunk_length != 0 and not finished:
|
||||
return None
|
||||
|
||||
emit_count = chunk_length if chunk_length != 0 else chunk_size
|
||||
emit_patches = list(new_patches[:emit_count])
|
||||
state["seen_patch_len"] = seen_patch_len + len(emit_patches)
|
||||
latent_patches = torch.stack(emit_patches, dim=0)
|
||||
observability = _build_chunk_observability(latent_patches, final_flush=finished)
|
||||
|
||||
kv_meta = {
|
||||
KEY_CHUNK_ID: chunk_id,
|
||||
KEY_REQUEST_ID: request_id,
|
||||
**observability,
|
||||
}
|
||||
if final_decode_step is not None:
|
||||
kv_meta[MING_FINAL_DECODE_STEP_KEY] = int(final_decode_step)
|
||||
if stop_reason is not None:
|
||||
kv_meta[MING_STOP_REASON_KEY] = stop_reason
|
||||
if finished:
|
||||
state["terminal_sent"] = True
|
||||
return OmniPayloadStruct(
|
||||
codes=CodesStruct(audio=torch.tensor([0], dtype=torch.long)),
|
||||
meta=MetaStruct(
|
||||
finished=torch.tensor(finished, dtype=torch.bool),
|
||||
stream_finished=torch.tensor(finished, dtype=torch.bool),
|
||||
),
|
||||
latent=latent_patches,
|
||||
kv_metadata=kv_meta,
|
||||
)
|
||||
|
||||
|
||||
def llm2audio_vae(
|
||||
source_outputs: list[Any],
|
||||
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||
requires_multimodal_data: bool = False,
|
||||
streaming_context: Any = None,
|
||||
) -> list[OmniTokensPrompt]:
|
||||
del prompt, requires_multimodal_data, streaming_context
|
||||
if not source_outputs:
|
||||
raise ValueError("source_outputs cannot be empty")
|
||||
|
||||
outputs = []
|
||||
for stage_output in source_outputs:
|
||||
finished = bool(getattr(stage_output, "finished", True))
|
||||
if not finished:
|
||||
continue
|
||||
output = stage_output.outputs[0]
|
||||
patches = _extract_all_patches(output.multimodal_output)
|
||||
additional_information = {
|
||||
"ming_latent_patches": patches
|
||||
if patches is not None
|
||||
else torch.zeros((0, PATCH_SIZE, LATENT_DIM), dtype=torch.float32),
|
||||
KEY_REQUEST_ID: getattr(stage_output, "request_id", None),
|
||||
"finished": torch.tensor(finished, dtype=torch.bool),
|
||||
}
|
||||
final_decode_step = _extract_last_value(output.multimodal_output, "ming_decode_step")
|
||||
stop_reason = _decode_stop_reason(_extract_last_value(output.multimodal_output, MING_STOP_REASON_KEY))
|
||||
if final_decode_step is not None:
|
||||
additional_information[MING_FINAL_DECODE_STEP_KEY] = int(final_decode_step)
|
||||
if stop_reason is not None:
|
||||
additional_information[MING_STOP_REASON_KEY] = stop_reason
|
||||
outputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=additional_information,
|
||||
)
|
||||
)
|
||||
return outputs
|
||||
Reference in new issue
Block a user