Windows: fix single-AMD-GPU hosts reading as "gpu none" and looping the installer - #8461
Conversation
…he installer
setup.ps1's WMI fallback built its adapter list with
$wmiGpus = if ($healthyGpus.Count -gt 0) { $healthyGpus } else { $amdGpus }
An unwrapped one-element branch unrolls to a bare WMI object on its way out of the
if, and a bare WMI object has no .Count in PS 5.1 (a string or hashtable does), so
`if ($wmiGpus.Count -gt 0)` never fired on a host with exactly one AMD adapter and
$ROCmGpuLabel stayed null. Setup reported "gpu none (chat-only / GGUF)", skipped the
name to gfx inference, and the stale-venv check then expected cpu torch against the
ROCm wheels install.ps1 had just placed:
Stale venv detected (torch rocm != required cpu).
[ERROR] The existing Unsloth environment needs repair.
The installer rolled back and the desktop app retried the same failure indefinitely.
Reproduced on a Radeon 8060S (gfx1151) Strix Halo laptop, where install.ps1 resolved
gfx1151 and setup.ps1 one second later saw no GPU at all.
Wrap the whole if in @(), the idiom the Intel scan below already documents.
install.ps1 also now exports the arch it resolved before invoking setup.ps1, so the
two never re-derive it independently. Setting UNSLOTH_ROCM_GFX_ARCH by hand was the
workaround for this bug, and doing it in the installer keeps any future divergence
between the two scans from turning into an unrecoverable rollback loop.
…g bug in the name list
Follow-up to the single-AMD-GPU scan fix in this branch, from reviewing what it
could do to hosts other than the one it was reported on.
install.ps1 forwarded its resolved arch as UNSLOTH_ROCM_GFX_ARCH. That name is
the documented operator override, and install_llama_prebuilt.py reads it back as
_manual to decide whether a forwarded --rocm-gfx outranks its own probe, so
publishing an auto-detected value there disarmed that safeguard. install.ps1's
scan is also the weaker of the two: it takes the first AMD adapter with no
visible-device mask and no shadowing-iGPU repick, both of which setup.ps1
applies. On a 780M + RX 9070 XT host setup resolves gfx1201 today; the forward
made it take the installer's gfx1103 verbatim and hand llama.cpp the iGPU
bundle. It was also never restored, so it outlived the install in the caller's
shell on the documented irm | iex path.
It now travels as _UNSLOTH_ROCM_GFX_ARCH_HANDOFF, matching the _UNSLOTH_ prefix
the neighbouring handoffs use, saved and restored in the same finally block, and
consumed by setup.ps1 only after its own probes and inference come up empty.
setup.ps1's gpu name list had the same unwrapped if as the adapter scan: it
wraps each branch but not the if, so a single adapter name unrolls to a bare
String and $gpuNames[$nameIdx] indexes the name and yields "A". The
$nameArches[0] rescue covers that unless a visible-device mask is set, so a
pinned single-GPU host still inferred no arch and looped the same way. Audited
every '= if (' site across the .ps1 files; this was the only other one.
The tests asserted on .Count, which pwsh answers as 1 for a scalar because
PowerShell/PowerShell#5745 added that fallback in 6.1 and Windows PowerShell 5.1
never got it, so they passed against the unfixed source and guarded nothing.
They now assert the shape of the value, re-run each case against stubs carrying
Count = $null to reproduce 5.1's behaviour, cover the mask and multi-adapter
paths and the handoff lifecycle, and pin their own failure against the merge
base.
for more information, see https://pre-commit.ci
The check resolved its pre-fix source through git merge-base against main. That holds only until this merges: after it, the merge base is a commit that already carries the fix, so the fixed source goes in as the before case and the three assertions fail on every host with pwsh. Reaching for an older revision at all also breaks in a shallow CI clone. It now undoes just the two @() wraps in the shipped source in memory, which is immutable, needs no git, and isolates the one thing under test since everything else about the two sources is identical by construction. Verified both ways: fed the fixed source in as before, the old assertion fails exactly as predicted; with the wraps undone it reports scalar and no label as it should.
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The inference above deliberately leaves $pickedName unset when a mask is set and the selected adapter's name is not in the table, rather than borrowing a peer's arch. The handoff then took it anyway: install.ps1 scans without the masks and forwards the FIRST recognized adapter, so a host masking an unknown discrete card while a 780M is listed first resolved gfx1103 and installed wheels and prebuilts for the iGPU the mask hides. ROCR_VISIBLE_DEVICES filters below HIP, so masked devices never reach the runtime's enumeration at all, which makes targeting one strictly wrong rather than merely suboptimal. Setup now skips the handoff whenever any of the three masks is set, matching what its own inference and Resolve-ShadowingGfxPick already do. UNSLOTH_ROCM_GFX_ARCH stays the escape hatch and still wins. Confirmed both ways: with the guard removed the masked host resolves gfx1103, with it in place it resolves nothing, and the unmasked gap-filling case the handoff exists for is unchanged.
install.ps1 took the first AMD adapter WMI listed with no health check, while setup.ps1 filters on ConfigManagerErrorCode. A disabled Radeon listed ahead of a healthy unsupported one therefore resolved that dead card's arch. Because a mapped arch installs ROCm wheels right there, the machine got wheels for a GPU it cannot use while the live card went unserved, and setup, which discards that adapter, disagreed and took the forwarded arch as its last resort. Fixed at the source rather than by teaching setup to distrust the handoff: rejecting it there would leave setup expecting cpu torch against the ROCm wheels install.ps1 had already placed, which is the stale-venv rollback loop this branch exists to end. Filtering here means the two scans start from the same healthy set, so a forwarded arch can only ever name an adapter setup also kept. Keeps setup's fallback for the case where the filter empties the list, since code 45 is routine on a muxless laptop with a parked dGPU. Confirmed both ways: before, the disabled card resolves gfx1201; after, the host resolves nothing and lands on CPU exactly as setup does.
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
for more information, see https://pre-commit.ci
#8398 landed the same $wmiGpus @() wrap from the same report, so the only conflict is the comment above an identical line. Took main's, which is the merged wording and cites #8335. What is left here on top of main: the second unwrapped if a block down ($gpuNames, still unwrapped on main), the ConfigManagerErrorCode filter in install.ps1's WMI fallback (still Select-Object -First 1 on main), the private arch handoff, and the tests.
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The bug
On Windows,
studio/setup.ps1builds its AMD adapter list with:A one-element branch unrolls into a bare WMI object on its way out of the
if, and a bare WMI object has no.Countin PowerShell 5.1:So on a host with exactly one AMD adapter the guard right after it never fires,
$ROCmGpuLabelstays null, the name to gfx inference is skipped, and setup reports:Two or more AMD adapters keep the array intact and work fine, so this only hits single-GPU machines.
This is engine behaviour, not a quirk of one host. PowerShell has two member-binding paths and only the optimized one carries the PSv3 scalar
Count/Lengthfallback; types with a custom adapter (CimInstance,ManagementObject, COM,PSCustomObject) take the other one, which returned null in the 5.1-era engine. PowerShell/PowerShell#5745 added the fallback there and shipped it in 6.1, which Windows PowerShell 5.1 never received. It also means pwsh on CI answers1for the same expression, which matters for the tests below.Why that bricks the install
install.ps1resolves the arch withSelect-Object -First 1and is unaffected, so the two scripts disagree inside the same run, one second apart:With no GPU detected,
$expectedTorchTagiscpu, the installed+rocmwheel reads as stale, setup exits 1, the installer rolls back, preflight still says stale, and the desktop app repairs again. It never converges. Rolling back also leaves the venv without a working CLI, so later passes degrade further intoModuleNotFoundError: No module named 'unsloth_cli'.Reproduced on a Radeon 8060S (gfx1151, Strix Halo) laptop with Unsloth Desktop 0.1.70-beta, and confirmed fixed there.
setx UNSLOTH_ROCM_GFX_ARCH gfx1151clears it, which is what pointed at the detection path.The fix
studio/setup.ps1: wrap the wholeifin@(), the idiom the Intel scan a few lines below already documents ("@()wraps the WHOLE if, not each branch").studio/setup.ps1: the gpu name list one block down had the same expression, wrapping each branch but not theif:A single adapter name unrolls to a bare
String, so$gpuNames[$nameIdx]indexes the name and yieldsA, and nothing maps. The$nameArches[0]rescue below covers that unless a visible-device mask is set, so a single-GPU host withHIP_VISIBLE_DEVICESset still inferred no arch and looped the same way, and a standaloneunsloth studio updatewas never covered by the installer handoff at all. Every= if (site across the.ps1files was audited; this was the only other one.install.ps1: hand the arch this run resolved to setup as_UNSLOTH_ROCM_GFX_ARCH_HANDOFF, so a future divergence between the two scans cannot become an unrecoverable rollback loop.Deliberately not
UNSLOTH_ROCM_GFX_ARCH. That is the documented operator override, andinstall_llama_prebuilt.pyreads it back as_manualto decide whether a forwarded--rocm-gfxoutranks its own probe, so publishing an auto-detected value there disarms that safeguard.install.ps1's scan is also the weaker of the two: first AMD adapter, no visible-device mask, no shadowing-iGPU repick, all of whichsetup.ps1applies. On aRadeon 780M+RX 9070 XThost setup resolves gfx1201, and forwarding the installer's gfx1103 would have handed llama.cpp the iGPU bundle.So it uses the
_UNSLOTH_prefix the neighbouring handoffs use, is saved and restored in the samefinallyblock (install.ps1is documented asirm ... | iex, so it runs in the caller's own shell), is cleared rather than left alone when nothing was resolved, andsetup.ps1consumes it only after its own probes and name inference come up empty.Tests
tests/test_windows_amd_gpu_scan_fallback.pyextracts the shipped blocks fromsetup.ps1andinstall.ps1and runs them against stubbed adapter lists, so it exercises the source rather than a copy.The first version of this file asserted on
.Count, which pwsh answers as1for a scalar for the reason above, so it passed against the unfixed source and guarded nothing. Every runtime case now asserts the shape of the value, which is identical on both engines, and re-runs each case against stubs carrying an explicitCount = $nullto reproduce 5.1's observable consequence rather than only its cause.A; 11 marketing names to arch; discrete card preferred over a shadowing iGPU; a pinned mask honoured over that preference0,1, empty,-1, unparseable, out of range, padded, list)@(if (forms, the private handoff is set before setup and restored after,UNSLOTH_ROCM_GFX_ARCHis never exported, and the handoff is consumed after inference71 pass on the branch. Run against the merge base the same file reports 38 failures, including the single-adapter, pinned-mask and name-indexing cases, and
test_these_assertions_fail_against_the_source_before_the_fixpins that so a later edit cannot quietly re-lose it.Both scripts parse clean through
[System.Management.Automation.Language.Parser]::ParseFile.tests/studio/install+tests/pythonreport an identical 5 failed / 2486 passed / 91 skipped / 32 errors on this branch and on the merge base, so the pre-existing failures are unchanged.test_rocm_support.py(486 passed), the arch-table parity and cross-platform parity suites, and the neighbouringtests/studio/*.ps1suites all pass. An oldsetup.ps1ignores the new handoff rather than breaking on it, andinstall.sh/setup.sh/ macOS are untouched.