0002e90451
Cap the cores-per-row group size (and the dashboard 'g' cycle) by total core count so the toggleable-row count stays sensible and small machines get finer control: ≤4 cores toggle individually (size 1), 5-8 cores in groups of up to 2, and more than 8 in groups of up to 4. The cap is also the default — the prior fixed default of 4 now clamps to the tier (1/2/4), and an explicit --cpu-group-size is clamped to the cap too. Add max_group_size() in cpu_groups; update the help text and the cpu_groups / control tests (the cycle test now uses 16 cores so it can exercise sizes 4/2/1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
441 lines
16 KiB
Rust
441 lines
16 KiB
Rust
//! CPU mining split into groups of cores that the dashboard can toggle on/off.
|
|
//!
|
|
//! The logical CPUs are partitioned into fixed-size groups (see
|
|
//! [`CpuGroups::new`]). Each group is mined by its own worker thread running on a
|
|
//! dedicated rayon pool sized to the group's core count, so enabling a group adds
|
|
//! roughly that many mining threads and disabling it stops them. Groups draw from
|
|
//! the same shared nonce counter and submit through the same pool client as the
|
|
//! GPU workers, so CPU mining runs alongside whatever backend is active.
|
|
//!
|
|
//! State here is pure atomics shared between the worker threads (which read
|
|
//! `enabled` and bump the counters) and the TUI (which toggles `enabled` and
|
|
//! reads the counters for the per-group Sol/s rows).
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
/// One toggleable group of specific CPU cores. The group's worker pins its
|
|
/// mining threads to exactly these logical cores (see `miner::cpu_group_worker`).
|
|
pub struct CpuGroup {
|
|
cores: Vec<usize>,
|
|
enabled: AtomicBool,
|
|
solutions: AtomicU64,
|
|
shares: AtomicU64,
|
|
}
|
|
|
|
impl CpuGroup {
|
|
/// The logical cores this group pins its threads to.
|
|
pub fn cores(&self) -> &[usize] {
|
|
&self.cores
|
|
}
|
|
|
|
/// Number of cores (and therefore mining threads) this group runs.
|
|
pub fn ncores(&self) -> usize {
|
|
self.cores.len()
|
|
}
|
|
|
|
/// Human-readable core list, e.g. "CPU cores 0-3", "CPU core 7", or
|
|
/// "CPU cores 0,2,4,6" for a non-contiguous selection.
|
|
pub fn label(&self) -> String {
|
|
format!("CPU {}", fmt_cores(&self.cores))
|
|
}
|
|
|
|
pub fn enabled(&self) -> bool {
|
|
self.enabled.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Flip the group between mining and idle (called from the TUI key handler).
|
|
pub fn toggle(&self) {
|
|
self.enabled.fetch_xor(true, Ordering::Relaxed);
|
|
}
|
|
|
|
/// Set the enabled state directly (used when rebuilding groups for a new
|
|
/// group size, to restore the previously-enabled cores).
|
|
pub fn set_enabled(&self, on: bool) {
|
|
self.enabled.store(on, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn solutions(&self) -> u64 {
|
|
self.solutions.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn shares(&self) -> u64 {
|
|
self.shares.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Counters the group's worker bumps as it mines (see `miner::submit_solutions`).
|
|
pub(crate) fn solutions_atomic(&self) -> &AtomicU64 {
|
|
&self.solutions
|
|
}
|
|
pub(crate) fn shares_atomic(&self) -> &AtomicU64 {
|
|
&self.shares
|
|
}
|
|
}
|
|
|
|
/// All CPU core groups, shared between the worker threads and the dashboard.
|
|
pub struct CpuGroups {
|
|
groups: Vec<CpuGroup>,
|
|
}
|
|
|
|
impl CpuGroups {
|
|
/// Group the selected `cores` into mining groups of up to `cores_per_group`,
|
|
/// **aligned to core-index blocks**: a core belongs to block
|
|
/// `core / cores_per_group`, and each non-empty block becomes one group. So a
|
|
/// group never straddles a `cores_per_group`-aligned boundary (keeping its
|
|
/// threads within one cache/NUMA-friendly range), and a partial selection
|
|
/// splits at the boundary rather than being sliced into raw chunks — e.g.
|
|
/// cores 2..=9 with size 4 give `[2,3] | [4,5,6,7] | [8,9]`, not
|
|
/// `[2,3,4,5] | [6,7,8,9]`. `cores` is the set to mine on (`--cpu-cores`,
|
|
/// default all); each group starts enabled iff `start_enabled`
|
|
/// (`--cpu-mining`). Groups (rows) are ordered by ascending core index.
|
|
pub fn new(cores: Vec<usize>, cores_per_group: usize, start_enabled: bool) -> Arc<Self> {
|
|
let cores_per_group = cores_per_group.max(1);
|
|
let mut blocks: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
|
for c in cores {
|
|
blocks.entry(c / cores_per_group).or_default().push(c);
|
|
}
|
|
let groups = blocks
|
|
.into_values()
|
|
.map(|mut cores| {
|
|
cores.sort_unstable();
|
|
CpuGroup {
|
|
cores,
|
|
enabled: AtomicBool::new(start_enabled),
|
|
solutions: AtomicU64::new(0),
|
|
shares: AtomicU64::new(0),
|
|
}
|
|
})
|
|
.collect();
|
|
Arc::new(Self { groups })
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.groups.len()
|
|
}
|
|
|
|
#[allow(dead_code)] // paired with len() for clippy::len_without_is_empty
|
|
pub fn is_empty(&self) -> bool {
|
|
self.groups.is_empty()
|
|
}
|
|
|
|
pub fn group(&self, i: usize) -> &CpuGroup {
|
|
&self.groups[i]
|
|
}
|
|
|
|
pub fn iter(&self) -> impl Iterator<Item = &CpuGroup> {
|
|
self.groups.iter()
|
|
}
|
|
}
|
|
|
|
/// Format a sorted core list compactly, collapsing contiguous runs into ranges:
|
|
/// `[5]` -> "core 5", `[0,1,2,3]` -> "cores 0-3", `[0,2,4]` -> "cores 0,2,4",
|
|
/// `[0,1,2,8,9]` -> "cores 0-2,8-9".
|
|
fn fmt_cores(cores: &[usize]) -> String {
|
|
if cores.is_empty() {
|
|
return "cores (none)".to_string();
|
|
}
|
|
let mut parts: Vec<String> = Vec::new();
|
|
let mut i = 0;
|
|
while i < cores.len() {
|
|
let start = cores[i];
|
|
let mut j = i;
|
|
while j + 1 < cores.len() && cores[j + 1] == cores[j] + 1 {
|
|
j += 1;
|
|
}
|
|
parts.push(if j == i {
|
|
format!("{start}")
|
|
} else {
|
|
format!("{start}-{}", cores[j])
|
|
});
|
|
i = j + 1;
|
|
}
|
|
let noun = if cores.len() == 1 { "core" } else { "cores" };
|
|
format!("{noun} {}", parts.join(","))
|
|
}
|
|
|
|
/// Build a [`CpuGroups`] for `cores`/`size`, enabling each group whose cores are
|
|
/// all in `enabled` (so the previously-enabled cores survive a regroup).
|
|
fn grouped(cores: &[usize], size: usize, enabled: &BTreeSet<usize>) -> Arc<CpuGroups> {
|
|
let groups = CpuGroups::new(cores.to_vec(), size, false);
|
|
for g in groups.iter() {
|
|
let on = !g.cores().is_empty() && g.cores().iter().all(|c| enabled.contains(c));
|
|
g.set_enabled(on);
|
|
}
|
|
groups
|
|
}
|
|
|
|
/// Largest cores-per-row the dashboard allows, by total core count. Finer
|
|
/// control on small machines, coarser on big ones so the toggleable-row count
|
|
/// stays manageable: **≤4 cores** toggle individually (size 1), **5-8 cores** in
|
|
/// groups of up to 2, and **more than 8** in groups of up to 4. This caps both
|
|
/// the starting group size and the 'g' cycle.
|
|
fn max_group_size(cores: usize) -> usize {
|
|
if cores <= 4 {
|
|
1
|
|
} else if cores <= 8 {
|
|
2
|
|
} else {
|
|
4
|
|
}
|
|
}
|
|
|
|
/// Live controller for CPU mining. Owns the selected cores plus the current
|
|
/// group size, and rebuilds the [`CpuGroups`] when the size is cycled from the
|
|
/// dashboard (the worker supervisor watches `group_size()` and respawns to
|
|
/// match). Which *cores* are enabled is tracked here so it survives a resize:
|
|
/// e.g. enabling cores 0-3 at size 4 keeps them on after switching to size 2.
|
|
pub struct CpuMining {
|
|
cores: Vec<usize>,
|
|
/// Group sizes the dashboard cycles through (sorted, includes the CLI value).
|
|
sizes: Vec<usize>,
|
|
size_idx: AtomicUsize,
|
|
/// The set of cores currently enabled for mining (drives each group's state).
|
|
enabled: Mutex<BTreeSet<usize>>,
|
|
/// The current grouping, swapped on resize; read by the TUI and supervisor.
|
|
groups: Mutex<Arc<CpuGroups>>,
|
|
/// Bumps on every regroup so the TUI can resync its per-group state.
|
|
generation: AtomicU64,
|
|
}
|
|
|
|
impl CpuMining {
|
|
/// `cores` are the logical cores to mine on (`--cpu-cores`), `initial_size`
|
|
/// the starting group size (`--cpu-group-size`), and `start_enabled` whether
|
|
/// mining begins on (`--cpu-mining`).
|
|
pub fn new(cores: Vec<usize>, initial_size: usize, start_enabled: bool) -> Arc<Self> {
|
|
// Toggle granularity scales with core count so the row count stays
|
|
// manageable (see [`max_group_size`]): the cap bounds both the starting
|
|
// size and the dashboard 'g' cycle.
|
|
let cap = max_group_size(cores.len());
|
|
let initial_size = initial_size.clamp(1, cap);
|
|
let enabled: BTreeSet<usize> = if start_enabled { cores.iter().copied().collect() } else { BTreeSet::new() };
|
|
let groups = grouped(&cores, initial_size, &enabled);
|
|
|
|
// Cycle list: powers of two from 1 up to the cap, plus the (clamped)
|
|
// requested size, sorted and de-duplicated.
|
|
let mut sizes: Vec<usize> = [1usize, 2, 4, 8]
|
|
.into_iter()
|
|
.chain([initial_size])
|
|
.filter(|&s| s >= 1 && s <= cap)
|
|
.collect();
|
|
sizes.sort_unstable();
|
|
sizes.dedup();
|
|
let size_idx = sizes.iter().position(|&s| s == initial_size).unwrap_or(0);
|
|
|
|
Arc::new(Self {
|
|
cores,
|
|
sizes,
|
|
size_idx: AtomicUsize::new(size_idx),
|
|
enabled: Mutex::new(enabled),
|
|
groups: Mutex::new(groups),
|
|
generation: AtomicU64::new(0),
|
|
})
|
|
}
|
|
|
|
/// The current cores-per-group.
|
|
pub fn group_size(&self) -> usize {
|
|
self.sizes[self.size_idx.load(Ordering::Relaxed).min(self.sizes.len() - 1)]
|
|
}
|
|
|
|
/// Advance to the next group size in the cycle (dashboard 'g' key). The
|
|
/// supervisor notices the change and regroups.
|
|
pub fn cycle_group_size(&self) {
|
|
let n = self.sizes.len();
|
|
if n > 1 {
|
|
let next = (self.size_idx.load(Ordering::Relaxed) + 1) % n;
|
|
self.size_idx.store(next, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
/// Set the group size to the nearest available value (control server). The
|
|
/// supervisor notices the change and regroups.
|
|
pub fn set_group_size(&self, n: usize) {
|
|
if let Some((idx, _)) = self
|
|
.sizes
|
|
.iter()
|
|
.enumerate()
|
|
.min_by_key(|(_, &s)| (s as i64 - n as i64).unsigned_abs())
|
|
{
|
|
self.size_idx.store(idx, Ordering::Relaxed);
|
|
}
|
|
}
|
|
|
|
/// A handle to the current grouping (cheap Arc clone).
|
|
pub fn groups(&self) -> Arc<CpuGroups> {
|
|
self.groups.lock().unwrap().clone()
|
|
}
|
|
|
|
/// Regroup counter; the TUI resets its per-group stats when this changes.
|
|
pub fn generation(&self) -> u64 {
|
|
self.generation.load(Ordering::Relaxed)
|
|
}
|
|
|
|
/// Toggle mining on the current grouping's `gi`-th group, updating the
|
|
/// persistent set of enabled cores so the choice survives a later resize.
|
|
pub fn toggle_group(&self, gi: usize) {
|
|
let groups = self.groups();
|
|
if gi >= groups.len() {
|
|
return;
|
|
}
|
|
let g = groups.group(gi);
|
|
g.toggle();
|
|
let on = g.enabled();
|
|
let mut set = self.enabled.lock().unwrap();
|
|
for &c in g.cores() {
|
|
if on {
|
|
set.insert(c);
|
|
} else {
|
|
set.remove(&c);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rebuild the grouping for the current size, preserving the enabled cores,
|
|
/// publish it, and bump the generation. Returns the new grouping.
|
|
pub fn rebuild(&self) -> Arc<CpuGroups> {
|
|
let size = self.group_size();
|
|
let set = self.enabled.lock().unwrap().clone();
|
|
let groups = grouped(&self.cores, size, &set);
|
|
*self.groups.lock().unwrap() = groups.clone();
|
|
self.generation.fetch_add(1, Ordering::Relaxed);
|
|
groups
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn partitions_cores_into_groups() {
|
|
// Even split.
|
|
let g = CpuGroups::new((0..24).collect(), 4, false);
|
|
assert_eq!(g.len(), 6);
|
|
assert!(g.iter().all(|x| x.ncores() == 4));
|
|
|
|
// Remainder lands in a smaller final group.
|
|
let g = CpuGroups::new((0..10).collect(), 4, false);
|
|
assert_eq!(g.len(), 3);
|
|
assert_eq!(g.group(0).ncores(), 4);
|
|
assert_eq!(g.group(2).ncores(), 2);
|
|
assert_eq!(g.group(2).cores(), &[8, 9]);
|
|
assert_eq!(g.group(2).label(), "CPU cores 8-9");
|
|
|
|
// Degenerate inputs don't panic or divide by zero.
|
|
assert!(CpuGroups::new(vec![], 4, false).is_empty());
|
|
assert_eq!(CpuGroups::new((0..3).collect(), 0, false).len(), 3); // per_group clamped to 1
|
|
}
|
|
|
|
#[test]
|
|
fn groups_use_the_selected_cores() {
|
|
// Cores bucket by aligned 4-core block (core/4 -> 0:cores0-3, 1:cores4-7,
|
|
// 2:cores8-11), so [1,3,5,7,8] becomes [1,3] | [5,7] | [8].
|
|
let g = CpuGroups::new(vec![1, 3, 5, 7, 8], 4, true);
|
|
assert_eq!(g.len(), 3);
|
|
assert_eq!(g.group(0).cores(), &[1, 3]);
|
|
assert_eq!(g.group(0).label(), "CPU cores 1,3");
|
|
assert_eq!(g.group(1).cores(), &[5, 7]);
|
|
assert_eq!(g.group(2).cores(), &[8]);
|
|
assert_eq!(g.group(2).label(), "CPU core 8");
|
|
assert!(g.iter().all(|x| x.enabled())); // start_enabled = true
|
|
}
|
|
|
|
#[test]
|
|
fn groups_align_to_blocks() {
|
|
// A selection crossing 4-core block boundaries splits at the boundary,
|
|
// not into raw chunks of 4: cores 2..=9 -> [2,3] | [4,5,6,7] | [8,9].
|
|
let g = CpuGroups::new((2..10).collect(), 4, false);
|
|
assert_eq!(g.len(), 3);
|
|
assert_eq!(g.group(0).cores(), &[2, 3]);
|
|
assert_eq!(g.group(1).cores(), &[4, 5, 6, 7]);
|
|
assert_eq!(g.group(2).cores(), &[8, 9]);
|
|
|
|
// Group size 1 is one row per core regardless of selection.
|
|
assert_eq!(CpuGroups::new((0..4).collect(), 1, false).len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn start_enabled_controls_initial_state() {
|
|
// Default (--cpu-mining absent): all groups start disabled.
|
|
assert!(CpuGroups::new((0..8).collect(), 4, false).iter().all(|g| !g.enabled()));
|
|
// --cpu-mining: all groups start enabled.
|
|
assert!(CpuGroups::new((0..8).collect(), 4, true).iter().all(|g| g.enabled()));
|
|
}
|
|
|
|
#[test]
|
|
fn group_size_tier_by_core_count() {
|
|
// ≤4 cores: individual cores only — size 1, no larger option.
|
|
let m = CpuMining::new((0..4).collect(), 4, false); // requested 4 clamps to 1
|
|
assert_eq!(m.group_size(), 1);
|
|
assert_eq!(m.groups().len(), 4);
|
|
m.cycle_group_size();
|
|
assert_eq!(m.group_size(), 1); // only one size, cycle is a no-op
|
|
|
|
// 5-8 cores: groups of up to 2 (requested 4 clamps to 2).
|
|
let m = CpuMining::new((0..8).collect(), 4, false);
|
|
assert_eq!(m.group_size(), 2);
|
|
assert_eq!(m.groups().len(), 4);
|
|
m.cycle_group_size();
|
|
assert_eq!(m.group_size(), 1); // cycle covers {1, 2}
|
|
|
|
// >8 cores: groups of up to 4 (the default).
|
|
let m = CpuMining::new((0..16).collect(), 4, false);
|
|
assert_eq!(m.group_size(), 4);
|
|
assert_eq!(m.groups().len(), 4);
|
|
// A larger explicit request is still capped at the tier max.
|
|
let m = CpuMining::new((0..16).collect(), 8, false);
|
|
assert_eq!(m.group_size(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn cpu_mining_cycles_size_and_preserves_enabled_cores() {
|
|
// 16 cores (tier cap 4): start at size 4 fully enabled -> four groups,
|
|
// all on.
|
|
let m = CpuMining::new((0..16).collect(), 4, true);
|
|
assert_eq!(m.group_size(), 4);
|
|
assert_eq!(m.groups().len(), 4);
|
|
assert!(m.groups().iter().all(|g| g.enabled()));
|
|
|
|
// Disable the last group (cores 12-15): now only cores 0-11 are enabled.
|
|
m.toggle_group(3);
|
|
assert!(!m.groups().group(3).enabled());
|
|
|
|
// Cycle to size 1 (individual cores) and rebuild: 16 rows; cores 0-11 on,
|
|
// 12-15 off.
|
|
while m.group_size() != 1 {
|
|
m.cycle_group_size();
|
|
}
|
|
let g = m.rebuild();
|
|
assert_eq!(g.len(), 16);
|
|
assert!(g.group(0).enabled()); // core 0
|
|
assert!(g.group(11).enabled()); // core 11
|
|
assert!(!g.group(12).enabled()); // core 12
|
|
assert!(!g.group(15).enabled()); // core 15
|
|
|
|
// Cycle to size 2 and rebuild: cores 0-11 are still tracked as enabled, so
|
|
// [0,1]..[10,11] come back on while [12,13],[14,15] stay off — the choice
|
|
// survived two regroups.
|
|
while m.group_size() != 2 {
|
|
m.cycle_group_size();
|
|
}
|
|
let g = m.rebuild();
|
|
assert_eq!(g.len(), 8);
|
|
assert!(g.group(0).enabled()); // [0,1]
|
|
assert!(g.group(5).enabled()); // [10,11]
|
|
assert!(!g.group(6).enabled()); // [12,13]
|
|
assert!(!g.group(7).enabled()); // [14,15]
|
|
assert!(m.generation() >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_flips_enabled() {
|
|
let g = CpuGroups::new((0..8).collect(), 4, false);
|
|
assert!(!g.group(0).enabled());
|
|
g.group(0).toggle();
|
|
assert!(g.group(0).enabled());
|
|
assert!(!g.group(1).enabled()); // independent
|
|
g.group(0).toggle();
|
|
assert!(!g.group(0).enabled());
|
|
}
|
|
}
|