Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Roland Tannous
1c3b3b36bd Windows: pick PyTorch wheel from installed CUDA Toolkit, not driver max
nvidia-smi reports the driver's max-supported CUDA, which can outrun the
userland — picking cu130 off a "CUDA 13.0" driver with a 12.9 toolkit ships
a torch that silently falls back to CPU. Resolve the wheel from the actual
toolkit version instead, winget-installing the highest <= driver max if no
toolkit is present.
2026-05-18 11:53:14 +04:00
2 changed files with 174 additions and 40 deletions

View file

@ -1213,24 +1213,124 @@ shell.Run cmd, 0, False
substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow" substep "Training and GPU inference require an NVIDIA GPU with drivers installed." "Yellow"
} }
# ── Choose the correct PyTorch index URL based on driver CUDA version ── # ── Detect installed CUDA Toolkit version ──
# Returns "12.9" / "13.0" / $null. Checks CUDA_PATH and the standard
# toolkit install root. The toolkit version is the actual userland CUDA
# available to PyTorch; nvidia-smi only reports the driver's *max
# supported* CUDA, which can be a release ahead of the userland.
function Get-CudaToolkitVersion {
$candidates = @()
foreach ($scope in @('Process','Machine','User')) {
$cudaRoot = [Environment]::GetEnvironmentVariable('CUDA_PATH', $scope)
if ($cudaRoot) { $candidates += (Join-Path $cudaRoot 'bin\nvcc.exe') }
}
$toolkitBase = 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA'
if (Test-Path -LiteralPath $toolkitBase) {
$highest = Get-ChildItem -Directory -LiteralPath $toolkitBase -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^v(\d+)\.(\d+)' } |
Sort-Object { [version]($_.Name -replace '^v','') } -Descending |
Select-Object -First 1
if ($highest) { $candidates += (Join-Path $highest.FullName 'bin\nvcc.exe') }
}
$onPath = Get-Command nvcc -ErrorAction SilentlyContinue
if ($onPath) { $candidates += $onPath.Source }
foreach ($nvcc in $candidates) {
if ($nvcc -and (Test-Path -LiteralPath $nvcc)) {
try {
$verOut = & $nvcc --version 2>&1 | Out-String
if ($verOut -match 'release\s+(\d+\.\d+)') { return $Matches[1] }
} catch {}
}
}
return $null
}
# ── Read driver's max CUDA from nvidia-smi (bound on toolkit install) ──
$DriverMaxCuda = $null
if ($NvidiaSmiExe) {
try {
$smiOut = & $NvidiaSmiExe 2>&1 | Out-String
if ($smiOut -match 'CUDA Version:\s+(\d+\.\d+)') { $DriverMaxCuda = $Matches[1] }
} catch {}
}
# ── Install CUDA Toolkit if missing, so Get-TorchIndexUrl can pick the ──
# PyTorch wheel from the *userland* CUDA version instead of nvidia-smi's
# driver-max ceiling. Mirrors phase 1e of setup.ps1; setup.ps1 still
# runs the full CUDA <-> VS Build Tools integration later. Bounded by
# the driver max so we never install a toolkit the driver can't load.
function Install-CudaToolkitIfMissing {
param([Parameter(Mandatory = $true)][string]$DriverMaxCuda)
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
substep "winget unavailable; skipping CUDA Toolkit install (studio setup will retry later)." "Yellow"
return
}
$drMajor = [int]$DriverMaxCuda.Split('.')[0]
$drMinor = [int]$DriverMaxCuda.Split('.')[1]
$available = @()
try {
$rawOutput = winget show Nvidia.CUDA --versions --accept-source-agreements 2>&1 | Out-String
foreach ($line in $rawOutput -split "`n") {
$line = $line.Trim()
if ($line -match '^\d+\.\d+') { $available += $line }
}
} catch {}
$best = $null
foreach ($ver in $available) {
$parts = $ver.Split('.')
$vMajor = [int]$parts[0]; $vMinor = [int]$parts[1]
if ($vMajor -lt $drMajor -or ($vMajor -eq $drMajor -and $vMinor -le $drMinor)) {
# winget show --versions returns the list in descending order,
# so the first compatible entry is the highest <= driver max.
$best = $ver
break
}
}
if (-not $best) {
substep "no CUDA Toolkit version <= driver $DriverMaxCuda available in winget (studio setup will retry later)." "Yellow"
return
}
substep "no CUDA Toolkit found; installing CUDA $best via winget (~3 GB, several minutes)..."
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
Invoke-InstallCommand { winget install --id=Nvidia.CUDA --version=$best -e --source winget --accept-package-agreements --accept-source-agreements } | Out-Null
} catch {}
$ErrorActionPreference = $prevEAP
Refresh-SessionPath
}
if ($HasNvidiaSmi -and -not $SkipTorch -and $DriverMaxCuda -and -not (Get-CudaToolkitVersion)) {
Write-TauriLog "STEP" "Installing CUDA Toolkit (winget)"
Install-CudaToolkitIfMissing -DriverMaxCuda $DriverMaxCuda
}
# ── Choose the correct PyTorch index URL based on installed CUDA ──
# Mirrors Get-PytorchCudaTag in setup.ps1. # Mirrors Get-PytorchCudaTag in setup.ps1.
#
# Prefer the installed CUDA Toolkit version: it is the hard constraint
# on which PyTorch wheels can actually load. nvidia-smi's "CUDA Version"
# is only the driver's *max supported* CUDA (a ceiling) — picking cu130
# off a "CUDA 13.0" driver that actually has toolkit 12.9 ships a torch
# that silently falls back to CPU. The toolkit install above eliminates
# the no-toolkit case on first-time installs; the nvidia-smi fallback
# here only fires when winget is unavailable or has no compatible
# toolkit, and setup.ps1 retries the install in phase 1e.
function Get-TorchIndexUrl { function Get-TorchIndexUrl {
$baseUrl = if ($env:UNSLOTH_PYTORCH_MIRROR) { $env:UNSLOTH_PYTORCH_MIRROR.TrimEnd('/') } else { "https://download.pytorch.org/whl" } $baseUrl = if ($env:UNSLOTH_PYTORCH_MIRROR) { $env:UNSLOTH_PYTORCH_MIRROR.TrimEnd('/') } else { "https://download.pytorch.org/whl" }
if (-not $NvidiaSmiExe) { return "$baseUrl/cpu" } if (-not $NvidiaSmiExe) { return "$baseUrl/cpu" }
try { $cudaVer = Get-CudaToolkitVersion
$output = & $NvidiaSmiExe 2>&1 | Out-String if (-not $cudaVer) { $cudaVer = $DriverMaxCuda }
if ($output -match 'CUDA Version:\s+(\d+)\.(\d+)') { if ($cudaVer -match '^(\d+)\.(\d+)$') {
$major = [int]$Matches[1]; $minor = [int]$Matches[2] $major = [int]$Matches[1]; $minor = [int]$Matches[2]
if ($major -ge 13) { return "$baseUrl/cu130" } if ($major -ge 13) { return "$baseUrl/cu130" }
if ($major -eq 12 -and $minor -ge 8) { return "$baseUrl/cu128" } if ($major -eq 12 -and $minor -ge 8) { return "$baseUrl/cu128" }
if ($major -eq 12 -and $minor -ge 6) { return "$baseUrl/cu126" } if ($major -eq 12 -and $minor -ge 6) { return "$baseUrl/cu126" }
if ($major -ge 12) { return "$baseUrl/cu124" } if ($major -ge 12) { return "$baseUrl/cu124" }
if ($major -ge 11) { return "$baseUrl/cu118" } if ($major -ge 11) { return "$baseUrl/cu118" }
return "$baseUrl/cpu" return "$baseUrl/cpu"
} }
} catch {} substep "could not determine CUDA version, defaulting to cu126" "Yellow"
substep "could not determine CUDA version from nvidia-smi, defaulting to cu126" "Yellow"
return "$baseUrl/cu126" return "$baseUrl/cu126"
} }
$TorchIndexUrl = Get-TorchIndexUrl $TorchIndexUrl = Get-TorchIndexUrl
@ -1248,6 +1348,21 @@ shell.Run cmd, 0, False
Write-Host "" Write-Host ""
} }
# ── Warn when CUDA wheel was picked from the driver report, not a toolkit ──
# Reached only when winget couldn't install a compatible CUDA Toolkit
# (no winget, or no <= driver-max version in winget's manifest). The
# PyTorch wheel may not load until a matching CUDA Toolkit is present;
# studio setup will retry the install, but if that also fails the user
# has to install it manually.
if ($HasNvidiaSmi -and -not $SkipTorch -and $TorchIndexFamily -like "cu*" -and -not (Get-CudaToolkitVersion)) {
Write-Host ""
substep "No CUDA Toolkit is installed; PyTorch wheel ($TorchIndexFamily) was picked from" "Yellow"
substep "your driver's max-CUDA report. Studio setup will retry the toolkit install;" "Yellow"
substep "if that also fails, install a CUDA Toolkit matching $TorchIndexFamily manually:" "Yellow"
substep " https://developer.nvidia.com/cuda-toolkit-archive" "Yellow"
Write-Host ""
}
# ── Install PyTorch first, then unsloth separately ── # ── Install PyTorch first, then unsloth separately ──
# #
# Why two steps? # Why two steps?

View file

@ -335,36 +335,55 @@ function Get-NvccMaxArch {
return $null return $null
} }
# Detect driver's max CUDA version from nvidia-smi and return the highest # Pick the PyTorch CUDA index tag (e.g. "cu128") for the installed stack.
# compatible PyTorch CUDA index tag (e.g. "cu128").
# PyTorch on Windows ships CPU-only by default from PyPI; CUDA wheels live at # PyTorch on Windows ships CPU-only by default from PyPI; CUDA wheels live at
# https://download.pytorch.org/whl/<tag>. The tag must not exceed the driver's # https://download.pytorch.org/whl/<tag>.
# capability: e.g. driver "CUDA Version: 12.9" → cu128 (not cu130). #
# Prefer the installed CUDA Toolkit version (already resolved into
# $script:NvccPath by phase 1e and proven <= driver max). nvidia-smi's
# "CUDA Version" line is only the driver's *max-supported* CUDA, which can
# be a release ahead of the userland — picking cu130 off a "CUDA 13.0"
# driver paired with a 12.9 toolkit ships a torch that silently falls back
# to CPU. Fall back to the driver report only when no toolkit is resolved
# (e.g. the function is called before phase 1e, or no GPU is present).
function Get-PytorchCudaTag { function Get-PytorchCudaTag {
$smiExe = if ($script:NvidiaSmiExe) { $script:NvidiaSmiExe } else { $major = $null
$cmd = Get-Command nvidia-smi -ErrorAction SilentlyContinue $minor = $null
if ($cmd) { $cmd.Source } else { $null } if ($script:NvccPath -and (Test-Path -LiteralPath $script:NvccPath)) {
try {
$verOut = & $script:NvccPath --version 2>&1 | Out-String
if ($verOut -match 'release\s+(\d+)\.(\d+)') {
$major = [int]$Matches[1]
$minor = [int]$Matches[2]
}
} catch { }
} }
if (-not $smiExe) { return "cu126" } if ($null -eq $major) {
$smiExe = if ($script:NvidiaSmiExe) { $script:NvidiaSmiExe } else {
try { $cmd = Get-Command nvidia-smi -ErrorAction SilentlyContinue
# 2>&1 | Out-String merges stderr into stdout then converts to a single if ($cmd) { $cmd.Source } else { $null }
# string. Plain 2>$null doesn't fully suppress stderr in PS 5.1 --
# ErrorRecord objects leak into $output and break the -match.
$output = & $smiExe 2>&1 | Out-String
if ($output -match 'CUDA Version:\s+(\d+)\.(\d+)') {
$major = [int]$Matches[1]
$minor = [int]$Matches[2]
# PyTorch 2.10 offers: cu124, cu126, cu128, cu130
if ($major -ge 13) { return "cu130" }
if ($major -eq 12 -and $minor -ge 8) { return "cu128" }
if ($major -eq 12 -and $minor -ge 6) { return "cu126" }
if ($major -ge 12) { return "cu124" }
if ($major -ge 11) { return "cu118" }
return "cpu"
} }
} catch { } if (-not $smiExe) { return "cu126" }
try {
# 2>&1 | Out-String merges stderr into stdout then converts to a single
# string. Plain 2>$null doesn't fully suppress stderr in PS 5.1 --
# ErrorRecord objects leak into $output and break the -match.
$output = & $smiExe 2>&1 | Out-String
if ($output -match 'CUDA Version:\s+(\d+)\.(\d+)') {
$major = [int]$Matches[1]
$minor = [int]$Matches[2]
}
} catch { }
}
if ($null -ne $major) {
# PyTorch 2.10 offers: cu118, cu124, cu126, cu128, cu130
if ($major -ge 13) { return "cu130" }
if ($major -eq 12 -and $minor -ge 8) { return "cu128" }
if ($major -eq 12 -and $minor -ge 6) { return "cu126" }
if ($major -ge 12) { return "cu124" }
if ($major -ge 11) { return "cu118" }
return "cpu"
}
return "cu126" return "cu126"
} }