Skip to main content

laddu_runtime/
backend.rs

1use laddu_compile::{CompiledModel, ReductionPlan};
2#[cfg(feature = "wgpu")]
3use laddu_data::BatchLayout;
4use laddu_data::data::Dataset;
5use laddu_data::data::EventBatch;
6#[cfg(feature = "wgpu")]
7use laddu_data::data::{CacheStorage, MemoryPolicy, accurate::AccurateF64};
8#[cfg(feature = "wgpu")]
9use laddu_data::schema::Precision as DataPrecision;
10use laddu_expr::parameters::ParamValues;
11#[cfg(feature = "wgpu")]
12use laddu_memory::{MemoryFitRequest, MemoryFootprint};
13use num::complex::Complex64;
14
15use crate::{
16    CpuBackend, CpuPlan, CpuPreparedDataset, Execution, PreparedDatasetStats, ReductionEvaluation,
17    RuntimeError, RuntimeResult,
18};
19
20/// A compiled model prepared for a concrete execution backend.
21#[derive(Clone, Debug)]
22pub enum PreparedModel {
23    /// A model prepared for CPU execution.
24    Cpu(Box<CpuPlan>),
25    #[cfg(feature = "wgpu")]
26    /// A model prepared for WebGPU execution.
27    Wgpu(WgpuPlan),
28}
29
30/// A dataset prepared for a concrete execution backend.
31#[derive(Clone, Debug)]
32pub enum PreparedDataset {
33    /// A dataset prepared for CPU execution.
34    Cpu(CpuPreparedDataset),
35    #[cfg(feature = "wgpu")]
36    /// A dataset prepared for WebGPU execution.
37    Wgpu(WgpuPreparedDataset),
38}
39
40impl PreparedDataset {
41    /// Returns statistics collected while preparing the dataset.
42    pub fn stats(&self) -> &PreparedDatasetStats {
43        match self {
44            Self::Cpu(dataset) => dataset.stats(),
45            #[cfg(feature = "wgpu")]
46            Self::Wgpu(dataset) => dataset.stats(),
47        }
48    }
49}
50
51impl PreparedModel {
52    /// Evaluates the model for every event in a batch.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`RuntimeError`] when parameters or event columns are
57    /// incompatible, evaluation fails, or a matrix solve is singular.
58    pub fn evaluate_batch(
59        &self,
60        params: &ParamValues,
61        batch: &EventBatch,
62    ) -> RuntimeResult<Vec<Complex64>> {
63        match self {
64            Self::Cpu(plan) => plan.evaluate_batch(params, batch),
65            #[cfg(feature = "wgpu")]
66            Self::Wgpu(plan) => plan
67                .kernel
68                .evaluate_batch(&plan.context, params, batch)
69                .map(|values| {
70                    values
71                        .into_iter()
72                        .map(|(re, im)| Complex64::new(re, im))
73                        .collect()
74                })
75                .map_err(wgpu_error),
76        }
77    }
78
79    /// Evaluates the model and its free-parameter gradient for every event in a batch.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`RuntimeError`] when inputs are incompatible, differentiation
84    /// or evaluation fails, or the selected backend lacks event-wise gradients.
85    pub fn evaluate_batch_with_gradient(
86        &self,
87        params: &ParamValues,
88        batch: &EventBatch,
89    ) -> RuntimeResult<Vec<crate::ValueGradient>> {
90        match self {
91            Self::Cpu(plan) => plan.evaluate_batch_with_gradient(params, batch),
92            #[cfg(feature = "wgpu")]
93            Self::Wgpu(_) => Err(RuntimeError::Wgpu(
94                "event-wise model gradients are not implemented by the WGPU backend".into(),
95            )),
96        }
97    }
98
99    /// Prepares a compiled model for the supplied execution context.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`RuntimeError`] when model lowering, differentiation, backend
104    /// initialization, or precision selection fails.
105    pub fn prepare(model: &CompiledModel, execution: &Execution) -> RuntimeResult<Self> {
106        #[cfg(feature = "wgpu")]
107        if let Some(context) = execution.wgpu_context() {
108            return Ok(Self::Wgpu(WgpuPlan {
109                context: context.clone(),
110                preparation_params: model.params().default_values(),
111                kernel: std::sync::Arc::new(
112                    laddu_wgpu::WgpuScalarKernel::compile(context, model).map_err(wgpu_error)?,
113                ),
114            }));
115        }
116        Ok(Self::Cpu(Box::new(
117            CpuBackend.prepare_for_execution(model, execution)?,
118        )))
119    }
120
121    /// Prepares a dataset for repeated evaluation with this model.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`RuntimeError`] when the dataset cannot be read or cached, its
126    /// schema is incompatible, or backend preparation fails.
127    pub fn prepare_dataset(
128        &self,
129        execution: &Execution,
130        dataset: &Dataset,
131    ) -> RuntimeResult<PreparedDataset> {
132        match self {
133            Self::Cpu(plan) => Ok(PreparedDataset::Cpu(
134                plan.prepare_dataset(execution, dataset)?,
135            )),
136            #[cfg(feature = "wgpu")]
137            Self::Wgpu(plan) => plan
138                .prepare_dataset(execution, dataset)
139                .map(PreparedDataset::Wgpu),
140        }
141    }
142
143    /// Executes a weighted scalar reduction over a prepared dataset.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`RuntimeError`] when model and dataset backends differ, inputs
148    /// are incompatible, evaluation fails, or the reduction domain is invalid.
149    pub fn reduce(
150        &self,
151        execution: &Execution,
152        params: &ParamValues,
153        dataset: &PreparedDataset,
154        reduction: ReductionPlan,
155    ) -> RuntimeResult<f64> {
156        #[allow(unreachable_patterns)]
157        match (self, dataset) {
158            (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
159                plan.reduce(execution, params, dataset, reduction)
160            }
161            #[cfg(feature = "wgpu")]
162            (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
163                plan.reduce(execution, params, dataset, reduction)
164            }
165            _ => Err(RuntimeError::InvalidShape {
166                index: 0,
167                message: "prepared model and dataset use different backends".into(),
168            }),
169        }
170    }
171
172    /// Executes a weighted reduction and computes its free-parameter gradient.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`RuntimeError`] when model and dataset backends differ,
177    /// differentiation or evaluation fails, or the reduction domain is invalid.
178    pub fn reduce_with_gradient(
179        &self,
180        execution: &Execution,
181        params: &ParamValues,
182        dataset: &PreparedDataset,
183        reduction: ReductionPlan,
184    ) -> RuntimeResult<ReductionEvaluation> {
185        #[allow(unreachable_patterns)]
186        match (self, dataset) {
187            (Self::Cpu(plan), PreparedDataset::Cpu(dataset)) => {
188                plan.reduce_with_gradient(execution, params, dataset, reduction)
189            }
190            #[cfg(feature = "wgpu")]
191            (Self::Wgpu(plan), PreparedDataset::Wgpu(dataset)) => {
192                plan.reduce_with_gradient(execution, params, dataset, reduction)
193            }
194            _ => Err(RuntimeError::InvalidShape {
195                index: 0,
196                message: "prepared model and dataset use different backends".into(),
197            }),
198        }
199    }
200}
201
202#[cfg(feature = "wgpu")]
203/// A compiled model prepared for WebGPU execution.
204#[derive(Clone)]
205pub struct WgpuPlan {
206    context: std::sync::Arc<laddu_wgpu::WgpuContext>,
207    preparation_params: ParamValues,
208    kernel: std::sync::Arc<laddu_wgpu::WgpuScalarKernel>,
209}
210
211#[cfg(feature = "wgpu")]
212impl std::fmt::Debug for WgpuPlan {
213    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        formatter
215            .debug_struct("WgpuPlan")
216            .field("adapter", &self.context.info().name)
217            .finish_non_exhaustive()
218    }
219}
220
221#[cfg(feature = "wgpu")]
222/// Dataset storage prepared for WebGPU evaluation.
223#[derive(Clone)]
224pub enum WgpuPreparedDataset {
225    /// GPU-resident prepared batches.
226    Resident {
227        /// Prepared GPU batches.
228        batches: std::sync::Arc<[laddu_wgpu::WgpuPreparedBatch]>,
229        /// Preparation statistics.
230        stats: PreparedDatasetStats,
231        /// Persistent device-memory reservation.
232        memory_lease: crate::MemoryLease,
233    },
234    /// Source data streamed and prepared one batch at a time.
235    Streaming {
236        /// Source dataset.
237        dataset: Dataset,
238        /// Read plan used on each pass.
239        read_plan: laddu_data::io::ReadPlan,
240        /// Reusable prepared-batch workspace.
241        workspace: std::sync::Arc<std::sync::Mutex<Option<laddu_wgpu::WgpuPreparedBatch>>>,
242        /// Preparation statistics.
243        stats: PreparedDatasetStats,
244        /// Peak transient device bytes reserved during reductions.
245        transient_bytes: u64,
246    },
247}
248
249#[cfg(feature = "wgpu")]
250impl std::fmt::Debug for WgpuPreparedDataset {
251    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252        formatter
253            .debug_struct("WgpuPreparedDataset")
254            .field("stats", self.stats())
255            .finish_non_exhaustive()
256    }
257}
258
259#[cfg(feature = "wgpu")]
260impl WgpuPreparedDataset {
261    /// Returns statistics collected while preparing the dataset.
262    pub fn stats(&self) -> &PreparedDatasetStats {
263        match self {
264            Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
265        }
266    }
267}
268
269#[cfg(feature = "wgpu")]
270impl WgpuPlan {
271    fn prepare_dataset(
272        &self,
273        execution: &Execution,
274        dataset: &Dataset,
275    ) -> RuntimeResult<WgpuPreparedDataset> {
276        let read_plan = execution.read_plan(dataset.read_plan());
277        let mut read_plan = read_plan;
278        let known_local_events = dataset
279            .num_events()
280            .map_err(|error| RuntimeError::Data(error.to_string()))?
281            .and_then(|events| usize::try_from(events).ok());
282        let discovered_events =
283            if known_local_events.is_none() && dataset.memory_policy() != MemoryPolicy::Streaming {
284                let local = (|| {
285                    let mut events = 0;
286                    for batch in dataset
287                        .stream_with_plan(read_plan)
288                        .map_err(|error| RuntimeError::Data(error.to_string()))?
289                    {
290                        events += batch
291                            .map_err(|error| RuntimeError::Data(error.to_string()))?
292                            .len();
293                    }
294                    Ok::<_, RuntimeError>(events)
295                })();
296                if !execution.all_succeeded(local.is_ok()) {
297                    return local.and(Err(RuntimeError::DistributedPeerFailure));
298                }
299                Some(local?)
300            } else {
301                None
302            };
303        let local_event_limit = known_local_events
304            .or(discovered_events)
305            .unwrap_or(usize::MAX);
306        let prepared_footprint = self
307            .kernel
308            .prepared_memory_footprint(&self.preparation_params)
309            .map_err(|error| RuntimeError::Data(format!("GPU working-set overflow: {error}")))?;
310        let schema = dataset
311            .schema()
312            .map_err(|error| RuntimeError::Data(error.to_string()))?;
313        let host_footprint = BatchLayout::from_schema(&schema)
314            .schema_working_set(DataPrecision::F64, 2)
315            .map_err(|error| RuntimeError::Data(format!("host working-set overflow: {error}")))?;
316        let host_decision = MemoryFitRequest {
317            label: "WGPU host staging".into(),
318            footprint: host_footprint,
319            available_bytes: execution.host_memory().remaining(),
320            event_limit: local_event_limit,
321            strategy: "bounded host staging".into(),
322        }
323        .evaluate()?;
324        let host_chunks = local_event_limit
325            .saturating_add(host_decision.chunk_events.saturating_sub(1))
326            / host_decision.chunk_events.max(1);
327        let resident_footprint = MemoryFootprint::fixed(prepared_footprint.fixed_bytes)
328            .checked_scale_usize(host_chunks)
329            .and_then(|fixed| {
330                fixed.checked_add(MemoryFootprint::per_event(
331                    prepared_footprint.bytes_per_event,
332                ))
333            })
334            .map_err(|error| RuntimeError::Data(format!("GPU working-set overflow: {error}")))?;
335        let resident_peak = resident_footprint.peak_bytes(local_event_limit);
336        let device_pool = execution
337            .device_memory()
338            .ok_or_else(|| RuntimeError::Wgpu("GPU execution has no device memory pool".into()))?;
339        let device_available = device_pool.remaining();
340        let storage = match dataset.memory_policy() {
341            MemoryPolicy::Streaming => CacheStorage::Streaming,
342            MemoryPolicy::Resident => {
343                if resident_peak > device_available {
344                    return Err(laddu_memory::MemoryError::BudgetExceeded {
345                        resource: "device".into(),
346                        requested: resident_peak,
347                        remaining: device_available,
348                    }
349                    .into());
350                }
351                CacheStorage::Resident
352            }
353            MemoryPolicy::Fastest if resident_peak <= device_available => CacheStorage::Resident,
354            MemoryPolicy::Fastest => CacheStorage::Streaming,
355        };
356        let memory_lease = if storage == CacheStorage::Resident {
357            Some(device_pool.reserve(resident_peak)?)
358        } else {
359            None
360        };
361        let device_decision = if storage == CacheStorage::Resident {
362            MemoryFitRequest {
363                label: "WGPU prepared dataset".into(),
364                footprint: resident_footprint,
365                available_bytes: device_available,
366                event_limit: local_event_limit,
367                strategy: "resident".into(),
368            }
369            .evaluate_resident(host_decision.chunk_events)?
370        } else {
371            MemoryFitRequest {
372                label: "WGPU prepared dataset".into(),
373                footprint: prepared_footprint,
374                available_bytes: device_available,
375                event_limit: local_event_limit,
376                strategy: "streaming".into(),
377            }
378            .evaluate()?
379        };
380        let chunk_events = device_decision
381            .chunk_events
382            .min(host_decision.chunk_events)
383            .max(1);
384        read_plan.chunk_size = Some(
385            read_plan
386                .chunk_size
387                .map_or(chunk_events, |manual| manual.min(chunk_events))
388                .max(1),
389        );
390        execution.record_memory_decision(device_decision.clone());
391        execution.record_memory_decision(host_decision);
392        let local = (|| {
393            let mut batches = Vec::new();
394            let mut events = 0;
395            let mut batch_count = 0;
396            let mut sum_weights = AccurateF64::zero();
397            for batch in dataset
398                .stream_with_plan(read_plan)
399                .map_err(|error| RuntimeError::Data(error.to_string()))?
400            {
401                let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
402                events += batch.len();
403                batch_count += 1;
404                for row in 0..batch.len() {
405                    sum_weights.push(batch.weights_at(row));
406                }
407                if storage == CacheStorage::Resident {
408                    batches.push(
409                        self.kernel
410                            .prepare_batch(&self.context, &self.preparation_params, &batch)
411                            .map_err(wgpu_error)?,
412                    );
413                }
414            }
415            Ok::<_, RuntimeError>((batches, events, batch_count, sum_weights.finish()))
416        })();
417        if !execution.all_succeeded(local.is_ok()) {
418            return local.and(Err(RuntimeError::DistributedPeerFailure));
419        }
420        let (batches, events, batch_count, sum_weights) = local?;
421        let resident_bytes = batches
422            .iter()
423            .map(laddu_wgpu::WgpuPreparedBatch::resident_bytes)
424            .sum();
425        let stats = PreparedDatasetStats::new(
426            events,
427            execution.sum_usize(events),
428            batch_count,
429            execution.sum_f64(sum_weights),
430            resident_bytes,
431            storage,
432        );
433        Ok(match storage {
434            CacheStorage::Resident => WgpuPreparedDataset::Resident {
435                batches: batches.into(),
436                stats,
437                memory_lease: memory_lease.ok_or_else(|| {
438                    RuntimeError::Wgpu("resident GPU dataset did not reserve device memory".into())
439                })?,
440            },
441            CacheStorage::Streaming => WgpuPreparedDataset::Streaming {
442                dataset: dataset.clone(),
443                read_plan,
444                workspace: Default::default(),
445                stats,
446                transient_bytes: device_decision.estimated_peak_bytes,
447            },
448        })
449    }
450
451    fn reduce(
452        &self,
453        execution: &Execution,
454        params: &ParamValues,
455        dataset: &WgpuPreparedDataset,
456        reduction: ReductionPlan,
457    ) -> RuntimeResult<f64> {
458        let mut total = AccurateF64::zero();
459        match dataset {
460            WgpuPreparedDataset::Resident { batches, .. } => {
461                for batch in batches.iter() {
462                    total.push(
463                        self.kernel
464                            .reduce_prepared_batch(&self.context, params, batch, reduction)
465                            .map_err(wgpu_error)?,
466                    );
467                }
468            }
469            WgpuPreparedDataset::Streaming {
470                dataset,
471                read_plan,
472                workspace,
473                transient_bytes,
474                ..
475            } => {
476                let _memory = execution
477                    .device_memory()
478                    .ok_or_else(|| {
479                        RuntimeError::Wgpu("GPU execution has no device memory pool".into())
480                    })?
481                    .reserve(*transient_bytes)?;
482                let mut workspace = workspace.lock().map_err(|_| {
483                    RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
484                })?;
485                for batch in dataset
486                    .stream_with_plan(*read_plan)
487                    .map_err(|error| RuntimeError::Data(error.to_string()))?
488                {
489                    let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
490                    if let Some(prepared) = workspace.as_mut() {
491                        if !self
492                            .kernel
493                            .refresh_batch(
494                                &self.context,
495                                &self.preparation_params,
496                                &batch,
497                                prepared,
498                            )
499                            .map_err(wgpu_error)?
500                        {
501                            *prepared = self
502                                .kernel
503                                .prepare_batch(&self.context, &self.preparation_params, &batch)
504                                .map_err(wgpu_error)?;
505                        }
506                    } else {
507                        *workspace = Some(
508                            self.kernel
509                                .prepare_batch(&self.context, &self.preparation_params, &batch)
510                                .map_err(wgpu_error)?,
511                        );
512                    }
513                    total.push(
514                        self.kernel
515                            .reduce_prepared_batch(
516                                &self.context,
517                                params,
518                                workspace
519                                    .as_ref()
520                                    .expect("streaming workspace was initialized"),
521                                reduction,
522                            )
523                            .map_err(wgpu_error)?,
524                    );
525                }
526            }
527        }
528        Ok(execution.sum_f64(total.finish()))
529    }
530
531    fn reduce_with_gradient(
532        &self,
533        execution: &Execution,
534        params: &ParamValues,
535        dataset: &WgpuPreparedDataset,
536        reduction: ReductionPlan,
537    ) -> RuntimeResult<ReductionEvaluation> {
538        let mut total = AccurateF64::zero();
539        let mut gradient = (0..params.layout().n_free())
540            .map(|_| AccurateF64::zero())
541            .collect::<Vec<_>>();
542        let mut consume = |batch: &laddu_wgpu::WgpuPreparedBatch| -> RuntimeResult<()> {
543            let (value, values) = self
544                .kernel
545                .reduce_prepared_batch_with_gradient(&self.context, params, batch, reduction)
546                .map_err(wgpu_error)?;
547            total.push(value);
548            for (sum, value) in gradient.iter_mut().zip(values) {
549                sum.push(value);
550            }
551            Ok(())
552        };
553        match dataset {
554            WgpuPreparedDataset::Resident { batches, .. } => {
555                for batch in batches.iter() {
556                    consume(batch)?;
557                }
558            }
559            WgpuPreparedDataset::Streaming {
560                dataset,
561                read_plan,
562                workspace,
563                transient_bytes,
564                ..
565            } => {
566                let _memory = execution
567                    .device_memory()
568                    .ok_or_else(|| {
569                        RuntimeError::Wgpu("GPU execution has no device memory pool".into())
570                    })?
571                    .reserve(*transient_bytes)?;
572                let mut workspace = workspace.lock().map_err(|_| {
573                    RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
574                })?;
575                for batch in dataset
576                    .stream_with_plan(*read_plan)
577                    .map_err(|error| RuntimeError::Data(error.to_string()))?
578                {
579                    let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
580                    if let Some(prepared) = workspace.as_mut() {
581                        if !self
582                            .kernel
583                            .refresh_batch(
584                                &self.context,
585                                &self.preparation_params,
586                                &batch,
587                                prepared,
588                            )
589                            .map_err(wgpu_error)?
590                        {
591                            *prepared = self
592                                .kernel
593                                .prepare_batch(&self.context, &self.preparation_params, &batch)
594                                .map_err(wgpu_error)?;
595                        }
596                    } else {
597                        *workspace = Some(
598                            self.kernel
599                                .prepare_batch(&self.context, &self.preparation_params, &batch)
600                                .map_err(wgpu_error)?,
601                        );
602                    }
603                    consume(
604                        workspace
605                            .as_ref()
606                            .expect("streaming workspace was initialized"),
607                    )?;
608                }
609            }
610        }
611        let gradient = gradient
612            .into_iter()
613            .map(|sum| execution.sum_f64(sum.finish()))
614            .collect();
615        Ok(ReductionEvaluation::new(
616            execution.sum_f64(total.finish()),
617            gradient,
618        ))
619    }
620}
621
622#[cfg(feature = "wgpu")]
623fn wgpu_error(error: laddu_wgpu::WgpuError) -> RuntimeError {
624    RuntimeError::Wgpu(error.to_string())
625}
626
627#[cfg(all(test, feature = "wgpu"))]
628mod tests {
629    use std::sync::Arc;
630
631    use laddu_compile::{CompiledModel, ReductionPlan};
632    use laddu_data::{
633        data::{Dataset, EventBatch, OwnedEvent},
634        schema::Schema,
635    };
636    use laddu_expr::{complex, event_scalar, parameter};
637
638    use super::*;
639    use crate::{CpuOptions, Device, ExecutionOptions, GpuBackend, GpuOptions, Precision};
640
641    #[test]
642    #[ignore = "requires a WGPU-compatible hardware adapter"]
643    fn wgpu_resident_and_streaming_reductions_match_f32_cpu() {
644        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
645        let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
646        let x = event_scalar("x");
647        let expression = (x.clone() * scale.clone() + offset.clone()).sin()
648            + complex(scale, offset).norm_sqr()
649            + 2.0;
650        let model = CompiledModel::from_expr(&expression).unwrap();
651        let params = model.params().default_values();
652        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
653        let dataset = Dataset::from_batches(vec![
654            EventBatch::from_events(
655                schema.clone(),
656                [
657                    OwnedEvent::weighted(vec![], vec![0.25], 0.5),
658                    OwnedEvent::weighted(vec![], vec![0.75], 1.5),
659                ],
660            )
661            .unwrap(),
662            EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![1.25], 2.0)])
663                .unwrap(),
664        ])
665        .unwrap();
666        let wgpu_execution = Execution::local(ExecutionOptions {
667            device: Device::Gpu(GpuOptions {
668                backend: GpuBackend::Wgpu,
669                ..GpuOptions::default()
670            }),
671            memory: crate::MemoryPlan::host_device(
672                crate::MemoryBudget::Auto,
673                crate::MemoryBudget::Bytes(256),
674            ),
675            precision: Precision::F32,
676            ..ExecutionOptions::default()
677        })
678        .unwrap();
679        let cpu_execution = Execution::local(ExecutionOptions {
680            device: Device::Cpu(CpuOptions::default()),
681            precision: Precision::F32,
682            ..ExecutionOptions::default()
683        })
684        .unwrap();
685        let wgpu = PreparedModel::prepare(&model, &wgpu_execution).unwrap();
686        let cpu = PreparedModel::prepare(&model, &cpu_execution).unwrap();
687        let resident = wgpu
688            .prepare_dataset(&wgpu_execution, &dataset.clone().resident())
689            .unwrap();
690        let streaming = wgpu
691            .prepare_dataset(&wgpu_execution, &dataset.clone().streaming())
692            .unwrap();
693        let cpu_data = cpu.prepare_dataset(&cpu_execution, &dataset).unwrap();
694
695        assert_eq!(resident.stats().storage(), CacheStorage::Resident);
696        assert_eq!(streaming.stats().storage(), CacheStorage::Streaming);
697        assert!(resident.stats().resident_bytes() > 0);
698        assert_eq!(streaming.stats().resident_bytes(), 0);
699
700        let cpu_reduction = cpu
701            .reduce_with_gradient(
702                &cpu_execution,
703                &params,
704                &cpu_data,
705                ReductionPlan::weighted_real(),
706            )
707            .unwrap();
708        let resident_reduction = wgpu
709            .reduce_with_gradient(
710                &wgpu_execution,
711                &params,
712                &resident,
713                ReductionPlan::weighted_real(),
714            )
715            .unwrap();
716        let streaming_reduction = wgpu
717            .reduce_with_gradient(
718                &wgpu_execution,
719                &params,
720                &streaming,
721                ReductionPlan::weighted_real(),
722            )
723            .unwrap();
724
725        for actual in [&resident_reduction, &streaming_reduction] {
726            assert!((actual.value() - cpu_reduction.value()).abs() <= 1.0e-4);
727            assert_eq!(actual.gradient().len(), cpu_reduction.gradient().len());
728            for (actual, expected) in actual.gradient().iter().zip(cpu_reduction.gradient()) {
729                assert!((actual - expected).abs() <= 1.0e-4);
730            }
731        }
732        assert!((resident_reduction.value() - streaming_reduction.value()).abs() <= 1.0e-6);
733        for (resident, streaming) in resident_reduction
734            .gradient()
735            .iter()
736            .zip(streaming_reduction.gradient())
737        {
738            assert!((resident - streaming).abs() <= 1.0e-6);
739        }
740    }
741}