From 69d15f3f5f7935f28692f42aa081cdd01c2bf73f Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:49:18 +0000 Subject: [PATCH 1/4] 0.3.25: the Windows node-source pin moves to e0644958 (7bd2940f plus proving_fee_ceiling_activation_daa 82,800 in the Devnet 3 object; digest 1b37cb9d; the 0.3.25 pin candidate) Co-Authored-By: Claude Fable 5.1 --- packaging/windows/node-source.pin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/windows/node-source.pin b/packaging/windows/node-source.pin index 8690938f3..bd9ff2fbd 100644 --- a/packaging/windows/node-source.pin +++ b/packaging/windows/node-source.pin @@ -1 +1 @@ -7bd2940fa5410f5168a0fb60a0cfa6835b46fe0c +e0644958c07c6c6fd7be3c3175e2c41609b4e798 From 87d8862d530c689d2789cc910149a504d5b3a574 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:53:33 +0000 Subject: [PATCH 2/4] 0.3.25: host.sha256 in the gate's line shape (the sha alone, the note as a comment line above it; the one-line form was not read by host-gate.py) Co-Authored-By: Claude Fable 5.1 --- packaging/windows/host.sha256 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packaging/windows/host.sha256 b/packaging/windows/host.sha256 index 1934deb92..24a05ab88 100644 --- a/packaging/windows/host.sha256 +++ b/packaging/windows/host.sha256 @@ -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 From 7e08d5663a5860a371c20b6ae6134dcc8b9a2520 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:04:58 +0000 Subject: [PATCH 3/4] Ember Tune tiers: the class flip (tiers_class, tiers_remeasure_at, knee_loss_pct): tiers measured under one program class read stale under another, the apply and /api/tune/tier refuse with the re-measuring line, the scheduler re-arms the search on the class change and says why once, the search's end stores the set under the new class with the knee loss against the old; the tier and the class survive a restart; known-failed first (v4 tiers under v5 read stale) Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/config.rs | 3 +++ app/igneum-app/src/ember.rs | 48 ++++++++++++++++++++++++++++++++++- app/igneum-app/src/engine.rs | 34 ++++++++++++++++++++++--- app/igneum-app/src/hotplug.rs | 4 +++ app/igneum-app/src/server.rs | 5 +++- app/igneum-app/src/state.rs | 5 ++++ 6 files changed, 93 insertions(+), 6 deletions(-) diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 7b863e7f4..6a49f307b 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -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)] diff --git a/app/igneum-app/src/ember.rs b/app/igneum-app/src/ember.rs index 05bdb20de..ab7b77e03 100644 --- a/app/igneum-app/src/ember.rs +++ b/app/igneum-app/src/ember.rs @@ -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 { tiers.iter().find(|t| t.id == id).map(|t| t.row.clone()) @@ -2125,7 +2151,27 @@ mod tests { assert!(tier_from_json(&[], "balanced").is_none()); } #[test] - fn the_tier_json_and_the_set_line_carry_the_fields_the_ui_reads() { + 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 = 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 = 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] + let t = tiers(&curve_5090()); let j = t[1].json(); for k in ["id", "clock_mhz", "power_pct", "w", "mhs", "mhw", "source"] { diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 347011f7d..836469efc 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -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 = 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; diff --git a/app/igneum-app/src/hotplug.rs b/app/igneum-app/src/hotplug.rs index f54a51627..56e394366 100644 --- a/app/igneum-app/src/hotplug.rs +++ b/app/igneum-app/src/hotplug.rs @@ -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; diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index f6c0e1bbd..f52e4b1a6 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -309,12 +309,15 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { 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)); diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index 018022227..8ac406aca 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -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, From 2d18889284fceab1e804199964c9690ade2d7f1d Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:06:15 +0000 Subject: [PATCH 4/4] tiers tests: the json test's fn line, dropped by the stale-test insertion Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/ember.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/igneum-app/src/ember.rs b/app/igneum-app/src/ember.rs index ab7b77e03..4fbaf842f 100644 --- a/app/igneum-app/src/ember.rs +++ b/app/igneum-app/src/ember.rs @@ -2171,7 +2171,7 @@ mod tests { 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(); for k in ["id", "clock_mhz", "power_pct", "w", "mhs", "mhw", "source"] {