pub mod context;
#[cfg(feature = "autodiff")]
mod analytic;
#[cfg(feature = "autodiff")]
mod contraction;
#[cfg(feature = "autodiff")]
mod diagonal;
#[cfg(feature = "autodiff")]
mod dynamic;
#[cfg(feature = "autodiff")]
mod elementwise;
#[cfg(feature = "autodiff")]
mod indexing;
#[cfg(feature = "autodiff")]
pub(crate) mod registry;
#[cfg(feature = "autodiff")]
mod semiring;
#[cfg(feature = "autodiff")]
mod structural;
#[cfg(feature = "autodiff")]
#[doc(hidden)]
pub mod support;
#[cfg(feature = "autodiff")]
#[doc(hidden)]
pub mod transpose_input;
#[cfg(feature = "autodiff")]
mod zeros;
#[cfg(feature = "autodiff")]
use crate::ext_op::ExtensionOp;
#[cfg(feature = "autodiff")]
use crate::std_tensor_op::StdTensorOp;
#[cfg(feature = "autodiff")]
use computegraph::graph::GraphBuilder;
#[cfg(feature = "autodiff")]
use computegraph::types::{LocalValueId, OperationRole, ValueKey, ValueRef};
#[cfg(feature = "autodiff")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ADRuleKind {
Jvp,
Transpose,
}
#[cfg(feature = "autodiff")]
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ADRuleError {
#[error("unsupported AD rule {rule:?} for {op}")]
Unsupported { op: String, rule: ADRuleKind },
#[error("invalid AD rule input for {op} ({rule:?}): {message}")]
InvalidInput {
op: String,
rule: ADRuleKind,
message: String,
},
}
#[cfg(feature = "autodiff")]
impl ADRuleError {
pub fn unsupported(op: impl Into<String>, rule: ADRuleKind) -> Self {
Self::Unsupported {
op: op.into(),
rule,
}
}
pub fn invalid_input(
op: impl Into<String>,
rule: ADRuleKind,
message: impl Into<String>,
) -> Self {
Self::InvalidInput {
op: op.into(),
rule,
message: message.into(),
}
}
pub const fn rule(&self) -> ADRuleKind {
match self {
Self::Unsupported { rule, .. } | Self::InvalidInput { rule, .. } => *rule,
}
}
}
#[cfg(feature = "autodiff")]
pub type ADRuleResult<T> = std::result::Result<T, ADRuleError>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ResidualSpec {
input_mask: u64,
output_mask: u64,
}
impl ResidualSpec {
pub const fn none() -> Self {
Self {
input_mask: 0,
output_mask: 0,
}
}
pub const fn input(index: usize) -> Self {
Self {
input_mask: 1 << index,
output_mask: 0,
}
}
pub const fn output(index: usize) -> Self {
Self {
input_mask: 0,
output_mask: 1 << index,
}
}
pub const fn with_input(mut self, index: usize) -> Self {
self.input_mask |= 1 << index;
self
}
pub const fn with_output(mut self, index: usize) -> Self {
self.output_mask |= 1 << index;
self
}
pub const fn with_all_inputs(mut self) -> Self {
self.input_mask = u64::MAX;
self
}
pub const fn with_all_outputs(mut self) -> Self {
self.output_mask = u64::MAX;
self
}
pub const fn all_inputs() -> Self {
Self {
input_mask: u64::MAX,
output_mask: 0,
}
}
pub const fn all_outputs() -> Self {
Self {
input_mask: 0,
output_mask: u64::MAX,
}
}
pub const fn declares_input(&self, index: usize) -> bool {
index < 64 && (self.input_mask >> index) & 1 == 1
}
pub const fn declares_output(&self, index: usize) -> bool {
index < 64 && (self.output_mask >> index) & 1 == 1
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.input_mask == 0 && self.output_mask == 0
}
}
#[cfg(feature = "autodiff")]
#[derive(Clone, Debug)]
pub enum PrimitiveTransposeInput<Op: computegraph::GraphOperation> {
Residual(ValueKey<Op>),
Linear {
key: ValueKey<Op>,
primal: Option<ValueKey<Op>>,
},
}
#[cfg(feature = "autodiff")]
impl<Op: computegraph::GraphOperation> PrimitiveTransposeInput<Op> {
pub fn key(&self) -> &ValueKey<Op> {
match self {
Self::Residual(key) | Self::Linear { key, .. } => key,
}
}
}
#[cfg(feature = "autodiff")]
fn missing_primitive_kind(op: &StdTensorOp, rule: ADRuleKind) -> ADRuleError {
ADRuleError::invalid_input(
"tenferro-internal-ops primitive AD dispatch",
rule,
format!("non-extension operation has no primitive kind: {op:?}"),
)
}
#[cfg(feature = "autodiff")]
pub trait PrimitiveRuleBuilder {
fn add_operation(
&mut self,
operation: StdTensorOp,
inputs: Vec<ValueRef<StdTensorOp>>,
role: OperationRole,
) -> Vec<LocalValueId>;
}
#[cfg(feature = "autodiff")]
impl PrimitiveRuleBuilder for GraphBuilder<StdTensorOp> {
fn add_operation(
&mut self,
operation: StdTensorOp,
inputs: Vec<ValueRef<StdTensorOp>>,
role: OperationRole,
) -> Vec<LocalValueId> {
GraphBuilder::add_operation(self, operation, inputs, role)
}
}
#[doc(hidden)]
#[cfg(feature = "autodiff")]
pub trait ExtensionAdDispatcher: std::fmt::Debug + Send + Sync + 'static {
fn has_primal_vjp(&self, family_id: &str) -> bool;
fn linearize(
&self,
op: &dyn ExtensionOp,
builder: &mut dyn PrimitiveRuleBuilder,
primal_in: &[ValueKey<StdTensorOp>],
primal_out: &[ValueKey<StdTensorOp>],
tangent_in: &[Option<LocalValueId>],
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>>;
fn transpose(
&self,
op: &dyn ExtensionOp,
builder: &mut dyn PrimitiveRuleBuilder,
cotangent_out: &[Option<LocalValueId>],
inputs: &[PrimitiveTransposeInput<StdTensorOp>],
mode: &OperationRole,
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>>;
}
#[cfg(feature = "autodiff")]
fn dispatch_extension_linearize(
op: &dyn ExtensionOp,
builder: &mut dyn PrimitiveRuleBuilder,
primal_in: &[ValueKey<StdTensorOp>],
primal_out: &[ValueKey<StdTensorOp>],
tangent_in: &[Option<LocalValueId>],
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>> {
if let Some(dispatcher) = ctx.extension_ad_dispatcher() {
return dispatcher.linearize(op, builder, primal_in, primal_out, tangent_in, ctx);
}
Err(ADRuleError::unsupported(op.family_id(), ADRuleKind::Jvp))
}
#[cfg(feature = "autodiff")]
fn dispatch_extension_transpose(
op: &dyn ExtensionOp,
builder: &mut dyn PrimitiveRuleBuilder,
cotangent_out: &[Option<LocalValueId>],
inputs: &[PrimitiveTransposeInput<StdTensorOp>],
mode: &OperationRole,
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>> {
if let Some(dispatcher) = ctx.extension_ad_dispatcher() {
return dispatcher.transpose(op, builder, cotangent_out, inputs, mode, ctx);
}
Err(ADRuleError::unsupported(
op.family_id(),
ADRuleKind::Transpose,
))
}
#[cfg(feature = "autodiff")]
pub fn linearize(
op: &StdTensorOp,
builder: &mut dyn PrimitiveRuleBuilder,
primal_in: &[ValueKey<StdTensorOp>],
primal_out: &[ValueKey<StdTensorOp>],
tangent_in: &[Option<LocalValueId>],
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>> {
if let StdTensorOp::Extension(ext) = op {
return dispatch_extension_linearize(
ext.as_ref(),
builder,
primal_in,
primal_out,
tangent_in,
ctx,
);
}
let kind = op
.primitive_kind()
.ok_or_else(|| missing_primitive_kind(op, ADRuleKind::Jvp))?;
let rule = registry::primitive_ad_rule(kind)
.ok_or_else(|| registry::missing_rule(kind, ADRuleKind::Jvp))?;
rule.linearize(op, builder, primal_in, primal_out, tangent_in, ctx)
}
#[cfg(feature = "autodiff")]
pub fn transpose_rule(
op: &StdTensorOp,
builder: &mut impl PrimitiveRuleBuilder,
cotangent_out: &[Option<LocalValueId>],
inputs: &[PrimitiveTransposeInput<StdTensorOp>],
mode: &OperationRole,
ctx: &mut context::ShapeGuardContext,
) -> ADRuleResult<Vec<Option<LocalValueId>>> {
if let StdTensorOp::Extension(ext) = op {
let builder_dyn: &mut dyn PrimitiveRuleBuilder = builder;
return dispatch_extension_transpose(
ext.as_ref(),
builder_dyn,
cotangent_out,
inputs,
mode,
ctx,
);
}
let kind = op
.primitive_kind()
.ok_or_else(|| missing_primitive_kind(op, ADRuleKind::Transpose))?;
let rule = registry::primitive_ad_rule(kind)
.ok_or_else(|| registry::missing_rule(kind, ADRuleKind::Transpose))?;
let mask = rule.residual_mask();
let transpose_inputs = inputs
.iter()
.enumerate()
.map(|(index, input)| transpose_input::TransposeInputRef::new(input, index, mask))
.collect::<Vec<_>>();
let builder_dyn: &mut dyn PrimitiveRuleBuilder = builder;
rule.transpose_rule(op, builder_dyn, cotangent_out, &transpose_inputs, mode, ctx)
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod residual_spec_tests {
use super::ResidualSpec;
#[test]
fn residual_spec_builders_and_queries_cover_always_on_api() {
let none = ResidualSpec::none();
assert!(none.is_empty());
assert!(!none.declares_input(0));
assert!(!none.declares_output(0));
let single = ResidualSpec::input(1).with_output(0);
assert!(!single.declares_input(0));
assert!(single.declares_input(1));
assert!(single.declares_output(0));
assert!(!single.is_empty());
let chained = ResidualSpec::output(2).with_input(0).with_input(3);
assert!(chained.declares_input(0) && chained.declares_input(3));
assert!(!chained.declares_input(1));
assert!(chained.declares_output(2));
let all_in = ResidualSpec::all_inputs();
assert!(all_in.declares_input(0) && all_in.declares_input(63));
assert!(!all_in.declares_output(0));
let all_out = ResidualSpec::all_outputs();
assert!(all_out.declares_output(0) && all_out.declares_output(63));
assert!(!all_out.declares_input(0));
let both = ResidualSpec::all_inputs().with_all_outputs();
assert!(both.declares_input(63) && both.declares_output(63));
assert!(!both.declares_input(64) && !both.declares_output(64));
assert!(ResidualSpec::default().is_empty());
}
}