Skip to content

Commit b826f7b

Browse files
authored
Dataset wrapper classes (#37)
1 parent 4717427 commit b826f7b

8 files changed

Lines changed: 208 additions & 115 deletions

File tree

fast_llm/data/config.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,6 @@ class Dataset(abc.ABC):
151151
A generic dataset class compatible with torch.utils.data.Dataset but with a slightly different signature.
152152
"""
153153

154-
@abc.abstractmethod
155-
def __getitem__(self, index: int):
156-
pass
157-
158-
@abc.abstractmethod
159-
def __len__(self):
160-
pass
161-
162154
@property
163155
@abc.abstractmethod
164156
def name(self):
@@ -167,17 +159,16 @@ def name(self):
167159
"""
168160

169161

170-
class RawDataset(Dataset): # noqa
171-
"""
172-
A raw dataset class containing a list of unsampled, unprocessed samples, i.e., matching what is stored on disk.
173-
(Excluding off-line processing prior to training.)
174-
Functionally identical to a `Dataset`, but renamed for clarity.
175-
"""
176-
177-
178162
class SampledDataset(Dataset): # noqa
179163
"""
180164
A sampled dataset class containing a prepared list of samples to be indexed sequentially (as-is) during training.
181165
(See the `Sampler` class below.)
182-
Functionally identical to a `Dataset`, but renamed for clarity.
183166
"""
167+
168+
@abc.abstractmethod
169+
def __getitem__(self, index: int):
170+
pass
171+
172+
@abc.abstractmethod
173+
def __len__(self):
174+
pass

fast_llm/data/gpt/concatenated.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import numpy as np
2+
3+
from fast_llm.data.gpt.dataset import GPTIndexedDataset
4+
from fast_llm.utils import padded_cumsum
5+
6+
7+
class GPTConcatenatedDataset(GPTIndexedDataset):
8+
9+
def __init__(
10+
self,
11+
name: str,
12+
datasets: list[GPTIndexedDataset],
13+
):
14+
self._name = name
15+
self._datasets = datasets
16+
sizes = [dataset.num_documents for dataset in self._datasets]
17+
self._dataset_splits = padded_cumsum(sizes)
18+
self._num_documents = sum(sizes)
19+
20+
@property
21+
def num_tokens(self):
22+
return sum(dataset.num_tokens for dataset in self._datasets)
23+
24+
def num_documents(self):
25+
return sum(dataset.num_documents for dataset in self._datasets)
26+
27+
def get_document_sizes(self) -> "np.ndarray":
28+
# TODO: This can be really big.
29+
return np.concatenate([dataset.get_document_sizes() for dataset in self._datasets])
30+
31+
def get(self, document: int, offset: int = 0, length: int | None = None):
32+
"""
33+
Get the sample (document) with the given index (in the dataset slice),
34+
optionally sub-sampled to a specific offset (starting point) and maximum length
35+
(end = min(offset + length, sample_length).
36+
"""
37+
dataset = np.searchsorted(self._dataset_splits[1:], document, side="right")
38+
return self._datasets[dataset].get(document - self._dataset_splits[dataset], offset, length)
39+
40+
@property
41+
def name(self):
42+
return self._name

fast_llm/data/gpt/data.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from fast_llm.data.blended import BlendedDataset
1212
from fast_llm.data.config import Data, DatasetSource, SampledDataset
1313
from fast_llm.data.gpt.config import DataConfig
14-
from fast_llm.data.gpt.dataset import GPTDataset
1514
from fast_llm.data.gpt.dummy import DummyGPTDataset
1615
from fast_llm.data.gpt.memmap import GPTMemmapDataset
17-
from fast_llm.data.gpt.sampled import GPTSampledDataset
16+
from fast_llm.data.gpt.sampled import GPTSampledIndexedDataset
17+
from fast_llm.data.gpt.slice import GPTDatasetSlice
1818
from fast_llm.data.iterator import SampledDatasetIterator
1919
from fast_llm.data.tokenizer import Tokenizer
2020
from fast_llm.engine.config_utils.run import get_run, log_main_rank
@@ -197,13 +197,15 @@ def get_iterator(
197197
)
198198

199199
def _build_and_sample_gpt_dataset(self, name: str, dataset_samples_per_phase: dict[PhaseType, int]):
200-
dataset_split = GPTDataset.from_splits(name, GPTMemmapDataset(self._dataset_prefixes[name]), self._phase_split)
200+
dataset_split = GPTDatasetSlice.from_splits(
201+
GPTMemmapDataset(name, self._dataset_prefixes[name]), self._phase_split
202+
)
201203

202204
sampled_datasets = {}
203205
for phase, num_samples in dataset_samples_per_phase.items():
204206
if num_samples == 0:
205207
continue
206-
sampled_datasets[phase] = GPTSampledDataset(
208+
sampled_datasets[phase] = GPTSampledIndexedDataset(
207209
dataset_split[phase],
208210
num_samples=num_samples,
209211
sequence_length=self._max_sequence_length,

fast_llm/data/gpt/dataset.py

Lines changed: 35 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import abc
12
import math
23

34
import numpy as np
45
import numpy.random
56

6-
from fast_llm.data.config import RawDataset
7-
from fast_llm.data.gpt.memmap import GPTMemmapDataset
8-
from fast_llm.engine.distributed.config import PhaseType
9-
from fast_llm.utils import Assert, padded_cumsum
7+
from fast_llm.data.config import Dataset
8+
from fast_llm.utils import Assert
109

1110
try:
1211
from fast_llm.csrc.data import build_sample_idx # noqa
@@ -16,99 +15,70 @@
1615
_extension_available = False
1716

1817

19-
class GPTDataset(RawDataset):
18+
class GPTIndexedDataset(Dataset):
2019
"""
21-
A GPT dataset, which reads samples from (a split of) a `MMapIndexedDataset` pointing to a GPT dataset.
20+
A GPT dataset containing a list of unsampled, unprocessed samples.
21+
TODO: Move sampling responsibility here?
2222
"""
2323

24-
def __init__(
25-
self,
26-
name: str,
27-
indexed_dataset: GPTMemmapDataset,
28-
split_begin: int | None = None,
29-
split_end: int | None = None,
30-
):
31-
self._name = name
32-
self._indexed_dataset = indexed_dataset
33-
34-
self._split_begin = 0 if split_begin is None else split_begin
35-
self._split_end = len(indexed_dataset) if split_end is None else split_end
36-
37-
# Checks
38-
try:
39-
Assert.geq(self._split_begin, 0)
40-
Assert.in_range_incl(self._split_end, self._split_begin + 1, len(indexed_dataset))
41-
except Exception as e:
42-
raise AssertionError(
43-
f"Invalid document indices for dataset {name} with length {len(indexed_dataset)}"
44-
) from e
45-
46-
def __len__(self):
47-
return self._split_end - self._split_begin
48-
49-
def __getitem__(self, index: int):
50-
"""
51-
Get the sample (document) with the given index (in the split dataset).
52-
"""
53-
return self.get(index)
24+
def get(self, document: int, offset: int = 0, length: int | None = None):
25+
pass
5426

55-
def get(self, idx, offset=0, length=None):
27+
@property
28+
def num_documents(self) -> int:
5629
"""
57-
Get the sample (document) with the given index (in the split dataset),
58-
optionally sub-sampled to a specific offset (starting point) and maximum length
59-
(end = min(offset + length, sample_length).
30+
Number of documents in the dataset.
31+
Can be calculated from document sizes but may be overridden if there is a better method.
6032
"""
61-
return self._indexed_dataset.get(idx, offset, length)
33+
return len(self.get_document_sizes())
6234

6335
@property
64-
def name(self):
65-
return self._name
36+
def num_tokens(self) -> int:
37+
"""
38+
Number of tokens in the dataset.
39+
Can be calculated from document sizes but may be overridden if there is a better method.
40+
"""
41+
return self.get_document_sizes().sum()
6642

67-
@classmethod
68-
def from_splits(cls, name: str, indexed_dataset: GPTMemmapDataset, phase_split: dict[PhaseType, float]):
43+
@abc.abstractmethod
44+
def get_document_sizes(self) -> "np.ndarray":
6945
"""
70-
Create a set of GPT datasets from a MMapIndexedDataset,
71-
each containing approximately the requested proportion of the total tokens.
46+
The size of each document in the dataset.
47+
The resulting array could be very large, so this method should be called cautiously,
48+
and derived classes should try to avoid holding the whole array im memory.
7249
"""
73-
split_probs = list(phase_split.values())
74-
Assert.eq(sum(split_probs), 1)
75-
num_documents = indexed_dataset.sizes.shape[0]
76-
splits = [round(x) for x in padded_cumsum(split_probs) * num_documents]
77-
return {
78-
phase: GPTDataset(f"{name}_{phase.value}", indexed_dataset, split_begin, split_end)
79-
for phase, split_begin, split_end in zip(phase_split, splits[:-1], splits[1:])
80-
}
8150

8251
def sample(self, num_samples: int, sequence_length: int, np_rng: numpy.random.RandomState, verbose: bool):
8352
"""
8453
Create a `GPTSampledDataset` with the requested parameters.
8554
"""
86-
tokens_per_epoch = np.sum(self._indexed_dataset.sizes[self._split_begin : self._split_end])
87-
num_epochs = math.ceil((sequence_length * num_samples + 1) / tokens_per_epoch)
55+
document_sizes = self.get_document_sizes()
56+
num_documents = len(document_sizes)
57+
num_tokens = document_sizes.sum()
58+
59+
num_epochs = math.ceil((sequence_length * num_samples + 1) / num_tokens)
8860
# For the last epoch, decide whether include the entire epoch
8961
# in the global shuffle or not.
9062
# Get the number of samples for the last epoch
91-
main_epochs_samples = ((num_epochs - 1) * tokens_per_epoch - 1) // sequence_length
63+
main_epochs_samples = ((num_epochs - 1) * num_tokens - 1) // sequence_length
9264
last_epoch_samples = num_samples - main_epochs_samples
93-
samples_per_epoch = (tokens_per_epoch - 1) // sequence_length
65+
samples_per_epoch = (num_tokens - 1) // sequence_length
9466
# If we have less than 80% of the samples for the last epoch, separate out the epoch and treat it differently.
9567
# Note: the 80% number is just based on common sense and can be adjusted if needed.
9668
separate_last_epoch = num_epochs > 1 and last_epoch_samples < 0.8 * samples_per_epoch
9769

98-
doc_idx = np.tile(np.arange(self._split_begin, self._split_end, dtype=np.int32), num_epochs)
70+
doc_idx = np.tile(np.arange(num_documents, dtype=np.int32), num_epochs)
9971
if separate_last_epoch:
100-
np_rng.shuffle(doc_idx[: -len(self)])
101-
np_rng.shuffle(doc_idx[-len(self) :])
72+
np_rng.shuffle(doc_idx[:-num_documents])
73+
np_rng.shuffle(doc_idx[-num_documents:])
10274
else:
10375
np_rng.shuffle(doc_idx)
10476

10577
assert _extension_available, (
10678
"The C++ extension for dataset sampling is missing." " Please make sure Fast-LLM is installed correctly."
10779
)
10880

109-
sample_idx = build_sample_idx(
110-
self._indexed_dataset.sizes, doc_idx, sequence_length, num_epochs, tokens_per_epoch, verbose
111-
)
81+
sample_idx = build_sample_idx(document_sizes, doc_idx, sequence_length, num_epochs, num_tokens, verbose)
11282

11383
# shuffle-idx.
11484
# -1 is due to data structure used to retrieve the index:

fast_llm/data/gpt/memmap.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
import numpy as np
55

6+
from fast_llm.data.gpt.dataset import GPTIndexedDataset
67
from fast_llm.utils import Assert, div, padded_cumsum
78

89

9-
class GPTMemmapDataset:
10+
class GPTMemmapDataset(GPTIndexedDataset):
1011
"""
1112
A memory map dataset, which handles lazy loading of a pre-processed dataset in the Megatron-LM format,
1213
i.e. a pair of numpy file containing
@@ -27,11 +28,12 @@ class GPTMemmapDataset:
2728
}
2829
_INDEX_HEADER = b"MMIDIDX\x00\x00"
2930

30-
def __init__(self, prefix: pathlib.Path | str):
31-
self._init(prefix)
31+
def __init__(self, name: str, prefix: pathlib.Path | str):
32+
self._init(name, prefix)
3233

33-
def _init(self, prefix: pathlib.Path | str):
34+
def _init(self, name: str, prefix: pathlib.Path | str):
3435
super().__init__()
36+
self._name = name
3537
self._prefix = pathlib.Path(prefix)
3638

3739
with self._prefix.with_suffix(".idx").open("rb") as stream:
@@ -45,48 +47,58 @@ def _init(self, prefix: pathlib.Path | str):
4547

4648
self._index_bin_buffer_mmap = np.memmap(self._prefix.with_suffix(".idx"), mode="r", order="C")
4749
self._index_bin_buffer = memoryview(self._index_bin_buffer_mmap)
48-
self.sizes = np.frombuffer(self._index_bin_buffer, dtype=np.int32, count=self._num_documents, offset=offset)
50+
self._document_sizes = np.frombuffer(
51+
self._index_bin_buffer, dtype=np.int32, count=self._num_documents, offset=offset
52+
)
4953
self._pointers = np.frombuffer(
50-
self._index_bin_buffer, dtype=np.int64, count=self._num_documents, offset=offset + self.sizes.nbytes
54+
self._index_bin_buffer,
55+
dtype=np.int64,
56+
count=self._num_documents,
57+
offset=offset + self._document_sizes.nbytes,
5158
)
5259

5360
self._bin_buffer_mmap = np.memmap(self._prefix.with_suffix(".bin"), mode="r", order="C")
5461
self._bin_buffer = memoryview(self._bin_buffer_mmap)
5562

5663
def __getstate__(self):
57-
return self.prefix
64+
return (self._name, self._prefix)
5865

5966
def __setstate__(self, state):
60-
self._init(state)
67+
self._init(*state)
6168

6269
def __del__(self):
6370
self._bin_buffer_mmap._mmap.close() # noqa
6471
del self._bin_buffer_mmap
6572
self._index_bin_buffer_mmap._mmap.close() # noqa
6673
del self._index_bin_buffer_mmap
6774

68-
def __len__(self):
69-
return self._num_documents
70-
7175
def get(self, idx, offset=0, length=None):
7276
return np.frombuffer(
7377
self._bin_buffer,
7478
dtype=self._dtype,
75-
count=self.sizes[idx] - offset if length is None else length,
79+
count=self._document_sizes[idx] - offset if length is None else length,
7680
offset=self._pointers[idx] + offset * np.dtype(self._dtype).itemsize,
7781
)
7882

7983
@property
80-
def num_documents(self):
84+
def name(self):
85+
return self._name
86+
87+
@property
88+
def num_documents(self) -> int:
8189
return self._num_documents
8290

8391
@property
84-
def num_tokens(self):
92+
def num_tokens(self) -> int:
8593
return div(self._bin_buffer_mmap.size, np.dtype(self._dtype).itemsize)
8694

87-
@property
88-
def prefix(self):
89-
return self._prefix
95+
def get_document_sizes(self) -> "np.ndarray":
96+
"""
97+
The size of each document in the dataset.
98+
The resulting array could be very large, so this method should be called cautiously,
99+
and derived classes should try to avoid holding the whole array im memory.
100+
"""
101+
return self._document_sizes
90102

91103
@classmethod
92104
def write_dataset(cls, prefix: pathlib.Path | str, documents: list[np.ndarray]):

0 commit comments

Comments
 (0)