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
45pub trait EventLookup {
47 fn scalar(&self, name: &str) -> Option<f64>;
49
50 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#[derive(Clone, Debug, Default)]
74pub struct CpuBackend;
75
76#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
78pub enum CpuExecutionMode {
79 #[default]
81 Auto,
82 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#[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 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
159static 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 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 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 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 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 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#[derive(Clone, Debug, PartialEq)]
298pub struct ValueGradient {
299 value: Complex64,
300 gradient: Vec<Complex64>,
301}
302
303#[derive(Copy, Clone, Debug, PartialEq)]
304pub 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#[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 pub fn value(&self) -> f64 {
329 self.value
330 }
331
332 pub fn gradient(&self) -> &[f64] {
334 &self.gradient
335 }
336
337 pub fn into_parts(self) -> (f64, Vec<f64>) {
339 (self.value, self.gradient)
340 }
341}
342
343impl ValueGradient {
344 pub fn value(&self) -> Complex64 {
346 self.value
347 }
348
349 pub fn gradient(&self) -> &[Complex64] {
351 &self.gradient
352 }
353
354 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 pub fn parameter_count(&self) -> usize {
400 self.params.len()
401 }
402
403 pub fn free_parameter_count(&self) -> usize {
405 self.params.n_free()
406 }
407
408 pub fn cache_plan(&self) -> &CachePlan {
410 &self.cache_plan
411 }
412
413 pub fn required_event_scalars(&self) -> &[String] {
415 &self.required_event_scalars
416 }
417
418 pub fn evaluate(&self, params: &ParamValues) -> RuntimeResult<Complex64> {
425 self.evaluate_inner(params, None)
426 }
427
428 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 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 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 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 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 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 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;