igneum/tools/ca3-pc1-amd/pc1-amd-watts-app.ps1

158 lines
12 KiB
PowerShell

# Counter ASIC 3.0, PC 1 AMD job 1a (6 October 2026): item 1's AMD per-joule row, the CONTROL (class v3) with the card
# ON in the installed app, mining the live program as it does now, taken the way main trusts after Ember run 6 (the
# app's path: engine telemetry 363 of 363 nonzero samples, /api/state 456 of 456). READ ONLY: no card is switched, no
# POST is made, nothing of the installed app is touched (rule of 6 October 2026: a script never posts to /api/cards;
# tools/ci/playbook-quit-check.sh rule 2). The candidate row (sh256x27, the card alone) is the separate job
# pc1-amd-watts.ps1 under the runner's `--cards-off`, because the runner switches the card off BEFORE a script starts,
# so a job cannot hold the card ON for this row and OFF for that one.
# What it does: the helper sampler (a second copy of the installed app's igneum-gpu-telemetry.exe at -l 1 through an
# environment-fed wrapper, ended by its own pid in finally) proves itself on 12 idle seconds (first three raw lines, a
# 9070 XT line with watts) or the job fails loudly; then /api/state is polled at 1 Hz for 90 s, the gfx1201 card's
# power_w (the app's own helper at -l 5: the value moves every 5 s) and hash_now (the app's last interval, hash_avg
# beside it), the key read from settings.json: `RESULT APPROW class=v3 mhs_wall= watts_mean= watts_min= watts_max=
# samples= uj=` (printed right after the window, robust to a missing field: what was read is printed); the sampler
# beside it over the same 90 s: `RESULT sampler_vs_app helper_mean= app_mean= ratio=`, the calibration the candidate
# job's row is read with. A `SUMMARY {json}` line ends the job. Read back with `node tools/jobs.mjs <job id>`.
$ErrorActionPreference = 'Continue'
$jobName = 'pc1-amd-watts-app'
$started = Get-Date
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Summary([string] $status, [hashtable] $extra) {
$o = [ordered]@{ job = $jobName; status = $status; duration_s = [int]((Get-Date) - $started).TotalSeconds; finished_at = (Stamp) }
foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }
'SUMMARY ' + ($o | ConvertTo-Json -Compress -Depth 4)
}
$appWindowS = 90
"RESULT start $(Stamp) job=$jobName machine=$env:COMPUTERNAME app_version=$env:IGNEUM_APP_VERSION"
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = Join-Path $env:TEMP 'igneum-ca3-pc1-amd' }; New-Item -ItemType Directory -Force -Path $job | Out-Null
$inst = @("$env:LOCALAPPDATA\Programs\Igneum Miner", "$env:ProgramFiles\Igneum Miner") | Where-Object { Test-Path (Join-Path $_ 'igneum-app.exe') } | Select-Object -First 1
$appDir = if ($env:IGNEUM_APP_DIR) { $env:IGNEUM_APP_DIR } else { Join-Path $env:LOCALAPPDATA 'igneum\app' }
$urlFile = Join-Path $appDir 'app.url'
$base = $null
if (Test-Path $urlFile) { $base = (Get-Content -LiteralPath $urlFile -Raw).Trim().TrimEnd('/') }
"RESULT app install=$inst app_dir=$appDir url_present=$([bool]$base)"
if (-not $base) { "RESULT error no app.url: the control row needs the app's /api/state"; Summary 'failed' @{ error = 'no app.url' }; exit 2 }
# the card's settings.json entries, READ ONLY, for the record
try {
$sj = Get-Content -LiteralPath (Join-Path $appDir 'settings.json') -Raw | ConvertFrom-Json
foreach ($prop in $sj.cards.PSObject.Properties) { if ($prop.Name -like 'amd:*' -and $prop.Name -match 'gfx1201') { "RESULT card settings_key=$($prop.Name) enabled=$([bool] $prop.Value.enabled) identities=$($prop.Value.identities)" } }
} catch { "RESULT settings_read_failed $($_.Exception.Message)" }
function Workers { @(Get-CimInstance Win32_Process -Filter "Name = 'igneum-worker-opencl.exe' OR Name = 'igneum-worker-cuda.exe'" -ErrorAction SilentlyContinue | ForEach-Object { "$($_.Name):$($_.ProcessId):[$($_.CommandLine -replace '\s+', ' ')]" }) }
function StateCard {
try {
$st = Invoke-RestMethod -Uri "$base/api/state" -Method GET -TimeoutSec 5
$cards = $st.mining.cards; if (-not $cards) { $cards = $st.cards }
return @($cards | Where-Object { $_.vendor -eq 'amd' -and ("$($_.key)" -match 'gfx1201') -and $_.state -ne 'off' } | Select-Object -First 1)
} catch { return @() }
}
"RESULT workers_before $(Stamp) $((Workers) -join ' ')"
# ---- the AMD helper: one in-process sample first ----
$tele = $null
if ($inst -and (Test-Path (Join-Path $inst 'igneum-gpu-telemetry.exe'))) { $tele = Join-Path $inst 'igneum-gpu-telemetry.exe' }
if (-not $tele) { $found = Get-ChildItem -Path (Join-Path $appDir 'jobs') -Recurse -Filter 'igneum-gpu-telemetry.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'amd-kit' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($found) { $tele = $found.FullName } }
if (-not $tele) { "RESULT watts error=no igneum-gpu-telemetry.exe in the install folder or under an amd-kit job folder"; Summary 'failed' @{ error = 'no helper' }; exit 2 }
$one = @(& $tele 2>&1 | ForEach-Object { "$_" })
$one | ForEach-Object { "RESULT tele_once $_" }
$line9070 = $one | Where-Object { $_ -match '^amd \d+ .*name "[^"]*9070[^"]*" watts (\S+) ' } | Select-Object -First 1
if (-not $line9070 -or $line9070 -notmatch ' watts ([\d.]+) ') { "RESULT watts error=the helper's one-shot sample has no 9070 XT line with a watts number"; Summary 'failed' @{ error = 'no 9070 watts in the one-shot sample' }; exit 2 }
"RESULT tele helper=$tele sha256=$((Get-FileHash -Algorithm SHA256 $tele).Hash.ToLower()) one_shot_watts=$($Matches[1]) (ADLX GPUPower, GPUTotalBoardPower as the fallback)"
$samples = Join-Path $job 'amd-samples.log'
$sampler = $null
$samplerOk = $false
$appRows = @()
$appT0 = $null; $appT1 = $null
try {
$wrapper = Join-Path $job 'sampler.ps1'
$wrapperText = @'
$exe = $env:IGNEUM_SAMPLER_EXE
$out = $env:IGNEUM_SAMPLER_OUT
Add-Content -LiteralPath $out -Value ('wrapper start ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exe=' + $exe + ' exists=' + (Test-Path -LiteralPath $exe))
& $exe -l 1 2>&1 | ForEach-Object { Add-Content -LiteralPath $out -Value ((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' ' + $_) }
Add-Content -LiteralPath $out -Value ('wrapper end ' + (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fff') + ' exit=' + $LASTEXITCODE)
'@
[IO.File]::WriteAllText($wrapper, $wrapperText, (New-Object System.Text.UTF8Encoding $false))
Remove-Item -LiteralPath $samples -Force -ErrorAction SilentlyContinue
$env:IGNEUM_SAMPLER_EXE = $tele
$env:IGNEUM_SAMPLER_OUT = $samples
$sampler = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ('"' + $wrapper + '"')) -WindowStyle Hidden -PassThru
"RESULT sampler pid=$($sampler.Id) wrapper=$wrapper file=$samples exe_via_env=$tele"
Start-Sleep -Seconds 12
$raw = @(); if (Test-Path -LiteralPath $samples) { $raw = @(Get-Content -LiteralPath $samples) }
$raw | Select-Object -First 3 | ForEach-Object { "RESULT sampler_raw $_" }
$n9070 = @($raw | Where-Object { $_ -match '^\S+ amd \d+ .*name "[^"]*9070[^"]*" watts [\d.]+ ' }).Count
"RESULT sampler_idle lines=$($raw.Count) lines_9070_with_watts=$n9070 sampler_alive=$(-not $sampler.HasExited)"
if ($n9070 -eq 0) {
$why = if ($raw.Count -eq 0) { 'the sampler wrote nothing in 12 s (file absent or empty)' } else { 'the sampler wrote ' + $raw.Count + ' line(s) but none is a 9070 XT line with a watts number (first lines above)' }
"RESULT watts error=$why; no row is taken"
"RESULT APPROW class=v3 error=$why"
} else {
$samplerOk = $true
"RESULT idle $(Stamp) sampler proven on the idle window"
$sc = @(StateCard)
if ($sc.Count -eq 0) { "RESULT APPROW class=v3 error=the app's /api/state lists no mining gfx1201 card (the card is off or the app is not running): this row needs the card ON in the app" }
else {
"RESULT app_card key=$($sc[0].key) identities=$($sc[0].identities) state=$($sc[0].state) pid=$($sc[0].pid) mhs_now=$($sc[0].hash_now) watts=$($sc[0].power_w) (before the window)"
$appT0 = (Get-Date).ToUniversalTime()
"RESULT approw start $(Stamp) window_s=$appWindowS"
for ($i = 0; $i -lt $appWindowS; $i++) {
$c = @(StateCard)
if ($c.Count -gt 0) {
$w = 0.0; $mh = 0.0; $av = 0.0
try { $w = [double] $c[0].power_w } catch { }
try { $mh = [double] $c[0].hash_now } catch { }
try { $av = [double] $c[0].hash_avg } catch { }
$appRows += [pscustomobject]@{ ts = (Get-Date).ToUniversalTime(); w = $w; mhs = $mh; avg = $av; state = "$($c[0].state)" }
}
Start-Sleep -Seconds 1
}
$appT1 = (Get-Date).ToUniversalTime()
"RESULT approw end $(Stamp) polls=$($appRows.Count)"
# the row, printed HERE (the first run of the watts job computed it after the try block and the job died before)
$valid = @($appRows | Where-Object { $_.w -gt 0 })
if ($valid.Count -eq 0) { "RESULT APPROW class=v3 error=no poll carried a watts value above 0 (polls=$($appRows.Count); states=$(($appRows | Select-Object -ExpandProperty state -Unique) -join ','))" }
else {
$m = $valid | Measure-Object -Property w -Average -Minimum -Maximum
$mh = ($valid | Measure-Object -Property mhs -Average).Average
$ma = ($valid | Measure-Object -Property avg -Average).Average
$distinct = ($valid | Select-Object -ExpandProperty w -Unique | Measure-Object).Count
$uj = if ($mh -gt 0) { [math]::Round($m.Average / $mh, 3) } else { 'owed' }
"RESULT APPROW class=v3 mhs_wall=$([math]::Round($mh, 3)) mhs_avg_app=$([math]::Round($ma, 3)) watts_mean=$([math]::Round($m.Average, 1)) watts_min=$($m.Minimum) watts_max=$($m.Maximum) samples=$($valid.Count) polls=$($appRows.Count) distinct_watts=$distinct uj=$uj window_s=$appWindowS source=api/state (the app's own helper at -l 5: the watts value moves every 5 s) states=$(($appRows | Select-Object -ExpandProperty state -Unique) -join ',')"
}
}
}
} finally {
"RESULT finally_begin $(Stamp)"
if ($sampler) { try { & taskkill /T /F /PID $sampler.Id 2>&1 | Out-Null; "RESULT sampler ended pid=$($sampler.Id)" } catch { "RESULT sampler end error=$($_.Exception.Message)" } }
"RESULT workers_after $(Stamp) $((Workers) -join ' ')"
}
# ---- the sampler against the app over the same window ----
$rows = @()
if (Test-Path -LiteralPath $samples) {
foreach ($l in (Get-Content -LiteralPath $samples)) {
if ($l -match '^(\S+) amd (\d+) bus (\S+) kind (\S+) name "([^"]*)" watts (\S+) ') {
if ($Matches[5] -notmatch '9070' -or $Matches[6] -eq '-') { continue }
try { $ts = [datetime]::ParseExact($Matches[1], 'yyyy-MM-ddTHH:mm:ss.fff', [Globalization.CultureInfo]::InvariantCulture) } catch { continue }
$rows += [pscustomobject]@{ ts = $ts; w = [double] $Matches[6] }
}
}
}
"RESULT samples total $($rows.Count) file $samples (9070 XT lines with watts)"
$ratio = $null
$valid = @($appRows | Where-Object { $_.w -gt 0 })
if ($valid.Count -gt 0 -and $appT0 -and $appT1) {
$appMean = ($valid | Measure-Object -Property w -Average).Average
$hin = @($rows | Where-Object { $_.ts -gt $appT0 -and $_.ts -lt $appT1 })
if ($hin.Count -gt 0 -and $appMean -gt 0) {
$helperMean = ($hin | Measure-Object -Property w -Average).Average
$ratio = [math]::Round($helperMean / $appMean, 4)
"RESULT sampler_vs_app helper_mean=$([math]::Round($helperMean, 1)) app_mean=$([math]::Round($appMean, 1)) ratio=$ratio helper_samples=$($hin.Count) app_samples=$($valid.Count) window_s=$appWindowS calibration=$(if ($ratio -ge 0.95 -and $ratio -le 1.05) { 'inside 0.95..1.05' } else { 'OUTSIDE 0.95..1.05' }) (the candidate job pc1-amd-watts reads its helper watts with this ratio beside them)"
} else { "RESULT sampler_vs_app error=no helper samples inside the app window (helper $($hin.Count), app mean $appMean)" }
}
foreach ($r in $rows) { "RESULT sample $($r.ts.ToString('HH:mm:ss')) $($r.w)" }
$status = if (-not $samplerOk) { 'failed' } elseif ($valid.Count -gt 0 -and $null -ne $ratio) { 'done' } elseif ($valid.Count -gt 0) { 'partial' } else { 'failed' }
"RESULT end $(Stamp)"
Summary $status @{ approw = ($valid.Count -gt 0); calibration_ratio = "$ratio"; sampler_proven = $samplerOk }
if ($status -eq 'failed') { exit 1 }
exit 0