1mod core_dynamic;
4mod core_indexing;
5mod core_reductions;
6mod core_structural;
7
8use std::collections::{HashMap, HashSet};
9
10use tenferro_ops::{dim_expr::DimExpr, ShapeExtent};
11use tenferro_runtime::program::{
12 CoreSemanticOp, FrozenProgram, ProgramBuildError, ProgramFinishError, ProgramImport,
13 ProgramInputSpec, ProgramQueryError, ProgramValue, SemanticOpRef, SemanticProgramBuilder,
14};
15use tenferro_runtime::{CompareDir, DType, DotGeneralConfig};
16
17use crate::semantic_extension::{AdValue, SemanticAdError, SemanticExtensionRuleSet};
18use core_dynamic::{dynamic_shape_vjp, linearize_dynamic_shape};
19use core_indexing::{indexing_vjp, linearize_indexing};
20use core_reductions::{linearize_nonlinear_reduction, nonlinear_reduction_vjp};
21use core_structural::{concatenate_vjp, linearize_concatenate, pad_vjp, slice_vjp};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum SemanticTransformRole {
26 Jvp,
28 Vjp,
30}
31
32#[derive(Debug, thiserror::Error)]
34pub enum SemanticAdTransformError {
35 #[error("semantic {role:?} {field} expects {expected} entries, got {actual}")]
37 ActivityArity {
38 role: SemanticTransformRole,
40 field: &'static str,
42 expected: usize,
44 actual: usize,
46 },
47 #[error("semantic {role:?} does not support active core operation {op}")]
49 UnsupportedCore {
50 role: SemanticTransformRole,
52 op: String,
54 },
55 #[error("semantic {role:?} does not support this semantic operation variant")]
57 UnsupportedOperationVariant {
58 role: SemanticTransformRole,
60 },
61 #[error("semantic {role:?} does not support derivative metadata: {message}")]
63 UnsupportedMetadata {
64 role: SemanticTransformRole,
66 message: String,
68 },
69 #[error("semantic AD source-program query failed: {0}")]
71 Query(#[from] ProgramQueryError),
72 #[error("semantic AD program construction failed: {0}")]
74 Build(#[from] ProgramBuildError),
75 #[error("semantic extension AD failed: {0}")]
77 Extension(#[from] SemanticAdError),
78 #[error("semantic AD program finalization failed: {0}")]
80 Finish(#[from] ProgramFinishError),
81 #[error("semantic AD transform cache failed: {0}")]
83 Cache(#[source] tenferro_runtime::Error),
84}
85
86#[derive(Clone, Debug)]
92pub struct SemanticAdProgram {
93 frozen: FrozenProgram,
94 derivative_input_indices: Box<[Option<usize>]>,
95 derivative_output_indices: Box<[Option<usize>]>,
96}
97
98struct ValueShapePlan {
99 shape: Vec<DimExpr>,
100 dynamic_axes: Vec<usize>,
101}
102
103impl SemanticAdProgram {
104 pub const fn frozen(&self) -> &FrozenProgram {
106 &self.frozen
107 }
108
109 pub fn derivative_input_indices(&self) -> &[Option<usize>] {
111 &self.derivative_input_indices
112 }
113
114 pub fn derivative_output_indices(&self) -> &[Option<usize>] {
116 &self.derivative_output_indices
117 }
118
119 pub fn into_frozen(self) -> FrozenProgram {
121 self.frozen
122 }
123
124 pub(crate) fn with_input_prefix_bindings_from(
125 &self,
126 source: &FrozenProgram,
127 ) -> Result<Self, ProgramFinishError> {
128 Ok(Self {
129 frozen: self.frozen.with_input_prefix_bindings_from(source)?,
130 derivative_input_indices: self.derivative_input_indices.clone(),
131 derivative_output_indices: self.derivative_output_indices.clone(),
132 })
133 }
134}
135
136pub fn semantic_jvp(
149 input: &FrozenProgram,
150 active_inputs: &[bool],
151 rules: &SemanticExtensionRuleSet,
152) -> Result<SemanticAdProgram, SemanticAdTransformError> {
153 validate_activity(
154 SemanticTransformRole::Jvp,
155 "active_inputs",
156 input.program.inputs().len(),
157 active_inputs.len(),
158 )?;
159 let mut builder = SemanticProgramBuilder::new();
160 let values = import_source(input, &mut builder)?;
161 let mut tangents = HashMap::new();
162 let mut derivative_input_indices = vec![None; input.program.inputs().len()];
163 let mut next_input = input.program.inputs().len();
164 for (index, source) in input.program.inputs().iter().copied().enumerate() {
165 if active_inputs[index] {
166 let imported_source = values[&source];
167 let tangent = builder.input(ProgramInputSpec::from_metadata(
168 builder.value_metadata(imported_source)?.clone(),
169 ))?;
170 derivative_input_indices[index] = Some(next_input);
171 next_input += 1;
172 tangents.insert(source, AdValue::Value(tangent));
173 } else {
174 tangents.insert(source, AdValue::Absent);
175 }
176 }
177
178 let live = source_output_liveness(input);
179 for operation in input.program.operations() {
180 let tangent_inputs: Vec<_> = operation
181 .inputs()
182 .iter()
183 .map(|value| tangents.get(value).copied().unwrap_or(AdValue::Absent))
184 .collect();
185 let active_outputs: Vec<_> = operation
186 .outputs()
187 .iter()
188 .map(|value| live.contains(value))
189 .collect();
190 let tangent_outputs = if tangent_inputs
191 .iter()
192 .all(|value| matches!(value, AdValue::Absent))
193 {
194 vec![AdValue::Absent; operation.outputs().len()].into_boxed_slice()
195 } else {
196 match operation.op() {
197 SemanticOpRef::Extension(_) => rules
198 .linearize_operation(
199 operation,
200 &mapped_values(operation.inputs(), &values),
201 &mapped_values(operation.outputs(), &values),
202 &tangent_inputs,
203 &active_outputs,
204 &mut builder,
205 )?
206 .tangent_outputs()
207 .into(),
208 SemanticOpRef::Core(op) => linearize_core(
209 op,
210 &mapped_values(operation.inputs(), &values),
211 &tangent_inputs,
212 &mut builder,
213 )?,
214 _ => {
215 return Err(SemanticAdTransformError::UnsupportedOperationVariant {
216 role: SemanticTransformRole::Jvp,
217 });
218 }
219 }
220 };
221 for (source, tangent) in operation.outputs().iter().copied().zip(tangent_outputs) {
222 tangents.insert(source, tangent);
223 }
224 }
225
226 let outputs = input
227 .program
228 .outputs()
229 .iter()
230 .map(|value| tangents.get(value).copied().unwrap_or(AdValue::Absent))
231 .collect();
232 finish_derivative(builder, derivative_input_indices, outputs)
233}
234
235pub fn semantic_vjp(
248 input: &FrozenProgram,
249 active_inputs: &[bool],
250 active_outputs: &[bool],
251 rules: &SemanticExtensionRuleSet,
252) -> Result<SemanticAdProgram, SemanticAdTransformError> {
253 validate_activity(
254 SemanticTransformRole::Vjp,
255 "active_inputs",
256 input.program.inputs().len(),
257 active_inputs.len(),
258 )?;
259 validate_activity(
260 SemanticTransformRole::Vjp,
261 "active_outputs",
262 input.program.outputs().len(),
263 active_outputs.len(),
264 )?;
265 let mut builder = SemanticProgramBuilder::new();
266 let values = import_source(input, &mut builder)?;
267 let forward_active = requested_input_reachability(input, active_inputs);
268 let mut cotangents = HashMap::new();
269 let mut derivative_input_indices = vec![None; input.program.outputs().len()];
270 let mut next_input = input.program.inputs().len();
271 for (index, source) in input.program.outputs().iter().copied().enumerate() {
272 if active_outputs[index] {
273 let imported_source = values[&source];
274 let cotangent = builder.input(ProgramInputSpec::from_metadata(
275 builder.value_metadata(imported_source)?.clone(),
276 ))?;
277 derivative_input_indices[index] = Some(next_input);
278 next_input += 1;
279 accumulate_cotangent(&mut builder, &mut cotangents, source, cotangent)?;
280 }
281 }
282
283 let operations: Vec<_> = input.program.operations().collect();
284 for operation in operations.into_iter().rev() {
285 let cotangent_outputs: Vec<_> = operation
286 .outputs()
287 .iter()
288 .map(|value| {
289 cotangents
290 .get(value)
291 .copied()
292 .map_or(AdValue::Absent, AdValue::Value)
293 })
294 .collect();
295 if cotangent_outputs
296 .iter()
297 .all(|value| matches!(value, AdValue::Absent))
298 {
299 continue;
300 }
301 let active_operation_inputs: Vec<_> = operation
302 .inputs()
303 .iter()
304 .map(|value| forward_active.contains(value))
305 .collect();
306 if active_operation_inputs.iter().all(|active| !active) {
307 continue;
308 }
309 let cotangent_inputs = match operation.op() {
310 SemanticOpRef::Extension(op) => {
311 if rules.lookup_primal_vjp(op.family_id()).is_some() {
312 rules.primal_vjp_operation(
313 operation,
314 &mapped_values(operation.inputs(), &values),
315 &mapped_values(operation.outputs(), &values),
316 &cotangent_outputs,
317 &active_operation_inputs,
318 &mut builder,
319 )?
320 } else {
321 let inactive_tangents = vec![AdValue::Absent; operation.inputs().len()];
322 let active_operation_outputs: Vec<_> = cotangent_outputs
323 .iter()
324 .map(|value| matches!(value, AdValue::Value(_)))
325 .collect();
326 let linearized = rules.linearize_operation(
327 operation,
328 &mapped_values(operation.inputs(), &values),
329 &mapped_values(operation.outputs(), &values),
330 &inactive_tangents,
331 &active_operation_outputs,
332 &mut builder,
333 )?;
334 rules.linear_transpose_operation(
335 operation,
336 &mapped_values(operation.inputs(), &values),
337 &mapped_values(operation.outputs(), &values),
338 &cotangent_outputs,
339 &active_operation_inputs,
340 linearized.residuals(),
341 &mut builder,
342 )?
343 }
344 }
345 SemanticOpRef::Core(op) => vjp_core(
346 op,
347 &mapped_values(operation.inputs(), &values),
348 &cotangent_outputs,
349 &active_operation_inputs,
350 &mut builder,
351 )?,
352 _ => {
353 return Err(SemanticAdTransformError::UnsupportedOperationVariant {
354 role: SemanticTransformRole::Vjp,
355 });
356 }
357 };
358 for (source, cotangent) in operation.inputs().iter().copied().zip(cotangent_inputs) {
359 if let AdValue::Value(cotangent) = cotangent {
360 accumulate_cotangent(&mut builder, &mut cotangents, source, cotangent)?;
361 }
362 }
363 }
364
365 let outputs = input
366 .program
367 .inputs()
368 .iter()
369 .enumerate()
370 .map(|(index, value)| {
371 if active_inputs[index] {
372 cotangents
373 .get(value)
374 .copied()
375 .map_or(AdValue::Absent, AdValue::Value)
376 } else {
377 AdValue::Absent
378 }
379 })
380 .collect();
381 finish_derivative(builder, derivative_input_indices, outputs)
382}
383
384fn import_source(
385 input: &FrozenProgram,
386 builder: &mut SemanticProgramBuilder,
387) -> Result<HashMap<ProgramValue, ProgramValue>, SemanticAdTransformError> {
388 let mut source_values = input.program.inputs().to_vec();
389 source_values.extend(
390 input
391 .program
392 .operations()
393 .flat_map(|operation| operation.outputs().iter().copied()),
394 );
395 let imported = builder.import(ProgramImport {
396 program: input.program.as_ref(),
397 bindings: &input.bindings,
398 roots: &source_values,
399 })?;
400 Ok(source_values
401 .into_iter()
402 .zip(imported.roots().iter().copied())
403 .collect())
404}
405
406fn mapped_values(
407 source: &[ProgramValue],
408 values: &HashMap<ProgramValue, ProgramValue>,
409) -> Vec<ProgramValue> {
410 source.iter().map(|value| values[value]).collect()
411}
412
413fn source_output_liveness(input: &FrozenProgram) -> HashSet<ProgramValue> {
414 let mut live: HashSet<_> = input.program.outputs().iter().copied().collect();
415 let operations: Vec<_> = input.program.operations().collect();
416 for operation in operations.into_iter().rev() {
417 if operation
418 .outputs()
419 .iter()
420 .any(|output| live.contains(output))
421 {
422 live.extend(operation.inputs().iter().copied());
423 }
424 }
425 live
426}
427
428fn requested_input_reachability(
429 input: &FrozenProgram,
430 active_inputs: &[bool],
431) -> HashSet<ProgramValue> {
432 let mut active: HashSet<_> = input
433 .program
434 .inputs()
435 .iter()
436 .copied()
437 .zip(active_inputs.iter().copied())
438 .filter_map(|(value, is_active)| is_active.then_some(value))
439 .collect();
440 for operation in input.program.operations() {
441 if operation
442 .inputs()
443 .iter()
444 .any(|value| active.contains(value))
445 {
446 active.extend(operation.outputs().iter().copied());
447 }
448 }
449 active
450}
451
452fn accumulate_cotangent(
453 builder: &mut SemanticProgramBuilder,
454 cotangents: &mut HashMap<ProgramValue, ProgramValue>,
455 source: ProgramValue,
456 cotangent: ProgramValue,
457) -> Result<(), ProgramBuildError> {
458 let combined = if let Some(existing) = cotangents.get(&source).copied() {
459 builder.add_op(CoreSemanticOp::Add, &[existing, cotangent])?[0]
460 } else {
461 cotangent
462 };
463 cotangents.insert(source, combined);
464 Ok(())
465}
466
467fn linearize_core(
468 op: &CoreSemanticOp,
469 primal_inputs: &[ProgramValue],
470 tangent_inputs: &[AdValue],
471 builder: &mut SemanticProgramBuilder,
472) -> Result<Box<[AdValue]>, SemanticAdTransformError> {
473 let output = match op {
474 CoreSemanticOp::Add => add_ad_values(builder, tangent_inputs[0], tangent_inputs[1])?,
475 CoreSemanticOp::Sub => sub_ad_values(builder, tangent_inputs[0], tangent_inputs[1])?,
476 CoreSemanticOp::Mul => {
477 let lhs = multiply_ad_value(builder, tangent_inputs[0], primal_inputs[1])?;
478 let rhs = multiply_ad_value(builder, tangent_inputs[1], primal_inputs[0])?;
479 add_ad_values(builder, lhs, rhs)?
480 }
481 CoreSemanticOp::Div => {
482 let lhs = divide_ad_value(builder, tangent_inputs[0], primal_inputs[1])?;
483 let rhs_numerator = multiply_ad_value(builder, tangent_inputs[1], primal_inputs[0])?;
484 let denominator =
485 builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], primal_inputs[1]])?[0];
486 let rhs = divide_ad_value(builder, rhs_numerator, denominator)?;
487 sub_ad_values(builder, lhs, rhs)?
488 }
489 CoreSemanticOp::Pow => {
490 let lhs = if matches!(tangent_inputs[0], AdValue::Value(_)) {
491 let one = one_like(builder, primal_inputs[1], SemanticTransformRole::Jvp)?;
492 let exponent_minus_one =
493 builder.add_op(CoreSemanticOp::Sub, &[primal_inputs[1], one])?[0];
494 let power = builder
495 .add_op(CoreSemanticOp::Pow, &[primal_inputs[0], exponent_minus_one])?[0];
496 let coefficient =
497 builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], power])?[0];
498 multiply_ad_value(builder, tangent_inputs[0], coefficient)?
499 } else {
500 AdValue::Absent
501 };
502 let rhs = if matches!(tangent_inputs[1], AdValue::Value(_)) {
503 let log = builder.add_op(CoreSemanticOp::Log, &[primal_inputs[0]])?[0];
504 let power =
505 builder.add_op(CoreSemanticOp::Pow, &[primal_inputs[0], primal_inputs[1]])?[0];
506 let coefficient = builder.add_op(CoreSemanticOp::Mul, &[log, power])?[0];
507 multiply_ad_value(builder, tangent_inputs[1], coefficient)?
508 } else {
509 AdValue::Absent
510 };
511 add_ad_values(builder, lhs, rhs)?
512 }
513 CoreSemanticOp::DotGeneral { config } => {
514 linearize_dot_general(builder, primal_inputs, tangent_inputs, config)?
515 }
516 CoreSemanticOp::Abs => {
517 let input_dtype = builder.value_metadata(primal_inputs[0])?.dtype();
518 let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_inputs[0]])?[0];
519 let coefficient = if is_complex_dtype(input_dtype) {
520 builder.add_op(CoreSemanticOp::Conj, &[sign])?[0]
521 } else {
522 sign
523 };
524 let tangent = multiply_ad_value(builder, tangent_inputs[0], coefficient)?;
525 convert_ad_value(builder, tangent, input_dtype, abs_output_dtype(input_dtype))?
526 }
527 CoreSemanticOp::Sign => linearize_sign(builder, primal_inputs[0], tangent_inputs[0])?,
528 CoreSemanticOp::Maximum | CoreSemanticOp::Minimum => {
529 linearize_extrema(builder, op, primal_inputs, tangent_inputs)?
530 }
531 CoreSemanticOp::Select => select_ad_values(
532 builder,
533 primal_inputs[0],
534 tangent_inputs[1],
535 tangent_inputs[2],
536 )?,
537 CoreSemanticOp::Clamp => linearize_clamp(builder, primal_inputs, tangent_inputs)?,
538 CoreSemanticOp::Neg | CoreSemanticOp::Conj => {
539 unary_ad_value(builder, op.clone(), tangent_inputs[0])?
540 }
541 CoreSemanticOp::Exp
542 | CoreSemanticOp::Log
543 | CoreSemanticOp::Sin
544 | CoreSemanticOp::Cos
545 | CoreSemanticOp::Tanh
546 | CoreSemanticOp::Sqrt
547 | CoreSemanticOp::Rsqrt
548 | CoreSemanticOp::Expm1
549 | CoreSemanticOp::Log1p => {
550 linearize_analytic_unary(builder, op, primal_inputs[0], tangent_inputs[0])?
551 }
552 CoreSemanticOp::Transpose { .. }
553 | CoreSemanticOp::Reshape { .. }
554 | CoreSemanticOp::BroadcastInDim { .. }
555 | CoreSemanticOp::ReduceSum { .. }
556 | CoreSemanticOp::ExtractDiag { .. }
557 | CoreSemanticOp::EmbedDiag { .. }
558 | CoreSemanticOp::Tril { .. }
559 | CoreSemanticOp::Triu { .. }
560 | CoreSemanticOp::Slice(_)
561 | CoreSemanticOp::Pad(_)
562 | CoreSemanticOp::Reverse { .. } => {
563 linearize_unary_core(builder, op.clone(), primal_inputs, tangent_inputs[0])?
564 }
565 CoreSemanticOp::ReduceSumSquares { axes } => core_reductions::linearize_sum_squares(
566 builder,
567 primal_inputs[0],
568 tangent_inputs[0],
569 axes,
570 )?,
571 CoreSemanticOp::Concatenate { axis, input_count } => {
572 linearize_concatenate(builder, primal_inputs, tangent_inputs, *axis, *input_count)?
573 }
574 CoreSemanticOp::Gather(_)
575 | CoreSemanticOp::GatherDynamicSliceSizes { .. }
576 | CoreSemanticOp::Scatter(_)
577 | CoreSemanticOp::DynamicSlice { .. }
578 | CoreSemanticOp::DynamicUpdateSlice => {
579 linearize_indexing(builder, op, primal_inputs, tangent_inputs)?
580 }
581 CoreSemanticOp::DynamicTruncate { .. } | CoreSemanticOp::PadToMatch { .. } => {
582 linearize_dynamic_shape(builder, op, primal_inputs, tangent_inputs[0])?
583 }
584 CoreSemanticOp::Convert { from, to } => {
585 if is_differentiable_dtype(*from) && is_differentiable_dtype(*to) {
586 linearize_unary_core(builder, op.clone(), primal_inputs, tangent_inputs[0])?
587 } else {
588 AdValue::Absent
589 }
590 }
591 CoreSemanticOp::ReduceProd { .. }
592 | CoreSemanticOp::ReduceMax { .. }
593 | CoreSemanticOp::ReduceMin { .. } => {
594 linearize_nonlinear_reduction(builder, op, primal_inputs, tangent_inputs[0])?
595 }
596 CoreSemanticOp::Rem
597 | CoreSemanticOp::Compare(_)
598 | CoreSemanticOp::ShapeOf { .. }
599 | CoreSemanticOp::Constant { .. } => AdValue::Absent,
600 _ => return Err(unsupported_core(SemanticTransformRole::Jvp, op)),
601 };
602 Ok([output].into())
603}
604
605fn vjp_core(
606 op: &CoreSemanticOp,
607 primal_inputs: &[ProgramValue],
608 cotangent_outputs: &[AdValue],
609 active_inputs: &[bool],
610 builder: &mut SemanticProgramBuilder,
611) -> Result<Box<[AdValue]>, SemanticAdTransformError> {
612 let cotangent = cotangent_outputs[0];
613 let inputs = match op {
614 CoreSemanticOp::Add => vec![
615 active_cotangent(builder, cotangent, active_inputs[0], primal_inputs[0])?,
616 active_cotangent(builder, cotangent, active_inputs[1], primal_inputs[1])?,
617 ],
618 CoreSemanticOp::Sub => {
619 let negated = unary_ad_value(builder, CoreSemanticOp::Neg, cotangent)?;
620 vec![
621 active_cotangent(builder, cotangent, active_inputs[0], primal_inputs[0])?,
622 normalize_ad_value(builder, negated, active_inputs[1], primal_inputs[1])?,
623 ]
624 }
625 CoreSemanticOp::Mul => {
626 let rhs_coefficient = conjugate_if_complex(builder, primal_inputs[1])?;
627 let lhs_coefficient = conjugate_if_complex(builder, primal_inputs[0])?;
628 let lhs = multiply_ad_value(builder, cotangent, rhs_coefficient)?;
629 let rhs = multiply_ad_value(builder, cotangent, lhs_coefficient)?;
630 vec![
631 normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
632 normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
633 ]
634 }
635 CoreSemanticOp::Div => {
636 let rhs_coefficient = conjugate_if_complex(builder, primal_inputs[1])?;
637 let lhs = divide_ad_value(builder, cotangent, rhs_coefficient)?;
638 let lhs_coefficient = conjugate_if_complex(builder, primal_inputs[0])?;
639 let denominator =
640 builder.add_op(CoreSemanticOp::Mul, &[rhs_coefficient, rhs_coefficient])?[0];
641 let rhs = multiply_ad_value(builder, cotangent, lhs_coefficient)?;
642 let rhs = divide_ad_value(builder, rhs, denominator)?;
643 let rhs = unary_ad_value(builder, CoreSemanticOp::Neg, rhs)?;
644 vec![
645 normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
646 normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
647 ]
648 }
649 CoreSemanticOp::Pow => {
650 let lhs = if active_inputs[0] {
651 let one = one_like(builder, primal_inputs[1], SemanticTransformRole::Vjp)?;
652 let exponent_minus_one =
653 builder.add_op(CoreSemanticOp::Sub, &[primal_inputs[1], one])?[0];
654 let power = builder
655 .add_op(CoreSemanticOp::Pow, &[primal_inputs[0], exponent_minus_one])?[0];
656 let coefficient =
657 builder.add_op(CoreSemanticOp::Mul, &[primal_inputs[1], power])?[0];
658 let coefficient = conjugate_if_complex(builder, coefficient)?;
659 multiply_ad_value(builder, cotangent, coefficient)?
660 } else {
661 AdValue::Absent
662 };
663 let rhs = if active_inputs[1] {
664 let log = builder.add_op(CoreSemanticOp::Log, &[primal_inputs[0]])?[0];
665 let power =
666 builder.add_op(CoreSemanticOp::Pow, &[primal_inputs[0], primal_inputs[1]])?[0];
667 let coefficient = builder.add_op(CoreSemanticOp::Mul, &[log, power])?[0];
668 let coefficient = conjugate_if_complex(builder, coefficient)?;
669 multiply_ad_value(builder, cotangent, coefficient)?
670 } else {
671 AdValue::Absent
672 };
673 vec![
674 normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
675 normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
676 ]
677 }
678 CoreSemanticOp::DotGeneral { config } => {
679 dot_general_vjp(builder, primal_inputs, cotangent, active_inputs, config)?
680 }
681 CoreSemanticOp::Abs => {
682 let input_dtype = builder.value_metadata(primal_inputs[0])?.dtype();
683 let output_dtype = abs_output_dtype(input_dtype);
684 let cotangent = convert_ad_value(builder, cotangent, output_dtype, input_dtype)?;
685 let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_inputs[0]])?[0];
686 let cotangent = multiply_ad_value(builder, cotangent, sign)?;
687 vec![normalize_ad_value(
688 builder,
689 cotangent,
690 active_inputs[0],
691 primal_inputs[0],
692 )?]
693 }
694 CoreSemanticOp::Sign => vec![AdValue::Absent],
695 CoreSemanticOp::Maximum | CoreSemanticOp::Minimum => {
696 extrema_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
697 }
698 CoreSemanticOp::Select => {
699 let (on_true, on_false) = split_select_cotangent(
700 builder,
701 primal_inputs[0],
702 cotangent,
703 active_inputs[1],
704 active_inputs[2],
705 )?;
706 vec![
707 AdValue::Absent,
708 normalize_ad_value(builder, on_true, active_inputs[1], primal_inputs[1])?,
709 normalize_ad_value(builder, on_false, active_inputs[2], primal_inputs[2])?,
710 ]
711 }
712 CoreSemanticOp::Clamp => clamp_vjp(builder, primal_inputs, cotangent, active_inputs)?,
713 CoreSemanticOp::Neg => {
714 let negated = unary_ad_value(builder, CoreSemanticOp::Neg, cotangent)?;
715 vec![normalize_ad_value(
716 builder,
717 negated,
718 active_inputs[0],
719 primal_inputs[0],
720 )?]
721 }
722 CoreSemanticOp::Conj => {
723 let conjugated = unary_ad_value(builder, CoreSemanticOp::Conj, cotangent)?;
724 vec![normalize_ad_value(
725 builder,
726 conjugated,
727 active_inputs[0],
728 primal_inputs[0],
729 )?]
730 }
731 CoreSemanticOp::Exp
732 | CoreSemanticOp::Log
733 | CoreSemanticOp::Sin
734 | CoreSemanticOp::Cos
735 | CoreSemanticOp::Tanh
736 | CoreSemanticOp::Sqrt
737 | CoreSemanticOp::Rsqrt
738 | CoreSemanticOp::Expm1
739 | CoreSemanticOp::Log1p => {
740 let coefficient = analytic_unary_coefficient(
741 builder,
742 op,
743 primal_inputs[0],
744 SemanticTransformRole::Vjp,
745 )?;
746 let coefficient = conjugate_if_complex(builder, coefficient)?;
747 let cotangent = multiply_ad_value(builder, cotangent, coefficient)?;
748 vec![normalize_ad_value(
749 builder,
750 cotangent,
751 active_inputs[0],
752 primal_inputs[0],
753 )?]
754 }
755 CoreSemanticOp::Transpose { perm } => {
756 let transposed = unary_ad_value(
757 builder,
758 CoreSemanticOp::Transpose {
759 perm: inverse_permutation(perm),
760 },
761 cotangent,
762 )?;
763 primary_cotangent(builder, transposed, active_inputs, primal_inputs, false)?
764 }
765 CoreSemanticOp::Reshape { .. } => {
766 let reshaped = reshape_ad_value_to_input(builder, cotangent, primal_inputs[0])?;
767 primary_cotangent(builder, reshaped, active_inputs, primal_inputs, false)?
768 }
769 CoreSemanticOp::BroadcastInDim { dims, .. } => {
770 let reduced =
771 transpose_broadcast(builder, cotangent, primal_inputs[0], dims.as_slice())?;
772 primary_cotangent(builder, reduced, active_inputs, primal_inputs, false)?
773 }
774 CoreSemanticOp::Convert { from, to } => {
775 let converted = if is_differentiable_dtype(*from) && is_differentiable_dtype(*to) {
776 unary_ad_value(
777 builder,
778 CoreSemanticOp::Convert {
779 from: *to,
780 to: *from,
781 },
782 cotangent,
783 )?
784 } else {
785 AdValue::Absent
786 };
787 primary_cotangent(builder, converted, active_inputs, primal_inputs, false)?
788 }
789 CoreSemanticOp::ReduceSum { axes } => {
790 let input_shape = value_shape_plan(
791 builder,
792 primal_inputs[0],
793 SemanticTransformRole::Vjp,
794 "reduce-sum input",
795 )?;
796 let dims = (0..input_shape.shape.len())
797 .filter(|axis| !axes.contains(axis))
798 .collect();
799 let broadcast = broadcast_ad_value_in_dim_to_shape(
800 builder,
801 cotangent,
802 primal_inputs[0],
803 &input_shape,
804 dims,
805 )?;
806 let broadcast = truncate_ad_value_to_dynamic_axes(
807 builder,
808 broadcast,
809 primal_inputs[0],
810 &input_shape.dynamic_axes,
811 )?;
812 primary_cotangent(builder, broadcast, active_inputs, primal_inputs, false)?
813 }
814 CoreSemanticOp::ReduceSumSquares { axes } => core_reductions::sum_squares_vjp(
815 builder,
816 primal_inputs[0],
817 cotangent,
818 active_inputs[0],
819 axes,
820 )?,
821 CoreSemanticOp::ExtractDiag { axis_a, axis_b } => {
822 let embedded = unary_ad_value(
823 builder,
824 CoreSemanticOp::EmbedDiag {
825 axis_a: if axis_a < axis_b { *axis_a } else { axis_a - 1 },
826 axis_b: *axis_b,
827 },
828 cotangent,
829 )?;
830 let padded = match embedded {
831 AdValue::Absent => AdValue::Absent,
832 AdValue::Value(value) => {
833 let value = builder.add_op(
834 CoreSemanticOp::PadToMatch { axis: *axis_a },
835 &[value, primal_inputs[0]],
836 )?[0];
837 AdValue::Value(
838 builder.add_op(
839 CoreSemanticOp::PadToMatch { axis: *axis_b },
840 &[value, primal_inputs[0]],
841 )?[0],
842 )
843 }
844 };
845 primary_cotangent(builder, padded, active_inputs, primal_inputs, false)?
846 }
847 CoreSemanticOp::EmbedDiag { axis_a, axis_b } => {
848 let source_axis = if axis_b <= axis_a {
849 axis_a + 1
850 } else {
851 *axis_a
852 };
853 let extracted = unary_ad_value(
854 builder,
855 CoreSemanticOp::ExtractDiag {
856 axis_a: source_axis,
857 axis_b: *axis_b,
858 },
859 cotangent,
860 )?;
861 let restored = if axis_b < axis_a {
862 let rank = builder.value_metadata(primal_inputs[0])?.shape().len();
863 let mut perm: Vec<_> = (0..rank).collect();
864 let diagonal_axis = perm.remove(*axis_b);
865 perm.insert(*axis_a, diagonal_axis);
866 unary_ad_value(builder, CoreSemanticOp::Transpose { perm }, extracted)?
867 } else {
868 extracted
869 };
870 primary_cotangent(builder, restored, active_inputs, primal_inputs, false)?
871 }
872 CoreSemanticOp::Tril { .. }
873 | CoreSemanticOp::Triu { .. }
874 | CoreSemanticOp::Reverse { .. } => {
875 let transformed = unary_ad_value(builder, op.clone(), cotangent)?;
876 primary_cotangent(builder, transformed, active_inputs, primal_inputs, false)?
877 }
878 CoreSemanticOp::Slice(config) => slice_vjp(
879 builder,
880 primal_inputs[0],
881 cotangent,
882 active_inputs[0],
883 config,
884 )?,
885 CoreSemanticOp::Pad(config) => pad_vjp(
886 builder,
887 primal_inputs[0],
888 cotangent,
889 active_inputs[0],
890 config,
891 )?,
892 CoreSemanticOp::Concatenate { axis, input_count } => concatenate_vjp(
893 builder,
894 primal_inputs,
895 cotangent,
896 active_inputs,
897 *axis,
898 *input_count,
899 )?,
900 CoreSemanticOp::Gather(_)
901 | CoreSemanticOp::GatherDynamicSliceSizes { .. }
902 | CoreSemanticOp::Scatter(_)
903 | CoreSemanticOp::DynamicSlice { .. }
904 | CoreSemanticOp::DynamicUpdateSlice => {
905 indexing_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
906 }
907 CoreSemanticOp::DynamicTruncate { .. } | CoreSemanticOp::PadToMatch { .. } => {
908 dynamic_shape_vjp(builder, op, primal_inputs, cotangent, active_inputs)?
909 }
910 CoreSemanticOp::ReduceProd { .. }
911 | CoreSemanticOp::ReduceMax { .. }
912 | CoreSemanticOp::ReduceMin { .. } => {
913 nonlinear_reduction_vjp(builder, op, primal_inputs, cotangent, active_inputs[0])?
914 }
915 CoreSemanticOp::Rem | CoreSemanticOp::Compare(_) => {
916 vec![AdValue::Absent, AdValue::Absent]
917 }
918 CoreSemanticOp::ShapeOf { .. } => vec![AdValue::Absent],
919 CoreSemanticOp::Constant { .. } => Vec::new(),
920 _ => return Err(unsupported_core(SemanticTransformRole::Vjp, op)),
921 };
922 Ok(inputs.into_boxed_slice())
923}
924
925fn linearize_unary_core(
926 builder: &mut SemanticProgramBuilder,
927 op: CoreSemanticOp,
928 primal_inputs: &[ProgramValue],
929 tangent: AdValue,
930) -> Result<AdValue, ProgramBuildError> {
931 let AdValue::Value(tangent) = tangent else {
932 return Ok(AdValue::Absent);
933 };
934 let mut inputs = Vec::with_capacity(primal_inputs.len());
935 inputs.push(tangent);
936 inputs.extend_from_slice(&primal_inputs[1..]);
937 Ok(AdValue::Value(builder.add_op(op, &inputs)?[0]))
938}
939
940fn linearize_dot_general(
941 builder: &mut SemanticProgramBuilder,
942 primal_inputs: &[ProgramValue],
943 tangent_inputs: &[AdValue],
944 config: &DotGeneralConfig,
945) -> Result<AdValue, SemanticAdTransformError> {
946 validate_dot_general_metadata(builder, primal_inputs, config, SemanticTransformRole::Jvp)?;
947 let mut terms = Vec::with_capacity(2);
948 if let AdValue::Value(tangent) = tangent_inputs[0] {
949 terms.push(
950 builder.add_op(
951 CoreSemanticOp::DotGeneral {
952 config: config.clone(),
953 },
954 &[tangent, primal_inputs[1]],
955 )?[0],
956 );
957 }
958 if let AdValue::Value(tangent) = tangent_inputs[1] {
959 terms.push(
960 builder.add_op(
961 CoreSemanticOp::DotGeneral {
962 config: config.clone(),
963 },
964 &[primal_inputs[0], tangent],
965 )?[0],
966 );
967 }
968 let mut terms = terms.into_iter();
969 let Some(mut result) = terms.next() else {
970 return Ok(AdValue::Absent);
971 };
972 for term in terms {
973 result = builder.add_op(CoreSemanticOp::Add, &[result, term])?[0];
974 }
975 Ok(AdValue::Value(result))
976}
977
978fn dot_general_vjp(
979 builder: &mut SemanticProgramBuilder,
980 primal_inputs: &[ProgramValue],
981 cotangent: AdValue,
982 active_inputs: &[bool],
983 config: &DotGeneralConfig,
984) -> Result<Vec<AdValue>, SemanticAdTransformError> {
985 let (lhs_rank, rhs_rank) =
986 validate_dot_general_metadata(builder, primal_inputs, config, SemanticTransformRole::Vjp)?;
987 let lhs_free = dot_general_free_dims(
988 lhs_rank,
989 &config.lhs_contracting_dims,
990 &config.lhs_batch_dims,
991 SemanticTransformRole::Vjp,
992 )?;
993 let rhs_free = dot_general_free_dims(
994 rhs_rank,
995 &config.rhs_contracting_dims,
996 &config.rhs_batch_dims,
997 SemanticTransformRole::Vjp,
998 )?;
999 let AdValue::Value(cotangent) = cotangent else {
1000 return Ok(vec![AdValue::Absent, AdValue::Absent]);
1001 };
1002 let mut result = vec![AdValue::Absent, AdValue::Absent];
1003
1004 if active_inputs[0] {
1005 let rhs = conjugate_if_complex(builder, primal_inputs[1])?;
1006 let (transpose_config, perm) =
1007 dot_general_transpose_plan_for_lhs(config, lhs_rank, rhs_rank, &lhs_free, &rhs_free)?;
1008 let value = builder.add_op(
1009 CoreSemanticOp::DotGeneral {
1010 config: transpose_config,
1011 },
1012 &[cotangent, rhs],
1013 )?[0];
1014 let value = transpose_if_needed(builder, value, &perm)?;
1015 result[0] = normalize_ad_value(builder, AdValue::Value(value), true, primal_inputs[0])?;
1016 }
1017 if active_inputs[1] {
1018 let lhs = conjugate_if_complex(builder, primal_inputs[0])?;
1019 let (transpose_config, perm) =
1020 dot_general_transpose_plan_for_rhs(config, lhs_rank, rhs_rank, &lhs_free, &rhs_free)?;
1021 let value = builder.add_op(
1022 CoreSemanticOp::DotGeneral {
1023 config: transpose_config,
1024 },
1025 &[lhs, cotangent],
1026 )?[0];
1027 let value = transpose_if_needed(builder, value, &perm)?;
1028 result[1] = normalize_ad_value(builder, AdValue::Value(value), true, primal_inputs[1])?;
1029 }
1030 Ok(result)
1031}
1032
1033fn validate_dot_general_metadata(
1034 builder: &SemanticProgramBuilder,
1035 primal_inputs: &[ProgramValue],
1036 config: &DotGeneralConfig,
1037 role: SemanticTransformRole,
1038) -> Result<(usize, usize), SemanticAdTransformError> {
1039 let lhs_rank = builder.value_metadata(primal_inputs[0])?.shape().len();
1040 let rhs_rank = builder.value_metadata(primal_inputs[1])?.shape().len();
1041 config
1042 .validate_dims_with_ranks(lhs_rank, rhs_rank)
1043 .map_err(|error| SemanticAdTransformError::UnsupportedMetadata {
1044 role,
1045 message: format!(
1046 "invalid dot_general dimensions for ranks {lhs_rank} and {rhs_rank}: {error}"
1047 ),
1048 })?;
1049 Ok((lhs_rank, rhs_rank))
1050}
1051
1052fn dot_general_free_dims(
1053 rank: usize,
1054 contracting: &[usize],
1055 batch: &[usize],
1056 role: SemanticTransformRole,
1057) -> Result<Vec<usize>, SemanticAdTransformError> {
1058 let mut bound = vec![false; rank];
1059 for &axis in batch.iter().chain(contracting) {
1060 let Some(slot) = bound.get_mut(axis) else {
1061 return Err(SemanticAdTransformError::UnsupportedMetadata {
1062 role,
1063 message: format!("dot_general axis {axis} is out of bounds for rank {rank}"),
1064 });
1065 };
1066 *slot = true;
1067 }
1068 Ok((0..rank).filter(|axis| !bound[*axis]).collect())
1069}
1070
1071fn dot_general_transpose_plan_for_lhs(
1072 config: &DotGeneralConfig,
1073 lhs_rank: usize,
1074 rhs_rank: usize,
1075 lhs_free: &[usize],
1076 rhs_free: &[usize],
1077) -> Result<(DotGeneralConfig, Vec<usize>), SemanticAdTransformError> {
1078 let batch_count = config.lhs_batch_dims.len();
1079 let output_rank = lhs_free.len() + rhs_free.len() + batch_count;
1080 let rhs_free_positions = (lhs_free.len()..lhs_free.len() + rhs_free.len()).collect();
1081 let rhs_contracting_order = dot_general_free_dims(
1082 rhs_rank,
1083 rhs_free,
1084 &config.rhs_batch_dims,
1085 SemanticTransformRole::Vjp,
1086 )?;
1087 let mut result_order = lhs_free.to_vec();
1088 for rhs_axis in rhs_contracting_order {
1089 let Some(pair) = config
1090 .rhs_contracting_dims
1091 .iter()
1092 .position(|&axis| axis == rhs_axis)
1093 else {
1094 return Err(dot_general_transpose_metadata_error(format!(
1095 "rhs contracting axis {rhs_axis} has no lhs pair"
1096 )));
1097 };
1098 result_order.push(config.lhs_contracting_dims[pair]);
1099 }
1100 result_order.extend(config.lhs_batch_dims.iter().copied());
1101 Ok((
1102 DotGeneralConfig {
1103 lhs_contracting_dims: rhs_free_positions,
1104 rhs_contracting_dims: rhs_free.to_vec(),
1105 lhs_batch_dims: (lhs_free.len() + rhs_free.len()..output_rank).collect(),
1106 rhs_batch_dims: config.rhs_batch_dims.clone(),
1107 },
1108 permutation_to_original_order(lhs_rank, &result_order)?,
1109 ))
1110}
1111
1112fn dot_general_transpose_plan_for_rhs(
1113 config: &DotGeneralConfig,
1114 lhs_rank: usize,
1115 rhs_rank: usize,
1116 lhs_free: &[usize],
1117 rhs_free: &[usize],
1118) -> Result<(DotGeneralConfig, Vec<usize>), SemanticAdTransformError> {
1119 let batch_count = config.lhs_batch_dims.len();
1120 let lhs_contracting_order = dot_general_free_dims(
1121 lhs_rank,
1122 lhs_free,
1123 &config.lhs_batch_dims,
1124 SemanticTransformRole::Vjp,
1125 )?;
1126 let mut result_order = Vec::with_capacity(rhs_rank);
1127 for lhs_axis in lhs_contracting_order {
1128 let Some(pair) = config
1129 .lhs_contracting_dims
1130 .iter()
1131 .position(|&axis| axis == lhs_axis)
1132 else {
1133 return Err(dot_general_transpose_metadata_error(format!(
1134 "lhs contracting axis {lhs_axis} has no rhs pair"
1135 )));
1136 };
1137 result_order.push(config.rhs_contracting_dims[pair]);
1138 }
1139 result_order.extend(rhs_free.iter().copied());
1140 result_order.extend(config.rhs_batch_dims.iter().copied());
1141 let output_rank = lhs_free.len() + rhs_free.len() + batch_count;
1142 Ok((
1143 DotGeneralConfig {
1144 lhs_contracting_dims: lhs_free.to_vec(),
1145 rhs_contracting_dims: (0..lhs_free.len()).collect(),
1146 lhs_batch_dims: config.lhs_batch_dims.clone(),
1147 rhs_batch_dims: (lhs_free.len() + rhs_free.len()..output_rank).collect(),
1148 },
1149 permutation_to_original_order(rhs_rank, &result_order)?,
1150 ))
1151}
1152
1153fn permutation_to_original_order(
1154 rank: usize,
1155 result_order: &[usize],
1156) -> Result<Vec<usize>, SemanticAdTransformError> {
1157 let mut permutation = vec![0; rank];
1158 for (result_axis, &original_axis) in result_order.iter().enumerate() {
1159 let Some(slot) = permutation.get_mut(original_axis) else {
1160 return Err(dot_general_transpose_metadata_error(format!(
1161 "dot_general transpose axis {original_axis} is out of bounds for rank {rank}"
1162 )));
1163 };
1164 *slot = result_axis;
1165 }
1166 Ok(permutation)
1167}
1168
1169fn transpose_if_needed(
1170 builder: &mut SemanticProgramBuilder,
1171 value: ProgramValue,
1172 permutation: &[usize],
1173) -> Result<ProgramValue, ProgramBuildError> {
1174 if permutation
1175 .iter()
1176 .enumerate()
1177 .all(|(axis, &mapped)| axis == mapped)
1178 {
1179 Ok(value)
1180 } else {
1181 Ok(builder.add_op(
1182 CoreSemanticOp::Transpose {
1183 perm: permutation.to_vec(),
1184 },
1185 &[value],
1186 )?[0])
1187 }
1188}
1189
1190fn dot_general_transpose_metadata_error(message: String) -> SemanticAdTransformError {
1191 SemanticAdTransformError::UnsupportedMetadata {
1192 role: SemanticTransformRole::Vjp,
1193 message,
1194 }
1195}
1196
1197fn primary_cotangent(
1198 builder: &mut SemanticProgramBuilder,
1199 cotangent: AdValue,
1200 active_inputs: &[bool],
1201 primal_inputs: &[ProgramValue],
1202 normalize: bool,
1203) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1204 let mut result = vec![AdValue::Absent; primal_inputs.len()];
1205 if active_inputs.first().copied().unwrap_or(false) {
1206 result[0] = if normalize {
1207 normalize_ad_value(builder, cotangent, true, primal_inputs[0])?
1208 } else {
1209 cotangent
1210 };
1211 }
1212 Ok(result)
1213}
1214
1215fn inverse_permutation(perm: &[usize]) -> Vec<usize> {
1216 let mut inverse = vec![0; perm.len()];
1217 for (axis, mapped) in perm.iter().copied().enumerate() {
1218 inverse[mapped] = axis;
1219 }
1220 inverse
1221}
1222
1223fn reshape_ad_value_to_input(
1224 builder: &mut SemanticProgramBuilder,
1225 value: AdValue,
1226 primal_input: ProgramValue,
1227) -> Result<AdValue, SemanticAdTransformError> {
1228 let shape = value_shape_plan(
1229 builder,
1230 primal_input,
1231 SemanticTransformRole::Vjp,
1232 "reshape input",
1233 )?;
1234 let reshaped = reshape_ad_value_to_shape(builder, value, primal_input, &shape)?;
1235 truncate_ad_value_to_dynamic_axes(builder, reshaped, primal_input, &shape.dynamic_axes)
1236}
1237
1238fn transpose_broadcast(
1239 builder: &mut SemanticProgramBuilder,
1240 value: AdValue,
1241 primal_input: ProgramValue,
1242 dims: &[usize],
1243) -> Result<AdValue, SemanticAdTransformError> {
1244 let AdValue::Value(mut value) = value else {
1245 return Ok(AdValue::Absent);
1246 };
1247 let input_shape = value_shape_plan(
1248 builder,
1249 primal_input,
1250 SemanticTransformRole::Vjp,
1251 "broadcast input",
1252 )?;
1253 let output_shape = value_shape_plan(
1254 builder,
1255 value,
1256 SemanticTransformRole::Vjp,
1257 "broadcast cotangent",
1258 )?;
1259 let mut reduce_axes: Vec<_> = (0..output_shape.shape.len())
1260 .filter(|axis| !dims.contains(axis))
1261 .collect();
1262 reduce_axes.extend(
1263 dims.iter()
1264 .copied()
1265 .enumerate()
1266 .filter_map(|(input_axis, output_axis)| {
1267 (matches!(
1268 input_shape.shape[input_axis],
1269 tenferro_ops::dim_expr::DimExpr::Const(1)
1270 ) && input_shape.shape[input_axis] != output_shape.shape[output_axis])
1271 .then_some(output_axis)
1272 }),
1273 );
1274 reduce_axes.sort_unstable();
1275 reduce_axes.dedup();
1276 if !reduce_axes.is_empty() {
1277 value = builder.add_op(
1278 CoreSemanticOp::ReduceSum {
1279 axes: reduce_axes.clone(),
1280 },
1281 &[value],
1282 )?[0];
1283 }
1284
1285 let remaining_output_axes: Vec<_> = (0..output_shape.shape.len())
1286 .filter(|axis| !reduce_axes.contains(axis))
1287 .collect();
1288 let perm: Vec<_> = dims
1289 .iter()
1290 .copied()
1291 .filter(|axis| !reduce_axes.contains(axis))
1292 .map(|axis| {
1293 remaining_output_axes
1294 .iter()
1295 .position(|candidate| *candidate == axis)
1296 .expect("broadcast dims survive unless reduced")
1297 })
1298 .collect();
1299 if perm.iter().copied().ne(0..perm.len()) {
1300 value = builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0];
1301 }
1302 if builder.value_metadata(value)?.shape() != builder.value_metadata(primal_input)?.shape() {
1303 value = reshape_value_to_shape(builder, value, primal_input, &input_shape)?;
1304 }
1305 value =
1306 truncate_value_to_dynamic_axes(builder, value, primal_input, &input_shape.dynamic_axes)?;
1307 Ok(AdValue::Value(value))
1308}
1309
1310fn exact_value_shape(
1311 builder: &SemanticProgramBuilder,
1312 value: ProgramValue,
1313 role: SemanticTransformRole,
1314 field: &'static str,
1315) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1316 exact_shape(builder.value_metadata(value)?.shape(), role, field)
1317}
1318
1319fn is_differentiable_dtype(dtype: DType) -> bool {
1320 matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
1321}
1322
1323fn is_complex_dtype(dtype: DType) -> bool {
1324 matches!(dtype, DType::C32 | DType::C64)
1325}
1326
1327fn abs_output_dtype(dtype: DType) -> DType {
1328 match dtype {
1329 DType::C32 => DType::F32,
1330 DType::C64 => DType::F64,
1331 other => other,
1332 }
1333}
1334
1335fn add_ad_values(
1336 builder: &mut SemanticProgramBuilder,
1337 lhs: AdValue,
1338 rhs: AdValue,
1339) -> Result<AdValue, ProgramBuildError> {
1340 match (lhs, rhs) {
1341 (AdValue::Absent, value) | (value, AdValue::Absent) => Ok(value),
1342 (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1343 builder.add_op(CoreSemanticOp::Add, &[lhs, rhs])?[0],
1344 )),
1345 }
1346}
1347
1348fn sub_ad_values(
1349 builder: &mut SemanticProgramBuilder,
1350 lhs: AdValue,
1351 rhs: AdValue,
1352) -> Result<AdValue, ProgramBuildError> {
1353 match (lhs, rhs) {
1354 (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1355 (value, AdValue::Absent) => Ok(value),
1356 (AdValue::Absent, AdValue::Value(rhs)) => Ok(AdValue::Value(
1357 builder.add_op(CoreSemanticOp::Neg, &[rhs])?[0],
1358 )),
1359 (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1360 builder.add_op(CoreSemanticOp::Sub, &[lhs, rhs])?[0],
1361 )),
1362 }
1363}
1364
1365fn unary_ad_value(
1366 builder: &mut SemanticProgramBuilder,
1367 op: CoreSemanticOp,
1368 value: AdValue,
1369) -> Result<AdValue, ProgramBuildError> {
1370 match value {
1371 AdValue::Absent => Ok(AdValue::Absent),
1372 AdValue::Value(value) => Ok(AdValue::Value(builder.add_op(op, &[value])?[0])),
1373 }
1374}
1375
1376fn multiply_ad_value(
1377 builder: &mut SemanticProgramBuilder,
1378 value: AdValue,
1379 coefficient: ProgramValue,
1380) -> Result<AdValue, ProgramBuildError> {
1381 match value {
1382 AdValue::Absent => Ok(AdValue::Absent),
1383 AdValue::Value(value) => Ok(AdValue::Value(
1384 builder.add_op(CoreSemanticOp::Mul, &[value, coefficient])?[0],
1385 )),
1386 }
1387}
1388
1389fn divide_ad_value(
1390 builder: &mut SemanticProgramBuilder,
1391 value: AdValue,
1392 denominator: ProgramValue,
1393) -> Result<AdValue, ProgramBuildError> {
1394 match value {
1395 AdValue::Absent => Ok(AdValue::Absent),
1396 AdValue::Value(value) => Ok(AdValue::Value(
1397 builder.add_op(CoreSemanticOp::Div, &[value, denominator])?[0],
1398 )),
1399 }
1400}
1401
1402fn convert_ad_value(
1403 builder: &mut SemanticProgramBuilder,
1404 value: AdValue,
1405 from: DType,
1406 to: DType,
1407) -> Result<AdValue, ProgramBuildError> {
1408 if from == to {
1409 return Ok(value);
1410 }
1411 unary_ad_value(builder, CoreSemanticOp::Convert { from, to }, value)
1412}
1413
1414fn zero_from_ad_value(
1415 builder: &mut SemanticProgramBuilder,
1416 value: AdValue,
1417) -> Result<AdValue, ProgramBuildError> {
1418 let negated = unary_ad_value(builder, CoreSemanticOp::Neg, value)?;
1419 add_ad_values(builder, value, negated)
1420}
1421
1422fn select_ad_values(
1423 builder: &mut SemanticProgramBuilder,
1424 condition: ProgramValue,
1425 on_true: AdValue,
1426 on_false: AdValue,
1427) -> Result<AdValue, ProgramBuildError> {
1428 match (on_true, on_false) {
1429 (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1430 (AdValue::Value(on_true), AdValue::Value(on_false)) => Ok(AdValue::Value(
1431 builder.add_op(CoreSemanticOp::Select, &[condition, on_true, on_false])?[0],
1432 )),
1433 (AdValue::Value(on_true), AdValue::Absent) => {
1434 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_true))? else {
1435 unreachable!();
1436 };
1437 Ok(AdValue::Value(
1438 builder.add_op(CoreSemanticOp::Select, &[condition, on_true, zero])?[0],
1439 ))
1440 }
1441 (AdValue::Absent, AdValue::Value(on_false)) => {
1442 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_false))?
1443 else {
1444 unreachable!();
1445 };
1446 Ok(AdValue::Value(
1447 builder.add_op(CoreSemanticOp::Select, &[condition, zero, on_false])?[0],
1448 ))
1449 }
1450 }
1451}
1452
1453fn split_select_cotangent(
1454 builder: &mut SemanticProgramBuilder,
1455 condition: ProgramValue,
1456 cotangent: AdValue,
1457 true_active: bool,
1458 false_active: bool,
1459) -> Result<(AdValue, AdValue), ProgramBuildError> {
1460 if !true_active && !false_active {
1461 return Ok((AdValue::Absent, AdValue::Absent));
1462 }
1463 let AdValue::Value(cotangent) = cotangent else {
1464 return Ok((AdValue::Absent, AdValue::Absent));
1465 };
1466 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(cotangent))? else {
1467 unreachable!();
1468 };
1469 let on_true = if true_active {
1470 AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, cotangent, zero])?[0])
1471 } else {
1472 AdValue::Absent
1473 };
1474 let on_false = if false_active {
1475 AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, zero, cotangent])?[0])
1476 } else {
1477 AdValue::Absent
1478 };
1479 Ok((on_true, on_false))
1480}
1481
1482fn linearize_extrema(
1483 builder: &mut SemanticProgramBuilder,
1484 op: &CoreSemanticOp,
1485 primal_inputs: &[ProgramValue],
1486 tangent_inputs: &[AdValue],
1487) -> Result<AdValue, SemanticAdTransformError> {
1488 let output = builder.add_op(op.clone(), primal_inputs)?[0];
1489 let lhs_eq_output = builder.add_op(
1490 CoreSemanticOp::Compare(CompareDir::Eq),
1491 &[primal_inputs[0], output],
1492 )?[0];
1493 let rhs_eq_output = builder.add_op(
1494 CoreSemanticOp::Compare(CompareDir::Eq),
1495 &[primal_inputs[1], output],
1496 )?[0];
1497 let lhs = balanced_extrema_contribution(
1498 builder,
1499 tangent_inputs[0],
1500 lhs_eq_output,
1501 rhs_eq_output,
1502 SemanticTransformRole::Jvp,
1503 )?;
1504 let rhs = balanced_extrema_contribution(
1505 builder,
1506 tangent_inputs[1],
1507 rhs_eq_output,
1508 lhs_eq_output,
1509 SemanticTransformRole::Jvp,
1510 )?;
1511 Ok(add_ad_values(builder, lhs, rhs)?)
1512}
1513
1514fn extrema_vjp(
1515 builder: &mut SemanticProgramBuilder,
1516 op: &CoreSemanticOp,
1517 primal_inputs: &[ProgramValue],
1518 cotangent: AdValue,
1519 active_inputs: &[bool],
1520) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1521 let output = builder.add_op(op.clone(), primal_inputs)?[0];
1522 let lhs_eq_output = builder.add_op(
1523 CoreSemanticOp::Compare(CompareDir::Eq),
1524 &[primal_inputs[0], output],
1525 )?[0];
1526 let rhs_eq_output = builder.add_op(
1527 CoreSemanticOp::Compare(CompareDir::Eq),
1528 &[primal_inputs[1], output],
1529 )?[0];
1530 let lhs = balanced_extrema_contribution(
1531 builder,
1532 cotangent,
1533 lhs_eq_output,
1534 rhs_eq_output,
1535 SemanticTransformRole::Vjp,
1536 )?;
1537 let rhs = balanced_extrema_contribution(
1538 builder,
1539 cotangent,
1540 rhs_eq_output,
1541 lhs_eq_output,
1542 SemanticTransformRole::Vjp,
1543 )?;
1544 Ok(vec![
1545 normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
1546 normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
1547 ])
1548}
1549
1550fn balanced_extrema_contribution(
1551 builder: &mut SemanticProgramBuilder,
1552 active: AdValue,
1553 self_eq_output: ProgramValue,
1554 other_eq_output: ProgramValue,
1555 role: SemanticTransformRole,
1556) -> Result<AdValue, SemanticAdTransformError> {
1557 let AdValue::Value(active) = active else {
1558 return Ok(AdValue::Absent);
1559 };
1560 let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1561 let selected = builder.add_op(CoreSemanticOp::Select, &[self_eq_output, active, zero])?[0];
1562 let one = one_like(builder, active, role)?;
1563 let two = builder.add_op(CoreSemanticOp::Add, &[one, one])?[0];
1564 let half = builder.add_op(CoreSemanticOp::Div, &[selected, two])?[0];
1565 Ok(AdValue::Value(
1566 builder.add_op(CoreSemanticOp::Select, &[other_eq_output, half, selected])?[0],
1567 ))
1568}
1569
1570fn linearize_clamp(
1571 builder: &mut SemanticProgramBuilder,
1572 primal_inputs: &[ProgramValue],
1573 tangent_inputs: &[AdValue],
1574) -> Result<AdValue, ProgramBuildError> {
1575 let masks = clamp_masks(builder, primal_inputs)?;
1576 let input = mask_ad_value(builder, tangent_inputs[0], &[masks[0], masks[1]])?;
1577 let lower = mask_ad_value(builder, tangent_inputs[1], &[masks[2], masks[3]])?;
1578 let upper = mask_ad_value(builder, tangent_inputs[2], &[masks[4]])?;
1579 let input_and_lower = add_ad_values(builder, input, lower)?;
1580 add_ad_values(builder, input_and_lower, upper)
1581}
1582
1583fn clamp_vjp(
1584 builder: &mut SemanticProgramBuilder,
1585 primal_inputs: &[ProgramValue],
1586 cotangent: AdValue,
1587 active_inputs: &[bool],
1588) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1589 let masks = clamp_masks(builder, primal_inputs)?;
1590 let input = mask_ad_value(builder, cotangent, &[masks[0], masks[1]])?;
1591 let lower = mask_ad_value(builder, cotangent, &[masks[2], masks[3]])?;
1592 let upper = mask_ad_value(builder, cotangent, &[masks[4]])?;
1593 Ok(vec![
1594 normalize_ad_value(builder, input, active_inputs[0], primal_inputs[0])?,
1595 normalize_ad_value(builder, lower, active_inputs[1], primal_inputs[1])?,
1596 normalize_ad_value(builder, upper, active_inputs[2], primal_inputs[2])?,
1597 ])
1598}
1599
1600fn clamp_masks(
1601 builder: &mut SemanticProgramBuilder,
1602 primal_inputs: &[ProgramValue],
1603) -> Result<[ProgramValue; 5], ProgramBuildError> {
1604 let input = primal_inputs[0];
1605 let lower = primal_inputs[1];
1606 let upper = primal_inputs[2];
1607 let input_gt_lower =
1608 builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[input, lower])?[0];
1609 let input_lt_upper =
1610 builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[input, upper])?[0];
1611 let lower_gt_input =
1612 builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[lower, input])?[0];
1613 let lower_lt_upper =
1614 builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[lower, upper])?[0];
1615 let max_input_lower = builder.add_op(CoreSemanticOp::Maximum, &[input, lower])?[0];
1616 let upper_lt_max_input_lower = builder.add_op(
1617 CoreSemanticOp::Compare(CompareDir::Lt),
1618 &[upper, max_input_lower],
1619 )?[0];
1620 Ok([
1621 input_gt_lower,
1622 input_lt_upper,
1623 lower_gt_input,
1624 lower_lt_upper,
1625 upper_lt_max_input_lower,
1626 ])
1627}
1628
1629fn mask_ad_value(
1630 builder: &mut SemanticProgramBuilder,
1631 active: AdValue,
1632 conditions: &[ProgramValue],
1633) -> Result<AdValue, ProgramBuildError> {
1634 let AdValue::Value(active) = active else {
1635 return Ok(AdValue::Absent);
1636 };
1637 let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1638 let mut value = active;
1639 for condition in conditions {
1640 value = builder.add_op(CoreSemanticOp::Select, &[*condition, value, zero])?[0];
1641 }
1642 Ok(AdValue::Value(value))
1643}
1644
1645fn linearize_analytic_unary(
1646 builder: &mut SemanticProgramBuilder,
1647 op: &CoreSemanticOp,
1648 primal_input: ProgramValue,
1649 tangent: AdValue,
1650) -> Result<AdValue, SemanticAdTransformError> {
1651 if matches!(tangent, AdValue::Absent) {
1652 return Ok(AdValue::Absent);
1653 }
1654 let coefficient =
1655 analytic_unary_coefficient(builder, op, primal_input, SemanticTransformRole::Jvp)?;
1656 Ok(multiply_ad_value(builder, tangent, coefficient)?)
1657}
1658
1659fn linearize_sign(
1660 builder: &mut SemanticProgramBuilder,
1661 primal_input: ProgramValue,
1662 tangent: AdValue,
1663) -> Result<AdValue, SemanticAdTransformError> {
1664 let AdValue::Value(tangent_value) = tangent else {
1665 return Ok(AdValue::Absent);
1666 };
1667 let input_dtype = builder.value_metadata(primal_input)?.dtype();
1668 if !is_complex_dtype(input_dtype) {
1669 return Ok(AdValue::Absent);
1670 }
1671
1672 let zero = builder.add_op(CoreSemanticOp::Sub, &[primal_input, primal_input])?[0];
1673 let zero_mask = builder.add_op(
1674 CoreSemanticOp::Compare(CompareDir::Eq),
1675 &[primal_input, zero],
1676 )?[0];
1677 let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_input])?[0];
1678 let abs = builder.add_op(CoreSemanticOp::Abs, &[primal_input])?[0];
1679 let output_dtype = abs_output_dtype(input_dtype);
1680 let complex_abs = builder.add_op(
1681 CoreSemanticOp::Convert {
1682 from: output_dtype,
1683 to: input_dtype,
1684 },
1685 &[abs],
1686 )?[0];
1687 let one = one_like(builder, complex_abs, SemanticTransformRole::Jvp)?;
1688 let safe_abs = builder.add_op(CoreSemanticOp::Select, &[zero_mask, one, complex_abs])?[0];
1689 let safe_sign = builder.add_op(CoreSemanticOp::Select, &[zero_mask, zero, sign])?[0];
1690 let conj_sign = builder.add_op(CoreSemanticOp::Conj, &[safe_sign])?[0];
1691
1692 let abs_tangent_complex = multiply_ad_value(builder, AdValue::Value(tangent_value), conj_sign)?;
1693 let abs_tangent = convert_ad_value(builder, abs_tangent_complex, input_dtype, output_dtype)?;
1694 let abs_tangent = convert_ad_value(builder, abs_tangent, output_dtype, input_dtype)?;
1695 let tangent_over_abs = divide_ad_value(builder, AdValue::Value(tangent_value), safe_abs)?;
1696 let sign_times_abs_tangent = multiply_ad_value(builder, abs_tangent, safe_sign)?;
1697 let correction = divide_ad_value(builder, sign_times_abs_tangent, safe_abs)?;
1698 let derivative = sub_ad_values(builder, tangent_over_abs, correction)?;
1699 let zero_derivative = zero_from_ad_value(builder, AdValue::Value(tangent_value))?;
1700 Ok(select_ad_values(
1701 builder,
1702 zero_mask,
1703 zero_derivative,
1704 derivative,
1705 )?)
1706}
1707
1708fn analytic_unary_coefficient(
1709 builder: &mut SemanticProgramBuilder,
1710 op: &CoreSemanticOp,
1711 primal_input: ProgramValue,
1712 role: SemanticTransformRole,
1713) -> Result<ProgramValue, SemanticAdTransformError> {
1714 let coefficient = match op {
1715 CoreSemanticOp::Exp | CoreSemanticOp::Expm1 => {
1716 builder.add_op(CoreSemanticOp::Exp, &[primal_input])?[0]
1717 }
1718 CoreSemanticOp::Log => {
1719 let one = one_like(builder, primal_input, role)?;
1720 builder.add_op(CoreSemanticOp::Div, &[one, primal_input])?[0]
1721 }
1722 CoreSemanticOp::Sin => builder.add_op(CoreSemanticOp::Cos, &[primal_input])?[0],
1723 CoreSemanticOp::Cos => {
1724 let sin = builder.add_op(CoreSemanticOp::Sin, &[primal_input])?[0];
1725 builder.add_op(CoreSemanticOp::Neg, &[sin])?[0]
1726 }
1727 CoreSemanticOp::Tanh => {
1728 let tanh = builder.add_op(CoreSemanticOp::Tanh, &[primal_input])?[0];
1729 let square = builder.add_op(CoreSemanticOp::Mul, &[tanh, tanh])?[0];
1730 let one = one_like(builder, primal_input, role)?;
1731 builder.add_op(CoreSemanticOp::Sub, &[one, square])?[0]
1732 }
1733 CoreSemanticOp::Sqrt => {
1734 let sqrt = builder.add_op(CoreSemanticOp::Sqrt, &[primal_input])?[0];
1735 let twice = builder.add_op(CoreSemanticOp::Add, &[sqrt, sqrt])?[0];
1736 let one = one_like(builder, primal_input, role)?;
1737 builder.add_op(CoreSemanticOp::Div, &[one, twice])?[0]
1738 }
1739 CoreSemanticOp::Rsqrt => {
1740 let rsqrt = builder.add_op(CoreSemanticOp::Rsqrt, &[primal_input])?[0];
1741 let negated = builder.add_op(CoreSemanticOp::Neg, &[rsqrt])?[0];
1742 let twice = builder.add_op(CoreSemanticOp::Add, &[primal_input, primal_input])?[0];
1743 builder.add_op(CoreSemanticOp::Div, &[negated, twice])?[0]
1744 }
1745 CoreSemanticOp::Log1p => {
1746 let one = one_like(builder, primal_input, role)?;
1747 let denominator = builder.add_op(CoreSemanticOp::Add, &[primal_input, one])?[0];
1748 builder.add_op(CoreSemanticOp::Div, &[one, denominator])?[0]
1749 }
1750 _ => return Err(unsupported_core(role, op)),
1751 };
1752 Ok(coefficient)
1753}
1754
1755fn one_like(
1756 builder: &mut SemanticProgramBuilder,
1757 anchor: ProgramValue,
1758 role: SemanticTransformRole,
1759) -> Result<ProgramValue, SemanticAdTransformError> {
1760 let metadata = builder.value_metadata(anchor)?.clone();
1761 let dtype = metadata.dtype();
1762 let bytes = match dtype {
1763 DType::F32 => 1.0_f32.to_le_bytes().to_vec(),
1764 DType::F64 => 1.0_f64.to_le_bytes().to_vec(),
1765 DType::C32 => {
1766 let mut bytes = 1.0_f32.to_le_bytes().to_vec();
1767 bytes.extend_from_slice(&0.0_f32.to_le_bytes());
1768 bytes
1769 }
1770 DType::C64 => {
1771 let mut bytes = 1.0_f64.to_le_bytes().to_vec();
1772 bytes.extend_from_slice(&0.0_f64.to_le_bytes());
1773 bytes
1774 }
1775 _ => {
1776 return Err(SemanticAdTransformError::UnsupportedMetadata {
1777 role,
1778 message: format!("cannot construct a differentiable one for {dtype:?}"),
1779 });
1780 }
1781 };
1782 let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1783 if metadata.shape().is_empty() {
1784 Ok(scalar)
1785 } else {
1786 let shape = shape_plan(metadata.shape(), role, "one-like anchor")?;
1787 let one = broadcast_value_in_dim_to_shape(builder, scalar, anchor, &shape, Vec::new())?;
1788 Ok(truncate_value_to_dynamic_axes(
1789 builder,
1790 one,
1791 anchor,
1792 &shape.dynamic_axes,
1793 )?)
1794 }
1795}
1796
1797fn active_cotangent(
1798 builder: &mut SemanticProgramBuilder,
1799 cotangent: AdValue,
1800 active: bool,
1801 primal_input: ProgramValue,
1802) -> Result<AdValue, SemanticAdTransformError> {
1803 normalize_ad_value(builder, cotangent, active, primal_input)
1804}
1805
1806fn normalize_ad_value(
1807 builder: &mut SemanticProgramBuilder,
1808 value: AdValue,
1809 active: bool,
1810 primal_input: ProgramValue,
1811) -> Result<AdValue, SemanticAdTransformError> {
1812 if !active {
1813 return Ok(AdValue::Absent);
1814 }
1815 let AdValue::Value(mut value) = value else {
1816 return Ok(AdValue::Absent);
1817 };
1818 let target_metadata = builder.value_metadata(primal_input)?.clone();
1819 let value_metadata = builder.value_metadata(value)?.clone();
1820 let target_shape = shape_plan(
1821 target_metadata.shape(),
1822 SemanticTransformRole::Vjp,
1823 "primal input",
1824 )?;
1825 let value_shape = shape_plan(
1826 value_metadata.shape(),
1827 SemanticTransformRole::Vjp,
1828 "cotangent",
1829 )?;
1830 if value_shape.shape.len() < target_shape.shape.len() {
1831 return Err(SemanticAdTransformError::UnsupportedMetadata {
1832 role: SemanticTransformRole::Vjp,
1833 message: "cotangent rank is smaller than its primal-input rank".into(),
1834 });
1835 }
1836 let leading = value_shape.shape.len() - target_shape.shape.len();
1837 let mut axes: Vec<_> = (0..leading).collect();
1838 axes.extend(
1839 target_shape
1840 .shape
1841 .iter()
1842 .zip(value_shape.shape.iter().skip(leading))
1843 .enumerate()
1844 .filter_map(|(axis, (target, actual))| {
1845 (matches!(target, tenferro_ops::dim_expr::DimExpr::Const(1)) && target != actual)
1846 .then_some(axis + leading)
1847 }),
1848 );
1849 if !axes.is_empty() {
1850 value = builder.add_op(CoreSemanticOp::ReduceSum { axes }, &[value])?[0];
1851 }
1852 if builder.value_metadata(value)?.shape() != target_metadata.shape() {
1853 value = reshape_value_to_shape(builder, value, primal_input, &target_shape)?;
1854 }
1855 value =
1856 truncate_value_to_dynamic_axes(builder, value, primal_input, &target_shape.dynamic_axes)?;
1857 let value_dtype = builder.value_metadata(value)?.dtype();
1858 if value_dtype != target_metadata.dtype() {
1859 value = builder.add_op(
1860 CoreSemanticOp::Convert {
1861 from: value_dtype,
1862 to: target_metadata.dtype(),
1863 },
1864 &[value],
1865 )?[0];
1866 }
1867 Ok(AdValue::Value(value))
1868}
1869
1870fn exact_shape(
1871 shape: &[tenferro_ops::ShapeExtent<tenferro_ops::dim_expr::DimExpr>],
1872 role: SemanticTransformRole,
1873 field: &'static str,
1874) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1875 shape
1876 .iter()
1877 .map(|extent| {
1878 extent.as_exact().cloned().ok_or_else(|| {
1879 SemanticAdTransformError::UnsupportedMetadata {
1880 role,
1881 message: format!("{field} has a bounded or unknown extent"),
1882 }
1883 })
1884 })
1885 .collect()
1886}
1887
1888fn value_shape_plan(
1889 builder: &SemanticProgramBuilder,
1890 value: ProgramValue,
1891 role: SemanticTransformRole,
1892 field: &'static str,
1893) -> Result<ValueShapePlan, SemanticAdTransformError> {
1894 shape_plan(builder.value_metadata(value)?.shape(), role, field)
1895}
1896
1897fn shape_plan(
1898 shape: &[ShapeExtent<DimExpr>],
1899 role: SemanticTransformRole,
1900 field: &'static str,
1901) -> Result<ValueShapePlan, SemanticAdTransformError> {
1902 let mut planned_shape = Vec::with_capacity(shape.len());
1903 let mut dynamic_axes = Vec::new();
1904 for (axis, extent) in shape.iter().enumerate() {
1905 match extent {
1906 ShapeExtent::Exact(expression) => planned_shape.push(expression.clone()),
1907 ShapeExtent::UpperBound(expression) => {
1908 planned_shape.push(expression.clone());
1909 dynamic_axes.push(axis);
1910 }
1911 ShapeExtent::Unknown => {
1912 return Err(SemanticAdTransformError::UnsupportedMetadata {
1913 role,
1914 message: format!("{field} has an unknown extent without an upper bound"),
1915 });
1916 }
1917 }
1918 }
1919 Ok(ValueShapePlan {
1920 shape: planned_shape,
1921 dynamic_axes,
1922 })
1923}
1924
1925fn reshape_ad_value_to_shape(
1926 builder: &mut SemanticProgramBuilder,
1927 value: AdValue,
1928 shape_source: ProgramValue,
1929 shape: &ValueShapePlan,
1930) -> Result<AdValue, ProgramBuildError> {
1931 match value {
1932 AdValue::Absent => Ok(AdValue::Absent),
1933 AdValue::Value(value) => Ok(AdValue::Value(reshape_value_to_shape(
1934 builder,
1935 value,
1936 shape_source,
1937 shape,
1938 )?)),
1939 }
1940}
1941
1942fn reshape_value_to_shape(
1943 builder: &mut SemanticProgramBuilder,
1944 value: ProgramValue,
1945 shape_source: ProgramValue,
1946 shape: &ValueShapePlan,
1947) -> Result<ProgramValue, ProgramBuildError> {
1948 let mut inputs = vec![value];
1949 let to_shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
1950 Ok(builder.add_op(CoreSemanticOp::Reshape { to_shape }, &inputs)?[0])
1951}
1952
1953fn broadcast_ad_value_in_dim_to_shape(
1954 builder: &mut SemanticProgramBuilder,
1955 value: AdValue,
1956 shape_source: ProgramValue,
1957 shape: &ValueShapePlan,
1958 dims: Vec<usize>,
1959) -> Result<AdValue, ProgramBuildError> {
1960 match value {
1961 AdValue::Absent => Ok(AdValue::Absent),
1962 AdValue::Value(value) => Ok(AdValue::Value(broadcast_value_in_dim_to_shape(
1963 builder,
1964 value,
1965 shape_source,
1966 shape,
1967 dims,
1968 )?)),
1969 }
1970}
1971
1972fn broadcast_value_in_dim_to_shape(
1973 builder: &mut SemanticProgramBuilder,
1974 value: ProgramValue,
1975 shape_source: ProgramValue,
1976 shape: &ValueShapePlan,
1977 dims: Vec<usize>,
1978) -> Result<ProgramValue, ProgramBuildError> {
1979 let mut inputs = vec![value];
1980 let shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
1981 Ok(builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &inputs)?[0])
1982}
1983
1984fn payload_shape_for_shape_source(
1985 shape: &ValueShapePlan,
1986 inputs: &mut Vec<ProgramValue>,
1987 shape_source: ProgramValue,
1988) -> Vec<DimExpr> {
1989 if DimExpr::max_input_idx_all(&shape.shape).is_none() {
1990 return shape.shape.clone();
1991 }
1992 let input_idx = inputs
1993 .iter()
1994 .position(|&input| input == shape_source)
1995 .unwrap_or_else(|| {
1996 let input_idx = inputs.len();
1997 inputs.push(shape_source);
1998 input_idx
1999 });
2000 DimExpr::input_shape(input_idx, shape.shape.len())
2001}
2002
2003fn truncate_ad_value_to_dynamic_axes(
2004 builder: &mut SemanticProgramBuilder,
2005 value: AdValue,
2006 shape_source: ProgramValue,
2007 dynamic_axes: &[usize],
2008) -> Result<AdValue, SemanticAdTransformError> {
2009 let AdValue::Value(value) = value else {
2010 return Ok(AdValue::Absent);
2011 };
2012 Ok(AdValue::Value(truncate_value_to_dynamic_axes(
2013 builder,
2014 value,
2015 shape_source,
2016 dynamic_axes,
2017 )?))
2018}
2019
2020fn truncate_value_to_dynamic_axes(
2021 builder: &mut SemanticProgramBuilder,
2022 mut value: ProgramValue,
2023 shape_source: ProgramValue,
2024 dynamic_axes: &[usize],
2025) -> Result<ProgramValue, ProgramBuildError> {
2026 for &axis in dynamic_axes {
2027 let size = builder.add_op(CoreSemanticOp::ShapeOf { axis }, &[shape_source])?[0];
2028 value = builder.add_op(CoreSemanticOp::DynamicTruncate { axis }, &[value, size])?[0];
2029 }
2030 Ok(value)
2031}
2032
2033fn conjugate_if_complex(
2034 builder: &mut SemanticProgramBuilder,
2035 value: ProgramValue,
2036) -> Result<ProgramValue, ProgramBuildError> {
2037 if matches!(
2038 builder.value_metadata(value)?.dtype(),
2039 DType::C32 | DType::C64
2040 ) {
2041 Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
2042 } else {
2043 Ok(value)
2044 }
2045}
2046
2047fn finish_derivative(
2048 builder: SemanticProgramBuilder,
2049 derivative_input_indices: Vec<Option<usize>>,
2050 values: Vec<AdValue>,
2051) -> Result<SemanticAdProgram, SemanticAdTransformError> {
2052 let mut outputs = Vec::new();
2053 let derivative_output_indices = values
2054 .into_iter()
2055 .map(|value| match value {
2056 AdValue::Absent => None,
2057 AdValue::Value(value) => {
2058 let index = outputs.len();
2059 outputs.push(value);
2060 Some(index)
2061 }
2062 })
2063 .collect();
2064 let frozen = builder.finish(&outputs)?;
2065 let frozen = prune_dead_derivative_operations(frozen)?;
2066 let frozen = cancel_double_neg_derivative_operations(frozen)?;
2067 Ok(SemanticAdProgram {
2068 frozen,
2069 derivative_input_indices: derivative_input_indices.into_boxed_slice(),
2070 derivative_output_indices,
2071 })
2072}
2073
2074fn prune_dead_derivative_operations(
2075 frozen: FrozenProgram,
2076) -> Result<FrozenProgram, SemanticAdTransformError> {
2077 let mut roots = frozen.program.inputs().to_vec();
2078 let output_offset = roots.len();
2079 roots.extend_from_slice(frozen.program.outputs());
2080
2081 let mut builder = SemanticProgramBuilder::new();
2082 let imported = builder.import(ProgramImport {
2083 program: frozen.program.as_ref(),
2084 bindings: &frozen.bindings,
2085 roots: &roots,
2086 })?;
2087 let outputs = imported.roots()[output_offset..].to_vec();
2088 Ok(builder.finish(&outputs)?)
2089}
2090
2091fn cancel_double_neg_derivative_operations(
2092 frozen: FrozenProgram,
2093) -> Result<FrozenProgram, SemanticAdTransformError> {
2094 let operations = frozen.program.operations().collect::<Vec<_>>();
2095 if operations.iter().any(|operation| {
2096 !operation.effects().is_empty()
2097 || !operation.shape_guards().is_empty()
2098 || !matches!(operation.op(), SemanticOpRef::Core(_))
2099 }) {
2100 return Ok(frozen);
2101 }
2102
2103 let mut builder = SemanticProgramBuilder::new();
2104 let imported = builder.import(ProgramImport {
2105 program: frozen.program.as_ref(),
2106 bindings: &frozen.bindings,
2107 roots: frozen.program.inputs(),
2108 })?;
2109 let mut values = frozen
2110 .program
2111 .inputs()
2112 .iter()
2113 .copied()
2114 .zip(imported.roots().iter().copied())
2115 .collect::<HashMap<_, _>>();
2116 let mut neg_inputs = HashMap::<ProgramValue, ProgramValue>::new();
2117 let mut changed = false;
2118
2119 for operation in operations {
2120 let inputs = operation
2121 .inputs()
2122 .iter()
2123 .copied()
2124 .map(|value| {
2125 values.get(&value).copied().ok_or_else(|| {
2126 SemanticAdTransformError::UnsupportedMetadata {
2127 role: SemanticTransformRole::Jvp,
2128 message: "derivative simplifier saw an unmapped value".into(),
2129 }
2130 })
2131 })
2132 .collect::<Result<Vec<_>, _>>()?;
2133 let SemanticOpRef::Core(op) = operation.op() else {
2134 unreachable!("non-core operations returned above");
2135 };
2136
2137 if matches!(op, CoreSemanticOp::Neg) {
2138 let input = inputs[0];
2139 if let Some(inner) = neg_inputs.get(&input).copied() {
2140 values.insert(operation.outputs()[0], inner);
2141 changed = true;
2142 continue;
2143 }
2144 let output = builder.add_op(CoreSemanticOp::Neg, &[input])?[0];
2145 neg_inputs.insert(output, input);
2146 values.insert(operation.outputs()[0], output);
2147 continue;
2148 }
2149
2150 let outputs = builder.add_op(op.clone(), &inputs)?;
2151 for (source, output) in operation
2152 .outputs()
2153 .iter()
2154 .copied()
2155 .zip(outputs.iter().copied())
2156 {
2157 values.insert(source, output);
2158 }
2159 }
2160
2161 if !changed {
2162 return Ok(frozen);
2163 }
2164
2165 let outputs = frozen
2166 .program
2167 .outputs()
2168 .iter()
2169 .copied()
2170 .map(|value| {
2171 values.get(&value).copied().ok_or_else(|| {
2172 SemanticAdTransformError::UnsupportedMetadata {
2173 role: SemanticTransformRole::Jvp,
2174 message: "derivative simplifier saw an unmapped output".into(),
2175 }
2176 })
2177 })
2178 .collect::<Result<Vec<_>, _>>()?;
2179 prune_dead_derivative_operations(builder.finish(&outputs)?)
2180}
2181
2182fn validate_activity(
2183 role: SemanticTransformRole,
2184 field: &'static str,
2185 expected: usize,
2186 actual: usize,
2187) -> Result<(), SemanticAdTransformError> {
2188 if expected == actual {
2189 Ok(())
2190 } else {
2191 Err(SemanticAdTransformError::ActivityArity {
2192 role,
2193 field,
2194 expected,
2195 actual,
2196 })
2197 }
2198}
2199
2200fn unsupported_core(role: SemanticTransformRole, op: &CoreSemanticOp) -> SemanticAdTransformError {
2201 SemanticAdTransformError::UnsupportedCore {
2202 role,
2203 op: format!("{op:?}"),
2204 }
2205}