Files
2025-01-12 18:52:26 +02:00

288 lines
12 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.
from omegaconf import DictConfig
from . import builders, musicgen
from .compression import CompressionSolver
from .. import models
from ..modules.conditioners import JascoCondConst, SegmentWithAttributes
import torch
import typing as tp
import flashy
import time
import math
class JascoSolver(musicgen.MusicGenSolver):
"""Solver for JASCO - Joint Audio and Symbolic Conditioning for Temporally Controlled Text-to-Music Generation
https://arxiv.org/abs/2406.10970.
"""
DATASET_TYPE: builders.DatasetType = builders.DatasetType.JASCO
def __init__(self, cfg: DictConfig):
super().__init__(cfg)
# initialize generation parameters by config
self.generation_params = {
'cfg_coef_all': self.cfg.generate.lm.cfg_coef_all,
'cfg_coef_txt': self.cfg.generate.lm.cfg_coef_txt
}
self.latent_mean = cfg.compression_model_latent_mean
self.latent_std = cfg.compression_model_latent_std
self.mse = torch.nn.MSELoss(reduction='none')
self._best_metric_name = 'loss'
def build_model(self) -> None:
"""Instantiate model and optimization."""
assert self.cfg.efficient_attention_backend == "xformers", "JASCO v1 models support only xformers backend."
self.compression_model = CompressionSolver.wrapped_model_from_checkpoint(
self.cfg, self.cfg.compression_model_checkpoint, device=self.device)
assert self.compression_model.sample_rate == self.cfg.sample_rate, (
f"Compression model sample rate is {self.compression_model.sample_rate} but "
f"Solver sample rate is {self.cfg.sample_rate}."
)
# instantiate JASCO model
self.model: models.FlowMatchingModel = models.builders.get_jasco_model(self.cfg,
self.compression_model).to(self.device)
# initialize optimization
self.initialize_optimization()
def _get_latents(self, audio):
with torch.no_grad():
latents = self.compression_model.model.encoder(audio)
return latents.permute(0, 2, 1) # [B, D, T] -> [B, T, D]
def _prepare_latents_and_attributes(
self, batch: tp.Tuple[torch.Tensor, tp.List[SegmentWithAttributes]],
) -> tp.Tuple[dict, torch.Tensor, torch.Tensor]:
"""Prepare input batchs for language model training.
Args:
batch (tuple[torch.Tensor, list[SegmentWithAttributes]]): Input batch with audio tensor of shape [B, C, T]
and corresponding metadata as SegmentWithAttributes (with B items).
Returns:
Condition tensors (dict[str, any]): Preprocessed condition attributes.
Tokens (torch.Tensor): Audio tokens from compression model, of shape [B, K, T_s],
with B the batch size, K the number of codebooks, T_s the token timesteps.
Padding mask (torch.Tensor): Mask with valid positions in the tokens tensor, of shape [B, K, T_s].
"""
audio, infos = batch
audio = audio.to(self.device)
assert audio.size(0) == len(infos), (
f"Mismatch between number of items in audio batch ({audio.size(0)})",
f" and in metadata ({len(infos)})"
)
latents = self._get_latents(audio)
# prepare attributes
if JascoCondConst.CRD.value in self.cfg.conditioners:
null_chord_idx = self.cfg.conditioners.chords.chords_emb.card
else:
null_chord_idx = -1
attributes = [info.to_condition_attributes() for info in infos]
if self.model.cfg_dropout is not None:
attributes = self.model.cfg_dropout(samples=attributes,
cond_types=["wav", "text", "symbolic"],
null_chord_idx=null_chord_idx)
attributes = self.model.att_dropout(attributes)
tokenized = self.model.condition_provider.tokenize(attributes)
with self.autocast:
condition_tensors = self.model.condition_provider(tokenized)
# create a padding mask to hold valid vs invalid positions
padding_mask = torch.ones_like(latents, dtype=torch.bool, device=latents.device)
return condition_tensors, latents, padding_mask
def _normalized_latents(self, latents: torch.Tensor) -> torch.Tensor:
"""Normalize latents."""
return (latents - self.latent_mean) / self.latent_std
def _unnormalized_latents(self, latents: torch.Tensor) -> torch.Tensor:
"""Unnormalize latents."""
return (latents * self.latent_std) + self.latent_mean
def _z(self, z_0: torch.Tensor, z_1: torch.Tensor, t: torch.Tensor, sigma_min: float = 1e-5) -> torch.Tensor:
"""Interpolate data and prior."""
return (1 - (1 - sigma_min) * t) * z_0 + t * z_1
def _vector_field(self, z_0: torch.Tensor, z_1: torch.Tensor, sigma_min: float = 1e-5) -> torch.Tensor:
"""Compute the GT vector field.
sigma_min is a small value to avoid numerical instabilities."""
return z_1 - (1 - sigma_min) * z_0
def _compute_loss(self, t: torch.Tensor, v_theta: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
"""Compute the loss."""
loss_func = self.cfg.get('loss_func', 'increasing')
if loss_func == 'uniform':
scales = 1
elif loss_func == 'increasing':
scales = 1 + t # type: ignore
elif loss_func == 'decreasing':
scales = 2 - t # type: ignore
else:
raise ValueError('unsupported loss_func was passed in config')
return (scales * self.mse(v_theta, v)).mean()
def run_step(self, idx: int, batch: tp.Tuple[torch.Tensor, tp.List[SegmentWithAttributes]], metrics: dict) -> dict:
"""Perform one training or valid step on a given batch."""
condition_tensors, latents, padding_mask = self._prepare_latents_and_attributes(batch)
self.deadlock_detect.update('tokens_and_conditions')
B, T, D = latents.shape
device = self.device
# normalize latents
z_1 = self._normalized_latents(latents)
# sample the N(0,1) prior
z_0 = torch.randn(B, T, D, device=device)
# random time parameter, between 0 to 1
t = torch.rand((B, 1, 1), device=device)
# interpolate data and prior
z = self._z(z_0, z_1, t)
# compute the GT vector field
v = self._vector_field(z_0, z_1)
with self.autocast:
v_theta = self.model(latents=z,
t=t,
conditions=[],
condition_tensors=condition_tensors)
loss = self._compute_loss(t, v_theta, v)
unscaled_loss = loss.clone()
self.deadlock_detect.update('loss')
if self.is_training:
metrics['lr'] = self.optimizer.param_groups[0]['lr']
if self.scaler is not None:
loss = self.scaler.scale(loss)
self.deadlock_detect.update('scale')
if self.cfg.fsdp.use:
loss.backward()
flashy.distrib.average_tensors(self.model.buffers())
elif self.cfg.optim.eager_sync:
with flashy.distrib.eager_sync_model(self.model):
loss.backward()
else:
# this should always be slower but can be useful
# for weird use cases like multiple backwards.
loss.backward()
flashy.distrib.sync_model(self.model)
self.deadlock_detect.update('backward')
if self.scaler is not None:
self.scaler.unscale_(self.optimizer)
if self.cfg.optim.max_norm:
if self.cfg.fsdp.use:
metrics['grad_norm'] = self.model.clip_grad_norm_(self.cfg.optim.max_norm) # type: ignore
else:
metrics['grad_norm'] = torch.nn.utils.clip_grad_norm_(
self.model.parameters(), self.cfg.optim.max_norm
)
if self.scaler is None:
self.optimizer.step()
else:
self.scaler.step(self.optimizer)
self.scaler.update()
if self.lr_scheduler:
self.lr_scheduler.step()
self.optimizer.zero_grad()
self.deadlock_detect.update('optim')
if self.scaler is not None:
scale = self.scaler.get_scale()
metrics['grad_scale'] = scale
if not loss.isfinite().all():
raise RuntimeError("Model probably diverged.")
metrics['loss'] = unscaled_loss
return metrics
def _decode_latents(self, latents):
return self.compression_model.model.decoder(latents.permute(0, 2, 1))
@torch.no_grad()
def run_generate_step(self, batch: tp.Tuple[torch.Tensor, tp.List[SegmentWithAttributes]],
gen_duration: float, prompt_duration: tp.Optional[float] = None,
remove_text_conditioning: bool = False,
**generation_params) -> dict:
"""Run generate step on a batch of optional audio tensor and corresponding attributes.
Args:
batch (tuple[torch.Tensor, list[SegmentWithAttributes]]):
use_prompt (bool): Whether to do audio continuation generation with prompt from audio batch.
gen_duration (float): Target audio duration for the generation.
prompt_duration (float, optional): Duration for the audio prompt to use for continuation.
remove_text_conditioning (bool, optional): Whether to remove the prompt from the generated audio.
generation_params: Additional generation parameters.
Returns:
gen_outputs (dict): Generation outputs, consisting in audio, audio tokens from both the generation
and the prompt along with additional information.
"""
bench_start = time.time()
audio, meta = batch
assert audio.size(0) == len(meta), (
f"Mismatch between number of items in audio batch ({audio.size(0)})",
f" and in metadata ({len(meta)})"
)
# prepare attributes
attributes = [x.to_condition_attributes() for x in meta]
# prepare audio prompt
if prompt_duration is None:
prompt_audio = None
else:
assert prompt_duration < gen_duration, "Prompt duration must be lower than target generation duration"
prompt_audio_frames = int(prompt_duration * self.compression_model.sample_rate)
prompt_audio = audio[..., :prompt_audio_frames]
# get audio tokens from compression model
if prompt_audio is None or prompt_audio.nelement() == 0:
num_samples = len(attributes)
prompt_tokens = None
else:
num_samples = None
prompt_audio = prompt_audio.to(self.device)
prompt_tokens, scale = self.compression_model.encode(prompt_audio)
assert scale is None, "Compression model in MusicGen should not require rescaling."
# generate by sampling from the LM
with self.autocast:
total_gen_len = math.ceil(gen_duration * self.compression_model.frame_rate)
gen_latents = self.model.generate(
prompt_tokens, attributes, max_gen_len=total_gen_len,
num_samples=num_samples, **self.generation_params)
# generate audio from latents
assert gen_latents.dim() == 3 # [B, T, D]
# unnormalize latents
gen_latents = self._unnormalized_latents(gen_latents)
gen_audio = self._decode_latents(gen_latents)
bench_end = time.time()
gen_outputs = {
'rtf': (bench_end - bench_start) / gen_duration,
'ref_audio': audio,
'gen_audio': gen_audio,
'gen_tokens': gen_latents,
'prompt_audio': prompt_audio,
'prompt_tokens': prompt_tokens,
}
return gen_outputs