|
| 1 | +import abc |
1 | 2 | import math |
2 | 3 |
|
3 | 4 | import numpy as np |
4 | 5 | import numpy.random |
5 | 6 |
|
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 |
10 | 9 |
|
11 | 10 | try: |
12 | 11 | from fast_llm.csrc.data import build_sample_idx # noqa |
|
16 | 15 | _extension_available = False |
17 | 16 |
|
18 | 17 |
|
19 | | -class GPTDataset(RawDataset): |
| 18 | +class GPTIndexedDataset(Dataset): |
20 | 19 | """ |
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? |
22 | 22 | """ |
23 | 23 |
|
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 |
54 | 26 |
|
55 | | - def get(self, idx, offset=0, length=None): |
| 27 | + @property |
| 28 | + def num_documents(self) -> int: |
56 | 29 | """ |
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. |
60 | 32 | """ |
61 | | - return self._indexed_dataset.get(idx, offset, length) |
| 33 | + return len(self.get_document_sizes()) |
62 | 34 |
|
63 | 35 | @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() |
66 | 42 |
|
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": |
69 | 45 | """ |
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. |
72 | 49 | """ |
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 | | - } |
81 | 50 |
|
82 | 51 | def sample(self, num_samples: int, sequence_length: int, np_rng: numpy.random.RandomState, verbose: bool): |
83 | 52 | """ |
84 | 53 | Create a `GPTSampledDataset` with the requested parameters. |
85 | 54 | """ |
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) |
88 | 60 | # For the last epoch, decide whether include the entire epoch |
89 | 61 | # in the global shuffle or not. |
90 | 62 | # 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 |
92 | 64 | 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 |
94 | 66 | # If we have less than 80% of the samples for the last epoch, separate out the epoch and treat it differently. |
95 | 67 | # Note: the 80% number is just based on common sense and can be adjusted if needed. |
96 | 68 | separate_last_epoch = num_epochs > 1 and last_epoch_samples < 0.8 * samples_per_epoch |
97 | 69 |
|
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) |
99 | 71 | 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:]) |
102 | 74 | else: |
103 | 75 | np_rng.shuffle(doc_idx) |
104 | 76 |
|
105 | 77 | assert _extension_available, ( |
106 | 78 | "The C++ extension for dataset sampling is missing." " Please make sure Fast-LLM is installed correctly." |
107 | 79 | ) |
108 | 80 |
|
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) |
112 | 82 |
|
113 | 83 | # shuffle-idx. |
114 | 84 | # -1 is due to data structure used to retrieve the index: |
|
0 commit comments