TUI: scale CPU core-toggle granularity by core count

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>
This commit is contained in:
jackpotincorporated
2026-06-06 18:34:10 -04:00
parent afd56bee1b
commit 0002e90451
3 changed files with 79 additions and 33 deletions
+3 -3
View File
@@ -175,15 +175,15 @@ mod tests {
#[test] #[test]
fn protocol_get_and_set() { fn protocol_get_and_set() {
let controls = Controls::new(1, 0, 0, 0, 0, true); let controls = Controls::new(1, 0, 0, 0, 0, true);
let cpu = CpuMining::new((0..8).collect(), 4, false); // sizes [1,2,4,8] let cpu = CpuMining::new((0..16).collect(), 4, false); // 16 cores -> tier cap 4, sizes [1,2,4]
let stats = Stats::for_test(vec!["GPU 0".into()]); let stats = Stats::for_test(vec!["GPU 0".into()]);
// get: one device, cpu rows reflect the grouping (8 cores / 4 = 2 rows). // get: one device, cpu rows reflect the grouping (16 cores / 4 = 4 rows).
let v = process("{\"op\":\"get\"}", &controls, &cpu, &stats); let v = process("{\"op\":\"get\"}", &controls, &cpu, &stats);
assert_eq!(v["ok"], true); assert_eq!(v["ok"], true);
assert_eq!(v["devices"].as_array().unwrap().len(), 1); assert_eq!(v["devices"].as_array().unwrap().len(), 1);
assert_eq!(v["cpu"]["group_size"], 4); assert_eq!(v["cpu"]["group_size"], 4);
assert_eq!(v["cpu"]["rows"].as_array().unwrap().len(), 2); assert_eq!(v["cpu"]["rows"].as_array().unwrap().len(), 4);
// set device 0 disabled. // set device 0 disabled.
assert!(controls.device(0).enabled()); assert!(controls.device(0).enabled());
+70 -25
View File
@@ -165,6 +165,21 @@ fn grouped(cores: &[usize], size: usize, enabled: &BTreeSet<usize>) -> Arc<CpuGr
groups 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 /// 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 /// group size, and rebuilds the [`CpuGroups`] when the size is cycled from the
/// dashboard (the worker supervisor watches `group_size()` and respawns to /// dashboard (the worker supervisor watches `group_size()` and respawns to
@@ -188,14 +203,16 @@ impl CpuMining {
/// the starting group size (`--cpu-group-size`), and `start_enabled` whether /// the starting group size (`--cpu-group-size`), and `start_enabled` whether
/// mining begins on (`--cpu-mining`). /// mining begins on (`--cpu-mining`).
pub fn new(cores: Vec<usize>, initial_size: usize, start_enabled: bool) -> Arc<Self> { pub fn new(cores: Vec<usize>, initial_size: usize, start_enabled: bool) -> Arc<Self> {
let initial_size = initial_size.max(1); // 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 enabled: BTreeSet<usize> = if start_enabled { cores.iter().copied().collect() } else { BTreeSet::new() };
let groups = grouped(&cores, initial_size, &enabled); let groups = grouped(&cores, initial_size, &enabled);
// Cycle list: the usual powers of two plus the requested size, capped so a // Cycle list: powers of two from 1 up to the cap, plus the (clamped)
// group never exceeds the core count (unless the user explicitly asked for // requested size, sorted and de-duplicated.
// a larger size), sorted and de-duplicated.
let cap = cores.len().max(initial_size).max(1);
let mut sizes: Vec<usize> = [1usize, 2, 4, 8] let mut sizes: Vec<usize> = [1usize, 2, 4, 8]
.into_iter() .into_iter()
.chain([initial_size]) .chain([initial_size])
@@ -346,39 +363,67 @@ mod tests {
} }
#[test] #[test]
fn cpu_mining_cycles_size_and_preserves_enabled_cores() { fn group_size_tier_by_core_count() {
// 8 cores, start at size 4 fully enabled -> two groups [0-3],[4-7], both on. // ≤4 cores: individual cores only — size 1, no larger option.
let m = CpuMining::new((0..8).collect(), 4, true); 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.group_size(), 4);
assert_eq!(m.groups().len(), 2); 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())); assert!(m.groups().iter().all(|g| g.enabled()));
// Disable the second group (cores 4-7): now only cores 0-3 are enabled. // Disable the last group (cores 12-15): now only cores 0-11 are enabled.
m.toggle_group(1); m.toggle_group(3);
assert!(m.groups().group(0).enabled()); assert!(!m.groups().group(3).enabled());
assert!(!m.groups().group(1).enabled());
// Cycle to size 8 and rebuild: the single [0-7] group is off, because not // Cycle to size 1 (individual cores) and rebuild: 16 rows; cores 0-11 on,
// all of its cores were enabled. // 12-15 off.
while m.group_size() != 8 { while m.group_size() != 1 {
m.cycle_group_size(); m.cycle_group_size();
} }
let g = m.rebuild(); let g = m.rebuild();
assert_eq!(g.len(), 1); assert_eq!(g.len(), 16);
assert!(!g.group(0).enabled()); 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-3 are still tracked as enabled, so // Cycle to size 2 and rebuild: cores 0-11 are still tracked as enabled, so
// [0,1] and [2,3] come back on while [4,5] and [6,7] stay off — the choice // [0,1]..[10,11] come back on while [12,13],[14,15] stay off — the choice
// survived two regroups (not derived from the all-off size-8 grouping). // survived two regroups.
while m.group_size() != 2 { while m.group_size() != 2 {
m.cycle_group_size(); m.cycle_group_size();
} }
let g = m.rebuild(); let g = m.rebuild();
assert_eq!(g.len(), 4); assert_eq!(g.len(), 8);
assert!(g.group(0).enabled()); // [0,1] assert!(g.group(0).enabled()); // [0,1]
assert!(g.group(1).enabled()); // [2,3] assert!(g.group(5).enabled()); // [10,11]
assert!(!g.group(2).enabled()); // [4,5] assert!(!g.group(6).enabled()); // [12,13]
assert!(!g.group(3).enabled()); // [6,7] assert!(!g.group(7).enabled()); // [14,15]
assert!(m.generation() >= 2); assert!(m.generation() >= 2);
} }
+6 -5
View File
@@ -125,11 +125,12 @@ struct Args {
#[arg(long, value_name = "SPEC")] #[arg(long, value_name = "SPEC")]
cpu_cores: Option<String>, cpu_cores: Option<String>,
/// Cores per CPU mining row (default 4). Each row runs one shared solve /// Cores per CPU mining row. Each row runs one shared solve across its
/// across its cores; larger groups cut memory sharply: total RAM is ~4 GB × /// cores; larger groups cut memory sharply: total RAM is ~4 GB × (enabled
/// (enabled cores / this size). Use 1 for one row (and one solve) per core. /// cores / this size). Rows align to core-index blocks of this size. Capped
/// Rows are aligned to core-index blocks of this size, so a row never /// by core count so the row count stays manageable — ≤4 cores toggle
/// straddles a boundary. Cycle it live in the dashboard with 'g'. /// individually (1), 5-8 cores in groups of ≤2, more than 8 in groups of ≤4
/// — and the default is that cap. Cycle it live (within the cap) with 'g'.
#[arg(long, value_name = "N", default_value_t = 4)] #[arg(long, value_name = "N", default_value_t = 4)]
cpu_group_size: usize, cpu_group_size: usize,