1use std::{
2 collections::HashMap,
3 sync::{Arc, Mutex, Weak},
4};
5
6use laddu_autodiff::AutodiffMode;
7use laddu_data::io::{Partitioning, ReadPlan};
8#[cfg(feature = "wgpu")]
9use laddu_memory::{DeviceIdentity, MemoryResource};
10use laddu_memory::{
11 MemoryBudget, MemoryDecision, MemoryPlan, MemoryPool, MemoryPoolReport, MemoryReport,
12 MemoryState,
13};
14use rayon::{ThreadPool, ThreadPoolBuilder};
15use serde::{Deserialize, Serialize};
16
17#[cfg(feature = "wgpu")]
18use crate::RuntimeError;
19use crate::{ExecutionError, RuntimeResult};
20
21pub(crate) type NormalizationCache =
22 HashMap<(u64, u64, NormalizationMode), Weak<crate::PreparedNormalization>>;
23
24#[cfg(feature = "mpi")]
25use mpi::{
26 collective::SystemOperation,
27 topology::SimpleCommunicator,
28 traits::{Communicator, CommunicatorCollectives},
29};
30
31#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
33pub enum Precision {
34 #[default]
36 Auto,
37 F32,
39 F64,
41}
42
43#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ThreadPolicy {
46 #[default]
48 Auto,
49 Serial,
51 Fixed(usize),
53}
54
55#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub enum JitPolicy {
58 #[default]
60 Auto,
61 Enabled,
63 Disabled,
65}
66
67#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum NormalizationMode {
70 #[default]
72 Auto,
73 General,
75 Verify,
77}
78
79#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub struct CpuOptions {
82 pub threads: ThreadPolicy,
84 pub jit: JitPolicy,
86}
87
88#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
90pub enum GpuBackend {
91 #[default]
93 Auto,
94 Wgpu,
96 Cuda,
98}
99
100#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
102pub enum GpuDeviceSelector {
103 #[default]
105 Auto,
106 Index(usize),
108 PciBusId(String),
110 Name(String),
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
116pub struct GpuOptions {
117 pub backend: GpuBackend,
119 pub device: GpuDeviceSelector,
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub enum Device {
126 #[default]
128 Auto,
129 Cpu(CpuOptions),
131 Gpu(GpuOptions),
133}
134
135#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
137pub struct ExecutionOptions {
138 pub device: Device,
140 pub precision: Precision,
142 pub autodiff: AutodiffMode,
144 #[serde(default)]
146 pub normalization: NormalizationMode,
147 pub partitioning: Partitioning,
149 pub memory: MemoryPlan,
151}
152
153#[derive(Clone)]
155pub struct Execution {
156 requested_device: Device,
157 precision: Precision,
158 autodiff: AutodiffMode,
159 normalization: NormalizationMode,
160 threads: ThreadPolicy,
161 jit: JitPolicy,
162 pool: Option<Arc<ThreadPool>>,
163 partitioning: Partitioning,
164 memory_state: MemoryState,
165 host_memory: MemoryPool,
166 device_memory: Option<MemoryPool>,
167 memory_decisions: Arc<Mutex<Vec<MemoryDecision>>>,
168 normalization_cache: Arc<Mutex<NormalizationCache>>,
169 #[cfg(feature = "wgpu")]
170 wgpu: Option<Arc<laddu_wgpu::WgpuContext>>,
171 #[cfg(feature = "mpi")]
172 communicator: Option<Arc<SimpleCommunicator>>,
173}
174
175impl std::fmt::Debug for Execution {
176 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 #[cfg(feature = "wgpu")]
178 let resolved_device = if self.wgpu.is_some() { "wgpu" } else { "cpu" };
179 #[cfg(not(feature = "wgpu"))]
180 let resolved_device = "cpu";
181 formatter
182 .debug_struct("Execution")
183 .field("requested_device", &self.requested_device)
184 .field("resolved_device", &resolved_device)
185 .field("precision", &self.precision)
186 .field("autodiff", &self.autodiff)
187 .field("normalization", &self.normalization)
188 .field("threads", &self.threads)
189 .field("jit", &self.jit)
190 .field("partitioning", &self.partitioning)
191 .field("host_memory", &self.host_memory.report())
192 .field(
193 "device_memory",
194 &self.device_memory.as_ref().map(MemoryPool::report),
195 )
196 .field("ranks", &self.nranks())
197 .finish_non_exhaustive()
198 }
199}
200
201impl Default for Execution {
202 fn default() -> Self {
203 let memory_state = MemoryState::current();
204 memory_state.refresh();
205 let host_memory = memory_state
206 .pool("host", MemoryBudget::Auto)
207 .expect("host memory discovery must resolve an automatic budget");
208 Self {
209 requested_device: Device::Auto,
210 precision: Precision::F64,
211 autodiff: AutodiffMode::Auto,
212 normalization: NormalizationMode::Auto,
213 threads: ThreadPolicy::Auto,
214 jit: JitPolicy::Auto,
215 pool: None,
216 partitioning: Partitioning::default(),
217 memory_state,
218 host_memory,
219 device_memory: None,
220 memory_decisions: Default::default(),
221 normalization_cache: Default::default(),
222 #[cfg(feature = "wgpu")]
223 wgpu: None,
224 #[cfg(feature = "mpi")]
225 communicator: None,
226 }
227 }
228}
229
230impl Execution {
231 pub fn local(options: ExecutionOptions) -> RuntimeResult<Self> {
239 let memory_state = MemoryState::current();
240 memory_state.refresh();
241 let host_memory = memory_state.pool("host", options.memory.host)?;
242 #[cfg(feature = "wgpu")]
243 let mut wgpu = None;
244 #[cfg(feature = "wgpu")]
245 let mut device_memory = None;
246 #[cfg(not(feature = "wgpu"))]
247 let device_memory = None;
248 let cpu = match &options.device {
249 Device::Auto => CpuOptions::default(),
250 Device::Cpu(options) => options.clone(),
251 Device::Gpu(gpu_options) => {
252 #[cfg(feature = "wgpu")]
253 {
254 if gpu_options.backend == GpuBackend::Cuda {
255 return Err(ExecutionError::GpuUnavailable(gpu_options.backend).into());
256 }
257 let selector = match &gpu_options.device {
258 GpuDeviceSelector::Auto => laddu_wgpu::WgpuDeviceSelector::Auto,
259 GpuDeviceSelector::Index(index) => {
260 laddu_wgpu::WgpuDeviceSelector::Index(*index)
261 }
262 GpuDeviceSelector::PciBusId(id) => {
263 laddu_wgpu::WgpuDeviceSelector::PciBusId(id.clone())
264 }
265 GpuDeviceSelector::Name(name) => {
266 laddu_wgpu::WgpuDeviceSelector::Name(name.clone())
267 }
268 };
269 let precision = match options.precision {
270 Precision::Auto => laddu_wgpu::WgpuPrecision::Auto,
271 Precision::F32 => laddu_wgpu::WgpuPrecision::F32,
272 Precision::F64 => laddu_wgpu::WgpuPrecision::F64,
273 };
274 let mut context = laddu_wgpu::WgpuBackend::default()
275 .open(
276 &laddu_wgpu::WgpuOptions {
277 device: selector,
278 memory_budget: None,
279 },
280 precision,
281 )
282 .map_err(|error| RuntimeError::Wgpu(error.to_string()))?;
283 let resource_id = if context.info().pci_bus_id.is_empty() {
284 format!("wgpu:{}", context.info().index)
285 } else {
286 format!("pci:{}", context.info().pci_bus_id)
287 };
288 let fallback = context
289 .info()
290 .max_buffer_size
291 .min(512 * 1024 * 1024)
292 .max(context.info().max_storage_buffer_binding_size);
293 let resource = MemoryResource::discover_device(
294 resource_id.clone(),
295 context.info().name.clone(),
296 DeviceIdentity {
297 adapter_index: context.info().index,
298 vendor_id: context.info().vendor,
299 device_id: context.info().device,
300 pci_bus_id: context.info().pci_bus_id.clone(),
301 },
302 fallback,
303 );
304 memory_state.register_device(resource);
305 let requested = options.memory.device.unwrap_or(MemoryBudget::Auto);
306 let pool = memory_state.pool(&resource_id, requested)?;
307 context
308 .set_memory_budget(usize::try_from(pool.capacity()).unwrap_or(usize::MAX));
309 device_memory = Some(pool);
310 wgpu = Some(Arc::new(context));
311 CpuOptions::default()
312 }
313 #[cfg(not(feature = "wgpu"))]
314 return Err(ExecutionError::GpuUnavailable(gpu_options.backend).into());
315 }
316 };
317 let precision = match options.precision {
318 Precision::Auto if matches!(options.device, Device::Gpu(_)) => Precision::F32,
319 Precision::Auto => Precision::F64,
320 precision => precision,
321 };
322 #[cfg(not(feature = "jit"))]
323 if cpu.jit == JitPolicy::Enabled {
324 return Err(ExecutionError::JitUnavailable.into());
325 }
326 let pool = match cpu.threads {
327 ThreadPolicy::Fixed(0) => return Err(ExecutionError::ZeroThreads.into()),
328 ThreadPolicy::Fixed(threads) => Some(Arc::new(
329 ThreadPoolBuilder::new()
330 .num_threads(threads)
331 .build()
332 .map_err(|error| ExecutionError::ThreadPool(error.to_string()))?,
333 )),
334 ThreadPolicy::Auto | ThreadPolicy::Serial => None,
335 };
336 Ok(Self {
337 requested_device: options.device,
338 precision,
339 autodiff: options.autodiff,
340 normalization: options.normalization,
341 threads: cpu.threads,
342 jit: cpu.jit,
343 pool,
344 partitioning: options.partitioning,
345 memory_state,
346 host_memory,
347 device_memory,
348 memory_decisions: Default::default(),
349 normalization_cache: Default::default(),
350 #[cfg(feature = "wgpu")]
351 wgpu,
352 #[cfg(feature = "mpi")]
353 communicator: None,
354 })
355 }
356
357 #[cfg(feature = "mpi")]
358 pub fn distributed<C>(options: ExecutionOptions, world: &C) -> RuntimeResult<Self>
364 where
365 C: Communicator,
366 {
367 let local_processes = mpi_local_process_count(world.size());
368 let mut options = options;
369 options.memory.host = shared_mpi_budget(options.memory.host, local_processes);
370 options.memory.device = options
371 .memory
372 .device
373 .map(|budget| shared_mpi_budget(budget, local_processes));
374 let mut execution = Self::local(options)?;
375 execution.record_memory_decision(MemoryDecision {
376 label: "mpi-memory-share".into(),
377 fixed_bytes: 0,
378 bytes_per_event: 0,
379 chunk_events: 0,
380 estimated_peak_bytes: 0,
381 actual_high_water_bytes: None,
382 strategy: format!("equal-share-across-{local_processes}-local-ranks"),
383 });
384 execution.communicator = Some(Arc::new(world.duplicate()));
385 Ok(execution)
386 }
387
388 pub fn requested_device(&self) -> &Device {
390 &self.requested_device
391 }
392
393 #[cfg(feature = "wgpu")]
394 pub(crate) fn wgpu_context(&self) -> Option<&Arc<laddu_wgpu::WgpuContext>> {
395 self.wgpu.as_ref()
396 }
397
398 pub fn precision(&self) -> Precision {
400 self.precision
401 }
402
403 pub fn autodiff_mode(&self) -> AutodiffMode {
405 self.autodiff
406 }
407
408 pub fn normalization_mode(&self) -> NormalizationMode {
410 self.normalization
411 }
412
413 pub(crate) fn normalization_cache(&self) -> &Mutex<NormalizationCache> {
414 &self.normalization_cache
415 }
416
417 pub fn thread_policy(&self) -> ThreadPolicy {
419 self.threads
420 }
421
422 pub fn jit_policy(&self) -> JitPolicy {
424 self.jit
425 }
426
427 pub fn partitioning(&self) -> Partitioning {
429 self.partitioning
430 }
431
432 pub fn memory_state(&self) -> &MemoryState {
434 &self.memory_state
435 }
436
437 pub fn host_memory(&self) -> &MemoryPool {
439 &self.host_memory
440 }
441
442 pub fn device_memory(&self) -> Option<&MemoryPool> {
444 self.device_memory.as_ref()
445 }
446
447 pub fn memory_report(&self) -> MemoryReport {
449 self.memory_state.report()
450 }
451
452 pub fn memory_pool_reports(&self) -> Vec<MemoryPoolReport> {
454 std::iter::once(self.host_memory.report())
455 .chain(self.device_memory.as_ref().map(MemoryPool::report))
456 .collect()
457 }
458
459 pub fn memory_decisions(&self) -> Vec<MemoryDecision> {
461 self.memory_decisions
462 .lock()
463 .unwrap_or_else(|error| error.into_inner())
464 .clone()
465 }
466
467 pub fn record_memory_decision(&self, decision: MemoryDecision) {
469 self.memory_decisions
470 .lock()
471 .unwrap_or_else(|error| error.into_inner())
472 .push(decision);
473 }
474
475 pub fn rank(&self) -> usize {
477 #[cfg(feature = "mpi")]
478 if let Some(communicator) = &self.communicator {
479 return communicator.rank() as usize;
480 }
481 0
482 }
483
484 pub fn nranks(&self) -> usize {
486 #[cfg(feature = "mpi")]
487 if let Some(communicator) = &self.communicator {
488 return communicator.size() as usize;
489 }
490 1
491 }
492
493 pub fn is_distributed(&self) -> bool {
495 self.nranks() > 1
496 }
497
498 #[allow(unused_mut)]
499 pub(crate) fn read_plan(&self, mut plan: ReadPlan) -> ReadPlan {
500 #[cfg(feature = "mpi")]
501 if let Some(communicator) = &self.communicator {
502 plan.distribution = laddu_data::io::Distribution::from_world(communicator.as_ref())
503 .with_partitioning(self.partitioning);
504 }
505 plan
506 }
507
508 pub(crate) fn sum_f64(&self, local: f64) -> f64 {
509 #[cfg(feature = "mpi")]
510 if let Some(communicator) = &self.communicator {
511 let mut global = 0.0;
512 communicator.all_reduce_into(&local, &mut global, SystemOperation::sum());
513 return global;
514 }
515 local
516 }
517
518 pub(crate) fn sum_usize(&self, local: usize) -> usize {
519 #[cfg(feature = "mpi")]
520 if let Some(communicator) = &self.communicator {
521 let local = local as u64;
522 let mut global = 0_u64;
523 communicator.all_reduce_into(&local, &mut global, SystemOperation::sum());
524 return global as usize;
525 }
526 local
527 }
528
529 pub(crate) fn sum_slice(&self, local: &[f64]) -> Vec<f64> {
530 #[cfg(feature = "mpi")]
531 if let Some(communicator) = &self.communicator {
532 let mut global = vec![0.0; local.len()];
533 communicator.all_reduce_into(local, &mut global, SystemOperation::sum());
534 return global;
535 }
536 local.to_vec()
537 }
538
539 pub(crate) fn all_succeeded(&self, local_success: bool) -> bool {
540 self.sum_usize(usize::from(local_success)) == self.nranks()
541 }
542
543 pub(crate) fn is_parallel(&self) -> bool {
544 self.threads != ThreadPolicy::Serial
545 }
546
547 pub(crate) fn install<R: Send>(&self, operation: impl FnOnce() -> R + Send) -> R {
548 match &self.pool {
549 Some(pool) => pool.install(operation),
550 None => operation(),
551 }
552 }
553}
554
555#[cfg(feature = "mpi")]
556fn shared_mpi_budget(budget: MemoryBudget, local_processes: u64) -> MemoryBudget {
557 let divisor = local_processes.max(1);
558 match budget {
559 MemoryBudget::Auto => MemoryBudget::PercentAvailable(0.80 / divisor as f64),
560 MemoryBudget::Bytes(bytes) => MemoryBudget::Bytes((bytes / divisor).max(1)),
561 MemoryBudget::PercentTotal(fraction) => {
562 MemoryBudget::PercentTotal(fraction / divisor as f64)
563 }
564 MemoryBudget::PercentAvailable(fraction) => {
565 MemoryBudget::PercentAvailable(fraction / divisor as f64)
566 }
567 }
568}
569
570#[cfg(feature = "mpi")]
571fn mpi_local_process_count(world_size: i32) -> u64 {
572 const VARIABLES: [&str; 4] = [
576 "OMPI_COMM_WORLD_LOCAL_SIZE",
577 "MPI_LOCALNRANKS",
578 "MV2_COMM_WORLD_LOCAL_SIZE",
579 "SLURM_NTASKS_PER_NODE",
580 ];
581 VARIABLES
582 .iter()
583 .filter_map(|name| std::env::var(name).ok())
584 .find_map(|value| {
585 value
586 .split(|character: char| !character.is_ascii_digit())
587 .find(|part| !part.is_empty())
588 .and_then(|part| part.parse::<u64>().ok())
589 .filter(|count| *count > 0)
590 })
591 .unwrap_or_else(|| u64::try_from(world_size).unwrap_or(1).max(1))
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 #[cfg(not(feature = "wgpu"))]
598 use crate::RuntimeError;
599 use crate::execution::GpuBackend;
600
601 #[test]
602 fn execution_options_roundtrip_through_json() {
603 let options = ExecutionOptions {
604 device: Device::Gpu(GpuOptions {
605 backend: GpuBackend::Wgpu,
606 device: GpuDeviceSelector::PciBusId("0000:01:00.0".into()),
607 }),
608 precision: Precision::F64,
609 autodiff: AutodiffMode::Reverse,
610 normalization: NormalizationMode::Verify,
611 partitioning: Partitioning::FileGroups,
612 memory: MemoryPlan::host_device(
613 MemoryBudget::PercentAvailable(0.5),
614 MemoryBudget::Bytes(1 << 30),
615 ),
616 };
617
618 let json = serde_json::to_string(&options).unwrap();
619 assert_eq!(
620 serde_json::from_str::<ExecutionOptions>(&json).unwrap(),
621 options
622 );
623 }
624
625 #[test]
626 fn execution_selects_nested_cpu_options() {
627 let serial = Execution::local(ExecutionOptions {
628 device: Device::Cpu(CpuOptions {
629 threads: ThreadPolicy::Serial,
630 jit: JitPolicy::Disabled,
631 }),
632 ..ExecutionOptions::default()
633 })
634 .unwrap();
635 assert!(!serial.is_parallel());
636 assert_eq!(serial.jit_policy(), JitPolicy::Disabled);
637 assert_eq!(serial.precision(), Precision::F64);
638
639 let fixed = Execution::local(ExecutionOptions {
640 device: Device::Cpu(CpuOptions {
641 threads: ThreadPolicy::Fixed(2),
642 ..CpuOptions::default()
643 }),
644 ..ExecutionOptions::default()
645 })
646 .unwrap();
647 assert_eq!(fixed.install(rayon::current_num_threads), 2);
648 }
649
650 #[test]
651 fn unavailable_execution_modes_return_capability_errors() {
652 #[cfg(not(feature = "wgpu"))]
653 assert!(matches!(
654 Execution::local(ExecutionOptions {
655 device: Device::Gpu(GpuOptions {
656 backend: GpuBackend::Wgpu,
657 ..GpuOptions::default()
658 }),
659 ..ExecutionOptions::default()
660 }),
661 Err(RuntimeError::Execution(ExecutionError::GpuUnavailable(
662 GpuBackend::Wgpu
663 )))
664 ));
665 #[cfg(feature = "wgpu")]
666 assert!(
667 Execution::local(ExecutionOptions {
668 device: Device::Gpu(GpuOptions {
669 backend: GpuBackend::Wgpu,
670 ..GpuOptions::default()
671 }),
672 ..ExecutionOptions::default()
673 })
674 .is_ok()
675 );
676 let f32 = Execution::local(ExecutionOptions {
677 device: Device::Cpu(CpuOptions::default()),
678 precision: Precision::F32,
679 ..ExecutionOptions::default()
680 })
681 .unwrap();
682 assert_eq!(f32.precision(), Precision::F32);
683
684 let reverse = Execution::local(ExecutionOptions {
685 autodiff: AutodiffMode::Reverse,
686 ..ExecutionOptions::default()
687 })
688 .unwrap();
689 assert_eq!(reverse.autodiff_mode(), AutodiffMode::Reverse);
690
691 let reverse_f32 = Execution::local(ExecutionOptions {
692 precision: Precision::F32,
693 autodiff: AutodiffMode::Reverse,
694 ..ExecutionOptions::default()
695 })
696 .unwrap();
697 assert_eq!(reverse_f32.precision(), Precision::F32);
698 assert_eq!(reverse_f32.autodiff_mode(), AutodiffMode::Reverse);
699 }
700}