MagicNodes / scripts /check_sageattention.ps1
DZRobo
Update comments and documentation for clarity
8e7e41a
raw
history blame
14.8 kB
<#
SageAttention Environment Checker/Installer (Windows, PowerShell)
- Detects Python, Torch, CUDA, GPU, and SageAttention version
- Warns about local shadow files (sageattention.py in Comfy root)
- Offers to install/upgrade SageAttention to 2.2.0
- If no wheel is available, can build from source (needs MSVC + CUDA Toolkit)
Usage:
powershell -ExecutionPolicy Bypass -File scripts\check_sageattention.ps1
or run scripts\check_sageattention.bat
#>
param(
[switch]$AutoYes,
[switch]$ForceSa2Source
)
function Write-Section($t){ Write-Host "`n=== $t ===" -ForegroundColor Cyan }
function Ask-YesNo($q){
if($AutoYes){ return $true }
$r = Read-Host "$q [y/N]"; return $r -match '^(?i:y|yes)$'
}
function Get-Python(){
$cands = @('python','py -3','py')
foreach($p in $cands){ try{ $v = & $p -c "import sys;print(sys.executable)" 2>$null; if($LASTEXITCODE -eq 0 -and $v){ return @{ exe=$p; path=$v.Trim() } } }catch{} }
return $null
}
function Py-Exec($pyExe, $code){
# Write code to a temporary .py to avoid complex quoting issues on Windows
$tmp = [System.IO.Path]::GetTempFileName()
$pyf = [System.IO.Path]::ChangeExtension($tmp, '.py')
Set-Content -Path $pyf -Value $code -Encoding UTF8
try { $out = & $pyExe $pyf } finally { Remove-Item -ErrorAction SilentlyContinue $tmp, $pyf }
return $out
}
function Invoke-Quiet($file, $argList, $label){
$logOut = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.out.log')
$logErr = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.err.log')
try {
if([string]::IsNullOrWhiteSpace($argList)){ throw "ArgumentList is empty for $file" }
$p = Start-Process -FilePath $file -ArgumentList $argList -NoNewWindow -PassThru `
-RedirectStandardOutput $logOut -RedirectStandardError $logErr
$spinner = @('|','/','-','\')
$i = 0
while(-not $p.HasExited){
Write-Host -NoNewline ("`r{0} {1}" -f $spinner[$i % $spinner.Count], $label)
Start-Sleep -Milliseconds 150
$i++
}
try { $p.Refresh() } catch {}
$exitCode = 1
try { $exitCode = [int]$p.ExitCode } catch { $exitCode = 1 }
if($exitCode -eq 0){
Write-Host ("`r{0} ... done " -f $label) -ForegroundColor Green
} else {
Write-Warning ("Failed: {0} (exit {1})" -f $label, $exitCode)
Write-Host "---- build tail ----" -ForegroundColor DarkYellow
if(Test-Path $logOut){ Get-Content $logOut -Tail 40 | ForEach-Object { Write-Host $_ -ForegroundColor DarkYellow } }
if(Test-Path $logErr){ Get-Content $logErr -Tail 40 | ForEach-Object { Write-Host $_ -ForegroundColor DarkYellow } }
Write-Host "--------------------" -ForegroundColor DarkYellow
}
return ($exitCode -eq 0)
} finally {
if(Test-Path $logOut){ Remove-Item -ErrorAction SilentlyContinue $logOut }
if(Test-Path $logErr){ Remove-Item -ErrorAction SilentlyContinue $logErr }
}
}
function Get-TorchInfo($pyExe){
$code = @'
try:
import torch
cuda = getattr(torch.version, "cuda", None)
is_cuda = torch.cuda.is_available()
name = torch.cuda.get_device_name(0) if is_cuda else ""
cc = ".".join(map(str, torch.cuda.get_device_capability(0))) if is_cuda else ""
print("|".join([torch.__version__, str(cuda or ""), "1" if is_cuda else "0", name.replace("|"," "), cc]))
except Exception:
print("")
'@
$out = Py-Exec $pyExe $code
if(-not $out){ return @{ has_torch=$false } }
$p = $out -split '\|'
if($p.Length -lt 3){ return @{ has_torch=$false } }
return @{ has_torch=$true; torch=$p[0]; cuda=$p[1]; is_cuda=($p[2] -eq '1'); device_name=($p[3] | ForEach-Object { $_ }); cc=($p[4] | ForEach-Object { $_ }) }
}
function Get-SageVersion($pyExe){
$code = @'
try:
import importlib, importlib.util
mod = None
for name in ("SageAttention","sageattention"):
try:
if importlib.util.find_spec(name) is not None:
mod = name; break
except Exception:
pass
if not mod:
print("")
else:
try:
import importlib.metadata as md
ver = md.version(mod)
except Exception:
ver = ""
print(f"{mod}|{ver}")
except Exception:
print("")
'@
$out = Py-Exec $pyExe $code
$p = $out -split '\|'
return @{ module=($p[0]); version=($p[1]) }
}
function Test-ShadowFile(){
# Look for a local sageattention.py that could shadow the package
$roots = @((Get-Location).Path)
# walk up to 3 parents
$d = Get-Item .
for($i=0;$i -lt 3;$i++){ $d = $d.PSParentPath; if(-not $d){ break }; $roots += $d }
foreach($r in $roots){ $f = Join-Path $r 'sageattention.py'; if(Test-Path $f){ return $f } }
return $null
}
Write-Section "Python"
$py = Get-Python
if(-not $py){ Write-Error "Python not found on PATH."; exit 1 }
Write-Host ("Using Python: {0} ({1})" -f $py.exe, $py.path)
Write-Section "Torch / CUDA / GPU"
$ti = Get-TorchInfo $py.path
if(-not $ti.has_torch){ Write-Warning "PyTorch not found: $($ti.err)" } else {
$ccdisp = if($ti.cc){ "sm_{0}" -f ($ti.cc -replace '\.','') } else { "-" }
Write-Host ("torch {0}, cuda {1}, cuda_available={2}, gpu='{3}', cc={4}" -f $ti.torch, $ti.cuda, $ti.is_cuda, $ti.device_name, $ccdisp)
}
Write-Section "SageAttention"
$target = 'SA2 (Attn2++)'
Write-Host ("Build target: {0}" -f $target) -ForegroundColor Green
$sv = Get-SageVersion $py.path
if($sv.module){
$svver = if($null -ne $sv.version -and $sv.version -ne ''){ $sv.version } else { 'unknown' }
Write-Host ("found module: {0} version: {1}" -f $sv.module, $svver)
} else {
Write-Host "not installed"
}
$shadow = Test-ShadowFile
if($shadow){ Write-Warning "Local file shadows package: $shadow"; if(Ask-YesNo "Rename to sageattention.py.disabled now?"){
Rename-Item -Path $shadow -NewName 'sageattention.py.disabled' -Force
Write-Host "Renamed."
}
}
$needInstall = $false
$wantSA3 = $false # SA3 disabled now
# Detect Windows and mark SA3 unsupported for now
$isWindows = ($env:OS -eq 'Windows_NT')
# Check if SA3 already present
$sa3check = @'
import importlib.util
print(importlib.util.find_spec("sageattn3") is not None)
'@
$sa3present = $false
if($wantSA3){
try {
$sa3present = ((Py-Exec $py.path $sa3check).Trim() -eq 'True')
} catch { $sa3present = $false }
}
if($wantSA3){
$needInstall = -not $sa3present
} else {
if(-not $sv.module){ $needInstall = $true }
else { try{ $ver=[Version]($sv.version -replace '[^0-9\.]',''); if($ver.Major -lt 2 -or ($ver.Major -eq 2 -and $ver.Minor -lt 2)){ $needInstall=$true } }catch{ $needInstall=$true } }
}
if(-not $needInstall){
Write-Host ("SageAttention present (target {0}) — nothing to do." -f $target) -ForegroundColor Green; exit 0
}
if($wantSA3){
if($isWindows){
Write-Warning "SageAttention 3 (Blackwell) is not supported on Windows currently. Falling back to SageAttention 2.2.x."
$wantSA3 = $false
}
}
if($wantSA3){
if(-not (Ask-YesNo "Install SageAttention SA3 (Blackwell) from source now?")){ Write-Host "Aborted by user."; exit 0 }
Write-Section "Installing (from source)"
$null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention SA3, please wait a few minutes"
# Ensure toolchain for source build
Write-Section "Toolchain check"
$hasCL = ($null -ne (Get-Command cl.exe -ErrorAction SilentlyContinue))
$hasNVCC = ($null -ne (Get-Command nvcc.exe -ErrorAction SilentlyContinue))
if(-not $hasCL -or -not $hasNVCC){
Write-Warning "MSVC cl or CUDA nvcc not found. Install MSVC Build Tools 2022 and CUDA Toolkit matching your torch (CUDA $($ti.cuda))."
exit 1
}
# Set CUDA arch list (e.g., 12.0 for Blackwell)
if($ti.is_cuda -and $ti.cc){ $env:TORCH_CUDA_ARCH_LIST = $ti.cc }
Write-Section "Building SA3 from source"
$null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps"
$sa3built = $false
# Install SA3 specifically from subdirectory
$env:GIT_TERMINAL_PROMPT = "0"
if($sa3present){ $null = Invoke-Quiet $py.path "-m pip uninstall -y sageattn3" "uninstall SA3 (old)" }
if(Invoke-Quiet $py.path "-m pip install -U --force-reinstall --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main#subdirectory=sageattention3_blackwell" "install SA3 from git subdir (main)"){ $sa3built = $true }
if(-not $sa3built){
# Try tags just in case
$tags = @('v2.2.1','v2.2.0','v2.2')
foreach($t in $tags){
if(Invoke-Quiet $py.path ("-m pip install -U --force-reinstall --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@{0}#subdirectory=sageattention3_blackwell" -f $t) ("install SA3 from git subdir: {0}" -f $t)){ $sa3built = $true; break }
}
}
try { $sa3present = ((Py-Exec $py.path $sa3check).Trim() -eq 'True') } catch { $sa3present = $false }
if(-not $sa3present){
Write-Warning "SA3 package not importable after installation. Possible env mismatch or build skipped."
# Minimal diagnostics to understand where pip installed to
$null = Invoke-Quiet $py.path "-m pip show -f sageattn3" "pip show sageattn3"
$diag = @'
import sys, site, importlib.util
print("py=", sys.executable)
paths = []
try:
paths += site.getsitepackages()
except Exception:
pass
try:
paths.append(site.getusersitepackages())
except Exception:
pass
print("site=", ";".join(paths))
spec = importlib.util.find_spec("sageattn3")
print("spec=", None if spec is None else (spec.origin or str(spec.submodule_search_locations)))
'@
$dout = Py-Exec $py.path $diag
if($dout){ Write-Host $dout -ForegroundColor DarkYellow }
}
} else {
if(-not (Ask-YesNo "Install/upgrade SageAttention to 2.2.x now?")){ Write-Host "Aborted by user."; exit 0 }
Write-Section "Installing (wheel if available)"
$null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention 2.2.x, please wait a few minutes"
# Remove older v1 if present to avoid 'already satisfied' noise
$null = Invoke-Quiet $py.path "-m pip uninstall -y SageAttention" "uninstall legacy SageAttention (if any)"
$null = Invoke-Quiet $py.path "-m pip uninstall -y sageattention" "uninstall legacy sageattention (if any)"
$wheelOk = Invoke-Quiet $py.path "-m pip install -U --no-cache-dir sageattention>=2.2,<3" "pip install sageattention 2.2.x (wheel)"
# Verify actual installed version >= 2.2; otherwise treat as failure to trigger fallback
$sv2 = Get-SageVersion $py.path
$wheelHas22 = $false
if($sv2.module -and $sv2.version){ try{ $v=[Version]($sv2.version -replace '[^0-9\.]',''); if($v.Major -gt 2 -or ($v.Major -eq 2 -and $v.Minor -ge 2)){ $wheelHas22=$true } }catch{}
}
if($wheelOk -and -not $wheelHas22){ Write-Warning "Wheel installation did not provide SageAttention >= 2.2. Falling back to source build."; $wheelOk=$false }
}
if(-not $wantSA3 -and -not $wheelOk){
Write-Warning "Wheel install failed - will try source build."
# Try to infer arch list
$arch = ''
if($ti.is_cuda -and $ti.cc){ $arch = $ti.cc }
if($arch){ $env:TORCH_CUDA_ARCH_LIST = $arch }
# Ensure toolchain
Write-Section "Toolchain check"
$hasCL = ($null -ne (Get-Command cl.exe -ErrorAction SilentlyContinue))
$hasNVCC = ($null -ne (Get-Command nvcc.exe -ErrorAction SilentlyContinue))
if(-not $hasCL -or -not $hasNVCC){
Write-Warning "MSVC cl or CUDA nvcc not found. Install MSVC Build Tools 2022 and CUDA Toolkit matching your torch (CUDA $($ti.cuda))."
exit 1
}
if($isWindows -and -not $ForceSa2Source){
Write-Warning "Attempting SageAttention 2.x source build on Windows (experimental upstream support)."
}
Write-Section "Building from source"
$null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps"
# Prefer upstream repo (thu-ml). First try main.zip to avoid git prompts
$built = $false
$urls = @(
'https://github.com/thu-ml/SageAttention/archive/refs/heads/main.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.1.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.0.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.zip'
)
foreach($u in $urls){
if(Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir `"$u`"" ("build from archive: {0}" -f $u)) { $built = $true; break }
}
if(-not $built){
Write-Warning "Tag archive not available; trying git main (noninteractive)."
$env:GIT_TERMINAL_PROMPT = "0"
$null = Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main" "build from git main"
}
}
# If SA3 was requested but still not importable, try SA2 as a fallback
if($wantSA3 -and -not $sa3present){
Write-Warning "Falling back to SageAttention 2.2.x (wheel/source)."
Write-Section "Installing SA2 (fallback)"
$null = Invoke-Quiet $py.path "-m pip install -U pip setuptools wheel" "Installing SageAttention 2.2.x, please wait a few minutes"
$wheelOk = Invoke-Quiet $py.path "-m pip install -U --no-cache-dir sageattention>=2.2,<3" "pip install sageattention 2.2.x (wheel)"
if(-not $wheelOk){
Write-Section "Building SA2 from source"
$null = Invoke-Quiet $py.path "-m pip install -U packaging cmake ninja" "toolchain python deps"
$built = $false
$urls = @(
'https://github.com/thu-ml/SageAttention/archive/refs/heads/main.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.1.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.0.zip',
'https://github.com/thu-ml/SageAttention/archive/refs/tags/v2.2.zip'
)
foreach($u in $urls){ if(Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir `"$u`"" ("build from archive: {0}" -f $u)) { $built = $true; break } }
if(-not $built){
$env:GIT_TERMINAL_PROMPT = "0"
$null = Invoke-Quiet $py.path "-m pip install --no-build-isolation --no-cache-dir git+https://github.com/thu-ml/SageAttention@main" "build from git main"
}
}
}
Write-Section "Validation"
$val = @'
try:
import importlib.util, torch
S = None
try:
import SageAttention as S
except Exception:
try:
import sageattention as S
except Exception:
S = None
sa_ok = False
if S is not None:
sa_ok = (
hasattr(S, 'sageattn_qk_int8_pv_fp16_cuda') or
hasattr(S, 'sageattn_qk_int8_pv_fp16_cuda_fp16')
)
if sa_ok:
print(f"OK: True torch {torch.__version__}")
else:
print(f"FAIL: torch {torch.__version__}")
except Exception as e:
print("ERR:", str(e))
'@
$out = Py-Exec $py.path $val
if($out -match '^OK:'){ Write-Host $out -ForegroundColor Green; Write-Host "Done." -ForegroundColor Green }
else { Write-Warning $out; Write-Warning "SageAttention not available. ComfyUI will fall back to stock attention (slower)." }