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]
fn protocol_get_and_set() {
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()]);
// 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);
assert_eq!(v["ok"], true);
assert_eq!(v["devices"].as_array().unwrap().len(), 1);
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.
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
}
/// 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
@@ -188,14 +203,16 @@ impl CpuMining {
/// 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> {
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 groups = grouped(&cores, initial_size, &enabled);
// Cycle list: the usual powers of two plus the requested size, capped so a
// group never exceeds the core count (unless the user explicitly asked for
// a larger size), sorted and de-duplicated.
let cap = cores.len().max(initial_size).max(1);
// 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])
@@ -346,39 +363,67 @@ mod tests {
}
#[test]
fn cpu_mining_cycles_size_and_preserves_enabled_cores() {
// 8 cores, start at size 4 fully enabled -> two groups [0-3],[4-7], both on.
let m = CpuMining::new((0..8).collect(), 4, true);
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(), 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()));
// Disable the second group (cores 4-7): now only cores 0-3 are enabled.
m.toggle_group(1);
assert!(m.groups().group(0).enabled());
assert!(!m.groups().group(1).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 8 and rebuild: the single [0-7] group is off, because not
// all of its cores were enabled.
while m.group_size() != 8 {
// 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(), 1);
assert!(!g.group(0).enabled());
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-3 are still tracked as enabled, so
// [0,1] and [2,3] come back on while [4,5] and [6,7] stay off — the choice
// survived two regroups (not derived from the all-off size-8 grouping).
// 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(), 4);
assert_eq!(g.len(), 8);
assert!(g.group(0).enabled()); // [0,1]
assert!(g.group(1).enabled()); // [2,3]
assert!(!g.group(2).enabled()); // [4,5]
assert!(!g.group(3).enabled()); // [6,7]
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);
}
+6 -5
View File
@@ -125,11 +125,12 @@ struct Args {
#[arg(long, value_name = "SPEC")]
cpu_cores: Option<String>,
/// Cores per CPU mining row (default 4). Each row runs one shared solve
/// across its cores; larger groups cut memory sharply: total RAM is ~4 GB ×
/// (enabled cores / this size). Use 1 for one row (and one solve) per core.
/// Rows are aligned to core-index blocks of this size, so a row never
/// straddles a boundary. Cycle it live in the dashboard with 'g'.
/// Cores per CPU mining row. Each row runs one shared solve across its
/// cores; larger groups cut memory sharply: total RAM is ~4 GB × (enabled
/// cores / this size). Rows align to core-index blocks of this size. Capped
/// by core count so the row count stays manageable — ≤4 cores toggle
/// 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)]
cpu_group_size: usize,