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