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 input_rank = input_shape.shape.len();
1260 let output_rank = output_shape.shape.len();
1261 let metadata_error = |message| SemanticAdTransformError::UnsupportedMetadata {
1262 role: SemanticTransformRole::Vjp,
1263 message,
1264 };
1265 if dims.len() != input_rank {
1266 return Err(metadata_error(format!(
1267 "broadcast dims length {} does not match input rank {input_rank}",
1268 dims.len()
1269 )));
1270 }
1271 let mut seen = HashSet::with_capacity(dims.len());
1272 for (input_axis, &output_axis) in dims.iter().enumerate() {
1273 if output_axis >= output_rank {
1274 return Err(metadata_error(format!(
1275 "broadcast dims[{input_axis}] = {output_axis} is out of bounds for output rank {output_rank}"
1276 )));
1277 }
1278 if !seen.insert(output_axis) {
1279 return Err(metadata_error(format!(
1280 "broadcast dims[{input_axis}] = {output_axis} duplicates an earlier output axis"
1281 )));
1282 }
1283 }
1284 let mut reduce_axes: Vec<_> = (0..output_rank)
1287 .filter(|axis| !dims.contains(axis))
1288 .collect();
1289 reduce_axes.extend(
1290 dims.iter()
1291 .copied()
1292 .enumerate()
1293 .filter_map(|(input_axis, output_axis)| {
1294 (matches!(
1295 input_shape.shape[input_axis],
1296 tenferro_ops::dim_expr::DimExpr::Const(1)
1297 ) && input_shape.shape[input_axis] != output_shape.shape[output_axis])
1298 .then_some(output_axis)
1299 }),
1300 );
1301 reduce_axes.sort_unstable();
1302 reduce_axes.dedup();
1303 if !reduce_axes.is_empty() {
1304 value = builder.add_op(
1305 CoreSemanticOp::ReduceSum {
1306 axes: reduce_axes.clone(),
1307 },
1308 &[value],
1309 )?[0];
1310 }
1311
1312 let remaining_output_axes: Vec<_> = (0..output_rank)
1313 .filter(|axis| !reduce_axes.contains(axis))
1314 .collect();
1315 let perm: Vec<_> = dims
1316 .iter()
1317 .copied()
1318 .filter(|axis| !reduce_axes.contains(axis))
1319 .map(|axis| {
1320 remaining_output_axes
1321 .iter()
1322 .position(|candidate| *candidate == axis)
1323 .ok_or_else(|| {
1324 metadata_error(format!(
1325 "broadcast output axis {axis} did not survive cotangent reduction"
1326 ))
1327 })
1328 })
1329 .collect::<Result<_, _>>()?;
1330 if perm.iter().copied().ne(0..perm.len()) {
1331 value = builder.add_op(CoreSemanticOp::Transpose { perm }, &[value])?[0];
1332 }
1333 if builder.value_metadata(value)?.shape() != builder.value_metadata(primal_input)?.shape() {
1334 value = reshape_value_to_shape(builder, value, primal_input, &input_shape)?;
1335 }
1336 value =
1337 truncate_value_to_dynamic_axes(builder, value, primal_input, &input_shape.dynamic_axes)?;
1338 Ok(AdValue::Value(value))
1339}
1340
1341fn exact_value_shape(
1342 builder: &SemanticProgramBuilder,
1343 value: ProgramValue,
1344 role: SemanticTransformRole,
1345 field: &'static str,
1346) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1347 exact_shape(builder.value_metadata(value)?.shape(), role, field)
1348}
1349
1350fn is_differentiable_dtype(dtype: DType) -> bool {
1351 matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
1352}
1353
1354fn is_complex_dtype(dtype: DType) -> bool {
1355 matches!(dtype, DType::C32 | DType::C64)
1356}
1357
1358fn abs_output_dtype(dtype: DType) -> DType {
1359 match dtype {
1360 DType::C32 => DType::F32,
1361 DType::C64 => DType::F64,
1362 other => other,
1363 }
1364}
1365
1366fn add_ad_values(
1367 builder: &mut SemanticProgramBuilder,
1368 lhs: AdValue,
1369 rhs: AdValue,
1370) -> Result<AdValue, ProgramBuildError> {
1371 match (lhs, rhs) {
1372 (AdValue::Absent, value) | (value, AdValue::Absent) => Ok(value),
1373 (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1374 builder.add_op(CoreSemanticOp::Add, &[lhs, rhs])?[0],
1375 )),
1376 }
1377}
1378
1379fn sub_ad_values(
1380 builder: &mut SemanticProgramBuilder,
1381 lhs: AdValue,
1382 rhs: AdValue,
1383) -> Result<AdValue, ProgramBuildError> {
1384 match (lhs, rhs) {
1385 (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1386 (value, AdValue::Absent) => Ok(value),
1387 (AdValue::Absent, AdValue::Value(rhs)) => Ok(AdValue::Value(
1388 builder.add_op(CoreSemanticOp::Neg, &[rhs])?[0],
1389 )),
1390 (AdValue::Value(lhs), AdValue::Value(rhs)) => Ok(AdValue::Value(
1391 builder.add_op(CoreSemanticOp::Sub, &[lhs, rhs])?[0],
1392 )),
1393 }
1394}
1395
1396fn unary_ad_value(
1397 builder: &mut SemanticProgramBuilder,
1398 op: CoreSemanticOp,
1399 value: AdValue,
1400) -> Result<AdValue, ProgramBuildError> {
1401 match value {
1402 AdValue::Absent => Ok(AdValue::Absent),
1403 AdValue::Value(value) => Ok(AdValue::Value(builder.add_op(op, &[value])?[0])),
1404 }
1405}
1406
1407fn multiply_ad_value(
1408 builder: &mut SemanticProgramBuilder,
1409 value: AdValue,
1410 coefficient: ProgramValue,
1411) -> Result<AdValue, ProgramBuildError> {
1412 match value {
1413 AdValue::Absent => Ok(AdValue::Absent),
1414 AdValue::Value(value) => Ok(AdValue::Value(
1415 builder.add_op(CoreSemanticOp::Mul, &[value, coefficient])?[0],
1416 )),
1417 }
1418}
1419
1420fn divide_ad_value(
1421 builder: &mut SemanticProgramBuilder,
1422 value: AdValue,
1423 denominator: ProgramValue,
1424) -> Result<AdValue, ProgramBuildError> {
1425 match value {
1426 AdValue::Absent => Ok(AdValue::Absent),
1427 AdValue::Value(value) => Ok(AdValue::Value(
1428 builder.add_op(CoreSemanticOp::Div, &[value, denominator])?[0],
1429 )),
1430 }
1431}
1432
1433fn convert_ad_value(
1434 builder: &mut SemanticProgramBuilder,
1435 value: AdValue,
1436 from: DType,
1437 to: DType,
1438) -> Result<AdValue, ProgramBuildError> {
1439 if from == to {
1440 return Ok(value);
1441 }
1442 unary_ad_value(builder, CoreSemanticOp::Convert { from, to }, value)
1443}
1444
1445fn zero_from_ad_value(
1446 builder: &mut SemanticProgramBuilder,
1447 value: AdValue,
1448) -> Result<AdValue, ProgramBuildError> {
1449 let negated = unary_ad_value(builder, CoreSemanticOp::Neg, value)?;
1450 add_ad_values(builder, value, negated)
1451}
1452
1453fn select_ad_values(
1454 builder: &mut SemanticProgramBuilder,
1455 condition: ProgramValue,
1456 on_true: AdValue,
1457 on_false: AdValue,
1458) -> Result<AdValue, ProgramBuildError> {
1459 match (on_true, on_false) {
1460 (AdValue::Absent, AdValue::Absent) => Ok(AdValue::Absent),
1461 (AdValue::Value(on_true), AdValue::Value(on_false)) => Ok(AdValue::Value(
1462 builder.add_op(CoreSemanticOp::Select, &[condition, on_true, on_false])?[0],
1463 )),
1464 (AdValue::Value(on_true), AdValue::Absent) => {
1465 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_true))? else {
1466 unreachable!();
1467 };
1468 Ok(AdValue::Value(
1469 builder.add_op(CoreSemanticOp::Select, &[condition, on_true, zero])?[0],
1470 ))
1471 }
1472 (AdValue::Absent, AdValue::Value(on_false)) => {
1473 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(on_false))?
1474 else {
1475 unreachable!();
1476 };
1477 Ok(AdValue::Value(
1478 builder.add_op(CoreSemanticOp::Select, &[condition, zero, on_false])?[0],
1479 ))
1480 }
1481 }
1482}
1483
1484fn split_select_cotangent(
1485 builder: &mut SemanticProgramBuilder,
1486 condition: ProgramValue,
1487 cotangent: AdValue,
1488 true_active: bool,
1489 false_active: bool,
1490) -> Result<(AdValue, AdValue), ProgramBuildError> {
1491 if !true_active && !false_active {
1492 return Ok((AdValue::Absent, AdValue::Absent));
1493 }
1494 let AdValue::Value(cotangent) = cotangent else {
1495 return Ok((AdValue::Absent, AdValue::Absent));
1496 };
1497 let AdValue::Value(zero) = zero_from_ad_value(builder, AdValue::Value(cotangent))? else {
1498 unreachable!();
1499 };
1500 let on_true = if true_active {
1501 AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, cotangent, zero])?[0])
1502 } else {
1503 AdValue::Absent
1504 };
1505 let on_false = if false_active {
1506 AdValue::Value(builder.add_op(CoreSemanticOp::Select, &[condition, zero, cotangent])?[0])
1507 } else {
1508 AdValue::Absent
1509 };
1510 Ok((on_true, on_false))
1511}
1512
1513fn linearize_extrema(
1514 builder: &mut SemanticProgramBuilder,
1515 op: &CoreSemanticOp,
1516 primal_inputs: &[ProgramValue],
1517 tangent_inputs: &[AdValue],
1518) -> Result<AdValue, SemanticAdTransformError> {
1519 let output = builder.add_op(op.clone(), primal_inputs)?[0];
1520 let lhs_eq_output = builder.add_op(
1521 CoreSemanticOp::Compare(CompareDir::Eq),
1522 &[primal_inputs[0], output],
1523 )?[0];
1524 let rhs_eq_output = builder.add_op(
1525 CoreSemanticOp::Compare(CompareDir::Eq),
1526 &[primal_inputs[1], output],
1527 )?[0];
1528 let lhs = balanced_extrema_contribution(
1529 builder,
1530 tangent_inputs[0],
1531 lhs_eq_output,
1532 rhs_eq_output,
1533 SemanticTransformRole::Jvp,
1534 )?;
1535 let rhs = balanced_extrema_contribution(
1536 builder,
1537 tangent_inputs[1],
1538 rhs_eq_output,
1539 lhs_eq_output,
1540 SemanticTransformRole::Jvp,
1541 )?;
1542 Ok(add_ad_values(builder, lhs, rhs)?)
1543}
1544
1545fn extrema_vjp(
1546 builder: &mut SemanticProgramBuilder,
1547 op: &CoreSemanticOp,
1548 primal_inputs: &[ProgramValue],
1549 cotangent: AdValue,
1550 active_inputs: &[bool],
1551) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1552 let output = builder.add_op(op.clone(), primal_inputs)?[0];
1553 let lhs_eq_output = builder.add_op(
1554 CoreSemanticOp::Compare(CompareDir::Eq),
1555 &[primal_inputs[0], output],
1556 )?[0];
1557 let rhs_eq_output = builder.add_op(
1558 CoreSemanticOp::Compare(CompareDir::Eq),
1559 &[primal_inputs[1], output],
1560 )?[0];
1561 let lhs = balanced_extrema_contribution(
1562 builder,
1563 cotangent,
1564 lhs_eq_output,
1565 rhs_eq_output,
1566 SemanticTransformRole::Vjp,
1567 )?;
1568 let rhs = balanced_extrema_contribution(
1569 builder,
1570 cotangent,
1571 rhs_eq_output,
1572 lhs_eq_output,
1573 SemanticTransformRole::Vjp,
1574 )?;
1575 Ok(vec![
1576 normalize_ad_value(builder, lhs, active_inputs[0], primal_inputs[0])?,
1577 normalize_ad_value(builder, rhs, active_inputs[1], primal_inputs[1])?,
1578 ])
1579}
1580
1581fn balanced_extrema_contribution(
1582 builder: &mut SemanticProgramBuilder,
1583 active: AdValue,
1584 self_eq_output: ProgramValue,
1585 other_eq_output: ProgramValue,
1586 role: SemanticTransformRole,
1587) -> Result<AdValue, SemanticAdTransformError> {
1588 let AdValue::Value(active) = active else {
1589 return Ok(AdValue::Absent);
1590 };
1591 let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1592 let selected = builder.add_op(CoreSemanticOp::Select, &[self_eq_output, active, zero])?[0];
1593 let one = one_like(builder, active, role)?;
1594 let two = builder.add_op(CoreSemanticOp::Add, &[one, one])?[0];
1595 let half = builder.add_op(CoreSemanticOp::Div, &[selected, two])?[0];
1596 Ok(AdValue::Value(
1597 builder.add_op(CoreSemanticOp::Select, &[other_eq_output, half, selected])?[0],
1598 ))
1599}
1600
1601fn linearize_clamp(
1602 builder: &mut SemanticProgramBuilder,
1603 primal_inputs: &[ProgramValue],
1604 tangent_inputs: &[AdValue],
1605) -> Result<AdValue, ProgramBuildError> {
1606 let masks = clamp_masks(builder, primal_inputs)?;
1607 let input = mask_ad_value(builder, tangent_inputs[0], &[masks[0], masks[1]])?;
1608 let lower = mask_ad_value(builder, tangent_inputs[1], &[masks[2], masks[3]])?;
1609 let upper = mask_ad_value(builder, tangent_inputs[2], &[masks[4]])?;
1610 let input_and_lower = add_ad_values(builder, input, lower)?;
1611 add_ad_values(builder, input_and_lower, upper)
1612}
1613
1614fn clamp_vjp(
1615 builder: &mut SemanticProgramBuilder,
1616 primal_inputs: &[ProgramValue],
1617 cotangent: AdValue,
1618 active_inputs: &[bool],
1619) -> Result<Vec<AdValue>, SemanticAdTransformError> {
1620 let masks = clamp_masks(builder, primal_inputs)?;
1621 let input = mask_ad_value(builder, cotangent, &[masks[0], masks[1]])?;
1622 let lower = mask_ad_value(builder, cotangent, &[masks[2], masks[3]])?;
1623 let upper = mask_ad_value(builder, cotangent, &[masks[4]])?;
1624 Ok(vec![
1625 normalize_ad_value(builder, input, active_inputs[0], primal_inputs[0])?,
1626 normalize_ad_value(builder, lower, active_inputs[1], primal_inputs[1])?,
1627 normalize_ad_value(builder, upper, active_inputs[2], primal_inputs[2])?,
1628 ])
1629}
1630
1631fn clamp_masks(
1632 builder: &mut SemanticProgramBuilder,
1633 primal_inputs: &[ProgramValue],
1634) -> Result<[ProgramValue; 5], ProgramBuildError> {
1635 let input = primal_inputs[0];
1636 let lower = primal_inputs[1];
1637 let upper = primal_inputs[2];
1638 let input_gt_lower =
1639 builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[input, lower])?[0];
1640 let input_lt_upper =
1641 builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[input, upper])?[0];
1642 let lower_gt_input =
1643 builder.add_op(CoreSemanticOp::Compare(CompareDir::Gt), &[lower, input])?[0];
1644 let lower_lt_upper =
1645 builder.add_op(CoreSemanticOp::Compare(CompareDir::Lt), &[lower, upper])?[0];
1646 let max_input_lower = builder.add_op(CoreSemanticOp::Maximum, &[input, lower])?[0];
1647 let upper_lt_max_input_lower = builder.add_op(
1648 CoreSemanticOp::Compare(CompareDir::Lt),
1649 &[upper, max_input_lower],
1650 )?[0];
1651 Ok([
1652 input_gt_lower,
1653 input_lt_upper,
1654 lower_gt_input,
1655 lower_lt_upper,
1656 upper_lt_max_input_lower,
1657 ])
1658}
1659
1660fn mask_ad_value(
1661 builder: &mut SemanticProgramBuilder,
1662 active: AdValue,
1663 conditions: &[ProgramValue],
1664) -> Result<AdValue, ProgramBuildError> {
1665 let AdValue::Value(active) = active else {
1666 return Ok(AdValue::Absent);
1667 };
1668 let zero = builder.add_op(CoreSemanticOp::Sub, &[active, active])?[0];
1669 let mut value = active;
1670 for condition in conditions {
1671 value = builder.add_op(CoreSemanticOp::Select, &[*condition, value, zero])?[0];
1672 }
1673 Ok(AdValue::Value(value))
1674}
1675
1676fn linearize_analytic_unary(
1677 builder: &mut SemanticProgramBuilder,
1678 op: &CoreSemanticOp,
1679 primal_input: ProgramValue,
1680 tangent: AdValue,
1681) -> Result<AdValue, SemanticAdTransformError> {
1682 if matches!(tangent, AdValue::Absent) {
1683 return Ok(AdValue::Absent);
1684 }
1685 let coefficient =
1686 analytic_unary_coefficient(builder, op, primal_input, SemanticTransformRole::Jvp)?;
1687 Ok(multiply_ad_value(builder, tangent, coefficient)?)
1688}
1689
1690fn linearize_sign(
1691 builder: &mut SemanticProgramBuilder,
1692 primal_input: ProgramValue,
1693 tangent: AdValue,
1694) -> Result<AdValue, SemanticAdTransformError> {
1695 let AdValue::Value(tangent_value) = tangent else {
1696 return Ok(AdValue::Absent);
1697 };
1698 let input_dtype = builder.value_metadata(primal_input)?.dtype();
1699 if !is_complex_dtype(input_dtype) {
1700 return Ok(AdValue::Absent);
1701 }
1702
1703 let zero = builder.add_op(CoreSemanticOp::Sub, &[primal_input, primal_input])?[0];
1704 let zero_mask = builder.add_op(
1705 CoreSemanticOp::Compare(CompareDir::Eq),
1706 &[primal_input, zero],
1707 )?[0];
1708 let sign = builder.add_op(CoreSemanticOp::Sign, &[primal_input])?[0];
1709 let abs = builder.add_op(CoreSemanticOp::Abs, &[primal_input])?[0];
1710 let output_dtype = abs_output_dtype(input_dtype);
1711 let complex_abs = builder.add_op(
1712 CoreSemanticOp::Convert {
1713 from: output_dtype,
1714 to: input_dtype,
1715 },
1716 &[abs],
1717 )?[0];
1718 let one = one_like(builder, complex_abs, SemanticTransformRole::Jvp)?;
1719 let safe_abs = builder.add_op(CoreSemanticOp::Select, &[zero_mask, one, complex_abs])?[0];
1720 let safe_sign = builder.add_op(CoreSemanticOp::Select, &[zero_mask, zero, sign])?[0];
1721 let conj_sign = builder.add_op(CoreSemanticOp::Conj, &[safe_sign])?[0];
1722
1723 let abs_tangent_complex = multiply_ad_value(builder, AdValue::Value(tangent_value), conj_sign)?;
1724 let abs_tangent = convert_ad_value(builder, abs_tangent_complex, input_dtype, output_dtype)?;
1725 let abs_tangent = convert_ad_value(builder, abs_tangent, output_dtype, input_dtype)?;
1726 let tangent_over_abs = divide_ad_value(builder, AdValue::Value(tangent_value), safe_abs)?;
1727 let sign_times_abs_tangent = multiply_ad_value(builder, abs_tangent, safe_sign)?;
1728 let correction = divide_ad_value(builder, sign_times_abs_tangent, safe_abs)?;
1729 let derivative = sub_ad_values(builder, tangent_over_abs, correction)?;
1730 let zero_derivative = zero_from_ad_value(builder, AdValue::Value(tangent_value))?;
1731 Ok(select_ad_values(
1732 builder,
1733 zero_mask,
1734 zero_derivative,
1735 derivative,
1736 )?)
1737}
1738
1739fn analytic_unary_coefficient(
1740 builder: &mut SemanticProgramBuilder,
1741 op: &CoreSemanticOp,
1742 primal_input: ProgramValue,
1743 role: SemanticTransformRole,
1744) -> Result<ProgramValue, SemanticAdTransformError> {
1745 let coefficient = match op {
1746 CoreSemanticOp::Exp | CoreSemanticOp::Expm1 => {
1747 builder.add_op(CoreSemanticOp::Exp, &[primal_input])?[0]
1748 }
1749 CoreSemanticOp::Log => {
1750 let one = one_like(builder, primal_input, role)?;
1751 builder.add_op(CoreSemanticOp::Div, &[one, primal_input])?[0]
1752 }
1753 CoreSemanticOp::Sin => builder.add_op(CoreSemanticOp::Cos, &[primal_input])?[0],
1754 CoreSemanticOp::Cos => {
1755 let sin = builder.add_op(CoreSemanticOp::Sin, &[primal_input])?[0];
1756 builder.add_op(CoreSemanticOp::Neg, &[sin])?[0]
1757 }
1758 CoreSemanticOp::Tanh => {
1759 let tanh = builder.add_op(CoreSemanticOp::Tanh, &[primal_input])?[0];
1760 let square = builder.add_op(CoreSemanticOp::Mul, &[tanh, tanh])?[0];
1761 let one = one_like(builder, primal_input, role)?;
1762 builder.add_op(CoreSemanticOp::Sub, &[one, square])?[0]
1763 }
1764 CoreSemanticOp::Sqrt => {
1765 let sqrt = builder.add_op(CoreSemanticOp::Sqrt, &[primal_input])?[0];
1766 let twice = builder.add_op(CoreSemanticOp::Add, &[sqrt, sqrt])?[0];
1767 let one = one_like(builder, primal_input, role)?;
1768 builder.add_op(CoreSemanticOp::Div, &[one, twice])?[0]
1769 }
1770 CoreSemanticOp::Rsqrt => {
1771 let rsqrt = builder.add_op(CoreSemanticOp::Rsqrt, &[primal_input])?[0];
1772 let negated = builder.add_op(CoreSemanticOp::Neg, &[rsqrt])?[0];
1773 let twice = builder.add_op(CoreSemanticOp::Add, &[primal_input, primal_input])?[0];
1774 builder.add_op(CoreSemanticOp::Div, &[negated, twice])?[0]
1775 }
1776 CoreSemanticOp::Log1p => {
1777 let one = one_like(builder, primal_input, role)?;
1778 let denominator = builder.add_op(CoreSemanticOp::Add, &[primal_input, one])?[0];
1779 builder.add_op(CoreSemanticOp::Div, &[one, denominator])?[0]
1780 }
1781 _ => return Err(unsupported_core(role, op)),
1782 };
1783 Ok(coefficient)
1784}
1785
1786fn one_like(
1787 builder: &mut SemanticProgramBuilder,
1788 anchor: ProgramValue,
1789 role: SemanticTransformRole,
1790) -> Result<ProgramValue, SemanticAdTransformError> {
1791 let metadata = builder.value_metadata(anchor)?.clone();
1792 let dtype = metadata.dtype();
1793 let bytes = match dtype {
1794 DType::F32 => 1.0_f32.to_le_bytes().to_vec(),
1795 DType::F64 => 1.0_f64.to_le_bytes().to_vec(),
1796 DType::C32 => {
1797 let mut bytes = 1.0_f32.to_le_bytes().to_vec();
1798 bytes.extend_from_slice(&0.0_f32.to_le_bytes());
1799 bytes
1800 }
1801 DType::C64 => {
1802 let mut bytes = 1.0_f64.to_le_bytes().to_vec();
1803 bytes.extend_from_slice(&0.0_f64.to_le_bytes());
1804 bytes
1805 }
1806 _ => {
1807 return Err(SemanticAdTransformError::UnsupportedMetadata {
1808 role,
1809 message: format!("cannot construct a differentiable one for {dtype:?}"),
1810 });
1811 }
1812 };
1813 let scalar = builder.add_op(CoreSemanticOp::Constant { dtype, bytes }, &[])?[0];
1814 if metadata.shape().is_empty() {
1815 Ok(scalar)
1816 } else {
1817 let shape = shape_plan(metadata.shape(), role, "one-like anchor")?;
1818 let one = broadcast_value_in_dim_to_shape(builder, scalar, anchor, &shape, Vec::new())?;
1819 Ok(truncate_value_to_dynamic_axes(
1820 builder,
1821 one,
1822 anchor,
1823 &shape.dynamic_axes,
1824 )?)
1825 }
1826}
1827
1828fn active_cotangent(
1829 builder: &mut SemanticProgramBuilder,
1830 cotangent: AdValue,
1831 active: bool,
1832 primal_input: ProgramValue,
1833) -> Result<AdValue, SemanticAdTransformError> {
1834 normalize_ad_value(builder, cotangent, active, primal_input)
1835}
1836
1837fn normalize_ad_value(
1838 builder: &mut SemanticProgramBuilder,
1839 value: AdValue,
1840 active: bool,
1841 primal_input: ProgramValue,
1842) -> Result<AdValue, SemanticAdTransformError> {
1843 if !active {
1844 return Ok(AdValue::Absent);
1845 }
1846 let AdValue::Value(mut value) = value else {
1847 return Ok(AdValue::Absent);
1848 };
1849 let target_metadata = builder.value_metadata(primal_input)?.clone();
1850 let value_metadata = builder.value_metadata(value)?.clone();
1851 let target_shape = shape_plan(
1852 target_metadata.shape(),
1853 SemanticTransformRole::Vjp,
1854 "primal input",
1855 )?;
1856 let value_shape = shape_plan(
1857 value_metadata.shape(),
1858 SemanticTransformRole::Vjp,
1859 "cotangent",
1860 )?;
1861 if value_shape.shape.len() < target_shape.shape.len() {
1862 return Err(SemanticAdTransformError::UnsupportedMetadata {
1863 role: SemanticTransformRole::Vjp,
1864 message: "cotangent rank is smaller than its primal-input rank".into(),
1865 });
1866 }
1867 let leading = value_shape.shape.len() - target_shape.shape.len();
1868 let mut axes: Vec<_> = (0..leading).collect();
1869 axes.extend(
1870 target_shape
1871 .shape
1872 .iter()
1873 .zip(value_shape.shape.iter().skip(leading))
1874 .enumerate()
1875 .filter_map(|(axis, (target, actual))| {
1876 (matches!(target, tenferro_ops::dim_expr::DimExpr::Const(1)) && target != actual)
1877 .then_some(axis + leading)
1878 }),
1879 );
1880 if !axes.is_empty() {
1881 value = builder.add_op(CoreSemanticOp::ReduceSum { axes }, &[value])?[0];
1882 }
1883 if builder.value_metadata(value)?.shape() != target_metadata.shape() {
1884 value = reshape_value_to_shape(builder, value, primal_input, &target_shape)?;
1885 }
1886 value =
1887 truncate_value_to_dynamic_axes(builder, value, primal_input, &target_shape.dynamic_axes)?;
1888 let value_dtype = builder.value_metadata(value)?.dtype();
1889 if value_dtype != target_metadata.dtype() {
1890 value = builder.add_op(
1891 CoreSemanticOp::Convert {
1892 from: value_dtype,
1893 to: target_metadata.dtype(),
1894 },
1895 &[value],
1896 )?[0];
1897 }
1898 Ok(AdValue::Value(value))
1899}
1900
1901fn exact_shape(
1902 shape: &[tenferro_ops::ShapeExtent<tenferro_ops::dim_expr::DimExpr>],
1903 role: SemanticTransformRole,
1904 field: &'static str,
1905) -> Result<Vec<tenferro_ops::dim_expr::DimExpr>, SemanticAdTransformError> {
1906 shape
1907 .iter()
1908 .map(|extent| {
1909 extent.as_exact().cloned().ok_or_else(|| {
1910 SemanticAdTransformError::UnsupportedMetadata {
1911 role,
1912 message: format!("{field} has a bounded or unknown extent"),
1913 }
1914 })
1915 })
1916 .collect()
1917}
1918
1919fn value_shape_plan(
1920 builder: &SemanticProgramBuilder,
1921 value: ProgramValue,
1922 role: SemanticTransformRole,
1923 field: &'static str,
1924) -> Result<ValueShapePlan, SemanticAdTransformError> {
1925 shape_plan(builder.value_metadata(value)?.shape(), role, field)
1926}
1927
1928fn shape_plan(
1929 shape: &[ShapeExtent<DimExpr>],
1930 role: SemanticTransformRole,
1931 field: &'static str,
1932) -> Result<ValueShapePlan, SemanticAdTransformError> {
1933 let mut planned_shape = Vec::with_capacity(shape.len());
1934 let mut dynamic_axes = Vec::new();
1935 for (axis, extent) in shape.iter().enumerate() {
1936 match extent {
1937 ShapeExtent::Exact(expression) => planned_shape.push(expression.clone()),
1938 ShapeExtent::UpperBound(expression) => {
1939 planned_shape.push(expression.clone());
1940 dynamic_axes.push(axis);
1941 }
1942 ShapeExtent::Unknown => {
1943 return Err(SemanticAdTransformError::UnsupportedMetadata {
1944 role,
1945 message: format!("{field} has an unknown extent without an upper bound"),
1946 });
1947 }
1948 }
1949 }
1950 Ok(ValueShapePlan {
1951 shape: planned_shape,
1952 dynamic_axes,
1953 })
1954}
1955
1956fn reshape_ad_value_to_shape(
1957 builder: &mut SemanticProgramBuilder,
1958 value: AdValue,
1959 shape_source: ProgramValue,
1960 shape: &ValueShapePlan,
1961) -> Result<AdValue, ProgramBuildError> {
1962 match value {
1963 AdValue::Absent => Ok(AdValue::Absent),
1964 AdValue::Value(value) => Ok(AdValue::Value(reshape_value_to_shape(
1965 builder,
1966 value,
1967 shape_source,
1968 shape,
1969 )?)),
1970 }
1971}
1972
1973fn reshape_value_to_shape(
1974 builder: &mut SemanticProgramBuilder,
1975 value: ProgramValue,
1976 shape_source: ProgramValue,
1977 shape: &ValueShapePlan,
1978) -> Result<ProgramValue, ProgramBuildError> {
1979 let mut inputs = vec![value];
1980 let to_shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
1981 Ok(builder.add_op(CoreSemanticOp::Reshape { to_shape }, &inputs)?[0])
1982}
1983
1984fn broadcast_ad_value_in_dim_to_shape(
1985 builder: &mut SemanticProgramBuilder,
1986 value: AdValue,
1987 shape_source: ProgramValue,
1988 shape: &ValueShapePlan,
1989 dims: Vec<usize>,
1990) -> Result<AdValue, ProgramBuildError> {
1991 match value {
1992 AdValue::Absent => Ok(AdValue::Absent),
1993 AdValue::Value(value) => Ok(AdValue::Value(broadcast_value_in_dim_to_shape(
1994 builder,
1995 value,
1996 shape_source,
1997 shape,
1998 dims,
1999 )?)),
2000 }
2001}
2002
2003fn broadcast_value_in_dim_to_shape(
2004 builder: &mut SemanticProgramBuilder,
2005 value: ProgramValue,
2006 shape_source: ProgramValue,
2007 shape: &ValueShapePlan,
2008 dims: Vec<usize>,
2009) -> Result<ProgramValue, ProgramBuildError> {
2010 let mut inputs = vec![value];
2011 let shape = payload_shape_for_shape_source(shape, &mut inputs, shape_source);
2012 Ok(builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &inputs)?[0])
2013}
2014
2015fn payload_shape_for_shape_source(
2016 shape: &ValueShapePlan,
2017 inputs: &mut Vec<ProgramValue>,
2018 shape_source: ProgramValue,
2019) -> Vec<DimExpr> {
2020 if DimExpr::max_input_idx_all(&shape.shape).is_none() {
2021 return shape.shape.clone();
2022 }
2023 let input_idx = inputs
2024 .iter()
2025 .position(|&input| input == shape_source)
2026 .unwrap_or_else(|| {
2027 let input_idx = inputs.len();
2028 inputs.push(shape_source);
2029 input_idx
2030 });
2031 DimExpr::input_shape(input_idx, shape.shape.len())
2032}
2033
2034fn truncate_ad_value_to_dynamic_axes(
2035 builder: &mut SemanticProgramBuilder,
2036 value: AdValue,
2037 shape_source: ProgramValue,
2038 dynamic_axes: &[usize],
2039) -> Result<AdValue, SemanticAdTransformError> {
2040 let AdValue::Value(value) = value else {
2041 return Ok(AdValue::Absent);
2042 };
2043 Ok(AdValue::Value(truncate_value_to_dynamic_axes(
2044 builder,
2045 value,
2046 shape_source,
2047 dynamic_axes,
2048 )?))
2049}
2050
2051fn truncate_value_to_dynamic_axes(
2052 builder: &mut SemanticProgramBuilder,
2053 mut value: ProgramValue,
2054 shape_source: ProgramValue,
2055 dynamic_axes: &[usize],
2056) -> Result<ProgramValue, ProgramBuildError> {
2057 for &axis in dynamic_axes {
2058 let size = builder.add_op(CoreSemanticOp::ShapeOf { axis }, &[shape_source])?[0];
2059 value = builder.add_op(CoreSemanticOp::DynamicTruncate { axis }, &[value, size])?[0];
2060 }
2061 Ok(value)
2062}
2063
2064fn conjugate_if_complex(
2065 builder: &mut SemanticProgramBuilder,
2066 value: ProgramValue,
2067) -> Result<ProgramValue, ProgramBuildError> {
2068 if matches!(
2069 builder.value_metadata(value)?.dtype(),
2070 DType::C32 | DType::C64
2071 ) {
2072 Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
2073 } else {
2074 Ok(value)
2075 }
2076}
2077
2078fn finish_derivative(
2079 builder: SemanticProgramBuilder,
2080 derivative_input_indices: Vec<Option<usize>>,
2081 values: Vec<AdValue>,
2082) -> Result<SemanticAdProgram, SemanticAdTransformError> {
2083 let mut outputs = Vec::new();
2084 let derivative_output_indices = values
2085 .into_iter()
2086 .map(|value| match value {
2087 AdValue::Absent => None,
2088 AdValue::Value(value) => {
2089 let index = outputs.len();
2090 outputs.push(value);
2091 Some(index)
2092 }
2093 })
2094 .collect();
2095 let frozen = builder.finish(&outputs)?;
2096 let frozen = prune_dead_derivative_operations(frozen)?;
2097 let frozen = cancel_double_neg_derivative_operations(frozen)?;
2098 Ok(SemanticAdProgram {
2099 frozen,
2100 derivative_input_indices: derivative_input_indices.into_boxed_slice(),
2101 derivative_output_indices,
2102 })
2103}
2104
2105fn prune_dead_derivative_operations(
2106 frozen: FrozenProgram,
2107) -> Result<FrozenProgram, SemanticAdTransformError> {
2108 let mut roots = frozen.program.inputs().to_vec();
2109 let output_offset = roots.len();
2110 roots.extend_from_slice(frozen.program.outputs());
2111
2112 let mut builder = SemanticProgramBuilder::new();
2113 let imported = builder.import(ProgramImport {
2114 program: frozen.program.as_ref(),
2115 bindings: &frozen.bindings,
2116 roots: &roots,
2117 })?;
2118 let outputs = imported.roots()[output_offset..].to_vec();
2119 Ok(builder.finish(&outputs)?)
2120}
2121
2122fn cancel_double_neg_derivative_operations(
2123 frozen: FrozenProgram,
2124) -> Result<FrozenProgram, SemanticAdTransformError> {
2125 let operations = frozen.program.operations().collect::<Vec<_>>();
2126 if operations.iter().any(|operation| {
2127 !operation.effects().is_empty()
2128 || !operation.shape_guards().is_empty()
2129 || !matches!(operation.op(), SemanticOpRef::Core(_))
2130 }) {
2131 return Ok(frozen);
2132 }
2133
2134 let mut builder = SemanticProgramBuilder::new();
2135 let imported = builder.import(ProgramImport {
2136 program: frozen.program.as_ref(),
2137 bindings: &frozen.bindings,
2138 roots: frozen.program.inputs(),
2139 })?;
2140 let mut values = frozen
2141 .program
2142 .inputs()
2143 .iter()
2144 .copied()
2145 .zip(imported.roots().iter().copied())
2146 .collect::<HashMap<_, _>>();
2147 let mut neg_inputs = HashMap::<ProgramValue, ProgramValue>::new();
2148 let mut changed = false;
2149
2150 for operation in operations {
2151 let inputs = operation
2152 .inputs()
2153 .iter()
2154 .copied()
2155 .map(|value| {
2156 values.get(&value).copied().ok_or_else(|| {
2157 SemanticAdTransformError::UnsupportedMetadata {
2158 role: SemanticTransformRole::Jvp,
2159 message: "derivative simplifier saw an unmapped value".into(),
2160 }
2161 })
2162 })
2163 .collect::<Result<Vec<_>, _>>()?;
2164 let SemanticOpRef::Core(op) = operation.op() else {
2165 unreachable!("non-core operations returned above");
2166 };
2167
2168 if matches!(op, CoreSemanticOp::Neg) {
2169 let input = inputs[0];
2170 if let Some(inner) = neg_inputs.get(&input).copied() {
2171 values.insert(operation.outputs()[0], inner);
2172 changed = true;
2173 continue;
2174 }
2175 let output = builder.add_op(CoreSemanticOp::Neg, &[input])?[0];
2176 neg_inputs.insert(output, input);
2177 values.insert(operation.outputs()[0], output);
2178 continue;
2179 }
2180
2181 let outputs = builder.add_op(op.clone(), &inputs)?;
2182 for (source, output) in operation
2183 .outputs()
2184 .iter()
2185 .copied()
2186 .zip(outputs.iter().copied())
2187 {
2188 values.insert(source, output);
2189 }
2190 }
2191
2192 if !changed {
2193 return Ok(frozen);
2194 }
2195
2196 let outputs = frozen
2197 .program
2198 .outputs()
2199 .iter()
2200 .copied()
2201 .map(|value| {
2202 values.get(&value).copied().ok_or_else(|| {
2203 SemanticAdTransformError::UnsupportedMetadata {
2204 role: SemanticTransformRole::Jvp,
2205 message: "derivative simplifier saw an unmapped output".into(),
2206 }
2207 })
2208 })
2209 .collect::<Result<Vec<_>, _>>()?;
2210 prune_dead_derivative_operations(builder.finish(&outputs)?)
2211}
2212
2213fn validate_activity(
2214 role: SemanticTransformRole,
2215 field: &'static str,
2216 expected: usize,
2217 actual: usize,
2218) -> Result<(), SemanticAdTransformError> {
2219 if expected == actual {
2220 Ok(())
2221 } else {
2222 Err(SemanticAdTransformError::ActivityArity {
2223 role,
2224 field,
2225 expected,
2226 actual,
2227 })
2228 }
2229}
2230
2231fn unsupported_core(role: SemanticTransformRole, op: &CoreSemanticOp) -> SemanticAdTransformError {
2232 SemanticAdTransformError::UnsupportedCore {
2233 role,
2234 op: format!("{op:?}"),
2235 }
2236}