tenferro-linalg 0.3.0

Linear algebra traced APIs, eager helpers, extension runtime, and optional AD rules for tenferro.
use super::*;

use tenferro_tensor::{CpuDomainId, MemoryKind, Placement};

fn remote_domain(selected: CpuDomainId) -> CpuDomainId {
    let candidate = CpuDomainId::new(selected.as_u64().wrapping_add(1));
    if candidate == selected {
        CpuDomainId::new(selected.as_u64().wrapping_sub(1))
    } else {
        candidate
    }
}

fn placed_matrix(values: Vec<f64>, domain: CpuDomainId) -> Tensor {
    let mut tensor = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], values).unwrap();
    tensor.set_placement(Placement {
        memory_kind: MemoryKind::UnpinnedHost,
        device: None,
        cpu_affinity: Some(domain),
    });
    Tensor::F64(tensor)
}

fn assert_selected(outputs: &[Tensor], selected: CpuDomainId) {
    assert!(!outputs.is_empty());
    assert!(outputs
        .iter()
        .all(|output| output.placement().cpu_affinity == Some(selected)));
}

#[test]
fn linalg_fresh_tagging_is_field_only_and_allocation_free() {
    let backend = include_str!("../backend.rs");
    let tagging = backend
        .split_once("trait FreshLinalgOutput")
        .expect("CPU linalg should define fresh-output tagging")
        .1
        .split_once("impl LinalgBackend for CpuExecSession")
        .expect("fresh-output tagging should precede the linalg implementation")
        .0;

    assert!(tagging.contains("set_cpu_affinity(Some(domain))"));
    for forbidden in ["placement().clone", "format!", "HashMap", ".hash("] {
        assert!(
            !tagging.contains(forbidden),
            "fresh linalg tagging must not contain `{forbidden}`"
        );
    }
}

#[test]
fn decomposition_vectors_tag_every_fresh_output_with_the_selected_domain() {
    let mut backend = CpuBackend::with_threads(1).unwrap();
    let selected = backend.execution_info().domain_id();
    let remote = remote_domain(selected);
    let general = placed_matrix(vec![4.0, 2.0, 1.0, 3.0], remote);
    let symmetric = placed_matrix(vec![4.0, 1.0, 1.0, 3.0], remote);

    let outputs = with_cpu_linalg(&mut backend, |backend| {
        vec![
            backend.svd(&general).unwrap(),
            backend.qr(&general).unwrap(),
            backend.lu(&general).unwrap(),
            backend.full_piv_lu(&general).unwrap(),
            backend.eigh(&symmetric).unwrap(),
            backend.eig(&general).unwrap(),
        ]
    });
    for outputs in outputs {
        assert_selected(&outputs, selected);
    }

    assert_eq!(general.placement().cpu_affinity, Some(remote));
    assert_eq!(symmetric.placement().cpu_affinity, Some(remote));
}

#[test]
fn linalg_single_outputs_tag_the_selected_domain() {
    let mut backend = CpuBackend::with_threads(1).unwrap();
    let selected = backend.execution_info().domain_id();
    let remote = remote_domain(selected);
    let general = placed_matrix(vec![4.0, 2.0, 1.0, 3.0], remote);
    let symmetric = placed_matrix(vec![4.0, 1.0, 1.0, 3.0], remote);

    let outputs = with_cpu_linalg(&mut backend, |backend| {
        vec![
            backend.svd_values(&general).unwrap(),
            backend.eigh_values(&symmetric).unwrap(),
            backend.eig_values(&general).unwrap(),
            backend.cholesky(&symmetric).unwrap(),
        ]
    });
    for output in outputs {
        assert_eq!(output.placement().cpu_affinity, Some(selected));
    }
}

#[test]
fn zero_extent_solve_output_is_still_tagged_as_a_fresh_allocation() {
    let mut backend = CpuBackend::with_threads(1).unwrap();
    let selected = backend.execution_info().domain_id();
    let remote = remote_domain(selected);
    let mut a = TypedTensor::<f64>::from_vec_col_major(vec![0, 0], vec![]).unwrap();
    let mut b = TypedTensor::<f64>::from_vec_col_major(vec![0], vec![]).unwrap();
    for tensor in [&mut a, &mut b] {
        tensor.set_placement(Placement {
            memory_kind: MemoryKind::UnpinnedHost,
            device: None,
            cpu_affinity: Some(remote),
        });
    }
    let a = Tensor::F64(a);
    let b = Tensor::F64(b);

    let output = with_cpu_linalg(&mut backend, |backend| {
        backend.full_piv_lu_solve(&a, &b, false)
    })
    .unwrap();

    assert_eq!(output.shape(), &[0]);
    assert_eq!(output.placement().cpu_affinity, Some(selected));
    assert_eq!(a.placement().cpu_affinity, Some(remote));
    assert_eq!(b.placement().cpu_affinity, Some(remote));
}

#[test]
fn prepared_lu_composition_tags_its_final_permutation_output() {
    let mut backend = CpuBackend::with_threads(1).unwrap();
    let selected = backend.execution_info().domain_id();
    let remote = remote_domain(selected);
    let a = placed_matrix(vec![4.0, 2.0, 1.0, 3.0], remote);
    let b = placed_matrix(vec![1.0, 2.0, 3.0, 4.0], remote);
    let (_factor, output) = with_cpu_linalg(&mut backend, |backend| {
        let factor = backend.lu_factor(&a)?;
        let output = backend.lu_solve_prepared(&a, &factor[0], &factor[1], &b, true, false)?;
        Ok::<_, tenferro_tensor::Error>((factor, output))
    })
    .unwrap();

    assert_eq!(output.placement().cpu_affinity, Some(selected));
    assert_eq!(a.placement().cpu_affinity, Some(remote));
    assert_eq!(b.placement().cpu_affinity, Some(remote));
}