use std::fmt;
use std::io;
use super::PLACEMENT_LOG_TARGET as LOG_TARGET;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CpuSet(
pub Vec<usize>,
);
impl CpuSet {
pub fn parse(s: &str) -> Result<Self, TopologyError> {
let invalid = || TopologyError::InvalidCpuList(s.trim().to_string());
let index = |token: &str| token.trim().parse::<usize>().map_err(|_| invalid());
let mut cpus = Vec::new();
for token in s.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
match token.split_once('-') {
None => cpus.push(index(token)?),
Some((lo, hi)) => {
let (lo, hi) = (index(lo)?, index(hi)?);
if hi < lo {
return Err(invalid());
}
cpus.extend(lo..=hi);
}
}
}
if cpus.is_empty() {
return Err(TopologyError::EmptySet);
}
cpus.sort_unstable();
cpus.dedup();
Ok(Self(cpus))
}
#[must_use]
pub fn len(&self) -> usize {
self.normalized().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn intersect(&self, other: &Self) -> Self {
let other = other.normalized();
Self(self.normalized().into_iter().filter(|cpu| other.binary_search(cpu).is_ok()).collect())
}
fn is_subset_of(&self, other: &Self) -> bool {
let other = other.normalized();
self.0.iter().all(|cpu| other.binary_search(cpu).is_ok())
}
fn normalized(&self) -> Vec<usize> {
let mut cpus = self.0.clone();
cpus.sort_unstable();
cpus.dedup();
cpus
}
fn union(&self, other: &Self) -> Self {
let mut cpus = self.normalized();
cpus.extend(other.normalized());
cpus.sort_unstable();
cpus.dedup();
Self(cpus)
}
}
impl fmt::Display for CpuSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let cpus = self.normalized();
let mut first = true;
let mut i = 0;
while i < cpus.len() {
let mut j = i;
while j + 1 < cpus.len() && cpus[j + 1] == cpus[j] + 1 {
j += 1;
}
if !first {
write!(f, ",")?;
}
first = false;
if i == j {
write!(f, "{}", cpus[i])?;
} else {
write!(f, "{}-{}", cpus[i], cpus[j])?;
}
i = j + 1;
}
Ok(())
}
}
#[must_use]
pub(crate) fn allowed_cpus() -> CpuSet {
#[cfg(target_os = "linux")]
{
unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut set) == 0 {
let cpus: Vec<usize> = (0..libc::CPU_SETSIZE as usize)
.filter(|&cpu| libc::CPU_ISSET(cpu, &set))
.collect();
if !cpus.is_empty() {
return CpuSet(cpus);
}
}
}
}
CpuSet((0..available_parallelism()).collect())
}
#[must_use]
pub fn numa_nodes() -> Vec<(usize, CpuSet)> {
let allowed = allowed_cpus();
let mut nodes = sysfs_numa_nodes(&allowed);
nodes.sort_unstable_by_key(|(id, _)| *id);
if nodes.is_empty() {
vec![(0, allowed)]
} else {
nodes
}
}
#[cfg(target_os = "linux")]
pub(crate) fn sysfs_numa_nodes(allowed: &CpuSet) -> Vec<(usize, CpuSet)> {
const ROOT: &str = "/sys/devices/system/node";
let Ok(entries) = std::fs::read_dir(ROOT) else {
return Vec::new();
};
let mut nodes = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(id) = name.to_str().and_then(|n| n.strip_prefix("node")) else {
continue;
};
let Ok(id) = id.parse::<usize>() else {
continue;
};
let Ok(list) = std::fs::read_to_string(entry.path().join("cpulist")) else {
continue;
};
let Ok(cpus) = CpuSet::parse(&list) else {
continue;
};
let cpus = cpus.intersect(allowed);
if !cpus.is_empty() {
nodes.push((id, cpus));
}
}
nodes
}
#[cfg(not(target_os = "linux"))]
pub(crate) fn sysfs_numa_nodes(_allowed: &CpuSet) -> Vec<(usize, CpuSet)> {
Vec::new()
}
pub(super) fn pin_current_thread(set: &CpuSet) -> io::Result<()> {
#[cfg(target_os = "linux")]
{
if set.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cannot pin a thread to an empty CPU set",
));
}
unsafe {
let mut mask: libc::cpu_set_t = std::mem::zeroed();
for &cpu in &set.0 {
if cpu >= libc::CPU_SETSIZE as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("CPU {cpu} is beyond CPU_SETSIZE"),
));
}
libc::CPU_SET(cpu, &mut mask);
}
if libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mask) != 0 {
return Err(io::Error::last_os_error());
}
}
}
#[cfg(not(target_os = "linux"))]
let _ = set;
Ok(())
}
pub(super) fn bind_current_thread_memory(node: Option<usize>) -> io::Result<()> {
#[cfg(target_os = "linux")]
{
const BITS: usize = 8 * std::mem::size_of::<libc::c_ulong>();
let rc = match node {
Some(node) => {
let words = node / BITS + 1;
let mut mask = vec![0 as libc::c_ulong; words];
mask[node / BITS] = 1 << (node % BITS);
unsafe {
libc::syscall(
libc::SYS_set_mempolicy,
libc::MPOL_BIND,
mask.as_ptr(),
(words * BITS) as libc::c_ulong,
)
}
}
None => unsafe {
libc::syscall(
libc::SYS_set_mempolicy,
libc::MPOL_DEFAULT,
std::ptr::null::<libc::c_ulong>(),
0 as libc::c_ulong,
)
},
};
if rc != 0 {
return Err(io::Error::last_os_error());
}
}
#[cfg(not(target_os = "linux"))]
let _ = node;
Ok(())
}
#[derive(Clone, Debug)]
pub(crate) struct PartitionSlot {
pub(crate) cpus: Option<CpuSet>,
pub(crate) node: Option<usize>,
pub(crate) threads: usize,
pub(crate) device: Option<u32>,
}
pub(super) fn build_pool(
slot: &PartitionSlot,
bind_memory: bool,
name: &str,
) -> Result<rayon::ThreadPool, TopologyError> {
let cpus = slot.cpus.clone();
let node = slot.node;
#[cfg(feature = "cuda")]
let device = slot.device;
let pool_name = name.to_string();
rayon::ThreadPoolBuilder::new()
.num_threads(slot.threads)
.thread_name(move |index| format!("{pool_name}-{index}"))
.spawn_handler(move |thread| {
let cpus = cpus.clone();
let mut builder = std::thread::Builder::new();
if let Some(name) = thread.name() {
builder = builder.name(name.to_string());
}
if let Some(size) = thread.stack_size() {
builder = builder.stack_size(size);
}
builder.spawn(move || {
#[cfg(feature = "cuda")]
if let Some(device) = device {
crate::engine::cuda_context::bind_device_context(device);
}
if let Some(cpus) = &cpus {
if let Err(err) = pin_current_thread(cpus) {
log::warn!(target: LOG_TARGET, "failed to pin worker to {cpus}: {err}");
}
}
if bind_memory {
if let Err(err) = bind_current_thread_memory(node) {
log::warn!(target: LOG_TARGET, "failed to bind worker memory to {node:?}: {err}");
}
}
thread.run();
})?;
Ok(())
})
.build()
.map_err(|err| TopologyError::Io(io::Error::other(err.to_string())))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Placement {
Auto {
max_partitions: Option<usize>,
},
Explicit(
Vec<CpuSet>,
),
Unpinned {
partitions: usize,
},
#[cfg(feature = "cuda")]
Devices {
devices: Vec<u32>,
per_device: usize,
},
}
#[cfg(feature = "cuda")]
const DEVICE_PARTITION_THREADS: usize = 4;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PartitionConfig {
pub placement: Placement,
pub bind_memory: bool,
pub threads_per_partition: Option<usize>,
}
impl Default for PartitionConfig {
fn default() -> Self {
Self {
placement: Placement::Auto { max_partitions: None },
bind_memory: true,
threads_per_partition: None,
}
}
}
impl PartitionConfig {
pub(crate) fn resolve(&self) -> Result<Vec<PartitionSlot>, TopologyError> {
let mut slots = match &self.placement {
Placement::Auto { max_partitions } => resolve_auto(*max_partitions),
Placement::Explicit(sets) => resolve_explicit(sets)?,
Placement::Unpinned { partitions } => resolve_unpinned(*partitions)?,
#[cfg(feature = "cuda")]
Placement::Devices { devices, per_device } => resolve_devices(devices, *per_device)?,
};
if let Some(threads) = self.threads_per_partition {
for slot in &mut slots {
slot.threads = threads.max(1);
}
}
Ok(slots)
}
pub fn num_partitions(&self) -> Result<usize, TopologyError> {
Ok(self.resolve()?.len())
}
}
#[cfg(feature = "cuda")]
fn resolve_devices(
devices: &[u32],
per_device: usize,
) -> Result<Vec<PartitionSlot>, TopologyError> {
let count = devices.len() * per_device;
if count == 0 || !count.is_power_of_two() {
return Err(TopologyError::NotPowerOfTwo(count));
}
Ok(devices
.iter()
.flat_map(|&d| {
std::iter::repeat_n(
PartitionSlot {
cpus: None,
node: None,
threads: DEVICE_PARTITION_THREADS,
device: Some(d),
},
per_device,
)
})
.collect())
}
fn resolve_auto(max_partitions: Option<usize>) -> Vec<PartitionSlot> {
let nodes = numa_nodes();
let mut count = prev_power_of_two(nodes.len());
if let Some(max) = max_partitions {
count = prev_power_of_two(count.min(max));
}
let (base, remainder) = (nodes.len() / count, nodes.len() % count);
let mut slots = Vec::with_capacity(count);
let mut rest = nodes.as_slice();
for partition in 0..count {
let take = base + usize::from(partition < remainder);
let (group, tail) = rest.split_at(take);
rest = tail;
let cpus =
group.iter().fold(CpuSet(Vec::new()), |accumulator, (_, set)| accumulator.union(set));
slots.push(PartitionSlot {
node: (group.len() == 1).then(|| group[0].0),
threads: cpus.len(),
cpus: Some(cpus),
device: None,
});
}
slots
}
fn resolve_explicit(sets: &[CpuSet]) -> Result<Vec<PartitionSlot>, TopologyError> {
if !sets.len().is_power_of_two() {
return Err(TopologyError::NotPowerOfTwo(sets.len()));
}
let allowed = allowed_cpus();
let nodes = numa_nodes();
let mut slots = Vec::with_capacity(sets.len());
for set in sets {
if set.is_empty() {
return Err(TopologyError::EmptySet);
}
let cpus = CpuSet(set.normalized());
if let Some(&cpu) = cpus.0.iter().find(|cpu| !allowed.0.contains(cpu)) {
return Err(TopologyError::CpuNotAllowed(cpu));
}
let mut containing =
nodes.iter().filter(|(_, node)| cpus.is_subset_of(node)).map(|(id, _)| *id);
let node = match (containing.next(), containing.next()) {
(Some(id), None) => Some(id),
_ => None,
};
slots.push(PartitionSlot { node, threads: cpus.len(), cpus: Some(cpus), device: None });
}
for (i, slot) in slots.iter().enumerate() {
for other in slots.iter().skip(i + 1) {
let (a, b) = (slot.cpus.as_ref(), other.cpus.as_ref());
if let (Some(a), Some(b)) = (a, b) {
let shared = a.intersect(b);
if !shared.is_empty() {
log::warn!(
target: LOG_TARGET,
"explicit partitions {a} and {b} overlap on {shared}; \
their pools will contend for those CPUs",
);
}
}
}
}
Ok(slots)
}
fn resolve_unpinned(partitions: usize) -> Result<Vec<PartitionSlot>, TopologyError> {
if !partitions.is_power_of_two() {
return Err(TopologyError::NotPowerOfTwo(partitions));
}
let threads = (available_parallelism() / partitions).max(1);
Ok(vec![PartitionSlot { cpus: None, node: None, threads, device: None }; partitions])
}
fn prev_power_of_two(n: usize) -> usize {
1 << n.max(1).ilog2()
}
fn available_parallelism() -> usize {
std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
}
#[derive(Debug)]
pub enum TopologyError {
InvalidCpuList(
String,
),
NotPowerOfTwo(
usize,
),
EmptySet,
Io(
io::Error,
),
CpuNotAllowed(
usize,
),
}
impl fmt::Display for TopologyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCpuList(list) => {
write!(f, "{list:?} is not a cpulist of the form \"0-7,16-23\"")
}
Self::NotPowerOfTwo(count) => write!(
f,
"partition count {count} is not a power of two; a partition index is a fixed \
set of GF(2) hash rows",
),
Self::EmptySet => write!(f, "a partition needs a non-empty CPU set"),
Self::Io(err) => write!(f, "topology I/O failed: {err}"),
Self::CpuNotAllowed(cpu) => {
write!(f, "CPU {cpu} is outside this process's affinity mask",)
}
}
}
}
impl std::error::Error for TopologyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for TopologyError {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
#[cfg(test)]
mod tests;