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: Vec<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 local_event_limit = dataset
273            .num_events()
274            .map_err(|error| RuntimeError::Data(error.to_string()))?
275            .and_then(|events| usize::try_from(events).ok())
276            .unwrap_or(usize::MAX);
277        let fixed = self
278            .kernel
279            .prepared_memory_estimate(&self.preparation_params, 0);
280        let one = self
281            .kernel
282            .prepared_memory_estimate(&self.preparation_params, 1);
283        let per_event = one.saturating_sub(fixed);
284        let schema = dataset
285            .schema()
286            .map_err(|error| RuntimeError::Data(error.to_string()))?;
287        let host_bytes_per_event =
288            (4 * schema.n_p4s() + schema.n_scalars() + usize::from(schema.has_weight()))
289                .saturating_mul(size_of::<f64>())
290                .saturating_mul(2);
291        let host_decision = crate::MemoryDecision::fit(
292            "WGPU host staging",
293            0,
294            u64::try_from(host_bytes_per_event).unwrap_or(u64::MAX),
295            execution.host_memory().remaining(),
296            local_event_limit,
297            "bounded host staging",
298        )?;
299        let host_chunks = local_event_limit
300            .saturating_add(host_decision.chunk_events.saturating_sub(1))
301            / host_decision.chunk_events.max(1);
302        let full = per_event
303            .saturating_mul(local_event_limit)
304            .saturating_add(fixed.saturating_mul(host_chunks));
305        let device_pool = execution
306            .device_memory()
307            .ok_or_else(|| RuntimeError::Wgpu("GPU execution has no device memory pool".into()))?;
308        let storage = match dataset.memory_policy() {
309            MemoryPolicy::Streaming => CacheStorage::Streaming,
310            MemoryPolicy::Resident => {
311                if u64::try_from(full).unwrap_or(u64::MAX) > device_pool.remaining() {
312                    return Err(laddu_memory::MemoryError::BudgetExceeded {
313                        resource: "device".into(),
314                        requested: u64::try_from(full).unwrap_or(u64::MAX),
315                        remaining: device_pool.remaining(),
316                    }
317                    .into());
318                }
319                CacheStorage::Resident
320            }
321            MemoryPolicy::Fastest
322                if u64::try_from(full).unwrap_or(u64::MAX) <= device_pool.remaining() =>
323            {
324                CacheStorage::Resident
325            }
326            MemoryPolicy::Fastest => CacheStorage::Streaming,
327        };
328        let memory_lease = if storage == CacheStorage::Resident {
329            Some(device_pool.reserve(u64::try_from(full).unwrap_or(u64::MAX))?)
330        } else {
331            None
332        };
333        let device_decision = if storage == CacheStorage::Resident {
334            crate::MemoryDecision {
335                label: "WGPU prepared dataset".into(),
336                fixed_bytes: u64::try_from(fixed.saturating_mul(host_chunks)).unwrap_or(u64::MAX),
337                bytes_per_event: u64::try_from(per_event).unwrap_or(u64::MAX),
338                chunk_events: host_decision.chunk_events,
339                estimated_peak_bytes: u64::try_from(full).unwrap_or(u64::MAX),
340                actual_high_water_bytes: None,
341                strategy: "resident".into(),
342            }
343        } else {
344            crate::MemoryDecision::fit(
345                "WGPU prepared dataset",
346                u64::try_from(fixed).unwrap_or(u64::MAX),
347                u64::try_from(per_event).unwrap_or(u64::MAX),
348                device_pool.remaining(),
349                local_event_limit,
350                "streaming",
351            )?
352        };
353        let chunk_events = device_decision
354            .chunk_events
355            .min(host_decision.chunk_events)
356            .max(1);
357        read_plan.chunk_size = Some(
358            read_plan
359                .chunk_size
360                .map_or(chunk_events, |manual| manual.min(chunk_events))
361                .max(1),
362        );
363        execution.record_memory_decision(device_decision.clone());
364        execution.record_memory_decision(host_decision);
365        let mut batches = Vec::new();
366        let mut events = 0;
367        let mut batch_count = 0;
368        let mut sum_weights = AccurateF64::zero();
369        for batch in dataset
370            .batches_with_plan(read_plan)
371            .map_err(|error| RuntimeError::Data(error.to_string()))?
372        {
373            let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
374            events += batch.len();
375            batch_count += 1;
376            for row in 0..batch.len() {
377                sum_weights.push(batch.weights_at(row));
378            }
379            if storage == CacheStorage::Resident {
380                batches.push(
381                    self.kernel
382                        .prepare_batch(&self.context, &self.preparation_params, &batch)
383                        .map_err(wgpu_error)?,
384                );
385            }
386        }
387        let resident_bytes = batches
388            .iter()
389            .map(laddu_wgpu::WgpuPreparedBatch::resident_bytes)
390            .sum();
391        let stats = PreparedDatasetStats::new(
392            events,
393            execution.sum_usize(events),
394            batch_count,
395            execution.sum_f64(sum_weights.finish()),
396            resident_bytes,
397            storage,
398        );
399        Ok(match storage {
400            CacheStorage::Resident => WgpuPreparedDataset::Resident {
401                batches,
402                stats,
403                memory_lease: memory_lease.ok_or_else(|| {
404                    RuntimeError::Wgpu("resident GPU dataset did not reserve device memory".into())
405                })?,
406            },
407            CacheStorage::Streaming => WgpuPreparedDataset::Streaming {
408                dataset: dataset.clone(),
409                read_plan,
410                workspace: Default::default(),
411                stats,
412                transient_bytes: device_decision.estimated_peak_bytes,
413            },
414        })
415    }
416
417    fn reduce(
418        &self,
419        execution: &Execution,
420        params: &ParamValues,
421        dataset: &WgpuPreparedDataset,
422        reduction: ReductionPlan,
423    ) -> RuntimeResult<f64> {
424        let mut total = AccurateF64::zero();
425        match dataset {
426            WgpuPreparedDataset::Resident { batches, .. } => {
427                for batch in batches {
428                    total.push(
429                        self.kernel
430                            .reduce_prepared_batch(&self.context, params, batch, reduction)
431                            .map_err(wgpu_error)?,
432                    );
433                }
434            }
435            WgpuPreparedDataset::Streaming {
436                dataset,
437                read_plan,
438                workspace,
439                transient_bytes,
440                ..
441            } => {
442                let _memory = execution
443                    .device_memory()
444                    .ok_or_else(|| {
445                        RuntimeError::Wgpu("GPU execution has no device memory pool".into())
446                    })?
447                    .reserve(*transient_bytes)?;
448                let mut workspace = workspace.lock().map_err(|_| {
449                    RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
450                })?;
451                for batch in dataset
452                    .batches_with_plan(*read_plan)
453                    .map_err(|error| RuntimeError::Data(error.to_string()))?
454                {
455                    let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
456                    if let Some(prepared) = workspace.as_mut() {
457                        if !self
458                            .kernel
459                            .refresh_batch(
460                                &self.context,
461                                &self.preparation_params,
462                                &batch,
463                                prepared,
464                            )
465                            .map_err(wgpu_error)?
466                        {
467                            *prepared = self
468                                .kernel
469                                .prepare_batch(&self.context, &self.preparation_params, &batch)
470                                .map_err(wgpu_error)?;
471                        }
472                    } else {
473                        *workspace = Some(
474                            self.kernel
475                                .prepare_batch(&self.context, &self.preparation_params, &batch)
476                                .map_err(wgpu_error)?,
477                        );
478                    }
479                    total.push(
480                        self.kernel
481                            .reduce_prepared_batch(
482                                &self.context,
483                                params,
484                                workspace
485                                    .as_ref()
486                                    .expect("streaming workspace was initialized"),
487                                reduction,
488                            )
489                            .map_err(wgpu_error)?,
490                    );
491                }
492            }
493        }
494        Ok(execution.sum_f64(total.finish()))
495    }
496
497    fn reduce_with_gradient(
498        &self,
499        execution: &Execution,
500        params: &ParamValues,
501        dataset: &WgpuPreparedDataset,
502        reduction: ReductionPlan,
503    ) -> RuntimeResult<ReductionEvaluation> {
504        let mut total = AccurateF64::zero();
505        let mut gradient = (0..params.layout().n_free())
506            .map(|_| AccurateF64::zero())
507            .collect::<Vec<_>>();
508        let mut consume = |batch: &laddu_wgpu::WgpuPreparedBatch| -> RuntimeResult<()> {
509            let (value, values) = self
510                .kernel
511                .reduce_prepared_batch_with_gradient(&self.context, params, batch, reduction)
512                .map_err(wgpu_error)?;
513            total.push(value);
514            for (sum, value) in gradient.iter_mut().zip(values) {
515                sum.push(value);
516            }
517            Ok(())
518        };
519        match dataset {
520            WgpuPreparedDataset::Resident { batches, .. } => {
521                for batch in batches {
522                    consume(batch)?;
523                }
524            }
525            WgpuPreparedDataset::Streaming {
526                dataset,
527                read_plan,
528                workspace,
529                transient_bytes,
530                ..
531            } => {
532                let _memory = execution
533                    .device_memory()
534                    .ok_or_else(|| {
535                        RuntimeError::Wgpu("GPU execution has no device memory pool".into())
536                    })?
537                    .reserve(*transient_bytes)?;
538                let mut workspace = workspace.lock().map_err(|_| {
539                    RuntimeError::Wgpu("streaming workspace lock is poisoned".into())
540                })?;
541                for batch in dataset
542                    .batches_with_plan(*read_plan)
543                    .map_err(|error| RuntimeError::Data(error.to_string()))?
544                {
545                    let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
546                    if let Some(prepared) = workspace.as_mut() {
547                        if !self
548                            .kernel
549                            .refresh_batch(
550                                &self.context,
551                                &self.preparation_params,
552                                &batch,
553                                prepared,
554                            )
555                            .map_err(wgpu_error)?
556                        {
557                            *prepared = self
558                                .kernel
559                                .prepare_batch(&self.context, &self.preparation_params, &batch)
560                                .map_err(wgpu_error)?;
561                        }
562                    } else {
563                        *workspace = Some(
564                            self.kernel
565                                .prepare_batch(&self.context, &self.preparation_params, &batch)
566                                .map_err(wgpu_error)?,
567                        );
568                    }
569                    consume(
570                        workspace
571                            .as_ref()
572                            .expect("streaming workspace was initialized"),
573                    )?;
574                }
575            }
576        }
577        let gradient = gradient
578            .into_iter()
579            .map(|sum| execution.sum_f64(sum.finish()))
580            .collect();
581        Ok(ReductionEvaluation::new(
582            execution.sum_f64(total.finish()),
583            gradient,
584        ))
585    }
586}
587
588#[cfg(feature = "wgpu")]
589fn wgpu_error(error: laddu_wgpu::WgpuError) -> RuntimeError {
590    RuntimeError::Wgpu(error.to_string())
591}
592
593#[cfg(all(test, feature = "wgpu"))]
594mod tests {
595    use std::sync::Arc;
596
597    use laddu_compile::{CompiledModel, ReductionPlan};
598    use laddu_data::{
599        data::{Dataset, EventBatch, OwnedEvent},
600        schema::Schema,
601    };
602    use laddu_expr::{complex, event_scalar, parameter};
603
604    use super::*;
605    use crate::{CpuOptions, Device, ExecutionOptions, GpuBackend, GpuOptions, Precision};
606
607    #[test]
608    #[ignore = "requires a WGPU-compatible hardware adapter"]
609    fn wgpu_resident_and_streaming_reductions_match_f32_cpu() {
610        let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
611        let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
612        let x = event_scalar("x");
613        let expression = (x.clone() * scale.clone() + offset.clone()).sin()
614            + complex(scale, offset).norm_sqr()
615            + 2.0;
616        let model = CompiledModel::from_expr(&expression).unwrap();
617        let params = model.params().default_values();
618        let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
619        let dataset = Dataset::from_batches(vec![
620            EventBatch::from_events(
621                schema.clone(),
622                [
623                    OwnedEvent::weighted(vec![], vec![0.25], 0.5),
624                    OwnedEvent::weighted(vec![], vec![0.75], 1.5),
625                ],
626            )
627            .unwrap(),
628            EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![1.25], 2.0)])
629                .unwrap(),
630        ])
631        .unwrap();
632        let wgpu_execution = Execution::local(ExecutionOptions {
633            device: Device::Gpu(GpuOptions {
634                backend: GpuBackend::Wgpu,
635                ..GpuOptions::default()
636            }),
637            memory: crate::MemoryPlan::host_device(
638                crate::MemoryBudget::Auto,
639                crate::MemoryBudget::Bytes(256),
640            ),
641            precision: Precision::F32,
642            ..ExecutionOptions::default()
643        })
644        .unwrap();
645        let cpu_execution = Execution::local(ExecutionOptions {
646            device: Device::Cpu(CpuOptions::default()),
647            precision: Precision::F32,
648            ..ExecutionOptions::default()
649        })
650        .unwrap();
651        let wgpu = PreparedModel::prepare(&model, &wgpu_execution).unwrap();
652        let cpu = PreparedModel::prepare(&model, &cpu_execution).unwrap();
653        let resident = wgpu
654            .prepare_dataset(&wgpu_execution, &dataset.clone().resident())
655            .unwrap();
656        let streaming = wgpu
657            .prepare_dataset(&wgpu_execution, &dataset.clone().streaming())
658            .unwrap();
659        let cpu_data = cpu.prepare_dataset(&cpu_execution, &dataset).unwrap();
660
661        assert_eq!(resident.stats().storage(), CacheStorage::Resident);
662        assert_eq!(streaming.stats().storage(), CacheStorage::Streaming);
663        assert!(resident.stats().resident_bytes() > 0);
664        assert_eq!(streaming.stats().resident_bytes(), 0);
665
666        let cpu_reduction = cpu
667            .reduce_with_gradient(
668                &cpu_execution,
669                &params,
670                &cpu_data,
671                ReductionPlan::weighted_real(),
672            )
673            .unwrap();
674        let resident_reduction = wgpu
675            .reduce_with_gradient(
676                &wgpu_execution,
677                &params,
678                &resident,
679                ReductionPlan::weighted_real(),
680            )
681            .unwrap();
682        let streaming_reduction = wgpu
683            .reduce_with_gradient(
684                &wgpu_execution,
685                &params,
686                &streaming,
687                ReductionPlan::weighted_real(),
688            )
689            .unwrap();
690
691        for actual in [&resident_reduction, &streaming_reduction] {
692            assert!((actual.value() - cpu_reduction.value()).abs() <= 1.0e-4);
693            assert_eq!(actual.gradient().len(), cpu_reduction.gradient().len());
694            for (actual, expected) in actual.gradient().iter().zip(cpu_reduction.gradient()) {
695                assert!((actual - expected).abs() <= 1.0e-4);
696            }
697        }
698        assert!((resident_reduction.value() - streaming_reduction.value()).abs() <= 1.0e-6);
699        for (resident, streaming) in resident_reduction
700            .gradient()
701            .iter()
702            .zip(streaming_reduction.gradient())
703        {
704            assert!((resident - streaming).abs() <= 1.0e-6);
705        }
706    }
707}