import itertools import fix_ulimit fix_ulimit.fix_ulimit() import torch import logging from data import Vocab, VocabError from mpire.exception import StopWorker from mpire import WorkerPool from dataclasses import dataclass, field from typing import List, Optional, Any from collections.abc import Iterator import copy import traceback class AugmentError(Exception): pass @dataclass class RawExample: # Persistent identifier for the example (e.g. database primary key) # Can be any integer, used only debugging/logging/QA id: int # List of descriptions of the example # A description may be any object that the data augmentation pipeline can # convert to a string descs: List[Any] # Example as JSON-serialized MIDI notes # # One of these notes may be marked descriptionAnchor: true, in which # case the system will assume descriptions apply starting at this note # (this defaults to the first note if not present) example: List[dict] # List of possible accompaniments as JSON-serialized MIDI notess accomps: List[List[dict]] # List of example tags (optional - used by data augmentation) example_tags: List[str] = field(default_factory=list) # List of lists of accompaniment tags (optional - used by data augmentation) accomp_tagss: List[List[str]] = field(default_factory=list) @dataclass class AugmentedExample: # Persistent identifier for the example, should match the corresponding # RawExample id: int # Description of the example, fed directly to the model desc: str # Example as JSON-serialized MIDI notes, as in RawExample example: List[dict] # Accompaniment for the example, as JSON-serialized MIDI notes accomp: List[dict] class HeritableDynamicInstancing: @classmethod def _inheritors(cls): subclasses = set() work = [cls] while work: parent = work.pop() for child in parent.__subclasses__(): if child not in subclasses: subclasses.add(child) work.append(child) return subclasses @classmethod def _class_from_config(cls, config, subkey): my_config = copy.deepcopy(config[subkey]) class_name = my_config.pop("name", None) if class_name is None: raise ValueError(f"no 'name' key in {subkey} config") candidates = [ subclass for subclass in cls._inheritors() if subclass.__name__ == class_name ] if len(candidates) == 0: raise ValueError(f"no subclass named {class_name} found") if len(candidates) > 1: raise ValueError(f"multiple subclasses named {class_name} found") return candidates[0], my_config class Dataset(HeritableDynamicInstancing): # returns a Dataset subclass instance from a configuration dictionary subkey, # where config[subkey].name is the subclass name @classmethod def dynamic_from_config(cls, config, subkey): subcls, my_config = cls._class_from_config(config, subkey) return subcls.from_config(config, my_config) # returns a Dataset object from a configuration dictionary @classmethod def from_config(cls, common_config, my_config): raise NotImplementedError("from_config must be implemented by subclass") # returns a dictionary { split_number: number_of_examples } def num_examples(self) -> dict[int, int]: raise NotImplementedError("num_examples must be implemented by subclass") # returns an iterator over RawExample objects def stream_examples( self, split: Optional[int], # split number (None means all splits) ranksize: tuple[int, int] = ( 0, 1, ), # (rank, size) tuple for distributed training order: str = "random", # order of examples, either "random" or "id", if "id" then the order should be deterministic ) -> Iterator[RawExample]: return self.stream_examples_impl(split, ranksize, order) # performs any mutation needed to shuffle the examples in the dataset # such that stream_examples will return a different order when called with order="random" def shuffle(self): raise NotImplementedError("shuffle must be implemented by subclass") def stream_examples_impl( self, split: Optional[int], ranksize: tuple[int, int], order: str ) -> Iterator[RawExample]: raise NotImplementedError( "stream_examples_impl must be implemented by subclass" ) class Augmenter(HeritableDynamicInstancing): # returns an Augmenter subclass instance from a configuration dictionary subkey, # where config[subkey].name is the subclass name @classmethod def dynamic_from_config(cls, vocab, config, subkey): subcls, my_config = cls._class_from_config(config, subkey) return subcls.from_config(vocab, config, my_config) # returns an Augmenter object from a configuration dictionary @classmethod def from_config(cls, vocab, common_config, my_config): raise NotImplementedError("from_config must be implemented by subclass") # can raise AugmentError def augment(self, example: RawExample) -> AugmentedExample: raise NotImplementedError("augment must be implemented by subclass") class TrivialAugmenter(Augmenter): @classmethod def from_config(cls, vocab, common_config, my_config): return cls() def augment(self, example: RawExample) -> AugmentedExample: return AugmentedExample( id=example.id, desc=" ".join(str(d) for d in example.descs), example=example.example, accomp=list(itertools.chain.from_iterable(example.accomps)), ) class Batch: def __init__( self, vocab, xs, to_device, texts=None, ids=None, hashes=None, encoder_input_ids=None, encoder_attention_mask=None, ): pad_mask = xs[:, :-1, 0] != vocab.pad.index self.ntokens = pad_mask.data.sum() self.fill = self.ntokens / (xs.shape[0] * xs.shape[1]) self.length = xs.shape[1] # copy to device xs = xs.to(to_device, copy=True) self.encoder_input_ids = ( encoder_input_ids.to(to_device, copy=True) if encoder_input_ids is not None else None ) self.encoder_attention_mask = ( encoder_attention_mask.to(to_device, copy=True) if encoder_attention_mask is not None else None ) self.tgt = xs[:, :-1].contiguous() self.tgt_y = xs[:, 1:, 0].contiguous() self.texts = texts self.ids = ids self.hashes = hashes def worker_prepare_example(worker_state, raw_example: RawExample): try: state = worker_state["state"] try: aug_example = state.augmenter.augment(raw_example) vec = state.vocab.midi_to_tensor( aug_example.example, accompany=aug_example.accomp, strict=True, ) except (VocabError, AugmentError) as e: logging.debug(f"failed to convert example to tensor: {str(e)}") return id, False, None, None example_hash = state.vocab.hash_tensor(vec) if vec is not False else None return ( aug_example.id, vec, example_hash, aug_example.desc, ) except Exception as e: if not isinstance(e, StopWorker): traceback.print_exception(e) # MPIRE mangles exceptions, so print them here raise e class WorkerState: def __init__(self, vocab, augmenter): self.vocab = vocab self.augmenter = augmenter def __call__(self, worker_state): from util import configure_logging configure_logging() worker_state["state"] = self class DataGenerator: def __init__( self, dataset: Dataset, vocab: Vocab, split: Optional[int], ranksize: tuple[int, int], batch_size: int, seq_len: int, seq_len_min: int, seq_len_max: Optional[int], train_target: str, parallelism: int, to_device: Any, text_tokenize: Any, example_continuous: bool, pack_batch: bool, augmenter: Augmenter, ): self.dataset = dataset self.vocab = vocab self.split = split self.ranksize = ranksize self.batch_size = batch_size self.seq_len = seq_len self.seq_len_min = seq_len_min self.seq_len_max = seq_len_max self.train_target = train_target self.parallelism = parallelism self.to_device = to_device self.text_tokenize = text_tokenize self.example_continuous = example_continuous self.pack_batch = pack_batch self.augmenter = augmenter if self.text_tokenize is not None: assert ( not self.pack_batch ), "full batch packing not supported with text prompts" def shuffle(self): self.dataset.shuffle() def num_examples(self): split_examples = self.dataset.num_examples() if self.split is None: return sum(split_examples.values()) return split_examples.get(self.split, 0) def generate( self, force_batches=None, order="random", seq_len_max=None, ): seq_len_max = self.seq_len_max if self.seq_len_max is not None else self.seq_len logging.info( f"start DataGenerator.generate split={self.split} ranksize={self.ranksize}" ) with WorkerPool( n_jobs=self.parallelism, start_method="spawn", use_worker_state=True ) as pool: batch = torch.zeros(self.batch_size, self.seq_len, 9, dtype=torch.long) batch[:, :, 0] = self.vocab.pad.index insert_pos = 0 insert_b = 0 maxlen = 0 num_batches = 0 batch_ids = [] batch_hashes = [] batch_texts = [] def write_batch(): nonlocal num_batches, batch, insert_pos, insert_b, maxlen, batch_ids, batch_hashes, batch_texts # encoder batch size must be the same as the decoder batch size # when there are too few examples to fill the batch, pad with tokenized empty strings encoder_input_ids, encoder_attention_mask = ( (None, None) if self.text_tokenize is None else self.text_tokenize( batch_texts + ["" for _ in range(self.batch_size - insert_b)] ) ) batch_out = Batch( self.vocab, batch, self.to_device, texts=batch_texts, ids=batch_ids, hashes=batch_hashes, encoder_input_ids=encoder_input_ids, encoder_attention_mask=encoder_attention_mask, ) batch = torch.zeros(self.batch_size, self.seq_len, 9, dtype=torch.long) batch[:, :, 0] = self.vocab.pad.index insert_pos = 0 insert_b = 0 maxlen = 0 batch_ids = [] batch_hashes = [] batch_texts = [] num_batches += 1 yield batch_out map_fn = pool.imap if order == "random" else pool.imap_unordered mapped_iterator = map_fn( worker_prepare_example, self.dataset.stream_examples( split=self.split, ranksize=self.ranksize, order=order ), chunk_size=8, worker_init=WorkerState(self.vocab, self.augmenter), ) continuing_examples = [] while True: if force_batches is not None and num_batches >= force_batches: return continued = False if len(continuing_examples) > 0: id, vec, hash, text = continuing_examples.pop() continued = True else: try: id, vec, hash, text = next(mapped_iterator) except StopIteration: break if vec is False: continue try: if not continued: if ( vec.shape[0] > seq_len_max or vec.shape[0] < self.seq_len_min ): continue insert_len = min(vec.shape[0], self.seq_len - insert_pos) if self.example_continuous and insert_len < vec.shape[0]: continuing_examples.append( (id, vec[insert_len - 1 :], hash, text) ) batch[insert_b, insert_pos : insert_pos + insert_len] = vec[ :insert_len ] batch_ids.append(id) batch_hashes.append(hash) batch_texts.append(text) insert_pos += insert_len maxlen = max(maxlen, insert_pos) if self.pack_batch: if insert_pos >= self.seq_len: insert_b += 1 insert_pos = 0 else: insert_b += 1 insert_pos = 0 finally: del vec if insert_b == self.batch_size: yield from write_batch() if force_batches is None: if insert_b > 0 or insert_pos > 0: yield from write_batch() else: while num_batches < force_batches: yield from write_batch()