Skip to main content

laddu_runtime/
backend.rs

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