Skip to main content

laddu_runtime/
execution.rs

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;
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/// Numeric precision used to execute a model.
32#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
33pub enum Precision {
34    /// Select a precision appropriate for the resolved device.
35    #[default]
36    Auto,
37    /// Use 32-bit floating-point arithmetic.
38    F32,
39    /// Use 64-bit floating-point arithmetic.
40    F64,
41}
42
43/// Policy controlling CPU worker-thread use.
44#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub enum ThreadPolicy {
46    /// Use the runtime's default parallelism.
47    #[default]
48    Auto,
49    /// Execute serially on the calling thread.
50    Serial,
51    /// Use exactly the given number of worker threads.
52    Fixed(usize),
53}
54
55/// Policy controlling CPU just-in-time compilation.
56#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub enum JitPolicy {
58    /// Use JIT compilation when it is available and applicable.
59    #[default]
60    Auto,
61    /// Require the JIT-capable execution path.
62    Enabled,
63    /// Always use the interpreter.
64    Disabled,
65}
66
67/// Policy controlling compiler-native accepted normalization.
68#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub enum NormalizationMode {
70    /// Select exact sufficient statistics when cost and memory estimates are favorable.
71    #[default]
72    Auto,
73    /// Always use the ordinary accepted-event reduction path.
74    General,
75    /// Evaluate both selected and general paths and reject disagreements.
76    Verify,
77}
78
79/// CPU-specific execution options.
80#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
81pub struct CpuOptions {
82    /// Worker-thread policy.
83    pub threads: ThreadPolicy,
84    /// JIT compilation policy.
85    pub jit: JitPolicy,
86}
87
88/// GPU implementation to use.
89#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
90pub enum GpuBackend {
91    /// Select an available GPU backend automatically.
92    #[default]
93    Auto,
94    /// Use the WebGPU backend.
95    Wgpu,
96    /// Use the CUDA backend.
97    Cuda,
98}
99
100/// Rule used to select a GPU device.
101#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
102pub enum GpuDeviceSelector {
103    /// Select a suitable device automatically.
104    #[default]
105    Auto,
106    /// Select the adapter at the given enumeration index.
107    Index(usize),
108    /// Select the adapter with the given PCI bus identifier.
109    PciBusId(String),
110    /// Select an adapter whose name matches the given string.
111    Name(String),
112}
113
114/// GPU-specific execution options.
115#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
116pub struct GpuOptions {
117    /// GPU backend to use.
118    pub backend: GpuBackend,
119    /// GPU device selection rule.
120    pub device: GpuDeviceSelector,
121}
122
123/// Device on which a model should execute.
124#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
125pub enum Device {
126    /// Select a suitable device automatically.
127    #[default]
128    Auto,
129    /// Execute on a CPU with the supplied options.
130    Cpu(CpuOptions),
131    /// Execute on a GPU with the supplied options.
132    Gpu(GpuOptions),
133}
134
135/// Options used to construct an [`Execution`] context.
136#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
137pub struct ExecutionOptions {
138    /// Requested execution device.
139    pub device: Device,
140    /// Requested numeric precision.
141    pub precision: Precision,
142    /// Automatic-differentiation strategy.
143    pub autodiff: AutodiffMode,
144    /// Accepted-normalization selection and verification policy.
145    #[serde(default)]
146    pub normalization: NormalizationMode,
147    /// Dataset partitioning strategy for distributed execution.
148    pub partitioning: Partitioning,
149    /// Host and accelerator memory budgets.
150    pub memory: MemoryPlan,
151}
152
153/// Resolved resources and policies used to execute models.
154#[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    /// Creates a non-distributed execution context from `options`.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`RuntimeError`] when the requested backend or precision is
236    /// unavailable, GPU initialization fails, or the CPU thread pool cannot be
237    /// created.
238    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                    memory_state.register_discovered_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                    let requested = options.memory.device.unwrap_or(MemoryBudget::Auto);
305                    let pool = memory_state.pool(&resource_id, requested)?;
306                    context
307                        .set_memory_budget(usize::try_from(pool.capacity()).unwrap_or(usize::MAX));
308                    device_memory = Some(pool);
309                    wgpu = Some(Arc::new(context));
310                    CpuOptions::default()
311                }
312                #[cfg(not(feature = "wgpu"))]
313                return Err(ExecutionError::GpuUnavailable(gpu_options.backend).into());
314            }
315        };
316        let precision = match options.precision {
317            Precision::Auto if matches!(options.device, Device::Gpu(_)) => Precision::F32,
318            Precision::Auto => Precision::F64,
319            precision => precision,
320        };
321        #[cfg(not(feature = "jit"))]
322        if cpu.jit == JitPolicy::Enabled {
323            return Err(ExecutionError::JitUnavailable.into());
324        }
325        let pool = match cpu.threads {
326            ThreadPolicy::Fixed(0) => return Err(ExecutionError::ZeroThreads.into()),
327            ThreadPolicy::Fixed(threads) => Some(Arc::new(
328                ThreadPoolBuilder::new()
329                    .num_threads(threads)
330                    .build()
331                    .map_err(|error| ExecutionError::ThreadPool(error.to_string()))?,
332            )),
333            ThreadPolicy::Auto | ThreadPolicy::Serial => None,
334        };
335        Ok(Self {
336            requested_device: options.device,
337            precision,
338            autodiff: options.autodiff,
339            normalization: options.normalization,
340            threads: cpu.threads,
341            jit: cpu.jit,
342            pool,
343            partitioning: options.partitioning,
344            memory_state,
345            host_memory,
346            device_memory,
347            memory_decisions: Default::default(),
348            normalization_cache: Default::default(),
349            #[cfg(feature = "wgpu")]
350            wgpu,
351            #[cfg(feature = "mpi")]
352            communicator: None,
353        })
354    }
355
356    #[cfg(feature = "mpi")]
357    /// Creates a distributed execution context over the supplied MPI communicator.
358    ///
359    /// # Errors
360    ///
361    /// Returns [`RuntimeError`] when local execution initialization fails.
362    pub fn distributed<C>(options: ExecutionOptions, world: &C) -> RuntimeResult<Self>
363    where
364        C: Communicator,
365    {
366        let local_processes = mpi_local_process_count(world.size());
367        let mut options = options;
368        options.memory.host = shared_mpi_budget(options.memory.host, local_processes);
369        options.memory.device = options
370            .memory
371            .device
372            .map(|budget| shared_mpi_budget(budget, local_processes));
373        let mut execution = Self::local(options)?;
374        execution.record_memory_decision(MemoryDecision {
375            label: "mpi-memory-share".into(),
376            fixed_bytes: 0,
377            bytes_per_event: 0,
378            chunk_events: 0,
379            estimated_peak_bytes: 0,
380            actual_high_water_bytes: None,
381            strategy: format!("equal-share-across-{local_processes}-local-ranks"),
382        });
383        execution.communicator = Some(Arc::new(world.duplicate()));
384        Ok(execution)
385    }
386
387    /// Returns the device requested when this context was created.
388    pub fn requested_device(&self) -> &Device {
389        &self.requested_device
390    }
391
392    #[cfg(feature = "wgpu")]
393    pub(crate) fn wgpu_context(&self) -> Option<&Arc<laddu_wgpu::WgpuContext>> {
394        self.wgpu.as_ref()
395    }
396
397    /// Returns the resolved numeric precision.
398    pub fn precision(&self) -> Precision {
399        self.precision
400    }
401
402    /// Returns the automatic-differentiation strategy.
403    pub fn autodiff_mode(&self) -> AutodiffMode {
404        self.autodiff
405    }
406
407    /// Returns the accepted-normalization policy.
408    pub fn normalization_mode(&self) -> NormalizationMode {
409        self.normalization
410    }
411
412    pub(crate) fn normalization_cache(&self) -> &Mutex<NormalizationCache> {
413        &self.normalization_cache
414    }
415
416    /// Returns the CPU worker-thread policy.
417    pub fn thread_policy(&self) -> ThreadPolicy {
418        self.threads
419    }
420
421    /// Returns the CPU JIT policy.
422    pub fn jit_policy(&self) -> JitPolicy {
423        self.jit
424    }
425
426    /// Returns the distributed dataset-partitioning strategy.
427    pub fn partitioning(&self) -> Partitioning {
428        self.partitioning
429    }
430
431    /// Returns the live memory state shared by this execution.
432    pub fn memory_state(&self) -> &MemoryState {
433        &self.memory_state
434    }
435
436    /// Returns the resolved host-memory pool.
437    pub fn host_memory(&self) -> &MemoryPool {
438        &self.host_memory
439    }
440
441    /// Returns the resolved accelerator-memory pool, if any.
442    pub fn device_memory(&self) -> Option<&MemoryPool> {
443        self.device_memory.as_ref()
444    }
445
446    /// Returns current physical-resource memory information.
447    pub fn memory_report(&self) -> MemoryReport {
448        self.memory_state.report()
449    }
450
451    /// Returns the resolved execution-pool reports.
452    pub fn memory_pool_reports(&self) -> Vec<MemoryPoolReport> {
453        std::iter::once(self.host_memory.report())
454            .chain(self.device_memory.as_ref().map(MemoryPool::report))
455            .collect()
456    }
457
458    /// Returns memory-derived decisions recorded by this execution.
459    pub fn memory_decisions(&self) -> Vec<MemoryDecision> {
460        self.memory_decisions
461            .lock()
462            .unwrap_or_else(|error| error.into_inner())
463            .clone()
464    }
465
466    /// Records a memory-planning decision for later diagnostics.
467    pub fn record_memory_decision(&self, decision: MemoryDecision) {
468        self.memory_decisions
469            .lock()
470            .unwrap_or_else(|error| error.into_inner())
471            .push(decision);
472    }
473
474    /// Returns the zero-based rank of this process.
475    pub fn rank(&self) -> usize {
476        #[cfg(feature = "mpi")]
477        if let Some(communicator) = &self.communicator {
478            return communicator.rank() as usize;
479        }
480        0
481    }
482
483    /// Returns the number of participating ranks.
484    pub fn nranks(&self) -> usize {
485        #[cfg(feature = "mpi")]
486        if let Some(communicator) = &self.communicator {
487            return communicator.size() as usize;
488        }
489        1
490    }
491
492    /// Returns whether this context spans more than one rank.
493    pub fn is_distributed(&self) -> bool {
494        self.nranks() > 1
495    }
496
497    #[allow(unused_mut)]
498    pub(crate) fn read_plan(&self, mut plan: ReadPlan) -> ReadPlan {
499        #[cfg(feature = "mpi")]
500        if let Some(communicator) = &self.communicator {
501            plan.distribution = laddu_data::io::Distribution::from_world(communicator.as_ref())
502                .with_partitioning(self.partitioning);
503        }
504        plan
505    }
506
507    pub(crate) fn sum_f64(&self, local: f64) -> f64 {
508        #[cfg(feature = "mpi")]
509        if let Some(communicator) = &self.communicator {
510            let mut global = 0.0;
511            communicator.all_reduce_into(&local, &mut global, SystemOperation::sum());
512            return global;
513        }
514        local
515    }
516
517    pub(crate) fn sum_usize(&self, local: usize) -> usize {
518        #[cfg(feature = "mpi")]
519        if let Some(communicator) = &self.communicator {
520            let local = local as u64;
521            let mut global = 0_u64;
522            communicator.all_reduce_into(&local, &mut global, SystemOperation::sum());
523            return global as usize;
524        }
525        local
526    }
527
528    pub(crate) fn sum_slice(&self, local: &[f64]) -> Vec<f64> {
529        #[cfg(feature = "mpi")]
530        if let Some(communicator) = &self.communicator {
531            let mut global = vec![0.0; local.len()];
532            communicator.all_reduce_into(local, &mut global, SystemOperation::sum());
533            return global;
534        }
535        local.to_vec()
536    }
537
538    pub(crate) fn all_succeeded(&self, local_success: bool) -> bool {
539        self.sum_usize(usize::from(local_success)) == self.nranks()
540    }
541
542    pub(crate) fn is_parallel(&self) -> bool {
543        self.threads != ThreadPolicy::Serial
544    }
545
546    pub(crate) fn install<R: Send>(&self, operation: impl FnOnce() -> R + Send) -> R {
547        match &self.pool {
548            Some(pool) => pool.install(operation),
549            None => operation(),
550        }
551    }
552}
553
554#[cfg(feature = "mpi")]
555fn shared_mpi_budget(budget: MemoryBudget, local_processes: u64) -> MemoryBudget {
556    let divisor = local_processes.max(1);
557    match budget {
558        MemoryBudget::Auto => MemoryBudget::PercentAvailable(0.80 / divisor as f64),
559        MemoryBudget::Bytes(bytes) => MemoryBudget::Bytes((bytes / divisor).max(1)),
560        MemoryBudget::PercentTotal(fraction) => {
561            MemoryBudget::PercentTotal(fraction / divisor as f64)
562        }
563        MemoryBudget::PercentAvailable(fraction) => {
564            MemoryBudget::PercentAvailable(fraction / divisor as f64)
565        }
566    }
567}
568
569#[cfg(feature = "mpi")]
570fn mpi_local_process_count(world_size: i32) -> u64 {
571    // Common launchers expose node-local process counts. Falling back to the
572    // world size is conservative on multi-node jobs and prevents accidental
573    // host/device overcommit when launcher metadata is unavailable.
574    const VARIABLES: [&str; 4] = [
575        "OMPI_COMM_WORLD_LOCAL_SIZE",
576        "MPI_LOCALNRANKS",
577        "MV2_COMM_WORLD_LOCAL_SIZE",
578        "SLURM_NTASKS_PER_NODE",
579    ];
580    VARIABLES
581        .iter()
582        .filter_map(|name| std::env::var(name).ok())
583        .find_map(|value| {
584            value
585                .split(|character: char| !character.is_ascii_digit())
586                .find(|part| !part.is_empty())
587                .and_then(|part| part.parse::<u64>().ok())
588                .filter(|count| *count > 0)
589        })
590        .unwrap_or_else(|| u64::try_from(world_size).unwrap_or(1).max(1))
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    #[cfg(not(feature = "wgpu"))]
597    use crate::RuntimeError;
598    use crate::execution::GpuBackend;
599
600    #[test]
601    fn execution_options_roundtrip_through_json() {
602        let options = ExecutionOptions {
603            device: Device::Gpu(GpuOptions {
604                backend: GpuBackend::Wgpu,
605                device: GpuDeviceSelector::PciBusId("0000:01:00.0".into()),
606            }),
607            precision: Precision::F64,
608            autodiff: AutodiffMode::Reverse,
609            normalization: NormalizationMode::Verify,
610            partitioning: Partitioning::FileGroups,
611            memory: MemoryPlan::host_device(
612                MemoryBudget::PercentAvailable(0.5),
613                MemoryBudget::Bytes(1 << 30),
614            ),
615        };
616
617        let json = serde_json::to_string(&options).unwrap();
618        assert_eq!(
619            serde_json::from_str::<ExecutionOptions>(&json).unwrap(),
620            options
621        );
622    }
623
624    #[test]
625    fn execution_selects_nested_cpu_options() {
626        let serial = Execution::local(ExecutionOptions {
627            device: Device::Cpu(CpuOptions {
628                threads: ThreadPolicy::Serial,
629                jit: JitPolicy::Disabled,
630            }),
631            ..ExecutionOptions::default()
632        })
633        .unwrap();
634        assert!(!serial.is_parallel());
635        assert_eq!(serial.jit_policy(), JitPolicy::Disabled);
636        assert_eq!(serial.precision(), Precision::F64);
637
638        let fixed = Execution::local(ExecutionOptions {
639            device: Device::Cpu(CpuOptions {
640                threads: ThreadPolicy::Fixed(2),
641                ..CpuOptions::default()
642            }),
643            ..ExecutionOptions::default()
644        })
645        .unwrap();
646        assert_eq!(fixed.install(rayon::current_num_threads), 2);
647    }
648
649    #[test]
650    fn unavailable_execution_modes_return_capability_errors() {
651        #[cfg(not(feature = "wgpu"))]
652        assert!(matches!(
653            Execution::local(ExecutionOptions {
654                device: Device::Gpu(GpuOptions {
655                    backend: GpuBackend::Wgpu,
656                    ..GpuOptions::default()
657                }),
658                ..ExecutionOptions::default()
659            }),
660            Err(RuntimeError::Execution(ExecutionError::GpuUnavailable(
661                GpuBackend::Wgpu
662            )))
663        ));
664        #[cfg(feature = "wgpu")]
665        assert!(
666            Execution::local(ExecutionOptions {
667                device: Device::Gpu(GpuOptions {
668                    backend: GpuBackend::Wgpu,
669                    ..GpuOptions::default()
670                }),
671                ..ExecutionOptions::default()
672            })
673            .is_ok()
674        );
675        let f32 = Execution::local(ExecutionOptions {
676            device: Device::Cpu(CpuOptions::default()),
677            precision: Precision::F32,
678            ..ExecutionOptions::default()
679        })
680        .unwrap();
681        assert_eq!(f32.precision(), Precision::F32);
682
683        let reverse = Execution::local(ExecutionOptions {
684            autodiff: AutodiffMode::Reverse,
685            ..ExecutionOptions::default()
686        })
687        .unwrap();
688        assert_eq!(reverse.autodiff_mode(), AutodiffMode::Reverse);
689
690        let reverse_f32 = Execution::local(ExecutionOptions {
691            precision: Precision::F32,
692            autodiff: AutodiffMode::Reverse,
693            ..ExecutionOptions::default()
694        })
695        .unwrap();
696        assert_eq!(reverse_f32.precision(), Precision::F32);
697        assert_eq!(reverse_f32.autodiff_mode(), AutodiffMode::Reverse);
698    }
699}