1
0

Initial commit

This commit is contained in:
Yatharth Sharma
2026-01-23 15:12:06 -05:00
committed by GitHub
commit b51a0820b3
41 changed files with 19498 additions and 0 deletions

View File

@@ -0,0 +1,570 @@
# Copyright 2021-2025 Xiaomi Corporation (authors: Fangjun Kuang,
# Zengwei Yao)
#
# See ../../LICENSE for clarification regarding multiple authors
#
# 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 glob
import logging
import os
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import torch
import torch.nn as nn
from lhotse.dataset.sampling.base import CutSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.optim import Optimizer
from zipvoice.utils.common import AttributeDict, GradScaler
# use duck typing for LRScheduler since we have different possibilities, see
# our class LRScheduler.
LRSchedulerType = object
def save_checkpoint(
filename: Path,
model: Union[nn.Module, DDP],
model_avg: Optional[nn.Module] = None,
model_ema: Optional[nn.Module] = None,
params: Optional[Dict[str, Any]] = None,
optimizer: Optional[Optimizer] = None,
scheduler: Optional[LRSchedulerType] = None,
scaler: Optional[GradScaler] = None,
sampler: Optional[CutSampler] = None,
rank: int = 0,
) -> None:
"""Save training information to a file.
Args:
filename:
The checkpoint filename.
model:
The model to be saved. We only save its `state_dict()`.
model_avg:
The stored model averaged from the start of training.
model_ema:
The EMA version of model.
params:
User defined parameters, e.g., epoch, loss.
optimizer:
The optimizer to be saved. We only save its `state_dict()`.
scheduler:
The scheduler to be saved. We only save its `state_dict()`.
scalar:
The GradScaler to be saved. We only save its `state_dict()`.
sampler:
The sampler used in the labeled training dataset. We only
save its `state_dict()`.
rank:
Used in DDP. We save checkpoint only for the node whose
rank is 0.
Returns:
Return None.
"""
if rank != 0:
return
logging.info(f"Saving checkpoint to {filename}")
if isinstance(model, DDP):
model = model.module
checkpoint = {
"model": model.state_dict(),
"optimizer": optimizer.state_dict() if optimizer is not None else None,
"scheduler": scheduler.state_dict() if scheduler is not None else None,
"grad_scaler": scaler.state_dict() if scaler is not None else None,
"sampler": sampler.state_dict() if sampler is not None else None,
}
if model_avg is not None:
checkpoint["model_avg"] = model_avg.to(torch.float32).state_dict()
if model_ema is not None:
checkpoint["model_ema"] = model_ema.to(torch.float32).state_dict()
if params:
for k, v in params.items():
assert k not in checkpoint
checkpoint[k] = v
torch.save(checkpoint, filename)
def load_checkpoint(
filename: Path,
model: Optional[nn.Module] = None,
model_avg: Optional[nn.Module] = None,
model_ema: Optional[nn.Module] = None,
strict: bool = False,
) -> Dict[str, Any]:
logging.info(f"Loading checkpoint from {filename}")
checkpoint = torch.load(filename, map_location="cpu", weights_only=False)
if model is not None:
if next(iter(checkpoint["model"])).startswith("module."):
logging.debug("Loading checkpoint saved by DDP")
dst_state_dict = model.state_dict()
src_state_dict = checkpoint["model"]
for key in dst_state_dict.keys():
src_key = "{}.{}".format("module", key)
dst_state_dict[key] = src_state_dict.pop(src_key)
assert len(src_state_dict) == 0
model.load_state_dict(dst_state_dict, strict=strict)
else:
logging.debug("Loading checkpoint")
model.load_state_dict(checkpoint["model"], strict=strict)
checkpoint.pop("model")
if model_avg is not None and "model_avg" in checkpoint:
logging.info("Loading averaged model")
model_avg.load_state_dict(checkpoint["model_avg"], strict=strict)
checkpoint.pop("model_avg")
if model_ema is not None and "model_ema" in checkpoint:
logging.info("Loading ema model")
model_ema.load_state_dict(checkpoint["model_ema"], strict=strict)
checkpoint.pop("model_ema")
return checkpoint
def load_checkpoint_extend_vocab_size(
filename: Path, extend_size: int, model: nn.Module, strict: bool = True
) -> Dict[str, Any]:
logging.info(f"Loading checkpoint from {filename}")
checkpoint = torch.load(filename, map_location="cpu", weights_only=False)
if model is not None:
if next(iter(checkpoint["model"])).startswith("module."):
logging.info("Loading checkpoint saved by DDP")
dst_state_dict = model.state_dict()
src_state_dict = checkpoint["model"]
for key in dst_state_dict.keys():
src_key = "{}.{}".format("module", key)
dst_state_dict[key] = src_state_dict.pop(src_key)
assert len(src_state_dict) == 0
else:
logging.info("Loading checkpoint")
dst_state_dict = checkpoint["model"]
dst_state_dict["spk_embed.weight"] = model.state_dict()["spk_embed.weight"]
embed_weight = model.state_dict()["embed.weight"]
embed_weight[:-extend_size, :] = dst_state_dict["embed.weight"]
dst_state_dict["embed.weight"] = embed_weight
model.load_state_dict(dst_state_dict, strict=strict)
def load_checkpoint_copy_proj_three_channel_alter(
filename: Path,
in_proj_key: str,
out_proj_key: str,
dim: int,
model: nn.Module,
) -> Dict[str, Any]:
logging.info(f"Loading checkpoint from {filename}")
checkpoint = torch.load(filename, map_location="cpu", weights_only=False)
if model is not None:
if next(iter(checkpoint["model"])).startswith("module."):
logging.info("Loading checkpoint saved by DDP")
dst_state_dict = dict()
src_state_dict = checkpoint["model"]
for key in src_state_dict.keys():
dst_state_dict[key.lstrip("module.")] = src_state_dict.pop(key)
assert len(src_state_dict) == 0
else:
logging.info("Loading checkpoint")
dst_state_dict = checkpoint["model"]
keys = list(dst_state_dict.keys())
for key in keys:
if in_proj_key in key:
if "weight" in key:
weight = dst_state_dict.pop(key)
dst_state_dict[key.replace("weight", "0.weight")] = torch.cat(
[
weight[:, :dim] / 2,
weight[:, :dim] / 2,
weight[:, dim : dim * 2],
weight[:, dim * 2 :] / 2,
weight[:, dim * 2 :] / 2,
],
dim=-1,
)
dst_state_dict[key.replace("weight", "1.weight")] = weight
if "bias" in key:
bias = dst_state_dict.pop(key)
dst_state_dict[key.replace("bias", "0.bias")] = bias
dst_state_dict[key.replace("bias", "1.bias")] = bias
if out_proj_key in key:
if "weight" in key:
weight = dst_state_dict.pop(key)
dst_state_dict[key.replace("weight", "0.weight")] = torch.cat(
[weight, weight], dim=0
)
dst_state_dict[key.replace("weight", "1.weight")] = weight
elif "bias" in key:
bias = dst_state_dict.pop(key)
dst_state_dict[key.replace("bias", "0.bias")] = torch.cat(
[bias, bias], dim=0
)
dst_state_dict[key.replace("bias", "1.bias")] = bias
model.load_state_dict(dst_state_dict, strict=True)
def find_checkpoints(out_dir: Path, iteration: int = 0) -> List[str]:
"""Find all available checkpoints in a directory.
The checkpoint filenames have the form: `checkpoint-xxx.pt`
where xxx is a numerical value.
Assume you have the following checkpoints in the folder `foo`:
- checkpoint-1.pt
- checkpoint-20.pt
- checkpoint-300.pt
- checkpoint-4000.pt
Case 1 (Return all checkpoints)::
find_checkpoints(out_dir='foo')
Case 2 (Return checkpoints newer than checkpoint-20.pt, i.e.,
checkpoint-4000.pt, checkpoint-300.pt, and checkpoint-20.pt)
find_checkpoints(out_dir='foo', iteration=20)
Case 3 (Return checkpoints older than checkpoint-20.pt, i.e.,
checkpoint-20.pt, checkpoint-1.pt)::
find_checkpoints(out_dir='foo', iteration=-20)
Args:
out_dir:
The directory where to search for checkpoints.
iteration:
If it is 0, return all available checkpoints.
If it is positive, return the checkpoints whose iteration number is
greater than or equal to `iteration`.
If it is negative, return the checkpoints whose iteration number is
less than or equal to `-iteration`.
Returns:
Return a list of checkpoint filenames, sorted in descending
order by the numerical value in the filename.
"""
checkpoints = list(glob.glob(f"{out_dir}/checkpoint-[0-9]*.pt"))
pattern = re.compile(r"checkpoint-([0-9]+).pt")
iter_checkpoints = []
for c in checkpoints:
result = pattern.search(c)
if not result:
logging.warn(f"Invalid checkpoint filename {c}")
continue
iter_checkpoints.append((int(result.group(1)), c))
# iter_checkpoints is a list of tuples. Each tuple contains
# two elements: (iteration_number, checkpoint-iteration_number.pt)
iter_checkpoints = sorted(iter_checkpoints, reverse=True, key=lambda x: x[0])
if iteration >= 0:
ans = [ic[1] for ic in iter_checkpoints if ic[0] >= iteration]
else:
ans = [ic[1] for ic in iter_checkpoints if ic[0] <= -iteration]
return ans
def average_checkpoints_with_averaged_model(
filename_start: str,
filename_end: str,
device: torch.device = torch.device("cpu"),
) -> Dict[str, torch.Tensor]:
"""Average model parameters over the range with given
start model (excluded) and end model.
Let start = batch_idx_train of model-start;
end = batch_idx_train of model-end;
interval = end - start.
Then the average model over range from start (excluded) to end is
(1) avg = (model_end * end - model_start * start) / interval.
It can be written as
(2) avg = model_end * weight_end + model_start * weight_start,
where weight_end = end / interval,
weight_start = -start / interval = 1 - weight_end.
Since the terms `weight_end` and `weight_start` would be large
if the model has been trained for lots of batches, which would cause
overflow when multiplying the model parameters.
To avoid this, we rewrite (2) as:
(3) avg = (model_end + model_start * (weight_start / weight_end))
* weight_end
The model index could be epoch number or iteration number.
Args:
filename_start:
Checkpoint filename of the start model. We assume it
is saved by :func:`save_checkpoint`.
filename_end:
Checkpoint filename of the end model. We assume it
is saved by :func:`save_checkpoint`.
device:
Move checkpoints to this device before averaging.
"""
state_dict_start = torch.load(
filename_start, map_location=device, weights_only=False
)
state_dict_end = torch.load(filename_end, map_location=device, weights_only=False)
average_period = state_dict_start["average_period"]
batch_idx_train_start = state_dict_start["batch_idx_train"]
batch_idx_train_start = (batch_idx_train_start // average_period) * average_period
batch_idx_train_end = state_dict_end["batch_idx_train"]
batch_idx_train_end = (batch_idx_train_end // average_period) * average_period
interval = batch_idx_train_end - batch_idx_train_start
assert interval > 0, interval
weight_end = batch_idx_train_end / interval
weight_start = 1 - weight_end
model_end = state_dict_end["model_avg"]
model_start = state_dict_start["model_avg"]
avg = model_end
# scale the weight to avoid overflow
average_state_dict(
state_dict_1=avg,
state_dict_2=model_start,
weight_1=1.0,
weight_2=weight_start / weight_end,
scaling_factor=weight_end,
)
return avg
def remove_checkpoints(
out_dir: Path,
topk: int,
rank: int = 0,
):
"""Remove checkpoints from the given directory.
We assume that checkpoint filename has the form `checkpoint-xxx.pt`
where xxx is a number, representing the number of processed batches
when saving that checkpoint. We sort checkpoints by filename and keep
only the `topk` checkpoints with the highest `xxx`.
Args:
out_dir:
The directory containing checkpoints to be removed.
topk:
Number of checkpoints to keep.
rank:
If using DDP for training, it is the rank of the current node.
Use 0 if no DDP is used for training.
"""
assert topk >= 1, topk
if rank != 0:
return
checkpoints = find_checkpoints(out_dir)
if len(checkpoints) == 0:
logging.warn(f"No checkpoints found in {out_dir}")
return
if len(checkpoints) <= topk:
return
to_remove = checkpoints[topk:]
for c in to_remove:
os.remove(c)
def resume_checkpoint(
params: AttributeDict,
model: nn.Module,
model_avg: nn.Module,
model_ema: Optional[nn.Module] = None,
) -> Optional[Dict[str, Any]]:
"""Load checkpoint from file.
If params.start_epoch is larger than 1, it will load the checkpoint from
`params.start_epoch - 1`.
Apart from loading state dict for `model` and `optimizer` it also updates
`best_train_epoch`, `best_train_loss`, `best_valid_epoch`,
and `best_valid_loss` in `params`.
Args:
params:
The return value of :func:`get_params`.
model:
The training model.
Returns:
Return a dict containing previously saved training info.
"""
filename = params.exp_dir / f"epoch-{params.start_epoch - 1}.pt"
assert filename.is_file(), f"{filename} does not exist!"
saved_params = load_checkpoint(
filename,
model=model,
model_avg=model_avg,
model_ema=model_ema,
strict=True,
)
if params.start_epoch > 1:
keys = [
"best_train_epoch",
"best_valid_epoch",
"batch_idx_train",
"best_train_loss",
"best_valid_loss",
]
for k in keys:
params[k] = saved_params[k]
return saved_params
def average_state_dict(
state_dict_1: Dict[str, torch.Tensor],
state_dict_2: Dict[str, torch.Tensor],
weight_1: float,
weight_2: float,
scaling_factor: float = 1.0,
) -> Dict[str, torch.Tensor]:
"""Average two state_dict with given weights:
state_dict_1 = (state_dict_1 * weight_1 + state_dict_2 * weight_2)
* scaling_factor
It is an in-place operation on state_dict_1 itself.
"""
# Identify shared parameters. Two parameters are said to be shared
# if they have the same data_ptr
uniqued: Dict[int, str] = dict()
for k, v in state_dict_1.items():
v_data_ptr = v.data_ptr()
if v_data_ptr in uniqued:
continue
uniqued[v_data_ptr] = k
uniqued_names = list(uniqued.values())
for k in uniqued_names:
v = state_dict_1[k]
if torch.is_floating_point(v):
v *= weight_1
v += state_dict_2[k].to(device=state_dict_1[k].device) * weight_2
v *= scaling_factor
def update_averaged_model(
params: Dict[str, torch.Tensor],
model_cur: Union[nn.Module, DDP],
model_avg: nn.Module,
) -> None:
"""Update the averaged model:
model_avg = model_cur * (average_period / batch_idx_train)
+ model_avg * ((batch_idx_train - average_period) / batch_idx_train)
Args:
params:
User defined parameters, e.g., epoch, loss.
model_cur:
The current model.
model_avg:
The averaged model to be updated.
"""
weight_cur = params.average_period / params.batch_idx_train
weight_avg = 1 - weight_cur
if isinstance(model_cur, DDP):
model_cur = model_cur.module
cur = model_cur.state_dict()
avg = model_avg.state_dict()
average_state_dict(
state_dict_1=avg,
state_dict_2=cur,
weight_1=weight_avg,
weight_2=weight_cur,
)
def save_checkpoint_with_global_batch_idx(
out_dir: Path,
global_batch_idx: int,
model: Union[nn.Module, DDP],
model_avg: Optional[nn.Module] = None,
params: Optional[Dict[str, Any]] = None,
optimizer: Optional[Optimizer] = None,
scheduler: Optional[LRSchedulerType] = None,
scaler: Optional[GradScaler] = None,
sampler: Optional[CutSampler] = None,
rank: int = 0,
):
"""Save training info after processing given number of batches.
Args:
out_dir:
The directory to save the checkpoint.
global_batch_idx:
The number of batches processed so far from the very start of the
training. The saved checkpoint will have the following filename:
f'out_dir / checkpoint-{global_batch_idx}.pt'
model:
The neural network model whose `state_dict` will be saved in the
checkpoint.
model_avg:
The stored model averaged from the start of training.
params:
A dict of training configurations to be saved.
optimizer:
The optimizer used in the training. Its `state_dict` will be saved.
scheduler:
The learning rate scheduler used in the training. Its `state_dict` will
be saved.
scaler:
The scaler used for mix precision training. Its `state_dict` will
be saved.
sampler:
The sampler used in the training dataset.
rank:
The rank ID used in DDP training of the current node. Set it to 0
if DDP is not used.
"""
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
filename = out_dir / f"checkpoint-{global_batch_idx}.pt"
save_checkpoint(
filename=filename,
model=model,
model_avg=model_avg,
params=params,
optimizer=optimizer,
scheduler=scheduler,
scaler=scaler,
sampler=sampler,
rank=rank,
)

670
zipvoice/utils/common.py Normal file
View File

@@ -0,0 +1,670 @@
import argparse
import collections
import json
import logging
import os
import socket
import subprocess
import sys
import warnings
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union
import torch
from packaging import version
from torch import distributed as dist
from torch import nn
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.tensorboard import SummaryWriter
if hasattr(torch.amp, "GradScaler"):
from torch.amp import GradScaler
else:
from torch.cuda.amp import GradScaler
Pathlike = Union[str, Path]
class AttributeDict(dict):
def __getattr__(self, key):
if key in self:
return self[key]
raise AttributeError(f"No such attribute '{key}'")
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
if key in self:
del self[key]
return
raise AttributeError(f"No such attribute '{key}'")
def __str__(self, indent: int = 2):
tmp = {}
for k, v in self.items():
# PosixPath is ont JSON serializable
if isinstance(v, (Path, torch.device, torch.dtype)):
v = str(v)
tmp[k] = v
return json.dumps(tmp, indent=indent, sort_keys=True)
class MetricsTracker(collections.defaultdict):
def __init__(self):
# Passing the type 'int' to the base-class constructor
# makes undefined items default to int() which is zero.
# This class will play a role as metrics tracker.
# It can record many metrics, including but not limited to loss.
super(MetricsTracker, self).__init__(int)
def __add__(self, other: "MetricsTracker") -> "MetricsTracker":
ans = MetricsTracker()
for k, v in self.items():
ans[k] = v
for k, v in other.items():
if v - v == 0:
ans[k] = ans[k] + v
return ans
def __mul__(self, alpha: float) -> "MetricsTracker":
ans = MetricsTracker()
for k, v in self.items():
ans[k] = v * alpha
return ans
def __str__(self) -> str:
ans_frames = ""
ans_utterances = ""
for k, v in self.norm_items():
norm_value = "%.4g" % v
if "utt_" not in k:
ans_frames += str(k) + "=" + str(norm_value) + ", "
else:
ans_utterances += str(k) + "=" + str(norm_value)
if k == "utt_duration":
ans_utterances += " frames, "
elif k == "utt_pad_proportion":
ans_utterances += ", "
else:
raise ValueError(f"Unexpected key: {k}")
frames = "%.2f" % self["frames"]
ans_frames += "over " + str(frames) + " frames. "
if ans_utterances != "":
utterances = "%.2f" % self["utterances"]
ans_utterances += "over " + str(utterances) + " utterances."
return ans_frames + ans_utterances
def norm_items(self) -> List[Tuple[str, float]]:
"""
Returns a list of pairs, like:
[('ctc_loss', 0.1), ('att_loss', 0.07)]
"""
num_frames = self["frames"] if "frames" in self else 1
num_utterances = self["utterances"] if "utterances" in self else 1
ans = []
for k, v in self.items():
if k == "frames" or k == "utterances":
continue
norm_value = (
float(v) / num_frames if "utt_" not in k else float(v) / num_utterances
)
ans.append((k, norm_value))
return ans
def reduce(self, device):
"""
Reduce using torch.distributed, which I believe ensures that
all processes get the total.
"""
keys = sorted(self.keys())
s = torch.tensor([float(self[k]) for k in keys], device=device)
dist.all_reduce(s, op=dist.ReduceOp.SUM)
for k, v in zip(keys, s.cpu().tolist()):
self[k] = v
def write_summary(
self,
tb_writer: SummaryWriter,
prefix: str,
batch_idx: int,
) -> None:
"""Add logging information to a TensorBoard writer.
Args:
tb_writer: a TensorBoard writer
prefix: a prefix for the name of the loss, e.g. "train/valid_",
or "train/current_"
batch_idx: The current batch index, used as the x-axis of the plot.
"""
for k, v in self.norm_items():
tb_writer.add_scalar(prefix + k, v, batch_idx)
@contextmanager
def torch_autocast(device_type="cuda", **kwargs):
"""
To fix the following warnings:
FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated.
Please use `torch.amp.autocast('cuda', args...)` instead.
with torch.cuda.amp.autocast(enabled=False):
"""
if version.parse(torch.__version__) >= version.parse("2.3.0"):
# Use new unified API
with torch.amp.autocast(device_type=device_type, **kwargs):
yield
else:
# Suppress deprecation warning and use old CUDA-specific autocast
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=FutureWarning)
with torch.cuda.amp.autocast(**kwargs):
yield
def create_grad_scaler(device="cuda", **kwargs):
"""
Creates a GradScaler compatible with both torch < 2.3.0 and >= 2.3.0.
Accepts all kwargs like: enabled, init_scale, growth_factor, etc.
FutureWarning: `torch.cuda.amp.GradScaler(args...)` is deprecated.
Please use `torch.amp.GradScaler('cuda', args...)` instead.
"""
if version.parse(torch.__version__) >= version.parse("2.3.0"):
from torch.amp import GradScaler
return GradScaler(device=device, **kwargs)
else:
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=FutureWarning)
return torch.cuda.amp.GradScaler(**kwargs)
def setup_dist(
rank=None,
world_size=None,
master_port=None,
use_ddp_launch=False,
master_addr=None,
):
"""
rank and world_size are used only if use_ddp_launch is False.
"""
if "MASTER_ADDR" not in os.environ:
os.environ["MASTER_ADDR"] = (
"localhost" if master_addr is None else str(master_addr)
)
if "MASTER_PORT" not in os.environ:
os.environ["MASTER_PORT"] = "12354" if master_port is None else str(master_port)
if use_ddp_launch is False:
dist.init_process_group("nccl", rank=rank, world_size=world_size)
torch.cuda.set_device(rank)
else:
dist.init_process_group("nccl")
def cleanup_dist():
dist.destroy_process_group()
def prepare_input(
params: AttributeDict,
batch: dict,
device: torch.device,
return_tokens: bool = True,
return_feature: bool = True,
return_audio: bool = False,
):
"""
Parse the features and targets of the current batch.
Args:
params:
It is returned by :func:`get_params`.
batch:
It is the return value from iterating
`lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation
for the format of the `batch`.
device:
The device of Tensor.
"""
return_list = []
if return_tokens:
return_list += [batch["tokens"]]
if return_feature:
features = batch["features"].to(device)
features_lens = batch["features_lens"].to(device)
return_list += [features * params.feat_scale, features_lens]
if return_audio:
return_list += [batch["audio"], batch["audio_lens"]]
return return_list
def prepare_avg_tokens_durations(features_lens, tokens_lens):
tokens_durations = []
for i in range(len(features_lens)):
utt_duration = features_lens[i]
avg_token_duration = utt_duration // tokens_lens[i]
tokens_durations.append([avg_token_duration] * tokens_lens[i])
return tokens_durations
def pad_labels(y: List[List[int]], pad_id: int, device: torch.device):
"""
Pad the transcripts to the same length with zeros.
Args:
y: the transcripts, which is a list of a list
Returns:
Return a Tensor of padded transcripts.
"""
y = [token_ids + [pad_id] for token_ids in y]
length = max([len(token_ids) for token_ids in y])
y = [token_ids + [pad_id] * (length - len(token_ids)) for token_ids in y]
return torch.tensor(y, dtype=torch.int64, device=device)
def get_tokens_index(durations: List[List[int]], num_frames: int) -> torch.Tensor:
"""
Gets position in the transcript for each frame, i.e. the position
in the symbol-sequence to look up.
Args:
durations:
Duration of each token in transcripts.
num_frames:
The maximum frame length of the current batch.
Returns:
Return a Tensor of shape (batch_size, num_frames)
"""
durations = [x + [num_frames - sum(x)] for x in durations]
batch_size = len(durations)
ans = torch.zeros(batch_size, num_frames, dtype=torch.int64)
for b in range(batch_size):
this_dur = durations[b]
cur_frame = 0
for i, d in enumerate(this_dur):
ans[b, cur_frame : cur_frame + d] = i
cur_frame += d
assert cur_frame == num_frames, (cur_frame, num_frames)
return ans
def to_int_tuple(s: Union[str, int]):
if isinstance(s, int):
return (s,)
return tuple(map(int, s.split(",")))
def get_adjusted_batch_count(params: AttributeDict) -> float:
# returns the number of batches we would have used so far if we had used the
# reference duration. This is for purposes of set_batch_count().
return (
params.batch_idx_train
* (params.max_duration * params.world_size)
/ params.ref_duration
)
def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None:
if isinstance(model, DDP):
# get underlying nn.Module
model = model.module
for name, module in model.named_modules():
if hasattr(module, "batch_count"):
module.batch_count = batch_count
if hasattr(module, "name"):
module.name = name
def condition_time_mask(
features_lens: torch.Tensor,
mask_percent: Tuple[float, float],
max_len: int = 0,
) -> torch.Tensor:
"""
Apply Time masking.
Args:
features_lens:
input tensor of shape ``(B)``
mask_size:
the width size for masking.
max_len:
the maximum length of the mask.
Returns:
Return a 2-D bool tensor (B, T), where masked positions
are filled with `True` and non-masked positions are
filled with `False`.
"""
mask_size = (
torch.zeros_like(features_lens, dtype=torch.float32).uniform_(*mask_percent)
* features_lens
).to(torch.int64)
mask_starts = (
torch.rand_like(mask_size, dtype=torch.float32) * (features_lens - mask_size)
).to(torch.int64)
mask_ends = mask_starts + mask_size
max_len = max(max_len, features_lens.max())
seq_range = torch.arange(0, max_len, device=features_lens.device)
mask = (seq_range[None, :] >= mask_starts[:, None]) & (
seq_range[None, :] < mask_ends[:, None]
)
return mask
def condition_time_mask_suffix(
features_lens: torch.Tensor,
mask_percent: Tuple[float, float],
max_len: int = 0,
) -> torch.Tensor:
"""
Apply Time masking, mask from the end time index.
Args:
features_lens:
input tensor of shape ``(B)``
mask_size:
the width size for masking.
max_len:
the maximum length of the mask.
Returns:
Return a 2-D bool tensor (B, T), where masked positions
are filled with `True` and non-masked positions are
filled with `False`.
"""
mask_size = (
torch.zeros_like(features_lens, dtype=torch.float32).uniform_(*mask_percent)
* features_lens
).to(torch.int64)
mask_starts = (
torch.ones_like(mask_size, dtype=torch.float32) * (features_lens - mask_size)
).to(torch.int64)
mask_ends = mask_starts + mask_size
max_len = max(max_len, features_lens.max())
seq_range = torch.arange(0, max_len, device=features_lens.device)
mask = (seq_range[None, :] >= mask_starts[:, None]) & (
seq_range[None, :] < mask_ends[:, None]
)
return mask
def make_pad_mask(lengths: torch.Tensor, max_len: int = 0) -> torch.Tensor:
"""
Args:
lengths:
A 1-D tensor containing sentence lengths.
max_len:
The length of masks.
Returns:
Return a 2-D bool tensor, where masked positions
are filled with `True` and non-masked positions are
filled with `False`.
>>> lengths = torch.tensor([1, 3, 2, 5])
>>> make_pad_mask(lengths)
tensor([[False, True, True, True, True],
[False, False, False, True, True],
[False, False, True, True, True],
[False, False, False, False, False]])
"""
assert lengths.ndim == 1, lengths.ndim
max_len = max(max_len, lengths.max())
n = lengths.size(0)
seq_range = torch.arange(0, max_len, device=lengths.device)
expaned_lengths = seq_range.unsqueeze(0).expand(n, max_len)
return expaned_lengths >= lengths.unsqueeze(-1)
def str2bool(v):
"""Used in argparse.ArgumentParser.add_argument to indicate
that a type is a bool type and user can enter
- yes, true, t, y, 1, to represent True
- no, false, f, n, 0, to represent False
See https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse # noqa
"""
if isinstance(v, bool):
return v
if v.lower() in ("yes", "true", "t", "y", "1"):
return True
elif v.lower() in ("no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError("Boolean value expected.")
def setup_logger(
log_filename: Pathlike,
log_level: str = "info",
use_console: bool = True,
) -> None:
"""Setup log level.
Args:
log_filename:
The filename to save the log.
log_level:
The log level to use, e.g., "debug", "info", "warning", "error",
"critical"
use_console:
True to also print logs to console.
"""
now = datetime.now()
date_time = now.strftime("%Y-%m-%d-%H-%M-%S")
if dist.is_available() and dist.is_initialized():
world_size = dist.get_world_size()
rank = dist.get_rank()
formatter = f"%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] ({rank}/{world_size}) %(message)s" # noqa
log_filename = f"{log_filename}-{date_time}-{rank}"
else:
formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s"
log_filename = f"{log_filename}-{date_time}"
os.makedirs(os.path.dirname(log_filename), exist_ok=True)
level = logging.ERROR
if log_level == "debug":
level = logging.DEBUG
elif log_level == "info":
level = logging.INFO
elif log_level == "warning":
level = logging.WARNING
elif log_level == "critical":
level = logging.CRITICAL
logging.basicConfig(
filename=log_filename,
format=formatter,
level=level,
filemode="w",
force=True,
)
if use_console:
console = logging.StreamHandler()
console.setLevel(level)
console.setFormatter(logging.Formatter(formatter))
logging.getLogger("").addHandler(console)
def get_git_sha1():
try:
git_commit = (
subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
check=True,
stdout=subprocess.PIPE,
)
.stdout.decode()
.rstrip("\n")
.strip()
)
dirty_commit = (
len(
subprocess.run(
["git", "diff", "--shortstat"],
check=True,
stdout=subprocess.PIPE,
)
.stdout.decode()
.rstrip("\n")
.strip()
)
> 0
)
git_commit = git_commit + "-dirty" if dirty_commit else git_commit + "-clean"
except: # noqa
return None
return git_commit
def get_git_date():
try:
git_date = (
subprocess.run(
["git", "log", "-1", "--format=%ad", "--date=local"],
check=True,
stdout=subprocess.PIPE,
)
.stdout.decode()
.rstrip("\n")
.strip()
)
except: # noqa
return None
return git_date
def get_git_branch_name():
try:
git_date = (
subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
check=True,
stdout=subprocess.PIPE,
)
.stdout.decode()
.rstrip("\n")
.strip()
)
except: # noqa
return None
return git_date
def get_env_info() -> Dict[str, Any]:
"""Get the environment information."""
return {
"torch-version": str(torch.__version__),
"torch-cuda-available": torch.cuda.is_available(),
"torch-cuda-version": torch.version.cuda,
"python-version": sys.version[:4],
"zipvoice-git-branch": get_git_branch_name(),
"zipvoice-git-sha1": get_git_sha1(),
"zipvoice-git-date": get_git_date(),
"zipvoice-path": str(Path(__file__).resolve().parent.parent),
"hostname": socket.gethostname(),
"IP address": socket.gethostbyname(socket.gethostname()),
}
def get_parameter_groups_with_lrs(
model: nn.Module,
lr: float,
include_names: bool = False,
freeze_modules: List[str] = [],
unfreeze_modules: List[str] = [],
) -> List[dict]:
"""
This is for use with the ScaledAdam optimizers (more recent versions that accept
lists of named-parameters; we can, if needed, create a version without the names).
It provides a way to specify learning-rate scales inside the module, so that if
any nn.Module in the hierarchy has a floating-point parameter 'lr_scale', it will
scale the LR of any parameters inside that module or its submodules. Note: you
can set module parameters outside the __init__ function, e.g.:
>>> a = nn.Linear(10, 10)
>>> a.lr_scale = 0.5
Returns: a list of dicts, of the following form:
if include_names == False:
[ { 'params': [ tensor1, tensor2, ... ], 'lr': 0.01 },
{ 'params': [ tensor3, tensor4, ... ], 'lr': 0.005 },
... ]
if include_names == true:
[ { 'named_params': [ (name1, tensor1, (name2, tensor2), ... ], 'lr': 0.01 },
{ 'named_params': [ (name3, tensor3), (name4, tensor4), ... ], 'lr': 0.005 },
... ]
"""
# Use freeze_modules or unfreeze_modules to freeze or unfreeze modules
assert not (len(freeze_modules) and len(unfreeze_modules))
# flat_lr_scale just contains the lr_scale explicitly specified
# for each prefix of the name, e.g. 'encoder.layers.3', these need
# to be multiplied for all prefix of the name of any given parameter.
flat_lr_scale = defaultdict(lambda: 1.0)
names = []
for name, m in model.named_modules():
names.append(name)
if hasattr(m, "lr_scale"):
flat_lr_scale[name] = m.lr_scale
# lr_to_parames is a dict from learning rate (floating point) to: if
# include_names == true, a list of (name, parameter) for that learning rate;
# otherwise a list of parameters for that learning rate.
lr_to_params = defaultdict(list)
for name, parameter in model.named_parameters():
if not parameter.requires_grad:
logging.info(f"Remove {name} from parameter")
continue
split_name = name.split(".")
# caution: as a special case, if the name is '', split_name will be [ '' ].
prefix = split_name[0]
if len(freeze_modules) > 0:
if prefix == "module": # DDP
module_name = split_name[1]
if module_name in freeze_modules:
logging.info(f"Remove {name} from parameters")
continue
else:
if prefix in freeze_modules:
logging.info(f"Remove {name} from parameters")
continue
elif len(unfreeze_modules) > 0:
if prefix == "module": # DDP
module_name = split_name[1]
if module_name not in unfreeze_modules:
logging.info(f"Remove {name} from parameters")
continue
else:
if prefix not in unfreeze_modules:
logging.info(f"Remove {name} from parameters")
continue
cur_lr = lr * flat_lr_scale[prefix]
if prefix != "":
cur_lr *= flat_lr_scale[""]
for part in split_name[1:]:
prefix = ".".join([prefix, part])
cur_lr *= flat_lr_scale[prefix]
lr_to_params[cur_lr].append((name, parameter) if include_names else parameter)
if include_names:
return [{"named_params": pairs, "lr": lr} for lr, pairs in lr_to_params.items()]
else:
return [{"params": params, "lr": lr} for lr, params in lr_to_params.items()]

View File

@@ -0,0 +1,723 @@
# Copyright 2022-2024 Xiaomi Corp. (authors: Daniel Povey
# Zengwei Yao
# Mingshuang Luo,
# Zengrui Jin,)
#
# See ../LICENSE for clarification regarding multiple authors
#
# 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 logging
import random
from dataclasses import dataclass
from typing import Optional, Tuple
import torch
from torch import Tensor, nn
class TensorDiagnosticOptions(object):
"""Options object for tensor diagnostics:
Args:
max_eig_dim:
The maximum dimension for which we print out eigenvalues
(limited for speed reasons).
"""
def __init__(self, max_eig_dim: int = 512):
self.max_eig_dim = max_eig_dim
def dim_is_summarized(self, size: int):
return size > 10 and size != 31
def get_tensor_stats(
x: Tensor,
dim: int,
stats_type: str,
) -> Tuple[Tensor, int]:
"""
Returns the specified transformation of the Tensor (either x or x.abs()
or (x > 0), summed over all but the index `dim`.
Args:
x:
Tensor, tensor to be analyzed
dim:
Dimension with 0 <= dim < x.ndim
stats_type:
The stats_type includes several types:
"abs" -> take abs() before summing
"positive" -> take (x > 0) before summing
"rms" -> square before summing, we'll take sqrt later
"value" -> just sum x itself
"max", "min" -> take the maximum or minimum [over all other dims but dim]
instead of summing
"rms-sort" -> this is a bit different than the others, it's based on computing
the rms over the specified dim and returning percentiles of the result
(11 of them).
Returns:
stats: a Tensor of shape (x.shape[dim],).
count: an integer saying how many items were counted in each element
of stats.
"""
if stats_type == "rms-sort":
rms = (x**2).mean(dim=dim).sqrt()
rms = rms.flatten()
rms = rms.sort()[0]
rms = rms[(torch.arange(11) * rms.numel() // 10).clamp(max=rms.numel() - 1)]
count = 1.0
return rms, count
count = x.numel() // x.shape[dim]
if stats_type == "eigs":
x = x.transpose(dim, -1)
x = x.reshape(-1, x.shape[-1])
# shape of returned tensor: (s, s),
# where s is size of dimension `dim` of original x.
return torch.matmul(x.transpose(0, 1), x), count
elif stats_type == "abs":
x = x.abs()
elif stats_type == "rms":
x = x**2
elif stats_type == "positive":
x = (x > 0).to(dtype=torch.float)
else:
assert stats_type in ["value", "max", "min"]
sum_dims = [d for d in range(x.ndim) if d != dim]
if len(sum_dims) > 0:
if stats_type == "max":
for dim in reversed(sum_dims):
x = torch.max(x, dim=dim)[0]
elif stats_type == "min":
for dim in reversed(sum_dims):
x = torch.min(x, dim=dim)[0]
else:
x = torch.sum(x, dim=sum_dims)
x = x.flatten().clone()
return x, count
@dataclass
class TensorAndCount:
tensor: Tensor
count: int
class TensorDiagnostic(object):
"""This class is not directly used by the user, it is responsible for
collecting diagnostics for a module or parameter tensor of a torch.nn.Module.
Args:
opts:
Options object.
name:
The name associated with this diagnostics object, will probably be
{module_name}.X where X is "output" or "grad", or {parameter_name}.
Y where Y is param_value or param_grad.
"""
def __init__(self, opts: TensorDiagnosticOptions, name: str):
self.opts = opts
self.name = name
self.class_name = None # will assign in accumulate()
self.stats = None # we'll later assign a list to self.stats.
# It's a list of dicts, indexed by dim (i.e. by the
# axis of the tensor). The dicts, in turn, are
# indexed by `stats-type` which are strings in
# ["abs", "max", "min", "positive", "value", "rms"].
# scalar_stats contains some analysis of the activations and gradients,
self.scalar_stats = None
# the keys into self.stats[dim] are strings, whose values can be
# "abs", "max", "min" ,"value", "positive", "rms", "value".
# The values e.g. self.stats[dim]["rms"] are lists of dataclass TensorAndCount,
# containing a tensor and its associated count (which is the sum of the other
# dims that we aggregated over, e.g. the number of frames and/or batch elements
# and/or channels.
# ... we actually accumulate the Tensors / counts any time we have the same-dim
# tensor, only adding a new element to the list if there was a different dim.
# if the string in the key is "eigs", if we detect a length mismatch we put None
# as the value.
def accumulate(self, x, class_name: Optional[str] = None):
"""
Accumulate tensors.
"""
if class_name is not None:
self.class_name = class_name
if isinstance(x, Tuple):
x = x[0]
if not isinstance(x, Tensor):
return
if x.numel() == 0: # for empty tensor
return
x = x.detach().clone()
if x.ndim == 0:
x = x.unsqueeze(0)
ndim = x.ndim
if self.stats is None:
self.stats = [dict() for _ in range(ndim)]
for dim in range(ndim):
this_dim_stats = self.stats[dim]
if ndim > 1:
# rms-sort is different from the others, it's based on summing over just
# this dim, then sorting and returning the percentiles.
stats_types = [
"abs",
"max",
"min",
"positive",
"value",
"rms",
"rms-sort",
]
if x.shape[dim] <= self.opts.max_eig_dim:
stats_types.append("eigs")
else:
stats_types = ["value", "abs", "max", "min"]
for stats_type in stats_types:
stats, count = get_tensor_stats(x, dim, stats_type)
if stats_type not in this_dim_stats:
this_dim_stats[stats_type] = [] # list of TensorAndCount
done = False
if this_dim_stats[stats_type] is None:
# we can reach here if we detected for stats_type "eigs" that
# where was more than one different size for this dim. Then we
# disable accumulating this stats type, as it uses too much memory.
continue
for s in this_dim_stats[stats_type]:
if s.tensor.shape == stats.shape:
if stats_type == "max":
s.tensor = torch.maximum(s.tensor, stats)
elif stats_type == "min":
s.tensor = torch.minimum(s.tensor, stats)
else:
assert stats_type != "max"
s.tensor += stats
s.count += count
done = True
break
if not done:
if this_dim_stats[stats_type] != [] and stats_type == "eigs":
# >1 size encountered on this dim, e.g. it's a batch or time
# dimension, don't accumulat "eigs" stats type, it uses too much
# memory
this_dim_stats[stats_type] = None
else:
this_dim_stats[stats_type].append(TensorAndCount(stats, count))
def print_diagnostics(self):
"""Print diagnostics for each dimension of the tensor."""
if self.stats is None:
print(f"Warning: the stats of {self.name} is None.")
return
for dim, this_dim_stats in enumerate(self.stats):
if "rms" in this_dim_stats and "value" in this_dim_stats:
# produce "stddev" stats, which is centered RMS.
rms_stats_list = this_dim_stats["rms"]
value_stats_list = this_dim_stats["value"]
if len(rms_stats_list) == len(value_stats_list):
stddev_stats_list = []
for r, v in zip(rms_stats_list, value_stats_list):
stddev_stats_list.append(
# r.count and v.count should be the same, but we don't check
# this.
TensorAndCount(
r.tensor - v.tensor * v.tensor / (v.count + 1.0e-20),
r.count,
)
)
this_dim_stats["stddev"] = stddev_stats_list
for stats_type, stats_list in this_dim_stats.items():
# stats_type could be "rms", "value", "abs", "eigs", "positive", "min"
# or "max". "stats_list" could be a list of TensorAndCount (one list per
# distinct tensor shape of the stats), or None
if stats_list is None:
assert stats_type == "eigs"
continue
def get_count(count):
return 1 if stats_type in ["max", "min"] else count
if len(stats_list) == 1:
stats = stats_list[0].tensor / get_count(stats_list[0].count)
else:
# a dimension that has variable size in different nnet
# forwards, e.g. a time dimension in an ASR model.
stats = torch.cat(
[x.tensor / get_count(x.count) for x in stats_list], dim=0
)
if stats_type == "eigs":
try:
if hasattr(torch, "linalg") and hasattr(torch.linalg, "eigh"):
eigs, _ = torch.linalg.eigh(stats)
else:
eigs, _ = torch.symeig(stats)
stats = eigs.abs().sqrt()
except: # noqa
print("Error getting eigenvalues, trying another method.")
if hasattr(torch, "linalg") and hasattr(torch.linalg, "eig"):
eigs, _ = torch.linalg.eig(stats)
eigs = eigs.abs()
else:
eigs, _ = torch.eig(stats)
eigs = eigs.norm(dim=1)
stats = eigs.sqrt()
# sqrt so it reflects data magnitude, like stddev- not variance
if stats_type in ["rms", "stddev"]:
# we stored the square; after aggregation we need to take sqrt.
stats = stats.sqrt()
# if `summarize` we print percentiles of the stats; else,
# we print out individual elements.
summarize = (len(stats_list) > 1) or self.opts.dim_is_summarized(
stats.numel()
)
if summarize: # usually `summarize` will be true
# print out percentiles.
stats = stats.sort()[0]
num_percentiles = 10
size = stats.numel()
percentiles = []
for i in range(num_percentiles + 1):
index = (i * (size - 1)) // num_percentiles
percentiles.append(stats[index].item())
percentiles = ["%.2g" % x for x in percentiles]
percentiles = " ".join(percentiles)
ans = f"percentiles: [{percentiles}]"
else:
ans = stats.tolist()
ans = ["%.2g" % x for x in ans]
ans = "[" + " ".join(ans) + "]"
if stats_type in ["value", "rms", "stddev", "eigs"]:
# This norm is useful because it is strictly less than the largest
# sqrt(eigenvalue) of the variance, which we print out, and shows,
# speaking in an approximate way, how much of that largest
# eigenvalue can be attributed to the mean of the distribution.
norm = (stats**2).sum().sqrt().item()
ans += f", norm={norm:.2g}"
mean = stats.mean().item()
rms = (stats**2).mean().sqrt().item()
ans += f", mean={mean:.3g}, rms={rms:.3g}"
# OK, "ans" contains the actual stats, e.g.
# ans = "percentiles: \
# [0.43 0.46 0.48 0.49 0.49 0.5 0.51 0.52 0.53 0.54 0.59], \
# mean=0.5, rms=0.5"
sizes = [x.tensor.shape[0] for x in stats_list]
size_str = (
f"{sizes[0]}" if len(sizes) == 1 else f"{min(sizes)}..{max(sizes)}"
)
maybe_class_name = (
f" type={self.class_name}," if self.class_name is not None else ""
)
print(
f"module={self.name},{maybe_class_name} dim={dim}, size={size_str}, "
f"{stats_type} {ans}"
)
class ScalarDiagnostic(object):
"""This class is not directly used by the user, it is responsible for
collecting diagnostics for a single module (subclass of torch.nn.Module) that
represents some kind of nonlinearity, e.g. ReLU, sigmoid, etc.
"""
def __init__(self, opts: TensorDiagnosticOptions, name: str):
self.opts = opts
self.name = name
self.class_name = None # will assign in accumulate()
self.is_forward_pass = True
self.tick_scale = None
self.saved_inputs = []
self.is_ok = True
self.counts = None
self.sum_grad = None
self.sum_gradsq = None
self.sum_abs_grad = None
def accumulate_input(self, x: Tensor, class_name: Optional[str] = None):
"""
Called in forward pass.
"""
if not self.is_forward_pass:
# in case we did a forward pass without a backward pass, for some reason.
self.saved_inputs = []
self.is_forward_pass = True
if class_name is not None:
self.class_name = class_name
if not self.is_ok:
return
limit = 10
if len(self.saved_inputs) > limit:
print(
f"ERROR: forward pass called for this module over {limit} times "
f"with no backward pass. Will not accumulate scalar stats."
)
self.is_ok = False
return
self.saved_inputs.append(x)
def accumulate_output_grad(self, grad: Tensor):
if not self.is_ok:
return
if self.is_forward_pass:
self.is_forward_pass = False
last_shape = (
"n/a" if len(self.saved_inputs) == 0 else self.saved_inputs[-1].shape
)
if len(self.saved_inputs) == 0 or grad.shape != last_shape:
print(
f"ERROR: shape mismatch or no forward activation present when backward "
f"pass called: grad shape ={tuple(grad.shape)}"
f", num-saved-inputs={len(self.saved_inputs)}"
f", shape-of-last-saved-input={last_shape}"
)
self.is_ok = False
return
x = self.saved_inputs.pop()
self.process_input_and_grad(x, grad)
def process_input_and_grad(self, x: Tensor, grad: Tensor):
assert x.shape == grad.shape
x = x.flatten()
grad = grad.flatten()
num_ticks_per_side = 256
if self.tick_scale is None:
x_abs_sorted = x.abs().sort()[0]
# take the 98th percentile as the largest value we count separately.
index = int(x.numel() * 0.98)
self.tick_scale = float(x_abs_sorted[index] / num_ticks_per_side)
# integerize from tick * (-num ticks_per_side .. num_ticks_per_side - 1]
self.counts = torch.zeros(
2 * num_ticks_per_side, dtype=torch.long, device=x.device
)
self.sum_grad = torch.zeros(
2 * num_ticks_per_side, dtype=torch.double, device=x.device
)
# sum_gradsq is for getting error bars.
self.sum_gradsq = torch.zeros(
2 * num_ticks_per_side, dtype=torch.double, device=x.device
)
self.sum_abs_grad = torch.zeros(
2 * num_ticks_per_side, dtype=torch.double, device=x.device
)
# this will round down.
x = (x / self.tick_scale).to(torch.long)
x = x.clamp_(min=-num_ticks_per_side, max=num_ticks_per_side - 1)
x = x + num_ticks_per_side
self.counts.index_add_(dim=0, index=x, source=torch.ones_like(x))
self.sum_grad.index_add_(dim=0, index=x, source=grad.to(torch.double))
self.sum_gradsq.index_add_(
dim=0, index=x, source=(grad * grad).to(torch.double)
)
self.sum_abs_grad.index_add_(dim=0, index=x, source=grad.abs().to(torch.double))
def print_diagnostics(self):
"""Print diagnostics."""
if self.is_ok is False or self.counts is None:
print(f"Warning: no stats accumulated for {self.name}, is_ok={self.is_ok}")
return
counts = self.counts.to("cpu")
sum_grad = self.sum_grad.to(device="cpu", dtype=torch.float32)
sum_gradsq = self.sum_gradsq.to(device="cpu", dtype=torch.float32)
sum_abs_grad = self.sum_abs_grad.to(device="cpu", dtype=torch.float32)
counts_cumsum = counts.cumsum(dim=0)
counts_tot = counts_cumsum[-1]
# subdivide the distribution up into `num_bins` intervals for analysis, for
# greater statistical significance. each bin corresponds to multiple of the
# original 'tick' intervals.
num_bins = 20
# integer division
counts_per_bin = (counts_tot // num_bins) + 1
bin_indexes = counts_cumsum // counts_per_bin
bin_indexes = bin_indexes.clamp(min=0, max=num_bins).to(torch.long)
bin_counts = torch.zeros(num_bins, dtype=torch.long)
bin_counts.index_add_(dim=0, index=bin_indexes, source=counts)
bin_grad = torch.zeros(num_bins)
bin_grad.index_add_(dim=0, index=bin_indexes, source=sum_grad)
bin_gradsq = torch.zeros(num_bins)
bin_gradsq.index_add_(dim=0, index=bin_indexes, source=sum_gradsq)
bin_abs_grad = torch.zeros(num_bins)
bin_abs_grad.index_add_(dim=0, index=bin_indexes, source=sum_abs_grad)
bin_boundary_counts = (
torch.arange(num_bins + 1, dtype=torch.long) * counts_per_bin
)
bin_tick_indexes = torch.searchsorted(counts_cumsum, bin_boundary_counts)
# boundaries are the "x" values between the bins, e.g. corresponding to the
# locations of percentiles of the distribution.
num_ticks_per_side = counts.numel() // 2
bin_boundaries = (bin_tick_indexes - num_ticks_per_side) * self.tick_scale
bin_grad = bin_grad / (bin_counts + 1)
bin_conf_interval = bin_gradsq.sqrt() / (
bin_counts + 1
) # consider this a standard deviation.
# bin_grad / bin_abs_grad will give us a sense for how important in a practical
# sense, the gradients are.
bin_abs_grad = bin_abs_grad / (bin_counts + 1)
bin_rel_grad = bin_grad / (bin_abs_grad + 1.0e-20)
bin_conf = bin_grad / (bin_conf_interval + 1.0e-20)
def tensor_to_str(x: Tensor):
x = ["%.2g" % f for f in x]
x = "[" + " ".join(x) + "]"
return x
maybe_class_name = (
f" type={self.class_name}," if self.class_name is not None else ""
)
print(
f"module={self.name},{maybe_class_name} "
f"bin-boundaries={tensor_to_str(bin_boundaries)}, "
f"rel_grad={tensor_to_str(bin_rel_grad)}, "
f"grad_conf={tensor_to_str(bin_conf)}"
)
class ModelDiagnostic(object):
"""This class stores diagnostics for all tensors in the torch.nn.Module.
Args:
opts:
Options object.
"""
def __init__(self, opts: Optional[TensorDiagnosticOptions] = None):
# In this dictionary, the keys are tensors names and the values
# are corresponding TensorDiagnostic objects.
if opts is None:
self.opts = TensorDiagnosticOptions()
else:
self.opts = opts
self.diagnostics = dict()
def __getitem__(self, name: str):
T = ScalarDiagnostic if name[-7:] == ".scalar" else TensorDiagnostic
if name not in self.diagnostics:
self.diagnostics[name] = T(self.opts, name)
return self.diagnostics[name]
def print_diagnostics(self):
"""Print diagnostics for each tensor."""
for k in sorted(self.diagnostics.keys()):
self.diagnostics[k].print_diagnostics()
def get_class_name(module: nn.Module):
ans = type(module).__name__
# we put the below in try blocks in case anyone is using a different version of
# these modules that might have different member names.
if ans == "Balancer" or ans == "ActivationBalancer":
try:
ans += f"[{float(module.min_positive)},{float(module.max_positive)},"
f"{float(module.min_abs)},{float(module.max_abs)}]"
except:
pass
elif ans == "AbsValuePenalizer":
try:
ans += f"[{module.limit}]"
except:
pass
return ans
def attach_diagnostics(
model: nn.Module, opts: Optional[TensorDiagnosticOptions] = None
) -> ModelDiagnostic:
"""Attach a ModelDiagnostic object to the model by
1) registering forward hook and backward hook on each module, to accumulate
its output tensors and gradient tensors, respectively;
2) registering backward hook on each module parameter, to accumulate its
values and gradients.
Args:
model:
the model to be analyzed.
opts:
Options object.
Returns:
The ModelDiagnostic object attached to the model.
"""
ans = ModelDiagnostic(opts)
for name, module in model.named_modules():
if name == "":
name = "<top-level>"
# Setting model_diagnostic=ans and n=name below, instead of trying to
# capture the variables, ensures that we use the current values.
# (this matters for `name`, since the variable gets overwritten).
# These closures don't really capture by value, only by
# "the final value the variable got in the function" :-(
def forward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name):
if isinstance(_output, tuple) and len(_output) == 1:
_output = _output[0]
if isinstance(_output, Tensor) and _output.dtype in (
torch.float32,
torch.float16,
torch.float64,
):
_model_diagnostic[f"{_name}.output"].accumulate(
_output, class_name=get_class_name(_module)
)
elif isinstance(_output, tuple):
for i, o in enumerate(_output):
if isinstance(o, Tensor) and o.dtype in (
torch.float32,
torch.float16,
torch.float64,
):
_model_diagnostic[f"{_name}.output[{i}]"].accumulate(
o, class_name=get_class_name(_module)
)
def backward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name):
if isinstance(_output, tuple) and len(_output) == 1:
_output = _output[0]
if isinstance(_output, Tensor) and _output.dtype in (
torch.float32,
torch.float16,
torch.float64,
):
_model_diagnostic[f"{_name}.grad"].accumulate(
_output, class_name=get_class_name(_module)
)
elif isinstance(_output, tuple):
for i, o in enumerate(_output):
if isinstance(o, Tensor) and o.dtype in (
torch.float32,
torch.float16,
torch.float64,
):
_model_diagnostic[f"{_name}.grad[{i}]"].accumulate(
o, class_name=get_class_name(_module)
)
module.register_forward_hook(forward_hook)
module.register_backward_hook(backward_hook)
if type(module).__name__ in [
"Sigmoid",
"Tanh",
"ReLU",
"TanSwish",
"Swish",
"DoubleSwish",
"Swoosh",
]:
# For these specific module types, accumulate some additional diagnostics
# that can help us improve the activation function. These require a lot of
# memory, to save the forward activations, so limit this to some select
# classes. Note: this will not work correctly for all model types.
def scalar_forward_hook(
_module, _input, _output, _model_diagnostic=ans, _name=name
):
if isinstance(_input, tuple):
(_input,) = _input
assert isinstance(_input, Tensor)
_model_diagnostic[f"{_name}.scalar"].accumulate_input(
_input, class_name=get_class_name(_module)
)
def scalar_backward_hook(
_module, _input, _output, _model_diagnostic=ans, _name=name
):
if isinstance(_output, tuple):
(_output,) = _output
assert isinstance(_output, Tensor)
_model_diagnostic[f"{_name}.scalar"].accumulate_output_grad(_output)
module.register_forward_hook(scalar_forward_hook)
module.register_backward_hook(scalar_backward_hook)
for name, parameter in model.named_parameters():
def param_backward_hook(
grad, _parameter=parameter, _model_diagnostic=ans, _name=name
):
_model_diagnostic[f"{_name}.param_value"].accumulate(_parameter)
_model_diagnostic[f"{_name}.param_grad"].accumulate(grad)
try:
parameter.register_hook(param_backward_hook)
except:
logging.warning(
f"Warning: could not register backward hook for parameter {name}, "
f"it might not be differentiable."
)
return ans
def _test_tensor_diagnostic():
opts = TensorDiagnosticOptions(512)
diagnostic = TensorDiagnostic(opts, "foo")
for _ in range(10):
diagnostic.accumulate(torch.randn(50, 100) * 10.0)
diagnostic.print_diagnostics()
model = nn.Sequential(nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 80))
diagnostic = attach_diagnostics(model, opts)
for _ in range(10):
T = random.randint(200, 300)
x = torch.randn(T, 100)
y = model(x)
y.sum().backward()
diagnostic.print_diagnostics()
if __name__ == "__main__":
_test_tensor_diagnostic()

120
zipvoice/utils/feature.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright 2024 Xiaomi Corp. (authors: Han Zhu)
#
# See ../../../../LICENSE for clarification regarding multiple authors
#
# 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.
from dataclasses import dataclass
from typing import Union
import numpy as np
import torch
import torchaudio
from lhotse.features.base import FeatureExtractor, register_extractor
from lhotse.utils import Seconds, compute_num_frames
@dataclass
class VocosFbankConfig:
sampling_rate: int = 24000
n_mels: int = 100
n_fft: int = 1024
hop_length: int = 256
@register_extractor
class VocosFbank(FeatureExtractor):
name = "VocosFbank"
config_type = VocosFbankConfig
def __init__(self, num_channels: int = 1):
config = VocosFbankConfig
super().__init__(config=config)
assert num_channels in (1, 2)
self.num_channels = num_channels
self.fbank = torchaudio.transforms.MelSpectrogram(
sample_rate=self.config.sampling_rate,
n_fft=self.config.n_fft,
hop_length=self.config.hop_length,
n_mels=self.config.n_mels,
center=True,
power=1,
)
def _feature_fn(self, sample):
mel = self.fbank(sample)
logmel = mel.clamp(min=1e-7).log()
return logmel
@property
def device(self) -> Union[str, torch.device]:
return self.config.device
def feature_dim(self, sampling_rate: int) -> int:
return self.config.n_mels
def extract(
self,
samples: Union[np.ndarray, torch.Tensor],
sampling_rate: int,
) -> Union[np.ndarray, torch.Tensor]:
# Check for sampling rate compatibility.
expected_sr = self.config.sampling_rate
assert sampling_rate == expected_sr, (
f"Mismatched sampling rate: extractor expects {expected_sr}, "
f"got {sampling_rate}"
)
is_numpy = False
if not isinstance(samples, torch.Tensor):
samples = torch.from_numpy(samples)
is_numpy = True
if len(samples.shape) == 1:
samples = samples.unsqueeze(0)
else:
assert samples.ndim == 2, samples.shape
if self.num_channels == 1:
if samples.shape[0] == 2:
samples = samples.mean(dim=0, keepdims=True)
else:
assert samples.shape[0] == 2, samples.shape
mel = self._feature_fn(samples)
# (1, n_mels, time) or (2, n_mels, time)
mel = mel.reshape(-1, mel.shape[-1]).t()
# (time, n_mels) or (time, 2 * n_mels)
num_frames = compute_num_frames(
samples.shape[1] / sampling_rate, self.frame_shift, sampling_rate
)
if mel.shape[0] > num_frames:
mel = mel[:num_frames]
elif mel.shape[0] < num_frames:
mel = mel.unsqueeze(0)
mel = torch.nn.functional.pad(
mel, (0, 0, 0, num_frames - mel.shape[1]), mode="replicate"
).squeeze(0)
if is_numpy:
return mel.cpu().numpy()
else:
return mel
@property
def frame_shift(self) -> Seconds:
return self.config.hop_length / self.config.sampling_rate

111
zipvoice/utils/hooks.py Normal file
View File

@@ -0,0 +1,111 @@
# Copyright 2021-2024 Xiaomi Corporation (authors: Zengwei Yao,
# Daniel Povey,
# Zengrui Jin,)
#
# See ../../LICENSE for clarification regarding multiple authors
#
# 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 logging
import random
import torch
from torch import Tensor, nn
def register_inf_check_hooks(model: nn.Module) -> None:
"""Registering forward hook on each module, to check
whether its output tensors is not finite.
Args:
model:
the model to be analyzed.
"""
for name, module in model.named_modules():
if name == "":
name = "<top-level>"
# default param _name is a way to capture the current value of the variable
# "name".
def forward_hook(_module, _input, _output, _name=name):
if isinstance(_output, Tensor):
try:
if not torch.isfinite(_output.to(torch.float32).sum()):
logging.warning(f"The sum of {_name}.output is not finite")
except RuntimeError: # e.g. CUDA out of memory
pass
elif isinstance(_output, tuple):
for i, o in enumerate(_output):
if isinstance(o, tuple):
o = o[0]
if not isinstance(o, Tensor):
continue
try:
if not torch.isfinite(o.to(torch.float32).sum()):
logging.warning(
f"The sum of {_name}.output[{i}] is not finite"
)
except RuntimeError: # e.g. CUDA out of memory
pass
# default param _name is a way to capture the current value of the variable
# "name".
def backward_hook(_module, _input, _output, _name=name):
if isinstance(_output, Tensor):
try:
if not torch.isfinite(_output.to(torch.float32).sum()):
logging.warning(f"The sum of {_name}.grad is not finite")
except RuntimeError: # e.g. CUDA out of memory
pass
elif isinstance(_output, tuple):
for i, o in enumerate(_output):
if isinstance(o, tuple):
o = o[0]
if not isinstance(o, Tensor):
continue
if not torch.isfinite(o.to(torch.float32).sum()):
logging.warning(f"The sum of {_name}.grad[{i}] is not finite")
module.register_forward_hook(forward_hook)
module.register_backward_hook(backward_hook)
for name, parameter in model.named_parameters():
def param_backward_hook(grad, _name=name):
if not torch.isfinite(grad.to(torch.float32).sum()):
logging.warning(f"The sum of {_name}.param_grad is not finite")
try:
parameter.register_hook(param_backward_hook)
except Exception as e:
logging.warning(
f"Warning: could not register backward hook for parameter {name}"
f" with error {e}, it might not be differentiable."
)
def _test_inf_check_hooks():
model = nn.Sequential(nn.Linear(100, 50), nn.Linear(50, 80))
register_inf_check_hooks(model)
for _ in range(10):
T = random.randint(200, 300)
x = torch.randn(T, 100) + float("inf") * (T % 2)
y = model(x)
y.sum().backward()
if __name__ == "__main__":
_test_inf_check_hooks()

414
zipvoice/utils/infer.py Normal file
View File

@@ -0,0 +1,414 @@
from typing import List
import numpy as np
import torch
import torchaudio
from pydub import AudioSegment
from pydub.silence import detect_leading_silence, split_on_silence
punctuation = {";", ":", ",", ".", "!", "?", "", "", "", "", "", ""}
def chunk_tokens_punctuation(tokens_list: List[str], max_tokens: int = 100):
"""
Splits the input tokens list into chunks according to punctuations,
each with a maximum number of tokens.
Args:
token_list (list of str): The list of tokens to be split.
max_tokens (int): The maximum number of tokens per chunk.
Returns:
List[str]: A list of text chunks.
"""
# 1. Split the tokens according to punctuations.
sentences = []
current_sentence = []
for token in tokens_list:
# If the first token of current sentence is punctuation or blank,
# append it to the end of the previous sentence.
if (
len(current_sentence) == 0
and len(sentences) != 0
and (token in punctuation or token == " ")
):
sentences[-1].append(token)
# Otherwise, append the current token to the current sentence.
else:
current_sentence.append(token)
# Split the sentence in positions of punctuations.
if token in punctuation:
sentences.append(current_sentence)
current_sentence = []
# Assume the last few tokens are also a sentence
if len(current_sentence) != 0:
sentences.append(current_sentence)
# 2. Merge short sentences.
chunks = []
current_chunk = []
for sentence in sentences:
if len(current_chunk) + len(sentence) <= max_tokens:
current_chunk.extend(sentence)
else:
if len(current_chunk) > 0:
chunks.append(current_chunk)
current_chunk = sentence
if len(current_chunk) > 0:
chunks.append(current_chunk)
return chunks
def chunk_tokens_dialog(tokens_list: List[str], max_tokens: int = 100):
"""
Splits the input tokens list into chunks according to speaker-turn
symbol [S1], each with a maximum number of tokens.
Args:
token_list (list of str): The list of tokens to be split.
max_tokens (int): The maximum number of tokens per chunk.
Returns:
List[str]: A list of text chunks.
"""
# 1. Split the tokens according to speaker-turn symbol [S1].
dialogs = []
current_dialog = []
for token in tokens_list:
if token == "[S1]":
if len(current_dialog) != 0:
dialogs.append(current_dialog)
current_dialog = []
current_dialog.append(token)
# Assume the last few tokens are also a dialog
if len(current_dialog) != 0:
dialogs.append(current_dialog)
# 2. Merge short dialogs.
chunks = []
current_chunk = []
for dialog in dialogs:
if len(current_chunk) + len(dialog) <= max_tokens:
current_chunk.extend(dialog)
else:
if len(current_chunk) > 0:
chunks.append(current_chunk)
current_chunk = dialog
if len(current_chunk) > 0:
chunks.append(current_chunk)
return chunks
def batchify_tokens(
tokens_list: List[List[int]],
max_duration: float,
prompt_duration: float,
token_duration: float,
):
"""
Sort and group the input list of token sequences into batches, where each batch's
total duration does not exceed the maximum.
Args:
tokens_list (List[List[int]]): A list of token sequences, where each inner
list represents a sequence of tokens.
max_duration (float): The maximum allowed total duration for each batch.
prompt_duration (float): The duration cost per prompt in the batch.
token_duration (float): The duration cost per token.
Returns:
batches: List[List[List[int]]]: A list of batches, where each batch is a list of
token sequences that fit within the max duration.
index: List[int]: The original index of each sentence, used to recover the
sequential order in the future.
"""
# Create index for each sentence
indexed_tokens = list(enumerate(tokens_list))
# Sort according to sentence length (for less padding)
indexed_sorted_tokens = sorted(indexed_tokens, key=lambda x: len(x[1]))
index = [indexed_sorted_tokens[i][0] for i in range(len(indexed_sorted_tokens))]
sorted_tokens = [
indexed_sorted_tokens[i][1] for i in range(len(indexed_sorted_tokens))
]
batches = []
batch = []
batch_size = 0 # Total number of tokens in current batch
for tokens in sorted_tokens:
# Calculate if adding current token sequence would exceed max duration
# Formula considers: existing tokens' duration + existing
# prompts' duration + new tokens' duration
if (
batch_size * token_duration
+ len(batch) * prompt_duration
+ len(tokens) * token_duration
<= max_duration
):
# Add to current batch if within duration limit
batch.append(tokens)
batch_size += len(tokens)
else:
# If exceeding limit, finalize current batch (if not empty)
if len(batch) > 0:
batches.append(batch)
# Start new batch with current token sequence
batch = [tokens]
batch_size = len(tokens)
# Add the last batch if it's not empty
if len(batch) > 0:
batches.append(batch)
return batches, index
def cross_fade_concat(
chunks: List[torch.Tensor], fade_duration: float = 0.1, sample_rate: int = 24000
) -> torch.Tensor:
"""
Concatenates audio chunks with cross-fading between consecutive chunks.
Args:
chunks: List of audio tensors, each with shape (C, T) where
C = number of channel, T = time dimension (samples)
fade_duration: Duration of cross-fade in seconds
sample_rate: Audio sample rate in Hz
Returns:
Concatenated audio tensor with shape (N, T_total)
"""
# Handle edge cases: empty input or single chunk
if len(chunks) <= 1:
return chunks[0] if chunks else torch.tensor([])
# Calculate total fade samples from duration and sample rate
fade_samples = int(fade_duration * sample_rate)
# Use simple concatenation if fade duration is non-positive
if fade_samples <= 0:
return torch.cat(chunks, dim=-1)
# Initialize final tensor with the first chunk
final = chunks[0]
# Iterate through remaining chunks to apply cross-fading
for next_chunk in chunks[1:]:
# Calculate safe fade length (cannot exceed either chunk's duration)
k = min(fade_samples, final.shape[-1], next_chunk.shape[-1])
# Fall back to simple concatenation if safe fade length is invalid
if k <= 0:
final = torch.cat([final, next_chunk], dim=-1)
continue
# Create fade curve (1 -> 0) with shape (1, k) for broadcasting
fade = torch.linspace(1, 0, k, device=final.device)[None]
# Concatenate three parts:
# 1. Non-overlapping part of previous audio
# 2. Cross-faded overlapping region
# 3. Non-overlapping part of next audio
final = torch.cat(
[
final[..., :-k], # All samples except last k from previous
final[..., -k:] * fade
+ next_chunk[..., :k] * (1 - fade), # Cross-fade region
next_chunk[..., k:], # All samples except first k from next
],
dim=-1,
)
return final
def add_punctuation(text: str):
"""Add punctuation if there is not in the end of text"""
text = text.strip()
if text[-1] not in punctuation:
text += "."
return text
def load_prompt_wav(prompt_wav: str, sampling_rate: int):
"""
Load the waveform with torchaudio and resampling if needed.
Parameters:
prompt_wav: path of the prompt wav.
sampling_rate: target sampling rate.
Returns:
Loaded prompt waveform with target sampling rate,
PyTorch tensor of shape (C, T)
"""
prompt_wav, prompt_sampling_rate = torchaudio.load(prompt_wav)
if prompt_sampling_rate != sampling_rate:
resampler = torchaudio.transforms.Resample(
orig_freq=prompt_sampling_rate, new_freq=sampling_rate
)
prompt_wav = resampler(prompt_wav)
return prompt_wav
def rms_norm(prompt_wav: torch.Tensor, target_rms: float):
"""
Normalize the rms of prompt_wav is it is smaller than target rms.
Parameters:
prompt_wav: PyTorch tensor with shape (C, T).
target_rms: target rms value
Returns:
prompt_wav: normalized prompt wav with shape (C, T).
promt_rms: rms of original prompt wav. Will be used to
re-normalize the generated wav.
"""
prompt_rms = torch.sqrt(torch.mean(torch.square(prompt_wav)))
if prompt_rms < target_rms:
prompt_wav = prompt_wav * target_rms / prompt_rms
return prompt_wav, prompt_rms
def remove_silence(
audio: torch.Tensor,
sampling_rate: int,
only_edge: bool = False,
trail_sil: float = 0,
):
"""
Remove silences longer than 1 second, and edge silences longer than 0.1 seconds
Parameters:
audio: PyTorch tensor with shape (C, T).
sampling_rate: sampling rate of the audio.
only_edge: If true, only remove edge silences.
trail_sil: the duration of added trailing silence in ms.
Returns:
PyTorch tensor with shape (C, T), where C is number of channels
and T is number of audio samples
"""
# Load audio file
wave = tensor_to_audiosegment(audio, sampling_rate)
if not only_edge:
# Split audio using silences longer than 1 second
non_silent_segs = split_on_silence(
wave,
min_silence_len=1000, # Silences longer than 1 second (1000ms)
silence_thresh=-50,
keep_silence=1000, # Keep 1.0 second of silence around segments
seek_step=10,
)
# Concatenate all non-silent segments
wave = AudioSegment.silent(duration=0)
for seg in non_silent_segs:
wave += seg
# Remove silence longer than 0.1 seconds in the begining and ending of wave
wave = remove_silence_edges(wave, 100, -50)
# Add trailing silence to avoid leaking prompt to generated speech.
wave = wave + AudioSegment.silent(duration=trail_sil)
# Convert to PyTorch tensor
return audiosegment_to_tensor(wave)
def remove_silence_edges(
audio: AudioSegment, keep_silence: int = 100, silence_threshold: float = -50
):
"""
Remove edge silences longer than `keep_silence` ms.
Parameters:
audio: an AudioSegment object.
keep_silence: kept silence in the edge.
only_edge: If true, only remove edge silences.
silence_threshold: the threshold of silence.
Returns:
An AudioSegment object
"""
# Remove leading silence
start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold)
start_idx = max(0, start_idx - keep_silence)
audio = audio[start_idx:]
# Remove trailing silence
audio = audio.reverse()
start_idx = detect_leading_silence(audio, silence_threshold=silence_threshold)
start_idx = max(0, start_idx - keep_silence)
audio = audio[start_idx:]
audio = audio.reverse()
return audio
def audiosegment_to_tensor(aseg):
"""
Convert a pydub.AudioSegment to PyTorch audio tensor
"""
audio_data = np.array(aseg.get_array_of_samples())
# Convert to float32 and normalize to [-1, 1] range
audio_data = audio_data.astype(np.float32) / 32768.0
# Handle channels
if aseg.channels == 1:
# Mono channel: add channel dimension (T) -> (1, T)
tensor_data = torch.from_numpy(audio_data).unsqueeze(0)
else:
# Multi-channel: reshape to (C, T)
tensor_data = torch.from_numpy(audio_data.reshape(-1, aseg.channels).T)
return tensor_data
def tensor_to_audiosegment(tensor, sample_rate):
"""
Convert a PyTorch audio tensor to pydub.AudioSegment
Parameters:
tensor: Tensor with shape (C, T), where C is the number of channels
and T is the time steps
sample_rate: Audio sample rate
"""
# Convert tensor to numpy array
audio_np = tensor.cpu().numpy()
# Add channel dimension if single channel
if audio_np.ndim == 1:
audio_np = audio_np[np.newaxis, :]
# Convert to int16 type (common format for pydub)
# Assumes tensor values are in [-1, 1] range as floating point
audio_np = (audio_np * 32768.0).clip(-32768, 32767).astype(np.int16)
# Convert to byte stream
# For multi-channel audio, pydub requires interleaved format
# (e.g., left-right-left-right)
if audio_np.shape[0] > 1:
# Convert to interleaved format
audio_np = audio_np.transpose(1, 0).flatten()
audio_bytes = audio_np.tobytes()
# Create AudioSegment
audio_segment = AudioSegment(
data=audio_bytes,
sample_width=2,
frame_rate=sample_rate,
channels=tensor.shape[0],
)
return audio_segment

View File

@@ -0,0 +1,245 @@
# Copyright 2022 Xiaomi Corp. (authors: Daniel Povey)
#
# See ../LICENSE for clarification regarding multiple authors
#
# 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 logging
from typing import List, Optional, Union
import torch
from torch.optim import Optimizer
class LRScheduler(object):
"""
Base-class for learning rate schedulers where the learning-rate depends on both the
batch and the epoch.
"""
def __init__(self, optimizer: Optimizer, verbose: bool = False):
# Attach optimizer
if not isinstance(optimizer, Optimizer):
raise TypeError("{} is not an Optimizer".format(type(optimizer).__name__))
self.optimizer = optimizer
self.verbose = verbose
for group in optimizer.param_groups:
group.setdefault("base_lr", group["lr"])
self.base_lrs = [group["base_lr"] for group in optimizer.param_groups]
self.epoch = 0
self.batch = 0
def state_dict(self):
"""Returns the state of the scheduler as a :class:`dict`.
It contains an entry for every variable in self.__dict__ which
is not the optimizer.
"""
return {
# the user might try to override the base_lr, so don't include this in the
# state. previously they were included.
# "base_lrs": self.base_lrs,
"epoch": self.epoch,
"batch": self.batch,
}
def load_state_dict(self, state_dict):
"""Loads the schedulers state.
Args:
state_dict (dict): scheduler state. Should be an object returned
from a call to :meth:`state_dict`.
"""
# the things with base_lrs are a work-around for a previous problem
# where base_lrs were written with the state dict.
base_lrs = self.base_lrs
self.__dict__.update(state_dict)
self.base_lrs = base_lrs
def get_last_lr(self) -> List[float]:
"""Return last computed learning rate by current scheduler.
Will be a list of float."""
return self._last_lr
def get_lr(self):
# Compute list of learning rates from self.epoch and self.batch and
# self.base_lrs; this must be overloaded by the user.
# e.g. return [some_formula(self.batch, self.epoch, base_lr)
# for base_lr in self.base_lrs ]
raise NotImplementedError
def step_batch(self, batch: Optional[int] = None) -> None:
# Step the batch index, or just set it. If `batch` is specified, it
# must be the batch index from the start of training, i.e. summed over
# all epochs.
# You can call this in any order; if you don't provide 'batch', it should
# of course be called once per batch.
if batch is not None:
self.batch = batch
else:
self.batch = self.batch + 1
self._set_lrs()
def step_epoch(self, epoch: Optional[int] = None):
# Step the epoch index, or just set it. If you provide the 'epoch' arg, you
# should call this at the start of the epoch; if you don't provide the 'epoch'
# arg, you should call it at the end of the epoch.
if epoch is not None:
self.epoch = epoch
else:
self.epoch = self.epoch + 1
self._set_lrs()
def _set_lrs(self):
values = self.get_lr()
assert len(values) == len(self.optimizer.param_groups)
for i, data in enumerate(zip(self.optimizer.param_groups, values)):
param_group, lr = data
param_group["lr"] = lr
self.print_lr(self.verbose, i, lr)
self._last_lr = [group["lr"] for group in self.optimizer.param_groups]
def print_lr(self, is_verbose, group, lr):
"""Display the current learning rate."""
if is_verbose:
logging.warning(
f"Epoch={self.epoch}, batch={self.batch}: adjusting learning rate"
f" of group {group} to {lr:.4e}."
)
class Eden(LRScheduler):
"""
Eden scheduler.
The basic formula (before warmup) is:
lr = base_lr * (((batch**2 + lr_batches**2) / lr_batches**2) ** -0.25 *
(((epoch**2 + lr_epochs**2) / lr_epochs**2) ** -0.25)) * warmup
where `warmup` increases from linearly 0.5 to 1 over `warmup_batches` batches
and then stays constant at 1.
If you don't have the concept of epochs, or one epoch takes a very long time,
you can replace the notion of 'epoch' with some measure of the amount of data
processed, e.g. hours of data or frames of data, with 'lr_epochs' being set to
some measure representing "quite a lot of data": say, one fifth or one third
of an entire training run, but it doesn't matter much. You could also use
Eden2 which has only the notion of batches.
We suggest base_lr = 0.04 (passed to optimizer) if used with ScaledAdam
Args:
optimizer: the optimizer to change the learning rates on
lr_batches: the number of batches after which we start significantly
decreasing the learning rate, suggest 5000.
lr_epochs: the number of epochs after which we start significantly
decreasing the learning rate, suggest 6 if you plan to do e.g.
20 to 40 epochs, but may need smaller number if dataset is huge
and you will do few epochs.
"""
def __init__(
self,
optimizer: Optimizer,
lr_batches: Union[int, float],
lr_epochs: Union[int, float],
warmup_batches: Union[int, float] = 500.0,
warmup_start: float = 0.5,
verbose: bool = False,
):
super(Eden, self).__init__(optimizer, verbose)
self.lr_batches = lr_batches
self.lr_epochs = lr_epochs
self.warmup_batches = warmup_batches
assert 0.0 <= warmup_start <= 1.0, warmup_start
self.warmup_start = warmup_start
def get_lr(self):
factor = (
(self.batch**2 + self.lr_batches**2) / self.lr_batches**2
) ** -0.25 * (
((self.epoch**2 + self.lr_epochs**2) / self.lr_epochs**2) ** -0.25
)
warmup_factor = (
1.0
if self.batch >= self.warmup_batches
else self.warmup_start
+ (1.0 - self.warmup_start) * (self.batch / self.warmup_batches)
# else 0.5 + 0.5 * (self.batch / self.warmup_batches)
)
return [x * factor * warmup_factor for x in self.base_lrs]
class FixedLRScheduler(LRScheduler):
"""
Fixed learning rate scheduler.
Args:
optimizer: the optimizer to change the learning rates on
"""
def __init__(
self,
optimizer: Optimizer,
verbose: bool = False,
):
super(FixedLRScheduler, self).__init__(optimizer, verbose)
def get_lr(self):
return [x for x in self.base_lrs]
def _test_eden():
m = torch.nn.Linear(100, 100)
from zipvoice.utils.optim import ScaledAdam
optim = ScaledAdam(m.parameters(), lr=0.03)
scheduler = Eden(optim, lr_batches=100, lr_epochs=2, verbose=True)
for epoch in range(10):
scheduler.step_epoch(epoch) # sets epoch to `epoch`
for step in range(20):
x = torch.randn(200, 100).detach()
x.requires_grad = True
y = m(x)
dy = torch.randn(200, 100).detach()
f = (y * dy).sum()
f.backward()
optim.step()
scheduler.step_batch()
optim.zero_grad()
logging.info(f"last lr = {scheduler.get_last_lr()}")
logging.info(f"state dict = {scheduler.state_dict()}")
if __name__ == "__main__":
torch.set_num_threads(1)
torch.set_num_interop_threads(1)
logging.getLogger().setLevel(logging.INFO)
import subprocess
s = subprocess.check_output(
"git status -uno .; git log -1; git diff HEAD .", shell=True
)
logging.info(s)
_test_eden()

868
zipvoice/utils/optim.py Normal file
View File

@@ -0,0 +1,868 @@
# Copyright 2022 Xiaomi Corp. (authors: Daniel Povey)
#
# See ../LICENSE for clarification regarding multiple authors
#
# 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 contextlib
import logging
from collections import defaultdict
from typing import Dict, List, Tuple
import torch
from lhotse.utils import fix_random_seed
from torch import Tensor
from torch.optim import Optimizer
class BatchedOptimizer(Optimizer):
"""
This class adds to class Optimizer the capability to optimize parameters in batches:
it will stack the parameters and their grads for you so the optimizer can work
on tensors with an extra leading dimension. This is intended for speed with GPUs,
as it reduces the number of kernels launched in the optimizer.
Args:
params:
"""
def __init__(self, params, defaults):
super(BatchedOptimizer, self).__init__(params, defaults)
@contextlib.contextmanager
def batched_params(self, param_group, group_params_names):
"""
This function returns (technically, yields) a list of
of tuples (p, state), where
p is a `fake` parameter that is stacked (over axis 0) from real parameters
that share the same shape, and its gradient is also stacked;
`state` is the state corresponding to this batch of parameters
(it will be physically located in the "state" for one of the real
parameters, the last one that has any particular shape and dtype).
This function is decorated as a context manager so that it can
write parameters back to their "real" locations.
The idea is, instead of doing:
<code>
for p in group["params"]:
state = self.state[p]
...
</code>
you can do:
<code>
with self.batched_params(group["params"]) as batches:
for p, state, p_names in batches:
...
</code>
Args:
group: a parameter group, which is a list of parameters; should be
one of self.param_groups.
group_params_names: name for each parameter in group,
which is List[str].
"""
batches = defaultdict(
list
) # `batches` maps from tuple (dtype_as_str,*shape) to list of nn.Parameter
batches_names = defaultdict(
list
) # `batches` maps from tuple (dtype_as_str,*shape) to list of str
assert len(param_group) == len(group_params_names)
for p, named_p in zip(param_group, group_params_names):
key = (str(p.dtype), *p.shape)
batches[key].append(p)
batches_names[key].append(named_p)
batches_names_keys = list(batches_names.keys())
sorted_idx = sorted(
range(len(batches_names)), key=lambda i: batches_names_keys[i]
)
batches_names = [batches_names[batches_names_keys[idx]] for idx in sorted_idx]
batches = [batches[batches_names_keys[idx]] for idx in sorted_idx]
stacked_params_dict = dict()
# turn batches into a list, in deterministic order.
# tuples will contain tuples of (stacked_param, state, stacked_params_names),
# one for each batch in `batches`.
tuples = []
for batch, batch_names in zip(batches, batches_names):
p = batch[0]
# we arbitrarily store the state in the
# state corresponding to the 1st parameter in the
# group. class Optimizer will take care of saving/loading state.
state = self.state[p]
p_stacked = torch.stack(batch)
grad = torch.stack(
[torch.zeros_like(p) if p.grad is None else p.grad for p in batch]
)
p_stacked.grad = grad
stacked_params_dict[key] = p_stacked
tuples.append((p_stacked, state, batch_names))
yield tuples # <-- calling code will do the actual optimization here!
for (stacked_params, _state, _names), batch in zip(tuples, batches):
for i, p in enumerate(batch): # batch is list of Parameter
p.copy_(stacked_params[i])
def basic_step(group, p, state, grad):
# computes basic Adam update using beta2 (dividing by gradient stddev) only. no
# momentum yet.
lr = group["lr"]
if p.numel() == p.shape[0]:
lr = lr * group["scalar_lr_scale"]
beta2 = group["betas"][1]
eps = group["eps"]
# p shape: (batch_size,) or (batch_size, 1, [1,..])
try:
exp_avg_sq = state[
"exp_avg_sq"
] # shape: (batch_size,) or (batch_size, 1, [1,..])
except KeyError:
exp_avg_sq = torch.zeros(*p.shape, device=p.device, dtype=torch.float)
state["exp_avg_sq"] = exp_avg_sq
exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
# bias_correction2 is like in Adam.
# slower update at the start will help stability anyway.
bias_correction2 = 1 - beta2 ** (state["step"] + 1)
if bias_correction2 < 0.99:
# note: not in-place.
exp_avg_sq = exp_avg_sq * (1.0 / bias_correction2)
denom = exp_avg_sq.sqrt().add_(eps)
return -lr * grad / denom
def scaling_step(group, p, state, grad):
delta = basic_step(group, p, state, grad)
if p.numel() == p.shape[0]:
return delta
# there is no scaling for scalar parameters.
# (p.shape[0] is the batch of parameters.)
step = state["step"]
size_update_period = group["size_update_period"]
try:
param_rms = state["param_rms"]
scale_grads = state["scale_grads"]
scale_exp_avg_sq = state["scale_exp_avg_sq"]
except KeyError:
# we know p.ndim > 1 because we'd have returned above if not, so don't worry
# about the speial case of dim=[] that pytorch treats inconsistently.
param_rms = (p**2).mean(dim=list(range(1, p.ndim)), keepdim=True).sqrt()
param_rms = param_rms.to(torch.float)
scale_exp_avg_sq = torch.zeros_like(param_rms)
scale_grads = torch.zeros(
size_update_period,
*param_rms.shape,
dtype=torch.float,
device=p.device,
)
state["param_rms"] = param_rms
state["scale_grads"] = scale_grads
state["scale_exp_avg_sq"] = scale_exp_avg_sq
# on every step, update the gradient w.r.t. the scale of the parameter, we
# store these as a batch and periodically update the size (for speed only, to
# avoid too many operations).
scale_grads[step % size_update_period] = (p * grad).sum(
dim=list(range(1, p.ndim)), keepdim=True
)
# periodically recompute the value of param_rms.
if step % size_update_period == size_update_period - 1:
param_rms.copy_((p**2).mean(dim=list(range(1, p.ndim)), keepdim=True).sqrt())
param_min_rms = group["param_min_rms"]
# scale the step size by param_rms. This is the most important "scaling" part of
# ScaledAdam
delta *= param_rms.clamp(min=param_min_rms)
if step % size_update_period == size_update_period - 1 and step > 0:
# This block updates the size of parameter by adding a step ("delta") value in
# the direction of either shrinking or growing it.
beta2 = group["betas"][1]
size_lr = group["lr"] * group["scalar_lr_scale"]
param_max_rms = group["param_max_rms"]
eps = group["eps"]
# correct beta2 for the size update period: we will have
# faster decay at this level.
beta2_corr = beta2**size_update_period
scale_exp_avg_sq.mul_(beta2_corr).add_(
(scale_grads**2).mean(dim=0), # mean over dim `size_update_period`
alpha=1 - beta2_corr,
) # shape is (batch_size, 1, 1, ...)
# The 1st time we reach here is when size_step == 1.
size_step = (step + 1) // size_update_period
bias_correction2 = 1 - beta2_corr**size_step
denom = scale_exp_avg_sq.sqrt() + eps
scale_step = (
-size_lr * (bias_correction2**0.5) * scale_grads.sum(dim=0) / denom
)
is_too_small = param_rms < param_min_rms
# when the param gets too small, just don't shrink it any further.
scale_step.masked_fill_(is_too_small, 0.0)
# The following may help prevent instability: don't allow the scale step to be
# too large in either direction.
scale_step.clamp_(min=-0.1, max=0.1)
# and ensure the parameter rms after update never exceeds param_max_rms.
# We have to look at the trained model for parameters at or around the
# param_max_rms, because sometimes they can indicate a problem with the
# topology or settings.
scale_step = torch.minimum(scale_step, (param_max_rms - param_rms) / param_rms)
delta.add_(p * scale_step)
return delta
def momentum_step(group, p, state, grad):
delta = scaling_step(group, p, state, grad)
beta1 = group["betas"][0]
try:
stored_delta = state["delta"]
except KeyError:
stored_delta = torch.zeros(*p.shape, device=p.device, dtype=torch.float)
state["delta"] = stored_delta
stored_delta.mul_(beta1)
stored_delta.add_(delta, alpha=(1 - beta1))
# we don't bother doing the "bias correction" part of Adam for beta1 because this is
# just an edge effect that affects the first 10 or so batches; and the effect of not
# doing it is just to do a slower update for the first few batches, which will help
# stability.
return stored_delta
class ScaledAdam(BatchedOptimizer):
"""
Implements 'Scaled Adam', a variant of Adam where we scale each parameter's update
proportional to the norm of that parameter; and also learn the scale of the
parameter, in log space, subject to upper and lower limits (as if we had factored
each parameter as param = underlying_param * log_scale.exp())
Args:
params: The parameters or param_groups to optimize (like other Optimizer
subclasses) Unlike common optimizers, which accept
model.parameters() or groups of parameters(), this optimizer
could accept model.named_parameters() or groups of
named_parameters(). See comments of function
_get_names_of_parameters for its 4 possible cases.
lr: The learning rate. We will typically use a learning rate schedule
that starts at 0.03 and decreases over time, i.e. much higher
than other common optimizers.
clipping_scale: (e.g. 2.0)
A scale for gradient-clipping: if specified, the normalized gradients
over the whole model will be clipped to have 2-norm equal to
`clipping_scale` times the median 2-norm over the most recent period
of `clipping_update_period` minibatches. By "normalized gradients",
we mean after multiplying by the rms parameter value for this tensor
[for non-scalars]; this is appropriate because our update is scaled
by this quantity.
betas: beta1,beta2 are momentum constants for regular momentum, and moving
sum-sq grad. Must satisfy 0 < beta <= beta2 < 1.
scalar_lr_scale: A scaling factor on the learning rate, that we use to update the
scale of each parameter tensor and scalar parameters of the mode..
If each parameter were decomposed as p * p_scale.exp(),
where (p**2).mean().sqrt() == 1.0, scalar_lr_scale would be a the
scaling factor on the learning rate of p_scale.
eps: A general-purpose epsilon to prevent division by zero
param_min_rms: Minimum root-mean-square value of parameter tensor, for purposes of
learning the scale on the parameters (we'll constrain the rms of
each non-scalar parameter tensor to be >= this value)
param_max_rms: Maximum root-mean-square value of parameter tensor, for purposes of
learning the scale on the parameters (we'll constrain the rms of
each non-scalar parameter tensor to be <= this value)
scalar_max: Maximum absolute value for scalar parameters (applicable if your
model has any parameters with numel() == 1).
size_update_period: The periodicity, in steps, with which we update the size (scale)
of the parameter tensor. This is provided to save a little time
in the update.
clipping_update_period: if clipping_scale is specified, this is the period
"""
def __init__(
self,
params,
lr=3e-02,
clipping_scale=None,
betas=(0.9, 0.98),
scalar_lr_scale=0.1,
eps=1.0e-08,
param_min_rms=1.0e-05,
param_max_rms=3.0,
scalar_max=10.0,
size_update_period=4,
clipping_update_period=100,
):
defaults = dict(
lr=lr,
clipping_scale=clipping_scale,
betas=betas,
scalar_lr_scale=scalar_lr_scale,
eps=eps,
param_min_rms=param_min_rms,
param_max_rms=param_max_rms,
scalar_max=scalar_max,
size_update_period=size_update_period,
clipping_update_period=clipping_update_period,
)
# If params only contains parameters or group of parameters,
# i.e when parameter names are not given,
# this flag will be set to False in funciton _get_names_of_parameters.
self.show_dominant_parameters = True
param_groups, parameters_names = self._get_names_of_parameters(params)
super(ScaledAdam, self).__init__(param_groups, defaults)
assert len(self.param_groups) == len(parameters_names)
self.parameters_names = parameters_names
def _get_names_of_parameters(
self, params_or_named_params
) -> Tuple[List[Dict], List[List[str]]]:
"""
Args:
params_or_named_params: according to the way ScaledAdam is initialized
in train.py, this argument could be one of following 4 cases,
case 1, a generator of parameter, e.g.:
optimizer = ScaledAdam(model.parameters(), lr=params.base_lr,
clipping_scale=3.0)
case 2, a list of parameter groups with different config, e.g.:
model_param_groups = [
{'params': model.encoder.parameters(), 'lr': 0.05},
{'params': model.decoder.parameters(), 'lr': 0.01},
{'params': model.joiner.parameters(), 'lr': 0.03},
]
optimizer = ScaledAdam(model_param_groups, lr=params.base_lr,
clipping_scale=3.0)
case 3, a generator of named_parameter, e.g.:
optimizer = ScaledAdam(model.named_parameters(), lr=params.base_lr,
clipping_scale=3.0)
case 4, a list of named_parameter groups with different config, e.g.:
model_named_param_groups = [
{'named_params': model.encoder.named_parameters(), 'lr': 0.05},
{'named_params': model.decoder.named_parameters(), 'lr': 0.01},
{'named_params': model.joiner.named_parameters(), 'lr': 0.03},
]
optimizer = ScaledAdam(model_named_param_groups, lr=params.base_lr,
clipping_scale=3.0)
For case 1 and case 2, input params is used to initialize the underlying
torch.optimizer.
For case 3 and case 4, firstly, names and params are extracted from input
named_params, then, these extracted params are used to initialize the
underlying torch.optimizer, and these extracted names are mainly used by
function `_show_gradient_dominating_parameter`
Returns:
Returns a tuple containing 2 elements:
- `param_groups` with type List[Dict], each Dict element is a parameter
group. An example of `param_groups` could be:
[
{'params': `one iterable of Parameter`, 'lr': 0.05},
{'params': `another iterable of Parameter`, 'lr': 0.08},
{'params': `a third iterable of Parameter`, 'lr': 0.1},
]
- `param_gruops_names` with type List[List[str]],
each `List[str]` is for a group['params'] in param_groups,
and each `str` is the name of a parameter.
A dummy name "foo" is related to each parameter,
if input are params without names, i.e. case 1 or case 2.
"""
# variable naming convention in this function:
# p is short for param.
# np is short for named_param.
# p_or_np is short for param_or_named_param.
# cur is short for current.
# group is a dict,
# e.g. {'params': iterable of parameter, 'lr': 0.05, other fields}.
# groups is a List[group]
iterable_or_groups = list(params_or_named_params)
if len(iterable_or_groups) == 0:
raise ValueError("optimizer got an empty parameter list")
# The first value of returned tuple. A list of dicts containing at
# least 'params' as a key.
param_groups = []
# The second value of returned tuple,
# a List[List[str]], each sub-List is for a group.
param_groups_names = []
if not isinstance(iterable_or_groups[0], dict):
# case 1 or case 3,
# the input is an iterable of parameter or named parameter.
param_iterable_cur_group = []
param_names_cur_group = []
for p_or_np in iterable_or_groups:
if isinstance(p_or_np, tuple):
# case 3
name, param = p_or_np
else:
# case 1
assert isinstance(p_or_np, torch.Tensor)
param = p_or_np
# Assign a dummy name as a placeholder
name = "foo"
self.show_dominant_parameters = False
param_iterable_cur_group.append(param)
param_names_cur_group.append(name)
param_groups.append({"params": param_iterable_cur_group})
param_groups_names.append(param_names_cur_group)
else:
# case 2 or case 4
# the input is groups of parameter or named parameter.
for cur_group in iterable_or_groups:
if "named_params" in cur_group:
name_list = [x[0] for x in cur_group["named_params"]]
p_list = [x[1] for x in cur_group["named_params"]]
del cur_group["named_params"]
cur_group["params"] = p_list
else:
assert "params" in cur_group
name_list = ["foo" for _ in cur_group["params"]]
param_groups.append(cur_group)
param_groups_names.append(name_list)
return param_groups, param_groups_names
def __setstate__(self, state):
super(ScaledAdam, self).__setstate__(state)
@torch.no_grad()
def step(self, closure=None):
"""Performs a single optimization step.
Arguments:
closure (callable, optional): A closure that reevaluates the model
and returns the loss.
"""
loss = None
if closure is not None:
with torch.enable_grad():
loss = closure()
for group, group_params_names in zip(self.param_groups, self.parameters_names):
with self.batched_params(group["params"], group_params_names) as batches:
# batches is list of pairs (stacked_param, state). stacked_param is
# like a regular parameter, and will have a .grad, but the 1st dim
# corresponds to a stacking dim, it is not a real dim.
if (
len(batches[0][1]) == 0
): # if len(first state) == 0: not yet initialized
clipping_scale = 1
else:
clipping_scale = self._get_clipping_scale(group, batches)
for p, state, _ in batches:
# Perform optimization step.
# grad is not going to be None, we handled that when creating the
# batches.
grad = p.grad
if grad.is_sparse:
raise RuntimeError(
"ScaledAdam optimizer does not support sparse gradients"
)
try:
cur_step = state["step"]
except KeyError:
state["step"] = 0
cur_step = 0
grad = (
p.grad if clipping_scale == 1.0 else p.grad.mul_(clipping_scale)
)
p += momentum_step(group, p.detach(), state, grad)
if p.numel() == p.shape[0]: # scalar parameter
scalar_max = group["scalar_max"]
p.clamp_(min=-scalar_max, max=scalar_max)
state["step"] = cur_step + 1
return loss
def _get_clipping_scale(
self, group: dict, tuples: List[Tuple[Tensor, dict, List[str]]]
) -> float:
"""
Returns a scalar factor <= 1.0 that dictates gradient clipping, i.e. we will
scale the gradients by this amount before applying the rest of the update.
Args:
group: the parameter group, an item in self.param_groups
tuples: a list of tuples of (param, state, param_names)
where param is a batched set of parameters,
with a .grad (1st dim is batch dim)
and state is the state-dict where optimization parameters are kept.
param_names is a List[str] while each str is name for a parameter
in batched set of parameters "param".
"""
assert len(tuples) >= 1
clipping_scale = group["clipping_scale"]
(first_p, first_state, _) = tuples[0]
step = first_state["step"]
if clipping_scale is None or step == 0:
# no clipping. return early on step == 0 because the other
# parameters' state won't have been initialized yet.
return 1.0
clipping_update_period = group["clipping_update_period"]
scalar_lr_scale = group["scalar_lr_scale"]
tot_sumsq = torch.tensor(0.0, device=first_p.device)
for p, state, param_names in tuples:
grad = p.grad
if grad.is_sparse:
raise RuntimeError(
"ScaledAdam optimizer does not support sparse gradients"
)
if p.numel() == p.shape[0]: # a batch of scalars
tot_sumsq += (grad**2).sum() * (
scalar_lr_scale**2
) # sum() to change shape [1] to []
else:
tot_sumsq += ((grad * state["param_rms"]) ** 2).sum()
tot_norm = tot_sumsq.sqrt()
if "model_norms" not in first_state:
first_state["model_norms"] = torch.zeros(
clipping_update_period, device=p.device
)
first_state["model_norms"][step % clipping_update_period] = tot_norm
irregular_estimate_steps = [
i for i in [10, 20, 40] if i < clipping_update_period
]
if step % clipping_update_period == 0 or step in irregular_estimate_steps:
# Print some stats.
# We don't reach here if step == 0 because we would have returned
# above.
sorted_norms = first_state["model_norms"].sort()[0].to("cpu")
if step in irregular_estimate_steps:
sorted_norms = sorted_norms[-step:]
num_norms = sorted_norms.numel()
quartiles = []
for n in range(0, 5):
index = min(num_norms - 1, (num_norms // 4) * n)
quartiles.append(sorted_norms[index].item())
median = quartiles[2]
if median - median != 0:
raise RuntimeError("Too many grads were not finite")
threshold = clipping_scale * median
if step in irregular_estimate_steps:
# use larger thresholds on first few steps of estimating threshold,
# as norm may be changing rapidly.
threshold = threshold * 2.0
first_state["model_norm_threshold"] = threshold
percent_clipped = (
first_state["num_clipped"] * 100.0 / num_norms
if "num_clipped" in first_state
else 0.0
)
first_state["num_clipped"] = 0
quartiles = " ".join(["%.3e" % x for x in quartiles])
logging.warning(
f"Clipping_scale={clipping_scale}, grad-norm quartiles {quartiles}, "
f"threshold={threshold:.3e}, percent-clipped={percent_clipped:.1f}"
)
try:
model_norm_threshold = first_state["model_norm_threshold"]
except KeyError:
return 1.0 # threshold has not yet been set.
ans = min(1.0, (model_norm_threshold / (tot_norm + 1.0e-20)).item())
if ans != ans: # e.g. ans is nan
ans = 0.0
if ans < 1.0:
first_state["num_clipped"] += 1
if ans < 0.5:
logging.debug(
f"Scaling gradients by {ans}, "
f"model_norm_threshold={model_norm_threshold}"
)
if self.show_dominant_parameters:
assert p.shape[0] == len(param_names)
self._show_gradient_dominating_parameter(
tuples, tot_sumsq, group["scalar_lr_scale"]
)
self._show_param_with_unusual_grad(tuples)
if ans == 0.0:
for p, state, param_names in tuples:
p.grad.zero_() # get rid of infinity()
return ans
def _show_param_with_unusual_grad(
self,
tuples: List[Tuple[Tensor, dict, List[str]]],
):
"""
Print information about parameter which has the largest ratio of
grad-on-this-batch divided by normal grad size.
tuples: a list of tuples of (param, state, param_names)
where param is a batched set of parameters,
with a .grad (1st dim is batch dim)
and state is the state-dict where optimization parameters are kept.
param_names is a List[str] while each str is name for a parameter
in batched set of parameters "param".
"""
# ratios_names is a list of 3-tuples: (grad_ratio, param_name, tensor)
ratios_names = []
for p, state, batch_param_names in tuples:
dims = list(range(1, p.ndim))
def mean(x):
# workaround for bad interface of torch's "mean" for when dims is the
# empty list.
if len(dims) > 0:
return x.mean(dim=dims)
else:
return x
grad_ratio = (
(mean(p.grad**2) / state["exp_avg_sq"].mean(dim=dims))
.sqrt()
.to("cpu")
)
ratios_names += zip(
grad_ratio.tolist(), batch_param_names, p.grad.unbind(dim=0)
)
ratios_names = sorted(ratios_names, reverse=True)
ratios_names = ratios_names[:10]
ratios_names = [
(ratio, name, largest_index(tensor))
for (ratio, name, tensor) in ratios_names
]
logging.debug(
f"Parameters with most larger-than-usual grads, with ratios, "
f"are: {ratios_names}"
)
def _show_gradient_dominating_parameter(
self,
tuples: List[Tuple[Tensor, dict, List[str]]],
tot_sumsq: Tensor,
scalar_lr_scale: float,
):
"""
Show information of parameter which dominates tot_sumsq.
Args:
tuples: a list of tuples of (param, state, param_names)
where param is a batched set of parameters,
with a .grad (1st dim is batch dim)
and state is the state-dict where optimization parameters are kept.
param_names is a List[str] while each str is name for a parameter
in batched set of parameters "param".
tot_sumsq: sumsq of all parameters. Though it's could be calculated
from tuples, we still pass it to save some time.
"""
all_sumsq_orig = {}
for p, state, batch_param_names in tuples:
# p is a stacked batch parameters.
batch_grad = p.grad
if p.numel() == p.shape[0]: # a batch of scalars
# Dummy values used by following `zip` statement.
batch_rms_orig = torch.full(
p.shape, scalar_lr_scale, device=batch_grad.device
)
else:
batch_rms_orig = state["param_rms"]
batch_sumsq_orig = (batch_grad * batch_rms_orig) ** 2
if batch_grad.ndim > 1:
# need to guard it with if-statement because sum() sums over
# all dims if dim == ().
batch_sumsq_orig = batch_sumsq_orig.sum(
dim=list(range(1, batch_grad.ndim))
)
for name, sumsq_orig, rms, grad in zip(
batch_param_names, batch_sumsq_orig, batch_rms_orig, batch_grad
):
proportion_orig = sumsq_orig / tot_sumsq
all_sumsq_orig[name] = (proportion_orig, sumsq_orig, rms, grad)
sorted_by_proportion = {
k: v
for k, v in sorted(
all_sumsq_orig.items(),
key=lambda item: item[1][0],
reverse=True,
)
}
dominant_param_name = next(iter(sorted_by_proportion))
(
dominant_proportion,
dominant_sumsq,
dominant_rms,
dominant_grad,
) = sorted_by_proportion[dominant_param_name]
logging.debug(
f"Parameter dominating tot_sumsq {dominant_param_name}"
f" with proportion {dominant_proportion:.2f},"
f" where dominant_sumsq=(grad_sumsq*orig_rms_sq)"
f"={dominant_sumsq:.3e},"
f" grad_sumsq={(dominant_grad**2).sum():.3e},"
f" orig_rms_sq={(dominant_rms**2).item():.3e}"
)
def largest_index(x: Tensor):
x = x.contiguous()
argmax = x.abs().argmax().item()
return [(argmax // x.stride(i)) % x.size(i) for i in range(x.ndim)]
def _test_scaled_adam(hidden_dim: int):
import timeit
from zipvoice.models.modules.scaling import ScaledLinear
from zipvoice.utils.lr_scheduler import Eden
E = 100
B = 4
T = 2
logging.info("in test_eve_cain")
# device = torch.device('cuda')
device = torch.device("cpu")
dtype = torch.float32
fix_random_seed(42)
# these input_magnitudes and output_magnitudes are to test that
# Abel is working as we expect and is able to adjust scales of
# different dims differently.
input_magnitudes = (1.0 * torch.randn(E, dtype=dtype, device=device)).exp()
output_magnitudes = (1.0 * torch.randn(E, dtype=dtype, device=device)).exp()
fix_random_seed(42)
Linear = ScaledLinear
m = torch.nn.Sequential(
Linear(E, hidden_dim),
torch.nn.PReLU(),
Linear(hidden_dim, hidden_dim),
torch.nn.PReLU(),
Linear(hidden_dim, E),
).to(device)
train_pairs = [
(
100.0 * torch.randn(B, T, E, device=device, dtype=dtype) * input_magnitudes,
torch.randn(B, T, E, device=device, dtype=dtype) * output_magnitudes,
)
for _ in range(20)
]
optim = ScaledAdam(m.named_parameters(), lr=0.03, clipping_scale=2.0)
scheduler = Eden(optim, lr_batches=200, lr_epochs=5, verbose=False)
start = timeit.default_timer()
avg_loss = 0.0
for epoch in range(180):
scheduler.step_epoch()
# if epoch == 100 and iter in [2,3]:
# optim.reset_speedup() # check it doesn't crash.
# if epoch == 130:
# opts = diagnostics.TensorDiagnosticOptions(
# 512
# ) # allow 4 megabytes per sub-module
# diagnostic = diagnostics.attach_diagnostics(m, opts)
for n, (x, y) in enumerate(train_pairs):
y_out = m(x)
loss = ((y_out - y) ** 2).mean() * 100.0
if epoch == 0 and n == 0:
avg_loss = loss.item()
else:
avg_loss = 0.98 * avg_loss + 0.02 * loss.item()
if n == 0 and epoch % 5 == 0:
# norm1 = '%.2e' % (m[0].weight**2).mean().sqrt().item()
# norm1b = '%.2e' % (m[0].bias**2).mean().sqrt().item()
# norm2 = '%.2e' % (m[2].weight**2).mean().sqrt().item()
# norm2b = '%.2e' % (m[2].bias**2).mean().sqrt().item()
# scale1 = '%.2e' % (m[0].weight_scale.exp().item())
# scale1b = '%.2e' % (m[0].bias_scale.exp().item())
# scale2 = '%.2e' % (m[2].weight_scale.exp().item())
# scale2b = '%.2e' % (m[2].bias_scale.exp().item())
lr = scheduler.get_last_lr()[0]
logging.info(
f"Iter {iter}, epoch {epoch}, batch {n}, "
f"avg_loss {avg_loss:.4g}, lr={lr:.4e}"
) # , norms={norm1,norm1b,norm2,norm2b}")
# scales={scale1,scale1b,scale2,scale2b}
loss.log().backward()
optim.step()
optim.zero_grad()
scheduler.step_batch()
# diagnostic.print_diagnostics()
stop = timeit.default_timer()
logging.info(f"Iter={iter}, Time taken: {stop - start}")
logging.info(f"last lr = {scheduler.get_last_lr()}")
# logging.info("state dict = ", scheduler.state_dict())
# logging.info("optim state_dict = ", optim.state_dict())
logging.info(f"input_magnitudes = {input_magnitudes}")
logging.info(f"output_magnitudes = {output_magnitudes}")
if __name__ == "__main__":
torch.set_num_threads(1)
torch.set_num_interop_threads(1)
logging.getLogger().setLevel(logging.INFO)
import subprocess
s = subprocess.check_output(
"git status -uno .; git log -1; git diff HEAD .", shell=True
)
logging.info(s)
import sys
if len(sys.argv) > 1:
hidden_dim = int(sys.argv[1])
else:
hidden_dim = 200
_test_scaled_adam(hidden_dim)

View File

@@ -0,0 +1,105 @@
# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang,
# Zengwei Yao)
#
# See ../../../../LICENSE for clarification regarding multiple authors
#
# 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.
"""
This file replaces various modules in a model.
Specifically, ActivationBalancer is replaced with an identity operator;
Whiten is also replaced with an identity operator;
BasicNorm is replaced by a module with `exp` removed.
"""
import copy
from typing import List
import torch
import torch.nn as nn
from zipvoice.models.modules.scaling import (
Balancer,
Dropout3,
SwooshL,
SwooshLOnnx,
SwooshR,
SwooshROnnx,
Whiten,
)
from zipvoice.models.modules.zipformer import CompactRelPositionalEncoding
# Copied from https://pytorch.org/docs/1.9.0/_modules/torch/nn/modules/module.html#Module.get_submodule # noqa
# get_submodule was added to nn.Module at v1.9.0
def get_submodule(model, target):
if target == "":
return model
atoms: List[str] = target.split(".")
mod: torch.nn.Module = model
for item in atoms:
if not hasattr(mod, item):
raise AttributeError(
mod._get_name() + " has no " "attribute `" + item + "`"
)
mod = getattr(mod, item)
if not isinstance(mod, torch.nn.Module):
raise AttributeError("`" + item + "` is not " "an nn.Module")
return mod
def convert_scaled_to_non_scaled(
model: nn.Module,
inplace: bool = False,
is_pnnx: bool = False,
is_onnx: bool = False,
):
"""
Args:
model:
The model to be converted.
inplace:
If True, the input model is modified inplace.
If False, the input model is copied and we modify the copied version.
is_pnnx:
True if we are going to export the model for PNNX.
is_onnx:
True if we are going to export the model for ONNX.
Return:
Return a model without scaled layers.
"""
if not inplace:
model = copy.deepcopy(model)
d = {}
for name, m in model.named_modules():
if isinstance(m, (Balancer, Dropout3, Whiten)):
d[name] = nn.Identity()
elif is_onnx and isinstance(m, SwooshR):
d[name] = SwooshROnnx()
elif is_onnx and isinstance(m, SwooshL):
d[name] = SwooshLOnnx()
elif is_onnx and isinstance(m, CompactRelPositionalEncoding):
# We want to recreate the positional encoding vector when
# the input changes, so we have to use torch.jit.script()
# to replace torch.jit.trace()
d[name] = torch.jit.script(m)
for k, v in d.items():
if "." in k:
parent, child = k.rsplit(".", maxsplit=1)
setattr(get_submodule(model, parent), child, v)
else:
setattr(model, k, v)
return model

143
zipvoice/utils/tensorrt.py Normal file
View File

@@ -0,0 +1,143 @@
# Copyright 2025 Nvidia Corp. (authors: Yuekai Zhang)
#
# See ../../../../LICENSE for clarification regarding multiple authors
#
# 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.
"""
This script provides utility functions for working with TensorRT in ZipVoice.
"""
import logging
import os
import queue
from typing import Any, Tuple, Optional
import torch
import torch.nn as nn
class TrtContextWrapper:
"""A wrapper class for managing TensorRT execution contexts."""
def __init__(
self, trt_engine: Any, trt_concurrent: int = 1, device: str = "cuda:0"
):
"""
Initializes the TrtContextWrapper.
Args:
trt_engine (Any): The TensorRT engine.
trt_concurrent (int, optional): The number of concurrent contexts. Defaults to 1.
device (str, optional): The device to use. Defaults to 'cuda:0'.
"""
self.trt_context_pool = queue.Queue(maxsize=trt_concurrent)
self.trt_engine = trt_engine
self.device = device
for _ in range(trt_concurrent):
trt_context = trt_engine.create_execution_context()
trt_stream = torch.cuda.stream(torch.cuda.Stream(torch.device(device)))
assert trt_context is not None, 'failed to create trt context, maybe not enough CUDA memory, try reduce current trt concurrent {}'.format(trt_concurrent)
self.trt_context_pool.put([trt_context, trt_stream])
assert self.trt_context_pool.empty() is False, 'no avaialbe estimator context'
self.feat_dim = 100
def acquire_estimator(self) -> Tuple[list, Any]:
"""Acquires a TensorRT context from the pool."""
return self.trt_context_pool.get(), self.trt_engine
def release_estimator(self, context: Any, stream: Any):
"""
Releases a TensorRT context back to the pool.
Args:
context (Any): The TensorRT context.
stream (Any): The CUDA stream.
"""
self.trt_context_pool.put([context, stream])
def __call__(
self,
x: torch.Tensor,
t: torch.Tensor,
padding_mask: torch.Tensor,
guidance_scale: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""
Executes the TensorRT engine.
Args:
x (torch.Tensor): The input tensor.
t (torch.Tensor): The time tensor.
padding_mask (torch.Tensor): The padding mask tensor.
guidance_scale (torch.Tensor): The guidance scale tensor.
Returns:
torch.Tensor: The output tensor.
"""
x = x.to(torch.float16)
t = t.to(torch.float16)
padding_mask = padding_mask.to(torch.float16)
if guidance_scale is not None:
guidance_scale = guidance_scale.to(torch.float16)
[estimator, stream], trt_engine = self.acquire_estimator()
# NOTE need to synchronize when switching stream
torch.cuda.current_stream().synchronize()
batch_size = x.size(0)
seq_len = x.size(1)
# Create output tensor with shape (N, T, 100)
output = torch.empty(batch_size, seq_len, self.feat_dim, dtype=x.dtype, device=x.device)
with stream:
estimator.set_input_shape('x', (batch_size, x.size(1), x.size(2)))
estimator.set_input_shape('t', (batch_size,))
estimator.set_input_shape('padding_mask', (batch_size, padding_mask.size(1)))
if guidance_scale is not None:
estimator.set_input_shape('guidance_scale', (batch_size,))
# Set input tensor addresses
input_data_ptrs = [x.contiguous().data_ptr(), t.contiguous().data_ptr(), padding_mask.contiguous().data_ptr()]
if guidance_scale is not None:
input_data_ptrs.append(guidance_scale.contiguous().data_ptr())
for i, j in enumerate(input_data_ptrs):
estimator.set_tensor_address(trt_engine.get_tensor_name(i), j)
# Set output tensor address
# The output tensor name should be the last tensor name in the engine
num_tensors = trt_engine.num_io_tensors
output_tensor_name = trt_engine.get_tensor_name(num_tensors - 1) # Last tensor is output
estimator.set_tensor_address(output_tensor_name, output.contiguous().data_ptr())
# run trt engine
assert estimator.execute_async_v3(torch.cuda.current_stream().cuda_stream) is True
torch.cuda.current_stream().synchronize()
self.release_estimator(estimator, stream)
return output.to(torch.float32)
def load_trt(model: nn.Module, trt_model: str, trt_concurrent: int = 1):
"""
Loads a TensorRT engine and replaces the model's fm_decoder with a TrtContextWrapper.
Args:
model (nn.Module): The model to modify.
trt_model (str): The path to the TensorRT engine file.
trt_concurrent (int, optional): The number of concurrent contexts. Defaults to 1.
"""
assert os.path.exists(trt_model), f"Please export trt model first."
import tensorrt as trt
with open(trt_model, 'rb') as f:
estimator_engine = trt.Runtime(trt.Logger(trt.Logger.INFO)).deserialize_cuda_engine(f.read())
assert estimator_engine is not None, 'failed to load trt {}'.format(trt_model)
del model.fm_decoder
model.fm_decoder = TrtContextWrapper(estimator_engine, trt_concurrent=trt_concurrent, device='cuda')