The one approved Power control step on PC 1 failed with exit 1 (15:45:21Z, no cap applied, no helper registered): cmd.exe strips the first and last quote of a /c line that starts with one and holds more than two (two NVIDIA cards, or a cap plus the registration script); both launchers now wrap the line in one outer pair (platform::cmd_c_args, host.cpp runElevated), unit-tested; Ember 2: the memory clock set through nvidia-smi -lmc/-rmc (direct and through the helper's lmc/rmc verbs), the goal, the electricity price and the hill-climb switch in Settings and /api/tune/goal, the chosen point's memory clock and the measured curve on the card

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 15:48:14 +00:00
parent 1bd7b687c9
commit fc2ed67df5
9 changed files with 131 additions and 7 deletions

View file

@ -39,6 +39,9 @@ pub struct CardPref {
pub sweep_class: String,
#[serde(default)]
pub sweep_source: String,
/// Ember 2: the memory clock the last tune chose (0 = the driver's default)
#[serde(default)]
pub sweep_mem_mhz: u32,
}
#[derive(Clone, Serialize, Deserialize)]
@ -90,6 +93,15 @@ pub struct Settings {
/// unanswered prompt switches it back off with a notice, no retries.
#[serde(default)]
pub power_control: bool,
/// Ember 2 (6 October 2026): the tune's goal ("efficiency" = most MH per watt within 10% of the top rate,
/// "balanced" = within 1%, "rate" = the fastest point), the electricity price in pence per kWh for the £/day
/// reading on each card, and the hill-climb switch (memory up, core down from the fleet prior; off = the ladders).
#[serde(default = "balanced")]
pub tune_goal: String,
#[serde(default)]
pub power_price_pence: f64,
#[serde(default)]
pub tune_climb: bool,
/// When this install first ran (unix s), for the "first hour after install" sweep.
#[serde(default)]
pub installed_at: u64,
@ -113,13 +125,16 @@ pub struct Settings {
fn one() -> u32 {
1
}
fn balanced() -> String {
"balanced".into()
}
fn yes() -> bool {
true
}
impl Default for Settings {
fn default() -> Settings {
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), power_price_pence: 0.0, tune_climb: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
}
}

View file

@ -75,6 +75,8 @@ pub enum Cmd {
TuneSet(u64, Result<String, String>),
/// POST /api/tune-progress from a measurement engine beside this app: the card's live tune state
TuneProgress(Value),
/// Ember 2: Settings > goal, electricity price, the hill-climb switch
TuneGoal(Option<String>, Option<f64>, Option<bool>),
/// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the
/// prover found a host that was not there when the node started
RestartNode(String),
@ -121,7 +123,7 @@ impl Shared {
st.mining.accepted_total = settings.accepted_total;
st.mining.fee_total = settings.fee_total;
st.address = address_state(&settings, &wallet_path);
st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust };
st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), tune_goal: settings.tune_goal.clone(), power_price_pence: settings.power_price_pence, tune_climb: settings.tune_climb, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust };
st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() };
st.live_page = packaged.live_page.clone();
st.finality.message = "waiting for the miner".into();
@ -1145,6 +1147,28 @@ impl Engine {
}
}
Cmd::TuneProgress(v) => self.tune_progress(&v),
Cmd::TuneGoal(goal, price, climb) => {
{
let mut s = self.shared.settings.lock().unwrap();
if let Some(g) = &goal {
s.tune_goal = g.clone();
}
if let Some(p) = price {
s.power_price_pence = p;
}
if let Some(c) = climb {
s.tune_climb = c;
}
}
self.shared.save_settings();
let s = self.shared.settings.lock().unwrap().clone();
let mut st = self.st();
st.settings.tune_goal = s.tune_goal.clone();
st.settings.power_price_pence = s.power_price_pence;
st.settings.tune_climb = s.tune_climb;
drop(st);
self.shared.event("info", &format!("tune goal: {}{}{}", s.tune_goal, if s.power_price_pence > 0.0 { format!(", electricity {:.1} p/kWh", s.power_price_pence) } else { String::new() }, if s.tune_climb { ", hill-climb on" } else { "" }));
}
Cmd::TuneSet(seq, r) => match r {
Ok(text) => {
self.tune_acked = Some(seq);
@ -2361,11 +2385,19 @@ impl Engine {
let key = crate::ember::prior_key(&c.name, &probe.driver, &c.program_class);
let prior = crate::ember::prior_of(tuning.as_ref(), &key, ember.min_samples);
let full_due = self.tune_full_due.remove(&idx);
let (goal, climb_on) = {
let s = self.shared.settings.lock().unwrap();
(crate::ember::Goal::parse(&s.tune_goal), s.tune_climb)
};
let plan = if let Some(why) = control.as_ref() {
if let Some(cc) = self.st().mining.cards.get_mut(idx) {
cc.sweep_note = why.clone();
}
crate::ember::Plan::baseline(&limits, before, ember.tolerance_pct)
} else if climb_on || prior.as_ref().map(|p| p.point.mem_mhz > 0).unwrap_or(false) {
// Ember 2: the hill-climb from the fleet prior (or the card's point) toward the goal
let start = prior.as_ref().map(|p| p.point).unwrap_or(before);
crate::ember::Plan::climb(&limits, start, goal, ember.tolerance_pct)
} else if let (Some(p), false, 0) = (prior.as_ref(), full_due, c.sweep_pct) {
crate::ember::Plan::confirm(&limits, p.point, before, ember.tolerance_pct)
} else {
@ -2477,6 +2509,7 @@ impl Engine {
let Some(c) = card else { return };
let w = step.watts.round() as u64;
let clock = step.point.clock_mhz;
let mem = step.point.mem_mhz;
let shared = self.shared.clone();
self.tune_acked = None;
match c.vendor.as_str() {
@ -2500,6 +2533,15 @@ impl Engine {
ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset");
text.push(' ');
text.push_str(out.trim());
// Ember 2: the memory clock, locked to one value (`-lmc m,m`) or reset (`-rmc`)
let out = if mem > 0 {
crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-lmc", &format!("{mem},{mem}")]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into())
} else {
crate::detect::run_timeout(std::process::Command::new(&smi).args(["-i", &device, "-rmc"]), None, Duration::from_secs(20)).unwrap_or_else(|| "nvidia-smi did not answer".into())
};
ok &= out.contains("All done") || out.to_ascii_lowercase().contains("clocks set") || out.to_ascii_lowercase().contains("reset") || out.to_ascii_lowercase().contains("not supported");
text.push(' ');
text.push_str(out.trim());
shared.send(Cmd::TuneSet(seq, if ok { Ok(text) } else { Err(text) }));
});
}
@ -2509,7 +2551,7 @@ impl Engine {
return;
}
let dev = device.to_string();
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() });
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n{seq}3 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() });
let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), cmd);
// the helper polls twice a second and nvidia-smi answers within a second or two
std::thread::spawn(move || {
@ -2551,7 +2593,7 @@ impl Engine {
}
_ => {}
}
self.shared.log(&format!("tune: {} MHz, {}% ({w} W) requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct));
self.shared.log(&format!("tune: {} MHz, {}% ({w} W), memory {} requested on device {device} (request {seq})", if clock > 0 { clock.to_string() } else { "unlocked".into() }, step.point.power_pct, if mem > 0 { format!("{mem} MHz") } else { "default".into() }));
}
/// A measurement engine's progress for one card (POST /api/tune-progress, forwarded by the job playbook from the
@ -2704,8 +2746,10 @@ impl Engine {
c.tune_steps = 0;
c.tune_eta_s = 0;
c.tune_clock_mhz = row.point.clock_mhz;
c.tune_mem_mhz = row.point.mem_mhz;
c.tune_source = kind.name().into();
c.tune_line = crate::ember::tuned_line(row.mhs, row.watts, row.eff);
c.tune_curve = run.rows.iter().map(|r| r.json()).collect();
let control = c.tune_control;
if kind == crate::ember::PlanKind::Baseline {
c.sweep_note = if control { String::new() } else { c.sweep_note.clone() };
@ -2714,6 +2758,7 @@ impl Engine {
} else {
c.power_pct = row.point.power_pct;
c.clock_cap_mhz = row.point.clock_mhz;
c.mem_cap_mhz = row.point.mem_mhz;
if c.vendor == "nvidia" {
c.power_limit_w = row.limit;
c.power_applied = true;
@ -2728,6 +2773,7 @@ impl Engine {
e.sweep_watts = row.watts;
e.sweep_mhs = row.mhs;
e.sweep_clock_mhz = row.point.clock_mhz;
e.sweep_mem_mhz = row.point.mem_mhz;
e.sweep_driver = c.driver.clone();
e.sweep_class = c.program_class.clone();
e.sweep_source = kind.name().into();
@ -2738,7 +2784,7 @@ impl Engine {
};
let _ = key;
self.shared.save_settings();
self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" }));
self.sweep_say(&format!("TUNE chosen card={} clock={} cap={} mem={} limit={:.0} watts={:.1} mhs={:.2} eff={:.4} plan={}{}", run.label, row.point.clock_mhz, row.point.power_pct, row.point.mem_mhz, row.limit, row.watts, row.mhs, row.eff, kind.name(), if pinned { " pinned=1" } else { "" }));
let before = run.rows.first().filter(|_| kind == crate::ember::PlanKind::Full).cloned();
let record = crate::ember::record_json(crate::platform::unix_now_f(), &crate::config::fingerprint8(&self.shared.runtime.machine_id), VERSION, crate::manifest::platform_name(), &name, &vendor, &driver, &class, kind, &run.rows, Some(&row), before.as_ref());
self.sweep_say(&format!("TUNE {record}"));

View file

@ -163,6 +163,8 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
c.sweep_at = p.sweep_at as f64;
// Ember Tune (src/ember.rs): the clock cap the last tune chose, its plan, and the row's Tuned line
c.tune_clock_mhz = p.sweep_clock_mhz;
c.tune_mem_mhz = p.sweep_mem_mhz;
c.mem_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_mem_mhz };
c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz };
c.tune_source = p.sweep_source.clone();
if p.sweep_mhs > 0.0 && p.sweep_watts > 0.0 {

View file

@ -430,11 +430,21 @@ pub fn sync_clock() -> Result<String, String> {
/// Runs a command line with administrator rights (one prompt): the NVIDIA power cap needs it on Windows.
/// Blocking; call from a thread.
/// The cmd.exe argument for one elevated line. cmd's documented rule: when the text after /c starts with a quote
/// and holds more than two quotes (two cards, or a cap plus a script), it strips the FIRST and LAST quote and runs
/// the broken remainder (PC 1, 6 October 2026, 15:45Z: the one approved Power control step ran
/// `"...\nvidia-smi.exe" -i 0 -pl 460 & "...\nvidia-smi.exe" -i 1 -pl 160 & "...\powershell.exe" ... -File "...ps1"`,
/// exit 1, no cap applied, no task registered; a single-card PC, two quotes, was fine, which is why PC 2 never
/// showed it). Wrapping the whole line in one outer pair makes cmd strip exactly those.
pub fn cmd_c_args(cmdline: &str) -> String {
format!("/c \"{cmdline}\"")
}
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
#[cfg(windows)]
{
let cmd = tool("cmd").display().to_string();
let mut c = elevated_command(&cmd, &format!("/c {cmdline}"));
let mut c = elevated_command(&cmd, &cmd_c_args(cmdline));
let out = c.output().map_err(|e| e.to_string())?;
if out.status.success() {
Ok(())
@ -511,6 +521,20 @@ pub fn quiet(cmd: &mut Command) -> &mut Command {
cmd
}
#[cfg(test)]
mod cmd_tests {
#[test]
fn an_elevated_line_is_wrapped_so_cmd_keeps_every_inner_quote() {
let line = r#""C:\WINDOWS\System32\nvidia-smi.exe" -i 0 -pl 460 & "C:\WINDOWS\System32\nvidia-smi.exe" -i 1 -pl 160 & "C:\WINDOWS\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -File "C:\x\register-power-task.ps1""#;
let a = super::cmd_c_args(line);
assert!(a.starts_with("/c \"\"C:\\WINDOWS"), "{a}");
assert!(a.ends_with("register-power-task.ps1\"\""), "{a}");
// the inner line is intact between the outer pair
assert_eq!(&a[4..a.len() - 1], line);
assert_eq!(super::cmd_c_args("echo hi"), "/c \"echo hi\"");
}
}
#[cfg(test)]
mod lock_tests {
#[test]

View file

@ -43,6 +43,9 @@ pub enum HelperCmd {
PowerLimit(u64),
ClockCap(u64),
ClockReset,
/// Ember 2: the memory clock locked to one value (`-lmc m,m`), or reset (`-rmc`)
MemClock(u64),
MemReset,
Quit,
Remove,
}
@ -65,6 +68,8 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
[_, "pl", w] if digits(w) => Some((seq, HelperCmd::PowerLimit(w.parse().ok()?))),
[_, "lgc", m] if digits(m) => Some((seq, HelperCmd::ClockCap(m.parse().ok()?))),
[_, "rgc"] => Some((seq, HelperCmd::ClockReset)),
[_, "lmc", m] if digits(m) => Some((seq, HelperCmd::MemClock(m.parse().ok()?))),
[_, "rmc"] => Some((seq, HelperCmd::MemReset)),
[_, "dev", d] if digits(d) => Some((seq, HelperCmd::Dev(d.to_string()))),
_ => None,
}
@ -76,6 +81,8 @@ pub fn smi_args(dev: &str, c: &HelperCmd) -> Option<Vec<String>> {
HelperCmd::PowerLimit(w) => Some(vec!["-i".into(), dev.into(), "-pl".into(), w.to_string()]),
HelperCmd::ClockCap(m) => Some(vec!["-i".into(), dev.into(), "-lgc".into(), format!("0,{m}")]),
HelperCmd::ClockReset => Some(vec!["-i".into(), dev.into(), "-rgc".into()]),
HelperCmd::MemClock(m) => Some(vec!["-i".into(), dev.into(), "-lmc".into(), format!("{m},{m}")]),
HelperCmd::MemReset => Some(vec!["-i".into(), dev.into(), "-rmc".into()]),
_ => None,
}
}
@ -229,6 +236,10 @@ mod tests {
assert_eq!(parse_line("7 pl 460"), Some((7, HelperCmd::PowerLimit(460))));
assert_eq!(parse_line("8 lgc 2472"), Some((8, HelperCmd::ClockCap(2472))));
assert_eq!(parse_line("9 rgc"), Some((9, HelperCmd::ClockReset)));
assert_eq!(parse_line("10 lmc 14001"), Some((10, HelperCmd::MemClock(14001))));
assert_eq!(parse_line("11 rmc"), Some((11, HelperCmd::MemReset)));
assert_eq!(smi_args("0", &HelperCmd::MemClock(14001)).unwrap(), vec!["-i", "0", "-lmc", "14001,14001"]);
assert_eq!(smi_args("0", &HelperCmd::MemReset).unwrap(), vec!["-i", "0", "-rmc"]);
assert_eq!(parse_line("3 dev 1"), Some((3, HelperCmd::Dev("1".into()))));
assert_eq!(parse_line("5 403"), Some((5, HelperCmd::PowerLimit(403))), "the 0.3.9 form");
assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit)));

View file

@ -251,6 +251,14 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
shared.send(Cmd::ApplyCards(choices));
Ok(json!({ "ok": true }))
}
"/api/tune/goal" => {
// Ember 2: the goal, the electricity price (pence per kWh) and the hill-climb switch
let goal = body.get("goal").and_then(|v| v.as_str()).map(|g| crate::ember::Goal::parse(g).name().to_string());
let price = body.get("price_pence").and_then(|v| v.as_f64()).filter(|p| (0.0..=500.0).contains(p));
let climb = body.get("climb").and_then(|v| v.as_bool());
shared.send(Cmd::TuneGoal(goal, price, climb));
Ok(json!({ "ok": true }))
}
"/api/settings" => {
let identities = body.get("identities").and_then(|v| v.as_u64()).map(|v| v.clamp(1, 64) as u32);
let vote = body.get("vote").and_then(|v| v.as_bool());

View file

@ -132,6 +132,9 @@ pub struct CardState {
pub tune_steps: u32,
pub tune_eta_s: i64,
pub tune_plan: String,
// 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 kernel variant race (docs/design/miner-tuning.md): what the worker's last race chose
pub variant: String,
pub race_mhs: f64,
@ -290,6 +293,10 @@ pub struct SettingsState {
/// Ember Tune is paused fleet-wide by the signed manifest's kill switch (tuning.ember.enabled = false)
pub tuning_off: bool,
pub tuning_note: String,
/// Ember 2: the goal (efficiency | balanced | rate), the electricity price in pence per kWh, the hill-climb switch
pub tune_goal: String,
pub power_price_pence: f64,
pub tune_climb: bool,
/// the miner software's dev fee switch (settings; `--dev-fee 0` when off)
pub dev_fee: bool,
/// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`)

View file

@ -375,6 +375,12 @@ while ($true) {
} elseif ($op -eq 'rgc') {
$out = (& $Smi -i $Device -rgc 2>&1 | Out-String).Trim()
"$(Get-Date -Format o) $($p[0]) -rgc : $out" | Out-File -FilePath $log -Append -Encoding utf8
} elseif ($op -eq 'lmc' -and $v -match '^\d+$') {
$out = (& $Smi -i $Device -lmc "$v,$v" 2>&1 | Out-String).Trim()
"$(Get-Date -Format o) $($p[0]) -lmc $v,$v : $out" | Out-File -FilePath $log -Append -Encoding utf8
} elseif ($op -eq 'rmc') {
$out = (& $Smi -i $Device -rmc 2>&1 | Out-String).Trim()
"$(Get-Date -Format o) $($p[0]) -rmc : $out" | Out-File -FilePath $log -Append -Encoding utf8
}
}
}
@ -411,6 +417,8 @@ while true; do
pl) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -pl $v : $("$smi" -i "$dev" -pl "$v" 2>&1)" >> "$dir/helper.log";; esac ;;
lgc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lgc 0,$v : $("$smi" -i "$dev" -lgc "0,$v" 2>&1)" >> "$dir/helper.log";; esac ;;
rgc) echo "$(date -u +%FT%TZ) $1 -rgc : $("$smi" -i "$dev" -rgc 2>&1)" >> "$dir/helper.log" ;;
lmc) case "$v" in ''|*[!0-9]*) ;; *) echo "$(date -u +%FT%TZ) $1 -lmc $v,$v : $("$smi" -i "$dev" -lmc "$v,$v" 2>&1)" >> "$dir/helper.log";; esac ;;
rmc) echo "$(date -u +%FT%TZ) $1 -rmc : $("$smi" -i "$dev" -rmc 2>&1)" >> "$dir/helper.log" ;;
esac
done
fi
@ -614,6 +622,7 @@ mod tests {
for s in [helper_script_windows(), helper_script_unix()] {
assert!(s.contains("cmd.txt") && s.contains("quit") && s.contains("-pl") && s.contains("20 min"));
assert!(s.contains("-lgc") && s.contains("-rgc"), "the clock cap and its reset");
assert!(s.contains("-lmc") && s.contains("-rmc"), "Ember 2: the memory clock and its reset");
}
}
}

View file

@ -112,7 +112,9 @@ static void applyState(const std::string& j) {
// Runs cmd /c <line> as administrator, waits, and answers on the engine's stdin.
static void runElevated(std::wstring line) {
std::thread([line] {
std::wstring params = L"/c " + line;
// cmd strips the first and last quote of a line that starts with one and holds more than two (two cards, or a
// cap plus a script): one outer pair around the whole line is what it may strip (PC 1, 6 October 2026)
std::wstring params = L"/c \"" + line + L"\"";
wchar_t sysdir[MAX_PATH];
GetSystemDirectoryW(sysdir, MAX_PATH);
std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)