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, 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/// 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                    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    /// Creates a distributed execution context over the supplied MPI communicator.
359    ///
360    /// # Errors
361    ///
362    /// Returns [`RuntimeError`] when local execution initialization fails.
363    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    /// Returns the device requested when this context was created.
389    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    /// Returns the resolved numeric precision.
399    pub fn precision(&self) -> Precision {
400        self.precision
401    }
402
403    /// Returns the automatic-differentiation strategy.
404    pub fn autodiff_mode(&self) -> AutodiffMode {
405        self.autodiff
406    }
407
408    /// Returns the accepted-normalization policy.
409    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    /// Returns the CPU worker-thread policy.
418    pub fn thread_policy(&self) -> ThreadPolicy {
419        self.threads
420    }
421
422    /// Returns the CPU JIT policy.
423    pub fn jit_policy(&self) -> JitPolicy {
424        self.jit
425    }
426
427    /// Returns the distributed dataset-partitioning strategy.
428    pub fn partitioning(&self) -> Partitioning {
429        self.partitioning
430    }
431
432    /// Returns the live memory state shared by this execution.
433    pub fn memory_state(&self) -> &MemoryState {
434        &self.memory_state
435    }
436
437    /// Returns the resolved host-memory pool.
438    pub fn host_memory(&self) -> &MemoryPool {
439        &self.host_memory
440    }
441
442    /// Returns the resolved accelerator-memory pool, if any.
443    pub fn device_memory(&self) -> Option<&MemoryPool> {
444        self.device_memory.as_ref()
445    }
446
447    /// Returns current physical-resource memory information.
448    pub fn memory_report(&self) -> MemoryReport {
449        self.memory_state.report()
450    }
451
452    /// Returns the resolved execution-pool reports.
453    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    /// Returns memory-derived decisions recorded by this execution.
460    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    /// Records a memory-planning decision for later diagnostics.
468    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    /// Returns the zero-based rank of this process.
476    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    /// Returns the number of participating ranks.
485    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    /// Returns whether this context spans more than one rank.
494    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    // Common launchers expose node-local process counts. Falling back to the
573    // world size is conservative on multi-node jobs and prevents accidental
574    // host/device overcommit when launcher metadata is unavailable.
575    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}