Skip to main content

flodl/data/
sampler.rs

1//! Sampling strategies for dataset index ordering.
2//!
3//! The [`Sampler`] trait controls how dataset indices are visited each epoch.
4//! Built-in implementations cover the common cases:
5//!
6//! - [`RandomSampler`] -- deterministic shuffle per epoch (default)
7//! - [`SequentialSampler`] -- in-order, same every epoch (for eval/inference)
8//!
9//! Custom samplers (weighted, stratified, curriculum learning) implement
10//! the [`Sampler`] trait directly.
11
12/// Controls the order in which dataset indices are visited each epoch.
13///
14/// # Implementing a custom sampler
15///
16/// ```ignore
17/// struct CurriculumSampler {
18///     n: usize,
19///     difficulty: Vec<f64>,
20/// }
21///
22/// impl Sampler for CurriculumSampler {
23///     fn len(&self) -> usize { self.n }
24///     fn indices(&mut self, epoch: usize) -> Vec<usize> {
25///         // Early epochs: easy samples first
26///         // Later epochs: full shuffle
27///         let mut idx: Vec<usize> = (0..self.n).collect();
28///         if epoch < 10 {
29///             idx.sort_by(|a, b| self.difficulty[*a].partial_cmp(&self.difficulty[*b]).unwrap());
30///         } else {
31///             let mut rng = Rng::seed(42 + epoch as u64);
32///             rng.shuffle(&mut idx);
33///         }
34///         idx
35///     }
36/// }
37/// ```
38pub trait Sampler: Send {
39    /// Total number of samples. Must match the dataset length.
40    fn len(&self) -> usize;
41
42    /// Whether the sampler is empty.
43    fn is_empty(&self) -> bool {
44        self.len() == 0
45    }
46
47    /// Generate the index ordering for a given epoch.
48    ///
49    /// Must return indices in `[0, len())`, as many as
50    /// [`epoch_len()`](Sampler::epoch_len) reports. Called once per epoch.
51    fn indices(&mut self, epoch: usize) -> Vec<usize>;
52
53    /// How many indices one epoch visits.
54    ///
55    /// Defaults to [`len()`](Sampler::len) — an epoch is a full pass over
56    /// the data, which is what every sampler did before epoch splitting
57    /// existed. [`SplitSampler`] overrides it, since there an epoch is a
58    /// slice of a pass; the count is nominal in that case, as balanced
59    /// slicing gives some epochs one extra index.
60    ///
61    /// Distinct from `len()` on purpose: `len()` describes the *dataset*
62    /// (what [`DataLoader::len`](super::DataLoader::len) reports) while
63    /// this describes an *epoch* (what
64    /// [`DataLoader::num_batches`](super::DataLoader::num_batches)
65    /// counts). They coincide unless the sampler splits.
66    fn epoch_len(&self) -> usize {
67        self.len()
68    }
69}
70
71/// Deterministic random sampler. Default for [`DataLoader`](super::DataLoader).
72///
73/// Uses a per-epoch seed derived from `base_seed + epoch` to produce a
74/// fresh permutation each epoch while remaining reproducible across runs.
75pub struct RandomSampler {
76    n: usize,
77    seed: u64,
78}
79
80impl RandomSampler {
81    /// Create a random sampler for `n` samples with the given base seed.
82    pub fn new(n: usize, seed: u64) -> Self {
83        RandomSampler { n, seed }
84    }
85}
86
87impl Sampler for RandomSampler {
88    fn len(&self) -> usize {
89        self.n
90    }
91
92    fn indices(&mut self, epoch: usize) -> Vec<usize> {
93        crate::rng::epoch_permutation(self.seed, epoch, self.n)
94    }
95}
96
97/// Like [`RandomSampler`], but an epoch is a *slice* of a data pass.
98///
99/// `splits` says how finely to cut one pass. The pass permutation is
100/// unchanged and still covers every sample exactly once; splitting only
101/// decides how much of it one epoch consumes, so `splits` epochs make one
102/// pass and no sample is seen twice along the way.
103///
104/// This is what makes single-pass training (the normal regime for LLM
105/// pretraining) workable: everything that keys off the epoch boundary —
106/// eval, checkpointing, reporting — gets a boundary to key off, where a
107/// naive one-epoch run has none until teardown.
108///
109/// ```ignore
110/// use flodl::SplitSampler;
111///
112/// // One pass over 10k samples, delivered as 20 epochs of 500.
113/// let sampler = SplitSampler::new(10_000, 42, 20);
114/// ```
115///
116/// At `splits = 1` it behaves exactly like [`RandomSampler`].
117pub struct SplitSampler {
118    n: usize,
119    seed: u64,
120    splits: usize,
121}
122
123impl SplitSampler {
124    /// Create a split sampler for `n` samples with the given base seed.
125    ///
126    /// `splits` is clamped to at least 1.
127    pub fn new(n: usize, seed: u64, splits: usize) -> Self {
128        SplitSampler {
129            n,
130            seed,
131            splits: splits.max(1),
132        }
133    }
134
135    /// Slices per data pass.
136    pub fn splits(&self) -> usize {
137        self.splits
138    }
139}
140
141impl Sampler for SplitSampler {
142    fn len(&self) -> usize {
143        self.n
144    }
145
146    fn epoch_len(&self) -> usize {
147        // The base slice. Balanced splitting hands the first `n % splits`
148        // epochs one extra index, so this is the nominal size — callers
149        // that need the exact count read `indices(epoch).len()`.
150        self.n / self.splits
151    }
152
153    fn indices(&mut self, epoch: usize) -> Vec<usize> {
154        crate::rng::epoch_split_permutation(self.seed, epoch, self.splits, self.n)
155    }
156}
157
158/// Sequential sampler: indices in order, same every epoch.
159///
160/// Use for evaluation or inference where order matters or
161/// shuffling is undesirable.
162pub struct SequentialSampler {
163    n: usize,
164}
165
166impl SequentialSampler {
167    /// Create a sequential sampler for `n` samples.
168    pub fn new(n: usize) -> Self {
169        SequentialSampler { n }
170    }
171}
172
173impl Sampler for SequentialSampler {
174    fn len(&self) -> usize {
175        self.n
176    }
177
178    fn indices(&mut self, _epoch: usize) -> Vec<usize> {
179        (0..self.n).collect()
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Tests
185// ---------------------------------------------------------------------------
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_random_sampler_permutation() {
193        let mut sampler = RandomSampler::new(10, 42);
194        let idx = sampler.indices(0);
195        assert_eq!(idx.len(), 10);
196        // Must contain all indices exactly once
197        let mut sorted = idx.clone();
198        sorted.sort();
199        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
200    }
201
202    #[test]
203    fn test_random_sampler_different_epochs() {
204        let mut sampler = RandomSampler::new(100, 42);
205        let epoch0 = sampler.indices(0);
206        let epoch1 = sampler.indices(1);
207        // Different epochs should produce different orderings
208        assert_ne!(epoch0, epoch1);
209    }
210
211    #[test]
212    fn test_random_sampler_reproducible() {
213        let mut s1 = RandomSampler::new(100, 42);
214        let mut s2 = RandomSampler::new(100, 42);
215        // Same seed + same epoch = same permutation
216        assert_eq!(s1.indices(5), s2.indices(5));
217    }
218
219    #[test]
220    fn test_random_sampler_different_seeds() {
221        let mut s1 = RandomSampler::new(100, 42);
222        let mut s2 = RandomSampler::new(100, 99);
223        // Different seeds = different permutation
224        assert_ne!(s1.indices(0), s2.indices(0));
225    }
226
227    #[test]
228    fn test_sequential_sampler() {
229        let mut sampler = SequentialSampler::new(5);
230        assert_eq!(sampler.indices(0), vec![0, 1, 2, 3, 4]);
231        assert_eq!(sampler.indices(10), vec![0, 1, 2, 3, 4]);
232    }
233
234    #[test]
235    fn test_sequential_sampler_stable() {
236        let mut sampler = SequentialSampler::new(20);
237        let a = sampler.indices(0);
238        let b = sampler.indices(1);
239        assert_eq!(a, b);
240    }
241
242    #[test]
243    fn split_sampler_epochs_tile_one_pass() {
244        // Four epochs consume one pass between them, in pass order and
245        // with no sample served twice.
246        let mut split = SplitSampler::new(100, 42, 4);
247        let mut seen = Vec::new();
248        for epoch in 0..4 {
249            seen.extend(split.indices(epoch));
250        }
251        assert_eq!(seen, RandomSampler::new(100, 42).indices(0));
252    }
253
254    #[test]
255    fn split_sampler_at_one_split_matches_random_sampler() {
256        let mut split = SplitSampler::new(100, 42, 1);
257        let mut random = RandomSampler::new(100, 42);
258        for epoch in 0..3 {
259            assert_eq!(split.indices(epoch), random.indices(epoch), "epoch {epoch}");
260        }
261    }
262
263    #[test]
264    fn split_sampler_reports_dataset_len_and_epoch_len_apart() {
265        let split = SplitSampler::new(100, 42, 4);
266        // The dataset is still 100 samples; an epoch is 25 of them.
267        assert_eq!(split.len(), 100);
268        assert_eq!(split.epoch_len(), 25);
269        assert_eq!(split.splits(), 4);
270    }
271
272    #[test]
273    fn unsplit_samplers_report_one_epoch_per_pass() {
274        // The defaulted trait method: sampler types that predate
275        // splitting must keep reporting the whole pass.
276        assert_eq!(RandomSampler::new(50, 0).epoch_len(), 50);
277        assert_eq!(SequentialSampler::new(30).epoch_len(), 30);
278    }
279
280    #[test]
281    fn split_sampler_clamps_zero_splits() {
282        let mut sampler = SplitSampler::new(10, 1, 0);
283        assert_eq!(sampler.splits(), 1);
284        assert_eq!(sampler.indices(0).len(), 10);
285    }
286
287    #[test]
288    fn test_sampler_len() {
289        let s1 = RandomSampler::new(50, 0);
290        assert_eq!(s1.len(), 50);
291        let s2 = SequentialSampler::new(30);
292        assert_eq!(s2.len(), 30);
293    }
294}