From e9ecb141db7ea2c33a8e0429eda90997d4f244ea Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Thu, 21 Mar 2024 23:05:18 +0100 Subject: [PATCH] Added backend abstraction to allow loading them at runtime depending on the capabilities of the system --- StableDiffusion.NET/Backends/Backends.cs | 44 +++++++ StableDiffusion.NET/Backends/CpuBackend.cs | 102 ++++++++++++++++ StableDiffusion.NET/Backends/CudaBackend.cs | 114 ++++++++++++++++++ StableDiffusion.NET/Backends/IBackend.cs | 9 ++ StableDiffusion.NET/Backends/RocmBackend.cs | 22 ++++ .../Extensions/EnumExtension.cs | 16 +++ StableDiffusion.NET/Native/Native.Load.cs | 103 ++++++++++++++++ StableDiffusion.NET/{ => Native}/Native.cs | 0 .../StableDiffusion.NET.csproj.DotSettings | 4 +- 9 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 StableDiffusion.NET/Backends/Backends.cs create mode 100644 StableDiffusion.NET/Backends/CpuBackend.cs create mode 100644 StableDiffusion.NET/Backends/CudaBackend.cs create mode 100644 StableDiffusion.NET/Backends/IBackend.cs create mode 100644 StableDiffusion.NET/Backends/RocmBackend.cs create mode 100644 StableDiffusion.NET/Extensions/EnumExtension.cs create mode 100644 StableDiffusion.NET/Native/Native.Load.cs rename StableDiffusion.NET/{ => Native}/Native.cs (100%) diff --git a/StableDiffusion.NET/Backends/Backends.cs b/StableDiffusion.NET/Backends/Backends.cs new file mode 100644 index 0000000..c660ed9 --- /dev/null +++ b/StableDiffusion.NET/Backends/Backends.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StableDiffusion.NET; + +public static class Backends +{ + #region Properties & Fields + + public static CpuBackend CpuBackend { get; } = new(); + public static CudaBackend CudaBackend { get; } = new(); + public static RocmBackend RocmBackend { get; } = new(); + + private static readonly List CUSTOM_BACKENDS = []; + public static IReadOnlyList CustomBackends => CUSTOM_BACKENDS.AsReadOnly(); + + public static IEnumerable RegisteredBackends => [CpuBackend, CudaBackend, RocmBackend, .. CUSTOM_BACKENDS]; + public static IEnumerable AvailableBackends => RegisteredBackends.Where(x => x.IsAvailable); + public static IEnumerable ActiveBackends => AvailableBackends.Where(x => x.IsEnabled); + + public static List SearchPaths { get; } = []; + + #endregion + + #region Methods + + public static bool RegisterBackend(IBackend backend) + { + if (backend is NET.CpuBackend or NET.CudaBackend or NET.RocmBackend) + throw new ArgumentException("Default backends can't be registered again."); + + if (CUSTOM_BACKENDS.Contains(backend)) + return false; + + CUSTOM_BACKENDS.Add(backend); + return true; + } + + public static bool UnregisterBackend(IBackend backend) + => CUSTOM_BACKENDS.Remove(backend); + + #endregion +} \ No newline at end of file diff --git a/StableDiffusion.NET/Backends/CpuBackend.cs b/StableDiffusion.NET/Backends/CpuBackend.cs new file mode 100644 index 0000000..ab5f70f --- /dev/null +++ b/StableDiffusion.NET/Backends/CpuBackend.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using StableDiffusion.NET.Extensions; + +namespace StableDiffusion.NET; + +public class CpuBackend : IBackend +{ + #region Properties & Fields + + public bool IsEnabled { get; set; } = true; + + public int Priority => 0; + + public bool IsAvailable => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + && (RuntimeInformation.OSArchitecture == Architecture.X64); + + public string PathPart => Avx.GetDescription(); + + private readonly List _availableAvxLevels = []; + public IEnumerable AvailableAvxLevels => _availableAvxLevels.AsReadOnly(); + + private AvxLevel _avx; + public AvxLevel Avx + { + get => _avx; + set + { + if (!_availableAvxLevels.Contains(value)) throw new ArgumentException("The selected AVX-Level is not supported on this system."); + _avx = value; + } + } + + #endregion + + #region Constructors + + internal CpuBackend() + { + _availableAvxLevels.Add(AvxLevel.None); + Avx = AvxLevel.None; + + if (System.Runtime.Intrinsics.X86.Avx.IsSupported) + { + _availableAvxLevels.Add(AvxLevel.Avx); + Avx = AvxLevel.Avx; + } + + if (System.Runtime.Intrinsics.X86.Avx2.IsSupported) + { + _availableAvxLevels.Add(AvxLevel.Avx2); + Avx = AvxLevel.Avx2; + } + + if (CheckAvx512()) + { + _availableAvxLevels.Add(AvxLevel.Avx512); + Avx = AvxLevel.Avx512; + } + } + + #endregion + + #region Methods + + private static bool CheckAvx512() + { + if (!System.Runtime.Intrinsics.X86.X86Base.IsSupported) + return false; + + (_, int _, int ecx, _) = System.Runtime.Intrinsics.X86.X86Base.CpuId(7, 0); + + bool vnni = (ecx & 0b_1000_0000_0000) != 0; + + bool f = System.Runtime.Intrinsics.X86.Avx512F.IsSupported; + bool bw = System.Runtime.Intrinsics.X86.Avx512BW.IsSupported; + bool vbmi = System.Runtime.Intrinsics.X86.Avx512Vbmi.IsSupported; + + return vnni && vbmi && bw && f; + } + + #endregion + + public enum AvxLevel + { + [Description("")] + None, + + [Description("avx")] + Avx, + + [Description("avx2")] + Avx2, + + [Description("avx512")] + Avx512, + } +} \ No newline at end of file diff --git a/StableDiffusion.NET/Backends/CudaBackend.cs b/StableDiffusion.NET/Backends/CudaBackend.cs new file mode 100644 index 0000000..e96d7d2 --- /dev/null +++ b/StableDiffusion.NET/Backends/CudaBackend.cs @@ -0,0 +1,114 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Text.Json; +using System; + +namespace StableDiffusion.NET; + +public class CudaBackend : IBackend +{ + #region Constants + + private const string CUDA_VERSION_FILE = "version.json"; + + #endregion + + #region Properties & Fields + + public bool IsEnabled { get; set; } = true; + + public int Priority => 10; + + public bool IsAvailable => (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + && (RuntimeInformation.OSArchitecture == Architecture.X64) + && CudaVersion is 11 or 12; + + public string PathPart => CudaVersion switch + { + 11 => "cuda11", + 12 => "cuda12", + _ => string.Empty + }; + + public int CudaVersion { get; } + + #endregion + + #region Constructors + + internal CudaBackend() + { + CudaVersion = GetCudaMajorVersion(); + } + + #endregion + + #region Methods + + private static int GetCudaMajorVersion() + { + try + { + string? cudaPath; + string version = ""; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + cudaPath = Environment.GetEnvironmentVariable("CUDA_PATH"); + if (cudaPath is null) return -1; + + version = GetCudaVersionFromPath(cudaPath); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + cudaPath = "/usr/local/bin/cuda"; + version = GetCudaVersionFromPath(cudaPath); + if (string.IsNullOrEmpty(version)) + { + cudaPath = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH"); + if (cudaPath is null) + return -1; + + foreach (string path in cudaPath.Split(':')) + { + version = GetCudaVersionFromPath(Path.Combine(path, "..")); + if (string.IsNullOrEmpty(version)) + break; + } + } + } + + if (string.IsNullOrEmpty(version)) + return -1; + + version = version.Split('.')[0]; + if (int.TryParse(version, out int majorVersion)) + return majorVersion; + } + catch { /* No version or error */ } + + return -1; + } + + private static string GetCudaVersionFromPath(string cudaPath) + { + try + { + string json = File.ReadAllText(Path.Combine(cudaPath, CUDA_VERSION_FILE)); + using JsonDocument document = JsonDocument.Parse(json); + JsonElement root = document.RootElement; + JsonElement cublasNode = root.GetProperty("libcublas"); + JsonElement versionNode = cublasNode.GetProperty("version"); + if (versionNode.ValueKind == JsonValueKind.Undefined) + return string.Empty; + + return versionNode.GetString() ?? ""; + } + catch (Exception) + { + return string.Empty; + } + } + + #endregion +} \ No newline at end of file diff --git a/StableDiffusion.NET/Backends/IBackend.cs b/StableDiffusion.NET/Backends/IBackend.cs new file mode 100644 index 0000000..75c43b2 --- /dev/null +++ b/StableDiffusion.NET/Backends/IBackend.cs @@ -0,0 +1,9 @@ +namespace StableDiffusion.NET; + +public interface IBackend +{ + bool IsEnabled { get; set; } + public int Priority { get; } + bool IsAvailable { get; } + string PathPart { get; } +} \ No newline at end of file diff --git a/StableDiffusion.NET/Backends/RocmBackend.cs b/StableDiffusion.NET/Backends/RocmBackend.cs new file mode 100644 index 0000000..33a0fb8 --- /dev/null +++ b/StableDiffusion.NET/Backends/RocmBackend.cs @@ -0,0 +1,22 @@ +namespace StableDiffusion.NET; + +public class RocmBackend : IBackend +{ + #region Properties & Fields + + public bool IsEnabled { get; set; } = true; + + public int Priority => 10; + + public bool IsAvailable => false; + + public string PathPart { get; } = string.Empty; + + #endregion + + #region Constructors + + internal RocmBackend() { } + + #endregion +} \ No newline at end of file diff --git a/StableDiffusion.NET/Extensions/EnumExtension.cs b/StableDiffusion.NET/Extensions/EnumExtension.cs new file mode 100644 index 0000000..33eeea5 --- /dev/null +++ b/StableDiffusion.NET/Extensions/EnumExtension.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; + +namespace StableDiffusion.NET.Extensions; + +internal static class EnumExtension +{ + public static string GetDescription(this Enum value) + { + DescriptionAttribute[]? attributes = (DescriptionAttribute[]?)value.GetType().GetField(value.ToString())?.GetCustomAttributes(typeof(DescriptionAttribute), false); + + return attributes?.Length > 0 + ? attributes[0].Description + : value.ToString(); + } +} \ No newline at end of file diff --git a/StableDiffusion.NET/Native/Native.Load.cs b/StableDiffusion.NET/Native/Native.Load.cs new file mode 100644 index 0000000..0556723 --- /dev/null +++ b/StableDiffusion.NET/Native/Native.Load.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace StableDiffusion.NET; + +internal static partial class Native +{ + #region Properties & Fields + + private static nint _loadedLibraryHandle; + + #endregion + + #region Constructors + + static Native() + { + NativeLibrary.SetDllImportResolver(typeof(Native).Assembly, ResolveDllImport); + } + + #endregion + + #region Methods + + private static nint ResolveDllImport(string libraryname, Assembly assembly, DllImportSearchPath? searchpath) + { + if (libraryname != LIB_NAME) return nint.Zero; + if (_loadedLibraryHandle != nint.Zero) return _loadedLibraryHandle; + + _loadedLibraryHandle = TryLoadLibrary(); + + return _loadedLibraryHandle; + } + + private static nint TryLoadLibrary() + { + GetPlatformPathParts(out string os, out string fileExtension, out string libPrefix); + + foreach (IBackend backend in Backends.ActiveBackends.OrderBy(x => x.Priority)) + { + string path = Path.Combine("runtimes", os, "native", backend.PathPart, $"{libPrefix}{LIB_NAME}{fileExtension}"); + + string fullPath = TryFindPath(path); + nint result = TryLoad(fullPath); + + if (result != nint.Zero) + return result; + } + + return nint.Zero; + + static nint TryLoad(string path) + { + if (NativeLibrary.TryLoad(path, out nint handle)) + return handle; + + return nint.Zero; + } + + static string TryFindPath(string filename) + { + IEnumerable searchPaths = [.. Backends.SearchPaths, AppDomain.CurrentDomain.BaseDirectory, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ""]; + foreach (string path in searchPaths) + { + string candidate = Path.Combine(path, filename); + if (File.Exists(candidate)) + return candidate; + } + + return filename; + } + } + + private static void GetPlatformPathParts(out string os, out string fileExtension, out string libPrefix) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + os = "win-x64"; + fileExtension = ".dll"; + libPrefix = ""; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + os = "linux-x64"; + fileExtension = ".so"; + libPrefix = "lib"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + fileExtension = ".dylib"; + os = "osx-x64"; + libPrefix = "lib"; + } + else + throw new NotSupportedException("Your operating system is not supported."); + } + + #endregion +} \ No newline at end of file diff --git a/StableDiffusion.NET/Native.cs b/StableDiffusion.NET/Native/Native.cs similarity index 100% rename from StableDiffusion.NET/Native.cs rename to StableDiffusion.NET/Native/Native.cs diff --git a/StableDiffusion.NET/StableDiffusion.NET.csproj.DotSettings b/StableDiffusion.NET/StableDiffusion.NET.csproj.DotSettings index eac4bf8..d4deeb5 100644 --- a/StableDiffusion.NET/StableDiffusion.NET.csproj.DotSettings +++ b/StableDiffusion.NET/StableDiffusion.NET.csproj.DotSettings @@ -1,5 +1,7 @@  True + True True True - True \ No newline at end of file + True + True \ No newline at end of file