Merge ember-tiers-25 2d188892 into tiers-class-25 (the class-flip engine half: tiers_class, tiers_remeasure_at, knee_loss_pct, the stale refusal, the re-armed search; on release-0.3.25 87d8862d, before the install-detach merge whose .iss reshaping reds ota::return_tests on 8779c80e)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
dc7df7bd9a
8 changed files with 95 additions and 7 deletions
|
|
@ -50,6 +50,9 @@ pub struct CardPref {
|
|||
pub tier: String,
|
||||
#[serde(default)]
|
||||
pub tier_at: u64,
|
||||
/// the program class the stored tiers were measured under ("" = none)
|
||||
#[serde(default)]
|
||||
pub tiers_class: String,
|
||||
/// the untuned point the last FULL plan measured first (its step 0), kept across confirm plans so the row can
|
||||
/// read "saves 84 W, 0.15% of rate"; 0 = never measured
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -685,6 +685,32 @@ pub fn tier_from_json(tiers: &[serde_json::Value], id: &str) -> Option<(Point, f
|
|||
Some((Point { clock_mhz: n("clock_mhz") as u32, power_pct: (n("power_pct") as u32).clamp(50, 100), mem_mhz: n("mem_mhz") as u32 }, n("limit_w"), n("mhs"), n("w")))
|
||||
}
|
||||
|
||||
/// The class flip (8 October 2026): tiers measured under one program class never apply under another. Stale when the
|
||||
/// stored class and the card's current class are both known and differ.
|
||||
pub fn tiers_stale(tiers_class: &str, program_class: &str) -> bool {
|
||||
!tiers_class.is_empty() && !program_class.is_empty() && tiers_class != program_class
|
||||
}
|
||||
|
||||
/// The refusal and the strip line for a stale set.
|
||||
pub fn stale_text(card: &str, tiers_class: &str, program_class: &str) -> String {
|
||||
format!("re-measuring {card} for class {program_class}: the stored tiers were measured under class {tiers_class}")
|
||||
}
|
||||
|
||||
/// The rate loss at the same lock under a new class against the old set: the new efficiency row against the old row at
|
||||
/// the same clock and cap when both exist, else the table of measured losses by card name (the 5090's class v5 rows at
|
||||
/// the 1,300 MHz lock, 8 October 2026), else 0 (unknown). Percent; negative = a gain.
|
||||
pub const KNEE_LOSS_TABLE: &[(&str, &str, f64)] = &[];
|
||||
pub fn knee_loss_pct(card: &str, new_class: &str, old: &[serde_json::Value], new: &[serde_json::Value]) -> f64 {
|
||||
let row = |v: &[serde_json::Value]| tier_from_json(v, "efficiency");
|
||||
if let (Some((op, _, omhs, _)), Some((np, _, nmhs, _))) = (row(old), row(new)) {
|
||||
if op.clock_mhz == np.clock_mhz && op.power_pct == np.power_pct && omhs > 0.0 && nmhs > 0.0 {
|
||||
return ((100.0 * (omhs - nmhs) / omhs) * 10.0).round() / 10.0;
|
||||
}
|
||||
}
|
||||
let n = card.to_ascii_uppercase();
|
||||
KNEE_LOSS_TABLE.iter().find(|(k, c, _)| n.contains(&k.to_ascii_uppercase()) && *c == new_class).map(|(_, _, l)| *l).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// The row a tier name points at in a tier list.
|
||||
pub fn tier_row(tiers: &[Tier], id: &str) -> Option<Row> {
|
||||
tiers.iter().find(|t| t.id == id).map(|t| t.row.clone())
|
||||
|
|
@ -2125,6 +2151,26 @@ mod tests {
|
|||
assert!(tier_from_json(&[], "balanced").is_none());
|
||||
}
|
||||
#[test]
|
||||
fn tiers_stored_under_v4_read_stale_under_v5_and_never_apply_as_current() {
|
||||
// known-failed first: the stored class and the live class differ
|
||||
assert!(tiers_stale("v4", "v5"));
|
||||
assert_eq!(stale_text("RTX 5090", "v4", "v5"), "re-measuring RTX 5090 for class v5: the stored tiers were measured under class v4");
|
||||
// the same class, or an unknown on either side, is not stale
|
||||
assert!(!tiers_stale("v4", "v4"));
|
||||
assert!(!tiers_stale("", "v5"));
|
||||
assert!(!tiers_stale("v4", ""));
|
||||
}
|
||||
#[test]
|
||||
fn the_knee_loss_reads_the_new_efficiency_row_against_the_old_at_the_same_lock() {
|
||||
let old: Vec<serde_json::Value> = tiers(&curve_5090()).iter().map(|t| t.json()).collect();
|
||||
let mut new_rows = curve_5090();
|
||||
for r in new_rows.iter_mut() { r.mhs *= 0.95; r.eff = r.mhs / r.watts; }
|
||||
let new: Vec<serde_json::Value> = tiers(&new_rows).iter().map(|t| t.json()).collect();
|
||||
assert_eq!(knee_loss_pct("NVIDIA GeForce RTX 5090", "v5", &old, &new), 5.0);
|
||||
// no old set and no table row: unknown reads 0
|
||||
assert_eq!(knee_loss_pct("NVIDIA GeForce RTX 5090", "v5", &[], &new), 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn the_tier_json_and_the_set_line_carry_the_fields_the_ui_reads() {
|
||||
let t = tiers(&curve_5090());
|
||||
let j = t[1].json();
|
||||
|
|
|
|||
|
|
@ -1541,11 +1541,16 @@ impl Engine {
|
|||
};
|
||||
let mut applied = Vec::new();
|
||||
for idx in targets {
|
||||
let (name, device, found) = {
|
||||
let (name, device, found, stale) = {
|
||||
let st = self.st();
|
||||
let Some(c) = st.mining.cards.get(idx) else { continue };
|
||||
(c.name.clone(), c.device.clone(), crate::ember::tier_from_json(&c.tiers, &tier))
|
||||
let stale = crate::ember::tiers_stale(&c.tiers_class, &c.program_class).then(|| crate::ember::stale_text(&c.name, &c.tiers_class, &c.program_class));
|
||||
(c.name.clone(), c.device.clone(), crate::ember::tier_from_json(&c.tiers, &tier), stale)
|
||||
};
|
||||
if let Some(why) = stale {
|
||||
self.shared.event("warn", &why);
|
||||
continue;
|
||||
}
|
||||
let Some((point, limit_w, mhs, w)) = found else {
|
||||
self.shared.event("warn", &format!("{name}: not measured yet: the search runs 2 min into steady mining"));
|
||||
continue;
|
||||
|
|
@ -3079,7 +3084,7 @@ impl Engine {
|
|||
let forced = self.sweep_queue.contains(&c.key);
|
||||
let due = match prefs.get(&c.key) {
|
||||
None => true,
|
||||
Some(p) => p.sweep_at == 0 || unix.saturating_sub(p.sweep_at) >= ember.period_s || (!c.driver.is_empty() && !p.sweep_driver.is_empty() && crate::ember::driver_major(&c.driver) != crate::ember::driver_major(&p.sweep_driver)) || (!c.program_class.is_empty() && !p.sweep_class.is_empty() && c.program_class != p.sweep_class),
|
||||
Some(p) => p.sweep_at == 0 || unix.saturating_sub(p.sweep_at) >= ember.period_s || crate::ember::tiers_stale(&p.tiers_class, &c.program_class) || (!c.driver.is_empty() && !p.sweep_driver.is_empty() && crate::ember::driver_major(&c.driver) != crate::ember::driver_major(&p.sweep_driver)) || (!c.program_class.is_empty() && !p.sweep_class.is_empty() && c.program_class != p.sweep_class),
|
||||
};
|
||||
let auto = auto_on || !c.tune_control;
|
||||
// a measure-only card (Apple, NVIDIA without Power control) measures its baseline once a period even
|
||||
|
|
@ -3133,6 +3138,20 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
if let Some((idx, forced)) = pick {
|
||||
// the class flip: the first pick after the flip says why it re-measures, once
|
||||
let flip = {
|
||||
let mut st = self.st();
|
||||
match st.mining.cards.get_mut(idx) {
|
||||
Some(c) if crate::ember::tiers_stale(&c.tiers_class, &c.program_class) && c.tiers_remeasure_at == 0.0 => {
|
||||
c.tiers_remeasure_at = unix as f64;
|
||||
Some(crate::ember::stale_text(&c.name, &c.tiers_class, &c.program_class))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
if let Some(line) = flip {
|
||||
self.shared.event("info", &line);
|
||||
}
|
||||
self.sweep_begin(idx, forced);
|
||||
} else if self.shared.runtime.sweep_only && self.sweep_queue.is_empty() && self.sweep_pending.is_none() {
|
||||
self.shared.send(Cmd::Quit("the --sweep run (every card done)"));
|
||||
|
|
@ -3749,9 +3768,15 @@ impl Engine {
|
|||
c.tier = "max".into();
|
||||
} else {
|
||||
let t = crate::ember::tiers(&run.rows);
|
||||
c.tiers = t.iter().map(|x| x.json()).collect();
|
||||
let new: Vec<serde_json::Value> = t.iter().map(|x| x.json()).collect();
|
||||
if crate::ember::tiers_stale(&c.tiers_class, &c.program_class) {
|
||||
c.knee_loss_pct = crate::ember::knee_loss_pct(&c.name, &c.program_class, &c.tiers, &new);
|
||||
}
|
||||
c.tiers = new;
|
||||
c.tier_note = String::new();
|
||||
}
|
||||
c.tiers_class = c.program_class.clone();
|
||||
c.tiers_remeasure_at = 0.0;
|
||||
// the core-clock knob's result (7 October 2026): the chosen lock against the cap point, and why it stopped
|
||||
if kind == crate::ember::PlanKind::Baseline {
|
||||
c.lock_note = if c.vendor == "apple" || run.plan.limits.clock_max_mhz == 0 { "no lever".into() } else { c.sweep_note.clone() };
|
||||
|
|
@ -3800,6 +3825,7 @@ impl Engine {
|
|||
e.tier = t.clone();
|
||||
e.tier_at = unix;
|
||||
}
|
||||
e.tiers_class = c.program_class.clone();
|
||||
// Miner UI 4: the row's sub-line reads "(the ladder's floor)" when the chosen clock is the lowest step
|
||||
let floor_point = row.point.clock_mhz > 0 && row.point.clock_mhz <= run.plan.limits.clock_floor();
|
||||
e.sweep_floor = floor_point;
|
||||
|
|
|
|||
|
|
@ -168,6 +168,10 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
|
|||
c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz };
|
||||
c.tune_source = p.sweep_source.clone();
|
||||
c.tune_goal = p.tune_goal.clone();
|
||||
// Ember Tune tiers (8 October 2026): the tier in force and the class the set was measured under survive a restart
|
||||
c.tier = p.tier.clone();
|
||||
c.tier_at = p.tier_at as f64;
|
||||
c.tiers_class = p.tiers_class.clone();
|
||||
c.tune_before_watts = p.sweep_before_watts;
|
||||
c.tune_before_mhs = p.sweep_before_mhs;
|
||||
c.tune_floor = p.sweep_floor;
|
||||
|
|
|
|||
|
|
@ -309,12 +309,15 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
match &key {
|
||||
Some(k) => {
|
||||
let Some(c) = st.mining.cards.iter().find(|c| &c.key == k) else { return Err("no such card".into()) };
|
||||
if crate::ember::tiers_stale(&c.tiers_class, &c.program_class) {
|
||||
return Err(crate::ember::stale_text(&c.name, &c.tiers_class, &c.program_class));
|
||||
}
|
||||
if crate::ember::tier_from_json(&c.tiers, &tier).is_none() {
|
||||
return Err("not measured yet: the search runs 2 min into steady mining".into());
|
||||
}
|
||||
vec![c.key.clone()]
|
||||
}
|
||||
None => st.mining.cards.iter().filter(|c| crate::ember::tier_from_json(&c.tiers, &tier).is_some()).map(|c| c.key.clone()).collect(),
|
||||
None => st.mining.cards.iter().filter(|c| !crate::ember::tiers_stale(&c.tiers_class, &c.program_class) && crate::ember::tier_from_json(&c.tiers, &tier).is_some()).map(|c| c.key.clone()).collect(),
|
||||
}
|
||||
};
|
||||
shared.send(Cmd::TuneTier(key, tier));
|
||||
|
|
|
|||
|
|
@ -187,6 +187,11 @@ pub struct CardState {
|
|||
pub tier: String, // "" | efficiency | balanced | max
|
||||
pub tier_note: String, // "" or "no lever: ..."
|
||||
pub tier_at: f64, // unix s (0 = never)
|
||||
// the class flip (8 October 2026, main's order): the program class the stored tiers were measured under, when the
|
||||
// class-flip re-run was queued (0 = not started), and the rate loss at the same lock under the new class (0 = unknown)
|
||||
pub tiers_class: String,
|
||||
pub tiers_remeasure_at: f64,
|
||||
pub knee_loss_pct: f64,
|
||||
// the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
|
||||
pub variant: String,
|
||||
pub race_mhs: f64,
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ e223db18bcfa74a1f60335de62390729a19975e6a6bddd7547bf9d6f0676df17
|
|||
365d32104ede41db22837ece4eb4fa8860505360955c974d3055f1114339d4c5
|
||||
# 8 October 2026: PC 1's MSVC 0.3.24 host from the installer kit at release-0.3.24 0c47b59a (run-20261008-084900, 7 s)
|
||||
0e241c94eecfa9323a5fb40a02e428dfaf4a115c30f444e517667c6753c6d7c6
|
||||
bc8d4f795a039636dd438fd13836f695193414ea4a50ba020d31265b1084365b Igneum Miner.exe 0.3.25 92f004f1 (PC 1, MSVC, 10:37 BST 8 October 2026)
|
||||
# 8 October 2026: PC 1's MSVC 0.3.25 host from the installer kit at release-0.3.25 92f004f1 (run-20261008-093513, 8 s)
|
||||
bc8d4f795a039636dd438fd13836f695193414ea4a50ba020d31265b1084365b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
7bd2940fa5410f5168a0fb60a0cfa6835b46fe0c
|
||||
e0644958c07c6c6fd7be3c3175e2c41609b4e798
|
||||
|
|
|
|||
Loading…
Reference in a new issue