tenferro-cpu 0.5.0

CPU backend, kernels, provider selection, and CPU resource pools for tenferro.
use super::*;

use tenferro_tensor::{ErrorKind, ValidationKind};

#[test]
fn cpu_context_from_env_respects_rayon_num_threads() {
    with_rayon_num_threads(Some("3"), || {
        let ctx = CpuContext::from_env();
        assert_eq!(ctx.num_threads(), 3);
    });
}

#[test]
fn cpu_context_from_env_falls_back_to_affinity_when_rayon_num_threads_is_absent() {
    with_rayon_num_threads(None, || {
        let ctx = CpuContext::from_env();
        assert_eq!(ctx.num_threads(), crate::available_parallelism());
    });
}

#[test]
fn cpu_context_from_env_falls_back_to_single_threaded_when_rayon_num_threads_is_invalid() {
    with_rayon_num_threads(Some("not-a-number"), || {
        let ctx = CpuContext::from_env();
        assert_eq!(ctx.num_threads(), 1);
    });
}

#[test]
fn cpu_context_try_from_env_rejects_invalid_rayon_num_threads() {
    with_rayon_num_threads(Some("not-a-number"), || {
        assert!(CpuContext::try_from_env().is_err());
    });
}

#[cfg(unix)]
#[test]
fn cpu_context_try_from_env_rejects_non_unicode_rayon_num_threads() {
    use std::os::unix::ffi::OsStringExt;

    let _guard = RayonNumThreadsEnvGuard::new(None);
    std::env::set_var("RAYON_NUM_THREADS", OsString::from_vec(vec![0xff]));

    let err = CpuContext::try_from_env().unwrap_err();

    assert!(matches!(
        &err,
        Error::Extension {
            op: "CpuContext::try_from_env",
            family: "cpu",
            kind: ErrorKind::Validation(ValidationKind::InvalidArgument),
            ..
        }
    ));
    assert!(std::error::Error::source(&err).is_some());
}

#[test]
fn cpu_context_try_from_env_rejects_zero_rayon_num_threads() {
    with_rayon_num_threads(Some("0"), || {
        let err = match CpuContext::try_from_env() {
            Ok(_) => panic!("expected zero RAYON_NUM_THREADS to be rejected"),
            Err(err) => err,
        };
        assert!(format!("{err}").contains("CpuContext::try_from_env"));
        assert!(format!("{err}").contains("thread count must be at least 1"));
    });
}

#[test]
fn cpu_context_with_threads_zero_returns_error() {
    assert!(CpuContext::with_threads(0).is_err());
}

#[test]
fn cpu_backend_new_matches_context_from_env() {
    with_rayon_num_threads(Some("2"), || {
        let backend = CpuBackend::new();
        assert_eq!(backend.num_threads(), 2);
    });
}

#[test]
fn cpu_backend_new_falls_back_to_affinity_when_rayon_num_threads_is_absent() {
    with_rayon_num_threads(None, || {
        let backend = CpuBackend::new();
        assert_eq!(backend.num_threads(), crate::available_parallelism());
    });
}

#[test]
fn cpu_backend_try_new_propagates_invalid_rayon_num_threads() {
    with_rayon_num_threads(Some("not-a-number"), || {
        assert!(CpuBackend::try_new().is_err());
    });
}

#[test]
fn test_with_backend_session_runs_compiled_ops() {
    let mut backend = CpuBackend::with_threads(2).unwrap();
    let result = backend.with_backend_session(|session| {
        session
            .add(
                &Tensor::F64(TypedTensor::from_vec_col_major(vec![2], vec![1.0, 2.0]).unwrap()),
                &Tensor::F64(TypedTensor::from_vec_col_major(vec![2], vec![3.0, 4.0]).unwrap()),
            )
            .unwrap()
    });
    assert_eq!(get_f64(&result, &[0]), 4.0);
    assert_eq!(get_f64(&result, &[1]), 6.0);
}

#[test]
fn cpu_context_install_enters_owned_pool() {
    let ctx = CpuContext::with_threads(2).unwrap();
    let seen_threads = ctx.install(rayon::current_num_threads);
    assert_eq!(seen_threads, 2);
}

#[test]
fn cpu_install_accepts_send_state() {
    let ctx = CpuContext::with_threads(2).unwrap();
    let state = Arc::new(41usize);
    let seen = ctx.install(|| *state + 1);
    assert_eq!(seen, 42);

    let backend = CpuBackend::with_threads(2).unwrap();
    let state = Arc::new(20usize);
    let seen = backend.install(|| *state + 2);
    assert_eq!(seen, 22);
}

#[test]
fn cpu_backend_multi_operation_session_enters_executor_once() {
    let context = Arc::new(CpuContext::with_threads(2).unwrap());
    let mut backend = CpuBackend::from_context(Arc::clone(&context));
    let lhs = Tensor::F64(TypedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap());
    let rhs = Tensor::F64(TypedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap());
    let before = context.executor_install_calls_for_test();

    backend.with_backend_session(|session| {
        session.add(&lhs, &rhs).unwrap();
        session.neg(&lhs).unwrap();
        session.mul(&lhs, &rhs).unwrap();
        session
            .dot_general(
                &lhs,
                &rhs,
                &DotGeneralConfig {
                    lhs_contracting_dims: vec![0],
                    rhs_contracting_dims: vec![0],
                    lhs_batch_dims: vec![],
                    rhs_batch_dims: vec![],
                },
            )
            .unwrap();
    });

    let install_delta = context.executor_install_calls_for_test() - before;
    match backend.execution_info().execution_mode() {
        crate::CpuExecutionMode::ProviderDefaultExclusive => assert_eq!(install_delta, 4),
        _ => assert_eq!(install_delta, 1),
    }

    let before_standalone = context.executor_install_calls_for_test();
    backend.add(&lhs, &rhs).unwrap();
    assert_eq!(
        context.executor_install_calls_for_test() - before_standalone,
        1
    );
}

#[test]
fn cpu_backend_shared_context() {
    let ctx = Arc::new(CpuContext::with_threads(3).unwrap());
    let b1 = CpuBackend::from_context(ctx.clone());
    let b2 = CpuBackend::from_context(ctx);
    assert_eq!(b1.context_id_for_test(), b2.context_id_for_test());
}

#[test]
fn cpu_affinity_available_parallelism_reports_positive_count() {
    assert!(crate::available_parallelism() >= 1);
}

#[test]
fn cpu_backend_from_context_shares_runtime_owner() {
    let ctx = Arc::new(CpuContext::with_threads(3).unwrap());
    let b1 = CpuBackend::from_context(ctx.clone());
    let b2 = CpuBackend::from_context(ctx);
    assert_eq!(b1.num_threads(), 3);
    assert_eq!(b2.num_threads(), 3);
}

#[test]
fn performance_notes_match_current_cpu_threading_contract() {
    let notes = include_str!("../../../../../docs/performance/tt-inner-product-overhead.md");
    let performance_tips = include_str!("../../../../../PERFORMANCE_TIPS.md");
    assert!(
        !notes.contains("The faer backend is therefore run without a tenferro-owned Rayon pool")
            && notes.contains("maps multi-threaded execution to explicit `Par::rayon(n)`")
            && !notes.contains("The global-Rayon columns became the production policy"),
        "performance notes must describe the explicit CpuContext thread budget, not ambient global-Rayon policy"
    );
    assert!(
        performance_tips.contains("explicit `Par::rayon(n)`")
            && !performance_tips
                .contains("Use `Par::Seq` for one-thread contexts and `Par::rayon(0)`"),
        "performance tips must derive Faer parallelism from the configured CpuContext degree"
    );
}

#[test]
fn cpu_context_with_threads_reports_requested_size() {
    let ctx = CpuContext::with_threads(2).unwrap();
    assert_eq!(ctx.num_threads(), 2);
}

#[test]
fn cpu_context_install_executes_closure() {
    let ctx = CpuContext::with_threads(1).unwrap();
    let seen = ctx.install(|| 1 + 1);
    assert_eq!(seen, 2);
}

fn env_lock() -> MutexGuard<'static, ()> {
    static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    ENV_LOCK
        .get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

struct RayonNumThreadsEnvGuard {
    _lock: MutexGuard<'static, ()>,
    prev: Option<OsString>,
}

impl RayonNumThreadsEnvGuard {
    fn new(value: Option<&str>) -> Self {
        let lock = env_lock();
        let prev = std::env::var_os("RAYON_NUM_THREADS");

        match value {
            Some(value) => std::env::set_var("RAYON_NUM_THREADS", value),
            None => std::env::remove_var("RAYON_NUM_THREADS"),
        }

        Self { _lock: lock, prev }
    }
}

impl Drop for RayonNumThreadsEnvGuard {
    fn drop(&mut self) {
        match self.prev.take() {
            Some(value) => std::env::set_var("RAYON_NUM_THREADS", value),
            None => std::env::remove_var("RAYON_NUM_THREADS"),
        }
    }
}

fn with_rayon_num_threads<T>(value: Option<&str>, f: impl FnOnce() -> T) -> T {
    let _guard = RayonNumThreadsEnvGuard::new(value);
    f()
}