feat(numa): introduce I/O sampling to prevent activation stalls
Replaces the monolithic CPU scaling threshold with separate CPU and I/O spawn thresholds. Introduces an `IoSample` struct with platform-specific byte reading and a relative throughput growth heuristic. Adds a 0.1s wall-clock guard to `CpuSample` to suppress artificial efficiency spikes, and updates `maybe_activate` to trigger worker scaling when either resource indicates headroom. Bumps `obikmer` to v1.1.33 and updates architecture documentation.
This commit is contained in:
+34
-17
@@ -20,7 +20,7 @@ use hwlocality::cpu::binding::CpuBindingFlags;
|
||||
use hwlocality::cpu::cpuset::CpuSet;
|
||||
#[cfg(feature = "numa")]
|
||||
use hwlocality::object::types::ObjectType;
|
||||
use obisys::CpuSample;
|
||||
use obisys::{CpuSample, IoSample};
|
||||
use tracing::debug;
|
||||
|
||||
// ── Public interface ──────────────────────────────────────────────────────────
|
||||
@@ -190,10 +190,13 @@ impl PartitionRunner {
|
||||
/// Run `f(i)` for every index in `order`.
|
||||
///
|
||||
/// Workers are pre-spawned dormant and activated adaptively. A timer thread
|
||||
/// fires a CPU-efficiency check every `TIMER_SECS` seconds; each completed
|
||||
/// fires an efficiency check every `TIMER_SECS` seconds; each completed
|
||||
/// partition resets that timer (forcing an immediate check) and also
|
||||
/// triggers its own inline check. A new worker is activated whenever
|
||||
/// efficiency falls below `SPAWN_THRESHOLD`.
|
||||
/// triggers its own inline check. A new worker is activated whenever CPU
|
||||
/// efficiency grows by at least `CPU_SPAWN_THRESHOLD` (absolute, in cores)
|
||||
/// or I/O throughput grows by at least `IO_SPAWN_THRESHOLD` (relative) since
|
||||
/// the last check — whichever resource is the actual bottleneck still shows
|
||||
/// headroom.
|
||||
///
|
||||
/// `on_done(i, result, elapsed)` is called from the controller thread as
|
||||
/// each partition completes — suitable for progress bars and result
|
||||
@@ -217,8 +220,9 @@ impl PartitionRunner {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const SPAWN_THRESHOLD: f64 = 0.2;
|
||||
const TIMER_SECS: u64 = 30;
|
||||
const CPU_SPAWN_THRESHOLD: f64 = 0.2;
|
||||
const IO_SPAWN_THRESHOLD: f64 = 0.2;
|
||||
const TIMER_SECS: u64 = 30;
|
||||
|
||||
// ── Channels ──────────────────────────────────────────────────────────
|
||||
let (part_tx, part_rx) = unbounded::<usize>();
|
||||
@@ -285,8 +289,9 @@ impl PartitionRunner {
|
||||
// ── Controller ────────────────────────────────────────────────────
|
||||
let initial_workers = n_nodes.min(max_workers).min(n_total);
|
||||
for _ in 0..initial_workers { activate_tx.send(()).ok(); }
|
||||
let mut n_active = initial_workers;
|
||||
let mut n_active = initial_workers;
|
||||
let mut cpu_sample = CpuSample::now();
|
||||
let mut io_sample = IoSample::now();
|
||||
let mut completed = 0usize;
|
||||
|
||||
while completed < n_total {
|
||||
@@ -303,13 +308,17 @@ impl PartitionRunner {
|
||||
// Inline check: same logic as a timer tick.
|
||||
maybe_activate(
|
||||
&activate_tx, &mut n_active, max_workers,
|
||||
&mut cpu_sample, SPAWN_THRESHOLD, completed, n_total,
|
||||
&mut cpu_sample, CPU_SPAWN_THRESHOLD,
|
||||
&mut io_sample, IO_SPAWN_THRESHOLD,
|
||||
completed, n_total,
|
||||
);
|
||||
}
|
||||
WorkerEvent::TimerTick => {
|
||||
maybe_activate(
|
||||
&activate_tx, &mut n_active, max_workers,
|
||||
&mut cpu_sample, SPAWN_THRESHOLD, completed, n_total,
|
||||
&mut cpu_sample, CPU_SPAWN_THRESHOLD,
|
||||
&mut io_sample, IO_SPAWN_THRESHOLD,
|
||||
completed, n_total,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -336,17 +345,25 @@ enum WorkerEvent<R, E> {
|
||||
}
|
||||
|
||||
fn maybe_activate(
|
||||
activate_tx: &crossbeam_channel::Sender<()>,
|
||||
n_active: &mut usize,
|
||||
max_workers: usize,
|
||||
cpu_sample: &mut CpuSample,
|
||||
threshold: f64,
|
||||
completed: usize,
|
||||
n_total: usize,
|
||||
activate_tx: &crossbeam_channel::Sender<()>,
|
||||
n_active: &mut usize,
|
||||
max_workers: usize,
|
||||
cpu_sample: &mut CpuSample,
|
||||
cpu_threshold: f64,
|
||||
io_sample: &mut IoSample,
|
||||
io_threshold: f64,
|
||||
completed: usize,
|
||||
n_total: usize,
|
||||
) {
|
||||
if *n_active >= max_workers || completed >= n_total { return; }
|
||||
|
||||
if cpu_sample.do_i_activate(threshold) {
|
||||
// Call both unconditionally (no `||` short-circuit): each sampler must
|
||||
// advance its own window every tick, regardless of what the other one
|
||||
// reports, or it would starve behind whichever signal fires first.
|
||||
let cpu_wants_more = cpu_sample.do_i_activate(cpu_threshold);
|
||||
let io_wants_more = io_sample.do_i_activate(io_threshold);
|
||||
|
||||
if cpu_wants_more || io_wants_more {
|
||||
activate_tx.send(()).ok();
|
||||
*n_active += 1;
|
||||
debug!("activated worker {}/{}", n_active, max_workers);
|
||||
|
||||
Reference in New Issue
Block a user