torsh_autograd/
parallel_gradient.rs1use crate::error_handling::AutogradResult;
45
46#[cfg(feature = "parallel")]
47
48#[derive(Debug, Clone)]
50pub struct ParallelConfig {
51 pub num_threads: usize,
53 pub min_parallel_size: usize,
55 pub chunk_size: usize,
57 pub topology_aware: bool,
59 pub dynamic_balancing: bool,
61}
62
63impl Default for ParallelConfig {
64 fn default() -> Self {
65 Self {
66 num_threads: 0, min_parallel_size: 1000,
68 chunk_size: 10000,
69 topology_aware: true,
70 dynamic_balancing: true,
71 }
72 }
73}
74
75impl ParallelConfig {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn with_num_threads(mut self, num_threads: usize) -> Self {
83 self.num_threads = num_threads;
84 self
85 }
86
87 pub fn with_min_parallel_size(mut self, min_size: usize) -> Self {
89 self.min_parallel_size = min_size;
90 self
91 }
92
93 pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
95 self.chunk_size = chunk_size;
96 self
97 }
98
99 pub fn with_topology_aware(mut self, enabled: bool) -> Self {
101 self.topology_aware = enabled;
102 self
103 }
104
105 pub fn with_dynamic_balancing(mut self, enabled: bool) -> Self {
107 self.dynamic_balancing = enabled;
108 self
109 }
110}
111
112pub struct ParallelGradientComputer {
114 config: ParallelConfig,
115 stats: ParallelStats,
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct ParallelStats {
122 pub total_ops: usize,
124 pub total_time_ms: f64,
126 pub avg_speedup: f64,
128 pub tensors_processed: usize,
130}
131
132impl ParallelGradientComputer {
133 pub fn new() -> Self {
135 Self {
136 config: ParallelConfig::default(),
137 stats: ParallelStats::default(),
138 }
139 }
140
141 pub fn with_config(config: ParallelConfig) -> Self {
143 Self {
144 config,
145 stats: ParallelStats::default(),
146 }
147 }
148
149 pub fn set_config(&mut self, config: ParallelConfig) {
151 self.config = config;
152 }
153
154 pub fn config(&self) -> &ParallelConfig {
156 &self.config
157 }
158
159 pub fn stats(&self) -> &ParallelStats {
161 &self.stats
162 }
163
164 pub fn reset_stats(&mut self) {
166 self.stats = ParallelStats::default();
167 }
168
169 pub fn should_parallelize(&self, tensor_size: usize) -> bool {
171 tensor_size >= self.config.min_parallel_size
172 }
173
174 pub fn compute_optimal_chunk_size(&self, tensor_size: usize) -> usize {
176 if !self.should_parallelize(tensor_size) {
177 return tensor_size;
178 }
179
180 let num_threads = if self.config.num_threads > 0 {
181 self.config.num_threads
182 } else {
183 num_cpus::get()
184 };
185
186 let target_chunks = num_threads * 2;
188 let chunk_size = (tensor_size + target_chunks - 1) / target_chunks;
189
190 chunk_size.max(1).min(self.config.chunk_size)
192 }
193
194 #[cfg(feature = "parallel")]
195 pub fn compute_gradients_parallel<T>(&mut self, data: &[T]) -> AutogradResult<Vec<T>>
200 where
201 T: Send + Sync + Clone + Copy,
202 {
203 use std::time::Instant;
204
205 let start = Instant::now();
206
207 if !self.should_parallelize(data.len()) {
208 return Ok(data.to_vec());
210 }
211
212 let result: Vec<T> = data.to_vec(); self.stats.total_ops += 1;
217 self.stats.total_time_ms += start.elapsed().as_secs_f64() * 1000.0;
218 self.stats.tensors_processed += 1;
219
220 Ok(result)
221 }
222
223 #[cfg(not(feature = "parallel"))]
224 pub fn compute_gradients_parallel<T>(&mut self, data: &[T]) -> AutogradResult<Vec<T>>
226 where
227 T: Clone,
228 {
229 tracing::warn!("Parallel feature not enabled, using sequential fallback");
230 Ok(data.to_vec())
231 }
232
233 #[cfg(feature = "parallel")]
234 pub fn parallel_element_wise_op<T, F>(&mut self, data: &[T], op: F) -> AutogradResult<Vec<T>>
239 where
240 T: Send + Sync + Clone,
241 F: Fn(&T) -> T + Send + Sync,
242 {
243 if !self.should_parallelize(data.len()) {
244 return Ok(data.iter().map(op).collect());
246 }
247
248 let chunk_size = self.compute_optimal_chunk_size(data.len());
249
250 let result: Vec<T> = data
252 .chunks(chunk_size)
253 .flat_map(|chunk| chunk.iter().map(&op).collect::<Vec<_>>())
254 .collect();
255
256 Ok(result)
257 }
258
259 #[cfg(not(feature = "parallel"))]
260 pub fn parallel_element_wise_op<T, F>(&mut self, data: &[T], op: F) -> AutogradResult<Vec<T>>
262 where
263 T: Clone,
264 F: Fn(&T) -> T,
265 {
266 Ok(data.iter().map(op).collect())
267 }
268
269 #[cfg(feature = "parallel")]
274 pub fn compute_with_intelligent_chunking<T>(
275 &mut self,
276 data: &[T],
277 grad_fn: impl Fn(&T) -> T + Send + Sync,
278 ) -> AutogradResult<Vec<T>>
279 where
280 T: Send + Sync + Clone,
281 {
282 self.parallel_element_wise_op(data, grad_fn)
289 }
290
291 #[cfg(not(feature = "parallel"))]
292 pub fn compute_with_intelligent_chunking<T>(
294 &mut self,
295 data: &[T],
296 grad_fn: impl Fn(&T) -> T,
297 ) -> AutogradResult<Vec<T>>
298 where
299 T: Clone,
300 {
301 Ok(data.iter().map(grad_fn).collect())
302 }
303
304 pub fn report_performance(&self) -> String {
306 format!(
307 "Parallel Gradient Computation Statistics:\n\
308 - Total operations: {}\n\
309 - Total time: {:.2}ms\n\
310 - Tensors processed: {}\n\
311 - Average speedup: {:.2}x\n\
312 - Average time per op: {:.2}ms",
313 self.stats.total_ops,
314 self.stats.total_time_ms,
315 self.stats.tensors_processed,
316 self.stats.avg_speedup,
317 if self.stats.total_ops > 0 {
318 self.stats.total_time_ms / self.stats.total_ops as f64
319 } else {
320 0.0
321 }
322 )
323 }
324}
325
326impl Default for ParallelGradientComputer {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332static GLOBAL_PARALLEL_COMPUTER: once_cell::sync::Lazy<
334 parking_lot::RwLock<ParallelGradientComputer>,
335> = once_cell::sync::Lazy::new(|| parking_lot::RwLock::new(ParallelGradientComputer::new()));
336
337pub fn get_global_parallel_computer(
339) -> parking_lot::RwLockReadGuard<'static, ParallelGradientComputer> {
340 GLOBAL_PARALLEL_COMPUTER.read()
341}
342
343pub fn get_global_parallel_computer_mut(
345) -> parking_lot::RwLockWriteGuard<'static, ParallelGradientComputer> {
346 GLOBAL_PARALLEL_COMPUTER.write()
347}
348
349pub fn configure_global_parallel(config: ParallelConfig) {
351 let mut computer = GLOBAL_PARALLEL_COMPUTER.write();
352 computer.set_config(config);
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn test_parallel_config() {
361 let config = ParallelConfig::default()
362 .with_num_threads(4)
363 .with_chunk_size(5000)
364 .with_min_parallel_size(500);
365
366 assert_eq!(config.num_threads, 4);
367 assert_eq!(config.chunk_size, 5000);
368 assert_eq!(config.min_parallel_size, 500);
369 }
370
371 #[test]
372 fn test_should_parallelize() {
373 let computer = ParallelGradientComputer::new();
374
375 assert!(!computer.should_parallelize(100)); assert!(computer.should_parallelize(10000)); }
378
379 #[test]
380 fn test_compute_optimal_chunk_size() {
381 let computer = ParallelGradientComputer::new();
382
383 let chunk_size = computer.compute_optimal_chunk_size(500);
385 assert_eq!(chunk_size, 500);
386
387 let chunk_size = computer.compute_optimal_chunk_size(100000);
389 assert!(chunk_size > 0 && chunk_size <= 10000);
390 }
391
392 #[test]
393 fn test_parallel_element_wise_op() {
394 let mut computer = ParallelGradientComputer::new();
395 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
396
397 let result = computer
398 .parallel_element_wise_op(&data, |&x| x * 2.0)
399 .unwrap();
400
401 assert_eq!(result, vec![2.0, 4.0, 6.0, 8.0, 10.0]);
402 }
403
404 #[test]
405 fn test_global_parallel_computer() {
406 let config = ParallelConfig::default().with_num_threads(2);
407 configure_global_parallel(config.clone());
408
409 let computer = get_global_parallel_computer();
410 assert_eq!(computer.config().num_threads, 2);
411 }
412
413 #[test]
414 fn test_report_performance() {
415 let computer = ParallelGradientComputer::new();
416 let report = computer.report_performance();
417
418 assert!(report.contains("Parallel Gradient Computation Statistics"));
419 assert!(report.contains("Total operations: 0"));
420 }
421}