diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index c1a9c5d34..b1ff7114b 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -1817,7 +1817,7 @@ impl Engine { // once, ever (src/powertask.rs): a registered task sets the caps with no prompt; the readback judges it if cfg!(windows) && self.power_task_registered() { let pairs: Vec<(String, u64)> = self.st().mining.cards.iter().filter(|c| c.vendor == "nvidia" && c.enabled && c.present() && c.power_default_w > 0.0 && !c.power_applied).map(|c| (c.device.clone(), requested_watts(c).round() as u64)).collect(); - let dir = self.sweep_dir(); + let dir = crate::powertask::helper_dir(); let shared = self.shared.clone(); self.shared.log("power cap: through the Igneum Power Helper task (no prompt)"); std::thread::spawn(move || { @@ -1936,8 +1936,9 @@ impl Engine { } if cfg!(windows) && self.power_task_registered() { // the kill switch: the task unregisters itself (elevated) and exits; nothing is left behind - let dir = self.sweep_dir(); - let _ = std::fs::write(dir.join("cmd.txt"), "remove\n"); + let hdir = crate::powertask::helper_dir(); + let _ = std::fs::create_dir_all(&hdir); + let _ = std::fs::write(hdir.join("cmd.txt"), "remove\n"); match crate::powertask::start() { Ok(()) => self.shared.log("power control off: the Igneum Power Helper task removes itself"), Err(e) => self.shared.log(&format!("power control off: the task could not be started to remove itself ({e}); remove it in Task Scheduler")), @@ -2142,6 +2143,16 @@ impl Engine { self.shared.runtime.app_dir.join("sweep") } + /// Where this engine's helper reads its commands: the task's own folder when the task is the helper, else this + /// engine's sweep folder (the prompted helper script was handed that path on its command line). + fn helper_cmd_dir(&self) -> PathBuf { + if self.sweep_helper_is_task { + crate::powertask::helper_dir() + } else { + self.sweep_dir() + } + } + /// The manifest's tuning object (/tuning.json, written by the updater from the signed manifest), or /// None: the Ember settings (kill switch, thresholds) and the fleet priors live under it. fn tuning_object(&self) -> Option { @@ -2470,8 +2481,11 @@ impl Engine { format!("sh \"{}\" \"{}\" \"{}\" {} {}", script.display(), dir.display(), smi, c.device, restore) }; if cfg!(windows) && self.power_task_registered() { - // once, ever: the registered task is the helper; it reads the same command file, no prompt - let _ = std::fs::write(dir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device)); + // once, ever: the registered task is the helper; it reads ITS command file (powertask::helper_dir, never + // under a scratch IGNEUM_APP_DATA), no prompt + let hdir = crate::powertask::helper_dir(); + let _ = std::fs::create_dir_all(&hdir); + let _ = std::fs::write(hdir.join("cmd.txt"), format!("{} dev {}\n", crate::platform::unix_now() % 1_000_000, c.device)); crate::powertask::start()?; self.shared.log("tune helper: the Igneum Power Helper task (no prompt)"); self.sweep_helper = true; @@ -2491,7 +2505,7 @@ impl Engine { fn sweep_helper_quit(&mut self) { if self.sweep_helper { - let _ = std::fs::write(self.sweep_dir().join("cmd.txt"), "quit\n"); + let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), "quit\n"); if self.sweep_helper_is_task { // the task exits on quit and reports nothing back; the next tune starts it again self.sweep_helper = false; @@ -2553,7 +2567,7 @@ impl Engine { } let dev = device.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); + let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), cmd); // the helper polls twice a second and nvidia-smi answers within a second or two std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(4)); diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index 809453382..670837ce0 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -58,7 +58,7 @@ fn main() { if args.iter().any(|a| a == "--power-helper") { // the scheduled task's action (src/powertask.rs): elevated, runs only digit-argument nvidia-smi commands // from /app/sweep/cmd.txt, exits on quit, remove or 20 idle minutes - let dir = powertask::sweep_dir(&platform::data_root().join("app")); + let dir = powertask::helper_dir(); std::process::exit(powertask::run_helper(&dir)); } if args.iter().any(|a| a == "--launch") { diff --git a/app/igneum-app/src/platform.rs b/app/igneum-app/src/platform.rs index 01f288c6d..55bfd2fae 100644 --- a/app/igneum-app/src/platform.rs +++ b/app/igneum-app/src/platform.rs @@ -103,6 +103,14 @@ pub fn data_root() -> PathBuf { if let Some(p) = std::env::var_os("IGNEUM_APP_DATA") { return PathBuf::from(p); } + fixed_data_root() +} + +/// The platform's own root, with IGNEUM_APP_DATA ignored: where the Power Helper task (started by Windows with no +/// environment of ours) reads its command file, so a measurement engine under a scratch root (IGNEUM_APP_DATA set) +/// writes its task commands here and not under its own root. 6 October 2026: found while planning the unelevated +/// climb through the task; the elevated runs never crossed it (they set the limits directly). +pub fn fixed_data_root() -> PathBuf { #[cfg(target_os = "macos")] { home().join("Library").join("Application Support").join("Igneum") diff --git a/app/igneum-app/src/powertask.rs b/app/igneum-app/src/powertask.rs index 6ccc412d4..2db95bb00 100644 --- a/app/igneum-app/src/powertask.rs +++ b/app/igneum-app/src/powertask.rs @@ -272,6 +272,12 @@ pub fn sweep_dir(app_dir: &Path) -> PathBuf { app_dir.join("sweep") } +/// The one folder the task's helper reads: `/app/sweep` (never under IGNEUM_APP_DATA). Every +/// engine that talks to the task writes its command file here; the `--power-helper` entry reads here. +pub fn helper_dir() -> PathBuf { + sweep_dir(&crate::platform::fixed_data_root().join("app")) +} + #[cfg(test)] mod tests { use super::*; @@ -293,6 +299,9 @@ mod tests { assert_eq!(parse_line("12 reregister C:\\evil.exe"), None, "no path argument: the helper picks the install folder itself"); assert_eq!(smi_args("0", &HelperCmd::Reregister), None); assert!(readback_command().contains("Actions[0].Execute")); + // the helper's folder never follows IGNEUM_APP_DATA (the task has no environment of ours) + assert!(helper_dir().ends_with(Path::new("app").join("sweep"))); + assert!(helper_dir().starts_with(crate::platform::fixed_data_root())); // nothing else: no shell, no path, no string argument, no oversized number for bad in ["7 pl 460; calc", "7 pl -460", "7 pl 4.60", "7 lgc 0,2472", "7 rm C:\\x", "x pl 460", "7 pl", "7 lgc 12345678", "7 dev ../1", "", "7 pl 460 extra"] { assert_eq!(parse_line(bad), None, "{bad:?}"); diff --git a/docs/plans/ember-tune.md b/docs/plans/ember-tune.md index c2d4c0aa0..6e437cd0e 100644 --- a/docs/plans/ember-tune.md +++ b/docs/plans/ember-tune.md @@ -302,6 +302,7 @@ For the 0.3.14 cut (6 October 2026, after run 6), on ember-tune: | Commit | What | Where | |---|---|---| +| (this cut) | every task command goes to the helper's fixed folder (`powertask::helper_dir` = `/app/sweep`, IGNEUM_APP_DATA ignored): a scratch-root measurement engine talking to the task wrote under its own root, where the task never reads (found 6 October 2026 planning the unelevated climb; the elevated runs set limits directly and never crossed it) | platform.rs `fixed_data_root`, powertask.rs, engine.rs `helper_cmd_dir`, main.rs + test | | (this cut) | a baseline result's row reads "Measured: ..." not "Tuned: ..." (`ember::result_line`); PC 1's 0.3.13 rows read "Tuned: 114.2 MH/s at 305 W" for a baseline | ember.rs, engine.rs + test | | bd7fcf4 | AMD: a power step with no limit readback is applied on the acknowledgement (run 6 aborted the 9070 XT's ladder at step 1 on "card reports 0 W") | ember.rs `Run::applied` + test | | 200362a | `task_exe` prefers the installed exe for every path that is not itself an install candidate; the helper's `reregister` verb; the clock ladder and floor to 45%; a chosen point on the floor says so; the fleet page's team rows | powertask.rs, ember.rs, engine.rs, site | diff --git a/relay/playbooks/ember-installed-tune-pc1.ps1 b/relay/playbooks/ember-installed-tune-pc1.ps1 new file mode 100644 index 000000000..4426ef81c --- /dev/null +++ b/relay/playbooks/ember-installed-tune-pc1.ps1 @@ -0,0 +1,68 @@ +# Ember: the SHIPPED app tunes itself (6 October 2026). Unelevated, no kit, no second engine, no --stop-miners: +# the installed app (0.3.14 or later: Ember Tune, the Power Helper task, /api/tune/goal, the climb) runs one tune +# per card through the registered task (no prompt) while it mines. This script only asks and watches: +# POST api/tune/goal (the climb on, balanced) and POST api/sweep/start per card, then reads api/state and the app's +# own log for the TUNE rows. It never quits, pauses or resumes the installed app (the 5 October rule) and never +# touches the cards itself. At the end the climb switch goes back to what it was. +# Order: the AMD card's ladder first (the ADLX apply-on-ack fix, bd7fcf4), then each NVIDIA card's climb. +$ErrorActionPreference = 'Continue' +function Say([string] $s) { Write-Output ('[' + (Get-Date -Format 'HH:mm:ss') + '] ' + $s) } +$appDir = Join-Path $env:LOCALAPPDATA 'igneum\app' +$u = Join-Path $appDir 'app.url' +if (-not (Test-Path -LiteralPath $u)) { Write-Output 'RESULT TUNE error=no_app_url (installed app not running)'; exit 1 } +$base = (Get-Content -LiteralPath $u -Raw).Trim() +function State() { try { return ((Invoke-WebRequest -Uri ($base + 'api/state') -UseBasicParsing -TimeoutSec 5).Content | ConvertFrom-Json) } catch { return $null } } +function Post([string] $path, [string] $json) { try { Invoke-WebRequest -Uri ($base + $path) -Method POST -Body $json -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null; return $true } catch { Say ($path + ': ' + $_.Exception.Message); return $false } } +$s = State +if (-not $s) { Write-Output 'RESULT TUNE error=state_unreadable'; exit 1 } +$ver = [string]$s.version +Write-Output ('RESULT TUNE installed app ' + $ver + ' power_control=' + $s.settings.power_control + ' tuning_off=' + $s.settings.tuning_off + ' climb_before=' + $s.settings.tune_climb + ' goal_before=' + $s.settings.tune_goal) +$t = Get-ScheduledTask -TaskName 'Igneum Power Helper' -ErrorAction SilentlyContinue +Write-Output ('RESULT TUNE task ' + $(if ($t) { $t.Actions[0].Execute + ' state=' + $t.State } else { 'NOT REGISTERED' })) +if (-not $t) { Write-Output 'RESULT TUNE error=task_not_registered (the NVIDIA climbs would need a prompt; stopping)'; exit 1 } +if (-not $s.settings.power_control) { Write-Output 'RESULT TUNE error=power_control_off'; exit 1 } +$climbBefore = [bool]$s.settings.tune_climb +$cards = @($s.mining.cards | Where-Object { $_.enabled -and ($_.vendor -eq 'nvidia' -or $_.vendor -eq 'amd') -and $_.name -notmatch 'Radeon\(TM\) Graphics' }) +Write-Output ('RESULT TUNE cards ' + (($cards | ForEach-Object { $_.key + ' (' + $_.name + ', ' + $_.vendor + ', sweep_state ' + $_.sweep_state + ', line "' + $_.tune_line + '")' }) -join ' | ')) +foreach ($c in $cards) { Write-Output ('RESULT TUNE before key=' + $c.key + ' power_w=' + $c.power_w + ' power_pct=' + $c.power_pct + ' clock_cap=' + $c.clock_cap_mhz + ' mhs=' + $c.mhs + ' line=' + $c.tune_line) } +# the climb on (NVIDIA cards; an AMD card has no clock knob and runs its ladder regardless) +if (-not (Post 'api/tune/goal' '{"goal":"balanced","climb":true}')) { Write-Output 'RESULT TUNE error=tune_goal_refused'; exit 1 } +Start-Sleep -Seconds 2 +$order = @($cards | Where-Object { $_.vendor -eq 'amd' }) + @($cards | Where-Object { $_.vendor -eq 'nvidia' }) +$deadline = (Get-Date).AddMinutes(27) +$logs = Join-Path $appDir 'logs' +function LogMark() { $l = Get-ChildItem -Path $logs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($l) { return @{ f = $l.FullName; n = (Get-Content -LiteralPath $l.FullName).Count } } else { return @{ f = ''; n = 0 } } } +function LogSince($m) { if (-not $m.f) { return @() }; $all = @(Get-Content -LiteralPath $m.f -ErrorAction SilentlyContinue); if ($all.Count -le $m.n) { return @() }; return @($all[$m.n..($all.Count - 1)]) } +$rows = 0 +foreach ($c in $order) { + if ((Get-Date) -gt $deadline) { Say 'budget spent before ' + $c.key; break } + $m = LogMark + $body = '{"key":"' + ($c.key -replace '"', '\"') + '"}' + if (-not (Post 'api/sweep/start' $body)) { Write-Output ('RESULT TUNE error=sweep_start_refused key=' + $c.key); continue } + Say ('tune asked for ' + $c.key) + $seen = 0; $done = $false; $started = Get-Date + while (-not $done -and (Get-Date) -lt $deadline) { + Start-Sleep -Seconds 10 + $new = @(LogSince $m) + while ($seen -lt $new.Count) { $l = [string]$new[$seen]; $seen++; if ($l -match 'TUNE (card=|chosen|start|aborted|progress)') { if ($l -notmatch 'TUNE progress') { Write-Output ('RESULT ' + ($l -replace '^.*?(TUNE )', 'TUNE ')) }; if ($l -match 'TUNE card=') { $rows++ } } } + $st = State + if ($st) { + $cc = $st.mining.cards | Where-Object { $_.key -eq $c.key } | Select-Object -First 1 + if ($cc) { + $running = ($cc.sweep_state -eq 'running') + if (-not $running -and ((Get-Date) - $started).TotalSeconds -gt 180) { $done = $true } + if ($running -and $cc.tune_steps -gt 0 -and ($seen % 6 -eq 0)) { Say ($c.key + ': step ' + $cc.tune_step + ' of ' + $cc.tune_steps + ', ' + $cc.sweep_note) } + } + } + } + $st = State + if ($st) { $cc = $st.mining.cards | Where-Object { $_.key -eq $c.key } | Select-Object -First 1; if ($cc) { Write-Output ('RESULT TUNE after key=' + $c.key + ' power_w=' + $cc.power_w + ' power_pct=' + $cc.power_pct + ' clock_cap=' + $cc.clock_cap_mhz + ' line=' + $cc.tune_line + ' source=' + $cc.tune_source + ' note=' + $cc.sweep_note); if ($cc.tune_curve) { Write-Output ('RESULT TUNE curve key=' + $c.key + ' ' + (($cc.tune_curve | ForEach-Object { ($_.clock_mhz) + 'MHz/' + ($_.mem_mhz) + 'mem/' + ($_.power_pct) + '%:' + $_.mhs + 'MH/s ' + $_.watts + 'W ' + $_.eff + ' ' + $_.mark }) -join ' | ')) } } } +} +# the climb switch back to what it was +Post 'api/tune/goal' ('{"climb":' + $(if ($climbBefore) { 'true' } else { 'false' }) + '}') | Out-Null +Write-Output ('RESULT TUNE climb_restored=' + $climbBefore + ' rows=' + $rows) +$smi = Join-Path $env:ProgramFiles 'NVIDIA Corporation\NVSMI\nvidia-smi.exe' +if (-not (Test-Path $smi)) { $smi = Join-Path $env:SystemRoot 'System32\nvidia-smi.exe' } +if (Test-Path $smi) { Write-Output ('RESULT TUNE nvidia_now ' + ((& $smi --query-gpu=index,name,power.draw,power.limit,clocks.gr,clocks.mem --format=csv,noheader 2>&1 | Out-String) -replace "`r?`n", ' | ')) } +if ($rows -eq 0) { Write-Output 'RESULT TUNE error=no_rows'; exit 1 } +exit 0