Skip to content

Add Intel Arc GPU detection and XPU PyTorch install to Windows installer - #7706

Open
legobele wants to merge 19 commits into
unslothai:mainfrom
legobele:add-intel-arc-xpu-detection
Open

Add Intel Arc GPU detection and XPU PyTorch install to Windows installer#7706
legobele wants to merge 19 commits into
unslothai:mainfrom
legobele:add-intel-arc-xpu-detection

Conversation

@legobele

Copy link
Copy Markdown

Fixes the installer's GPU detection chain which currently only checks for NVIDIA (CUDA) and AMD (ROCm), causing Intel Arc GPUs to fall into the cpu-only Torch path.

What changed

  • WMI-based Intel GPU detection (Arc, Iris, UHD, HD Graphics) before the final fallback
  • Torch XPU availability check for migrated/upgraded environments
  • XPU PyTorch install path using download.pytorch.org/whl/xpu index
  • CPU fallback with pointer to the Intel oneAPI docs
  • Updated messaging from "NVIDIA or AMD ROCm" to include Intel Arc

Why it works

PyTorch publishes XPU (SYCL) wheels at download.pytorch.org/whl/xpu that bundle their own oneAPI runtime (intel-sycl-rt, intel-cmplr-lib-rt, etc.), so no Intel oneAPI Base Toolkit installation is required for GPU training.

Tested on

Windows 11, Intel Arc 140V GPU (8GB), PyTorch 2.9.0+xpu, Qwen3.5-0.8B loaded on xpu:0

Related

legobele and others added 2 commits July 31, 2026 14:27
The installer's GPU detection chain (NVIDIA -> AMD ROCm -> else)
has no Intel Arc/SYCL/XPU branch, so Intel Arc GPUs fall into the
"none (chat-only / GGUF)" branch and get CPU PyTorch despite
PyTorch publishing XPU wheels at download.pytorch.org/whl/xpu.

This adds:
- WMI-based Intel GPU detection (Arc, Iris, UHD, HD Graphics)
- Torch XPU availability check for migrated/upgraded environments
- An XPU PyTorch install path with the whl/xpu index
- CPU fallback with a pointer to the Intel oneAPI docs when XPU
  isn't available
- Updated messaging from "NVIDIA or AMD ROCm" to include Intel Arc

The XPU wheels ship their own oneAPI runtime (intel-sycl-rt et al.)
so no Intel oneAPI Base Toolkit is required for GPU training.

Tested on: Windows 11, Intel Arc 140V GPU (8GB), PyTorch 2.9.0+xpu

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
The XPU index selected during GPU detection was overwritten by
Get-TorchIndexUrl before the install branch read it, so Intel hosts still
got CPU PyTorch while being told XPU wheels were being installed.

- Move the XPU reroute after Get-TorchIndexUrl, and let an explicit pin win
- Detect via Get-CimInstance (Get-WmiObject is absent in PowerShell 7)
- Match only Arc / Data Center GPU, so UHD / HD / Iris Xe are not promised XPU
- Split Intel GPU present from XPU-capable so the CPU fallback hint works
- Bound the XPU torch trio like every other index (bare names resolved
  torch 2.13.0 + torchaudio 2.11.0 and pulled unsloth back to an old release)
- Clear the XPU state after a CPU fallback, mirroring the ROCm path
- Teach the index family, GPU branch and torch flavor helpers about xpu
@danielhanchen

Copy link
Copy Markdown
Member

Thanks for this, Intel Arc support in the Windows installer is something we want. The direction is right, and unsloth/device_type.py already treats xpu as a first class device, so the runtime side is ready for it.

I pushed a commit to your branch to fix a few things that stopped the feature from actually engaging. Details below so nothing is a surprise.

The main issue

install.ps1 set the XPU index during GPU detection, but $TorchIndexUrl = Get-TorchIndexUrl further down overwrote it unconditionally before the install branch ever read it. On an Intel only host Get-TorchIndexUrl returns the CPU index, so the guard on the XPU branch was never true.

The visible result on an Arc box was the installer printing "Intel GPU detected" and "PyTorch XPU (SYCL) wheels will be installed from ...", then printing "No NVIDIA GPU detected." and installing CPU PyTorch. The XPU branch only ran if UNSLOTH_TORCH_INDEX_FAMILY=xpu was already set, which is probably how the Arc 140V run in the description worked.

Simulating both revisions across 34 Windows scenarios, before and after your commit were identical on every path except the messaging.

What the commit changes

  • Moves the XPU reroute to after Get-TorchIndexUrl, so it survives. An explicit UNSLOTH_TORCH_INDEX_URL / _FAMILY pin still wins, matching how the AMD ROCm reroute behaves.
  • Switches detection to Get-CimInstance. Get-WmiObject does not exist in PowerShell 7, and the catch {} swallowed the error, so the whole feature silently no-opped for anyone running the installer under pwsh.
  • Narrows the adapter match to Arc and Data Center GPU. Intel.*Graphics matched every Intel iGPU ever shipped, including UHD 620 and HD 4000, which PyTorch XPU does not support. It also made the Iris, UHD and HD Graphics alternatives redundant while still missing Data Center GPU Max, which the comment claimed to cover.
  • Separates "an Intel GPU is present" ($HasIntelGpu) from "XPU wheels are appropriate" ($script:IsIntelXpu). Previously the first implied the second, which made the "Intel GPU detected but XPU not available" hint unreachable. Legacy iGPU users now get an accurate message instead of a promise of GPU wheels.
  • Bounds the torch trio like every other index. This one matters: unpinned resolved to torch 2.13.0+xpu with torchaudio 2.11.0+xpu, and since unsloth requires torch<2.12.0 that silently pulled unsloth back to an older release. Bounded resolves to a clean torch 2.10.0+xpu set.
unpinned:  torch==2.13.0+xpu  torchvision==0.28.0+xpu  torchaudio==2.11.0+xpu  ->  unsloth==2026.3.11
bounded:   torch==2.10.0+xpu  torchvision==0.25.0+xpu  torchaudio==2.10.0+xpu  ->  unsloth==2026.7.6
  • Clears the XPU state after a CPU fallback, mirroring what the ROCm path does, so the flavor repair block does not retry the index that just failed.
  • Teaches Get-TauriTorchIndexFamily, Get-TauriGpuBranch, ConvertTo-TorchFlavorTag and Get-ExpectedTorchFlavorTag about xpu. Without this an XPU install reported gpu_branch=unknown to diagnostics and a 2.9.0+xpu wheel was classified as cpu.

Verification

Arc A770 / B580 / 140V now select the XPU index and take the XPU install branch. Iris Xe, UHD 620 and HD 4000 correctly stay on CPU with the accurate hint. NVIDIA, AMD ROCm, CPU only and hybrid Intel plus NVIDIA scenarios are byte for byte identical to main, as is install.sh. install.ps1 and studio/setup.ps1 both parse clean, and tests/studio/test_torch_flavor.ps1, test_node_decision.ps1, test_node_probe_guard.ps1, test_install_rollback_lifecycle.ps1, test_torch_index_pin_hardening.ps1, test_setup_pin_stale.ps1 plus tests/python/test_cross_platform_parity.py all pass.

Two follow ups, happy to take these separately

  1. studio/setup.ps1 runs its own torch resolution and does not know about xpu, so a later Studio setup with a changed pin can still reinstall over an XPU venv.
  2. pyproject.toml already ships curated intel-gpu-torch* extras that pin torch, pytorch_triton_xpu and an Intel capable bitsandbytes wheel. Selecting the matching extra would be better than a bare torch install, since the current path never installs that bitsandbytes build. Also worth noting our Windows Intel docs ask for oneAPI plus Level Zero, which the header comment says is unnecessary. That is true for stock PyTorch but not for the full Unsloth stack on Windows.

Also worth rebasing on latest main when you get a chance, the branch predates #7692.

install.ps1 now classifies an /xpu index leaf as family xpu / branch xpu, so
mirror the same two cases in _tauri_torch_index_family and _tauri_gpu_branch.
These feed the [TAURI:DIAG] line only, and a Linux user can already reach the
xpu index via UNSLOTH_TORCH_INDEX_FAMILY, where it previously reported
auto/unknown. Linux Intel auto-detection is not added here.
@danielhanchen

Copy link
Copy Markdown
Member

Checked whether this needs an install.sh counterpart. Short answer: one small parity item, which I have pushed, and Linux auto-detection is a genuine gap but belongs in its own PR.

What I changed (7d45c78)

My previous commit taught install.ps1 to classify an /xpu index leaf as family xpu / branch xpu. Those helpers are documented as mirroring install.sh, so leaving it would have created a silent divergence. Added the matching two cases to _tauri_torch_index_family and _tauri_gpu_branch.

This matters today even without Linux detection, because a Linux user can already reach the XPU index by hand:

UNSLOTH_TORCH_INDEX_FAMILY=xpu   -> https://download.pytorch.org/whl/xpu
UNSLOTH_TORCH_INDEX_URL=.../xpu  -> https://download.pytorch.org/whl/xpu

Before, that install reported gpu_branch=unknown torch_index_family=auto in diagnostics. Now both installers agree:

leaf      install.sh              install.ps1
xpu       family=xpu   branch=xpu family=xpu   branch=xpu
cpu       family=cpu   branch=cpu family=cpu   branch=cpu
cu128     family=cu128 branch=cuda family=cu128 branch=cuda
rocm7.2   family=rocm7.2 branch=rocm family=rocm7.2 branch=rocm

These feed the [TAURI:DIAG] line only, and the Tauri side records the marker as an opaque string rather than matching on it, so there is no behavioural risk. bash -n and sh -n pass, the 12 scenario Linux/WSL/macOS index matrix is byte identical to main, and 120 installer tests pass.

What I did not add, and why

Auto-detecting an Intel GPU on Linux and rerouting to the XPU index is a real gap, but it is a feature rather than a fix for this PR:

  • The pieces are there. install.sh already enumerates PCI vendors via /sys/bus/pci/devices/*/vendor for AMD, so an 0x8086 check would slot in beside it, and the XPU index publishes linux_x86_64 wheels alongside win_amd64.
  • Bounds are already safe. install.sh applies TORCH_CONSTRAINT globally rather than per branch, so a Linux XPU path would inherit torch>=2.4,<2.11.0 automatically and would not hit the unpinned resolution problem the Windows branch had.
  • The runtime story is different enough to want its own testing. Linux XPU needs the compute runtime and Level Zero from the distro rather than the bundled Windows driver path, and pyproject.toml carries separate Linux pytorch_triton_xpu pins.

Since neither of us can test on an Arc box under Linux in this PR, I would rather land the Windows path here and do Linux detection separately where it can be exercised properly.

studio/setup.ps1 and studio/install_python_stack.py are deliberately untouched. install_python_stack.py treats an unrecognised leaf as unknown and leaves torch alone, which is the safe behaviour for an XPU venv, so teaching it xpu without the rest of the plumbing would make things worse rather than better. That is still the follow up I mentioned above.

@danielhanchen

Copy link
Copy Markdown
Member

Ran a regression sweep across every vendor we support, since this touches the shared index and diagnostics helpers. No regressions found.

Staging CI, including macOS

Replicated onto a staging repo rather than loading the org queue. All green at 7d45c78:

pr-7706-ci         success  Mac Studio UI CI
pr-7706-ci         success  Clean machine install
pr-7706-ci         success  Interrupted install recovery
pr-7706-ci         success  Startup profile
pr-7706-xplat-ci   success  staging-7706 macos-14
pr-7706-xplat-ci   success  staging-7706 ubuntu-latest
pr-7706-xplat-ci   success  staging-7706 windows-latest
pr-7706-xplat-ci   success  Mac Studio UI CI, Clean machine install,
                            Interrupted install recovery, Startup profile

Four runs show as cancelled at 10:41:29. Those were superseded three seconds later by the 10:41:32 runs on the same branch, which all passed, so it is concurrency group cancellation rather than failure. Every workflow has a green terminal run. Staging PRs closed, not merged.

Vendor spoofing in an isolated venv

Built a clean uv venv (torch 2.10.0+cpu, structlog, pytest) and ran the existing spoof suites against main and this branch:

suite covers main this branch
test_hardware_dispatch_matrix.py CUDA / ROCm / XPU / MLX / CPU profiles 23 passed 23 passed
test_is_mlx_dispatch_gate.py MLX gating 5 passed, 1 skipped 5 passed, 1 skipped
test_xpu_spoof_pipeline.py fake torch.xpu pipeline, Arc B580 / Lunar Lake mem_get_info 29 passed 29 passed

Also spoofed each vendor in its own venv to check installer side leaf classification, identical on both branches:

nvidia  leaf=cu128    cuda_family=true   rocm_family=false
amd     leaf=gfx1201  cuda_family=false  rocm_family=true
intel   leaf=xpu      cuda_family=false  rocm_family=false
cpu     leaf=cpu      cuda_family=false  rocm_family=false

Real NVIDIA

4x B200, driver 590.48.01, torch 2.9.1+cu128:

main          DEVICE_TYPE=cuda  torch_dev=cuda  count=4  is_hip=False
this branch   DEVICE_TYPE=cuda  torch_dev=cuda  count=4  is_hip=False

Installer matrices

The tests that actually exercise the change: 34 Windows scenarios and 12 Linux/WSL/macOS scenarios, run against main, the original commit and the current branch. NVIDIA (cu126/cu128/cu130), AMD ROCm, CPU only, hybrid Intel plus NVIDIA and hybrid Intel plus AMD are byte identical to main. Arc A770, B580 and 140V now select the XPU index and take the XPU install branch. Iris Xe, UHD 620 and HD 4000 correctly stay on CPU with the accurate message rather than being promised XPU wheels.

@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
Comment and whitespace only, no code change.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- Run the Intel scan before the GPU report chain instead of inside its final
  else. A WMI-named-only AMD adapter set ROCmGpuLabel and took that chain, so a
  discrete Arc card next to an AMD CPU's integrated Radeon was never detected.
  The scan is gated on no usable NVIDIA or AMD, and the Intel branch ranks above
  the two AMD-present-but-unusable branches, so a usable AMD host is unaffected.
- Let a migrated env's torch veto the hardware match only when it is itself an
  XPU build. A CPU build reports torch.xpu.is_available() False for lacking XPU
  support, not for unsuitable hardware, and was blocking the CPU to XPU upgrade.
- Detect Intel in studio/setup.ps1 too. It only knew NVIDIA and AMD, so every
  successful Intel install printed none (chat-only / GGUF) right after
  install.ps1 reported a usable Arc GPU. Self-contained so studio update works.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- Reset $script:IsIntelXpu at the start of each invocation. Under the documented
  irm | iex path $script: is the caller's session scope, so a second run in the
  same session inherited a stale true, skipped the scan on a now-NVIDIA host and
  still rerouted to the xpu index. Reproduced in pwsh before fixing.
- Gate the Intel scan on whether AMD actually gets a wheel, not on whether an AMD
  arch was seen. An arch missing from the family map has no ROCm wheels and lands
  on CPU torch, so it must not outrank a usable Arc card. The map is hoisted above
  the scan and consumed unchanged by the AMD reroute.
- Select the XPU index in studio/setup.ps1, not just report it. Previously setup
  printed Intel GPU detected and then installed CPU torch, so studio update never
  migrated an Arc box off CPU. Adds a bounded XPU install with a CPU fallback,
  teaches the stale-venv check about +xpu, and mirrors the wheel-aware AMD gate so
  the two files agree instead of wiping the venv on every update.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

- Force the dependency pass on an Arc host whose torch is not XPU-capable, the
  Intel counterpart of the existing AMD escape. Without it the fast up-to-date
  path skipped the install block, so the xpu index selection was never reached
  and a CPU venv never migrated.
- Confirm a working XPU runtime before treating an xpu venv as stale. If CIM is
  unavailable or returns an Intel name outside the Arc match, the expected tag
  fell through to cpu and a valid XPU environment was rebuilt and lost.
- Force-reinstall the XPU trio only when the installed wheel is not already
  +xpu, or the pin changed. It was unconditional, so a fresh install re-fetched
  multiple GB immediately and again on every update.
- Warn when torch.xpu.is_available() is false after installing XPU torch, naming
  the Intel driver floor. Otherwise the installer promised GPU training while
  unsloth raised NotImplementedError at import on a stale driver.
- Stop the detection probe vetoing the hardware match. Its cpu fallback could not
  displace the installed +xpu wheel, so it only mislabelled a capable GPU as
  unusable; the driver warning covers that case honestly, and setup.ps1 agrees.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

…dbytes on the Intel path

install.sh: teach _torch_flavor_tag, _expected_torch_flavor_tag and
_torch_index_repairable about the xpu leaf. The diagnostic already reported
gpu_branch=xpu, but an xpu pin fell to the custom arm so a migrated env kept
its CPU wheel. The +xpu flavor arm is required alongside, otherwise a correct
2.10.0+xpu wheel reads as cpu and gets force-reinstalled every run.

install.ps1 / studio/setup.ps1: route every torch probe through a new bounded
Invoke-BoundedPythonProbe (ProcessStartInfo, both streams drained async,
WaitForExit, kill on timeout). A hanging Intel driver init is exactly what
these probes detect, and an unbounded one would hang the installer instead of
reaching the warning. Timeouts read as not-available. Get-InstalledTorchTag
now shares the helper rather than carrying a second copy of the pattern.

install.ps1: install bitsandbytes>=0.50.0 on the XPU path. unsloth's floor is
>=0.45.5, so a migrated venv keeps a pre-0.49 wheel with no XPU library and
4-bit QLoRA silently turns off. Same floor the AMD paths use, since <=0.49.2
NaNs at 4-bit decode and an Arc card can sit next to a Radeon.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

… pin

studio/setup.ps1: `unsloth studio update` migrating a CPU venv to XPU replaced
only the torch trio. install_python_stack.py then upgrades unsloth and
unsloth-zoo alone, so an installed bitsandbytes 0.45.x kept satisfying the base
floor while carrying no Windows XPU kernels, and 4-bit QLoRA silently turned
off. Adds the same bitsandbytes>=0.50.0 --no-deps pass install.ps1 got, placed
after the stack so it is the last word, gated on $XpuIndexUrl (the CPU fallback
clears it, no-torch never sets it) and still inside the -not $SkipPythonDeps
block so the up-to-date escape does not reach it.

install.ps1: key the bitsandbytes pass off the index leaf instead of
$script:IsIntelXpu. An explicit UNSLOTH_TORCH_INDEX_FAMILY=xpu pin on a
non-Intel host skips the XPU branch but still installs the trio from the xpu
index, so torch is +xpu and needs the same floor. The CPU fallback rewrites
$TorchIndexUrl, so a failed XPU install reads as cpu and stays quiet.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
danielhanchen added a commit to danielhanchen/unsloth-staging-2 that referenced this pull request Aug 2, 2026
Comment-only pass over the previous commit's additions, which had not been
through one: 15 lines removed across the two bounded-scan headers, the two
registry-fallback headers and the Triton block.

Kept the facts that cost measurement: -OperationTimeoutSec not being enforced
for a local COM session, Ok being false on an empty answer because a Windows
host always has an adapter, the registry class key being fallback rather than
fast path here, the 151 shared Triton paths, and why the uninstall has to be
paired with a reinstall after the stack.

No code tokens changed; verified with a PowerShell token-stream diff of both
files, which also confirms the two helper copies stay identical.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

…t strand the venv

The replacement uninstalled triton-windows and then installed the XPU triton
from the index. A failure between the two left the venv with a partially
deleted triton, since the uninstall drops the paths shared with the XPU
distribution, and the warning made that look like a skipped optional repair.

The uninstall cannot go last, because it removes the paths in triton-windows'
own record and those are the shared ones. So fetch first: pip download the
wheel, confirm one is actually on disk (exit 0 alone is not enough, an
sdist-only mirror satisfies that), and only then uninstall and install the
local file. A local wheel installs with the network refused, so nothing after
the destructive step depends on the index. A failed fetch leaves
triton-windows in place, which is the pre-existing shadowing rather than a
broken venv, and says so.

Past that point only disk or permissions can fail, so restore triton-windows
if the local install does, leaving a triton that imports. If both fail the
message is loud and carries the repair command, with the index URL redacted
since a mirror pin can carry a token.

pip only: uv has no pip download (astral-sh/uv#3163).
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 2, 2026
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 75e28096ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

…ll state up front

Get-IntelRegistryAdapterNames wrapped the whole enumeration in a single try, so one
unreadable subkey discarded every adapter found before it. windows_intel_gpu_in_registry(),
the in-process Python probe over the same class key, skips per subkey and continues; the
PowerShell copy now does too. It also matched on the PCI vendor id but returned DriverDesc,
which the callers re-filter on "Intel", so a localized or OEM-branded Arc was found here and
dropped there. Both installers carry the same copy and a test asserts they stay identical.

setup.ps1 read $installedTorchTag and $XpuIndexUrl from outside the blocks that assign them.
Unset and $null are both falsy so behaviour is unchanged, but a caller running with
Set-StrictMode -Version Latest turned those reads into terminating errors, and install.ps1
is documented as irm | iex into the caller's own session.

Two comment corrections: 0.48.2, not 0.49.0, is the first win_amd64 bitsandbytes wheel
carrying libbitsandbytes_xpu.dll, and the triton package overlap is version-dependent
rather than a fixed 151 paths.

The new test drives the shipped helper with the registry cmdlets mocked rather than reading
a hive, so it runs on Linux and macOS as well as Windows.
hardware.py has always emitted versions["xpu"], but HardwareInfo only ever declared cuda and
rocm. On an Arc host both of those are null, so the runtime row disappeared entirely while
the GPU name and VRAM rows still rendered, leaving a host that looks half detected. That was
unreachable on Windows until the installer learned to select XPU wheels, which is what makes
it worth fixing here.

The three-way choice is lifted into a helper at module scope: inlining it pushes AboutTab
past the cognitive-complexity ceiling. The label is a proper noun, so every locale carries
the same literal.
@danielhanchen

Copy link
Copy Markdown
Member

Pushed two commits after simulating this end to end rather than only reasoning about it. Three of the findings are in code I added earlier in this PR.

Registry fallback (Get-IntelRegistryAdapterNames, both installers)

Two defects, both found by driving the shipped function with the registry cmdlets mocked:

  • The whole foreach sat inside one try, so a single unreadable subkey discarded every adapter enumerated before it. windows_intel_gpu_in_registry() in studio/install_llama_prebuilt.py reads the same class key and skips per subkey; the PowerShell copy now matches it.
  • The ven_8086 arm was close to dead. It matched on the PCI vendor id but returned DriverDesc, which both callers then re-filter with (?i)Intel, so a localized or OEM-branded Arc was found here and dropped there. A Japanese DriverDesc on an A770 is a concrete case: the Python probe returns true, the PowerShell path returned nothing.

22 scenarios, 3 cells changed, the other 19 (NVIDIA, AMD, CPU, stale entry, empty key, non-numeric subkey, class key unreadable) bit-identical. Negative control against the previous commit fails exactly those 3.

tests/studio/test_intel_registry_fallback.ps1 covers this. It mocks the cmdlets rather than reading a hive, so it runs on all three platforms, and it asserts the two copies of the helper stay identical.

StrictMode (studio/setup.ps1)

$installedTorchTag and $XpuIndexUrl were read from outside the blocks that assign them. Unset and $null are both falsy, so behaviour is unchanged, but under Set-StrictMode -Version Latest those reads are terminating errors: 53 scenarios for the first (every fresh install on an Arc host), 2 for the second (no-torch mode). install.ps1 is documented as irm | iex into the caller's own session, and unsloth studio update from a terminal loads the user profile, so a strict caller is reachable. Both declarations are now hoisted ahead of their reads. 432 scenarios, 0 rows changed.

Worth being precise about scope: setup.ps1 has many pre-existing reads of this shape, so this does not make the file StrictMode-clean. It removes what this PR added.

Two comment corrections

  • 0.48.2, not 0.49.0, is the first win_amd64 bitsandbytes wheel carrying libbitsandbytes_xpu.dll. Verified by parsing the export tables: 0.48.0 and 0.48.1 ship no XPU DLL, 0.48.2 does.
  • The triton package overlap is version-dependent (80 to 160 shared paths across the pairs measured), not a fixed 151.

About tab

hardware.py has always emitted versions["xpu"], but HardwareInfo only declared cuda and rocm. On an Arc host both are null, so the runtime row vanished while the GPU name and VRAM rows still rendered.

What the simulations covered

  • 578 install.sh driver executions across spoofed NVIDIA / AMD / Intel / CPU on Linux and WSL, each against merge base and head: 0 non-xpu cells moved. Anti-thrash proven on 12 real venvs (torch 2.4 through 2.10, cu118/cu126/cu128/cpu/xpu, real wheels): 0 matching-flavour cells repair, and a real round trip on a 2.9.1+cpu venv under an xpu pin goes REPAIR then keep, keep.
  • 432 PowerShell scenarios per build across 18 gfx arches, 14 Intel names and 6 CUDA driver strings. Non-Intel movers: install.ps1 4, all the MsgRequiresText display string only; setup.ps1 0.
  • Staging CI on Windows, windows-11-arm, a virgin Windows container, macOS 14, Ubuntu and a real WSL Ubuntu 24.04 job: 57/58 and 59/60 green. The one failure is test_chat_autoload_failure_gate.py, which fails identically on a clean origin/main and whose file does not exist on this branch.
  • A real 4x B200 run to confirm the primary supported path is undisturbed.

One thing I checked and did not change: the comment on _torch_flavor_tag's xpu arm. It reads as a claim about merge base, where the repair block is inert for xpu, but it describes the arm's role in the shipped design. Removing just that arm from head makes a correct 2.10.0+xpu wheel force-reinstall on every run, which is what it says.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 237cf1e8f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Comment thread studio/setup.ps1
Comment on lines +3380 to +3384
if ($script:IsIntelXpu -and $SkipPythonDeps) {
$_torchIsXpu = Test-TorchXpuAvailable -PythonExe "python"
if (-not $_torchIsXpu) {
substep "Intel GPU detected but PyTorch XPU is unavailable -- reinstalling XPU PyTorch" "Cyan"
$SkipPythonDeps = $false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run XPU remediation before taking the fast path

When upgrading an existing Windows XPU environment whose torch.xpu.is_available() is already true, this check leaves $SkipPythonDeps enabled, so the newly added bitsandbytes upgrade and triton-windows replacement below never run. This affects migrations in particular: the first studio update executes the old installed setup.ps1, upgrades Unsloth, and cannot perform these new repairs; subsequent updates execute this version but take the fast path because Unsloth is current and XPU is available, leaving the old bitsandbytes wheel and triton-windows shadowing the XPU Triton indefinitely. The fast-path decision needs to detect those stale XPU dependencies or use a migration marker.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and the migration framing is the right one: the escape only fired when XPU was unavailable, so a venv already on +xpu took the fast path forever and never picked up either remediation. Fixed in 2603fc8 by probing for exactly what the two passes install for, bitsandbytes below 0.50.0 or triton-windows still present, and clearing the fast path when either is outstanding. An unreadable version reads as stale, which costs one dependency pass rather than leaving a venv that cannot do 4-bit.

New matrix rows: stale bnb -> pass forced; triton-windows present -> pass forced; unreadable bnb -> pass forced; deps current -> fast path kept; NVIDIA host with the same stale bnb -> fast path kept, the probe never runs off an Intel host.

Comment thread install.ps1
Comment on lines +2305 to +2307
$_gpuScan = Invoke-BoundedVideoControllerScan
$_gpuNames = if ($_gpuScan.Ok) { $_gpuScan.Names } else { @(Get-IntelRegistryAdapterNames) }
$intelGpus = @($_gpuNames | Where-Object { $_ -match "(?i)Intel" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fall back to vendor IDs after localized WMI results

When Win32_VideoController succeeds but returns a localized Intel name without the ASCII word Intel (for example the Japanese Arc name already modeled in tests/studio/test_intel_registry_fallback.ps1), _gpuScan.Ok selects the WMI names exclusively and the following Intel filter drops the adapter. The registry helper was specifically written to normalize these localized names using VEN_8086, but it is only called when the entire WMI scan fails, so affected Arc hosts are classified as CPU-only. Retry or merge the registry results when the successful WMI result contains no recognizable Intel adapter.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and it makes the helper I just fixed unreachable on the path that matters. Fixed in 2603fc8, but narrower than "retry or merge": the registry now re-labels an adapter WMI already reported, matched by name. A registry entry naming nothing in the WMI list stays ignored, so a driver record outliving its card still cannot promote a host WMI answered for, which is an existing control (R-Registry Arc, WMI answers).

Truth table from the matrix, all new rows: localized Arc + registry naming the same adapter -> xpu; localized Arc + unrelated stale registry Arc -> cpu; localized Arc + empty registry -> cpu; localized iGPU + matching registry -> Intel detected, not xpu. NVIDIA and AMD controls with a stale registry Arc unchanged.

Comment thread studio/setup.ps1 Outdated
Comment on lines +3869 to +3871
Fast-Uninstall "triton-windows" | Out-Null
if ($script:UnslothVerbose) {
Fast-Install --force-reinstall --no-deps $_tritonWheel | ForEach-Object { Redact-InstallOutput "$_" } | Out-Host

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate the manifest before replacing Triton

If setup is interrupted after Fast-Uninstall "triton-windows" but before the XPU wheel is reinstalled, the environment is left without the shared Triton files even though install_python_stack.py has already written the completion manifest immediately before returning. The next studio update can therefore accept that stale manifest and take the fast path, since the boot-dependency check only examines studio.txt and does not validate either Triton distribution. Remove the manifest before this destructive post-stack operation and write it only after all XPU remediation completes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The round-9 pre-fetch shrank the window to uninstall-then-install but did not close it, and the manifest is written before either. Fixed in 2603fc8: the manifest is read and removed before the uninstall and the same bytes are written back after, gated on a triton actually being importable again. If neither the XPU wheel nor triton-windows reinstalls the manifest stays gone, which is what makes the next update repair instead of fast-pathing past a broken torch.compile.

The path is asked for via install_manifest.manifest_path() rather than assembled, so it cannot drift, and an older tree without the helper skips the hold entirely.

Comment thread install.ps1 Outdated
substep "installing PyTorch from $(Remove-IndexUrlCredentials $TorchIndexUrl)..."
# Bound the trio like every other index: the xpu index serves torch past our
# ceiling (up to 2.13.0), and torchaudio dropped its exact torch pin.
$torchInstallExit = Invoke-InstallCommandRetry -Label "install PyTorch (Intel XPU)" { uv pip install --python $VenvPython --force-reinstall "torch>=2.4,<2.11.0" "torchvision>=0.19,<0.26.0" "torchaudio>=2.4,<2.11.0" --default-index $TorchIndexUrl }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the minimum supported XPU torch version

The XPU install accepts torch 2.4 and 2.5 even though unsloth/models/_utils.py unconditionally raises RuntimeError for an XPU device when torch is below 2.6. This breaks installs against an XPU mirror that only contains an older wheel, and it also lets setup retain an existing 2.5+xpu wheel because that version satisfies the range. Use an XPU-specific lower bound of 2.6 so the installer cannot report success with a runtime that Unsloth immediately rejects.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed at unsloth/models/_utils.py:1929, which raises unconditionally for an XPU device below 2.6. Fixed in 2603fc8: torch>=2.6,<2.11.0 with torchvision>=0.21 and torchaudio>=2.6 on the XPU install in both installers and on the xpu arm of the flavor repair. The CPU fallback after a failed XPU install keeps 2.4, since that path is CPU torch.

Matrix diff: 53 rows moved on install.ps1 and 75 on setup.ps1, every one only on the install command, and every one on the xpu index. Nothing on a cu*, rocm or cpu index moved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Measuring the exposure after the fact, since I claimed more than I had checked: the official xpu index carries no torch below 2.6 at all. Earliest wheel on download.pytorch.org/whl/xpu is 2.6.0+xpu, for both cp3xx-win_amd64 and cp3xx-linux_x86_64, so the old 2.4 floor was never reachable there and torch>=2.4,<2.11.0 resolved to the same wheel the new range does. Confirmed across cp310 to cp313 on both platforms: 2.10.0+xpu either way.

So the change is defensive rather than a live break. It bites on a custom UNSLOTH_PYTORCH_MIRROR or UNSLOTH_TORCH_INDEX_URL carrying older wheels, and it makes the constraint state the floor Unsloth actually supports instead of one that happens to be safe because of what upstream publishes. The retain-an-existing-2.5+xpu case needs a wheel this index never shipped, so it would have to come from elsewhere.

Keeping the fix on those grounds, but the severity is lower than the report reads.

…and an old wheel

Four holes in the XPU paths, all found by driving the shipped code rather than reading it.

The registry fallback only ran when the CIM scan failed. When it succeeds and returns a
localized adapter name, which on non-English Windows carries no ASCII "Intel", the filter
dropped the adapter and the host went to CPU torch. The registry now re-labels an adapter
WMI already reported, matched by name so an entry naming nothing WMI listed stays ignored:
a driver record outliving its card still cannot promote a host WMI answered for.

The XPU trio accepted torch 2.4 and 2.5, which unsloth/models/_utils.py rejects at import
for an XPU device. An xpu mirror carrying only an older wheel produced an install that
reported success and then failed on the first import, and an existing 2.5+xpu venv was kept
because it satisfied the range. The floor is 2.6 on the XPU paths only; the CPU fallback
keeps 2.4.

The "package is up to date" fast path escaped for an Arc host on CPU torch, but not for one
already on XPU torch whose bitsandbytes predates the XPU kernels or whose triton-windows
still shadows the XPU Triton. Those two live in the dependency pass, so a venv that reached
+xpu without them, an explicit pin or an update whose first pass ran the pre-XPU setup.ps1,
never got them on any later update either. An unreadable version reads as stale.

install_python_stack.py writes its completion manifest immediately before returning, so an
interrupt between the triton-windows uninstall and the XPU wheel install left a venv with no
triton that the next update read as complete. The manifest is now held aside across the swap
and restored only once a triton is importable again.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2603fc809a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Comment thread studio/setup.ps1 Outdated
Comment on lines +3972 to +3973
if ($_manifestSaved -and $_tritonPresent) {
try { Set-Content -LiteralPath $_manifestPath -Value $_manifestSaved -NoNewline -ErrorAction Stop } catch {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve UTF-8 when restoring the manifest

On Windows PowerShell 5.1, Set-Content defaults to the system ANSI encoding when recreating this deleted file. For users whose venv path contains non-ASCII characters, this can corrupt the UTF-8 JSON that studio/install_manifest.py::read_manifest explicitly reads with encoding="utf-8"; every subsequent update then treats the installation as incomplete, reruns the dependency pass, and repeats the Triton replacement. Restore the original bytes or specify UTF-8 explicitly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and worse than stated in one direction while the suggested route is unavailable in the other. Get-Content is ANSI on a BOM-less file in 5.1 too, so the read already lost bytes before the write got a chance to. And 5.1 has no BOM-less UTF-8: -Encoding utf8 there always emits a BOM, utf8NoBOM arrived in PowerShell 6, and read_manifest is json.load with encoding utf-8, which rejects a BOM outright ("Unexpected UTF-8 BOM"). I checked both failure modes locally: BOM raises JSONDecodeError, cp1252 bytes raise UnicodeDecodeError.

So in af928dd the manifest is MOVED into the wheel temp directory and moved back, with no encoding involved at either end. That directory is already deleted in the finally, which is what keeps an unrestored manifest gone.

Sources: about_Character_Encoding and Set-Content.

Comment thread studio/setup.ps1 Outdated
Comment on lines +3937 to +3939
$_manifestSaved = Get-Content -Raw -LiteralPath $_manifestPath -ErrorAction Stop
Remove-Item -LiteralPath $_manifestPath -Force -ErrorAction Stop
} catch { $_manifestSaved = $null }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort the Triton swap when manifest removal fails

If the manifest is briefly locked or cannot be removed, this catch only clears $_manifestSaved and then proceeds to uninstall triton-windows. The current fix therefore still leaves the old valid manifest in place during the destructive window; fresh evidence is that the removal failure at this exact catch is ignored, unlike the earlier dependency-pass removal which aborts. If the process is interrupted after the uninstall, the next update can accept the stale manifest and fast-path past the missing Triton files, so the swap should be skipped or aborted unless the manifest is confirmed absent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, that catch defeated the hold in exactly the case it exists for. Fixed in af928dd: a manifest that will not move now skips the swap outright rather than proceeding into the uninstall with the old valid one still in place. triton-windows keeps shadowing the XPU Triton, which costs torch.compile on the GPU and the next run can still fix, and that beats risking a venv with no Triton that reads as complete.

243 setup scenarios, 0 rows moved by the rework once the random temp directory names are normalised, and StrictMode still clean.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my own fix here, because it was still wrong.

Modelling the manifest in the setup.ps1 scenario matrix (which had no coverage for it) showed the swap still running with the manifest in place: Move-Item across volumes is a copy followed by a delete, and it reports success when only the delete fails, leaving the original exactly where it was. Reproduced on pwsh 7 with a read-only parent directory: the call reports success, the destination exists, and so does the source. The abort added in af928dd therefore never fired, and a valid manifest stood through the whole destructive window, which is the case it exists to prevent.

83d188c confirms the move by testing the source path afterwards and treats a manifest still standing as a failure to move it, aborting the swap.

Four scenarios now cover it: swap succeeds and the manifest comes back byte for byte; neither Triton reinstalls and it stays gone; the fetch fails and it is never touched; it cannot move and the swap does not start. Before the fix the last one reported the swap running; after it, it does not. 247 scenarios, StrictMode still clean.

…rewriting it

Two problems with the hold added in 2603fc8, both on the restore side.

Reading and rewriting the file cannot survive a manifest carrying a non-ASCII path. Windows
PowerShell 5.1 writes Set-Content in the ANSI code page by default, and its -Encoding utf8
emits a BOM that install_manifest.read_manifest's json.load rejects outright
("Unexpected UTF-8 BOM"); Get-Content is ANSI on a BOM-less file too, so the read lost bytes
before the write got a chance to. The manifest is now MOVED into the wheel's temp directory
and moved back, so no encoding is involved at either end. That directory is already removed
in the finally, which is what keeps an unrestored manifest gone.

A manifest that would not move left the old valid one in place for the whole destructive
window, since the failure only cleared the saved copy and carried on into the uninstall.
That is the case the hold exists for, so it now skips the swap entirely and says so:
triton-windows keeps shadowing the XPU Triton, which costs torch.compile on the GPU and is
repairable on the next run, rather than risking a venv with no Triton that reads as complete.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af928dd888

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Comment thread studio/setup.ps1
# torch forever. $SkipPythonDeps is re-tested so an escape already taken above (AMD,
# anyio, incomplete install) does not probe twice. The probe is bounded, and a timeout
# reads as "not XPU" -- that costs one dependency pass, never a silent CPU venv.
if ($script:IsIntelXpu -and $SkipPythonDeps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit XPU pins in fast-path remediation

On a mixed NVIDIA+Intel host using an explicit xpu index pin, $HasNvidiaSmi prevents both the hardware scan and the runtime promotion from setting $script:IsIntelXpu, even though the pin later authoritatively selects XPU. Consequently, an up-to-date migrated +xpu environment with old bitsandbytes or triton-windows still takes the fast path here and never reaches either remediation. Fresh evidence beyond the earlier fast-path report is this explicit-pin mixed-GPU path, which remains excluded by the $script:IsIntelXpu gate; the check should also recognize the pinned/installed XPU flavor.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same gating mistake as round 4, reintroduced in the check I added in 2603fc8. Fixed in bb71199.

The bitsandbytes pass had this exact shape before: gated on $script:IsIntelXpu, which a FAMILY=xpu pin on a non-scanned host never sets, and the fix then was to key off the index leaf instead. The leaf is not resolved yet at the fast path, but the installed flavour tag is, so the staleness check now fires when the scan says Intel OR the venv is already on a +xpu wheel. Whatever put it there, the two remediations apply. The runtime probe above stays on the scan, since reinstalling XPU torch is only right where an Intel GPU was actually found.

Two new matrix rows for the mixed NVIDIA + Intel box under UNSLOTH_TORCH_INDEX_FAMILY=xpu with a +xpu venv: stale bitsandbytes now forces the dependency pass with IsIntelXpu false, and current dependencies keep the fast path. A pure NVIDIA host on a cu wheel with the same stale bitsandbytes never runs the probe at all, which the matrix asserts as the control. 247 pre-existing scenarios, 0 moved.

Worth noting the harness needed widening first: its fast-path region anchor matched only the original if, so the new statement sat outside the extracted region and would never have run. The two new rows would have passed for nothing.

…n swap

Move-Item across volumes is a copy followed by a delete, and it reports success when only
the delete fails, leaving the original exactly where it was. So the guard added in af928dd
could believe it had set the manifest aside while a valid one sat there for the whole
destructive window, which is the case that guard exists to prevent.

Found by modelling the manifest in the setup.ps1 scenario matrix, which this had no coverage
for: with the parent directory read-only the swap still ran, and the locked scenario passed
for the wrong reason. The move is now confirmed by testing the source path afterwards, and a
manifest still standing aborts the swap like any other failure to move it.

Four new scenarios cover it: the swap keeping a byte-identical manifest, a swap where neither
Triton reinstalls correctly leaving it gone, a failed fetch never touching it, and a manifest
that cannot move aborting the swap.
…ot just the GPU scan

$HasNvidiaSmi suppresses the Intel scan, so on a mixed NVIDIA + Intel box under an explicit
xpu pin $script:IsIntelXpu stays false while the pin still lands the venv on a +xpu wheel.
The staleness check added in 2603fc8 was gated on that flag alone, so those hosts kept
taking the fast path and never reached the bitsandbytes floor or the Triton replacement.

This is the same gating mistake the bitsandbytes pass had in round 4, where the fix was to
key off the index leaf rather than the scan. The leaf is not resolved yet at the fast path,
but the installed flavor tag is, and whatever put the venv on a +xpu wheel the two
remediations still apply. The runtime probe above stays on the scan: reinstalling XPU torch
is only right where an Intel GPU was actually found.

A pure NVIDIA host on a cu wheel never runs the probe, which the matrix asserts alongside the
two new mixed-host rows.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

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.

2 participants