Skip to main content

laddu_runtime/
cpu.rs

1use std::{
2    collections::HashMap,
3    sync::{Arc, OnceLock},
4};
5
6use laddu_autodiff::{AutodiffMode, AutodiffPlan, AutodiffResult};
7use laddu_compile::{CachePlan, CompiledModel, SolveComponentPlan, SolveRowMatrixPlan};
8use laddu_data::data::{CacheStorage, EventBatch};
9use laddu_expr::{
10    ExprGraph, ExprId, P4Component,
11    parameters::{ParamId, ParamLayout, ParamValues},
12};
13#[cfg(test)]
14use laddu_kernel::ir::KernelValueClass;
15use laddu_kernel::ir::{GradientKernelIr, ScalarKernelIr};
16use nalgebra::{Dyn, LU};
17use num::complex::Complex64;
18
19use crate::{JitPolicy, Precision, RuntimeError, RuntimeResult, execution::Execution};
20
21mod gradient_interpreter;
22use gradient_interpreter::GradientInterpreter;
23mod autodiff;
24use autodiff::{DerivativeWorkspace, ReverseDerivativeWorkspace};
25mod cache;
26#[cfg(feature = "jit")]
27pub(crate) use cache::{CacheDescriptor, JitDescriptorSet};
28pub use cache::{CpuBatchCache, CpuCachedBatch, CpuCachedDataset, CpuPreparedDataset};
29mod layout;
30use layout::{Value, matrix_at, matrix_values_row_major, scalar_at, vector_at};
31
32#[cfg(feature = "jit")]
33use crate::jit::{JitGradientKernel, JitScalarKernel};
34
35mod scalar;
36use scalar::{SCALAR_BLOCK_SIZE, ScalarEvaluationPlan, ScalarEventWorkspace, ScalarExecutor};
37mod planning;
38use planning::GradientExecutor;
39mod evaluation;
40use evaluation::F32KernelInput;
41mod prepared;
42mod reduction;
43
44/// Supplies event-dependent scalar values for direct CPU evaluation.
45pub trait EventLookup {
46    /// Returns the scalar named `name`, or `None` when it is unavailable.
47    fn scalar(&self, name: &str) -> Option<f64>;
48
49    /// Returns one component of a named four-momentum.
50    fn p4_component(&self, name: &str, component: P4Component) -> Option<f64> {
51        let key = format!("{}.{}", name, component.label());
52        self.scalar(&key)
53    }
54}
55
56impl<F> EventLookup for F
57where
58    F: for<'a> Fn(&'a str) -> Option<f64>,
59{
60    fn scalar(&self, name: &str) -> Option<f64> {
61        self(name)
62    }
63}
64
65impl EventLookup for HashMap<String, f64> {
66    fn scalar(&self, name: &str) -> Option<f64> {
67        self.get(name).copied()
68    }
69}
70
71/// Prepares compiled models for CPU execution.
72#[derive(Clone, Debug, Default)]
73pub struct CpuBackend;
74
75/// CPU scalar-kernel execution strategy.
76#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
77pub enum CpuExecutionMode {
78    /// Prefer JIT execution when available and fall back to interpretation.
79    #[default]
80    Auto,
81    /// Always interpret the scalar kernel.
82    Interpreter,
83}
84
85/// A compiled model prepared for CPU evaluation.
86#[derive(Clone, Debug)]
87pub struct CpuPlan {
88    pub(super) precision: Precision,
89    pub(super) graph: ExprGraph,
90    pub(super) required_event_scalars: Vec<String>,
91    pub(super) params: ParamLayout,
92    pub(in crate::cpu) parameter_slots: Vec<Option<ParamId>>,
93    pub(super) autodiff: AutodiffPlan,
94    pub(super) cache_plan: CachePlan,
95    pub(super) cache_slots: Vec<Option<usize>>,
96    pub(super) cached_evaluation_nodes: Vec<ExprId>,
97    pub(super) cached_value_slots: Vec<Option<usize>>,
98    pub(super) scalar_kernel: Option<ScalarKernelIr>,
99    pub(in crate::cpu) scalar_executor: Option<ScalarExecutor>,
100    #[cfg_attr(not(feature = "jit"), allow(dead_code))]
101    pub(in crate::cpu) gradient_executor: GradientExecutor,
102    // Direct EventLookup evaluation cannot use block JIT kernels, so f32 plans retain
103    // an interpreter fallback even when cached and invariant gradients use the JIT.
104    pub(in crate::cpu) f32_gradient_fallback_real: Option<GradientKernelIr>,
105    pub(super) f32_gradient_fallback_imag: Option<GradientKernelIr>,
106    pub(super) cache_materialization_nodes: Vec<ExprId>,
107    pub(super) solve_components: Vec<Option<SolveComponentPlan>>,
108    pub(super) solve_rhs_elements: Vec<Option<Vec<ExprId>>>,
109    pub(in crate::cpu) solve_row_matrices: Vec<SolveRowMatrixPlan>,
110    pub(super) solve_row_keys: Vec<(ExprId, usize, usize)>,
111    pub(super) factor_matrix_slots: Vec<Option<usize>>,
112    pub(super) factor_matrices: Vec<(ExprId, usize)>,
113    pub(super) constant_factor_slots: Vec<Option<usize>>,
114    pub(super) constant_factors: Vec<Arc<OnceLock<DynamicLu>>>,
115}
116
117impl CpuBackend {
118    /// Prepares a model using the policies resolved by an execution context.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`RuntimeError`] when model lowering or differentiation fails,
123    /// or the requested precision is unsupported for the model.
124    pub fn prepare_for_execution(
125        &self,
126        model: &CompiledModel,
127        execution: &Execution,
128    ) -> RuntimeResult<CpuPlan> {
129        let mode = match execution.jit_policy() {
130            JitPolicy::Auto | JitPolicy::Enabled => CpuExecutionMode::Auto,
131            JitPolicy::Disabled => CpuExecutionMode::Interpreter,
132        };
133        let plan = self
134            .prepare_with_modes_precision(
135                model,
136                execution.autodiff_mode(),
137                mode,
138                execution.precision(),
139            )
140            .map_err(|error| RuntimeError::Data(error.to_string()))?;
141        if execution.precision() == Precision::F32 && !plan.supports_f32_scalar_execution() {
142            return Err(crate::ExecutionError::UnsupportedCpuF32Model.into());
143        }
144        Ok(plan)
145    }
146
147    /// Prepares a model with forward autodiff and automatic execution-mode selection.
148    ///
149    /// # Panics
150    ///
151    /// Panics if forward differentiation or executable-plan construction fails
152    /// for the compiled model.
153    pub fn prepare(&self, model: &CompiledModel) -> CpuPlan {
154        self.prepare_with_modes(model, AutodiffMode::Forward, CpuExecutionMode::Auto)
155            .expect("forward autodiff supports every compiled expression node")
156    }
157
158    /// Prepares a model with an explicit scalar-kernel execution mode.
159    ///
160    /// # Panics
161    ///
162    /// Panics if forward differentiation or executable-plan construction fails
163    /// for the compiled model.
164    pub fn prepare_with_execution_mode(
165        &self,
166        model: &CompiledModel,
167        execution_mode: CpuExecutionMode,
168    ) -> CpuPlan {
169        self.prepare_with_modes(model, AutodiffMode::Forward, execution_mode)
170            .expect("forward autodiff supports every compiled expression node")
171    }
172
173    /// Prepares a model with an explicit automatic-differentiation mode.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`laddu_autodiff::AutodiffError`] when model lowering or
178    /// differentiation fails.
179    pub fn prepare_with_autodiff_mode(
180        &self,
181        model: &CompiledModel,
182        mode: AutodiffMode,
183    ) -> AutodiffResult<CpuPlan> {
184        self.prepare_with_modes(model, mode, CpuExecutionMode::Auto)
185    }
186
187    /// Prepares a model with explicit autodiff and scalar execution modes.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`laddu_autodiff::AutodiffError`] when model lowering or
192    /// differentiation fails.
193    pub fn prepare_with_modes(
194        &self,
195        model: &CompiledModel,
196        autodiff_mode: AutodiffMode,
197        execution_mode: CpuExecutionMode,
198    ) -> AutodiffResult<CpuPlan> {
199        self.prepare_with_modes_precision(model, autodiff_mode, execution_mode, Precision::F64)
200    }
201}
202
203/// A complex model value and its derivatives with respect to free parameters.
204#[derive(Clone, Debug, PartialEq)]
205pub struct ValueGradient {
206    value: Complex64,
207    gradient: Vec<Complex64>,
208}
209
210#[derive(Copy, Clone, Debug, PartialEq)]
211/// Fixed statistics collected when a dataset is prepared for repeated evaluation.
212pub struct PreparedDatasetStats {
213    pub(super) local_events: usize,
214    pub(super) global_events: usize,
215    pub(super) local_batches: usize,
216    pub(super) sum_weights: f64,
217    pub(super) resident_bytes: usize,
218    pub(super) storage: CacheStorage,
219}
220
221/// The scalar value and free-parameter gradient produced by a reduction.
222#[derive(Clone, Debug, PartialEq)]
223pub struct ReductionEvaluation {
224    value: f64,
225    gradient: Vec<f64>,
226}
227
228impl ReductionEvaluation {
229    #[cfg(feature = "wgpu")]
230    pub(crate) fn new(value: f64, gradient: Vec<f64>) -> Self {
231        Self { value, gradient }
232    }
233
234    /// Returns the reduced scalar value.
235    pub fn value(&self) -> f64 {
236        self.value
237    }
238
239    /// Returns derivatives in free-parameter order.
240    pub fn gradient(&self) -> &[f64] {
241        &self.gradient
242    }
243
244    /// Consumes the evaluation and returns its value and gradient.
245    pub fn into_parts(self) -> (f64, Vec<f64>) {
246        (self.value, self.gradient)
247    }
248}
249
250impl ValueGradient {
251    /// Returns the complex model value.
252    pub fn value(&self) -> Complex64 {
253        self.value
254    }
255
256    /// Returns complex derivatives in free-parameter order.
257    pub fn gradient(&self) -> &[Complex64] {
258        &self.gradient
259    }
260
261    /// Consumes the evaluation and returns its value and gradient.
262    pub fn into_parts(self) -> (Complex64, Vec<Complex64>) {
263        (self.value, self.gradient)
264    }
265}
266
267impl CpuPlan {
268    pub(in crate::cpu) fn scalar_interpreter_plan(&self) -> Option<&ScalarEvaluationPlan> {
269        match (&self.scalar_kernel, &self.scalar_executor) {
270            (Some(_), Some(ScalarExecutor::Interpreter(plan))) => Some(plan),
271            #[cfg(feature = "jit")]
272            (Some(_), Some(ScalarExecutor::Jit(_))) => None,
273            (Some(_), None) | (None, None) => None,
274            (None, Some(_)) => unreachable!("executor requires kernel IR"),
275        }
276    }
277
278    #[cfg(feature = "jit")]
279    pub(super) fn scalar_jit_kernel(&self) -> Option<&JitScalarKernel> {
280        match (&self.scalar_kernel, &self.scalar_executor) {
281            (Some(_), Some(ScalarExecutor::Jit(kernel))) => Some(kernel),
282            (Some(_), Some(ScalarExecutor::Interpreter(_))) | (Some(_), None) | (None, None) => {
283                None
284            }
285            (None, Some(_)) => unreachable!("executor requires kernel IR"),
286        }
287    }
288
289    #[cfg(feature = "jit")]
290    pub(in crate::cpu) fn gradient_jit_kernel(&self) -> Option<&JitGradientKernel> {
291        match &self.gradient_executor {
292            GradientExecutor::Jit(kernel) => Some(kernel),
293            GradientExecutor::Interpreter(_) => None,
294        }
295    }
296
297    pub(in crate::cpu) fn gradient_interpreter(&self) -> Option<&GradientInterpreter> {
298        match &self.gradient_executor {
299            GradientExecutor::Interpreter(interpreter) => interpreter.as_ref(),
300            #[cfg(feature = "jit")]
301            GradientExecutor::Jit(_) => None,
302        }
303    }
304
305    /// Returns the number of parameters, including fixed parameters.
306    pub fn parameter_count(&self) -> usize {
307        self.params.len()
308    }
309
310    /// Returns the number of free parameters.
311    pub fn free_parameter_count(&self) -> usize {
312        self.params.n_free()
313    }
314
315    /// Returns the event-cache layout required by this plan.
316    pub fn cache_plan(&self) -> &CachePlan {
317        &self.cache_plan
318    }
319
320    /// Returns the event-scalar columns required by this prepared model.
321    pub fn required_event_scalars(&self) -> &[String] {
322        &self.required_event_scalars
323    }
324
325    /// Evaluates a model that has no event-dependent inputs.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`RuntimeError`] when parameters are incompatible, the model
330    /// requires event data, evaluation fails, or a matrix is singular.
331    pub fn evaluate(&self, params: &ParamValues) -> RuntimeResult<Complex64> {
332        self.evaluate_inner(params, None)
333    }
334
335    /// Evaluates the model using values supplied by an event lookup.
336    ///
337    /// # Errors
338    ///
339    /// Returns [`RuntimeError`] when a required event value is missing,
340    /// parameters are incompatible, evaluation fails, or a solve is singular.
341    pub fn evaluate_with_event(
342        &self,
343        params: &ParamValues,
344        event: &impl EventLookup,
345    ) -> RuntimeResult<Complex64> {
346        self.evaluate_inner(params, Some(event))
347    }
348
349    /// Evaluates the model and gradient using values supplied by an event lookup.
350    ///
351    /// # Errors
352    ///
353    /// Returns [`RuntimeError`] when a required event value is missing,
354    /// parameters are incompatible, or differentiation or evaluation fails.
355    pub fn evaluate_with_event_and_gradient(
356        &self,
357        params: &ParamValues,
358        event: &impl EventLookup,
359    ) -> RuntimeResult<ValueGradient> {
360        if self.precision == Precision::F32 {
361            return self.evaluate_f32_gradient(params, F32KernelInput::Event(event));
362        }
363        self.require_f64_gradient()?;
364        let values = self.evaluate_values(params, Some(event))?;
365        self.value_gradient(values, None)
366    }
367
368    /// Evaluates one row in a materialized batch cache.
369    ///
370    /// # Errors
371    ///
372    /// Returns [`RuntimeError`] when `row` is out of range, parameters or cache
373    /// layout are incompatible, evaluation fails, or a matrix is singular.
374    pub fn evaluate_cache_row(
375        &self,
376        params: &ParamValues,
377        cache: &CpuBatchCache,
378        row: usize,
379    ) -> RuntimeResult<Complex64> {
380        self.check_batch_cache(cache)?;
381        self.evaluate_cache_row_unchecked(params, cache, row)
382    }
383}
384
385impl CpuPlan {
386    /// Evaluates one cached row and its free-parameter gradient.
387    ///
388    /// # Errors
389    ///
390    /// Returns [`RuntimeError`] when `row` is out of range, parameters or cache
391    /// layout are incompatible, or differentiation or evaluation fails.
392    pub fn evaluate_cache_row_with_gradient(
393        &self,
394        params: &ParamValues,
395        cache: &CpuBatchCache,
396        row: usize,
397    ) -> RuntimeResult<ValueGradient> {
398        self.check_batch_cache(cache)?;
399        self.evaluate_cache_row_with_gradient_unchecked(params, cache, row)
400    }
401
402    /// Evaluates the model for every event in a batch.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`RuntimeError`] when cache materialization or evaluation fails.
407    pub fn evaluate_batch(
408        &self,
409        params: &ParamValues,
410        batch: &EventBatch,
411    ) -> RuntimeResult<Vec<Complex64>> {
412        let cache = self.cache_event_batch(batch)?;
413        self.evaluate_cache(params, &cache)
414    }
415
416    /// Evaluates ordered scalar output nodes once and returns one column per
417    /// output. This is the execution seam used by compiled queries; scalar
418    /// models should continue to use [`Self::evaluate_batch`].
419    pub(crate) fn evaluate_batch_outputs(
420        &self,
421        params: &ParamValues,
422        batch: &EventBatch,
423        outputs: &[ExprId],
424    ) -> RuntimeResult<Vec<Vec<Complex64>>> {
425        let cache = self.cache_event_batch(batch)?;
426        let root = self.graph.root();
427        let root_elements = match self.graph.node(root) {
428            Some(laddu_expr::ExprNode::Vector { elements }) => elements,
429            _ => {
430                return Err(RuntimeError::InvalidShape {
431                    index: root.index(),
432                    message: "multi-output evaluation requires a vector root".into(),
433                });
434            }
435        };
436        let positions = outputs
437            .iter()
438            .map(|output| {
439                root_elements
440                    .iter()
441                    .position(|element| element == output)
442                    .ok_or_else(|| RuntimeError::InvalidShape {
443                        index: output.index(),
444                        message: "query output is not a vector root element".into(),
445                    })
446            })
447            .collect::<RuntimeResult<Vec<_>>>()?;
448        let mut values = outputs
449            .iter()
450            .map(|_| Vec::with_capacity(cache.len()))
451            .collect::<Vec<_>>();
452        for row in 0..cache.len() {
453            let evaluated = self.evaluate_values_from_cache(params, &cache, row)?;
454            let row_values = self.cached_vector_at(&evaluated, root)?;
455            for (column, position) in values.iter_mut().zip(&positions) {
456                column.push(row_values[*position]);
457            }
458        }
459        Ok(values)
460    }
461
462    /// Evaluates the model and gradient for every event in a batch.
463    ///
464    /// # Errors
465    ///
466    /// Returns [`RuntimeError`] when cache materialization, differentiation, or
467    /// evaluation fails.
468    pub fn evaluate_batch_with_gradient(
469        &self,
470        params: &ParamValues,
471        batch: &EventBatch,
472    ) -> RuntimeResult<Vec<ValueGradient>> {
473        let cache = self.cache_event_batch(batch)?;
474        self.evaluate_cache_with_gradient(params, &cache)
475    }
476
477    pub(in crate::cpu) fn value_gradient(
478        &self,
479        values: Vec<Value>,
480        cached_factors: Option<(&CpuBatchCache, usize)>,
481    ) -> RuntimeResult<ValueGradient> {
482        let value = if cached_factors.is_some() {
483            self.cached_scalar_at(&values, self.graph.root())?
484        } else {
485            scalar_at(&values, self.graph.root().index())?
486        };
487        let gradient = match self.autodiff.mode() {
488            AutodiffMode::Auto => unreachable!("autodiff mode is resolved during preparation"),
489            AutodiffMode::Forward => {
490                DerivativeWorkspace::new(self, &values, cached_factors).gradient()?
491            }
492            AutodiffMode::Reverse => {
493                ReverseDerivativeWorkspace::new(self, &values, cached_factors).gradient()?
494            }
495        };
496        Ok(ValueGradient { value, gradient })
497    }
498}
499
500pub(super) type DynamicLu = LU<Complex64, Dyn, Dyn>;
501
502#[cfg(test)]
503#[path = "cpu/tests/mod.rs"]
504mod tests;