Skip to content

Commit 3f30b98

Browse files
committed
support smart batching in torchacc
1 parent 75dd4af commit 3f30b98

2 files changed

Lines changed: 134 additions & 0 deletions

File tree

torchacc/data/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from smart_batching import SmartBatchingSampler, flatten_mapfn_for_swift

torchacc/data/smart_batching.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import binpacking
2+
import torch
3+
import numpy as np
4+
from typing import List, Dict, Any
5+
6+
def flatten_mapfn_for_swift(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
7+
"""
8+
Data collator used for padding free approach. Does the following:
9+
- concatate the entire mini batch into single long sequence [1, total_tokens]
10+
- no padding will be added, returns `input_ids`, `labels` and `position_ids`
11+
Args:
12+
batch(`List[Dict[str, Any]]`): The input data in batch
13+
padding_to(`int`, optional): Whether padding the batch to a fixed length, if none, the batch
14+
will be padded to the `longest`
15+
"""
16+
packed_data = {}
17+
position_id_lengths = [len(item['input_ids']) for item in batch]
18+
packed_data['input_ids'] = np.concatenate([item['input_ids'] for item in batch])
19+
packed_data['labels'] = np.concatenate([item['labels'] for item in batch])
20+
packed_data['position_ids'] = np.concatenate([list(range(pil)) for pil in position_id_lengths])
21+
return packed_data
22+
23+
24+
class SmartBatchingSampler:
25+
"""Smart batching sampler for Megatron-LM.
26+
Args:
27+
dataset: A list of sequence lengths, each length is the length of a sequence.
28+
total_samples: Total number of samples to be consumed.
29+
micro_batch_size: Micro batch size.
30+
data_parallel_rank: Data parallel rank.
31+
data_parallel_size: Data parallel size.
32+
consumed_samples: Consumed samples, mainly usedfor continue train from the last checkpoint.
33+
"""
34+
def __init__(self,
35+
dataset, # Lengths of sequences,
36+
dataset_type, # Workload type
37+
total_samples, # Total number of samples
38+
micro_batch_size, # Micro batch size
39+
data_parallel_rank, # Data parallel rank
40+
data_parallel_size, # Data parallel size
41+
consumed_samples = 0, # Consumed samples, mainly used for continue train from the last checkpoint
42+
balance_strategy='micro-batch', # Balance strategy
43+
):
44+
# Keep a copy of input params for later use.
45+
self.dataset = dataset
46+
self.total_samples = total_samples
47+
self.dataset_type = dataset_type
48+
self.consumed_samples = consumed_samples
49+
self.micro_batch_size = micro_batch_size
50+
self.data_parallel_rank = data_parallel_rank
51+
self.data_parallel_size = data_parallel_size
52+
self.micro_batch_times_data_parallel_size = \
53+
self.micro_batch_size * data_parallel_size
54+
self.balance_strategy = balance_strategy
55+
self.last_batch_size = self.total_samples % self.micro_batch_times_data_parallel_size
56+
57+
# Sanity checks.
58+
assert self.total_samples > 0, \
59+
'no sample to consume: {}'.format(self.total_samples)
60+
assert self.micro_batch_size > 0
61+
assert data_parallel_size > 0
62+
assert self.data_parallel_rank < data_parallel_size, \
63+
'data_parallel_rank should be smaller than data size: {}, ' \
64+
'{}'.format(self.data_parallel_rank, data_parallel_size)
65+
assert self.balance_strategy in ['micro-batch', "none"], \
66+
'invalid balance_strategy: {}, only {} and {} are supported'.format(self.balance_strategy, 'micro-batch', 'none')
67+
assert self.dataset_type in ['swift'] \
68+
'invalid dataset_type: {}, only {} are supported'.format(self.dataset_type, 'swift')
69+
def __len__(self):
70+
return self.total_samples // self.data_parallel_size
71+
72+
def binpack_to_constant_bin_number_with_max_weight_limit(self, packages, max_length, bin_num):
73+
"""A bin-packing algorithm to pack the packages into bins with constant bin number and max length limit""" \
74+
"""Returns None if the max weight limit cannot be satisfied"""
75+
packages.sort(key=lambda item: item[1], reverse=True)
76+
bins = [[] for _ in range(bin_num)]
77+
bin_sum = [0] * bin_num
78+
package_idx = 0
79+
bin_idx_list = list(range(0, bin_num, 1)) + list(range(bin_num - 1, -1, -1))
80+
while True:
81+
processed = False
82+
for bin_idx in bin_idx_list:
83+
if bin_sum[bin_idx] + packages[package_idx][1] <= max_length:
84+
bins[bin_idx].append(packages[package_idx])
85+
bin_sum[bin_idx] += packages[package_idx][1]
86+
package_idx += 1
87+
processed = True
88+
if package_idx == len(packages):
89+
break
90+
if not processed or package_idx == len(packages):
91+
break
92+
return bins if package_idx == len(packages) else None
93+
94+
def get_sequence_length(self, idx):
95+
if self.dataset_type == "swift":
96+
return self.dataset[idx]['input_ids'].shape[0]
97+
def construct_balanced_batch(self, batch):
98+
# No balancing, just flatten the batch
99+
if self.balance_strategy == "none":
100+
return batch[self.data_parallel_rank::self.data_parallel_size]
101+
# Micro-batch level balancing
102+
if self.balance_strategy == "micro-batch":
103+
packages = {}
104+
for idx, sample_idx in enumerate(batch):
105+
packages[idx] = self.get_sequence_length(sample_idx)
106+
bins = binpacking.to_constant_bin_number(packages, self.data_parallel_size)
107+
current_batch = []
108+
for idx in bins[self.data_parallel_rank].keys():
109+
current_batch.append(batch[idx])
110+
return current_batch
111+
112+
def __iter__(self):
113+
# Sanity checks:
114+
active_total_samples = self.total_samples - self.last_batch_size
115+
self.epoch = self.consumed_samples // active_total_samples
116+
current_epoch_samples = self.consumed_samples % active_total_samples
117+
assert current_epoch_samples % self.micro_batch_times_data_parallel_size == 0
118+
119+
# Continue train from where it left
120+
g = torch.Generator()
121+
g.manual_seed(self.epoch)
122+
shuffle_samples = torch.randperm(self.total_samples, generator=g).tolist()
123+
shuffle_samples = shuffle_samples[current_epoch_samples: ]
124+
# Get one batch
125+
batch = []
126+
for idx in shuffle_samples:
127+
batch.append(idx)
128+
# Balance micro-batch across data parallel ranks
129+
if (self.balance_strategy == "micro-batch" or self.balance_strategy == "none") and \
130+
len(batch) == self.micro_batch_times_data_parallel_size:
131+
self.consumed_samples += self.micro_batch_size
132+
yield self.construct_balanced_batch(batch)
133+
batch.clear()

0 commit comments

Comments
 (0)