facebookresearch--audiocraft
329 行
14 KiB
Python
329 行
14 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the license found in the
|
|
# LICENSE file in the root directory of this source tree.
|
|
|
|
import logging
|
|
import multiprocessing
|
|
from pathlib import Path
|
|
import typing as tp
|
|
|
|
import flashy
|
|
import omegaconf
|
|
import torch
|
|
from torch import nn
|
|
|
|
from . import base, builders
|
|
from .. import models, quantization
|
|
from ..utils import checkpoint
|
|
from ..utils.samples.manager import SampleManager
|
|
from ..utils.utils import get_pool_executor
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CompressionSolver(base.StandardSolver):
|
|
"""Solver for compression task.
|
|
|
|
The compression task combines a set of perceptual and objective losses
|
|
to train an EncodecModel (composed of an encoder-decoder and a quantizer)
|
|
to perform high fidelity audio reconstruction.
|
|
"""
|
|
def __init__(self, cfg: omegaconf.DictConfig):
|
|
super().__init__(cfg)
|
|
self.rng: torch.Generator # set at each epoch
|
|
self.adv_losses = builders.get_adversarial_losses(self.cfg)
|
|
self.aux_losses = nn.ModuleDict()
|
|
self.info_losses = nn.ModuleDict()
|
|
assert not cfg.fsdp.use, "FSDP not supported by CompressionSolver."
|
|
loss_weights = dict()
|
|
for loss_name, weight in self.cfg.losses.items():
|
|
if loss_name in ['adv', 'feat']:
|
|
for adv_name, _ in self.adv_losses.items():
|
|
loss_weights[f'{loss_name}_{adv_name}'] = weight
|
|
elif weight > 0:
|
|
self.aux_losses[loss_name] = builders.get_loss(loss_name, self.cfg)
|
|
loss_weights[loss_name] = weight
|
|
else:
|
|
self.info_losses[loss_name] = builders.get_loss(loss_name, self.cfg)
|
|
self.balancer = builders.get_balancer(loss_weights, self.cfg.balancer)
|
|
self.register_stateful('adv_losses')
|
|
|
|
@property
|
|
def best_metric_name(self) -> tp.Optional[str]:
|
|
# best model is the last for the compression model
|
|
return None
|
|
|
|
def build_model(self):
|
|
"""Instantiate model and optimizer."""
|
|
# Model and optimizer
|
|
self.model = models.builders.get_compression_model(self.cfg).to(self.device)
|
|
self.optimizer = builders.get_optimizer(self.model.parameters(), self.cfg.optim)
|
|
self.register_stateful('model', 'optimizer')
|
|
self.register_best_state('model')
|
|
self.register_ema('model')
|
|
|
|
def build_dataloaders(self):
|
|
"""Instantiate audio dataloaders for each stage."""
|
|
self.dataloaders = builders.get_audio_datasets(self.cfg)
|
|
|
|
def show(self):
|
|
"""Show the compression model and employed adversarial loss."""
|
|
self.logger.info(f"Compression model with {self.model.quantizer.total_codebooks} codebooks:")
|
|
self.log_model_summary(self.model)
|
|
self.logger.info("Adversarial loss:")
|
|
self.log_model_summary(self.adv_losses)
|
|
self.logger.info("Auxiliary losses:")
|
|
self.logger.info(self.aux_losses)
|
|
self.logger.info("Info losses:")
|
|
self.logger.info(self.info_losses)
|
|
|
|
def run_step(self, idx: int, batch: torch.Tensor, metrics: dict):
|
|
"""Perform one training or valid step on a given batch."""
|
|
x = batch.to(self.device)
|
|
y = x.clone()
|
|
|
|
qres = self.model(x)
|
|
assert isinstance(qres, quantization.QuantizedResult)
|
|
y_pred = qres.x
|
|
# Log bandwidth in kb/s
|
|
metrics['bandwidth'] = qres.bandwidth.mean()
|
|
|
|
if self.is_training:
|
|
d_losses: dict = {}
|
|
if len(self.adv_losses) > 0 and torch.rand(1, generator=self.rng).item() <= 1 / self.cfg.adversarial.every:
|
|
for adv_name, adversary in self.adv_losses.items():
|
|
disc_loss = adversary.train_adv(y_pred, y)
|
|
d_losses[f'd_{adv_name}'] = disc_loss
|
|
metrics['d_loss'] = torch.sum(torch.stack(list(d_losses.values())))
|
|
metrics.update(d_losses)
|
|
|
|
balanced_losses: dict = {}
|
|
other_losses: dict = {}
|
|
|
|
# penalty from quantization
|
|
if qres.penalty is not None and qres.penalty.requires_grad:
|
|
other_losses['penalty'] = qres.penalty # penalty term from the quantizer
|
|
|
|
# adversarial losses
|
|
for adv_name, adversary in self.adv_losses.items():
|
|
adv_loss, feat_loss = adversary(y_pred, y)
|
|
balanced_losses[f'adv_{adv_name}'] = adv_loss
|
|
balanced_losses[f'feat_{adv_name}'] = feat_loss
|
|
|
|
# auxiliary losses
|
|
for loss_name, criterion in self.aux_losses.items():
|
|
loss = criterion(y_pred, y)
|
|
balanced_losses[loss_name] = loss
|
|
|
|
# weighted losses
|
|
metrics.update(balanced_losses)
|
|
metrics.update(other_losses)
|
|
metrics.update(qres.metrics)
|
|
|
|
if self.is_training:
|
|
# backprop losses that are not handled by balancer
|
|
other_loss = torch.tensor(0., device=self.device)
|
|
if 'penalty' in other_losses:
|
|
other_loss += other_losses['penalty']
|
|
if other_loss.requires_grad:
|
|
other_loss.backward(retain_graph=True)
|
|
ratio1 = sum(p.grad.data.norm(p=2).pow(2)
|
|
for p in self.model.parameters() if p.grad is not None)
|
|
assert isinstance(ratio1, torch.Tensor)
|
|
metrics['ratio1'] = ratio1.sqrt()
|
|
|
|
# balancer losses backward, returns effective training loss
|
|
# with effective weights at the current batch.
|
|
metrics['g_loss'] = self.balancer.backward(balanced_losses, y_pred)
|
|
# add metrics corresponding to weight ratios
|
|
metrics.update(self.balancer.metrics)
|
|
ratio2 = sum(p.grad.data.norm(p=2).pow(2)
|
|
for p in self.model.parameters() if p.grad is not None)
|
|
assert isinstance(ratio2, torch.Tensor)
|
|
metrics['ratio2'] = ratio2.sqrt()
|
|
|
|
# optim
|
|
flashy.distrib.sync_model(self.model)
|
|
if self.cfg.optim.max_norm:
|
|
torch.nn.utils.clip_grad_norm_(
|
|
self.model.parameters(), self.cfg.optim.max_norm
|
|
)
|
|
self.optimizer.step()
|
|
self.optimizer.zero_grad()
|
|
|
|
# informative losses only
|
|
info_losses: dict = {}
|
|
with torch.no_grad():
|
|
for loss_name, criterion in self.info_losses.items():
|
|
loss = criterion(y_pred, y)
|
|
info_losses[loss_name] = loss
|
|
|
|
metrics.update(info_losses)
|
|
|
|
# aggregated GAN losses: this is useful to report adv and feat across different adversarial loss setups
|
|
adv_losses = [loss for loss_name, loss in metrics.items() if loss_name.startswith('adv')]
|
|
if len(adv_losses) > 0:
|
|
metrics['adv'] = torch.sum(torch.stack(adv_losses))
|
|
feat_losses = [loss for loss_name, loss in metrics.items() if loss_name.startswith('feat')]
|
|
if len(feat_losses) > 0:
|
|
metrics['feat'] = torch.sum(torch.stack(feat_losses))
|
|
|
|
return metrics
|
|
|
|
def run_epoch(self):
|
|
# reset random seed at the beginning of the epoch
|
|
self.rng = torch.Generator()
|
|
self.rng.manual_seed(1234 + self.epoch)
|
|
# run epoch
|
|
super().run_epoch()
|
|
|
|
def evaluate(self):
|
|
"""Evaluate stage. Runs audio reconstruction evaluation."""
|
|
self.model.eval()
|
|
evaluate_stage_name = str(self.current_stage)
|
|
|
|
loader = self.dataloaders['evaluate']
|
|
updates = len(loader)
|
|
lp = self.log_progress(f'{evaluate_stage_name} inference', loader, total=updates, updates=self.log_updates)
|
|
average = flashy.averager()
|
|
|
|
pendings = []
|
|
ctx = multiprocessing.get_context('spawn')
|
|
with get_pool_executor(self.cfg.evaluate.num_workers, mp_context=ctx) as pool:
|
|
for idx, batch in enumerate(lp):
|
|
x = batch.to(self.device)
|
|
with torch.no_grad():
|
|
qres = self.model(x)
|
|
|
|
y_pred = qres.x.cpu()
|
|
y = batch.cpu() # should already be on CPU but just in case
|
|
pendings.append(pool.submit(evaluate_audio_reconstruction, y_pred, y, self.cfg))
|
|
|
|
metrics_lp = self.log_progress(f'{evaluate_stage_name} metrics', pendings, updates=self.log_updates)
|
|
for pending in metrics_lp:
|
|
metrics = pending.result()
|
|
metrics = average(metrics)
|
|
|
|
metrics = flashy.distrib.average_metrics(metrics, len(loader))
|
|
return metrics
|
|
|
|
def generate(self):
|
|
"""Generate stage."""
|
|
self.model.eval()
|
|
sample_manager = SampleManager(self.xp, map_reference_to_sample_id=True)
|
|
generate_stage_name = str(self.current_stage)
|
|
|
|
loader = self.dataloaders['generate']
|
|
updates = len(loader)
|
|
lp = self.log_progress(generate_stage_name, loader, total=updates, updates=self.log_updates)
|
|
|
|
for batch in lp:
|
|
reference, _ = batch
|
|
reference = reference.to(self.device)
|
|
with torch.no_grad():
|
|
qres = self.model(reference)
|
|
assert isinstance(qres, quantization.QuantizedResult)
|
|
|
|
reference = reference.cpu()
|
|
estimate = qres.x.cpu()
|
|
sample_manager.add_samples(estimate, self.epoch, ground_truth_wavs=reference)
|
|
|
|
flashy.distrib.barrier()
|
|
|
|
def load_from_pretrained(self, name: str) -> dict:
|
|
model = models.CompressionModel.get_pretrained(name)
|
|
if isinstance(model, models.DAC):
|
|
raise RuntimeError("Cannot fine tune a DAC model.")
|
|
elif isinstance(model, models.HFEncodecCompressionModel):
|
|
self.logger.warning('Trying to automatically convert a HuggingFace model '
|
|
'to AudioCraft, this might fail!')
|
|
state = model.model.state_dict()
|
|
new_state = {}
|
|
for k, v in state.items():
|
|
if k.startswith('decoder.layers') and '.conv.' in k and '.block.' not in k:
|
|
# We need to determine if this a convtr or a regular conv.
|
|
layer = int(k.split('.')[2])
|
|
if isinstance(model.model.decoder.layers[layer].conv, torch.nn.ConvTranspose1d):
|
|
|
|
k = k.replace('.conv.', '.convtr.')
|
|
k = k.replace('encoder.layers.', 'encoder.model.')
|
|
k = k.replace('decoder.layers.', 'decoder.model.')
|
|
k = k.replace('conv.', 'conv.conv.')
|
|
k = k.replace('convtr.', 'convtr.convtr.')
|
|
k = k.replace('quantizer.layers.', 'quantizer.vq.layers.')
|
|
k = k.replace('.codebook.', '._codebook.')
|
|
new_state[k] = v
|
|
state = new_state
|
|
elif isinstance(model, models.EncodecModel):
|
|
state = model.state_dict()
|
|
else:
|
|
raise RuntimeError(f"Cannot fine tune model type {type(model)}.")
|
|
return {
|
|
'best_state': {'model': state}
|
|
}
|
|
|
|
@staticmethod
|
|
def model_from_checkpoint(checkpoint_path: tp.Union[Path, str],
|
|
device: tp.Union[torch.device, str] = 'cpu') -> models.CompressionModel:
|
|
"""Instantiate a CompressionModel from a given checkpoint path or dora sig.
|
|
This method is a convenient endpoint to load a CompressionModel to use in other solvers.
|
|
|
|
Args:
|
|
checkpoint_path (Path or str): Path to checkpoint or dora sig from where the checkpoint is resolved.
|
|
This also supports pre-trained models by using a path of the form //pretrained/NAME.
|
|
See `model_from_pretrained` for a list of supported pretrained models.
|
|
use_ema (bool): Use EMA variant of the model instead of the actual model.
|
|
device (torch.device or str): Device on which the model is loaded.
|
|
"""
|
|
checkpoint_path = str(checkpoint_path)
|
|
if checkpoint_path.startswith('//pretrained/'):
|
|
name = checkpoint_path.split('/', 3)[-1]
|
|
return models.CompressionModel.get_pretrained(name, device)
|
|
logger = logging.getLogger(__name__)
|
|
logger.info(f"Loading compression model from checkpoint: {checkpoint_path}")
|
|
_checkpoint_path = checkpoint.resolve_checkpoint_path(checkpoint_path, use_fsdp=False)
|
|
assert _checkpoint_path is not None, f"Could not resolve compression model checkpoint path: {checkpoint_path}"
|
|
state = checkpoint.load_checkpoint(_checkpoint_path)
|
|
assert state is not None and 'xp.cfg' in state, f"Could not load compression model from ckpt: {checkpoint_path}"
|
|
cfg = state['xp.cfg']
|
|
cfg.device = device
|
|
compression_model = models.builders.get_compression_model(cfg).to(device)
|
|
assert compression_model.sample_rate == cfg.sample_rate, "Compression model sample rate should match"
|
|
|
|
assert 'best_state' in state and state['best_state'] != {}
|
|
assert 'exported' not in state, "When loading an exported checkpoint, use the //pretrained/ prefix."
|
|
compression_model.load_state_dict(state['best_state']['model'])
|
|
compression_model.eval()
|
|
logger.info("Compression model loaded!")
|
|
return compression_model
|
|
|
|
@staticmethod
|
|
def wrapped_model_from_checkpoint(cfg: omegaconf.DictConfig,
|
|
checkpoint_path: tp.Union[Path, str],
|
|
device: tp.Union[torch.device, str] = 'cpu') -> models.CompressionModel:
|
|
"""Instantiate a wrapped CompressionModel from a given checkpoint path or dora sig.
|
|
|
|
Args:
|
|
cfg (omegaconf.DictConfig): Configuration to read from for wrapped mode.
|
|
checkpoint_path (Path or str): Path to checkpoint or dora sig from where the checkpoint is resolved.
|
|
use_ema (bool): Use EMA variant of the model instead of the actual model.
|
|
device (torch.device or str): Device on which the model is loaded.
|
|
"""
|
|
compression_model = CompressionSolver.model_from_checkpoint(checkpoint_path, device)
|
|
compression_model = models.builders.get_wrapped_compression_model(compression_model, cfg)
|
|
return compression_model
|
|
|
|
|
|
def evaluate_audio_reconstruction(y_pred: torch.Tensor, y: torch.Tensor, cfg: omegaconf.DictConfig) -> dict:
|
|
"""Audio reconstruction evaluation method that can be conveniently pickled."""
|
|
metrics = {}
|
|
if cfg.evaluate.metrics.visqol:
|
|
visqol = builders.get_visqol(cfg.metrics.visqol)
|
|
metrics['visqol'] = visqol(y_pred, y, cfg.sample_rate)
|
|
sisnr = builders.get_loss('sisnr', cfg)
|
|
metrics['sisnr'] = sisnr(y_pred, y)
|
|
return metrics
|