ReUseX  0.0.5
3D Point Cloud Processing for Building Reuse
Loading...
Searching...
No Matches
Dataloader.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Povl Filip Sonne-Frederiksen
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4#pragma once
5#include "reusex/vision/IData.hpp"
6#include "reusex/vision/IDataset.hpp"
7
8#include <atomic>
9#include <condition_variable>
10#include <cstdint>
11#include <map>
12#include <memory>
13#include <mutex>
14#include <optional>
15#include <span>
16#include <thread>
17#include <vector>
18
19namespace reusex::vision {
20
21/* * Dataloader is a class that provides an iterable interface to a dataset.
22 * It loads batches of data from the dataset in a separate thread and provides
23 * them to the user when requested. It supports shuffling, multiple worker
24 * threads, and prefetching batches. The user can iterate over the dataloader
25 * using a range-based for loop or by manually creating an iterator. The
26 * dataloader will automatically stop the worker threads when the iteration is
27 * complete or when the dataloader is destroyed.
28 * */
30 public:
32 using Batch = std::vector<Pair>;
33 using BatchView = std::span<Pair>;
34
35 /* * The default seed used for deterministic shuffling when the caller does
36 * not request entropy. Per docs/STANDARDS.md §6, algorithms with randomness
37 * default to a fixed seed (42) so runs are reproducible; pass
38 * std::nullopt to opt into non-determinism via std::random_device.
39 * */
40 static constexpr uint32_t default_seed = 42;
41
42 /* * Constructs a Dataloader for the given dataset with the specified batch
43 * size, shuffle option, number of worker threads, and number of prefetch
44 * batches.
45 * @param dataset The dataset to load data from.
46 * @param batch_size The number of samples in each batch.
47 * @param shuffle Whether to shuffle the dataset at the beginning of each
48 * epoch.
49 * @param num_workers The number of worker threads to use for loading batches.
50 * @param prefetch_batches The number of batches to prefetch in the
51 * background.
52 * @param seed Seed for the shuffle RNG. Defaults to a fixed value
53 * (default_seed) so shuffling is deterministic and runs are reproducible.
54 * Pass std::nullopt to seed from std::random_device (non-deterministic).
55 * Ignored when shuffle is false.
56 * */
57 Dataloader(IDataset &dataset, size_t batch_size, bool shuffle = false,
58 size_t num_workers = 4, size_t prefetch_batches = 2,
59 std::optional<uint32_t> seed = default_seed);
60
62
63 /* * Produces the (optionally shuffled) index order for a dataset of the given
64 * size. When seed holds a value, the shuffle is deterministic and repeatable;
65 * when seed is std::nullopt, entropy is drawn from std::random_device. This
66 * is the single source of truth for shuffle ordering, exposed as a static
67 * helper so the ordering can be unit-tested without a live dataset.
68 * @param count The number of indices to produce (0..count-1).
69 * @param seed Optional RNG seed; std::nullopt draws entropy from the device.
70 * @return A vector of size count containing a permutation of [0, count).
71 * */
72 static std::vector<size_t> shuffled_indices(size_t count,
73 std::optional<uint32_t> seed);
74
75 /* * Iterator is a class that provides an input iterator interface to the
76 * Dataloader. It allows the user to iterate over the batches of data in the
77 * dataloader using a range-based for loop or by manually creating an
78 * iterator. The iterator will automatically load batches from the dataloader
79 * as needed and will stop when the iteration is complete.
80 * */
81 class Iterator {
82 public:
83 using iterator_category = std::input_iterator_tag;
85 using difference_type = std::ptrdiff_t;
86 using pointer = Batch *;
87 using reference = Batch &;
88
89 /* * Constructs an iterator for the given dataloader and batch index.
90 * @param loader The dataloader to iterate over.
91 * @param batch_idx The index of the batch to start iterating from.
92 * */
93 Iterator(Dataloader *loader, size_t batch_idx);
94
95 /* * Copy constructor and copy assignment operator for the iterator. The
96 * current batch is not copied and will be reloaded when the iterator is
97 * dereferenced.
98 * @param other The iterator to copy from.
99 * @return A reference to the copied iterator.
100 * */
101 Iterator(const Iterator &other)
102 : loader_(other.loader_), batch_idx_(other.batch_idx_),
103 current_batch_(std::nullopt) {}
104
105 /* * Copy assignment operator for the iterator. The current batch is not
106 * copied and will be reloaded when the iterator is dereferenced.
107 * @param other The iterator to copy from.
108 * @return A reference to the copied iterator.
109 * */
110 Iterator &operator=(const Iterator &other) {
111 if (this != &other) {
112 loader_ = other.loader_;
113 batch_idx_ = other.batch_idx_;
114 current_batch_ = std::nullopt;
115 }
116 return *this;
117 }
118
119 /* * Move constructor and move assignment operator for the iterator. The
120 * current batch is not moved and will be reloaded when the iterator is
121 * dereferenced.
122 * @param other The iterator to move from.
123 * @return A reference to the moved iterator.
124 * */
125 Iterator(Iterator &&) = default;
126
127 /* * Move assignment operator for the iterator. The current batch is not
128 * moved and will be reloaded when the iterator is dereferenced.
129 * @param other The iterator to move from.
130 * @return A reference to the moved iterator.
131 * */
133
134 /* * Dereference operator for the iterator. It returns a view of the current
135 * batch of data. If the current batch is not loaded, it will be loaded
136 * from the dataloader.
137 * @return A view of the current batch of data.
138 * @throws std::runtime_error if no batch is available for this index
139 * because the epoch has already finished — i.e. the loader was stopped
140 * (reconfigured or destroyed) while this iterator was live. Dereferencing
141 * an iterator of a stopped epoch is a caller error, and is reported rather
142 * than silently reading an empty optional (#280).
143 * */
145
146 /* * Moves the current batch out of the iterator. If the current batch is
147 * not loaded, it will be loaded from the dataloader first.
148 * @return An rvalue reference to the current batch.
149 * @throws std::runtime_error under the same stopped-epoch condition as
150 * operator*.
151 * */
153
154 /* * Pre-increment operator for the iterator. It advances the iterator to
155 * the next batch of data. If the next batch is not loaded, it will be
156 * loaded from the dataloader.
157 * @return A reference to the advanced iterator.
158 * */
160
161 /* * Post-increment operator for the iterator. It is deleted to prevent
162 * inefficient copying of batches. Use the pre-increment operator instead.
163 * */
164 Iterator operator++(int) = delete;
165
166 /* * Equality operator for the iterator. It checks if two iterators are
167 * equal by comparing their dataloader pointers and batch indices.
168 * @param other The iterator to compare with.
169 * @return True if the iterators are equal, false otherwise.
170 * */
171 bool operator==(const Iterator &other) const;
172
173 /* * Inequality operator for the iterator. It checks if two iterators are
174 * not equal by comparing their dataloader pointers and batch indices.
175 * @param other The iterator to compare with.
176 * @return True if the iterators are not equal, false otherwise.
177 * */
178 bool operator!=(const Iterator &other) const;
179
180 private:
181 /* * The dataloader that this iterator belongs to. It is used to load
182 * batches of data when the iterator is dereferenced or advanced.
183 * */
184 Dataloader *loader_;
185 /* * The index of the current batch that this iterator points to. It is used
186 * to determine which batch to load from the dataloader when the iterator is
187 * dereferenced or advanced.
188 * */
189 size_t batch_idx_;
190 /* * The current batch of data that this iterator points to. It is stored as
191 * an optional value because it may not be loaded yet. When the iterator is
192 * dereferenced or advanced, the current batch will be loaded from the
193 * dataloader if it is not already loaded.
194 * */
195 mutable std::optional<Batch> current_batch_;
196 };
197
198 /* * Returns an iterator to the beginning of the dataloader. The iterator will
199 * point to the first batch of data in the dataloader.
200 * @return An iterator to the beginning of the dataloader.
201 * */
203
204 /* * Returns an iterator to the end of the dataloader. The iterator will point
205 * to one past the last batch of data in the dataloader.
206 * @return An iterator to the end of the dataloader.
207 * */
209
210 /* * Returns the total number of batches in the dataloader. This is calculated
211 * based on the size of the dataset and the batch size.
212 * @return The total number of batches in the dataloader.
213 * */
214 size_t size() const;
215
216 /* * Sets the number of worker threads to use for loading batches. This will
217 * affect the performance of the dataloader, as more worker threads can load
218 * batches in parallel, but may also increase the overhead of thread
219 * management. The default number of worker threads is 4.
220 * @param num_workers The number of worker threads to use for loading batches.
221 * */
222 void set_num_workers(size_t num_workers);
223
224 /* * Sets the number of batches to prefetch in the background. This will
225 * affect the performance of the dataloader, as more prefetch batches can
226 * reduce the waiting time for batches to be loaded, but may also increase the
227 * memory usage of the dataloader. The default number of prefetch batches is
228 * 2.
229 * @param prefetch_batches The number of batches to prefetch in the
230 * background.
231 * */
232 void set_prefetch_batches(size_t prefetch_batches);
233
234 /* * Returns the number of worker threads currently used for loading batches.
235 * @return The number of worker threads currently used for loading batches.
236 * */
237 size_t get_num_workers() const;
238
239 /* * Returns the number of batches currently prefetched in the background.
240 * @return The number of batches currently prefetched in the background.
241 * */
242 size_t get_prefetch_batches() const;
243
244 private:
245 /* * Starts the worker threads for loading batches. This function is called at
246 * the beginning of each epoch to initialize the worker threads and start
247 * loading batches from the dataset. The worker threads will continue to load
248 * batches in the background until the epoch is finished or the dataloader is
249 * destroyed.
250 * */
251 void start_epoch();
252
253 /* * Stops the worker threads for loading batches. This function is called at
254 * the end of each epoch or when the dataloader is destroyed to signal the
255 * worker threads to stop loading batches and exit. The worker threads will
256 * check for this signal and exit gracefully when it is set.
257 * */
258 void stop();
259
260 /* * The function that each worker thread runs to load batches of data from
261 * the dataset. Each worker thread will continuously load batches of data from
262 * the dataset and add them to the batch queue until the epoch is finished or
263 * the dataloader is destroyed. The worker threads will synchronize access to
264 * the batch queue using mutexes and condition variables to ensure thread
265 * safety.
266 * */
267 void worker_thread();
268
269 /* * Loads a batch of data from the dataset for the given batch index. This
270 * function is called by the worker threads to load batches of data from the
271 * dataset. It will calculate the indices of the samples in the batch based on
272 * the batch index and batch size, and then load the corresponding samples
273 * from the dataset. The loaded batch will be returned as a vector of pairs.
274 * @param batch_idx The index of the batch to load.
275 * @return A vector of pairs representing the loaded batch of data.
276 * */
277 Batch load_batch(size_t batch_idx);
278
279 /* * Retrieves a batch of data from the batch queue for the given batch index.
280 * This function is called by the iterator to retrieve batches of data from
281 * the dataloader. It will check if the requested batch is already loaded in
282 * the batch queue, and if so, it will return it. If the requested batch is
283 * not loaded yet, it will wait for the worker threads to load it and add it
284 * to the batch queue. The function will return an optional value, which will
285 * be empty if the epoch is finished or if the dataloader is destroyed.
286 * @param batch_idx The index of the batch to retrieve.
287 * @return An optional value containing the retrieved batch of data, or empty
288 * if the epoch is finished or if the dataloader is destroyed.
289 * */
290 std::optional<Batch> get_batch(size_t batch_idx);
291
292 /* * The dataset that this dataloader loads data from. It is a reference to an
293 * IDataset object, which provides the interface for accessing the samples in
294 * the dataset. The dataloader will use this dataset to load batches of data
295 * in the worker threads.
296 * */
297 IDataset &dataset_;
298
299 /* * The batch size that this dataloader uses to load batches of data. It
300 * determines how many samples are included in each batch that the dataloader
301 * loads from the dataset. The batch size is set at the construction of the
302 * dataloader and cannot be changed afterwards.
303 * */
304 size_t batch_size_;
305
306 /* * Whether to shuffle the dataset at the beginning of each epoch. If true,
307 * the dataloader will shuffle the indices of the samples in the dataset at
308 * the beginning of each epoch, which will result in different batches being
309 * loaded in each epoch. If false, the dataloader will load batches in a fixed
310 * order based on the original order of the samples in the dataset.
311 * */
312 bool shuffle_;
313
314 /* * The seed used for the shuffle RNG. When it holds a value the shuffle is
315 * deterministic (repeatable across epochs and runs); when it is std::nullopt
316 * the RNG is seeded from std::random_device for non-deterministic ordering.
317 * Only consulted when shuffle_ is true.
318 * */
319 std::optional<uint32_t> seed_;
320
321 /* * The number of worker threads to use for loading batches. This determines
322 * how many threads will be running in the background to load batches of data
323 * from the dataset. More worker threads can load batches in parallel, but may
324 * also increase the overhead of thread management. The default number of
325 * worker threads is 4.
326 * */
327 size_t num_workers_;
328
329 /* * The number of batches to prefetch in the background. This determines how
330 * many batches will be loaded in advance by the worker threads while the user
331 * is consuming the batches. More prefetch batches can reduce the waiting time
332 * for batches to be loaded, but may also increase the memory usage of the
333 * dataloader. The default number of prefetch batches is 2.
334 * */
335 size_t prefetch_batches_;
336
337 /* * The total number of samples in the dataset. This is obtained from the
338 * dataset object and is used to calculate the total number of batches in the
339 * dataloader.
340 * */
341 size_t dataset_size_;
342
343 /* * The total number of batches in the dataloader. This is calculated based
344 * on the size of the dataset and the batch size. It is used to determine how
345 * many batches are available for iteration in the dataloader.
346 * */
347 size_t num_batches_;
348
349 /* * The indices of the samples in the dataset. This is a vector of size equal
350 * to the number of samples in the dataset, containing the indices of the
351 * samples in the original order. If shuffle is true, this vector will be
352 * shuffled at the beginning of each epoch to provide different batches in
353 * each epoch.
354 * */
355 std::vector<size_t> indices_;
356
357 /* * The worker threads that are responsible for loading batches of data from
358 * the dataset. This is a vector of threads that are created at the beginning
359 * of each epoch and run the worker_thread function to load batches in the
360 * background. The worker threads will continue to run until the epoch is
361 * finished or the dataloader is destroyed.
362 * */
363 std::vector<std::thread> workers_;
364
365 /* * The batch queue that holds the batches of data that have been loaded by
366 * the worker threads. This is a queue of pairs, where each pair consists of a
367 * batch index and the corresponding batch of data. The worker threads will
368 * add batches to this queue as they load them, and the iterator will retrieve
369 * batches from this queue when requested. The batch queue is synchronized
370 * using mutexes and condition variables to ensure thread safety.
371 * */
372 std::map<size_t, Batch> batch_queue_;
373
374 /* * Mutex and condition variables for synchronizing access to the batch queue
375 * and signaling the worker threads. The queue_mutex is used to protect access
376 * to the batch_queue, while the queue_cv is used to signal the worker threads
377 * when a new batch is added to the queue, and the ready_cv is used to signal
378 * the iterator when a batch is ready to be retrieved from the queue.
379 * */
380 std::mutex queue_mutex_;
381
382 /* * The condition variable used to signal the worker threads when a new batch
383 * is added to the batch queue. The worker threads will wait on this condition
384 * variable when they are idle and will be notified when a new batch is added
385 * to the queue, allowing them to wake up and continue loading batches.
386 * */
387 std::condition_variable queue_cv_;
388
389 /* * The condition variable used to signal the iterator when a batch is ready
390 * to be retrieved from the batch queue. The iterator will wait on this
391 * condition variable when it is waiting for a batch to be loaded, and will be
392 * notified when a new batch is added to the queue, allowing it to wake up and
393 * retrieve the batch.
394 * */
395 std::condition_variable ready_cv_;
396
397 /* * Atomic flags for controlling the worker threads and the epoch state. The
398 * stop_workers_ flag is used to signal the worker threads to stop loading
399 * batches and exit when the epoch is finished or the dataloader is destroyed.
400 * The epoch_finished_ flag is used to indicate whether the current epoch is
401 * finished, which can be checked by the worker threads to determine when to
402 * stop loading batches.
403 * */
404 std::atomic<bool> stop_workers_;
405
406 /* * The epoch_finished_ flag is used to indicate whether the current epoch is
407 * finished. It is set to true when the iteration over the dataloader is
408 * complete or when the dataloader is destroyed, and it is checked by the
409 * worker threads to determine when to stop loading batches. When this flag is
410 * set to true, the worker threads will stop loading batches and exit
411 * gracefully.
412 * */
413 std::atomic<bool> epoch_finished_;
414
415 /* * The current batch index that the iterator is pointing to. This is used by
416 * the iterator to determine which batch to load from the dataloader when it
417 * is dereferenced or advanced. The current batch index is updated by the
418 * iterator as it advances through the batches, and it is used to retrieve the
419 * correct batch from the dataloader when requested.
420 * */
421 size_t current_batch_idx_ = 0;
422};
423
424} // namespace reusex::vision
Iterator & operator=(const Iterator &other)
Iterator & operator=(Iterator &&)=default
bool operator!=(const Iterator &other) const
std::input_iterator_tag iterator_category
bool operator==(const Iterator &other) const
Iterator(Dataloader *loader, size_t batch_idx)
void set_prefetch_batches(size_t prefetch_batches)
static constexpr uint32_t default_seed
size_t get_num_workers() const
std::vector< Pair > Batch
std::span< Pair > BatchView
size_t get_prefetch_batches() const
static std::vector< size_t > shuffled_indices(size_t count, std::optional< uint32_t > seed)
Dataloader(IDataset &dataset, size_t batch_size, bool shuffle=false, size_t num_workers=4, size_t prefetch_batches=2, std::optional< uint32_t > seed=default_seed)
void set_num_workers(size_t num_workers)
std::pair< std::unique_ptr< IData >, size_t > Pair
Definition IDataset.hpp:44