From b1cf687f59700558e285427ad45f24a9a4924468 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Sun, 18 Oct 2026 15:16:59 +0000 Subject: [PATCH 01/11] fix(rocm): scope torch upgrade and reinstall to the torch trio - Pass --upgrade-package and --reinstall-package for torch, torchvision and torchaudio instead of blanket --upgrade and --force-reinstall on the helper-managed ROCm torch install - Stop the resolver from re-evaluating transitive dependencies against the multi-arch index, where numpy is only published for cp312+ (2.5.2), which fails on 3.11 venvs and overrides per-package numpy pins (ComfyUI <2, reForge 1.26.4, Wan2GP 2.1.2) - Keep the torch pairing intact: amd-torch-device-gfx* and triton are exact-pinned by the resolved wheels, so they still follow the torch build, and raised floors are still enforced - Leave declared transitive needs to the package profile pip args, where they stay visible and reviewable - Make ApplyRocmTorchInstallOptions internal and pass it the overrides list so the emitted flags can be asserted - Add tests covering an upgrading profile (no blanket flags, three pairs of each scoped flag) and a non-upgrading one (reinstall without upgrade) --- .../Services/Rocm/RocmPackageHelper.cs | 39 ++++++++++++--- .../Core/RocmPackageHelperTests.cs | 50 ++++++++++++++++++- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 94e9a9b26..a8e68466b 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Injectio.Attributes; using NLog; using StabilityMatrix.Core.Exceptions; @@ -25,6 +25,18 @@ public class RocmPackageHelper : IRocmPackageHelper private static readonly StringComparer EnvComparer = StringComparer.OrdinalIgnoreCase; private const string RocmSdkDevelPackageName = "rocm-sdk-devel"; + + // The ROCm torch install only ever needs to move the torch trio. Bundling or upgrading anything + // wider lets the resolver re-evaluate transitive dependencies against the multi-arch index. + private static readonly string[] RocmTorchPackageNames = ["torch", "torchvision", "torchaudio"]; + private static readonly string[] RocmTorchUpgradePackageArgs = BuildRepeatablePackageArgs( + "--upgrade-package", + RocmTorchPackageNames + ); + private static readonly string[] RocmTorchReinstallPackageArgs = BuildRepeatablePackageArgs( + "--reinstall-package", + RocmTorchPackageNames + ); private static readonly string[] WindowsLaunchNoticeLines = [ "Stability Matrix Windows ROCm Notice: Windows AMD ROCm support is experimental. Please report any issues to Stability Matrix first so it can be determined whether the issue is package-specific.", @@ -280,7 +292,7 @@ public async Task InstallWindowsNativeTorchAsync( torchResolution.TorchvisionSpecifier ), installConfig, - installedPackage + installedPackage.PipOverrides ); if (torchResolution.AllowPrerelease) @@ -310,31 +322,42 @@ await VerifyWindowsNativeTorchInstallAsync(venvRunner, onConsoleOutput, cancella /// /// Applies the profile's upgrade/reinstall flags and any user pip overrides to a ROCm torch install command. + /// Scope is deliberately limited to the torch trio. A blanket --upgrade lets the resolver move + /// transitive dependencies against the multi-arch index, numpy foremost, where the only build may be + /// numpy 2.5.2 (cp312+). A blanket --force-reinstall reinstalls the whole graph for the same reason. + /// Packages that must follow the torch build, amd-torch-device-gfx* and triton, are exact-pinned by the + /// wheel metadata, so they are still replaced to satisfy the resolved pin. /// - private static PipInstallArgs ApplyRocmTorchInstallOptions( + internal static PipInstallArgs ApplyRocmTorchInstallOptions( PipInstallArgs args, PipInstallConfig installConfig, - InstalledPackage installedPackage + IEnumerable? pipOverrides ) { if (installConfig.UpgradePackages) { - args = args.AddArg("--upgrade"); + args = args.AddKeyedArgs("--upgrade-package", RocmTorchUpgradePackageArgs); } if (installConfig.ForceReinstallTorch) { - args = args.AddArg("--force-reinstall"); + args = args.AddKeyedArgs("--reinstall-package", RocmTorchReinstallPackageArgs); } - if (installedPackage.PipOverrides != null) + if (pipOverrides != null) { - args = args.WithUserOverrides(installedPackage.PipOverrides); + args = args.WithUserOverrides(pipOverrides.ToList()); } return args; } + /// + /// Expands a repeatable flag so each package gets its own flag value pair. + /// + private static string[] BuildRepeatablePackageArgs(string flag, IEnumerable packages) => + packages.SelectMany(package => new[] { flag, package }).ToArray(); + /// /// Builds the pip args for the resolved torch/torchvision pair. The specs are keyed so a user /// override replaces the pin instead of being appended next to it (which pip cannot satisfy). diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 16c52eebf..c95fc87a9 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -1,7 +1,9 @@ -using System.Text.Json; +using System.Text.Json; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper.HardwareInfo; +using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Rocm; +using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Services.Rocm; @@ -131,6 +133,52 @@ public void BuildRocmTorchArgs_PinsTorchAndTorchvisionWithDeviceExtras() StringAssert.Contains(text, "torchaudio"); } + [TestMethod] + public void ApplyRocmTorchInstallOptions_ScopesUpgradeAndReinstallToTorchTrio() + { + var args = RocmPackageHelper.ApplyRocmTorchInstallOptions( + new PipInstallArgs().AddArg("torch[device-gfx1201]==2.14.0"), + new PipInstallConfig { UpgradePackages = true, ForceReinstallTorch = true }, + pipOverrides: null + ); + + var values = args.Arguments.Select(argument => argument.Value).ToList(); + var expectedTargets = new[] { "torch", "torchvision", "torchaudio" }; + + // A blanket --upgrade would re-resolve numpy (and friends) against the multi-arch index; a blanket + // --force-reinstall would reinstall the whole graph. Neither may come back. + CollectionAssert.DoesNotContain(values, "--upgrade"); + CollectionAssert.DoesNotContain(values, "--force-reinstall"); + + Assert.AreEqual(3, values.Count(value => value == "--upgrade-package")); + Assert.AreEqual(3, values.Count(value => value == "--reinstall-package")); + CollectionAssert.AreEqual(expectedTargets, TargetsOf(values, "--upgrade-package")); + CollectionAssert.AreEqual(expectedTargets, TargetsOf(values, "--reinstall-package")); + } + + [TestMethod] + public void ApplyRocmTorchInstallOptions_WithoutUpgradePackages_ReinstallsTrioWithoutUpgrading() + { + var args = RocmPackageHelper.ApplyRocmTorchInstallOptions( + new PipInstallArgs().AddArg("torch[device-gfx1201]==2.14.0"), + new PipInstallConfig { UpgradePackages = false, ForceReinstallTorch = true }, + pipOverrides: null + ); + + var values = args.Arguments.Select(argument => argument.Value).ToList(); + + CollectionAssert.DoesNotContain(values, "--upgrade"); + CollectionAssert.DoesNotContain(values, "--upgrade-package"); + Assert.AreEqual(3, values.Count(value => value == "--reinstall-package")); + } + + private static string[] TargetsOf(List values, string flag) => + values + .Select((value, index) => (value, index)) + .Where(pair => pair.value == flag) + .Select(pair => values[pair.index + 1]) + .ToArray(); + [TestMethod] public void BuildRocmTorchArgs_UserTorchOverrideReplacesPinnedTorch() { From a10b5a93cc50c5bb88a74d54dcc8a0ff6d5ee0a1 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Sun, 18 Oct 2026 16:38:48 +0000 Subject: [PATCH 02/11] feat(rocm): add CDNA target support and fused-SDPA classification - Map MI100 and MI210 by name to gfx908 and gfx90a, covering the PCIe-slot Instincts - Add gfx908, gfx90a, gfx942 and gfx950 to TryGetCanonicalArchitecture; the stable multi-arch feed publishes device packages for all four - Leave gfx942 and gfx950 without name entries, since the feed ships linux_x86_64 device wheels for them and no win_amd64 ones, so resolving them from an adapter name would produce an install Windows cannot satisfy; they are picked up by the KFD detector on Linux - Add SupportsFusedSdpa/PreferMathSdpaFallback keyed to the AOTriton image list (gfx90a, gfx942, gfx950, gfx11xx, gfx120x) and route the math-SDP fallback through it, so CDNA2+ keeps its fused kernels while gfx908 falls back to math like any target without AOTriton images - Map RX 9050 to gfx1200 alongside the existing 9060 mapping - Cover the gate, the CDNA name resolution and the predicate with tests --- .../Helper/HardwareInfo/GpuInfo.cs | 6 ++- .../Models/Rocm/WindowsRocmSupport.cs | 31 ++++++++++++++- .../Services/Rocm/RocmPackageHelper.cs | 2 +- .../Core/RocmPackageHelperTests.cs | 38 +++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs index a19002dd8..7448904c8 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs @@ -83,7 +83,7 @@ public bool IsWindowsRocmSupportedGpu() { // RDNA4 _ when Has("R9700") || Has("R9600") || Has("9070") => "gfx1201", - _ when Has("9060") => "gfx1200", + _ when Has("9060") || Has("9050") => "gfx1200", // RDNA3.5 APUs _ when Has("860M") => "gfx1152", @@ -131,6 +131,10 @@ _ when Has("v520") => "gfx1011", // RDNA1 Navi10 XT _ when Has("5600") || Has("5700") => "gfx1010", + // CDNA (PCI-E slot Instincts) + _ when Has("mi100") => "gfx908", + _ when Has("mi210") => "gfx90a", + // Vega/GCN5 Dedicated GPUs _ when Has("rx vega") || Has("vega 64") || Has("vega 56") || Has("vega frontier") => "gfx900", _ when Has("radeon vii") || HasNoSpace("radeonvii") || Has("pro vii") || HasNoSpace("provii") => diff --git a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs b/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs index 01d8b0a1c..0041bba11 100644 --- a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs +++ b/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs @@ -1,4 +1,4 @@ -using StabilityMatrix.Core.Helper.HardwareInfo; +using StabilityMatrix.Core.Helper.HardwareInfo; namespace StabilityMatrix.Core.Models.Rocm; @@ -57,6 +57,24 @@ public static bool PreferLegacyAttentionFallback(string? gfxArch) return IsLegacyArchitecture(gfxArch); } + /// + /// True for targets that ship fused SDPA kernels, which on ROCm means the target has an AOTriton + /// image: RDNA3, RDNA3.5, RDNA4 and CDNA2 or newer. gfx908 and the GCN/RDNA1/RDNA2 targets have + /// no fused kernels and fall back to the math backend. + /// + public static bool SupportsFusedSdpa(string? gfxArch) + { + var canonicalArch = TryGetCanonicalArchitecture(gfxArch); + return canonicalArch is not null + && (IsModernArchitecture(canonicalArch) || canonicalArch is "gfx90a" or "gfx942" or "gfx950"); + } + + /// + /// True when the target has no fused SDPA kernels and the math backend has to be forced. + /// + public static bool PreferMathSdpaFallback(string? gfxArch) => + IsSupportedArchitecture(gfxArch) && !SupportsFusedSdpa(gfxArch); + public static bool UsesNightlyMultiArchPythonPackageIndex(string? gfxArch) { var canonicalArch = TryGetCanonicalArchitecture(gfxArch); @@ -73,7 +91,16 @@ public static bool UsesNightlyMultiArchPythonPackageIndex(string? gfxArch) return normalizedArch switch { - "gfx900" or "gfx906" or "gfx1150" or "gfx1151" or "gfx1152" or "gfx1153" => normalizedArch, + "gfx900" + or "gfx906" + or "gfx908" + or "gfx90a" + or "gfx942" + or "gfx950" + or "gfx1150" + or "gfx1151" + or "gfx1152" + or "gfx1153" => normalizedArch, var s when s.StartsWith("gfx101", StringComparison.Ordinal) || s.StartsWith("gfx103", StringComparison.Ordinal) diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index a8e68466b..cce3a3c48 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -819,7 +819,7 @@ RocmEnvironmentOptions options environment["TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL"] = "1"; } - if (options.ApplyLegacySdpFallback && WindowsRocmSupport.IsLegacyArchitecture(gfxArch)) + if (options.ApplyLegacySdpFallback && WindowsRocmSupport.PreferMathSdpaFallback(gfxArch)) { environment["TORCH_BACKENDS_CUDA_FLASH_SDP_ENABLED"] = "0"; environment["TORCH_BACKENDS_CUDA_MEM_EFF_SDP_ENABLED"] = "0"; diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index c95fc87a9..c452f4606 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -133,6 +133,44 @@ public void BuildRocmTorchArgs_PinsTorchAndTorchvisionWithDeviceExtras() StringAssert.Contains(text, "torchaudio"); } + [TestMethod] + [DataRow("gfx908")] + [DataRow("gfx90a")] + [DataRow("gfx942")] + [DataRow("gfx950")] + public void WindowsRocmSupport_IsSupportedArchitecture_AcceptsCdnaTargets(string arch) + { + Assert.IsTrue(WindowsRocmSupport.IsSupportedArchitecture(arch)); + } + + [TestMethod] + [DataRow("AMD Instinct MI100", "gfx908")] + [DataRow("AMD Instinct MI210", "gfx90a")] + public void GpuInfo_GetAmdGfxArch_ResolvesInstinctCdnaTargets(string name, string expected) + { + var gpu = new GpuInfo { Name = name }; + + Assert.AreEqual(expected, gpu.GetAmdGfxArch()); + } + + [TestMethod] + [DataRow("gfx908", true)] + [DataRow("gfx90a", false)] + [DataRow("gfx942", false)] + [DataRow("gfx950", false)] + [DataRow("gfx1030", true)] + [DataRow("gfx1100", false)] + [DataRow("gfx1201", false)] + public void WindowsRocmSupport_PreferMathSdpaFallback_MatchesAotritonImageList( + string arch, + bool expectFallback + ) + { + // AOTriton ships images for gfx90a, gfx942, gfx950, gfx11xx and gfx120x. Everything else has no + // fused SDPA kernels and must be forced onto the math backend. + Assert.AreEqual(expectFallback, WindowsRocmSupport.PreferMathSdpaFallback(arch)); + } + [TestMethod] public void ApplyRocmTorchInstallOptions_ScopesUpgradeAndReinstallToTorchTrio() { From 11c7f1d9faf4da064dfc0a2ba7a2cb44f6e652eb Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Mon, 19 Oct 2026 00:16:56 +0000 Subject: [PATCH 03/11] feat(rocm): scrub inherited ROCm/HIP SDK variables from helper environments - Add ProcessRunner.UnsetEnvironmentVariable, a sentinel value that removes the named variable from the child environment instead of setting it - Mark ROCM_PATH, ROCM_HOME, HIP_PATH, ROCM_DIR and HIP_DIR for removal in helper-managed launch environments so a system ROCm or HIP SDK cannot shadow the venv's self-contained pip SDK - Gate it behind RocmEnvironmentOptions.SanitizeRocmSdkEnvironment, default on, so installs that intentionally use a system or PyTorch-bundled ROCm can opt out - Apply the scrub before user environment overrides, so an explicit user value still wins - Cover the sentinel behaviour and the scrub with tests --- .../Models/Rocm/RocmEnvironmentOptions.cs | 7 ++++++ .../Models/Rocm/WindowsRocmSupport.cs | 14 +++++++++++ .../Processes/ProcessRunner.cs | 24 +++++++++++++++---- .../Services/Rocm/RocmPackageHelper.cs | 19 +++++++++++++++ .../Core/ProcessRunnerTests.cs | 18 ++++++++++++++ .../Core/RocmPackageHelperTests.cs | 19 +++++++++++++++ 6 files changed, 97 insertions(+), 4 deletions(-) diff --git a/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs b/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs index 88216ec12..380ef7379 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs @@ -15,6 +15,13 @@ public class RocmEnvironmentOptions /// public bool IncludeUserOverrides { get; init; } = true; + /// + /// When true, inherited system ROCm/HIP SDK variables are neutralized so a helper-managed, + /// self-contained ROCm SDK is used. Disable for installs that intentionally rely on a system + /// ROCm or a PyTorch-bundled runtime instead. + /// + public bool SanitizeRocmSdkEnvironment { get; init; } = true; + /// /// When set, overrides the default PyTorch allocator tuning string added by the ROCm helper. /// diff --git a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs b/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs index 0041bba11..0463530c1 100644 --- a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs +++ b/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs @@ -19,6 +19,20 @@ public static class WindowsRocmSupport "gfx1153", ]; + /// + /// System-level ROCm/HIP SDK variables that conflict with the self-contained pip ROCm SDK the + /// helper manages. They are removed from helper-managed child environments so a stale system + /// ROCm or HIP SDK install cannot shadow the venv toolchain or its compiled extensions. + /// + public static readonly string[] ConflictingRocmEnvironmentVariables = + [ + "ROCM_PATH", + "ROCM_HOME", + "HIP_PATH", + "ROCM_DIR", + "HIP_DIR", + ]; + public static bool IsSupportedGpu(GpuInfo? gpu) { if (gpu is null || !gpu.IsAmd || string.IsNullOrWhiteSpace(gpu.Name)) diff --git a/StabilityMatrix.Core/Processes/ProcessRunner.cs b/StabilityMatrix.Core/Processes/ProcessRunner.cs index 665e91fc6..f606bafac 100644 --- a/StabilityMatrix.Core/Processes/ProcessRunner.cs +++ b/StabilityMatrix.Core/Processes/ProcessRunner.cs @@ -10,6 +10,13 @@ public static class ProcessRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + /// + /// Sentinel value for an environment variable entry. When used as a value, the named variable is + /// removed from the child process environment instead of being set, which is how inherited system + /// settings are neutralized without passing an empty string that would still shadow the default. + /// + public const string UnsetEnvironmentVariable = "\0SM_UNSET\0"; + // Managed Python/uv subprocesses should not inherit ambient Python/venv activation state. private static readonly string[] PythonEnvironmentVariablesToSanitize = [ @@ -115,7 +122,15 @@ private static void ApplyEnvironmentVariables( var keys = new List(); foreach (var (key, value) in environmentVariables) { - info.EnvironmentVariables[key] = value; + if (value == UnsetEnvironmentVariable) + { + info.Environment.Remove(key); + } + else + { + info.EnvironmentVariables[key] = value; + } + keys.Add(key); } @@ -651,9 +666,10 @@ public static Task RunBashCommand( /// public static string Quote(string argument) { - var inner = argument.Length >= 2 && argument.StartsWith('"') && argument.EndsWith('"') - ? argument[1..^1] - : argument; + var inner = + argument.Length >= 2 && argument.StartsWith('"') && argument.EndsWith('"') + ? argument[1..^1] + : argument; if (!inner.Contains(' ') && !inner.Contains('"')) return argument; diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index cce3a3c48..89d0b9166 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -857,6 +857,13 @@ RocmEnvironmentOptions options } } + // Neutralize inherited system ROCm/HIP SDK variables so the venv's self-contained SDK is used. + // User overrides below can still intentionally re-set these keys. + if (options.SanitizeRocmSdkEnvironment) + { + ApplyRocmSdkEnvironmentSanitization(merged); + } + if ( options.IncludeUserOverrides && settingsManager.Settings.EnvironmentVariables is { Count: > 0 } userOverrides @@ -870,4 +877,16 @@ RocmEnvironmentOptions options return merged; } + + /// + /// Marks the inherited system ROCm/HIP SDK variables for removal from the child process, so the + /// venv's self-contained SDK wins over any system install that happens to be on PATH. + /// + internal static void ApplyRocmSdkEnvironmentSanitization(IDictionary environment) + { + foreach (var key in WindowsRocmSupport.ConflictingRocmEnvironmentVariables) + { + environment[key] = ProcessRunner.UnsetEnvironmentVariable; + } + } } diff --git a/StabilityMatrix.Tests/Core/ProcessRunnerTests.cs b/StabilityMatrix.Tests/Core/ProcessRunnerTests.cs index 9309303f9..137fd1086 100644 --- a/StabilityMatrix.Tests/Core/ProcessRunnerTests.cs +++ b/StabilityMatrix.Tests/Core/ProcessRunnerTests.cs @@ -6,6 +6,24 @@ namespace StabilityMatrix.Tests.Core; [TestClass] public class ProcessRunnerTests { + [TestMethod] + public void PrepareEnvironment_RemovesInheritedVariable_WhenUnsetSentinelIsUsed() + { + var info = new ProcessStartInfo(); + info.Environment["ROCM_PATH"] = "/opt/rocm"; + info.Environment["HIP_PATH"] = "/opt/rocm"; + + ProcessRunner.PrepareEnvironment( + info, + "python3", + new Dictionary { ["ROCM_PATH"] = ProcessRunner.UnsetEnvironmentVariable } + ); + + // The sentinel removes the inherited value instead of shadowing it with an empty string. + Assert.IsFalse(info.Environment.ContainsKey("ROCM_PATH")); + Assert.AreEqual("/opt/rocm", info.Environment["HIP_PATH"]); + } + [TestMethod] public void PrepareEnvironment_SanitizesInheritedPythonVariables_ForPythonProcesses() { diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index c452f4606..8ab52cb45 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -171,6 +171,25 @@ bool expectFallback Assert.AreEqual(expectFallback, WindowsRocmSupport.PreferMathSdpaFallback(arch)); } + [TestMethod] + public void ApplyRocmSdkEnvironmentSanitization_MarksInheritedSdkVariablesForRemoval() + { + var environment = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["ROCM_PATH"] = "/opt/rocm", + ["PATH"] = "/usr/bin", + }; + + RocmPackageHelper.ApplyRocmSdkEnvironmentSanitization(environment); + + foreach (var key in WindowsRocmSupport.ConflictingRocmEnvironmentVariables) + { + Assert.AreEqual(ProcessRunner.UnsetEnvironmentVariable, environment[key], key); + } + + Assert.AreEqual("/usr/bin", environment["PATH"]); + } + [TestMethod] public void ApplyRocmTorchInstallOptions_ScopesUpgradeAndReinstallToTorchTrio() { From baa806eaf1afa5bd194c08492d31a010340452aa Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Mon, 19 Oct 2026 09:42:08 +0000 Subject: [PATCH 04/11] refactor(rocm): make the helper surface and comments platform-neutral - Rename WindowsRocmSupport to RocmSupport, the six *WindowsRocmProfile types to *RocmProfile, and InstallWindowsRocmPackageCommandStep to InstallRocmPackageCommandStep - Rename the helper members that will be shared across platforms: GetLaunchNoticeLines, ShouldApplyLaunchEnvironment, ApplyRocmLaunchDefaults and EnsureRocmSdkDevelAsync - Rename the BaseGitPackage helpers to HasRocmSupport and GetRocmCompatibility, and the GPU predicates to HasRocmSupportedGpu, GetRocmSupportedGpu and IsRocmSupportedGpu - Rename IsUsableWindowsNativeTorchBuild to IsUsableRocmTorchBuild, plus the ComfyUI ROCm command runner and flag that carried a Windows prefix - Reword the comments and doc comments that described the helper's identity as Windows-specific rather than describing a Windows-specific code path: class summaries, the launch-option hooks, the AOTriton gate (which is arch-based, not OS-based), the compatibility and runtime context summaries, the ComfyUI eligibility and cross-attention notes, and the Forge Neo launch-notice note - Keep the Windows references where Windows is the actual subject: the bitsandbytes Windows wheel notes, the Reforge accelerate note about Windows ROCm torch builds, the DirectML branch, the Compat.IsWindows gate comments, the platform-contrast notes, and the two install members (BuildWindowsNativeInstallConfig, InstallWindowsNativeTorchAsync) that the profile seam removes later --- .../Helper/HardwareInfo/GpuInfo.cs | 4 +- .../Helper/HardwareInfo/HardwareHelper.cs | 8 +-- ...ep.cs => InstallRocmPackageCommandStep.cs} | 28 ++++---- .../Models/Packages/BaseGitPackage.cs | 6 +- .../Models/Packages/ComfyUI.cs | 65 +++++++++---------- .../Models/Packages/ForgeNeo.cs | 8 +-- .../Models/Packages/InvokeAI.cs | 14 ++-- .../Models/Packages/OneTrainer.cs | 14 ++-- .../Models/Packages/Reforge.cs | 17 ++--- .../Models/Packages/StableSwarm.cs | 2 +- .../Models/Packages/Wan2GP.cs | 21 +++--- ...dowsRocmProfile.cs => ComfyRocmProfile.cs} | 12 ++-- ...sRocmProfile.cs => ForgeNeoRocmProfile.cs} | 16 ++--- ...sRocmProfile.cs => InvokeAiRocmProfile.cs} | 8 +-- ...ocmProfile.cs => OneTrainerRocmProfile.cs} | 10 +-- ...wsRocmProfile.cs => ReforgeRocmProfile.cs} | 16 ++--- .../Models/Rocm/RocmEnvironmentOptions.cs | 4 +- .../Models/Rocm/RocmMachineState.cs | 4 +- .../Models/Rocm/RocmPackageProfile.cs | 10 +-- .../{WindowsRocmSupport.cs => RocmSupport.cs} | 4 +- ...owsRocmProfile.cs => Wan2GpRocmProfile.cs} | 10 +-- .../Services/Rocm/IRocmPackageHelper.cs | 18 ++--- .../Services/Rocm/RocmPackageHelper.cs | 50 +++++++------- .../Core/RocmPackageHelperTests.cs | 48 ++++++-------- 24 files changed, 189 insertions(+), 208 deletions(-) rename StabilityMatrix.Core/Models/PackageModification/{InstallWindowsRocmPackageCommandStep.cs => InstallRocmPackageCommandStep.cs} (90%) rename StabilityMatrix.Core/Models/Rocm/{ComfyWindowsRocmProfile.cs => ComfyRocmProfile.cs} (65%) rename StabilityMatrix.Core/Models/Rocm/{ForgeNeoWindowsRocmProfile.cs => ForgeNeoRocmProfile.cs} (87%) rename StabilityMatrix.Core/Models/Rocm/{InvokeAiWindowsRocmProfile.cs => InvokeAiRocmProfile.cs} (87%) rename StabilityMatrix.Core/Models/Rocm/{OneTrainerWindowsRocmProfile.cs => OneTrainerRocmProfile.cs} (87%) rename StabilityMatrix.Core/Models/Rocm/{ReforgeWindowsRocmProfile.cs => ReforgeRocmProfile.cs} (82%) rename StabilityMatrix.Core/Models/Rocm/{WindowsRocmSupport.cs => RocmSupport.cs} (97%) rename StabilityMatrix.Core/Models/Rocm/{Wan2GpWindowsRocmProfile.cs => Wan2GpRocmProfile.cs} (65%) diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs index 7448904c8..beedf1876 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/GpuInfo.cs @@ -62,9 +62,9 @@ public bool IsLegacyNvidiaGpu() return ComputeCapabilityValue < 7.5m; } - public bool IsWindowsRocmSupportedGpu() + public bool IsRocmSupportedGpu() { - return WindowsRocmSupport.IsSupportedGpu(this); + return RocmSupport.IsSupportedGpu(this); } public bool IsAmd => Name?.Contains("amd", StringComparison.OrdinalIgnoreCase) ?? false; diff --git a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs index 3efaa3c33..b8ce71d4b 100644 --- a/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs +++ b/StabilityMatrix.Core/Helper/HardwareInfo/HardwareHelper.cs @@ -320,11 +320,11 @@ public static bool HasAmdGpu() return IterGpuInfo().Any(gpu => gpu.IsAmd); } - public static bool HasWindowsRocmSupportedGpu() => IterGpuInfo().Any(WindowsRocmSupport.IsSupportedGpu); + public static bool HasRocmSupportedGpu() => IterGpuInfo().Any(RocmSupport.IsSupportedGpu); - public static GpuInfo? GetWindowsRocmSupportedGpu() + public static GpuInfo? GetRocmSupportedGpu() { - return IterGpuInfo().FirstOrDefault(WindowsRocmSupport.IsSupportedGpu); + return IterGpuInfo().FirstOrDefault(RocmSupport.IsSupportedGpu); } public static bool HasIntelGpu() => IterGpuInfo().Any(gpu => gpu.IsIntel); @@ -334,7 +334,7 @@ public static bool HasAmdGpu() // Set DirectML for default if AMD and Windows public static bool PreferDirectMLOrZluda() => - !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows && !HasWindowsRocmSupportedGpu(); + !HasNvidiaGpu() && HasAmdGpu() && Compat.IsWindows && !HasRocmSupportedGpu(); private static readonly Lazy IsMemoryInfoAvailableLazy = new(() => TryGetMemoryInfo(out _)); public static bool IsMemoryInfoAvailable => IsMemoryInfoAvailableLazy.Value; diff --git a/StabilityMatrix.Core/Models/PackageModification/InstallWindowsRocmPackageCommandStep.cs b/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs similarity index 90% rename from StabilityMatrix.Core/Models/PackageModification/InstallWindowsRocmPackageCommandStep.cs rename to StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs index 8621f6128..095d6c164 100644 --- a/StabilityMatrix.Core/Models/PackageModification/InstallWindowsRocmPackageCommandStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs @@ -1,4 +1,4 @@ -using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Rocm; @@ -9,7 +9,7 @@ namespace StabilityMatrix.Core.Models.PackageModification; -public enum WindowsRocmPackageCommandType +public enum RocmPackageCommandType { SageAttention, DevelopmentSdk, @@ -17,7 +17,7 @@ public enum WindowsRocmPackageCommandType FlashAttention, } -public class InstallWindowsRocmPackageCommandStep( +public class InstallRocmPackageCommandStep( IDownloadService downloadService, IPyInstallationManager pyInstallationManager, IPrerequisiteHelper prerequisiteHelper, @@ -44,16 +44,16 @@ IRocmPackageHelper rocmPackageHelper public required InstalledPackage InstalledPackage { get; init; } public required DirectoryPath WorkingDirectory { get; init; } - public required WindowsRocmPackageCommandType CommandType { get; init; } + public required RocmPackageCommandType CommandType { get; init; } public IReadOnlyDictionary? EnvironmentVariables { get; init; } public string ProgressTitle => CommandType switch { - WindowsRocmPackageCommandType.SageAttention => "Installing Windows ROCm SageAttention", - WindowsRocmPackageCommandType.DevelopmentSdk => "Installing Windows ROCm Development SDK", - WindowsRocmPackageCommandType.BitsAndBytes => "Installing Windows ROCm bitsandbytes", - WindowsRocmPackageCommandType.FlashAttention => "Installing Windows ROCm Flash Attention", + RocmPackageCommandType.SageAttention => "Installing Windows ROCm SageAttention", + RocmPackageCommandType.DevelopmentSdk => "Installing Windows ROCm Development SDK", + RocmPackageCommandType.BitsAndBytes => "Installing Windows ROCm bitsandbytes", + RocmPackageCommandType.FlashAttention => "Installing Windows ROCm Flash Attention", _ => "Running Windows ROCm package command", }; @@ -97,16 +97,16 @@ await pyInstallationManager.GetInstallationAsync(pyVersion).ConfigureAwait(false switch (CommandType) { - case WindowsRocmPackageCommandType.SageAttention: + case RocmPackageCommandType.SageAttention: await ExecuteSageAttentionAsync(venvRunner, progress).ConfigureAwait(false); break; - case WindowsRocmPackageCommandType.DevelopmentSdk: + case RocmPackageCommandType.DevelopmentSdk: await ExecuteDevelopmentSdkAsync(venvRunner, progress).ConfigureAwait(false); break; - case WindowsRocmPackageCommandType.BitsAndBytes: + case RocmPackageCommandType.BitsAndBytes: await ExecuteBitsAndBytesAsync(venvRunner, pyVersion, progress).ConfigureAwait(false); break; - case WindowsRocmPackageCommandType.FlashAttention: + case RocmPackageCommandType.FlashAttention: await ExecuteFlashAttentionAsync(venvRunner, progress).ConfigureAwait(false); break; default: @@ -144,7 +144,7 @@ private async Task ExecuteDevelopmentSdkAsync( ) { EnsureRocmCompatibility(); - await rocmPackageHelper.EnsureWindowsSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); + await rocmPackageHelper.EnsureRocmSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); } private async Task ExecuteSageAttentionAsync( @@ -154,7 +154,7 @@ private async Task ExecuteSageAttentionAsync( { EnsureRocmCompatibility(); await EnsureVcBuildToolsAsync(progress).ConfigureAwait(false); - await rocmPackageHelper.EnsureWindowsSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); + await rocmPackageHelper.EnsureRocmSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); progress?.Report( new ProgressReport( diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index 67c877bb0..ee04ddf7c 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -84,12 +84,12 @@ IPipWheelService pipWheelService PipWheelService = pipWheelService; } - protected bool HasWindowsRocmSupport(IRocmPackageHelper rocmPackageHelper) + protected bool HasRocmSupport(IRocmPackageHelper rocmPackageHelper) { - return GetWindowsRocmCompatibility(rocmPackageHelper).IsCompatible; + return GetRocmCompatibility(rocmPackageHelper).IsCompatible; } - protected RocmCompatibilityResult GetWindowsRocmCompatibility(IRocmPackageHelper rocmPackageHelper) + protected RocmCompatibilityResult GetRocmCompatibility(IRocmPackageHelper rocmPackageHelper) { if (!Compat.IsWindows) { diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index dc6c2cdc0..8c4bc0430 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -314,9 +314,7 @@ private static DirectoryPath GetSharedWorkflowsLinkDir(DirectoryPath installDire Name = "Enable DirectML", Type = LaunchOptionType.Bool, InitialValue = - !HasWindowsRocmSupport() - && HardwareHelper.PreferDirectMLOrZluda() - && this is not ComfyZluda, + !HasRocmSupport() && HardwareHelper.PreferDirectMLOrZluda() && this is not ComfyZluda, Options = ["--directml"], }, new() @@ -413,7 +411,7 @@ public override List GetExtraCommands() ); } - if (Compat.IsWindows && HasWindowsRocmSupport()) + if (Compat.IsWindows && HasRocmSupport()) { commands.Add( new ExtraPackageCommand @@ -428,10 +426,7 @@ public override List GetExtraCommands() { CommandName = "Install Flash Attention (ROCm)", Command = InstallWindowsRocmFlashAttention, - IsVisible = _ => - WindowsRocmSupport.IsLegacyArchitecture( - GetWindowsRocmCompatibility().ResolvedGfxArch - ), + IsVisible = _ => RocmSupport.IsLegacyArchitecture(GetRocmCompatibility().ResolvedGfxArch), } ); @@ -493,9 +488,9 @@ public override async Task InstallPackage( ?? HardwareHelper.HasLegacyNvidiaGpu() ); - if (Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasWindowsRocmSupport()) + if (Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasRocmSupport()) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(ComfyWindowsRocmProfile.Default); + var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(ComfyRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -512,7 +507,7 @@ await rocmPackageHelper .InstallWindowsNativeTorchAsync( venvRunner, installedPackage, - ComfyWindowsRocmProfile.Default, + ComfyRocmProfile.Default, progress, onConsoleOutput, cancellationToken @@ -545,7 +540,7 @@ await StandardPipInstallProcessAsync( .ConfigureAwait(false); } - if (!(Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasWindowsRocmSupport())) + if (!(Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasRocmSupport())) { try { @@ -690,7 +685,7 @@ private void EmitWindowsRocmLaunchNotice( ) { var torchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - foreach (var line in rocmPackageHelper.GetWindowsLaunchNoticeLines(torchIndex)) + foreach (var line in rocmPackageHelper.GetLaunchNoticeLines(torchIndex)) { onConsoleOutput?.Invoke(ProcessOutput.FromStdOutLine($"{line}{Environment.NewLine}")); } @@ -728,7 +723,7 @@ public override TorchIndex GetRecommendedTorchVersion() { var preferRocm = (Compat.IsLinux && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferRocm())) - || HasWindowsRocmSupport(); + || HasRocmSupport(); if (AvailableTorchIndices.Contains(TorchIndex.Rocm) && preferRocm) { @@ -738,26 +733,26 @@ public override TorchIndex GetRecommendedTorchVersion() return base.GetRecommendedTorchVersion(); } - /// Uses the shared ROCm helper for Windows ROCm eligibility checks so ComfyUI does not maintain its own support matrix. - private bool HasWindowsRocmSupport() + /// Uses the shared ROCm helper for ROCm eligibility checks so ComfyUI does not maintain its own support matrix. + private bool HasRocmSupport() { - return HasWindowsRocmSupport(rocmPackageHelper); + return HasRocmSupport(rocmPackageHelper); } - private RocmCompatibilityResult GetWindowsRocmCompatibility() + private RocmCompatibilityResult GetRocmCompatibility() { - return GetWindowsRocmCompatibility(rocmPackageHelper); + return GetRocmCompatibility(rocmPackageHelper); } - /// Defaults legacy Windows ROCm GPUs to quad cross-attention because PyTorch cross-attention is considerably slower + /// Defaults legacy ROCm GPUs to quad cross-attention because PyTorch cross-attention is considerably slower /// and not as supported on older AMD architectures. private bool DefaultToQuadCrossAttention() { - var compatibility = GetWindowsRocmCompatibility(); + var compatibility = GetRocmCompatibility(); if (!compatibility.IsCompatible) return false; - return WindowsRocmSupport.PreferLegacyAttentionFallback(compatibility.ResolvedGfxArch); + return RocmSupport.PreferLegacyAttentionFallback(compatibility.ResolvedGfxArch); } public override IPackageExtensionManager ExtensionManager => @@ -1057,9 +1052,9 @@ await PipWheelService private async Task InstallWindowsRocmSageAttention(InstalledPackage? installedPackage) { - var succeeded = await RunWindowsRocmPackageCommandAsync( + var succeeded = await RunRocmPackageCommandAsync( installedPackage, - WindowsRocmPackageCommandType.SageAttention, + RocmPackageCommandType.SageAttention, "Windows ROCm SageAttention installed successfully", includeEnvironmentVariables: true ) @@ -1073,9 +1068,9 @@ private async Task InstallWindowsRocmSageAttention(InstalledPackage? installedPa private async Task InstallWindowsRocmDevelopmentSdk(InstalledPackage? installedPackage) { - await RunWindowsRocmPackageCommandAsync( + await RunRocmPackageCommandAsync( installedPackage, - WindowsRocmPackageCommandType.DevelopmentSdk, + RocmPackageCommandType.DevelopmentSdk, "Windows ROCm Development SDK installed successfully", includeEnvironmentVariables: false ) @@ -1084,9 +1079,9 @@ await RunWindowsRocmPackageCommandAsync( private async Task InstallWindowsRocmBitsAndBytes(InstalledPackage? installedPackage) { - await RunWindowsRocmPackageCommandAsync( + await RunRocmPackageCommandAsync( installedPackage, - WindowsRocmPackageCommandType.BitsAndBytes, + RocmPackageCommandType.BitsAndBytes, "Windows ROCm bitsandbytes installed successfully", includeEnvironmentVariables: true ) @@ -1095,18 +1090,18 @@ await RunWindowsRocmPackageCommandAsync( private async Task InstallWindowsRocmFlashAttention(InstalledPackage? installedPackage) { - await RunWindowsRocmPackageCommandAsync( + await RunRocmPackageCommandAsync( installedPackage, - WindowsRocmPackageCommandType.FlashAttention, + RocmPackageCommandType.FlashAttention, "Windows ROCm Flash Attention installed successfully", includeEnvironmentVariables: true ) .ConfigureAwait(false); } - private async Task RunWindowsRocmPackageCommandAsync( + private async Task RunRocmPackageCommandAsync( InstalledPackage? installedPackage, - WindowsRocmPackageCommandType commandType, + RocmPackageCommandType commandType, string completionMessage, bool includeEnvironmentVariables ) @@ -1133,7 +1128,7 @@ bool includeEnvironmentVariables await runner .ExecuteSteps( [ - new InstallWindowsRocmPackageCommandStep( + new InstallRocmPackageCommandStep( downloadService, pyInstallationManager, prerequisiteHelper, @@ -1235,7 +1230,7 @@ InstalledPackage installedPackage ) { // if we're not on windows or we don't have a windows rocm gpu, return original env - var hasRocmGpu = HasWindowsRocmSupport(); + var hasRocmGpu = HasRocmSupport(); var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); // Add FFmpeg to PATH if it's installed (optional - for video processing) @@ -1253,7 +1248,7 @@ InstalledPackage installedPackage if (!Compat.IsWindows || !hasRocmGpu || selectedTorchIndex != TorchIndex.Rocm) return env; - var rocmEnvironment = rocmPackageHelper.BuildLaunchEnvironment(ComfyWindowsRocmProfile.Default); + var rocmEnvironment = rocmPackageHelper.BuildLaunchEnvironment(ComfyRocmProfile.Default); return env.SetItems(rocmEnvironment); } diff --git a/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs b/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs index 0dc0c29eb..2ae5eba78 100644 --- a/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs +++ b/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs @@ -49,7 +49,7 @@ IRocmPackageHelper rocmPackageHelper /// Windows installs use AMD repo wheels; Linux uses the upstream PyTorch ROCm index. /// private RocmPackageProfile ActiveRocmProfile => - Compat.IsWindows ? ForgeNeoWindowsRocmProfile.Default : LinuxRocmProfile; + Compat.IsWindows ? ForgeNeoRocmProfile.Default : LinuxRocmProfile; public override string Name => "forge-neo"; public override string DisplayName { get; set; } = "Stable Diffusion WebUI Forge - Neo"; @@ -103,7 +103,7 @@ public override List LaunchOptions } ); - ForgeNeoWindowsRocmProfile.Default.ApplyWindowsRocmLaunchDefaults(options, rocmPackageHelper); + ForgeNeoRocmProfile.Default.ApplyRocmLaunchDefaults(options, rocmPackageHelper); return options; } @@ -214,8 +214,8 @@ InstalledPackage installedPackage protected override IReadOnlyList GetLaunchNoticeLines(InstalledPackage installedPackage) { var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - // The helper returns an empty list on non-Windows platforms. - return rocmPackageHelper.GetWindowsLaunchNoticeLines(selectedTorchIndex); + // Launch notices only apply where the helper manages a native ROCm install. + return rocmPackageHelper.GetLaunchNoticeLines(selectedTorchIndex); } public override string Blurb => diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 870f598ff..81328b177 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -232,7 +232,7 @@ public override async Task InstallPackage( progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true)); var torchVersion = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); - var isSupportedWindowsRocmInstall = + var isSupportedRocmInstall = Compat.IsWindows && torchVersion == TorchIndex.Rocm && rocmPackageHelper.GetCompatibility().IsCompatible; @@ -268,7 +268,7 @@ public override async Task InstallPackage( torchVersion.ToString().ToLowerInvariant() ]?.GetValue(); - if (isSupportedWindowsRocmInstall) + if (isSupportedRocmInstall) { index = null; } @@ -286,9 +286,9 @@ public override async Task InstallPackage( await venvRunner.PipInstall(invokeInstallArgs, onConsoleOutput).ConfigureAwait(false); - if (isSupportedWindowsRocmInstall) + if (isSupportedRocmInstall) { - var installProfile = InvokeAiWindowsRocmProfile.CreateInstallProfile( + var installProfile = InvokeAiRocmProfile.CreateInstallProfile( options.PythonOptions.PythonVersion ?? RecommendedPythonVersion ); @@ -662,17 +662,17 @@ InstalledPackage installedPackage env = GetEnvVars(env, installPath); var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - if (!rocmPackageHelper.ShouldApplyWindowsLaunchEnvironment(selectedTorchIndex)) + if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) { return env; } - return env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(InvokeAiWindowsRocmProfile.Default)); + return env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(InvokeAiRocmProfile.Default)); } private IReadOnlyList GetLaunchNoticeLines(InstalledPackage installedPackage) { var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - return rocmPackageHelper.GetWindowsLaunchNoticeLines(selectedTorchIndex); + return rocmPackageHelper.GetLaunchNoticeLines(selectedTorchIndex); } } diff --git a/StabilityMatrix.Core/Models/Packages/OneTrainer.cs b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs index f6b69278c..76aac105f 100644 --- a/StabilityMatrix.Core/Models/Packages/OneTrainer.cs +++ b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs @@ -93,9 +93,7 @@ public override async Task InstallPackage( if (isWindowsRocm) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig( - OneTrainerWindowsRocmProfile.Default - ); + var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(OneTrainerRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -119,7 +117,7 @@ await rocmPackageHelper .InstallWindowsNativeTorchAsync( venvRunner, installedPackage, - OneTrainerWindowsRocmProfile.CreateInstallProfile( + OneTrainerRocmProfile.CreateInstallProfile( options.PythonOptions.PythonVersion ?? RecommendedPythonVersion ), progress, @@ -174,10 +172,10 @@ await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - if (rocmPackageHelper.ShouldApplyWindowsLaunchEnvironment(selectedTorchIndex)) + if (rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) { VenvRunner.UpdateEnvironmentVariables(env => - env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(OneTrainerWindowsRocmProfile.Default)) + env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(OneTrainerRocmProfile.Default)) ); } @@ -197,12 +195,12 @@ private IReadOnlyList GetLaunchNoticeLines(InstalledPackage installedPac { var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - if (!rocmPackageHelper.ShouldApplyWindowsLaunchEnvironment(selectedTorchIndex)) + if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) { return []; } - return [.. rocmPackageHelper.GetWindowsLaunchNoticeLines(selectedTorchIndex)]; + return [.. rocmPackageHelper.GetLaunchNoticeLines(selectedTorchIndex)]; } public override List LaunchOptions => [LaunchOptionDefinition.Extras]; diff --git a/StabilityMatrix.Core/Models/Packages/Reforge.cs b/StabilityMatrix.Core/Models/Packages/Reforge.cs index f31cb1dd8..665d9dde5 100644 --- a/StabilityMatrix.Core/Models/Packages/Reforge.cs +++ b/StabilityMatrix.Core/Models/Packages/Reforge.cs @@ -58,10 +58,7 @@ public override List LaunchOptions // Adjust inherited launch defaults for the Windows ROCm path before inserting reForge-specific options. // Makes the reForge-specific attention options visible in the UI and leaves them unset by default // for non-Windows-ROCm installs so reForge can keep using its normal internal attention selection. - ReforgeWindowsRocmProfile.Default.ApplyWindowsRocmLaunchDefaults( - baseLaunchOptions, - rocmPackageHelper - ); + ReforgeRocmProfile.Default.ApplyRocmLaunchDefaults(baseLaunchOptions, rocmPackageHelper); baseLaunchOptions.Insert( extrasIndex >= 0 ? extrasIndex : baseLaunchOptions.Count, @@ -69,7 +66,7 @@ public override List LaunchOptions { Name = "Cross Attention Method", Type = LaunchOptionType.Bool, - InitialValue = ReforgeWindowsRocmProfile.Default.GetPreferredCrossAttentionArgument( + InitialValue = ReforgeRocmProfile.Default.GetPreferredCrossAttentionArgument( rocmPackageHelper ), Options = ["--attention-split", "--attention-quad", "--attention-pytorch"], @@ -106,7 +103,7 @@ public override async Task InstallPackage( { var torchIndex = options.PythonOptions.TorchIndex ?? GetRecommendedTorchVersion(); - if (!rocmPackageHelper.ShouldApplyWindowsLaunchEnvironment(torchIndex)) + if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(torchIndex)) { await base.InstallPackage( installLocation, @@ -127,7 +124,7 @@ await base.InstallPackage( ) .ConfigureAwait(false); - var profile = ReforgeWindowsRocmProfile.CreateProfile(GetRequirementsPaths(installLocation)); + var profile = ReforgeRocmProfile.CreateProfile(GetRequirementsPaths(installLocation)); var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(profile); await StandardPipInstallProcessAsync( @@ -173,17 +170,17 @@ InstalledPackage installedPackage env = base.GetEnvVars(env, installedPackage); env = env.SetItem("STABLE_DIFFUSION_REPO", StableDiffusionRepoOverride); - if (!rocmPackageHelper.ShouldApplyWindowsLaunchEnvironment(selectedTorchIndex)) + if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) { return env; } - return env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(ReforgeWindowsRocmProfile.Default)); + return env.SetItems(rocmPackageHelper.BuildLaunchEnvironment(ReforgeRocmProfile.Default)); } protected override IReadOnlyList GetLaunchNoticeLines(InstalledPackage installedPackage) { var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - return rocmPackageHelper.GetWindowsLaunchNoticeLines(selectedTorchIndex); + return rocmPackageHelper.GetLaunchNoticeLines(selectedTorchIndex); } } diff --git a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs index b9d43abbb..49dcb5c06 100644 --- a/StabilityMatrix.Core/Models/Packages/StableSwarm.cs +++ b/StabilityMatrix.Core/Models/Packages/StableSwarm.cs @@ -593,7 +593,7 @@ x.PackageName is nameof(ComfyUI) or "ComfyUI-Zluda" return new Dictionary(); } - return rocmPackageHelper.BuildLaunchEnvironment(ComfyWindowsRocmProfile.Default); + return rocmPackageHelper.BuildLaunchEnvironment(ComfyRocmProfile.Default); } /// diff --git a/StabilityMatrix.Core/Models/Packages/Wan2GP.cs b/StabilityMatrix.Core/Models/Packages/Wan2GP.cs index 497599e9c..79ce53182 100644 --- a/StabilityMatrix.Core/Models/Packages/Wan2GP.cs +++ b/StabilityMatrix.Core/Models/Packages/Wan2GP.cs @@ -67,8 +67,7 @@ IRocmPackageHelper rocmPackageHelper public override IEnumerable AvailableTorchIndices => [TorchIndex.Cuda, TorchIndex.Rocm]; public override bool IsCompatible => - HardwareHelper.HasNvidiaGpu() - || (Compat.IsWindows ? HasWindowsRocmSupport() : HardwareHelper.HasAmdGpu()); + HardwareHelper.HasNvidiaGpu() || (Compat.IsWindows ? HasRocmSupport() : HardwareHelper.HasAmdGpu()); public override string MainBranch => "main"; public override bool ShouldIgnoreReleases => true; @@ -90,14 +89,14 @@ IRocmPackageHelper rocmPackageHelper /// private bool IsAmdRocm => GetRecommendedTorchVersion() == TorchIndex.Rocm; - private bool HasWindowsRocmSupport() + private bool HasRocmSupport() { - return HasWindowsRocmSupport(rocmPackageHelper); + return HasRocmSupport(rocmPackageHelper); } - private RocmCompatibilityResult GetWindowsRocmCompatibility() + private RocmCompatibilityResult GetRocmCompatibility() { - return GetWindowsRocmCompatibility(rocmPackageHelper); + return GetRocmCompatibility(rocmPackageHelper); } /// @@ -229,7 +228,7 @@ public override TorchIndex GetRecommendedTorchVersion() { // Check for AMD ROCm support (Windows or Linux) var preferRocm = - (Compat.IsWindows && HasWindowsRocmSupport()) + (Compat.IsWindows && HasRocmSupport()) || ( Compat.IsLinux && (SettingsManager.Settings.PreferredGpu?.IsAmd ?? HardwareHelper.PreferRocm()) @@ -389,7 +388,7 @@ CancellationToken cancellationToken { if (Compat.IsWindows) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(Wan2GpWindowsRocmProfile.Default); + var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(Wan2GpRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -406,7 +405,7 @@ await rocmPackageHelper .InstallWindowsNativeTorchAsync( venvRunner, installedPackage, - Wan2GpWindowsRocmProfile.Default, + Wan2GpRocmProfile.Default, progress, onConsoleOutput, cancellationToken @@ -450,9 +449,9 @@ await SetupVenv(installLocation, pythonVersion: PyVersion.Parse(installedPackage var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - if (Compat.IsWindows && selectedTorchIndex == TorchIndex.Rocm && HasWindowsRocmSupport()) + if (Compat.IsWindows && selectedTorchIndex == TorchIndex.Rocm && HasRocmSupport()) { - var rocmEnvironment = rocmPackageHelper.BuildLaunchEnvironment(Wan2GpWindowsRocmProfile.Default); + var rocmEnvironment = rocmPackageHelper.BuildLaunchEnvironment(Wan2GpRocmProfile.Default); VenvRunner.UpdateEnvironmentVariables(env => env.SetItems(rocmEnvironment)); } diff --git a/StabilityMatrix.Core/Models/Rocm/ComfyWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/ComfyRocmProfile.cs similarity index 65% rename from StabilityMatrix.Core/Models/Rocm/ComfyWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/ComfyRocmProfile.cs index 70cddbbb9..b6e955548 100644 --- a/StabilityMatrix.Core/Models/Rocm/ComfyWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/ComfyRocmProfile.cs @@ -1,13 +1,13 @@ -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Shared Windows ROCm profile for Comfy backends launched either directly by Stability Matrix or indirectly via SwarmUI. +/// Shared ROCm profile for Comfy backends launched either directly by Stability Matrix or indirectly via SwarmUI. /// -public class ComfyWindowsRocmProfile : RocmPackageProfile +public class ComfyRocmProfile : RocmPackageProfile { - public ComfyWindowsRocmProfile() + public ComfyRocmProfile() { InstallConfig = new PipInstallConfig { @@ -22,10 +22,10 @@ public ComfyWindowsRocmProfile() private IReadOnlyDictionary BuildEnvironment(RocmRuntimeContext runtimeContext) { - return WindowsRocmSupport.IsModernArchitecture(runtimeContext.RuntimeGfxArch) + return RocmSupport.IsModernArchitecture(runtimeContext.RuntimeGfxArch) ? new Dictionary { ["COMFYUI_ENABLE_MIOPEN"] = "1" } : new Dictionary(); } - public static RocmPackageProfile Default { get; } = new ComfyWindowsRocmProfile(); + public static RocmPackageProfile Default { get; } = new ComfyRocmProfile(); } diff --git a/StabilityMatrix.Core/Models/Rocm/ForgeNeoWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs similarity index 87% rename from StabilityMatrix.Core/Models/Rocm/ForgeNeoWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs index 69d5e56b3..5ab6533d7 100644 --- a/StabilityMatrix.Core/Models/Rocm/ForgeNeoWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs @@ -1,15 +1,15 @@ -using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services.Rocm; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Windows ROCm profile for Forge Neo. +/// ROCm profile for Forge Neo. /// Forge Neo has no native AMD support, so the helper owns the ROCm torch install and the launch /// environment, while Forge Neo's own launch.py still installs the rest of its dependencies. /// -public class ForgeNeoWindowsRocmProfile : RocmPackageProfile +public class ForgeNeoRocmProfile : RocmPackageProfile { private const string DisableSmartMemoryOptionName = "Disable Smart Memory"; private const string UsePytorchCrossAttentionOptionName = "Use PyTorch Cross Attention"; @@ -18,7 +18,7 @@ public class ForgeNeoWindowsRocmProfile : RocmPackageProfile private static readonly string[] EnableLaunchOptionNames = ["Pin Shared Memory", "CUDA Stream"]; private static readonly string[] DisableLaunchOptionNames = ["CUDA Malloc"]; - public ForgeNeoWindowsRocmProfile() + public ForgeNeoRocmProfile() { InstallConfig = new PipInstallConfig { @@ -31,12 +31,12 @@ public ForgeNeoWindowsRocmProfile() ExtraEnvironmentFactory = BuildEnvironment; } - public static ForgeNeoWindowsRocmProfile Default { get; } = new ForgeNeoWindowsRocmProfile(); + public static ForgeNeoRocmProfile Default { get; } = new ForgeNeoRocmProfile(); private static IReadOnlyDictionary BuildEnvironment(RocmRuntimeContext runtimeContext) { - var indexUrl = WindowsRocmSupport.GetMultiArchPythonPackageIndexUrl(runtimeContext.RuntimeGfxArch); - var deviceExtra = WindowsRocmSupport.TryGetMultiArchDeviceExtra(runtimeContext.RuntimeGfxArch); + var indexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl(runtimeContext.RuntimeGfxArch); + var deviceExtra = RocmSupport.TryGetMultiArchDeviceExtra(runtimeContext.RuntimeGfxArch); if (string.IsNullOrWhiteSpace(deviceExtra)) { @@ -56,7 +56,7 @@ private static IReadOnlyDictionary BuildEnvironment(RocmRuntimeC }; } - public override void ApplyWindowsRocmLaunchDefaults( + public override void ApplyRocmLaunchDefaults( List launchOptions, IRocmPackageHelper rocmPackageHelper ) diff --git a/StabilityMatrix.Core/Models/Rocm/InvokeAiWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/InvokeAiRocmProfile.cs similarity index 87% rename from StabilityMatrix.Core/Models/Rocm/InvokeAiWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/InvokeAiRocmProfile.cs index 23dae333b..503b43fca 100644 --- a/StabilityMatrix.Core/Models/Rocm/InvokeAiWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/InvokeAiRocmProfile.cs @@ -1,14 +1,14 @@ -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Shared Windows ROCm profile for InvokeAI. +/// Shared ROCm profile for InvokeAI. /// -public class InvokeAiWindowsRocmProfile : RocmPackageProfile +public class InvokeAiRocmProfile : RocmPackageProfile { - public static RocmPackageProfile Default { get; } = new InvokeAiWindowsRocmProfile(); + public static RocmPackageProfile Default { get; } = new InvokeAiRocmProfile(); //Restores flop counter functionality requiring triton module private const string TritonWindowsPackage = "triton-windows"; diff --git a/StabilityMatrix.Core/Models/Rocm/OneTrainerWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/OneTrainerRocmProfile.cs similarity index 87% rename from StabilityMatrix.Core/Models/Rocm/OneTrainerWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/OneTrainerRocmProfile.cs index 10f3c6a86..fc88a264e 100644 --- a/StabilityMatrix.Core/Models/Rocm/OneTrainerWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/OneTrainerRocmProfile.cs @@ -1,15 +1,15 @@ -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Shared Windows ROCm profile for OneTrainer. -/// Python 3.12 only - required by compatible bitsandbytes wheel. +/// Shared ROCm profile for OneTrainer. +/// Python 3.12 only - required by compatible bitsandbytes wheel on Windows. /// -public class OneTrainerWindowsRocmProfile : RocmPackageProfile +public class OneTrainerRocmProfile : RocmPackageProfile { - public static RocmPackageProfile Default { get; } = new OneTrainerWindowsRocmProfile(); + public static RocmPackageProfile Default { get; } = new OneTrainerRocmProfile(); // Restores flop counter functionality requiring triton module private const string TritonWindowsPackage = "triton-windows"; diff --git a/StabilityMatrix.Core/Models/Rocm/ReforgeWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/ReforgeRocmProfile.cs similarity index 82% rename from StabilityMatrix.Core/Models/Rocm/ReforgeWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/ReforgeRocmProfile.cs index 1ff866ca1..f2674cf48 100644 --- a/StabilityMatrix.Core/Models/Rocm/ReforgeWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/ReforgeRocmProfile.cs @@ -1,20 +1,20 @@ -using StabilityMatrix.Core.Helper; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Services.Rocm; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Shared Windows ROCm profile for reForge. +/// Shared ROCm profile for reForge. /// -public class ReforgeWindowsRocmProfile : RocmPackageProfile +public class ReforgeRocmProfile : RocmPackageProfile { // reForge currently pins accelerate 0.21.0, but 0.22.0 avoids the early distributed.torch import that breaks on Windows ROCm torch builds // caused by the marigold depth controlnet preprocessor - public const string WindowsRocmAccelerateVersion = "0.22.0"; + public const string AccelerateVersion = "0.22.0"; private static readonly string[] DisabledCudaLaunchOptionNames = ["CUDA Malloc", "CUDA Stream"]; - public static ReforgeWindowsRocmProfile Default { get; } = new ReforgeWindowsRocmProfile(); + public static ReforgeRocmProfile Default { get; } = new ReforgeRocmProfile(); public static RocmPackageProfile CreateProfile(IEnumerable requirementsFilePaths) { @@ -32,7 +32,7 @@ public static RocmPackageProfile CreateProfile(IEnumerable requirementsF [ "numpy==1.26.4", "setuptools==69.5.1", - $"accelerate=={WindowsRocmAccelerateVersion}", + $"accelerate=={AccelerateVersion}", ], PostTorchInstallPipArgs = [ @@ -46,7 +46,7 @@ public static RocmPackageProfile CreateProfile(IEnumerable requirementsF }; } - public override void ApplyWindowsRocmLaunchDefaults( + public override void ApplyRocmLaunchDefaults( List launchOptions, IRocmPackageHelper rocmPackageHelper ) @@ -76,7 +76,7 @@ IRocmPackageHelper rocmPackageHelper return null; } - return WindowsRocmSupport.PreferLegacyAttentionFallback(compatibility.ResolvedGfxArch) + return RocmSupport.PreferLegacyAttentionFallback(compatibility.ResolvedGfxArch) ? "--attention-quad" : "--attention-pytorch"; } diff --git a/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs b/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs index 380ef7379..b97eba32a 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmEnvironmentOptions.cs @@ -1,4 +1,4 @@ -namespace StabilityMatrix.Core.Models.Rocm; +namespace StabilityMatrix.Core.Models.Rocm; /// /// Controls how ROCm helper defaults, package-specific variables, and user overrides are layered at launch. @@ -48,7 +48,7 @@ public class RocmEnvironmentOptions public string? FlashAttentionTritonAmdEnable { get; init; } = "TRUE"; /// - /// When true, helper-managed defaults will enable ROCm AOTriton on modern Windows ROCm architectures. + /// When true, helper-managed defaults will enable ROCm AOTriton on modern ROCm architectures. /// public bool ApplyAotritonExperimental { get; init; } = true; diff --git a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs index bf6c07dc5..0c1f68dc1 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs @@ -1,9 +1,9 @@ -using StabilityMatrix.Core.Helper.HardwareInfo; +using StabilityMatrix.Core.Helper.HardwareInfo; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Captures the canonical Windows ROCm machine state resolved by the helper. +/// Captures the canonical ROCm machine state resolved by the helper. /// public class RocmMachineState { diff --git a/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs b/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs index 41e13f8f4..0df6535ab 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs @@ -1,4 +1,4 @@ -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Services.Rocm; @@ -11,7 +11,7 @@ namespace StabilityMatrix.Core.Models.Rocm; public class RocmPackageProfile { /// - /// Standard package install configuration used before the helper-managed Windows ROCm torch step. + /// Standard package install configuration used before the helper-managed ROCm torch step. /// public PipInstallConfig InstallConfig { get; init; } = new(); @@ -29,17 +29,17 @@ public Func< public RocmEnvironmentOptions EnvironmentOptions { get; init; } = new(); /// - /// Allows a package profile to adjust default launch options for Windows ROCm. + /// Allows a package profile to adjust default launch options for ROCm launches. /// Default implementation is a no-op; profiles that need package-specific adjustments /// should override this method. /// - public virtual void ApplyWindowsRocmLaunchDefaults( + public virtual void ApplyRocmLaunchDefaults( List launchOptions, IRocmPackageHelper rocmPackageHelper ) { } /// - /// Returns a package-specific preferred cross-attention argument for Windows ROCm launches. + /// Returns a package-specific preferred cross-attention argument for ROCm launches. /// Default implementation returns null indicating no preference. /// public virtual string? GetPreferredCrossAttentionArgument(IRocmPackageHelper rocmPackageHelper) diff --git a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs similarity index 97% rename from StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs rename to StabilityMatrix.Core/Models/Rocm/RocmSupport.cs index 0463530c1..be1d19636 100644 --- a/StabilityMatrix.Core/Models/Rocm/WindowsRocmSupport.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs @@ -3,10 +3,10 @@ namespace StabilityMatrix.Core.Models.Rocm; /// -/// Centralizes Windows ROCm support and architecture policy so hardware detection, package selection, +/// Centralizes ROCm support and architecture policy so hardware detection, package selection, /// installation, and shared launch decisions use the same support map. /// -public static class WindowsRocmSupport +public static class RocmSupport { public const string StableMultiArchPythonPackageIndexUrl = "https://stable.repo.amd.com/rocm/whl-next/"; public const string NightlyMultiArchPythonPackageIndexUrl = "https://nightly.repo.amd.com/rocm/whl-next/"; diff --git a/StabilityMatrix.Core/Models/Rocm/Wan2GpWindowsRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/Wan2GpRocmProfile.cs similarity index 65% rename from StabilityMatrix.Core/Models/Rocm/Wan2GpWindowsRocmProfile.cs rename to StabilityMatrix.Core/Models/Rocm/Wan2GpRocmProfile.cs index 91fa8e1db..1657616e6 100644 --- a/StabilityMatrix.Core/Models/Rocm/Wan2GpWindowsRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/Wan2GpRocmProfile.cs @@ -1,13 +1,13 @@ -using StabilityMatrix.Core.Models.Packages; +using StabilityMatrix.Core.Models.Packages; namespace StabilityMatrix.Core.Models.Rocm; /// -/// Shared Windows ROCm profile for Wan2GP. +/// Shared ROCm profile for Wan2GP. /// -public class Wan2GpWindowsRocmProfile : RocmPackageProfile +public class Wan2GpRocmProfile : RocmPackageProfile { - public Wan2GpWindowsRocmProfile() + public Wan2GpRocmProfile() { InstallConfig = new PipInstallConfig { @@ -17,5 +17,5 @@ public Wan2GpWindowsRocmProfile() }; } - public static RocmPackageProfile Default { get; } = new Wan2GpWindowsRocmProfile(); + public static RocmPackageProfile Default { get; } = new Wan2GpRocmProfile(); } diff --git a/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs index 84b60b2ee..25fdffebf 100644 --- a/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs @@ -1,4 +1,4 @@ -using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models.Packages; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Rocm; @@ -23,22 +23,22 @@ public interface IRocmPackageHelper IReadOnlyDictionary BuildLaunchEnvironment(RocmPackageProfile profile); /// - /// Returns shared Windows ROCm launch notice lines if the current machine and selected torch index - /// qualify for the Windows native ROCm launch environment; otherwise returns an empty list. + /// Returns shared ROCm launch notice lines when the selected torch index qualifies for the + /// helper-managed ROCm launch environment on this machine; otherwise returns an empty list. /// - IReadOnlyList GetWindowsLaunchNoticeLines(TorchIndex selectedTorchIndex); + IReadOnlyList GetLaunchNoticeLines(TorchIndex selectedTorchIndex); /// - /// Returns true when the current machine is Windows, the selected torch index is ROCm, - /// and the machine is compatible with Windows native ROCm. + /// Returns true when the selected torch index is ROCm and the helper can manage a native ROCm + /// install on the current machine. /// - bool ShouldApplyWindowsLaunchEnvironment(TorchIndex selectedTorchIndex); + bool ShouldApplyLaunchEnvironment(TorchIndex selectedTorchIndex); /// - /// Ensures a usable Windows ROCm SDK devel package is installed from the ROCm multi-arch index, + /// Ensures a usable ROCm SDK devel package is installed from the ROCm index, /// preferring the same build date token as the installed torch build and falling back to the latest available build. /// - Task EnsureWindowsSdkDevelAsync( + Task EnsureRocmSdkDevelAsync( IPyVenvRunner venvRunner, IProgress? progress = null, Action? onConsoleOutput = null, diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 89d0b9166..9e1babd59 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -37,7 +37,7 @@ public class RocmPackageHelper : IRocmPackageHelper "--reinstall-package", RocmTorchPackageNames ); - private static readonly string[] WindowsLaunchNoticeLines = + private static readonly string[] LaunchNoticeLines = [ "Stability Matrix Windows ROCm Notice: Windows AMD ROCm support is experimental. Please report any issues to Stability Matrix first so it can be determined whether the issue is package-specific.", "Because this setup may not be officially supported by package developers, only contact upstream support for issues clearly caused by the package itself.", @@ -56,7 +56,7 @@ public RocmPackageHelper(ISettingsManager settingsManager) } /// - /// Evaluates the current Windows machine state and returns the resolved ROCm compatibility result. + /// Evaluates the current machine state and returns the resolved ROCm compatibility result. /// public RocmCompatibilityResult GetCompatibility() { @@ -64,7 +64,7 @@ public RocmCompatibilityResult GetCompatibility() } /// - /// Resolves launch-time ROCm runtime details from the current Windows machine state. + /// Resolves launch-time ROCm runtime details from the current machine state. /// This is used to build helper-managed environment variables for package launch. /// private RocmRuntimeContext ResolveRuntimeContext() @@ -113,13 +113,13 @@ public IReadOnlyDictionary BuildLaunchEnvironment(RocmPackagePro return mergedEnvironment; } - private IReadOnlyList GetWindowsLaunchNoticeLines() + private IReadOnlyList GetLaunchNoticeLines() { - return WindowsLaunchNoticeLines; + return LaunchNoticeLines; } /// - public bool ShouldApplyWindowsLaunchEnvironment(TorchIndex selectedTorchIndex) + public bool ShouldApplyLaunchEnvironment(TorchIndex selectedTorchIndex) { if (!Compat.IsWindows || selectedTorchIndex != TorchIndex.Rocm) return false; @@ -127,16 +127,16 @@ public bool ShouldApplyWindowsLaunchEnvironment(TorchIndex selectedTorchIndex) } /// - public IReadOnlyList GetWindowsLaunchNoticeLines(TorchIndex selectedTorchIndex) + public IReadOnlyList GetLaunchNoticeLines(TorchIndex selectedTorchIndex) { - return ShouldApplyWindowsLaunchEnvironment(selectedTorchIndex) ? GetWindowsLaunchNoticeLines() : []; + return ShouldApplyLaunchEnvironment(selectedTorchIndex) ? GetLaunchNoticeLines() : []; } /// /// Ensures rocm-sdk-devel is installed from the ROCm multi-arch index. /// It prefers a build whose date token matches the installed ROCm torch build and falls back to the latest available build when no exact match is available. /// - public async Task EnsureWindowsSdkDevelAsync( + public async Task EnsureRocmSdkDevelAsync( IPyVenvRunner venvRunner, IProgress? progress = null, Action? onConsoleOutput = null, @@ -144,7 +144,7 @@ public async Task EnsureWindowsSdkDevelAsync( ) { var state = machineState.Value; - var multiArchPythonPackageIndexUrl = WindowsRocmSupport.GetMultiArchPythonPackageIndexUrl( + var multiArchPythonPackageIndexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl( state.RuntimeGfxArch ); @@ -156,7 +156,7 @@ public async Task EnsureWindowsSdkDevelAsync( ); } - if (!IsUsableWindowsNativeTorchBuild(torchInfo.Version, null)) + if (!IsUsableRocmTorchBuild(torchInfo.Version, null)) { throw new InvalidOperationException( $"Installed torch is not a usable Windows ROCm build (detected version: {torchInfo.Version})." @@ -262,12 +262,10 @@ public async Task InstallWindowsNativeTorchAsync( } var installConfig = profile.InstallConfig; - var multiArchPythonPackageIndexUrl = WindowsRocmSupport.GetMultiArchPythonPackageIndexUrl( - state.RuntimeGfxArch - ); - var usesNightlyIndex = WindowsRocmSupport.UsesNightlyMultiArchPythonPackageIndex( + var multiArchPythonPackageIndexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl( state.RuntimeGfxArch ); + var usesNightlyIndex = RocmSupport.UsesNightlyMultiArchPythonPackageIndex(state.RuntimeGfxArch); // AMD's torchvision wheels carry no torch constraint, and the matched build may be published only // as a pre-release that pip skips. Resolve the newest paired torch/torchvision and pin both. @@ -552,7 +550,7 @@ private RocmMachineState ResolveWindowsMachineState() }; } - var supportedAmdGpus = amdGpus.Where(WindowsRocmSupport.IsSupportedGpu).ToList(); + var supportedAmdGpus = amdGpus.Where(RocmSupport.IsSupportedGpu).ToList(); if (supportedAmdGpus.Count == 0) { return new RocmMachineState @@ -566,7 +564,7 @@ private RocmMachineState ResolveWindowsMachineState() TryResolvePreferredAmdGpu(supportedAmdGpus, settingsManager.Settings.PreferredGpu) ?? supportedAmdGpus.First(); var runtimeGfxArch = - WindowsRocmSupport.TryGetCanonicalArchitecture(selectedGpu.GetAmdGfxArch()) + RocmSupport.TryGetCanonicalArchitecture(selectedGpu.GetAmdGfxArch()) ?? GetSupportedFallbackGfxArch(supportedAmdGpus); var isCompatible = !string.IsNullOrWhiteSpace(runtimeGfxArch); @@ -578,7 +576,7 @@ private RocmMachineState ResolveWindowsMachineState() : "No supported AMD GFX architecture could be resolved for ROCm.", SelectedGpu = selectedGpu, RuntimeGfxArch = runtimeGfxArch, - MultiArchDeviceExtra = WindowsRocmSupport.TryGetMultiArchDeviceExtra(runtimeGfxArch), + MultiArchDeviceExtra = RocmSupport.TryGetMultiArchDeviceExtra(runtimeGfxArch), }; } @@ -623,9 +621,9 @@ private static IReadOnlyList GetAmdGpuCandidates() private static string? GetSupportedFallbackGfxArch(IEnumerable availableGpus) { return availableGpus - .Where(WindowsRocmSupport.IsSupportedGpu) - .Select(gpu => WindowsRocmSupport.TryGetCanonicalArchitecture(gpu.GetAmdGfxArch())) - .FirstOrDefault(WindowsRocmSupport.IsSupportedArchitecture); + .Where(RocmSupport.IsSupportedGpu) + .Select(gpu => RocmSupport.TryGetCanonicalArchitecture(gpu.GetAmdGfxArch())) + .FirstOrDefault(RocmSupport.IsSupportedArchitecture); } /// @@ -695,7 +693,7 @@ CancellationToken cancellationToken var hipVersion = root.TryGetProperty("hip", out var hipElement) ? hipElement.GetString() : null; var cudaAvailable = root.TryGetProperty("cuda", out var cudaElement) && cudaElement.GetBoolean(); - if (!IsUsableWindowsNativeTorchBuild(version, hipVersion)) + if (!IsUsableRocmTorchBuild(version, hipVersion)) { throw new InvalidOperationException( $"Installed torch is not a usable ROCm build. Verification output: {verificationOutput}" @@ -729,7 +727,7 @@ CancellationToken cancellationToken } } - internal static bool IsUsableWindowsNativeTorchBuild(string? version, string? hipVersion) + internal static bool IsUsableRocmTorchBuild(string? version, string? hipVersion) { if (!string.IsNullOrWhiteSpace(hipVersion)) return true; @@ -814,12 +812,12 @@ RocmEnvironmentOptions options SetIfNotNull(environment, "MIOPEN_FIND_ENFORCE", options.MiopenFindEnforce); SetIfNotNull(environment, "PYTORCH_ALLOC_CONF", options.PyTorchAllocConf); - if (options.ApplyAotritonExperimental && WindowsRocmSupport.SupportsAotritonExperimental(gfxArch)) + if (options.ApplyAotritonExperimental && RocmSupport.SupportsAotritonExperimental(gfxArch)) { environment["TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL"] = "1"; } - if (options.ApplyLegacySdpFallback && WindowsRocmSupport.PreferMathSdpaFallback(gfxArch)) + if (options.ApplyLegacySdpFallback && RocmSupport.PreferMathSdpaFallback(gfxArch)) { environment["TORCH_BACKENDS_CUDA_FLASH_SDP_ENABLED"] = "0"; environment["TORCH_BACKENDS_CUDA_MEM_EFF_SDP_ENABLED"] = "0"; @@ -884,7 +882,7 @@ RocmEnvironmentOptions options /// internal static void ApplyRocmSdkEnvironmentSanitization(IDictionary environment) { - foreach (var key in WindowsRocmSupport.ConflictingRocmEnvironmentVariables) + foreach (var key in RocmSupport.ConflictingRocmEnvironmentVariables) { environment[key] = ProcessRunner.UnsetEnvironmentVariable; } diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 8ab52cb45..4e7d6c087 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -13,33 +13,33 @@ namespace StabilityMatrix.Tests.Core; public class RocmPackageHelperTests { [TestMethod] - public void WindowsRocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForSupportedArch() + public void RocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForSupportedArch() { - var deviceExtra = WindowsRocmSupport.TryGetMultiArchDeviceExtra("gfx1201"); + var deviceExtra = RocmSupport.TryGetMultiArchDeviceExtra("gfx1201"); Assert.AreEqual("device-gfx1201", deviceExtra); } [TestMethod] - public void WindowsRocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForCanonicalVega20Arch() + public void RocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForCanonicalVega20Arch() { - var deviceExtra = WindowsRocmSupport.TryGetMultiArchDeviceExtra("gfx906"); + var deviceExtra = RocmSupport.TryGetMultiArchDeviceExtra("gfx906"); Assert.AreEqual("device-gfx906", deviceExtra); } [TestMethod] - public void WindowsRocmSupport_TryGetCanonicalArchitecture_ReturnsCanonicalArch_WhenAlreadyCanonical() + public void RocmSupport_TryGetCanonicalArchitecture_ReturnsCanonicalArch_WhenAlreadyCanonical() { - var canonicalArch = WindowsRocmSupport.TryGetCanonicalArchitecture("gfx906"); + var canonicalArch = RocmSupport.TryGetCanonicalArchitecture("gfx906"); Assert.AreEqual("gfx906", canonicalArch); } [TestMethod] - public void IsUsableWindowsNativeTorchBuild_ReturnsTrue_WhenHipMetadataExists() + public void IsUsableRocmTorchBuild_ReturnsTrue_WhenHipMetadataExists() { - var isUsable = RocmPackageHelper.IsUsableWindowsNativeTorchBuild( + var isUsable = RocmPackageHelper.IsUsableRocmTorchBuild( version: "test-version", hipVersion: "test-hip-version" ); @@ -48,9 +48,9 @@ public void IsUsableWindowsNativeTorchBuild_ReturnsTrue_WhenHipMetadataExists() } [TestMethod] - public void IsUsableWindowsNativeTorchBuild_ReturnsTrue_WhenVersionContainsRocm() + public void IsUsableRocmTorchBuild_ReturnsTrue_WhenVersionContainsRocm() { - var isUsable = RocmPackageHelper.IsUsableWindowsNativeTorchBuild( + var isUsable = RocmPackageHelper.IsUsableRocmTorchBuild( version: "test-version+rocm", hipVersion: null ); @@ -59,12 +59,9 @@ public void IsUsableWindowsNativeTorchBuild_ReturnsTrue_WhenVersionContainsRocm( } [TestMethod] - public void IsUsableWindowsNativeTorchBuild_ReturnsFalse_WhenNoRocmMetadataExists() + public void IsUsableRocmTorchBuild_ReturnsFalse_WhenNoRocmMetadataExists() { - var isUsable = RocmPackageHelper.IsUsableWindowsNativeTorchBuild( - version: "test-version", - hipVersion: null - ); + var isUsable = RocmPackageHelper.IsUsableRocmTorchBuild(version: "test-version", hipVersion: null); Assert.IsFalse(isUsable); } @@ -102,19 +99,19 @@ public void TryExtractJsonObject_ReturnsNull_WhenOutputContainsNoJson() } [TestMethod] - public void WindowsRocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForKrakenPoint() + public void RocmSupport_TryGetMultiArchDeviceExtra_ReturnsExpectedExtra_ForKrakenPoint() { - var deviceExtra = WindowsRocmSupport.TryGetMultiArchDeviceExtra("gfx1152"); + var deviceExtra = RocmSupport.TryGetMultiArchDeviceExtra("gfx1152"); Assert.AreEqual("device-gfx1152", deviceExtra); } [TestMethod] - public void WindowsRocmSupport_IsSupportedGpu_ReturnsTrue_ForSupportedAmdGpu() + public void RocmSupport_IsSupportedGpu_ReturnsTrue_ForSupportedAmdGpu() { var gpu = new GpuInfo { Name = "AMD Radeon RX 9070 XT", MemoryBytes = 16UL * Size.GiB }; - Assert.IsTrue(WindowsRocmSupport.IsSupportedGpu(gpu)); + Assert.IsTrue(RocmSupport.IsSupportedGpu(gpu)); } [TestMethod] @@ -138,9 +135,9 @@ public void BuildRocmTorchArgs_PinsTorchAndTorchvisionWithDeviceExtras() [DataRow("gfx90a")] [DataRow("gfx942")] [DataRow("gfx950")] - public void WindowsRocmSupport_IsSupportedArchitecture_AcceptsCdnaTargets(string arch) + public void RocmSupport_IsSupportedArchitecture_AcceptsCdnaTargets(string arch) { - Assert.IsTrue(WindowsRocmSupport.IsSupportedArchitecture(arch)); + Assert.IsTrue(RocmSupport.IsSupportedArchitecture(arch)); } [TestMethod] @@ -161,14 +158,11 @@ public void GpuInfo_GetAmdGfxArch_ResolvesInstinctCdnaTargets(string name, strin [DataRow("gfx1030", true)] [DataRow("gfx1100", false)] [DataRow("gfx1201", false)] - public void WindowsRocmSupport_PreferMathSdpaFallback_MatchesAotritonImageList( - string arch, - bool expectFallback - ) + public void RocmSupport_PreferMathSdpaFallback_MatchesAotritonImageList(string arch, bool expectFallback) { // AOTriton ships images for gfx90a, gfx942, gfx950, gfx11xx and gfx120x. Everything else has no // fused SDPA kernels and must be forced onto the math backend. - Assert.AreEqual(expectFallback, WindowsRocmSupport.PreferMathSdpaFallback(arch)); + Assert.AreEqual(expectFallback, RocmSupport.PreferMathSdpaFallback(arch)); } [TestMethod] @@ -182,7 +176,7 @@ public void ApplyRocmSdkEnvironmentSanitization_MarksInheritedSdkVariablesForRem RocmPackageHelper.ApplyRocmSdkEnvironmentSanitization(environment); - foreach (var key in WindowsRocmSupport.ConflictingRocmEnvironmentVariables) + foreach (var key in RocmSupport.ConflictingRocmEnvironmentVariables) { Assert.AreEqual(ProcessRunner.UnsetEnvironmentVariable, environment[key], key); } From 1d67bf9a073a0e9909b5c8562f216214a4ddd22f Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Tue, 20 Oct 2026 00:30:09 +0000 Subject: [PATCH 05/11] feat(rocm): resolve the install target and index from the architecture - Add RocmInstallTarget and RocmIndexKind (AMD stable, AMD nightly, PyTorch rocm7.2, PyTorch rocm7.14); the target carries the index URL, the device extra and the index's capabilities: per-architecture device packages, SDK devel and whether torch must be paired with torchvision - Add RocmSupport.ResolveTarget, resolving those from the architecture and an optional profile override, keeping Vega on AMD's nightly feed and everything else on stable - Replace GetMultiArchPythonPackageIndexUrl with GetIndexUrl plus ResolveTarget, so the URL follows the index kind rather than being inferred at each call site - Read the index URL and device extra from the target in InstallWindowsNativeTorchAsync, EnsureRocmSdkDevelAsync and the Forge Neo profile; the SDK step returns early when the index publishes no rocm-sdk-devel - Drop RocmMachineState.MultiArchDeviceExtra, since the target owns that decision now, and add RocmPackageProfile.Index for per-package overrides - Windows behaviour is unchanged: same feed URLs, same device extra, same nightly rule - Cover Windows stable, Vega nightly, the AMD feed, a PyTorch override with no device extras or SDK step, and an unsupported architecture --- .../Models/Rocm/ForgeNeoRocmProfile.cs | 11 ++-- .../Models/Rocm/RocmInstallTarget.cs | 45 ++++++++++++++++ .../Models/Rocm/RocmMachineState.cs | 2 - .../Models/Rocm/RocmPackageProfile.cs | 6 +++ .../Models/Rocm/RocmSupport.cs | 43 +++++++++++++-- .../Services/Rocm/RocmPackageHelper.cs | 31 +++++++---- .../Core/RocmPackageHelperTests.cs | 53 +++++++++++++++++++ 7 files changed, 168 insertions(+), 23 deletions(-) create mode 100644 StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs diff --git a/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs b/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs index 5ab6533d7..2a14b2a5b 100644 --- a/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/ForgeNeoRocmProfile.cs @@ -35,10 +35,9 @@ public ForgeNeoRocmProfile() private static IReadOnlyDictionary BuildEnvironment(RocmRuntimeContext runtimeContext) { - var indexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl(runtimeContext.RuntimeGfxArch); - var deviceExtra = RocmSupport.TryGetMultiArchDeviceExtra(runtimeContext.RuntimeGfxArch); + var target = RocmSupport.ResolveTarget(runtimeContext.RuntimeGfxArch); - if (string.IsNullOrWhiteSpace(deviceExtra)) + if (string.IsNullOrWhiteSpace(target.DeviceExtra)) { return new Dictionary(); } @@ -46,12 +45,12 @@ private static IReadOnlyDictionary BuildEnvironment(RocmRuntimeC // Persisted so that if Forge Neo re-runs its own torch install step it targets AMD's index // instead of pulling the NVIDIA CUDA build from PyTorch. var torchCommand = - $"pip install --index-url {indexUrl} " - + $"\"torch[{deviceExtra}]\" \"torchvision[{deviceExtra}]\" torchaudio \"rocm[devel]\""; + $"pip install --index-url {target.IndexUrl} " + + $"\"torch[{target.DeviceExtra}]\" \"torchvision[{target.DeviceExtra}]\" torchaudio \"rocm[devel]\""; return new Dictionary { - ["TORCH_INDEX_URL"] = indexUrl, + ["TORCH_INDEX_URL"] = target.IndexUrl, ["TORCH_COMMAND"] = torchCommand, }; } diff --git a/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs b/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs new file mode 100644 index 000000000..6dc23bd8f --- /dev/null +++ b/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs @@ -0,0 +1,45 @@ +namespace StabilityMatrix.Core.Models.Rocm; + +/// +/// Package index a helper-managed ROCm install resolves torch from. AMD's feeds carry per-architecture +/// device packages and the ROCm SDK; the upstream PyTorch feeds carry neither. +/// +public enum RocmIndexKind +{ + /// AMD's stable multi-arch feed, the default on both platforms. + AmdStable, + + /// AMD's nightly multi-arch feed, used for targets the stable feed does not publish. + AmdNightly, + + /// Upstream PyTorch ROCm 7.2 channel, available to a package as an override. + PyTorchRocm72, + + /// Upstream PyTorch ROCm 7.14 channel, available to a package as an override. + PyTorchRocm714, +} + +/// +/// Everything the helper needs to install ROCm torch: which index to use, which device +/// package applies, and which index features are available. Resolved from the architecture and the +/// package profile. +/// +public sealed record RocmInstallTarget +{ + public required RocmIndexKind IndexKind { get; init; } + + public required string IndexUrl { get; init; } + + /// Per-architecture device package extra, e.g. device-gfx1201. Null when the index has none. + public string? DeviceExtra { get; init; } + + public required bool SupportsDeviceExtras { get; init; } + + public required bool SupportsSdkDevel { get; init; } + + /// + /// True when torchvision on this index declares no torch constraint, so the helper has to resolve the + /// torch/torchvision pair and pin both. + /// + public required bool RequiresPairResolution { get; init; } +} diff --git a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs index 0c1f68dc1..9fa00eb4a 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs @@ -14,6 +14,4 @@ public class RocmMachineState public GpuInfo? SelectedGpu { get; init; } public string? RuntimeGfxArch { get; init; } - - public string? MultiArchDeviceExtra { get; init; } } diff --git a/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs b/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs index 0df6535ab..37b1af6a3 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmPackageProfile.cs @@ -15,6 +15,12 @@ public class RocmPackageProfile /// public PipInstallConfig InstallConfig { get; init; } = new(); + /// + /// Index to install torch from, overriding the platform default. Leave null unless the package needs a + /// specific ROCm channel. + /// + public RocmIndexKind? Index { get; init; } + /// /// Optional callback for package-specific environment variables derived from a resolved ROCm context. /// diff --git a/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs index be1d19636..c089a9416 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs @@ -130,10 +130,45 @@ when s.StartsWith("gfx101", StringComparison.Ordinal) return canonicalArch is null ? null : $"device-{canonicalArch}"; } - public static string GetMultiArchPythonPackageIndexUrl(string? gfxArch) + /// URL of the index a target resolves torch from. + public static string GetIndexUrl(RocmIndexKind kind) => + kind switch + { + RocmIndexKind.AmdStable => StableMultiArchPythonPackageIndexUrl, + RocmIndexKind.AmdNightly => NightlyMultiArchPythonPackageIndexUrl, + RocmIndexKind.PyTorchRocm72 => "https://download.pytorch.org/whl/rocm7.2", + RocmIndexKind.PyTorchRocm714 => "https://download.pytorch.org/whl/rocm7.14", + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown ROCm index kind."), + }; + + /// + /// Resolves everything an install needs: the index to use, whether per-architecture + /// device packages apply, and whether torch has to be paired with torchvision by the helper. + /// + public static RocmInstallTarget ResolveTarget(string? gfxArch, RocmIndexKind? indexOverride = null) { - return UsesNightlyMultiArchPythonPackageIndex(gfxArch) - ? NightlyMultiArchPythonPackageIndexUrl - : StableMultiArchPythonPackageIndexUrl; + // Vega is only published on AMD's nightly feed. Every other target uses stable unless the package + // profile pins a specific index. + var indexKind = + indexOverride + ?? ( + UsesNightlyMultiArchPythonPackageIndex(gfxArch) + ? RocmIndexKind.AmdNightly + : RocmIndexKind.AmdStable + ); + var usesAmdIndex = indexKind is RocmIndexKind.AmdStable or RocmIndexKind.AmdNightly; + + return new RocmInstallTarget + { + IndexKind = indexKind, + IndexUrl = GetIndexUrl(indexKind), + // AMD's feeds publish per-architecture device packages for both win_amd64 and linux_x86_64. + // The upstream PyTorch includes GPU libraries for all supported device architectures. + DeviceExtra = usesAmdIndex ? TryGetMultiArchDeviceExtra(gfxArch) : null, + SupportsDeviceExtras = usesAmdIndex, + SupportsSdkDevel = usesAmdIndex, + // AMD's torchvision wheels declare no torch constraint, so the pair has to be resolved here. + RequiresPairResolution = usesAmdIndex, + }; } } diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 9e1babd59..d613df1c8 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -144,9 +144,19 @@ public async Task EnsureRocmSdkDevelAsync( ) { var state = machineState.Value; - var multiArchPythonPackageIndexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl( - state.RuntimeGfxArch - ); + var target = RocmSupport.ResolveTarget(state.RuntimeGfxArch); + + if (!target.SupportsSdkDevel) + { + Logger.Info( + "The {IndexKind} index does not publish {Package}; skipping the SDK devel step.", + target.IndexKind, + RocmSdkDevelPackageName + ); + return; + } + + var multiArchPythonPackageIndexUrl = target.IndexUrl; var torchInfo = await venvRunner.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) @@ -252,20 +262,20 @@ public async Task InstallWindowsNativeTorchAsync( ); } - var multiArchDeviceExtra = state.MultiArchDeviceExtra; + var target = RocmSupport.ResolveTarget(state.RuntimeGfxArch, profile.Index); - if (string.IsNullOrWhiteSpace(multiArchDeviceExtra)) + if (string.IsNullOrWhiteSpace(target.DeviceExtra)) { throw new InvalidOperationException( - $"No Windows ROCm multi-arch device extra is available for '{state.RuntimeGfxArch ?? "unknown"}'." + $"No ROCm device package is available for '{state.RuntimeGfxArch ?? "unknown"}' " + + $"on the {target.IndexKind} index." ); } + var multiArchDeviceExtra = target.DeviceExtra; var installConfig = profile.InstallConfig; - var multiArchPythonPackageIndexUrl = RocmSupport.GetMultiArchPythonPackageIndexUrl( - state.RuntimeGfxArch - ); - var usesNightlyIndex = RocmSupport.UsesNightlyMultiArchPythonPackageIndex(state.RuntimeGfxArch); + var multiArchPythonPackageIndexUrl = target.IndexUrl; + var usesNightlyIndex = target.IndexKind == RocmIndexKind.AmdNightly; // AMD's torchvision wheels carry no torch constraint, and the matched build may be published only // as a pre-release that pip skips. Resolve the newest paired torch/torchvision and pin both. @@ -576,7 +586,6 @@ private RocmMachineState ResolveWindowsMachineState() : "No supported AMD GFX architecture could be resolved for ROCm.", SelectedGpu = selectedGpu, RuntimeGfxArch = runtimeGfxArch, - MultiArchDeviceExtra = RocmSupport.TryGetMultiArchDeviceExtra(runtimeGfxArch), }; } diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 4e7d6c087..b0d8b7644 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -165,6 +165,59 @@ public void RocmSupport_PreferMathSdpaFallback_MatchesAotritonImageList(string a Assert.AreEqual(expectFallback, RocmSupport.PreferMathSdpaFallback(arch)); } + [TestMethod] + public void ResolveTarget_WindowsStable_KeepsAmdFeedAndDeviceExtra() + { + var target = RocmSupport.ResolveTarget("gfx1201"); + + Assert.AreEqual(RocmIndexKind.AmdStable, target.IndexKind); + Assert.AreEqual(RocmSupport.StableMultiArchPythonPackageIndexUrl, target.IndexUrl); + Assert.AreEqual("device-gfx1201", target.DeviceExtra); + Assert.IsTrue(target.SupportsDeviceExtras); + Assert.IsTrue(target.SupportsSdkDevel); + Assert.IsTrue(target.RequiresPairResolution); + } + + [TestMethod] + public void ResolveTarget_VegaUsesNightlyFeed() + { + var target = RocmSupport.ResolveTarget("gfx906"); + + Assert.AreEqual(RocmIndexKind.AmdNightly, target.IndexKind); + Assert.AreEqual(RocmSupport.NightlyMultiArchPythonPackageIndexUrl, target.IndexUrl); + } + + [TestMethod] + public void ResolveTarget_LinuxAmdFeed_KeepsDeviceExtras() + { + // The feed publishes linux_x86_64 device packages, so the extra applies on both platforms. + var target = RocmSupport.ResolveTarget("gfx1100"); + + Assert.AreEqual(RocmSupport.StableMultiArchPythonPackageIndexUrl, target.IndexUrl); + Assert.AreEqual("device-gfx1100", target.DeviceExtra); + } + + [TestMethod] + public void ResolveTarget_PyTorchOverride_DropsDeviceExtrasAndSdkDevel() + { + var target = RocmSupport.ResolveTarget("gfx1100", RocmIndexKind.PyTorchRocm72); + + Assert.AreEqual("https://download.pytorch.org/whl/rocm7.2", target.IndexUrl); + Assert.IsNull(target.DeviceExtra); + Assert.IsFalse(target.SupportsDeviceExtras); + Assert.IsFalse(target.SupportsSdkDevel); + Assert.IsFalse(target.RequiresPairResolution); + } + + [TestMethod] + public void ResolveTarget_UnsupportedArch_HasNoDeviceExtra() + { + var target = RocmSupport.ResolveTarget("gfx9999"); + + Assert.IsNull(target.DeviceExtra); + Assert.AreEqual(RocmIndexKind.AmdStable, target.IndexKind); + } + [TestMethod] public void ApplyRocmSdkEnvironmentSanitization_MarksInheritedSdkVariablesForRemoval() { From 411a37fe5766549ef502583f3fb81d5cb65e9dfa Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Tue, 20 Oct 2026 00:49:59 +0000 Subject: [PATCH 06/11] feat(rocm): detect the ISA target from KFD topology on Linux - Add RocmArchDetector, which reads gfx_target_version from every KFD topology node, skips CPU nodes, and decodes the value into its ISA name (110000 to gfx1100, 110500 to gfx1150, 100300 to gfx1030, 90010 to gfx90a) - Cache the result per process; the nodes are written by the amdgpu module, so no ROCm userspace and no marketing-name table is involved - Resolve the machine's architecture from the driver first on Linux, and fall back to the adapter name table only when KFD has no answer - Let a driver-resolved architecture make the machine compatible even when no adapter name maps to a supported target, which is the case for cards the table has never heard of - Correct the failure message, which claimed Windows-only architecture support - Cover the decoder across RDNA2/3/3.5/4 and CDNA2/3 encodings, plus CPU-node skipping --- .../Services/Rocm/RocmArchDetector.cs | 125 ++++++++++++++++++ .../Services/Rocm/RocmPackageHelper.cs | 36 ++++- .../Core/RocmPackageHelperTests.cs | 22 +++ 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs diff --git a/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs b/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs new file mode 100644 index 000000000..0beb0ed45 --- /dev/null +++ b/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs @@ -0,0 +1,125 @@ +using NLog; + +namespace StabilityMatrix.Core.Services.Rocm; + +/// +/// Reads the ISA targets the amdgpu kernel module exports through the KFD topology. On Linux this is the +/// driver's own answer, so no ROCm userspace and no marketing-name table is involved. Windows has no KFD +/// node and keeps resolving architectures from the adapter name table. +/// +internal static class RocmArchDetector +{ + private const string KfdNodesPath = "/sys/class/kfd/kfd/topology/nodes"; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private static readonly object Gate = new(); + private static IReadOnlyList? cached; + + /// ISA targets the driver reports, in node order. Empty when KFD is unavailable. + internal static IReadOnlyList Architectures + { + get + { + if (cached is { } existing) + { + return existing; + } + + lock (Gate) + { + return cached ??= Detect(); + } + } + } + + /// Forces the next access to re-read the topology. + internal static void ResetCache() + { + lock (Gate) + { + cached = null; + } + } + + private static IReadOnlyList Detect() + { + try + { + if (!OperatingSystem.IsLinux() || !Directory.Exists(KfdNodesPath)) + { + return []; + } + + List architectures = []; + + foreach (var nodePath in Directory.EnumerateDirectories(KfdNodesPath)) + { + var propertiesPath = Path.Combine(nodePath, "properties"); + if (!File.Exists(propertiesPath)) + { + continue; + } + + var architecture = TryParseNode(File.ReadAllText(propertiesPath)); + if (architecture is not null) + { + architectures.Add(architecture); + } + } + + return architectures; + } + catch (Exception exception) + { + Logger.Warn(exception, "Failed to read the KFD topology; falling back to adapter names."); + return []; + } + } + + /// + /// Decodes one node's properties text, or returns null for CPU nodes and unparsable nodes. + /// + internal static string? TryParseNode(string properties) + { + Dictionary values = new(StringComparer.Ordinal); + + foreach (var line in properties.Split('\n')) + { + var parts = line.Split( + ' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ); + if (parts.Length == 2 && int.TryParse(parts[1], out var value)) + { + values[parts[0]] = value; + } + } + + // CPU nodes carry simd_count 0 and no target version. + if (!values.TryGetValue("simd_count", out var simdCount) || simdCount == 0) + { + return null; + } + + if (!values.TryGetValue("gfx_target_version", out var targetVersion) || targetVersion == 0) + { + return null; + } + + return FormatGfxTarget(targetVersion); + } + + /// + /// Decodes the KFD gfx_target_version integer into its ISA name: 110000 to gfx1100, + /// 120001 to gfx1201, 100300 to gfx1030, 110500 to gfx1150, 90010 to gfx90a. The name is always + /// major, then minor, then the stepping as a single hex digit, including when the stepping is zero. + /// + internal static string FormatGfxTarget(int gfxTargetVersion) + { + var major = gfxTargetVersion / 10000; + var minor = gfxTargetVersion / 100 % 100; + var stepping = gfxTargetVersion % 100; + + return $"gfx{major}{minor}{stepping:x}"; + } +} diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index d613df1c8..e71676d69 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -561,20 +561,26 @@ private RocmMachineState ResolveWindowsMachineState() } var supportedAmdGpus = amdGpus.Where(RocmSupport.IsSupportedGpu).ToList(); - if (supportedAmdGpus.Count == 0) + var detectedArch = TryDetectLinuxArchitecture(); + + // The driver can resolve an architecture the adapter-name table has never heard of, so only fail + // when neither source produced one. + if (supportedAmdGpus.Count == 0 && detectedArch is null) { return new RocmMachineState { IsCompatible = false, - FailureReason = "No AMD GPU with a supported Windows ROCm architecture was detected.", + FailureReason = "No AMD GPU with a supported ROCm architecture was detected.", }; } var selectedGpu = TryResolvePreferredAmdGpu(supportedAmdGpus, settingsManager.Settings.PreferredGpu) - ?? supportedAmdGpus.First(); + ?? supportedAmdGpus.FirstOrDefault() + ?? amdGpus[0]; var runtimeGfxArch = - RocmSupport.TryGetCanonicalArchitecture(selectedGpu.GetAmdGfxArch()) + detectedArch + ?? RocmSupport.TryGetCanonicalArchitecture(selectedGpu.GetAmdGfxArch()) ?? GetSupportedFallbackGfxArch(supportedAmdGpus); var isCompatible = !string.IsNullOrWhiteSpace(runtimeGfxArch); @@ -589,6 +595,28 @@ private RocmMachineState ResolveWindowsMachineState() }; } + /// + /// First supported ISA target the kernel reports through KFD, or null when KFD is unavailable. The + /// driver's answer takes precedence over the adapter-name table on Linux. + /// + private static string? TryDetectLinuxArchitecture() + { + if (!OperatingSystem.IsLinux()) + { + return null; + } + + foreach (var architecture in RocmArchDetector.Architectures) + { + if (RocmSupport.TryGetCanonicalArchitecture(architecture) is { } canonical) + { + return canonical; + } + } + + return null; + } + /// /// Returns AMD GPUs from Stability Matrix's internal hardware model. /// This is the canonical GPU source for the ROCm helper and intentionally avoids package-local probing. diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index b0d8b7644..b920c837b 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -165,6 +165,28 @@ public void RocmSupport_PreferMathSdpaFallback_MatchesAotritonImageList(string a Assert.AreEqual(expectFallback, RocmSupport.PreferMathSdpaFallback(arch)); } + [TestMethod] + [DataRow(110000, "gfx1100")] + [DataRow(120001, "gfx1201")] + [DataRow(100300, "gfx1030")] + [DataRow(90402, "gfx942")] + [DataRow(90010, "gfx90a")] + [DataRow(110500, "gfx1150")] + public void RocmArchDetector_FormatGfxTarget_DecodesKfdTargetVersion(int version, string expected) + { + Assert.AreEqual(expected, RocmArchDetector.FormatGfxTarget(version)); + } + + [TestMethod] + public void RocmArchDetector_TryParseNode_SkipsCpuNodesAndParsesGpuNodes() + { + const string cpuNode = "cpu_cores_count 16\nsimd_count 0\ngfx_target_version 0\n"; + const string gpuNode = "simd_count 192\ngfx_target_version 110000\ndevice_id 29772\n"; + + Assert.IsNull(RocmArchDetector.TryParseNode(cpuNode)); + Assert.AreEqual("gfx1100", RocmArchDetector.TryParseNode(gpuNode)); + } + [TestMethod] public void ResolveTarget_WindowsStable_KeepsAmdFeedAndDeviceExtra() { From 8afb9c5e6eef69e2576d79a70905c30f129a57b7 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Tue, 20 Oct 2026 01:04:07 +0000 Subject: [PATCH 07/11] feat(rocm): rank candidate GPUs so an APU cannot win by enumeration order - RocmArchDetector now returns one device per KFD node (arch, device index, SIMD count) instead of a bare architecture, and sorts nodes numerically so device indices match the order the runtime enumerates them - Add SelectPreferred, which picks the device with the most compute: an APU reports a handful of SIMD units against hundreds on a discrete card, and node order carries no information about which is which - Resolve the Linux architecture from that selection after filtering to targets the feed supports, so a 2 CU RDNA2 APU cannot shadow the discrete card and an unsupported node falls through to the next candidate - Rank the name-based Windows candidates by memory the same way, which separates a laptop APU's carveout from a discrete adapter when the user has no preference - PreferredGpu still wins over both rankings - Tests cover node parsing, both node orders for an APU plus a dGPU, tie-breaking and the memory ranking --- .../Services/Rocm/RocmArchDetector.cs | 65 +++++++++++--- .../Services/Rocm/RocmPackageHelper.cs | 27 +++--- .../Core/RocmPackageHelperTests.cs | 87 ++++++++++++++++++- 3 files changed, 152 insertions(+), 27 deletions(-) diff --git a/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs b/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs index 0beb0ed45..54cb5ff74 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmArchDetector.cs @@ -3,7 +3,24 @@ namespace StabilityMatrix.Core.Services.Rocm; /// -/// Reads the ISA targets the amdgpu kernel module exports through the KFD topology. On Linux this is the +/// One AMD GPU exposed through the KFD topology. +/// +internal sealed record RocmDetectedDevice +{ + public required string GfxArch { get; init; } + + /// + /// ROCm device index: the position among GPU nodes in KFD node order, which is the order the runtime + /// enumerates devices in. + /// + public required int DeviceIndex { get; init; } + + /// SIMD count reported by the driver. Used to tell a discrete card from an APU. + public required int SimdCount { get; init; } +} + +/// +/// Reads the GPUs the amdgpu kernel module exports through the KFD topology. On Linux this is the /// driver's own answer, so no ROCm userspace and no marketing-name table is involved. Windows has no KFD /// node and keeps resolving architectures from the adapter name table. /// @@ -13,10 +30,10 @@ internal static class RocmArchDetector private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly object Gate = new(); - private static IReadOnlyList? cached; + private static IReadOnlyList? cached; - /// ISA targets the driver reports, in node order. Empty when KFD is unavailable. - internal static IReadOnlyList Architectures + /// GPUs the driver reports, in node order. Empty when KFD is unavailable. + internal static IReadOnlyList Devices { get { @@ -41,7 +58,7 @@ internal static void ResetCache() } } - private static IReadOnlyList Detect() + private static IReadOnlyList Detect() { try { @@ -50,9 +67,15 @@ private static IReadOnlyList Detect() return []; } - List architectures = []; + List devices = []; - foreach (var nodePath in Directory.EnumerateDirectories(KfdNodesPath)) + // Node ids ascend with PCI topology, so sort numerically rather than by directory name. The + // device index counts GPU nodes only, because that is what the runtime exposes. + var nodePaths = Directory + .EnumerateDirectories(KfdNodesPath) + .OrderBy(path => int.TryParse(Path.GetFileName(path), out var node) ? node : int.MaxValue); + + foreach (var nodePath in nodePaths) { var propertiesPath = Path.Combine(nodePath, "properties"); if (!File.Exists(propertiesPath)) @@ -60,14 +83,14 @@ private static IReadOnlyList Detect() continue; } - var architecture = TryParseNode(File.ReadAllText(propertiesPath)); - if (architecture is not null) + var device = TryParseNode(File.ReadAllText(propertiesPath), devices.Count); + if (device is not null) { - architectures.Add(architecture); + devices.Add(device); } } - return architectures; + return devices; } catch (Exception exception) { @@ -79,7 +102,7 @@ private static IReadOnlyList Detect() /// /// Decodes one node's properties text, or returns null for CPU nodes and unparsable nodes. /// - internal static string? TryParseNode(string properties) + internal static RocmDetectedDevice? TryParseNode(string properties, int deviceIndex) { Dictionary values = new(StringComparer.Ordinal); @@ -106,9 +129,25 @@ private static IReadOnlyList Detect() return null; } - return FormatGfxTarget(targetVersion); + return new RocmDetectedDevice + { + GfxArch = FormatGfxTarget(targetVersion), + DeviceIndex = deviceIndex, + SimdCount = simdCount, + }; } + /// + /// Picks the device an install should target when the user has no preference: the one with the most + /// compute. An APU reports a handful of SIMD units, a discrete card reports tens to hundreds, and KFD + /// node order is not a reliable indicator of which is which. + /// + internal static RocmDetectedDevice? SelectPreferred(IReadOnlyList devices) => + devices + .OrderByDescending(device => device.SimdCount) + .ThenBy(device => device.DeviceIndex) + .FirstOrDefault(); + /// /// Decodes the KFD gfx_target_version integer into its ISA name: 110000 to gfx1100, /// 120001 to gfx1201, 100300 to gfx1030, 110500 to gfx1150, 90010 to gfx90a. The name is always diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index e71676d69..0bfcc849a 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -576,7 +576,8 @@ private RocmMachineState ResolveWindowsMachineState() var selectedGpu = TryResolvePreferredAmdGpu(supportedAmdGpus, settingsManager.Settings.PreferredGpu) - ?? supportedAmdGpus.FirstOrDefault() + ?? SelectPreferredGpu(supportedAmdGpus) + ?? SelectPreferredGpu(amdGpus) ?? amdGpus[0]; var runtimeGfxArch = detectedArch @@ -596,8 +597,8 @@ private RocmMachineState ResolveWindowsMachineState() } /// - /// First supported ISA target the kernel reports through KFD, or null when KFD is unavailable. The - /// driver's answer takes precedence over the adapter-name table on Linux. + /// ISA target of the GPU the kernel reports through KFD, or null when KFD is unavailable or reports + /// nothing supported. The driver's answer takes precedence over the adapter-name table on Linux. /// private static string? TryDetectLinuxArchitecture() { @@ -606,17 +607,21 @@ private RocmMachineState ResolveWindowsMachineState() return null; } - foreach (var architecture in RocmArchDetector.Architectures) - { - if (RocmSupport.TryGetCanonicalArchitecture(architecture) is { } canonical) - { - return canonical; - } - } + var supportedDevices = RocmArchDetector + .Devices.Where(device => RocmSupport.TryGetCanonicalArchitecture(device.GfxArch) is not null) + .ToList(); + var selected = RocmArchDetector.SelectPreferred(supportedDevices); - return null; + return selected is null ? null : RocmSupport.TryGetCanonicalArchitecture(selected.GfxArch); } + /// + /// Picks between candidate adapters when the user has no preference: the one with the most memory, + /// which separates a discrete card from an APU's shared carveout. + /// + internal static GpuInfo? SelectPreferredGpu(IEnumerable candidates) => + candidates.OrderByDescending(gpu => gpu.MemoryBytes).FirstOrDefault(); + /// /// Returns AMD GPUs from Stability Matrix's internal hardware model. /// This is the canonical GPU source for the ROCm helper and intentionally avoids package-local probing. diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index b920c837b..7341bcba7 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -178,13 +178,94 @@ public void RocmArchDetector_FormatGfxTarget_DecodesKfdTargetVersion(int version } [TestMethod] - public void RocmArchDetector_TryParseNode_SkipsCpuNodesAndParsesGpuNodes() + public void RocmArchDetector_TryParseNode_SkipsCpuNodes() { const string cpuNode = "cpu_cores_count 16\nsimd_count 0\ngfx_target_version 0\n"; + + Assert.IsNull(RocmArchDetector.TryParseNode(cpuNode, 0)); + } + + [TestMethod] + public void RocmArchDetector_TryParseNode_ParsesGpuNodes() + { const string gpuNode = "simd_count 192\ngfx_target_version 110000\ndevice_id 29772\n"; - Assert.IsNull(RocmArchDetector.TryParseNode(cpuNode)); - Assert.AreEqual("gfx1100", RocmArchDetector.TryParseNode(gpuNode)); + var device = RocmArchDetector.TryParseNode(gpuNode, 1); + + Assert.IsNotNull(device); + Assert.AreEqual("gfx1100", device.GfxArch); + Assert.AreEqual(1, device.DeviceIndex); + Assert.AreEqual(192, device.SimdCount); + } + + [TestMethod] + public void RocmArchDetector_SelectPreferred_PrefersTheDiscreteDevice() + { + // An APU is a couple of SIMD units; the discrete card is two orders of magnitude more. Node order + // carries no information about which is which, so both orders have to resolve the same way. + var apuFirst = new[] + { + new RocmDetectedDevice + { + GfxArch = "gfx1036", + DeviceIndex = 0, + SimdCount = 4, + }, + new RocmDetectedDevice + { + GfxArch = "gfx1100", + DeviceIndex = 1, + SimdCount = 192, + }, + }; + var dgpuFirst = new[] + { + new RocmDetectedDevice + { + GfxArch = "gfx1100", + DeviceIndex = 0, + SimdCount = 192, + }, + new RocmDetectedDevice + { + GfxArch = "gfx1036", + DeviceIndex = 1, + SimdCount = 4, + }, + }; + + Assert.AreEqual("gfx1100", RocmArchDetector.SelectPreferred(apuFirst)!.GfxArch); + Assert.AreEqual("gfx1100", RocmArchDetector.SelectPreferred(dgpuFirst)!.GfxArch); + } + + [TestMethod] + public void RocmArchDetector_SelectPreferred_BreaksTiesOnDeviceIndex() + { + var first = new RocmDetectedDevice + { + GfxArch = "gfx1100", + DeviceIndex = 0, + SimdCount = 192, + }; + var second = new RocmDetectedDevice + { + GfxArch = "gfx1100", + DeviceIndex = 1, + SimdCount = 192, + }; + + Assert.AreEqual(0, RocmArchDetector.SelectPreferred([first, second])!.DeviceIndex); + } + + [TestMethod] + public void SelectPreferredGpu_PrefersTheCardWithMoreMemory() + { + // On Windows the APU reports its carveout, so memory is the only discrete signal available. + var apu = new GpuInfo { Name = "AMD Radeon(TM) Graphics", MemoryBytes = 512UL * 1024 * 1024 }; + var dgpu = new GpuInfo { Name = "AMD Radeon RX 7900 XTX", MemoryBytes = 24UL * 1024 * 1024 * 1024 }; + + Assert.AreEqual(dgpu, RocmPackageHelper.SelectPreferredGpu([apu, dgpu])); + Assert.AreEqual(dgpu, RocmPackageHelper.SelectPreferredGpu([dgpu, apu])); } [TestMethod] From 7f61d7b1746960f95b62fbd58769a69eb0f279da Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Wed, 21 Oct 2026 02:22:18 +0000 Subject: [PATCH 08/11] feat(rocm): pin ROCm device visibility to the selected GPU on mixed machines - Carry the selection through RocmMachineState and RocmRuntimeContext: the number of AMD GPU agents and, where known, the ROCm device index - When more than one AMD GPU is present, helper-managed launches set ROCR_VISIBLE_DEVICES to that index, so an APU that enumerates first cannot become the device an app opens - Leave single-GPU machines untouched, and skip the pin when the selection has no device index - Nothing is written to settings; the value is rebuilt per launch and sits in the helper layer, so package and user overrides still win - Tests cover the multi-GPU pin, the single-GPU no-op and the missing-index case --- .../Models/Rocm/RocmMachineState.cs | 6 +++ .../Models/Rocm/RocmRuntimeContext.cs | 8 ++- .../Services/Rocm/RocmPackageHelper.cs | 42 +++++++++++++-- .../Core/RocmPackageHelperTests.cs | 54 +++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs index 9fa00eb4a..b1c801b6d 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmMachineState.cs @@ -14,4 +14,10 @@ public class RocmMachineState public GpuInfo? SelectedGpu { get; init; } public string? RuntimeGfxArch { get; init; } + + /// Number of AMD GPUs the machine exposes, GPU agents only. + public int AmdDeviceCount { get; init; } + + /// ROCm device index of the selected GPU, when known. + public int? DeviceIndex { get; init; } } diff --git a/StabilityMatrix.Core/Models/Rocm/RocmRuntimeContext.cs b/StabilityMatrix.Core/Models/Rocm/RocmRuntimeContext.cs index 1fdda7914..0a6d4e61e 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmRuntimeContext.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmRuntimeContext.cs @@ -1,4 +1,4 @@ -using StabilityMatrix.Core.Helper.HardwareInfo; +using StabilityMatrix.Core.Helper.HardwareInfo; namespace StabilityMatrix.Core.Models.Rocm; @@ -15,4 +15,10 @@ public class RocmRuntimeContext public GpuInfo? SelectedGpu { get; init; } public string? RuntimeGfxArch { get; init; } + + /// Number of AMD GPUs the machine exposes, GPU agents only. + public int AmdDeviceCount { get; init; } + + /// ROCm device index of the selected GPU, when known. + public int? DeviceIndex { get; init; } } diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 0bfcc849a..35f6d7ace 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -86,6 +86,8 @@ private RocmRuntimeContext ResolveRuntimeContext() IsSupported = true, SelectedGpu = state.SelectedGpu, RuntimeGfxArch = state.RuntimeGfxArch, + AmdDeviceCount = state.AmdDeviceCount, + DeviceIndex = state.DeviceIndex, }; } @@ -561,7 +563,13 @@ private RocmMachineState ResolveWindowsMachineState() } var supportedAmdGpus = amdGpus.Where(RocmSupport.IsSupportedGpu).ToList(); - var detectedArch = TryDetectLinuxArchitecture(); + var linuxDevice = TrySelectLinuxDevice(); + var detectedArch = linuxDevice is null + ? null + : RocmSupport.TryGetCanonicalArchitecture(linuxDevice.GfxArch); + // Prefer the driver's count when it has one, since it counts GPU agents rather than adapters. + var driverDeviceCount = RocmArchDetector.Devices.Count; + var amdDeviceCount = driverDeviceCount > 0 ? driverDeviceCount : amdGpus.Count; // The driver can resolve an architecture the adapter-name table has never heard of, so only fail // when neither source produced one. @@ -593,6 +601,8 @@ private RocmMachineState ResolveWindowsMachineState() : "No supported AMD GFX architecture could be resolved for ROCm.", SelectedGpu = selectedGpu, RuntimeGfxArch = runtimeGfxArch, + AmdDeviceCount = amdDeviceCount, + DeviceIndex = linuxDevice?.DeviceIndex, }; } @@ -600,7 +610,7 @@ private RocmMachineState ResolveWindowsMachineState() /// ISA target of the GPU the kernel reports through KFD, or null when KFD is unavailable or reports /// nothing supported. The driver's answer takes precedence over the adapter-name table on Linux. /// - private static string? TryDetectLinuxArchitecture() + private static RocmDetectedDevice? TrySelectLinuxDevice() { if (!OperatingSystem.IsLinux()) { @@ -610,9 +620,8 @@ private RocmMachineState ResolveWindowsMachineState() var supportedDevices = RocmArchDetector .Devices.Where(device => RocmSupport.TryGetCanonicalArchitecture(device.GfxArch) is not null) .ToList(); - var selected = RocmArchDetector.SelectPreferred(supportedDevices); - return selected is null ? null : RocmSupport.TryGetCanonicalArchitecture(selected.GfxArch); + return RocmArchDetector.SelectPreferred(supportedDevices); } /// @@ -838,10 +847,35 @@ RocmPackageProfile profile var gfxArch = runtimeContext.RuntimeGfxArch; ApplyDefaultLaunchEnvironment(environment, gfxArch, options); + ApplyDeviceVisibility(environment, runtimeContext); return environment; } + /// + /// Pins the runtime to the GPU the helper selected when the machine exposes more than one, so an APU + /// that enumerates first cannot become the device an app opens. ROCR_VISIBLE_DEVICES is the + /// lowest device filter and the runtime renumbers what remains from zero. Single-GPU machines are + /// left alone, as are machines whose selection has no device index. + /// + internal static void ApplyDeviceVisibility( + IDictionary environment, + RocmRuntimeContext runtimeContext + ) + { + if (runtimeContext.AmdDeviceCount <= 1 || runtimeContext.DeviceIndex is not { } deviceIndex) + { + return; + } + + environment["ROCR_VISIBLE_DEVICES"] = deviceIndex.ToString(); + Logger.Info( + "Pinning ROCm device visibility to device {Index} of {Count}.", + deviceIndex, + runtimeContext.AmdDeviceCount + ); + } + private void ApplyDefaultLaunchEnvironment( IDictionary environment, string? gfxArch, diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 7341bcba7..7b1efea48 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -321,6 +321,60 @@ public void ResolveTarget_UnsupportedArch_HasNoDeviceExtra() Assert.AreEqual(RocmIndexKind.AmdStable, target.IndexKind); } + [TestMethod] + public void ApplyDeviceVisibility_PinsSelectedDeviceWhenSeveralAmdGpusExist() + { + var environment = new Dictionary(StringComparer.OrdinalIgnoreCase); + + RocmPackageHelper.ApplyDeviceVisibility( + environment, + new RocmRuntimeContext + { + IsSupported = true, + AmdDeviceCount = 2, + DeviceIndex = 1, + } + ); + + Assert.AreEqual("1", environment["ROCR_VISIBLE_DEVICES"]); + } + + [TestMethod] + public void ApplyDeviceVisibility_LeavesSingleGpuMachinesAlone() + { + var environment = new Dictionary(StringComparer.OrdinalIgnoreCase); + + RocmPackageHelper.ApplyDeviceVisibility( + environment, + new RocmRuntimeContext + { + IsSupported = true, + AmdDeviceCount = 1, + DeviceIndex = 0, + } + ); + + Assert.IsFalse(environment.ContainsKey("ROCR_VISIBLE_DEVICES")); + } + + [TestMethod] + public void ApplyDeviceVisibility_SkipsWhenThePlatformReportsNoIndex() + { + var environment = new Dictionary(StringComparer.OrdinalIgnoreCase); + + RocmPackageHelper.ApplyDeviceVisibility( + environment, + new RocmRuntimeContext + { + IsSupported = true, + AmdDeviceCount = 2, + DeviceIndex = null, + } + ); + + Assert.IsFalse(environment.ContainsKey("ROCR_VISIBLE_DEVICES")); + } + [TestMethod] public void ApplyRocmSdkEnvironmentSanitization_MarksInheritedSdkVariablesForRemoval() { From e88d61c8e9eee917414c67f8d44d64b639ac5dcd Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Wed, 21 Oct 2026 02:44:55 +0000 Subject: [PATCH 09/11] feat(rocm): make the ROCm torch install path platform-capable - Rename the install surface to InstallRocmTorchAsync and BuildRocmInstallConfig, and ResolveWindowsMachineState to ResolveMachineState, so the names no longer describe a Windows-only path - Install from an index without per-architecture device packages: the upstream PyTorch feeds ship one multi-architecture wheel, so the device extra is optional and only required when the resolved index declares it - Build the torch and torchvision requirements through one helper, so the requirement form follows the index instead of a hardcoded extra - Drop RocmInstallTarget.RequiresPairResolution, since the pair resolver runs for every index and the flag had no consumer - Rename the install verification step and remove Windows wording from it and from the SDK devel progress messages - Update the interface documentation, which still described both methods as Windows-native and multi-arch specific - Cover the requirement forms with and without a device extra --- .../Models/Packages/ComfyUI.cs | 4 +- .../Models/Packages/ForgeNeo.cs | 4 +- .../Models/Packages/InvokeAI.cs | 2 +- .../Models/Packages/OneTrainer.cs | 4 +- .../Models/Packages/Reforge.cs | 4 +- .../Models/Packages/Wan2GP.cs | 4 +- .../Models/Rocm/RocmInstallTarget.cs | 6 -- .../Models/Rocm/RocmSupport.cs | 2 - .../Services/Rocm/IRocmPackageHelper.cs | 10 ++- .../Services/Rocm/RocmPackageHelper.cs | 83 ++++++++++--------- .../Core/RocmPackageHelperTests.cs | 16 +++- 11 files changed, 77 insertions(+), 62 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 8c4bc0430..d83ac8299 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -490,7 +490,7 @@ public override async Task InstallPackage( if (Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasRocmSupport()) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(ComfyRocmProfile.Default); + var config = rocmPackageHelper.BuildRocmInstallConfig(ComfyRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -504,7 +504,7 @@ await StandardPipInstallProcessAsync( .ConfigureAwait(false); await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, ComfyRocmProfile.Default, diff --git a/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs b/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs index 2ae5eba78..7c8904ff9 100644 --- a/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs +++ b/StabilityMatrix.Core/Models/Packages/ForgeNeo.cs @@ -144,10 +144,10 @@ CancellationToken cancellationToken if (Compat.IsWindows) { - // Install the AMD multi-arch torch build before launch.py runs, so its is_installed("torch") + // Install the helper-managed ROCm torch build before launch.py runs, so its is_installed("torch") // gate skips the default NVIDIA CUDA torch install. await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, profile, diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 81328b177..272249c4b 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -297,7 +297,7 @@ public override async Task InstallPackage( ); await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, installProfile, diff --git a/StabilityMatrix.Core/Models/Packages/OneTrainer.cs b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs index 76aac105f..6c862fcf9 100644 --- a/StabilityMatrix.Core/Models/Packages/OneTrainer.cs +++ b/StabilityMatrix.Core/Models/Packages/OneTrainer.cs @@ -93,7 +93,7 @@ public override async Task InstallPackage( if (isWindowsRocm) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(OneTrainerRocmProfile.Default); + var config = rocmPackageHelper.BuildRocmInstallConfig(OneTrainerRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -114,7 +114,7 @@ config with ); await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, OneTrainerRocmProfile.CreateInstallProfile( diff --git a/StabilityMatrix.Core/Models/Packages/Reforge.cs b/StabilityMatrix.Core/Models/Packages/Reforge.cs index 665d9dde5..30b7df825 100644 --- a/StabilityMatrix.Core/Models/Packages/Reforge.cs +++ b/StabilityMatrix.Core/Models/Packages/Reforge.cs @@ -125,7 +125,7 @@ await base.InstallPackage( .ConfigureAwait(false); var profile = ReforgeRocmProfile.CreateProfile(GetRequirementsPaths(installLocation)); - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(profile); + var config = rocmPackageHelper.BuildRocmInstallConfig(profile); await StandardPipInstallProcessAsync( venvRunner, @@ -139,7 +139,7 @@ await StandardPipInstallProcessAsync( .ConfigureAwait(false); await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, profile, diff --git a/StabilityMatrix.Core/Models/Packages/Wan2GP.cs b/StabilityMatrix.Core/Models/Packages/Wan2GP.cs index 79ce53182..e46151b6a 100644 --- a/StabilityMatrix.Core/Models/Packages/Wan2GP.cs +++ b/StabilityMatrix.Core/Models/Packages/Wan2GP.cs @@ -388,7 +388,7 @@ CancellationToken cancellationToken { if (Compat.IsWindows) { - var config = rocmPackageHelper.BuildWindowsNativeInstallConfig(Wan2GpRocmProfile.Default); + var config = rocmPackageHelper.BuildRocmInstallConfig(Wan2GpRocmProfile.Default); await StandardPipInstallProcessAsync( venvRunner, @@ -402,7 +402,7 @@ await StandardPipInstallProcessAsync( .ConfigureAwait(false); await rocmPackageHelper - .InstallWindowsNativeTorchAsync( + .InstallRocmTorchAsync( venvRunner, installedPackage, Wan2GpRocmProfile.Default, diff --git a/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs b/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs index 6dc23bd8f..a9c263fad 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmInstallTarget.cs @@ -36,10 +36,4 @@ public sealed record RocmInstallTarget public required bool SupportsDeviceExtras { get; init; } public required bool SupportsSdkDevel { get; init; } - - /// - /// True when torchvision on this index declares no torch constraint, so the helper has to resolve the - /// torch/torchvision pair and pin both. - /// - public required bool RequiresPairResolution { get; init; } } diff --git a/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs index c089a9416..be382da11 100644 --- a/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs +++ b/StabilityMatrix.Core/Models/Rocm/RocmSupport.cs @@ -167,8 +167,6 @@ public static RocmInstallTarget ResolveTarget(string? gfxArch, RocmIndexKind? in DeviceExtra = usesAmdIndex ? TryGetMultiArchDeviceExtra(gfxArch) : null, SupportsDeviceExtras = usesAmdIndex, SupportsSdkDevel = usesAmdIndex, - // AMD's torchvision wheels declare no torch constraint, so the pair has to be resolved here. - RequiresPairResolution = usesAmdIndex, }; } } diff --git a/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs index 25fdffebf..086640cd3 100644 --- a/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/IRocmPackageHelper.cs @@ -46,14 +46,16 @@ Task EnsureRocmSdkDevelAsync( ); /// - /// Builds the standard pip install config for a Windows ROCm package, ensuring torch installation stays helper-managed. + /// Builds the standard pip install config for a ROCm package, ensuring torch installation stays + /// helper-managed. /// - PipInstallConfig BuildWindowsNativeInstallConfig(RocmPackageProfile profile); + PipInstallConfig BuildRocmInstallConfig(RocmPackageProfile profile); /// - /// Installs the Windows-native ROCm torch wheel set for a package using helper-resolved multi-arch device extras. + /// Installs the ROCm torch wheel set for a package from the resolved index, selecting device code + /// through the index's device extra when it has one. /// - Task InstallWindowsNativeTorchAsync( + Task InstallRocmTorchAsync( IPyVenvRunner venvRunner, InstalledPackage installedPackage, RocmPackageProfile profile, diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 35f6d7ace..359019952 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -27,7 +27,7 @@ public class RocmPackageHelper : IRocmPackageHelper private const string RocmSdkDevelPackageName = "rocm-sdk-devel"; // The ROCm torch install only ever needs to move the torch trio. Bundling or upgrading anything - // wider lets the resolver re-evaluate transitive dependencies against the multi-arch index. + // wider lets the resolver re-evaluate transitive dependencies against AMD's feed. private static readonly string[] RocmTorchPackageNames = ["torch", "torchvision", "torchaudio"]; private static readonly string[] RocmTorchUpgradePackageArgs = BuildRepeatablePackageArgs( "--upgrade-package", @@ -52,7 +52,7 @@ public class RocmPackageHelper : IRocmPackageHelper public RocmPackageHelper(ISettingsManager settingsManager) { this.settingsManager = settingsManager; - machineState = new Lazy(ResolveWindowsMachineState); + machineState = new Lazy(ResolveMachineState); } /// @@ -135,7 +135,7 @@ public IReadOnlyList GetLaunchNoticeLines(TorchIndex selectedTorchIndex) } /// - /// Ensures rocm-sdk-devel is installed from the ROCm multi-arch index. + /// Ensures rocm-sdk-devel is installed from the resolved ROCm index. /// It prefers a build whose date token matches the installed ROCm torch build and falls back to the latest available build when no exact match is available. /// public async Task EnsureRocmSdkDevelAsync( @@ -158,20 +158,20 @@ public async Task EnsureRocmSdkDevelAsync( return; } - var multiArchPythonPackageIndexUrl = target.IndexUrl; + var indexUrl = target.IndexUrl; var torchInfo = await venvRunner.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) { throw new InvalidOperationException( - "torch is not installed in this environment. Install the Windows ROCm torch build first." + "torch is not installed in this environment. Install the ROCm torch build first." ); } if (!IsUsableRocmTorchBuild(torchInfo.Version, null)) { throw new InvalidOperationException( - $"Installed torch is not a usable Windows ROCm build (detected version: {torchInfo.Version})." + $"Installed torch is not a usable ROCm build (detected version: {torchInfo.Version})." ); } @@ -185,9 +185,7 @@ public async Task EnsureRocmSdkDevelAsync( return; } - var indexResult = await venvRunner - .PipIndex(RocmSdkDevelPackageName, multiArchPythonPackageIndexUrl) - .ConfigureAwait(false); + var indexResult = await venvRunner.PipIndex(RocmSdkDevelPackageName, indexUrl).ConfigureAwait(false); var latestVersion = indexResult?.AvailableVersions.FirstOrDefault(); var matchingVersion = string.IsNullOrWhiteSpace(nightlyBuildDateToken) @@ -209,7 +207,7 @@ public async Task EnsureRocmSdkDevelAsync( progress?.Report( new ProgressReport( -1f, - $"Installing {RocmSdkDevelPackageName} {matchingVersion} for Windows ROCm...", + $"Installing {RocmSdkDevelPackageName} {matchingVersion} for ROCm...", isIndeterminate: true ) ); @@ -219,7 +217,7 @@ public async Task EnsureRocmSdkDevelAsync( progress?.Report( new ProgressReport( -1f, - $"Falling back to latest available {RocmSdkDevelPackageName} build {versionToInstall} for Windows ROCm...", + $"Falling back to latest available {RocmSdkDevelPackageName} build {versionToInstall} for ROCm...", isIndeterminate: true ) ); @@ -229,7 +227,7 @@ await venvRunner .PipInstall( new PipInstallArgs() .AddArg("--upgrade") - .AddKeyedArgs("--index-url", ["--index-url", multiArchPythonPackageIndexUrl]) + .AddKeyedArgs("--index-url", ["--index-url", indexUrl]) .AddArg($"{RocmSdkDevelPackageName}=={versionToInstall}"), onConsoleOutput ) @@ -237,17 +235,18 @@ await venvRunner } /// - /// Builds the standard pip install config for helper-managed Windows ROCm package installs. + /// Builds the standard pip install config for helper-managed ROCm package installs. /// - public PipInstallConfig BuildWindowsNativeInstallConfig(RocmPackageProfile profile) + public PipInstallConfig BuildRocmInstallConfig(RocmPackageProfile profile) { return profile.InstallConfig with { SkipTorchInstall = true }; } /// - /// Installs the ROCm torch wheel set from the multi-arch index and verifies that the resulting torch installation reports usable ROCm metadata. + /// Installs the ROCm torch wheel set from the resolved index and verifies that the resulting torch + /// installation reports usable ROCm metadata. /// - public async Task InstallWindowsNativeTorchAsync( + public async Task InstallRocmTorchAsync( IPyVenvRunner venvRunner, InstalledPackage installedPackage, RocmPackageProfile profile, @@ -260,13 +259,16 @@ public async Task InstallWindowsNativeTorchAsync( if (!state.IsCompatible) { throw new InvalidOperationException( - state.FailureReason ?? "Windows ROCm installation is not supported for the current machine." + state.FailureReason ?? "ROCm installation is not supported for the current machine." ); } var target = RocmSupport.ResolveTarget(state.RuntimeGfxArch, profile.Index); - if (string.IsNullOrWhiteSpace(target.DeviceExtra)) + // AMD's feeds select device code through a per-architecture extra; the upstream PyTorch feeds + // either ship one multi-architecture wheel (e.g: 7.2) or explicitly force-installs all device extras (e.g 7.14), + // so an index without device extras is still installable. + if (target.SupportsDeviceExtras && string.IsNullOrWhiteSpace(target.DeviceExtra)) { throw new InvalidOperationException( $"No ROCm device package is available for '{state.RuntimeGfxArch ?? "unknown"}' " @@ -274,9 +276,8 @@ public async Task InstallWindowsNativeTorchAsync( ); } - var multiArchDeviceExtra = target.DeviceExtra; var installConfig = profile.InstallConfig; - var multiArchPythonPackageIndexUrl = target.IndexUrl; + var indexUrl = target.IndexUrl; var usesNightlyIndex = target.IndexKind == RocmIndexKind.AmdNightly; // AMD's torchvision wheels carry no torch constraint, and the matched build may be published only @@ -285,8 +286,8 @@ public async Task InstallWindowsNativeTorchAsync( var torchResolution = await ResolveTorchInstallAsync( venvRunner, - multiArchPythonPackageIndexUrl, - multiArchDeviceExtra, + indexUrl, + target.DeviceExtra, usesNightlyIndex, onConsoleOutput, cancellationToken @@ -297,7 +298,7 @@ public async Task InstallWindowsNativeTorchAsync( var torchArgs = ApplyRocmTorchInstallOptions( BuildRocmTorchArgs( - multiArchPythonPackageIndexUrl, + indexUrl, torchResolution.TorchSpecifier, torchResolution.TorchvisionSpecifier ), @@ -326,14 +327,14 @@ public async Task InstallWindowsNativeTorchAsync( await venvRunner.PipInstall(postTorchInstallPipArgs, onConsoleOutput).ConfigureAwait(false); } - await VerifyWindowsNativeTorchInstallAsync(venvRunner, onConsoleOutput, cancellationToken) + await VerifyRocmTorchInstallAsync(venvRunner, onConsoleOutput, cancellationToken) .ConfigureAwait(false); } /// /// Applies the profile's upgrade/reinstall flags and any user pip overrides to a ROCm torch install command. /// Scope is deliberately limited to the torch trio. A blanket --upgrade lets the resolver move - /// transitive dependencies against the multi-arch index, numpy foremost, where the only build may be + /// transitive dependencies against AMD's feed, numpy foremost, where the only build may be /// numpy 2.5.2 (cp312+). A blanket --force-reinstall reinstalls the whole graph for the same reason. /// Packages that must follow the torch build, amd-torch-device-gfx* and triton, are exact-pinned by the /// wheel metadata, so they are still replaced to satisfy the resolved pin. @@ -372,14 +373,22 @@ private static string[] BuildRepeatablePackageArgs(string flag, IEnumerable + /// + /// Requirement form for a torch package on the resolved index: a per-architecture extra on AMD's + /// feeds, a plain requirement on the upstream PyTorch feeds, which ship one multi-architecture wheel + /// or all supported device libraries. + /// + internal static string BuildTorchSpecifier(string packageName, string? deviceExtra) => + string.IsNullOrWhiteSpace(deviceExtra) ? packageName : $"{packageName}[{deviceExtra}]"; + internal static PipInstallArgs BuildRocmTorchArgs( - string multiArchPythonPackageIndexUrl, + string indexUrl, string torchSpecifier, string torchvisionSpecifier ) { return new PipInstallArgs() - .AddKeyedArgs("--index-url", ["--index-url", multiArchPythonPackageIndexUrl]) + .AddKeyedArgs("--index-url", ["--index-url", indexUrl]) .AddArg(new Argument(key: "torch", value: torchSpecifier)) .AddArg(new Argument(key: "torchvision", value: torchvisionSpecifier)) .AddArg("torchaudio"); @@ -401,27 +410,27 @@ bool AllowPrerelease /// private async Task ResolveTorchInstallAsync( IPyVenvRunner venvRunner, - string multiArchPythonPackageIndexUrl, - string multiArchDeviceExtra, + string indexUrl, + string? deviceExtra, bool usesNightlyIndex, Action? onConsoleOutput, CancellationToken cancellationToken ) { - var torchSpecifierBase = $"torch[{multiArchDeviceExtra}]"; - var torchvisionSpecifierBase = $"torchvision[{multiArchDeviceExtra}]"; + var torchSpecifierBase = BuildTorchSpecifier("torch", deviceExtra); + var torchvisionSpecifierBase = BuildTorchSpecifier("torchvision", deviceExtra); try { // torch: finals only on the stable channel, pre/dev builds on the nightly channel. var torchIndex = await venvRunner - .PipIndex("torch", multiArchPythonPackageIndexUrl, includePrerelease: usesNightlyIndex) + .PipIndex("torch", indexUrl, includePrerelease: usesNightlyIndex) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); // torchvision: allow pre-releases so the matched alpha (e.g. 0.29.0a0) is visible. var torchvisionIndex = await venvRunner - .PipIndex("torchvision", multiArchPythonPackageIndexUrl, includePrerelease: true) + .PipIndex("torchvision", indexUrl, includePrerelease: true) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -539,7 +548,7 @@ private RocmCompatibilityResult BuildCompatibilityResult() }; } - private RocmMachineState ResolveWindowsMachineState() + private RocmMachineState ResolveMachineState() { if (settingsManager.Settings.PreferredGpu is { } preferredGpu && !preferredGpu.IsAmd) { @@ -680,7 +689,7 @@ private static IReadOnlyList GetAmdGpuCandidates() /// /// Verifies that the installed torch build still reports usable ROCm metadata after helper-managed installs complete. /// - private static async Task VerifyWindowsNativeTorchInstallAsync( + private static async Task VerifyRocmTorchInstallAsync( IPyVenvRunner venvRunner, Action? onConsoleOutput, CancellationToken cancellationToken @@ -691,7 +700,7 @@ CancellationToken cancellationToken var torchInfo = await venvRunner.PipShow("torch").ConfigureAwait(false); if (torchInfo is null) { - throw new InvalidOperationException("torch was not installed after Windows ROCm setup."); + throw new InvalidOperationException("torch was not installed after ROCm setup."); } var verificationResult = await venvRunner @@ -751,7 +760,7 @@ CancellationToken cancellationToken ); } - // AMD's torchvision wheels declare no torch version constraint, so an ABI-mismatched pair only + // If AMD's torchvision wheels declare no torch version constraint, so an ABI-mismatched pair only // fails at import time. Importing it here surfaces that as an install failure instead of a // confusing runtime error later. if (string.IsNullOrWhiteSpace(torchvisionVersion)) diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 7b1efea48..0f615597d 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -278,7 +278,6 @@ public void ResolveTarget_WindowsStable_KeepsAmdFeedAndDeviceExtra() Assert.AreEqual("device-gfx1201", target.DeviceExtra); Assert.IsTrue(target.SupportsDeviceExtras); Assert.IsTrue(target.SupportsSdkDevel); - Assert.IsTrue(target.RequiresPairResolution); } [TestMethod] @@ -309,7 +308,6 @@ public void ResolveTarget_PyTorchOverride_DropsDeviceExtrasAndSdkDevel() Assert.IsNull(target.DeviceExtra); Assert.IsFalse(target.SupportsDeviceExtras); Assert.IsFalse(target.SupportsSdkDevel); - Assert.IsFalse(target.RequiresPairResolution); } [TestMethod] @@ -321,6 +319,20 @@ public void ResolveTarget_UnsupportedArch_HasNoDeviceExtra() Assert.AreEqual(RocmIndexKind.AmdStable, target.IndexKind); } + [TestMethod] + [DataRow("torch", "device-gfx1201", "torch[device-gfx1201]")] + [DataRow("torchvision", "device-gfx1201", "torchvision[device-gfx1201]")] + [DataRow("torch", null, "torch")] + [DataRow("torchvision", "", "torchvision")] + public void BuildTorchSpecifier_UsesDeviceExtrasOnlyWhenTheIndexHasThem( + string package, + string? deviceExtra, + string expected + ) + { + Assert.AreEqual(expected, RocmPackageHelper.BuildTorchSpecifier(package, deviceExtra)); + } + [TestMethod] public void ApplyDeviceVisibility_PinsSelectedDeviceWhenSeveralAmdGpusExist() { From bd6e0deae64dcc930fad8af2feccfa65dfda8240 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Wed, 21 Oct 2026 03:03:44 +0000 Subject: [PATCH 10/11] feat(rocm): run ComfyUI's ROCm installs through the helper on Linux - Stop short circuiting the compatibility answer on non-Windows, so GetCompatibility is the single source of truth on both platforms - Drop the Windows-only gate from ShouldApplyLaunchEnvironment and from ComfyUI's install, sage-attention check and launch environment paths - ComfyUI now installs torch from the resolved index on Linux, for example on a gfx1100 box means AMD's stable feed with the device-gfx1100 extra and the matching rocm-sdk-devel, and picks up the helper's launch environment - Move ComfyUI's fallback ROCm index from the rocm7.2 channel to rocm7.14, which is the upstream PyTorch channel the fallback path should target as a last resort - Keep InvokeAI's launch environment gated to Windows, so only ComfyUI's behaviour changes here - Make the experimental-support notice platform-neutral, since it now appears on Linux too --- StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs | 5 ----- StabilityMatrix.Core/Models/Packages/ComfyUI.cs | 10 ++++------ StabilityMatrix.Core/Models/Packages/InvokeAI.cs | 3 ++- .../Services/Rocm/RocmPackageHelper.cs | 4 ++-- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs index ee04ddf7c..948815b58 100644 --- a/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs +++ b/StabilityMatrix.Core/Models/Packages/BaseGitPackage.cs @@ -91,11 +91,6 @@ protected bool HasRocmSupport(IRocmPackageHelper rocmPackageHelper) protected RocmCompatibilityResult GetRocmCompatibility(IRocmPackageHelper rocmPackageHelper) { - if (!Compat.IsWindows) - { - return new RocmCompatibilityResult { IsCompatible = false }; - } - return rocmPackageHelper.GetCompatibility(); } diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index d83ac8299..c3cb1b1d7 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -488,7 +488,7 @@ public override async Task InstallPackage( ?? HardwareHelper.HasLegacyNvidiaGpu() ); - if (Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasRocmSupport()) + if (torchIndex == TorchIndex.Rocm && HasRocmSupport()) { var config = rocmPackageHelper.BuildRocmInstallConfig(ComfyRocmProfile.Default); @@ -522,7 +522,7 @@ await rocmPackageHelper ExtraPipArgs = ["numpy<2"], TorchaudioVersion = " ", // Request torchaudio without a specific version CudaIndex = isLegacyNvidia ? "cu126" : "cu130", - RocmIndex = "rocm7.2", + RocmIndex = "rocm7.14", XpuIndex = "xpu", UpgradePackages = true, PostInstallPipArgs = ["typing-extensions>=4.15.0"], @@ -540,7 +540,7 @@ await StandardPipInstallProcessAsync( .ConfigureAwait(false); } - if (!(Compat.IsWindows && torchIndex == TorchIndex.Rocm && HasRocmSupport())) + if (!(torchIndex == TorchIndex.Rocm && HasRocmSupport())) { try { @@ -1229,8 +1229,6 @@ private ImmutableDictionary GetEnvVars( InstalledPackage installedPackage ) { - // if we're not on windows or we don't have a windows rocm gpu, return original env - var hasRocmGpu = HasRocmSupport(); var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); // Add FFmpeg to PATH if it's installed (optional - for video processing) @@ -1245,7 +1243,7 @@ InstalledPackage installedPackage } } - if (!Compat.IsWindows || !hasRocmGpu || selectedTorchIndex != TorchIndex.Rocm) + if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) return env; var rocmEnvironment = rocmPackageHelper.BuildLaunchEnvironment(ComfyRocmProfile.Default); diff --git a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs index 272249c4b..ade44a62e 100644 --- a/StabilityMatrix.Core/Models/Packages/InvokeAI.cs +++ b/StabilityMatrix.Core/Models/Packages/InvokeAI.cs @@ -662,7 +662,8 @@ InstalledPackage installedPackage env = GetEnvVars(env, installPath); var selectedTorchIndex = installedPackage.PreferredTorchIndex ?? GetRecommendedTorchVersion(); - if (!rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) + // Gated to Windows until this package installs through the helper on Linux as well. + if (!Compat.IsWindows || !rocmPackageHelper.ShouldApplyLaunchEnvironment(selectedTorchIndex)) { return env; } diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 359019952..1d5c6c252 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -39,7 +39,7 @@ public class RocmPackageHelper : IRocmPackageHelper ); private static readonly string[] LaunchNoticeLines = [ - "Stability Matrix Windows ROCm Notice: Windows AMD ROCm support is experimental. Please report any issues to Stability Matrix first so it can be determined whether the issue is package-specific.", + "Stability Matrix ROCm Notice: AMD ROCm support is experimental. Please report any issues to Stability Matrix first so it can be determined whether the issue is package-specific.", "Because this setup may not be officially supported by package developers, only contact upstream support for issues clearly caused by the package itself.", ]; @@ -123,7 +123,7 @@ private IReadOnlyList GetLaunchNoticeLines() /// public bool ShouldApplyLaunchEnvironment(TorchIndex selectedTorchIndex) { - if (!Compat.IsWindows || selectedTorchIndex != TorchIndex.Rocm) + if (selectedTorchIndex != TorchIndex.Rocm) return false; return GetCompatibility().IsCompatible; } From 1559da9a1e8ccff53921afcc260e15f57cb3a817 Mon Sep 17 00:00:00 2001 From: NeuralFault Date: Thu, 22 Oct 2026 01:14:12 +0000 Subject: [PATCH 11/11] feat(rocm): make the development SDK follow torch, work on Linux, and report what it does - Let the Development SDK command run on Linux - Derive the ROCm line from the local label on the installed torch (2.14.0+rocm10.0.0 gives 10.0.0) and prefer a rocm-sdk-devel build that matches it, keeping the nightly build date token as the first match for dev builds - Warn explicitly when that line has no published build and the newest is installed instead, since headers and libraries would then come from a different ROCm release than the runtime loading them - Realign an already-installed development SDK at the end of every ROCm torch install or upgrade, so existing venvs stop drifting when torch moves - Run `rocm-sdk init` after the devel package installs, to expand the tree eagerly; a non-zero exit is a warning, since the SDK also initialises lazily on first use - Stream pip and init output into the install console through progress.AsProcessOutputHandler, so the dialog shows the resolve, download and install instead of only the final line - Remove Windows specific labeling for devel-sdk install output and dialog title - Tests covering label parsing, selection order, the exact match predicate and the empty-index case --- .../InstallRocmPackageCommandStep.cs | 84 +++++++++-- .../Models/Packages/ComfyUI.cs | 23 +-- .../Services/Rocm/RocmPackageHelper.cs | 136 ++++++++++++++++-- .../Core/RocmPackageHelperTests.cs | 68 +++++++++ 4 files changed, 280 insertions(+), 31 deletions(-) diff --git a/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs b/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs index 095d6c164..69a7705f3 100644 --- a/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs +++ b/StabilityMatrix.Core/Models/PackageModification/InstallRocmPackageCommandStep.cs @@ -1,4 +1,6 @@ -using StabilityMatrix.Core.Helper; +using NLog; +using StabilityMatrix.Core.Extensions; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Rocm; @@ -24,6 +26,8 @@ public class InstallRocmPackageCommandStep( IRocmPackageHelper rocmPackageHelper ) : IPackageStep { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + private const string BitsAndBytesWheelUrl = "https://github.com/0xDELUXA/bitsandbytes_win_rocm/releases/download/0.50.0.dev0-py3.12-rocm7.15-win_amd64_all/bitsandbytes-0.50.0.dev0-cp312-cp312-win_amd64.whl"; private const string AmdAiterWheelUrl = @@ -51,7 +55,7 @@ IRocmPackageHelper rocmPackageHelper CommandType switch { RocmPackageCommandType.SageAttention => "Installing Windows ROCm SageAttention", - RocmPackageCommandType.DevelopmentSdk => "Installing Windows ROCm Development SDK", + RocmPackageCommandType.DevelopmentSdk => "Installing ROCm Development SDK", RocmPackageCommandType.BitsAndBytes => "Installing Windows ROCm bitsandbytes", RocmPackageCommandType.FlashAttention => "Installing Windows ROCm Flash Attention", _ => "Running Windows ROCm package command", @@ -59,11 +63,10 @@ IRocmPackageHelper rocmPackageHelper public async Task ExecuteAsync(IProgress? progress = null) { - if (!OperatingSystem.IsWindows()) + // The development SDK is published for Linux as well; the compiled extras are Windows-only. + if (!OperatingSystem.IsWindows() && CommandType != RocmPackageCommandType.DevelopmentSdk) { - throw new PlatformNotSupportedException( - "Windows ROCm package commands are only supported on Windows." - ); + throw new PlatformNotSupportedException($"{CommandType} is only available on Windows."); } if (InstalledPackage.FullPath is null) @@ -144,7 +147,70 @@ private async Task ExecuteDevelopmentSdkAsync( ) { EnsureRocmCompatibility(); - await rocmPackageHelper.EnsureRocmSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); + await rocmPackageHelper + .EnsureRocmSdkDevelAsync(venvRunner, progress, progress.AsProcessOutputHandler()) + .ConfigureAwait(false); + await TryInitializeRocmSdkAsync(venvRunner, progress).ConfigureAwait(false); + } + + /// + /// Runs rocm-sdk init so the installed devel tree is expanded eagerly. Failures are logged and + /// surfaced as a warning rather than thrown, because the SDK also initialises lazily on first use. + /// + private async Task TryInitializeRocmSdkAsync( + IPyVenvRunner venvRunner, + IProgress? progress + ) + { + var sdkPath = venvRunner.RootPath.JoinFile( + Compat.IsWindows ? Path.Combine("Scripts", "rocm-sdk.exe") : Path.Combine("bin", "rocm-sdk") + ); + + if (!sdkPath.Exists) + { + Logger.Warn("rocm-sdk was not found at '{Path}'; skipping initialization.", sdkPath.FullPath); + return; + } + + var environment = venvRunner.EnvironmentVariables.ToDictionary( + pair => pair.Key, + pair => pair.Value, + StringComparer.OrdinalIgnoreCase + ); + + // Keep a stale system ROCm or HIP SDK from taking over the venv's toolchain. + foreach (var key in RocmSupport.ConflictingRocmEnvironmentVariables) + { + environment[key] = ProcessRunner.UnsetEnvironmentVariable; + } + + progress?.Report(new ProgressReport(-1f, "Initializing ROCm SDK...", isIndeterminate: true)); + + using var process = ProcessRunner.StartAnsiProcess( + fileName: sdkPath.FullPath, + arguments: "init", + workingDirectory: venvRunner.RootPath.FullPath, + outputDataReceived: progress.AsProcessOutputHandler(), + environmentVariables: environment + ); + + await process.WaitForExitAsync().ConfigureAwait(false); + await process.WaitUntilOutputEOF().ConfigureAwait(false); + + if (process.ExitCode == 0) + { + Logger.Info("rocm-sdk init completed."); + return; + } + + Logger.Warn("rocm-sdk init exited with {Code}.", process.ExitCode); + progress?.Report( + new ProgressReport( + -1f, + "ROCm SDK initialization reported an error; continuing.", + isIndeterminate: true + ) + ); } private async Task ExecuteSageAttentionAsync( @@ -154,7 +220,9 @@ private async Task ExecuteSageAttentionAsync( { EnsureRocmCompatibility(); await EnsureVcBuildToolsAsync(progress).ConfigureAwait(false); - await rocmPackageHelper.EnsureRocmSdkDevelAsync(venvRunner, progress).ConfigureAwait(false); + await rocmPackageHelper + .EnsureRocmSdkDevelAsync(venvRunner, progress, progress.AsProcessOutputHandler()) + .ConfigureAwait(false); progress?.Report( new ProgressReport( diff --git a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs index 5573afa5f..cc550f051 100644 --- a/StabilityMatrix.Core/Models/Packages/ComfyUI.cs +++ b/StabilityMatrix.Core/Models/Packages/ComfyUI.cs @@ -432,14 +432,6 @@ public override List GetExtraCommands() } ); - commands.Add( - new ExtraPackageCommand - { - CommandName = "Install ROCm Development SDK", - Command = InstallWindowsRocmDevelopmentSdk, - } - ); - commands.Add( new ExtraPackageCommand { @@ -456,6 +448,17 @@ public override List GetExtraCommands() ); } + if (HasRocmSupport()) + { + commands.Add( + new ExtraPackageCommand + { + CommandName = "Install ROCm Development SDK", + Command = InstallRocmDevelopmentSdk, + } + ); + } + if (!Compat.IsMacOS && SettingsManager.Settings.PreferredGpu?.ComputeCapabilityValue is >= 7.5m) { commands.Add( @@ -1063,12 +1066,12 @@ private async Task InstallWindowsRocmSageAttention(InstalledPackage? installedPa await EnableSageAttentionAsync(installedPackage).ConfigureAwait(false); } - private async Task InstallWindowsRocmDevelopmentSdk(InstalledPackage? installedPackage) + private async Task InstallRocmDevelopmentSdk(InstalledPackage? installedPackage) { await RunRocmPackageCommandAsync( installedPackage, RocmPackageCommandType.DevelopmentSdk, - "Windows ROCm Development SDK installed successfully", + "ROCm Development SDK installed successfully", includeEnvironmentVariables: false ) .ConfigureAwait(false); diff --git a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs index 1d5c6c252..bde4f1d2b 100644 --- a/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs +++ b/StabilityMatrix.Core/Services/Rocm/RocmPackageHelper.cs @@ -135,8 +135,9 @@ public IReadOnlyList GetLaunchNoticeLines(TorchIndex selectedTorchIndex) } /// - /// Ensures rocm-sdk-devel is installed from the resolved ROCm index. - /// It prefers a build whose date token matches the installed ROCm torch build and falls back to the latest available build when no exact match is available. + /// Ensures rocm-sdk-devel is installed from the resolved ROCm index, on the same ROCm line as + /// the installed torch build. Nightly builds match on their build date token, stable builds on the + /// ROCm version in torch's local label, and only when neither matches is the newest build used. /// public async Task EnsureRocmSdkDevelAsync( IPyVenvRunner venvRunner, @@ -176,10 +177,16 @@ public async Task EnsureRocmSdkDevelAsync( } var nightlyBuildDateToken = TryGetNightlyBuildDateToken(torchInfo.Version); + var rocmVersion = TryGetRocmVersionLabel(torchInfo.Version); var installedRocmSdkDevel = await venvRunner.PipShow(RocmSdkDevelPackageName).ConfigureAwait(false); + + // Already on the right line: a nightly build with the matching date token, or a stable build + // matching the ROCm version the torch wheel was compiled against. if ( - !string.IsNullOrWhiteSpace(nightlyBuildDateToken) - && HasNightlyBuildDateToken(installedRocmSdkDevel?.Version, nightlyBuildDateToken) + ( + !string.IsNullOrWhiteSpace(nightlyBuildDateToken) + && HasNightlyBuildDateToken(installedRocmSdkDevel?.Version, nightlyBuildDateToken) + ) || IsRocmSdkDevelMatch(installedRocmSdkDevel?.Version, rocmVersion) ) { return; @@ -187,21 +194,22 @@ public async Task EnsureRocmSdkDevelAsync( var indexResult = await venvRunner.PipIndex(RocmSdkDevelPackageName, indexUrl).ConfigureAwait(false); - var latestVersion = indexResult?.AvailableVersions.FirstOrDefault(); - var matchingVersion = string.IsNullOrWhiteSpace(nightlyBuildDateToken) - ? null - : indexResult?.AvailableVersions.FirstOrDefault(version => - HasNightlyBuildDateToken(version, nightlyBuildDateToken) - ); - var versionToInstall = matchingVersion ?? latestVersion; + var selection = SelectSdkDevelVersion( + indexResult?.AvailableVersions, + rocmVersion, + nightlyBuildDateToken + ); - if (string.IsNullOrWhiteSpace(versionToInstall)) + if (selection is not { } chosen) { throw new InvalidOperationException( - $"No {RocmSdkDevelPackageName} builds were found on the ROCm multi-arch index." + $"No {RocmSdkDevelPackageName} builds were found on the ROCm index." ); } + var versionToInstall = chosen.Version; + var matchingVersion = chosen.IsExactMatch ? versionToInstall : null; + if (!string.IsNullOrWhiteSpace(matchingVersion)) { progress?.Report( @@ -214,6 +222,19 @@ public async Task EnsureRocmSdkDevelAsync( } else { + if (!string.IsNullOrWhiteSpace(rocmVersion)) + { + Logger.Warn( + "No {Package} build matches the ROCm {RocmVersion} line of the installed torch " + + "({TorchVersion}); installing {Version} instead. Builds against the development " + + "SDK may then not load in this environment.", + RocmSdkDevelPackageName, + rocmVersion, + torchInfo.Version, + versionToInstall + ); + } + progress?.Report( new ProgressReport( -1f, @@ -329,6 +350,14 @@ public async Task InstallRocmTorchAsync( await VerifyRocmTorchInstallAsync(venvRunner, onConsoleOutput, cancellationToken) .ConfigureAwait(false); + + // A development SDK that is already present has to follow the torch build it sits beside, or the + // headers and libraries come from a different ROCm line than the runtime loading them. + if (await venvRunner.PipShow(RocmSdkDevelPackageName).ConfigureAwait(false) is not null) + { + await EnsureRocmSdkDevelAsync(venvRunner, progress, onConsoleOutput, cancellationToken) + .ConfigureAwait(false); + } } /// @@ -820,6 +849,87 @@ private static bool HasNightlyBuildDateToken(string? version, string nightlyBuil && version.Contains($"dev{nightlyBuildDateToken}", StringComparison.OrdinalIgnoreCase); } + /// + /// ROCm version a torch build was compiled against, taken from its local label: 2.14.0+rocm10.0.0 + /// becomes 10.0.0. Returns null when the build carries no rocm local label. + /// + internal static string? TryGetRocmVersionLabel(string? torchVersion) + { + if (string.IsNullOrWhiteSpace(torchVersion)) + { + return null; + } + + var plusIndex = torchVersion.IndexOf('+'); + if (plusIndex < 0 || plusIndex == torchVersion.Length - 1) + { + return null; + } + + var localLabel = torchVersion[(plusIndex + 1)..].Trim(); + const string prefix = "rocm"; + if (!localLabel.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var version = localLabel[prefix.Length..].Trim(); + return version.Length == 0 ? null : version; + } + + /// True when a rocm-sdk-devel build sits on the given ROCm version line. + internal static bool IsRocmSdkDevelMatch(string? sdkDevelVersion, string? rocmVersion) + { + return !string.IsNullOrWhiteSpace(sdkDevelVersion) + && !string.IsNullOrWhiteSpace(rocmVersion) + && string.Equals(sdkDevelVersion.Trim(), rocmVersion.Trim(), StringComparison.OrdinalIgnoreCase); + } + + /// A rocm-sdk-devel build to install, and whether it matched the torch build exactly. + internal readonly record struct SdkDevelSelection(string Version, bool IsExactMatch); + + /// + /// Chooses the rocm-sdk-devel build for a torch install: the nightly build date token first, then the + /// ROCm line of the torch wheel, and the newest published build only when neither matches. + /// + internal static SdkDevelSelection? SelectSdkDevelVersion( + IReadOnlyList? availableVersions, + string? rocmVersion, + string? nightlyBuildDateToken + ) + { + if (availableVersions is null || availableVersions.Count == 0) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(nightlyBuildDateToken)) + { + var nightly = availableVersions.FirstOrDefault(version => + HasNightlyBuildDateToken(version, nightlyBuildDateToken) + ); + + if (nightly is not null) + { + return new SdkDevelSelection(nightly, true); + } + } + + if (!string.IsNullOrWhiteSpace(rocmVersion)) + { + var exact = availableVersions.FirstOrDefault(version => + IsRocmSdkDevelMatch(version, rocmVersion) + ); + + if (exact is not null) + { + return new SdkDevelSelection(exact, true); + } + } + + return new SdkDevelSelection(availableVersions[0], false); + } + internal static string? TryExtractJsonObject(string output) { if (string.IsNullOrWhiteSpace(output)) diff --git a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs index 0f615597d..d5fb8c641 100644 --- a/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs +++ b/StabilityMatrix.Tests/Core/RocmPackageHelperTests.cs @@ -268,6 +268,74 @@ public void SelectPreferredGpu_PrefersTheCardWithMoreMemory() Assert.AreEqual(dgpu, RocmPackageHelper.SelectPreferredGpu([dgpu, apu])); } + [TestMethod] + [DataRow("2.14.0+rocm10.0.0", "10.0.0")] + [DataRow("2.14.0+rocm10.1.0", "10.1.0")] + [DataRow("2.13.0+rocm7.2.0", "7.2.0")] + [DataRow("2.14.0", null)] + [DataRow("2.14.0+cpu", null)] + [DataRow("2.14.0+cu130", null)] + [DataRow("", null)] + public void TryGetRocmVersionLabel_ReadsTheLocalLabel(string torchVersion, string? expected) + { + Assert.AreEqual(expected, RocmPackageHelper.TryGetRocmVersionLabel(torchVersion)); + } + + [TestMethod] + public void SelectSdkDevelVersion_PrefersTheRocmLineOfTheTorchBuild() + { + // The exact case from a gfx1100 install: torch landed on 10.0.0 because torchvision 0.29.0 only + // exists for that line, and the newest devel on the index was 10.1.0. + string[] available = ["10.1.0", "10.0.0"]; + + var selection = RocmPackageHelper.SelectSdkDevelVersion(available, "10.0.0", null); + + Assert.IsNotNull(selection); + Assert.AreEqual("10.0.0", selection.Value.Version); + Assert.IsTrue(selection.Value.IsExactMatch); + } + + [TestMethod] + public void SelectSdkDevelVersion_PrefersTheNightlyDateToken() + { + string[] available = ["10.1.0.dev20261006", "10.1.0.dev20261001", "10.0.0"]; + + var selection = RocmPackageHelper.SelectSdkDevelVersion(available, "10.1.0", "20261001"); + + Assert.IsNotNull(selection); + Assert.AreEqual("10.1.0.dev20261001", selection.Value.Version); + Assert.IsTrue(selection.Value.IsExactMatch); + } + + [TestMethod] + public void SelectSdkDevelVersion_FallsBackToNewestWhenTheLineIsMissing() + { + string[] available = ["10.1.0"]; + + var selection = RocmPackageHelper.SelectSdkDevelVersion(available, "10.0.0", null); + + Assert.IsNotNull(selection); + Assert.AreEqual("10.1.0", selection.Value.Version); + Assert.IsFalse(selection.Value.IsExactMatch); + } + + [TestMethod] + public void SelectSdkDevelVersion_ReturnsNullWhenNothingIsPublished() + { + Assert.IsNull(RocmPackageHelper.SelectSdkDevelVersion([], "10.0.0", null)); + Assert.IsNull(RocmPackageHelper.SelectSdkDevelVersion(null, "10.0.0", null)); + } + + [TestMethod] + [DataRow("10.0.0", "10.0.0", true)] + [DataRow("10.1.0", "10.0.0", false)] + [DataRow("10.0.0", null, false)] + [DataRow(null, "10.0.0", false)] + public void IsRocmSdkDevelMatch_ComparesTheRocmLine(string? sdkDevel, string? rocm, bool expected) + { + Assert.AreEqual(expected, RocmPackageHelper.IsRocmSdkDevelMatch(sdkDevel, rocm)); + } + [TestMethod] public void ResolveTarget_WindowsStable_KeepsAmdFeedAndDeviceExtra() {