use cudarc::driver::{result, sys};
use super::super::device::device_count;
use super::super::error::GpuError;
use crate::engine::cuda_context::libcuda_present;
use crate::engine::gpu::GPU_LOG_TARGET as LOG_TARGET;
use crate::engine::partitioned::mpi::rsmpi::collective::CommunicatorCollectives;
use crate::engine::partitioned::mpi::rsmpi::topology::Communicator;
use crate::engine::partitioned::topology::{allowed_cpus, sysfs_numa_nodes};
const MAX_DEVICES: usize = 16;
const RECORD: usize = 2 + 2 * MAX_DEVICES;
pub fn local_device_for_comm(comm: &impl Communicator) -> Result<u32, GpuError> {
let node = comm.split_shared(comm.rank());
let local = node.rank() as usize;
let count = device_count();
let mine = own_report(count).encode();
let mut all = vec![0u64; RECORD * node.size() as usize];
node.all_gather_into(&mine[..], &mut all[..]);
if count == 0 {
return Err(GpuError::NoDevice);
}
let reports: Vec<Report> = all.chunks_exact(RECORD).map(Report::decode).collect();
let pick = pick_for(local, &reports);
let me = &reports[local];
let device_numa = me.numa.get(pick as usize).copied().flatten();
let numa_name =
|numa: Option<u32>| numa.map_or_else(|| "unknown".to_string(), |n| n.to_string());
log::info!(
target: LOG_TARGET,
"device pick: rank {} (node-local {local} of {}, CPU NUMA {}) drives device {pick} (NUMA {})",
comm.rank(),
node.size(),
mask_list(me.cpu_numa),
numa_name(device_numa),
);
if device_numa.is_some_and(|n| me.cpu_numa != 0 && !on_node(me.cpu_numa, n)) {
log::warn!(
target: LOG_TARGET,
"device pick: rank {} drives device {pick} on NUMA node {}, but its CPUs are on NUMA {}, so its host staging crosses sockets",
comm.rank(),
numa_name(device_numa),
mask_list(me.cpu_numa),
);
}
Ok(pick)
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Report {
cpu_numa: u64,
count: usize,
pci: Vec<Option<u64>>,
numa: Vec<Option<u32>>,
}
impl Report {
fn encode(&self) -> [u64; RECORD] {
let mut out = [0u64; RECORD];
out[0] = self.cpu_numa;
out[1] = self.count as u64;
for (i, (pci, numa)) in self.pci.iter().zip(&self.numa).enumerate() {
out[2 + i] = pci.map_or(0, |k| k + 1);
out[2 + MAX_DEVICES + i] = numa.map_or(0, |n| u64::from(n) + 1);
}
out
}
fn decode(record: &[u64]) -> Self {
let count = record[1] as usize;
let shown = count.min(MAX_DEVICES);
let decode = |value: u64| value.checked_sub(1);
Self {
cpu_numa: record[0],
count,
pci: record[2..2 + shown].iter().map(|&value| decode(value)).collect(),
numa: record[2 + MAX_DEVICES..2 + MAX_DEVICES + shown]
.iter()
.map(|&value| decode(value).and_then(|n| u32::try_from(n).ok()))
.collect(),
}
}
fn devices(&self) -> Option<&[Option<u64>]> {
(self.count <= MAX_DEVICES && self.pci.iter().all(Option::is_some)).then_some(&self.pci)
}
}
fn own_report(count: usize) -> Report {
let cpu_numa = sysfs_numa_nodes(&allowed_cpus())
.iter()
.try_fold(0u64, |mask, (node, _)| (*node < 64).then(|| mask | 1 << node))
.unwrap_or(0);
let shown = count.min(MAX_DEVICES);
let pci: Vec<Option<(u32, u32, u32)>> = (0..shown as u32).map(device_pci).collect();
Report {
cpu_numa,
count,
pci: pci
.iter()
.map(|p| p.map(|(d, b, s)| u64::from(d) << 16 | u64::from(b) << 8 | u64::from(s)))
.collect(),
numa: pci
.iter()
.map(|p| p.and_then(|(d, b, s)| pci_numa_node(&pci_sysfs_name(d, b, s))))
.collect(),
}
}
fn pick_for(local: usize, reports: &[Report]) -> u32 {
let me = &reports[local];
let shared = me.devices().is_some_and(|mine| reports.iter().all(|r| r.devices() == Some(mine)));
if shared {
let masks: Vec<u64> = reports.iter().map(|r| r.cpu_numa).collect();
assign_devices(&masks, &me.numa)[local]
} else {
(local % me.count) as u32
}
}
fn on_node(mask: u64, node: u32) -> bool {
node < 64 && mask >> node & 1 == 1
}
fn mask_list(mask: u64) -> String {
if mask == 0 {
return "unknown".to_string();
}
(0..64).filter(|&n| on_node(mask, n)).map(|n| n.to_string()).collect::<Vec<_>>().join(",")
}
fn assign_devices(ranks: &[u64], devices: &[Option<u32>]) -> Vec<u32> {
let num_devices = devices.len();
assert!(num_devices > 0, "assign_devices needs a device");
let slots = num_devices * ranks.len().div_ceil(num_devices).max(1);
let local = |rank: usize, slot: usize| {
devices[slot % num_devices].is_some_and(|n| on_node(ranks[rank], n))
};
let mut owner: Vec<Option<usize>> = vec![None; slots];
fn augment(
rank: usize,
seen: &mut [bool],
owner: &mut [Option<usize>],
local: &dyn Fn(usize, usize) -> bool,
) -> bool {
for slot in 0..owner.len() {
if seen[slot] || !local(rank, slot) {
continue;
}
seen[slot] = true;
if owner[slot].is_none_or(|other| augment(other, seen, owner, local)) {
owner[slot] = Some(rank);
return true;
}
}
false
}
for rank in 0..ranks.len() {
if let Some(slot) = (0..slots).find(|&slot| owner[slot].is_none() && local(rank, slot)) {
owner[slot] = Some(rank);
} else {
augment(rank, &mut vec![false; slots], &mut owner, &local);
}
}
let mut pick: Vec<Option<u32>> = vec![None; ranks.len()];
for (slot, rank) in owner.iter().enumerate() {
if let Some(rank) = *rank {
pick[rank] = Some((slot % num_devices) as u32);
}
}
let cap = slots / num_devices;
let mut load = vec![0usize; num_devices];
for &device in pick.iter().flatten() {
load[device as usize] += 1;
}
for unplaced in pick.iter_mut().filter(|p| p.is_none()) {
let device = (0..num_devices)
.filter(|&d| load[d] < cap)
.min_by_key(|&d| load[d])
.expect("the slots cover every rank");
load[device] += 1;
*unplaced = Some(device as u32);
}
pick.into_iter().map(|placed| placed.expect("every rank placed")).collect()
}
fn device_pci(ordinal: u32) -> Option<(u32, u32, u32)> {
use sys::CUdevice_attribute_enum as A;
if !libcuda_present() {
return None;
}
result::init().ok()?;
let dev = result::device::get(i32::try_from(ordinal).ok()?).ok()?;
let attr = |a| {
unsafe { result::device::get_attribute(dev, a) }.ok().and_then(|v| u32::try_from(v).ok())
};
Some((
attr(A::CU_DEVICE_ATTRIBUTE_PCI_DOMAIN_ID)?,
attr(A::CU_DEVICE_ATTRIBUTE_PCI_BUS_ID)?,
attr(A::CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID)?,
))
}
fn pci_sysfs_name(domain: u32, bus: u32, device: u32) -> String {
format!("{domain:04x}:{bus:02x}:{device:02x}.0")
}
fn pci_numa_node(name: &str) -> Option<u32> {
let text = std::fs::read_to_string(format!("/sys/bus/pci/devices/{name}/numa_node")).ok()?;
parse_numa_node(&text)
}
fn parse_numa_node(text: &str) -> Option<u32> {
text.trim().parse::<i64>().ok().and_then(|n| u32::try_from(n).ok())
}
#[cfg(test)]
mod tests;