Skip to main content

diskann_providers/utils/
rayon_util.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5use diskann::{ANNError, ANNResult};
6use rayon::prelude::ParallelIterator;
7
8/// Creates a new thread pool with the specified number of threads.
9/// If `num_threads` is 0, it defaults to the number of logical CPUs.
10pub fn create_thread_pool(num_threads: usize) -> ANNResult<RayonThreadPool> {
11    let pool = rayon::ThreadPoolBuilder::new()
12        .num_threads(num_threads)
13        .build()
14        .map_err(|err| ANNError::log_thread_pool_error(err.to_string()))?;
15    Ok(RayonThreadPool(pool))
16}
17
18/// Creates a thread pool with a configurable number of threads for testing purposes.
19/// The number of threads can be set using the environment variable `DISKANN_TEST_POOL_THREADS`.
20/// If the environment variable is not set or cannot be parsed, it defaults to 3 threads.
21#[allow(clippy::unwrap_used)]
22pub fn create_thread_pool_for_test() -> RayonThreadPool {
23    use std::env;
24
25    let num_threads = env::var("DISKANN_TEST_POOL_THREADS")
26        .ok()
27        .and_then(|val| val.parse().ok())
28        .unwrap_or(3);
29
30    create_thread_pool(num_threads).unwrap()
31}
32/// Creates a thread pool for benchmarking purposes without specifying the number of threads.
33/// The Rayon runtime will automatically determine the optimal number of threads to use.
34/// It uses the `RAYON_NUM_THREADS` environment variable if set,
35/// or defaults to the number of logical CPUs otherwise
36#[allow(clippy::unwrap_used)]
37pub fn create_thread_pool_for_bench() -> RayonThreadPool {
38    let pool = rayon::ThreadPoolBuilder::new()
39        .build()
40        .map_err(|err| ANNError::log_thread_pool_error(err.to_string()))
41        .unwrap();
42    RayonThreadPool(pool)
43}
44
45pub struct RayonThreadPool(rayon::ThreadPool);
46
47impl RayonThreadPool {
48    pub fn install<OP, R>(&self, op: OP) -> R
49    where
50        OP: FnOnce() -> R + Send,
51        R: Send,
52    {
53        self.0.install(op)
54    }
55
56    pub fn as_ref(&self) -> RayonThreadPoolRef<'_> {
57        RayonThreadPoolRef(&self.0)
58    }
59}
60
61#[derive(Clone, Copy)]
62pub struct RayonThreadPoolRef<'a>(&'a rayon::ThreadPool);
63
64impl<'a> RayonThreadPoolRef<'a> {
65    /// Wrap an externally-owned `rayon::ThreadPool`.
66    pub fn new(pool: &'a rayon::ThreadPool) -> Self {
67        Self(pool)
68    }
69
70    pub fn install<OP, R>(self, op: OP) -> R
71    where
72        OP: FnOnce() -> R + Send,
73        R: Send,
74    {
75        self.0.install(op)
76    }
77}
78
79// Allow use of disallowed methods within this trait to provide custom
80// implementations of common parallel operations that enforce execution
81// within a specified thread pool.
82#[allow(clippy::disallowed_methods)]
83pub trait ParallelIteratorInPool: ParallelIterator + Sized {
84    fn for_each_in_pool<OP>(self, pool: RayonThreadPoolRef<'_>, op: OP)
85    where
86        OP: Fn(Self::Item) + Sync + Send,
87    {
88        pool.install(|| self.for_each(op));
89    }
90
91    fn for_each_with_in_pool<OP, T>(self, pool: RayonThreadPoolRef<'_>, init: T, op: OP)
92    where
93        OP: Fn(&mut T, Self::Item) + Sync + Send,
94        T: Send + Clone,
95    {
96        pool.install(|| self.for_each_with(init, op))
97    }
98
99    fn for_each_init_in_pool<OP, INIT, T>(self, pool: RayonThreadPoolRef<'_>, init: INIT, op: OP)
100    where
101        OP: Fn(&mut T, Self::Item) + Sync + Send,
102        INIT: Fn() -> T + Sync + Send,
103    {
104        pool.install(|| self.for_each_init(init, op))
105    }
106
107    fn try_for_each_in_pool<OP, E>(self, pool: RayonThreadPoolRef<'_>, op: OP) -> Result<(), E>
108    where
109        OP: Fn(Self::Item) -> Result<(), E> + Sync + Send,
110        E: Send,
111    {
112        pool.install(|| self.try_for_each(op))
113    }
114
115    fn try_for_each_with_in_pool<OP, T, E>(
116        self,
117        pool: RayonThreadPoolRef<'_>,
118        init: T,
119        op: OP,
120    ) -> Result<(), E>
121    where
122        OP: Fn(&mut T, Self::Item) -> Result<(), E> + Sync + Send,
123        E: Send,
124        T: Send + Clone,
125    {
126        pool.install(|| self.try_for_each_with(init, op))
127    }
128
129    fn try_for_each_init_in_pool<OP, INIT, T, E>(
130        self,
131        pool: RayonThreadPoolRef<'_>,
132        init: INIT,
133        op: OP,
134    ) -> Result<(), E>
135    where
136        OP: Fn(&mut T, Self::Item) -> Result<(), E> + Sync + Send,
137        INIT: Fn() -> T + Sync + Send,
138        E: Send,
139    {
140        pool.install(|| self.try_for_each_init(init, op))
141    }
142
143    fn count_in_pool(self, pool: RayonThreadPoolRef<'_>) -> usize {
144        pool.install(|| self.count())
145    }
146
147    fn collect_in_pool<C>(self, pool: RayonThreadPoolRef<'_>) -> C
148    where
149        C: rayon::iter::FromParallelIterator<Self::Item> + Send,
150    {
151        pool.install(|| self.collect())
152    }
153
154    fn sum_in_pool<S>(self, pool: RayonThreadPoolRef<'_>) -> S
155    where
156        S: Send + std::iter::Sum<Self::Item> + std::iter::Sum<S>,
157    {
158        pool.install(|| self.sum())
159    }
160}
161
162// Implement the `ParallelIteratorInPool` trait for any type that implements `ParallelIterator`.
163impl<T> ParallelIteratorInPool for T where T: ParallelIterator {}
164
165#[cfg(test)]
166mod tests {
167    use std::sync::{Mutex, mpsc::channel};
168
169    use super::*;
170    use rayon::prelude::IntoParallelIterator;
171
172    fn get_num_cpus() -> usize {
173        std::thread::available_parallelism()
174            .map(|n| n.get())
175            .unwrap()
176    }
177
178    #[test]
179    fn test_create_thread_pool_for_test_default() {
180        // Ensure the environment variable is not set
181        //
182        // SAFETY: These environment variables are only set and removed using `std::env`
183        // functions (probably).
184        unsafe { std::env::remove_var("DISKANN_TEST_POOL_THREADS") };
185        let pool = create_thread_pool_for_test();
186        // Assuming RayonThreadPool has a method to get the number of threads
187        assert_eq!(pool.0.current_num_threads(), 3);
188    }
189
190    #[test]
191    fn test_create_thread_pool_for_test_from_env() {
192        // Set the environment variable to a specific value
193        //
194        // SAFETY: These environment variables are only set and removed using `std::env`
195        // functions (probably).
196        unsafe { std::env::set_var("DISKANN_TEST_POOL_THREADS", "5") };
197        let pool = create_thread_pool_for_test();
198        // Assuming RayonThreadPool has a method to get the number of threads
199        assert_eq!(pool.0.current_num_threads(), 5);
200
201        // Clean up the environment variable
202        //
203        // SAFETY: These environment variables are only set and removed using `std::env`
204        // functions (probably).
205        unsafe { std::env::remove_var("DISKANN_TEST_POOL_THREADS") };
206    }
207
208    #[test]
209    fn test_create_thread_pool_for_test_invalid_env() {
210        // Set the environment variable to an invalid value
211        //
212        // SAFETY: These environment variables are only set and removed using `std::env`
213        // functions (probably).
214        unsafe { std::env::set_var("DISKANN_TEST_POOL_THREADS", "invalid") };
215        let pool = create_thread_pool_for_test();
216        // Assuming RayonThreadPool has a method to get the number of threads
217        assert_eq!(pool.0.current_num_threads(), 3);
218
219        // Clean up the environment variable
220        //
221        // SAFETY: These environment variables are only set and removed using `std::env`
222        // functions (probably).
223        unsafe { std::env::remove_var("DISKANN_TEST_POOL_THREADS") };
224    }
225
226    #[test]
227    fn test_create_thread_pool_for_bench() {
228        let pool = create_thread_pool_for_bench();
229        assert_eq!(pool.0.current_num_threads(), get_num_cpus());
230    }
231
232    fn assert_run_in_rayon_thread() {
233        println!(
234            "Thread name: {:?}, Thread id: {:?}, Rayon thread index: {:?}, Rayon num_threads: {:?}",
235            std::thread::current().name(),
236            std::thread::current().id(),
237            rayon::current_thread_index(),
238            rayon::current_num_threads()
239        );
240        assert!(rayon::current_thread_index().is_some());
241    }
242
243    #[test]
244    fn test_bring_your_own_pool() {
245        let external_pool = rayon::ThreadPoolBuilder::new()
246            .num_threads(2)
247            .build()
248            .unwrap();
249        let pool_ref = RayonThreadPoolRef::new(&external_pool);
250
251        let res = Mutex::new(Vec::new());
252        (0..5).into_par_iter().for_each_in_pool(pool_ref, |x| {
253            let mut res = res.lock().unwrap();
254            res.push(x);
255            assert_run_in_rayon_thread();
256        });
257
258        let mut res = res.lock().unwrap();
259        res.sort();
260        assert_eq!(&res[..], &[0, 1, 2, 3, 4]);
261    }
262
263    #[test]
264    fn test_for_each_in_pool() {
265        let pool = create_thread_pool(4).unwrap();
266
267        let res = Mutex::new(Vec::new());
268        (0..5).into_par_iter().for_each_in_pool(pool.as_ref(), |x| {
269            let mut res = res.lock().unwrap();
270            res.push(x);
271            assert_run_in_rayon_thread();
272        });
273
274        let mut res = res.lock().unwrap();
275        res.sort();
276
277        assert_eq!(&res[..], &[0, 1, 2, 3, 4]);
278    }
279    #[test]
280    fn test_for_each_with_in_pool() {
281        let pool = create_thread_pool(4).unwrap();
282        let (sender, receiver) = channel();
283
284        (0..5)
285            .into_par_iter()
286            .for_each_with_in_pool(pool.as_ref(), sender, |s, x| s.send(x).unwrap());
287
288        let mut res: Vec<_> = receiver.iter().collect();
289
290        res.sort();
291
292        assert_eq!(&res[..], &[0, 1, 2, 3, 4]);
293    }
294
295    #[test]
296    fn test_for_each_init_in_pool() {
297        let pool = create_thread_pool(4).unwrap();
298        let iter = (0..100).into_par_iter();
299        iter.for_each_init_in_pool(
300            pool.as_ref(),
301            || 0,
302            |s, i| {
303                assert_run_in_rayon_thread();
304                *s += i;
305            },
306        );
307    }
308
309    #[test]
310    fn test_map_in_pool() {
311        let pool = create_thread_pool(4).unwrap();
312        let iter = (0..100).into_par_iter();
313        let mapped_iter = iter.map(|i| {
314            assert_run_in_rayon_thread();
315            i as f32
316        });
317        let list = mapped_iter.collect_in_pool::<Vec<f32>>(pool.as_ref());
318        assert!(list.len() == 100);
319    }
320
321    #[test]
322    fn test_try_for_each_in_pool() {
323        let pool = create_thread_pool(4).unwrap();
324        let iter = (0..100).into_par_iter();
325        let result = iter.try_for_each_in_pool(pool.as_ref(), |i| {
326            assert_run_in_rayon_thread();
327            if i < 50 { Ok(()) } else { Err("Error") }
328        });
329        assert!(result.is_err());
330    }
331
332    #[test]
333    fn test_try_for_each_init_in_pool() {
334        let pool = create_thread_pool(4).unwrap();
335        let iter = (0..100).into_par_iter();
336        let result = iter.try_for_each_init_in_pool(
337            pool.as_ref(),
338            || 0,
339            |_, i| {
340                assert_run_in_rayon_thread();
341                if i < 50 { Ok(()) } else { Err("Error") }
342            },
343        );
344        assert!(result.is_err());
345    }
346
347    #[test]
348    fn test_try_for_each_with_in_pool() {
349        let pool = create_thread_pool(4).unwrap();
350        let iter = (0..100).into_par_iter();
351        let result = iter.try_for_each_with_in_pool(pool.as_ref(), 0, |acc, i| {
352            assert_run_in_rayon_thread();
353            if i < 50 {
354                *acc += i;
355                Ok(())
356            } else {
357                Err("Error")
358            }
359        });
360        assert!(result.is_err());
361    }
362
363    #[test]
364    fn test_count_in_pool() {
365        let pool = create_thread_pool(4).unwrap();
366        let iter = (0..100).into_par_iter();
367        let count = iter.count_in_pool(pool.as_ref());
368        assert_eq!(count, 100);
369    }
370
371    #[test]
372    fn test_collect_in_pool() {
373        let pool = create_thread_pool(4).unwrap();
374        let iter = (0..100).into_par_iter();
375        let vec = iter.collect_in_pool::<Vec<_>>(pool.as_ref());
376        assert_eq!(vec.len(), 100);
377    }
378
379    #[test]
380    fn test_sum_in_pool() {
381        let pool = create_thread_pool(4).unwrap();
382        let iter = (0..100).into_par_iter();
383        let sum: i32 = iter.sum_in_pool(pool.as_ref());
384        assert_eq!(sum, (0..100).sum::<i32>());
385    }
386}