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