tenferro-einsum 0.3.0

Subscripts, contraction planning, concrete/traced/eager einsum APIs, extension runtime, and AD rule for tenferro.
#![cfg(feature = "autodiff")]

use tenferro_einsum::{EinsumOptimize, TraceContextEinsumExt};
use tenferro_ops::dim_expr::DimExpr;
use tenferro_runtime::program::{ProgramInputSpec, SemanticFingerprint};
use tenferro_runtime::{ExtensionCacheSelector, GraphCompiler, TraceContext};
use tenferro_tensor::{DType, Tensor};

use super::support;

fn matrix() -> ProgramInputSpec {
    ProgramInputSpec::new(DType::F64, [DimExpr::Const(2), DimExpr::Const(2)])
}

#[test]
fn trace_context_parse_cache_reuses_exact_notation() {
    let mut trace = TraceContext::new();
    let lhs = trace.input(matrix()).unwrap();
    let rhs = trace.input(matrix()).unwrap();

    trace.einsum(&[lhs, rhs], "ij,jk->ik").unwrap();
    let first = trace
        .extension_caches_mut()
        .stats(ExtensionCacheSelector::All);
    trace.einsum(&[lhs, rhs], "ij,jk->ik").unwrap();
    let second = trace
        .extension_caches_mut()
        .stats(ExtensionCacheSelector::All);

    assert_eq!(first.entries, second.entries);
    assert!(first.entries >= 1);
}

#[test]
fn plan_policy_participates_in_semantic_fingerprint() {
    fn fingerprint(optimize: EinsumOptimize) -> SemanticFingerprint {
        let mut trace = TraceContext::new();
        let lhs = trace.input(matrix()).unwrap();
        let rhs = trace.input(matrix()).unwrap();
        let output = trace
            .einsum_with(&[lhs, rhs], "ij,jk->ik", optimize)
            .unwrap();
        trace
            .finish(&[output])
            .unwrap()
            .program()
            .semantic_fingerprint()
    }

    assert_ne!(
        fingerprint(EinsumOptimize::False),
        fingerprint(EinsumOptimize::Path(vec![(0, 1)]))
    );
}

#[test]
fn compiler_does_not_own_semantic_runtime_staging_cache() {
    let mut trace = TraceContext::new();
    let lhs = trace.input(matrix()).unwrap();
    let rhs = trace.input(matrix()).unwrap();
    let output = trace.einsum(&[lhs, rhs], "ij,jk->ik").unwrap();
    let graph = trace.finish(&[output]).unwrap();
    let mut compiler = GraphCompiler::new();

    compiler.compile_traced_graph(&graph).unwrap();
    let after_first = compiler.cache_stats().entries;
    compiler.compile_traced_graph(&graph).unwrap();

    assert_eq!(compiler.cache_stats().entries, after_first);
    assert_eq!(after_first, 0);
}

#[test]
fn runtime_plan_cache_reuses_identical_shapes() {
    let mut trace = TraceContext::new();
    let lhs = trace.input(matrix()).unwrap();
    let rhs = trace.input(matrix()).unwrap();
    let output = trace.einsum(&[lhs, rhs], "ij,jk->ik").unwrap();
    let graph = trace.finish(&[output]).unwrap();
    let compiled = GraphCompiler::new().compile_traced_graph(&graph).unwrap();
    let lhs = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0]).unwrap();
    let rhs = Tensor::from_vec_col_major(vec![2, 2], vec![5.0_f64, 7.0, 6.0, 8.0]).unwrap();
    let backend = tenferro_cpu::CpuBackend::new();
    let runtime = support::cpu_runtime_with_einsum(&backend).unwrap();

    runtime.run_compiled(&compiled, &[&lhs, &rhs]).unwrap();
    let after_first = runtime.cache_stats().unwrap().extensions.entries;
    runtime.run_compiled(&compiled, &[&lhs, &rhs]).unwrap();

    assert_eq!(
        runtime.cache_stats().unwrap().extensions.entries,
        after_first
    );
    assert!(after_first >= 1);
}