chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:18:33 +08:00
commit 4ececc111a
2017 changed files with 331736 additions and 0 deletions
@@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
'''Copyright The Microsoft DeepSpeed Team'''
@@ -0,0 +1,887 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import glob
import os
import sys
from collections import defaultdict
import csv
import time
from multiprocessing import Process, Manager
import numpy as np
import torch
from torch.utils.data import BatchSampler, SequentialSampler, DataLoader, Subset
import deepspeed.comm as dist
from deepspeed.utils import logger
from deepspeed.runtime.data_pipeline.data_sampling.indexed_dataset import MMapIndexedDataset, valid_dtypes
from deepspeed.runtime.data_pipeline.data_sampling.utils import split_dataset, split_index, create_mmap_dataset_builder, close_mmap_dataset_builder, find_fit_int_dtype
class DataAnalyzer(object):
def __init__(self,
dataset,
num_workers=1,
worker_id=0,
num_threads=1,
num_threads_reduce=1,
specific_threads=[],
batch_size=1,
metric_names=[],
metric_functions=[],
metric_types=[],
metric_dtypes=[],
save_path="./",
collate_fn=None,
custom_map_init=None,
custom_map_update=None,
custom_map_finalize=None,
custom_reduce=None,
sample_indices=None):
super().__init__()
self.dataset = dataset
self.num_workers = num_workers
self.worker_id = worker_id
self.num_threads = num_threads
self.num_threads_reduce = num_threads_reduce
self.specific_threads = specific_threads
self.batch_size = batch_size
self.metric_names = metric_names
self.metric_functions = metric_functions
self.metric_types = metric_types
self.metric_dtypes = metric_dtypes
self.save_path = save_path
self.collate_fn = collate_fn
self.custom_map_init = custom_map_init
self.custom_map_update = custom_map_update
self.custom_map_finalize = custom_map_finalize
self.custom_reduce = custom_reduce
self.sample_indices = sample_indices
def init_metric_results(self, thread_id, metric_names, metric_types, metric_dtypes, save_path, worker_id):
metric_results = []
for m_idx in range(len(metric_names)):
metric_name, metric_type, metric_dtype = metric_names[m_idx], \
metric_types[m_idx], metric_dtypes[m_idx]
assert metric_dtype in valid_dtypes, f"metric_dtype {metric_dtype} not supported. Supported dtypes {valid_dtypes}"
metric_save_path = f"{save_path}/{metric_name}/worker{worker_id}_thread{thread_id}/"
os.makedirs(metric_save_path, exist_ok=True)
if metric_type == 'single_value_per_sample':
sample_to_metric_fname = f"{metric_save_path}/{metric_name}_sample_to_metric"
sample_to_metric_builder = create_mmap_dataset_builder(sample_to_metric_fname, metric_dtype)
metric_to_sample_fname = f"{metric_save_path}/{metric_name}_metric_to_sample"
for _f in glob.glob(f"{glob.escape(metric_to_sample_fname)}*"):
os.remove(_f)
metric_to_sample_dict = defaultdict(list)
metric_results.append({
"sample_to_metric_fname": sample_to_metric_fname,
"sample_to_metric_builder": sample_to_metric_builder,
"metric_to_sample_fname": metric_to_sample_fname,
"metric_to_sample_dict": metric_to_sample_dict
})
elif metric_type == 'accumulate_value_over_samples':
metric_value = None
metric_value_fname = f"{metric_save_path}/{metric_name}_metric_value"
metric_results.append({"metric_value": metric_value, "metric_value_fname": metric_value_fname})
return metric_results
def update_metric_results(self,
data,
metric_types,
metric_dtypes,
metric_functions,
metric_results,
batch_start_idx=0):
for m_idx in range(len(metric_types)):
metric_type, metric_dtype, metric_function, metric_result = metric_types[m_idx], \
metric_dtypes[m_idx], metric_functions[m_idx], metric_results[m_idx]
metric_values = metric_function(data)
assert torch.is_tensor(metric_values) or isinstance(metric_values, np.ndarray), \
"metric_function must return a tensor or array"
assert metric_values.dtype == metric_dtype, \
f"metric_function result dtype {metric_values.dtype} does not match metric_dtype {metric_dtype}"
if isinstance(metric_values, np.ndarray):
metric_values = torch.from_numpy(metric_values)
if metric_type == 'single_value_per_sample':
for row in range(metric_values.size()[0]):
sample_idx = batch_start_idx + row # sample idx following dataset iteration order
if isinstance(data, dict) and 'index' in data: # Megatron use case, idx provided in 'index' field
sample_idx = data['index'][row][0].item()
elif self.sample_indices is not None: # user defined shuffling of indices
sample_idx = self.sample_indices[sample_idx]
metric_result["sample_to_metric_builder"].add_item(metric_values[row].reshape(-1))
metric_result["metric_to_sample_dict"][metric_values[row].item()].append(sample_idx)
for m_value in metric_result["metric_to_sample_dict"]:
if len(metric_result["metric_to_sample_dict"][m_value]) > 100:
metric_fname = metric_result["metric_to_sample_fname"]
with open(f"{metric_fname}_{m_value}.csv", 'a') as f:
writer = csv.writer(f)
writer.writerows([metric_result["metric_to_sample_dict"][m_value]])
metric_result["metric_to_sample_dict"][m_value] = []
elif metric_type == 'accumulate_value_over_samples':
if metric_result["metric_value"] is None:
metric_result["metric_value"] = metric_values
else:
metric_result["metric_value"].add_(metric_values)
def finalize_metric_results(self, metric_types, metric_dtypes, metric_results):
for m_idx in range(len(metric_types)):
metric_type, metric_dtype, metric_result = metric_types[m_idx], \
metric_dtypes[m_idx], metric_results[m_idx]
if metric_type == 'single_value_per_sample':
metric_fname = metric_result["sample_to_metric_fname"]
close_mmap_dataset_builder(metric_result["sample_to_metric_builder"], metric_fname)
for m_value in metric_result["metric_to_sample_dict"]:
if len(metric_result["metric_to_sample_dict"][m_value]) > 0:
metric_fname = metric_result["metric_to_sample_fname"]
with open(f"{metric_fname}_{m_value}.csv", 'a') as f:
writer = csv.writer(f)
writer.writerows([metric_result["metric_to_sample_dict"][m_value]])
metric_result["metric_to_sample_dict"][m_value] = []
elif metric_type == 'accumulate_value_over_samples':
if metric_result["metric_value"] is not None:
metric_value_builder = create_mmap_dataset_builder(metric_result["metric_value_fname"],
metric_dtype)
metric_value_builder.add_item(metric_result["metric_value"].reshape(-1))
close_mmap_dataset_builder(metric_value_builder, metric_result["metric_value_fname"])
def run_map_helper(self, thread_id):
start_idx, end_idx = self.thread_splits[thread_id][0], \
self.thread_splits[thread_id][1]
logger.info(f"worker {self.worker_id} thread {thread_id}: start working " \
f"on data subset {start_idx} to {end_idx}")
thread_dataset = Subset(self.dataset, list(range(start_idx, end_idx)))
sampler = BatchSampler(SequentialSampler(thread_dataset), batch_size=self.batch_size, drop_last=False)
iterator = iter(
DataLoader(thread_dataset,
batch_sampler=sampler,
num_workers=0,
collate_fn=self.collate_fn,
pin_memory=False))
if self.custom_map_init is None:
metric_results = self.init_metric_results(thread_id, self.metric_names, self.metric_types,
self.metric_dtypes, self.save_path, self.worker_id)
else:
metric_results = self.custom_map_init(thread_id, self.metric_names, self.metric_types, self.metric_dtypes,
self.save_path, self.worker_id)
total_sample = len(thread_dataset)
processed_sample = 0
start = time.time()
while True:
try:
data = next(iterator)
batch_start_idx = start_idx + processed_sample
if self.custom_map_update is None:
self.update_metric_results(data, self.metric_types, self.metric_dtypes, self.metric_functions,
metric_results, batch_start_idx)
else:
self.custom_map_update(data, self.metric_types, self.metric_dtypes, self.metric_functions,
metric_results, batch_start_idx)
processed_sample += len(data)
duration = (time.time() - start) / 3600.0
remain_duration = duration * total_sample / processed_sample - duration
logger.info(
f"worker {self.worker_id} thread {thread_id}: {processed_sample} " \
f"out of {total_sample} processed in {duration:.2f} hr, " \
f"estimated to finish in {remain_duration:.2f} hr")
except StopIteration:
logger.info(f"worker {self.worker_id} thread {thread_id}: reach end of file")
break
if self.custom_map_finalize is None:
self.finalize_metric_results(self.metric_types, self.metric_dtypes, metric_results)
else:
self.custom_map_finalize(self.metric_types, self.metric_dtypes, metric_results)
logger.info(f"worker {self.worker_id} thread {thread_id}: finished")
def run_map(self):
self.worker_splits, self.thread_splits = split_dataset(self.dataset, self.num_workers, self.worker_id,
self.num_threads)
if len(self.specific_threads) > 0:
threads_to_run = self.specific_threads
else:
threads_to_run = list(range(self.num_threads))
if self.num_threads > 1:
p = []
for thread in threads_to_run:
p.append(Process(target=self.run_map_helper, args=(thread, )))
p[thread].start()
for thread in threads_to_run:
p[thread].join()
else:
assert self.num_threads == 1
self.run_map_helper(0)
def get_metric_value_percentiles(self, metric_name, num_sample_per_value, total_num_samples):
logger.info(f"Checking the value percentiles of metric {metric_name}...")
processed_samples = 0
current_percentile = 5
for key in sorted(num_sample_per_value.keys()):
processed_samples += num_sample_per_value[key]
if processed_samples >= total_num_samples * current_percentile / 100.0:
logger.info(f"Metric {metric_name} {current_percentile}th percentile: {key}")
current_percentile += 5
def merge_gather_map_stats(self, num_workers, num_threads, num_threads_reduce, t_idx_reduce, metric_save_path,
metric_name, return_dict):
results = []
for w_idx in range(num_workers):
for t_idx in range(num_threads):
if (w_idx * num_threads + t_idx) % num_threads_reduce == t_idx_reduce:
w_metric_save_path = f"{metric_save_path}/worker{w_idx}_thread{t_idx}/"
w_sample_to_metric_fname = f"{w_metric_save_path}/{metric_name}_sample_to_metric"
w_sample_to_metric = MMapIndexedDataset(w_sample_to_metric_fname, skip_warmup=True)
unique_v = list(np.unique(w_sample_to_metric))
sample_to_metric_count = len(w_sample_to_metric)
logger.info(f"Finished gathering map stats from worker {w_idx} thread {t_idx}.")
results.append([unique_v, sample_to_metric_count])
return_dict[t_idx_reduce] = results
def merge_sample_to_metric(self, t_idx_reduce, metric_save_path, metric_name, metric_value_dtype,
map_worker_thread):
sample_to_metric_fname = f"{metric_save_path}/{metric_name}_sample_to_metric_thread{t_idx_reduce}"
sample_to_metric_builder = create_mmap_dataset_builder(sample_to_metric_fname, metric_value_dtype)
for w_t in map_worker_thread:
w_metric_save_path = f"{metric_save_path}/worker{w_t[0]}_thread{w_t[1]}/"
w_sample_to_metric_fname = f"{w_metric_save_path}/{metric_name}_sample_to_metric"
w_data = MMapIndexedDataset(w_sample_to_metric_fname, skip_warmup=True)
for row in range(len(w_data)):
sample_to_metric_builder.add_item(torch.tensor(w_data[row].astype(np.int64), dtype=torch.long))
logger.info(f"Finished merge_sample_to_metric from worker {w_t[0]} thread {w_t[1]}.")
close_mmap_dataset_builder(sample_to_metric_builder, sample_to_metric_fname)
def merge_metric_to_sample(self, t_idx_reduce, metric_save_path, metric_name, sample_idx_dtype, metric_value_dtype,
unique_metric_values, num_workers, num_threads):
index_to_sample_fname = f"{metric_save_path}/{metric_name}_index_to_sample_thread{t_idx_reduce}"
index_to_sample_builder = create_mmap_dataset_builder(index_to_sample_fname, sample_idx_dtype)
index_to_metric_fname = f"{metric_save_path}/{metric_name}_index_to_metric_thread{t_idx_reduce}"
index_to_metric_builder = create_mmap_dataset_builder(index_to_metric_fname, metric_value_dtype)
for unique_v in unique_metric_values:
samples = []
for w_idx in range(num_workers):
for t_idx in range(num_threads):
w_metric_save_path = f"{metric_save_path}/worker{w_idx}_thread{t_idx}/"
w_metric_to_sample_fname = f"{w_metric_save_path}/{metric_name}_metric_to_sample_{unique_v}.csv"
if os.path.isfile(w_metric_to_sample_fname):
with open(w_metric_to_sample_fname, 'r') as f:
datareader = csv.reader(f)
for row in datareader:
samples += [int(x) for x in row]
index_to_sample_builder.add_item(torch.tensor(samples, dtype=torch.long))
index_to_metric_builder.add_item(torch.tensor([unique_v], dtype=torch.long))
logger.info(f"Finished reducing metric {metric_name} value {unique_v}.")
close_mmap_dataset_builder(index_to_sample_builder, index_to_sample_fname)
close_mmap_dataset_builder(index_to_metric_builder, index_to_metric_fname)
def merge_map_results(self, dataset, metric_names, metric_types, save_path, num_workers, num_threads,
num_threads_reduce):
total_num_samples = len(dataset)
sample_idx_dtype = find_fit_int_dtype(0, total_num_samples - 1)
logger.info(
f"Total number of data samples: {total_num_samples}. Will use {sample_idx_dtype} to store the sample indexes."
)
for m_idx in range(len(metric_names)):
metric_name, metric_type = metric_names[m_idx], metric_types[m_idx]
if metric_type == 'single_value_per_sample':
metric_save_path = f"{save_path}/{metric_name}/"
sample_to_metric_count = 0
unique_metric_values = set([])
manager = Manager()
return_dict = manager.dict()
p = []
for t_idx_reduce in range(num_threads_reduce):
p.append(
Process(target=self.merge_gather_map_stats,
args=(
num_workers,
num_threads,
num_threads_reduce,
t_idx_reduce,
metric_save_path,
metric_name,
return_dict,
)))
p[t_idx_reduce].start()
for t_idx_reduce in range(num_threads_reduce):
p[t_idx_reduce].join()
for t_idx_reduce in range(num_threads_reduce):
results = return_dict[t_idx_reduce]
for res in results:
unique_metric_values = unique_metric_values.union(set(res[0]))
sample_to_metric_count += res[1]
value_max = max(unique_metric_values)
value_min = min(unique_metric_values)
assert sample_to_metric_count == total_num_samples, "The number of samples in map result files are not correct. It's possible that some map worker didn't finish successfully."
metric_value_dtype = find_fit_int_dtype(value_min, value_max)
logger.info(
f"Metric {metric_name} has values between {value_min} and {value_max}. Will use {metric_value_dtype} to store the metric values."
)
# sample_to_metric
map_worker_thread = []
for w_idx in range(num_workers):
for t_idx in range(num_threads):
map_worker_thread.append([w_idx, t_idx])
thread_splits = split_index(0, len(map_worker_thread), num_threads_reduce)
p = []
for t_idx_reduce in range(num_threads_reduce):
start_idx, end_idx = thread_splits[t_idx_reduce][0], thread_splits[t_idx_reduce][1]
p.append(
Process(target=self.merge_sample_to_metric,
args=(
t_idx_reduce,
metric_save_path,
metric_name,
metric_value_dtype,
map_worker_thread[start_idx:end_idx],
)))
p[t_idx_reduce].start()
for t_idx_reduce in range(num_threads_reduce):
p[t_idx_reduce].join()
sample_to_metric_fname = f"{metric_save_path}/{metric_name}_sample_to_metric"
sample_to_metric_builder = create_mmap_dataset_builder(sample_to_metric_fname, metric_value_dtype)
for t_idx_reduce in range(num_threads_reduce):
chunk_fname = f"{metric_save_path}/{metric_name}_sample_to_metric_thread{t_idx_reduce}"
logger.info(f"Merging file {chunk_fname}")
sample_to_metric_builder.merge_file_(chunk_fname)
close_mmap_dataset_builder(sample_to_metric_builder, sample_to_metric_fname)
sample_to_metric = MMapIndexedDataset(sample_to_metric_fname, skip_warmup=True)
assert len(sample_to_metric) == total_num_samples
# metric_to_sample
unique_metric_values = list(sorted(unique_metric_values))
thread_splits = split_index(0, len(unique_metric_values), num_threads_reduce)
p = []
for t_idx_reduce in range(num_threads_reduce):
start_idx, end_idx = thread_splits[t_idx_reduce][0], thread_splits[t_idx_reduce][1]
p.append(
Process(target=self.merge_metric_to_sample,
args=(
t_idx_reduce,
metric_save_path,
metric_name,
sample_idx_dtype,
metric_value_dtype,
unique_metric_values[start_idx:end_idx],
num_workers,
num_threads,
)))
p[t_idx_reduce].start()
for t_idx_reduce in range(num_threads_reduce):
p[t_idx_reduce].join()
index_to_sample_fname = f"{metric_save_path}/{metric_name}_index_to_sample"
index_to_sample_builder = create_mmap_dataset_builder(index_to_sample_fname, sample_idx_dtype)
index_to_metric_fname = f"{metric_save_path}/{metric_name}_index_to_metric"
index_to_metric_builder = create_mmap_dataset_builder(index_to_metric_fname, metric_value_dtype)
for t_idx_reduce in range(num_threads_reduce):
chunk_is_fname = f"{metric_save_path}/{metric_name}_index_to_sample_thread{t_idx_reduce}"
logger.info(f"Merging file {chunk_is_fname}")
index_to_sample_builder.merge_file_(chunk_is_fname)
chunk_im_fname = f"{metric_save_path}/{metric_name}_index_to_metric_thread{t_idx_reduce}"
logger.info(f"Merging file {chunk_im_fname}")
index_to_metric_builder.merge_file_(chunk_im_fname)
close_mmap_dataset_builder(index_to_sample_builder, index_to_sample_fname)
close_mmap_dataset_builder(index_to_metric_builder, index_to_metric_fname)
num_sample_per_value = DataAnalyzer.output_index_to_sample_percentile(
index_to_sample_fname, index_to_metric_fname, metric_name, metric_save_path, total_num_samples,
sample_idx_dtype)
self.get_metric_value_percentiles(metric_name, num_sample_per_value, total_num_samples)
elif metric_type == 'accumulate_value_over_samples':
metric_save_path = f"{save_path}/{metric_name}/"
metric_value = None
for w_idx in range(num_workers):
for t_idx in range(num_threads):
w_metric_save_path = f"{metric_save_path}/worker{w_idx}_thread{t_idx}/"
w_metric_value_fname = f"{w_metric_save_path}/{metric_name}_metric_value"
w_metric_value = MMapIndexedDataset(w_metric_value_fname, skip_warmup=True)
if metric_value is None:
metric_value = np.copy(w_metric_value[0])
else:
metric_value += np.copy(w_metric_value[0])
value_max = int(max(metric_value))
value_min = int(min(metric_value))
metric_value_dtype = find_fit_int_dtype(value_min, value_max)
metric_value_fname = f"{metric_save_path}/{metric_name}_metric_value"
metric_value_builder = create_mmap_dataset_builder(metric_value_fname, metric_value_dtype)
metric_value_builder.add_item(torch.tensor(metric_value.astype(np.int64), dtype=torch.long))
close_mmap_dataset_builder(metric_value_builder, metric_value_fname)
@staticmethod
def output_index_to_sample_percentile(index_to_sample_fname, index_to_metric_fname, metric_name, metric_save_path,
total_num_samples, sample_idx_dtype):
""" read index_to_metric and index_to_sample files and write distribution to index_to_sample_percentage_merged """
num_sample_per_value = {}
index_to_sample = MMapIndexedDataset(index_to_sample_fname, skip_warmup=True)
index_to_metric = MMapIndexedDataset(index_to_metric_fname, skip_warmup=True)
index_to_sample_merged_fname = f"{metric_save_path}/{metric_name}_index_to_sample_percentile_merged"
index_to_sample_merged_builder = create_mmap_dataset_builder(index_to_sample_merged_fname, sample_idx_dtype)
for v_idx in range(len(index_to_sample)):
if v_idx > 0:
assert index_to_metric[v_idx] > index_to_metric[v_idx - 1]
num_sample_per_value[index_to_metric[v_idx][0]] = len(index_to_sample[v_idx])
assert sum(list(num_sample_per_value.values())) == total_num_samples
merge_step = max(1, len(index_to_sample) // 100)
for v_idx in range(0, len(index_to_sample), merge_step):
merged_samples = np.copy(
np.concatenate(index_to_sample[v_idx:min(len(index_to_sample), (v_idx + merge_step))], axis=None))
index_to_sample_merged_builder.add_item(torch.tensor(merged_samples.astype(np.int64), dtype=torch.long))
logger.info(f"Finished merging index_to_sample {v_idx} to {v_idx+merge_step}.")
close_mmap_dataset_builder(index_to_sample_merged_builder, index_to_sample_merged_fname)
return num_sample_per_value
def run_reduce(self):
if self.custom_reduce is None:
self.merge_map_results(self.dataset, self.metric_names, self.metric_types, self.save_path,
self.num_workers, self.num_threads, self.num_threads_reduce)
else:
self.custom_reduce(self.dataset, self.metric_names, self.metric_types, self.save_path, self.num_workers,
self.num_threads, self.num_threads_reduce)
def run_map_reduce(self, comm_group=None):
self.run_map()
# wait for the mapping operation, where all nodes outputs their own (partial) result files
dist.barrier(group=comm_group)
if self.worker_id == 0:
self.run_reduce()
# wait for the reduce, where rank 0 merges all (partial) files. Dataset can then be used by all nodes.
dist.barrier(group=comm_group)
class DistributedDataAnalyzer(object):
def __init__(
self,
dataset,
num_workers=1,
num_threads=1,
worker_id=0,
batch_size=1,
metric_names=[],
metric_functions=[],
metric_types=[],
save_path="./",
collate_fn=None,
device='cuda',
comm_group=None,
sample_indices=None,
) -> None:
self.dataset = dataset
self.batch_size = batch_size
self.metric_names = metric_names
self.metric_functions = metric_functions
self.metric_types = metric_types
self.save_path = save_path
self.collate_fn = collate_fn
self.device = device
self.sample_indices = sample_indices
self.num_threads = num_threads
self.worker_id = worker_id
if not dist.is_initialized():
dist.init_distributed()
# comm_group and worker_id+num_workers are mutually exclusive
self.comm_group = comm_group
if self.comm_group is None:
# self.comm_group = deepspeed.utils.groups._clone_world_group()
self.num_workers = num_workers
self.worker_id = worker_id
else:
self.num_workers = self.comm_group.size()
self.worker_id = self.comm_group.rank()
if self.worker_id == 0:
logger.info(f"Distributed data analyzer initialized with {self.num_workers} workers.")
def run_map_helper(self, thread_id=0, metric_queues=None):
thread_start_idx, thread_end_idx = self.thread_splits[thread_id][0], self.thread_splits[thread_id][1]
worker_dataset = Subset(self.dataset, list(range(thread_start_idx, thread_end_idx)))
sampler = BatchSampler(SequentialSampler(worker_dataset), batch_size=self.batch_size, drop_last=False)
dataloader = DataLoader(dataset=worker_dataset,
batch_sampler=sampler,
num_workers=0,
collate_fn=self.collate_fn,
pin_memory=False)
# set initial results list
metric_results = []
for metric_type in self.metric_types:
assert metric_type in ['single_value_per_sample', 'accumulate_value_over_samples'], \
f"metric_type {metric_type} not implemented."
metric_results.append([] if metric_type == 'single_value_per_sample' else None)
# iterate dataloader and store metric results
batch_start_idx = thread_start_idx
for data in dataloader:
for m_idx in range(len(self.metric_names)):
metric_type, metric_function = self.metric_types[m_idx], self.metric_functions[m_idx]
metric_values = metric_function(data)
assert torch.is_tensor(metric_values) or isinstance(metric_values, np.ndarray), \
"metric_function must return a tensor or array"
if isinstance(metric_values, np.ndarray):
metric_values = torch.from_numpy(metric_values)
assert metric_values.dtype in valid_dtypes, \
f"metric_function result dtype {metric_values.dtype} not supported. Supported dtypes {valid_dtypes}"
if metric_type == 'single_value_per_sample':
for row in range(metric_values.size()[0]):
value = metric_values[row].item()
sample_idx = batch_start_idx + row # sample idx following dataset iteration order
if isinstance(data, dict) and 'index' in data: # Megatron use case
sample_idx = data['index'][row][0].item()
elif self.sample_indices is not None: # user defined shuffling of indices
sample_idx = self.sample_indices[sample_idx]
metric_results[m_idx].append((value, sample_idx))
elif metric_type == 'accumulate_value_over_samples':
if metric_results[m_idx] is None:
metric_results[m_idx] = metric_values
else:
metric_results[m_idx].add_(metric_values)
batch_start_idx += len(data)
if self.num_threads == 1:
return metric_results
# copy metric_results to the shared queue
assert metric_queues
for m_idx in range(len(self.metric_names)):
results = metric_results[m_idx]
if torch.is_tensor(results):
results = results.item() if results.dim() == 0 else results.tolist()
try:
metric_queues[m_idx].put((thread_id, results))
except Exception as e:
logger.error(f"Error putting metric results to queue: {e}")
sys.exit(1)
def run_map_reduce(self):
# setup individual dataloaders
self.worker_splits, self.thread_splits = split_dataset(self.dataset,
self.num_workers,
self.worker_id,
num_threads=self.num_threads)
node_start_idx, node_end_idx = self.worker_splits[self.worker_id]
logger.info(f"worker {self.worker_id} working on data subset {node_start_idx} to {node_end_idx}.")
if self.num_threads in [0, 1, None]:
metric_results = self.run_map_helper()
metric_results = [torch.tensor(m).to(self.device) for m in metric_results]
else:
# create a shared queue of results per metric to be populated by individual threads
with Manager() as manager:
metric_queues = [manager.Queue() for _ in self.metric_names]
threads = [
Process(target=self.run_map_helper, args=(t, metric_queues)) for t in range(self.num_threads)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
# gather results from shared queues into metric_results
metric_results = [None for _ in self.metric_names]
for m_idx, (queue, metric_type) in enumerate(zip(metric_queues, self.metric_types)):
while not queue.empty():
t_idx, t_results = queue.get()
t_start_idx, t_end_idx = self.thread_splits[t_idx]
if t_start_idx >= t_end_idx: # no results from this thread
continue #corner case for small datasets and high thread count
t_results = torch.tensor(t_results)
if metric_type == 'single_value_per_sample':
# add thread results to the metric_results list, ordered by thread idx
if metric_results[m_idx] is None: # initialize if needed
metric_results[m_idx] = torch.zeros(node_end_idx - node_start_idx,
t_results.size(1)).to(self.device)
metric_results[m_idx][t_start_idx - node_start_idx:t_end_idx - node_start_idx] = t_results
else:
if metric_results[m_idx] is None: # initialize if needed
metric_results[m_idx] = torch.zeros(t_results.size()).to(self.device)
metric_results[m_idx].add_(t_results)
# compute dtype for sample ids
total_num_samples = len(self.dataset)
sample_idx_dtype = find_fit_int_dtype(0, total_num_samples - 1)
logger.info(f"Total number of data samples: {total_num_samples}.")
logger.info(f"Will use {sample_idx_dtype} to store the sample indexes.")
for m_idx in range(len(self.metric_names)):
metric_values, metric_name, metric_type = \
metric_results[m_idx], self.metric_names[m_idx], self.metric_types[m_idx]
metric_save_path = f"{self.save_path}/{metric_name}/"
os.makedirs(metric_save_path, exist_ok=True)
if metric_type == 'single_value_per_sample':
# Compute sample and metric value dtypes based on range
values, samples = metric_values[:, 0], metric_values[:, 1]
value_min, value_max = Dist.min_max(values, self.comm_group)
sample_min, sample_max = Dist.min_max(samples, self.comm_group)
metric_value_dtype = find_fit_int_dtype(value_min, value_max)
sample_value_dtype = find_fit_int_dtype(sample_min, sample_max)
# sample_to_metric maps sample ids to metric values, as a list of metric values
sample_to_metric_fname = f"{metric_save_path}/{metric_name}_sample_to_metric"
values = [torch.tensor([x]) for x in metric_values[:, 0]]
self.file_write_ordered(values, sample_to_metric_fname, metric_value_dtype)
# distributed sorting by values, gives an ordered disjoint subset of keys on nodes
metric_values = Dist.sample_sort(metric_values, self.comm_group, self.num_workers)
metric_to_samples_dict = {}
if len(metric_values) > 0:
for value, sample in metric_values:
if value.item() not in metric_to_samples_dict:
metric_to_samples_dict[value.item()] = []
metric_to_samples_dict[value.item()].append(sample.item())
# index_to_metric and index_to_sample serialize a dicitonary from metric to samples
# index_to_metric stores a key per row, index_to_sample stores the values per row
values = [torch.tensor([x]) for x in metric_to_samples_dict.keys()]
samples = [torch.tensor(metric_to_samples_dict[x]) for x in metric_to_samples_dict.keys()]
index_to_metric_fname = f"{metric_save_path}/{metric_name}_index_to_metric" #dict keys
index_to_sample_fname = f"{metric_save_path}/{metric_name}_index_to_sample" #dict values
self.file_write_ordered(values, index_to_metric_fname, metric_value_dtype)
self.file_write_ordered(samples, index_to_sample_fname, sample_value_dtype)
if self.worker_id == 0:
DataAnalyzer.output_index_to_sample_percentile(index_to_sample_fname, index_to_metric_fname,
metric_name, metric_save_path, total_num_samples,
sample_idx_dtype)
dist.barrier(self.comm_group)
elif metric_type == 'accumulate_value_over_samples':
metric_value_fname = f"{metric_save_path}/{metric_name}_metric_value"
dist.reduce(metric_values, dst=0, op=dist.ReduceOp.SUM, group=self.comm_group)
metric_value_dtype = find_fit_int_dtype(metric_values.min(), metric_values.max())
if self.worker_id == 0:
builder = create_mmap_dataset_builder(metric_value_fname, metric_value_dtype)
builder.add_item(metric_values.cpu())
close_mmap_dataset_builder(builder, metric_value_fname)
dist.barrier(self.comm_group)
def file_write_ordered(self, tensor_list, fname, numpy_dtype):
""" MPI_file_write_ordered extended to write a list of tensors, by one rank, iteratively """
# each node has a list of rows (tensors) to be written to the file.
# we will serialize it in order to communicate it in one comm step.
tkwargs = dict(dtype=torch.int64, device=self.device)
# 1. gather on rank 0 the number of rows to be sent/recv
row_count = torch.tensor([len(tensor_list)], **tkwargs)
row_counts = torch.zeros(self.num_workers, **tkwargs)
dist.all_gather_into_tensor(row_counts, row_count, group=self.comm_group)
assert row_counts[self.worker_id] == row_count == len(tensor_list), "all_gather failed"
# 2. gather on rank 0 the sizes of the rows to be sent/recv
row_len = torch.tensor([len(l) for l in tensor_list], **tkwargs)
row_lens = Dist.gather_v(row_len, 0, self.comm_group, self.num_workers, self.worker_id)
# 4. gather on rank 0 of the total size (sum of all row lengths) to be received
size = torch.tensor([sum(row_len).item()], **tkwargs)
sizes = torch.zeros(self.num_workers, **tkwargs)
dist.all_gather_into_tensor(sizes, size, group=self.comm_group)
assert sizes[self.worker_id] == size.item(), "all_gather did not return the same sizes" #sanity check
# method to deserializes a buffer into rows of different lengths and write them to file
def write_buffer_to_file(buff, src, builder):
assert self.worker_id == 0, "only rank 0 can write to file"
# collect all buffers and write them at once
buff = buff.cpu().detach().numpy()
row_offsets = np.cumsum([0] + row_lens[src].tolist())
arr_list = []
for i in range(len(row_lens[src])):
arr_list.append(buff[row_offsets[i]:row_offsets[i + 1]])
builder.add_items(arr_list)
# 5. rank 0 prepares output folder and file
if self.worker_id == 0:
os.makedirs(os.path.dirname(fname), exist_ok=True)
builder = create_mmap_dataset_builder(fname, numpy_dtype)
# iterate through ranks that have data to be sent/recv/written
for src in [rank for rank, count in enumerate(row_counts) if count > 0]:
dist.barrier(group=self.comm_group)
if self.worker_id == 0 and src == 0: # rank 0's write its own data
buffer = torch.cat(tensor_list, dim=0).to(self.device)
write_buffer_to_file(buffer, 0, builder)
elif self.worker_id == 0 and src > 0: # rank 0 receives other rank's data and writes it
buffer = torch.empty(sizes[src].item(), dtype=numpy_dtype, device=self.device)
err = dist.recv(buffer, src=src, group=self.comm_group, tag=src)
assert err == src and len(buffer) > 0, "recv failed"
write_buffer_to_file(buffer, src, builder)
elif self.worker_id == src: # current rank sends data to rank 0
buffer = torch.cat(tensor_list, dim=0).to(self.device)
dist.send(buffer, 0, group=self.comm_group, tag=src)
# rank 0 closes the file
if self.worker_id == 0:
close_mmap_dataset_builder(builder, fname) # close file
dist.barrier(self.comm_group)
class Dist:
""" auxiliary class to perform distributed operations on tensors"""
@staticmethod
def min_max(tensor, comm_group):
""" given a distributed tensor, return the min/max values across all ranks"""
value_min, value_max = tensor.min(), tensor.max()
dist.reduce(value_min, 0, op=dist.ReduceOp.MIN, group=comm_group)
dist.reduce(value_max, 0, op=dist.ReduceOp.MAX, group=comm_group)
return value_min.item(), value_max.item()
@staticmethod
def gather_v(tensor, dst, comm_group, num_workers, worker_id):
""" MPI_Gatherv. gather tensors of variable sizes in a single rank """
# gather the number of rows to be sent/recv
size = torch.tensor([len(tensor)], dtype=torch.int64, device=tensor.device)
sizes = torch.zeros(num_workers, dtype=torch.int64, device=tensor.device)
dist.all_gather_into_tensor(sizes, size, group=comm_group)
assert sizes[worker_id] == size, "all_gather failed"
# all_gather requires all tensors to be of same size so we need to pad them
max_size = max(sizes).item()
buffer = torch.empty(max_size, dtype=tensor.dtype, device=tensor.device)
buffer[0:size] = tensor.data
buffer_list = None
if worker_id == 0: # create padded recv buffers
buffer_list = [torch.empty(max_size, dtype=tensor.dtype, device=tensor.device) for _ in range(num_workers)]
dist.gather(buffer, buffer_list, dst=dst, group=comm_group)
# revert padding and return value
if worker_id == 0:
buffer_list = [r[:s.item()] for r, s in zip(buffer_list, sizes)]
return buffer_list
@staticmethod
def sample_sort(tensor, comm_group, num_workers, n_samples=100):
""" perform a distributed random sort of a tensor, and returns the sorted partial tensor"""
device, dims = tensor.device, tensor.size()[1]
# 1 - sort rows by first column, then second column, then third, etc...
tensor = torch.tensor(sorted(tensor.tolist()), dtype=tensor.dtype, device=tensor.device)
# 2 - collect few samples per rank
idx = torch.round(torch.linspace(0, len(tensor) - 1, n_samples)).to(int)
samples = tensor[idx][:, 0].contiguous().to(device) #only first column, all but last row
# 2 - Allgather samples
all_samples = [torch.zeros(n_samples, dtype=samples.dtype, device=device) for _ in range(num_workers)]
dist.all_gather(all_samples, samples, group=comm_group)
all_samples = torch.cat(all_samples, dim=0).to(device)
# 3 - Sort all samples and collect the ranges of each rank as equidistant
all_samples = all_samples.sort()[0]
idx = torch.round(torch.linspace(0, len(all_samples) - 1, num_workers + 1)).to(int)
ranges = all_samples[idx] # range of each rank r as ranges[r] <= x < ranges[r+1]
ranges[-1] += 1 # increase upper limit of last rank so that x < ranges[r+1].
# 4 - collect elements to send to each rank, based on the rank ranges
send = []
for rank in range(num_workers):
mask = (tensor[:, 0] >= ranges[rank]) & (tensor[:, 0] < ranges[rank + 1])
send.append(tensor[mask])
# 5. all to all to communicate the sizes to be sent/recv
send_count = [torch.tensor([len(s) * dims], dtype=torch.int64, device=device) for s in send]
recv_count = list(torch.empty([num_workers], dtype=torch.int64, device=device).chunk(num_workers))
dist.all_to_all(recv_count, send_count, group=comm_group)
# 6. all-to-all-v to communicate the elements to be sent/recv as a single tensor
send = torch.cat(send, dim=0).flatten().to(device)
recv = torch.zeros(sum(recv_count), dtype=send.dtype).to(device)
send_count = [s.item() for s in send_count] # convert to list of ints
recv_count = [r.item() for r in recv_count]
dist.all_to_all_single(recv, send, recv_count, send_count, group=comm_group)
del send
# 7. the received tensor is the 1D disjoint subset of the distributed tensor.
# We will recover the original dimensionality and sort it by columns again.
recv = recv.view(-1, dims)
recv = torch.tensor(sorted(recv.tolist()), dtype=recv.dtype, device=recv.device)
return recv
def test_compare_both_data_analyzers(dataset):
""" given a dataset, compare file and memory based data analyser"""
id = lambda t: t.to(torch.int64) # identity
batch_sum = lambda t: id(t).sum() #sum batch
num_threads = 4
kwargs = dict(
dataset=dataset,
batch_size=2**10,
worker_id=int(os.environ['RANK']),
num_workers=int(os.environ['WORLD_SIZE']),
metric_names=["mod", "batch_sum"],
metric_functions=[id, batch_sum],
metric_types=['single_value_per_sample', 'accumulate_value_over_samples'],
num_threads=num_threads,
)
dda = DistributedDataAnalyzer(
save_path="./output_dist",
device=f"cuda:{int(os.environ['LOCAL_RANK'])}",
**kwargs,
)
start_time = time.time()
dda.run_map_reduce()
if dda.worker_id == 0:
print("DistributedDataAnalyzer runtime: %s seconds " % (time.time() - start_time))
da = DataAnalyzer(num_threads_reduce=num_threads,
save_path="./output_disk",
metric_dtypes=[torch.int64, torch.int64],
**kwargs)
start_time = time.time()
da.run_map_reduce()
if da.worker_id == 0:
print("DataAnalyzer runtime: %s seconds " % (time.time() - start_time))
output_paths = [
"batch_sum/batch_sum_metric_value.bin", "batch_sum/batch_sum_metric_value.idx", \
"mod/mod_index_to_metric.bin", "mod/mod_index_to_metric.idx", \
"mod/mod_index_to_sample.bin", "mod/mod_index_to_sample.idx", \
"mod/mod_index_to_sample_percentile_merged.bin", "mod/mod_index_to_sample_percentile_merged.idx", \
"mod/mod_sample_to_metric.bin", "mod/mod_sample_to_metric.idx"
]
if dda.worker_id == 0:
for path in output_paths:
with open(os.path.join(da.save_path, path), 'rb') as f1, \
open(os.path.join(dda.save_path, path), 'rb') as f2:
# if files have suffix .bin, they should be identical
if path.endswith(".bin"):
assert f1.read() == f2.read(), f"files {path} are not identical."
elif f1.read() != f2.read():
print(f"files {path} are not identical.")
dist.barrier()
dist.destroy_process_group()
if __name__ == "__main__":
class TestDataset(torch.utils.data.Dataset):
def __init__(self, size=10_000_000):
self.values = [(x + 7) % 10_000 for x in range(size)]
self.size = size
__len__ = lambda self: self.size
__getitem__ = lambda self, idx: self.values[idx]
test_compare_both_data_analyzers(TestDataset())
@@ -0,0 +1,349 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""
coding=utf-8
Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
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.
Part of this code was adopted from https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/data/data_samplers.py
"""
import torch
import os
import numpy as np
import deepspeed.comm as dist
from deepspeed.utils import logger
from deepspeed.accelerator import get_accelerator
from ..constants import *
from ..curriculum_scheduler import CurriculumScheduler
from .indexed_dataset import MMapIndexedDataset
from .utils import create_mmap_dataset_builder, close_mmap_dataset_builder, find_fit_int_dtype
class DeepSpeedDataSampler(object):
def __init__(self,
data_efficiency_config,
one_epoch_total_samples,
micro_batch_size,
data_parallel_rank,
data_parallel_size,
data_parallel_group,
gradient_accumulation_steps,
global_rank,
drop_last=True):
# Keep a copy of input params for later use.
self.data_efficiency_config = data_efficiency_config
self.one_epoch_total_samples = one_epoch_total_samples
self.index_dtype = find_fit_int_dtype(0, one_epoch_total_samples)
self.total_samples = one_epoch_total_samples * self.data_efficiency_config[DATA_SAMPLING][
DATA_SAMPLING_NUM_EPOCHS]
self.micro_batch_size = micro_batch_size
self.data_parallel_rank = data_parallel_rank
self.data_parallel_group = data_parallel_group
self.micro_batch_times_data_parallel_size = \
self.micro_batch_size * data_parallel_size
self.gradient_accumulation_steps = gradient_accumulation_steps
self.global_batch_size = self.micro_batch_times_data_parallel_size * \
self.gradient_accumulation_steps
self.global_rank = global_rank
self.drop_last = drop_last
self.np_rng = np.random.default_rng(self.data_efficiency_config[DATA_EFFICIENCY_SEED])
self.state = {}
self.batch = []
self.consumed_samples = 0
if self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_ENABLED]:
self.curriculum_step = 0
self.current_difficulties = {}
self.data_cluster_paths = []
self.data_cluster_current_position = []
self.curriculum_schedulers = {}
self.curriculum_index_to_sample = {}
self.curriculum_index_to_metric = {}
self.difficulty_type = {}
self.clustering_type = {}
self.data_1epoch_size = None
if self.global_rank == 0:
self.data_clusters = []
self.data_cluster_sizes = []
cluster_path = self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_CLUSTER_PATH]
if not os.path.exists(cluster_path):
os.makedirs(cluster_path)
for metric in self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_METRICS]:
self.curriculum_schedulers[metric] = CurriculumScheduler(
data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_METRICS][metric])
self.difficulty_type[metric] = data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_METRICS][metric][CURRICULUM_LEARNING_DIFFICULTY_TYPE]
self.clustering_type[metric] = data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_METRICS][metric][CURRICULUM_LEARNING_CLUSTERING_TYPE]
if self.global_rank == 0:
if self.clustering_type[metric] != CURRICULUM_LEARNING_SINGLE_CLUSTER:
self.curriculum_index_to_sample[metric] = MMapIndexedDataset(
data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_METRICS]
[metric][CURRICULUM_LEARNING_SAMPLE_PATH],
skip_warmup=True)
if self.difficulty_type[metric] == CURRICULUM_LEARNING_VALUE_BASED:
self.curriculum_index_to_metric[metric] = MMapIndexedDataset(
data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_METRICS]
[metric][CURRICULUM_LEARNING_METRIC_PATH],
skip_warmup=True)
# Sanity checks.
assert self.total_samples > 0, \
'no sample to consume: {}'.format(self.total_samples)
assert self.micro_batch_size > 0
assert data_parallel_size > 0
assert self.data_parallel_rank < data_parallel_size, \
'data_parallel_rank should be smaller than data size: {}, ' \
'{}'.format(self.data_parallel_rank, data_parallel_size)
def __len__(self):
return self.total_samples
def set_custom_curriculum_learning_schedule(self, schedule_func_dict):
for metric in self.curriculum_schedulers:
if metric in schedule_func_dict:
self.curriculum_schedulers[metric].set_custom_get_difficulty(schedule_func_dict[metric])
def get_start_end_idx(self, batch_len=None):
"""
given the length of a minibatch (defaults to micro-batch size * data_parallel_size),
return the start and end indices of the current data parallel rank
"""
batch_len = batch_len or self.micro_batch_times_data_parallel_size
start_idx_fn = lambda r: round(r * batch_len / self.data_parallel_group.size())
start_idx = start_idx_fn(self.data_parallel_rank)
end_idx = start_idx_fn(self.data_parallel_rank + 1)
return start_idx, end_idx
def get_sample_based_on_metric_value(self, metric, value_start, value_end):
new_samples = None
for row in range(len(self.curriculum_index_to_sample[metric])):
if self.curriculum_index_to_metric[metric][row] <= value_end and self.curriculum_index_to_metric[metric][
row] > value_start:
row_samples = np.copy(self.curriculum_index_to_sample[metric][row])
new_samples = row_samples if new_samples is None else np.concatenate(
(new_samples, row_samples), axis=None)
return new_samples
def get_sample_based_on_metric_percentile(self, metric, percentile_start, percentile_end):
new_samples = None
if self.data_1epoch_size is None:
self.data_1epoch_size = sum(len(x) for x in self.curriculum_index_to_sample[metric])
max_percentile = self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_METRICS][
metric][CURRICULUM_LEARNING_MAX_DIFFICULTY]
sample_per_percentile = self.data_1epoch_size // max_percentile
start_count = sample_per_percentile * percentile_start
end_count = sample_per_percentile * percentile_end
if percentile_end == max_percentile:
end_count = self.data_1epoch_size
current_count = 0
for row in range(len(self.curriculum_index_to_sample[metric])):
row_size = len(self.curriculum_index_to_sample[metric][row])
if current_count + row_size > start_count:
row_start = max(0, start_count - current_count)
if current_count + row_size <= end_count:
row_end = row_size
else:
row_end = end_count - current_count
row_samples = np.copy(self.curriculum_index_to_sample[metric][row][row_start:row_end])
new_samples = row_samples if new_samples is None else np.concatenate(
(new_samples, row_samples), axis=None)
current_count += row_size
if current_count >= end_count:
break
return new_samples
def get_new_cluster(self, previous_difficulties):
cluster_fname = CURRICULUM_LEARNING_CLUSTER_PREFIX
for metric in self.curriculum_schedulers:
cluster_fname = f"{cluster_fname}_{metric}{self.current_difficulties[metric]}"
cluster_path = self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_CLUSTER_PATH]
cluster_path = f"{cluster_path}/{cluster_fname}"
if self.global_rank == 0:
new_cluster = None
need_clustering = 0
for metric in self.clustering_type:
if self.clustering_type[metric] != CURRICULUM_LEARNING_SINGLE_CLUSTER:
need_clustering += 1
if need_clustering > 1:
for metric in self.curriculum_schedulers:
if self.clustering_type[metric] == CURRICULUM_LEARNING_SINGLE_CLUSTER:
metric_cluster = np.arange(start=0,
stop=self.one_epoch_total_samples,
step=1,
dtype=self.index_dtype)
else:
if self.difficulty_type[metric] == CURRICULUM_LEARNING_VALUE_BASED:
metric_cluster = self.get_sample_based_on_metric_value(metric, float('-inf'),
self.current_difficulties[metric])
elif self.difficulty_type[metric] == CURRICULUM_LEARNING_PERCENTILE_BASED:
metric_cluster = self.get_sample_based_on_metric_percentile(
metric, 0, self.current_difficulties[metric])
new_cluster = metric_cluster if new_cluster is None else \
np.intersect1d(new_cluster, metric_cluster, assume_unique=True)
for cluster in self.data_clusters:
new_cluster = np.setdiff1d(new_cluster, cluster[0], assume_unique=True)
else:
if len(self.data_clusters) == 0:
new_cluster = np.arange(start=0, stop=self.one_epoch_total_samples, step=1, dtype=self.index_dtype)
for metric in self.curriculum_schedulers:
if self.clustering_type[metric] != CURRICULUM_LEARNING_SINGLE_CLUSTER:
if self.difficulty_type[metric] == CURRICULUM_LEARNING_VALUE_BASED:
new_cluster = self.get_sample_based_on_metric_value(metric, previous_difficulties[metric],
self.current_difficulties[metric])
elif self.difficulty_type[metric] == CURRICULUM_LEARNING_PERCENTILE_BASED:
new_cluster = self.get_sample_based_on_metric_percentile(
metric, previous_difficulties[metric], self.current_difficulties[metric])
if new_cluster is not None and len(new_cluster) > 0:
logger.info(
f"new data cluster (previous_difficulties {previous_difficulties}, current_difficulties {self.current_difficulties}) with size {len(new_cluster)} generated."
)
self.np_rng.shuffle(new_cluster)
cluster_builder = create_mmap_dataset_builder(cluster_path, self.index_dtype)
cluster_builder.add_item_numpy(new_cluster)
close_mmap_dataset_builder(cluster_builder, cluster_path)
self.data_clusters.append(MMapIndexedDataset(cluster_path, skip_warmup=True))
self.data_cluster_sizes.append(len(self.data_clusters[-1][0]))
else:
logger.info(
f"new data cluster (previous_difficulties {previous_difficulties}, current_difficulties {self.current_difficulties}) has no matched data thus skipped."
)
dist.barrier(group=self.data_parallel_group)
if os.path.isfile(f"{cluster_path}.bin"):
self.data_cluster_paths.append(cluster_fname)
self.data_cluster_current_position.append(0)
def sample_from_clusters(self):
num_clusters = len(self.data_clusters)
weight_sum = sum(self.data_cluster_sizes)
weights = [x / weight_sum for x in self.data_cluster_sizes]
samples = self.np_rng.choice(num_clusters, self.global_batch_size, replace=True, p=weights)
samples = np.bincount(samples, minlength=num_clusters)
return samples
def reshuffle_clusters(self, cidx):
cluster_fname = self.data_cluster_paths[cidx]
cluster_path = self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_CLUSTER_PATH]
cluster_path = f"{cluster_path}/{cluster_fname}"
cluster = np.copy(self.data_clusters[cidx][0])
self.np_rng.shuffle(cluster)
cluster_builder = create_mmap_dataset_builder(cluster_path, self.index_dtype)
cluster_builder.add_item_numpy(cluster)
close_mmap_dataset_builder(cluster_builder, cluster_path)
self.data_clusters[cidx] = MMapIndexedDataset(cluster_path, skip_warmup=True)
def get_sample_from_cluster(self, cidx, num_samples):
start_idx = self.data_cluster_current_position[cidx]
samples = list(np.copy(self.data_clusters[cidx][0][start_idx:(start_idx + num_samples)]))
self.data_cluster_current_position[cidx] += num_samples
if len(samples) < num_samples:
num_samples_remained = num_samples - len(samples)
logger.info(f"reshuffling cluster {cidx}.")
self.reshuffle_clusters(cidx)
samples += list(np.copy(self.data_clusters[cidx][0][:num_samples_remained]))
self.data_cluster_current_position[cidx] = num_samples_remained
return samples
def get_next_global_batch(self):
if self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][CURRICULUM_LEARNING_ENABLED]:
self.curriculum_step += 1
new_cluster = False
previous_difficulties = {}
for metric in self.curriculum_schedulers:
next_difficulty = self.curriculum_schedulers[metric].update_difficulty(self.curriculum_step)
if metric not in self.current_difficulties or \
next_difficulty != self.current_difficulties[metric]:
new_cluster = True
if metric in self.current_difficulties:
previous_difficulties[metric] = self.current_difficulties[metric]
else:
if self.difficulty_type[metric] == CURRICULUM_LEARNING_VALUE_BASED:
previous_difficulties[metric] = float('-inf')
elif self.difficulty_type[metric] == CURRICULUM_LEARNING_PERCENTILE_BASED:
previous_difficulties[metric] = 0
self.current_difficulties[metric] = next_difficulty
if new_cluster:
self.get_new_cluster(previous_difficulties)
if self.global_rank == 0:
samples_per_cluster = self.sample_from_clusters()
batch = []
for cidx in range(len(samples_per_cluster)):
batch += self.get_sample_from_cluster(cidx, samples_per_cluster[cidx])
self.np_rng.shuffle(batch)
# broadcast tensor must have same shape across participants. So we fill batch with -1s when not full
assert len(batch) <= self.global_batch_size
batch += [-1] * (self.global_batch_size - len(batch))
batch = torch.tensor(batch, device=get_accelerator().current_device_name(), dtype=torch.long).view(-1)
else:
batch = torch.empty(self.global_batch_size,
device=get_accelerator().current_device_name(),
dtype=torch.long)
dist.broadcast(batch, 0, group=self.data_parallel_group)
batch = batch[batch != -1] # remove trailing -1s used to fill incomplete batch tensor
self.batch = batch.tolist()
def __iter__(self):
while self.consumed_samples <= self.total_samples:
if len(self.batch) == 0:
self.get_next_global_batch()
current_batch = self.batch[:self.micro_batch_times_data_parallel_size]
self.batch = self.batch[self.micro_batch_times_data_parallel_size:]
if len(current_batch) == self.micro_batch_times_data_parallel_size or \
(len(current_batch) > 0 and not self.drop_last):
start_idx, end_idx = self.get_start_end_idx(len(current_batch))
yield current_batch[start_idx:end_idx]
self.consumed_samples += len(current_batch)
current_batch = []
def state_dict(self):
return {
CURRICULUM_LEARNING_BATCH: self.batch,
CURRICULUM_LEARNING_CONSUMED_SAMPLES: self.consumed_samples,
CURRICULUM_LEARNING_STEP: self.curriculum_step,
CURRICULUM_LEARNING_CURRENT_DIFFICULTIES: self.current_difficulties,
CURRICULUM_LEARNING_DATA_CLUSTER_PATHS: self.data_cluster_paths,
CURRICULUM_LEARNING_DATA_CLUSTER_CURRENT_POSITION: self.data_cluster_current_position,
CURRICULUM_LEARNING_NP_RNG_STATE: np.random.get_state()
}
def load_state_dict(self, state_dict):
self.batch = state_dict[CURRICULUM_LEARNING_BATCH]
self.consumed_samples = state_dict[CURRICULUM_LEARNING_CONSUMED_SAMPLES]
self.curriculum_step = state_dict[CURRICULUM_LEARNING_STEP]
self.current_difficulties = state_dict[CURRICULUM_LEARNING_CURRENT_DIFFICULTIES]
self.data_cluster_paths = state_dict[CURRICULUM_LEARNING_DATA_CLUSTER_PATHS]
self.data_cluster_current_position = state_dict[CURRICULUM_LEARNING_DATA_CLUSTER_CURRENT_POSITION]
np.random.set_state(state_dict[CURRICULUM_LEARNING_NP_RNG_STATE])
cluster_root_path = self.data_efficiency_config[DATA_SAMPLING][CURRICULUM_LEARNING][
CURRICULUM_LEARNING_CLUSTER_PATH]
# Backward compatibility: previously data_cluster_paths were stored as
# absolute paths. Now we changed it to just the file name so that even
# if user moved the cluster files, the checkpoint loading still works
# as long as user set the correct new CURRICULUM_LEARNING_CLUSTER_PATH
# in deepspeed json config.
for idx in range(len(self.data_cluster_paths)):
if '/' in self.data_cluster_paths[idx]:
self.data_cluster_paths[idx] = self.data_cluster_paths[idx].split('/')[-1]
if self.global_rank == 0:
for cluster_fname in self.data_cluster_paths:
cluster_path = f"{cluster_root_path}/{cluster_fname}"
self.data_clusters.append(MMapIndexedDataset(cluster_path, skip_warmup=True))
self.data_cluster_sizes.append(len(self.data_clusters[-1][0]))
@@ -0,0 +1,627 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""
Part of this code was adopted from https://github.com/NVIDIA/Megatron-LM/blob/main/megatron/data/indexed_dataset.py
"""
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# copied from fairseq/fairseq/data/indexed_dataset.py
# Removed IndexedRawTextDataset since it relied on Fairseq dictionary
# other slight modifications to remove fairseq dependencies
# Added document index to index file and made it accessible.
# An empty sentence no longer separates documents.
# Some of the fixes/improvements are adopted from
# https://github.com/bigscience-workshop/Megatron-DeepSpeed/blob/main/megatron/data/indexed_dataset.py
from functools import lru_cache
import os
import shutil
import struct
from itertools import accumulate
import numpy as np
import torch
def __best_fitting_dtype(vocab_size=None):
if vocab_size is not None and vocab_size < 65500:
return np.uint16
else:
return np.int32
def get_available_dataset_impl():
return ['lazy', 'cached', 'mmap']
def infer_dataset_impl(path):
if IndexedDataset.exists(path):
with open(index_file_path(path), 'rb') as f:
magic = f.read(8)
if magic == IndexedDataset._HDR_MAGIC:
return 'cached'
elif magic == MMapIndexedDataset.Index._HDR_MAGIC[:8]:
return 'mmap'
else:
return None
else:
print(f"Dataset does not exist: {path}")
print("Path should be a basename that both .idx and .bin can be appended to get full filenames.")
return None
def make_builder(out_file, impl, vocab_size=None):
if impl == 'mmap':
return MMapIndexedDatasetBuilder(out_file, dtype=__best_fitting_dtype(vocab_size))
else:
return IndexedDatasetBuilder(out_file)
def make_dataset(path, impl, skip_warmup=False):
if not IndexedDataset.exists(path):
print(f"Dataset does not exist: {path}")
print("Path should be a basename that both .idx and .bin can be appended to get full filenames.")
return None
if impl == 'infer':
impl = infer_dataset_impl(path)
if impl == 'lazy' and IndexedDataset.exists(path):
return IndexedDataset(path)
elif impl == 'cached' and IndexedDataset.exists(path):
return IndexedCachedDataset(path)
elif impl == 'mmap' and MMapIndexedDataset.exists(path):
return MMapIndexedDataset(path, skip_warmup)
print(f"Unknown dataset implementation: {impl}")
return None
def dataset_exists(path, impl):
if impl == 'mmap':
return MMapIndexedDataset.exists(path)
else:
return IndexedDataset.exists(path)
def read_longs(f, n):
a = np.empty(n, dtype=np.int64)
f.readinto(a)
return a
def write_longs(f, a):
f.write(np.array(a, dtype=np.int64))
# valid metric_dtypes as numpy and torch types
dtypes = {
1: (np.uint8, torch.uint8),
2: (np.int8, torch.int8),
3: (np.int16, torch.int16),
4: (np.int32, torch.int32),
5: (np.int64, torch.int64),
6: (np.uint16, None),
7: (np.uint32, None),
8: (np.uint64, None),
}
valid_dtypes = set([dt[0] for dt in dtypes.values()] + [dt[1] for dt in dtypes.values() if dt[1] is not None])
def code(dtype):
for c, (np_dt, torch_dt) in dtypes.items():
if dtype in [np_dt, torch_dt]:
return c
raise ValueError(f"{dtype} not supported. Supported types: {valid_dtypes}")
def index_file_path(prefix_path):
return prefix_path + '.idx'
def data_file_path(prefix_path):
return prefix_path + '.bin'
def create_doc_idx(sizes):
doc_idx = [0]
for i, s in enumerate(sizes):
if s == 0:
doc_idx.append(i + 1)
return doc_idx
class IndexedDataset(torch.utils.data.Dataset):
"""Loader for IndexedDataset"""
_HDR_MAGIC = b'TNTIDX\x00\x00'
def __init__(self, path):
super().__init__()
self.path = path
self.data_file = None
self.read_index(path)
def read_index(self, path):
with open(index_file_path(path), 'rb') as f:
magic = f.read(8)
assert magic == self._HDR_MAGIC, ('Index file doesn\'t match expected format. '
'Make sure that --dataset-impl is configured properly.')
version = f.read(8)
assert struct.unpack('<Q', version) == (1, )
code, self.element_size = struct.unpack('<QQ', f.read(16))
self.dtype = dtypes[code][0] #numpy type
self._len, self.s = struct.unpack('<QQ', f.read(16))
self.doc_count = struct.unpack('<Q', f.read(8))
self.dim_offsets = read_longs(f, self._len + 1)
self.data_offsets = read_longs(f, self._len + 1)
self.sizes = read_longs(f, self.s)
self.doc_idx = read_longs(f, self.doc_count)
def read_data(self, path):
self.data_file = open(data_file_path(path), 'rb', buffering=0)
def check_index(self, i):
if i < 0 or i >= self._len:
raise IndexError('index out of range')
def __del__(self):
if self.data_file:
self.data_file.close()
# @lru_cache(maxsize=8)
def __getitem__(self, idx):
if not self.data_file:
self.read_data(self.path)
if isinstance(idx, int):
i = idx
self.check_index(i)
tensor_size = self.sizes[self.dim_offsets[i]:self.dim_offsets[i + 1]]
a = np.empty(tensor_size, dtype=self.dtype)
self.data_file.seek(self.data_offsets[i] * self.element_size)
self.data_file.readinto(a)
return a
elif isinstance(idx, slice):
start, stop, step = idx.indices(len(self))
if step != 1:
raise ValueError("Slices into indexed_dataset must be contiguous")
sizes = self.sizes[self.dim_offsets[start]:self.dim_offsets[stop]]
size = sum(sizes)
a = np.empty(size, dtype=self.dtype)
self.data_file.seek(self.data_offsets[start] * self.element_size)
self.data_file.readinto(a)
offsets = list(accumulate(sizes))
sents = np.split(a, offsets[:-1])
return sents
def __len__(self):
return self._len
def num_tokens(self, index):
return self.sizes[index]
def size(self, index):
return self.sizes[index]
@staticmethod
def exists(path):
return (os.path.exists(index_file_path(path)) and os.path.exists(data_file_path(path)))
@property
def supports_prefetch(self):
return False # avoid prefetching to save memory
class IndexedCachedDataset(IndexedDataset):
def __init__(self, path):
super().__init__(path)
self.cache = None
self.cache_index = {}
@property
def supports_prefetch(self):
return True
def prefetch(self, indices):
if all(i in self.cache_index for i in indices):
return
if not self.data_file:
self.read_data(self.path)
indices = sorted(set(indices))
total_size = 0
for i in indices:
total_size += self.data_offsets[i + 1] - self.data_offsets[i]
self.cache = np.empty(total_size, dtype=self.dtype)
ptx = 0
self.cache_index.clear()
for i in indices:
self.cache_index[i] = ptx
size = self.data_offsets[i + 1] - self.data_offsets[i]
a = self.cache[ptx:ptx + size]
self.data_file.seek(self.data_offsets[i] * self.element_size)
self.data_file.readinto(a)
ptx += size
if self.data_file:
# close and delete data file after prefetch so we can pickle
self.data_file.close()
self.data_file = None
# @lru_cache(maxsize=8)
def __getitem__(self, idx):
if isinstance(idx, int):
i = idx
self.check_index(i)
tensor_size = self.sizes[self.dim_offsets[i]:self.dim_offsets[i + 1]]
a = np.empty(tensor_size, dtype=self.dtype)
ptx = self.cache_index[i]
np.copyto(a, self.cache[ptx:ptx + a.size])
return a
elif isinstance(idx, slice):
# Hack just to make this work, can optimizer later if necessary
sents = []
for i in range(*idx.indices(len(self))):
sents.append(self[i])
return sents
class IndexedDatasetBuilder(object):
def __init__(self, out_file, dtype=np.int32):
self.out_file = open(out_file, 'wb')
self.dtype = dtype
self.data_offsets = [0]
self.dim_offsets = [0]
self.sizes = []
self.element_size = self.dtype().itemsize
self.doc_idx = [0]
def add_item(self, tensor):
bytes = self.out_file.write(np.array(tensor.numpy(), dtype=self.dtype))
self.data_offsets.append(self.data_offsets[-1] + bytes / self.element_size)
for s in tensor.size():
self.sizes.append(s)
self.dim_offsets.append(self.dim_offsets[-1] + len(tensor.size()))
def end_document(self):
self.doc_idx.append(len(self.sizes))
def merge_file_(self, another_file):
index = IndexedDataset(another_file)
assert index.dtype == self.dtype
doc_offset = len(self.sizes)
begin = self.data_offsets[-1]
for data_offset in index.data_offsets[1:]:
self.data_offsets.append(begin + data_offset)
self.sizes.extend(index.sizes)
begin = self.dim_offsets[-1]
for dim_offset in index.dim_offsets[1:]:
self.dim_offsets.append(begin + dim_offset)
self.doc_idx.extend((doc_offset + index.doc_idx)[1:])
with open(data_file_path(another_file), 'rb') as f:
while True:
data = f.read(1024)
if data:
self.out_file.write(data)
else:
break
def finalize(self, index_file):
self.out_file.close()
index = open(index_file, 'wb')
index.write(b'TNTIDX\x00\x00')
index.write(struct.pack('<Q', 1))
index.write(struct.pack('<QQ', code(self.dtype), self.element_size))
index.write(struct.pack('<QQ', len(self.data_offsets) - 1, len(self.sizes)))
index.write(struct.pack('<Q', len(self.doc_idx)))
write_longs(index, self.dim_offsets)
write_longs(index, self.data_offsets)
write_longs(index, self.sizes)
write_longs(index, self.doc_idx)
index.close()
def _warmup_mmap_file(path):
with open(path, 'rb') as stream:
while stream.read(100 * 1024 * 1024):
pass
def exscan_from_cumsum_(arr):
# given an array holding the result of an inclusive scan (cumsum),
# convert to an exclusive scan (shift to the right)
# [10, 30, 35, 50] --> [0, 10, 30, 35]
if arr.size > 1:
arr[1:] = arr[:-1]
if arr.size > 0:
arr[0] = 0
def get_pointers_with_total(sizes, elemsize, dtype):
"""Return a numpy array of type np.dtype giving the byte offsets.
Multiplies values in the sizes array by elemsize (bytes),
and then computes an exclusive scan to get byte offsets.
Returns the total number of bytes as second item in a tuple.
"""
# scale values in sizes array by elemsize to get sizes in bytes
pointers = np.array(sizes, dtype=dtype)
pointers *= elemsize
np.cumsum(pointers, axis=0, out=pointers)
# get total number of bytes from all sizes (last element)
bytes_last = pointers[-1] if len(sizes) > 0 else 0
# convert to byte offsets
exscan_from_cumsum_(pointers)
return pointers, bytes_last
class MMapIndexedDataset(torch.utils.data.Dataset):
class Index(object):
_HDR_MAGIC = b'MMIDIDX\x00\x00'
@classmethod
def writer(cls, path, dtype):
class _Writer(object):
def __enter__(self):
self._file = open(path, 'wb')
self._file.write(cls._HDR_MAGIC)
self._file.write(struct.pack('<Q', 1))
self._file.write(struct.pack('<B', code(dtype)))
return self
@staticmethod
def _get_pointers(sizes, npdtype):
"""Return a numpy array of byte offsets given a list of sizes.
Multiplies values in the sizes array by dtype size (bytes),
and then computes an exclusive scan to get byte offsets.
"""
# compute element sizes in bytes
pointers, _ = get_pointers_with_total(sizes, dtype().itemsize, npdtype)
return pointers
def write(self, sizes, doc_idx):
self._file.write(struct.pack('<Q', len(sizes)))
self._file.write(struct.pack('<Q', len(doc_idx)))
sizes32 = np.array(sizes, dtype=np.int32)
self._file.write(sizes32.tobytes(order='C'))
del sizes32
pointers = self._get_pointers(sizes, np.int64)
del sizes
self._file.write(pointers.tobytes(order='C'))
del pointers
doc_idx = np.array(doc_idx, dtype=np.int64)
self._file.write(doc_idx.tobytes(order='C'))
def __exit__(self, exc_type, exc_val, exc_tb):
self._file.close()
return _Writer()
def __init__(self, path, skip_warmup=False):
with open(path, 'rb') as stream:
magic_test = stream.read(9)
assert self._HDR_MAGIC == magic_test, ('Index file doesn\'t match expected format. '
'Make sure that --dataset-impl is configured properly.')
version = struct.unpack('<Q', stream.read(8))
assert (1, ) == version
dtype_code, = struct.unpack('<B', stream.read(1))
self._dtype = dtypes[dtype_code][0] #numpy type
self._dtype_size = self._dtype().itemsize
self._len = struct.unpack('<Q', stream.read(8))[0]
self._doc_count = struct.unpack('<Q', stream.read(8))[0]
offset = stream.tell()
if not skip_warmup:
print(" warming up index mmap file...")
_warmup_mmap_file(path)
self._bin_buffer_mmap = np.memmap(path, mode='r', order='C')
self._bin_buffer = memoryview(self._bin_buffer_mmap)
print(" reading sizes...")
self._sizes = np.frombuffer(self._bin_buffer, dtype=np.int32, count=self._len, offset=offset)
print(" reading pointers...")
self._pointers = np.frombuffer(self._bin_buffer,
dtype=np.int64,
count=self._len,
offset=offset + self._sizes.nbytes)
print(" reading document index...")
self._doc_idx = np.frombuffer(self._bin_buffer,
dtype=np.int64,
count=self._doc_count,
offset=offset + self._sizes.nbytes + self._pointers.nbytes)
def __del__(self):
self._bin_buffer_mmap._mmap.close()
del self._bin_buffer_mmap
@property
def dtype(self):
return self._dtype
@property
def sizes(self):
return self._sizes
@property
def doc_idx(self):
return self._doc_idx
@lru_cache(maxsize=8)
def __getitem__(self, i):
return self._pointers[i], self._sizes[i]
def __len__(self):
return self._len
def __init__(self, path, skip_warmup=False):
super().__init__()
self._path = None
self._index = None
self._bin_buffer = None
self._do_init(path, skip_warmup)
def __getstate__(self):
return self._path
def __setstate__(self, state):
self._do_init(state)
def _do_init(self, path, skip_warmup):
self._path = path
self._index = self.Index(index_file_path(self._path), skip_warmup)
if not skip_warmup:
print(" warming up data mmap file...")
_warmup_mmap_file(data_file_path(self._path))
print(" creating numpy buffer of mmap...")
self._bin_buffer_mmap = np.memmap(data_file_path(self._path), mode='r', order='C')
print(" creating memory view of numpy buffer...")
self._bin_buffer = memoryview(self._bin_buffer_mmap)
def __del__(self):
self._bin_buffer_mmap._mmap.close()
del self._bin_buffer_mmap
del self._index
def __len__(self):
return len(self._index)
# @lru_cache(maxsize=8)
def __getitem__(self, idx):
if isinstance(idx, int):
ptr, size = self._index[idx]
np_array = np.frombuffer(self._bin_buffer, dtype=self._index.dtype, count=size, offset=ptr)
return np_array
elif isinstance(idx, slice):
start, stop, step = idx.indices(len(self))
if step != 1:
raise ValueError("Slices into indexed_dataset must be contiguous")
ptr = self._index._pointers[start]
sizes = self._index._sizes[idx]
offsets = list(accumulate(sizes))
total_size = sum(sizes)
np_array = np.frombuffer(self._bin_buffer, dtype=self._index.dtype, count=total_size, offset=ptr)
sents = np.split(np_array, offsets[:-1])
return sents
def get(self, idx, offset=0, length=None):
""" Retrieves a single item from the dataset with the option to only
return a portion of the item.
get(idx) is the same as [idx] but get() does not support slicing.
"""
ptr, size = self._index[idx]
if length is None:
length = size - offset
ptr += offset * np.dtype(self._index.dtype).itemsize
np_array = np.frombuffer(self._bin_buffer, dtype=self._index.dtype, count=length, offset=ptr)
return np_array
@property
def sizes(self):
return self._index.sizes
def size(self, index):
return self._index.sizes[index]
@property
def doc_idx(self):
return self._index.doc_idx
def get_doc_idx(self):
return self._index._doc_idx
def set_doc_idx(self, doc_idx_):
self._index._doc_idx = doc_idx_
@property
def supports_prefetch(self):
return False
@staticmethod
def exists(path):
return (os.path.exists(index_file_path(path)) and os.path.exists(data_file_path(path)))
@property
def dtype(self):
return self._index.dtype
class MMapIndexedDatasetBuilder(object):
def __init__(self, out_file, dtype=np.int64):
self._data_file = open(out_file, 'wb')
self._dtype = [np_dt for np_dt, torch_dt in dtypes.values() if dtype in [np_dt, torch_dt]][0]
self._sizes = []
self._doc_idx = [0]
def add_item(self, tensor):
""" write the tensor to the file and update its size in the index"""
np_array = np.array(tensor.numpy(), dtype=self._dtype)
self._data_file.write(np_array.tobytes(order='C'))
self._sizes.append(np_array.size)
def add_items(self, arr_list):
""" write a list of arrays to the file and update their sizes in the index"""
np_arrays = [arr.astype(self._dtype) for arr in arr_list]
self._data_file.writelines([arr.tobytes(order='C') for arr in np_arrays])
for arr in np_arrays:
self._sizes.append(arr.size)
def add_item_numpy(self, np_array):
if np_array.dtype != self._dtype:
np_array = np_array.astype(self._dtype)
self._data_file.write(np_array.tobytes(order='C'))
self._sizes.append(np_array.size)
def end_document(self):
self._doc_idx.append(len(self._sizes))
def merge_file_(self, another_file):
# Concatenate index
index = MMapIndexedDataset.Index(index_file_path(another_file))
assert index.dtype == self._dtype
total_len = len(index.sizes) + len(self._sizes)
print(f" concat {another_file} size={len(index.sizes)} for a total size of {total_len}")
offset = len(self._sizes)
self._sizes.extend(index.sizes)
self._doc_idx.extend((offset + index.doc_idx)[1:])
# Concatenate data
with open(data_file_path(another_file), 'rb') as f:
shutil.copyfileobj(f, self._data_file)
self._data_file.flush()
assert os.stat(self._data_file.name).st_size != 0, f"Zero-sized file: {self._data_file.name}"
def finalize(self, index_file):
self._data_file.close()
with MMapIndexedDataset.Index.writer(index_file, self._dtype) as index:
index.write(self._sizes, self._doc_idx)
@@ -0,0 +1,52 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import numpy as np
from deepspeed.utils import logger
from .indexed_dataset import MMapIndexedDatasetBuilder
def find_fit_int_dtype(min_value, max_value):
if min_value >= 0:
if max_value <= 255:
return np.uint8
elif max_value <= 65535:
return np.uint16
elif max_value <= 4294967295:
return np.uint32
else:
return np.uint64
else:
if max_value <= 127 and min_value >= -128:
return np.int8
elif max_value <= 32767 and min_value >= -32768:
return np.int16
elif max_value <= 2147483647 and min_value >= -2147483648:
return np.int32
else:
return np.int64
def split_index(start_idx, end_idx, num_partitions):
partition_boundaries = np.linspace(start_idx, end_idx, dtype=int, num=num_partitions + 1)
return [(partition_boundaries[i], partition_boundaries[i + 1]) for i in range(num_partitions)]
def split_dataset(dataset, num_workers, worker_id, num_threads):
worker_splits = split_index(0, len(dataset), num_workers)
thread_splits = split_index(worker_splits[worker_id][0], worker_splits[worker_id][1], num_threads)
return worker_splits, thread_splits
def create_mmap_dataset_builder(fname, dtype):
logger.info(f"Creating mmap dataset builder at {fname}.")
return MMapIndexedDatasetBuilder(f"{fname}.bin", dtype=dtype)
def close_mmap_dataset_builder(builder, fname):
builder.end_document()
builder.finalize(f"{fname}.idx")
logger.info(f"Finalized mmap dataset builder at {fname}.")
@@ -0,0 +1,492 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
# support/questions/maintenance: github user @brunomaga or @deepspeedai/deepspeed
import random
import os
import math
import numpy as np
from torch.optim.lr_scheduler import LRScheduler
from torch.optim.optimizer import Optimizer
from torch.utils.data import DataLoader, DistributedSampler
from deepspeed.utils import logger
from deepspeed.runtime.pipe.engine import PipelineEngine
from deepspeed.runtime.data_pipeline.constants import *
from deepspeed.runtime.data_pipeline.data_sampling.indexed_dataset import MMapIndexedDataset
from deepspeed.runtime.data_pipeline.data_sampling.data_analyzer import DistributedDataAnalyzer
import pathlib
def batch_by_seqlens(
seqlens,
max_tokens,
sequence_ids_per_mb=None,
min_batch_size=1,
max_batch_size=None,
sequence_picking_order="dataloader",
effective_batch_size=1,
required_microbatches_of_same_size=False,
verbose=False,
seed=None,
):
"""
Yield mini-batches of indices bucketed by size. Batches may contain sequences of different lengths.
Similar to "Attention is all you need", Section 5.1:
"sequence pairs were batched together by approximate sequence length. Each training batch
contained a set of sequence pairs containing approximately X source tokens and X target tokens"
Arguments:
- `seqlens`: a list of difficulties (metric values) for every sample in the dataset;
- `max_tokens`: maximum cap in total difficulty in a batch;
- `min_batch_size`: smallest allowed size of a batch;
- `min_batch_size`: largest allowed size of a batch;
- `sequence_picking_order`: order in which to process samples: "dataloader" (default), "random" or "seqlen" (ascending)
- `effective_batch_size`: effective batch size;
- `required_microbatches_of_same_size`: enable if each mini-batch (in a total of `batch_size_multiple`
micro-batches per batch), should have all micro-batches with the same batch size ie the same
number of sequences.
- `verbose`: print debug information;
- `seed`: random seed for reproducibility;
Returns:
- `microbatch_ids`: list of tuple of batch id and samples ids per microbatch
- `batch_sizes`: the effective batch size of each batch, used for to compute the scaled LR
- `batch_max_seqlens`: the max seqlen across all microbatches in a batch
"""
assert sequence_picking_order in ["random", "seqlen", "dataloader"]
if sequence_ids_per_mb is None:
metrics = list(zip(seqlens, range(len(seqlens)))) # use all samples
else:
metrics = list(zip(np.array(seqlens)[sequence_ids_per_mb], sequence_ids_per_mb))
if sequence_picking_order == 'random':
metric_random = random.Random(seed)
metric_random.shuffle(metrics)
if sequence_picking_order == 'seqlen':
metrics = sorted(metrics)
# go through metrics, warn user, and filter samples that alone exceed the max batch threshold
long_ids = [idx for val, idx in metrics if val > max_tokens]
if len(long_ids) > 0:
logger.warning(f"Data indices {long_ids} ignored as metrics exceed {max_tokens}.")
logger.info(f"Original dataset length: {len(metrics)}. New dataset length: {len(long_ids)}")
metrics = [m for m in metrics if m[1] not in long_ids]
def is_microbatch_valid(metrics):
if min_batch_size and len(metrics) < min_batch_size: return False # insufficient sample count
if max_batch_size and len(metrics) > max_batch_size: return False # too many samples
if sum([m[0] for m in metrics]) > max_tokens: return False # exceeds max
return True
# go through all samples and pack then in microbatches of metric sums below the threshold
# `required_microbatches_of_same_size` means all minibatches in a batch must be of equal size
equal_size_multiple = effective_batch_size if required_microbatches_of_same_size else 1
microbatches = []
batch_init = 0
while batch_init < len(metrics):
# we iterate over possible effective batch sizes (groups of microbatches of same size)
valid_batch_end = batch_init
for batch_end in range(batch_init + equal_size_multiple, len(metrics), equal_size_multiple):
# attempt effective batch
batch = metrics[batch_init:batch_end]
# pick interleaved samples for each microbatch to help with load balancing
# (in the ordered use case), and to replicate what the distributed sampler does.
mbs = [batch[b::equal_size_multiple] for b in range(equal_size_multiple)]
# if they are all valid micro-batches, keep them until you find longer mbatches, if any
is_batch_valid = all([is_microbatch_valid(mb) for mb in mbs])
if is_batch_valid:
valid_batch_end = batch_end
if batch_init == valid_batch_end: break # last batch is not valid (size zero), so we are done
batch = metrics[batch_init:valid_batch_end]
mbs = [batch[b::equal_size_multiple] for b in range(equal_size_multiple)]
batch_init += sum([len(l) for l in mbs])
microbatches += mbs
# make sure we give the same number of (micro-)batches to each dataloader by trimming the dataset
assert len(microbatches) >= effective_batch_size, "not enough datapoints to create a single sample per dataloader"
microbatches = microbatches[:len(microbatches) - len(microbatches) % effective_batch_size]
#compute the effective batch size for each microbatch.
batch_sizes, batch_max_seqlens, microbatch_ids = [], [], []
for rank in range(0, len(microbatches), effective_batch_size):
batch_id = rank // effective_batch_size
mbs = microbatches[rank:rank + effective_batch_size]
# compute the number of samples (not tokens) in this batch (not microbatch)
n_sequences = sum([len(mb) for mb in mbs])
# compute the longest sequence (as number of tokens) in this batch (not microbatch)
sequence_ids_per_mb = [[m[1] for m in metrics] for metrics in mbs]
sequence_lens_per_mb = [[m[0] for m in metrics] for metrics in mbs]
batch_max_seqlen = max([max(seqlens) for seqlens in sequence_lens_per_mb])
batch_and_mb_ids = zip([batch_id] * effective_batch_size, sequence_ids_per_mb)
batch_sizes.append(n_sequences)
batch_max_seqlens.append(batch_max_seqlen)
microbatch_ids += batch_and_mb_ids
if verbose:
n_tokens_per_mb = [sum([m[0] for m in mb]) for mb in mbs]
n_sequences_per_mb = [len(mb) for mb in mbs]
assert all([n <= max_tokens for n in n_tokens_per_mb]), "size of microbatch exceeds max tokens"
logger.info(
f"Batch id {batch_id} contains in total {len(mbs)} microbatches or {n_sequences} sequences. "\
f"n_sequences per microbatch {n_sequences_per_mb}. "\
f"n_tokens per microbatch {n_tokens_per_mb}. "\
f"sequence ids per microbatch: {sequence_ids_per_mb}. "\
f"sequence lengths per microbatch: {sequence_lens_per_mb}.")
# return the sample ids of each microbatch, and the batch sizes
assert len(batch_sizes) == len(microbatch_ids) // effective_batch_size
return microbatch_ids, batch_sizes, batch_max_seqlens
def scale_lr(base_batch_size, batch_size, base_lr=1, method="linear"):
""" given a reference lr and batch_size, compute the new LR for a given batch size """
if method == "linear":
# Linear Scaling Rule: "When the minibatch size is multiplied by k, multiply the learning
# rate by k" (Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour, Goyal et al)
return base_lr * batch_size / base_batch_size
if method == "sqrt":
# Square Root scaling: "when multiplying the batch size by k, multiply the learning rate
# by √k, to keep the variance in the gradient expectation constant"
# (A. Krizhevsky. One weird trick for parallelizing convolutional neural networks)
return base_lr * math.sqrt(batch_size / base_batch_size)
elif method == None or method.upper() == "NONE":
return base_lr
raise ValueError("Unknown scaling method: {}".format(method))
def dataloader_for_variable_batch_size(
dataset,
microbatch_ids,
batch_max_seqlens,
dataloader_rank=0,
dataloader_batch_size=1,
dataloader_num_replicas=1,
dataloader_collate_fn=None,
dataloader_num_workers=2,
dataloader_pin_memory=False,
required_microbatches_of_same_seqlen=False,
sample_padding_fn=None,
):
# equidistantly distribute the microbatches across the replicas in an interleaved fashion.
sampler = DistributedSampler(
dataset=microbatch_ids,
num_replicas=dataloader_num_replicas,
rank=dataloader_rank,
shuffle=False,
drop_last=False,
)
# collate function wraps user-defined collate function to the variable batch data
def collate_fn_wrapper(list_microbatch_ids):
# each batch is a list of sample ids that fill up to the max tokens per batch
# we return the collated batch of all dataset samples of all input batches.
batch = []
for batch_id, microbatch_ids in list_microbatch_ids:
batch_data = [dataset[idx] for idx in microbatch_ids]
if required_microbatches_of_same_seqlen:
assert sample_padding_fn is not None, \
"padding dataloader_padding_fn must be provided if required_microbatches_of_same_seqlen is True"
max_seqlen = batch_max_seqlens[batch_id]
assert all([len(sample) <= max_seqlen for sample in batch_data]), \
"some samples are longer than the computed max seqlen for the batch those samples belong to"
batch_data = [sample_padding_fn(sample, max_seqlen) for sample in batch_data]
batch += batch_data
return dataloader_collate_fn(batch) if dataloader_collate_fn else batch
dataloader = DataLoader(
dataset=microbatch_ids,
batch_size=dataloader_batch_size,
sampler=sampler,
num_workers=dataloader_num_workers,
collate_fn=collate_fn_wrapper,
pin_memory=dataloader_pin_memory,
)
deepspeed_io_kwargs = dict(
dataset=microbatch_ids,
batch_size=dataloader_batch_size,
pin_memory=dataloader_pin_memory,
data_sampler=sampler,
collate_fn=collate_fn_wrapper,
num_local_io_workers=dataloader_num_workers,
)
return dataloader, deepspeed_io_kwargs
class VariableBatchSizeLR(LRScheduler):
""" an LR scheduler that scales the LR of a given scheduler's LR """
@property
def optimizer(self):
return self.base_lr_scheduler.optimizer
def __init__(self,
lr_scheduler,
base_batch_size,
batch_sizes,
dataloader,
lr_scaling_method="linear",
last_epoch=-1,
verbose=False):
self.batch_sizes = batch_sizes
self.base_batch_size = base_batch_size
self.lr_scaling_method = lr_scaling_method
self.dataloader = dataloader
self.base_lr_scheduler = lr_scheduler
# the following exist in LRScheduler but not in DeepSpeed's LRScheduler so we redefine them here
self.base_lrs = self.base_lr_scheduler.get_lr()
self.last_epoch = last_epoch
self.verbose = verbose
self.step(0) # scale LR for first sample in the dataloader
def state_dict(self):
return {
'base_lr_scheduler': self.base_lr_scheduler.state_dict()
} | {
'base_batch_size': self.base_batch_size,
'lr_scaling_method': self.lr_scaling_method,
'batch_sizes': self.batch_sizes,
'base_lrs': self.base_lrs,
'last_epoch': self.last_epoch,
'verbose': self.verbose,
}
def load_state_dict(self, state_dict):
self.base_lr_scheduler.load_state_dict(state_dict['base_lr_scheduler'])
self.base_batch_size = state_dict['base_batch_size']
self.lr_scaling_method = state_dict['lr_scaling_method']
self.batch_sizes = state_dict['batch_sizes']
self.base_lrs = state_dict['base_lrs']
self.last_epoch = state_dict['last_epoch']
self.verbose = state_dict['verbose']
def get_last_lr(self):
return self.base_lr_scheduler._last_lr
def get_lr(self):
return [group['lr'] for group in self.base_lr_scheduler.optimizer.param_groups]
def step(self, epoch=None):
# call the base scheduler's step method to get LR for next epoch
# Note: optimizer.step precedes lr_scheduler.step(), so the stepping workflow is:
# init: lr_scheduler.step(0) --> set LR for epoch 0
# epoch 0: optimizer.step(); lr_scheduler.step(1) --> set LR for epoch 1
# epoch 1: optimizer.step(); lr_scheduler.step(2) --> set LR for epoch 2
# reset unscaled LRs (to the original scheduler's one) to be able to step the base LR scheduler
# Note: epoch==0: reset LR scheduler; epoch==None: scale LR for next epoch;
unscaled_lrs = self.base_lrs if epoch == 0 else self.get_last_lr()
for group, lr in zip(self.base_lr_scheduler.optimizer.param_groups, unscaled_lrs):
group['lr'] = lr
self.base_lr_scheduler.step(epoch) # set unscaled lr, _step_count, last_epoch, _last_lr for new epoch
# scale the learning rate for the next iteration for each parameter group.
self.last_epoch = self.last_epoch + 1 if epoch is None else epoch
# batch sizes are precomputed and stored in batch_sizes se we loop around to get the next one
batch_size = self.batch_sizes[self.last_epoch % len(self.batch_sizes)]
for group in self.base_lr_scheduler.optimizer.param_groups:
group['lr'] = scale_lr(self.base_batch_size, batch_size, group['lr'], self.lr_scaling_method)
if self.verbose:
logger.info(
f"Next batch id {self.last_epoch}. "\
f"Reference batch_size {self.base_batch_size} and lr {unscaled_lrs}. "\
f"Scaled batch_size {batch_size} and lr {self.get_lr()}.")
def lr_scheduler_for_variable_batch_size(base_batch_size,
batch_sizes,
dataloader,
lr_scheduler_or_optimizer,
lr_scaling_method='linear',
verbose=False):
"""
returns a class that provides an LR scheduler that scales the learning rate at every
iteration taking into account the batch size of that iteration.
If learning rate is constant, ie no LR scheduler, then the base LR will be taken from the
constant LR values in the optimizer param groups. Otherwise from the scheduler's LR.
Arguments:
- `base_batch_size`: the batch size that the base LR in the optimizer or scheduler refers to;
- `lr_scaling_method`: method to use to scale LR - see `scale_lr()`;
- `lr_scheduler_or_optimizer`: one instance of `LRScheduler` or `Optimizer` to be used as base;
- `batch_sizes`: the effective batch size of each batch in the dataloader;
Returns the new LRScheduler
"""
class StubLRScheduler(LRScheduler):
""" a stub LR scheduler that does not change the LR, keeps it constant """
def get_lr(self) -> float:
return self.base_lrs
if isinstance(lr_scheduler_or_optimizer, Optimizer):
lr_scheduler = StubLRScheduler(lr_scheduler_or_optimizer)
elif hasattr(lr_scheduler_or_optimizer, 'optimizer'): #LRScheduler or DeepSpeed 'object' schedulers
assert isinstance(lr_scheduler_or_optimizer.optimizer, Optimizer)
lr_scheduler = lr_scheduler_or_optimizer
else:
raise ValueError("Unknown type for lr_scheduler_or_optimizer: {}".format(type(lr_scheduler_or_optimizer)))
return VariableBatchSizeLR(lr_scheduler=lr_scheduler,
base_batch_size=base_batch_size,
batch_sizes=batch_sizes,
dataloader=dataloader,
lr_scaling_method=lr_scaling_method,
verbose=verbose)
def get_dataloader_and_lr_scheduler_for_variable_batch_size_deepspeed(dataset,
engine,
dataset_seqlens=None,
dataset_filter_ids=None,
dataloader_collate_fn=None,
sample_padding_fn=None,
batch_seqlens_fn=None):
"""
a simplified call to get_dataloader_and_lr_scheduler_for_variable_batch_size for the deepspeed runtime.
Needs the seqlens of every sample. It will try three alternatives:
- if `dataset_seqlens` is provided by user, use that.
- otherwise, looks for the seqlen metric path (in the connfig) that contains the output of the Data Analyzer
- otherwise, use the user-provided function `batch_seqlens_fn` and call Data Analyzer to output seqlen metric
See `batch_by_seqlens()` for arguments and more documentation.
"""
data_efficiency_config = engine._config.data_efficiency_config
data_sampling_config = data_efficiency_config[DATA_SAMPLING]
batching_config = data_sampling_config[DYNAMIC_BATCHING]
assert batching_config[DYNAMIC_BATCHING_ENABLED], "Dynamic batching is not enabled in the config"
if dataset_seqlens is None:
# In seqlen provided by user, look for the seqlen metric that was output by the Data Analyzer
# (see the main in deepspeed/runtime/data_pipeline/data_sampling/data_analyzer.py for an example)
metrics_path = batching_config[DYNAMIC_BATCHING_METRICS_PATH]
sample_to_seqlen_path = os.path.join(metrics_path, "seqlen/seqlen_sample_to_metric")
if not (os.path.exists(f"{sample_to_seqlen_path}.bin") and os.path.exists(f"{sample_to_seqlen_path}.idx")):
# if the metric files are not found, we run the DataAnalyzer to write the metric files
msg = f"Cannot find metric files for sequence length in {sample_to_seqlen_path}.idx or *.bin."
msg += " We will run data analyzer to generated them..."
logger.warning(msg)
if batch_seqlens_fn is None:
raise ValueError("sample_seqlen_fn must be provided if dataset_seqlens is not provided")
DistributedDataAnalyzer(
dataset=dataset,
metric_functions=[batch_seqlens_fn],
collate_fn=dataloader_collate_fn,
batch_size=2**10, # batch size for map-reduce, not training
num_workers=engine.world_size,
worker_id=engine.global_rank,
save_path=pathlib.Path(metrics_path),
metric_types=['single_value_per_sample'],
metric_names=["seqlen"],
device=engine.device,
).run_map_reduce()
dataset_seqlens = MMapIndexedDataset(sample_to_seqlen_path, skip_warmup=True)
assert len(dataset_seqlens) == len(dataset), \
"Seqlens size does not match the input dataset size. If you changed the dataset, delete the metrics_path folder."
# TODO we are copying all seqlens into memory, we should adapt the code to use an iterative streamer
# and use the other files output by DataAnalyzer that returns an ordered dictionary of seqlen to sample ids
dataset_seqlens = np.array(list(dataset_seqlens), dtype=np.int64).flatten() # from Nx1 to N
dataloader, lr_scheduler, deepspeed_io_kwargs = get_dataloader_and_lr_scheduler_for_variable_batch_size(
dataset=dataset,
dataset_filter_ids=dataset_filter_ids,
dataset_seqlens=dataset_seqlens,
effective_batch_size=engine.train_batch_size(),
max_tokens=batching_config[DYNAMIC_BATCHING_MAX_TOKENS],
lr_scaling_method=batching_config[DYNAMIC_BATCHING_LR_SCALING_METHOD],
sequence_picking_order=batching_config[DYNAMIC_BATCHING_SEQUENCE_PICKING_ORDER],
min_batch_size=batching_config[DYNAMIC_BATCHING_MIN_BATCH_SIZE],
max_batch_size=batching_config[DYNAMIC_BATCHING_MAX_BATCH_SIZE],
dataloader_batch_size=engine.train_micro_batch_size_per_gpu(),
dataloader_rank=engine.data_parallel_group.rank(),
dataloader_num_replicas=engine.data_parallel_group.size(),
dataloader_num_workers=data_sampling_config[DATA_SAMPLING_NUM_WORKERS],
dataloader_collate_fn=dataloader_collate_fn,
dataloader_pin_memory=data_sampling_config[DATA_SAMPLING_PIN_MEMORY],
sample_padding_fn=sample_padding_fn,
lr_scheduler_or_optimizer=engine.lr_scheduler or engine.optimizer,
required_microbatches_of_same_size=isinstance(engine, PipelineEngine),
required_microbatches_of_same_seqlen=isinstance(engine, PipelineEngine),
verbose=batching_config[DYNAMIC_BATCHING_VERBOSE],
seed=data_efficiency_config[DATA_EFFICIENCY_SEED],
)
return dataloader, lr_scheduler, deepspeed_io_kwargs
def get_dataloader_and_lr_scheduler_for_variable_batch_size(
dataset,
dataset_seqlens,
max_tokens,
effective_batch_size,
dataset_filter_ids=None,
lr_scaling_method="linear",
min_batch_size=1,
max_batch_size=None,
sequence_picking_order="dataloader",
dataloader_batch_size=1,
dataloader_rank=0,
dataloader_num_replicas=1,
dataloader_num_workers=0,
dataloader_collate_fn=None,
dataloader_pin_memory=False,
lr_scheduler_or_optimizer=None,
required_microbatches_of_same_size=False,
required_microbatches_of_same_seqlen=False,
sample_padding_fn=None,
verbose=False,
seed=None,
):
""" returns a dataloader and LR scheduler for the variable batch size. see `batch_by_seqlens()` for details. """
# effective_batch_size = train_micro_batch_size_per_gpu * gradient_accumulation_steps * number of dataloaders
microbatch_ids, batch_sizes, batch_max_seqlens = batch_by_seqlens(
seqlens=dataset_seqlens,
max_tokens=max_tokens,
sequence_ids_per_mb=dataset_filter_ids,
min_batch_size=min_batch_size,
max_batch_size=max_batch_size,
sequence_picking_order=sequence_picking_order,
effective_batch_size=effective_batch_size,
required_microbatches_of_same_size=required_microbatches_of_same_size,
verbose=verbose,
seed=seed,
)
dataloader, deepspeed_io_kwargs = dataloader_for_variable_batch_size(
dataset=dataset,
microbatch_ids=microbatch_ids,
batch_max_seqlens=batch_max_seqlens,
dataloader_rank=dataloader_rank,
dataloader_num_replicas=dataloader_num_replicas,
dataloader_batch_size=dataloader_batch_size,
dataloader_collate_fn=dataloader_collate_fn,
dataloader_num_workers=dataloader_num_workers,
dataloader_pin_memory=dataloader_pin_memory,
required_microbatches_of_same_seqlen=required_microbatches_of_same_seqlen,
sample_padding_fn=sample_padding_fn,
)
lr_scheduler = lr_scheduler_for_variable_batch_size(base_batch_size=effective_batch_size,
batch_sizes=batch_sizes,
lr_scaling_method=lr_scaling_method,
lr_scheduler_or_optimizer=lr_scheduler_or_optimizer,
dataloader=dataloader,
verbose=verbose)
return dataloader, lr_scheduler, deepspeed_io_kwargs