1use std::{
2 collections::HashMap,
3 mem::size_of,
4 sync::{Arc, OnceLock},
5};
6
7use laddu_autodiff::{AutodiffMode, AutodiffPlan, AutodiffResult, gradient_ir};
8use laddu_compile::{
9 CachePlan, CompiledModel, ExecutablePlan, ReductionPlan, ReductionTransform,
10 SolveComponentPlan, SolveRowMatrixPlan,
11};
12#[cfg(test)]
13use laddu_data::data::accurate::AccurateComplex64;
14use laddu_data::{
15 data::accurate::AccurateF64,
16 data::{CacheStorage, Dataset, EventBatch, MemoryPolicy},
17 schema::Schema,
18};
19use laddu_expr::{
20 BinaryOp, ExprGraph, ExprId, ExprNode, P4Component, UnaryOp, ValueKind,
21 parameters::{ParamId, ParamLayout, ParamValues},
22};
23use laddu_kernel::ir::{
24 GradientKernelIr, KernelInstruction, KernelValue, KernelValueClass, KernelValueId,
25 KernelValueKind, OutputComponent, ScalarKernelIr,
26};
27use nalgebra::{DMatrix, DVector, Dyn, LU};
28use num::{
29 complex::{Complex, Complex32, Complex64},
30 traits::Float,
31};
32use rayon::prelude::*;
33
34use crate::{
35 JitPolicy, MemoryDecision, MemoryLease, Precision, RuntimeError, RuntimeResult,
36 execution::Execution,
37};
38
39mod gradient_interpreter;
40use gradient_interpreter::GradientInterpreter;
41
42#[cfg(feature = "jit")]
43use crate::jit::{JitCacheView, JitGradientKernel, JitPrecision, JitScalarKernel};
44
45const SCALAR_BLOCK_SIZE: usize = 32;
46
47pub trait EventLookup {
49 fn scalar(&self, name: &str) -> Option<f64>;
51
52 fn p4_component(&self, name: &str, component: P4Component) -> Option<f64> {
54 let key = format!("{}.{}", name, component.label());
55 self.scalar(&key)
56 }
57}
58
59impl<F> EventLookup for F
60where
61 F: for<'a> Fn(&'a str) -> Option<f64>,
62{
63 fn scalar(&self, name: &str) -> Option<f64> {
64 self(name)
65 }
66}
67
68impl EventLookup for HashMap<String, f64> {
69 fn scalar(&self, name: &str) -> Option<f64> {
70 self.get(name).copied()
71 }
72}
73
74#[derive(Clone, Debug, Default)]
76pub struct CpuBackend;
77
78#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
80pub enum CpuExecutionMode {
81 #[default]
83 Auto,
84 Interpreter,
86}
87
88#[derive(Clone, Debug)]
90pub struct CpuPlan {
91 precision: Precision,
92 graph: ExprGraph,
93 params: ParamLayout,
94 parameter_slots: Vec<Option<ParamId>>,
95 autodiff: AutodiffPlan,
96 cache_plan: CachePlan,
97 cache_slots: Vec<Option<usize>>,
98 cached_evaluation_nodes: Vec<ExprId>,
99 cached_value_slots: Vec<Option<usize>>,
100 scalar_kernel: Option<ScalarKernelIr>,
101 scalar_executor: Option<ScalarExecutor>,
102 #[cfg_attr(not(feature = "jit"), allow(dead_code))]
103 gradient_executor: GradientExecutor,
104 f32_gradient_fallback_real: Option<GradientKernelIr>,
107 f32_gradient_fallback_imag: Option<GradientKernelIr>,
108 cache_materialization_nodes: Vec<ExprId>,
109 solve_components: Vec<Option<SolveComponentPlan>>,
110 solve_rhs_elements: Vec<Option<Vec<ExprId>>>,
111 solve_row_matrices: Vec<SolveRowMatrixPlan>,
112 solve_row_keys: Vec<(ExprId, usize, usize)>,
113 factor_matrix_slots: Vec<Option<usize>>,
114 factor_matrices: Vec<(ExprId, usize)>,
115 constant_factor_slots: Vec<Option<usize>>,
116 constant_factors: Vec<Arc<OnceLock<DynamicLu>>>,
117}
118
119#[derive(Clone, Debug)]
120enum ScalarExecutor {
121 Interpreter(ScalarEvaluationPlan),
122 #[cfg(feature = "jit")]
123 Jit(JitScalarKernel),
124}
125
126impl ScalarExecutor {
127 fn prepare(
128 plan: &ScalarKernelIr,
129 mode: CpuExecutionMode,
130 precision: Precision,
131 ) -> Option<Self> {
132 #[cfg(not(feature = "jit"))]
133 let _ = precision;
134 match mode {
135 CpuExecutionMode::Auto => {
136 #[cfg(feature = "jit")]
137 {
138 let jit_precision = match precision {
139 Precision::F32 => JitPrecision::F32,
140 Precision::Auto | Precision::F64 => JitPrecision::F64,
141 };
142 if let Ok(Some(kernel)) =
143 JitScalarKernel::compile_with_precision(plan, jit_precision)
144 {
145 return Some(Self::Jit(kernel));
146 }
147 }
148 ScalarEvaluationPlan::from_kernel_ir(plan).map(Self::Interpreter)
149 }
150 CpuExecutionMode::Interpreter => {
151 ScalarEvaluationPlan::from_kernel_ir(plan).map(Self::Interpreter)
152 }
153 }
154 }
155}
156
157#[derive(Clone, Debug)]
158enum GradientExecutor {
159 Interpreter(Option<GradientInterpreter>),
160 #[cfg(feature = "jit")]
161 Jit(JitGradientKernel),
162}
163
164impl GradientExecutor {
165 fn prepare(
166 plan: Option<&ScalarKernelIr>,
167 params: &ParamLayout,
168 mode: CpuExecutionMode,
169 precision: Precision,
170 gradient_ir: Option<(&GradientKernelIr, Option<&GradientKernelIr>)>,
171 ) -> AutodiffResult<Self> {
172 #[cfg(not(feature = "jit"))]
173 let _ = (plan, params, mode, precision, gradient_ir);
174 #[cfg(feature = "jit")]
175 if mode == CpuExecutionMode::Auto
176 && let Some(plan) = plan
177 && let Ok(kernel) = (if let Some((real, imag)) = gradient_ir {
178 JitGradientKernel::compile_gradient_ir(real, imag, JitPrecision::F32)
179 } else {
180 JitGradientKernel::compile_with_precision(
181 plan,
182 params.free_params(),
183 match precision {
184 Precision::F32 => JitPrecision::F32,
185 Precision::Auto | Precision::F64 => JitPrecision::F64,
186 },
187 )
188 .and_then(|kernel| kernel.ok_or_else(|| "missing gradient kernel".into()))
189 })
190 {
191 return Ok(Self::Jit(kernel));
192 }
193 Ok(Self::Interpreter(
194 plan.map(|plan| GradientInterpreter::new(plan, params.free_params()))
195 .transpose()?,
196 ))
197 }
198}
199
200#[derive(Clone, Debug)]
201struct ScalarEvaluationPlan {
202 invariant_instructions: Vec<ScalarInvariantInstruction>,
203 invariant_real_slot_count: usize,
204 invariant_complex_slot_count: usize,
205 event_instructions: Vec<ScalarEventInstruction>,
206 event_real_slot_count: usize,
207 event_complex_slot_count: usize,
208 outputs: Vec<ScalarOperand>,
209}
210
211impl ScalarEvaluationPlan {
212 fn from_kernel_ir(ir: &ScalarKernelIr) -> Option<Self> {
213 Self::from_kernel_values(ir.values(), &[ir.root()])
214 }
215
216 fn from_kernel_values(values: &[KernelValue], outputs: &[KernelValueId]) -> Option<Self> {
217 let mut required = vec![false; values.len()];
218 let mut pending = outputs.to_vec();
219 while let Some(id) = pending.pop() {
220 if required[id.index()] {
221 continue;
222 }
223 required[id.index()] = true;
224 pending.extend(values[id.index()].instruction.operands());
225 }
226 let mut operands = Vec::with_capacity(values.len());
227 let mut invariant_instructions = Vec::new();
228 let mut invariant_real_slots = 0;
229 let mut invariant_complex_slots = 0;
230 let mut event_instructions = Vec::new();
231
232 for (index, value) in values.iter().enumerate() {
233 if !required[index] {
234 operands.push(None);
235 continue;
236 }
237 if !matches!(value.kind, KernelValueKind::Real | KernelValueKind::Complex) {
238 return None;
239 }
240 let instruction = ScalarInstruction::from_kernel(&value.instruction, &operands);
241 let operand = match value.class {
242 KernelValueClass::Invariant => match value.kind {
243 KernelValueKind::Real => {
244 let slot = invariant_real_slots;
245 invariant_real_slots += 1;
246 invariant_instructions.push((ScalarSlot::Real(slot), instruction));
247 ScalarOperand::InvariantReal(slot)
248 }
249 KernelValueKind::Complex => {
250 let slot = invariant_complex_slots;
251 invariant_complex_slots += 1;
252 invariant_instructions.push((ScalarSlot::Complex(slot), instruction));
253 ScalarOperand::InvariantComplex(slot)
254 }
255 KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
256 unreachable!("aggregate values were rejected before scalar lowering")
257 }
258 },
259 KernelValueClass::Event => {
260 let slot = event_instructions.len();
261 let output = match value.kind {
262 KernelValueKind::Real => ScalarSlot::Real(slot),
263 KernelValueKind::Complex => ScalarSlot::Complex(slot),
264 KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
265 unreachable!("aggregate values were rejected before scalar lowering")
266 }
267 };
268 event_instructions.push((output, instruction));
269 match value.kind {
270 KernelValueKind::Real => ScalarOperand::EventReal(slot),
271 KernelValueKind::Complex => ScalarOperand::EventComplex(slot),
272 KernelValueKind::Vector { .. } | KernelValueKind::Matrix { .. } => {
273 unreachable!("aggregate values were rejected before scalar lowering")
274 }
275 }
276 }
277 };
278 operands.push(Some(operand));
279 }
280
281 Some(Self::new(
282 invariant_instructions,
283 event_instructions,
284 outputs
285 .iter()
286 .map(|output| operands[output.index()].expect("kernel output is required"))
287 .collect(),
288 invariant_real_slots,
289 invariant_complex_slots,
290 ))
291 }
292
293 fn new(
294 invariant_instructions: Vec<(ScalarSlot, ScalarInstruction)>,
295 event_instructions: Vec<(ScalarSlot, ScalarInstruction)>,
296 outputs: Vec<ScalarOperand>,
297 invariant_real_slot_count: usize,
298 invariant_complex_slot_count: usize,
299 ) -> Self {
300 let mut last_use = vec![0; event_instructions.len()];
301 for (index, (_, instruction)) in event_instructions.iter().enumerate() {
302 instruction.record_event_uses(&mut last_use, index);
303 }
304 for output in &outputs {
305 output.record_event_use(&mut last_use, event_instructions.len());
306 }
307
308 let mut logical_to_physical = vec![usize::MAX; event_instructions.len()];
309 let mut free_real_slots = Vec::new();
310 let mut free_complex_slots = Vec::new();
311 let mut next_real_slot = 0;
312 let mut next_complex_slot = 0;
313 let mut slotted_instructions = Vec::with_capacity(event_instructions.len());
314
315 for (index, (output, instruction)) in event_instructions.into_iter().enumerate() {
316 let output_slot = match output {
317 ScalarSlot::Real(_) => {
318 let slot = if let Some(slot) = free_real_slots.pop() {
319 slot
320 } else {
321 let slot = next_real_slot;
322 next_real_slot += 1;
323 slot
324 };
325 ScalarSlot::Real(slot)
326 }
327 ScalarSlot::Complex(_) => {
328 let slot = if let Some(slot) = free_complex_slots.pop() {
329 slot
330 } else {
331 let slot = next_complex_slot;
332 next_complex_slot += 1;
333 slot
334 };
335 ScalarSlot::Complex(slot)
336 }
337 };
338 logical_to_physical[index] = output_slot.index();
339
340 let mut event_inputs = Vec::new();
341 instruction.collect_event_slots(&mut event_inputs);
342 event_inputs.sort_unstable();
343 event_inputs.dedup();
344
345 slotted_instructions.push(ScalarEventInstruction {
346 output_slot,
347 instruction: instruction.remap_event_operands(&logical_to_physical),
348 });
349
350 for input in event_inputs {
351 if last_use[input] == index {
352 match output_slot_for_event(&slotted_instructions, input) {
353 ScalarSlot::Real(slot) => free_real_slots.push(slot),
354 ScalarSlot::Complex(slot) => free_complex_slots.push(slot),
355 }
356 }
357 }
358 }
359
360 Self {
361 invariant_instructions: invariant_instructions
362 .into_iter()
363 .map(|(output_slot, instruction)| ScalarInvariantInstruction {
364 output_slot,
365 instruction,
366 })
367 .collect(),
368 invariant_real_slot_count,
369 invariant_complex_slot_count,
370 event_instructions: slotted_instructions,
371 event_real_slot_count: next_real_slot,
372 event_complex_slot_count: next_complex_slot,
373 outputs: outputs
374 .into_iter()
375 .map(|output| output.remap_event(&logical_to_physical))
376 .collect(),
377 }
378 }
379
380 fn root(&self) -> ScalarOperand {
381 self.outputs[0]
382 }
383}
384
385fn output_slot_for_event(
386 instructions: &[ScalarEventInstruction],
387 logical_slot: usize,
388) -> ScalarSlot {
389 instructions
390 .get(logical_slot)
391 .map(|instruction| instruction.output_slot)
392 .expect("event input is produced by an earlier event instruction")
393}
394
395#[derive(Copy, Clone, Debug)]
396enum ScalarSlot {
397 Real(usize),
398 Complex(usize),
399}
400
401impl ScalarSlot {
402 fn index(self) -> usize {
403 match self {
404 Self::Real(slot) | Self::Complex(slot) => slot,
405 }
406 }
407}
408
409#[derive(Clone, Default)]
410struct ScalarInvariantValues {
411 real: Vec<f64>,
412 complex: Vec<Complex64>,
413}
414
415#[derive(Clone, Default)]
416struct ScalarEventWorkspace {
417 real: Vec<[f64; SCALAR_BLOCK_SIZE]>,
418 complex: Vec<[Complex64; SCALAR_BLOCK_SIZE]>,
419}
420
421#[derive(Copy, Clone, Debug)]
422enum ScalarOperand {
423 InvariantReal(usize),
424 InvariantComplex(usize),
425 EventReal(usize),
426 EventComplex(usize),
427}
428
429impl ScalarOperand {
430 fn complex_value(
431 self,
432 invariant: &ScalarInvariantValues,
433 event: &ScalarEventWorkspace,
434 ) -> Complex64 {
435 match self {
436 Self::InvariantReal(slot) => Complex64::from(invariant.real[slot]),
437 Self::InvariantComplex(slot) => invariant.complex[slot],
438 Self::EventReal(slot) => Complex64::from(event.real[slot][0]),
439 Self::EventComplex(slot) => event.complex[slot][0],
440 }
441 }
442
443 fn real_value(self, invariant: &ScalarInvariantValues, event: &ScalarEventWorkspace) -> f64 {
444 match self {
445 Self::InvariantReal(slot) => invariant.real[slot],
446 Self::InvariantComplex(slot) => invariant.complex[slot].re,
447 Self::EventReal(slot) => event.real[slot][0],
448 Self::EventComplex(slot) => event.complex[slot][0].re,
449 }
450 }
451
452 fn block_complex_value(
453 self,
454 invariant: &ScalarInvariantValues,
455 event: &ScalarEventWorkspace,
456 lane: usize,
457 ) -> Complex64 {
458 match self {
459 Self::InvariantReal(slot) => Complex64::from(invariant.real[slot]),
460 Self::InvariantComplex(slot) => invariant.complex[slot],
461 Self::EventReal(slot) => Complex64::from(event.real[slot][lane]),
462 Self::EventComplex(slot) => event.complex[slot][lane],
463 }
464 }
465
466 fn block_real_value(
467 self,
468 invariant: &ScalarInvariantValues,
469 event: &ScalarEventWorkspace,
470 lane: usize,
471 ) -> f64 {
472 match self {
473 Self::InvariantReal(slot) => invariant.real[slot],
474 Self::InvariantComplex(slot) => invariant.complex[slot].re,
475 Self::EventReal(slot) => event.real[slot][lane],
476 Self::EventComplex(slot) => event.complex[slot][lane].re,
477 }
478 }
479
480 fn collect_event_slot(self, slots: &mut Vec<usize>) {
481 if let Self::EventReal(slot) | Self::EventComplex(slot) = self {
482 slots.push(slot);
483 }
484 }
485
486 fn record_event_use(self, last_use: &mut [usize], instruction_index: usize) {
487 if let Self::EventReal(slot) | Self::EventComplex(slot) = self {
488 last_use[slot] = instruction_index;
489 }
490 }
491
492 fn remap_event(self, logical_to_physical: &[usize]) -> Self {
493 match self {
494 Self::InvariantReal(slot) => Self::InvariantReal(slot),
495 Self::InvariantComplex(slot) => Self::InvariantComplex(slot),
496 Self::EventReal(slot) => Self::EventReal(logical_to_physical[slot]),
497 Self::EventComplex(slot) => Self::EventComplex(logical_to_physical[slot]),
498 }
499 }
500}
501
502#[derive(Clone, Debug)]
503enum OperandRun {
504 InvariantReal(Vec<usize>),
505 InvariantComplex(Vec<usize>),
506 EventReal(Vec<usize>),
507 EventComplex(Vec<usize>),
508}
509
510impl OperandRun {
511 fn from_operands(operands: impl IntoIterator<Item = ScalarOperand>) -> Vec<Self> {
512 let mut runs = Vec::new();
513 for operand in operands {
514 match (runs.last_mut(), operand) {
515 (Some(Self::InvariantReal(slots)), ScalarOperand::InvariantReal(slot))
516 | (Some(Self::InvariantComplex(slots)), ScalarOperand::InvariantComplex(slot))
517 | (Some(Self::EventReal(slots)), ScalarOperand::EventReal(slot))
518 | (Some(Self::EventComplex(slots)), ScalarOperand::EventComplex(slot)) => {
519 slots.push(slot)
520 }
521 (_, ScalarOperand::InvariantReal(slot)) => {
522 runs.push(Self::InvariantReal(vec![slot]))
523 }
524 (_, ScalarOperand::InvariantComplex(slot)) => {
525 runs.push(Self::InvariantComplex(vec![slot]))
526 }
527 (_, ScalarOperand::EventReal(slot)) => runs.push(Self::EventReal(vec![slot])),
528 (_, ScalarOperand::EventComplex(slot)) => runs.push(Self::EventComplex(vec![slot])),
529 }
530 }
531 runs
532 }
533
534 fn add_to_complex(
535 &self,
536 value: &mut Complex64,
537 invariant: &ScalarInvariantValues,
538 event: &ScalarEventWorkspace,
539 ) {
540 match self {
541 Self::InvariantReal(slots) => {
542 for slot in slots {
543 *value += invariant.real[*slot];
544 }
545 }
546 Self::InvariantComplex(slots) => {
547 for slot in slots {
548 *value += invariant.complex[*slot];
549 }
550 }
551 Self::EventReal(slots) => {
552 for slot in slots {
553 *value += event.real[*slot][0];
554 }
555 }
556 Self::EventComplex(slots) => {
557 for slot in slots {
558 *value += event.complex[*slot][0];
559 }
560 }
561 }
562 }
563
564 fn add_to_real(
565 &self,
566 value: &mut f64,
567 invariant: &ScalarInvariantValues,
568 event: &ScalarEventWorkspace,
569 ) {
570 match self {
571 Self::InvariantReal(slots) => {
572 for slot in slots {
573 *value += invariant.real[*slot];
574 }
575 }
576 Self::EventReal(slots) => {
577 for slot in slots {
578 *value += event.real[*slot][0];
579 }
580 }
581 Self::InvariantComplex(_) | Self::EventComplex(_) => {
582 unreachable!("complex operand appeared in real add instruction")
583 }
584 }
585 }
586
587 fn multiply_into_complex(
588 &self,
589 value: &mut Complex64,
590 invariant: &ScalarInvariantValues,
591 event: &ScalarEventWorkspace,
592 ) {
593 match self {
594 Self::InvariantReal(slots) => {
595 for slot in slots {
596 *value *= invariant.real[*slot];
597 }
598 }
599 Self::InvariantComplex(slots) => {
600 for slot in slots {
601 *value *= invariant.complex[*slot];
602 }
603 }
604 Self::EventReal(slots) => {
605 for slot in slots {
606 *value *= event.real[*slot][0];
607 }
608 }
609 Self::EventComplex(slots) => {
610 for slot in slots {
611 *value *= event.complex[*slot][0];
612 }
613 }
614 }
615 }
616
617 fn multiply_into_real(
618 &self,
619 value: &mut f64,
620 invariant: &ScalarInvariantValues,
621 event: &ScalarEventWorkspace,
622 ) {
623 match self {
624 Self::InvariantReal(slots) => {
625 for slot in slots {
626 *value *= invariant.real[*slot];
627 }
628 }
629 Self::EventReal(slots) => {
630 for slot in slots {
631 *value *= event.real[*slot][0];
632 }
633 }
634 Self::InvariantComplex(_) | Self::EventComplex(_) => {
635 unreachable!("complex operand appeared in real multiply instruction")
636 }
637 }
638 }
639
640 fn collect_event_slots(&self, slots: &mut Vec<usize>) {
641 if let Self::EventReal(event_slots) | Self::EventComplex(event_slots) = self {
642 slots.extend(event_slots);
643 }
644 }
645
646 fn record_event_uses(&self, last_use: &mut [usize], instruction_index: usize) {
647 if let Self::EventReal(event_slots) | Self::EventComplex(event_slots) = self {
648 for slot in event_slots {
649 last_use[*slot] = instruction_index;
650 }
651 }
652 }
653
654 fn remap_events(&self, logical_to_physical: &[usize]) -> Self {
655 match self {
656 Self::InvariantReal(slots) => Self::InvariantReal(slots.clone()),
657 Self::InvariantComplex(slots) => Self::InvariantComplex(slots.clone()),
658 Self::EventReal(slots) => Self::EventReal(
659 slots
660 .iter()
661 .map(|slot| logical_to_physical[*slot])
662 .collect(),
663 ),
664 Self::EventComplex(slots) => Self::EventComplex(
665 slots
666 .iter()
667 .map(|slot| logical_to_physical[*slot])
668 .collect(),
669 ),
670 }
671 }
672}
673
674#[derive(Clone, Debug)]
675enum ScalarInstruction {
676 Cached(usize),
677 Constant(Complex64),
678 Parameter(ParamId),
679 Unary {
680 op: UnaryOp,
681 input: ScalarOperand,
682 },
683 Binary {
684 op: BinaryOp,
685 lhs: ScalarOperand,
686 rhs: ScalarOperand,
687 },
688 Add(Vec<OperandRun>),
689 Mul(Vec<OperandRun>),
690 Complex {
691 re: ScalarOperand,
692 im: ScalarOperand,
693 },
694 SolveRow {
695 row_slot: usize,
696 rhs: Vec<ScalarOperand>,
697 },
698 SolveRowAdjointElement {
699 row_slot: usize,
700 index: usize,
701 len: usize,
702 adjoint: ScalarOperand,
703 },
704}
705
706impl ScalarInstruction {
707 fn from_kernel(instruction: &KernelInstruction, operands: &[Option<ScalarOperand>]) -> Self {
708 let operand = |id: KernelValueId| {
709 operands[id.index()].expect("required instruction operand was lowered")
710 };
711 match instruction {
712 KernelInstruction::Cached(slot) => Self::Cached(*slot),
713 KernelInstruction::RealConstant(value) => Self::Constant(Complex64::from(*value)),
714 KernelInstruction::ComplexConstant(value) => Self::Constant(*value),
715 KernelInstruction::Parameter(id) => Self::Parameter(*id),
716 KernelInstruction::Unary { op, input } => Self::Unary {
717 op: *op,
718 input: operand(*input),
719 },
720 KernelInstruction::Binary { op, lhs, rhs } => Self::Binary {
721 op: *op,
722 lhs: operand(*lhs),
723 rhs: operand(*rhs),
724 },
725 KernelInstruction::Add(terms) => Self::Add(OperandRun::from_operands(
726 terms.iter().map(|id| operand(*id)),
727 )),
728 KernelInstruction::Mul(factors) => Self::Mul(OperandRun::from_operands(
729 factors.iter().map(|id| operand(*id)),
730 )),
731 KernelInstruction::Complex { re, im } => Self::Complex {
732 re: operand(*re),
733 im: operand(*im),
734 },
735 KernelInstruction::SolveRow { row_slot, rhs } => Self::SolveRow {
736 row_slot: *row_slot,
737 rhs: rhs.iter().map(|id| operand(*id)).collect(),
738 },
739 KernelInstruction::SolveRowAdjointElement {
740 row_slot,
741 index,
742 len,
743 adjoint,
744 } => Self::SolveRowAdjointElement {
745 row_slot: *row_slot,
746 index: *index,
747 len: *len,
748 adjoint: operand(*adjoint),
749 },
750 KernelInstruction::Vector(_)
751 | KernelInstruction::Matrix { .. }
752 | KernelInstruction::Component { .. }
753 | KernelInstruction::MatrixElement { .. }
754 | KernelInstruction::MatMul { .. }
755 | KernelInstruction::MatVec { .. }
756 | KernelInstruction::Dot { .. }
757 | KernelInstruction::Solve { .. } => {
758 unreachable!("aggregate instruction cannot enter the scalar interpreter")
759 }
760 }
761 }
762
763 fn collect_event_slots(&self, slots: &mut Vec<usize>) {
764 match self {
765 Self::Cached(_) | Self::Constant(_) | Self::Parameter(_) => {}
766 Self::Unary { input, .. } => input.collect_event_slot(slots),
767 Self::Binary { lhs, rhs, .. } => {
768 lhs.collect_event_slot(slots);
769 rhs.collect_event_slot(slots);
770 }
771 Self::Add(runs) | Self::Mul(runs) => {
772 for run in runs {
773 run.collect_event_slots(slots);
774 }
775 }
776 Self::Complex { re, im } => {
777 re.collect_event_slot(slots);
778 im.collect_event_slot(slots);
779 }
780 Self::SolveRow { rhs, .. } => {
781 for operand in rhs {
782 operand.collect_event_slot(slots);
783 }
784 }
785 Self::SolveRowAdjointElement { adjoint, .. } => {
786 adjoint.collect_event_slot(slots);
787 }
788 }
789 }
790
791 fn record_event_uses(&self, last_use: &mut [usize], instruction_index: usize) {
792 match self {
793 Self::Cached(_) | Self::Constant(_) | Self::Parameter(_) => {}
794 Self::Unary { input, .. } => input.record_event_use(last_use, instruction_index),
795 Self::Binary { lhs, rhs, .. } => {
796 lhs.record_event_use(last_use, instruction_index);
797 rhs.record_event_use(last_use, instruction_index);
798 }
799 Self::Add(runs) | Self::Mul(runs) => {
800 for run in runs {
801 run.record_event_uses(last_use, instruction_index);
802 }
803 }
804 Self::Complex { re, im } => {
805 re.record_event_use(last_use, instruction_index);
806 im.record_event_use(last_use, instruction_index);
807 }
808 Self::SolveRow { rhs, .. } => {
809 for operand in rhs {
810 operand.record_event_use(last_use, instruction_index);
811 }
812 }
813 Self::SolveRowAdjointElement { adjoint, .. } => {
814 adjoint.record_event_use(last_use, instruction_index);
815 }
816 }
817 }
818
819 fn remap_event_operands(self, logical_to_physical: &[usize]) -> Self {
820 match self {
821 Self::Cached(slot) => Self::Cached(slot),
822 Self::Constant(value) => Self::Constant(value),
823 Self::Parameter(id) => Self::Parameter(id),
824 Self::Unary { op, input } => Self::Unary {
825 op,
826 input: input.remap_event(logical_to_physical),
827 },
828 Self::Binary { op, lhs, rhs } => Self::Binary {
829 op,
830 lhs: lhs.remap_event(logical_to_physical),
831 rhs: rhs.remap_event(logical_to_physical),
832 },
833 Self::Add(runs) => Self::Add(
834 runs.iter()
835 .map(|run| run.remap_events(logical_to_physical))
836 .collect(),
837 ),
838 Self::Mul(runs) => Self::Mul(
839 runs.iter()
840 .map(|run| run.remap_events(logical_to_physical))
841 .collect(),
842 ),
843 Self::Complex { re, im } => Self::Complex {
844 re: re.remap_event(logical_to_physical),
845 im: im.remap_event(logical_to_physical),
846 },
847 Self::SolveRow { row_slot, rhs } => Self::SolveRow {
848 row_slot,
849 rhs: rhs
850 .iter()
851 .map(|operand| operand.remap_event(logical_to_physical))
852 .collect(),
853 },
854 Self::SolveRowAdjointElement {
855 row_slot,
856 index,
857 len,
858 adjoint,
859 } => Self::SolveRowAdjointElement {
860 row_slot,
861 index,
862 len,
863 adjoint: adjoint.remap_event(logical_to_physical),
864 },
865 }
866 }
867
868 fn evaluate_real(
869 &self,
870 params: Option<&ParamValues>,
871 cache: Option<(&CpuBatchCache, usize)>,
872 invariant: &ScalarInvariantValues,
873 event: &ScalarEventWorkspace,
874 ) -> RuntimeResult<f64> {
875 Ok(match self {
876 Self::Cached(slot) => {
877 cache
878 .expect("cached instruction requires an event cache")
879 .0
880 .scalar(
881 *slot,
882 cache.expect("cached instruction requires an event cache").1,
883 )?
884 .re
885 }
886 Self::Constant(value) => value.re,
887 Self::Parameter(id) => params
888 .expect("parameter instruction requires parameter values")
889 .get(*id)
890 .map_err(|err| RuntimeError::Parameter(err.to_string()))?,
891 Self::Unary { op, input } => match op {
892 UnaryOp::Neg => -input.real_value(invariant, event),
893 UnaryOp::Real | UnaryOp::Conj => input.complex_value(invariant, event).re,
894 UnaryOp::Imag => input.complex_value(invariant, event).im,
895 UnaryOp::NormSqr => input.complex_value(invariant, event).norm_sqr(),
896 UnaryOp::Sqrt => input.real_value(invariant, event).sqrt(),
897 UnaryOp::Exp => input.real_value(invariant, event).exp(),
898 UnaryOp::Sin => input.real_value(invariant, event).sin(),
899 UnaryOp::Cos => input.real_value(invariant, event).cos(),
900 UnaryOp::Log => input.real_value(invariant, event).ln(),
901 UnaryOp::PowI(power) => input.real_value(invariant, event).powi(*power),
902 },
903 Self::Binary { op, lhs, rhs } => {
904 let lhs = lhs.real_value(invariant, event);
905 let rhs = rhs.real_value(invariant, event);
906 match op {
907 BinaryOp::Add => lhs + rhs,
908 BinaryOp::Sub => lhs - rhs,
909 BinaryOp::Mul => lhs * rhs,
910 BinaryOp::Div => lhs / rhs,
911 BinaryOp::Atan2 => lhs.atan2(rhs),
912 }
913 }
914 Self::Add(runs) => {
915 let mut value = 0.0;
916 for run in runs {
917 run.add_to_real(&mut value, invariant, event);
918 }
919 value
920 }
921 Self::Mul(runs) => {
922 let mut value = 1.0;
923 for run in runs {
924 run.multiply_into_real(&mut value, invariant, event);
925 }
926 value
927 }
928 Self::Complex { .. } | Self::SolveRow { .. } | Self::SolveRowAdjointElement { .. } => {
929 unreachable!("complex-only instruction appeared in real scalar slot")
930 }
931 })
932 }
933
934 fn evaluate_complex(
935 &self,
936 params: Option<&ParamValues>,
937 cache: Option<(&CpuBatchCache, usize)>,
938 invariant: &ScalarInvariantValues,
939 event: &ScalarEventWorkspace,
940 ) -> RuntimeResult<Complex64> {
941 Ok(match self {
942 Self::Cached(slot) => cache
943 .expect("cached instruction requires an event cache")
944 .0
945 .scalar(
946 *slot,
947 cache.expect("cached instruction requires an event cache").1,
948 )?,
949 Self::Constant(value) => *value,
950 Self::Parameter(id) => Complex64::from(
951 params
952 .expect("parameter instruction requires parameter values")
953 .get(*id)
954 .map_err(|err| RuntimeError::Parameter(err.to_string()))?,
955 ),
956 Self::Unary { op, input } => eval_unary(*op, input.complex_value(invariant, event)),
957 Self::Binary { op, lhs, rhs } => eval_binary(
958 *op,
959 lhs.complex_value(invariant, event),
960 rhs.complex_value(invariant, event),
961 ),
962 Self::Add(runs) => {
963 let mut value = Complex64::ZERO;
964 for run in runs {
965 run.add_to_complex(&mut value, invariant, event);
966 }
967 value
968 }
969 Self::Mul(runs) => {
970 let mut value = Complex64::ONE;
971 for run in runs {
972 run.multiply_into_complex(&mut value, invariant, event);
973 }
974 value
975 }
976 Self::Complex { re, im } => Complex64::new(
977 re.real_value(invariant, event),
978 im.real_value(invariant, event),
979 ),
980 Self::SolveRow { row_slot, rhs } => {
981 let (cache, row) = cache.expect("solve row instruction requires an event cache");
982 let inverse_row = cache.solve_row(*row_slot, row)?;
983 if inverse_row.len() != rhs.len() {
984 return Err(RuntimeError::InvalidShape {
985 index: row,
986 message: format!(
987 "specialized solve row has len {}, expected {}",
988 inverse_row.len(),
989 rhs.len()
990 ),
991 });
992 }
993 inverse_row
994 .iter()
995 .zip(rhs)
996 .map(|(lhs, operand)| lhs * operand.complex_value(invariant, event))
997 .sum()
998 }
999 Self::SolveRowAdjointElement {
1000 row_slot,
1001 index,
1002 len,
1003 adjoint,
1004 } => {
1005 let (cache, row) =
1006 cache.expect("solve-row adjoint instruction requires an event cache");
1007 let inverse_row = cache.solve_row(*row_slot, row)?;
1008 if inverse_row.len() != *len {
1009 return Err(RuntimeError::InvalidShape {
1010 index: row,
1011 message: format!(
1012 "specialized solve row has len {}, expected {len}",
1013 inverse_row.len()
1014 ),
1015 });
1016 }
1017 adjoint.complex_value(invariant, event) * inverse_row[*index].conj()
1018 }
1019 })
1020 }
1021}
1022
1023#[derive(Clone, Debug)]
1024struct ScalarEventInstruction {
1025 output_slot: ScalarSlot,
1026 instruction: ScalarInstruction,
1027}
1028
1029#[derive(Clone, Debug)]
1030struct ScalarInvariantInstruction {
1031 output_slot: ScalarSlot,
1032 instruction: ScalarInstruction,
1033}
1034
1035impl CpuBackend {
1036 pub fn prepare_for_execution(
1043 &self,
1044 model: &CompiledModel,
1045 execution: &Execution,
1046 ) -> RuntimeResult<CpuPlan> {
1047 let mode = match execution.jit_policy() {
1048 JitPolicy::Auto | JitPolicy::Enabled => CpuExecutionMode::Auto,
1049 JitPolicy::Disabled => CpuExecutionMode::Interpreter,
1050 };
1051 let plan = self
1052 .prepare_with_modes_precision(
1053 model,
1054 execution.autodiff_mode(),
1055 mode,
1056 execution.precision(),
1057 )
1058 .map_err(|error| RuntimeError::Data(error.to_string()))?;
1059 if execution.precision() == Precision::F32 && !plan.supports_f32_scalar_execution() {
1060 return Err(crate::ExecutionError::UnsupportedCpuF32Model.into());
1061 }
1062 Ok(plan)
1063 }
1064
1065 pub fn prepare(&self, model: &CompiledModel) -> CpuPlan {
1072 self.prepare_with_modes(model, AutodiffMode::Forward, CpuExecutionMode::Auto)
1073 .expect("forward autodiff supports every compiled expression node")
1074 }
1075
1076 pub fn prepare_with_execution_mode(
1083 &self,
1084 model: &CompiledModel,
1085 execution_mode: CpuExecutionMode,
1086 ) -> CpuPlan {
1087 self.prepare_with_modes(model, AutodiffMode::Forward, execution_mode)
1088 .expect("forward autodiff supports every compiled expression node")
1089 }
1090
1091 pub fn prepare_with_autodiff_mode(
1098 &self,
1099 model: &CompiledModel,
1100 mode: AutodiffMode,
1101 ) -> AutodiffResult<CpuPlan> {
1102 self.prepare_with_modes(model, mode, CpuExecutionMode::Auto)
1103 }
1104
1105 pub fn prepare_with_modes(
1112 &self,
1113 model: &CompiledModel,
1114 autodiff_mode: AutodiffMode,
1115 execution_mode: CpuExecutionMode,
1116 ) -> AutodiffResult<CpuPlan> {
1117 self.prepare_with_modes_precision(model, autodiff_mode, execution_mode, Precision::F64)
1118 }
1119
1120 fn prepare_with_modes_precision(
1121 &self,
1122 model: &CompiledModel,
1123 autodiff_mode: AutodiffMode,
1124 execution_mode: CpuExecutionMode,
1125 precision: Precision,
1126 ) -> AutodiffResult<CpuPlan> {
1127 let executable = ExecutablePlan::from_model(model)
1128 .map_err(|error| laddu_autodiff::AutodiffError::InvalidKernel(error.to_string()))?;
1129 let scalar_kernel = executable.scalar_kernel().cloned();
1130 let scalar_executor = scalar_kernel
1131 .as_ref()
1132 .and_then(|kernel| ScalarExecutor::prepare(kernel, execution_mode, precision));
1133 let f32_gradient_fallback_real = if precision == Precision::F32 {
1134 scalar_kernel
1135 .as_ref()
1136 .map(|kernel| {
1137 gradient_ir(kernel, model.params().free_params(), OutputComponent::Real)
1138 })
1139 .transpose()?
1140 } else {
1141 None
1142 };
1143 let f32_gradient_fallback_imag = if precision == Precision::F32
1144 && scalar_kernel.as_ref().is_some_and(|kernel| {
1145 kernel.values()[kernel.root().index()].kind == KernelValueKind::Complex
1146 }) {
1147 scalar_kernel
1148 .as_ref()
1149 .map(|kernel| {
1150 gradient_ir(kernel, model.params().free_params(), OutputComponent::Imag)
1151 })
1152 .transpose()?
1153 } else {
1154 None
1155 };
1156 let gradient_executor = GradientExecutor::prepare(
1157 scalar_kernel.as_ref(),
1158 model.params(),
1159 execution_mode,
1160 precision,
1161 f32_gradient_fallback_real
1162 .as_ref()
1163 .map(|real| (real, f32_gradient_fallback_imag.as_ref())),
1164 )?;
1165 let constant_factors = executable
1166 .constant_factor_matrices()
1167 .iter()
1168 .map(|_| Arc::new(OnceLock::new()))
1169 .collect();
1170 Ok(CpuPlan {
1171 precision,
1172 graph: executable.graph().clone(),
1173 params: executable.params().clone(),
1174 parameter_slots: executable.parameter_slots().to_vec(),
1175 autodiff: AutodiffPlan::from_model(model, autodiff_mode)?,
1176 cache_plan: executable.cache_plan().clone(),
1177 cache_slots: executable.cache_slots().to_vec(),
1178 cached_evaluation_nodes: executable.evaluation_nodes().to_vec(),
1179 cached_value_slots: executable.value_slots().to_vec(),
1180 scalar_kernel,
1181 scalar_executor,
1182 gradient_executor,
1183 f32_gradient_fallback_real,
1184 f32_gradient_fallback_imag,
1185 cache_materialization_nodes: executable.cache_materialization_nodes().to_vec(),
1186 solve_components: executable.solve_components().to_vec(),
1187 solve_rhs_elements: executable.solve_rhs_elements().to_vec(),
1188 solve_row_matrices: executable.solve_row_matrices().to_vec(),
1189 solve_row_keys: executable.solve_row_keys().to_vec(),
1190 factor_matrix_slots: executable.factor_matrix_slots().to_vec(),
1191 factor_matrices: executable.factor_matrices().to_vec(),
1192 constant_factor_slots: executable.constant_factor_slots().to_vec(),
1193 constant_factors,
1194 })
1195 }
1196}
1197
1198#[derive(Clone, Debug, PartialEq)]
1200pub struct ValueGradient {
1201 value: Complex64,
1202 gradient: Vec<Complex64>,
1203}
1204
1205#[derive(Clone, Debug, PartialEq)]
1207pub struct ReductionEvaluation {
1208 value: f64,
1209 gradient: Vec<f64>,
1210}
1211
1212impl ReductionEvaluation {
1213 #[cfg(feature = "wgpu")]
1214 pub(crate) fn new(value: f64, gradient: Vec<f64>) -> Self {
1215 Self { value, gradient }
1216 }
1217
1218 pub fn value(&self) -> f64 {
1220 self.value
1221 }
1222
1223 pub fn gradient(&self) -> &[f64] {
1225 &self.gradient
1226 }
1227
1228 pub fn into_parts(self) -> (f64, Vec<f64>) {
1230 (self.value, self.gradient)
1231 }
1232}
1233
1234impl ValueGradient {
1235 pub fn value(&self) -> Complex64 {
1237 self.value
1238 }
1239
1240 pub fn gradient(&self) -> &[Complex64] {
1242 &self.gradient
1243 }
1244
1245 pub fn into_parts(self) -> (Complex64, Vec<Complex64>) {
1247 (self.value, self.gradient)
1248 }
1249}
1250
1251struct RealGradientAccumulator {
1252 value: AccurateF64,
1253 gradient: Vec<AccurateF64>,
1254}
1255
1256impl RealGradientAccumulator {
1257 fn zero(parameter_count: usize) -> Self {
1258 Self {
1259 value: AccurateF64::zero(),
1260 gradient: (0..parameter_count).map(|_| AccurateF64::zero()).collect(),
1261 }
1262 }
1263
1264 fn push(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[Complex64]) {
1265 self.value.push(weight * value);
1266 for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
1267 sum.push(weight * derivative * model_derivative.re);
1268 }
1269 }
1270
1271 fn push_f32(&mut self, weight: f64, value: f64, derivative: f64, model_gradient: &[f32]) {
1272 self.value.push(weight * value);
1273 for (sum, model_derivative) in self.gradient.iter_mut().zip(model_gradient) {
1274 sum.push(weight * derivative * f64::from(*model_derivative));
1275 }
1276 }
1277
1278 fn merge(&mut self, other: Self) {
1279 self.value.merge(other.value);
1280 for (target, source) in self.gradient.iter_mut().zip(other.gradient) {
1281 target.merge(source);
1282 }
1283 }
1284
1285 fn finish(self) -> (f64, Vec<f64>) {
1286 (
1287 self.value.finish(),
1288 self.gradient.into_iter().map(AccurateF64::finish).collect(),
1289 )
1290 }
1291}
1292
1293#[derive(Copy, Clone, Debug)]
1294struct DatasetScanStats {
1295 events: usize,
1296 batches: usize,
1297 sum_weights: f64,
1298}
1299
1300fn scan_dataset_stats(
1301 dataset: &Dataset,
1302 read_plan: laddu_data::io::ReadPlan,
1303) -> RuntimeResult<DatasetScanStats> {
1304 let mut events = 0;
1305 let mut batches = 0;
1306 let mut sum_weights = AccurateF64::zero();
1307 for batch in dataset
1308 .batches_with_plan(read_plan)
1309 .map_err(|error| RuntimeError::Data(error.to_string()))?
1310 {
1311 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
1312 events += batch.len();
1313 batches += 1;
1314 for row in 0..batch.len() {
1315 sum_weights.push(batch.weights_at(row));
1316 }
1317 }
1318 Ok(DatasetScanStats {
1319 events,
1320 batches,
1321 sum_weights: sum_weights.finish(),
1322 })
1323}
1324
1325impl CpuPlan {
1326 fn supports_f32_scalar_execution(&self) -> bool {
1327 self.scalar_kernel.as_ref().is_some_and(|kernel| {
1328 kernel.values().iter().all(|value| {
1329 matches!(value.kind, KernelValueKind::Real | KernelValueKind::Complex)
1330 || !matches!(
1331 value.instruction,
1332 KernelInstruction::SolveRowAdjointElement { .. }
1333 )
1334 })
1335 })
1336 }
1337
1338 fn scalar_interpreter_plan(&self) -> Option<&ScalarEvaluationPlan> {
1339 match (&self.scalar_kernel, &self.scalar_executor) {
1340 (Some(_), Some(ScalarExecutor::Interpreter(plan))) => Some(plan),
1341 #[cfg(feature = "jit")]
1342 (Some(_), Some(ScalarExecutor::Jit(_))) => None,
1343 (Some(_), None) | (None, None) => None,
1344 (None, Some(_)) => unreachable!("executor requires kernel IR"),
1345 }
1346 }
1347
1348 #[cfg(feature = "jit")]
1349 fn scalar_jit_kernel(&self) -> Option<&JitScalarKernel> {
1350 match (&self.scalar_kernel, &self.scalar_executor) {
1351 (Some(_), Some(ScalarExecutor::Jit(kernel))) => Some(kernel),
1352 (Some(_), Some(ScalarExecutor::Interpreter(_))) | (Some(_), None) | (None, None) => {
1353 None
1354 }
1355 (None, Some(_)) => unreachable!("executor requires kernel IR"),
1356 }
1357 }
1358
1359 #[cfg(feature = "jit")]
1360 fn gradient_jit_kernel(&self) -> Option<&JitGradientKernel> {
1361 match &self.gradient_executor {
1362 GradientExecutor::Jit(kernel) => Some(kernel),
1363 GradientExecutor::Interpreter(_) => None,
1364 }
1365 }
1366
1367 fn gradient_interpreter(&self) -> Option<&GradientInterpreter> {
1368 match &self.gradient_executor {
1369 GradientExecutor::Interpreter(interpreter) => interpreter.as_ref(),
1370 #[cfg(feature = "jit")]
1371 GradientExecutor::Jit(_) => None,
1372 }
1373 }
1374
1375 fn parameter_value(&self, params: &ParamValues, node: usize) -> RuntimeResult<f64> {
1376 let id = self.parameter_slots[node].ok_or_else(|| RuntimeError::InvalidShape {
1377 index: node,
1378 message: "node is not a parameter".into(),
1379 })?;
1380 params
1381 .get(id)
1382 .map_err(|err| RuntimeError::Parameter(err.to_string()))
1383 }
1384
1385 pub fn parameter_count(&self) -> usize {
1387 self.params.len()
1388 }
1389
1390 pub fn free_parameter_count(&self) -> usize {
1392 self.params.n_free()
1393 }
1394
1395 pub fn cache_plan(&self) -> &CachePlan {
1397 &self.cache_plan
1398 }
1399
1400 pub fn evaluate(&self, params: &ParamValues) -> RuntimeResult<Complex64> {
1407 self.evaluate_inner(params, None)
1408 }
1409
1410 pub fn evaluate_with_gradient(&self, params: &ParamValues) -> RuntimeResult<ValueGradient> {
1418 #[cfg(feature = "jit")]
1419 if let (Some(value_kernel), Some(gradient_kernel)) =
1420 (self.scalar_jit_kernel(), self.gradient_jit_kernel())
1421 {
1422 let value = value_kernel.evaluate_invariant(params)?;
1423 let mut real = Vec::new();
1424 let mut imag = Vec::new();
1425 gradient_kernel.evaluate_invariant_component(params, 0, &mut real)?;
1426 gradient_kernel.evaluate_invariant_component(params, 1, &mut imag)?;
1427 let gradient = real
1428 .into_iter()
1429 .zip(imag)
1430 .map(|(re, im)| Complex64::new(re, im))
1431 .collect();
1432 return Ok(ValueGradient { value, gradient });
1433 }
1434 if self.precision == Precision::F32 {
1435 return self.evaluate_f32_gradient(params, F32KernelInput::Cache(None));
1436 }
1437 self.require_f64_gradient()?;
1438 if let Some(interpreter) = self.gradient_interpreter() {
1439 let (value, gradient) = interpreter.evaluate(params, None)?;
1440 return Ok(ValueGradient { value, gradient });
1441 }
1442 let values = self.evaluate_values(params, None)?;
1443 self.value_gradient(values, None)
1444 }
1445
1446 pub fn evaluate_with_event(
1453 &self,
1454 params: &ParamValues,
1455 event: &impl EventLookup,
1456 ) -> RuntimeResult<Complex64> {
1457 self.evaluate_inner(params, Some(event))
1458 }
1459
1460 pub fn evaluate_with_event_and_gradient(
1467 &self,
1468 params: &ParamValues,
1469 event: &impl EventLookup,
1470 ) -> RuntimeResult<ValueGradient> {
1471 if self.precision == Precision::F32 {
1472 return self.evaluate_f32_gradient(params, F32KernelInput::Event(event));
1473 }
1474 self.require_f64_gradient()?;
1475 let values = self.evaluate_values(params, Some(event))?;
1476 self.value_gradient(values, None)
1477 }
1478
1479 pub fn cache_event_batch(&self, batch: &EventBatch) -> RuntimeResult<CpuBatchCache> {
1490 let event_columns = self.event_columns(batch.schema())?;
1491 let mut cache = CpuBatchCache::new(
1492 &self.cache_plan,
1493 &self.factor_matrices,
1494 &self.solve_row_keys,
1495 batch.len(),
1496 );
1497 for row in 0..batch.len() {
1498 let values = self.evaluate_cache_values_for_row(batch, row, &event_columns)?;
1499 for (slot, entry) in self.cache_plan.entries().iter().enumerate() {
1500 let value = values[entry.node().index()]
1501 .as_ref()
1502 .expect("cacheable node should have been evaluated")
1503 .clone();
1504 cache.push(slot, value)?;
1505 }
1506 for plan in &self.solve_row_matrices {
1507 let (rows, cols, values) = matrix_at_optional(&values, plan.matrix().index())?;
1508 if rows != plan.dimension() || cols != plan.dimension() {
1509 return Err(RuntimeError::InvalidShape {
1510 index: plan.matrix().index(),
1511 message: format!(
1512 "specialized solve expected a {}x{} matrix, got {rows}x{cols}",
1513 plan.dimension(),
1514 plan.dimension()
1515 ),
1516 });
1517 }
1518 let transpose_factor = DMatrix::from_row_slice(rows, cols, values).transpose().lu();
1519 for (slot, index) in plan.rows() {
1520 let mut basis = DVector::zeros(plan.dimension());
1521 basis[*index] = Complex64::ONE;
1522 let inverse_row = transpose_factor
1523 .solve(&basis)
1524 .ok_or(RuntimeError::SingularMatrix(plan.matrix().index()))?;
1525 cache.push_solve_row(*slot, inverse_row.iter().copied())?;
1526 }
1527 }
1528 for (slot, (matrix, _)) in self.factor_matrices.iter().enumerate() {
1529 let (rows, cols, values) = matrix_at_optional(&values, matrix.index())?;
1530 cache.push_factor(slot, DMatrix::from_row_slice(rows, cols, values).lu())?;
1531 }
1532 }
1533 cache.set_weights((0..batch.len()).map(|row| batch.weights_at(row)).collect());
1534 Ok(cache)
1535 }
1536
1537 pub fn evaluate_cache(
1544 &self,
1545 params: &ParamValues,
1546 cache: &CpuBatchCache,
1547 ) -> RuntimeResult<Vec<Complex64>> {
1548 self.check_batch_cache(cache)?;
1549 #[cfg(feature = "jit")]
1550 if let Some(kernel) = self.scalar_jit_kernel() {
1551 let mut output = Vec::with_capacity(cache.len());
1552 kernel.evaluate(params, cache, 0, cache.len(), &mut output)?;
1553 return Ok(output);
1554 }
1555 let invariant = self.scalar_invariant_values(params)?;
1556 let mut out = Vec::with_capacity(cache.len());
1557 let mut workspace = ScalarEventWorkspace::default();
1558 for row in 0..cache.len() {
1559 out.push(self.evaluate_cache_row_prepared(
1560 params,
1561 cache,
1562 row,
1563 invariant.as_ref(),
1564 &mut workspace,
1565 )?);
1566 }
1567 Ok(out)
1568 }
1569
1570 pub fn evaluate_cache_row(
1577 &self,
1578 params: &ParamValues,
1579 cache: &CpuBatchCache,
1580 row: usize,
1581 ) -> RuntimeResult<Complex64> {
1582 self.check_batch_cache(cache)?;
1583 self.evaluate_cache_row_unchecked(params, cache, row)
1584 }
1585
1586 fn evaluate_cache_row_unchecked(
1587 &self,
1588 params: &ParamValues,
1589 cache: &CpuBatchCache,
1590 row: usize,
1591 ) -> RuntimeResult<Complex64> {
1592 let invariant = self.scalar_invariant_values(params)?;
1593 self.evaluate_cache_row_prepared(
1594 params,
1595 cache,
1596 row,
1597 invariant.as_ref(),
1598 &mut ScalarEventWorkspace::default(),
1599 )
1600 }
1601
1602 fn evaluate_cache_row_prepared(
1603 &self,
1604 params: &ParamValues,
1605 cache: &CpuBatchCache,
1606 row: usize,
1607 invariant: Option<&ScalarInvariantValues>,
1608 workspace: &mut ScalarEventWorkspace,
1609 ) -> RuntimeResult<Complex64> {
1610 #[cfg(feature = "jit")]
1611 if let Some(kernel) = self.scalar_jit_kernel() {
1612 let mut output = Vec::with_capacity(1);
1613 kernel.evaluate(params, cache, row, row + 1, &mut output)?;
1614 return Ok(output[0]);
1615 }
1616 if self.precision == Precision::F32 {
1617 return self.evaluate_f32_scalar(params, F32KernelInput::Cache(Some((cache, row))));
1618 }
1619 if let (Some(plan), Some(invariant)) = (self.scalar_interpreter_plan(), invariant) {
1620 return self.evaluate_scalar_cache_row(cache, row, plan, invariant, workspace);
1621 }
1622 let values = self.evaluate_values_from_cache(params, cache, row)?;
1623 self.cached_scalar_at(&values, self.graph.root())
1624 }
1625
1626 fn scalar_invariant_values(
1627 &self,
1628 params: &ParamValues,
1629 ) -> RuntimeResult<Option<ScalarInvariantValues>> {
1630 if self.precision == Precision::F32 {
1631 return Ok(None);
1632 }
1633 let Some(plan) = self.scalar_interpreter_plan() else {
1634 return Ok(None);
1635 };
1636 let mut values = ScalarInvariantValues {
1637 real: vec![0.0; plan.invariant_real_slot_count],
1638 complex: vec![Complex64::ZERO; plan.invariant_complex_slot_count],
1639 };
1640 let event = ScalarEventWorkspace::default();
1641 for instruction in &plan.invariant_instructions {
1642 match instruction.output_slot {
1643 ScalarSlot::Real(slot) => {
1644 values.real[slot] = instruction.instruction.evaluate_real(
1645 Some(params),
1646 None,
1647 &values,
1648 &event,
1649 )?;
1650 }
1651 ScalarSlot::Complex(slot) => {
1652 values.complex[slot] = instruction.instruction.evaluate_complex(
1653 Some(params),
1654 None,
1655 &values,
1656 &event,
1657 )?;
1658 }
1659 }
1660 }
1661 Ok(Some(values))
1662 }
1663
1664 fn evaluate_scalar_cache_row(
1665 &self,
1666 cache: &CpuBatchCache,
1667 row: usize,
1668 plan: &ScalarEvaluationPlan,
1669 invariant: &ScalarInvariantValues,
1670 values: &mut ScalarEventWorkspace,
1671 ) -> RuntimeResult<Complex64> {
1672 values.real.clear();
1673 values
1674 .real
1675 .resize(plan.event_real_slot_count, [0.0; SCALAR_BLOCK_SIZE]);
1676 values.complex.clear();
1677 values.complex.resize(
1678 plan.event_complex_slot_count,
1679 [Complex64::ZERO; SCALAR_BLOCK_SIZE],
1680 );
1681 for event_instruction in &plan.event_instructions {
1682 match event_instruction.output_slot {
1683 ScalarSlot::Real(slot) => {
1684 values.real[slot][0] = event_instruction.instruction.evaluate_real(
1685 None,
1686 Some((cache, row)),
1687 invariant,
1688 values,
1689 )?;
1690 }
1691 ScalarSlot::Complex(slot) => {
1692 values.complex[slot][0] = event_instruction.instruction.evaluate_complex(
1693 None,
1694 Some((cache, row)),
1695 invariant,
1696 values,
1697 )?;
1698 }
1699 }
1700 }
1701 Ok(plan.root().complex_value(invariant, values))
1702 }
1703
1704 #[allow(clippy::too_many_arguments)]
1705 fn evaluate_cache_block_prepared(
1706 &self,
1707 params: &ParamValues,
1708 cache: &CpuBatchCache,
1709 start: usize,
1710 end: usize,
1711 invariant: Option<&ScalarInvariantValues>,
1712 workspace: &mut ScalarEventWorkspace,
1713 output: &mut Vec<Complex64>,
1714 #[cfg(feature = "jit")] jit_cache: Option<&JitCacheView>,
1715 ) -> RuntimeResult<()> {
1716 #[cfg(feature = "jit")]
1717 if let Some(kernel) = self.scalar_jit_kernel() {
1718 let owned;
1719 let jit_cache = if let Some(jit_cache) = jit_cache {
1720 jit_cache
1721 } else {
1722 owned = JitScalarKernel::prepare_cache(cache);
1723 &owned
1724 };
1725 return kernel.evaluate_prepared(params, jit_cache, start, end, output);
1726 }
1727 if self.precision == Precision::F32 {
1728 output.clear();
1729 output.reserve(end - start);
1730 for row in start..end {
1731 output.push(
1732 self.evaluate_f32_scalar(params, F32KernelInput::Cache(Some((cache, row))))?,
1733 );
1734 }
1735 return Ok(());
1736 }
1737 if let (Some(plan), Some(invariant)) = (self.scalar_interpreter_plan(), invariant) {
1738 return evaluate_scalar_cache_block(
1739 cache, start, end, plan, invariant, workspace, output,
1740 );
1741 }
1742 output.clear();
1743 for row in start..end {
1744 output
1745 .push(self.evaluate_cache_row_prepared(params, cache, row, invariant, workspace)?);
1746 }
1747 Ok(())
1748 }
1749}
1750
1751fn evaluate_scalar_cache_block(
1752 cache: &CpuBatchCache,
1753 start: usize,
1754 end: usize,
1755 plan: &ScalarEvaluationPlan,
1756 invariant: &ScalarInvariantValues,
1757 workspace: &mut ScalarEventWorkspace,
1758 output: &mut Vec<Complex64>,
1759) -> RuntimeResult<()> {
1760 let block_len = end - start;
1761 workspace
1762 .real
1763 .resize(plan.event_real_slot_count, [0.0; SCALAR_BLOCK_SIZE]);
1764 workspace.complex.resize(
1765 plan.event_complex_slot_count,
1766 [Complex64::ZERO; SCALAR_BLOCK_SIZE],
1767 );
1768
1769 for event_instruction in &plan.event_instructions {
1770 match event_instruction.output_slot {
1771 ScalarSlot::Real(slot) => {
1772 let output_slot = slot;
1773 match &event_instruction.instruction {
1774 ScalarInstruction::Cached(slot) => {
1775 workspace.real[output_slot][..block_len]
1776 .copy_from_slice(cache.real_range(*slot, start, end)?);
1777 }
1778 ScalarInstruction::Unary { op, input } => {
1779 for lane in 0..block_len {
1780 workspace.real[output_slot][lane] = match op {
1781 UnaryOp::Neg => -input.block_real_value(invariant, workspace, lane),
1782 UnaryOp::Real | UnaryOp::Conj => {
1783 input.block_complex_value(invariant, workspace, lane).re
1784 }
1785 UnaryOp::Imag => {
1786 input.block_complex_value(invariant, workspace, lane).im
1787 }
1788 UnaryOp::NormSqr => input
1789 .block_complex_value(invariant, workspace, lane)
1790 .norm_sqr(),
1791 UnaryOp::Sqrt => {
1792 input.block_real_value(invariant, workspace, lane).sqrt()
1793 }
1794 UnaryOp::Exp => {
1795 input.block_real_value(invariant, workspace, lane).exp()
1796 }
1797 UnaryOp::Sin => {
1798 input.block_real_value(invariant, workspace, lane).sin()
1799 }
1800 UnaryOp::Cos => {
1801 input.block_real_value(invariant, workspace, lane).cos()
1802 }
1803 UnaryOp::Log => {
1804 input.block_real_value(invariant, workspace, lane).ln()
1805 }
1806 UnaryOp::PowI(power) => input
1807 .block_real_value(invariant, workspace, lane)
1808 .powi(*power),
1809 };
1810 }
1811 }
1812 ScalarInstruction::Binary { op, lhs, rhs } => {
1813 for lane in 0..block_len {
1814 let lhs = lhs.block_real_value(invariant, workspace, lane);
1815 let rhs = rhs.block_real_value(invariant, workspace, lane);
1816 workspace.real[output_slot][lane] = match op {
1817 BinaryOp::Add => lhs + rhs,
1818 BinaryOp::Sub => lhs - rhs,
1819 BinaryOp::Mul => lhs * rhs,
1820 BinaryOp::Div => lhs / rhs,
1821 BinaryOp::Atan2 => lhs.atan2(rhs),
1822 };
1823 }
1824 }
1825 ScalarInstruction::Add(runs) => {
1826 workspace.real[output_slot][..block_len].fill(0.0);
1827 for run in runs {
1828 match run {
1829 OperandRun::InvariantReal(slots) => {
1830 for slot in slots {
1831 let operand = invariant.real[*slot];
1832 for lane in 0..block_len {
1833 workspace.real[output_slot][lane] += operand;
1834 }
1835 }
1836 }
1837 OperandRun::EventReal(slots) => {
1838 for slot in slots {
1839 for lane in 0..block_len {
1840 workspace.real[output_slot][lane] +=
1841 workspace.real[*slot][lane];
1842 }
1843 }
1844 }
1845 OperandRun::InvariantComplex(_) | OperandRun::EventComplex(_) => {
1846 unreachable!("complex operand appeared in real add")
1847 }
1848 }
1849 }
1850 }
1851 ScalarInstruction::Mul(runs) => {
1852 workspace.real[output_slot][..block_len].fill(1.0);
1853 for run in runs {
1854 match run {
1855 OperandRun::InvariantReal(slots) => {
1856 for slot in slots {
1857 let operand = invariant.real[*slot];
1858 for lane in 0..block_len {
1859 workspace.real[output_slot][lane] *= operand;
1860 }
1861 }
1862 }
1863 OperandRun::EventReal(slots) => {
1864 for slot in slots {
1865 for lane in 0..block_len {
1866 workspace.real[output_slot][lane] *=
1867 workspace.real[*slot][lane];
1868 }
1869 }
1870 }
1871 OperandRun::InvariantComplex(_) | OperandRun::EventComplex(_) => {
1872 unreachable!("complex operand appeared in real multiply")
1873 }
1874 }
1875 }
1876 }
1877 ScalarInstruction::Constant(_)
1878 | ScalarInstruction::Parameter(_)
1879 | ScalarInstruction::Complex { .. }
1880 | ScalarInstruction::SolveRow { .. }
1881 | ScalarInstruction::SolveRowAdjointElement { .. } => {
1882 unreachable!("non-real event instruction appeared in a real slot")
1883 }
1884 }
1885 }
1886 ScalarSlot::Complex(slot) => {
1887 let output_slot = slot;
1888 match &event_instruction.instruction {
1889 ScalarInstruction::Cached(slot) => {
1890 workspace.complex[output_slot][..block_len]
1891 .copy_from_slice(cache.complex_range(*slot, start, end)?);
1892 }
1893 ScalarInstruction::Unary { op, input } => {
1894 for lane in 0..block_len {
1895 let input = input.block_complex_value(invariant, workspace, lane);
1896 workspace.complex[output_slot][lane] = eval_unary(*op, input);
1897 }
1898 }
1899 ScalarInstruction::Binary { op, lhs, rhs } => {
1900 for lane in 0..block_len {
1901 let lhs = lhs.block_complex_value(invariant, workspace, lane);
1902 let rhs = rhs.block_complex_value(invariant, workspace, lane);
1903 workspace.complex[output_slot][lane] = eval_binary(*op, lhs, rhs);
1904 }
1905 }
1906 ScalarInstruction::Add(runs) => {
1907 workspace.complex[output_slot][..block_len].fill(Complex64::ZERO);
1908 for run in runs {
1909 match run {
1910 OperandRun::InvariantReal(slots) => {
1911 for slot in slots {
1912 let operand = invariant.real[*slot];
1913 for lane in 0..block_len {
1914 workspace.complex[output_slot][lane] += operand;
1915 }
1916 }
1917 }
1918 OperandRun::InvariantComplex(slots) => {
1919 for slot in slots {
1920 let operand = invariant.complex[*slot];
1921 for lane in 0..block_len {
1922 workspace.complex[output_slot][lane] += operand;
1923 }
1924 }
1925 }
1926 OperandRun::EventReal(slots) => {
1927 for slot in slots {
1928 for lane in 0..block_len {
1929 workspace.complex[output_slot][lane] +=
1930 workspace.real[*slot][lane];
1931 }
1932 }
1933 }
1934 OperandRun::EventComplex(slots) => {
1935 for slot in slots {
1936 for lane in 0..block_len {
1937 let operand = workspace.complex[*slot][lane];
1938 workspace.complex[output_slot][lane] += operand;
1939 }
1940 }
1941 }
1942 }
1943 }
1944 }
1945 ScalarInstruction::Mul(runs) => {
1946 workspace.complex[output_slot][..block_len].fill(Complex64::ONE);
1947 for run in runs {
1948 match run {
1949 OperandRun::InvariantReal(slots) => {
1950 for slot in slots {
1951 let operand = invariant.real[*slot];
1952 for lane in 0..block_len {
1953 workspace.complex[output_slot][lane] *= operand;
1954 }
1955 }
1956 }
1957 OperandRun::InvariantComplex(slots) => {
1958 for slot in slots {
1959 let operand = invariant.complex[*slot];
1960 for lane in 0..block_len {
1961 workspace.complex[output_slot][lane] *= operand;
1962 }
1963 }
1964 }
1965 OperandRun::EventReal(slots) => {
1966 for slot in slots {
1967 for lane in 0..block_len {
1968 workspace.complex[output_slot][lane] *=
1969 workspace.real[*slot][lane];
1970 }
1971 }
1972 }
1973 OperandRun::EventComplex(slots) => {
1974 for slot in slots {
1975 for lane in 0..block_len {
1976 let operand = workspace.complex[*slot][lane];
1977 workspace.complex[output_slot][lane] *= operand;
1978 }
1979 }
1980 }
1981 }
1982 }
1983 }
1984 ScalarInstruction::Complex { re, im } => {
1985 for lane in 0..block_len {
1986 workspace.complex[output_slot][lane] = Complex64::new(
1987 re.block_real_value(invariant, workspace, lane),
1988 im.block_real_value(invariant, workspace, lane),
1989 );
1990 }
1991 }
1992 ScalarInstruction::SolveRow { row_slot, rhs } => {
1993 for lane in 0..block_len {
1994 let inverse_row = cache.solve_row(*row_slot, start + lane)?;
1995 if inverse_row.len() != rhs.len() {
1996 return Err(RuntimeError::InvalidShape {
1997 index: start + lane,
1998 message: format!(
1999 "specialized solve row has len {}, expected {}",
2000 inverse_row.len(),
2001 rhs.len()
2002 ),
2003 });
2004 }
2005 workspace.complex[output_slot][lane] = inverse_row
2006 .iter()
2007 .zip(rhs)
2008 .map(|(lhs, operand)| {
2009 lhs * operand.block_complex_value(invariant, workspace, lane)
2010 })
2011 .sum();
2012 }
2013 }
2014 ScalarInstruction::SolveRowAdjointElement {
2015 row_slot,
2016 index,
2017 len,
2018 adjoint,
2019 } => {
2020 for lane in 0..block_len {
2021 let inverse_row = cache.solve_row(*row_slot, start + lane)?;
2022 if inverse_row.len() != *len {
2023 return Err(RuntimeError::InvalidShape {
2024 index: start + lane,
2025 message: format!(
2026 "specialized solve row has len {}, expected {len}",
2027 inverse_row.len()
2028 ),
2029 });
2030 }
2031 workspace.complex[output_slot][lane] = adjoint
2032 .block_complex_value(invariant, workspace, lane)
2033 * inverse_row[*index].conj();
2034 }
2035 }
2036 ScalarInstruction::Constant(_) | ScalarInstruction::Parameter(_) => {
2037 unreachable!("invariant instruction appeared in the event tape")
2038 }
2039 }
2040 }
2041 }
2042 }
2043
2044 output.clear();
2045 output.reserve(block_len * plan.outputs.len());
2046 for lane in 0..block_len {
2047 for output_operand in &plan.outputs {
2048 output.push(output_operand.block_complex_value(invariant, workspace, lane));
2049 }
2050 }
2051 Ok(())
2052}
2053
2054impl CpuPlan {
2055 pub fn evaluate_cache_row_with_gradient(
2062 &self,
2063 params: &ParamValues,
2064 cache: &CpuBatchCache,
2065 row: usize,
2066 ) -> RuntimeResult<ValueGradient> {
2067 self.check_batch_cache(cache)?;
2068 self.evaluate_cache_row_with_gradient_unchecked(params, cache, row)
2069 }
2070
2071 fn evaluate_cache_row_with_gradient_unchecked(
2072 &self,
2073 params: &ParamValues,
2074 cache: &CpuBatchCache,
2075 row: usize,
2076 ) -> RuntimeResult<ValueGradient> {
2077 #[cfg(feature = "jit")]
2078 if self.gradient_jit_kernel().is_some() {
2079 return self
2080 .evaluate_cache_gradient_jit(params, cache, row, row + 1)?
2081 .pop()
2082 .ok_or_else(|| RuntimeError::InvalidShape {
2083 index: row,
2084 message: "single-row JIT gradient produced no value".into(),
2085 });
2086 }
2087 if self.precision == Precision::F32 {
2088 return self.evaluate_f32_gradient(params, F32KernelInput::Cache(Some((cache, row))));
2089 }
2090 if self.autodiff.mode() == AutodiffMode::Reverse {
2091 self.require_f64_gradient()?;
2092 let values = self.evaluate_values_from_cache(params, cache, row)?;
2093 return self.value_gradient(values, Some((cache, row)));
2094 }
2095 if let Some(interpreter) = self.gradient_interpreter() {
2096 let (value, gradient) = interpreter.evaluate(params, Some((cache, row)))?;
2097 return Ok(ValueGradient { value, gradient });
2098 }
2099 let values = self.evaluate_values_from_cache(params, cache, row)?;
2100 self.value_gradient(values, Some((cache, row)))
2101 }
2102
2103 pub fn evaluate_cache_with_gradient(
2110 &self,
2111 params: &ParamValues,
2112 cache: &CpuBatchCache,
2113 ) -> RuntimeResult<Vec<ValueGradient>> {
2114 self.check_batch_cache(cache)?;
2115 #[cfg(feature = "jit")]
2116 if self.gradient_jit_kernel().is_some() {
2117 return self.evaluate_cache_gradient_jit(params, cache, 0, cache.len());
2118 }
2119 if self.precision == Precision::F32 {
2120 return (0..cache.len())
2121 .map(|row| {
2122 self.evaluate_f32_gradient(params, F32KernelInput::Cache(Some((cache, row))))
2123 })
2124 .collect();
2125 }
2126 self.require_f64_gradient()?;
2127 (0..cache.len())
2128 .map(|row| self.evaluate_cache_row_with_gradient_unchecked(params, cache, row))
2129 .collect()
2130 }
2131
2132 #[cfg(feature = "jit")]
2133 fn evaluate_cache_gradient_jit(
2134 &self,
2135 params: &ParamValues,
2136 cache: &CpuBatchCache,
2137 start: usize,
2138 end: usize,
2139 ) -> RuntimeResult<Vec<ValueGradient>> {
2140 let (Some(value_kernel), Some(gradient_kernel)) =
2141 (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2142 else {
2143 return Err(RuntimeError::InvalidShape {
2144 index: self.graph.root().index(),
2145 message: "JIT gradient evaluation requires both scalar and gradient kernels".into(),
2146 });
2147 };
2148 let view = JitScalarKernel::prepare_cache(cache);
2149 let mut values = Vec::new();
2150 let mut real = Vec::new();
2151 let mut imag = Vec::new();
2152 value_kernel.evaluate_prepared(params, &view, start, end, &mut values)?;
2153 gradient_kernel.evaluate_prepared(params, &view, start, end, 0, &mut real)?;
2154 gradient_kernel.evaluate_prepared(params, &view, start, end, 1, &mut imag)?;
2155 let parameter_count = self.free_parameter_count();
2156 Ok(values
2157 .into_iter()
2158 .enumerate()
2159 .map(|(row, value)| ValueGradient {
2160 value,
2161 gradient: (0..parameter_count)
2162 .map(|parameter| {
2163 let index = row * parameter_count + parameter;
2164 Complex64::new(real[index], imag[index])
2165 })
2166 .collect(),
2167 })
2168 .collect())
2169 }
2170
2171 pub fn evaluate_batch(
2177 &self,
2178 params: &ParamValues,
2179 batch: &EventBatch,
2180 ) -> RuntimeResult<Vec<Complex64>> {
2181 let cache = self.cache_event_batch(batch)?;
2182 self.evaluate_cache(params, &cache)
2183 }
2184
2185 pub fn evaluate_batch_with_gradient(
2192 &self,
2193 params: &ParamValues,
2194 batch: &EventBatch,
2195 ) -> RuntimeResult<Vec<ValueGradient>> {
2196 let cache = self.cache_event_batch(batch)?;
2197 self.evaluate_cache_with_gradient(params, &cache)
2198 }
2199
2200 pub fn cache_dataset(&self, dataset: &Dataset) -> RuntimeResult<CpuCachedDataset> {
2207 self.cache_dataset_with_plan(dataset, dataset.read_plan())
2208 }
2209
2210 pub fn cache_memory_estimate(&self, events: usize) -> usize {
2212 let fixed = self.cache_plan.entries().len() * size_of::<CachedSlot>()
2213 + self.factor_matrices.len() * size_of::<CachedFactorSlot>()
2214 + self.solve_row_keys.len() * size_of::<CachedSolveRowSlot>()
2215 + self.cache_plan.entries().len() * size_of::<ExprId>()
2216 + self.factor_matrices.len() * size_of::<ExprId>()
2217 + self.solve_row_keys.len() * size_of::<(ExprId, usize, usize)>();
2218 let slot_bytes = self
2219 .cache_plan
2220 .entries()
2221 .iter()
2222 .map(|entry| match entry.value_kind() {
2223 ValueKind::Real => size_of::<f64>(),
2224 ValueKind::Complex => size_of::<Complex64>(),
2225 ValueKind::Vector { len } => len * size_of::<Complex64>(),
2226 ValueKind::Matrix { rows, cols } => rows * cols * size_of::<Complex64>(),
2227 })
2228 .sum::<usize>();
2229 let factor_bytes = self
2230 .factor_matrices
2231 .iter()
2232 .map(|(_, dimension)| {
2233 size_of::<DynamicLu>()
2234 + dimension * dimension * size_of::<Complex64>()
2235 + dimension * size_of::<usize>()
2236 })
2237 .sum::<usize>();
2238 let solve_bytes = self
2239 .solve_row_keys
2240 .iter()
2241 .map(|(_, _, dimension)| dimension * size_of::<Complex64>())
2242 .sum::<usize>();
2243 fixed.saturating_add(
2244 events.saturating_mul(
2245 size_of::<f64>()
2246 .saturating_add(slot_bytes)
2247 .saturating_add(factor_bytes)
2248 .saturating_add(solve_bytes),
2249 ),
2250 )
2251 }
2252
2253 fn cache_dataset_with_plan(
2254 &self,
2255 dataset: &Dataset,
2256 read_plan: laddu_data::io::ReadPlan,
2257 ) -> RuntimeResult<CpuCachedDataset> {
2258 let mut batches = Vec::new();
2259 let mut sum_weights = 0.0;
2260 for batch in dataset
2261 .batches_with_plan(read_plan)
2262 .map_err(|err| RuntimeError::Data(err.to_string()))?
2263 {
2264 let batch = batch.map_err(|err| RuntimeError::Data(err.to_string()))?;
2265 let cached = CpuCachedBatch {
2266 cache: self.cache_event_batch(&batch)?,
2267 };
2268 sum_weights += cached.sum_weights();
2269 batches.push(cached);
2270 }
2271 Ok(CpuCachedDataset {
2272 batches,
2273 sum_weights,
2274 })
2275 }
2276
2277 pub fn prepare_dataset(
2284 &self,
2285 execution: &Execution,
2286 dataset: &Dataset,
2287 ) -> RuntimeResult<CpuPreparedDataset> {
2288 let mut read_plan = execution.read_plan(dataset.read_plan());
2289 let schema = dataset
2290 .schema()
2291 .map_err(|error| RuntimeError::Data(error.to_string()))?;
2292 let source_bytes_per_event =
2293 (4 * schema.n_p4s() + schema.n_scalars() + usize::from(schema.has_weight()))
2294 * size_of::<f64>();
2295 let cache_one = self.cache_memory_estimate(1);
2296 let cache_zero = self.cache_memory_estimate(0);
2297 let cache_bytes_per_event = cache_one.saturating_sub(cache_zero);
2298 let known_local_events = dataset
2299 .num_events()
2300 .map_err(|error| RuntimeError::Data(error.to_string()))?
2301 .and_then(|events| usize::try_from(events).ok());
2302 let discovered =
2303 if known_local_events.is_none() && dataset.memory_policy() != MemoryPolicy::Streaming {
2304 let local = scan_dataset_stats(dataset, read_plan);
2305 if !execution.all_succeeded(local.is_ok()) {
2306 return local.and(Err(RuntimeError::DistributedPeerFailure));
2307 }
2308 Some(local?)
2309 } else {
2310 None
2311 };
2312 let local_event_limit = known_local_events
2313 .or_else(|| discovered.map(|stats| stats.events))
2314 .unwrap_or(usize::MAX);
2315 let host_remaining = execution.host_memory().remaining();
2316 let resident_plan = resident_cache_plan(
2317 cache_zero,
2318 cache_bytes_per_event,
2319 source_bytes_per_event.saturating_mul(2),
2320 local_event_limit,
2321 usize::try_from(host_remaining).unwrap_or(usize::MAX),
2322 );
2323 let requested_storage = match dataset.memory_policy() {
2324 MemoryPolicy::Streaming => CacheStorage::Streaming,
2325 MemoryPolicy::Resident => {
2326 if resident_plan.is_none() {
2327 return Err(laddu_memory::MemoryError::BudgetExceeded {
2328 resource: "host".into(),
2329 requested: u64::try_from(
2330 cache_bytes_per_event
2331 .saturating_mul(local_event_limit)
2332 .saturating_add(cache_zero)
2333 .saturating_add(source_bytes_per_event.saturating_mul(2)),
2334 )
2335 .unwrap_or(u64::MAX),
2336 remaining: host_remaining,
2337 }
2338 .into());
2339 }
2340 CacheStorage::Resident
2341 }
2342 MemoryPolicy::Fastest if resident_plan.is_some() => CacheStorage::Resident,
2343 MemoryPolicy::Fastest => CacheStorage::Streaming,
2344 };
2345 let persistent_lease = if requested_storage == CacheStorage::Resident {
2346 let (resident_bytes, _) = resident_plan
2347 .ok_or_else(|| RuntimeError::Data("resident cache plan was not resolved".into()))?;
2348 Some(
2349 execution
2350 .host_memory()
2351 .reserve(u64::try_from(resident_bytes).unwrap_or(u64::MAX))?,
2352 )
2353 } else {
2354 None
2355 };
2356 let available_for_batch = execution.host_memory().remaining();
2357 let (fixed_peak, per_event_peak) = if requested_storage == CacheStorage::Streaming {
2361 (
2362 cache_zero,
2363 source_bytes_per_event
2364 .saturating_mul(2)
2365 .saturating_add(cache_bytes_per_event),
2366 )
2367 } else {
2368 (0, source_bytes_per_event.saturating_mul(2))
2369 };
2370 let decision = MemoryDecision::fit(
2371 "CPU prepared dataset",
2372 u64::try_from(fixed_peak).unwrap_or(u64::MAX),
2373 u64::try_from(per_event_peak).unwrap_or(u64::MAX),
2374 available_for_batch,
2375 local_event_limit,
2376 if requested_storage == CacheStorage::Resident {
2377 "resident"
2378 } else {
2379 "streaming"
2380 },
2381 )?;
2382 read_plan.chunk_size = Some(
2383 read_plan
2384 .chunk_size
2385 .map_or(decision.chunk_events, |manual| {
2386 manual.min(decision.chunk_events)
2387 })
2388 .max(1),
2389 );
2390 execution.record_memory_decision(decision.clone());
2391 match requested_storage {
2392 CacheStorage::Resident => {
2393 let local = self.cache_dataset_with_plan(dataset, read_plan);
2394 if !execution.all_succeeded(local.is_ok()) {
2395 return local.and(Err(RuntimeError::DistributedPeerFailure));
2396 }
2397 let dataset = local?;
2398 let stats = PreparedDatasetStats {
2399 local_events: dataset.len(),
2400 global_events: execution.sum_usize(dataset.len()),
2401 local_batches: dataset.batches().len(),
2402 sum_weights: execution.sum_f64(dataset.sum_weights()),
2403 resident_bytes: dataset.resident_bytes(),
2404 storage: CacheStorage::Resident,
2405 };
2406 let memory_lease = persistent_lease.ok_or_else(|| {
2407 RuntimeError::Data(
2408 "resident dataset preparation did not reserve host memory".into(),
2409 )
2410 })?;
2411 Ok(CpuPreparedDataset::Resident {
2412 dataset: Arc::new(dataset),
2413 stats,
2414 memory_lease,
2415 })
2416 }
2417 CacheStorage::Streaming => {
2418 let local = scan_dataset_stats(dataset, read_plan);
2419 if !execution.all_succeeded(local.is_ok()) {
2420 return local.and(Err(RuntimeError::DistributedPeerFailure));
2421 }
2422 let local = local?;
2423 Ok(CpuPreparedDataset::Streaming {
2424 dataset: dataset.clone(),
2425 stats: PreparedDatasetStats {
2426 local_events: local.events,
2427 global_events: execution.sum_usize(local.events),
2428 local_batches: local.batches,
2429 sum_weights: execution.sum_f64(local.sum_weights),
2430 resident_bytes: 0,
2431 storage: CacheStorage::Streaming,
2432 },
2433 read_plan,
2434 transient_bytes: decision.estimated_peak_bytes,
2435 })
2436 }
2437 }
2438 }
2439
2440 pub fn reduce(
2447 &self,
2448 execution: &Execution,
2449 params: &ParamValues,
2450 dataset: &CpuPreparedDataset,
2451 reduction: ReductionPlan,
2452 ) -> RuntimeResult<f64> {
2453 let local = match dataset {
2454 CpuPreparedDataset::Resident { dataset, .. } => {
2455 self.reduce_cached(execution, params, dataset, reduction)
2456 }
2457 CpuPreparedDataset::Streaming {
2458 dataset,
2459 read_plan,
2460 transient_bytes,
2461 ..
2462 } => (|| {
2463 let _memory = execution
2464 .host_memory()
2465 .reserve(*transient_bytes)
2466 .map_err(RuntimeError::from)?;
2467 let mut total = AccurateF64::zero();
2468 for batch in dataset
2469 .batches_with_plan(*read_plan)
2470 .map_err(|error| RuntimeError::Data(error.to_string()))?
2471 {
2472 let batch = batch.map_err(|error| RuntimeError::Data(error.to_string()))?;
2473 let cached = CpuCachedDataset {
2474 sum_weights: (0..batch.len()).map(|row| batch.weights_at(row)).sum(),
2475 batches: vec![CpuCachedBatch {
2476 cache: self.cache_event_batch(&batch)?,
2477 }],
2478 };
2479 total.push(self.reduce_cached(execution, params, &cached, reduction)?);
2480 }
2481 Ok(total.finish())
2482 })(),
2483 };
2484 if !execution.all_succeeded(local.is_ok()) {
2485 return local.and(Err(RuntimeError::DistributedPeerFailure));
2486 }
2487 Ok(execution.sum_f64(local?))
2488 }
2489
2490 pub fn reduce_with_gradient(
2498 &self,
2499 execution: &Execution,
2500 params: &ParamValues,
2501 dataset: &CpuPreparedDataset,
2502 reduction: ReductionPlan,
2503 ) -> RuntimeResult<ReductionEvaluation> {
2504 let (value, gradient) =
2505 self.try_reduce_weighted_with_gradient(execution, params, dataset, |value| {
2506 reduction
2507 .apply(value)
2508 .map(|output| output.into_parts())
2509 .map_err(RuntimeError::from)
2510 })?;
2511 Ok(ReductionEvaluation { value, gradient })
2512 }
2513
2514 fn try_reduce_weighted_with_gradient<E, F>(
2515 &self,
2516 execution: &Execution,
2517 params: &ParamValues,
2518 dataset: &CpuPreparedDataset,
2519 transform: F,
2520 ) -> Result<(f64, Vec<f64>), E>
2521 where
2522 E: From<RuntimeError> + Send,
2523 F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2524 {
2525 let local = match dataset {
2526 CpuPreparedDataset::Resident { dataset, .. } => {
2527 self.try_reduce_weighted_with_gradient_cached(execution, params, dataset, transform)
2528 }
2529 CpuPreparedDataset::Streaming {
2530 dataset,
2531 read_plan,
2532 transient_bytes,
2533 ..
2534 } => (|| {
2535 let _memory = execution
2536 .host_memory()
2537 .reserve(*transient_bytes)
2538 .map_err(RuntimeError::from)
2539 .map_err(E::from)?;
2540 let mut value = AccurateF64::zero();
2541 let mut gradient = (0..self.free_parameter_count())
2542 .map(|_| AccurateF64::zero())
2543 .collect::<Vec<_>>();
2544 for batch in dataset
2545 .batches_with_plan(*read_plan)
2546 .map_err(|error| E::from(RuntimeError::Data(error.to_string())))?
2547 {
2548 let batch =
2549 batch.map_err(|error| E::from(RuntimeError::Data(error.to_string())))?;
2550 let cached = CpuCachedDataset {
2551 sum_weights: (0..batch.len()).map(|row| batch.weights_at(row)).sum(),
2552 batches: vec![CpuCachedBatch {
2553 cache: self.cache_event_batch(&batch)?,
2554 }],
2555 };
2556 let (partial_value, partial_gradient) = self
2557 .try_reduce_weighted_with_gradient_cached(
2558 execution, params, &cached, &transform,
2559 )?;
2560 value.push(partial_value);
2561 for (sum, partial) in gradient.iter_mut().zip(partial_gradient) {
2562 sum.push(partial);
2563 }
2564 }
2565 Ok::<_, E>((
2566 value.finish(),
2567 gradient.into_iter().map(AccurateF64::finish).collect(),
2568 ))
2569 })(),
2570 };
2571 if !execution.all_succeeded(local.is_ok()) {
2572 return local.and(Err(E::from(RuntimeError::DistributedPeerFailure)));
2573 }
2574 let (local_value, local_gradient) = local?;
2575 Ok((
2576 execution.sum_f64(local_value),
2577 execution.sum_slice(&local_gradient),
2578 ))
2579 }
2580
2581 pub fn evaluate_cached_dataset(
2588 &self,
2589 params: &ParamValues,
2590 dataset: &CpuCachedDataset,
2591 ) -> RuntimeResult<Vec<Complex64>> {
2592 let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
2593 let mut out = Vec::with_capacity(total_len);
2594 let invariant = self.scalar_invariant_values(params)?;
2595 let mut workspace = ScalarEventWorkspace::default();
2596 for batch in &dataset.batches {
2597 self.check_batch_cache(batch.cache())?;
2598 for row in 0..batch.len() {
2599 out.push(self.evaluate_cache_row_prepared(
2600 params,
2601 batch.cache(),
2602 row,
2603 invariant.as_ref(),
2604 &mut workspace,
2605 )?);
2606 }
2607 }
2608 Ok(out)
2609 }
2610
2611 pub fn evaluate_cached_dataset_with_gradient(
2618 &self,
2619 params: &ParamValues,
2620 dataset: &CpuCachedDataset,
2621 ) -> RuntimeResult<Vec<ValueGradient>> {
2622 let total_len = dataset.batches.iter().map(CpuCachedBatch::len).sum();
2623 let mut out = Vec::with_capacity(total_len);
2624 for batch in &dataset.batches {
2625 out.extend(self.evaluate_cache_with_gradient(params, batch.cache())?);
2626 }
2627 Ok(out)
2628 }
2629
2630 fn try_weighted_sum_cached<E, F>(
2631 &self,
2632 params: &ParamValues,
2633 dataset: &CpuCachedDataset,
2634 mut f: F,
2635 ) -> Result<f64, E>
2636 where
2637 E: From<RuntimeError>,
2638 F: FnMut(Complex64) -> Result<f64, E>,
2639 {
2640 let mut sum = 0.0;
2641 let invariant = self.scalar_invariant_values(params)?;
2642 let mut workspace = ScalarEventWorkspace::default();
2643 for batch in dataset.batches() {
2644 self.check_batch_cache(batch.cache())?;
2645 for row in 0..batch.len() {
2646 let value = self.evaluate_cache_row_prepared(
2647 params,
2648 batch.cache(),
2649 row,
2650 invariant.as_ref(),
2651 &mut workspace,
2652 )?;
2653 sum += batch.weights()[row] * f(value)?;
2654 }
2655 }
2656 Ok(sum)
2657 }
2658
2659 #[cfg(test)]
2660 fn weighted_sum_cached<F>(
2661 &self,
2662 params: &ParamValues,
2663 dataset: &CpuCachedDataset,
2664 mut f: F,
2665 ) -> RuntimeResult<f64>
2666 where
2667 F: FnMut(Complex64) -> f64,
2668 {
2669 self.try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
2670 }
2671
2672 fn try_weighted_real_sum_with_gradient_cached<E, F>(
2673 &self,
2674 params: &ParamValues,
2675 dataset: &CpuCachedDataset,
2676 mut transform: F,
2677 ) -> Result<(f64, Vec<f64>), E>
2678 where
2679 E: From<RuntimeError>,
2680 F: FnMut(Complex64) -> Result<(f64, f64), E>,
2681 {
2682 #[cfg(feature = "jit")]
2683 if let (Some(value_kernel), Some(gradient_kernel)) =
2684 (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2685 {
2686 return self.try_weighted_real_sum_with_jit_gradient_cached(
2687 params,
2688 dataset,
2689 transform,
2690 value_kernel,
2691 gradient_kernel,
2692 );
2693 }
2694 if self.precision != Precision::F32
2695 && let Some(interpreter) = self.gradient_interpreter()
2696 && let Some(mut state) = interpreter.prepare_real_blocks(params)?
2697 {
2698 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2699 let output_count = state.output_count();
2700 for batch in dataset.batches() {
2701 self.check_batch_cache(batch.cache())?;
2702 for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
2703 let start = block * SCALAR_BLOCK_SIZE;
2704 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2705 let outputs = state.evaluate(batch.cache(), start, end)?;
2706 for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
2707 let (value, derivative) = transform(row[0])?;
2708 total.push(batch.weights()[start + lane], value, derivative, &row[1..]);
2709 }
2710 }
2711 }
2712 return Ok(total.finish());
2713 }
2714 if self.precision == Precision::F32
2715 && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
2716 {
2717 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2718 let mut gradient = Vec::new();
2719 for batch in dataset.batches() {
2720 self.check_batch_cache(batch.cache())?;
2721 for row in 0..batch.len() {
2722 let (value, model_gradient) = self.evaluate_f32_gradient_component_prepared(
2723 ir,
2724 params,
2725 F32KernelInput::Cache(Some((batch.cache(), row))),
2726 &mut gradient,
2727 )?;
2728 let (value, derivative) = transform(value)?;
2729 total.push_f32(batch.weights()[row], value, derivative, model_gradient);
2730 }
2731 }
2732 return Ok(total.finish());
2733 }
2734 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2735 for batch in dataset.batches() {
2736 self.check_batch_cache(batch.cache())?;
2737 for row in 0..batch.len() {
2738 let evaluation =
2739 self.evaluate_cache_row_with_gradient_unchecked(params, batch.cache(), row)?;
2740 let (value, derivative) = transform(evaluation.value())?;
2741 total.push(
2742 batch.weights()[row],
2743 value,
2744 derivative,
2745 evaluation.gradient(),
2746 );
2747 }
2748 }
2749 Ok(total.finish())
2750 }
2751
2752 #[cfg(feature = "jit")]
2753 fn try_weighted_real_sum_with_jit_gradient_cached<E, F>(
2754 &self,
2755 params: &ParamValues,
2756 dataset: &CpuCachedDataset,
2757 mut transform: F,
2758 value_kernel: &JitScalarKernel,
2759 gradient_kernel: &JitGradientKernel,
2760 ) -> Result<(f64, Vec<f64>), E>
2761 where
2762 E: From<RuntimeError>,
2763 F: FnMut(Complex64) -> Result<(f64, f64), E>,
2764 {
2765 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
2766 let mut values = Vec::new();
2767 let mut tangents = Vec::new();
2768 let mut derivatives = Vec::new();
2769 for batch in dataset.batches() {
2770 self.check_batch_cache(batch.cache())?;
2771 let cache = JitScalarKernel::prepare_cache(batch.cache());
2772 for block in 0..batch.len().div_ceil(SCALAR_BLOCK_SIZE) {
2773 let start = block * SCALAR_BLOCK_SIZE;
2774 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2775 value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
2776 derivatives.clear();
2777 derivatives.reserve(values.len());
2778 for (lane, value) in values.iter().copied().enumerate() {
2779 let (value, derivative) = transform(value)?;
2780 let weight = batch.weights()[start + lane];
2781 total.value.push(weight * value);
2782 derivatives.push(weight * derivative);
2783 }
2784 gradient_kernel.evaluate_prepared(params, &cache, start, end, 0, &mut tangents)?;
2785 for (lane, factor) in derivatives.iter().enumerate() {
2786 for free_index in 0..self.free_parameter_count() {
2787 total.gradient[free_index].push(
2788 factor * tangents[lane * self.free_parameter_count() + free_index],
2789 );
2790 }
2791 }
2792 }
2793 }
2794 Ok(total.finish())
2795 }
2796
2797 #[cfg(test)]
2798 fn try_weighted_complex_sum_cached<E, F>(
2799 &self,
2800 params: &ParamValues,
2801 dataset: &CpuCachedDataset,
2802 mut f: F,
2803 ) -> Result<Complex64, E>
2804 where
2805 E: From<RuntimeError>,
2806 F: FnMut(Complex64) -> Result<Complex64, E>,
2807 {
2808 let mut sum = Complex64::default();
2809 let invariant = self.scalar_invariant_values(params)?;
2810 let mut workspace = ScalarEventWorkspace::default();
2811 for batch in dataset.batches() {
2812 self.check_batch_cache(batch.cache())?;
2813 for row in 0..batch.len() {
2814 let value = self.evaluate_cache_row_prepared(
2815 params,
2816 batch.cache(),
2817 row,
2818 invariant.as_ref(),
2819 &mut workspace,
2820 )?;
2821 sum += f(value)? * batch.weights()[row];
2822 }
2823 }
2824 Ok(sum)
2825 }
2826
2827 #[cfg(test)]
2828 fn weighted_complex_sum_cached<F>(
2829 &self,
2830 params: &ParamValues,
2831 dataset: &CpuCachedDataset,
2832 mut f: F,
2833 ) -> RuntimeResult<Complex64>
2834 where
2835 F: FnMut(Complex64) -> Complex64,
2836 {
2837 self.try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
2838 }
2839
2840 fn reduce_cached(
2841 &self,
2842 execution: &Execution,
2843 params: &ParamValues,
2844 dataset: &CpuCachedDataset,
2845 reduction: ReductionPlan,
2846 ) -> RuntimeResult<f64> {
2847 if execution.is_parallel() && dataset.len().div_ceil(SCALAR_BLOCK_SIZE) >= 2 {
2848 execution.install(|| {
2849 self.par_try_weighted_sum_cached(params, dataset, |value| {
2850 self.apply_reduction(reduction, value)
2851 })
2852 })
2853 } else {
2854 self.try_weighted_sum_cached(params, dataset, |value| {
2855 self.apply_reduction(reduction, value)
2856 })
2857 }
2858 }
2859
2860 fn apply_reduction(&self, reduction: ReductionPlan, value: Complex64) -> RuntimeResult<f64> {
2861 if self.precision != Precision::F32 {
2862 return reduction
2863 .apply(value)
2864 .map(|output| output.value())
2865 .map_err(RuntimeError::from);
2866 }
2867 let real = value.re as f32;
2868 match reduction.transform() {
2869 ReductionTransform::Real => Ok(real as f64),
2870 ReductionTransform::PositiveReal if real > 0.0 => Ok(real as f64),
2871 ReductionTransform::LogPositiveReal if real > 0.0 => Ok(real.ln() as f64),
2872 ReductionTransform::PositiveReal | ReductionTransform::LogPositiveReal => reduction
2873 .apply(Complex64::from(real as f64))
2874 .map(|output| output.value())
2875 .map_err(RuntimeError::from),
2876 }
2877 }
2878
2879 fn try_reduce_weighted_with_gradient_cached<E, F>(
2880 &self,
2881 execution: &Execution,
2882 params: &ParamValues,
2883 dataset: &CpuCachedDataset,
2884 transform: F,
2885 ) -> Result<(f64, Vec<f64>), E>
2886 where
2887 E: From<RuntimeError> + Send,
2888 F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2889 {
2890 if execution.is_parallel() && dataset.len().div_ceil(SCALAR_BLOCK_SIZE) >= 2 {
2891 execution.install(|| {
2892 self.par_try_weighted_real_sum_with_gradient_cached(params, dataset, transform)
2893 })
2894 } else {
2895 self.try_weighted_real_sum_with_gradient_cached(params, dataset, transform)
2896 }
2897 }
2898
2899 pub(crate) fn par_try_weighted_sum_cached<E, F>(
2900 &self,
2901 params: &ParamValues,
2902 dataset: &CpuCachedDataset,
2903 f: F,
2904 ) -> Result<f64, E>
2905 where
2906 E: From<RuntimeError> + Send,
2907 F: Fn(Complex64) -> Result<f64, E> + Send + Sync,
2908 {
2909 let mut total = AccurateF64::zero();
2910 let invariant = self.scalar_invariant_values(params)?;
2911 for batch in dataset.batches() {
2912 self.check_batch_cache(batch.cache())?;
2913 #[cfg(feature = "jit")]
2914 let jit_cache = self
2915 .scalar_jit_kernel()
2916 .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
2917 let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
2918 let partial = (0..n_blocks)
2919 .into_par_iter()
2920 .try_fold(
2921 || {
2922 (
2923 AccurateF64::zero(),
2924 ScalarEventWorkspace::default(),
2925 Vec::new(),
2926 )
2927 },
2928 |(mut acc, mut workspace, mut output), block| {
2929 let start = block * SCALAR_BLOCK_SIZE;
2930 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
2931 self.evaluate_cache_block_prepared(
2932 params,
2933 batch.cache(),
2934 start,
2935 end,
2936 invariant.as_ref(),
2937 &mut workspace,
2938 &mut output,
2939 #[cfg(feature = "jit")]
2940 jit_cache.as_ref(),
2941 )?;
2942 for (lane, value) in output.iter().copied().enumerate() {
2943 acc.push(batch.weights()[start + lane] * f(value)?);
2944 }
2945 Ok::<_, E>((acc, workspace, output))
2946 },
2947 )
2948 .try_reduce(
2949 || {
2950 (
2951 AccurateF64::zero(),
2952 ScalarEventWorkspace::default(),
2953 Vec::new(),
2954 )
2955 },
2956 |(mut lhs, workspace, output), (rhs, _, _)| {
2957 lhs.merge(rhs);
2958 Ok::<_, E>((lhs, workspace, output))
2959 },
2960 )?;
2961 total.merge(partial.0);
2962 }
2963 Ok(total.finish())
2964 }
2965
2966 #[cfg(test)]
2967 pub(crate) fn par_weighted_sum_cached<F>(
2968 &self,
2969 params: &ParamValues,
2970 dataset: &CpuCachedDataset,
2971 f: F,
2972 ) -> RuntimeResult<f64>
2973 where
2974 F: Fn(Complex64) -> f64 + Send + Sync,
2975 {
2976 self.par_try_weighted_sum_cached(params, dataset, |value| Ok(f(value)))
2977 }
2978
2979 pub(crate) fn par_try_weighted_real_sum_with_gradient_cached<E, F>(
2980 &self,
2981 params: &ParamValues,
2982 dataset: &CpuCachedDataset,
2983 transform: F,
2984 ) -> Result<(f64, Vec<f64>), E>
2985 where
2986 E: From<RuntimeError> + Send,
2987 F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
2988 {
2989 #[cfg(feature = "jit")]
2990 if let (Some(value_kernel), Some(gradient_kernel)) =
2991 (self.scalar_jit_kernel(), self.gradient_jit_kernel())
2992 {
2993 return self.par_try_weighted_real_sum_with_jit_gradient_cached(
2994 params,
2995 dataset,
2996 transform,
2997 value_kernel,
2998 gradient_kernel,
2999 );
3000 }
3001 if self.precision != Precision::F32
3002 && let Some(interpreter) = self.gradient_interpreter()
3003 && let Some(state) = interpreter.prepare_real_blocks(params)?
3004 {
3005 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3006 let output_count = state.output_count();
3007 for batch in dataset.batches() {
3008 self.check_batch_cache(batch.cache())?;
3009 let partial = (0..batch.len().div_ceil(SCALAR_BLOCK_SIZE))
3010 .into_par_iter()
3011 .try_fold(
3012 || {
3013 (
3014 RealGradientAccumulator::zero(self.free_parameter_count()),
3015 state.clone(),
3016 )
3017 },
3018 |(mut accumulator, mut state), block| {
3019 let start = block * SCALAR_BLOCK_SIZE;
3020 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
3021 let outputs = state.evaluate(batch.cache(), start, end)?;
3022 for (lane, row) in outputs.chunks_exact(output_count).enumerate() {
3023 let (value, derivative) = transform(row[0])?;
3024 accumulator.push(
3025 batch.weights()[start + lane],
3026 value,
3027 derivative,
3028 &row[1..],
3029 );
3030 }
3031 Ok::<_, E>((accumulator, state))
3032 },
3033 )
3034 .try_reduce(
3035 || {
3036 (
3037 RealGradientAccumulator::zero(self.free_parameter_count()),
3038 state.clone(),
3039 )
3040 },
3041 |(mut lhs, state), (rhs, _)| {
3042 lhs.merge(rhs);
3043 Ok::<_, E>((lhs, state))
3044 },
3045 )?;
3046 total.merge(partial.0);
3047 }
3048 return Ok(total.finish());
3049 }
3050 if self.precision == Precision::F32
3051 && let Some(ir) = self.f32_gradient_fallback_real.as_ref()
3052 {
3053 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3054 for batch in dataset.batches() {
3055 self.check_batch_cache(batch.cache())?;
3056 let partial = (0..batch.len())
3057 .into_par_iter()
3058 .try_fold(
3059 || {
3060 (
3061 RealGradientAccumulator::zero(self.free_parameter_count()),
3062 Vec::new(),
3063 )
3064 },
3065 |(mut accumulator, mut gradient), row| {
3066 let (value, model_gradient) = self
3067 .evaluate_f32_gradient_component_prepared(
3068 ir,
3069 params,
3070 F32KernelInput::Cache(Some((batch.cache(), row))),
3071 &mut gradient,
3072 )?;
3073 let (value, derivative) = transform(value)?;
3074 accumulator.push_f32(
3075 batch.weights()[row],
3076 value,
3077 derivative,
3078 model_gradient,
3079 );
3080 Ok::<_, E>((accumulator, gradient))
3081 },
3082 )
3083 .try_reduce(
3084 || {
3085 (
3086 RealGradientAccumulator::zero(self.free_parameter_count()),
3087 Vec::new(),
3088 )
3089 },
3090 |(mut lhs, gradient), (rhs, _)| {
3091 lhs.merge(rhs);
3092 Ok::<_, E>((lhs, gradient))
3093 },
3094 )?;
3095 total.merge(partial.0);
3096 }
3097 return Ok(total.finish());
3098 }
3099 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3100 for batch in dataset.batches() {
3101 self.check_batch_cache(batch.cache())?;
3102 let partial = (0..batch.len())
3103 .into_par_iter()
3104 .try_fold(
3105 || RealGradientAccumulator::zero(self.free_parameter_count()),
3106 |mut accumulator, row| {
3107 let evaluation = self.evaluate_cache_row_with_gradient_unchecked(
3108 params,
3109 batch.cache(),
3110 row,
3111 )?;
3112 let (value, derivative) = transform(evaluation.value())?;
3113 accumulator.push(
3114 batch.weights()[row],
3115 value,
3116 derivative,
3117 evaluation.gradient(),
3118 );
3119 Ok::<_, E>(accumulator)
3120 },
3121 )
3122 .try_reduce(
3123 || RealGradientAccumulator::zero(self.free_parameter_count()),
3124 |mut lhs, rhs| {
3125 lhs.merge(rhs);
3126 Ok::<_, E>(lhs)
3127 },
3128 )?;
3129 total.merge(partial);
3130 }
3131 Ok(total.finish())
3132 }
3133
3134 #[cfg(feature = "jit")]
3135 fn par_try_weighted_real_sum_with_jit_gradient_cached<E, F>(
3136 &self,
3137 params: &ParamValues,
3138 dataset: &CpuCachedDataset,
3139 transform: F,
3140 value_kernel: &JitScalarKernel,
3141 gradient_kernel: &JitGradientKernel,
3142 ) -> Result<(f64, Vec<f64>), E>
3143 where
3144 E: From<RuntimeError> + Send,
3145 F: Fn(Complex64) -> Result<(f64, f64), E> + Send + Sync,
3146 {
3147 let mut total = RealGradientAccumulator::zero(self.free_parameter_count());
3148 for batch in dataset.batches() {
3149 self.check_batch_cache(batch.cache())?;
3150 let cache = JitScalarKernel::prepare_cache(batch.cache());
3151 let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
3152 let partial = (0..n_blocks)
3153 .into_par_iter()
3154 .try_fold(
3155 || {
3156 (
3157 RealGradientAccumulator::zero(self.free_parameter_count()),
3158 Vec::new(),
3159 Vec::new(),
3160 Vec::new(),
3161 )
3162 },
3163 |(mut accumulator, mut values, mut tangents, mut derivatives), block| {
3164 let start = block * SCALAR_BLOCK_SIZE;
3165 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
3166 value_kernel.evaluate_prepared(params, &cache, start, end, &mut values)?;
3167 derivatives.clear();
3168 derivatives.reserve(values.len());
3169 for (lane, value) in values.iter().copied().enumerate() {
3170 let (value, derivative) = transform(value)?;
3171 let weight = batch.weights()[start + lane];
3172 accumulator.value.push(weight * value);
3173 derivatives.push(weight * derivative);
3174 }
3175 gradient_kernel.evaluate_prepared(
3176 params,
3177 &cache,
3178 start,
3179 end,
3180 0,
3181 &mut tangents,
3182 )?;
3183 for (lane, factor) in derivatives.iter().enumerate() {
3184 for free_index in 0..self.free_parameter_count() {
3185 accumulator.gradient[free_index].push(
3186 factor
3187 * tangents[lane * self.free_parameter_count() + free_index],
3188 );
3189 }
3190 }
3191 Ok::<_, E>((accumulator, values, tangents, derivatives))
3192 },
3193 )
3194 .try_reduce(
3195 || {
3196 (
3197 RealGradientAccumulator::zero(self.free_parameter_count()),
3198 Vec::new(),
3199 Vec::new(),
3200 Vec::new(),
3201 )
3202 },
3203 |(mut lhs, values, tangents, derivatives), (rhs, _, _, _)| {
3204 lhs.merge(rhs);
3205 Ok::<_, E>((lhs, values, tangents, derivatives))
3206 },
3207 )?;
3208 total.merge(partial.0);
3209 }
3210 Ok(total.finish())
3211 }
3212
3213 #[cfg(test)]
3214 pub(crate) fn par_try_weighted_complex_sum_cached<E, F>(
3215 &self,
3216 params: &ParamValues,
3217 dataset: &CpuCachedDataset,
3218 f: F,
3219 ) -> Result<Complex64, E>
3220 where
3221 E: From<RuntimeError> + Send,
3222 F: Fn(Complex64) -> Result<Complex64, E> + Send + Sync,
3223 {
3224 let mut total = AccurateComplex64::zero();
3225 let invariant = self.scalar_invariant_values(params)?;
3226 for batch in dataset.batches() {
3227 self.check_batch_cache(batch.cache())?;
3228 #[cfg(feature = "jit")]
3229 let jit_cache = self
3230 .scalar_jit_kernel()
3231 .map(|_| JitScalarKernel::prepare_cache(batch.cache()));
3232 let n_blocks = batch.len().div_ceil(SCALAR_BLOCK_SIZE);
3233 let partial = (0..n_blocks)
3234 .into_par_iter()
3235 .try_fold(
3236 || {
3237 (
3238 AccurateComplex64::zero(),
3239 ScalarEventWorkspace::default(),
3240 Vec::new(),
3241 )
3242 },
3243 |(mut acc, mut workspace, mut output), block| {
3244 let start = block * SCALAR_BLOCK_SIZE;
3245 let end = (start + SCALAR_BLOCK_SIZE).min(batch.len());
3246 self.evaluate_cache_block_prepared(
3247 params,
3248 batch.cache(),
3249 start,
3250 end,
3251 invariant.as_ref(),
3252 &mut workspace,
3253 &mut output,
3254 #[cfg(feature = "jit")]
3255 jit_cache.as_ref(),
3256 )?;
3257 for (lane, value) in output.iter().copied().enumerate() {
3258 acc.push(f(value)? * batch.weights()[start + lane]);
3259 }
3260 Ok::<_, E>((acc, workspace, output))
3261 },
3262 )
3263 .try_reduce(
3264 || {
3265 (
3266 AccurateComplex64::zero(),
3267 ScalarEventWorkspace::default(),
3268 Vec::new(),
3269 )
3270 },
3271 |(mut lhs, workspace, output), (rhs, _, _)| {
3272 lhs.merge(rhs);
3273 Ok::<_, E>((lhs, workspace, output))
3274 },
3275 )?;
3276 total.merge(partial.0);
3277 }
3278 Ok(total.finish())
3279 }
3280
3281 #[cfg(test)]
3282 pub(crate) fn par_weighted_complex_sum_cached<F>(
3283 &self,
3284 params: &ParamValues,
3285 dataset: &CpuCachedDataset,
3286 f: F,
3287 ) -> RuntimeResult<Complex64>
3288 where
3289 F: Fn(Complex64) -> Complex64 + Send + Sync,
3290 {
3291 self.par_try_weighted_complex_sum_cached(params, dataset, |value| Ok(f(value)))
3292 }
3293
3294 fn evaluate_inner(
3295 &self,
3296 params: &ParamValues,
3297 event: Option<&dyn EventLookup>,
3298 ) -> RuntimeResult<Complex64> {
3299 #[cfg(feature = "jit")]
3300 if event.is_none()
3301 && let Some(kernel) = self.scalar_jit_kernel()
3302 {
3303 if params.as_slice().len() != self.params.len() {
3304 return Err(RuntimeError::Parameter(format!(
3305 "expected {} parameter values, got {}",
3306 self.params.len(),
3307 params.as_slice().len()
3308 )));
3309 }
3310 return kernel.evaluate_invariant(params);
3311 }
3312 if self.precision == Precision::F32 {
3313 let input = match event {
3314 Some(event) => F32KernelInput::Event(event),
3315 None => F32KernelInput::Cache(None),
3316 };
3317 return self.evaluate_f32_scalar(params, input);
3318 }
3319 let values = self.evaluate_values(params, event)?;
3320 scalar_at(&values, self.graph.root().index())
3321 }
3322
3323 fn require_f64_gradient(&self) -> RuntimeResult<()> {
3324 if self.precision == Precision::F32 {
3325 return Err(crate::ExecutionError::UnsupportedCpuF32Gradient.into());
3326 }
3327 Ok(())
3328 }
3329
3330 fn evaluate_f32_scalar(
3331 &self,
3332 params: &ParamValues,
3333 input: F32KernelInput<'_>,
3334 ) -> RuntimeResult<Complex64> {
3335 let kernel = self
3336 .scalar_kernel
3337 .as_ref()
3338 .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3339 let values = self.evaluate_f32_kernel_values(kernel.values(), params, input)?;
3340 let value = f32_scalar_at(&values, kernel.root())?;
3341 Ok(Complex64::new(value.re as f64, value.im as f64))
3342 }
3343
3344 fn evaluate_f32_kernel_values(
3345 &self,
3346 kernel_values: &[KernelValue],
3347 params: &ParamValues,
3348 input: F32KernelInput<'_>,
3349 ) -> RuntimeResult<Vec<F32Value>> {
3350 let mut values = Vec::with_capacity(kernel_values.len());
3351 for (index, value) in kernel_values.iter().enumerate() {
3352 let result = match &value.instruction {
3353 KernelInstruction::Cached(slot) => self.evaluate_f32_cached_value(
3354 *slot,
3355 params,
3356 input,
3357 crate::ExecutionError::UnsupportedCpuF32Model,
3358 )?,
3359 KernelInstruction::RealConstant(value) => {
3360 F32Value::Scalar(Complex32::from(*value as f32))
3361 }
3362 KernelInstruction::ComplexConstant(value) => {
3363 F32Value::Scalar(Complex32::new(value.re as f32, value.im as f32))
3364 }
3365 KernelInstruction::Parameter(id) => F32Value::Scalar(Complex32::from(
3366 params
3367 .get(*id)
3368 .map_err(|error| RuntimeError::Parameter(error.to_string()))?
3369 as f32,
3370 )),
3371 KernelInstruction::Unary { op, input } => {
3372 F32Value::Scalar(eval_unary(*op, f32_scalar_at(&values, *input)?))
3373 }
3374 KernelInstruction::Binary { op, lhs, rhs } => F32Value::Scalar(eval_binary(
3375 *op,
3376 f32_scalar_at(&values, *lhs)?,
3377 f32_scalar_at(&values, *rhs)?,
3378 )),
3379 KernelInstruction::Add(terms) => F32Value::Scalar(
3380 terms
3381 .iter()
3382 .map(|id| f32_scalar_at(&values, *id))
3383 .sum::<RuntimeResult<Complex32>>()?,
3384 ),
3385 KernelInstruction::Mul(factors) => {
3386 F32Value::Scalar(factors.iter().try_fold(Complex32::ONE, |product, id| {
3387 Ok::<_, RuntimeError>(product * f32_scalar_at(&values, *id)?)
3388 })?)
3389 }
3390 KernelInstruction::Complex { re, im } => F32Value::Scalar(Complex32::new(
3391 f32_scalar_at(&values, *re)?.re,
3392 f32_scalar_at(&values, *im)?.re,
3393 )),
3394 KernelInstruction::Vector(elements) => F32Value::Vector(
3395 elements
3396 .iter()
3397 .map(|id| f32_scalar_at(&values, *id))
3398 .collect::<RuntimeResult<_>>()?,
3399 ),
3400 KernelInstruction::Matrix {
3401 rows,
3402 cols,
3403 elements,
3404 } => F32Value::Matrix {
3405 rows: *rows,
3406 cols: *cols,
3407 values: elements
3408 .iter()
3409 .map(|id| f32_scalar_at(&values, *id))
3410 .collect::<RuntimeResult<_>>()?,
3411 },
3412 KernelInstruction::Component {
3413 input,
3414 index: element,
3415 } => {
3416 let vector = f32_vector_at(&values, *input)?;
3417 F32Value::Scalar(*vector.get(*element).ok_or_else(|| {
3418 RuntimeError::InvalidShape {
3419 index,
3420 message: format!(
3421 "component index {element} out of bounds for len {}",
3422 vector.len()
3423 ),
3424 }
3425 })?)
3426 }
3427 KernelInstruction::MatrixElement { input, row, col } => {
3428 let (rows, cols, matrix) = f32_matrix_at(&values, *input)?;
3429 if *row >= rows || *col >= cols {
3430 return Err(RuntimeError::InvalidShape {
3431 index,
3432 message: format!(
3433 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
3434 ),
3435 });
3436 }
3437 F32Value::Scalar(matrix[row * cols + col])
3438 }
3439 KernelInstruction::Dot { lhs, rhs } => {
3440 let lhs = f32_vector_at(&values, *lhs)?;
3441 let rhs = f32_vector_at(&values, *rhs)?;
3442 if lhs.len() != rhs.len() {
3443 return Err(RuntimeError::InvalidShape {
3444 index,
3445 message: format!(
3446 "cannot dot len {} vector with len {} vector",
3447 lhs.len(),
3448 rhs.len()
3449 ),
3450 });
3451 }
3452 F32Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
3453 }
3454 KernelInstruction::MatVec { matrix, vector } => {
3455 let (rows, cols, matrix) = f32_matrix_at(&values, *matrix)?;
3456 let vector = f32_vector_at(&values, *vector)?;
3457 if cols != vector.len() {
3458 return Err(RuntimeError::InvalidShape {
3459 index,
3460 message: format!(
3461 "cannot multiply {rows}x{cols} matrix by len {} vector",
3462 vector.len()
3463 ),
3464 });
3465 }
3466 let output = DMatrix::from_row_slice(rows, cols, matrix)
3467 * DVector::from_row_slice(vector);
3468 F32Value::Vector(output.iter().copied().collect())
3469 }
3470 KernelInstruction::MatMul { lhs, rhs } => {
3471 let (lhs_rows, lhs_cols, lhs) = f32_matrix_at(&values, *lhs)?;
3472 let (rhs_rows, rhs_cols, rhs) = f32_matrix_at(&values, *rhs)?;
3473 if lhs_cols != rhs_rows {
3474 return Err(RuntimeError::InvalidShape {
3475 index,
3476 message: format!(
3477 "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
3478 ),
3479 });
3480 }
3481 let output = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs)
3482 * DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
3483 F32Value::Matrix {
3484 rows: output.nrows(),
3485 cols: output.ncols(),
3486 values: matrix_values_row_major_f32(&output),
3487 }
3488 }
3489 KernelInstruction::Solve { matrix, rhs } => {
3490 let (rows, cols, matrix) = f32_matrix_at(&values, *matrix)?;
3491 let rhs = f32_vector_at(&values, *rhs)?;
3492 if rows != cols || rows != rhs.len() {
3493 return Err(RuntimeError::InvalidShape {
3494 index,
3495 message: format!(
3496 "cannot solve {rows}x{cols} matrix against len {} vector",
3497 rhs.len()
3498 ),
3499 });
3500 }
3501 let solution = DMatrix::from_row_slice(rows, cols, matrix)
3502 .lu()
3503 .solve(&DVector::from_row_slice(rhs))
3504 .ok_or(RuntimeError::SingularMatrix(index))?;
3505 F32Value::Vector(solution.iter().copied().collect())
3506 }
3507 KernelInstruction::SolveRow { row_slot, rhs } => {
3508 let (cache, row) = input
3509 .cache()
3510 .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3511 let inverse = cache.solve_row(*row_slot, row)?;
3512 if inverse.len() != rhs.len() {
3513 return Err(RuntimeError::InvalidShape {
3514 index,
3515 message: format!(
3516 "specialized solve row has len {}, expected {}",
3517 inverse.len(),
3518 rhs.len()
3519 ),
3520 });
3521 }
3522 F32Value::Scalar(
3523 inverse
3524 .iter()
3525 .zip(rhs)
3526 .map(|(coefficient, rhs)| {
3527 Ok::<_, RuntimeError>(
3528 Complex32::new(coefficient.re as f32, coefficient.im as f32)
3529 * f32_scalar_at(&values, *rhs)?,
3530 )
3531 })
3532 .sum::<RuntimeResult<Complex32>>()?,
3533 )
3534 }
3535 KernelInstruction::SolveRowAdjointElement {
3536 row_slot,
3537 index: element,
3538 len,
3539 adjoint,
3540 } => {
3541 let (cache, row) = input
3542 .cache()
3543 .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3544 let inverse = cache.solve_row(*row_slot, row)?;
3545 if inverse.len() != *len {
3546 return Err(RuntimeError::InvalidShape {
3547 index,
3548 message: format!(
3549 "specialized solve row has len {}, expected {len}",
3550 inverse.len()
3551 ),
3552 });
3553 }
3554 let coefficient = inverse[*element];
3555 F32Value::Scalar(
3556 f32_scalar_at(&values, *adjoint)?
3557 * Complex32::new(coefficient.re as f32, coefficient.im as f32).conj(),
3558 )
3559 }
3560 };
3561 values.push(result);
3562 }
3563 Ok(values)
3564 }
3565
3566 fn evaluate_f32_cached_value(
3567 &self,
3568 slot: usize,
3569 params: &ParamValues,
3570 input: F32KernelInput<'_>,
3571 missing_input: crate::ExecutionError,
3572 ) -> RuntimeResult<F32Value> {
3573 match input {
3574 F32KernelInput::Cache(Some((cache, row))) => {
3575 Ok(F32Value::from_value(cache.value(slot, row)?))
3576 }
3577 F32KernelInput::Cache(None) => Err(missing_input.into()),
3578 F32KernelInput::Event(event) => {
3579 let entry =
3580 self.cache_plan
3581 .entries()
3582 .get(slot)
3583 .ok_or(RuntimeError::InvalidShape {
3584 index: self.graph.root().index(),
3585 message: format!("cache slot {slot} is out of bounds"),
3586 })?;
3587 let values = self.evaluate_values(params, Some(event))?;
3588 let value = values
3589 .get(entry.node().index())
3590 .ok_or(RuntimeError::InvalidShape {
3591 index: entry.node().index(),
3592 message: "cached node is out of bounds".into(),
3593 })?
3594 .clone();
3595 Ok(F32Value::from_value(value))
3596 }
3597 }
3598 }
3599
3600 fn evaluate_f32_gradient(
3601 &self,
3602 params: &ParamValues,
3603 input: F32KernelInput<'_>,
3604 ) -> RuntimeResult<ValueGradient> {
3605 let real_ir = self
3606 .f32_gradient_fallback_real
3607 .as_ref()
3608 .ok_or(crate::ExecutionError::UnsupportedCpuF32Model)?;
3609 let mut real = Vec::new();
3610 let (value, _) =
3611 self.evaluate_f32_gradient_component_prepared(real_ir, params, input, &mut real)?;
3612 let imag = if let Some(imag_ir) = self.f32_gradient_fallback_imag.as_ref() {
3613 let mut imag = Vec::new();
3614 self.evaluate_f32_gradient_component_prepared(imag_ir, params, input, &mut imag)?;
3615 imag
3616 } else {
3617 vec![0.0; real.len()]
3618 };
3619 Ok(ValueGradient {
3620 value,
3621 gradient: real
3622 .into_iter()
3623 .zip(imag)
3624 .map(|(re, im)| Complex64::new(re as f64, im as f64))
3625 .collect(),
3626 })
3627 }
3628
3629 fn evaluate_f32_gradient_component_prepared<'a>(
3630 &self,
3631 ir: &GradientKernelIr,
3632 params: &ParamValues,
3633 input: F32KernelInput<'_>,
3634 gradient: &'a mut Vec<f32>,
3635 ) -> RuntimeResult<(Complex64, &'a [f32])> {
3636 let values = self.evaluate_f32_kernel_values(ir.values(), params, input)?;
3637 let value = f32_scalar_at(&values, ir.primal_root())?;
3638 gradient.clear();
3639 gradient.reserve(ir.outputs().len());
3640 for output in ir.outputs() {
3641 gradient.push(f32_scalar_at(&values, *output)?.re);
3642 }
3643 Ok((Complex64::new(value.re as f64, value.im as f64), gradient))
3644 }
3645
3646 fn value_gradient(
3647 &self,
3648 values: Vec<Value>,
3649 cached_factors: Option<(&CpuBatchCache, usize)>,
3650 ) -> RuntimeResult<ValueGradient> {
3651 let value = if cached_factors.is_some() {
3652 self.cached_scalar_at(&values, self.graph.root())?
3653 } else {
3654 scalar_at(&values, self.graph.root().index())?
3655 };
3656 let gradient = match self.autodiff.mode() {
3657 AutodiffMode::Auto => unreachable!("autodiff mode is resolved during preparation"),
3658 AutodiffMode::Forward => {
3659 DerivativeWorkspace::new(self, &values, cached_factors).gradient()?
3660 }
3661 AutodiffMode::Reverse => {
3662 ReverseDerivativeWorkspace::new(self, &values, cached_factors).gradient()?
3663 }
3664 };
3665 Ok(ValueGradient { value, gradient })
3666 }
3667
3668 fn solve_primal(
3669 &self,
3670 matrix_id: ExprId,
3671 dimension: usize,
3672 matrix: &[Complex64],
3673 rhs: &DVector<Complex64>,
3674 node_index: usize,
3675 cached: Option<(&CpuBatchCache, usize)>,
3676 ) -> RuntimeResult<DVector<Complex64>> {
3677 let solution = if let (Some(slot), Some((cache, row))) =
3678 (self.factor_matrix_slots[matrix_id.index()], cached)
3679 {
3680 cache.factor(slot, row)?.solve(rhs)
3681 } else if let Some(slot) = self.constant_factor_slots[matrix_id.index()] {
3682 self.constant_factors[slot]
3683 .get_or_init(|| DMatrix::from_row_slice(dimension, dimension, matrix).lu())
3684 .solve(rhs)
3685 } else {
3686 DMatrix::from_row_slice(dimension, dimension, matrix)
3687 .lu()
3688 .solve(rhs)
3689 };
3690 solution.ok_or(RuntimeError::SingularMatrix(node_index))
3691 }
3692
3693 fn event_columns(&self, schema: &Schema) -> RuntimeResult<Vec<Option<EventColumn>>> {
3694 self.graph
3695 .nodes()
3696 .iter()
3697 .map(|node| {
3698 if let ExprNode::EventScalar(name) = node {
3699 Ok(Some(EventColumn::Scalar(
3700 schema
3701 .scalar_index(name)
3702 .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?,
3703 )))
3704 } else if let ExprNode::EventP4Component { name, component } = node {
3705 Ok(Some(EventColumn::P4Component {
3706 col: schema
3707 .p4_index(name)
3708 .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?,
3709 component: *component,
3710 }))
3711 } else {
3712 Ok(None)
3713 }
3714 })
3715 .collect()
3716 }
3717
3718 fn evaluate_cache_values_for_row(
3719 &self,
3720 batch: &EventBatch,
3721 row: usize,
3722 event_columns: &[Option<EventColumn>],
3723 ) -> RuntimeResult<Vec<Option<Value>>> {
3724 let mut values = vec![None; self.graph.nodes().len()];
3725
3726 for id in &self.cache_materialization_nodes {
3727 let index = id.index();
3728 let node = &self.graph.nodes()[index];
3729 let value = match node {
3730 ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
3731 ExprNode::ComplexConst(value) => Value::Scalar(*value),
3732 ExprNode::EventScalar(name) => {
3733 let col = event_columns[index]
3734 .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?;
3735 let EventColumn::Scalar(col) = col else {
3736 return Err(RuntimeError::MissingEventColumn(name.to_string()));
3737 };
3738 Value::Scalar(Complex64::from(batch.scalar_at(col, row)))
3739 }
3740 ExprNode::EventP4Component { name, component } => {
3741 let col = event_columns[index]
3742 .ok_or_else(|| RuntimeError::MissingEventColumn(name.to_string()))?;
3743 let EventColumn::P4Component {
3744 col,
3745 component: actual,
3746 } = col
3747 else {
3748 return Err(RuntimeError::MissingEventColumn(name.to_string()));
3749 };
3750 debug_assert_eq!(actual, *component);
3751 let p4 = batch.p4_at(col, row);
3752 let value = match component {
3753 P4Component::Px => p4.px,
3754 P4Component::Py => p4.py,
3755 P4Component::Pz => p4.pz,
3756 P4Component::E => p4.e,
3757 };
3758 Value::Scalar(Complex64::from(value))
3759 }
3760 ExprNode::Unary { op, input } => {
3761 let input = scalar_at_optional(&values, input.index())?;
3762 Value::Scalar(eval_unary(*op, input))
3763 }
3764 ExprNode::Binary { op, lhs, rhs } => {
3765 let lhs = scalar_at_optional(&values, lhs.index())?;
3766 let rhs = scalar_at_optional(&values, rhs.index())?;
3767 Value::Scalar(eval_binary(*op, lhs, rhs))
3768 }
3769 ExprNode::NaryAdd { terms } => {
3770 let mut sum = Complex64::ZERO;
3771 for term in terms {
3772 sum += scalar_at_optional(&values, term.index())?;
3773 }
3774 Value::Scalar(sum)
3775 }
3776 ExprNode::NaryMul { factors } => {
3777 let mut product = Complex64::ONE;
3778 for factor in factors {
3779 product *= scalar_at_optional(&values, factor.index())?;
3780 }
3781 Value::Scalar(product)
3782 }
3783 ExprNode::Complex { re, im } => {
3784 let re = scalar_at_optional(&values, re.index())?;
3785 let im = scalar_at_optional(&values, im.index())?;
3786 Value::Scalar(Complex64::new(re.re, im.re))
3787 }
3788 ExprNode::Vector { elements } => Value::Vector(
3789 elements
3790 .iter()
3791 .map(|id| scalar_at_optional(&values, id.index()))
3792 .collect::<RuntimeResult<_>>()?,
3793 ),
3794 ExprNode::Matrix {
3795 rows,
3796 cols,
3797 elements,
3798 } => {
3799 if elements.len() != rows * cols {
3800 return Err(RuntimeError::InvalidShape {
3801 index,
3802 message: format!(
3803 "matrix has {} elements for shape {rows}x{cols}",
3804 elements.len()
3805 ),
3806 });
3807 }
3808 Value::Matrix {
3809 rows: *rows,
3810 cols: *cols,
3811 values: elements
3812 .iter()
3813 .map(|id| scalar_at_optional(&values, id.index()))
3814 .collect::<RuntimeResult<_>>()?,
3815 }
3816 }
3817 ExprNode::Component { input, index: i } => {
3818 let vector = vector_at_optional(&values, input.index())?;
3819 Value::Scalar(*vector.get(*i).ok_or_else(|| RuntimeError::InvalidShape {
3820 index,
3821 message: format!(
3822 "component index {i} out of bounds for len {}",
3823 vector.len()
3824 ),
3825 })?)
3826 }
3827 ExprNode::MatrixElement { input, row, col } => {
3828 let (rows, cols, matrix) = matrix_at_optional(&values, input.index())?;
3829 if *row >= rows || *col >= cols {
3830 return Err(RuntimeError::InvalidShape {
3831 index,
3832 message: format!(
3833 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
3834 ),
3835 });
3836 }
3837 Value::Scalar(matrix[row * cols + col])
3838 }
3839 ExprNode::MatMul { lhs, rhs } => {
3840 let (lhs_rows, lhs_cols, lhs) = matrix_at_optional(&values, lhs.index())?;
3841 let (rhs_rows, rhs_cols, rhs) = matrix_at_optional(&values, rhs.index())?;
3842 if lhs_cols != rhs_rows {
3843 return Err(RuntimeError::InvalidShape {
3844 index,
3845 message: format!(
3846 "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
3847 ),
3848 });
3849 }
3850 let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
3851 let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
3852 let out = lhs * rhs;
3853 Value::Matrix {
3854 rows: out.nrows(),
3855 cols: out.ncols(),
3856 values: matrix_values_row_major(&out),
3857 }
3858 }
3859 ExprNode::MatVec { matrix, vector } => {
3860 let (rows, cols, matrix) = matrix_at_optional(&values, matrix.index())?;
3861 let vector = vector_at_optional(&values, vector.index())?;
3862 if cols != vector.len() {
3863 return Err(RuntimeError::InvalidShape {
3864 index,
3865 message: format!(
3866 "cannot multiply {rows}x{cols} matrix by len {} vector",
3867 vector.len()
3868 ),
3869 });
3870 }
3871 let matrix = DMatrix::from_row_slice(rows, cols, matrix);
3872 let vector = DVector::from_row_slice(vector);
3873 Value::Vector((matrix * vector).iter().copied().collect())
3874 }
3875 ExprNode::Dot { lhs, rhs } => {
3876 let lhs = vector_at_optional(&values, lhs.index())?;
3877 let rhs = vector_at_optional(&values, rhs.index())?;
3878 if lhs.len() != rhs.len() {
3879 return Err(RuntimeError::InvalidShape {
3880 index,
3881 message: format!(
3882 "cannot dot len {} vector with len {} vector",
3883 lhs.len(),
3884 rhs.len()
3885 ),
3886 });
3887 }
3888 Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
3889 }
3890 ExprNode::Solve { matrix, rhs } => {
3891 let matrix_id = *matrix;
3892 let (rows, cols, matrix) = matrix_at_optional(&values, matrix_id.index())?;
3893 let rhs = vector_at_optional(&values, rhs.index())?;
3894 if rows != cols || rows != rhs.len() {
3895 return Err(RuntimeError::InvalidShape {
3896 index,
3897 message: format!(
3898 "cannot solve {rows}x{cols} matrix against len {} vector",
3899 rhs.len()
3900 ),
3901 });
3902 }
3903 let rhs = DVector::from_row_slice(rhs);
3904 let solution = self.solve_primal(matrix_id, rows, matrix, &rhs, index, None)?;
3905 Value::Vector(solution.iter().copied().collect())
3906 }
3907 ExprNode::ScalarParam(_) => {
3908 return Err(RuntimeError::InvalidShape {
3909 index,
3910 message: "parameter-dependent node cannot be part of an event cache".into(),
3911 });
3912 }
3913 };
3914 values[index] = Some(value);
3915 }
3916
3917 Ok(values)
3918 }
3919
3920 fn evaluate_values(
3921 &self,
3922 params: &ParamValues,
3923 event: Option<&dyn EventLookup>,
3924 ) -> RuntimeResult<Vec<Value>> {
3925 let mut values = Vec::with_capacity(self.graph.nodes().len());
3926
3927 for (index, node) in self.graph.nodes().iter().enumerate() {
3928 let value = match node {
3929 ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
3930 ExprNode::ComplexConst(value) => Value::Scalar(*value),
3931 ExprNode::ScalarParam(_) => {
3932 Value::Scalar(Complex64::from(self.parameter_value(params, index)?))
3933 }
3934 ExprNode::EventScalar(name) => {
3935 let Some(event) = event else {
3936 return Err(RuntimeError::MissingEventScalar(name.to_string()));
3937 };
3938 Value::Scalar(Complex64::from(
3939 event
3940 .scalar(name)
3941 .ok_or_else(|| RuntimeError::MissingEventScalar(name.to_string()))?,
3942 ))
3943 }
3944 ExprNode::EventP4Component { name, component } => {
3945 let Some(event) = event else {
3946 return Err(RuntimeError::MissingEventScalar(format!(
3947 "{name}.{}",
3948 component.label()
3949 )));
3950 };
3951 Value::Scalar(Complex64::from(
3952 event.p4_component(name, *component).ok_or_else(|| {
3953 RuntimeError::MissingEventScalar(format!(
3954 "{name}.{}",
3955 component.label()
3956 ))
3957 })?,
3958 ))
3959 }
3960 ExprNode::Unary { op, input } => {
3961 let input = scalar_at(&values, input.index())?;
3962 Value::Scalar(eval_unary(*op, input))
3963 }
3964 ExprNode::Binary { op, lhs, rhs } => {
3965 let lhs = scalar_at(&values, lhs.index())?;
3966 let rhs = scalar_at(&values, rhs.index())?;
3967 Value::Scalar(eval_binary(*op, lhs, rhs))
3968 }
3969 ExprNode::NaryAdd { terms } => {
3970 let mut sum = Complex64::ZERO;
3971 for term in terms {
3972 sum += scalar_at(&values, term.index())?;
3973 }
3974 Value::Scalar(sum)
3975 }
3976 ExprNode::NaryMul { factors } => {
3977 let mut product = Complex64::ONE;
3978 for factor in factors {
3979 product *= scalar_at(&values, factor.index())?;
3980 }
3981 Value::Scalar(product)
3982 }
3983 ExprNode::Complex { re, im } => {
3984 let re = scalar_at(&values, re.index())?;
3985 let im = scalar_at(&values, im.index())?;
3986 Value::Scalar(Complex64::new(re.re, im.re))
3987 }
3988 ExprNode::Vector { elements } => Value::Vector(
3989 elements
3990 .iter()
3991 .map(|id| scalar_at(&values, id.index()))
3992 .collect::<RuntimeResult<_>>()?,
3993 ),
3994 ExprNode::Matrix {
3995 rows,
3996 cols,
3997 elements,
3998 } => {
3999 if elements.len() != rows * cols {
4000 return Err(RuntimeError::InvalidShape {
4001 index,
4002 message: format!(
4003 "matrix has {} elements for shape {rows}x{cols}",
4004 elements.len()
4005 ),
4006 });
4007 }
4008 Value::Matrix {
4009 rows: *rows,
4010 cols: *cols,
4011 values: elements
4012 .iter()
4013 .map(|id| scalar_at(&values, id.index()))
4014 .collect::<RuntimeResult<_>>()?,
4015 }
4016 }
4017 ExprNode::Component { input, index: i } => {
4018 let vector = vector_at(&values, input.index())?;
4019 Value::Scalar(*vector.get(*i).ok_or_else(|| RuntimeError::InvalidShape {
4020 index,
4021 message: format!(
4022 "component index {i} out of bounds for len {}",
4023 vector.len()
4024 ),
4025 })?)
4026 }
4027 ExprNode::MatrixElement { input, row, col } => {
4028 let (rows, cols, matrix) = matrix_at(&values, input.index())?;
4029 if *row >= rows || *col >= cols {
4030 return Err(RuntimeError::InvalidShape {
4031 index,
4032 message: format!(
4033 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4034 ),
4035 });
4036 }
4037 Value::Scalar(matrix[row * cols + col])
4038 }
4039 ExprNode::MatMul { lhs, rhs } => {
4040 let (lhs_rows, lhs_cols, lhs) = matrix_at(&values, lhs.index())?;
4041 let (rhs_rows, rhs_cols, rhs) = matrix_at(&values, rhs.index())?;
4042 if lhs_cols != rhs_rows {
4043 return Err(RuntimeError::InvalidShape {
4044 index,
4045 message: format!(
4046 "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4047 ),
4048 });
4049 }
4050 let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
4051 let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
4052 let out = lhs * rhs;
4053 Value::Matrix {
4054 rows: out.nrows(),
4055 cols: out.ncols(),
4056 values: matrix_values_row_major(&out),
4057 }
4058 }
4059 ExprNode::MatVec { matrix, vector } => {
4060 let (rows, cols, matrix) = matrix_at(&values, matrix.index())?;
4061 let vector = vector_at(&values, vector.index())?;
4062 if cols != vector.len() {
4063 return Err(RuntimeError::InvalidShape {
4064 index,
4065 message: format!(
4066 "cannot multiply {rows}x{cols} matrix by len {} vector",
4067 vector.len()
4068 ),
4069 });
4070 }
4071 let matrix = DMatrix::from_row_slice(rows, cols, matrix);
4072 let vector = DVector::from_row_slice(vector);
4073 Value::Vector((matrix * vector).iter().copied().collect())
4074 }
4075 ExprNode::Dot { lhs, rhs } => {
4076 let lhs = vector_at(&values, lhs.index())?;
4077 let rhs = vector_at(&values, rhs.index())?;
4078 if lhs.len() != rhs.len() {
4079 return Err(RuntimeError::InvalidShape {
4080 index,
4081 message: format!(
4082 "cannot dot len {} vector with len {} vector",
4083 lhs.len(),
4084 rhs.len()
4085 ),
4086 });
4087 }
4088 Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
4089 }
4090 ExprNode::Solve { matrix, rhs } => {
4091 let matrix_id = *matrix;
4092 let (rows, cols, matrix) = matrix_at(&values, matrix_id.index())?;
4093 let rhs = vector_at(&values, rhs.index())?;
4094 if rows != cols || rows != rhs.len() {
4095 return Err(RuntimeError::InvalidShape {
4096 index,
4097 message: format!(
4098 "cannot solve {rows}x{cols} matrix against len {} vector",
4099 rhs.len()
4100 ),
4101 });
4102 }
4103 let rhs = DVector::from_row_slice(rhs);
4104 let solution = self.solve_primal(matrix_id, rows, matrix, &rhs, index, None)?;
4105 Value::Vector(solution.iter().copied().collect())
4106 }
4107 };
4108 values.push(value);
4109 }
4110
4111 Ok(values)
4112 }
4113
4114 fn evaluate_values_from_cache(
4115 &self,
4116 params: &ParamValues,
4117 cache: &CpuBatchCache,
4118 row: usize,
4119 ) -> RuntimeResult<Vec<Value>> {
4120 let mut values = Vec::with_capacity(self.cached_evaluation_nodes.len());
4121
4122 for id in &self.cached_evaluation_nodes {
4123 let index = id.index();
4124 let node = &self.graph.nodes()[index];
4125 if let Some(slot) = self.cache_slots[index] {
4126 values.push(cache.value(slot, row)?);
4127 continue;
4128 }
4129 let value = match node {
4130 ExprNode::RealConst(value) => Value::Scalar(Complex64::from(*value)),
4131 ExprNode::ComplexConst(value) => Value::Scalar(*value),
4132 ExprNode::ScalarParam(_) => {
4133 Value::Scalar(Complex64::from(self.parameter_value(params, index)?))
4134 }
4135 ExprNode::EventScalar(name) => {
4136 return Err(RuntimeError::MissingEventScalar(name.to_string()));
4137 }
4138 ExprNode::EventP4Component { name, component } => {
4139 return Err(RuntimeError::MissingEventScalar(format!(
4140 "{name}.{}",
4141 component.label()
4142 )));
4143 }
4144 ExprNode::Unary { op, input } => {
4145 let input = self.cached_scalar_at(&values, *input)?;
4146 Value::Scalar(eval_unary(*op, input))
4147 }
4148 ExprNode::Binary { op, lhs, rhs } => {
4149 let lhs = self.cached_scalar_at(&values, *lhs)?;
4150 let rhs = self.cached_scalar_at(&values, *rhs)?;
4151 Value::Scalar(eval_binary(*op, lhs, rhs))
4152 }
4153 ExprNode::NaryAdd { terms } => {
4154 let mut sum = Complex64::ZERO;
4155 for term in terms {
4156 sum += self.cached_scalar_at(&values, *term)?;
4157 }
4158 Value::Scalar(sum)
4159 }
4160 ExprNode::NaryMul { factors } => {
4161 let mut product = Complex64::ONE;
4162 for factor in factors {
4163 product *= self.cached_scalar_at(&values, *factor)?;
4164 }
4165 Value::Scalar(product)
4166 }
4167 ExprNode::Complex { re, im } => {
4168 let re = self.cached_scalar_at(&values, *re)?;
4169 let im = self.cached_scalar_at(&values, *im)?;
4170 Value::Scalar(Complex64::new(re.re, im.re))
4171 }
4172 ExprNode::Vector { elements } => Value::Vector(
4173 elements
4174 .iter()
4175 .map(|id| self.cached_scalar_at(&values, *id))
4176 .collect::<RuntimeResult<_>>()?,
4177 ),
4178 ExprNode::Matrix {
4179 rows,
4180 cols,
4181 elements,
4182 } => {
4183 if elements.len() != rows * cols {
4184 return Err(RuntimeError::InvalidShape {
4185 index,
4186 message: format!(
4187 "matrix has {} elements for shape {rows}x{cols}",
4188 elements.len()
4189 ),
4190 });
4191 }
4192 Value::Matrix {
4193 rows: *rows,
4194 cols: *cols,
4195 values: elements
4196 .iter()
4197 .map(|id| self.cached_scalar_at(&values, *id))
4198 .collect::<RuntimeResult<_>>()?,
4199 }
4200 }
4201 ExprNode::Component { input, index: i } => {
4202 if let Some(plan) = self.solve_components[index] {
4203 let inverse_row = cache.solve_row(plan.row_slot(), row)?;
4204 if inverse_row.len() != plan.dimension() {
4205 return Err(RuntimeError::InvalidShape {
4206 index,
4207 message: format!(
4208 "specialized solve expected row len {}, got {}",
4209 plan.dimension(),
4210 inverse_row.len()
4211 ),
4212 });
4213 }
4214 if let Some(elements) = &self.solve_rhs_elements[plan.rhs().index()] {
4215 if elements.len() != plan.dimension() {
4216 return Err(RuntimeError::InvalidShape {
4217 index,
4218 message: format!(
4219 "specialized solve expected {} RHS elements, got {}",
4220 plan.dimension(),
4221 elements.len()
4222 ),
4223 });
4224 }
4225 Value::Scalar(
4226 inverse_row
4227 .iter()
4228 .zip(elements)
4229 .map(|(lhs, rhs)| {
4230 Ok(lhs * self.cached_scalar_at(&values, *rhs)?)
4231 })
4232 .sum::<RuntimeResult<Complex64>>()?,
4233 )
4234 } else {
4235 let rhs = self.cached_vector_at(&values, plan.rhs())?;
4236 if rhs.len() != plan.dimension() {
4237 return Err(RuntimeError::InvalidShape {
4238 index,
4239 message: format!(
4240 "specialized solve expected RHS len {}, got {}",
4241 plan.dimension(),
4242 rhs.len()
4243 ),
4244 });
4245 }
4246 Value::Scalar(
4247 inverse_row
4248 .iter()
4249 .zip(rhs)
4250 .map(|(lhs, rhs)| lhs * rhs)
4251 .sum(),
4252 )
4253 }
4254 } else {
4255 let vector = self.cached_vector_at(&values, *input)?;
4256 Value::Scalar(*vector.get(*i).ok_or_else(|| {
4257 RuntimeError::InvalidShape {
4258 index,
4259 message: format!(
4260 "component index {i} out of bounds for len {}",
4261 vector.len()
4262 ),
4263 }
4264 })?)
4265 }
4266 }
4267 ExprNode::MatrixElement { input, row, col } => {
4268 let (rows, cols, matrix) = self.cached_matrix_at(&values, *input)?;
4269 if *row >= rows || *col >= cols {
4270 return Err(RuntimeError::InvalidShape {
4271 index,
4272 message: format!(
4273 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4274 ),
4275 });
4276 }
4277 Value::Scalar(matrix[row * cols + col])
4278 }
4279 ExprNode::MatMul { lhs, rhs } => {
4280 let (lhs_rows, lhs_cols, lhs) = self.cached_matrix_at(&values, *lhs)?;
4281 let (rhs_rows, rhs_cols, rhs) = self.cached_matrix_at(&values, *rhs)?;
4282 if lhs_cols != rhs_rows {
4283 return Err(RuntimeError::InvalidShape {
4284 index,
4285 message: format!(
4286 "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4287 ),
4288 });
4289 }
4290 let lhs = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs);
4291 let rhs = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs);
4292 let out = lhs * rhs;
4293 Value::Matrix {
4294 rows: out.nrows(),
4295 cols: out.ncols(),
4296 values: matrix_values_row_major(&out),
4297 }
4298 }
4299 ExprNode::MatVec { matrix, vector } => {
4300 let (rows, cols, matrix) = self.cached_matrix_at(&values, *matrix)?;
4301 let vector = self.cached_vector_at(&values, *vector)?;
4302 if cols != vector.len() {
4303 return Err(RuntimeError::InvalidShape {
4304 index,
4305 message: format!(
4306 "cannot multiply {rows}x{cols} matrix by len {} vector",
4307 vector.len()
4308 ),
4309 });
4310 }
4311 let matrix = DMatrix::from_row_slice(rows, cols, matrix);
4312 let vector = DVector::from_row_slice(vector);
4313 Value::Vector((matrix * vector).iter().copied().collect())
4314 }
4315 ExprNode::Dot { lhs, rhs } => {
4316 let lhs = self.cached_vector_at(&values, *lhs)?;
4317 let rhs = self.cached_vector_at(&values, *rhs)?;
4318 if lhs.len() != rhs.len() {
4319 return Err(RuntimeError::InvalidShape {
4320 index,
4321 message: format!(
4322 "cannot dot len {} vector with len {} vector",
4323 lhs.len(),
4324 rhs.len()
4325 ),
4326 });
4327 }
4328 Value::Scalar(lhs.iter().zip(rhs).map(|(lhs, rhs)| lhs * rhs).sum())
4329 }
4330 ExprNode::Solve { matrix, rhs } => {
4331 let matrix_id = *matrix;
4332 let (rows, cols, matrix) = self.cached_matrix_at(&values, matrix_id)?;
4333 let rhs = self.cached_vector_at(&values, *rhs)?;
4334 if rows != cols || rows != rhs.len() {
4335 return Err(RuntimeError::InvalidShape {
4336 index,
4337 message: format!(
4338 "cannot solve {rows}x{cols} matrix against len {} vector",
4339 rhs.len()
4340 ),
4341 });
4342 }
4343 let rhs = DVector::from_row_slice(rhs);
4344 let solution = self.solve_primal(
4345 matrix_id,
4346 rows,
4347 matrix,
4348 &rhs,
4349 index,
4350 Some((cache, row)),
4351 )?;
4352 Value::Vector(solution.iter().copied().collect())
4353 }
4354 };
4355 values.push(value);
4356 }
4357
4358 Ok(values)
4359 }
4360
4361 fn cached_value_slot(&self, id: ExprId) -> RuntimeResult<usize> {
4362 self.cached_value_slots[id.index()].ok_or_else(|| RuntimeError::InvalidShape {
4363 index: id.index(),
4364 message: "node is not part of the cached evaluation schedule".into(),
4365 })
4366 }
4367
4368 fn cached_scalar_at(&self, values: &[Value], id: ExprId) -> RuntimeResult<Complex64> {
4369 scalar_at(values, self.cached_value_slot(id)?)
4370 }
4371
4372 fn cached_vector_at<'a>(
4373 &self,
4374 values: &'a [Value],
4375 id: ExprId,
4376 ) -> RuntimeResult<&'a [Complex64]> {
4377 vector_at(values, self.cached_value_slot(id)?)
4378 }
4379
4380 fn cached_matrix_at<'a>(
4381 &self,
4382 values: &'a [Value],
4383 id: ExprId,
4384 ) -> RuntimeResult<(usize, usize, &'a [Complex64])> {
4385 matrix_at(values, self.cached_value_slot(id)?)
4386 }
4387
4388 fn check_batch_cache(&self, cache: &CpuBatchCache) -> RuntimeResult<()> {
4389 if cache.nodes
4390 == self
4391 .cache_plan
4392 .entries()
4393 .iter()
4394 .map(|entry| entry.node())
4395 .collect::<Vec<_>>()
4396 && cache.factor_nodes
4397 == self
4398 .factor_matrices
4399 .iter()
4400 .map(|(node, _)| *node)
4401 .collect::<Vec<_>>()
4402 && cache.solve_row_keys == self.solve_row_keys
4403 {
4404 Ok(())
4405 } else {
4406 Err(RuntimeError::InvalidCacheLayout)
4407 }
4408 }
4409}
4410
4411fn resident_cache_plan(
4412 fixed_per_batch: usize,
4413 cache_bytes_per_event: usize,
4414 source_bytes_per_event: usize,
4415 events: usize,
4416 available: usize,
4417) -> Option<(usize, usize)> {
4418 if events == 0 {
4419 return Some((fixed_per_batch, 1));
4420 }
4421 let event_cache = cache_bytes_per_event.checked_mul(events)?;
4422 let minimum = event_cache
4423 .checked_add(fixed_per_batch)?
4424 .checked_add(source_bytes_per_event)?;
4425 if minimum > available {
4426 return None;
4427 }
4428 let mut chunk = events;
4429 for _ in 0..16 {
4430 let batches = events.saturating_add(chunk - 1) / chunk;
4431 let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
4432 let next = available
4433 .saturating_sub(resident)
4434 .checked_div(source_bytes_per_event.max(1))?
4435 .min(events);
4436 if next == 0 {
4437 return None;
4438 }
4439 if next == chunk {
4440 return Some((resident, chunk));
4441 }
4442 chunk = next;
4443 }
4444 let batches = events.saturating_add(chunk - 1) / chunk;
4445 let resident = event_cache.checked_add(fixed_per_batch.checked_mul(batches)?)?;
4446 (resident.checked_add(source_bytes_per_event.checked_mul(chunk)?)? <= available)
4447 .then_some((resident, chunk))
4448}
4449
4450#[derive(Clone, Debug, PartialEq)]
4451enum Value {
4452 Scalar(Complex64),
4453 Vector(Vec<Complex64>),
4454 Matrix {
4455 rows: usize,
4456 cols: usize,
4457 values: Vec<Complex64>,
4458 },
4459}
4460
4461#[derive(Clone, Copy)]
4462enum F32KernelInput<'a> {
4463 Cache(Option<(&'a CpuBatchCache, usize)>),
4464 Event(&'a dyn EventLookup),
4465}
4466
4467impl<'a> F32KernelInput<'a> {
4468 fn cache(self) -> Option<(&'a CpuBatchCache, usize)> {
4469 match self {
4470 Self::Cache(cache) => cache,
4471 Self::Event(_) => None,
4472 }
4473 }
4474}
4475
4476#[derive(Clone, Debug, PartialEq)]
4477enum F32Value {
4478 Scalar(Complex32),
4479 Vector(Vec<Complex32>),
4480 Matrix {
4481 rows: usize,
4482 cols: usize,
4483 values: Vec<Complex32>,
4484 },
4485}
4486
4487impl F32Value {
4488 fn from_value(value: Value) -> Self {
4489 match value {
4490 Value::Scalar(value) => Self::Scalar(Complex32::new(value.re as f32, value.im as f32)),
4491 Value::Vector(values) => Self::Vector(
4492 values
4493 .into_iter()
4494 .map(|value| Complex32::new(value.re as f32, value.im as f32))
4495 .collect(),
4496 ),
4497 Value::Matrix { rows, cols, values } => Self::Matrix {
4498 rows,
4499 cols,
4500 values: values
4501 .into_iter()
4502 .map(|value| Complex32::new(value.re as f32, value.im as f32))
4503 .collect(),
4504 },
4505 }
4506 }
4507
4508 fn kind(&self) -> &'static str {
4509 match self {
4510 Self::Scalar(_) => "scalar",
4511 Self::Vector(_) => "vector",
4512 Self::Matrix { .. } => "matrix",
4513 }
4514 }
4515}
4516
4517type DynamicLu = LU<Complex64, Dyn, Dyn>;
4518
4519struct DerivativeWorkspace<'a> {
4520 plan: &'a CpuPlan,
4521 primals: &'a [Value],
4522 tangents: Vec<Option<Value>>,
4523 factors: HashMap<usize, DynamicLu>,
4524 cached_factors: Option<(&'a CpuBatchCache, usize)>,
4525}
4526
4527impl<'a> DerivativeWorkspace<'a> {
4528 fn new(
4529 plan: &'a CpuPlan,
4530 primals: &'a [Value],
4531 cached_factors: Option<(&'a CpuBatchCache, usize)>,
4532 ) -> Self {
4533 Self {
4534 plan,
4535 primals,
4536 tangents: vec![None; plan.graph.nodes().len()],
4537 factors: HashMap::new(),
4538 cached_factors,
4539 }
4540 }
4541
4542 fn gradient(&mut self) -> RuntimeResult<Vec<Complex64>> {
4543 let mut gradient = Vec::with_capacity(self.plan.autodiff.parameter_count());
4544 for parameter in 0..self.plan.autodiff.parameter_count() {
4545 let active = self
4546 .plan
4547 .autodiff
4548 .active_nodes(parameter)
4549 .expect("free parameter index is valid");
4550 for id in active {
4551 self.differentiate_node(*id)?;
4552 }
4553 gradient.push(self.scalar_tangent(self.plan.graph.root())?);
4554 for id in active {
4555 self.tangents[id.index()] = None;
4556 }
4557 }
4558 Ok(gradient)
4559 }
4560
4561 fn differentiate_node(&mut self, id: ExprId) -> RuntimeResult<()> {
4562 let index = id.index();
4563 let node = self.plan.graph.nodes()[index].clone();
4564 let tangent = match node {
4565 ExprNode::ScalarParam(_) => Value::Scalar(Complex64::ONE),
4566 ExprNode::Unary { op, input } => {
4567 let input_value = self.primal_scalar(input)?;
4568 let output_value = self.primal_scalar(id)?;
4569 let input_tangent = self.scalar_tangent(input)?;
4570 let value = match op {
4571 UnaryOp::Neg => -input_tangent,
4572 UnaryOp::Real => Complex64::from(input_tangent.re),
4573 UnaryOp::Imag => Complex64::from(input_tangent.im),
4574 UnaryOp::Conj => input_tangent.conj(),
4575 UnaryOp::NormSqr => {
4576 Complex64::from(2.0 * (input_value.conj() * input_tangent).re)
4577 }
4578 UnaryOp::Sqrt => input_tangent / (2.0 * output_value),
4579 UnaryOp::Exp => output_value * input_tangent,
4580 UnaryOp::Sin => input_value.cos() * input_tangent,
4581 UnaryOp::Cos => -input_value.sin() * input_tangent,
4582 UnaryOp::Log => input_tangent / input_value,
4583 UnaryOp::PowI(power) => {
4584 if power == 0 {
4585 Complex64::ZERO
4586 } else if power == i32::MIN {
4587 power as f64 * output_value * input_tangent / input_value
4588 } else {
4589 power as f64 * input_value.powi(power - 1) * input_tangent
4590 }
4591 }
4592 };
4593 Value::Scalar(value)
4594 }
4595 ExprNode::Binary { op, lhs, rhs } => {
4596 let lhs_value = self.primal_scalar(lhs)?;
4597 let rhs_value = self.primal_scalar(rhs)?;
4598 let lhs_tangent = self.scalar_tangent(lhs)?;
4599 let rhs_tangent = self.scalar_tangent(rhs)?;
4600 let value = match op {
4601 BinaryOp::Add => lhs_tangent + rhs_tangent,
4602 BinaryOp::Sub => lhs_tangent - rhs_tangent,
4603 BinaryOp::Mul => lhs_tangent * rhs_value + lhs_value * rhs_tangent,
4604 BinaryOp::Div => {
4605 (lhs_tangent * rhs_value - lhs_value * rhs_tangent) / rhs_value.powi(2)
4606 }
4607 BinaryOp::Atan2 => {
4608 let denominator = lhs_value.re.powi(2) + rhs_value.re.powi(2);
4609 Complex64::from(
4610 (rhs_value.re * lhs_tangent.re - lhs_value.re * rhs_tangent.re)
4611 / denominator,
4612 )
4613 }
4614 };
4615 Value::Scalar(value)
4616 }
4617 ExprNode::NaryAdd { terms } => {
4618 Value::Scalar(terms.into_iter().try_fold(Complex64::ZERO, |sum, term| {
4619 Ok::<_, RuntimeError>(sum + self.scalar_tangent(term)?)
4620 })?)
4621 }
4622 ExprNode::NaryMul { factors } => {
4623 let mut product = Complex64::ONE;
4624 let mut derivative = Complex64::ZERO;
4625 for factor in factors {
4626 let value = self.primal_scalar(factor)?;
4627 derivative = derivative * value + product * self.scalar_tangent(factor)?;
4628 product *= value;
4629 }
4630 Value::Scalar(derivative)
4631 }
4632 ExprNode::Complex { re, im } => Value::Scalar(Complex64::new(
4633 self.scalar_tangent(re)?.re,
4634 self.scalar_tangent(im)?.re,
4635 )),
4636 ExprNode::Vector { .. }
4637 if self.cached_factors.is_some()
4638 && self.plan.cached_value_slots[index].is_none()
4639 && self.plan.solve_rhs_elements[index].is_some() =>
4640 {
4641 Value::Vector(Vec::new())
4642 }
4643 ExprNode::Vector { elements } => Value::Vector(
4644 elements
4645 .into_iter()
4646 .map(|element| self.scalar_tangent(element))
4647 .collect::<RuntimeResult<_>>()?,
4648 ),
4649 ExprNode::Matrix {
4650 rows,
4651 cols,
4652 elements,
4653 } => {
4654 if elements.len() != rows * cols {
4655 return Err(RuntimeError::InvalidShape {
4656 index,
4657 message: format!(
4658 "matrix has {} elements for shape {rows}x{cols}",
4659 elements.len()
4660 ),
4661 });
4662 }
4663 Value::Matrix {
4664 rows,
4665 cols,
4666 values: elements
4667 .into_iter()
4668 .map(|element| self.scalar_tangent(element))
4669 .collect::<RuntimeResult<_>>()?,
4670 }
4671 }
4672 ExprNode::Component { input, index: i } => {
4673 if let (Some(plan), Some((cache, row))) =
4674 (self.plan.solve_components[index], self.cached_factors)
4675 {
4676 let inverse_row = cache.solve_row(plan.row_slot(), row)?;
4677 if let Some(elements) = &self.plan.solve_rhs_elements[plan.rhs().index()] {
4678 Value::Scalar(
4679 inverse_row
4680 .iter()
4681 .zip(elements)
4682 .map(|(lhs, rhs)| Ok(lhs * self.scalar_tangent(*rhs)?))
4683 .sum::<RuntimeResult<Complex64>>()?,
4684 )
4685 } else {
4686 let rhs_tangent =
4687 self.vector_tangent_value(plan.rhs(), plan.dimension())?;
4688 Value::Scalar(
4689 inverse_row
4690 .iter()
4691 .zip(rhs_tangent)
4692 .map(|(lhs, rhs)| lhs * rhs)
4693 .sum(),
4694 )
4695 }
4696 } else {
4697 let vector = self.vector_tangent(input)?;
4698 Value::Scalar(*vector.get(i).ok_or_else(|| RuntimeError::InvalidShape {
4699 index,
4700 message: format!(
4701 "component index {i} out of bounds for len {}",
4702 vector.len()
4703 ),
4704 })?)
4705 }
4706 }
4707 ExprNode::MatrixElement { input, row, col } => {
4708 let (rows, cols, matrix) = self.matrix_tangent(input)?;
4709 if row >= rows || col >= cols {
4710 return Err(RuntimeError::InvalidShape {
4711 index,
4712 message: format!(
4713 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
4714 ),
4715 });
4716 }
4717 Value::Scalar(matrix[row * cols + col])
4718 }
4719 ExprNode::MatMul { lhs, rhs } => {
4720 let (lhs_rows, lhs_cols, lhs_value) = self.primal_matrix(lhs)?;
4721 let (rhs_rows, rhs_cols, rhs_value) = self.primal_matrix(rhs)?;
4722 if lhs_cols != rhs_rows {
4723 return Err(RuntimeError::InvalidShape {
4724 index,
4725 message: format!(
4726 "cannot multiply {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
4727 ),
4728 });
4729 }
4730 let lhs_value = DMatrix::from_row_slice(lhs_rows, lhs_cols, lhs_value);
4731 let rhs_value = DMatrix::from_row_slice(rhs_rows, rhs_cols, rhs_value);
4732 let lhs_tangent = self.matrix_tangent_value(lhs, lhs_rows, lhs_cols)?;
4733 let rhs_tangent = self.matrix_tangent_value(rhs, rhs_rows, rhs_cols)?;
4734 let output = lhs_tangent * &rhs_value + lhs_value * rhs_tangent;
4735 Value::Matrix {
4736 rows: output.nrows(),
4737 cols: output.ncols(),
4738 values: matrix_values_row_major(&output),
4739 }
4740 }
4741 ExprNode::MatVec { matrix, vector } => {
4742 let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
4743 let vector_value = self.primal_vector(vector)?;
4744 if cols != vector_value.len() {
4745 return Err(RuntimeError::InvalidShape {
4746 index,
4747 message: format!(
4748 "cannot multiply {rows}x{cols} matrix by len {} vector",
4749 vector_value.len()
4750 ),
4751 });
4752 }
4753 let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
4754 let vector_value = DVector::from_row_slice(vector_value);
4755 let matrix_tangent = self.matrix_tangent_value(matrix, rows, cols)?;
4756 let vector_tangent = DVector::from_vec(self.vector_tangent_value(vector, cols)?);
4757 Value::Vector(
4758 (matrix_tangent * vector_value + matrix_value * vector_tangent)
4759 .iter()
4760 .copied()
4761 .collect(),
4762 )
4763 }
4764 ExprNode::Dot { lhs, rhs } => {
4765 let lhs_value = self.primal_vector(lhs)?;
4766 let rhs_value = self.primal_vector(rhs)?;
4767 if lhs_value.len() != rhs_value.len() {
4768 return Err(RuntimeError::InvalidShape {
4769 index,
4770 message: format!(
4771 "cannot dot len {} vector with len {} vector",
4772 lhs_value.len(),
4773 rhs_value.len()
4774 ),
4775 });
4776 }
4777 let lhs_tangent = self.vector_tangent_value(lhs, lhs_value.len())?;
4778 let rhs_tangent = self.vector_tangent_value(rhs, rhs_value.len())?;
4779 Value::Scalar(
4780 lhs_tangent
4781 .iter()
4782 .zip(rhs_value)
4783 .map(|(lhs, rhs)| lhs * rhs)
4784 .sum::<Complex64>()
4785 + lhs_value
4786 .iter()
4787 .zip(rhs_tangent)
4788 .map(|(lhs, rhs)| lhs * rhs)
4789 .sum::<Complex64>(),
4790 )
4791 }
4792 ExprNode::Solve { matrix, rhs } => {
4793 if self.cached_factors.is_some() && self.plan.cached_value_slots[index].is_none() {
4794 self.tangents[index] = Some(Value::Vector(Vec::new()));
4796 return Ok(());
4797 }
4798 let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
4799 let solution = self.primal_vector(id)?;
4800 let rhs_value = self.primal_vector(rhs)?;
4801 if rows != cols || rows != rhs_value.len() {
4802 return Err(RuntimeError::InvalidShape {
4803 index,
4804 message: format!(
4805 "cannot solve {rows}x{cols} matrix against len {} vector",
4806 rhs_value.len()
4807 ),
4808 });
4809 }
4810 let matrix_tangent = self.matrix_tangent_value(matrix, rows, cols)?;
4811 let rhs_tangent = DVector::from_vec(self.vector_tangent_value(rhs, rows)?);
4812 let solution = DVector::from_row_slice(solution);
4813 let tangent_rhs = rhs_tangent - matrix_tangent * solution;
4814 let tangent = if let (Some(slot), Some((cache, row))) = (
4815 self.plan.factor_matrix_slots[matrix.index()],
4816 self.cached_factors,
4817 ) {
4818 cache
4819 .factor(slot, row)?
4820 .solve(&tangent_rhs)
4821 .ok_or(RuntimeError::SingularMatrix(index))?
4822 } else if let Some(slot) = self.plan.constant_factor_slots[matrix.index()] {
4823 self.plan.constant_factors[slot]
4824 .get_or_init(|| DMatrix::from_row_slice(rows, cols, matrix_value).lu())
4825 .solve(&tangent_rhs)
4826 .ok_or(RuntimeError::SingularMatrix(index))?
4827 } else {
4828 let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
4829 self.factors
4830 .entry(matrix.index())
4831 .or_insert_with(|| matrix_value.lu())
4832 .solve(&tangent_rhs)
4833 .ok_or(RuntimeError::SingularMatrix(index))?
4834 };
4835 Value::Vector(tangent.iter().copied().collect())
4836 }
4837 ExprNode::RealConst(_)
4838 | ExprNode::ComplexConst(_)
4839 | ExprNode::EventScalar(_)
4840 | ExprNode::EventP4Component { .. } => {
4841 return Err(RuntimeError::InvalidShape {
4842 index,
4843 message: "parameter-independent node appeared in a derivative lane".into(),
4844 });
4845 }
4846 };
4847 self.tangents[index] = Some(tangent);
4848 Ok(())
4849 }
4850
4851 fn primal_scalar(&self, id: ExprId) -> RuntimeResult<Complex64> {
4852 if self.cached_factors.is_some() {
4853 self.plan.cached_scalar_at(self.primals, id)
4854 } else {
4855 scalar_at(self.primals, id.index())
4856 }
4857 }
4858
4859 fn primal_vector(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
4860 if self.cached_factors.is_some() {
4861 self.plan.cached_vector_at(self.primals, id)
4862 } else {
4863 vector_at(self.primals, id.index())
4864 }
4865 }
4866
4867 fn primal_matrix(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
4868 if self.cached_factors.is_some() {
4869 self.plan.cached_matrix_at(self.primals, id)
4870 } else {
4871 matrix_at(self.primals, id.index())
4872 }
4873 }
4874
4875 fn scalar_tangent(&self, id: ExprId) -> RuntimeResult<Complex64> {
4876 match &self.tangents[id.index()] {
4877 Some(Value::Scalar(value)) => Ok(*value),
4878 Some(value) => Err(RuntimeError::TypeMismatch {
4879 index: id.index(),
4880 expected: "scalar tangent",
4881 actual: value.kind(),
4882 }),
4883 None => Ok(Complex64::ZERO),
4884 }
4885 }
4886
4887 fn vector_tangent(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
4888 match &self.tangents[id.index()] {
4889 Some(Value::Vector(values)) => Ok(values),
4890 Some(value) => Err(RuntimeError::TypeMismatch {
4891 index: id.index(),
4892 expected: "vector tangent",
4893 actual: value.kind(),
4894 }),
4895 None => Err(RuntimeError::InvalidShape {
4896 index: id.index(),
4897 message: "inactive vector tangent requested without a target length".into(),
4898 }),
4899 }
4900 }
4901
4902 fn vector_tangent_value(&self, id: ExprId, len: usize) -> RuntimeResult<Vec<Complex64>> {
4903 match &self.tangents[id.index()] {
4904 Some(Value::Vector(values)) if values.len() == len => Ok(values.clone()),
4905 Some(Value::Vector(values)) => Err(RuntimeError::InvalidShape {
4906 index: id.index(),
4907 message: format!("vector tangent has len {}, expected {len}", values.len()),
4908 }),
4909 Some(value) => Err(RuntimeError::TypeMismatch {
4910 index: id.index(),
4911 expected: "vector tangent",
4912 actual: value.kind(),
4913 }),
4914 None => Ok(vec![Complex64::ZERO; len]),
4915 }
4916 }
4917
4918 fn matrix_tangent(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
4919 match &self.tangents[id.index()] {
4920 Some(Value::Matrix { rows, cols, values }) => Ok((*rows, *cols, values)),
4921 Some(value) => Err(RuntimeError::TypeMismatch {
4922 index: id.index(),
4923 expected: "matrix tangent",
4924 actual: value.kind(),
4925 }),
4926 None => Err(RuntimeError::InvalidShape {
4927 index: id.index(),
4928 message: "inactive matrix tangent requested without a target shape".into(),
4929 }),
4930 }
4931 }
4932
4933 fn matrix_tangent_value(
4934 &self,
4935 id: ExprId,
4936 rows: usize,
4937 cols: usize,
4938 ) -> RuntimeResult<DMatrix<Complex64>> {
4939 match &self.tangents[id.index()] {
4940 Some(Value::Matrix {
4941 rows: actual_rows,
4942 cols: actual_cols,
4943 values,
4944 }) if *actual_rows == rows && *actual_cols == cols => {
4945 Ok(DMatrix::from_row_slice(rows, cols, values))
4946 }
4947 Some(Value::Matrix {
4948 rows: actual_rows,
4949 cols: actual_cols,
4950 ..
4951 }) => Err(RuntimeError::InvalidShape {
4952 index: id.index(),
4953 message: format!(
4954 "matrix tangent has shape {actual_rows}x{actual_cols}, expected {rows}x{cols}"
4955 ),
4956 }),
4957 Some(value) => Err(RuntimeError::TypeMismatch {
4958 index: id.index(),
4959 expected: "matrix tangent",
4960 actual: value.kind(),
4961 }),
4962 None => Ok(DMatrix::zeros(rows, cols)),
4963 }
4964 }
4965}
4966
4967#[derive(Clone, Copy, Debug, Default, PartialEq)]
4968struct ScalarAdjoint {
4969 dz: Complex64,
4970 dz_conj: Complex64,
4971}
4972
4973impl ScalarAdjoint {
4974 fn seed() -> Self {
4975 Self {
4976 dz: Complex64::ONE,
4977 dz_conj: Complex64::ZERO,
4978 }
4979 }
4980
4981 fn gradient(self) -> Complex64 {
4982 self.dz + self.dz_conj
4983 }
4984}
4985
4986#[derive(Clone, Debug, PartialEq)]
4987enum ReverseAdjoint {
4988 Scalar(ScalarAdjoint),
4989 Vector(Vec<ScalarAdjoint>),
4990 Matrix {
4991 rows: usize,
4992 cols: usize,
4993 values: Vec<ScalarAdjoint>,
4994 },
4995}
4996
4997impl ReverseAdjoint {
4998 fn kind(&self) -> &'static str {
4999 match self {
5000 Self::Scalar(_) => "scalar adjoint",
5001 Self::Vector(_) => "vector adjoint",
5002 Self::Matrix { .. } => "matrix adjoint",
5003 }
5004 }
5005}
5006
5007struct ReverseDerivativeWorkspace<'a> {
5008 plan: &'a CpuPlan,
5009 primals: &'a [Value],
5010 adjoints: Vec<Option<ReverseAdjoint>>,
5011 cached_factors: Option<(&'a CpuBatchCache, usize)>,
5012}
5013
5014impl<'a> ReverseDerivativeWorkspace<'a> {
5015 fn new(
5016 plan: &'a CpuPlan,
5017 primals: &'a [Value],
5018 cached_factors: Option<(&'a CpuBatchCache, usize)>,
5019 ) -> Self {
5020 Self {
5021 plan,
5022 primals,
5023 adjoints: vec![None; plan.graph.nodes().len()],
5024 cached_factors,
5025 }
5026 }
5027
5028 fn gradient(&mut self) -> RuntimeResult<Vec<Complex64>> {
5029 self.accumulate_scalar(self.plan.graph.root(), ScalarAdjoint::seed())?;
5030 if self.cached_factors.is_some() {
5031 for id in self.plan.cached_evaluation_nodes.iter().rev().copied() {
5032 self.propagate_node(id)?;
5033 }
5034 } else {
5035 for index in (0..self.plan.graph.nodes().len()).rev() {
5036 let id = ExprId::from_index(index);
5037 self.propagate_node(id)?;
5038 }
5039 }
5040
5041 let mut gradient = vec![Complex64::ZERO; self.plan.autodiff.parameter_count()];
5042 for (index, parameter) in self.plan.parameter_slots.iter().enumerate() {
5043 let Some(parameter) = parameter else {
5044 continue;
5045 };
5046 let Ok(Some(free_id)) = self.plan.params.free_id(*parameter) else {
5047 continue;
5048 };
5049 if let Some(adjoint) = self.scalar_adjoint_at(index)? {
5050 gradient[free_id.index()] += adjoint.gradient();
5051 }
5052 }
5053 Ok(gradient)
5054 }
5055
5056 fn propagate_node(&mut self, id: ExprId) -> RuntimeResult<()> {
5057 let index = id.index();
5058 if matches!(self.plan.graph.nodes()[index], ExprNode::ScalarParam(_)) {
5059 return Ok(());
5060 }
5061 let Some(adjoint) = self.adjoints[index].take() else {
5062 return Ok(());
5063 };
5064 if self.cached_factors.is_some() && self.plan.cache_slots[index].is_some() {
5065 return Ok(());
5066 }
5067 let node = self.plan.graph.nodes()[index].clone();
5068 match node {
5069 ExprNode::Unary { op, input } => {
5070 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5071 let input_value = self.primal_scalar(input)?;
5072 let output_value = self.primal_scalar(id)?;
5073 self.propagate_unary(op, input, input_value, output_value, adjoint)?;
5074 }
5075 ExprNode::Binary { op, lhs, rhs } => {
5076 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5077 let lhs_value = self.primal_scalar(lhs)?;
5078 let rhs_value = self.primal_scalar(rhs)?;
5079 self.propagate_binary(op, lhs, rhs, lhs_value, rhs_value, adjoint)?;
5080 }
5081 ExprNode::NaryAdd { terms } => {
5082 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5083 for term in terms {
5084 self.accumulate_scalar(term, adjoint)?;
5085 }
5086 }
5087 ExprNode::NaryMul { factors } => {
5088 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5089 let values = factors
5090 .iter()
5091 .map(|factor| self.primal_scalar(*factor))
5092 .collect::<RuntimeResult<Vec<_>>>()?;
5093 for (target, _) in factors.iter().enumerate() {
5094 let mut derivative = Complex64::ONE;
5095 for (source, value) in values.iter().copied().enumerate() {
5096 if source != target {
5097 derivative *= value;
5098 }
5099 }
5100 self.accumulate_analytic_scalar(factors[target], adjoint, derivative)?;
5101 }
5102 }
5103 ExprNode::Complex { re, im } => {
5104 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5105 let re_part = (adjoint.dz + adjoint.dz_conj) * 0.5;
5106 let im_part = Complex64::I * (adjoint.dz - adjoint.dz_conj) * 0.5;
5107 self.accumulate_scalar(
5108 re,
5109 ScalarAdjoint {
5110 dz: re_part,
5111 dz_conj: re_part,
5112 },
5113 )?;
5114 self.accumulate_scalar(
5115 im,
5116 ScalarAdjoint {
5117 dz: im_part,
5118 dz_conj: im_part,
5119 },
5120 )?;
5121 }
5122 ExprNode::Vector { elements } => {
5123 let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5124 if elements.len() != adjoint.len() {
5125 return Err(RuntimeError::InvalidShape {
5126 index,
5127 message: format!(
5128 "vector adjoint has len {}, expected {}",
5129 adjoint.len(),
5130 elements.len()
5131 ),
5132 });
5133 }
5134 for (element, contribution) in elements.into_iter().zip(adjoint) {
5135 self.accumulate_scalar(element, contribution)?;
5136 }
5137 }
5138 ExprNode::Matrix {
5139 rows,
5140 cols,
5141 elements,
5142 } => {
5143 let adjoint = Self::expect_matrix_adjoint(index, adjoint)?;
5144 if adjoint.0 != rows || adjoint.1 != cols || elements.len() != adjoint.2.len() {
5145 return Err(RuntimeError::InvalidShape {
5146 index,
5147 message: format!(
5148 "matrix adjoint has shape {}x{}, expected {rows}x{cols}",
5149 adjoint.0, adjoint.1
5150 ),
5151 });
5152 }
5153 for (element, contribution) in elements.into_iter().zip(adjoint.2) {
5154 self.accumulate_scalar(element, contribution)?;
5155 }
5156 }
5157 ExprNode::Component { input, index: i } => {
5158 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5159 if let (Some(plan), Some((cache, row))) =
5160 (self.plan.solve_components[index], self.cached_factors)
5161 {
5162 let inverse_row = cache.solve_row(plan.row_slot(), row)?;
5163 if inverse_row.len() != plan.dimension() {
5164 return Err(RuntimeError::InvalidShape {
5165 index,
5166 message: format!(
5167 "specialized solve expected row len {}, got {}",
5168 plan.dimension(),
5169 inverse_row.len()
5170 ),
5171 });
5172 }
5173 let rhs_contributions = inverse_row
5174 .iter()
5175 .map(|value| ScalarAdjoint {
5176 dz: adjoint.dz * value,
5177 dz_conj: adjoint.dz_conj * value.conj(),
5178 })
5179 .collect::<Vec<_>>();
5180 self.accumulate_solve_rhs_adjoint(
5181 plan.rhs(),
5182 plan.dimension(),
5183 rhs_contributions,
5184 )?;
5185 } else {
5186 self.accumulate_vector_element(input, i, adjoint)?;
5187 }
5188 }
5189 ExprNode::MatrixElement { input, row, col } => {
5190 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5191 let (rows, cols, _) = self.primal_matrix(input)?;
5192 if row >= rows || col >= cols {
5193 return Err(RuntimeError::InvalidShape {
5194 index,
5195 message: format!(
5196 "matrix element ({row}, {col}) out of bounds for shape {rows}x{cols}"
5197 ),
5198 });
5199 }
5200 self.accumulate_matrix_element(input, rows, cols, row, col, adjoint)?;
5201 }
5202 ExprNode::MatMul { lhs, rhs } => {
5203 let (out_rows, out_cols, adjoint) = Self::expect_matrix_adjoint(index, adjoint)?;
5204 self.propagate_matmul(index, lhs, rhs, out_rows, out_cols, &adjoint)?;
5205 }
5206 ExprNode::MatVec { matrix, vector } => {
5207 let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5208 self.propagate_matvec(index, matrix, vector, &adjoint)?;
5209 }
5210 ExprNode::Dot { lhs, rhs } => {
5211 let adjoint = Self::expect_scalar_adjoint(index, adjoint)?;
5212 self.propagate_dot(index, lhs, rhs, adjoint)?;
5213 }
5214 ExprNode::Solve { matrix, rhs } => {
5215 let adjoint = Self::expect_vector_adjoint(index, adjoint)?;
5216 self.propagate_solve(index, matrix, rhs, &adjoint)?;
5217 }
5218 ExprNode::RealConst(_)
5219 | ExprNode::ComplexConst(_)
5220 | ExprNode::ScalarParam(_)
5221 | ExprNode::EventScalar(_)
5222 | ExprNode::EventP4Component { .. } => {}
5223 }
5224 Ok(())
5225 }
5226
5227 fn propagate_unary(
5228 &mut self,
5229 op: UnaryOp,
5230 input: ExprId,
5231 input_value: Complex64,
5232 output_value: Complex64,
5233 adjoint: ScalarAdjoint,
5234 ) -> RuntimeResult<()> {
5235 match op {
5236 UnaryOp::Neg => self.accumulate_scalar(
5237 input,
5238 ScalarAdjoint {
5239 dz: -adjoint.dz,
5240 dz_conj: -adjoint.dz_conj,
5241 },
5242 ),
5243 UnaryOp::Real => {
5244 let contribution = (adjoint.dz + adjoint.dz_conj) * 0.5;
5245 self.accumulate_scalar(
5246 input,
5247 ScalarAdjoint {
5248 dz: contribution,
5249 dz_conj: contribution,
5250 },
5251 )
5252 }
5253 UnaryOp::Imag => {
5254 let contribution = -Complex64::I * (adjoint.dz + adjoint.dz_conj) * 0.5;
5255 self.accumulate_scalar(
5256 input,
5257 ScalarAdjoint {
5258 dz: contribution,
5259 dz_conj: -contribution,
5260 },
5261 )
5262 }
5263 UnaryOp::Conj => self.accumulate_scalar(
5264 input,
5265 ScalarAdjoint {
5266 dz: adjoint.dz_conj,
5267 dz_conj: adjoint.dz,
5268 },
5269 ),
5270 UnaryOp::NormSqr => {
5271 let sum = adjoint.dz + adjoint.dz_conj;
5272 self.accumulate_scalar(
5273 input,
5274 ScalarAdjoint {
5275 dz: sum * input_value.conj(),
5276 dz_conj: sum * input_value,
5277 },
5278 )
5279 }
5280 UnaryOp::Sqrt => {
5281 self.accumulate_analytic_scalar(input, adjoint, 1.0 / (2.0 * output_value))
5282 }
5283 UnaryOp::Exp => self.accumulate_analytic_scalar(input, adjoint, output_value),
5284 UnaryOp::Sin => self.accumulate_analytic_scalar(input, adjoint, input_value.cos()),
5285 UnaryOp::Cos => self.accumulate_analytic_scalar(input, adjoint, -input_value.sin()),
5286 UnaryOp::Log => self.accumulate_analytic_scalar(input, adjoint, 1.0 / input_value),
5287 UnaryOp::PowI(power) => {
5288 let derivative = if power == 0 {
5289 Complex64::ZERO
5290 } else if power == i32::MIN {
5291 power as f64 * output_value / input_value
5292 } else {
5293 power as f64 * input_value.powi(power - 1)
5294 };
5295 self.accumulate_analytic_scalar(input, adjoint, derivative)
5296 }
5297 }
5298 }
5299
5300 fn propagate_binary(
5301 &mut self,
5302 op: BinaryOp,
5303 lhs: ExprId,
5304 rhs: ExprId,
5305 lhs_value: Complex64,
5306 rhs_value: Complex64,
5307 adjoint: ScalarAdjoint,
5308 ) -> RuntimeResult<()> {
5309 match op {
5310 BinaryOp::Add => {
5311 self.accumulate_scalar(lhs, adjoint)?;
5312 self.accumulate_scalar(rhs, adjoint)
5313 }
5314 BinaryOp::Sub => {
5315 self.accumulate_scalar(lhs, adjoint)?;
5316 self.accumulate_scalar(
5317 rhs,
5318 ScalarAdjoint {
5319 dz: -adjoint.dz,
5320 dz_conj: -adjoint.dz_conj,
5321 },
5322 )
5323 }
5324 BinaryOp::Mul => {
5325 self.accumulate_analytic_scalar(lhs, adjoint, rhs_value)?;
5326 self.accumulate_analytic_scalar(rhs, adjoint, lhs_value)
5327 }
5328 BinaryOp::Div => {
5329 self.accumulate_analytic_scalar(lhs, adjoint, 1.0 / rhs_value)?;
5330 self.accumulate_analytic_scalar(rhs, adjoint, -lhs_value / rhs_value.powi(2))
5331 }
5332 BinaryOp::Atan2 => {
5333 let denominator = lhs_value.re.powi(2) + rhs_value.re.powi(2);
5334 let sum = adjoint.dz + adjoint.dz_conj;
5335 self.accumulate_real_linear_scalar(lhs, sum * rhs_value.re / denominator)?;
5336 self.accumulate_real_linear_scalar(rhs, -sum * lhs_value.re / denominator)
5337 }
5338 }
5339 }
5340
5341 fn propagate_matmul(
5342 &mut self,
5343 index: usize,
5344 lhs: ExprId,
5345 rhs: ExprId,
5346 out_rows: usize,
5347 out_cols: usize,
5348 adjoint: &[ScalarAdjoint],
5349 ) -> RuntimeResult<()> {
5350 let (lhs_rows, lhs_cols, lhs_value) = self.primal_matrix(lhs)?;
5351 let (rhs_rows, rhs_cols, rhs_value) = self.primal_matrix(rhs)?;
5352 if lhs_cols != rhs_rows || lhs_rows != out_rows || rhs_cols != out_cols {
5353 return Err(RuntimeError::InvalidShape {
5354 index,
5355 message: format!(
5356 "matmul adjoint has shape {out_rows}x{out_cols} for {lhs_rows}x{lhs_cols} by {rhs_rows}x{rhs_cols}"
5357 ),
5358 });
5359 }
5360 let mut lhs_adjoint = vec![ScalarAdjoint::default(); lhs_rows * lhs_cols];
5361 let mut rhs_adjoint = vec![ScalarAdjoint::default(); rhs_rows * rhs_cols];
5362 for row in 0..lhs_rows {
5363 for col in 0..rhs_cols {
5364 let output_adjoint = adjoint[row * rhs_cols + col];
5365 for mid in 0..lhs_cols {
5366 let rhs_entry = rhs_value[mid * rhs_cols + col];
5367 let lhs_entry = lhs_value[row * lhs_cols + mid];
5368 let lhs_target = &mut lhs_adjoint[row * lhs_cols + mid];
5369 lhs_target.dz += output_adjoint.dz * rhs_entry;
5370 lhs_target.dz_conj += output_adjoint.dz_conj * rhs_entry.conj();
5371 let rhs_target = &mut rhs_adjoint[mid * rhs_cols + col];
5372 rhs_target.dz += output_adjoint.dz * lhs_entry;
5373 rhs_target.dz_conj += output_adjoint.dz_conj * lhs_entry.conj();
5374 }
5375 }
5376 }
5377 self.accumulate_matrix(lhs, lhs_rows, lhs_cols, lhs_adjoint)?;
5378 self.accumulate_matrix(rhs, rhs_rows, rhs_cols, rhs_adjoint)
5379 }
5380
5381 fn propagate_matvec(
5382 &mut self,
5383 index: usize,
5384 matrix: ExprId,
5385 vector: ExprId,
5386 adjoint: &[ScalarAdjoint],
5387 ) -> RuntimeResult<()> {
5388 let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
5389 let vector_value = self.primal_vector(vector)?;
5390 if cols != vector_value.len() || rows != adjoint.len() {
5391 return Err(RuntimeError::InvalidShape {
5392 index,
5393 message: format!(
5394 "matvec adjoint has len {}, expected {rows} for matrix {rows}x{cols} and vector len {}",
5395 adjoint.len(),
5396 vector_value.len()
5397 ),
5398 });
5399 }
5400 let mut matrix_adjoint = vec![ScalarAdjoint::default(); rows * cols];
5401 let mut vector_adjoint = vec![ScalarAdjoint::default(); cols];
5402 for row in 0..rows {
5403 let output_adjoint = adjoint[row];
5404 for col in 0..cols {
5405 let vector_entry = vector_value[col];
5406 let matrix_entry = matrix_value[row * cols + col];
5407 let matrix_target = &mut matrix_adjoint[row * cols + col];
5408 matrix_target.dz += output_adjoint.dz * vector_entry;
5409 matrix_target.dz_conj += output_adjoint.dz_conj * vector_entry.conj();
5410 vector_adjoint[col].dz += output_adjoint.dz * matrix_entry;
5411 vector_adjoint[col].dz_conj += output_adjoint.dz_conj * matrix_entry.conj();
5412 }
5413 }
5414 self.accumulate_matrix(matrix, rows, cols, matrix_adjoint)?;
5415 self.accumulate_vector(vector, vector_adjoint)
5416 }
5417
5418 fn propagate_dot(
5419 &mut self,
5420 index: usize,
5421 lhs: ExprId,
5422 rhs: ExprId,
5423 adjoint: ScalarAdjoint,
5424 ) -> RuntimeResult<()> {
5425 let lhs_value = self.primal_vector(lhs)?;
5426 let rhs_value = self.primal_vector(rhs)?;
5427 if lhs_value.len() != rhs_value.len() {
5428 return Err(RuntimeError::InvalidShape {
5429 index,
5430 message: format!(
5431 "cannot dot len {} vector with len {} vector",
5432 lhs_value.len(),
5433 rhs_value.len()
5434 ),
5435 });
5436 }
5437 let lhs_adjoint = rhs_value
5438 .iter()
5439 .map(|value| ScalarAdjoint {
5440 dz: adjoint.dz * value,
5441 dz_conj: adjoint.dz_conj * value.conj(),
5442 })
5443 .collect();
5444 let rhs_adjoint = lhs_value
5445 .iter()
5446 .map(|value| ScalarAdjoint {
5447 dz: adjoint.dz * value,
5448 dz_conj: adjoint.dz_conj * value.conj(),
5449 })
5450 .collect();
5451 self.accumulate_vector(lhs, lhs_adjoint)?;
5452 self.accumulate_vector(rhs, rhs_adjoint)
5453 }
5454
5455 fn propagate_solve(
5456 &mut self,
5457 index: usize,
5458 matrix: ExprId,
5459 rhs: ExprId,
5460 adjoint: &[ScalarAdjoint],
5461 ) -> RuntimeResult<()> {
5462 let (rows, cols, matrix_value) = self.primal_matrix(matrix)?;
5463 let rhs_value = self.primal_vector(rhs)?;
5464 let solution = self.primal_vector(ExprId::from_index(index))?;
5465 if rows != cols || rows != rhs_value.len() || rows != adjoint.len() {
5466 return Err(RuntimeError::InvalidShape {
5467 index,
5468 message: format!(
5469 "solve adjoint has len {}, expected {rows} for {rows}x{cols} solve",
5470 adjoint.len()
5471 ),
5472 });
5473 }
5474 let matrix_value = DMatrix::from_row_slice(rows, cols, matrix_value);
5475 let transposed = matrix_value.transpose();
5476 let conjugate_transposed = matrix_value.map(|value| value.conj()).transpose();
5477 let alpha = DVector::from_iterator(rows, adjoint.iter().map(|adjoint| adjoint.dz));
5478 let beta = DVector::from_iterator(rows, adjoint.iter().map(|adjoint| adjoint.dz_conj));
5479 let lambda = transposed
5480 .lu()
5481 .solve(&alpha)
5482 .ok_or(RuntimeError::SingularMatrix(index))?;
5483 let lambda_conj = conjugate_transposed
5484 .lu()
5485 .solve(&beta)
5486 .ok_or(RuntimeError::SingularMatrix(index))?;
5487 let solution = DVector::from_row_slice(solution);
5488 let mut matrix_adjoint = vec![ScalarAdjoint::default(); rows * cols];
5489 for row in 0..rows {
5490 for col in 0..cols {
5491 matrix_adjoint[row * cols + col].dz -= lambda[row] * solution[col];
5492 matrix_adjoint[row * cols + col].dz_conj -= lambda_conj[row] * solution[col].conj();
5493 }
5494 }
5495 let rhs_adjoint = (0..rows)
5496 .map(|row| ScalarAdjoint {
5497 dz: lambda[row],
5498 dz_conj: lambda_conj[row],
5499 })
5500 .collect();
5501 self.accumulate_matrix(matrix, rows, cols, matrix_adjoint)?;
5502 self.accumulate_vector(rhs, rhs_adjoint)
5503 }
5504
5505 fn accumulate_analytic_scalar(
5506 &mut self,
5507 id: ExprId,
5508 adjoint: ScalarAdjoint,
5509 derivative: Complex64,
5510 ) -> RuntimeResult<()> {
5511 self.accumulate_scalar(
5512 id,
5513 ScalarAdjoint {
5514 dz: adjoint.dz * derivative,
5515 dz_conj: adjoint.dz_conj * derivative.conj(),
5516 },
5517 )
5518 }
5519
5520 fn accumulate_real_linear_scalar(
5521 &mut self,
5522 id: ExprId,
5523 contribution: Complex64,
5524 ) -> RuntimeResult<()> {
5525 self.accumulate_scalar(
5526 id,
5527 ScalarAdjoint {
5528 dz: contribution * 0.5,
5529 dz_conj: contribution * 0.5,
5530 },
5531 )
5532 }
5533
5534 fn accumulate_solve_rhs_adjoint(
5535 &mut self,
5536 rhs: ExprId,
5537 len: usize,
5538 contributions: Vec<ScalarAdjoint>,
5539 ) -> RuntimeResult<()> {
5540 if let Some(elements) = &self.plan.solve_rhs_elements[rhs.index()] {
5541 if elements.len() != len || elements.len() != contributions.len() {
5542 return Err(RuntimeError::InvalidShape {
5543 index: rhs.index(),
5544 message: format!(
5545 "specialized solve expected {len} RHS elements, got {}",
5546 elements.len()
5547 ),
5548 });
5549 }
5550 for (element, contribution) in elements.iter().copied().zip(contributions) {
5551 self.accumulate_scalar(element, contribution)?;
5552 }
5553 Ok(())
5554 } else {
5555 self.accumulate_vector(rhs, contributions)
5556 }
5557 }
5558
5559 fn accumulate_scalar(&mut self, id: ExprId, contribution: ScalarAdjoint) -> RuntimeResult<()> {
5560 match &mut self.adjoints[id.index()] {
5561 Some(ReverseAdjoint::Scalar(adjoint)) => {
5562 adjoint.dz += contribution.dz;
5563 adjoint.dz_conj += contribution.dz_conj;
5564 }
5565 Some(value) => {
5566 return Err(RuntimeError::TypeMismatch {
5567 index: id.index(),
5568 expected: "scalar adjoint",
5569 actual: value.kind(),
5570 });
5571 }
5572 None => {
5573 self.adjoints[id.index()] = Some(ReverseAdjoint::Scalar(contribution));
5574 }
5575 }
5576 Ok(())
5577 }
5578
5579 fn accumulate_vector(
5580 &mut self,
5581 id: ExprId,
5582 contributions: Vec<ScalarAdjoint>,
5583 ) -> RuntimeResult<()> {
5584 match &mut self.adjoints[id.index()] {
5585 Some(ReverseAdjoint::Vector(adjoint)) if adjoint.len() == contributions.len() => {
5586 for (target, source) in adjoint.iter_mut().zip(contributions) {
5587 target.dz += source.dz;
5588 target.dz_conj += source.dz_conj;
5589 }
5590 }
5591 Some(ReverseAdjoint::Vector(adjoint)) => {
5592 return Err(RuntimeError::InvalidShape {
5593 index: id.index(),
5594 message: format!(
5595 "vector adjoint has len {}, expected {}",
5596 adjoint.len(),
5597 contributions.len()
5598 ),
5599 });
5600 }
5601 Some(value) => {
5602 return Err(RuntimeError::TypeMismatch {
5603 index: id.index(),
5604 expected: "vector adjoint",
5605 actual: value.kind(),
5606 });
5607 }
5608 None => {
5609 self.adjoints[id.index()] = Some(ReverseAdjoint::Vector(contributions));
5610 }
5611 }
5612 Ok(())
5613 }
5614
5615 fn accumulate_vector_element(
5616 &mut self,
5617 id: ExprId,
5618 element: usize,
5619 contribution: ScalarAdjoint,
5620 ) -> RuntimeResult<()> {
5621 let len = self.primal_vector(id)?.len();
5622 if element >= len {
5623 return Err(RuntimeError::InvalidShape {
5624 index: id.index(),
5625 message: format!("component index {element} out of bounds for len {len}"),
5626 });
5627 }
5628 let mut contributions = vec![ScalarAdjoint::default(); len];
5629 contributions[element] = contribution;
5630 self.accumulate_vector(id, contributions)
5631 }
5632
5633 fn accumulate_matrix(
5634 &mut self,
5635 id: ExprId,
5636 rows: usize,
5637 cols: usize,
5638 contributions: Vec<ScalarAdjoint>,
5639 ) -> RuntimeResult<()> {
5640 match &mut self.adjoints[id.index()] {
5641 Some(ReverseAdjoint::Matrix {
5642 rows: actual_rows,
5643 cols: actual_cols,
5644 values,
5645 }) if *actual_rows == rows
5646 && *actual_cols == cols
5647 && values.len() == contributions.len() =>
5648 {
5649 for (target, source) in values.iter_mut().zip(contributions) {
5650 target.dz += source.dz;
5651 target.dz_conj += source.dz_conj;
5652 }
5653 }
5654 Some(ReverseAdjoint::Matrix {
5655 rows: actual_rows,
5656 cols: actual_cols,
5657 ..
5658 }) => {
5659 return Err(RuntimeError::InvalidShape {
5660 index: id.index(),
5661 message: format!(
5662 "matrix adjoint has shape {actual_rows}x{actual_cols}, expected {rows}x{cols}"
5663 ),
5664 });
5665 }
5666 Some(value) => {
5667 return Err(RuntimeError::TypeMismatch {
5668 index: id.index(),
5669 expected: "matrix adjoint",
5670 actual: value.kind(),
5671 });
5672 }
5673 None => {
5674 self.adjoints[id.index()] = Some(ReverseAdjoint::Matrix {
5675 rows,
5676 cols,
5677 values: contributions,
5678 });
5679 }
5680 }
5681 Ok(())
5682 }
5683
5684 fn accumulate_matrix_element(
5685 &mut self,
5686 id: ExprId,
5687 rows: usize,
5688 cols: usize,
5689 row: usize,
5690 col: usize,
5691 contribution: ScalarAdjoint,
5692 ) -> RuntimeResult<()> {
5693 let mut contributions = vec![ScalarAdjoint::default(); rows * cols];
5694 contributions[row * cols + col] = contribution;
5695 self.accumulate_matrix(id, rows, cols, contributions)
5696 }
5697
5698 fn scalar_adjoint_at(&self, index: usize) -> RuntimeResult<Option<ScalarAdjoint>> {
5699 match &self.adjoints[index] {
5700 Some(ReverseAdjoint::Scalar(adjoint)) => Ok(Some(*adjoint)),
5701 Some(value) => Err(RuntimeError::TypeMismatch {
5702 index,
5703 expected: "scalar adjoint",
5704 actual: value.kind(),
5705 }),
5706 None => Ok(None),
5707 }
5708 }
5709
5710 fn expect_scalar_adjoint(
5711 index: usize,
5712 adjoint: ReverseAdjoint,
5713 ) -> RuntimeResult<ScalarAdjoint> {
5714 match adjoint {
5715 ReverseAdjoint::Scalar(adjoint) => Ok(adjoint),
5716 value => Err(RuntimeError::TypeMismatch {
5717 index,
5718 expected: "scalar adjoint",
5719 actual: value.kind(),
5720 }),
5721 }
5722 }
5723
5724 fn expect_vector_adjoint(
5725 index: usize,
5726 adjoint: ReverseAdjoint,
5727 ) -> RuntimeResult<Vec<ScalarAdjoint>> {
5728 match adjoint {
5729 ReverseAdjoint::Vector(adjoint) => Ok(adjoint),
5730 value => Err(RuntimeError::TypeMismatch {
5731 index,
5732 expected: "vector adjoint",
5733 actual: value.kind(),
5734 }),
5735 }
5736 }
5737
5738 fn expect_matrix_adjoint(
5739 index: usize,
5740 adjoint: ReverseAdjoint,
5741 ) -> RuntimeResult<(usize, usize, Vec<ScalarAdjoint>)> {
5742 match adjoint {
5743 ReverseAdjoint::Matrix { rows, cols, values } => Ok((rows, cols, values)),
5744 value => Err(RuntimeError::TypeMismatch {
5745 index,
5746 expected: "matrix adjoint",
5747 actual: value.kind(),
5748 }),
5749 }
5750 }
5751
5752 fn primal_scalar(&self, id: ExprId) -> RuntimeResult<Complex64> {
5753 if self.cached_factors.is_some() {
5754 self.plan.cached_scalar_at(self.primals, id)
5755 } else {
5756 scalar_at(self.primals, id.index())
5757 }
5758 }
5759
5760 fn primal_vector(&self, id: ExprId) -> RuntimeResult<&[Complex64]> {
5761 if self.cached_factors.is_some() {
5762 self.plan.cached_vector_at(self.primals, id)
5763 } else {
5764 vector_at(self.primals, id.index())
5765 }
5766 }
5767
5768 fn primal_matrix(&self, id: ExprId) -> RuntimeResult<(usize, usize, &[Complex64])> {
5769 if self.cached_factors.is_some() {
5770 self.plan.cached_matrix_at(self.primals, id)
5771 } else {
5772 matrix_at(self.primals, id.index())
5773 }
5774 }
5775}
5776
5777#[derive(Copy, Clone, Debug, PartialEq, Eq)]
5778enum EventColumn {
5779 Scalar(usize),
5780 P4Component { col: usize, component: P4Component },
5781}
5782
5783#[derive(Clone, Debug)]
5785pub struct CpuBatchCache {
5786 len: usize,
5787 weights: Vec<f64>,
5788 sum_weights: f64,
5789 nodes: Vec<ExprId>,
5790 pub(crate) slots: Vec<CachedSlot>,
5791 factor_nodes: Vec<ExprId>,
5792 factor_slots: Vec<CachedFactorSlot>,
5793 solve_row_keys: Vec<(ExprId, usize, usize)>,
5794 pub(crate) solve_row_slots: Vec<CachedSolveRowSlot>,
5795}
5796
5797impl CpuBatchCache {
5798 fn new(
5799 cache_plan: &CachePlan,
5800 factor_matrices: &[(ExprId, usize)],
5801 solve_row_keys: &[(ExprId, usize, usize)],
5802 len: usize,
5803 ) -> Self {
5804 Self {
5805 len,
5806 weights: vec![1.0; len],
5807 sum_weights: len as f64,
5808 nodes: cache_plan
5809 .entries()
5810 .iter()
5811 .map(|entry| entry.node())
5812 .collect(),
5813 slots: cache_plan
5814 .entries()
5815 .iter()
5816 .map(|entry| CachedSlot::new(entry.value_kind(), len))
5817 .collect(),
5818 factor_nodes: factor_matrices.iter().map(|(node, _)| *node).collect(),
5819 factor_slots: factor_matrices
5820 .iter()
5821 .map(|(_, dimension)| CachedFactorSlot::new(*dimension))
5822 .collect(),
5823 solve_row_keys: solve_row_keys.to_vec(),
5824 solve_row_slots: solve_row_keys
5825 .iter()
5826 .map(|(_, _, dimension)| CachedSolveRowSlot::new(*dimension))
5827 .collect(),
5828 }
5829 }
5830
5831 pub fn len(&self) -> usize {
5833 self.len
5834 }
5835
5836 pub fn is_empty(&self) -> bool {
5838 self.len == 0
5839 }
5840
5841 pub fn weights(&self) -> &[f64] {
5843 &self.weights
5844 }
5845
5846 pub fn sum_weights(&self) -> f64 {
5848 self.sum_weights
5849 }
5850
5851 pub fn resident_bytes(&self) -> usize {
5853 self.weights.capacity() * size_of::<f64>()
5854 + self.nodes.capacity() * size_of::<ExprId>()
5855 + self
5856 .slots
5857 .iter()
5858 .map(CachedSlot::resident_bytes)
5859 .sum::<usize>()
5860 + self.factor_nodes.capacity() * size_of::<ExprId>()
5861 + self
5862 .factor_slots
5863 .iter()
5864 .map(CachedFactorSlot::resident_bytes)
5865 .sum::<usize>()
5866 + self.solve_row_keys.capacity() * size_of::<(ExprId, usize, usize)>()
5867 + self
5868 .solve_row_slots
5869 .iter()
5870 .map(CachedSolveRowSlot::resident_bytes)
5871 .sum::<usize>()
5872 }
5873
5874 fn set_weights(&mut self, weights: Vec<f64>) {
5875 self.sum_weights = weights.iter().sum();
5876 self.weights = weights;
5877 }
5878
5879 fn push(&mut self, slot: usize, value: Value) -> RuntimeResult<()> {
5880 let len = self.slots.len();
5881 self.slots
5882 .get_mut(slot)
5883 .ok_or(RuntimeError::InvalidCache {
5884 expected: len,
5885 actual: slot + 1,
5886 })?
5887 .push(value)
5888 }
5889
5890 fn value(&self, slot: usize, row: usize) -> RuntimeResult<Value> {
5891 if row >= self.len {
5892 return Err(RuntimeError::InvalidShape {
5893 index: row,
5894 message: format!("cache row {row} out of bounds for len {}", self.len),
5895 });
5896 }
5897 self.slots
5898 .get(slot)
5899 .ok_or(RuntimeError::InvalidCache {
5900 expected: self.slots.len(),
5901 actual: slot + 1,
5902 })?
5903 .value(row)
5904 }
5905
5906 fn scalar(&self, slot: usize, row: usize) -> RuntimeResult<Complex64> {
5907 if row >= self.len {
5908 return Err(RuntimeError::InvalidShape {
5909 index: row,
5910 message: format!("cache row {row} out of bounds for len {}", self.len),
5911 });
5912 }
5913 self.slots
5914 .get(slot)
5915 .ok_or(RuntimeError::InvalidCache {
5916 expected: self.slots.len(),
5917 actual: slot + 1,
5918 })?
5919 .scalar(row)
5920 }
5921
5922 fn real_range(&self, slot: usize, start: usize, end: usize) -> RuntimeResult<&[f64]> {
5923 if start > end || end > self.len {
5924 return Err(RuntimeError::InvalidShape {
5925 index: start,
5926 message: format!(
5927 "cache range {start}..{end} out of bounds for len {}",
5928 self.len
5929 ),
5930 });
5931 }
5932 self.slots
5933 .get(slot)
5934 .ok_or(RuntimeError::InvalidCache {
5935 expected: self.slots.len(),
5936 actual: slot + 1,
5937 })?
5938 .real_range(start, end)
5939 }
5940
5941 fn complex_range(&self, slot: usize, start: usize, end: usize) -> RuntimeResult<&[Complex64]> {
5942 if start > end || end > self.len {
5943 return Err(RuntimeError::InvalidShape {
5944 index: start,
5945 message: format!(
5946 "cache range {start}..{end} out of bounds for len {}",
5947 self.len
5948 ),
5949 });
5950 }
5951 self.slots
5952 .get(slot)
5953 .ok_or(RuntimeError::InvalidCache {
5954 expected: self.slots.len(),
5955 actual: slot + 1,
5956 })?
5957 .complex_range(start, end)
5958 }
5959
5960 fn push_factor(&mut self, slot: usize, factor: DynamicLu) -> RuntimeResult<()> {
5961 let len = self.factor_slots.len();
5962 self.factor_slots
5963 .get_mut(slot)
5964 .ok_or(RuntimeError::InvalidCache {
5965 expected: len,
5966 actual: slot + 1,
5967 })?
5968 .push(factor)
5969 }
5970
5971 fn factor(&self, slot: usize, row: usize) -> RuntimeResult<&DynamicLu> {
5972 self.factor_slots
5973 .get(slot)
5974 .ok_or(RuntimeError::InvalidCache {
5975 expected: self.factor_slots.len(),
5976 actual: slot + 1,
5977 })?
5978 .factor(row)
5979 }
5980
5981 fn push_solve_row(
5982 &mut self,
5983 slot: usize,
5984 values: impl IntoIterator<Item = Complex64>,
5985 ) -> RuntimeResult<()> {
5986 let len = self.solve_row_slots.len();
5987 self.solve_row_slots
5988 .get_mut(slot)
5989 .ok_or(RuntimeError::InvalidCache {
5990 expected: len,
5991 actual: slot + 1,
5992 })?
5993 .push(values)
5994 }
5995
5996 fn solve_row(&self, slot: usize, row: usize) -> RuntimeResult<&[Complex64]> {
5997 self.solve_row_slots
5998 .get(slot)
5999 .ok_or(RuntimeError::InvalidCache {
6000 expected: self.solve_row_slots.len(),
6001 actual: slot + 1,
6002 })?
6003 .row(row)
6004 }
6005}
6006
6007#[derive(Clone, Debug)]
6009pub struct CpuCachedBatch {
6010 cache: CpuBatchCache,
6011}
6012
6013impl CpuCachedBatch {
6014 pub fn cache(&self) -> &CpuBatchCache {
6016 &self.cache
6017 }
6018
6019 pub fn len(&self) -> usize {
6021 self.cache.len()
6022 }
6023
6024 pub fn is_empty(&self) -> bool {
6026 self.cache.is_empty()
6027 }
6028
6029 pub fn weights(&self) -> &[f64] {
6031 self.cache.weights()
6032 }
6033
6034 pub fn sum_weights(&self) -> f64 {
6036 self.cache.sum_weights()
6037 }
6038
6039 pub fn resident_bytes(&self) -> usize {
6041 self.cache.resident_bytes()
6042 }
6043}
6044
6045#[derive(Clone, Debug, Default)]
6047pub struct CpuCachedDataset {
6048 batches: Vec<CpuCachedBatch>,
6049 sum_weights: f64,
6050}
6051
6052#[derive(Copy, Clone, Debug, PartialEq)]
6053pub struct PreparedDatasetStats {
6055 local_events: usize,
6056 global_events: usize,
6057 local_batches: usize,
6058 sum_weights: f64,
6059 resident_bytes: usize,
6060 storage: CacheStorage,
6061}
6062
6063impl PreparedDatasetStats {
6064 pub(crate) fn new(
6065 local_events: usize,
6066 global_events: usize,
6067 local_batches: usize,
6068 sum_weights: f64,
6069 resident_bytes: usize,
6070 storage: CacheStorage,
6071 ) -> Self {
6072 Self {
6073 local_events,
6074 global_events,
6075 local_batches,
6076 sum_weights,
6077 resident_bytes,
6078 storage,
6079 }
6080 }
6081
6082 pub fn local_events(&self) -> usize {
6084 self.local_events
6085 }
6086
6087 pub fn global_events(&self) -> usize {
6089 self.global_events
6090 }
6091
6092 pub fn local_batches(&self) -> usize {
6094 self.local_batches
6095 }
6096
6097 pub fn sum_weights(&self) -> f64 {
6099 self.sum_weights
6100 }
6101
6102 pub fn resident_bytes(&self) -> usize {
6104 self.resident_bytes
6105 }
6106
6107 pub fn storage(&self) -> CacheStorage {
6109 self.storage
6110 }
6111}
6112
6113#[derive(Clone)]
6114pub enum CpuPreparedDataset {
6119 Resident {
6121 dataset: Arc<CpuCachedDataset>,
6123 stats: PreparedDatasetStats,
6125 memory_lease: MemoryLease,
6127 },
6128 Streaming {
6130 dataset: Dataset,
6132 read_plan: laddu_data::io::ReadPlan,
6134 stats: PreparedDatasetStats,
6136 transient_bytes: u64,
6138 },
6139}
6140
6141impl std::fmt::Debug for CpuPreparedDataset {
6142 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6143 formatter
6144 .debug_struct("CpuPreparedDataset")
6145 .field("stats", self.stats())
6146 .finish_non_exhaustive()
6147 }
6148}
6149
6150impl CpuPreparedDataset {
6151 pub fn stats(&self) -> &PreparedDatasetStats {
6153 match self {
6154 Self::Resident { stats, .. } | Self::Streaming { stats, .. } => stats,
6155 }
6156 }
6157}
6158
6159impl CpuCachedDataset {
6160 pub fn batches(&self) -> &[CpuCachedBatch] {
6162 &self.batches
6163 }
6164
6165 pub fn len(&self) -> usize {
6167 self.batches.iter().map(CpuCachedBatch::len).sum()
6168 }
6169
6170 pub fn is_empty(&self) -> bool {
6172 self.batches.iter().all(CpuCachedBatch::is_empty)
6173 }
6174
6175 pub fn sum_weights(&self) -> f64 {
6177 self.sum_weights
6178 }
6179
6180 pub fn resident_bytes(&self) -> usize {
6182 self.batches
6183 .iter()
6184 .map(CpuCachedBatch::resident_bytes)
6185 .sum()
6186 }
6187}
6188
6189#[derive(Clone, Debug)]
6190struct CachedFactorSlot {
6191 dimension: usize,
6192 factors: Vec<DynamicLu>,
6193}
6194
6195#[derive(Clone, Debug)]
6196pub(crate) struct CachedSolveRowSlot {
6197 pub(crate) dimension: usize,
6198 pub(crate) values: Vec<Complex64>,
6199}
6200
6201impl CachedSolveRowSlot {
6202 fn new(dimension: usize) -> Self {
6203 Self {
6204 dimension,
6205 values: Vec::new(),
6206 }
6207 }
6208
6209 fn push(&mut self, values: impl IntoIterator<Item = Complex64>) -> RuntimeResult<()> {
6210 let start = self.values.len();
6211 self.values.extend(values);
6212 let actual = self.values.len() - start;
6213 if actual != self.dimension {
6214 return Err(RuntimeError::InvalidShape {
6215 index: start / self.dimension,
6216 message: format!(
6217 "cached solve row has len {actual}, expected {}",
6218 self.dimension
6219 ),
6220 });
6221 }
6222 Ok(())
6223 }
6224
6225 fn row(&self, row: usize) -> RuntimeResult<&[Complex64]> {
6226 let start = row
6227 .checked_mul(self.dimension)
6228 .ok_or_else(|| RuntimeError::InvalidShape {
6229 index: row,
6230 message: "cached solve row offset overflowed".into(),
6231 })?;
6232 self.values
6233 .get(start..start + self.dimension)
6234 .ok_or_else(|| RuntimeError::InvalidShape {
6235 index: row,
6236 message: format!(
6237 "cached solve row {row} out of bounds for len {}",
6238 self.values.len() / self.dimension
6239 ),
6240 })
6241 }
6242
6243 fn resident_bytes(&self) -> usize {
6244 self.values.capacity() * size_of::<Complex64>()
6245 }
6246}
6247
6248impl CachedFactorSlot {
6249 fn new(dimension: usize) -> Self {
6250 Self {
6251 dimension,
6252 factors: Vec::new(),
6253 }
6254 }
6255
6256 fn push(&mut self, factor: DynamicLu) -> RuntimeResult<()> {
6257 self.factors.push(factor);
6258 Ok(())
6259 }
6260
6261 fn factor(&self, row: usize) -> RuntimeResult<&DynamicLu> {
6262 self.factors
6263 .get(row)
6264 .ok_or_else(|| RuntimeError::InvalidShape {
6265 index: row,
6266 message: format!(
6267 "factor row {row} out of bounds for len {}",
6268 self.factors.len()
6269 ),
6270 })
6271 }
6272
6273 fn resident_bytes(&self) -> usize {
6274 self.factors.capacity()
6275 * (self.dimension * self.dimension * size_of::<Complex64>()
6276 + self.dimension * size_of::<usize>())
6277 }
6278}
6279
6280#[derive(Clone, Debug, PartialEq)]
6281pub(crate) enum CachedSlot {
6282 Real(Vec<f64>),
6283 Complex(Vec<Complex64>),
6284 Vector {
6285 len: usize,
6286 values: Vec<Complex64>,
6287 },
6288 Matrix {
6289 rows: usize,
6290 cols: usize,
6291 values: Vec<Complex64>,
6292 },
6293}
6294
6295impl CachedSlot {
6296 #[cfg(feature = "jit")]
6297 pub(crate) fn values_ptr(&self) -> *const u8 {
6298 match self {
6299 Self::Real(values) => values.as_ptr().cast(),
6300 Self::Complex(values) | Self::Vector { values, .. } | Self::Matrix { values, .. } => {
6301 values.as_ptr().cast()
6302 }
6303 }
6304 }
6305
6306 #[cfg(feature = "jit")]
6307 pub(crate) fn width(&self) -> usize {
6308 match self {
6309 Self::Real(_) | Self::Complex(_) => 1,
6310 Self::Vector { len, .. } => *len,
6311 Self::Matrix { rows, cols, .. } => rows * cols,
6312 }
6313 }
6314
6315 fn new(kind: ValueKind, events: usize) -> Self {
6316 match kind {
6317 ValueKind::Real => Self::Real(Vec::with_capacity(events)),
6318 ValueKind::Complex => Self::Complex(Vec::with_capacity(events)),
6319 ValueKind::Vector { len } => Self::Vector {
6320 len,
6321 values: Vec::with_capacity(events.saturating_mul(len)),
6322 },
6323 ValueKind::Matrix { rows, cols } => Self::Matrix {
6324 rows,
6325 cols,
6326 values: Vec::with_capacity(events.saturating_mul(rows).saturating_mul(cols)),
6327 },
6328 }
6329 }
6330
6331 fn resident_bytes(&self) -> usize {
6332 match self {
6333 Self::Real(values) => values.capacity() * size_of::<f64>(),
6334 Self::Complex(values) => values.capacity() * size_of::<Complex64>(),
6335 Self::Vector { values, .. } | Self::Matrix { values, .. } => {
6336 values.capacity() * size_of::<Complex64>()
6337 }
6338 }
6339 }
6340
6341 fn push(&mut self, value: Value) -> RuntimeResult<()> {
6342 match (self, value) {
6343 (Self::Real(values), Value::Scalar(value)) => {
6344 values.push(value.re);
6345 Ok(())
6346 }
6347 (Self::Complex(values), Value::Scalar(value)) => {
6348 values.push(value);
6349 Ok(())
6350 }
6351 (Self::Vector { len, values }, Value::Vector(value)) if *len == value.len() => {
6352 values.extend(value);
6353 Ok(())
6354 }
6355 (
6356 Self::Matrix { rows, cols, values },
6357 Value::Matrix {
6358 rows: value_rows,
6359 cols: value_cols,
6360 values: value,
6361 },
6362 ) if *rows == value_rows && *cols == value_cols => {
6363 values.extend(value);
6364 Ok(())
6365 }
6366 (_, value) => Err(RuntimeError::InvalidShape {
6367 index: 0,
6368 message: format!("cached value kind did not match slot: {}", value.kind()),
6369 }),
6370 }
6371 }
6372
6373 fn value(&self, row: usize) -> RuntimeResult<Value> {
6374 match self {
6375 Self::Real(values) => values
6376 .get(row)
6377 .copied()
6378 .map(Complex64::from)
6379 .map(Value::Scalar)
6380 .ok_or_else(|| RuntimeError::InvalidShape {
6381 index: row,
6382 message: format!("cache row {row} out of bounds"),
6383 }),
6384 Self::Complex(values) => values.get(row).copied().map(Value::Scalar).ok_or_else(|| {
6385 RuntimeError::InvalidShape {
6386 index: row,
6387 message: format!("cache row {row} out of bounds"),
6388 }
6389 }),
6390 Self::Vector { len, values } => {
6391 let start = row
6392 .checked_mul(*len)
6393 .ok_or_else(|| RuntimeError::InvalidShape {
6394 index: row,
6395 message: "cache vector row offset overflowed".into(),
6396 })?;
6397 let end = start + *len;
6398 values
6399 .get(start..end)
6400 .map(|value| Value::Vector(value.to_vec()))
6401 .ok_or_else(|| RuntimeError::InvalidShape {
6402 index: row,
6403 message: format!("cache row {row} out of bounds"),
6404 })
6405 }
6406 Self::Matrix { rows, cols, values } => {
6407 let len = rows * cols;
6408 let start = row
6409 .checked_mul(len)
6410 .ok_or_else(|| RuntimeError::InvalidShape {
6411 index: row,
6412 message: "cache matrix row offset overflowed".into(),
6413 })?;
6414 let end = start + len;
6415 values
6416 .get(start..end)
6417 .map(|value| Value::Matrix {
6418 rows: *rows,
6419 cols: *cols,
6420 values: value.to_vec(),
6421 })
6422 .ok_or_else(|| RuntimeError::InvalidShape {
6423 index: row,
6424 message: format!("cache row {row} out of bounds"),
6425 })
6426 }
6427 }
6428 }
6429
6430 fn scalar(&self, row: usize) -> RuntimeResult<Complex64> {
6431 match self {
6432 Self::Real(values) => values
6433 .get(row)
6434 .copied()
6435 .map(Complex64::from)
6436 .ok_or_else(|| RuntimeError::InvalidShape {
6437 index: row,
6438 message: format!("cache row {row} out of bounds"),
6439 }),
6440 Self::Complex(values) => {
6441 values
6442 .get(row)
6443 .copied()
6444 .ok_or_else(|| RuntimeError::InvalidShape {
6445 index: row,
6446 message: format!("cache row {row} out of bounds"),
6447 })
6448 }
6449 Self::Vector { .. } | Self::Matrix { .. } => Err(RuntimeError::TypeMismatch {
6450 index: row,
6451 expected: "scalar",
6452 actual: match self {
6453 Self::Vector { .. } => "vector",
6454 Self::Matrix { .. } => "matrix",
6455 Self::Real(_) | Self::Complex(_) => unreachable!(),
6456 },
6457 }),
6458 }
6459 }
6460
6461 fn real_range(&self, start: usize, end: usize) -> RuntimeResult<&[f64]> {
6462 match self {
6463 Self::Real(values) => {
6464 values
6465 .get(start..end)
6466 .ok_or_else(|| RuntimeError::InvalidShape {
6467 index: start,
6468 message: format!("cache range {start}..{end} out of bounds"),
6469 })
6470 }
6471 Self::Complex(_) | Self::Vector { .. } | Self::Matrix { .. } => {
6472 Err(RuntimeError::TypeMismatch {
6473 index: start,
6474 expected: "real scalar",
6475 actual: match self {
6476 Self::Complex(_) => "complex scalar",
6477 Self::Vector { .. } => "vector",
6478 Self::Matrix { .. } => "matrix",
6479 Self::Real(_) => unreachable!(),
6480 },
6481 })
6482 }
6483 }
6484 }
6485
6486 fn complex_range(&self, start: usize, end: usize) -> RuntimeResult<&[Complex64]> {
6487 match self {
6488 Self::Complex(values) => {
6489 values
6490 .get(start..end)
6491 .ok_or_else(|| RuntimeError::InvalidShape {
6492 index: start,
6493 message: format!("cache range {start}..{end} out of bounds"),
6494 })
6495 }
6496 Self::Real(_) | Self::Vector { .. } | Self::Matrix { .. } => {
6497 Err(RuntimeError::TypeMismatch {
6498 index: start,
6499 expected: "complex scalar",
6500 actual: match self {
6501 Self::Real(_) => "real scalar",
6502 Self::Vector { .. } => "vector",
6503 Self::Matrix { .. } => "matrix",
6504 Self::Complex(_) => unreachable!(),
6505 },
6506 })
6507 }
6508 }
6509 }
6510}
6511
6512impl Value {
6513 fn kind(&self) -> &'static str {
6514 match self {
6515 Self::Scalar(_) => "scalar",
6516 Self::Vector(_) => "vector",
6517 Self::Matrix { .. } => "matrix",
6518 }
6519 }
6520}
6521
6522fn f32_scalar_at(values: &[F32Value], id: KernelValueId) -> RuntimeResult<Complex32> {
6523 match &values[id.index()] {
6524 F32Value::Scalar(value) => Ok(*value),
6525 value => Err(RuntimeError::TypeMismatch {
6526 index: id.index(),
6527 expected: "scalar",
6528 actual: value.kind(),
6529 }),
6530 }
6531}
6532
6533fn f32_vector_at(values: &[F32Value], id: KernelValueId) -> RuntimeResult<&[Complex32]> {
6534 match &values[id.index()] {
6535 F32Value::Vector(values) => Ok(values),
6536 value => Err(RuntimeError::TypeMismatch {
6537 index: id.index(),
6538 expected: "vector",
6539 actual: value.kind(),
6540 }),
6541 }
6542}
6543
6544fn f32_matrix_at(
6545 values: &[F32Value],
6546 id: KernelValueId,
6547) -> RuntimeResult<(usize, usize, &[Complex32])> {
6548 match &values[id.index()] {
6549 F32Value::Matrix { rows, cols, values } => Ok((*rows, *cols, values)),
6550 value => Err(RuntimeError::TypeMismatch {
6551 index: id.index(),
6552 expected: "matrix",
6553 actual: value.kind(),
6554 }),
6555 }
6556}
6557
6558fn scalar_at(values: &[Value], index: usize) -> RuntimeResult<Complex64> {
6559 match &values[index] {
6560 Value::Scalar(value) => Ok(*value),
6561 value => Err(RuntimeError::TypeMismatch {
6562 index,
6563 expected: "scalar",
6564 actual: value.kind(),
6565 }),
6566 }
6567}
6568
6569fn vector_at(values: &[Value], index: usize) -> RuntimeResult<&[Complex64]> {
6570 match &values[index] {
6571 Value::Vector(value) => Ok(value),
6572 value => Err(RuntimeError::TypeMismatch {
6573 index,
6574 expected: "vector",
6575 actual: value.kind(),
6576 }),
6577 }
6578}
6579
6580fn matrix_at(values: &[Value], index: usize) -> RuntimeResult<(usize, usize, &[Complex64])> {
6581 match &values[index] {
6582 Value::Matrix { rows, cols, values } => Ok((*rows, *cols, values)),
6583 value => Err(RuntimeError::TypeMismatch {
6584 index,
6585 expected: "matrix",
6586 actual: value.kind(),
6587 }),
6588 }
6589}
6590
6591fn scalar_at_optional(values: &[Option<Value>], index: usize) -> RuntimeResult<Complex64> {
6592 match values.get(index).and_then(Option::as_ref) {
6593 Some(Value::Scalar(value)) => Ok(*value),
6594 Some(value) => Err(RuntimeError::TypeMismatch {
6595 index,
6596 expected: "scalar",
6597 actual: value.kind(),
6598 }),
6599 None => Err(RuntimeError::InvalidShape {
6600 index,
6601 message: "required cache prerequisite was not evaluated".into(),
6602 }),
6603 }
6604}
6605
6606fn vector_at_optional(values: &[Option<Value>], index: usize) -> RuntimeResult<&[Complex64]> {
6607 match values.get(index).and_then(Option::as_ref) {
6608 Some(Value::Vector(value)) => Ok(value),
6609 Some(value) => Err(RuntimeError::TypeMismatch {
6610 index,
6611 expected: "vector",
6612 actual: value.kind(),
6613 }),
6614 None => Err(RuntimeError::InvalidShape {
6615 index,
6616 message: "required cache prerequisite was not evaluated".into(),
6617 }),
6618 }
6619}
6620
6621fn matrix_at_optional(
6622 values: &[Option<Value>],
6623 index: usize,
6624) -> RuntimeResult<(usize, usize, &[Complex64])> {
6625 match values.get(index).and_then(Option::as_ref) {
6626 Some(Value::Matrix { rows, cols, values }) => Ok((*rows, *cols, values)),
6627 Some(value) => Err(RuntimeError::TypeMismatch {
6628 index,
6629 expected: "matrix",
6630 actual: value.kind(),
6631 }),
6632 None => Err(RuntimeError::InvalidShape {
6633 index,
6634 message: "required cache prerequisite was not evaluated".into(),
6635 }),
6636 }
6637}
6638
6639fn matrix_values_row_major(matrix: &DMatrix<Complex64>) -> Vec<Complex64> {
6640 let mut values = Vec::with_capacity(matrix.nrows() * matrix.ncols());
6641 for row in 0..matrix.nrows() {
6642 for col in 0..matrix.ncols() {
6643 values.push(matrix[(row, col)]);
6644 }
6645 }
6646 values
6647}
6648
6649fn matrix_values_row_major_f32(matrix: &DMatrix<Complex32>) -> Vec<Complex32> {
6650 let mut values = Vec::with_capacity(matrix.nrows() * matrix.ncols());
6651 for row in 0..matrix.nrows() {
6652 for col in 0..matrix.ncols() {
6653 values.push(matrix[(row, col)]);
6654 }
6655 }
6656 values
6657}
6658
6659fn eval_unary<T: Float>(op: UnaryOp, input: Complex<T>) -> Complex<T> {
6660 match op {
6661 UnaryOp::Neg => -input,
6662 UnaryOp::Real => Complex::from(input.re),
6663 UnaryOp::Imag => Complex::from(input.im),
6664 UnaryOp::Conj => input.conj(),
6665 UnaryOp::NormSqr => Complex::from(input.norm_sqr()),
6666 UnaryOp::Sqrt => input.sqrt(),
6667 UnaryOp::Exp => input.exp(),
6668 UnaryOp::Sin => input.sin(),
6669 UnaryOp::Cos => input.cos(),
6670 UnaryOp::Log => input.ln(),
6671 UnaryOp::PowI(power) => input.powi(power),
6672 }
6673}
6674
6675fn eval_binary<T: Float>(op: BinaryOp, lhs: Complex<T>, rhs: Complex<T>) -> Complex<T> {
6676 match op {
6677 BinaryOp::Add => lhs + rhs,
6678 BinaryOp::Sub => lhs - rhs,
6679 BinaryOp::Mul => lhs * rhs,
6680 BinaryOp::Div => lhs / rhs,
6681 BinaryOp::Atan2 => Complex::from(lhs.re.atan2(rhs.re)),
6682 }
6683}
6684
6685#[cfg(test)]
6686mod tests {
6687 use std::sync::Arc;
6688
6689 use laddu_compile::{CompileOptions, CompiledModel};
6690 use laddu_data::{
6691 RealVec4,
6692 data::{Dataset, EventBatch, OwnedEvent},
6693 schema::Schema,
6694 };
6695 use laddu_expr::{
6696 P4Component, atan2, complex, dot, event_p4_component, event_scalar, matmul, matrix, matvec,
6697 parameter, polar_complex, solve, vector,
6698 };
6699
6700 use super::*;
6701
6702 #[test]
6703 fn resident_cache_plan_accounts_for_batch_overhead_and_source_prefetch() {
6704 let (resident, chunk) = resident_cache_plan(100, 10, 20, 100, 2_000).unwrap();
6709 let batches = 100_usize.div_ceil(chunk);
6710 assert_eq!(resident, 1_000 + 100 * batches);
6711 assert!(resident + 20 * chunk <= 2_000);
6712 assert!(resident_cache_plan(100, 10, 20, 100, 1_119).is_none());
6713 }
6714
6715 fn evaluate(expr: &laddu_expr::Expr) -> Complex64 {
6716 let model = CompiledModel::from_expr(expr).unwrap();
6717 let params = Arc::new(model.params().clone()).default_values();
6718 CpuBackend.prepare(&model).evaluate(¶ms).unwrap()
6719 }
6720
6721 fn finite_difference(plan: &CpuPlan, params: &ParamValues, parameter: usize) -> Complex64 {
6722 let h = 1.0e-6;
6723 let mut plus = params.clone();
6724 let mut minus = params.clone();
6725 let id = params.layout().free_params()[parameter];
6726 let free_id = params.layout().free_id(id).unwrap().unwrap();
6727 let value = params.get(id).unwrap();
6728 plus.set_free(free_id, value + h).unwrap();
6729 minus.set_free(free_id, value - h).unwrap();
6730 (plan.evaluate(&plus).unwrap() - plan.evaluate(&minus).unwrap()) / (2.0 * h)
6731 }
6732
6733 fn assert_gradient_close(actual: &[Complex64], expected: &[Complex64], tolerance: f64) {
6734 assert_eq!(actual.len(), expected.len());
6735 for (actual, expected) in actual.iter().zip(expected) {
6736 assert!(
6737 (actual - expected).norm() < tolerance,
6738 "{actual} != {expected}"
6739 );
6740 }
6741 }
6742
6743 #[cfg(feature = "jit")]
6744 fn f32_execution(jit: JitPolicy) -> Execution {
6745 Execution::local(crate::ExecutionOptions {
6746 device: crate::Device::Cpu(crate::CpuOptions {
6747 jit,
6748 ..crate::CpuOptions::default()
6749 }),
6750 precision: Precision::F32,
6751 ..crate::ExecutionOptions::default()
6752 })
6753 .unwrap()
6754 }
6755
6756 #[cfg(feature = "jit")]
6757 fn f32_jit_and_interpreter(model: &CompiledModel) -> (CpuPlan, CpuPlan) {
6758 let automatic = CpuBackend
6759 .prepare_for_execution(model, &f32_execution(JitPolicy::Auto))
6760 .unwrap();
6761 let interpreted = CpuBackend
6762 .prepare_for_execution(model, &f32_execution(JitPolicy::Disabled))
6763 .unwrap();
6764
6765 let Some(ScalarExecutor::Jit(kernel)) = &automatic.scalar_executor else {
6766 panic!("f32 auto execution should select scalar JIT");
6767 };
6768 assert_eq!(kernel.precision(), JitPrecision::F32);
6769 let GradientExecutor::Jit(kernel) = &automatic.gradient_executor else {
6770 panic!("f32 auto execution should select gradient JIT");
6771 };
6772 assert_eq!(kernel.precision(), JitPrecision::F32);
6773 if interpreted.scalar_executor.is_some() {
6774 assert!(matches!(
6775 interpreted.scalar_executor,
6776 Some(ScalarExecutor::Interpreter(_))
6777 ));
6778 }
6779 (automatic, interpreted)
6780 }
6781
6782 #[cfg(feature = "jit")]
6783 fn f64_jit_and_interpreter(model: &CompiledModel) -> (CpuPlan, CpuPlan) {
6784 let automatic = CpuBackend.prepare(model);
6785 let interpreted =
6786 CpuBackend.prepare_with_execution_mode(model, CpuExecutionMode::Interpreter);
6787
6788 if automatic.scalar_executor.is_some() {
6789 assert!(matches!(
6790 automatic.scalar_executor,
6791 Some(ScalarExecutor::Jit(_))
6792 ));
6793 }
6794 if interpreted.scalar_executor.is_some() {
6795 assert!(matches!(
6796 interpreted.scalar_executor,
6797 Some(ScalarExecutor::Interpreter(_))
6798 ));
6799 }
6800 (automatic, interpreted)
6801 }
6802
6803 #[cfg(feature = "jit")]
6804 fn assert_complex_close(actual: Complex64, expected: Complex64) {
6805 assert!(
6806 (actual - expected).norm() < 1.0e-6,
6807 "{actual} != {expected}"
6808 );
6809 }
6810
6811 #[cfg(feature = "jit")]
6812 fn assert_complex_slices_close(actual: &[Complex64], expected: &[Complex64]) {
6813 assert_eq!(actual.len(), expected.len());
6814 for (actual, expected) in actual.iter().zip(expected) {
6815 assert_complex_close(*actual, *expected);
6816 }
6817 }
6818
6819 #[cfg(feature = "jit")]
6820 fn assert_complex_close_f64(actual: Complex64, expected: Complex64) {
6821 assert!(
6822 (actual - expected).norm() < 1.0e-10,
6823 "{actual} != {expected}"
6824 );
6825 }
6826
6827 #[cfg(feature = "jit")]
6828 fn assert_complex_slices_close_f64(actual: &[Complex64], expected: &[Complex64]) {
6829 assert_eq!(actual.len(), expected.len());
6830 for (actual, expected) in actual.iter().zip(expected) {
6831 assert_complex_close_f64(*actual, *expected);
6832 }
6833 }
6834
6835 #[test]
6836 fn evaluates_scalar_expression_with_parameters() {
6837 let expr = (2.0 * parameter!("x", initial: 3.0)
6838 + complex(
6839 parameter!("re", initial: 1.0),
6840 parameter!("im", initial: 2.0),
6841 ))
6842 .norm_sqr();
6843
6844 assert_eq!(evaluate(&expr), Complex64::from(53.0));
6845 }
6846
6847 #[test]
6848 fn forward_gradients_match_scalar_complex_finite_differences() {
6849 let x = laddu_expr::Expr::from(parameter!("x", initial: 0.4));
6850 let y = laddu_expr::Expr::from(parameter!("y", initial: -0.2));
6851 let expression = complex(x.clone().sin(), y.clone().exp()).norm_sqr() + (x * y).cos();
6852 let model = CompiledModel::from_expr(&expression).unwrap();
6853 let params = Arc::new(model.params().clone()).default_values();
6854 let plan = CpuBackend.prepare(&model);
6855 let result = plan.evaluate_with_gradient(¶ms).unwrap();
6856 let ir_result = gradient_interpreter::GradientInterpreter::new(
6857 plan.scalar_kernel.as_ref().unwrap(),
6858 model.params().free_params(),
6859 )
6860 .unwrap()
6861 .evaluate(¶ms, None)
6862 .unwrap()
6863 .1;
6864
6865 for (actual, expected) in ir_result.iter().zip(result.gradient()) {
6866 assert!(
6867 (actual - expected).norm() < 1.0e-12,
6868 "{actual} != {expected}"
6869 );
6870 }
6871
6872 for (parameter, derivative) in result.gradient().iter().enumerate() {
6873 let expected = finite_difference(&plan, ¶ms, parameter);
6874 assert!((derivative - expected).norm() < 1.0e-8);
6875 }
6876 }
6877
6878 #[test]
6879 fn reverse_gradients_match_forward_for_scalar_complex_operations() {
6880 let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
6881 let y = laddu_expr::Expr::from(parameter!("y", initial: -0.3));
6882 let z = complex(x.clone(), y.clone());
6883 let expression = x.clone().sqrt()
6884 + x.clone().log()
6885 + x.clone().powi(-2)
6886 + x.clone().sin()
6887 + x.clone().cos()
6888 + x.clone().exp()
6889 + z.clone().conj()
6890 + z.clone().real()
6891 + z.clone().imag()
6892 + z.norm_sqr()
6893 + atan2(y.clone(), x.clone())
6894 + x * y;
6895 let model = CompiledModel::from_expr(&expression).unwrap();
6896 let params = Arc::new(model.params().clone()).default_values();
6897 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6898 let reverse = CpuBackend
6899 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6900 .unwrap();
6901
6902 let expected = forward.evaluate_with_gradient(¶ms).unwrap();
6903 let actual = reverse.evaluate_with_gradient(¶ms).unwrap();
6904
6905 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6906 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6907 }
6908
6909 #[test]
6910 fn reverse_gradients_match_forward_for_structured_linear_algebra() {
6911 let a = laddu_expr::Expr::from(parameter!("a", initial: 0.7));
6912 let b = laddu_expr::Expr::from(parameter!("b", initial: -0.2));
6913 let c = laddu_expr::Expr::from(parameter!("c", initial: 1.1));
6914 let d = laddu_expr::Expr::from(parameter!("d", initial: 0.4));
6915 let x = laddu_expr::Expr::from(parameter!("x", initial: -0.3));
6916 let y = laddu_expr::Expr::from(parameter!("y", initial: 0.9));
6917 let left = matrix([
6918 [a.clone(), complex(b.clone(), 0.2)],
6919 [1.3.into(), c.clone()],
6920 ]);
6921 let right = matrix([[complex(0.5, -0.1), d.clone()], [b.clone(), 0.8.into()]]);
6922 let product = matmul(left, right);
6923 let input_vector = vector([x.clone(), complex(y.clone(), -0.4)]);
6924 let projected = matvec(product.clone(), input_vector);
6925 let expression = dot(projected.clone(), vector([complex(0.25, 0.3), c.clone()]))
6926 + product.matrix_element(1, 0)
6927 + projected.component(1);
6928 let model = CompiledModel::from_expr(&expression).unwrap();
6929 let params = Arc::new(model.params().clone()).default_values();
6930 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6931 let reverse = CpuBackend
6932 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6933 .unwrap();
6934
6935 let expected = forward.evaluate_with_gradient(¶ms).unwrap();
6936 let actual = reverse.evaluate_with_gradient(¶ms).unwrap();
6937
6938 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6939 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6940 }
6941
6942 #[test]
6943 fn reverse_gradients_match_forward_for_parameter_dependent_solve() {
6944 let a = laddu_expr::Expr::from(parameter!("a", initial: 2.0));
6945 let b = laddu_expr::Expr::from(parameter!("b", initial: 0.3));
6946 let r = laddu_expr::Expr::from(parameter!("r", initial: 1.2));
6947 let solution = solve(
6948 matrix([[a, complex(b.clone(), 0.1)], [b, 1.7.into()]]),
6949 vector([r, complex(0.5, -0.1)]),
6950 );
6951 let expression = dot(solution, vector([complex(1.0, 0.2), (-0.4).into()]));
6952 let model = CompiledModel::from_expr(&expression).unwrap();
6953 let params = Arc::new(model.params().clone()).default_values();
6954 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6955 let reverse = CpuBackend
6956 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6957 .unwrap();
6958
6959 let expected = forward.evaluate_with_gradient(¶ms).unwrap();
6960 let actual = reverse.evaluate_with_gradient(¶ms).unwrap();
6961
6962 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6963 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6964 }
6965
6966 #[test]
6967 fn reverse_cached_event_gradients_match_forward() {
6968 let x = event_scalar("x");
6969 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
6970 let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
6971 let expression =
6972 complex((x.clone() * &scale).sin(), (x.clone() + phase).cos()).norm_sqr() + x * scale;
6973 let model = CompiledModel::from_expr(&expression).unwrap();
6974 let params = Arc::new(model.params().clone()).default_values();
6975 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
6976 let reverse = CpuBackend
6977 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
6978 .unwrap();
6979 let batch = EventBatch::from_events(
6980 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
6981 [
6982 OwnedEvent::new(vec![], vec![0.25]),
6983 OwnedEvent::new(vec![], vec![0.75]),
6984 OwnedEvent::new(vec![], vec![1.25]),
6985 ],
6986 )
6987 .unwrap();
6988
6989 let expected = forward
6990 .evaluate_cache_with_gradient(¶ms, &forward.cache_event_batch(&batch).unwrap())
6991 .unwrap();
6992 let actual = reverse
6993 .evaluate_cache_with_gradient(¶ms, &reverse.cache_event_batch(&batch).unwrap())
6994 .unwrap();
6995
6996 for (actual, expected) in actual.iter().zip(&expected) {
6997 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
6998 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
6999 }
7000 }
7001
7002 #[test]
7003 fn reverse_cached_event_materialization_is_a_leaf() {
7004 let event_sum = event_scalar("x") + event_scalar("y").sin();
7005 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7006 let expression = scale * event_sum;
7007 let model = CompiledModel::from_expr(&expression).unwrap();
7008 let params = Arc::new(model.params().clone()).default_values();
7009 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7010 let reverse = CpuBackend
7011 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
7012 .unwrap();
7013 let batch = EventBatch::from_events(
7014 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
7015 [OwnedEvent::new(vec![], vec![0.5, 0.25])],
7016 )
7017 .unwrap();
7018 assert!(
7019 reverse
7020 .cache_slots
7021 .iter()
7022 .enumerate()
7023 .any(|(index, slot)| slot.is_some() && reverse.cached_value_slots[index].is_some())
7024 );
7025
7026 let expected = forward
7027 .evaluate_cache_row_with_gradient(
7028 ¶ms,
7029 &forward.cache_event_batch(&batch).unwrap(),
7030 0,
7031 )
7032 .unwrap();
7033 let actual = reverse
7034 .evaluate_cache_row_with_gradient(
7035 ¶ms,
7036 &reverse.cache_event_batch(&batch).unwrap(),
7037 0,
7038 )
7039 .unwrap();
7040
7041 assert_eq!(actual.value(), expected.value());
7042 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
7043 }
7044
7045 #[test]
7046 fn reverse_f32_gradients_match_forward() {
7047 let expression = laddu_expr::Expr::from(parameter!("x", initial: 0.4)).sin();
7048 let model = CompiledModel::from_expr(&expression).unwrap();
7049 let params = Arc::new(model.params().clone()).default_values();
7050 let reverse = CpuBackend
7051 .prepare_with_modes_precision(
7052 &model,
7053 AutodiffMode::Reverse,
7054 CpuExecutionMode::Interpreter,
7055 Precision::F32,
7056 )
7057 .unwrap();
7058 let forward = CpuBackend
7059 .prepare_with_modes_precision(
7060 &model,
7061 AutodiffMode::Forward,
7062 CpuExecutionMode::Interpreter,
7063 Precision::F32,
7064 )
7065 .unwrap();
7066
7067 assert_eq!(
7068 reverse.evaluate_with_gradient(¶ms).unwrap(),
7069 forward.evaluate_with_gradient(¶ms).unwrap()
7070 );
7071 }
7072
7073 #[cfg(feature = "jit")]
7074 #[test]
7075 fn reverse_jit_gradients_match_interpreters_in_both_precisions() {
7076 let event = event_scalar("x");
7077 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
7078 let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
7079 let expression = complex(
7080 (event.clone() * scale.clone()).sin(),
7081 (event.clone() + phase).cos(),
7082 )
7083 .norm_sqr()
7084 + event * scale;
7085 let model = CompiledModel::from_expr(&expression).unwrap();
7086 let params = Arc::new(model.params().clone()).default_values();
7087 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
7088 let batch = EventBatch::from_events(
7089 schema,
7090 [
7091 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7092 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7093 ],
7094 )
7095 .unwrap();
7096
7097 for (precision, tolerance) in [(Precision::F32, 1.0e-6), (Precision::F64, 1.0e-12)] {
7098 let jit = CpuBackend
7099 .prepare_with_modes_precision(
7100 &model,
7101 AutodiffMode::Reverse,
7102 CpuExecutionMode::Auto,
7103 precision,
7104 )
7105 .unwrap();
7106 let interpreter = CpuBackend
7107 .prepare_with_modes_precision(
7108 &model,
7109 AutodiffMode::Reverse,
7110 CpuExecutionMode::Interpreter,
7111 precision,
7112 )
7113 .unwrap();
7114 assert!(matches!(jit.gradient_executor, GradientExecutor::Jit(_)));
7115
7116 let actual = jit
7117 .evaluate_cache_with_gradient(¶ms, &jit.cache_event_batch(&batch).unwrap())
7118 .unwrap();
7119 let expected = interpreter
7120 .evaluate_cache_with_gradient(
7121 ¶ms,
7122 &interpreter.cache_event_batch(&batch).unwrap(),
7123 )
7124 .unwrap();
7125 for (actual, expected) in actual.iter().zip(expected) {
7126 assert!((actual.value() - expected.value()).norm() < tolerance);
7127 assert_gradient_close(actual.gradient(), expected.gradient(), tolerance);
7128 }
7129 }
7130 }
7131
7132 #[cfg(feature = "jit")]
7133 #[test]
7134 fn jit_gradient_reduction_matches_interpreter() {
7135 let x = event_scalar("x").real();
7136 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 0.4));
7137 let phase = laddu_expr::Expr::from(parameter!("phase", initial: -0.2));
7138 let intensity =
7139 complex((x.clone() * &scale).sin(), (x.clone() + phase).cos()).norm_sqr() + 0.5;
7140 let expression = complex(intensity, x * scale);
7141 let model = CompiledModel::from_expr(&expression).unwrap();
7142 let params = Arc::new(model.params().clone()).default_values();
7143 let automatic = CpuBackend.prepare(&model);
7144 let interpreted =
7145 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7146 assert!(matches!(
7147 automatic.gradient_executor,
7148 GradientExecutor::Jit(_)
7149 ));
7150
7151 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
7152 let batch = EventBatch::from_events(
7153 schema,
7154 [
7155 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7156 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7157 OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7158 ],
7159 )
7160 .unwrap();
7161 let actual = automatic
7162 .evaluate_cache_with_gradient(¶ms, &automatic.cache_event_batch(&batch).unwrap())
7163 .unwrap();
7164 let expected = interpreted
7165 .evaluate_cache_with_gradient(¶ms, &interpreted.cache_event_batch(&batch).unwrap())
7166 .unwrap();
7167 for (actual, expected) in actual.iter().zip(&expected) {
7168 assert!(
7169 (actual.value() - expected.value()).norm() < 1.0e-12,
7170 "{} != {}",
7171 actual.value(),
7172 expected.value()
7173 );
7174 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7175 assert!((actual - expected).norm() < 1.0e-12);
7176 }
7177 }
7178 let dataset = Dataset::from_batch(batch);
7179 let execution = Execution::local(crate::ExecutionOptions {
7180 device: crate::Device::Cpu(crate::CpuOptions {
7181 threads: crate::ThreadPolicy::Serial,
7182 ..crate::CpuOptions::default()
7183 }),
7184 ..crate::ExecutionOptions::default()
7185 })
7186 .unwrap();
7187 let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
7188 let interpreted_data = interpreted.prepare_dataset(&execution, &dataset).unwrap();
7189 let automatic_result = automatic
7190 .reduce_with_gradient(
7191 &execution,
7192 ¶ms,
7193 &automatic_data,
7194 ReductionPlan::weighted_log_positive_real(),
7195 )
7196 .unwrap();
7197 let interpreted_result = interpreted
7198 .reduce_with_gradient(
7199 &execution,
7200 ¶ms,
7201 &interpreted_data,
7202 ReductionPlan::weighted_log_positive_real(),
7203 )
7204 .unwrap();
7205
7206 assert!((automatic_result.value() - interpreted_result.value()).abs() < 1.0e-12);
7207 for (actual, expected) in automatic_result
7208 .gradient()
7209 .iter()
7210 .zip(interpreted_result.gradient())
7211 {
7212 assert!((actual - expected).abs() < 1.0e-12);
7213 }
7214 }
7215
7216 #[test]
7217 fn forward_gradients_cover_unary_atan2_and_zero_products() {
7218 let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7219 let y = laddu_expr::Expr::from(parameter!("y", initial: 0.0));
7220 let z = complex(x.clone(), y.clone());
7221 let expression = x.clone().sqrt()
7222 + x.clone().log()
7223 + x.clone().powi(-2)
7224 + x.clone().sin()
7225 + x.clone().cos()
7226 + x.clone().exp()
7227 + z.clone().conj().real()
7228 + z.clone().imag()
7229 + z.norm_sqr()
7230 + atan2(y.clone(), x.clone())
7231 + y * x;
7232 let model = CompiledModel::from_expr(&expression).unwrap();
7233 let params = Arc::new(model.params().clone()).default_values();
7234 let plan = CpuBackend.prepare(&model);
7235 let result = plan.evaluate_with_gradient(¶ms).unwrap();
7236
7237 for (parameter, derivative) in result.gradient().iter().enumerate() {
7238 let expected = finite_difference(&plan, ¶ms, parameter);
7239 assert!((derivative - expected).norm() < 1.0e-7);
7240 }
7241 }
7242
7243 #[test]
7244 fn forward_gradients_cover_matrix_vector_and_dot_operations() {
7245 let a = laddu_expr::Expr::from(parameter!("a", initial: 0.7));
7246 let b = laddu_expr::Expr::from(parameter!("b", initial: -0.2));
7247 let c = laddu_expr::Expr::from(parameter!("c", initial: 1.1));
7248 let d = laddu_expr::Expr::from(parameter!("d", initial: 0.4));
7249 let x = laddu_expr::Expr::from(parameter!("x", initial: -0.3));
7250 let y = laddu_expr::Expr::from(parameter!("y", initial: 0.9));
7251 let left = matrix([
7252 [a.clone(), complex(b.clone(), 0.2)],
7253 [1.3.into(), c.clone()],
7254 ]);
7255 let right = matrix([[complex(0.5, -0.1), d.clone()], [b.clone(), 0.8.into()]]);
7256 let product = matmul(left, right);
7257 let input_vector = vector([x.clone(), complex(y.clone(), -0.4)]);
7258 let projected = matvec(product.clone(), input_vector);
7259 let expression = dot(projected.clone(), vector([complex(0.25, 0.3), c.clone()]))
7260 + product.matrix_element(1, 0)
7261 + projected.component(1);
7262 let model = CompiledModel::from_expr(&expression).unwrap();
7263 let params = Arc::new(model.params().clone()).default_values();
7264 let plan = CpuBackend.prepare(&model);
7265 let result = plan.evaluate_with_gradient(¶ms).unwrap();
7266
7267 for (parameter, derivative) in result.gradient().iter().enumerate() {
7268 let expected = finite_difference(&plan, ¶ms, parameter);
7269 assert!(
7270 (derivative - expected).norm() < 1.0e-7,
7271 "{derivative} != {expected}"
7272 );
7273 }
7274 }
7275
7276 #[test]
7277 fn solve_gradients_match_finite_differences_for_matrix_and_rhs_parameters() {
7278 let a = laddu_expr::Expr::from(parameter!("a", initial: 2.0));
7279 let b = laddu_expr::Expr::from(parameter!("b", initial: 0.3));
7280 let r = laddu_expr::Expr::from(parameter!("r", initial: 1.2));
7281 let solution = solve(
7282 matrix([[a, b], [0.2.into(), 1.7.into()]]),
7283 vector([r, complex(0.5, -0.1)]),
7284 );
7285 let expression = dot(solution, vector([complex(1.0, 0.2), (-0.4).into()]));
7286 let model = CompiledModel::from_expr(&expression).unwrap();
7287 let params = Arc::new(model.params().clone()).default_values();
7288 let plan = CpuBackend.prepare(&model);
7289 let result = plan.evaluate_with_gradient(¶ms).unwrap();
7290
7291 for (parameter, derivative) in result.gradient().iter().enumerate() {
7292 let expected = finite_difference(&plan, ¶ms, parameter);
7293 assert!((derivative - expected).norm() < 1.0e-8);
7294 }
7295 }
7296
7297 #[test]
7298 fn evaluates_event_scalars() {
7299 let expr = laddu_expr::event_scalar("x") * 2.0;
7300 let model = CompiledModel::from_expr(&expr).unwrap();
7301 let params = Arc::new(model.params().clone()).default_values();
7302 let plan = CpuBackend.prepare(&model);
7303 let event = HashMap::from([("x".to_owned(), 3.0)]);
7304
7305 assert_eq!(
7306 plan.evaluate_with_event(¶ms, &event).unwrap(),
7307 Complex64::from(6.0)
7308 );
7309 }
7310
7311 #[test]
7312 fn scalar_kernel_ir_preserves_typed_dependency_classes() {
7313 let coefficient = complex(parameter!("re", initial: 2.0), 1.0);
7314 let expr = coefficient * event_scalar("x");
7315 let model = CompiledModel::from_expr(&expr).unwrap();
7316 let plan = CpuBackend.prepare(&model);
7317 let kernel = plan.scalar_kernel.as_ref().unwrap();
7318
7319 assert!(
7320 kernel
7321 .values()
7322 .iter()
7323 .any(|value| value.class == KernelValueClass::Invariant)
7324 );
7325 assert!(
7326 kernel
7327 .values()
7328 .iter()
7329 .any(|value| value.class == KernelValueClass::Event)
7330 );
7331 let root = &kernel.values()[kernel.root().index()];
7332 assert_eq!(root.kind, KernelValueKind::Complex);
7333 assert_eq!(root.class, KernelValueClass::Event);
7334 assert!(matches!(root.instruction, KernelInstruction::Mul(_)));
7335 }
7336
7337 #[test]
7338 fn cpu_execution_mode_selects_retained_interpreter() {
7339 let expr = laddu_expr::Expr::from(parameter!("x", initial: 2.0)).exp() + 1.0;
7340 let model = CompiledModel::from_expr(&expr).unwrap();
7341 let params = Arc::new(model.params().clone()).default_values();
7342 let automatic = CpuBackend.prepare(&model);
7343 let interpreted =
7344 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7345 let execution = Execution::local(crate::ExecutionOptions {
7346 device: crate::Device::Cpu(crate::CpuOptions {
7347 jit: crate::JitPolicy::Disabled,
7348 ..crate::CpuOptions::default()
7349 }),
7350 ..crate::ExecutionOptions::default()
7351 })
7352 .unwrap();
7353 let configured = CpuBackend
7354 .prepare_for_execution(&model, &execution)
7355 .unwrap();
7356
7357 assert!(matches!(
7358 interpreted.scalar_executor,
7359 Some(ScalarExecutor::Interpreter(_))
7360 ));
7361 assert!(matches!(
7362 configured.scalar_executor,
7363 Some(ScalarExecutor::Interpreter(_))
7364 ));
7365 assert!(matches!(
7366 configured.gradient_executor,
7367 GradientExecutor::Interpreter(_)
7368 ));
7369 assert_eq!(
7370 automatic.evaluate(¶ms).unwrap(),
7371 interpreted.evaluate(¶ms).unwrap()
7372 );
7373
7374 let empty_model = CompiledModel::from_expr(&laddu_expr::Expr::from(1.0)).unwrap();
7375 let wrong_params = empty_model.params().default_values();
7376 assert!(matches!(
7377 automatic.evaluate(&wrong_params),
7378 Err(RuntimeError::Parameter(_))
7379 ));
7380 }
7381
7382 #[test]
7383 fn cpu_plan_executes_parameter_only_scalar_kernel_in_f32() {
7384 let x = laddu_expr::Expr::from(parameter!("x", initial: 16_777_216.0));
7385 let y = laddu_expr::Expr::from(parameter!("y", initial: 1.0));
7386 let model = CompiledModel::from_expr(&(x + y)).unwrap();
7387 let params = model.params().default_values();
7388 let execution = Execution::local(crate::ExecutionOptions {
7389 device: crate::Device::Cpu(crate::CpuOptions::default()),
7390 precision: Precision::F32,
7391 ..crate::ExecutionOptions::default()
7392 })
7393 .unwrap();
7394 let f32_plan = CpuBackend
7395 .prepare_for_execution(&model, &execution)
7396 .unwrap();
7397 let f64_plan = CpuBackend.prepare(&model);
7398
7399 assert_eq!(f32_plan.evaluate(¶ms).unwrap().re, 16_777_216.0);
7400 assert_eq!(f64_plan.evaluate(¶ms).unwrap().re, 16_777_217.0);
7401 let gradient = f32_plan.evaluate_with_gradient(¶ms).unwrap();
7402 assert_eq!(gradient.value().re, 16_777_216.0);
7403 assert_eq!(gradient.gradient(), &[Complex64::ONE, Complex64::ONE]);
7404 }
7405
7406 #[cfg(feature = "jit")]
7407 #[test]
7408 fn cpu_f32_auto_jit_matches_f32_interpreter_for_parameter_arithmetic() {
7409 let x = laddu_expr::Expr::from(parameter!("x", initial: 16_777_216.0));
7410 let y = laddu_expr::Expr::from(parameter!("y", initial: 1.0));
7411 let model = CompiledModel::from_expr(&(x + y)).unwrap();
7412 let params = model.params().default_values();
7413 let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7414 let GradientExecutor::Jit(kernel) = &automatic.gradient_executor else {
7415 unreachable!("f32_jit_and_interpreter already requires a gradient JIT")
7416 };
7417 assert_eq!(kernel.compiled_component_count(), 1);
7418
7419 assert_eq!(
7420 automatic.evaluate(¶ms).unwrap(),
7421 interpreted.evaluate(¶ms).unwrap()
7422 );
7423 }
7424
7425 #[cfg(feature = "jit")]
7426 #[test]
7427 fn cpu_f32_auto_jit_matches_f32_interpreter_for_unary_and_binary_ops() {
7428 let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7429 let y = laddu_expr::Expr::from(parameter!("y", initial: -0.35));
7430 let z = complex(x.clone(), y.clone());
7431 let expression = x.clone().sqrt()
7432 + x.clone().log()
7433 + x.clone().powi(-2)
7434 + x.clone().sin()
7435 + x.clone().cos()
7436 + x.clone().exp()
7437 + z.clone().conj().real()
7438 + z.clone().imag()
7439 + z.norm_sqr()
7440 + atan2(y.clone(), x.clone())
7441 + complex(x.clone(), y.clone()) / complex(1.25, -0.5);
7442 let model = CompiledModel::from_expr(&expression).unwrap();
7443 let params = model.params().default_values();
7444 let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7445
7446 assert_complex_close(
7447 automatic.evaluate(¶ms).unwrap(),
7448 interpreted.evaluate(¶ms).unwrap(),
7449 );
7450 }
7451
7452 #[test]
7453 fn cpu_f32_reduces_event_scalar_arithmetic_with_f64_accumulation() {
7454 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.0));
7455 let model = CompiledModel::from_expr(&(event_scalar("x") + scale)).unwrap();
7456 let params = model.params().default_values();
7457 let execution = Execution::local(crate::ExecutionOptions {
7458 device: crate::Device::Cpu(crate::CpuOptions {
7459 threads: crate::ThreadPolicy::Serial,
7460 ..crate::CpuOptions::default()
7461 }),
7462 precision: Precision::F32,
7463 ..crate::ExecutionOptions::default()
7464 })
7465 .unwrap();
7466 let plan = CpuBackend
7467 .prepare_for_execution(&model, &execution)
7468 .unwrap();
7469 let dataset = Dataset::from_batch(
7470 EventBatch::from_events(
7471 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7472 [OwnedEvent::new(vec![], vec![16_777_216.0])],
7473 )
7474 .unwrap(),
7475 );
7476 let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
7477
7478 assert_eq!(
7479 plan.reduce(
7480 &execution,
7481 ¶ms,
7482 &prepared,
7483 ReductionPlan::weighted_real(),
7484 )
7485 .unwrap(),
7486 16_777_216.0
7487 );
7488 }
7489
7490 #[test]
7491 fn cpu_f32_evaluates_complex_linear_algebra() {
7492 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7493 let lhs = matrix([
7494 [scale.clone() + 2.0, complex(0.25, -0.5)],
7495 [0.5.into(), scale.clone() + 3.0],
7496 ]);
7497 let rhs = vector([1.0.into(), scale]);
7498 let model = CompiledModel::from_expr(&dot(
7499 vector([1.0.into(), complex(0.0, 1.0)]),
7500 solve(lhs, rhs),
7501 ))
7502 .unwrap();
7503 let params = model.params().default_values();
7504 let execution = Execution::local(crate::ExecutionOptions {
7505 device: crate::Device::Cpu(crate::CpuOptions::default()),
7506 precision: Precision::F32,
7507 ..crate::ExecutionOptions::default()
7508 })
7509 .unwrap();
7510 let f32_plan = CpuBackend
7511 .prepare_for_execution(&model, &execution)
7512 .unwrap();
7513 let f64_plan = CpuBackend.prepare(&model);
7514
7515 let actual = f32_plan.evaluate(¶ms).unwrap();
7516 let expected = f64_plan.evaluate(¶ms).unwrap();
7517 assert!((actual.re - expected.re).abs() < 1.0e-6);
7518 assert!((actual.im - expected.im).abs() < 1.0e-6);
7519 }
7520
7521 #[test]
7522 fn cpu_f32_evaluates_computed_event_cache_entries() {
7523 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 2.0));
7524 let model =
7525 CompiledModel::from_expr(&(event_scalar("x").sin() * scale + 16_777_216.0)).unwrap();
7526 let execution = Execution::local(crate::ExecutionOptions {
7527 device: crate::Device::Cpu(crate::CpuOptions::default()),
7528 precision: Precision::F32,
7529 ..crate::ExecutionOptions::default()
7530 })
7531 .unwrap();
7532 let plan = CpuBackend
7533 .prepare_for_execution(&model, &execution)
7534 .unwrap();
7535 let params = model.params().default_values();
7536 let dataset = Dataset::from_batch(
7537 EventBatch::from_events(
7538 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7539 [OwnedEvent::new(vec![], vec![1.0])],
7540 )
7541 .unwrap(),
7542 );
7543 let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
7544
7545 let reduction = plan
7546 .reduce_with_gradient(
7547 &execution,
7548 ¶ms,
7549 &prepared,
7550 ReductionPlan::weighted_real(),
7551 )
7552 .unwrap();
7553 assert_eq!(reduction.value(), 16_777_218.0);
7554 assert_eq!(reduction.gradient(), &[(1.0_f32.sin() as f64)]);
7555 }
7556
7557 #[test]
7558 fn cpu_f32_direct_event_gradient_matches_cached_event_gradient() {
7559 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.5));
7560 let offset = laddu_expr::Expr::from(parameter!("offset", initial: -0.25));
7561 let x = event_scalar("x");
7562 let expression =
7563 (x.clone().sin() * scale.clone() + offset.clone()).exp() + complex(scale, x).norm_sqr();
7564 let model = CompiledModel::from_expr(&expression).unwrap();
7565 let execution = Execution::local(crate::ExecutionOptions {
7566 device: crate::Device::Cpu(crate::CpuOptions::default()),
7567 precision: Precision::F32,
7568 ..crate::ExecutionOptions::default()
7569 })
7570 .unwrap();
7571 let plan = CpuBackend
7572 .prepare_for_execution(&model, &execution)
7573 .unwrap();
7574 let params = model.params().default_values();
7575 let event = HashMap::from([("x".to_owned(), 0.75)]);
7576 let batch = EventBatch::from_events(
7577 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7578 [OwnedEvent::new(vec![], vec![0.75])],
7579 )
7580 .unwrap();
7581 let cache = plan.cache_event_batch(&batch).unwrap();
7582
7583 let direct_value = plan.evaluate_with_event(¶ms, &event).unwrap();
7584 let direct = plan
7585 .evaluate_with_event_and_gradient(¶ms, &event)
7586 .unwrap();
7587 let cached_value = plan.evaluate_cache_row(¶ms, &cache, 0).unwrap();
7588 let cached = plan
7589 .evaluate_cache_row_with_gradient(¶ms, &cache, 0)
7590 .unwrap();
7591
7592 assert_eq!(direct_value, cached_value);
7593 assert_eq!(direct.value(), cached.value());
7594 assert_eq!(direct.gradient(), cached.gradient());
7595 }
7596
7597 #[cfg(feature = "jit")]
7598 #[test]
7599 fn cpu_f32_auto_jit_matches_f32_interpreter_for_cached_linear_algebra() {
7600 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7601 let matrix = matrix([
7602 [event_scalar("x") + 2.0, complex(0.25, -0.5)],
7603 [0.5.into(), scale.clone() + 3.0],
7604 ]);
7605 let rhs = vector([1.0.into(), scale]);
7606 let model = CompiledModel::from_expr(&dot(
7607 vector([1.0.into(), complex(0.0, 1.0)]),
7608 solve(matrix, rhs),
7609 ))
7610 .unwrap();
7611 let params = model.params().default_values();
7612 let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7613 let batch = EventBatch::from_events(
7614 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7615 [OwnedEvent::new(vec![], vec![0.75])],
7616 )
7617 .unwrap();
7618 let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7619 let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7620
7621 let actual = automatic.evaluate_cache(¶ms, &automatic_cache).unwrap();
7622 let expected = interpreted
7623 .evaluate_cache(¶ms, &interpreted_cache)
7624 .unwrap();
7625 assert_complex_slices_close(&actual, &expected);
7626 }
7627
7628 #[cfg(feature = "jit")]
7629 #[test]
7630 fn cpu_f32_auto_jit_matches_f32_interpreter_for_event_cache_ops() {
7631 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.75));
7632 let x = event_scalar("x");
7633 let y = event_scalar("y");
7634 let expression = ((x.clone() + scale.clone()).sin()
7635 + (y.clone() - 0.25).cos()
7636 + (x.clone() * y.clone()).exp()
7637 + atan2(y.clone(), x.clone() + 1.0))
7638 / complex(scale, -0.5);
7639 let model = CompiledModel::from_expr(&expression).unwrap();
7640 let params = model.params().default_values();
7641 let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7642 let batch = EventBatch::from_events(
7643 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
7644 [
7645 OwnedEvent::new(vec![], vec![0.25, -0.5]),
7646 OwnedEvent::new(vec![], vec![0.75, 0.125]),
7647 OwnedEvent::new(vec![], vec![1.5, 0.5]),
7648 ],
7649 )
7650 .unwrap();
7651 let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7652 let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7653
7654 assert_complex_slices_close(
7655 &automatic.evaluate_cache(¶ms, &automatic_cache).unwrap(),
7656 &interpreted
7657 .evaluate_cache(¶ms, &interpreted_cache)
7658 .unwrap(),
7659 );
7660 }
7661
7662 #[cfg(feature = "jit")]
7663 #[test]
7664 fn cpu_f32_auto_jit_matches_f32_interpreter_for_reductions_and_gradients() {
7665 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7666 let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
7667 let x = event_scalar("x");
7668 let expression = (x.clone() * scale.clone() + offset.clone()).sin()
7669 + complex(scale, offset).norm_sqr()
7670 + 2.0;
7671 let model = CompiledModel::from_expr(&expression).unwrap();
7672 let params = model.params().default_values();
7673 let (automatic, interpreted) = f32_jit_and_interpreter(&model);
7674 let auto_execution = f32_execution(JitPolicy::Auto);
7675 let interpreter_execution = f32_execution(JitPolicy::Disabled);
7676 let dataset = Dataset::from_batch(
7677 EventBatch::from_events(
7678 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap()),
7679 [
7680 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7681 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7682 OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7683 ],
7684 )
7685 .unwrap(),
7686 );
7687 let automatic_data = automatic
7688 .prepare_dataset(&auto_execution, &dataset)
7689 .unwrap();
7690 let interpreted_data = interpreted
7691 .prepare_dataset(&interpreter_execution, &dataset)
7692 .unwrap();
7693
7694 let actual = automatic
7695 .reduce_with_gradient(
7696 &auto_execution,
7697 ¶ms,
7698 &automatic_data,
7699 ReductionPlan::weighted_real(),
7700 )
7701 .unwrap();
7702 let expected = interpreted
7703 .reduce_with_gradient(
7704 &interpreter_execution,
7705 ¶ms,
7706 &interpreted_data,
7707 ReductionPlan::weighted_real(),
7708 )
7709 .unwrap();
7710 assert!((actual.value() - expected.value()).abs() < 1.0e-6);
7711 assert_eq!(actual.gradient().len(), expected.gradient().len());
7712 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7713 assert!((actual - expected).abs() < 1.0e-6);
7714 }
7715 }
7716
7717 #[cfg(feature = "jit")]
7718 #[test]
7719 fn auto_jit_matches_interpreter_for_supported_real_arithmetic() {
7720 let x = laddu_expr::Expr::from(parameter!("x", initial: 2.0));
7721 let y = laddu_expr::Expr::from(parameter!("y", initial: -0.5));
7722 let expr = (x * 3.0 + y) / 2.0;
7723 let model = CompiledModel::from_expr(&expr).unwrap();
7724 let params = Arc::new(model.params().clone()).default_values();
7725 let automatic = CpuBackend.prepare(&model);
7726 let interpreted =
7727 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7728
7729 assert!(matches!(
7730 automatic.scalar_executor,
7731 Some(ScalarExecutor::Jit(_))
7732 ));
7733 assert!(matches!(
7734 automatic.gradient_executor,
7735 GradientExecutor::Jit(_)
7736 ));
7737 assert_eq!(
7738 automatic.evaluate(¶ms).unwrap(),
7739 interpreted.evaluate(¶ms).unwrap()
7740 );
7741 assert_eq!(
7742 automatic.evaluate_with_gradient(¶ms).unwrap(),
7743 interpreted.evaluate_with_gradient(¶ms).unwrap()
7744 );
7745 }
7746
7747 #[cfg(feature = "jit")]
7748 #[test]
7749 fn auto_jit_supports_complex_transcendentals() {
7750 let expr = laddu_expr::Expr::from(parameter!("x", initial: 2.0)).exp();
7751 let model = CompiledModel::from_expr(&expr).unwrap();
7752 let plan = CpuBackend.prepare(&model);
7753
7754 assert!(matches!(plan.scalar_executor, Some(ScalarExecutor::Jit(_))));
7755 let params = Arc::new(model.params().clone()).default_values();
7756 assert_eq!(plan.evaluate(¶ms).unwrap(), Complex64::from(2.0).exp());
7757 }
7758
7759 #[cfg(feature = "jit")]
7760 #[test]
7761 fn cpu_f64_auto_jit_matches_interpreter_for_unary_binary_ops_and_gradients() {
7762 let x = laddu_expr::Expr::from(parameter!("x", initial: 0.8));
7763 let y = laddu_expr::Expr::from(parameter!("y", initial: -0.35));
7764 let z = complex(x.clone(), y.clone());
7765 let expression = x.clone().sqrt()
7766 + x.clone().log()
7767 + x.clone().powi(-2)
7768 + x.clone().sin()
7769 + x.clone().cos()
7770 + x.clone().exp()
7771 + z.clone().conj().real()
7772 + z.clone().imag()
7773 + z.norm_sqr()
7774 + atan2(y.clone(), x.clone())
7775 + complex(x.clone(), y.clone()) / complex(1.25, -0.5);
7776 let model = CompiledModel::from_expr(&expression).unwrap();
7777 let params = model.params().default_values();
7778 let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7779
7780 assert_complex_close_f64(
7781 automatic.evaluate(¶ms).unwrap(),
7782 interpreted.evaluate(¶ms).unwrap(),
7783 );
7784 let actual_gradient = automatic.evaluate_with_gradient(¶ms).unwrap();
7785 let expected_gradient = interpreted.evaluate_with_gradient(¶ms).unwrap();
7786 assert_complex_close_f64(actual_gradient.value(), expected_gradient.value());
7787 assert_complex_slices_close_f64(actual_gradient.gradient(), expected_gradient.gradient());
7788 }
7789
7790 #[cfg(feature = "jit")]
7791 #[test]
7792 fn cpu_f64_auto_jit_matches_interpreter_for_event_cache_ops_and_gradients() {
7793 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.75));
7794 let x = event_scalar("x");
7795 let y = event_scalar("y");
7796 let expression = ((x.clone() + scale.clone()).sin()
7797 + (y.clone() - 0.25).cos()
7798 + (x.clone() * y.clone()).exp()
7799 + atan2(y.clone(), x.clone() + 1.0))
7800 / complex(scale, -0.5);
7801 let model = CompiledModel::from_expr(&expression).unwrap();
7802 let params = model.params().default_values();
7803 let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7804 let batch = EventBatch::from_events(
7805 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x", "y"], false).unwrap()),
7806 [
7807 OwnedEvent::new(vec![], vec![0.25, -0.5]),
7808 OwnedEvent::new(vec![], vec![0.75, 0.125]),
7809 OwnedEvent::new(vec![], vec![1.5, 0.5]),
7810 ],
7811 )
7812 .unwrap();
7813 let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
7814 let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
7815
7816 assert_complex_slices_close_f64(
7817 &automatic.evaluate_cache(¶ms, &automatic_cache).unwrap(),
7818 &interpreted
7819 .evaluate_cache(¶ms, &interpreted_cache)
7820 .unwrap(),
7821 );
7822 for (actual, expected) in automatic
7823 .evaluate_cache_with_gradient(¶ms, &automatic_cache)
7824 .unwrap()
7825 .iter()
7826 .zip(
7827 interpreted
7828 .evaluate_cache_with_gradient(¶ms, &interpreted_cache)
7829 .unwrap(),
7830 )
7831 {
7832 assert_complex_close_f64(actual.value(), expected.value());
7833 assert_complex_slices_close_f64(actual.gradient(), expected.gradient());
7834 }
7835 }
7836
7837 #[cfg(feature = "jit")]
7838 #[test]
7839 fn cpu_f64_auto_jit_matches_interpreter_for_reductions_and_gradients() {
7840 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.25));
7841 let offset = laddu_expr::Expr::from(parameter!("offset", initial: 0.5));
7842 let x = event_scalar("x");
7843 let expression = (x.clone() * scale.clone() + offset.clone()).sin()
7844 + complex(scale, offset).norm_sqr()
7845 + 2.0;
7846 let model = CompiledModel::from_expr(&expression).unwrap();
7847 let params = model.params().default_values();
7848 let (automatic, interpreted) = f64_jit_and_interpreter(&model);
7849 assert!(matches!(
7850 automatic.gradient_executor,
7851 GradientExecutor::Jit(_)
7852 ));
7853 let execution = Execution::default();
7854 let interpreter_execution = Execution::local(crate::ExecutionOptions {
7855 device: crate::Device::Cpu(crate::CpuOptions {
7856 jit: JitPolicy::Disabled,
7857 ..crate::CpuOptions::default()
7858 }),
7859 ..crate::ExecutionOptions::default()
7860 })
7861 .unwrap();
7862 let dataset = Dataset::from_batch(
7863 EventBatch::from_events(
7864 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap()),
7865 [
7866 OwnedEvent::weighted(vec![], vec![0.25], 0.5),
7867 OwnedEvent::weighted(vec![], vec![0.75], 1.5),
7868 OwnedEvent::weighted(vec![], vec![1.25], 2.0),
7869 ],
7870 )
7871 .unwrap(),
7872 );
7873 let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
7874 let interpreted_data = interpreted
7875 .prepare_dataset(&interpreter_execution, &dataset)
7876 .unwrap();
7877
7878 let actual = automatic
7879 .reduce_with_gradient(
7880 &execution,
7881 ¶ms,
7882 &automatic_data,
7883 ReductionPlan::weighted_real(),
7884 )
7885 .unwrap();
7886 let expected = interpreted
7887 .reduce_with_gradient(
7888 &interpreter_execution,
7889 ¶ms,
7890 &interpreted_data,
7891 ReductionPlan::weighted_real(),
7892 )
7893 .unwrap();
7894 assert!((actual.value() - expected.value()).abs() < 1.0e-10);
7895 assert_eq!(actual.gradient().len(), expected.gradient().len());
7896 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
7897 assert!((actual - expected).abs() < 1.0e-10);
7898 }
7899 }
7900
7901 #[test]
7902 fn evaluates_p4_schema_components_and_atan2() {
7903 let expr = event_p4_component("ks1", P4Component::E)
7904 + event_p4_component("ks1", P4Component::Px)
7905 + atan2(
7906 event_p4_component("ks1", P4Component::Py),
7907 event_p4_component("ks1", P4Component::Px),
7908 );
7909 let model = CompiledModel::from_expr(&expr).unwrap();
7910 let params = Arc::new(model.params().clone()).default_values();
7911 let plan = CpuBackend.prepare(&model);
7912 let batch = EventBatch::from_events(
7913 Arc::new(Schema::new(["ks1"], std::iter::empty::<&str>(), false).unwrap()),
7914 [OwnedEvent::new(
7915 vec![RealVec4::new(10.0, 3.0, 4.0, 5.0)],
7916 vec![],
7917 )],
7918 )
7919 .unwrap();
7920
7921 assert_eq!(
7922 plan.evaluate_batch(¶ms, &batch).unwrap()[0],
7923 Complex64::from(13.0 + 4.0_f64.atan2(3.0))
7924 );
7925 }
7926
7927 #[test]
7928 fn batch_cache_evaluates_without_original_event_batch() {
7929 let expr = event_scalar("x").real().sin() * parameter!("scale", initial: 2.0);
7930 let model = CompiledModel::from_expr(&expr).unwrap();
7931 let layout = Arc::new(model.params().clone());
7932 let mut params = layout.default_values();
7933 let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
7934 let batch = EventBatch::from_events(
7935 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7936 [
7937 OwnedEvent::new(vec![], vec![0.5]),
7938 OwnedEvent::new(vec![], vec![1.0]),
7939 ],
7940 )
7941 .unwrap();
7942 let cache = plan.cache_event_batch(&batch).unwrap();
7943
7944 assert_eq!(cache.weights(), &[1.0, 1.0]);
7945 assert!(matches!(&cache.slots[0], CachedSlot::Real(values) if values.len() == 2));
7946 assert_eq!(cache.slots[0].resident_bytes(), 2 * size_of::<f64>());
7947 assert_eq!(
7948 plan.evaluate_cache(¶ms, &cache).unwrap(),
7949 vec![
7950 Complex64::from(2.0 * 0.5_f64.sin()),
7951 Complex64::from(2.0 * 1.0_f64.sin())
7952 ]
7953 );
7954
7955 let scale = layout
7956 .free_id(layout.id("scale").unwrap())
7957 .unwrap()
7958 .unwrap();
7959 params.set_free(scale, 3.0).unwrap();
7960 assert_eq!(
7961 plan.evaluate_cache(¶ms, &cache).unwrap(),
7962 vec![
7963 Complex64::from(3.0 * 0.5_f64.sin()),
7964 Complex64::from(3.0 * 1.0_f64.sin())
7965 ]
7966 );
7967 }
7968
7969 #[test]
7970 fn real_cache_slots_use_half_the_scalar_payload_of_complex_slots() {
7971 let real_model =
7972 CompiledModel::from_expr(&(parameter!("scale") * event_scalar("x").real().sin()))
7973 .unwrap();
7974 let x = event_scalar("x");
7975 let complex_model =
7976 CompiledModel::from_expr(&(parameter!("scale") * complex(x.clone().sin(), x.cos())))
7977 .unwrap();
7978 let batch = EventBatch::from_events(
7979 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
7980 [
7981 OwnedEvent::new(vec![], vec![0.5]),
7982 OwnedEvent::new(vec![], vec![1.0]),
7983 ],
7984 )
7985 .unwrap();
7986 let real_cache = CpuBackend
7987 .prepare(&real_model)
7988 .cache_event_batch(&batch)
7989 .unwrap();
7990 let complex_cache = CpuBackend
7991 .prepare(&complex_model)
7992 .cache_event_batch(&batch)
7993 .unwrap();
7994
7995 assert!(matches!(&real_cache.slots[0], CachedSlot::Real(_)));
7996 assert!(matches!(&complex_cache.slots[0], CachedSlot::Complex(_)));
7997 assert_eq!(
7998 complex_cache.slots[0].resident_bytes(),
7999 2 * real_cache.slots[0].resident_bytes()
8000 );
8001 }
8002
8003 #[test]
8004 fn selected_event_only_solve_components_cache_inverse_rows() {
8005 let expression = solve(
8006 matrix([[event_scalar("x") + 2.0]]),
8007 vector([parameter!("rhs", initial: 3.0)]),
8008 )
8009 .component(0);
8010 let model = CompiledModel::from_expr(&expression).unwrap();
8011 let params = Arc::new(model.params().clone()).default_values();
8012 let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8013 let scalar_plan = plan.scalar_interpreter_plan().unwrap();
8014 assert!(!scalar_plan.invariant_instructions.is_empty());
8015 assert!(!scalar_plan.event_instructions.is_empty());
8016 let batch = EventBatch::from_events(
8017 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8018 [
8019 OwnedEvent::new(vec![], vec![0.0]),
8020 OwnedEvent::new(vec![], vec![1.0]),
8021 ],
8022 )
8023 .unwrap();
8024 let cache = plan.cache_event_batch(&batch).unwrap();
8025
8026 assert!(cache.factor_slots.is_empty());
8027 assert_eq!(cache.solve_row_slots.len(), 1);
8028 assert_eq!(cache.solve_row_slots[0].values.len(), 2);
8029 assert!(cache.resident_bytes() > 0);
8030 let first = plan
8031 .evaluate_cache_row_with_gradient(¶ms, &cache, 0)
8032 .unwrap();
8033 let second = plan
8034 .evaluate_cache_row_with_gradient(¶ms, &cache, 1)
8035 .unwrap();
8036 assert_eq!(first.value(), Complex64::from(1.5));
8037 assert_eq!(first.gradient(), &[Complex64::from(0.5)]);
8038 assert_eq!(second.value(), Complex64::from(1.0));
8039 assert_eq!(second.gradient(), &[Complex64::from(1.0 / 3.0)]);
8040 }
8041
8042 #[test]
8043 fn cached_solve_component_matches_general_complex_nonsymmetric_solve() {
8044 let expression = solve(
8045 matrix([
8046 [event_scalar("x") + 2.0, Complex64::I.into()],
8047 [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8048 ]),
8049 vector([
8050 parameter!("p", initial: 1.5),
8051 parameter!("q", initial: -0.25),
8052 ]),
8053 )
8054 .component(1);
8055 let model = CompiledModel::from_expr(&expression).unwrap();
8056 let params = Arc::new(model.params().clone()).default_values();
8057 let plan = CpuBackend.prepare(&model);
8058 assert!(plan.solve_components.iter().any(Option::is_some));
8059
8060 let event = HashMap::from([("x".to_owned(), 0.75)]);
8061 let direct = plan
8062 .evaluate_with_event_and_gradient(¶ms, &event)
8063 .unwrap();
8064 let batch = EventBatch::from_events(
8065 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8066 [OwnedEvent::new(vec![], vec![0.75])],
8067 )
8068 .unwrap();
8069 let cache = plan.cache_event_batch(&batch).unwrap();
8070 let cached = plan
8071 .evaluate_cache_row_with_gradient(¶ms, &cache, 0)
8072 .unwrap();
8073 let ir_gradient = gradient_interpreter::GradientInterpreter::new(
8074 plan.scalar_kernel.as_ref().unwrap(),
8075 model.params().free_params(),
8076 )
8077 .unwrap()
8078 .evaluate(¶ms, Some((&cache, 0)))
8079 .unwrap()
8080 .1;
8081
8082 assert!((cached.value() - direct.value()).norm() < 1.0e-12);
8083 for (cached, direct) in cached.gradient().iter().zip(direct.gradient()) {
8084 assert!((cached - direct).norm() < 1.0e-12);
8085 }
8086 for (actual, expected) in ir_gradient.iter().zip(cached.gradient()) {
8087 assert!((actual - expected).norm() < 1.0e-12);
8088 }
8089 }
8090
8091 #[test]
8092 fn reverse_cached_solve_component_matches_forward_nonsymmetric_solve() {
8093 let expression = solve(
8094 matrix([
8095 [event_scalar("x") + 2.0, Complex64::I.into()],
8096 [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8097 ]),
8098 vector([
8099 parameter!("p", initial: 1.5),
8100 parameter!("q", initial: -0.25),
8101 ]),
8102 )
8103 .component(1);
8104 let model = CompiledModel::from_expr(&expression).unwrap();
8105 let params = Arc::new(model.params().clone()).default_values();
8106 let forward = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8107 let reverse = CpuBackend
8108 .prepare_with_autodiff_mode(&model, AutodiffMode::Reverse)
8109 .unwrap();
8110 assert!(reverse.solve_components.iter().any(Option::is_some));
8111
8112 let batch = EventBatch::from_events(
8113 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8114 [OwnedEvent::new(vec![], vec![0.75])],
8115 )
8116 .unwrap();
8117 let expected = forward
8118 .evaluate_cache_row_with_gradient(
8119 ¶ms,
8120 &forward.cache_event_batch(&batch).unwrap(),
8121 0,
8122 )
8123 .unwrap();
8124 let actual = reverse
8125 .evaluate_cache_row_with_gradient(
8126 ¶ms,
8127 &reverse.cache_event_batch(&batch).unwrap(),
8128 0,
8129 )
8130 .unwrap();
8131
8132 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8133 assert_gradient_close(actual.gradient(), expected.gradient(), 1.0e-12);
8134 }
8135
8136 #[cfg(feature = "jit")]
8137 #[test]
8138 fn jit_gradient_reduction_handles_cached_solve_rows() {
8139 let expression = solve(
8140 matrix([
8141 [event_scalar("x") + 2.0, Complex64::I.into()],
8142 [Complex64::new(2.0, -1.0).into(), 3.0.into()],
8143 ]),
8144 vector([
8145 parameter!("p", initial: 1.5),
8146 parameter!("q", initial: -0.25),
8147 ]),
8148 )
8149 .component(1);
8150 let model = CompiledModel::from_expr(&expression).unwrap();
8151 let params = Arc::new(model.params().clone()).default_values();
8152 let automatic = CpuBackend.prepare(&model);
8153 let interpreted =
8154 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8155 assert!(matches!(
8156 automatic.gradient_executor,
8157 GradientExecutor::Jit(_)
8158 ));
8159
8160 let dataset = Dataset::from_batch(
8161 EventBatch::from_events(
8162 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8163 [
8164 OwnedEvent::new(vec![], vec![0.25]),
8165 OwnedEvent::new(vec![], vec![0.75]),
8166 OwnedEvent::new(vec![], vec![1.25]),
8167 ],
8168 )
8169 .unwrap(),
8170 );
8171 let execution = Execution::local(crate::ExecutionOptions {
8172 device: crate::Device::Cpu(crate::CpuOptions {
8173 threads: crate::ThreadPolicy::Fixed(2),
8174 ..crate::CpuOptions::default()
8175 }),
8176 ..crate::ExecutionOptions::default()
8177 })
8178 .unwrap();
8179 let automatic_data = automatic.prepare_dataset(&execution, &dataset).unwrap();
8180 let interpreted_data = interpreted.prepare_dataset(&execution, &dataset).unwrap();
8181 let actual = automatic
8182 .reduce_with_gradient(
8183 &execution,
8184 ¶ms,
8185 &automatic_data,
8186 ReductionPlan::weighted_real(),
8187 )
8188 .unwrap();
8189 let expected = interpreted
8190 .reduce_with_gradient(
8191 &execution,
8192 ¶ms,
8193 &interpreted_data,
8194 ReductionPlan::weighted_real(),
8195 )
8196 .unwrap();
8197
8198 assert!((actual.value() - expected.value()).abs() < 1.0e-12);
8199 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8200 assert!((actual - expected).abs() < 1.0e-12);
8201 }
8202 }
8203
8204 #[test]
8205 fn batch_cache_reports_missing_event_columns() {
8206 let expr = event_scalar("missing");
8207 let model = CompiledModel::from_expr(&expr).unwrap();
8208 let plan = CpuBackend.prepare(&model);
8209 let batch = EventBatch::from_events(
8210 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8211 [OwnedEvent::new(vec![], vec![0.5])],
8212 )
8213 .unwrap();
8214
8215 assert!(matches!(
8216 plan.cache_event_batch(&batch),
8217 Err(RuntimeError::MissingEventColumn(name)) if name == "missing"
8218 ));
8219 }
8220
8221 #[test]
8222 fn cached_dataset_preserves_transformed_batches_and_weights() {
8223 let expr = event_scalar("x") * parameter!("scale", initial: 2.0);
8224 let model = CompiledModel::from_expr(&expr).unwrap();
8225 let params = Arc::new(model.params().clone()).default_values();
8226 let plan = CpuBackend.prepare(&model);
8227 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8228 let batch = EventBatch::from_events(
8229 schema,
8230 [
8231 OwnedEvent::weighted(vec![], vec![0.5], 2.0),
8232 OwnedEvent::weighted(vec![], vec![1.0], 3.0),
8233 ],
8234 )
8235 .unwrap();
8236 let dataset = Dataset::from_batch(batch).filter(|event| event.scalar(0) > 0.75);
8237 let cached = plan.cache_dataset(&dataset).unwrap();
8238
8239 assert_eq!(cached.len(), 1);
8240 assert_eq!(cached.batches()[0].weights(), &[3.0]);
8241 assert_eq!(cached.batches()[0].sum_weights(), 3.0);
8242 assert_eq!(
8243 plan.evaluate_cached_dataset(¶ms, &cached).unwrap(),
8244 vec![Complex64::from(2.0)]
8245 );
8246 }
8247
8248 #[test]
8249 fn cached_dataset_weighted_reductions_match_dataset_path() {
8250 let expr = event_scalar("x") * parameter!("scale", initial: 2.0);
8251 let model = CompiledModel::from_expr(&expr).unwrap();
8252 let params = Arc::new(model.params().clone()).default_values();
8253 let plan = CpuBackend.prepare(&model);
8254 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8255 let first = EventBatch::from_events(
8256 Arc::clone(&schema),
8257 [
8258 OwnedEvent::weighted(vec![], vec![1.0], 2.0),
8259 OwnedEvent::weighted(vec![], vec![2.0], 3.0),
8260 ],
8261 )
8262 .unwrap();
8263 let second =
8264 EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![3.0], 4.0)])
8265 .unwrap();
8266 let dataset = Dataset::from_batches(vec![first, second]).unwrap();
8267 let cached = plan.cache_dataset(&dataset).unwrap();
8268
8269 let expected = dataset.weighted_sum(|event| 2.0 * event.scalar(0)).unwrap();
8270 assert_eq!(cached.sum_weights(), dataset.sum_weights().unwrap());
8271 assert_eq!(
8272 plan.weighted_sum_cached(¶ms, &cached, |value| value.re)
8273 .unwrap(),
8274 expected
8275 );
8276 assert_eq!(
8277 plan.weighted_complex_sum_cached(¶ms, &cached, |value| value * Complex64::I)
8278 .unwrap(),
8279 Complex64::I * expected
8280 );
8281 assert_eq!(
8282 plan.par_weighted_sum_cached(¶ms, &cached, |value| value.re)
8283 .unwrap(),
8284 expected
8285 );
8286 assert_eq!(
8287 plan.par_weighted_complex_sum_cached(¶ms, &cached, |value| value * Complex64::I)
8288 .unwrap(),
8289 Complex64::I * expected
8290 );
8291 let serial_gradient = plan
8292 .try_weighted_real_sum_with_gradient_cached(¶ms, &cached, |value| {
8293 Ok::<_, RuntimeError>((value.re.powi(2), 2.0 * value.re))
8294 })
8295 .unwrap();
8296 let parallel_gradient = plan
8297 .par_try_weighted_real_sum_with_gradient_cached(¶ms, &cached, |value| {
8298 Ok::<_, RuntimeError>((value.re.powi(2), 2.0 * value.re))
8299 })
8300 .unwrap();
8301 assert_eq!(serial_gradient, parallel_gradient);
8302 }
8303
8304 #[test]
8305 fn reduction_plans_match_across_storage_and_thread_policies() {
8306 let expr = event_scalar("x") * parameter!("scale", initial: 2.0) + 1.0;
8307 let model = CompiledModel::from_expr(&expr).unwrap();
8308 let params = Arc::new(model.params().clone()).default_values();
8309 let plan = CpuBackend.prepare(&model);
8310 let schema = Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], true).unwrap());
8311 let first = EventBatch::from_events(
8312 Arc::clone(&schema),
8313 [
8314 OwnedEvent::weighted(vec![], vec![1.0], 2.0),
8315 OwnedEvent::weighted(vec![], vec![2.0], 3.0),
8316 ],
8317 )
8318 .unwrap();
8319 let second =
8320 EventBatch::from_events(schema, [OwnedEvent::weighted(vec![], vec![3.0], 4.0)])
8321 .unwrap();
8322 let resident = Dataset::from_batches(vec![first, second]).unwrap();
8323 let datasets = [resident.clone(), resident.streaming()];
8324 let executions = [
8325 Execution::local(crate::ExecutionOptions {
8326 device: crate::Device::Cpu(crate::CpuOptions {
8327 threads: crate::ThreadPolicy::Serial,
8328 ..crate::CpuOptions::default()
8329 }),
8330 ..crate::ExecutionOptions::default()
8331 })
8332 .unwrap(),
8333 Execution::local(crate::ExecutionOptions {
8334 device: crate::Device::Cpu(crate::CpuOptions {
8335 threads: crate::ThreadPolicy::Fixed(2),
8336 ..crate::CpuOptions::default()
8337 }),
8338 ..crate::ExecutionOptions::default()
8339 })
8340 .unwrap(),
8341 ];
8342 let expected_real = 49.0;
8343 let expected_log = 2.0 * 3.0_f64.ln() + 3.0 * 5.0_f64.ln() + 4.0 * 7.0_f64.ln();
8344 let expected_log_gradient = 2.0 / 3.0 + 6.0 / 5.0 + 12.0 / 7.0;
8345
8346 for execution in &executions {
8347 for dataset in &datasets {
8348 let prepared = plan.prepare_dataset(execution, dataset).unwrap();
8349 assert_eq!(
8350 plan.reduce(
8351 execution,
8352 ¶ms,
8353 &prepared,
8354 ReductionPlan::weighted_real(),
8355 )
8356 .unwrap(),
8357 expected_real
8358 );
8359 assert_eq!(
8360 plan.reduce(
8361 execution,
8362 ¶ms,
8363 &prepared,
8364 ReductionPlan::weighted_positive_real(),
8365 )
8366 .unwrap(),
8367 expected_real
8368 );
8369 let evaluation = plan
8370 .reduce_with_gradient(
8371 execution,
8372 ¶ms,
8373 &prepared,
8374 ReductionPlan::weighted_log_positive_real(),
8375 )
8376 .unwrap();
8377 assert!((evaluation.value() - expected_log).abs() < 1.0e-12);
8378 assert!((evaluation.gradient()[0] - expected_log_gradient).abs() < 1.0e-12);
8379 }
8380 }
8381 }
8382
8383 #[test]
8384 fn positive_reduction_reports_the_invalid_value() {
8385 let model = CompiledModel::from_expr(&event_scalar("x")).unwrap();
8386 let params = Arc::new(model.params().clone()).default_values();
8387 let plan = CpuBackend.prepare(&model);
8388 let dataset = Dataset::from_batch(
8389 EventBatch::from_events(
8390 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8391 [OwnedEvent::new(vec![], vec![-2.0])],
8392 )
8393 .unwrap(),
8394 );
8395 let execution = Execution::default();
8396 let prepared = plan.prepare_dataset(&execution, &dataset).unwrap();
8397
8398 assert!(matches!(
8399 plan.reduce(
8400 &execution,
8401 ¶ms,
8402 &prepared,
8403 ReductionPlan::weighted_log_positive_real(),
8404 ),
8405 Err(RuntimeError::Reduction(
8406 laddu_compile::ReductionError::NonPositiveValue {
8407 transform: laddu_compile::ReductionTransform::LogPositiveReal,
8408 value: -2.0,
8409 }
8410 ))
8411 ));
8412 }
8413
8414 #[test]
8415 fn evaluates_linear_algebra_nodes() {
8416 let a = matrix([[2.0, 0.0], [0.0, 4.0]]);
8417 let b = vector([8.0, 12.0]);
8418 let x = solve(a, b);
8419 let expr = dot(&x, vector([1.0, 1.0]));
8420 let model = CompiledModel::from_expr(&expr).unwrap();
8421 let params = Arc::new(model.params().clone()).default_values();
8422 let plan = CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8423
8424 assert_eq!(plan.evaluate(¶ms).unwrap(), Complex64::from(7.0));
8425 assert_eq!(plan.constant_factors.len(), 1);
8426 assert!(plan.constant_factors[0].get().is_some());
8427 }
8428
8429 #[cfg(feature = "jit")]
8430 #[test]
8431 fn auto_jit_matches_interpreter_for_complex_linear_algebra() {
8432 let diagonal = laddu_expr::Expr::from(parameter!("diagonal", initial: 4.0));
8433 let matrix = matrix([
8434 [complex(2.0, 0.5), complex(0.25, -0.1)],
8435 [complex(-0.2, 0.3), diagonal],
8436 ]);
8437 let rhs = vector([complex(8.0, 1.0), complex(12.0, -0.5)]);
8438 let solution = solve(matrix, rhs);
8439 let expression = dot(solution, vector([complex(1.0, -0.2), complex(0.5, 0.3)])).exp();
8440 let model = CompiledModel::from_expr(&expression).unwrap();
8441 let params = Arc::new(model.params().clone()).default_values();
8442 let automatic = CpuBackend.prepare(&model);
8443 let interpreted =
8444 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8445
8446 assert!(matches!(
8447 automatic.scalar_executor,
8448 Some(ScalarExecutor::Jit(_))
8449 ));
8450 assert!(matches!(
8451 automatic.gradient_executor,
8452 GradientExecutor::Jit(_)
8453 ));
8454 let actual = automatic.evaluate(¶ms).unwrap();
8455 let expected = interpreted.evaluate(¶ms).unwrap();
8456 assert!(
8457 (actual - expected).norm() < 1.0e-12,
8458 "{actual} != {expected}"
8459 );
8460 let actual = automatic.evaluate_with_gradient(¶ms).unwrap();
8461 let expected = interpreted.evaluate_with_gradient(¶ms).unwrap();
8462 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8463 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8464 assert!((actual - expected).norm() < 1.0e-12);
8465 }
8466 }
8467
8468 #[cfg(feature = "jit")]
8469 #[test]
8470 fn auto_jit_gradients_support_parameter_dependent_solve() {
8471 let x = event_scalar("x");
8472 let coupling = laddu_expr::Expr::from(parameter!("coupling", initial: 0.2));
8473 let drive = laddu_expr::Expr::from(parameter!("drive", initial: -0.4));
8474 let matrix = matrix([
8475 [x.clone() + 2.0, complex(coupling.clone(), 0.1)],
8476 [complex(-0.3, coupling), 3.0.into()],
8477 ]);
8478 let expression = solve(
8479 matrix,
8480 vector([x.clone().sin() + drive, complex(x.cos(), 0.5)]),
8481 )
8482 .component(1)
8483 .norm_sqr();
8484 let model = CompiledModel::from_expr(&expression).unwrap();
8485 let params = Arc::new(model.params().clone()).default_values();
8486 let automatic = CpuBackend.prepare(&model);
8487 let interpreted =
8488 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8489 let batch = EventBatch::from_events(
8490 Arc::new(Schema::new(std::iter::empty::<&str>(), ["x"], false).unwrap()),
8491 [
8492 OwnedEvent::new(vec![], vec![0.25]),
8493 OwnedEvent::new(vec![], vec![0.75]),
8494 OwnedEvent::new(vec![], vec![1.25]),
8495 ],
8496 )
8497 .unwrap();
8498 let automatic_cache = automatic.cache_event_batch(&batch).unwrap();
8499 let interpreted_cache = interpreted.cache_event_batch(&batch).unwrap();
8500
8501 assert!(matches!(
8502 automatic.scalar_executor,
8503 Some(ScalarExecutor::Jit(_))
8504 ));
8505 assert!(matches!(
8506 automatic.gradient_executor,
8507 GradientExecutor::Jit(_)
8508 ));
8509 let actual = automatic.evaluate_cache(¶ms, &automatic_cache).unwrap();
8510 let expected = interpreted
8511 .evaluate_cache(¶ms, &interpreted_cache)
8512 .unwrap();
8513 for (actual, expected) in actual.iter().zip(expected) {
8514 assert!(
8515 (*actual - expected).norm() < 1.0e-12,
8516 "{actual} != {expected}"
8517 );
8518 }
8519 let actual = automatic
8520 .evaluate_cache_with_gradient(¶ms, &automatic_cache)
8521 .unwrap();
8522 let expected = interpreted
8523 .evaluate_cache_with_gradient(¶ms, &interpreted_cache)
8524 .unwrap();
8525 for (actual, expected) in actual.iter().zip(&expected) {
8526 assert!((actual.value() - expected.value()).norm() < 1.0e-12);
8527 for (actual, expected) in actual.gradient().iter().zip(expected.gradient()) {
8528 assert!((actual - expected).norm() < 1.0e-12);
8529 }
8530 }
8531 let ir_interpreter = gradient_interpreter::GradientInterpreter::new(
8532 automatic.scalar_kernel.as_ref().unwrap(),
8533 model.params().free_params(),
8534 )
8535 .unwrap();
8536 for (row, expected) in expected.iter().enumerate() {
8537 let actual = ir_interpreter
8538 .evaluate(¶ms, Some((&automatic_cache, row)))
8539 .unwrap()
8540 .1;
8541 for (actual, expected) in actual.iter().zip(expected.gradient()) {
8542 assert!((actual - expected).norm() < 1.0e-12);
8543 }
8544 }
8545 for (row, actual) in actual.iter().enumerate() {
8546 for parameter in 0..params.layout().n_free() {
8547 let h = 1.0e-6;
8548 let id = params.layout().free_params()[parameter];
8549 let free_id = params.layout().free_id(id).unwrap().unwrap();
8550 let value = params.get(id).unwrap();
8551 let mut plus = params.clone();
8552 let mut minus = params.clone();
8553 plus.set_free(free_id, value + h).unwrap();
8554 minus.set_free(free_id, value - h).unwrap();
8555 let expected = (automatic.evaluate_cache(&plus, &automatic_cache).unwrap()[row]
8556 - automatic.evaluate_cache(&minus, &automatic_cache).unwrap()[row])
8557 / (2.0 * h);
8558 assert!((actual.gradient()[parameter] - expected).norm() < 1.0e-8);
8559 }
8560 }
8561 }
8562
8563 #[cfg(feature = "jit")]
8564 #[test]
8565 fn jit_and_interpreter_reject_singular_parameter_dependent_solve() {
8566 let scale = laddu_expr::Expr::from(parameter!("scale", initial: 1.0));
8567 let expression = solve(
8568 matrix([[scale.clone(), 2.0.into()], [scale * 2.0, 4.0.into()]]),
8569 vector([1.0, 2.0]),
8570 )
8571 .component(0);
8572 let model = CompiledModel::from_expr(&expression).unwrap();
8573 let params = Arc::new(model.params().clone()).default_values();
8574 let automatic = CpuBackend.prepare(&model);
8575 let interpreted =
8576 CpuBackend.prepare_with_execution_mode(&model, CpuExecutionMode::Interpreter);
8577
8578 assert!(matches!(
8579 automatic.gradient_executor,
8580 GradientExecutor::Jit(_)
8581 ));
8582 assert!(automatic.evaluate_with_gradient(¶ms).is_err());
8583 assert!(interpreted.evaluate_with_gradient(¶ms).is_err());
8584 }
8585
8586 #[test]
8587 fn optimized_and_unoptimized_plans_evaluate_the_same_expression() {
8588 let solved = solve(matrix([[2.0, 0.0], [0.0, 4.0]]), vector([8.0, 12.0]));
8589 let complex_offset = complex(
8590 parameter!("offset_re", initial: 1.5),
8591 parameter!("offset_im", initial: -0.5),
8592 );
8593 let polar_product = polar_complex(
8594 parameter!("mag1", initial: 2.0),
8595 parameter!("phase1", initial: 0.25),
8596 ) * polar_complex(
8597 parameter!("mag2", initial: 3.0),
8598 parameter!("phase2", initial: -0.5),
8599 );
8600 let expr = ((laddu_expr::event_scalar("mass") + 0.0) * 1.0
8601 + dot(solved, vector([1.0, 1.0]))
8602 + complex_offset.conj().real()
8603 + polar_product.real()
8604 + parameter!("unused", initial: 3.0) * 0.0)
8605 .norm_sqr();
8606 let no_optimization = CompileOptions::without_optimizations();
8607 let optimized = CompiledModel::from_expr(&expr).unwrap();
8608 let unoptimized = CompiledModel::from_expr_with_options(&expr, &no_optimization).unwrap();
8609 let optimized_params = Arc::new(optimized.params().clone()).default_values();
8610 let unoptimized_params = Arc::new(unoptimized.params().clone()).default_values();
8611 let event = HashMap::from([("mass".to_owned(), 2.0)]);
8612
8613 let optimized = CpuBackend
8614 .prepare(&optimized)
8615 .evaluate_with_event_and_gradient(&optimized_params, &event)
8616 .unwrap();
8617 let unoptimized = CpuBackend
8618 .prepare(&unoptimized)
8619 .evaluate_with_event_and_gradient(&unoptimized_params, &event)
8620 .unwrap();
8621 assert_eq!(optimized.value(), unoptimized.value());
8622 for (optimized, unoptimized) in optimized.gradient().iter().zip(unoptimized.gradient()) {
8623 assert!((optimized - unoptimized).norm() < 1.0e-12);
8624 }
8625 }
8626}