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