tenferro-cpu 0.2.0

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

#[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!(
        err.to_string().contains("failed to read RAYON_NUM_THREADS"),
        "{err}"
    );
}

#[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_exec_session_enters_owned_pool() {
    let mut backend = CpuBackend::with_threads(2).unwrap();
    let seen_threads = backend.with_backend_session(|_| rayon::current_num_threads());
    assert_eq!(seen_threads, 2);
}

#[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!(Arc::ptr_eq(&b1.ctx, &b2.ctx));
}

#[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);
}

#[cfg(feature = "cpu-faer")]
#[test]
fn cpu_context_faer_policy_is_seq_for_one_thread() {
    let ctx = CpuContext::with_threads(1).unwrap();
    assert!(matches!(ctx.faer_par(), faer::Par::Seq));
}

#[cfg(feature = "cpu-faer")]
#[test]
fn cpu_context_faer_policy_uses_current_context_pool_for_multithreaded_context() {
    let ctx = CpuContext::with_threads(2).unwrap();
    let par = ctx.install(|| ctx.faer_par());
    assert_eq!(par.degree(), 2);
}

#[test]
fn performance_notes_match_current_cpu_threading_contract() {
    let notes = include_str!("../../../../../docs/performance/tt-inner-product-overhead.md");
    assert!(
        !notes.contains("The faer backend is therefore run without a tenferro-owned Rayon pool")
            && !notes.contains("maps multi-threaded execution to `Par::rayon(n)`")
            && !notes.contains("The global-Rayon columns became the production policy"),
        "performance notes must describe CpuContext::install plus Par::rayon(0), not the stale global-Rayon policy"
    );
}

#[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()
}