[Ming] Cleanup Ming-family shared modules (#6119)

Signed-off-by: Yuanheng <jonathan.zhaoyh@gmail.com>
Signed-off-by: Yuanheng Zhao <jonathan.zhaoyh@gmail.com>
This commit is contained in:
Yuanheng Zhao authored and GitHub committed 2026-08-19 14:13:40 +08:00
1 parent adf6d0c1ea
commit a630b0c2ec
22 files changed
+380 -192

No files matched your search

@@ -1,2 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Components shared by the Ming-family model packages.
Naming: The flow-matching pieces all use the cfm_ prefix,
because "flow matching", "fm" and "cfm" were three names for one concept here:
* dit_blocks - DiT building blocks (DiTBlock, FinalLayer, CondEmbedder)
* cfm_dit - the DiT used as the CFM velocity model
* cfm_solver - timestep schedules and the ODE/SDE integrator
* cfm_head - inference-only CFM wrapper preserving cfm.model.* names
* cfm_cudagraph - batch-1 CUDA-graph executor for the sampling loop
"""
@@ -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_blocks 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,154 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CUDAGraph-accelerated CFM (flow-matching) diffusion head for Ming-TTS.
The per-AR-step flow head runs ``steps`` ODE iterations, each a tiny DiT forward
(history+patch tokens, CFG cond+null batched). In eager mode these are dozens of
micro kernels per step and the wall time is dominated by kernel-launch / Python
overhead, not GPU math. Capturing the whole sampling loop + Aggregator + stop
head into one CUDA graph removes that overhead.
Adapted from adaspeech/ming_flash ``CFMGraphExecutor``. Captured for batch=1
(Ming Stage-0 runs ``max_num_seqs: 1``); other batch sizes fall back to eager.
"""
from __future__ import annotations
import torch
import torch.nn as nn
from vllm_omni.model_executor.models.common.ming.cfm_solver import build_timesteps
class CFMSampler(nn.Module):
"""Unrolled CFM ODE solver, graph-capturable (no ``torch.randn``/``.item()``).
All randomness (``y0``, ``sde_rnd``) and the timestep schedule ``t`` are
passed in as float32 tensors so the loop is deterministic during replay and
the integration matches the eager float32 path. Equivalent to eager
``Solver.integrate`` for ``temperature==0`` (the TTS default).
"""
def __init__(self, dit_model: nn.Module, steps: int = 10):
super().__init__()
self.model = dit_model
self.steps = steps
@torch.no_grad()
def forward(self, llm_cond, lat_cond, y0, t, sde_args, sde_rnd):
# y0 / t / sde_rnd / sde_args are float32; integrate in float32 and cast
# only the DiT inputs to the model dtype, mirroring eager FlowLoss.sample.
model_dtype = llm_cond.dtype
for step in range(self.steps):
dt = t[step + 1] - t[step]
pred_cfg = self.model.forward_with_cfg(
x=y0.to(model_dtype),
t=t[step].to(model_dtype),
c=llm_cond,
latent_history=lat_cond,
cfg_scale=sde_args[0],
patch_size=y0.shape[1],
)
pred, null_pred = torch.chunk(pred_cfg, 2, dim=0)
flow = (pred + (pred - null_pred) * sde_args[0]).float()
y0 = y0 + flow * dt
if step < self.steps - 1:
y0 = y0 + sde_args[1] * (sde_args[2] ** 0.5) * (dt.abs() ** 0.5) * sde_rnd[step]
return y0
class CFMGraphExecutor:
"""Capture CFM sampling + Aggregator + stop head into one CUDA graph.
First ``execute`` captures the graph; later calls copy inputs into the
static placeholders and ``replay()``. Runtime controls (cfg/sigma/temp) are
fed via the ``sde_args`` placeholder, so they are tunable without recapture.
"""
def __init__(
self,
cfm_sampler: CFMSampler,
aggregator: nn.Module,
stop_head: nn.Module,
*,
patch_size: int,
latent_dim: int,
steps: int = 10,
):
self.cfm_sampler = cfm_sampler
self.aggregator = aggregator
self.stop_head = stop_head
self.patch_size = int(patch_size)
self.latent_dim = int(latent_dim)
self.steps = int(steps)
self.initialized = False
self.graph: torch.cuda.CUDAGraph | None = None
def _timesteps(self, device):
# Raw EPSS schedule + sway in float32, identical to eager build_timesteps.
return build_timesteps(self.steps, device=device, dtype=torch.float32, use_epss=True, sway_sampling_coef=-1.0)
@torch.no_grad()
def execute(self, llm_cond, his_lat, cfg_strength, sigma, temperature):
bsz = llm_cond.shape[0]
device = llm_cond.device
# ODE state / noise / timesteps are float32 to match the eager path.
y0 = torch.randn((bsz, self.patch_size, self.latent_dim), device=device, dtype=torch.float32)
t = self._timesteps(device)
sde_rnd = torch.randn((self.steps, bsz, self.patch_size, self.latent_dim), device=device, dtype=torch.float32)
if not self.initialized:
self._initialize_graph(llm_cond, his_lat, y0, t, sde_rnd)
self._llm_cond_ph.copy_(llm_cond)
self._his_lat_ph.copy_(his_lat)
self._y0_ph.copy_(y0)
self._t_ph.copy_(t)
self._sde_args_ph[0] = float(cfg_strength)
self._sde_args_ph[1] = float(sigma)
self._sde_args_ph[2] = float(temperature)
self._sde_rnd_ph.copy_(sde_rnd)
self.graph.replay()
return self._gen_lat_ph.clone(), self._embeds_ph.clone(), self._stop_ph.clone()
def _initialize_graph(self, llm_cond, his_lat, y0, t, sde_rnd):
model_dtype = llm_cond.dtype
self._llm_cond_ph = torch.empty_like(llm_cond)
self._his_lat_ph = torch.empty_like(his_lat)
self._y0_ph = torch.empty_like(y0) # float32
self._t_ph = torch.empty_like(t) # float32
self._sde_args_ph = torch.empty(3, device=llm_cond.device, dtype=torch.float32)
self._sde_rnd_ph = torch.empty_like(sde_rnd) # float32
self._llm_cond_ph.copy_(llm_cond)
self._his_lat_ph.copy_(his_lat)
self._y0_ph.copy_(y0)
self._t_ph.copy_(t)
self._sde_args_ph[0], self._sde_args_ph[1], self._sde_args_ph[2] = 2.0, 0.25, 0.0
self._sde_rnd_ph.copy_(sde_rnd)
def _run():
# Solver runs in float32; cast the final latent to the model dtype
# for the Aggregator / stop head (matches eager ``.to(dtype)``).
gen_lat = self.cfm_sampler(
self._llm_cond_ph, self._his_lat_ph, self._y0_ph, self._t_ph, self._sde_args_ph, self._sde_rnd_ph
).to(model_dtype)
embeds = self.aggregator(gen_lat)
stop = self.stop_head(self._llm_cond_ph[:, -1, :]).softmax(dim=-1)
return gen_lat, embeds, stop
# Warm up on a side stream to stabilize allocations before capture.
warmup = torch.cuda.Stream()
warmup.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(warmup):
for _ in range(3):
_run()
torch.cuda.current_stream().wait_stream(warmup)
self.graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(self.graph):
self._gen_lat_ph, self._embeds_ph, self._stop_ph = _run()
self.initialized = True
@@ -0,0 +1,103 @@
# 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_blocks 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):
# Always run the classifier-free-guidance path (cond + uncond). TTS
# inference always uses cfg>1, and branching on ``cfg_scale`` is avoided
# because under CUDA-graph capture it is a CUDA tensor — ``bool(cfg_scale)``
# would force a device sync and break capture. ``expand`` (not ``repeat``)
# avoids a fresh allocation each step.
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.reshape(1)
t = t.expand(x.shape[0])
model_out = self.forward(x, t, c, latent_history)
return model_out[:, -patch_size:, :]
@@ -0,0 +1,62 @@
# 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.cfm_dit import DiT
from vllm_omni.model_executor.models.common.ming.cfm_solver import Solver, build_timesteps
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)
@@ -3,7 +3,21 @@
import torch
from .dit import get_epss_timesteps
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)
def apply_sway_sampling(t, sway_sampling_coef):
@@ -216,19 +216,3 @@ class CondEmbedder(nn.Module):
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,56 @@
# 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
"""CAM++ speaker-embedding extraction, shared by the Ming model packages."""
from __future__ import annotations
import os
import torch
import torchaudio
from vllm.multimodal.media.audio import load_audio
from vllm_omni.model_executor.model_loader.weight_utils import resolve_model_to_local_path
from vllm_omni.model_executor.models.common.ming.spk_embedding import SpkembExtractor
class SpeakerEmbeddingExtractor:
"""CAM++ ONNX speaker embeddings, resampled to the extractor's rate."""
def __init__(self, model, target_sr=16000, *, allow_download: bool = False):
local_model_path = resolve_model_to_local_path(
model,
allow_download=allow_download,
allow_patterns=["campplus.onnx"],
)
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]
__all__ = ["SpeakerEmbeddingExtractor"]
@@ -0,0 +1,44 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2025 The vLLM-Omni team.
# Copyright (c) Ant Group. All rights reserved.
# Ported from:
# https://github.com/inclusionAI/Ming/blob/e58533db227031990c5a6864dcf5f08fb53ed0d2/modeling_bailing_talker.py
from __future__ import annotations
import torch
class SpkembExtractor:
"""CAMPPlus ONNX-based speaker embedding extractor (runs on CPU)."""
def __init__(self, campplus_model: str, target_sr: int = 16000):
import onnxruntime
import torchaudio.compliance.kaldi as kaldi
self.kaldi = kaldi
option = onnxruntime.SessionOptions()
option.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
option.intra_op_num_threads = 2
self.campplus_session = onnxruntime.InferenceSession(
campplus_model, sess_options=option, providers=["CPUExecutionProvider"]
)
self.target_sr = target_sr
def _extract_spk_embedding(self, speech):
feat = self.kaldi.fbank(speech, num_mel_bins=80, dither=0, sample_frequency=16000)
feat = feat - feat.mean(dim=0, keepdim=True)
embedding = (
self.campplus_session.run(
None,
{self.campplus_session.get_inputs()[0].name: feat.unsqueeze(dim=0).cpu().numpy()},
)[0]
.flatten()
.tolist()
)
embedding = torch.tensor([embedding])
return embedding
def __call__(self, waveform, **kwargs) -> torch.Tensor | None:
spk_emb = self._extract_spk_embedding(waveform)
return spk_emb