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