Skip to main content

laddu_runtime/
cpu.rs

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