Ember Tune: the core-clock knob (0.3.24, main's order of 7 October 2026): the clock ladder continues below 45 percent in 100 MHz steps to a 20 percent floor; the search stops at the knee (the first row more than the tolerance under the cap point's rate) or on a faulted row (a rejected or mismatched hash: the fingerprint check), the best MH per watt within tolerance is the point; lock_result and the card's lock_* fields for the UI; tests known-failed first on a fake helper

ember.rs: CLOCK_FLOOR_PCT 45 -> 20 (the PC 1 passes of 7 October: the 5090's best MH/W at 1,200 to 1,300 MHz, 39 to 42 percent of 3,090, under the old floor), CLOCK_FINE_STEP_MHZ 100, Plan.clock_fine, Plan::cap_row, Plan::clock_stop_reason ("rate fell 5.2 percent at 1200 MHz", "fingerprint mismatch at 2163 MHz, clocks reset", "the floor at 700 MHz"), the stop rule in Plan::next, LockResult + lock_result (lock_mhz/mhs/w/mhw, unlocked_mhs/w, lock_note; "no lever" without a clock maximum). state.rs: CardState lock_mhz, lock_mhs, lock_w, lock_mhw, unlocked_mhs, unlocked_w, lock_step, lock_steps, lock_at, lock_note. engine.rs: sweep_finish fills them (Baseline: "no lever" on Apple or without a maximum clock), the progress tick fills lock_step/lock_steps while a clock step runs. Tests: the ladder to the floor (2,781 .. 1,390, 1,300 .. 700), the knee at 1 and 1.5 percent on the measured 5090 rows (1,854 and 1,300 as the points), a Faulted clock row ends the search and the note says so, and the Run-level fake helper: a mismatch during 2,163's hold -> the row Faulted, no next step, the final Apply is the chosen 2,472 (the reset), Finished; the existing full-plan test updated to the stop rule. The 18 Ember tests green on box 2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-josh 2026-10-07 23:08:34 +01:00
parent cdada77922
commit 74585c916e
3 changed files with 296 additions and 23 deletions

View file

@ -23,8 +23,15 @@ pub const POWER_STEPS_PCT: [u32; 6] = [100, 90, 80, 70, 60, 50];
/// 6 October 2026, run 6: the 5090's best MH/W sat on the 60% floor (1,854 MHz: 0.563 MH/W, the rate within 0.15%),
/// so the ladder and the floor go to 45% of the maximum; the 1% rate tolerance is the guard below that
pub const CLOCK_STEPS_PCT: [u32; 7] = [100, 90, 80, 70, 60, 50, 45];
/// A card's clock floor when the vendor reports none: this share of its maximum core clock.
pub const CLOCK_FLOOR_PCT: u32 = 45;
/// A card's clock floor when the vendor reports none: this share of its maximum core clock. 7 October 2026, the PC 1
/// efficiency passes (docs/bench-log.md): the 5090's best MH per watt sat at 1,200 to 1,300 MHz (39 to 42 percent of
/// 3,090) and the rate fell past 5 percent only at 1,200 (class v3) and 1,100 (class v4), under the old 45 percent
/// floor; so the ladder continues below 45 percent in [`CLOCK_FINE_STEP_MHZ`] steps down to 20 percent of the maximum
/// (618 MHz on the 5090) and the stop rule, not the floor, ends the search.
pub const CLOCK_FLOOR_PCT: u32 = 20;
/// Below the percent ladder's last rung (45 percent) the clock ladder descends in steps of this many MHz until the
/// rate falls more than the tolerance under the cap point's rate (the knee), a step faults, or the floor is reached.
pub const CLOCK_FINE_STEP_MHZ: u32 = 100;
/// A point may lose this much rate against the fastest point and still win on MH per watt (the manifest can change it).
pub const RATE_TOLERANCE_PCT: f64 = 1.0;
/// A step whose hottest GPU reading reaches this is marked hot and cannot win (the engine aborts at 90).
@ -193,6 +200,9 @@ pub struct Plan {
pub tolerance_pct: f64,
power: Vec<Step>,
clock_pcts: Vec<u32>,
/// the clock ladder below the percent rungs: MHz values from the last rung minus one fine step down to the floor
/// (the core-clock knob of 7 October 2026; empty when the card has no readable maximum clock)
clock_fine: Vec<u32>,
fixed: Vec<Step>,
/// Ember 2 (Climb): the start point, the step sizes and the step budget
climb: Option<Climb>,
@ -218,7 +228,7 @@ impl Plan {
let mem_step = if limits.mem_max_mhz > limits.mem_default_mhz { ((limits.mem_max_mhz - limits.mem_default_mhz) / 20).max(25) } else { 0 };
let core_step = if limits.clock_max_mhz > 0 { (limits.clock_max_mhz / 20).max(25) } else { 0 };
let start = Point { clock_mhz: limits.clamp_clock(start.clock_mhz), power_pct: start.power_pct.clamp(50, 100), mem_mhz: limits.clamp_mem(start.mem_mhz) };
Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) }
Plan { kind: PlanKind::Climb, limits: limits.clone(), before: start, tolerance_pct: goal.tolerance_pct(tolerance_pct), power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed: Vec::new(), climb: Some(Climb { start, mem_step, core_step, budget: 5, goal }) }
}
/// The goal's score of a row: MH per watt for efficiency and balanced, the rate for maximum rate.
@ -285,7 +295,22 @@ impl Plan {
}
}
let clock_pcts = if limits.clock_max_mhz > 0 { CLOCK_STEPS_PCT[1..].to_vec() } else { Vec::new() };
Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, fixed: Vec::new(), climb: None }
// the fine ladder: from the last percent rung down to the floor in CLOCK_FINE_STEP_MHZ steps (the knob of
// 7 October 2026; the stop rule in `next` ends it at the knee)
let mut clock_fine = Vec::new();
if limits.clock_max_mhz > 0 {
let last_pct = limits.clamp_clock(limits.clock_max_mhz * CLOCK_STEPS_PCT[CLOCK_STEPS_PCT.len() - 1] / 100);
let floor = limits.clock_floor();
let mut m = (last_pct / CLOCK_FINE_STEP_MHZ) * CLOCK_FINE_STEP_MHZ;
if m >= last_pct {
m = m.saturating_sub(CLOCK_FINE_STEP_MHZ);
}
while m >= floor && m > 0 {
clock_fine.push(m);
m = m.saturating_sub(CLOCK_FINE_STEP_MHZ);
}
}
Plan { kind: PlanKind::Full, limits: limits.clone(), before, tolerance_pct, power, clock_pcts, clock_fine, fixed: Vec::new(), climb: None }
}
/// The prior's point, then one neighbour: the next clock step up when the prior caps the clock (is the cap
@ -304,14 +329,14 @@ impl Plan {
if neighbour != p {
fixed.push(Step { point: neighbour, watts: limits.watts_for(neighbour.power_pct), kind: Kind::Confirm });
}
Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed, climb: None }
Plan { kind: PlanKind::Confirm, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None }
}
/// One step at the card's current point: the before number, and all a measure-only card (Apple, or NVIDIA
/// with Power control off) reports.
pub fn baseline(limits: &Limits, before: Point, tolerance_pct: f64) -> Plan {
let fixed = vec![Step { point: before, watts: limits.watts_for(before.power_pct), kind: Kind::Baseline }];
Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), fixed, climb: None }
Plan { kind: PlanKind::Baseline, limits: limits.clone(), before, tolerance_pct, power: Vec::new(), clock_pcts: Vec::new(), clock_fine: Vec::new(), fixed, climb: None }
}
/// How many steps the plan has at most (the clock ladder counts whether or not it runs).
@ -319,7 +344,42 @@ impl Plan {
if let Some(c) = &self.climb {
return c.budget;
}
self.fixed.len() + self.power.len() + self.clock_pcts.len()
self.fixed.len() + self.power.len() + self.clock_pcts.len() + self.clock_fine.len()
}
/// The cap point's row: the power ladder's choice (the row the clock search is read against), else the first row.
pub fn cap_row(&self, rows: &[Row]) -> Option<Row> {
if self.power.is_empty() {
rows.first().cloned()
} else {
choose(&rows[..self.power.len().min(rows.len())], self.tolerance_pct).or_else(|| rows.first().cloned())
}
}
/// Why the clock search ended after `rows`, in words, or None while it runs: a faulted clock row (a rejected or
/// mismatched hash during the hold: the fingerprint check), the knee (the rate under the cap point's by more than
/// the tolerance), or the floor.
pub fn clock_stop_reason(&self, rows: &[Row]) -> Option<String> {
let last = rows.last()?;
if last.point.clock_mhz == 0 || !matches!(self.kind, PlanKind::Full) {
return None;
}
let clock_rows = rows.len().saturating_sub(self.power.len());
if clock_rows == 0 {
return None;
}
if last.mark == Some(Mark::Faulted) {
return Some(format!("fingerprint mismatch at {} MHz, clocks reset", last.point.clock_mhz));
}
if let Some(cap) = self.cap_row(rows) {
if last.usable() && cap.usable() && cap.mhs > 0.0 && last.mhs < cap.mhs * (1.0 - self.tolerance_pct.max(0.0) / 100.0) {
return Some(format!("rate fell {:.1} percent at {} MHz", 100.0 * (cap.mhs - last.mhs) / cap.mhs, last.point.clock_mhz));
}
}
if clock_rows >= self.clock_pcts.len() + self.clock_fine.len() {
return Some(format!("the floor at {} MHz", last.point.clock_mhz));
}
None
}
pub fn is_empty(&self) -> bool {
self.len() == 0
@ -338,10 +398,22 @@ impl Plan {
return Some(self.power[i].clone());
}
let k = i - self.power.len();
let pct = *self.clock_pcts.get(k)?;
// the stop rule (7 October 2026): a faulted clock row or the knee ends the search; the choice is made among
// the rows so far
if k > 0 {
if let Some(reason) = self.clock_stop_reason(rows) {
if !reason.starts_with("the floor") {
return None;
}
}
}
let clock = if k < self.clock_pcts.len() {
self.limits.clamp_clock(self.limits.clock_max_mhz * self.clock_pcts[k] / 100)
} else {
*self.clock_fine.get(k - self.clock_pcts.len())?
};
// the clock ladder rides the power point the power ladder chose (the before point when nothing won)
let power_pct = if self.power.is_empty() { self.before.power_pct } else { choose(&rows[..self.power.len()], self.tolerance_pct).map(|r| r.point.power_pct).unwrap_or(self.before.power_pct) };
let clock = self.limits.clamp_clock(self.limits.clock_max_mhz * pct / 100);
// a step whose clamp lands on the previous step's clock is dropped (the floor was reached)
if rows.last().map(|r| r.point.clock_mhz == clock).unwrap_or(false) {
return None;
@ -921,6 +993,38 @@ pub fn result_line(kind: PlanKind, mhs: f64, watts: f64, eff: f64) -> String {
}
}
/// The core-clock knob's result on a card (7 October 2026; the UI lane's field shape): the chosen lock against the cap
/// point's unlocked row, and the stop reason in words.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LockResult {
/// the chosen core clock cap (0 = unlocked)
pub lock_mhz: u32,
pub lock_mhs: f64,
pub lock_w: f64,
pub lock_mhw: f64,
/// the cap point's row (clock 0): the rate and draw the lock is read against
pub unlocked_mhs: f64,
pub unlocked_w: f64,
/// "rate fell 5.1 percent at 1,200 MHz", "fingerprint mismatch at 1,400 MHz, clocks reset", "the floor at 618 MHz",
/// "no lever" (a card without a clock cap), "" while nothing ran
pub lock_note: String,
}
/// The knob's result from a finished plan's rows and its chosen row.
pub fn lock_result(plan: &Plan, rows: &[Row], chosen: &Row) -> LockResult {
let cap = plan.cap_row(rows);
let (unlocked_mhs, unlocked_w) = cap.as_ref().map(|c| (c.mhs, c.watts)).unwrap_or((0.0, 0.0));
let ran_clocks = rows.iter().any(|r| r.point.clock_mhz > 0);
let note = if plan.limits.clock_max_mhz == 0 {
"no lever".to_string()
} else if !ran_clocks {
String::new()
} else {
plan.clock_stop_reason(rows).unwrap_or_else(|| format!("stopped at {} MHz", rows.last().map(|r| r.point.clock_mhz).unwrap_or(0)))
};
LockResult { lock_mhz: chosen.point.clock_mhz, lock_mhs: chosen.mhs, lock_w: chosen.watts, lock_mhw: chosen.eff, unlocked_mhs, unlocked_w, lock_note: note }
}
/// Why a card cannot be tuned beyond measuring, or None when both knobs are available.
pub fn control_reason(vendor: &str, limits: &Limits, device: &str, power_control: bool, amd_helper: bool) -> Option<String> {
match vendor {
@ -952,7 +1056,9 @@ mod tests {
#[test]
fn the_full_plan_is_the_power_ladder_then_the_clock_ladder_at_the_chosen_power() {
let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 80, mem_mhz: 0 }, 1.0);
assert_eq!(plan.len(), 5 + 6, "five power steps (60% and 50% clamp to 400 W; one kept) and six clock steps (90% down to 45%)");
// five power steps (60% and 50% clamp to 400 W; one kept), six percent rungs (90% down to 45% = 1,390) and the
// fine ladder 1,300 down to the 20% floor (618): 1,300, 1,200, ..., 700 = 7 steps
assert_eq!(plan.len(), 5 + 6 + 7);
let first = plan.next(&[]).unwrap();
assert_eq!((first.point, first.watts, first.kind), (Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 575.0, Kind::Power));
// the power ladder: 575, 518, 460, 403, 400
@ -974,24 +1080,21 @@ mod tests {
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, 2472);
rows.push(row_at(s.point, 220.0, 123.5));
rows.push(row_at(plan.next(&rows).unwrap().point, 200.0, 118.0));
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, 1854, "60% of 3,090");
rows.push(row_at(s.point, 180.0, 100.0));
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, 1545, "50%");
rows.push(row_at(s.point, 170.0, 90.0));
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, 1390, "45% of 3,090 is the floor (6 October 2026)");
rows.push(row_at(s.point, 160.0, 80.0));
assert_eq!(s.point.clock_mhz, 2163, "70%");
rows.push(row_at(s.point, 200.0, 118.0));
// the stop rule (7 October 2026): 118 is 4.8% under the cap point's 124, past the 1% tolerance, so the search
// ends here (the 6 October ladder went on to 1,854, 1,545 and 1,390)
assert_eq!(plan.next(&rows), None);
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("rate fell 4.8 percent at 2163 MHz"));
// the choice: 2,472 MHz keeps 99.6% of the top rate at 220 W = 0.561 MH/W; 2,163 MHz (118 MH/s) is outside the 1% tolerance
let best = choose(&rows, 1.0).unwrap();
assert_eq!(best.point, Point { clock_mhz: 2472, power_pct: 100, mem_mhz: 0 });
// a wider tolerance lets the 2,163 MHz step (0.590 MH/W, 4.8% slower) win
assert_eq!(choose(&rows, 5.0).unwrap().point.clock_mhz, 2163);
// no power limits, clocks only; no clocks, power only; nothing, empty
assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 6);
// clocks only: six percent rungs (1,800 .. 900) then the fine ladder 800 .. 400 (the 20% floor) = 5 more
assert_eq!(Plan::full(&Limits { clock_max_mhz: 2000, ..Default::default() }, Point::default(), 1.0).len(), 6 + 5);
assert_eq!(Plan::full(&Limits { power_default_w: 300.0, ..Default::default() }, Point::default(), 1.0).len(), 6);
assert!(Plan::full(&Limits::default(), Point::default(), 1.0).is_empty());
}
@ -1017,11 +1120,12 @@ mod tests {
#[test]
fn limits_never_exceed_the_vendor_or_undercut_the_floor() {
let l = l5090();
assert_eq!(l.clock_floor(), 1390);
assert_eq!(l.clamp_clock(1000), 1390);
assert_eq!(l.clock_floor(), 618, "20% of 3,090 (7 October 2026; the 45% floor of 6 October sat on the 5090's knee)");
assert_eq!(l.clamp_clock(1000), 1000);
assert_eq!(l.clamp_clock(500), 618);
assert_eq!(l.clamp_clock(5000), 3090);
assert_eq!(l.clamp_clock(0), 0, "unlocked stays unlocked");
assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 45% rule");
assert_eq!(Limits { clock_max_mhz: 3000, clock_min_mhz: 2100, ..Default::default() }.clamp_clock(1500), 2100, "the vendor's floor wins over the 20% rule");
assert_eq!(l.watts_for(100), 575.0);
assert_eq!(l.watts_for(50), 400.0);
assert_eq!(Limits { power_default_w: 300.0, power_max_w: 250.0, ..Default::default() }.watts_for(100), 250.0);
@ -1321,4 +1425,138 @@ mod tests {
assert!(control_reason("amd", &Limits::default(), "1", false, true).is_none());
}
}
/// The core-clock knob (7 October 2026, the PC 1 efficiency passes): a flat ladder walks below the old 45 percent
/// floor in 100 MHz steps to the 20 percent floor, and the result names the floor.
#[test]
fn the_clock_ladder_continues_below_45_percent_in_100_mhz_steps_to_the_floor() {
let plan = Plan::full(&l5090(), Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
let mut rows = Vec::new();
for _ in 0..5 {
let s = plan.next(&rows).unwrap();
rows.push(row_at(s.point, 300.0, 136.8));
}
let mut clocks = Vec::new();
while let Some(s) = plan.next(&rows) {
assert_eq!(s.kind, Kind::Clock);
clocks.push(s.point.clock_mhz);
// the rate holds (memory-bound): the draw falls with the clock
rows.push(row_at(s.point, 300.0 - clocks.len() as f64 * 10.0, 136.0));
}
assert_eq!(clocks, vec![2781, 2472, 2163, 1854, 1545, 1390, 1300, 1200, 1100, 1000, 900, 800, 700]);
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("the floor at 700 MHz"));
let chosen = choose(&rows, 1.0).unwrap();
assert_eq!(chosen.point.clock_mhz, 700, "flat rate: the lowest draw wins");
let r = lock_result(&plan, &rows, &chosen);
assert_eq!((r.lock_mhz, r.lock_w, r.unlocked_mhs, r.unlocked_w), (700, 170.0, 136.8, 300.0));
assert_eq!(r.lock_note, "the floor at 700 MHz");
}
/// The stop rule on PC 1's RTX 5090 rows of 7 October 2026 (class v3, the card alone): the first clock row more
/// than the tolerance under the cap point's rate ends the search and the best MH per watt among the rows within
/// tolerance is chosen. At the 1 percent tolerance the 5090's rate (136.6 at 2,781) is 1.24 percent down at
/// 1,545 MHz, so the search ends there and 1,854 MHz (135.6 MH/s at 239.6 W) is the point; at 1.5 percent it runs
/// on to 1,200 (5.2 percent down) and 1,300 MHz is the point. The tolerance is the manifest's.
#[test]
fn the_clock_search_stops_at_the_knee_and_names_it() {
let measured: Vec<(u32, f64, f64)> = vec![(2781, 317.9, 136.6), (2472, 276.0, 136.4), (2163, 252.4, 136.1), (1854, 239.6, 135.6), (1545, 232.1, 134.9), (1390, 229.0, 134.85), (1300, 223.3, 134.6), (1200, 215.7, 129.5)];
let walk = |tolerance: f64| -> (Plan, Vec<Row>) {
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, tolerance);
let mut rows = vec![];
for (mhz, w, mhs) in &measured {
let Some(s) = plan.next(&rows) else { break };
assert_eq!(s.point.clock_mhz, *mhz);
rows.push(row_at(s.point, *w, *mhs));
}
(plan, rows)
};
let (plan, rows) = walk(1.0);
assert_eq!(rows.last().unwrap().point.clock_mhz, 1545, "the search ends on the first row over 1 percent under the cap row");
assert_eq!(plan.next(&rows), None);
let reason = plan.clock_stop_reason(&rows).unwrap();
assert!(reason.starts_with("rate fell 1.2 percent at 1545 MHz"), "{reason}");
let chosen = choose(&rows, 1.0).unwrap();
assert_eq!(chosen.point.clock_mhz, 1854, "the best MH per watt within 1 percent of the fastest row");
let r = lock_result(&plan, &rows, &chosen);
assert_eq!((r.lock_mhz, r.unlocked_mhs), (1854, 136.6));
assert!((r.lock_mhw - 135.6 / 239.6).abs() < 1e-6);
let (plan, rows) = walk(1.5);
assert_eq!(rows.last().unwrap().point.clock_mhz, 1200);
assert_eq!(plan.next(&rows), None);
assert!(plan.clock_stop_reason(&rows).unwrap().starts_with("rate fell 5.2 percent at 1200 MHz"));
assert_eq!(choose(&rows, 1.5).unwrap().point.clock_mhz, 1300);
}
/// The fingerprint rule: a clock row marked Faulted (a rejected or mismatched hash during the hold) ends the search
/// at once; the choice is made among the usable rows and the note says why.
#[test]
fn a_faulted_clock_row_ends_the_search_and_the_note_says_so() {
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
let mut rows = vec![];
for (mhz, w, mhs) in [(2781, 317.9, 136.6), (2472, 276.0, 136.4)] {
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, mhz);
rows.push(row_at(s.point, w, mhs));
}
let s = plan.next(&rows).unwrap();
assert_eq!(s.point.clock_mhz, 2163);
let mut bad = row_at(s.point, 252.4, 136.1);
bad.faults = 1;
bad.mark = Some(Mark::Faulted);
rows.push(bad);
assert_eq!(plan.next(&rows), None, "the search ends on the faulted row");
assert_eq!(plan.clock_stop_reason(&rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset"));
let chosen = choose(&rows, 1.0).unwrap();
assert_eq!(chosen.point.clock_mhz, 2472, "the faulted row never wins");
assert_eq!(lock_result(&plan, &rows, &chosen).lock_note, "fingerprint mismatch at 2163 MHz, clocks reset");
}
/// The same through the state machine with a fake helper (the known-failed case first: a mismatch mid-search must
/// reset and abort): the run applies 2,781 and 2,472, a fault lands during 2,163's hold, the row comes out Faulted,
/// the next step is none, and the run's final Apply is the chosen 2,472 point (the reset), then Finished.
#[test]
fn a_mismatch_mid_search_resets_to_the_chosen_point_and_finishes() {
let plan = Plan::full(&Limits { clock_max_mhz: 3090, ..Default::default() }, Point { clock_mhz: 0, power_pct: 100, mem_mhz: 0 }, 1.0);
let timing = Timing { settle: Duration::from_secs(1), hold: Duration::from_secs(2), apply: Duration::from_secs(3) };
let t0 = Instant::now();
let mut run = Run::new(0, "0", "card-0", plan, 300.0, false, timing, t0);
let mut t = t0;
let mut applied: Vec<Step> = Vec::new();
let mut finished: Option<Row> = None;
let watts_for = |mhz: u32| -> f64 { match mhz { 2781 => 317.9, 2472 => 276.0, _ => 252.4 } };
for _ in 0..200 {
t += Duration::from_millis(500);
let acked = true;
let limit = run.current.as_ref().map(|s| s.watts).unwrap_or(0.0);
// the fake helper: every setting takes; during 2,163's hold the worker reports a mismatched hash
if let Some(cur) = run.current.clone() {
if matches!(run.phase, Phase::Holding { .. }) {
run.sample_rate(136.4);
run.sample_telemetry(watts_for(cur.point.clock_mhz), cur.point.clock_mhz as f64, 13801.0, 60.0);
if cur.point.clock_mhz == 2163 {
run.sample_fault();
}
}
}
for o in run.tick(t, Readback { limit_w: limit, acked }) {
match o {
Out::Apply(s) => applied.push(s),
Out::Finished(r) => finished = Some(r),
Out::Failed(e) => panic!("the run failed: {e}"),
Out::Row(_) => {}
}
}
if finished.is_some() {
break;
}
}
let clocks: Vec<u32> = applied.iter().map(|s| s.point.clock_mhz).collect();
assert_eq!(clocks, vec![2781, 2472, 2163, 2472], "2,781, 2,472, the faulted 2,163, then the reset to the chosen 2,472");
assert_eq!(applied.last().unwrap().kind, Kind::Confirm);
let f = finished.expect("finished");
assert_eq!(f.point.clock_mhz, 2472);
assert_eq!(run.rows.len(), 3);
assert_eq!(run.rows[2].mark, Some(Mark::Faulted));
assert_eq!(run.plan.clock_stop_reason(&run.rows).as_deref(), Some("fingerprint mismatch at 2163 MHz, clocks reset"));
}
}

View file

@ -2727,6 +2727,13 @@ impl Engine {
cc.tune_steps = of;
cc.tune_eta_s = eta;
cc.tune_plan = plan.into();
// the clock search's own step count while a clock step runs (the UI's "locking clocks: step 4 of 9")
if let Some(r) = self.sweep.as_ref() {
let power_steps = r.rows.iter().filter(|x| x.point.clock_mhz == 0 && x.mark.is_some()).count() as u32;
let on_clock = r.current.as_ref().map(|s| s.kind == crate::ember::Kind::Clock).unwrap_or(false);
cc.lock_step = if on_clock { step.saturating_sub(power_steps) } else { 0 };
cc.lock_steps = if on_clock { of.saturating_sub(power_steps) } else { 0 };
}
}
if self.shared.runtime.sweep_only {
// the job playbook forwards this to the installed app's /api/tune-progress
@ -2765,6 +2772,22 @@ impl Engine {
c.tune_source = kind.name().into();
c.tune_line = crate::ember::result_line(kind, row.mhs, row.watts, row.eff);
c.tune_curve = run.rows.iter().map(|r| r.json()).collect();
// 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() };
} else {
let lr = crate::ember::lock_result(&run.plan, &run.rows, &row);
c.lock_mhz = lr.lock_mhz;
c.lock_mhs = lr.lock_mhs;
c.lock_w = lr.lock_w;
c.lock_mhw = lr.lock_mhw;
c.unlocked_mhs = lr.unlocked_mhs;
c.unlocked_w = lr.unlocked_w;
c.lock_at = unix as f64;
c.lock_note = lr.lock_note;
}
c.lock_step = 0;
c.lock_steps = 0;
let control = c.tune_control;
if kind == crate::ember::PlanKind::Baseline {
c.sweep_note = if control { String::new() } else { c.sweep_note.clone() };

View file

@ -135,6 +135,18 @@ pub struct CardState {
// Ember 2: the memory clock the last tune chose and the measured curve (every row of the last plan)
pub tune_mem_mhz: u32,
pub tune_curve: Vec<serde_json::Value>,
// the core-clock knob (7 October 2026, src/ember.rs lock_result; the UI lane's field shape): the chosen lock
// against the cap point's unlocked row, the step while the clock search runs, the moment and the stop reason
pub lock_mhz: u32, // the chosen core clock cap (0 = unlocked)
pub lock_mhs: f64,
pub lock_w: f64,
pub lock_mhw: f64,
pub unlocked_mhs: f64, // the cap point's row the lock is read against
pub unlocked_w: f64,
pub lock_step: u32, // the clock search's step while it runs (0 otherwise)
pub lock_steps: u32,
pub lock_at: f64, // unix s the lock point was taken (0 = never)
pub lock_note: String, // "rate fell 5.1 percent at 1200 MHz", "fingerprint mismatch at 1400 MHz, clocks reset", "the floor at 700 MHz", "no lever"
// the kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
pub variant: String,
pub race_mhs: f64,