[Feat][Qwen3TTS][Code2wav] triton SnakeBeta and Cuda Graph (#1797)

Signed-off-by: pablo <pablo@agigo.ai>
Signed-off-by: JuanPZuluaga <juanz9312@gmal.com>
Co-authored-by: pablo <pablo@agigo.ai>
Co-authored-by: JuanPZuluaga <juanz9312@gmal.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-03-20 12:11:37 +08:00
1 parent 6ac7ba692d
commit 81a90d24ef
4 files changed
+247 -77

No files matched your search

@@ -270,3 +270,63 @@ def test_deterministic_across_calls(decoder, wrapper):
out1 = wrapper.decode(codes)
out2 = wrapper.decode(codes)
torch.testing.assert_close(out1, out2, atol=0, rtol=0)
# ──────────────────────────────────────────────────────────────────
# 6. compute_capture_sizes
# ──────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"kwargs,expected_in,not_expected",
[
({}, [2, 4, 8, 16, 32, 64, 128, 256, 325], [512]),
(
{"codec_chunk_frames": 33, "codec_left_context_frames": 25},
[2, 4, 8, 16, 32, 33, 58, 64, 128, 256, 325],
[512],
),
(
{"codec_chunk_frames": 25, "codec_left_context_frames": 25},
[2, 4, 8, 16, 25, 32, 50, 64, 128, 256, 325],
[512],
),
],
ids=["default", "streaming_c33", "streaming_c25"],
)
def test_compute_capture_sizes(kwargs, expected_in, not_expected):
"""compute_capture_sizes produces expected sizes capped by max useful size."""
sizes = CUDAGraphDecoderWrapper.compute_capture_sizes(**kwargs)
for val in expected_in:
assert val in sizes, f"{val} not in {sizes}"
for val in not_expected:
assert val not in sizes, f"{val} should not be in {sizes}"
# ──────────────────────────────────────────────────────────────────
# 7. SnakeBeta Triton kernel vs eager equivalence
# ──────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"batch,channels,seq_len",
[(2, 64, 1000), (1, 32, 1), (1, 32, 7), (1, 32, 128), (1, 32, 1024), (1, 32, 4096)],
)
def test_snakebeta_triton_vs_eager(batch, channels, seq_len):
"""Fused Triton SnakeBeta kernel must match eager PyTorch output."""
from vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import (
SnakeBeta,
)
if not SnakeBeta._init_triton():
pytest.skip("Triton not available")
torch.manual_seed(42)
snake = SnakeBeta(in_features=channels).to(DEVICE).eval()
x = torch.randn(batch, channels, seq_len, device=DEVICE)
with torch.no_grad():
eager_out = snake._eager_forward(x)
triton_out = snake._triton_forward(x)
torch.testing.assert_close(triton_out, eager_out, atol=1e-5, rtol=1e-5)
@@ -29,8 +29,6 @@ class CUDAGraphDecoderWrapper:
output = wrapper.decode(codes) # Automatically uses CUDA graph if possible
"""
DEFAULT_CAPTURE_SIZES = [2, 4, 8, 16, 25, 32, 50, 100, 150, 200, 250, 300]
def __init__(
self,
decoder: torch.nn.Module,
@@ -39,7 +37,8 @@ class CUDAGraphDecoderWrapper:
enabled: bool = True,
):
self.decoder = decoder
self.capture_sizes = capture_sizes or self.DEFAULT_CAPTURE_SIZES
self._explicit_sizes = capture_sizes is not None
self.capture_sizes = sorted(capture_sizes) if capture_sizes else []
self.num_quantizers = num_quantizers
self.enabled = enabled
@@ -50,66 +49,82 @@ class CUDAGraphDecoderWrapper:
self._warmed_up = False
self._device = None
@staticmethod
def compute_capture_sizes(
codec_chunk_frames: int = 0,
codec_left_context_frames: int = 0,
decode_chunk_size: int = 300,
decode_left_context: int = 25,
) -> list[int]:
"""Compute capture sizes from chunking config for high graph hit rate."""
sizes: set[int] = set()
# Streaming exact hits
if codec_chunk_frames > 0:
sizes.add(codec_chunk_frames)
if codec_left_context_frames > 0:
sizes.add(codec_chunk_frames + codec_left_context_frames)
# Non-streaming chunked decode: full chunk + last-chunk buckets
non_stream_max = decode_chunk_size + decode_left_context
sizes.add(non_stream_max)
# Power-of-2 buckets covering both streaming IC sizes and non-streaming last-chunk sizes
for p2 in [2, 4, 8, 16, 32, 64, 128, 256]:
if p2 <= non_stream_max:
sizes.add(p2)
return sorted(sizes)
def _get_padded_size(self, actual_size: int) -> int | None:
for size in self.capture_sizes:
if actual_size <= size:
return size
return None
def warmup(self, device: torch.device, dtype: torch.dtype = torch.long):
if device.type != "cuda":
logger.info("CUDA Graph warmup skipped: device %s is not CUDA", device)
return
if not self.enabled:
logger.info("CUDA Graph is disabled, skipping warmup")
return
if self._warmed_up:
logger.warning("CUDA Graph already warmed up, skipping")
def warmup(
self,
device: torch.device,
dtype: torch.dtype = torch.long,
codec_chunk_frames: int = 0,
codec_left_context_frames: int = 0,
):
if device.type != "cuda" or not self.enabled or self._warmed_up:
return
self._device = device
self.decoder.eval()
if not self._explicit_sizes:
self.capture_sizes = self.compute_capture_sizes(
codec_chunk_frames=codec_chunk_frames,
codec_left_context_frames=codec_left_context_frames,
)
logger.info("Starting CUDA Graph warmup for %d sizes: %s", len(self.capture_sizes), self.capture_sizes)
# Warmup runs to ensure CUDA memory is allocated
for size in self.capture_sizes:
dummy_codes = torch.zeros(
1,
self.num_quantizers,
size,
dtype=dtype,
device=device,
)
dummy = torch.zeros(1, self.num_quantizers, size, dtype=dtype, device=device)
with torch.no_grad():
_ = self.decoder(dummy_codes)
_ = self.decoder(dummy)
torch.cuda.synchronize(device)
for size in self.capture_sizes:
try:
self._capture_graph_for_size(size, device, dtype)
self._capture(size, device, dtype)
logger.info(" Captured CUDA Graph for size=%d", size)
except Exception:
logger.warning(" Failed to capture CUDA Graph for size=%d", size, exc_info=True)
logger.warning(" Failed to capture graph for size=%d", size, exc_info=True)
self._warmed_up = True
logger.info("CUDA Graph warmup complete. Captured %d graphs.", len(self.graphs))
def _capture_graph_for_size(self, size: int, device: torch.device, dtype: torch.dtype):
static_input = torch.zeros(
1,
self.num_quantizers,
size,
dtype=dtype,
device=device,
)
logger.info("CUDA Graph warmup complete: %d/%d captured", len(self.graphs), len(self.capture_sizes))
def _capture(self, size: int, device: torch.device, dtype: torch.dtype):
static_input = torch.zeros(1, self.num_quantizers, size, dtype=dtype, device=device)
with torch.no_grad():
_ = self.decoder(static_input)
torch.cuda.synchronize(device)
graph = CUDAGraph()
@@ -122,10 +137,7 @@ class CUDAGraphDecoderWrapper:
self.static_outputs[size] = static_output
def decode(self, codes: torch.Tensor) -> torch.Tensor:
if not self.enabled or not self._warmed_up:
return self.decoder(codes)
if codes.shape[0] != 1:
if not self.enabled or not self._warmed_up or codes.shape[0] != 1:
return self.decoder(codes)
actual_size = codes.shape[-1]
@@ -136,14 +148,10 @@ class CUDAGraphDecoderWrapper:
self.static_inputs[padded_size].zero_()
self.static_inputs[padded_size][:, :, :actual_size] = codes
self.graphs[padded_size].replay()
output = self.static_outputs[padded_size]
total_upsample = self.decoder.total_upsample
actual_output_len = actual_size * total_upsample
return output[..., :actual_output_len].clone()
actual_out_len = actual_size * self.decoder.total_upsample
return self.static_outputs[padded_size][..., :actual_out_len].clone()
def chunked_decode_with_cudagraph(
self,
@@ -107,11 +107,17 @@ class Qwen3TTSCode2Wav(nn.Module):
self._output_sample_rate = out_sr
self._total_upsample = int(decoder.total_upsample)
# Precompute SnakeBeta exp caches (benefits both Triton and eager paths)
if hasattr(decoder, "precompute_snake_caches"):
decoder.precompute_snake_caches()
if hasattr(decoder, "enable_cudagraph"):
device = self._module_device(decoder)
if device.type == "cuda":
try:
capture_sizes = None
chunk_frames = 0
left_frames = 0
model_cfg = getattr(self.vllm_config, "model_config", None)
connector_cfg = getattr(model_cfg, "stage_connector_config", None)
extra_cfg = (
@@ -122,12 +128,12 @@ class Qwen3TTSCode2Wav(nn.Module):
if isinstance(extra_cfg, dict):
chunk_frames = int(extra_cfg.get("codec_chunk_frames") or 0)
left_frames = int(extra_cfg.get("codec_left_context_frames") or 0)
if chunk_frames > 0 and left_frames >= 0:
from .cuda_graph_decoder_wrapper import CUDAGraphDecoderWrapper
steady_window = left_frames + chunk_frames
capture_sizes = sorted({*CUDAGraphDecoderWrapper.DEFAULT_CAPTURE_SIZES, steady_window})
decoder.enable_cudagraph(capture_sizes=capture_sizes, device=device)
decoder.enable_cudagraph(
device=device,
codec_chunk_frames=chunk_frames,
codec_left_context_frames=left_frames,
)
logger.info("Code2Wav decoder CUDA Graph enabled")
except Exception:
logger.warning("Failed to enable CUDA Graph for Code2Wav decoder", exc_info=True)
@@ -265,18 +271,12 @@ class Qwen3TTSCode2Wav(nn.Module):
pass
# Decode directly via decoder.chunked_decode(), staying entirely on GPU.
# For single request: no padding needed, fast path.
# For multiple requests: decode each individually to avoid padding overhead.
# Each request decoded individually with CUDA graph replay at bs=1.
wav_tensors: list[torch.Tensor] = []
if len(valid_codes_qf) == 1:
codes_bqf = valid_codes_qf[0].unsqueeze(0) # [1, Q, F]
for codes_qf in valid_codes_qf:
codes_bqf = codes_qf.unsqueeze(0) # [1, Q, F]
wav = decoder.chunked_decode(codes_bqf) # [1, 1, wav_len]
wav_tensors.append(wav.squeeze(0).squeeze(0)) # [wav_len]
else:
for codes_qf in valid_codes_qf:
codes_bqf = codes_qf.unsqueeze(0) # [1, Q, F]
wav = decoder.chunked_decode(codes_bqf)
wav_tensors.append(wav.squeeze(0).squeeze(0))
audios: list[torch.Tensor] = [empty] * num_req
srs = [sr_tensor] * num_req
@@ -614,32 +614,114 @@ class SnakeBeta(nn.Module):
https://huggingface.co/papers/2006.08195
"""
_triton_kernel = None # None = untried, False = unavailable, callable = ready
_TRITON_MAX_BLOCK_T = 4096 # upper bound for time-axis tile size
@staticmethod
def _init_triton():
"""Load and JIT-compile the fused Triton kernel (once)."""
if SnakeBeta._triton_kernel is not None:
return SnakeBeta._triton_kernel is not False
try:
import triton
import triton.language as tl
except ImportError:
SnakeBeta._triton_kernel = False
return False
@triton.jit
def _kernel( # noqa: N803
x_ptr,
exp_alpha_ptr,
inv_beta_ptr,
out_ptr,
stride_b,
stride_c,
t_len,
block_t: tl.constexpr,
):
"""Fused SnakeBeta using precomputed exp(α) and 1/(exp(β)+ε)."""
bid = tl.program_id(0)
cid = tl.program_id(1)
t_off = tl.program_id(2) * block_t + tl.arange(0, block_t)
mask = t_off < t_len
x = tl.load(x_ptr + bid * stride_b + cid * stride_c + t_off, mask=mask, other=0.0)
ea = tl.load(exp_alpha_ptr + cid)
ib = tl.load(inv_beta_ptr + cid)
sin_val = tl.sin(x * ea)
result = x + ib * sin_val * sin_val
tl.store(out_ptr + bid * stride_b + cid * stride_c + t_off, result, mask=mask)
SnakeBeta._triton_kernel = _kernel
return True
def __init__(self, in_features, alpha=1.0):
super().__init__()
self.in_features = in_features
# initialize alpha
self.alpha = Parameter(torch.zeros(in_features) * alpha)
self.beta = Parameter(torch.zeros(in_features) * alpha)
self.no_div_by_zero = 0.000000001
def forward(self, hidden_states):
"""
Forward pass of the function.
Applies the function to the input elementwise.
SnakeBeta ∶= x + 1/b * sin^2 (xa)
"""
alpha = self.alpha.unsqueeze(0).unsqueeze(-1) # line up with x to [B, C, T]
beta = self.beta.unsqueeze(0).unsqueeze(-1)
alpha = torch.exp(alpha)
beta = torch.exp(beta)
hidden_states = hidden_states + (1.0 / (beta + self.no_div_by_zero)) * torch.pow(
torch.sin(hidden_states * alpha), 2
)
# Precomputed buffers (populated by precompute_exp_cache)
self.register_buffer("_exp_alpha", None, persistent=False)
self.register_buffer("_inv_beta", None, persistent=False)
def precompute_exp_cache(self):
"""Materialize exp(alpha) and 1/(exp(beta)+eps) as frozen buffers."""
with torch.no_grad():
self._exp_alpha = torch.exp(self.alpha).contiguous()
self._inv_beta = (1.0 / (torch.exp(self.beta) + self.no_div_by_zero)).contiguous()
@property
def _cached(self):
return self._exp_alpha is not None
def forward(self, hidden_states):
"""SnakeBeta := x + 1/b * sin^2(x*a)"""
if hidden_states.is_cuda and not torch.is_grad_enabled() and self._init_triton():
try:
return self._triton_forward(hidden_states)
except Exception:
logger.warning("Triton SnakeBeta failed, falling back to eager", exc_info=True)
SnakeBeta._triton_kernel = False
return self._eager_forward(hidden_states)
def _eager_forward(self, hidden_states):
if self._cached:
exp_alpha = self._exp_alpha.unsqueeze(0).unsqueeze(-1)
inv_beta = self._inv_beta.unsqueeze(0).unsqueeze(-1)
else:
exp_alpha = torch.exp(self.alpha).unsqueeze(0).unsqueeze(-1)
inv_beta = (1.0 / (torch.exp(self.beta) + self.no_div_by_zero)).unsqueeze(0).unsqueeze(-1)
hidden_states = hidden_states + inv_beta * torch.pow(torch.sin(hidden_states * exp_alpha), 2)
return hidden_states
def _triton_forward(self, x):
import triton
if not self._cached:
self.precompute_exp_cache()
x = x.contiguous()
B, C, T = x.shape
out = torch.empty_like(x)
block_t = min(triton.next_power_of_2(T), self._TRITON_MAX_BLOCK_T)
self._triton_kernel[(B, C, triton.cdiv(T, block_t))](
x,
self._exp_alpha,
self._inv_beta,
out,
x.stride(0),
x.stride(1),
t_len=T,
block_t=block_t,
)
return out
class Qwen3TTSTokenizerV2DecoderDecoderResidualUnit(nn.Module):
def __init__(self, dim: int = 16, dilation: int = 1):
@@ -876,10 +958,22 @@ class Qwen3TTSTokenizerV2Decoder(Qwen3TTSTokenizerV2DecoderPreTrainedModel):
self._cudagraph_enabled = False
self._cudagraph_wrapper = None
def precompute_snake_caches(self):
"""Precompute exp(alpha) and 1/(exp(beta)+eps) for all SnakeBeta modules."""
count = 0
for module in self.modules():
if isinstance(module, SnakeBeta):
module.precompute_exp_cache()
count += 1
if count > 0:
logger.info("Precomputed exp caches for %d SnakeBeta activations", count)
def enable_cudagraph(
self,
capture_sizes: list[int] | None = None,
device: torch.device | None = None,
codec_chunk_frames: int = 0,
codec_left_context_frames: int = 0,
):
from ..cuda_graph_decoder_wrapper import CUDAGraphDecoderWrapper
@@ -888,16 +982,24 @@ class Qwen3TTSTokenizerV2Decoder(Qwen3TTSTokenizerV2DecoderPreTrainedModel):
if device.type != "cuda":
logger.warning("Cannot enable CUDA Graph: decoder is not on a CUDA device (got %s)", device)
return
self._cudagraph_wrapper = CUDAGraphDecoderWrapper(
decoder=self,
capture_sizes=capture_sizes,
num_quantizers=self.config.num_quantizers,
enabled=True,
)
self._cudagraph_wrapper.warmup(device, dtype=torch.long)
self._cudagraph_wrapper.warmup(
device,
dtype=torch.long,
codec_chunk_frames=codec_chunk_frames,
codec_left_context_frames=codec_left_context_frames,
)
self._cudagraph_enabled = True
sizes = self._cudagraph_wrapper.capture_sizes
logger.info("CUDA Graph enabled for decoder with sizes: %s", sizes)
logger.info(
"CUDA Graph enabled for decoder: seq_lens=%s",
self._cudagraph_wrapper.capture_sizes,
)
def disable_cudagraph(self):
self._cudagraph_enabled = False