Skip to content

Commit 7989595

Browse files
authored
Samplable dataset (#40)
1 parent b826f7b commit 7989595

8 files changed

Lines changed: 156 additions & 125 deletions

File tree

fast_llm/data/config.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22
import enum
3+
import pathlib
34
import typing
45

56
from fast_llm.config import Config, Field, FieldHint, check_field, config_class, skip_valid_if_none
@@ -159,7 +160,20 @@ def name(self):
159160
"""
160161

161162

162-
class SampledDataset(Dataset): # noqa
163+
@config_class
164+
class SamplingConfig(Config):
165+
num_samples: int = Field(default=1, desc="Number of samples to generate.")
166+
seed: int = Field(default=0, desc="Random seed.")
167+
cache_directory: pathlib.Path | None = Field(default=None, desc="Path to the sampling cache directory.")
168+
verbose: bool = Field(default=True, desc="Log sampling progress.")
169+
170+
171+
class SamplableDataset(Dataset):
172+
def sample(self, config: SamplingConfig, data: Data):
173+
pass
174+
175+
176+
class SampledDataset(Dataset):
163177
"""
164178
A sampled dataset class containing a prepared list of samples to be indexed sequentially (as-is) during training.
165179
(See the `Sampler` class below.)

fast_llm/data/gpt/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
@config_class()
15-
class DataConfig(DataConfig):
15+
class GPTDataConfig(DataConfig):
1616
"""
1717
Configuration for the dataset(s), split and sampling.
1818
Currently hard-coded to a GPT dataset.

fast_llm/data/gpt/data.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010

1111
from fast_llm.data.blended import BlendedDataset
1212
from fast_llm.data.config import Data, DatasetSource, SampledDataset
13-
from fast_llm.data.gpt.config import DataConfig
13+
from fast_llm.data.gpt.config import GPTDataConfig
14+
from fast_llm.data.gpt.dataset import GPTSamplingConfig
1415
from fast_llm.data.gpt.dummy import DummyGPTDataset
1516
from fast_llm.data.gpt.memmap import GPTMemmapDataset
16-
from fast_llm.data.gpt.sampled import GPTSampledIndexedDataset
1717
from fast_llm.data.gpt.slice import GPTDatasetSlice
1818
from fast_llm.data.iterator import SampledDatasetIterator
1919
from fast_llm.data.tokenizer import Tokenizer
@@ -40,10 +40,11 @@ class GPTData(Data):
4040
_cache_directory: pathlib.Path | None
4141
_samples_per_phase: dict[PhaseType, int]
4242
_phases: typing.ClassVar[tuple[PhaseType, ...]] = (PhaseType.training, PhaseType.validation, PhaseType.test)
43+
_is_setup: bool = False
4344

4445
def __init__(
4546
self,
46-
config: DataConfig,
47+
config: GPTDataConfig,
4748
distributed_config: DistributedConfig,
4849
vocab_size: int,
4950
max_sequence_length: int,
@@ -52,8 +53,8 @@ def __init__(
5253
Create the data and gather some basic information on the dataset(s).
5354
Should be `setup` before use.
5455
"""
55-
self._config = config.validate()
56-
self._distributed_config = distributed_config.validate()
56+
self._config = config
57+
self._distributed_config = distributed_config
5758
self._vocab_size = vocab_size
5859
self._max_sequence_length = max_sequence_length
5960
Assert.eq(len(self._config.split), len(self._phases))
@@ -166,6 +167,20 @@ def setup(self, distributed: Distributed, samples_per_phase: dict[PhaseType, int
166167
)
167168
for phase, datasets in self._sampled_datasets.items()
168169
}
170+
self._is_setup = True
171+
172+
@property
173+
def config(self):
174+
return self._config
175+
176+
@property
177+
def tokenizer(self):
178+
assert self._is_setup
179+
return self._tokenizer
180+
181+
@property
182+
def distributed(self):
183+
return self._distributed
169184

170185
def get_iterator(
171186
self,
@@ -176,6 +191,7 @@ def get_iterator(
176191
num_workers: int,
177192
prefetch_factor: int | None = None,
178193
):
194+
assert self._is_setup
179195
Assert.incl(phase, self._blended_datasets)
180196
Assert.in_range_incl(batch_config.sequence_length, 1, self._max_sequence_length)
181197
log_main_rank(f"Initializing {phase} data iterator from sample {consumed_samples}...")
@@ -205,25 +221,23 @@ def _build_and_sample_gpt_dataset(self, name: str, dataset_samples_per_phase: di
205221
for phase, num_samples in dataset_samples_per_phase.items():
206222
if num_samples == 0:
207223
continue
208-
sampled_datasets[phase] = GPTSampledIndexedDataset(
209-
dataset_split[phase],
210-
num_samples=num_samples,
211-
sequence_length=self._max_sequence_length,
212-
seed=self._distributed.config.seed,
213-
group=self._distributed.world_group,
214-
config=self._config,
215-
tokenizer=self._tokenizer,
216-
cache_directory=(
217-
self._dataset_prefixes[name].parent if self._cache_directory is None else self._cache_directory
224+
sampled_datasets[phase] = dataset_split[phase].sample(
225+
GPTSamplingConfig(
226+
num_samples=num_samples,
227+
sequence_length=self._max_sequence_length,
228+
seed=self._distributed_config.seed,
229+
cache_directory=(
230+
self._dataset_prefixes[name].parent if self._cache_directory is None else self._cache_directory
231+
),
232+
verbose=self._num_datasets <= 5,
218233
),
219-
verbose=self._num_datasets <= 5,
234+
self,
220235
)
221236
return sampled_datasets
222237

223238
def _build_and_sample_dummy_dataset(self, name: str, dataset_samples_per_phase: dict[PhaseType, int]):
224239
return {
225240
phase: DummyGPTDataset(
226-
self._dataset_prefixes[name],
227241
dataset_samples_per_phase[phase],
228242
self._max_sequence_length,
229243
self._vocab_size,

fast_llm/data/gpt/dataset.py

Lines changed: 17 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import abc
2-
import math
2+
import typing
33

44
import numpy as np
5-
import numpy.random
65

7-
from fast_llm.data.config import Dataset
8-
from fast_llm.utils import Assert
6+
from fast_llm.config import Field, config_class
7+
from fast_llm.data.config import SamplableDataset, SamplingConfig
8+
9+
if typing.TYPE_CHECKING:
10+
from fast_llm.data.gpt.data import GPTData
11+
912

1013
try:
1114
from fast_llm.csrc.data import build_sample_idx # noqa
@@ -15,7 +18,12 @@
1518
_extension_available = False
1619

1720

18-
class GPTIndexedDataset(Dataset):
21+
@config_class
22+
class GPTSamplingConfig(SamplingConfig):
23+
sequence_length: int = Field(default=None, desc="Number of token in each sample.")
24+
25+
26+
class GPTIndexedDataset(SamplableDataset):
1927
"""
2028
A GPT dataset containing a list of unsampled, unprocessed samples.
2129
TODO: Move sampling responsibility here?
@@ -48,52 +56,7 @@ def get_document_sizes(self) -> "np.ndarray":
4856
and derived classes should try to avoid holding the whole array im memory.
4957
"""
5058

51-
def sample(self, num_samples: int, sequence_length: int, np_rng: numpy.random.RandomState, verbose: bool):
52-
"""
53-
Create a `GPTSampledDataset` with the requested parameters.
54-
"""
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)
60-
# For the last epoch, decide whether include the entire epoch
61-
# in the global shuffle or not.
62-
# Get the number of samples for the last epoch
63-
main_epochs_samples = ((num_epochs - 1) * num_tokens - 1) // sequence_length
64-
last_epoch_samples = num_samples - main_epochs_samples
65-
samples_per_epoch = (num_tokens - 1) // sequence_length
66-
# If we have less than 80% of the samples for the last epoch, separate out the epoch and treat it differently.
67-
# Note: the 80% number is just based on common sense and can be adjusted if needed.
68-
separate_last_epoch = num_epochs > 1 and last_epoch_samples < 0.8 * samples_per_epoch
69-
70-
doc_idx = np.tile(np.arange(num_documents, dtype=np.int32), num_epochs)
71-
if separate_last_epoch:
72-
np_rng.shuffle(doc_idx[:-num_documents])
73-
np_rng.shuffle(doc_idx[-num_documents:])
74-
else:
75-
np_rng.shuffle(doc_idx)
76-
77-
assert _extension_available, (
78-
"The C++ extension for dataset sampling is missing." " Please make sure Fast-LLM is installed correctly."
79-
)
80-
81-
sample_idx = build_sample_idx(document_sizes, doc_idx, sequence_length, num_epochs, num_tokens, verbose)
82-
83-
# shuffle-idx.
84-
# -1 is due to data structure used to retrieve the index:
85-
# sample i --> [sample_idx[i], sample_idx[i+1])
86-
total_size = sample_idx.shape[0] - 1
87-
# TODO: Isn't the dataset already shuffled above?
88-
shuffle_idx = np.arange(
89-
0, total_size, dtype=np.int64 if total_size >= (np.iinfo(np.uint32).max - 1) else np.uint32
90-
)
91-
if separate_last_epoch:
92-
np_rng.shuffle(shuffle_idx[:main_epochs_samples])
93-
np_rng.shuffle(shuffle_idx[main_epochs_samples:])
94-
else:
95-
np_rng.shuffle(shuffle_idx)
96-
97-
Assert.geq(len(shuffle_idx), num_samples)
98-
# TODO: The doc and sample idx are way bigger than needed when sampling for << 1 epoch.
99-
return doc_idx, sample_idx, shuffle_idx[:num_samples]
59+
def sample(self, config: GPTSamplingConfig, data: "GPTData"):
60+
from fast_llm.data.gpt.sampled import GPTSampledIndexedDataset
61+
62+
return GPTSampledIndexedDataset(self, config, data)

fast_llm/data/gpt/dummy.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import pathlib
2-
31
import numpy as np
42

53
from fast_llm.data.config import SampledDataset
6-
from fast_llm.engine.config_utils.run import log_main_rank
7-
from fast_llm.utils import Assert
84

95

106
class DummyGPTDataset(SampledDataset):
@@ -13,19 +9,9 @@ class DummyGPTDataset(SampledDataset):
139
The sample can be purely random, or read from a file to allow reproducing in other runs.
1410
"""
1511

16-
def __init__(
17-
self, prefix: pathlib.Path | None, num_samples: int, sequence_length: int, vocab_size: int, name: str = "dummy"
18-
):
12+
def __init__(self, num_samples: int, sequence_length: int, vocab_size: int, name: str = "dummy"):
1913
self._num_samples = num_samples
20-
if prefix is None:
21-
self._dummy_sample = np.random.randint(0, vocab_size, size=(sequence_length + 1,), dtype=np.int64)
22-
else:
23-
log_main_rank(f"> Loading dummy dataset from file {prefix}")
24-
self._dummy_sample = np.load(prefix, allow_pickle=True)[: sequence_length + 1]
25-
Assert.eq(self._dummy_sample.shape, (sequence_length + 1,))
26-
Assert.eq(self._dummy_sample.dtype, np.int64)
27-
Assert.lt(self._dummy_sample.max(), vocab_size)
28-
Assert.geq(self._dummy_sample.min(), 0)
14+
self._dummy_sample = np.random.randint(0, vocab_size, size=(sequence_length + 1,), dtype=np.int64)
2915
self._name = name
3016

3117
def __len__(self):

0 commit comments

Comments
 (0)