Skip to content

Automatic Test Mode: run until every core has a final value, persist per-core results, continue after resume (#106) - #182

Open
kristofferkhansen wants to merge 5 commits into
sp00n:masterfrom
kristofferkhansen:automode-per-core-results
Open

Automatic Test Mode: run until every core has a final value, persist per-core results, continue after resume (#106)#182
kristofferkhansen wants to merge 5 commits into
sp00n:masterfrom
kristofferkhansen:automode-per-core-results

Conversation

@kristofferkhansen

Copy link
Copy Markdown

Automatic Test Mode: run until every core has a final value, persist per-core results, continue after resume (#106)

Replaces #181 — same branch, plus fixes for three issues found during live testing on a Ryzen 9 9950X3D (details at the bottom).

Problem

Running the Automatic Test Mode (e.g. configs/Ryzen.AutomaticTestMode.Start.ini, startValues = Minimum) on a modern Ryzen frequently ends after effectively testing only the first core, reporting a Curve Optimizer value for that core alone. Two independent defects cause this:

  1. Every crash restarts the whole run (this is Enhancement: Continue with the remaining core test order after automatic resume #106). The .automode file only stores lastCoreTested + voltageValues — there is no record of which cores already finished. After each reboot the script rebuilds the full core order, restarts at iteration 1, and prepends the crashed core. With an aggressive start value that hard-freezes the machine (common at -50 on Zen 4/5), the run spends its whole life stepping the first core from -50 upward and never reaches the others.
  2. The only ATM "done" abort counts duplicates against a unique count. $coresWithErrorAndMaxVoltageValue is appended without a uniqueness check from both the in-loop error handler and the RESUME handler, then its .Count is compared to $numUniqueAvailableCores (and the compared value is stale — only refreshed inside the core loop). A single core pinned at maxValue across several crash/resume cycles inflates the counter until the script announces "All Cores have reached the maximum Curve Optimizer value and thrown an error, aborting!" while 15 cores were never tested.

On top of that, the finally block deletes .automode on every exit, so any values that were found only ever existed in the console/log output.

What this PR does

One new concept: a per-core state machine ($coreStates: pending → testing → confirmed / unstable, plus ignored), persisted across crashes, which drives all ATM scheduling:

  • A core is confirmed after passesToConfirmCoreValue (new setting, default 3) consecutive error-free test runs at an unchanged value. Any error charged to the core bumps its value by incrementBy and resets the counter.
  • A core is finished before the next one starts (repeatCoreUntilConfirmed, new setting, default 1): after a successful test run the core is re-inserted at the front of the test order — the same mechanism repeatCoreOnError already uses after an error — so its confirmation passes are collected back-to-back instead of one per full sweep of the core order. Aborting a run early therefore still leaves you with final values for the cores tested so far. Set to 0 to collect passes round-robin across the order instead (the Intel example config does, since a single voltage offset is shared by all cores there).
  • A confirmed core is never tested again — not in later iterations, not after a crash/resume.
  • Results are persisted immediately: the moment a core is confirmed (or terminally unstable), a timestamped line is appended to a permanent, human-readable results file next to the log — it survives crashes, reboots and later runs.
  • The run ends when every core is resolved (confirmed / user-provided / terminally unstable at maxValue). In ATM, maxIterations no longer governs the loop (its default of 10000 made it meaningless there); classic-mode semantics are unchanged. Termination is provable: values increase monotonically toward maxValue, and an internal sanity limit catches pathological cores.
  • Resume continues the order (Enhancement: Continue with the remaining core test order after automatic resume #106): .automode now stores per-core states, the current iteration and the remaining core order (written atomically, previous generation kept as .automode-bak until the next good write). After a crash the crashed core is retested first (with its value bumped), then the order continues exactly where it left off; resolved cores are skipped.
  • knownGoodValues (new setting, e.g. knownGoodValues = 3:-25, 7:-18): declare values you already trust from a previous run. Those cores are seeded as confirmed (source = config), never tested, and marked as user-provided in the summary and results file. The results file ends with a ready-to-paste knownGoodValues line. Fatal error on Intel (per-core values are an AMD concept).
  • Crash-window hardening: after a resume, the crashed core's adjusted value is persisted before any voltage is applied to the CPU (previously the crash-causing value could be re-applied first with setVoltageOnlyForTestedCore = 0); terminal results are merged back from the results file on resume, so a torn state write can never lead to retesting a confirmed core; a persisted no-progress boot counter aborts a pathological crash-reboot loop after ~10 attempts (new exit code 7) and cleanly removes the scheduled task; the startup helper falls back to the backup state generation on any primary read/validation failure.
  • The end-of-run summary gains a per-core status row (OK / CFG / MAX / skipped) and the banner/exit code now reflects the actual outcome (no success banner while cores are unresolved).

WHEA errors without a usable APIC ID are no longer attributed to core 0

Convert-WheaMessageToApicId returns a negative value for any WHEA entry it cannot parse (any event id other than 18/19 — e.g. id 47, "corrected machine check", component Memory — or an entry without a Properties member). Convert-WheaMessageToCoreId fed that straight into the $coresInfo['apicIdToCore'] hash table; a missing key returns $null, and [Int] $null is 0, so every unattributable WHEA entry was silently reported as core 0. With treatWheaWarningAsError = 1 that meant a false error against core 0 whenever it was under test, and a silent "does not match the tested core" warning (APIC ID -1) for every other core — the test run then still counted as clean. The same applied to valid APIC IDs that are not in the map (possible on parts whose APIC IDs have gaps, e.g. a 5900X). Such entries are now treated as an error for the currently tested core — since no core can be ruled out, letting them pass silently would allow an unstable core to be confirmed — and the APIC ID / core are displayed as "not available" instead of a wrong number. (Found live: an event id 47 fired mid-test and was waved through.)

Fixed along the way

  • The alternate core order builder produced duplicate/incomplete orders for odd physical-core counts (e.g. 5 cores → 0,2,4,2,4, cores 1 and 3 never tested — also affects classic mode). It now covers all cores for any count; even counts produce the exact same order as before.
  • Split-Path -LiteralPath ... -Leaf cannot bind to any parameter set (LiteralPathSet has no -Leaf) and throws "Parameter set cannot be resolved" at runtime. Both call sites were on the crash-resume path only: one aborted every resume with a fatal error (which also deleted the .automode state), the other silently prevented re-using the previous run's log file. Both now use [System.IO.Path]::GetFileName(). (Found live: the first actual crash-resume on the 9950X3D died on this.)
  • The duplicate-accepting ArrayList adds ($coresWithErrorAndMaxVoltageValue, $coresWithIncreasedVoltageValue) are now guarded.
  • The stress-test-log shrink guard in Get-NewLogfileEntries no longer permanently disables pass detection if the log file is recreated smaller after a stress-program restart.
  • corepairs resume: $_ used outside a pipeline (always $null under Set-StrictMode 3.0).

New settings ([AutomaticTestMode])

Setting Default Meaning
passesToConfirmCoreValue 3 Consecutive error-free test runs at an unchanged value before a core counts as good
repeatCoreUntilConfirmed 1 Stay on a core until it has a final value; 0 collects passes round-robin across the core order
knownGoodValues (empty) Per-core values to trust without testing, e.g. 3:-25, 7:-18

All are documented in configs/default.config.ini and readme.txt; the ATM example configs surface them.

Compatibility

  • Classic (non-ATM) mode: control flow byte-for-byte unchanged (maxIterations, error handling, summary).
  • Intel automatic voltage mode: unchanged except it now benefits from the same state machine; knownGoodValues is rejected with a clear fatal error; the Intel example config sets repeatCoreUntilConfirmed = 0 (one shared voltage offset — a core confirmed early would keep a value a later core then adjusts).
  • Existing ATM configs keep working; the behavior change (run until all cores resolved, resume continues the order) is the bug fix itself.
  • The WHEA change makes treatWheaWarningAsError = 1 strictly more conservative (entries that previously slipped through as core-0 mismatches now count); set it to 0 to keep WHEA entries advisory.

Validation

  • Syntax: [System.Management.Automation.Language.Parser]::ParseFile clean on both changed scripts.
  • A 130+ assertion harness (extracted real functions run under pwsh with StrictMode 3.0) covers the state machine, persistence/atomic-write/recovery paths, order builders (n=1..24 incl. coresToIgnore), the banner/exit-code matrix, the resume flow incl. crash-attribution ordering, and the torn-save/resume-merge scenarios. Additional suites: WHEA attribution (16 assertions, incl. the live event id 47 case and a 5900X-style APIC gap), core scheduling with repeatCoreUntilConfirmed on/off (20 assertions simulating the real loop mechanics incl. errors and duplicate orders), and the resume-only branches of the results-file initialization (23 assertions).
  • A parameter-set sweep over all ~2,600 command invocations in the script verifies no cmdlet call uses a parameter combination that fits no parameter set (the Split-Path failure class); it flags the two old call sites on the pre-fix file and nothing on the fixed one.
  • Multiple independent adversarial review passes on the full diff (including an external reviewer), each followed by a fix + re-verification round.
  • Live on a Ryzen 9 9950X3D: partial runs incl. knownGoodValues seeding confirmed working; the crash-resume cycle is what surfaced the Split-Path and WHEA findings above. A full end-to-end run is still outstanding.

Closes #106.

🤖 Generated with Claude Code

kristofferkhansen and others added 5 commits July 28, 2026 11:58
…per-core results, continue after resume (sp00n#106)

- Introduce a per-core state machine (pending/testing/confirmed/unstable/ignored)
  that drives all Automatic Test Mode scheduling; a core is confirmed after
  passesToConfirmCoreValue (new setting, default 3) consecutive error-free test
  runs at an unchanged value and is then never tested again, in-run or after a
  crash/resume
- Append confirmed/unstable results immediately to a permanent, human-readable
  results file that survives crashes and reboots
- In Automatic Test Mode the run now loops until every non-ignored core is
  resolved; maxIterations no longer governs the ATM loop (classic mode
  unchanged); an internal sanity limit catches pathological cores
- Persist per-core states, iteration and the remaining core order in the
  .automode file (atomic writes, previous generation kept as backup) so an
  automatic resume continues the order where it left off instead of restarting
  from the beginning (fixes sp00n#106)
- Add knownGoodValues setting to declare trusted per-core values that are never
  tested and reported as user-provided (AMD only)
- Fix the false "All Cores have reached the maximum Curve Optimizer value"
  abort: duplicate-accepting error lists were counted against a de-duplicated
  core count
- Harden the crash/resume machinery: the adjusted value of a crashed core is
  persisted before any voltage is applied to the CPU after a resume; terminal
  results from the results file are merged back on resume so a torn state
  write can never cause a confirmed core to be retested; a no-progress boot
  counter aborts a pathological crash-reboot loop (new exit code 7); the
  startup helper falls back to the backup state generation on any primary
  read/validation failure
- Fix the alternate core order builder producing duplicate/incomplete orders
  for odd physical-core counts (even counts unchanged)
- Fix pass detection dying permanently when a stress-test log is recreated
  smaller after a program restart; fix corepairs resume using $_ outside a
  pipeline
- Per-core status row (OK/CFG/MAX) in the final summary; banner and exit code
  now reflect the actual outcome
- Document everything in default.config.ini, the ATM example configs and
  readme.txt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Convert-WheaMessageToApicId returns a negative value when the APIC ID
cannot be determined: -1 for every event id other than 18 and 19, -2 when
the entry has no Properties member at all. Convert-WheaMessageToCoreId
then passed that value straight into the $coresInfo['apicIdToCore'] hash
table. A key that doesn't exist returns $null, and [Int] $null is 0, so
every WHEA entry that cannot be attributed to a core was silently
reported as core 0.

With treatWheaWarningAsError = 1 that had two consequences:

- While core 0 was being tested, an unrelated WHEA entry (e.g. event id
  47, "corrected machine check", component "Memory") was counted as a
  real error for core 0.
- While any other core was being tested, the same entry printed
  "the APIC ID from the WHEA message does not match the tested core"
  with an APIC ID of -1 and the test continued as if nothing had
  happened.

The same happened for a valid APIC ID that isn't in the map, which can
occur on processors whose APIC IDs have a gap (e.g. a Ryzen 5900X).

Convert-WheaMessageToCoreId now returns -1 in these cases instead, and
the WHEA check treats an entry without a usable core as an error for the
tested core, because it cannot be ruled out that it caused it. A negative
APIC ID / core is displayed as "not available" instead of as a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntil it has a final value

Until now a core collected one confirmation pass per iteration, so with
the default passesToConfirmCoreValue = 3 the whole test order had to be
cycled through three times before the first core could be confirmed. On a
16 core processor that means the run has no final value for any core for
a long time, and aborting it early leaves you with nothing.

The new repeatCoreUntilConfirmed setting (default 1) re-inserts a core at
the front of the test order after a successful test run, the same way
repeatCoreOnError already does after an error, so the core is tested
again right away. A core is therefore finished before the next one in
line is started, and its value is written to the results file at that
point. Setting it to 0 restores the previous behavior.

The order in which the cores are selected is unchanged, it still comes
from coreTestOrder. The loop index is decremented together with the
re-insertion, so the number of test slots per iteration is unaffected and
the persisted remaining core order stays correct for a resume.

For Intel there is only a single voltage offset for all of the cores, so
a core that is confirmed early keeps the value it was confirmed with even
if a later core makes the shared value less aggressive. The Intel example
config therefore sets this to 0 and the setting comment explains why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iteralPath and -Leaf

Split-Path's LiteralPathSet consists of -LiteralPath, -Resolve and
-Credential only. Combining -LiteralPath with -Leaf therefore cannot be
bound to any parameter set and throws

    Parameter set cannot be resolved using the specified named parameters

Both call sites are only reached when resuming after a crash, which is
why this was not noticed earlier:

- Initialize-AutoModeResultsFile takes the else branch only when the
  results file path was restored from the .automode file. It is not
  inside a try/catch, so this aborted the whole run with a fatal error
  on every single resume, which also removed the .automode file.
- The log file re-use in Import-Settings is inside a try/catch, so it
  only lost the ability to continue writing to the log file of the
  interrupted run and silently started a new one.

Both now use [System.IO.Path]::GetFileName(), which is a pure string
operation with no parameter sets and no wildcard handling, so it also
works for paths containing characters that Split-Path -Path would
interpret as wildcards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kristofferkhansen

Copy link
Copy Markdown
Author

I have ran this from start to finish twice now on my 9950@X3D, seems to work exactly as intended.

privacywhen added a commit to privacywhen/AutoCoreCycler that referenced this pull request Aug 15, 2026
Add the Phase 2 foundation for opt-in descending Curve Optimizer
discovery while preserving legacy Automatic Test Mode behavior.

- Add pure discovery state and evidence-transition helpers.
- Enforce candidate-attempt and stage-attempt identity separation.
- Add strict snapshot serialization and all-or-nothing recovery.
- Reject stale, torn, missing-core, and unexpected-core snapshots.
- Persist optional discovery snapshots through the existing atomic .automode
  temp-write and backup-generation mechanism.
- Keep discovery state separate from legacy ATM core states and confirmation
  semantics.
- Preserve legacy .automode output when no discovery state is present.
- Add focused Windows PowerShell 5.1 / Pester 3.4 synthetic coverage for
  transitions, persistence, stale recovery, torn recovery, and backup fallback.
- Document the boundary between reusable PR sp00n#182/CoreCycler infrastructure
  and the unchanged legacy uphill ATM policy.

Verification:
- 17 focused tests passed
- 0 failed
- PowerShell parser errors: 0
- git diff --check: passed
- No workload, CO write, UAC, or hardware execution performed

Additionally:
- docs: add generated AutoCoreCycler architecture wiki
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: Continue with the remaining core test order after automatic resume

1 participant