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