use std::ffi::{CStr, c_int, c_void};
use std::sync::OnceLock;
use thiserror::Error;
pub fn get_num_cores() -> usize {
num_cpus::get()
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlasBackend {
OpenBlas,
Mkl,
Blis,
FlexiBlas,
Accelerate,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlasThreads {
Count(usize),
SingleThreaded,
BackendManaged,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BlasThreadSetting {
pub backend: BlasBackend,
pub threads: BlasThreads,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
pub enum BlasThreadingError {
#[error("the number of BLAS threads must be positive")]
ZeroThreads,
#[error("{0} BLAS threads requested, more than a C int can represent")]
TooManyThreads(usize),
#[error(
"no BLAS backend with thread control was found; statically linked backends require \
the matching `*_threading` feature"
)]
NoBackendFound,
#[error("{backend:?} rejected the threading setting with status {code}")]
Rejected {
backend: BlasBackend,
code: i32,
},
}
pub fn set_blas_threads(nthreads: usize) -> Result<Vec<BlasThreadSetting>, BlasThreadingError> {
let count = validate_thread_count(nthreads)?;
let backends = backends();
if backends.is_empty() {
return Err(BlasThreadingError::NoBackendFound);
}
for backend in backends {
backend.set(count)?;
}
Ok(backends.iter().map(Backend::get).collect())
}
pub fn get_blas_threads() -> Vec<BlasThreadSetting> {
backends().iter().map(Backend::get).collect()
}
fn validate_thread_count(nthreads: usize) -> Result<c_int, BlasThreadingError> {
if nthreads == 0 {
return Err(BlasThreadingError::ZeroThreads);
}
c_int::try_from(nthreads).map_err(|_| BlasThreadingError::TooManyThreads(nthreads))
}
fn reported_count(count: i64) -> BlasThreads {
usize::try_from(count)
.ok()
.filter(|&count| count > 0)
.map_or(BlasThreads::Unknown, BlasThreads::Count)
}
const ACCELERATE_MULTI_THREADED: u32 = 0;
const ACCELERATE_SINGLE_THREADED: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlisInt {
I32,
I64,
}
impl BlisInt {
fn from_type_size(bits: Option<c_int>) -> Self {
match bits {
Some(32) => BlisInt::I32,
_ => BlisInt::I64,
}
}
}
#[derive(Clone, Copy)]
enum Backend {
Int {
backend: BlasBackend,
set: unsafe extern "C" fn(c_int),
get: unsafe extern "C" fn() -> c_int,
},
Blis64 {
set: unsafe extern "C" fn(i64),
get: unsafe extern "C" fn() -> i64,
},
Accelerate {
set: unsafe extern "C" fn(u32) -> c_int,
get: unsafe extern "C" fn() -> u32,
},
}
impl Backend {
fn backend(&self) -> BlasBackend {
match *self {
Backend::Int { backend, .. } => backend,
Backend::Blis64 { .. } => BlasBackend::Blis,
Backend::Accelerate { .. } => BlasBackend::Accelerate,
}
}
fn set(&self, count: c_int) -> Result<(), BlasThreadingError> {
match *self {
Backend::Int { set, .. } => unsafe { set(count) },
Backend::Blis64 { set, .. } => unsafe { set(i64::from(count)) },
Backend::Accelerate { set, .. } => {
let mode = if count == 1 {
ACCELERATE_SINGLE_THREADED
} else {
ACCELERATE_MULTI_THREADED
};
let code = unsafe { set(mode) };
if code != 0 {
return Err(BlasThreadingError::Rejected {
backend: BlasBackend::Accelerate,
code,
});
}
}
}
Ok(())
}
fn get(&self) -> BlasThreadSetting {
let threads = match *self {
Backend::Int { get, .. } => reported_count(i64::from(unsafe { get() })),
Backend::Blis64 { get, .. } => reported_count(unsafe { get() }),
Backend::Accelerate { get, .. } => match unsafe { get() } {
ACCELERATE_SINGLE_THREADED => BlasThreads::SingleThreaded,
ACCELERATE_MULTI_THREADED => BlasThreads::BackendManaged,
_ => BlasThreads::Unknown,
},
};
BlasThreadSetting {
backend: self.backend(),
threads,
}
}
}
fn backends() -> &'static [Backend] {
static BACKENDS: OnceLock<Vec<Backend>> = OnceLock::new();
BACKENDS.get_or_init(resolve)
}
struct Symbols {
set: *mut c_void,
get: *mut c_void,
}
unsafe fn as_fn<F: Copy>(ptr: *mut c_void) -> F {
const { assert!(size_of::<F>() == size_of::<*mut c_void>()) };
debug_assert!(!ptr.is_null());
unsafe { std::mem::transmute_copy(&ptr) }
}
fn resolve() -> Vec<Backend> {
let mut backends = Vec::new();
let int_backends = [
(BlasBackend::FlexiBlas, flexiblas_symbols()),
(BlasBackend::OpenBlas, openblas_symbols()),
(BlasBackend::Mkl, mkl_symbols()),
];
for (backend, symbols) in int_backends {
if let Some(Symbols { set, get }) = symbols {
backends.push(unsafe {
Backend::Int {
backend,
set: as_fn(set),
get: as_fn(get),
}
});
}
}
if let Some((Symbols { set, get }, int_type_size)) = blis_symbols() {
let bits = (!int_type_size.is_null()).then(|| {
unsafe { as_fn::<unsafe extern "C" fn() -> c_int>(int_type_size)() }
});
backends.push(unsafe {
match BlisInt::from_type_size(bits) {
BlisInt::I32 => Backend::Int {
backend: BlasBackend::Blis,
set: as_fn(set),
get: as_fn(get),
},
BlisInt::I64 => Backend::Blis64 {
set: as_fn(set),
get: as_fn(get),
},
}
});
}
if let Some(Symbols { set, get }) = lookup_pair(&[(c"BLASSetThreading", c"BLASGetThreading")]) {
backends.push(unsafe {
Backend::Accelerate {
set: as_fn(set),
get: as_fn(get),
}
});
}
backends
}
fn lookup(name: &CStr) -> *mut c_void {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
dl::lookup(name)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = name;
std::ptr::null_mut()
}
}
fn lookup_pair(candidates: &[(&CStr, &CStr)]) -> Option<Symbols> {
candidates.iter().find_map(|&(set, get)| {
let (set, get) = (lookup(set), lookup(get));
(!set.is_null() && !get.is_null()).then_some(Symbols { set, get })
})
}
fn flexiblas_symbols() -> Option<Symbols> {
lookup_pair(&[(c"flexiblas_set_num_threads", c"flexiblas_get_num_threads")])
}
fn openblas_symbols() -> Option<Symbols> {
#[cfg(feature = "openblas_threading")]
{
Some(Symbols {
set: linked::openblas_set_num_threads as *mut c_void,
get: linked::openblas_get_num_threads as *mut c_void,
})
}
#[cfg(not(feature = "openblas_threading"))]
{
lookup_pair(&[
(c"openblas_set_num_threads", c"openblas_get_num_threads"),
(
c"openblas_set_num_threads64_",
c"openblas_get_num_threads64_",
),
])
}
}
fn mkl_symbols() -> Option<Symbols> {
#[cfg(feature = "mkl_threading")]
{
Some(Symbols {
set: linked::MKL_Set_Num_Threads as *mut c_void,
get: linked::MKL_Get_Max_Threads as *mut c_void,
})
}
#[cfg(not(feature = "mkl_threading"))]
{
lookup_pair(&[(c"MKL_Set_Num_Threads", c"MKL_Get_Max_Threads")])
}
}
fn blis_symbols() -> Option<(Symbols, *mut c_void)> {
#[cfg(feature = "blis_threading")]
{
Some((
Symbols {
set: linked::bli_thread_set_num_threads as *mut c_void,
get: linked::bli_thread_get_num_threads as *mut c_void,
},
linked::bli_info_get_int_type_size as *mut c_void,
))
}
#[cfg(not(feature = "blis_threading"))]
{
lookup_pair(&[(c"bli_thread_set_num_threads", c"bli_thread_get_num_threads")])
.map(|symbols| (symbols, lookup(c"bli_info_get_int_type_size")))
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod dl {
use std::ffi::{CStr, c_char, c_void};
unsafe extern "C" {
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
}
#[cfg(target_os = "linux")]
const RTLD_DEFAULT: *mut c_void = std::ptr::null_mut();
#[cfg(target_os = "macos")]
const RTLD_DEFAULT: *mut c_void = -2isize as *mut c_void;
pub(super) fn lookup(name: &CStr) -> *mut c_void {
unsafe { dlsym(RTLD_DEFAULT, name.as_ptr()) }
}
}
#[cfg(any(
feature = "openblas_threading",
feature = "mkl_threading",
feature = "blis_threading"
))]
mod linked {
#[cfg(any(feature = "openblas_threading", feature = "mkl_threading"))]
use std::ffi::c_int;
#[cfg(feature = "openblas_threading")]
unsafe extern "C" {
pub(super) fn openblas_set_num_threads(nthreads: c_int);
pub(super) fn openblas_get_num_threads() -> c_int;
}
#[cfg(feature = "mkl_threading")]
unsafe extern "C" {
pub(super) fn MKL_Set_Num_Threads(nthreads: c_int);
pub(super) fn MKL_Get_Max_Threads() -> c_int;
}
#[cfg(feature = "blis_threading")]
unsafe extern "C" {
pub(super) fn bli_thread_set_num_threads(nthreads: i64);
pub(super) fn bli_thread_get_num_threads() -> i64;
pub(super) fn bli_info_get_int_type_size() -> i64;
}
}
#[cfg(test)]
mod test {
use super::{BlasThreadingError, BlasThreads, BlisInt, reported_count, set_blas_threads};
#[test]
fn test_invalid_thread_counts() {
assert_eq!(set_blas_threads(0), Err(BlasThreadingError::ZeroThreads));
#[cfg(target_pointer_width = "64")]
{
let too_many = std::ffi::c_int::MAX as usize + 1;
assert_eq!(
set_blas_threads(too_many),
Err(BlasThreadingError::TooManyThreads(too_many))
);
}
}
#[test]
fn test_reported_count() {
assert_eq!(reported_count(4), BlasThreads::Count(4));
assert_eq!(reported_count(0), BlasThreads::Unknown);
assert_eq!(reported_count(-1), BlasThreads::Unknown);
}
#[test]
fn test_blis_int_width() {
assert_eq!(BlisInt::from_type_size(Some(32)), BlisInt::I32);
assert_eq!(BlisInt::from_type_size(Some(64)), BlisInt::I64);
assert_eq!(BlisInt::from_type_size(Some(0)), BlisInt::I64);
assert_eq!(BlisInt::from_type_size(None), BlisInt::I64);
}
#[cfg(target_os = "macos")]
#[test]
fn test_accelerate_threading() {
use super::{BlasBackend, get_blas_threads, lookup};
let accelerate = |settings: Vec<super::BlasThreadSetting>| {
settings
.into_iter()
.find(|setting| setting.backend == BlasBackend::Accelerate)
.map(|setting| setting.threads)
};
let initial = accelerate(get_blas_threads());
assert_eq!(initial.is_some(), !lookup(c"BLASSetThreading").is_null());
if initial.is_none() {
return;
}
assert_eq!(
accelerate(set_blas_threads(1).unwrap()),
Some(BlasThreads::SingleThreaded)
);
assert_eq!(
accelerate(get_blas_threads()),
Some(BlasThreads::SingleThreaded)
);
assert_eq!(
accelerate(set_blas_threads(4).unwrap()),
Some(BlasThreads::BackendManaged)
);
}
}