Skip to main content

laddu_runtime/cpu/
reduction.rs

1use laddu_compile::{ReductionPlan, ReductionTransform};
2#[cfg(test)]
3use laddu_data::data::accurate::AccurateComplex64;
4use laddu_data::data::accurate::AccurateF64;
5use laddu_data::{LadduDataResult, data::EventBatch};
6use laddu_expr::parameters::ParamValues;
7use laddu_memory::MemoryLease;
8use num::complex::Complex64;
9use rayon::prelude::*;
10
11use crate::execution::Execution;
12#[cfg(feature = "jit")]
13use crate::jit::{JitGradientKernel, JitScalarKernel};
14
15use super::{
16    CpuCachedBatch, CpuCachedDataset, CpuPlan, CpuPreparedDataset, F32KernelInput, Precision,
17    ReductionEvaluation, RuntimeError, RuntimeResult, SCALAR_BLOCK_SIZE, ScalarEventWorkspace,
18    ValueGradient,
19};
20
21struct RealGradientAccumulator {
22    value: AccurateF64,
23    gradient: Vec<AccurateF64>,
24}
25
26/// The common input boundary for all CPU reductions.
27///
28/// Resident batches are borrowed, while streaming batches are cached as they
29/// are pulled. Keeping that distinction private avoids rebuilding a temporary
30/// one-batch dataset merely to select a reduction implementation.
31struct PreparedBatchStream<'a> {
32    source: PreparedBatchSource<'a>,
33    plan: &'a CpuPlan,
34}
35
36enum PreparedBatchSource<'a> {
37    Resident(std::slice::Iter<'a, CpuCachedBatch>),
38    Streaming {
39        batches: Box<dyn Iterator<Item = LadduDataResult<EventBatch>> + Send + 'a>,
40        _memory: MemoryLease,
41    },
42}
43
44enum PreparedBatch<'a> {
45    Borrowed(&'a CpuCachedBatch),
46    Owned(CpuCachedBatch),
47}
48
49type PreparedManyEvaluation = (Vec<Vec<Complex64>>, Option<Vec<f64>>);
50
51impl<'a> PreparedBatch<'a> {
52    fn cached(&self) -> &CpuCachedBatch {
53        match self {
54            Self::Borrowed(batch) => batch,
55            Self::Owned(batch) => batch,
56        }
57    }
58}
59
60impl<'a> PreparedBatchStream<'a> {
61    fn prepare(
62        plan: &'a CpuPlan,
63        execution: &Execution,
64        dataset: &'a CpuPreparedDataset,
65    ) -> RuntimeResult<Self> {
66        let source = match dataset {
67            CpuPreparedDataset::Resident { dataset, .. } => {
68                PreparedBatchSource::Resident(dataset.batches().iter())
69            }
70            CpuPreparedDataset::Streaming {
71                dataset,
72                read_plan,
73                transient_bytes,
74                ..
75            } => {
76                let memory = execution
77                    .host_memory()
78                    .reserve(*transient_bytes)
79                    .map_err(RuntimeError::from)?;
80                let batches = dataset
81                    .stream_with_plan(*read_plan)
82                    .map_err(|error| RuntimeError::Data(error.to_string()))?;
83                PreparedBatchSource::Streaming {
84                    batches,
85                    _memory: memory,
86                }
87            }
88        };
89        Ok(Self { source, plan })
90    }
91
92    fn next(&mut self) -> RuntimeResult<Option<PreparedBatch<'a>>> {
93        match &mut self.source {
94            PreparedBatchSource::Resident(batches) => {
95                Ok(batches.next().map(PreparedBatch::Borrowed))
96            }
97            PreparedBatchSource::Streaming { batches, .. } => batches
98                .next()
99                .transpose()
100                .map_err(|error| RuntimeError::Data(error.to_string()))?
101                .map(|batch| {
102                    self.plan
103                        .cache_event_batch(&batch)
104                        .map(|cache| PreparedBatch::Owned(CpuCachedBatch { cache }))
105                })
106                .transpose(),
107        }
108    }
109}
110
111struct ValueReducer {
112    total: AccurateF64,
113}
114
115impl ValueReducer {
116    fn new() -> Self {
117        Self {
118            total: AccurateF64::zero(),
119        }
120    }
121
122    fn consume(
123        &mut self,
124        plan: &CpuPlan,
125        execution: &Execution,
126        params: &ParamValues,
127        batch: &CpuCachedBatch,
128        reduction: ReductionPlan,
129    ) -> RuntimeResult<()> {
130        let value = if execution.is_parallel() && batch.len().div_ceil(SCALAR_BLOCK_SIZE) >= 2 {
131            execution.install(|| {
132                plan.par_try_weighted_sum_batch(params, batch, |value| {
133                    plan.apply_reduction(reduction, value)
134                })
135            })?
136        } else {
137            plan.try_weighted_sum_batch(params, batch, |value| {
138                plan.apply_reduction(reduction, value)
139            })?
140        };
141        self.total.push(value);
142        Ok(())
143    }
144
145    fn finish(self) -> f64 {
146        self.total.finish()
147    }
148}
149
150struct GradientReducer {
151    total: RealGradientAccumulator,
152}
153
154impl GradientReducer {
155    fn new(parameter_count: usize) -> Self {
156        Self {
157            total: RealGradientAccumulator::zero(parameter_count),
158        }
159    }
160
161    fn consume<F>(
162        &mut self,
163        plan: &CpuPlan,
164        execution: &Execution,
165        params: &ParamValues,
166        batch: &CpuCachedBatch,
167        transform: &F,
168    ) -> RuntimeResult<()>
169    where
170        F: Fn(Complex64) -> RuntimeResult<(f64, f64)> + Send + Sync,
171    {
172        let mut transform = transform;
173        let (value, gradient) =
174            if execution.is_parallel() && batch.len().div_ceil(SCALAR_BLOCK_SIZE) >= 2 {
175                execution.install(|| {
176                    plan.par_try_weighted_real_sum_with_gradient_batch(params, batch, &transform)
177                })?
178            } else {
179                plan.try_weighted_real_sum_with_gradient_batch(params, batch, &mut transform)?
180            };
181        self.total.value.push(value);
182        for (sum, partial) in self.total.gradient.iter_mut().zip(gradient) {
183            sum.push(partial);
184        }
185        Ok(())
186    }
187
188    fn finish(self) -> (f64, Vec<f64>) {
189        self.total.finish()
190    }
191}
192
193impl RealGradientAccumulator {
194    fn zero(parameter_count: usize) -> Self {
195        Self {
196            value: AccurateF64::zero(),
197            gradient: (0..parameter_count).map(|_| AccurateF64::zero()).collect(),
198        }
199    }
200
201    fn push(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[Complex64]) {
202        self.value.push(weight * value);
203        for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
204            sum.push(weight * derivative * model_derivative.re);
205        }
206    }
207
208    fn push_f32(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[f32]) {
209        self.value.push(weight * value);
210        for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
211            sum.push(weight * derivative * f64::from(*model_derivative));
212        }
213    }
214
215    fn merge(&mut self, other: Self) {
216        self.value.merge(other.value);
217        for (target, source) in self.gradient.iter_mut().zip(other.gradient) {
218            target.merge(source);
219        }
220    }
221
222    fn finish(self) -> (f64, Vec<f64>) {
223        (
224            self.value.finish(),
225            self.gradient.into_iter().map(AccurateF64::finish).collect(),
226        )
227    }
228}
229
230impl CpuPlan {
231    pub(crate) fn visit_prepared_dataset_many<F>(
232        &self,
233        execution: &Execution,
234        parameter_sets: &[(&ParamValues, &str)],
235        dataset: &CpuPreparedDataset,
236        reduction: Option<ReductionPlan>,
237        pool_installed: bool,
238        mut consume: F,
239    ) -> RuntimeResult<Vec<f64>>
240    where
241        F: FnMut(usize, usize, &[Complex64]) -> RuntimeResult<()>,
242    {
243        let local = (|| {
244            let mut stream = PreparedBatchStream::prepare(self, execution, dataset)?;
245            let mut offset = 0;
246            let mut sums = reduction.map(|_| {
247                parameter_sets
248                    .iter()
249                    .map(|_| AccurateF64::zero())
250                    .collect::<Vec<_>>()
251            });
252            while let Some(batch) = stream.next()? {
253                let batch = batch.cached();
254                for (index, &(parameters, context)) in parameter_sets.iter().enumerate() {
255                    let values = self
256                        .evaluate_prepared_batch(execution, parameters, batch, pool_installed)
257                        .map_err(|error| {
258                            RuntimeError::Parameter(format!("{context} evaluation failed: {error}"))
259                        })?;
260                    if let (Some(reduction), Some(sums)) = (reduction, sums.as_mut()) {
261                        for (weight, value) in batch.weights().iter().zip(&values) {
262                            let reduced = reduction.apply(*value).map_err(|error| {
263                                RuntimeError::Parameter(format!(
264                                    "{context} reduction failed: {error}"
265                                ))
266                            })?;
267                            sums[index].push(*weight * reduced.value());
268                        }
269                    }
270                    consume(offset, index, &values)?;
271                }
272                offset += batch.weights().len();
273            }
274            Ok(sums
275                .unwrap_or_default()
276                .into_iter()
277                .map(AccurateF64::finish)
278                .collect::<Vec<_>>())
279        })();
280        if !execution.all_succeeded(local.is_ok()) {
281            return local.and(Err(RuntimeError::DistributedPeerFailure));
282        }
283        Ok(local?
284            .into_iter()
285            .map(|sum| execution.sum_f64(sum))
286            .collect())
287    }
288
289    fn evaluate_prepared_batch(
290        &self,
291        execution: &Execution,
292        params: &ParamValues,
293        batch: &CpuCachedBatch,
294        pool_installed: bool,
295    ) -> RuntimeResult<Vec<Complex64>> {
296        let block_count = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
297        if !execution.is_parallel() || block_count < 2 {
298            return self.evaluate_cache(params, batch.cache());
299        }
300        self.check_batch_cache(batch.cache())?;
301        let invariant = self.scalar_invariant_values(params)?;
302        #[cfg(feature = "jit")]
303        let jit_cache = self
304            .scalar_jit_kernel()
305            .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
306        let evaluate = || {
307            (0..block_count)
308                .into_par_iter()
309                .map(|block| {
310                    let start = block * SCALAR_BLOCK_SIZE;
311                    let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
312                    let mut workspace = ScalarEventWorkspace::default();
313                    let mut output = Vec::with_capacity(end - start);
314                    self.evaluate_cache_block_prepared(
315                        params,
316                        batch.cache(),
317                        start,
318                        end,
319                        invariant.as_ref(),
320                        &mut workspace,
321                        &mut output,
322                        #[cfg(feature = "jit")]
323                        jit_cache.as_ref(),
324                    )?;
325                    Ok(output)
326                })
327                .collect::<RuntimeResult<Vec<_>>>()
328        };
329        let blocks = if pool_installed {
330            evaluate()?
331        } else {
332            execution.install(evaluate)?
333        };
334        Ok(blocks.into_iter().flatten().collect())
335    }
336
337    pub(crate) fn evaluate_prepared_dataset_many(
338        &self,
339        execution: &Execution,
340        params: &[ParamValues],
341        dataset: &CpuPreparedDataset,
342    ) -> RuntimeResult<Vec<Vec<Complex64>>> {
343        self.evaluate_prepared_dataset_many_local(execution, params, dataset, None)
344            .map(|(values, _)| values)
345    }
346
347    pub(crate) fn evaluate_prepared_dataset_many_with_reduction(
348        &self,
349        execution: &Execution,
350        params: &[ParamValues],
351        dataset: &CpuPreparedDataset,
352        reduction: ReductionPlan,
353    ) -> RuntimeResult<(Vec<Vec<Complex64>>, Vec<f64>)> {
354        let local =
355            self.evaluate_prepared_dataset_many_local(execution, params, dataset, Some(reduction));
356        if !execution.all_succeeded(local.is_ok()) {
357            return local.and(Err(RuntimeError::DistributedPeerFailure));
358        }
359        let (output, sums) = local?;
360        Ok((
361            output,
362            sums.unwrap_or_default()
363                .into_iter()
364                .map(|sum| execution.sum_f64(sum))
365                .collect(),
366        ))
367    }
368
369    fn evaluate_prepared_dataset_many_local(
370        &self,
371        execution: &Execution,
372        params: &[ParamValues],
373        dataset: &CpuPreparedDataset,
374        reduction: Option<ReductionPlan>,
375    ) -> RuntimeResult<PreparedManyEvaluation> {
376        let mut stream = PreparedBatchStream::prepare(self, execution, dataset)?;
377        let mut output = params
378            .iter()
379            .map(|_| Vec::with_capacity(dataset.stats().local_events()))
380            .collect::<Vec<_>>();
381        let mut sums = reduction.map(|_| {
382            params
383                .iter()
384                .map(|_| AccurateF64::zero())
385                .collect::<Vec<_>>()
386        });
387        while let Some(batch) = stream.next()? {
388            let batch = batch.cached();
389            for (index, (parameters, values)) in params.iter().zip(&mut output).enumerate() {
390                let batch_values = self.evaluate_cache(parameters, batch.cache())?;
391                if let (Some(reduction), Some(sums)) = (reduction, sums.as_mut()) {
392                    for (weight, value) in batch.weights().iter().zip(&batch_values) {
393                        sums[index].push(*weight * reduction.apply(*value)?.value());
394                    }
395                }
396                values.extend(batch_values);
397            }
398        }
399        Ok((
400            output,
401            sums.map(|sums| sums.into_iter().map(AccurateF64::finish).collect()),
402        ))
403    }
404
405    /// Execute a weighted reduction over a prepared dataset.
406    ///
407    /// # Errors
408    ///
409    /// Returns [`RuntimeError`] when streaming, cache validation, evaluation,
410    /// or reduction fails, or another distributed worker reports failure.
411    pub fn reduce(
412        &self,
413        execution: &Execution,
414        params: &ParamValues,
415        dataset: &CpuPreparedDataset,
416        reduction: ReductionPlan,
417    ) -> RuntimeResult<f64> {
418        let local = (|| {
419            let mut stream = PreparedBatchStream::prepare(self, execution, dataset)?;
420            let mut reducer = ValueReducer::new();
421            while let Some(batch) = stream.next()? {
422                reducer.consume(self, execution, params, batch.cached(), reduction)?;
423            }
424            Ok(reducer.finish())
425        })();
426        if !execution.all_succeeded(local.is_ok()) {
427            return local.and(Err(RuntimeError::DistributedPeerFailure));
428        }
429        Ok(execution.sum_f64(local?))
430    }
431
432    /// Execute a weighted reduction and its free-parameter gradient.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`RuntimeError`] when streaming, cache validation,
437    /// differentiation, evaluation, or reduction fails, or another distributed
438    /// worker reports failure.
439    pub fn reduce_with_gradient(
440        &self,
441        execution: &Execution,
442        params: &ParamValues,
443        dataset: &CpuPreparedDataset,
444        reduction: ReductionPlan,
445    ) -> RuntimeResult<ReductionEvaluation> {
446        let local = (|| {
447            let mut stream = PreparedBatchStream::prepare(self, execution, dataset)?;
448            let mut reducer = GradientReducer::new(self.free_parameter_count());
449            let transform = |value| {
450                reduction
451                    .apply(value)
452                    .map(|output| output.into_parts())
453                    .map_err(RuntimeError::from)
454            };
455            while let Some(batch) = stream.next()? {
456                reducer.consume(self, execution, params, batch.cached(), &transform)?;
457            }
458            Ok(reducer.finish())
459        })();
460        if !execution.all_succeeded(local.is_ok()) {
461            return local.and(Err(RuntimeError::DistributedPeerFailure));
462        }
463        let (value, gradient) = local?;
464        let value = execution.sum_f64(value);
465        let gradient = execution.sum_slice(&gradient);
466        Ok(ReductionEvaluation { value, gradient })
467    }
468
469    /// Evaluates every event in a fully cached dataset.
470    ///
471    /// # Errors
472    ///
473    /// Returns [`RuntimeError`] when parameters or a cache layout are
474    /// incompatible, evaluation fails, or a matrix is singular.
475    pub fn evaluate_cached_dataset(
476        &self,
477        params: &ParamValues,
478        dataset: &CpuCachedDataset,
479    ) -> RuntimeResult<Vec<Complex64>> {
480        let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
481        let mut out = Vec::with_capacity(total_len);
482        let invariant = self.scalar_invariant_values(params)?;
483        let mut workspace = ScalarEventWorkspace::default();
484        for batch in &dataset.batches {
485            self.check_batch_cache(batch.cache())?;
486            for row in 0..batch.len() {
487                out.push(self.evaluate_cache_row_prepared(
488                    params,
489                    batch.cache(),
490                    row,
491                    invariant.as_ref(),
492                    &mut workspace,
493                )?);
494            }
495        }
496        Ok(out)
497    }
498
499    /// Evaluates every event and gradient in a fully cached dataset.
500    ///
501    /// # Errors
502    ///
503    /// Returns [`RuntimeError`] when parameters or a cache layout are
504    /// incompatible, or differentiation or evaluation fails.
505    pub fn evaluate_cached_dataset_with_gradient(
506        &self,
507        params: &ParamValues,
508        dataset: &CpuCachedDataset,
509    ) -> RuntimeResult<Vec<ValueGradient>> {
510        let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
511        let mut out = Vec::with_capacity(total_len);
512        for batch in &dataset.batches {
513            out.extend(self.evaluate_cache_with_gradient(params, batch.cache())?);
514        }
515        Ok(out)
516    }
517
518    fn try_weighted_sum_batch<E, F>(
519        &self,
520        params: &ParamValues,
521        batch: &CpuCachedBatch,
522        mut f: F,
523    ) -> Result<f64, E>
524    where
525        E: From<RuntimeError>,
526        F: FnMut(Complex64) -> Result<f64, E>,
527    {
528        self.check_batch_cache(batch.cache())?;
529        let invariant = self.scalar_invariant_values(params)?;
530        let mut workspace = ScalarEventWorkspace::default();
531        let mut sum = AccurateF64::zero();
532        for row in 0..batch.len() {
533            let value = self.evaluate_cache_row_prepared(
534                params,
535                batch.cache(),
536                row,
537                invariant.as_ref(),
538                &mut workspace,
539            )?;
540            sum.push(batch.weights()[row] * f(value)?);
541        }
542        Ok(sum.finish())
543    }
544
545    fn par_try_weighted_sum_batch<E, F>(
546        &self,
547        params: &ParamValues,
548        batch: &CpuCachedBatch,
549        f: F,
550    ) -> Result<f64, E>
551    where
552        E: From<RuntimeError> + Send,
553        F: Fn(Complex64) -> Result<f64, E> + Send + Sync,
554    {
555        self.check_batch_cache(batch.cache())?;
556        let invariant = self.scalar_invariant_values(params)?;
557        #[cfg(feature = "jit")]
558        let jit_cache = self
559            .scalar_jit_kernel()
560            .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
561        let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
562        let total = (0..n_blocks)
563            .into_par_iter()
564            .try_fold(
565                || {
566                    (
567                        AccurateF64::zero(),
568                        ScalarEventWorkspace::default(),
569                        Vec::new(),
570                    )
571                },
572                |(mut acc, mut workspace, mut output), block| {
573                    let start = block * SCALAR_BLOCK_SIZE;
574                    let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
575                    self.evaluate_cache_block_prepared(
576                        params,
577                        batch.cache(),
578                        start,
579                        end,
580                        invariant.as_ref(),
581                        &mut workspace,
582                        &mut output,
583                        #[cfg(feature = "jit")]
584                        jit_cache.as_ref(),
585                    )?;
586                    for (lane, value) in output.iter().copied().enumerate() {
587                        acc.push(batch.weights()[start + lane] * f(value)?);
588                    }
589                    Ok::<_, E>((acc, workspace, output))
590                },
591            )
592            .try_reduce(
593                || {
594                    (
595                        AccurateF64::zero(),
596                        ScalarEventWorkspace::default(),
597                        Vec::new(),
598                    )
599                },
600                |(mut lhs, workspace, output), (rhs, _, _)| {
601                    lhs.merge(rhs);
602                    Ok::<_, E>((lhs, workspace, output))
603                },
604            )?;
605        Ok(total.0.finish())
606    }
607
608    fn try_weighted_real_sum_with_gradient_batch<E, F>(
609        &self,
610        params: &ParamValues,
611        batch: &CpuCachedBatch,
612        transform: &mut F,
613    ) -> Result<(f64, Vec<f64>), E>
614    where
615        E: From<RuntimeError>,
616        F: FnMut(Complex64) -> Result<(f64, f64), E>,
617    {
618        self.check_batch_cache(batch.cache())?;
619        #[cfg(feature = "jit")]
620        if let (Some(value_kernel), Some(gradient_kernel)) =
621            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
622        {
623            return self.try_weighted_real_sum_with_jit_gradient_batch(
624                params,
625                batch,
626                transform,
627                value_kernel,
628                gradient_kernel,
629            );
630        }
631        if self.precision != Precision::F32
632            && let Some(interpreter) = self.gradient_interpreter()
633            && let Some(mut state) = interpreter.prepare_real_blocks(params)?
634        {
635            let output_count = state.output_count();
636            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
637            for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
638                let start = block * SCALAR_BLOCK_SIZE;
639                let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
640                let outputs = state.evaluate(batch.cache(), start, end)?;
641                for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
642                    let (value, derivative) = transform(row[0])?;
643                    total.push(batch.weights()[start + lane], value, derivative, &row[1..]);
644                }
645            }
646            return Ok(total.finish());
647        }
648        if self.precision == Precision::F32
649            && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
650        {
651            let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
652            let mut gradient = Vec::new();
653            for row in 0..batch.len() {
654                let (value, model_gradient) = self.evaluate_f32_gradient_component_prepared(
655                    ir,
656                    params,
657                    F32KernelInput::Cache(Some((batch.cache(), row))),
658                    &mut gradient,
659                )?;
660                let (value, derivative) = transform(value)?;
661                total.push_f32(batch.weights()[row], value, derivative, model_gradient);
662            }
663            return Ok(total.finish());
664        }
665        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
666        for row in 0..batch.len() {
667            let evaluation =
668                self.evaluate_cache_row_with_gradient_unchecked(params, batch.cache(), row)?;
669            let (value, derivative) = transform(evaluation.value())?;
670            total.push(
671                batch.weights()[row],
672                value,
673                derivative,
674                evaluation.gradient(),
675            );
676        }
677        Ok(total.finish())
678    }
679
680    fn par_try_weighted_real_sum_with_gradient_batch<E, F>(
681        &self,
682        params: &ParamValues,
683        batch: &CpuCachedBatch,
684        transform: &F,
685    ) -> Result<(f64, Vec<f64>), E>
686    where
687        E: From<RuntimeError> + Send,
688        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
689    {
690        #[cfg(feature = "jit")]
691        if let (Some(value_kernel), Some(gradient_kernel)) =
692            (self.scalar_jit_kernel(), self.gradient_jit_kernel())
693        {
694            return self.par_try_weighted_real_sum_with_jit_gradient_batch(
695                params,
696                batch,
697                transform,
698                value_kernel,
699                gradient_kernel,
700            );
701        }
702        if self.precision != Precision::F32
703            && let Some(interpreter) = self.gradient_interpreter()
704            && let Some(state) = interpreter.prepare_real_blocks(params)?
705        {
706            let output_count = state.output_count();
707            let partial = (0..batch.len().div_ceil(SCALAR_BLOCK_SIZE))
708                .into_par_iter()
709                .try_fold(
710                    || {
711                        (
712                            RealGradientAccumulator::zero(self.free_parameter_count()),
713                            state.clone(),
714                        )
715                    },
716                    |(mut accumulator, mut state), block| {
717                        let start = block * SCALAR_BLOCK_SIZE;
718                        let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
719                        let outputs = state.evaluate(batch.cache(), start, end)?;
720                        for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
721                            let (value, derivative) = transform(row[0])?;
722                            accumulator.push(
723                                batch.weights()[start + lane],
724                                value,
725                                derivative,
726                                &row[1..],
727                            );
728                        }
729                        Ok::<_, E>((accumulator, state))
730                    },
731                )
732                .try_reduce(
733                    || {
734                        (
735                            RealGradientAccumulator::zero(self.free_parameter_count()),
736                            state.clone(),
737                        )
738                    },
739                    |(mut lhs, state), (rhs, _)| {
740                        lhs.merge(rhs);
741                        Ok::<_, E>((lhs, state))
742                    },
743                )?;
744            return Ok(partial.0.finish());
745        }
746        if self.precision == Precision::F32
747            && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
748        {
749            let partial = (0..batch.len())
750                .into_par_iter()
751                .try_fold(
752                    || {
753                        (
754                            RealGradientAccumulator::zero(self.free_parameter_count()),
755                            Vec::new(),
756                        )
757                    },
758                    |(mut accumulator, mut gradient), row| {
759                        let (value, model_gradient) = self
760                            .evaluate_f32_gradient_component_prepared(
761                                ir,
762                                params,
763                                F32KernelInput::Cache(Some((batch.cache(), row))),
764                                &mut gradient,
765                            )?;
766                        let (value, derivative) = transform(value)?;
767                        accumulator.push_f32(
768                            batch.weights()[row],
769                            value,
770                            derivative,
771                            model_gradient,
772                        );
773                        Ok::<_, E>((accumulator, gradient))
774                    },
775                )
776                .try_reduce(
777                    || {
778                        (
779                            RealGradientAccumulator::zero(self.free_parameter_count()),
780                            Vec::new(),
781                        )
782                    },
783                    |(mut lhs, gradient), (rhs, _)| {
784                        lhs.merge(rhs);
785                        Ok::<_, E>((lhs, gradient))
786                    },
787                )?;
788            return Ok(partial.0.finish());
789        }
790        let partial = (0..batch.len())
791            .into_par_iter()
792            .try_fold(
793                || RealGradientAccumulator::zero(self.free_parameter_count()),
794                |mut accumulator, row| {
795                    let evaluation = self.evaluate_cache_row_with_gradient_unchecked(
796                        params,
797                        batch.cache(),
798                        row,
799                    )?;
800                    let (value, derivative) = transform(evaluation.value())?;
801                    accumulator.push(
802                        batch.weights()[row],
803                        value,
804                        derivative,
805                        evaluation.gradient(),
806                    );
807                    Ok::<_, E>(accumulator)
808                },
809            )
810            .try_reduce(
811                || RealGradientAccumulator::zero(self.free_parameter_count()),
812                |mut lhs, rhs| {
813                    lhs.merge(rhs);
814                    Ok::<_, E>(lhs)
815                },
816            )?;
817        Ok(partial.finish())
818    }
819
820    #[cfg(feature = "jit")]
821    fn try_weighted_real_sum_with_jit_gradient_batch<E, F>(
822        &self,
823        params: &ParamValues,
824        batch: &CpuCachedBatch,
825        transform: &mut F,
826        value_kernel: &JitScalarKernel,
827        gradient_kernel: &JitGradientKernel,
828    ) -> Result<(f64, Vec<f64>), E>
829    where
830        E: From<RuntimeError>,
831        F: FnMut(Complex64) -> Result<(f64, f64), E>,
832    {
833        let cache = JitScalarKernel::prepare_cache(batch.cache());
834        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
835        let mut values = Vec::new();
836        let mut tangents = Vec::new();
837        let mut derivatives = Vec::new();
838        for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
839            let start = block * SCALAR_BLOCK_SIZE;
840            let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
841            value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
842            derivatives.clear();
843            derivatives.reserve(values.len());
844            for (lane, value) in values.iter().copied().enumerate() {
845                let (value, derivative) = transform(value)?;
846                total.value.push(batch.weights()[start + lane] * value);
847                derivatives.push(batch.weights()[start + lane] * derivative);
848            }
849            gradient_kernel.evaluate_prepared(params, &cache, start, end, 0, &mut tangents)?;
850            for (lane, factor) in derivatives.iter().enumerate() {
851                for free_index in 0..self.free_parameter_count() {
852                    total.gradient[free_index]
853                        .push(factor * tangents[lane * self.free_parameter_count() + free_index]);
854                }
855            }
856        }
857        Ok(total.finish())
858    }
859
860    #[cfg(feature = "jit")]
861    fn par_try_weighted_real_sum_with_jit_gradient_batch<E, F>(
862        &self,
863        params: &ParamValues,
864        batch: &CpuCachedBatch,
865        transform: &F,
866        value_kernel: &JitScalarKernel,
867        gradient_kernel: &JitGradientKernel,
868    ) -> Result<(f64, Vec<f64>), E>
869    where
870        E: From<RuntimeError> + Send,
871        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
872    {
873        let cache = JitScalarKernel::prepare_cache(batch.cache());
874        let partial = (0..batch.len().div_ceil(SCALAR_BLOCK_SIZE))
875            .into_par_iter()
876            .try_fold(
877                || {
878                    (
879                        RealGradientAccumulator::zero(self.free_parameter_count()),
880                        Vec::new(),
881                        Vec::new(),
882                        Vec::new(),
883                    )
884                },
885                |(mut accumulator, mut values, mut tangents, mut derivatives), block| {
886                    let start = block * SCALAR_BLOCK_SIZE;
887                    let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
888                    value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
889                    derivatives.clear();
890                    for (lane, value) in values.iter().copied().enumerate() {
891                        let (value, derivative) = transform(value)?;
892                        let weight = batch.weights()[start + lane];
893                        accumulator.value.push(weight * value);
894                        derivatives.push(weight * derivative);
895                    }
896                    gradient_kernel.evaluate_prepared(
897                        params,
898                        &cache,
899                        start,
900                        end,
901                        0,
902                        &mut tangents,
903                    )?;
904                    for (lane, factor) in derivatives.iter().enumerate() {
905                        for free_index in 0..self.free_parameter_count() {
906                            accumulator.gradient[free_index].push(
907                                factor * tangents[lane * self.free_parameter_count() + free_index],
908                            );
909                        }
910                    }
911                    Ok::<_, E>((accumulator, values, tangents, derivatives))
912                },
913            )
914            .try_reduce(
915                || {
916                    (
917                        RealGradientAccumulator::zero(self.free_parameter_count()),
918                        Vec::new(),
919                        Vec::new(),
920                        Vec::new(),
921                    )
922                },
923                |(mut lhs, values, tangents, derivatives), (rhs, _, _, _)| {
924                    lhs.merge(rhs);
925                    Ok::<_, E>((lhs, values, tangents, derivatives))
926                },
927            )?;
928        Ok(partial.0.finish())
929    }
930
931    #[cfg(test)]
932    fn try_weighted_sum_cached<E, F>(
933        &self,
934        params: &ParamValues,
935        dataset: &CpuCachedDataset,
936        mut f: F,
937    ) -> Result<f64, E>
938    where
939        E: From<RuntimeError>,
940        F: FnMut(Complex64) -> Result<f64, E>,
941    {
942        let mut sum = AccurateF64::zero();
943        for batch in dataset.batches() {
944            sum.push(self.try_weighted_sum_batch(params, batch, &mut f)?);
945        }
946        Ok(sum.finish())
947    }
948
949    #[cfg(test)]
950    pub(in crate::cpu) fn weighted_sum_cached<F>(
951        &self,
952        params: &ParamValues,
953        dataset: &CpuCachedDataset,
954        mut f: F,
955    ) -> RuntimeResult<f64>
956    where
957        F: FnMut(Complex64) -> f64,
958    {
959        self.try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
960    }
961
962    #[cfg(test)]
963    pub(in crate::cpu) fn try_weighted_real_sum_with_gradient_cached<E, F>(
964        &self,
965        params: &ParamValues,
966        dataset: &CpuCachedDataset,
967        mut transform: F,
968    ) -> Result<(f64, Vec<f64>), E>
969    where
970        E: From<RuntimeError>,
971        F: FnMut(Complex64) -> Result<(f64, f64), E>,
972    {
973        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
974        for batch in dataset.batches() {
975            let (value, gradient) =
976                self.try_weighted_real_sum_with_gradient_batch(params, batch, &mut transform)?;
977            total.value.push(value);
978            for (sum, partial) in total.gradient.iter_mut().zip(gradient) {
979                sum.push(partial);
980            }
981        }
982        Ok(total.finish())
983    }
984
985    #[cfg(test)]
986    fn try_weighted_complex_sum_cached<E, F>(
987        &self,
988        params: &ParamValues,
989        dataset: &CpuCachedDataset,
990        mut f: F,
991    ) -> Result<Complex64, E>
992    where
993        E: From<RuntimeError>,
994        F: FnMut(Complex64) -> Result<Complex64, E>,
995    {
996        let mut sum = Complex64::default();
997        let invariant = self.scalar_invariant_values(params)?;
998        let mut workspace = ScalarEventWorkspace::default();
999        for batch in dataset.batches() {
1000            self.check_batch_cache(batch.cache())?;
1001            for row in 0..batch.len() {
1002                let value = self.evaluate_cache_row_prepared(
1003                    params,
1004                    batch.cache(),
1005                    row,
1006                    invariant.as_ref(),
1007                    &mut workspace,
1008                )?;
1009                sum += f(value)? * batch.weights()[row];
1010            }
1011        }
1012        Ok(sum)
1013    }
1014
1015    #[cfg(test)]
1016    pub(in crate::cpu) fn weighted_complex_sum_cached<F>(
1017        &self,
1018        params: &ParamValues,
1019        dataset: &CpuCachedDataset,
1020        mut f: F,
1021    ) -> RuntimeResult<Complex64>
1022    where
1023        F: FnMut(Complex64) -> Complex64,
1024    {
1025        self.try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
1026    }
1027
1028    fn apply_reduction(&self, reduction: ReductionPlan, value: Complex64) -> RuntimeResult<f64> {
1029        if self.precision != Precision::F32 {
1030            return reduction
1031                .apply(value)
1032                .map(|output| output.value())
1033                .map_err(RuntimeError::from);
1034        }
1035        let real = value.re as f32;
1036        match reduction.transform() {
1037            ReductionTransform::Real => Ok(real as f64),
1038            ReductionTransform::PositiveReal if real > 0.0 => Ok(real as f64),
1039            ReductionTransform::LogPositiveReal if real > 0.0 => Ok(real.ln() as f64),
1040            ReductionTransform::PositiveReal | ReductionTransform::LogPositiveReal => reduction
1041                .apply(Complex64::from(real as f64))
1042                .map(|output| output.value())
1043                .map_err(RuntimeError::from),
1044        }
1045    }
1046
1047    #[cfg(test)]
1048    pub(crate) fn par_try_weighted_sum_cached<E, F>(
1049        &self,
1050        params: &ParamValues,
1051        dataset: &CpuCachedDataset,
1052        f: F,
1053    ) -> Result<f64, E>
1054    where
1055        E: From<RuntimeError> + Send,
1056        F: Fn(Complex64) -> Result<f64, E> + Send + Sync,
1057    {
1058        let mut total = AccurateF64::zero();
1059        for batch in dataset.batches() {
1060            total.push(self.par_try_weighted_sum_batch(params, batch, &f)?);
1061        }
1062        Ok(total.finish())
1063    }
1064
1065    #[cfg(test)]
1066    pub(crate) fn par_weighted_sum_cached<F>(
1067        &self,
1068        params: &ParamValues,
1069        dataset: &CpuCachedDataset,
1070        f: F,
1071    ) -> RuntimeResult<f64>
1072    where
1073        F: Fn(Complex64) -> f64 + Send + Sync,
1074    {
1075        self.par_try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
1076    }
1077
1078    #[cfg(test)]
1079    pub(in crate::cpu) fn par_try_weighted_real_sum_with_gradient_cached<E, F>(
1080        &self,
1081        params: &ParamValues,
1082        dataset: &CpuCachedDataset,
1083        transform: F,
1084    ) -> Result<(f64, Vec<f64>), E>
1085    where
1086        E: From<RuntimeError> + Send,
1087        F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
1088    {
1089        let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
1090        for batch in dataset.batches() {
1091            let (value, gradient) =
1092                self.par_try_weighted_real_sum_with_gradient_batch(params, batch, &transform)?;
1093            total.value.push(value);
1094            for (sum, partial) in total.gradient.iter_mut().zip(gradient) {
1095                sum.push(partial);
1096            }
1097        }
1098        Ok(total.finish())
1099    }
1100
1101    #[cfg(test)]
1102    pub(crate) fn par_try_weighted_complex_sum_cached<E, F>(
1103        &self,
1104        params: &ParamValues,
1105        dataset: &CpuCachedDataset,
1106        f: F,
1107    ) -> Result<Complex64, E>
1108    where
1109        E: From<RuntimeError> + Send,
1110        F: Fn(Complex64) -> Result<Complex64, E> + Send + Sync,
1111    {
1112        let mut total = AccurateComplex64::zero();
1113        let invariant = self.scalar_invariant_values(params)?;
1114        for batch in dataset.batches() {
1115            self.check_batch_cache(batch.cache())?;
1116            #[cfg(feature = "jit")]
1117            let jit_cache = self
1118                .scalar_jit_kernel()
1119                .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
1120            let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
1121            let partial = (0..n_blocks)
1122                .into_par_iter()
1123                .try_fold(
1124                    || {
1125                        (
1126                            AccurateComplex64::zero(),
1127                            ScalarEventWorkspace::default(),
1128                            Vec::new(),
1129                        )
1130                    },
1131                    |(mut acc, mut workspace, mut output), block| {
1132                        let start = block * SCALAR_BLOCK_SIZE;
1133                        let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
1134                        self.evaluate_cache_block_prepared(
1135                            params,
1136                            batch.cache(),
1137                            start,
1138                            end,
1139                            invariant.as_ref(),
1140                            &mut workspace,
1141                            &mut output,
1142                            #[cfg(feature = "jit")]
1143                            jit_cache.as_ref(),
1144                        )?;
1145                        for (lane, value) in output.iter().copied().enumerate() {
1146                            acc.push(f(value)? * batch.weights()[start + lane]);
1147                        }
1148                        Ok::<_, E>((acc, workspace, output))
1149                    },
1150                )
1151                .try_reduce(
1152                    || {
1153                        (
1154                            AccurateComplex64::zero(),
1155                            ScalarEventWorkspace::default(),
1156                            Vec::new(),
1157                        )
1158                    },
1159                    |(mut lhs, workspace, output), (rhs, _, _)| {
1160                        lhs.merge(rhs);
1161                        Ok::<_, E>((lhs, workspace, output))
1162                    },
1163                )?;
1164            total.merge(partial.0);
1165        }
1166        Ok(total.finish())
1167    }
1168
1169    #[cfg(test)]
1170    pub(crate) fn par_weighted_complex_sum_cached<F>(
1171        &self,
1172        params: &ParamValues,
1173        dataset: &CpuCachedDataset,
1174        f: F,
1175    ) -> RuntimeResult<Complex64>
1176    where
1177        F: Fn(Complex64) -> Complex64 + Send + Sync,
1178    {
1179        self.par_try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
1180    }
1181}