Files
wehub-resource-sync ba4be087d5
Create PR to main with cherry-pick from release / cherry-pick (push) Failing after 0s
CICD NeMo / pre-flight (push) Failing after 0s
CICD NeMo / configure (push) Has been skipped
Build, validate, and release Neural Modules / pre-flight (push) Failing after 1s
CICD NeMo / code-linting (push) Has been skipped
Build, validate, and release Neural Modules / release (push) Has been skipped
Build, validate, and release Neural Modules / release-summary (push) Has been cancelled
CICD NeMo / cicd-test-container-build (push) Has been cancelled
CICD NeMo / cicd-import-tests (push) Has been cancelled
CICD NeMo / L0_Setup_Test_Data_And_Models (push) Has been cancelled
CICD NeMo / cicd-main-unit-tests (push) Has been cancelled
CICD NeMo / cicd-main-speech (push) Has been cancelled
CICD NeMo / Nemo_CICD_Test (push) Has been cancelled
CICD NeMo / Coverage (e2e) (push) Has been cancelled
CICD NeMo / Coverage (unit-test) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
CICD NeMo / cicd-wait-in-queue (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:28:58 +08:00

1584 lines
65 KiB
Python

# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import math
import os
from dataclasses import dataclass, fields
from typing import Any
import torch
import transformers
from omegaconf import DictConfig, OmegaConf
from torch import Tensor, nn
from torch.nn import functional as F
from transformers import AutoConfig, AutoModel, AutoModelForTextEncoding, Cache
from transformers.generation.logits_process import TopKLogitsWarper, TopPLogitsWarper
from nemo.collections.common.tokenizers import AutoTokenizer
from nemo.collections.speechlm2.parts.precision import fp32_precision
from nemo.collections.speechlm2.parts.pretrained import set_model_dict_for_partial_init
from nemo.utils import logging
# ==============================================================================
# MLP module and Norm
# ==============================================================================
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.zeros(dim))
def _norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
output = self._norm(x.float())
# Llama does x.to(float16) * w whilst Gemma3 is (x * w).to(float16)
output = output * (1.0 + self.weight.float())
return output.type_as(x)
def extra_repr(self):
return f"{tuple(self.weight.shape)}, eps={self.eps}"
class MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
):
super().__init__()
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
self.act_fn = nn.GELU(approximate="tanh")
def forward(self, x: Tensor) -> Tensor:
down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
return down_proj
class MLPLayer(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
eps: float = 1e-6,
):
super().__init__()
self.pre_norm = RMSNorm(hidden_size, eps=eps)
self.mlp = MLP(hidden_size, intermediate_size)
self.post_norm = RMSNorm(hidden_size, eps=eps)
def forward(self, x: Tensor) -> Tensor:
y = self.pre_norm(x)
y = self.mlp(y)
y = self.post_norm(y)
x = x + y
return x
# ==============================================================================
# Core Mathematical and Masking Functions
# ==============================================================================
def batch_matmul(x: Tensor, w: Tensor, y: Tensor, *args, **kwargs) -> Tensor:
"""
Performs a batched matrix multiplication using PyTorch's native functions.
Args:
x (Tensor): The input tensor of shape `[batch_size, d_in]`.
w (Tensor): The weight tensor of shape `[num_weights, d_out, d_in]`.
y (Tensor): The index tensor of shape `[batch_size]`.
Returns:
Tensor: The result of the multiplication, shape `[batch_size, d_out]`.
"""
# w[y] gathers the weight matrices for each item in the batch.
# x.unsqueeze(2) reshapes x to [batch_size, d_in, 1] for bmm.
# The result is squeezed to remove the trailing dimension of size 1.
return torch.bmm(w[y], x.unsqueeze(2)).squeeze(2)
def gumbel_like(tensor: Tensor, eps: float = 1e-8) -> Tensor:
"""
Generates a tensor of Gumbel noise with the same shape as the input tensor.
This is used for the Gumbel-Max trick, a technique to sample from a categorical
distribution in a differentiable way (using a straight-through estimator).
Args:
tensor (torch.Tensor): The input tensor to match the shape of.
eps (float): A small epsilon value for numerical stability.
Returns:
torch.Tensor: A tensor containing Gumbel noise.
"""
# Sample from a uniform distribution
u = torch.rand_like(tensor)
# Apply the inverse CDF of the Gumbel distribution
return -torch.log(-torch.log(u + eps) + eps)
def sequence_mask(lengths: Tensor, max_length: Tensor | int | None = None) -> Tensor:
"""
Creates a boolean mask from a 1D tensor of sequence lengths.
This function is useful for masking out padding in sequences. Given a tensor
of lengths, it produces a 2D boolean tensor where `mask[i, j]` is `True` if
`j < lengths[i]` and `False` otherwise.
Args:
lengths (Long Tensor): A 1D tensor of integer lengths. Shape: `[batch_size]`.
max_length (Long Tensor | int | None, optional): The maximum length of the mask. If None,
it is inferred from the maximum value
in `lengths`. Defaults to None.
Returns:
Tensor: The boolean mask. Shape: `[batch_size, max_length]`.
"""
if max_length is None:
max_length = lengths.max()
# Create a range tensor from 0 to max_length - 1
x = torch.arange(max_length, dtype=lengths.dtype, device=lengths.device) # type: ignore[arg-type]
# Compare each length with the range tensor to create the mask via broadcasting
return x.unsqueeze(0) < lengths.unsqueeze(1)
def get_masking_rate(rate: Tensor, exponent: float = 3.0) -> Tensor:
"""
Converts a desired token keep rate to a masking rate using a power function.
This function is part of a scheduling strategy for masking, where the effective
masking rate changes non-linearly with the desired keep rate. This function is
its own inverse.
Args:
rate (Tensor): The desired rate of tokens to keep (0 to 1).
exponent (float, optional): The exponent for the transformation. Defaults to 3.0.
Returns:
Tensor: The corresponding masking rate.
"""
return (1 - rate.pow(exponent)).pow(1 / exponent)
# Alias the function for clarity in the inverse context
get_rate = get_masking_rate
def get_mask(
code_mask: Tensor,
num_masking: Tensor,
unmasking: bool = False,
validate: bool = False,
) -> Tensor:
"""
Adjusts a boolean mask by masking or unmasking tokens from the end.
This function operates on a `code_mask` where `True` values represent valid
tokens and are assumed to be contiguous at the start of the sequence. It
calculates a new mask by decreasing (masking) or increasing (unmasking)
the number of `True` values.
Args:
code_mask (Tensor): The input boolean mask. Shape: `[..., depth]`.
num_masking (Tensor): The number of tokens to mask or unmask.
Shape matching `code_mask`'s batch dimensions.
unmasking (bool, optional): If `True`, increases the number of valid
tokens (unmasking). Defaults to `False`.
validate (bool, optional): If `True`, asserts that the input `code_mask`
is contiguous. This adds a slight overhead and
is mainly for debugging. Defaults to `False`.
Returns:
Tensor: A new boolean mask with the adjusted length of valid tokens.
Shape is identical to `code_mask`.
"""
depth = code_mask.size(-1)
num_valid = code_mask.sum(dim=-1, dtype=torch.long)
if validate:
# Reconstruct the expected contiguous mask and assert equality.
expected_mask = sequence_mask(num_valid.view(-1), depth).view_as(code_mask)
assert torch.equal(
code_mask, expected_mask
), "Input `code_mask` must have contiguous `True` values at the beginning."
# Calculate the target number of valid tokens.
if not unmasking:
# Masking: reduce the number of valid tokens, ensuring it's not negative.
num_to_keep = (num_valid - num_masking).clamp_min(0)
else:
# Unmasking: increase the number of valid tokens, capped by total depth.
num_to_keep = (num_valid + num_masking).clamp_max(depth)
# Generate the new mask using the final number of tokens to keep.
return sequence_mask(num_to_keep.view(-1), depth).view_as(code_mask)
# ==============================================================================
# Model and Vocabulary Utilities
# ==============================================================================
@dataclass
class RVQEARTTSOutput:
"""
Output type for the RVQEARTTSModel, providing a structured way to return model outputs.
This class allows accessing outputs by attribute, key, or index.
"""
loss: Tensor | None = None
lm_loss: Tensor | None = None
c_loss: Tensor | None = None
k_loss: Tensor | None = None
hidden_states: Tensor | None = None
past_key_values: Tensor | None = None
audio_prompt_lantent: Tensor | None = None
codes: Tensor | None = None
lm_logits: Tensor | None = None
eos_flag: Tensor | None = None
def __getitem__(self, item: str | int):
"""Allows for accessing attributes by key or index."""
if isinstance(item, str):
return getattr(self, item)
else:
# Access fields in the order they are defined in the dataclass
return getattr(self, fields(self)[item].name)
def find_and_delete_module(parent_module: nn.Module, target_module: nn.Module, parent_name: str) -> str | None:
"""
Recursively searches for a specific module instance and deletes it from its parent.
This is useful for dynamically modifying a model's architecture, such as replacing
an existing embedding layer with a custom one.
Args:
parent_module (nn.Module): The module to search within.
target_module (nn.Module): The exact module instance to find and delete.
parent_name (str): The initial name of the parent module for constructing the path.
Returns:
str | None: The full dotted name of the deleted attribute if found, otherwise None.
"""
# Iterate over all direct children of the parent module
for name, module in parent_module.named_children():
# Use the 'is' operator to check for object identity, not just value equality
if module is target_module:
# If found, delete the attribute from the parent and return its name
delattr(parent_module, name)
return f"{parent_name}.{name}"
# If not found, recurse into the child module
found_path = find_and_delete_module(module, target_module, parent_name=f"{parent_name}.{name}")
if found_path:
return found_path
return None
def build_vocabs(
tokenizer: AutoTokenizer, vocab_dir: str | None = None
) -> tuple[dict[int, tuple[int, ...]], dict[str, int], int]:
"""
Builds or loads a character-level vocabulary derived from a subword tokenizer.
This function creates a mapping from each subword in a pretrained tokenizer to a
sequence of character IDs. It follows a modern practice of using a directory
to save and load vocabulary files, making the process more robust and extensible.
The primary source of truth is the `char_vocab.json` file. If it exists, it's
loaded. Otherwise, it's created from the pretrained tokenizer and saved.
Args:
tokenizer (AutoTokenizer): The pretrained Hugging Face tokenizer class.
vocab_dir (str | None, optional): The directory to save or load the character
vocabulary from. Defaults to None.
Returns:
tuple[dict[int, tuple[int, ...]], dict[str, int], int]: A tuple containing:
- A mapping from subword IDs to tuples of character IDs.
- The character-to-ID vocabulary dictionary.
- The ID for the subword padding token.
"""
def _build_char_vocab() -> dict[str, int]:
# Find all single-character tokens in the original tokenizer's vocabulary
single_chars = {
subword: subword_id for subword, subword_id in tokenizer.tokenizer.vocab.items() if len(subword) == 1
}
# Create a new, dense character vocabulary sorted by the original token ID
sorted_chars = sorted(single_chars.keys(), key=lambda k: single_chars[k])
char_vocab = {char: i for i, char in enumerate(sorted_chars)}
return char_vocab
# 1. Load or build the character vocabulary
if vocab_dir:
from filelock import FileLock
char_vocab_file = os.path.join(vocab_dir, "char_vocab.json")
os.makedirs(vocab_dir, exist_ok=True)
with FileLock(char_vocab_file + ".lock", timeout=60):
if not os.path.exists(char_vocab_file):
char_vocab = _build_char_vocab()
logging.info(f"Saving character vocabulary to {char_vocab_file}")
with open(char_vocab_file, "w", encoding="utf-8") as f:
json.dump(char_vocab, f, ensure_ascii=False, indent=2)
# All processes can now safely load the file.
logging.info(f"Loading character vocabulary from {char_vocab_file}")
with open(char_vocab_file, encoding="utf-8") as f:
char_vocab = json.load(f)
else:
# No cache directory provided, build in memory.
logging.info("Building character vocabulary from tokenizer.")
char_vocab = _build_char_vocab()
# 2. Reconstruct the subword-to-character mapping on the fly
subword_id_to_char_ids = {
subword_id: tuple(char_vocab[char] for char in subword if char in char_vocab)
for subword, subword_id in tokenizer.tokenizer.vocab.items()
}
# Filter out subwords that contain characters not in our character vocabulary
subword_id_to_char_ids = {k: v for k, v in subword_id_to_char_ids.items() if v}
# 3. Define a padding index for subwords
subword_padding_idx = len(tokenizer.vocab)
# The padding subword maps to a new character padding ID
subword_id_to_char_ids[subword_padding_idx] = (len(char_vocab),)
return subword_id_to_char_ids, char_vocab, subword_padding_idx
@torch._dynamo.disable
def depthsum_encoding_step(
embs: Tensor,
r: Tensor,
code: Tensor,
depth_str: int = 0,
k: int = 72,
) -> Tensor:
for i in range(depth_str, depth_str + k):
idx_sel = (
embs[i].pow(2).sum(-1) # [g?, v]
- 2
* (r.unsqueeze(-2) @ embs[i].transpose(-1, -2)).squeeze(-2) # [b, ?, g?, h] , [g?, h, v] -> [b, ?, g?, v]
).argmin(-1)
emb_i = F.embedding(idx_sel, embs[i])
r = r - emb_i
code[..., i] = idx_sel
return code
class MoGHead(nn.Module):
"""
A Mixture of Gaussians (MoG) prediction head.
This module takes a hidden state and predicts the parameters for a mixture of
Gaussian distributions. It's suitable for modeling continuous, multi-modal data.
Args:
hidden_size (int): The dimensionality of the input hidden state.
intermediate_size (int): The dimensionality of the MLP layers.
out_size (int): The dimensionality of the output vectors (the mean of each Gaussian).
num_layers (int): The number of MLP layers in the stack.
num_predictions (int): The number of Gaussian components in the mixture.
low_rank (int | None): The dimensionality used for compressing the hidden states.
min_log_std (float): The minimum value for the logarithm of the standard deviation.
eps (float): A small epsilon value for the RMSNorm layers.
"""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
out_size: int,
num_layers: int,
num_predictions: int,
low_rank: int | None = 64,
min_log_std: float = -4.0,
eps: float = 1e-6,
):
super().__init__()
self.out_size = out_size
self.low_rank = low_rank
self.num_predictions = num_predictions
self.min_log_std = min_log_std
self.mlp_stack = nn.Sequential(
*[MLPLayer(hidden_size, intermediate_size, eps=eps) for _ in range(num_layers)],
RMSNorm(hidden_size, eps=eps),
)
if low_rank is None:
self.proj_logits = nn.Linear(hidden_size, num_predictions, bias=False) # Predicts mixture weights
self.proj_mus = nn.Linear(hidden_size, num_predictions * out_size, bias=False) # Predicts means
self.proj_logs = nn.Linear(hidden_size, 1, bias=False) # Predicts log standard deviations
else:
assert low_rank < out_size
self.proj_logits = nn.Linear(hidden_size, num_predictions, bias=False) # Predicts mixture weights
self.proj_mus = nn.Linear(hidden_size, num_predictions * low_rank, bias=False) # Predicts means
self.proj_logs = nn.Linear(hidden_size, 1, bias=False) # Predicts log standard deviations
self.proj_else = nn.Linear(hidden_size, out_size, bias=False)
self.low_mat = nn.Parameter(torch.randn(num_predictions, out_size, low_rank) * (low_rank**-0.5))
def infer(self, x: Tensor, guidance_scale: float = 0.0, top_p_or_k: float | int = 1.0) -> tuple[Tensor, Tensor]:
"""
Performs inference by sampling from the predicted mixture distribution.
Args:
x (Tensor): The input hidden state.
guidance_scale (float): The weight for classifier-free guidance.
top_p_or_k (float | int): The value for top-p (nucleus) or top-k sampling of the mixture components.
Returns:
tuple[Tensor, Tensor]: A tuple containing the mean of the chosen component,
and the log standard deviations.
"""
b, t, _ = x.size()
n, d = self.num_predictions, self.low_rank or self.out_size
x = self.mlp_stack(x)
if guidance_scale > 0:
b //= 2
x_cond, x_uncond = x.chunk(2, dim=0)
x = x_cond + guidance_scale * (x_cond - x_uncond)
logits = self.proj_logits(x)
# Apply top-p or top-k filtering to the mixture logits
if top_p_or_k is not None:
logits = (
TopPLogitsWarper(top_p_or_k)(
None,
logits.view(-1, n),
).view_as(logits)
if isinstance(top_p_or_k, float)
else TopKLogitsWarper(top_p_or_k)(
None,
logits.view(-1, n),
).view_as(logits)
)
logits = logits.to(x.dtype)
# Sample a mixture component using the Gumbel-Max trick
with fp32_precision():
mixture_indices = (F.log_softmax(logits, dim=-1) + gumbel_like(logits)).argmax(-1)
# Select the mean corresponding to the sampled component
mu = batch_matmul(
x.view(b * t, -1),
self.proj_mus.weight.detach().view(n, d, -1),
mixture_indices.view(b * t),
).view(b, t, d)
if self.proj_mus.bias is not None:
mu += self.proj_mus.bias.detach().view(n, d)[mixture_indices]
if self.low_rank:
assert math.log2(d).is_integer() and math.log2(self.out_size).is_integer()
mu = batch_matmul(
mu.view(b * t, -1),
self.low_mat.detach().view(n, self.out_size, -1),
mixture_indices.view(b * t),
BLOCK_SIZE_DIN=d,
BLOCK_SIZE_DOUT=self.out_size,
).view(b, t, self.out_size)
mu_res = self.proj_else(x)
else:
mu_res = torch.zeros((b, t, d), device=x.device, dtype=x.dtype)
logs = self.proj_logs(x).clamp_min(self.min_log_std)
return mu * torch.exp(logs.float()).to(logs.dtype) + mu_res, logs
def forward(self, x: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor]:
"""
Performs a forward pass for training.
Args:
x (Tensor): The input hidden state.
Returns:
tuple[Tensor, Tensor, Tensor]: A tuple containing the mixture logits,
the means for all components, and the
log standard deviations.
"""
b, t, _ = x.size()
d = self.low_rank or self.out_size
x = self.mlp_stack(x)
logits = self.proj_logits(x)
mus = self.proj_mus(x).view(b, t, self.num_predictions, d)
logs = self.proj_logs(x).clamp_min(self.min_log_std)
if self.low_rank:
mu_res = self.proj_else(x)
else:
mu_res = torch.zeros((b, t, d), device=x.device)
return logits, mus, mu_res, logs
def dist(self, mus: Tensor, mu: Tensor) -> Tensor:
"""
mus: [b, t, n, d]
mu: [b, t, d]
return: [b, t, n]
"""
if self.low_rank is None:
return (mus - mu.unsqueeze(-2)).pow(2).sum(-1)
else:
low_mat_sq = self.low_mat.transpose(-1, -2) @ self.low_mat
x, y = mus, mu
wx_sq = (
x
* torch.einsum(
"btni,nij->btnj",
x,
low_mat_sq.to(x),
)
).sum(
-1
) # [b, t, n]
y_sq = y.pow(2).sum(-1, keepdim=True) # [b, t, 1]
xwy = (x * torch.einsum("bti,nij->btnj", y, self.low_mat.to(y))).sum(
-1
) # [b, t, n, d_l], [n, d_i, d_l], [b, t, d_i] -> [b, t, n]
dist = wx_sq + y_sq - 2 * xwy
return torch.abs(dist)
class NeMoSubwordFlagEmbedding(nn.Module):
"""
Adds a tiny embedding table for continuation tokens
(subwords that do NOT start with Ġ or the word-boundary marker).
Compatible with NeMo AutoTokenizer.
"""
def __init__(self, tokenizer: AutoTokenizer, d_model: int):
super().__init__()
self.tokenizer = tokenizer
self.vocab_size = self.tokenizer.vocab_size
self.d_model = d_model
# Precompute continuation flags
tokens = [self.tokenizer.ids_to_tokens(i) for i in range(self.vocab_size)]
self.register_buffer(
'is_continuation',
torch.tensor(
[1 if not (tok.startswith("Ġ") or tok.startswith("▁")) else 0 for tok in tokens], dtype=torch.long
),
)
# Tiny embedding table: 0 = word-start, 1 = continuation
init_std = self.d_model**-0.5
self.cont_emb = nn.Embedding(2, self.d_model)
nn.init.normal_(self.cont_emb.weight, mean=0.0, std=init_std)
# Force word-start embedding to zero so only continuation tokens get shifted
self.cont_emb.weight.data[0].zero_()
def forward(self, subword_embeds: torch.Tensor, token_ids: torch.LongTensor):
# Continuation flags
cont_flags = self.is_continuation[token_ids]
# Add continuation embedding
cont_emb = self.cont_emb(cont_flags)
return subword_embeds + cont_emb
class SubwordFlagEmbedding(nn.Module):
"""
Adds a small continuation embedding for subwords (tokens without word-boundary marker).
Automatically adds a custom padding token at index vocab_size.
Ignores special tokens (starting with '<') when computing continuation flags.
"""
def __init__(self, tokenizer: AutoTokenizer, d_model: int):
super().__init__()
self.tokenizer = tokenizer
self.vocab_size = self.tokenizer.vocab_size
self.d_model = d_model
# Custom pad token at vocab_size
self.pad_id = self.vocab_size
# register pad_id as a tensor buffer to avoid device issues
self.register_buffer("pad_tensor", torch.tensor(self.pad_id, dtype=torch.long))
# Precompute continuation flags
tokens = [self.tokenizer.ids_to_tokens(i) for i in range(self.vocab_size)]
cont_flags = [
1 if not (tok.startswith("Ġ") or tok.startswith("▁") or tok.startswith("<")) else 0 for tok in tokens
]
cont_flags.append(0) # for the custom pad token
self.register_buffer("is_continuation", torch.tensor(cont_flags, dtype=torch.long))
# Continuation embedding
init_std = self.d_model**-0.5
self.cont_emb = nn.Embedding(2, self.d_model)
nn.init.normal_(self.cont_emb.weight, mean=0.0, std=init_std)
self.cont_emb.weight.data[0].zero_()
def forward(self, subword_embeds: torch.Tensor, token_ids: torch.LongTensor):
# Replace OOV token IDs with pad_id safely
token_ids_clamped = torch.where(token_ids >= self.vocab_size, self.pad_tensor, token_ids)
# Continuation flags
cont_flags = self.is_continuation[token_ids_clamped]
# Add continuation embedding
cont_emb = self.cont_emb(cont_flags)
return subword_embeds + cont_emb
class BOSEOSEmbedding(nn.Module):
"""
Adds independent embeddings for BOS and EOS tokens using a single embedding table.
Index 0 = regular token (ignored), 1 = BOS, 2 = EOS.
Compatible with Hugging Face tokenizers that may or may not have BOS/EOS.
"""
def __init__(self, tokenizer: AutoTokenizer, d_model: int):
super().__init__()
self.tokenizer = tokenizer
# vocab size that includes special tokens
vocab_dict = self.tokenizer.tokenizer.get_vocab()
self.vocab_size = max(vocab_dict.values())
self.d_model = d_model
# Custom pad token for OOVs
self.pad_id = self.vocab_size
self.register_buffer("pad_tensor", torch.tensor(self.pad_id, dtype=torch.long))
# Identify BOS and EOS tokens (may be None)
tokens = [self.tokenizer.ids_to_tokens(i) for i in range(self.vocab_size)]
special_flags = []
for tok in tokens:
if self.tokenizer.bos_token is not None and tok == self.tokenizer.bos_token:
special_flags.append(1)
elif self.tokenizer.eos_token is not None and tok == self.tokenizer.eos_token:
special_flags.append(2)
else:
special_flags.append(0)
special_flags.append(0) # for custom pad token
self.register_buffer("special_flags", torch.tensor(special_flags, dtype=torch.long))
# Embedding table: 0 = regular, 1 = BOS, 2 = EOS
init_std = self.d_model**-0.5
self.special_emb = nn.Embedding(3, d_model)
nn.init.normal_(self.special_emb.weight, mean=0.0, std=init_std)
self.special_emb.weight.data[0].zero_() # regular tokens ignored
def forward(self, token_embeds: torch.Tensor, token_ids: torch.LongTensor):
"""
token_embeds: (B, T, d_model)
token_ids: (B, T)
"""
# Clamp OOVs to custom pad token
safe_ids = torch.where(token_ids >= self.vocab_size, self.pad_tensor, token_ids)
# Lookup flags (0=regular, 1=BOS, 2=EOS)
flags = self.special_flags[safe_ids]
return token_embeds + self.special_emb(flags)
class SubwordEmbedding(nn.Module):
"""
Produces subword embeddings from a Hugging Face tokenizer vocabulary.
No special handling for OOVs or padding — assumes token_ids are valid.
"""
def __init__(self, tokenizer: AutoTokenizer, d_model: int):
super().__init__()
self.tokenizer = tokenizer
# Get vocab size from tokenizer
vocab_dict = self.tokenizer.tokenizer.get_vocab()
self.vocab_size = max(vocab_dict.values()) + 1 # +1 for safety
self.d_model = d_model
# Subword embedding table
init_std = d_model**-0.5
self.subword_emb = nn.Embedding(self.vocab_size, d_model)
nn.init.normal_(self.subword_emb.weight, mean=0.0, std=init_std)
def forward(self, token_ids: torch.LongTensor, subword_mask: torch.tensor = None):
"""
token_ids: (B, T)
subword_mask: (B, T)
Returns:
subword_embeds: (B, T, d_model)
"""
return self.subword_emb(token_ids)
class CharAwareSubwordEncoder(nn.Module):
"""
An encoder that creates subword embeddings from character-level embeddings.
This module replaces a standard subword embedding layer. It breaks down each
subword into its constituent characters, embeds the characters, and then
aggregates these character embeddings (e.g., via mean pooling) to form the
final subword representation. This allows the model to handle rare or out-of-vocabulary
subwords more gracefully.
Args:
out_size (int): The dimensionality of the output embedding vectors.
tokenizer (AutoTokenizer): The Hugging Face tokenizer class.
vocab_dir (str | None): Directory to save/load the character vocabulary.
backbone_type (str | None): The type of backbone model from Hugging Face (e.g., "t5gemma").
backbone_model_class (str | None): The class name of the backbone model if not using AutoModel.
backbone_config_class (str | None): The class name of the backbone config.
backbone_config (DictConfig | None): A configuration for the backbone model.
"""
def __init__(
self,
out_size: int,
tokenizer: AutoTokenizer,
vocab_dir: str | None = None,
backbone_type: str | None = "t5gemma",
backbone_model_class: str | None = None,
backbone_config_class: str | None = None,
backbone_config: DictConfig | None = None,
use_subword_flag_emb: bool = True,
use_bos_eos_emb: bool = True,
use_cumulative_word_emb: bool = False,
):
super().__init__()
# 1. Build or load the character vocabulary
self.subword_id_to_char_ids, self.char_vocab, self.subword_padding_idx = build_vocabs(
tokenizer,
vocab_dir,
)
self.char_padding_idx = len(self.char_vocab)
self.use_subword_flag_emb = use_subword_flag_emb
self.use_bos_eos_emb = use_bos_eos_emb
# 2. Initialize the backbone model
if backbone_type:
config = AutoConfig.for_model(
backbone_type, **(OmegaConf.to_container(backbone_config, resolve=True) if backbone_config else {})
)
self.backbone = AutoModelForTextEncoding.from_config(config)
else:
assert backbone_model_class and backbone_config_class
config_class = getattr(transformers, backbone_config_class)
model_class = getattr(transformers, backbone_model_class)
config = config_class(**(OmegaConf.to_container(backbone_config, resolve=True) if backbone_config else {}))
self.backbone = model_class(config)
self.hidden_size = self.backbone.get_input_embeddings().weight.size(-1)
# 3. Delete the original subword embedding layer and replace it with our character embedding layer
find_and_delete_module(self.backbone, self.backbone.get_input_embeddings(), "backbone")
self.embed_tokens = nn.Embedding(len(self.char_vocab) + 1, self.hidden_size, padding_idx=self.char_padding_idx)
self.proj_embedding = nn.Linear(self.hidden_size, out_size, bias=False)
if self.use_subword_flag_emb:
self.subword_flag_emb = SubwordFlagEmbedding(tokenizer, self.hidden_size)
if self.use_bos_eos_emb:
self.bos_eos_emb = BOSEOSEmbedding(tokenizer, self.hidden_size)
def prepare_inputs(self, subword_ids: Tensor, padding_mask: Tensor) -> tuple[Tensor, Tensor]:
"""
Converts a batch of subword IDs into a padded batch of character IDs.
Args:
subword_ids (Tensor): A tensor of subword IDs. Shape: `[batch, seq_len]`.
padding_mask (Tensor): A boolean mask indicating valid (non-padding) subwords.
Returns:
tuple[Tensor, Tensor]: A tuple containing:
- Padded character IDs. Shape: `[num_valid_subwords, max_char_len]`.
- Lengths of each character sequence. Shape: `[num_valid_subwords]`.
"""
device = subword_ids.device
# Select only the valid subword IDs
subword_id_list = torch.masked_select(subword_ids, padding_mask).cpu().tolist()
# Map each subword ID to its sequence of character IDs
char_id_list = [list(self.subword_id_to_char_ids.get(x, ())) for x in subword_id_list]
char_lengths = torch.tensor([len(x) for x in char_id_list], dtype=torch.long, device=device)
batch_size = char_lengths.size(0)
max_len = int(char_lengths.max().item()) if batch_size > 0 else 0
# Create a padded tensor for the character IDs
char_ids = torch.full((batch_size, max_len), self.char_padding_idx, dtype=torch.long, device=device)
for i, char_seq in enumerate(char_id_list):
char_ids[i, : len(char_seq)] = torch.tensor(char_seq, dtype=torch.long, device=device)
return char_ids, char_lengths
def forward(self, subword_ids: Tensor, subword_mask: Tensor | None = None) -> Tensor:
"""
Performs the forward pass to get character-aware subword embeddings.
Args:
subword_ids (Tensor): A tensor of subword IDs. Shape: `[batch, seq_len]`.
subword_mask (Tensor | None): A boolean mask for padding. Defaults to None.
Returns:
Tensor: The final subword embeddings. Shape: `[batch, seq_len, hidden_size]`.
"""
if subword_mask is None:
subword_mask = torch.ones_like(subword_ids, dtype=torch.bool)
# 1. Convert subword IDs to character IDs
char_ids, char_lengths = self.prepare_inputs(subword_ids, subword_mask)
char_mask = sequence_mask(char_lengths)
# 2. Get character embeddings and pass them through the backbone
char_embeds = self.embed_tokens(char_ids)
# The backbone model should be able to accept `inputs_embeds`
char_hidden_states = self.backbone(inputs_embeds=char_embeds, attention_mask=char_mask).last_hidden_state
# 3. Aggregate character embeddings to form subword embeddings (mean pooling)
# We mask the padding characters before summing to get a correct mean.
masked_sum = (char_hidden_states * char_mask.unsqueeze(-1)).sum(dim=1)
# Avoid division by zero for empty sequences
mean_emb = masked_sum / (char_lengths.unsqueeze(-1).clamp(min=1))
# 4. Scatter the aggregated embeddings back to the original subword sequence shape
out_emb = self.proj_embedding(mean_emb)
subword_embeds = torch.zeros(
subword_ids.shape + (out_emb.size(-1),), device=subword_ids.device, dtype=out_emb.dtype
)
subword_embeds[subword_mask] = out_emb
if self.use_subword_flag_emb:
subword_embeds = self.subword_flag_emb(subword_embeds, subword_ids)
if self.use_bos_eos_emb:
subword_embeds = self.bos_eos_emb(subword_embeds, subword_ids)
return subword_embeds
class GatedProjectedSumRMSNorm(nn.Module):
def __init__(self, audio_dim, text_dim, hidden_dim, final_norm=True, num_codebooks=31, init_residual_scale=0.5):
super().__init__()
self.num_codebooks = num_codebooks
self.audio_proj = nn.Linear(audio_dim, hidden_dim)
self.text_proj = nn.Linear(text_dim, hidden_dim)
nn.init.normal_(self.audio_proj.weight, mean=0.0, std=0.015)
nn.init.zeros_(self.audio_proj.bias)
nn.init.normal_(self.text_proj.weight, mean=0.0, std=0.015)
nn.init.zeros_(self.text_proj.bias)
# FP32 gate params
self.gate = nn.Parameter(torch.zeros(hidden_dim, dtype=torch.float32))
self.residual_scale = nn.Parameter(torch.tensor(init_residual_scale, dtype=torch.float32))
self.final_norm = RMSNorm(hidden_dim) if final_norm else nn.Identity()
def forward(self, audio_emb, text_emb):
audio_emb = audio_emb / self.num_codebooks
# projections run in model dtype (BF16)
audio_h = self.audio_proj(audio_emb)
text_h = self.text_proj(text_emb)
dtype = audio_h.dtype
with fp32_precision():
gate = torch.sigmoid(self.gate) # FP32
res = torch.sigmoid(self.residual_scale) # FP32
h = gate.to(dtype) * audio_h + (1 - gate).to(dtype) * text_h
h = res.to(dtype) * h
h = self.final_norm(h).to(dtype)
return h
class RVQEARTTSModel(nn.Module):
"""
Main RVQEARTTS model for training and inference.
This model integrates a character-aware text encoder with a transformer backbone
and a Mixture-of-Gaussians (MoG) prediction head.
The architecture is based on the Streaming TTS model proposed in
"Audio Flamingo 3" (https://arxiv.org/abs/2507.08128), with several improvements:
1. Gated fusion of text and audio representations
(`GatedProjectedSumRMSNorm`).
2. Subword-aware embeddings for improved pronunciation of multi-token words
(`SubwordFlagEmbedding`).
3. Custom BOS and EOS embeddings for duplex interaction support,
enabling interruption-aware generation (`BOSEOSEmbedding`).
Args:
config (DictConfig | dict[str, Any]): The configuration object for the model.
"""
config_class = DictConfig
rvq_embs: Tensor
def __init__(self, config: DictConfig | dict[str, Any], tokenizer: AutoTokenizer = None):
super().__init__()
self.config = config
# Backbone module
if self.config.get("pretrained_text_name", None):
# Load pretrained backbone from huggingface
from nemo.collections.speechlm2.parts.pretrained import load_pretrained_hf
llm = load_pretrained_hf(self.config.pretrained_text_name, pretrained_weights=True).train()
self.backbone = llm.model # fetch PretrainedBaseModel from model "ForCausalLM"
else:
if self.config.get("backbone_type", None) is None:
assert (
self.config.get("backbone_model_class", None) is not None
and self.config.get("backbone_config_class", None) is not None
)
backbone_config = getattr(transformers, self.config.backbone_config_class)(
**(
OmegaConf.to_container(self.config.backbone_config, resolve=True)
if self.config.backbone_config
else {}
),
)
self.backbone = getattr(transformers, self.config.backbone_model_class)(backbone_config)
else:
backbone_config = AutoConfig.for_model(
self.config.backbone_type,
**(
OmegaConf.to_container(self.config.backbone_config, resolve=True)
if self.config.backbone_config
else {}
),
)
self.backbone = AutoModel.from_config(backbone_config)
self.hidden_size = self.backbone.get_input_embeddings().weight.size(-1)
find_and_delete_module(self.backbone, self.backbone.get_input_embeddings(), "backbone")
# Embedding and projection layers
self.bos_emb = nn.Parameter(torch.randn(self.hidden_size))
self.null_emb = nn.Parameter(torch.randn(self.hidden_size))
if self.config.random_target_masking:
self.embed_target_mask = nn.Embedding(self.config.num_quantizers, self.hidden_size)
self.embed_code = nn.Linear(self.config.latent_size, self.hidden_size, bias=False)
self.embed_context = (
nn.Linear(self.config.context_hidden_size, self.hidden_size, bias=False)
if self.config.context_hidden_size
else None
)
self.embed_subword = (
CharAwareSubwordEncoder(
tokenizer=tokenizer,
out_size=self.hidden_size,
use_subword_flag_emb=self.config.use_subword_flag_emb,
use_bos_eos_emb=self.config.use_bos_eos_emb,
**self.config.cas_config,
)
if self.config.cas_config
else None
)
if self.config.use_gated_fusion_for_text_audio:
self.gated_fusion_audio_text = GatedProjectedSumRMSNorm(
self.hidden_size, self.hidden_size, self.hidden_size, self.config.num_quantizers
)
if self.config.get("use_audio_prompt_frozen_projection", False):
with fp32_precision():
U, _ = torch.linalg.qr(torch.randn(self.hidden_size, self.hidden_size))
V, _ = torch.linalg.qr(torch.randn(self.hidden_size, self.hidden_size))
smin, smax = 0.4, 2.5
s = smin + (smax - smin) * torch.rand(self.hidden_size)
W = U @ torch.diag(s) @ V.T
self.register_buffer("audio_prompt_projection_W", W) # register as buffer to avoid weight update
# Prediction Heads
if not self.config.disable_eos_prediction:
self.lm_head = nn.Linear(self.hidden_size, 2, bias=False)
self.mog_head = MoGHead(
hidden_size=self.hidden_size,
out_size=self.config.latent_size,
**self.config.mog_head_config,
)
def set_rvq_embs(self, rvq_embs: Tensor):
self.register_buffer("rvq_embs", rvq_embs.detach().clone())
def depthsum_embedding(self, code: Tensor) -> Tensor:
"""
code: [b, t, d]
rvq_embs: [d, v, h]
ret: [b, t, h]
"""
b, t, d = code.size()
_, v, h = self.rvq_embs.size()
device = code.device
ret = torch.zeros((b, t, h), device=device, dtype=self.rvq_embs.dtype)
embs = F.pad(self.rvq_embs, [0, 0, 0, 1])
for i in range(d):
emb = embs[i]
ret = ret + F.embedding(code[..., i], emb)
return ret
def prepare_training_inputs(self, code: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor]:
"""Prepares masked and dropped-out versions of the code for training."""
b, t, d = code.size()
device = code.device
src_rate = torch.rand((b, t), device=device) * self.config.max_training_rate
src_masking_rate = get_masking_rate(src_rate, self.config.exponent)
src_num_masking = torch.ceil(src_masking_rate * self.config.num_quantizers).long()
src_code_mask = torch.ones((b, t, d), dtype=torch.bool, device=device)
src_code_mask = get_mask(src_code_mask, src_num_masking)
src_masked_code = code * src_code_mask + (torch.zeros_like(code) + self.config.codebook_size) * (
~src_code_mask
)
if self.config.random_target_masking:
tgt_rate = src_rate + (1.0 - src_rate) * torch.rand((b, t), device=device)
tgt_masking_rate = get_masking_rate(tgt_rate, self.config.exponent)
tgt_num_masking = torch.floor(tgt_masking_rate * self.config.num_quantizers).long()
tgt_code_mask = torch.ones((b, t, d), dtype=torch.bool, device=device)
tgt_code_mask = get_mask(tgt_code_mask, tgt_num_masking)
tgt_masked_code = code * tgt_code_mask + (torch.zeros_like(code) + self.config.codebook_size) * (
~tgt_code_mask
)
else:
tgt_code_mask = torch.ones((b, t, d), dtype=torch.bool, device=device)
tgt_masked_code = code
dropout_mask = torch.where(
torch.rand((b, t, 1), device=device) < self.config.quantizer_dropout,
(torch.randint(0, self.config.num_quantizers + 1, (b, t, 1), device=device)),
self.config.num_quantizers,
) > torch.arange(d, dtype=torch.long, device=device)
dropped_code = code * dropout_mask + (torch.zeros_like(code) + self.config.codebook_size) * (~dropout_mask)
return src_masked_code, src_code_mask, tgt_masked_code, tgt_code_mask, dropped_code
def _prepare_conditioning(
self,
context_hidden_state: Tensor | None,
subword_ids: Tensor | None,
subword_mask: Tensor | None,
uncond_dec_flag: Tensor,
asr_speech_tokens_emb: Tensor | None,
) -> Tensor:
"""Computes the final conditioning tensor by combining all sources."""
cond = torch.zeros((1, 1, self.hidden_size), device=uncond_dec_flag.device, dtype=self.rvq_embs.dtype)
if self.embed_context is not None and context_hidden_state is not None:
cond = cond + self.embed_context(context_hidden_state)
if self.embed_subword is not None and subword_ids is not None:
# Infer subword mask from context if not provided
if subword_mask is None and context_hidden_state is not None:
subword_mask = torch.any(context_hidden_state != 0, dim=-1)
# at least one value should be true, otherwise we can completly skip it to avoid errors
if subword_mask is not None and subword_mask.any():
cond = cond + self.embed_subword(subword_ids, subword_mask)
if asr_speech_tokens_emb is not None:
cond = cond + asr_speech_tokens_emb
# Replace with null embedding for unconditional generation
cond = torch.where(uncond_dec_flag, self.null_emb, cond)
return cond
def _compute_losses(
self,
code: Tensor,
lm_logits: Tensor,
mog_logits: Tensor,
mog_mus: Tensor,
mog_mu_res: Tensor,
mog_logs: Tensor,
src_code_mask: Tensor,
tgt_code_mask: Tensor,
audio_mask: Tensor,
) -> tuple[Tensor, Tensor, Tensor]:
"""Helper to compute all losses for the training step."""
with torch.autocast(code.device.type, enabled=False):
# 1. LM Loss (predicting discrete tokens)
if not self.config.disable_eos_prediction:
eos_mask = (~audio_mask) & F.pad(audio_mask[:, :-1], [1, 0])
lm_mask = eos_mask | audio_mask
lm_target = torch.where(eos_mask, 1, 0)
lm_loss = (
F.cross_entropy(lm_logits.transpose(1, 2), lm_target, reduction="none") * lm_mask
).sum() / lm_mask.sum().clamp_min(1)
else:
lm_loss = 0.0
# 2. Continuous & KL Losses (for the MoG head)
target_mask = (~src_code_mask & tgt_code_mask) & audio_mask.unsqueeze(-1)
reduced_target_mask = target_mask.any(dim=-1)
cont_code_target = self.depthsum_embedding(
code * target_mask + (torch.zeros_like(code) + self.config.codebook_size) * (~target_mask)
)
mog_logits = mog_logits.float()
mog_mus = mog_mus.float()
mog_mu_res = mog_mu_res.float()
mog_logs = mog_logs.float()
with fp32_precision():
# Log probability of the true code under each Gaussian component
logp_code = (
-0.5 * math.log(2 * math.pi) - mog_logs
) * self.config.latent_size - 0.5 * self.mog_head.dist(
mog_mus, (cont_code_target - mog_mu_res) * torch.exp(-mog_logs)
)
# Compute posterior q(k|c)
q_kc = (
torch.softmax(
logp_code,
-1,
)
* (1 - self.config.label_smoothing)
+ self.config.label_smoothing / self.mog_head.num_predictions
).detach()
log_q_kc = torch.log(q_kc + 1e-8).detach()
# Continuous Loss (negative log-likelihood)
c_loss = (-(q_kc * logp_code).sum(-1) * reduced_target_mask).sum() / target_mask.sum().clamp_min(1)
# KL Divergence Loss
k_loss = (
(q_kc * (log_q_kc - F.log_softmax(mog_logits, -1))).sum(-1) * reduced_target_mask
).sum() / target_mask.sum().clamp_min(1)
return lm_loss, c_loss, k_loss
def forward(
self,
code: Tensor,
attention_mask: Tensor | None = None,
position_ids: Tensor | None = None,
context_hidden_state: Tensor | None = None,
subword_ids: Tensor | None = None,
subword_mask: Tensor | None = None,
audio_mask: Tensor | None = None,
non_prompt_mask: Tensor | None = None,
past_key_values: Cache | None = None,
use_cache: bool = False,
training: bool | None = None,
guidance_enabled: bool = False,
generation_config: dict[str, Any] | None = None,
teacher_forcing_inference: bool = False,
ignore_eos_flag_stop: bool = False,
asr_speech_tokens_emb: Tensor | None = None,
audio_prompt_lantent: Tensor | None = None,
dataset_type: list[str] | None = None,
) -> RVQEARTTSOutput:
"""
Performs a forward pass handling training, generation, or single-step inference.
Args:
code (Tensor): Input audio codes. For training, this is the ground truth.
For generation, this is the previously generated code token.
attention_mask (Tensor | None): Attention mask for the backbone transformer.
position_ids (Tensor | None): Position ids for the backbone transformer.
context_hidden_state (Tensor | None): Conditioning from a language model.
subword_ids (Tensor | None): Subword token IDs for conditioning.
subword_mask (Tensor | None): Mask for subword IDs.
audio_mask (Tensor | None): Mask for valid audio positions (for training and inference initialization).
past_key_values (Cache | None): Cache for past key-values for fast decoding.
use_cache (bool): If True, returns the updated `past_key_values`.
training (bool | None): Explicitly set training mode. If `None`, uses `self.training`.
guidance_enabled (bool): If True, duplicates inputs internally to run both
conditional and unconditional passes
generation_config (dict[str, Any] | None): If provided, triggers an iterative code generation.
Returns:
RVQEARTTSOutput: A dataclass containing losses (for training) or generated outputs
and the cache (for inference).
"""
# Determine operating mode.
if training is None:
training = self.training
if audio_mask is not None:
if training:
(src_masked_code, src_code_mask, tgt_masked_code, tgt_code_mask, dropped_code) = (
self.prepare_training_inputs(code)
)
uncond_dec_flag = torch.rand(code.size(0), 1, 1, device=code.device) < self.config.p_uncond
else:
dropped_code = code
uncond_dec_flag = torch.zeros(code.size(0), 1, 1, device=code.device, dtype=torch.bool)
# Shifted code
shifted_code = F.pad(dropped_code[:, :-1], [0, 0, 1, 0])
# Base embeddings
code_embed = self.depthsum_embedding(shifted_code)
# BOS mask
bos_mask = audio_mask & (~F.pad(audio_mask[:, :-1], [1, 0])) # [B, T]
bos_mask = bos_mask.unsqueeze(-1) # [B, T, 1]
# Mask for tokens BEFORE BOS
pre_bos_mask = bos_mask.cumsum(dim=1) == 0 # [B, T, 1]
# Apply projection to model size
code_embed = self.embed_code(code_embed)
# audio frozen projection
if self.config.get("use_audio_prompt_frozen_projection", False):
if audio_prompt_lantent is None:
# Training-only anti-cloning augmentation for pure TTS batches.
all_tts = (
training
and dataset_type is not None
and len(dataset_type) == code_embed.size(0)
and all(str(p).strip().lower() == "tts" for p in dataset_type)
)
if (
self.config.get("force_no_audio_cond_latent_on_prompt", False)
and all_tts
and torch.rand(1, device=code_embed.device).item() < 0.3
):
perm = torch.randperm(code_embed.size(0), device=code_embed.device)
audio_prompt_lantent = code_embed[perm]
else:
W = self.audio_prompt_projection_W.to(code_embed.device, code_embed.dtype)
audio_prompt_lantent = torch.nn.functional.linear(code_embed, W.T)
code_embed = torch.where(
pre_bos_mask,
audio_prompt_lantent,
code_embed,
)
# Add BOS embedding
code_embeds = code_embed + bos_mask * self.bos_emb
else: # Inference
code_embeds = self.embed_code(self.depthsum_embedding(code))
uncond_dec_flag = torch.zeros(code.size(0), 1, 1, device=code.device, dtype=torch.bool)
if guidance_enabled:
assert not training, "Classifier-free guidance can only be used when `training` is False."
code_embeds = torch.cat([code_embeds] * 2, 0)
if attention_mask is not None:
attention_mask = torch.cat([attention_mask] * 2, 0)
if position_ids is not None:
position_ids = torch.cat([position_ids] * 2, 0)
if context_hidden_state is not None:
context_hidden_state = torch.cat([context_hidden_state] * 2, 0)
if subword_ids is not None:
subword_ids = torch.cat([subword_ids] * 2, 0)
if subword_mask is not None:
subword_mask = torch.cat([subword_mask] * 2, 0)
if asr_speech_tokens_emb is not None:
asr_speech_tokens_emb = torch.cat([asr_speech_tokens_emb] * 2, 0)
uncond_dec_flag = torch.cat([uncond_dec_flag, torch.ones_like(uncond_dec_flag)], 0)
# Prepare conditioning
cond = self._prepare_conditioning(
context_hidden_state,
subword_ids,
subword_mask,
uncond_dec_flag,
asr_speech_tokens_emb=asr_speech_tokens_emb,
)
if self.config.use_gated_fusion_for_text_audio:
inputs_embeds = self.gated_fusion_audio_text(code_embeds, cond)
else:
inputs_embeds = code_embeds + cond
# Main backbone pass
backbone_outputs = self.backbone(
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
)
hidden_states = backbone_outputs.last_hidden_state
if audio_mask is not None and training:
# --- Training-specific loss computation ---
if not self.config.disable_eos_prediction:
lm_logits = self.lm_head(hidden_states)
else:
lm_logits = None
mog_input_embeds = self.embed_code(self.depthsum_embedding(src_masked_code))
if self.config.random_target_masking:
mog_input_embeds = mog_input_embeds + self.embed_target_mask((tgt_code_mask.sum(-1) - 1).clamp_min(0))
mog_input_embeds = mog_input_embeds + hidden_states
mog_logits, mog_mus, mog_mu_res, mog_logs = self.mog_head(mog_input_embeds)
lm_loss, c_loss, k_loss = self._compute_losses(
code, lm_logits, mog_logits, mog_mus, mog_mu_res, mog_logs, src_code_mask, tgt_code_mask, audio_mask
)
total_loss = lm_loss + c_loss + k_loss
return RVQEARTTSOutput(
loss=total_loss,
lm_loss=lm_loss,
c_loss=c_loss,
k_loss=k_loss,
hidden_states=hidden_states,
audio_prompt_lantent=audio_prompt_lantent,
)
else: # Inference
if not generation_config:
return RVQEARTTSOutput(
hidden_states=hidden_states,
past_key_values=backbone_outputs.past_key_values,
audio_prompt_lantent=audio_prompt_lantent,
)
else:
if teacher_forcing_inference:
generated_codes, lm_logits, eos_flag = self.generate_teacher_forcing(
hidden_states, generation_config
)
else:
generated_codes, lm_logits, eos_flag = self.generate_step(
hidden_states, ignore_eos_flag_stop=ignore_eos_flag_stop, **generation_config
)
return RVQEARTTSOutput(
past_key_values=backbone_outputs.past_key_values,
codes=generated_codes,
lm_logits=lm_logits,
eos_flag=eos_flag,
hidden_states=hidden_states,
)
@torch.no_grad()
def generate_teacher_forcing(self, hidden_states: Tensor, generation_config: dict):
"""
Teacher-forcing wrapper for generate_step, processing all frames in parallel
using a per-frame loop internally.
Args:
hidden_states: [B, T, H] hidden states
generation_config: kwargs for self.generate_step()
Returns:
generated_codes: [B, T, ...] generated codes per frame
lm_logits: [B, T, vocab_size] language model logits
eos_flag: [B, T] boolean tensor indicating EOS
"""
B, T, H = hidden_states.shape
# Preallocate caches
generated_codes_cache = []
lm_logits_cache = []
eos_flag_cache = []
# Iterate over time steps (frames)
for t in range(T):
# extract one frame (as the original generate_step expects)
frame_hidden = hidden_states[:, t, :] # [B, H]
# call original generate_step
generated_codes, lm_logits, eos_flag = self.generate_step(
frame_hidden.unsqueeze(1), **generation_config # keep batch dim + frame dim
)
if generated_codes is not None:
# store in cache
generated_codes_cache.append(generated_codes)
lm_logits_cache.append(lm_logits)
eos_flag_cache.append(eos_flag)
# Stack results along time dimension
generated_codes = torch.stack(generated_codes_cache, dim=1) # [B, T, ...]
if not self.config.disable_eos_prediction:
lm_logits = torch.stack(lm_logits_cache, dim=1) # [B, T, vocab_size]
eos_flag = torch.stack(eos_flag_cache, dim=1) # [B, T]
else:
lm_logits = None
eos_flag = None
return generated_codes, lm_logits, eos_flag
@torch.no_grad()
def generate_step(
self,
hidden_states: Tensor,
num_iter: int,
guidance_scale: list[float] | float | None = None,
top_p_or_k: list[float | int] | float | int | None = None,
noise_scale: list[float] | float | None = None,
exponent: float | None = None,
eos_threshold: float | None = None,
ignore_eos_flag_stop: bool = False,
) -> tuple[Tensor | None, Tensor, Tensor]:
"""
Performs the iterative unmasking process for a single generation step.
This function takes the hidden state from the backbone transformer and generates
codes through an iterative unmasking process.
Args:
hidden_states (Tensor): The hidden states from the backbone. If using CFG,
this should be the combined [uncond, cond] tensor.
num_iter (int): The number of unmasking iterations.
guidance_scale (list[float] | float | None): The scale for Classifier-Free Guidance.
top_p_or_k (ist[float | int] | float | int | None): The value for top-p or top-k sampling.
noise_scale (list[float] | float | None): The scale of noise to add during MoG sampling.
exponent (float | None): The exponent for the masking schedule.
eos_threshold (float | None): The threshold for EOS prediction.
Returns:
tuple[Tensor | None, Tensor, Tensor]: A tuple containing:
- the generated codes.
- The logits from `lm_head`.
- The EOS flag.
"""
# 1. Preparation
if guidance_scale is not None:
if not isinstance(guidance_scale, list):
guidance_scale = [guidance_scale] * (1 + num_iter) # includes one step for `lm_head`
assert len(guidance_scale) == 1 + num_iter
if top_p_or_k is not None:
if not isinstance(top_p_or_k, list):
top_p_or_k = [top_p_or_k] * (1 + num_iter) # includes one step for `lm_head`
assert len(top_p_or_k) == 1 + num_iter
if noise_scale is not None:
if not isinstance(noise_scale, list):
noise_scale = [noise_scale] * num_iter
assert len(noise_scale) == num_iter
if exponent is None:
exponent = self.config.exponent
if guidance_scale is not None:
# The effective batch size is halved
hidden_states, uncond_hidden_states = hidden_states.chunk(2, dim=0)
else:
uncond_hidden_states = hidden_states[:0, :0, :0]
b, t, _ = hidden_states.size()
d = self.config.num_quantizers
device = hidden_states.device
# 2. Predict the discrete part of the code
if not self.config.disable_eos_prediction:
if guidance_scale is not None:
lm_logits = self.lm_head(hidden_states + guidance_scale[0] * (hidden_states - uncond_hidden_states))
else:
lm_logits = self.lm_head(hidden_states)
if top_p_or_k is not None:
lm_logits = (
TopPLogitsWarper(top_p_or_k[0])(
None,
lm_logits.view(-1, lm_logits.size(-1)),
).view_as(lm_logits)
if isinstance(top_p_or_k[0], float)
else TopKLogitsWarper(top_p_or_k[0])(
None,
lm_logits.view(-1, lm_logits.size(-1)),
).view_as(lm_logits)
)
with fp32_precision():
lm_logits = F.log_softmax(lm_logits, -1)
if eos_threshold is not None:
eos_flag = lm_logits[..., -1] > eos_threshold
else:
eos_flag = lm_logits.argmax(-1) == 1
if torch.all(eos_flag) and not ignore_eos_flag_stop:
return None, lm_logits, eos_flag
else:
lm_logits = None
eos_flag = None
# Initialize the full code tensor
code = torch.zeros((b, t, d), dtype=torch.long, device=device) + self.config.codebook_size
# 3. Set up the iterative denoising schedule for the continuous part
rates = torch.linspace(0.0, 1.0, num_iter + 1, device=device)[:-1].unsqueeze(-1)
masking_rates = get_masking_rate(rates, exponent=exponent)
num_maskings = torch.ceil(masking_rates * self.config.num_quantizers).long()
ks = num_maskings - F.pad(num_maskings[1:], [0, 0, 0, 1])
# 4. Iteratively unmask the continuous part of the code
cnt = 0
for i, k in enumerate(ks):
if torch.all(k == 0):
continue
# Prepare input for the MoG head
guidance_scale_i = guidance_scale[i] if guidance_scale is not None else 0.0
top_p_or_k_i = top_p_or_k[i] if top_p_or_k is not None else 1.0
noise_scale_i = noise_scale[i] if noise_scale is not None else 1.0
mog_input_embeds = self.embed_code(self.depthsum_embedding(code))
if self.config.random_target_masking:
mog_input_embeds += self.embed_target_mask(cnt + k - 1)
if guidance_scale_i > 0.0:
mog_input_embeds = torch.cat(
[mog_input_embeds + hidden_states, mog_input_embeds + uncond_hidden_states], 0
)
else:
mog_input_embeds += hidden_states
mog_mu, mog_logs = self.mog_head.infer(
mog_input_embeds,
guidance_scale=guidance_scale_i,
top_p_or_k=top_p_or_k_i,
)
z = mog_mu + torch.exp(mog_logs) * torch.randn_like(mog_mu) * noise_scale_i
code = depthsum_encoding_step(self.rvq_embs, z, code, cnt, k[0].item())
cnt += k[0].item()
return code, lm_logits, eos_flag
def load_state_dict(self, state_dict, strict: bool = True):
try:
super().load_state_dict(state_dict, strict=strict)
except RuntimeError:
logging.info("Error loading model state_dict !! Retrying with partial initialization!")
model_dict = set_model_dict_for_partial_init(state_dict, self.state_dict())
super().load_state_dict(model_dict, strict=False)