import os
import warnings
import torch
import pandas as pd
import numpy as np
from torch.utils import data
from typing import Dict
from collections import OrderedDict
def get_id_lengths(df: pd.DataFrame, ids: np.ndarray, length_column: str = "LEN_EXIST") -> torch.Tensor:
sub_df = df.groupby("CLUSTER")[[length_column]].mean().reset_index()
length_dict = dict(zip(sub_df["CLUSTER"], sub_df[length_column]))
lengths_list = [length_dict[id] for id in ids]
lengths = torch.tensor(lengths_list, dtype=torch.float32)
return lengths
def get_lengths_dict(dataset) -> Dict[str, torch.Tensor]:
lengths_dict = {}
for dataset_key in dataset.correct_dataset_ordering:
length_column = "nheavy" if dataset_key == "sm" else "LEN_EXIST"
lengths_dict[dataset_key] = get_id_lengths(
dataset.dataset_dict[dataset_key],
dataset.ID_dict[dataset_key],
length_column,
)
return lengths_dict
def adjust_samples_for_num_replicas(num_per_epoch_dict: Dict[str, int], num_replicas: int) -> Dict[str, int]:
"""
Modifies the number of examples per epoch for each dataset to be divisible by num_replicas.
Args:
num_per_epoch_dict (dict): Mapping from dataset name to number of examples to sample from that dataset per epoch
num_replicas (int): The number of nodes/GPUs in the distributed training setup.
Returns:
dict: The modified num_per_epoch_dict where the number of examples per epoch for each dataset is divisible by num_replicas.
"""
adjusted_dict = {}
for dataset, num_per_epoch in num_per_epoch_dict.items():
adjusted_num = (num_per_epoch//num_replicas) * num_replicas
adjusted_dict[dataset] = adjusted_num
return adjusted_dict
class DistributedWeightedSampler(data.Sampler):
def __init__(
self,
dataset,
weights_dict,
num_example_per_epoch=25600,
fractions=dict(
pdb=1.0,
fb=0,
compl=0,
neg_compl=0,
na_compl=0,
neg_na_compl=0,
distil_tf=0,
tf=0,
neg_tf=0,
rna=0,
dna=0,
sm_compl=0,
metal_compl=0,
sm_compl_multi=0,
sm_compl_covale=0,
sm_compl_asmb=0,
sm=0,
atomize_pdb=0,
atomize_complex=0,
),
num_replicas=None,
rank=None,
batch_by_dataset=False,
batch_by_length=False,
**unused,
):
assert (num_replicas is not None), "Please provide the number of replicas in DistributedWeightedSampler"
assert (rank is not None), "Please provide the rank of the current node in DistributedWeightedSampler"
assert (num_example_per_epoch % num_replicas
) == 0, "Please ensure that the number of examples per epoch is evenly divisible by the number of nodes"
assert np.allclose(
sum([v for k, v in fractions.items()]),
1.0), f"Fractions of datasets add up to {sum([v for k,v in fractions.items()])}, should add up to 1.0"
if not batch_by_dataset:
assert (not batch_by_length), "Cannot batch by length without also batching by dataset."
self.dataset = dataset
self.lengths_dict = get_lengths_dict(dataset)
self.weights_dict = weights_dict
self.num_replicas = num_replicas
self.batch_by_length = batch_by_length
self.batch_by_dataset = batch_by_dataset
self.rank = rank
self.epoch = 0
self.num_per_epoch_dict = {
dataset_name: int(num_example_per_epoch * fractions[dataset_name])
for dataset_name in self.dataset.correct_dataset_ordering
}
self.num_per_epoch_dict = adjust_samples_for_num_replicas(self.num_per_epoch_dict, num_replicas)
self.total_size = sum(self.num_per_epoch_dict.values())
self.num_samples = self.total_size // self.num_replicas
if rank == 0:
print("Total examples:")
for k, v in self.dataset.ID_dict.items():
print(" " + k, ":", len(v))
print(f"Training examples per epoch ({self.total_size} total):")
for k, v in self.num_per_epoch_dict.items():
print(" " + k, ":", v)
def _sample_from_dataset(self, dataset_name, g):
"""
Samples a specified number of sequences from the given dataset.
Samples with replacement based on the dataset type, forcing replacement if sampling more than dataset length.
Parameters:
dataset_name (str): The name of the dataset to sample from.
g (torch.Generator): A pre-seeded generator to ensure consistency across nodes.
Returns:
Tensor: A tensor of sampled indices from the dataset.
"""
if self.num_per_epoch_dict[dataset_name] > len(self.dataset.ID_dict[dataset_name]):
warnings.warn(
f"Number of sequences to be sampled in one epoch is greater than the number of "
f"sequences in the dataset. Must sample with replacement. Ensure that this is the desired behavior. Dataset: {dataset_name}, "
f"Dataset length: {len(self.dataset.ID_dict[dataset_name])}, "
f"# to be sampled: {self.num_per_epoch_dict[dataset_name]}")
replacement = self.num_per_epoch_dict[dataset_name] > len(self.dataset.ID_dict[dataset_name])
return torch.multinomial(
self.weights_dict[dataset_name],
self.num_per_epoch_dict[dataset_name],
generator=g,
replacement=replacement,
)
def __iter__(self):
g = torch.Generator()
g.manual_seed(self.epoch)
indices = torch.arange(len(self.dataset))
offset = 0
sel_indices = torch.tensor((), dtype=int)
if self.batch_by_dataset:
for dataset_name in self.dataset.correct_dataset_ordering:
if self.num_per_epoch_dict[dataset_name] > 0:
sampled_indices = self._sample_from_dataset(dataset_name, g)
split_sampled_indices = torch.split(sampled_indices, len(sampled_indices) // self.num_replicas)
assert all([len(x) == len(split_sampled_indices[0]) for x in split_sampled_indices])
indices_for_rank = split_sampled_indices[self.rank]
if self.batch_by_length:
lengths_of_dataset = self.lengths_dict[dataset_name]
lengths_per_rank = lengths_of_dataset[indices_for_rank]
sorted_length_order = torch.argsort(lengths_per_rank)
indices_for_rank = indices_for_rank[sorted_length_order]
indices_for_rank += offset
sel_indices = torch.cat((sel_indices, indices_for_rank))
offset += len(self.dataset.ID_dict[dataset_name])
indices = sel_indices[torch.randperm(len(sel_indices), generator=g)]
else:
for dataset_name in self.dataset.correct_dataset_ordering:
if self.num_per_epoch_dict[dataset_name] > 0:
sampled_idx = self._sample_from_dataset(dataset_name, g)
sel_indices = torch.cat((sel_indices, indices[sampled_idx + offset]))
offset += len(self.dataset.ID_dict[dataset_name])
indices = sel_indices[torch.randperm(len(sel_indices), generator=g)]
indices = indices[self.rank:self.total_size:self.num_replicas]
assert len(indices) == self.num_samples
return iter(indices.tolist())
def __len__(self):
return self.num_samples
def set_epoch(self, epoch):
self.epoch = epoch
class DistributedWeightedSamplerOld(data.Sampler):
def __init__(
self,
dataset,
weights_dict,
num_example_per_epoch=25600,
fractions=OrderedDict(
pdb=1.0,
fb=0,
compl=0,
neg_compl=0,
na_compl=0,
neg_na_compl=0,
distil_tf=0,
tf=0,
neg_tf=0,
rna=0,
dna=0,
sm_compl=0,
metal_compl=0,
sm_compl_multi=0,
sm_compl_covale=0,
sm_compl_asmb=0,
sm=0,
atomize_pdb=0,
atomize_complex=0,
),
num_replicas=None,
rank=None,
datasets_with_replacement=[
"pdb",
"fb",
"compl",
"neg_compl",
"na_compl",
"neg_na_compl",
"distil_tf",
"tf",
"neg_tf",
"rna",
"dna",
],
lengths=None,
batch_by_dataset=False,
batch_by_length=False,
**unused,
):
if num_replicas is None:
if not torch.distributed.is_available():
raise RuntimeError("Requires distributed package to be available")
num_replicas = torch.distributed.get_world_size()
if rank is None:
if not torch.distributed.is_available():
raise RuntimeError("Requires distributed package to be available")
rank = torch.distributed.get_rank()
assert (num_example_per_epoch % num_replicas
) == 0, "Please ensure that the number of examples per epoch is evenly divisible by the number of nodes"
assert np.allclose(
sum([v for k, v in fractions.items()]),
1.0), f"Fractions of datasets add up to {sum([v for k,v in fractions.items()])}, should add up to 1.0"
if lengths is not None and os.path.isfile(lengths):
lengths = torch.load(lengths, weights_only=False)
else:
if lengths is not None:
warnings.warn(f"Lengths file {lengths} does not exist. Ignoring lengths file.")
lengths = None
if batch_by_length:
assert (lengths is not None), "If batching by length, must pass a valid lengths tensor."
if not batch_by_dataset:
assert (not batch_by_length), "Cannot batch by length without also batching by dataset."
self.dataset = dataset
self.weights_dict = weights_dict
self.num_replicas = num_replicas
self.lengths = lengths
self.batch_by_length = batch_by_length
self.batch_by_dataset = batch_by_dataset
if batch_by_dataset:
self.num_per_epoch_dict = adjust_samples_for_num_replicas(
OrderedDict([(
dataset_name,
int(round(num_example_per_epoch * fractions[dataset_name])),
) for dataset_name in self.dataset.dataset_dict.keys()]),
num_replicas,
)
else:
self.num_per_epoch_dict = OrderedDict([(
dataset_name,
int(round(num_example_per_epoch * fractions[dataset_name])),
) for dataset_name in self.dataset.dataset_dict.keys()])
dataset_names = list(self.dataset.dataset_dict.keys())
nonzero_dataset_names = [name for name in dataset_names if self.num_per_epoch_dict[name] > 0]
num_per_epoch_actual = sum([self.num_per_epoch_dict[name] for name in nonzero_dataset_names])
remainder = num_example_per_epoch - num_per_epoch_actual
remainder = remainder - (remainder%num_replicas)
self.num_per_epoch_dict[nonzero_dataset_names[0]] += remainder
self.total_size = num_per_epoch_actual + remainder
self.num_samples = self.total_size // self.num_replicas
self.rank = rank
self.epoch = 0
self.datasets_with_replacement = datasets_with_replacement
if rank == 0:
print("Total examples:")
for k, v in self.dataset.ID_dict.items():
print(" " + k, ":", len(v))
print(f"Training examples per epoch ({self.total_size} total):")
for k, v in self.num_per_epoch_dict.items():
print(" " + k, ":", v)
def _sample_from_dataset(self, dataset_name, g):
"""
Samples a specified number of sequences from the given dataset.
Samples with replacement based on the dataset type, forcing replacement if sampling more than dataset length.
Parameters:
dataset_name (str): The name of the dataset to sample from.
g (torch.Generator): A pre-seeded generator to ensure consistency across nodes.
Returns:
Tensor: A tensor of sampled indices from the dataset.
"""
if self.num_per_epoch_dict[dataset_name] > len(self.dataset.ID_dict[dataset_name]):
warnings.warn(
f"Number of sequences to be sampled in one epoch is greater than the number of "
f"sequences in the dataset. Must sample with replacement. Ensure that this is the desired behavior. Dataset: {dataset_name}, "
f"Dataset length: {len(self.dataset.ID_dict[dataset_name])}, "
f"# to be sampled: {self.num_per_epoch_dict[dataset_name]}")
replacement = (dataset_name in self.datasets_with_replacement
or self.num_per_epoch_dict[dataset_name] > len(self.dataset.ID_dict[dataset_name]))
return torch.multinomial(
self.weights_dict[dataset_name],
self.num_per_epoch_dict[dataset_name],
generator=g,
replacement=replacement,
)
def __iter__(self):
g = torch.Generator()
g.manual_seed(self.epoch)
indices = torch.arange(len(self.dataset))
offset = 0
sel_indices = torch.tensor((), dtype=int)
if self.batch_by_dataset:
for dataset_name in self.dataset.dataset_dict.keys():
if self.num_per_epoch_dict[dataset_name] > 0:
sampled_idx = self._sample_from_dataset(dataset_name, g) + offset
sampled_idx_split = torch.split(sampled_idx, len(sampled_idx) // self.num_replicas)
assert all([len(x) == len(sampled_idx_split[0]) for x in sampled_idx_split])
if self.batch_by_length and self.lengths is not None:
sampled_idx_split = [x[torch.argsort(self.lengths[x])] for x in sampled_idx_split]
sel_indices = torch.cat((sel_indices, indices[sampled_idx_split[self.rank]]))
offset += len(self.dataset.ID_dict[dataset_name])
indices = sel_indices[torch.randperm(len(sel_indices), generator=g)]
else:
for dataset_name in self.dataset.dataset_dict.keys():
if self.num_per_epoch_dict[dataset_name] > 0:
sampled_idx = self._sample_from_dataset(dataset_name, g)
sel_indices = torch.cat((sel_indices, indices[sampled_idx + offset]))
offset += len(self.dataset.ID_dict[dataset_name])
indices = sel_indices[torch.randperm(len(sel_indices), generator=g)]
indices = indices[self.rank:self.total_size:self.num_replicas]
assert (len(indices) == self.num_samples)
return iter(indices.tolist())
def __len__(self):
return self.num_samples
def set_epoch(self, epoch):
self.epoch = epoch
sampler_factory = {
"DistributedWeightedSampler": DistributedWeightedSampler,
"DistributedWeightedSamplerOld": DistributedWeightedSamplerOld,
}