1use std::cell::UnsafeCell;
14use std::mem::MaybeUninit;
15
16use crate::jet_scalar::{
17 Order2, RuntimeJetScalar, SymmetricQuadraticCoefficients, aggregate_shared_source_derivatives,
18 canonical_shared_source_schedule,
19};
20
21#[derive(Clone, Copy, Debug)]
22struct GraphNode {
23 value: f64,
24 support: u16,
25 edge_start: u16,
26 edge_len: u16,
27 primary_axis: u8,
28}
29
30#[derive(Clone, Copy, Debug)]
31enum CurvatureEvent {
32 RankOne {
33 owner: u8,
34 input: u8,
35 second: f64,
36 },
37 Cross {
38 owner: u8,
39 left: u8,
40 right: u8,
41 },
42 Diagonal {
43 owner: u8,
44 term_start: u16,
45 len: u16,
46 },
47 Projected {
48 owner: u8,
49 curvature_start: u16,
50 support: u16,
51 },
52}
53
54const MAX_PRIMARY_DIMENSION: usize = 16;
55const MAX_QUADRATIC_ARITY: usize = 32;
56const MAX_GRAPH_NODES: usize = 64;
57const MAX_GRAPH_EDGES: usize = MAX_GRAPH_NODES * MAX_GRAPH_NODES;
58const NO_PRIMARY_AXIS: u8 = u8::MAX;
59const MAX_PROJECTED_CURVATURE_VALUES: usize =
60 MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION * (MAX_PRIMARY_DIMENSION + 1) / 2;
61const EMPTY_NODE: GraphNode = GraphNode {
62 value: 0.0,
63 support: 0,
64 edge_start: 0,
65 edge_len: 0,
66 primary_axis: NO_PRIMARY_AXIS,
67};
68const EMPTY_CURVATURE_EVENT: CurvatureEvent = CurvatureEvent::RankOne {
69 owner: 0,
70 input: 0,
71 second: 0.0,
72};
73
74struct InlineScalars {
80 slots: [MaybeUninit<f64>; MAX_QUADRATIC_ARITY],
81 len: usize,
82}
83
84impl InlineScalars {
85 #[inline(always)]
86 fn initialize_zeros(storage: &mut MaybeUninit<Self>, len: usize) -> &mut Self {
87 assert!(
88 len <= MAX_QUADRATIC_ARITY,
89 "inline scalar capacity exceeded"
90 );
91 let scalars = storage.as_mut_ptr();
92 unsafe {
96 std::ptr::addr_of_mut!((*scalars).len).write(len);
97 let slots = &mut *std::ptr::addr_of_mut!((*scalars).slots);
98 for slot in &mut slots[..len] {
99 slot.write(0.0);
100 }
101 &mut *scalars
102 }
103 }
104
105 #[inline(always)]
106 fn as_slice(&self) -> &[f64] {
107 unsafe { std::slice::from_raw_parts(self.slots.as_ptr().cast::<f64>(), self.len) }
110 }
111
112 #[inline(always)]
113 fn as_mut_slice(&mut self) -> &mut [f64] {
114 unsafe { std::slice::from_raw_parts_mut(self.slots.as_mut_ptr().cast::<f64>(), self.len) }
116 }
117}
118
119#[derive(Debug)]
120struct GraphTape {
121 dimension: usize,
122 node_len: usize,
123 edge_len: usize,
124 event_len: usize,
125 diagonal_len: usize,
126 projected_curvature_len: usize,
127 nodes: [GraphNode; MAX_GRAPH_NODES],
128 gradients: [f64; MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION],
129 adjoints: [f64; MAX_GRAPH_NODES],
130 edge_parents: [u8; MAX_GRAPH_EDGES],
131 edge_firsts: [f64; MAX_GRAPH_EDGES],
132 events: [CurvatureEvent; MAX_GRAPH_NODES],
133 diagonal_inputs: [u8; MAX_GRAPH_EDGES],
134 diagonal_seconds: [f64; MAX_GRAPH_EDGES],
135 projected_curvatures: [f64; MAX_PROJECTED_CURVATURE_VALUES],
136}
137
138impl GraphTape {
139 fn new() -> Self {
140 Self {
141 dimension: 0,
142 node_len: 0,
143 edge_len: 0,
144 event_len: 0,
145 diagonal_len: 0,
146 projected_curvature_len: 0,
147 nodes: [EMPTY_NODE; MAX_GRAPH_NODES],
148 gradients: [0.0; MAX_GRAPH_NODES * MAX_PRIMARY_DIMENSION],
149 adjoints: [0.0; MAX_GRAPH_NODES],
150 edge_parents: [0; MAX_GRAPH_EDGES],
151 edge_firsts: [0.0; MAX_GRAPH_EDGES],
152 events: [EMPTY_CURVATURE_EVENT; MAX_GRAPH_NODES],
153 diagonal_inputs: [0; MAX_GRAPH_EDGES],
154 diagonal_seconds: [0.0; MAX_GRAPH_EDGES],
155 projected_curvatures: [0.0; MAX_PROJECTED_CURVATURE_VALUES],
156 }
157 }
158}
159
160trait HessianSink<const K: usize> {
161 fn reset(&mut self);
162 fn add_upper(&mut self, row: usize, column: usize, value: f64);
163 fn reflect_upper(&mut self);
164}
165
166struct ArrayHessianSink<'a, const K: usize>(&'a mut [[f64; K]; K]);
167
168impl<const K: usize> HessianSink<K> for ArrayHessianSink<'_, K> {
169 #[inline(always)]
170 fn reset(&mut self) {
171 }
173
174 #[inline(always)]
175 fn add_upper(&mut self, row: usize, column: usize, value: f64) {
176 self.0[row][column] += value;
177 }
178
179 #[inline(always)]
180 fn reflect_upper(&mut self) {
181 for row in 0..K {
182 for column in row + 1..K {
183 self.0[column][row] = self.0[row][column];
184 }
185 }
186 }
187}
188
189struct RowMajorHessianSink<'a, const K: usize>(&'a mut [f64]);
190
191impl<const K: usize> HessianSink<K> for RowMajorHessianSink<'_, K> {
192 #[inline(always)]
193 fn reset(&mut self) {
194 self.0.fill(0.0);
195 }
196
197 #[inline(always)]
198 fn add_upper(&mut self, row: usize, column: usize, value: f64) {
199 self.0[row * K + column] += value;
200 }
201
202 #[inline(always)]
203 fn reflect_upper(&mut self) {
204 for row in 0..K {
205 for column in row + 1..K {
206 self.0[column * K + row] = self.0[row * K + column];
207 }
208 }
209 }
210}
211
212#[derive(Debug)]
218pub struct Order2GraphWorkspace {
219 tape: UnsafeCell<Box<GraphTape>>,
220}
221
222impl Default for Order2GraphWorkspace {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228impl Order2GraphWorkspace {
229 #[must_use]
231 pub fn new() -> Self {
232 Self {
233 tape: UnsafeCell::new(Box::new(GraphTape::new())),
234 }
235 }
236
237 pub fn reset(&mut self, dimension: usize) {
239 assert!(
240 dimension <= MAX_PRIMARY_DIMENSION,
241 "compiled graph supports at most {MAX_PRIMARY_DIMENSION} primaries"
242 );
243 let tape = self.tape.get_mut().as_mut();
244 tape.dimension = dimension;
245 tape.node_len = 0;
246 tape.edge_len = 0;
247 tape.event_len = 0;
248 tape.diagonal_len = 0;
249 tape.projected_curvature_len = 0;
250 }
251
252 #[inline(always)]
258 fn tape(&self) -> &GraphTape {
259 unsafe { (&*self.tape.get()).as_ref() }
263 }
264
265 #[inline(always)]
266 fn tape_mut(&self) -> &mut GraphTape {
267 unsafe { (&mut *self.tape.get()).as_mut() }
270 }
271
272 #[inline(always)]
273 fn push<const K: usize>(
274 tape: &mut GraphTape,
275 value: f64,
276 support: u16,
277 gradient: [f64; K],
278 edge_start: usize,
279 ) -> usize {
280 assert_eq!(tape.dimension, K, "compiled graph dimension mismatch");
281 assert!(
282 tape.node_len < MAX_GRAPH_NODES,
283 "compiled graph node capacity exceeded"
284 );
285 let node = tape.node_len;
286 let edge_len = tape.edge_len - edge_start;
287 tape.nodes[node] = GraphNode {
288 value,
289 support,
290 edge_start: edge_start as u16,
291 edge_len: edge_len as u16,
292 primary_axis: NO_PRIMARY_AXIS,
293 };
294 tape.gradients[node * K..(node + 1) * K].copy_from_slice(&gradient);
295 tape.node_len += 1;
296 node
297 }
298
299 #[inline(always)]
300 fn push_edge(tape: &mut GraphTape, parent: usize, first: f64) {
301 assert!(
302 tape.edge_len < MAX_GRAPH_EDGES,
303 "compiled graph edge capacity exceeded"
304 );
305 assert!(
306 parent < tape.node_len,
307 "compiled graph edge parent must name an existing node"
308 );
309 tape.edge_parents[tape.edge_len] = parent as u8;
310 tape.edge_firsts[tape.edge_len] = first;
311 tape.edge_len += 1;
312 }
313
314 #[inline(always)]
315 fn push_event(tape: &mut GraphTape, event: CurvatureEvent) {
316 assert!(
317 tape.event_len < MAX_GRAPH_NODES,
318 "compiled graph curvature-event capacity exceeded"
319 );
320 tape.events[tape.event_len] = event;
321 tape.event_len += 1;
322 }
323
324 #[inline(always)]
325 fn push_diagonal_term(tape: &mut GraphTape, input: usize, second: f64) {
326 assert!(
327 tape.diagonal_len < MAX_GRAPH_EDGES,
328 "compiled graph diagonal-term capacity exceeded"
329 );
330 assert!(
331 input < tape.node_len,
332 "compiled graph diagonal input must name an existing node"
333 );
334 tape.diagonal_inputs[tape.diagonal_len] = input as u8;
335 tape.diagonal_seconds[tape.diagonal_len] = second;
336 tape.diagonal_len += 1;
337 }
338
339 #[inline(always)]
340 fn lower_into<const K: usize, H: HessianSink<K>>(
341 &self,
342 output: usize,
343 gradient: &mut [f64],
344 hessian: &mut H,
345 ) -> f64 {
346 let tape = self.tape_mut();
347 assert_eq!(tape.dimension, K, "compiled graph dimension mismatch");
348 assert!(output < tape.node_len, "compiled graph output is absent");
349 assert_eq!(gradient.len(), K, "compiled graph gradient width mismatch");
350 tape.adjoints[..tape.node_len].fill(0.0);
351 tape.adjoints[output] = 1.0;
352
353 gradient.copy_from_slice(&tape.gradients[output * K..(output + 1) * K]);
354 hessian.reset();
355
356 for node_index in (0..=output).rev() {
357 let adjoint = tape.adjoints[node_index];
358 if adjoint == 0.0 {
359 continue;
360 }
361 let node = tape.nodes[node_index];
362 let edge_start = node.edge_start as usize;
363 for edge in edge_start..edge_start + node.edge_len as usize {
364 let parent = tape.edge_parents[edge] as usize;
365 tape.adjoints[parent] += adjoint * tape.edge_firsts[edge];
366 }
367 }
368
369 for event_index in (0..tape.event_len).rev() {
370 match tape.events[event_index] {
371 CurvatureEvent::RankOne {
372 owner,
373 input,
374 second,
375 } => {
376 let owner_adjoint = tape.adjoints[owner as usize];
377 if owner_adjoint == 0.0 {
378 continue;
379 }
380 let input = input as usize;
381 let curvature_scale = owner_adjoint * second;
382 for_each_supported_upper(tape.nodes[input].support, |primary, other| {
383 hessian.add_upper(
384 primary,
385 other,
386 curvature_scale
387 * tape.gradients[input * K + primary]
388 * tape.gradients[input * K + other],
389 );
390 });
391 }
392 CurvatureEvent::Cross { owner, left, right } => {
393 let owner_adjoint = tape.adjoints[owner as usize];
394 if owner_adjoint == 0.0 {
395 continue;
396 }
397 let left = left as usize;
398 let right = right as usize;
399 for_each_supported_upper(
400 tape.nodes[left].support | tape.nodes[right].support,
401 |primary, other| {
402 let left_primary = tape.gradients[left * K + primary];
403 let right_primary = tape.gradients[right * K + primary];
404 let curvature = left_primary * tape.gradients[right * K + other]
405 + right_primary * tape.gradients[left * K + other];
406 hessian.add_upper(primary, other, owner_adjoint * curvature);
407 },
408 );
409 }
410 CurvatureEvent::Diagonal {
411 owner,
412 term_start,
413 len,
414 } => {
415 let owner_adjoint = tape.adjoints[owner as usize];
416 if owner_adjoint == 0.0 {
417 continue;
418 }
419 let term_start = term_start as usize;
420 for term in term_start..term_start + len as usize {
421 let input = tape.diagonal_inputs[term] as usize;
422 let curvature_scale = owner_adjoint * tape.diagonal_seconds[term];
423 if curvature_scale == 0.0 {
424 continue;
425 }
426 for_each_supported_upper(tape.nodes[input].support, |primary, other| {
427 hessian.add_upper(
428 primary,
429 other,
430 curvature_scale
431 * tape.gradients[input * K + primary]
432 * tape.gradients[input * K + other],
433 );
434 });
435 }
436 }
437 CurvatureEvent::Projected {
438 owner,
439 curvature_start,
440 support,
441 } => {
442 let owner_adjoint = tape.adjoints[owner as usize];
443 if owner_adjoint == 0.0 {
444 continue;
445 }
446 let mut curvature_offset = 0;
447 let curvature_start = curvature_start as usize;
448 for_each_supported_upper_by_column(support, |primary, other| {
449 hessian.add_upper(
450 primary,
451 other,
452 owner_adjoint
453 * tape.projected_curvatures[curvature_start + curvature_offset],
454 );
455 curvature_offset += 1;
456 });
457 }
458 }
459 }
460
461 hessian.reflect_upper();
462 tape.nodes[output].value
463 }
464}
465
466#[inline(always)]
467fn for_each_supported_upper_by_column(support: u16, mut visit: impl FnMut(usize, usize)) {
468 let mut columns = support;
469 while columns != 0 {
470 let other = columns.trailing_zeros() as usize;
471 columns &= columns - 1;
472 let mut rows = support & (u16::MAX >> (MAX_PRIMARY_DIMENSION - other - 1));
473 while rows != 0 {
474 let primary = rows.trailing_zeros() as usize;
475 rows &= rows - 1;
476 visit(primary, other);
477 }
478 }
479}
480
481#[inline(always)]
483fn supported_upper_offset(support: u16, primary: usize, other: usize) -> usize {
484 let primary_rank = (support & ((1_u16 << primary) - 1)).count_ones() as usize;
485 let other_rank = (support & ((1_u16 << other) - 1)).count_ones() as usize;
486 other_rank * (other_rank + 1) / 2 + primary_rank
487}
488
489#[inline(always)]
490fn for_each_supported_upper(mut rows: u16, mut visit: impl FnMut(usize, usize)) {
491 while rows != 0 {
492 let primary = rows.trailing_zeros() as usize;
493 rows &= rows - 1;
494 let mut columns = rows | (1_u16 << primary);
495 while columns != 0 {
496 let other = columns.trailing_zeros() as usize;
497 columns &= columns - 1;
498 visit(primary, other);
499 }
500 }
501}
502
503#[derive(Clone, Copy, Debug)]
508pub struct Order2Graph<'arena, const K: usize> {
509 workspace: &'arena Order2GraphWorkspace,
510 node: usize,
511}
512
513impl<'arena, const K: usize> Order2Graph<'arena, K> {
514 #[must_use]
516 pub fn into_order2(self) -> Order2<K> {
517 let mut out = crate::jet_tower::Tower2::zero();
518 let mut hessian = ArrayHessianSink(&mut out.h);
519 out.v = self
520 .workspace
521 .lower_into(self.node, &mut out.g, &mut hessian);
522 Order2(out)
523 }
524
525 #[must_use]
530 pub fn lower_into(self, gradient: &mut [f64], hessian_row_major: &mut [f64]) -> f64 {
531 assert_eq!(gradient.len(), K, "compiled graph gradient width mismatch");
532 assert_eq!(
533 hessian_row_major.len(),
534 K * K,
535 "compiled graph Hessian width mismatch"
536 );
537 let mut hessian = RowMajorHessianSink::<K>(hessian_row_major);
538 self.workspace.lower_into(self.node, gradient, &mut hessian)
539 }
540
541 #[inline(always)]
542 fn assert_compatible(&self, other: &Self) {
543 assert!(
544 std::ptr::eq(self.workspace, other.workspace),
545 "compiled graph scalars belong to different workspaces"
546 );
547 }
548
549 #[inline(always)]
550 fn unary(&self, value: f64, first: f64, second: f64) -> Self {
551 let tape = self.workspace.tape_mut();
552 let mut gradient = [0.0; K];
553 for primary in 0..K {
554 gradient[primary] = first * tape.gradients[self.node * K + primary];
555 }
556 let support = tape.nodes[self.node].support;
557 let owner = tape.node_len;
558 let edge_start = tape.edge_len;
559 Order2GraphWorkspace::push_edge(tape, self.node, first);
560 Order2GraphWorkspace::push_event(
561 tape,
562 CurvatureEvent::RankOne {
563 owner: owner as u8,
564 input: self.node as u8,
565 second,
566 },
567 );
568 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
569 Self {
570 workspace: self.workspace,
571 node,
572 }
573 }
574}
575
576impl<'arena, const K: usize> RuntimeJetScalar<'arena> for Order2Graph<'arena, K> {
577 type Workspace = Order2GraphWorkspace;
578
579 #[inline(always)]
580 fn constant(c: f64, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
581 assert_eq!(dimension, K, "compiled graph dimension mismatch");
582 let tape = workspace.tape_mut();
583 let edge_start = tape.edge_len;
584 let node = Order2GraphWorkspace::push(tape, c, 0, [0.0; K], edge_start);
585 Self { workspace, node }
586 }
587
588 #[inline(always)]
589 fn variable(x: f64, axis: usize, dimension: usize, workspace: &'arena Self::Workspace) -> Self {
590 assert_eq!(dimension, K, "compiled graph dimension mismatch");
591 assert!(axis < K, "compiled graph variable axis out of bounds");
592 let mut gradient = [0.0; K];
593 gradient[axis] = 1.0;
594 let tape = workspace.tape_mut();
595 let edge_start = tape.edge_len;
596 let node = Order2GraphWorkspace::push(tape, x, 1_u16 << axis, gradient, edge_start);
597 tape.nodes[node].primary_axis = axis as u8;
598 Self { workspace, node }
599 }
600
601 #[inline(always)]
602 fn constant_like(&self, c: f64) -> Self {
603 Self::constant(c, K, self.workspace)
604 }
605
606 #[inline(always)]
607 fn with_value(&self, value: f64) -> Self {
608 self.unary(value, 1.0, 0.0)
609 }
610
611 #[inline(always)]
612 fn symmetric_quadratic_form<C: SymmetricQuadraticCoefficients>(
613 inputs: &[Self],
614 coefficients: &C,
615 dimension: usize,
616 workspace: &'arena Self::Workspace,
617 ) -> Self {
618 assert_eq!(dimension, K, "compiled graph dimension mismatch");
619 assert_eq!(inputs.len(), coefficients.dimension());
620 assert!(
621 inputs.len() <= MAX_QUADRATIC_ARITY,
622 "compiled graph quadratic arity exceeds graph capacity"
623 );
624 assert!(
625 inputs
626 .iter()
627 .all(|input| std::ptr::eq(input.workspace, workspace)),
628 "compiled graph quadratic inputs belong to different workspaces"
629 );
630
631 let input_dimension = inputs.len();
632 let mut values_storage = MaybeUninit::uninit();
633 let values = InlineScalars::initialize_zeros(&mut values_storage, input_dimension);
634 let mut support = 0_u16;
635 {
636 let tape = workspace.tape();
637 for (value, input) in values.as_mut_slice().iter_mut().zip(inputs) {
638 *value = tape.nodes[input.node].value;
639 support |= tape.nodes[input.node].support;
640 }
641 }
642 let mut input_primary_axes = [NO_PRIMARY_AXIS; MAX_QUADRATIC_ARITY];
648 let all_inputs_primary = {
649 let tape = workspace.tape();
650 inputs.iter().enumerate().all(|(position, input)| {
651 let primary_axis = tape.nodes[input.node].primary_axis;
652 if primary_axis == NO_PRIMARY_AXIS {
653 false
654 } else {
655 input_primary_axes[position] = primary_axis;
656 true
657 }
658 })
659 };
660 let mut projected_values_storage = MaybeUninit::uninit();
661 let projected_values =
662 InlineScalars::initialize_zeros(&mut projected_values_storage, input_dimension);
663 coefficients.multiply(values.as_slice(), projected_values.as_mut_slice());
664 let value = values
665 .as_slice()
666 .iter()
667 .zip(projected_values.as_slice())
668 .map(|(&input, &projected)| input * projected)
669 .sum();
670
671 let supported_dimension = support.count_ones() as usize;
672 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
673 let curvature_start = {
674 let tape = workspace.tape_mut();
675 assert!(
676 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
677 "compiled graph projected-curvature capacity exceeded"
678 );
679 let start = tape.projected_curvature_len;
680 tape.projected_curvature_len += curvature_len;
681 start
682 };
683 let mut curvature_offset = 0;
684 if all_inputs_primary {
685 workspace.tape_mut().projected_curvatures
690 [curvature_start..curvature_start + curvature_len]
691 .fill(0.0);
692 let mut direction_storage = MaybeUninit::uninit();
693 let direction =
694 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
695 let mut projected_direction_storage = MaybeUninit::uninit();
696 let projected_direction =
697 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
698 coefficients.visit_upper_triangle(
699 direction.as_mut_slice(),
700 projected_direction.as_mut_slice(),
701 |row, column, coefficient| {
702 let row_axis = input_primary_axes[row] as usize;
703 let column_axis = input_primary_axes[column] as usize;
704 let primary = row_axis.min(column_axis);
705 let other = row_axis.max(column_axis);
706 let offset = supported_upper_offset(support, primary, other);
707 let repeated_axis_multiplicity = if row != column && row_axis == column_axis {
708 2.0
709 } else {
710 1.0
711 };
712 workspace.tape_mut().projected_curvatures[curvature_start + offset] +=
713 2.0 * repeated_axis_multiplicity * coefficient;
714 },
715 );
716 curvature_offset = curvature_len;
717 } else {
718 let mut direction_storage = MaybeUninit::uninit();
723 let direction =
724 InlineScalars::initialize_zeros(&mut direction_storage, input_dimension);
725 let mut projected_direction_storage = MaybeUninit::uninit();
726 let projected_direction =
727 InlineScalars::initialize_zeros(&mut projected_direction_storage, input_dimension);
728 let mut supported_columns = support;
729 while supported_columns != 0 {
730 let other = supported_columns.trailing_zeros() as usize;
731 supported_columns &= supported_columns - 1;
732 {
733 let tape = workspace.tape();
734 for (channel, input) in direction.as_mut_slice().iter_mut().zip(inputs) {
735 *channel = tape.gradients[input.node * K + other];
736 }
737 }
738 coefficients.multiply(direction.as_slice(), projected_direction.as_mut_slice());
739 let mut supported_rows =
740 support & (u16::MAX >> (MAX_PRIMARY_DIMENSION - other - 1));
741 while supported_rows != 0 {
742 let primary = supported_rows.trailing_zeros() as usize;
743 supported_rows &= supported_rows - 1;
744 let curvature = {
745 let tape = workspace.tape();
746 inputs
747 .iter()
748 .zip(projected_direction.as_slice())
749 .map(|(input, &projected)| {
750 tape.gradients[input.node * K + primary] * projected
751 })
752 .sum::<f64>()
753 };
754 workspace.tape_mut().projected_curvatures[curvature_start + curvature_offset] =
755 2.0 * curvature;
756 curvature_offset += 1;
757 }
758 }
759 }
760 assert_eq!(
761 curvature_offset, curvature_len,
762 "compiled graph projected-curvature schedule must fill its exact packed range"
763 );
764
765 let tape = workspace.tape_mut();
766 let owner = tape.node_len;
767 let edge_start = tape.edge_len;
768 let mut gradient = [0.0; K];
769 for axis in 0..input_dimension {
770 let first = 2.0 * projected_values.as_slice()[axis];
771 Order2GraphWorkspace::push_edge(tape, inputs[axis].node, first);
772 if all_inputs_primary {
773 let primary = tape.nodes[inputs[axis].node].primary_axis as usize;
774 gradient[primary] += first;
775 } else {
776 for primary in 0..K {
777 gradient[primary] += first * tape.gradients[inputs[axis].node * K + primary];
778 }
779 }
780 }
781 Order2GraphWorkspace::push_event(
782 tape,
783 CurvatureEvent::Projected {
784 owner: owner as u8,
785 curvature_start: curvature_start as u16,
786 support,
787 },
788 );
789 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
790 Self { workspace, node }
791 }
792
793 #[inline(always)]
794 fn linear_combination(
795 inputs: &[Self],
796 weights: &[f64],
797 dimension: usize,
798 workspace: &'arena Self::Workspace,
799 ) -> Self {
800 assert_eq!(dimension, K, "compiled graph dimension mismatch");
801 assert_eq!(inputs.len(), weights.len());
802 assert!(
803 inputs
804 .iter()
805 .all(|input| std::ptr::eq(input.workspace, workspace)),
806 "compiled graph linear inputs belong to different workspaces"
807 );
808 let tape = workspace.tape_mut();
809 let all_inputs_primary = inputs
810 .iter()
811 .all(|input| tape.nodes[input.node].primary_axis != NO_PRIMARY_AXIS);
812 let edge_start = tape.edge_len;
813 let mut value = 0.0;
814 let mut gradient = [0.0; K];
815 let mut support = 0_u16;
816 for (input, &weight) in inputs.iter().zip(weights) {
817 value += weight * tape.nodes[input.node].value;
818 Order2GraphWorkspace::push_edge(tape, input.node, weight);
819 support |= tape.nodes[input.node].support;
820 if all_inputs_primary {
821 let primary = tape.nodes[input.node].primary_axis as usize;
822 gradient[primary] += weight;
823 } else {
824 for primary in 0..K {
825 gradient[primary] += weight * tape.gradients[input.node * K + primary];
826 }
827 }
828 }
829 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
830 Self { workspace, node }
831 }
832
833 #[inline(always)]
834 fn multiply_add(&self, right: &Self, addend: &Self) -> Self {
835 self.assert_compatible(right);
836 self.assert_compatible(addend);
837 let tape = self.workspace.tape_mut();
838 let left_value = tape.nodes[self.node].value;
839 let right_value = tape.nodes[right.node].value;
840 let mut gradient = [0.0; K];
841 for primary in 0..K {
842 gradient[primary] = left_value * tape.gradients[right.node * K + primary]
843 + tape.gradients[self.node * K + primary] * right_value
844 + tape.gradients[addend.node * K + primary];
845 }
846 let value = left_value * right_value + tape.nodes[addend.node].value;
847 let support = tape.nodes[self.node].support
848 | tape.nodes[right.node].support
849 | tape.nodes[addend.node].support;
850 let owner = tape.node_len;
851 let edge_start = tape.edge_len;
852 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
853 Order2GraphWorkspace::push_edge(tape, right.node, left_value);
854 Order2GraphWorkspace::push_edge(tape, addend.node, 1.0);
855 Order2GraphWorkspace::push_event(
856 tape,
857 CurvatureEvent::Cross {
858 owner: owner as u8,
859 left: self.node as u8,
860 right: right.node as u8,
861 },
862 );
863 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
864 Self {
865 workspace: self.workspace,
866 node,
867 }
868 }
869
870 #[inline(always)]
871 fn composed_sum(
872 inputs: &[Self],
873 derivative_stacks: &[[f64; 5]],
874 dimension: usize,
875 workspace: &'arena Self::Workspace,
876 ) -> Self {
877 assert_eq!(dimension, K, "compiled graph dimension mismatch");
878 assert_eq!(inputs.len(), derivative_stacks.len());
879 assert!(
880 inputs
881 .iter()
882 .all(|input| std::ptr::eq(input.workspace, workspace)),
883 "compiled graph composed inputs belong to different workspaces"
884 );
885 let tape = workspace.tape_mut();
886 let owner = tape.node_len;
887 let edge_start = tape.edge_len;
888 let term_start = tape.diagonal_len;
889 let mut value = 0.0;
890 let mut gradient = [0.0; K];
891 let mut support = 0_u16;
892 for (input, stack) in inputs.iter().zip(derivative_stacks) {
893 value += stack[0];
894 Order2GraphWorkspace::push_edge(tape, input.node, stack[1]);
895 Order2GraphWorkspace::push_diagonal_term(tape, input.node, stack[2]);
896 support |= tape.nodes[input.node].support;
897 for primary in 0..K {
898 gradient[primary] += stack[1] * tape.gradients[input.node * K + primary];
899 }
900 }
901 Order2GraphWorkspace::push_event(
902 tape,
903 CurvatureEvent::Diagonal {
904 owner: owner as u8,
905 term_start: term_start as u16,
906 len: inputs.len() as u16,
907 },
908 );
909 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
910 Self { workspace, node }
911 }
912
913 #[inline(always)]
914 fn product(&self, right: &Self) -> Self {
915 self.mul(right)
916 }
917
918 #[inline(always)]
919 fn affine_compose(
920 &self,
921 input_scale: f64,
922 input_shift: f64,
923 derivative_stack: [f64; 5],
924 ) -> Self {
925 assert!(input_shift.is_finite(), "affine input shift must be finite");
926 self.unary(
927 derivative_stack[0],
928 derivative_stack[1] * input_scale,
929 derivative_stack[2] * input_scale * input_scale,
930 )
931 }
932
933 #[inline(always)]
934 fn affine_composed_sum(
935 inputs: &[Self],
936 input_scales: &[f64],
937 derivative_stacks: &[[f64; 5]],
938 dimension: usize,
939 workspace: &'arena Self::Workspace,
940 ) -> Self {
941 assert_eq!(dimension, K, "compiled graph dimension mismatch");
942 assert_eq!(inputs.len(), input_scales.len());
943 assert_eq!(inputs.len(), derivative_stacks.len());
944 assert!(
945 inputs
946 .iter()
947 .all(|input| std::ptr::eq(input.workspace, workspace)),
948 "compiled graph composed inputs belong to different workspaces"
949 );
950 let tape = workspace.tape_mut();
951 let owner = tape.node_len;
952 let edge_start = tape.edge_len;
953 let term_start = tape.diagonal_len;
954 let mut value = 0.0;
955 let mut gradient = [0.0; K];
956 let mut support = 0_u16;
957 for ((input, &input_scale), stack) in inputs.iter().zip(input_scales).zip(derivative_stacks)
958 {
959 let first = stack[1] * input_scale;
960 let second = stack[2] * input_scale * input_scale;
961 value += stack[0];
962 Order2GraphWorkspace::push_edge(tape, input.node, first);
963 Order2GraphWorkspace::push_diagonal_term(tape, input.node, second);
964 support |= tape.nodes[input.node].support;
965 for primary in 0..K {
966 gradient[primary] += first * tape.gradients[input.node * K + primary];
967 }
968 }
969 Order2GraphWorkspace::push_event(
970 tape,
971 CurvatureEvent::Diagonal {
972 owner: owner as u8,
973 term_start: term_start as u16,
974 len: inputs.len() as u16,
975 },
976 );
977 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
978 Self { workspace, node }
979 }
980
981 #[inline(always)]
982 fn shared_multiply_add_affine_composed_sum<const N: usize>(
983 lefts: &[&Self; N],
984 right: &Self,
985 addend: &Self,
986 addend_scales: &[f64; N],
987 input_scales: &[f64; N],
988 derivative_stacks: &[[f64; 5]; N],
989 dimension: usize,
990 workspace: &'arena Self::Workspace,
991 ) -> Self {
992 assert_eq!(dimension, K, "compiled graph dimension mismatch");
993 assert!(
994 lefts
995 .iter()
996 .all(|input| std::ptr::eq(input.workspace, workspace))
997 && (N == 0 || std::ptr::eq(right.workspace, workspace)),
998 "compiled fused product-composition inputs belong to different workspaces"
999 );
1000 let addend_live = addend_scales.iter().any(|&scale| scale != 0.0);
1001 assert!(
1002 !addend_live || std::ptr::eq(addend.workspace, workspace),
1003 "live compiled fused addends belong to different workspaces"
1004 );
1005
1006 let tape = workspace.tape_mut();
1007 let right_node = right.node;
1008 let addend_node = addend.node;
1009 let (representatives, term_sources, source_count) =
1010 canonical_shared_source_schedule::<N>(|term, representative| {
1011 lefts[term].node == lefts[representative].node
1012 && addend_scales[term] == addend_scales[representative]
1013 });
1014 let (value, source_derivatives) =
1015 aggregate_shared_source_derivatives(&term_sources, input_scales, derivative_stacks);
1016 let mut source_gradients = [[0.0; K]; N];
1017 let mut source_primary_axes = [NO_PRIMARY_AXIS; N];
1018 let mut gradient = [0.0; K];
1019 let mut support = 0_u16;
1020 let mut right_first = 0.0;
1021 let mut addend_first = 0.0;
1022 if N != 0 {
1023 support |= tape.nodes[right_node].support;
1024 }
1025 if addend_live {
1026 support |= tape.nodes[addend_node].support;
1027 }
1028 for source in 0..source_count {
1029 let term = representatives[source];
1030 let left = lefts[term].node;
1031 let left_value = tape.nodes[left].value;
1032 let right_value = tape.nodes[right_node].value;
1033 let first = source_derivatives[source][1];
1034 let left_primary_axis = tape.nodes[left].primary_axis;
1035 source_primary_axes[source] = left_primary_axis;
1036 support |= tape.nodes[left].support;
1037 right_first += first * left_value;
1038 addend_first += first * addend_scales[term];
1039 for primary in 0..K {
1040 let product_gradient = left_value * tape.gradients[right_node * K + primary];
1041 let inner_gradient = if addend_scales[term] == 0.0 {
1042 product_gradient
1043 } else if addend_scales[term] == 1.0 {
1044 product_gradient + tape.gradients[addend_node * K + primary]
1045 } else {
1046 product_gradient
1047 + addend_scales[term] * tape.gradients[addend_node * K + primary]
1048 };
1049 source_gradients[source][primary] = inner_gradient;
1050 }
1051 if left_primary_axis == NO_PRIMARY_AXIS {
1052 for primary in 0..K {
1053 let left_contribution = tape.gradients[left * K + primary] * right_value;
1054 source_gradients[source][primary] += left_contribution;
1055 gradient[primary] += first * left_contribution;
1056 }
1057 } else {
1058 let primary = left_primary_axis as usize;
1059 source_gradients[source][primary] += right_value;
1060 gradient[primary] += first * right_value;
1061 }
1062 }
1063 if N != 0 {
1064 for primary in 0..K {
1065 gradient[primary] += right_first * tape.gradients[right_node * K + primary];
1066 }
1067 }
1068 if addend_live {
1069 for primary in 0..K {
1070 gradient[primary] += addend_first * tape.gradients[addend_node * K + primary];
1071 }
1072 }
1073
1074 let supported_dimension = support.count_ones() as usize;
1075 let curvature_len = supported_dimension * (supported_dimension + 1) / 2;
1076 assert!(
1077 curvature_len <= MAX_PROJECTED_CURVATURE_VALUES - tape.projected_curvature_len,
1078 "compiled graph projected-curvature capacity exceeded"
1079 );
1080 let curvature_start = tape.projected_curvature_len;
1081 tape.projected_curvature_len += curvature_len;
1082 let mut curvature_offset = 0;
1083 for_each_supported_upper_by_column(support, |primary, other| {
1084 let mut channel = 0.0;
1085 for source in 0..source_count {
1086 channel += source_derivatives[source][2]
1087 * source_gradients[source][primary]
1088 * source_gradients[source][other];
1089 if source_primary_axes[source] == NO_PRIMARY_AXIS {
1090 let term = representatives[source];
1091 let left = lefts[term].node;
1092 let cross = tape.gradients[left * K + primary]
1093 * tape.gradients[right_node * K + other]
1094 + tape.gradients[left * K + other]
1095 * tape.gradients[right_node * K + primary];
1096 channel += source_derivatives[source][1] * cross;
1097 }
1098 }
1099 tape.projected_curvatures[curvature_start + curvature_offset] = channel;
1100 curvature_offset += 1;
1101 });
1102 for source in 0..source_count {
1103 let primary_axis = source_primary_axes[source];
1104 if primary_axis == NO_PRIMARY_AXIS {
1105 continue;
1106 }
1107 let primary_axis = primary_axis as usize;
1108 let first = source_derivatives[source][1];
1109 let mut right_support = tape.nodes[right_node].support;
1110 while right_support != 0 {
1111 let right_axis = right_support.trailing_zeros() as usize;
1112 right_support &= right_support - 1;
1113 let primary = primary_axis.min(right_axis);
1114 let other = primary_axis.max(right_axis);
1115 let offset = supported_upper_offset(support, primary, other);
1116 let diagonal_scale = if primary_axis == right_axis { 2.0 } else { 1.0 };
1117 let cross = diagonal_scale * tape.gradients[right_node * K + right_axis];
1118 tape.projected_curvatures[curvature_start + offset] += first * cross;
1119 }
1120 }
1121 assert_eq!(
1122 curvature_offset, curvature_len,
1123 "compiled fused product-composition must fill its packed curvature"
1124 );
1125
1126 let owner = tape.node_len;
1127 let edge_start = tape.edge_len;
1128 for source in 0..source_count {
1129 let term = representatives[source];
1130 let left = lefts[term].node;
1131 let left_first = source_derivatives[source][1] * tape.nodes[right_node].value;
1132 Order2GraphWorkspace::push_edge(tape, left, left_first);
1133 }
1134 if N != 0 {
1135 Order2GraphWorkspace::push_edge(tape, right_node, right_first);
1136 }
1137 if addend_live {
1138 Order2GraphWorkspace::push_edge(tape, addend_node, addend_first);
1139 }
1140 Order2GraphWorkspace::push_event(
1141 tape,
1142 CurvatureEvent::Projected {
1143 owner: owner as u8,
1144 curvature_start: curvature_start as u16,
1145 support,
1146 },
1147 );
1148 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1149 Self { workspace, node }
1150 }
1151
1152 #[inline(always)]
1153 fn dimension(&self) -> usize {
1154 K
1155 }
1156
1157 #[inline(always)]
1158 fn value(&self) -> f64 {
1159 self.workspace.tape().nodes[self.node].value
1160 }
1161
1162 #[inline(always)]
1163 fn add(&self, other: &Self) -> Self {
1164 self.assert_compatible(other);
1165 let tape = self.workspace.tape_mut();
1166 let mut gradient = [0.0; K];
1167 for primary in 0..K {
1168 gradient[primary] =
1169 tape.gradients[self.node * K + primary] + tape.gradients[other.node * K + primary];
1170 }
1171 let value = tape.nodes[self.node].value + tape.nodes[other.node].value;
1172 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1173 let edge_start = tape.edge_len;
1174 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1175 Order2GraphWorkspace::push_edge(tape, other.node, 1.0);
1176 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1177 Self {
1178 workspace: self.workspace,
1179 node,
1180 }
1181 }
1182
1183 #[inline(always)]
1184 fn sub(&self, other: &Self) -> Self {
1185 self.assert_compatible(other);
1186 let tape = self.workspace.tape_mut();
1187 let mut gradient = [0.0; K];
1188 for primary in 0..K {
1189 gradient[primary] =
1190 tape.gradients[self.node * K + primary] - tape.gradients[other.node * K + primary];
1191 }
1192 let value = tape.nodes[self.node].value - tape.nodes[other.node].value;
1193 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1194 let edge_start = tape.edge_len;
1195 Order2GraphWorkspace::push_edge(tape, self.node, 1.0);
1196 Order2GraphWorkspace::push_edge(tape, other.node, -1.0);
1197 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1198 Self {
1199 workspace: self.workspace,
1200 node,
1201 }
1202 }
1203
1204 #[inline(always)]
1205 fn mul(&self, other: &Self) -> Self {
1206 self.assert_compatible(other);
1207 let tape = self.workspace.tape_mut();
1208 let left_value = tape.nodes[self.node].value;
1209 let right_value = tape.nodes[other.node].value;
1210 let mut gradient = [0.0; K];
1211 for primary in 0..K {
1212 gradient[primary] = left_value * tape.gradients[other.node * K + primary]
1213 + tape.gradients[self.node * K + primary] * right_value;
1214 }
1215 let support = tape.nodes[self.node].support | tape.nodes[other.node].support;
1216 let owner = tape.node_len;
1217 let edge_start = tape.edge_len;
1218 Order2GraphWorkspace::push_edge(tape, self.node, right_value);
1219 Order2GraphWorkspace::push_edge(tape, other.node, left_value);
1220 Order2GraphWorkspace::push_event(
1221 tape,
1222 CurvatureEvent::Cross {
1223 owner: owner as u8,
1224 left: self.node as u8,
1225 right: other.node as u8,
1226 },
1227 );
1228 let node = Order2GraphWorkspace::push(
1229 tape,
1230 left_value * right_value,
1231 support,
1232 gradient,
1233 edge_start,
1234 );
1235 Self {
1236 workspace: self.workspace,
1237 node,
1238 }
1239 }
1240
1241 #[inline(always)]
1242 fn neg(&self) -> Self {
1243 self.unary(-self.value(), -1.0, 0.0)
1244 }
1245
1246 #[inline(always)]
1247 fn scale(&self, scale: f64) -> Self {
1248 let tape = self.workspace.tape_mut();
1249 let mut gradient = [0.0; K];
1250 for primary in 0..K {
1251 gradient[primary] = scale * tape.gradients[self.node * K + primary];
1252 }
1253 let value = scale * tape.nodes[self.node].value;
1254 let support = tape.nodes[self.node].support;
1255 let edge_start = tape.edge_len;
1256 Order2GraphWorkspace::push_edge(tape, self.node, scale);
1257 let node = Order2GraphWorkspace::push(tape, value, support, gradient, edge_start);
1258 Self {
1259 workspace: self.workspace,
1260 node,
1261 }
1262 }
1263
1264 #[inline(always)]
1265 fn compose_unary(&self, derivatives: [f64; 5]) -> Self {
1266 self.unary(derivatives[0], derivatives[1], derivatives[2])
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use crate::jet_scalar::{FixedRuntimeJet, JetScalar};
1274 use crate::nested_dual::JetField;
1275 use std::cell::Cell;
1276
1277 #[test]
1278 fn lower_into_overwrites_every_channel_across_workspace_reset() {
1279 let mut workspace = Order2GraphWorkspace::new();
1280 workspace.reset(3);
1281 let x = Order2Graph::<3>::variable(0.5, 0, 3, &workspace);
1282 let y = Order2Graph::<3>::variable(-0.25, 1, 3, &workspace);
1283 let output = x.product(&y);
1284 let mut gradient = [f64::NAN; 3];
1285 let mut hessian = [17.0; 9];
1286
1287 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -0.125);
1288 assert_eq!(gradient, [-0.25, 0.5, 0.0]);
1289 assert_eq!(hessian, [0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1290
1291 workspace.reset(3);
1292 let x = Order2Graph::<3>::variable(0.25, 0, 3, &workspace);
1293 let z = Order2Graph::<3>::variable(0.5, 2, 3, &workspace);
1294 let output = Order2Graph::linear_combination(&[x, z], &[2.0, -4.0], 3, &workspace);
1295 gradient.fill(f64::NAN);
1296 hessian.fill(-9.0);
1297
1298 assert_eq!(output.lower_into(&mut gradient, &mut hessian), -1.5);
1299 assert_eq!(gradient, [2.0, 0.0, -4.0]);
1300 assert_eq!(hessian, [0.0; 9]);
1301 }
1302
1303 struct DenseSymmetric3([[f64; 3]; 3]);
1304
1305 impl SymmetricQuadraticCoefficients for DenseSymmetric3 {
1306 fn dimension(&self) -> usize {
1307 3
1308 }
1309
1310 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1311 for row in 0..3 {
1312 output[row] = (0..3)
1313 .map(|column| self.0[row][column] * input[column])
1314 .sum();
1315 }
1316 }
1317
1318 fn coefficient(&self, row: usize, column: usize) -> f64 {
1319 self.0[row][column]
1320 }
1321 }
1322
1323 struct MatrixFreeIdentity32<'arena> {
1324 workspace: &'arena Order2GraphWorkspace,
1325 }
1326
1327 impl SymmetricQuadraticCoefficients for MatrixFreeIdentity32<'_> {
1328 fn dimension(&self) -> usize {
1329 MAX_QUADRATIC_ARITY
1330 }
1331
1332 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1333 assert!(self.workspace.tape().node_len != 0);
1334 output.copy_from_slice(input);
1335 }
1336
1337 fn coefficient(&self, row: usize, column: usize) -> f64 {
1338 panic!(
1339 "compiled graph quadratic lowering must preserve matrix-free multiply, \
1340 but entry ({row}, {column}) was read densely"
1341 )
1342 }
1343 }
1344
1345 struct CountingIdentity3<'arena> {
1346 workspace: &'arena Order2GraphWorkspace,
1347 multiply_calls: Cell<usize>,
1348 }
1349
1350 impl SymmetricQuadraticCoefficients for CountingIdentity3<'_> {
1351 fn dimension(&self) -> usize {
1352 3
1353 }
1354
1355 fn multiply(&self, input: &[f64], output: &mut [f64]) {
1356 assert!(self.workspace.tape().node_len != 0);
1357 self.multiply_calls.set(self.multiply_calls.get() + 1);
1358 output.copy_from_slice(input);
1359 }
1360
1361 fn coefficient(&self, row: usize, column: usize) -> f64 {
1362 panic!(
1363 "compiled graph quadratic lowering must preserve matrix-free multiply, \
1364 but entry ({row}, {column}) was read densely"
1365 )
1366 }
1367 }
1368
1369 #[test]
1370 fn compiled_graph_accepts_maximum_quadratic_arity_plus_output_node() {
1371 let mut workspace = Order2GraphWorkspace::new();
1372 workspace.reset(MAX_PRIMARY_DIMENSION);
1373 let values: [f64; MAX_QUADRATIC_ARITY] =
1374 std::array::from_fn(|axis| 0.01 * (axis + 1) as f64);
1375 let vars: [Order2Graph<'_, MAX_PRIMARY_DIMENSION>; MAX_QUADRATIC_ARITY] =
1376 std::array::from_fn(|axis| {
1377 Order2Graph::variable(
1378 values[axis],
1379 axis % MAX_PRIMARY_DIMENSION,
1380 MAX_PRIMARY_DIMENSION,
1381 &workspace,
1382 )
1383 });
1384 let coefficients = MatrixFreeIdentity32 {
1385 workspace: &workspace,
1386 };
1387 let graph = Order2Graph::symmetric_quadratic_form(
1388 &vars,
1389 &coefficients,
1390 MAX_PRIMARY_DIMENSION,
1391 &workspace,
1392 )
1393 .into_order2();
1394
1395 let expected_value = values.iter().map(|value| value * value).sum::<f64>();
1396 let tolerance = 2.0e-13;
1397 assert!((graph.value() - expected_value).abs() <= tolerance);
1398 for primary in 0..MAX_PRIMARY_DIMENSION {
1399 let expected_gradient = 2.0 * (values[primary] + values[primary + 16]);
1400 assert!((graph.g()[primary] - expected_gradient).abs() <= tolerance);
1401 for other in 0..MAX_PRIMARY_DIMENSION {
1402 let expected_hessian = if primary == other { 4.0 } else { 0.0 };
1403 assert!((graph.h()[primary][other] - expected_hessian).abs() <= tolerance);
1404 }
1405 }
1406 }
1407
1408 #[test]
1409 fn compiled_graph_projects_only_sparse_supported_primary_directions() {
1410 let mut workspace = Order2GraphWorkspace::new();
1411 workspace.reset(MAX_PRIMARY_DIMENSION);
1412 let x = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1413 0.4,
1414 0,
1415 MAX_PRIMARY_DIMENSION,
1416 &workspace,
1417 );
1418 let y = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1419 -0.7,
1420 7,
1421 MAX_PRIMARY_DIMENSION,
1422 &workspace,
1423 );
1424 let z = Order2Graph::<MAX_PRIMARY_DIMENSION>::variable(
1425 1.1,
1426 15,
1427 MAX_PRIMARY_DIMENSION,
1428 &workspace,
1429 );
1430 let coefficients = CountingIdentity3 {
1431 workspace: &workspace,
1432 multiply_calls: Cell::new(0),
1433 };
1434 let graph = Order2Graph::symmetric_quadratic_form(
1435 &[x, y, z],
1436 &coefficients,
1437 MAX_PRIMARY_DIMENSION,
1438 &workspace,
1439 )
1440 .into_order2();
1441
1442 assert_eq!(coefficients.multiply_calls.get(), 4);
1443 for primary in 0..MAX_PRIMARY_DIMENSION {
1444 let active = matches!(primary, 0 | 7 | 15);
1445 let expected_gradient = match primary {
1446 0 => 0.8,
1447 7 => -1.4,
1448 15 => 2.2,
1449 _ => 0.0,
1450 };
1451 assert_eq!(graph.g()[primary], expected_gradient);
1452 for other in 0..MAX_PRIMARY_DIMENSION {
1453 assert_eq!(
1454 graph.h()[primary][other],
1455 if active && primary == other { 2.0 } else { 0.0 }
1456 );
1457 }
1458 }
1459 }
1460
1461 #[test]
1462 fn compiled_fused_addend_support_obeys_structural_coefficient() {
1463 let mut workspace = Order2GraphWorkspace::new();
1464 {
1465 workspace.reset(3);
1466 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1467 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1468 let omitted = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1469 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1470 &[&left],
1471 &right,
1472 &omitted,
1473 &[-0.0],
1474 &[1.0],
1475 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1476 3,
1477 &workspace,
1478 );
1479 assert_eq!(workspace.tape().nodes[output.node].support, 0b011);
1480 let channels = output.into_order2();
1481 assert!(channels.g()[2] == 0.0);
1482 assert!((0..3).all(|axis| channels.h()[axis][2] == 0.0));
1483 }
1484
1485 workspace.reset(3);
1486 let left = Order2Graph::<3>::variable(0.4, 0, 3, &workspace);
1487 let right = Order2Graph::<3>::variable(-0.7, 1, 3, &workspace);
1488 let live = Order2Graph::<3>::variable(1.1, 2, 3, &workspace);
1489 let output = Order2Graph::shared_multiply_add_affine_composed_sum(
1490 &[&left],
1491 &right,
1492 &live,
1493 &[1.0],
1494 &[1.0],
1495 &[[0.2, 0.0, 1.0, 0.0, 0.0]],
1496 3,
1497 &workspace,
1498 );
1499 assert_eq!(workspace.tape().nodes[output.node].support, 0b111);
1500 let channels = output.into_order2();
1501 assert_eq!(channels.g()[2], 0.0);
1502 assert_eq!(channels.h()[2][2], 1.0);
1503 }
1504
1505 fn mixed_primary_derived_fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1506 vars: &[S; 5],
1507 workspace: &'arena S::Workspace,
1508 ) -> S {
1509 let right =
1510 vars[4].affine_compose(1.2, -0.1, [0.7, -1.2, 0.45, 0.0, 0.0]);
1511 let derived_left = vars[2].multiply_add(&vars[3], &vars[0]);
1512 let addend = S::linear_combination(vars, &[0.3, -0.8, 0.5, 1.1, -0.4], 5, workspace);
1513 S::shared_multiply_add_affine_composed_sum(
1514 &[&vars[1], &vars[0], &vars[1], &derived_left],
1515 &right,
1516 &addend,
1517 &[1.0, 1.0, 1.0, -0.4],
1518 &[-1.0, -1.0, 1.0, 0.75],
1519 &[
1520 [0.4, -0.8, 0.3, 0.0, 0.0],
1521 [-0.2, 0.5, -0.7, 0.0, 0.0],
1522 [0.9, 1.1, 0.2, 0.0, 0.0],
1523 [-0.6, 0.4, 0.8, 0.0, 0.0],
1524 ],
1525 5,
1526 workspace,
1527 )
1528 }
1529
1530 #[test]
1531 fn compiled_fused_primary_scatter_matches_eager_with_mixed_lefts() {
1532 let values = [0.4, -0.7, 1.1, -0.3, 0.8];
1533 let eager_vars: [FixedRuntimeJet<Order2<5>, 5>; 5] = std::array::from_fn(|axis| {
1534 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1535 });
1536 let eager = mixed_primary_derived_fused_expression(&eager_vars, &()).into_inner();
1537
1538 let mut workspace = Order2GraphWorkspace::new();
1539 workspace.reset(5);
1540 let graph_vars: [Order2Graph<'_, 5>; 5] =
1541 std::array::from_fn(|axis| Order2Graph::variable(values[axis], axis, 5, &workspace));
1542 let graph_output = mixed_primary_derived_fused_expression(&graph_vars, &workspace);
1543 let output_node = workspace.tape().nodes[graph_output.node];
1544 assert_eq!(output_node.edge_len, 5);
1545 let graph = graph_output.into_order2();
1546
1547 let close = |actual: f64, expected: f64| {
1548 let tolerance = 2.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1549 assert!((actual - expected).abs() <= tolerance);
1550 };
1551 close(graph.value(), eager.value());
1552 for primary in 0..5 {
1553 close(graph.g()[primary], eager.g()[primary]);
1554 for other in 0..5 {
1555 close(graph.h()[primary][other], eager.h()[primary][other]);
1556 }
1557 }
1558 }
1559
1560 fn expression<'arena, S: RuntimeJetScalar<'arena>>(
1561 vars: &[S; 6],
1562 coefficients: &DenseSymmetric3,
1563 scales: &[f64; 4],
1564 stacks: &[[f64; 5]; 4],
1565 workspace: &'arena S::Workspace,
1566 ) -> S {
1567 let nonlinear = [
1568 vars[0].product(&vars[1]),
1569 vars[2].affine_compose(scales[0], scales[1], stacks[0]),
1570 vars[3].multiply_add(&vars[4], &vars[5]),
1571 ];
1572 let quadratic = S::symmetric_quadratic_form(&nonlinear, coefficients, 6, workspace);
1573 let linear = S::linear_combination(vars, &[0.2, -0.7, 1.1, 0.4, -0.3, 0.8], 6, workspace);
1574 let product = quadratic.product(&linear);
1575 S::affine_composed_sum(
1576 &[quadratic, linear, product, nonlinear[1].clone()],
1577 &[scales[0], scales[1], scales[2], scales[3]],
1578 stacks,
1579 6,
1580 workspace,
1581 )
1582 }
1583
1584 fn fused_expression<'arena, S: RuntimeJetScalar<'arena>>(
1585 vars: &[S; 6],
1586 scales: &[f64; 4],
1587 stacks: &[[f64; 5]; 4],
1588 workspace: &'arena S::Workspace,
1589 ) -> S {
1590 const N: usize = 10;
1591 let upstream = [
1592 vars[0].product(&vars[1]),
1593 vars[2].affine_compose(scales[0], scales[1], stacks[0]),
1594 vars[3].multiply_add(&vars[4], &vars[5]),
1595 ];
1596 let repeated_left = upstream[1].clone();
1597 let mut lefts: [&S; N] = std::array::from_fn(|term| &upstream[term % upstream.len()]);
1598 lefts[9] = &repeated_left;
1599 let right = &upstream[1];
1600 let addend = &vars[2];
1601 let addend_scales: [f64; N] = std::array::from_fn(|term| match term % 4 {
1602 0 => 0.0,
1603 1 => 1.0,
1604 2 => -0.75,
1605 _ => 0.35,
1606 });
1607 let mut input_scales: [f64; N] = std::array::from_fn(|term| match term {
1608 0 => 0.0,
1609 1 => -1.25,
1610 _ => scales[term % scales.len()],
1611 });
1612 input_scales[9] = 1.25;
1613 let derivative_stacks: [[f64; 5]; N] =
1614 std::array::from_fn(|term| stacks[term % stacks.len()]);
1615 S::shared_multiply_add_affine_composed_sum(
1616 &lefts,
1617 right,
1618 addend,
1619 &addend_scales,
1620 &input_scales,
1621 &derivative_stacks,
1622 6,
1623 workspace,
1624 )
1625 }
1626
1627 #[test]
1628 fn compiled_graph_matches_eager_order2_randomized_full_vgh() {
1629 fn sample(state: &mut u64) -> f64 {
1630 *state ^= *state << 13;
1631 *state ^= *state >> 7;
1632 *state ^= *state << 17;
1633 let unit = (*state >> 11) as f64 * (1.0 / ((1_u64 << 53) as f64));
1634 2.0 * unit - 1.0
1635 }
1636
1637 fn close(actual: f64, expected: f64, case: usize, label: &str) {
1638 let tolerance = 5.0e-12 * actual.abs().max(expected.abs()).max(1.0);
1639 assert!(
1640 (actual - expected).abs() <= tolerance,
1641 "case {case} {label}: graph={actual:+.16e}, eager={expected:+.16e}, tolerance={tolerance:.3e}"
1642 );
1643 }
1644
1645 let mut state = 0x932d_a660_5eed_f00d_u64;
1646 let mut workspace = Order2GraphWorkspace::new();
1647 let mut fused_workspace = Order2GraphWorkspace::new();
1648
1649 fused_workspace.reset(6);
1650 let empty_terms: [&Order2Graph<'_, 6>; 0] = [];
1651 let empty_shared = Order2Graph::constant(1.0, 6, &fused_workspace);
1652 let empty_scales: [f64; 0] = [];
1653 let empty_stacks: [[f64; 5]; 0] = [];
1654 let empty = Order2Graph::shared_multiply_add_affine_composed_sum(
1655 &empty_terms,
1656 &empty_shared,
1657 &empty_shared,
1658 &empty_scales,
1659 &empty_scales,
1660 &empty_stacks,
1661 6,
1662 &fused_workspace,
1663 )
1664 .into_order2();
1665 assert_eq!(empty.value().to_bits(), 0.0_f64.to_bits());
1666 assert!(empty.g().iter().all(|&channel| channel == 0.0));
1667 assert!(empty.h().iter().flatten().all(|&channel| channel == 0.0));
1668
1669 for case in 0..256 {
1670 let values: [f64; 6] = std::array::from_fn(|_| sample(&mut state));
1671 let scales: [f64; 4] = std::array::from_fn(|_| sample(&mut state));
1672 let stacks: [[f64; 5]; 4] =
1673 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1674 let raw: [[f64; 3]; 3] =
1675 std::array::from_fn(|_| std::array::from_fn(|_| sample(&mut state)));
1676 let coefficients = DenseSymmetric3([
1677 [raw[0][0], raw[0][1], raw[0][2]],
1678 [raw[0][1], raw[1][1], raw[1][2]],
1679 [raw[0][2], raw[1][2], raw[2][2]],
1680 ]);
1681
1682 let eager_vars: [FixedRuntimeJet<Order2<6>, 6>; 6] = std::array::from_fn(|axis| {
1683 FixedRuntimeJet::from_inner(Order2::variable(values[axis], axis))
1684 });
1685 let eager = expression(&eager_vars, &coefficients, &scales, &stacks, &()).into_inner();
1686 let eager_fused = fused_expression(&eager_vars, &scales, &stacks, &()).into_inner();
1687
1688 workspace.reset(6);
1689 let graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1690 Order2Graph::variable(values[axis], axis, 6, &workspace)
1691 });
1692 let graph =
1693 expression(&graph_vars, &coefficients, &scales, &stacks, &workspace).into_order2();
1694 fused_workspace.reset(6);
1695 let fused_graph_vars: [Order2Graph<'_, 6>; 6] = std::array::from_fn(|axis| {
1696 Order2Graph::variable(values[axis], axis, 6, &fused_workspace)
1697 });
1698 let graph_fused =
1699 fused_expression(&fused_graph_vars, &scales, &stacks, &fused_workspace)
1700 .into_order2();
1701
1702 close(graph.value(), eager.value(), case, "value");
1703 close(
1704 graph_fused.value(),
1705 eager_fused.value(),
1706 case,
1707 "fused value",
1708 );
1709 for primary in 0..6 {
1710 close(graph.g()[primary], eager.g()[primary], case, "gradient");
1711 close(
1712 graph_fused.g()[primary],
1713 eager_fused.g()[primary],
1714 case,
1715 "fused gradient",
1716 );
1717 for other in 0..6 {
1718 close(
1719 graph.h()[primary][other],
1720 eager.h()[primary][other],
1721 case,
1722 "Hessian",
1723 );
1724 close(
1725 graph_fused.h()[primary][other],
1726 eager_fused.h()[primary][other],
1727 case,
1728 "fused Hessian",
1729 );
1730 }
1731 }
1732 }
1733 }
1734}