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}