From 83b7827dbd0aa5a68fdefb544ebfbf9b849131c8 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Fri, 28 Nov 2025 23:45:40 +0800 Subject: [PATCH 01/34] fix --- AIDevGallery/Models/GenAIConfig.cs | 34 +++++++---- AIDevGallery/Models/ModelCompatibility.cs | 20 ++++++- AIDevGallery/Utils/DeviceUtils.cs | 73 +++++++++++++++++++++++ AIDevGallery/Utils/UserAddedModelUtil.cs | 54 +++++++++++++++-- 4 files changed, 165 insertions(+), 16 deletions(-) diff --git a/AIDevGallery/Models/GenAIConfig.cs b/AIDevGallery/Models/GenAIConfig.cs index 738e107e..10a1b109 100644 --- a/AIDevGallery/Models/GenAIConfig.cs +++ b/AIDevGallery/Models/GenAIConfig.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Linq; +using System.Text.Json; using System.Text.Json.Serialization; namespace AIDevGallery.Models; @@ -35,16 +37,28 @@ internal class GenAISessionOptions internal class ProviderOptions { - [JsonPropertyName("dml")] - public Dml? Dml { get; set; } - [JsonPropertyName("cuda")] - public Cuda? Cuda { get; set; } -} + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } -internal class Dml -{ -} + public bool HasProvider(string name) + { + if (ExtensionData == null) return false; + return ExtensionData.Keys.Any(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); + } -internal class Cuda -{ + public Dictionary? GetProviderOptions(string name) + { + if (ExtensionData == null) return null; + var key = ExtensionData.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (key == null) return null; + + try + { + return JsonSerializer.Deserialize>(ExtensionData[key].GetRawText()); + } + catch + { + return new Dictionary(); + } + } } \ No newline at end of file diff --git a/AIDevGallery/Models/ModelCompatibility.cs b/AIDevGallery/Models/ModelCompatibility.cs index 2676212f..187bb5e4 100644 --- a/AIDevGallery/Models/ModelCompatibility.cs +++ b/AIDevGallery/Models/ModelCompatibility.cs @@ -53,11 +53,27 @@ public static ModelCompatibility GetModelCompatibility(ModelDetails modelDetails compatibility = ModelCompatibilityState.NotCompatible; description = "This model is not currently supported on Arm64 devices."; } - else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.CPU) || - (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.QNN) && DeviceUtils.IsArm64())) + else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.CPU)) { compatibility = ModelCompatibilityState.Compatible; } + else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.QNN) && DeviceUtils.IsArm64()) + { + compatibility = ModelCompatibilityState.Compatible; + } + else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.NPU)) + { + // NPU (non-QNN) support - typically Intel OpenVINO NPU + if (DeviceUtils.HasOpenVINONPU()) + { + compatibility = ModelCompatibilityState.Compatible; + } + else + { + compatibility = ModelCompatibilityState.NotCompatible; + description = "This model requires an Intel NPU with OpenVINO support. No compatible NPU was detected on your device."; + } + } else if ( (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.DML) || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.GPU)) && !DeviceUtils.IsArm64()) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 6943dd9a..63cc5511 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -111,4 +111,77 @@ public static bool IsArm64() { return System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; } + + public static bool HasOpenVINONPU() + { + try + { + // Method 1: Check for Intel NPU Software & Drivers installation + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var intelNpuPath = System.IO.Path.Combine(programFiles, "Intel", "Intel(R) NPU Software & Drivers"); + if (System.IO.Directory.Exists(intelNpuPath)) + { + return true; + } + + // Method 2: Check Windows Registry for NPU service + if (CheckIntelNPUInRegistry()) + { + return true; + } + + // Method 3: Check for OpenVINO environment variable + var openvinoPath = Environment.GetEnvironmentVariable("OPENVINO_INSTALL_DIR"); + if (!string.IsNullOrEmpty(openvinoPath) && System.IO.Directory.Exists(openvinoPath)) + { + return true; + } + + // Method 4: Check for OpenVINO runtime in common installation paths + var openvinoPaths = new[] + { + System.IO.Path.Combine(programFiles, "Intel", "openvino_2024"), + System.IO.Path.Combine(programFiles, "Intel", "openvino_2025"), + System.IO.Path.Combine(programFiles, "Intel", "openvino"), + }; + + foreach (var path in openvinoPaths) + { + if (System.IO.Directory.Exists(path)) + { + return true; + } + } + + return false; + } + catch + { + return false; + } + } + + private static bool CheckIntelNPUInRegistry() + { + try + { + // Check for NPU service in registry (the service is named "npu") + using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\npu"); + if (key != null) + { + // Verify it's an NPU driver by checking ImagePath + var imagePath = key.GetValue("ImagePath") as string; + if (!string.IsNullOrEmpty(imagePath) && imagePath.Contains("npu", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + catch + { + return false; + } + } } \ No newline at end of file diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 743853bd..debda217 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -314,10 +314,10 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable DML > NPU > GPU > CPU + if (configContents.Contains("\"backend_path\": \"QnnHtp.dll\"", StringComparison.OrdinalIgnoreCase)) { return HardwareAccelerator.QNN; } @@ -328,11 +328,57 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config throw new InvalidDataException("genai_config.json is not valid"); } - if (config.Model.Decoder.SessionOptions.ProviderOptions.Any(p => p.Dml != null)) + bool hasGpu = false; + bool hasNpu = false; + bool hasCpu = false; + + foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) { - return HardwareAccelerator.DML; + // Check QNN provider (highest priority) + if (provider.HasProvider("qnn")) + { + return HardwareAccelerator.QNN; + } + + // Check DML provider + if (provider.HasProvider("dml")) + { + return HardwareAccelerator.DML; + } + + // Check OpenVINO provider + var openvinoOptions = provider.GetProviderOptions("OpenVINO"); + if (openvinoOptions != null && openvinoOptions.TryGetValue("device_type", out var deviceType)) + { + var devType = deviceType.ToLowerInvariant(); + if (devType == "npu") hasNpu = true; + else if (devType == "gpu") hasGpu = true; + else if (devType == "cpu") hasCpu = true; + } + + // Check GPU providers + if (provider.HasProvider("cuda") || provider.HasProvider("tensorrt") || + provider.HasProvider("rocm") || provider.HasProvider("webgpu")) + { + hasGpu = true; + } + + // Check VitisAI provider (typically FPGA/NPU) + if (provider.HasProvider("vitisai")) + { + hasNpu = true; + } + + // Check CPU provider + if (provider.HasProvider("cpu")) + { + hasCpu = true; + } } + if (hasNpu) return HardwareAccelerator.NPU; + if (hasGpu) return HardwareAccelerator.GPU; + if (hasCpu) return HardwareAccelerator.CPU; return HardwareAccelerator.CPU; } From 84880e5b281495e9b8003aa910d63bd9740f9783 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Fri, 28 Nov 2025 23:47:12 +0800 Subject: [PATCH 02/34] fix --- AIDevGallery/Models/ModelCompatibility.cs | 19 ++++- AIDevGallery/Utils/DeviceUtils.cs | 90 ++++++++++++----------- 2 files changed, 62 insertions(+), 47 deletions(-) diff --git a/AIDevGallery/Models/ModelCompatibility.cs b/AIDevGallery/Models/ModelCompatibility.cs index 187bb5e4..039e4bc9 100644 --- a/AIDevGallery/Models/ModelCompatibility.cs +++ b/AIDevGallery/Models/ModelCompatibility.cs @@ -63,15 +63,28 @@ public static ModelCompatibility GetModelCompatibility(ModelDetails modelDetails } else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.NPU)) { - // NPU (non-QNN) support - typically Intel OpenVINO NPU - if (DeviceUtils.HasOpenVINONPU()) + // Check if any NPU is available using ONNX Runtime's EP detection + if (DeviceUtils.HasNPU()) { compatibility = ModelCompatibilityState.Compatible; } else { compatibility = ModelCompatibilityState.NotCompatible; - description = "This model requires an Intel NPU with OpenVINO support. No compatible NPU was detected on your device."; + description = "This model requires an NPU (Neural Processing Unit). No compatible NPU was detected on your device."; + } + } + else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.OpenVINO)) + { + // Specific OpenVINO EP check (can run on CPU/GPU/NPU) + if (DeviceUtils.HasOpenVINO()) + { + compatibility = ModelCompatibilityState.Compatible; + } + else + { + compatibility = ModelCompatibilityState.NotCompatible; + description = "This model requires OpenVINO Execution Provider. No compatible OpenVINO runtime was detected on your device."; } } else if ( diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 63cc5511..171c2cbc 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.ML.OnnxRuntime; using System; +using System.Linq; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dxgi; @@ -112,48 +114,59 @@ public static bool IsArm64() return System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; } - public static bool HasOpenVINONPU() + private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; + private static readonly object _epDevicesLock = new(); + + /// + /// Gets the list of available ONNX Runtime Execution Provider devices. + /// This method ensures that certified EPs (like OpenVINO, QNN, DML) are registered before querying, + /// as OrtEnv.GetEpDevices() only returns already-registered providers. + /// Results are cached to avoid repeated registration overhead. + /// + private static System.Collections.Generic.IReadOnlyList GetEpDevices() { - try + if (_cachedEpDevices != null) { - // Method 1: Check for Intel NPU Software & Drivers installation - var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); - var intelNpuPath = System.IO.Path.Combine(programFiles, "Intel", "Intel(R) NPU Software & Drivers"); - if (System.IO.Directory.Exists(intelNpuPath)) - { - return true; - } - - // Method 2: Check Windows Registry for NPU service - if (CheckIntelNPUInRegistry()) - { - return true; - } + return _cachedEpDevices; + } - // Method 3: Check for OpenVINO environment variable - var openvinoPath = Environment.GetEnvironmentVariable("OPENVINO_INSTALL_DIR"); - if (!string.IsNullOrEmpty(openvinoPath) && System.IO.Directory.Exists(openvinoPath)) + lock (_epDevicesLock) + { + if (_cachedEpDevices != null) { - return true; + return _cachedEpDevices; } - // Method 4: Check for OpenVINO runtime in common installation paths - var openvinoPaths = new[] + try { - System.IO.Path.Combine(programFiles, "Intel", "openvino_2024"), - System.IO.Path.Combine(programFiles, "Intel", "openvino_2025"), - System.IO.Path.Combine(programFiles, "Intel", "openvino"), - }; + OrtEnv.Instance(); + var catalog = Microsoft.Windows.AI.MachineLearning.ExecutionProviderCatalog.GetDefault(); - foreach (var path in openvinoPaths) - { - if (System.IO.Directory.Exists(path)) + try { - return true; + catalog.EnsureAndRegisterCertifiedAsync().GetAwaiter().GetResult(); } + catch + { + } + + _cachedEpDevices = OrtEnv.Instance().GetEpDevices(); + } + catch + { + _cachedEpDevices = System.Array.Empty(); } - return false; + return _cachedEpDevices; + } + } + + public static bool HasNPU() + { + try + { + return GetEpDevices().Any(device => + device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); } catch { @@ -161,23 +174,12 @@ public static bool HasOpenVINONPU() } } - private static bool CheckIntelNPUInRegistry() + public static bool HasOpenVINO() { try { - // Check for NPU service in registry (the service is named "npu") - using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Services\npu"); - if (key != null) - { - // Verify it's an NPU driver by checking ImagePath - var imagePath = key.GetValue("ImagePath") as string; - if (!string.IsNullOrEmpty(imagePath) && imagePath.Contains("npu", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; + return GetEpDevices().Any(device => + device.EpName.Equals("OpenVINOExecutionProvider", StringComparison.OrdinalIgnoreCase)); } catch { From e2842a484bfae09d21ea137161a99fccb284228a Mon Sep 17 00:00:00 2001 From: MillyWei Date: Fri, 28 Nov 2025 23:52:22 +0800 Subject: [PATCH 03/34] fix --- AIDevGallery/Models/GenAIConfig.cs | 2 ++ AIDevGallery/Utils/UserAddedModelUtil.cs | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/AIDevGallery/Models/GenAIConfig.cs b/AIDevGallery/Models/GenAIConfig.cs index 10a1b109..7a3c8eb0 100644 --- a/AIDevGallery/Models/GenAIConfig.cs +++ b/AIDevGallery/Models/GenAIConfig.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index debda217..1c97b5e2 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -348,12 +348,15 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config // Check OpenVINO provider var openvinoOptions = provider.GetProviderOptions("OpenVINO"); - if (openvinoOptions != null && openvinoOptions.TryGetValue("device_type", out var deviceType)) + if (openvinoOptions != null) { - var devType = deviceType.ToLowerInvariant(); - if (devType == "npu") hasNpu = true; - else if (devType == "gpu") hasGpu = true; - else if (devType == "cpu") hasCpu = true; + if (openvinoOptions.TryGetValue("device_type", out var deviceType)) + { + var devType = deviceType.ToLowerInvariant(); + if (devType == "npu") hasNpu = true; + else if (devType == "gpu") hasGpu = true; + else if (devType == "cpu") hasCpu = true; + } } // Check GPU providers From 360bb5cea57d63cd8c31c061c6e4055da75b5553 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Sat, 29 Nov 2025 00:41:50 +0800 Subject: [PATCH 04/34] update --- AIDevGallery/Models/ModelCompatibility.cs | 31 +++++--- AIDevGallery/Utils/DeviceUtils.cs | 86 +++++++++++++++-------- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/AIDevGallery/Models/ModelCompatibility.cs b/AIDevGallery/Models/ModelCompatibility.cs index 039e4bc9..35296c4e 100644 --- a/AIDevGallery/Models/ModelCompatibility.cs +++ b/AIDevGallery/Models/ModelCompatibility.cs @@ -91,33 +91,44 @@ public static ModelCompatibility GetModelCompatibility(ModelDetails modelDetails (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.DML) || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.GPU)) && !DeviceUtils.IsArm64()) { - var vram = DeviceUtils.GetVram(); + var dedicatedVram = DeviceUtils.GetVram(); + var totalVram = DeviceUtils.GetTotalVram(); var minimumSizeNeeded = Math.Round((float)(modelDetails.Size / BytesInGB), 1); - var vramInGb = Math.Round(vram / BytesInGB, 1); + var totalVramInGb = Math.Round(totalVram / BytesInGB, 1); + var isIntegratedGpu = dedicatedVram < BytesInGB; // we want at least 2GB more than model size for good performance - if (modelDetails.Size + (2 * BytesInGB) < vram) + if (modelDetails.Size + (2 * BytesInGB) < totalVram) { - compatibility = ModelCompatibilityState.Compatible; + // Warn about potential performance issues on integrated GPUs for larger models + if (isIntegratedGpu && modelDetails.Size > 0.5 * BytesInGB) + { + compatibility = ModelCompatibilityState.NotRecomended; + description = $"This model can run on your integrated GPU, but performance may be significantly slower than on a dedicated GPU. Your system has {totalVramInGb}GB of shared GPU memory available."; + } + else + { + compatibility = ModelCompatibilityState.Compatible; + } } // we want at least 1GB more than model size for some breathing room - else if (modelDetails.Size + BytesInGB < vram) + else if (modelDetails.Size + BytesInGB < totalVram) { compatibility = ModelCompatibilityState.NotRecomended; - description = $"This model is not recommended for your device. We recommend minimum {minimumSizeNeeded + 2}GB of dedicated GPU memory. Your GPU has {vramInGb}GB"; + description = $"This model is not recommended for your device. We recommend minimum {minimumSizeNeeded + 2}GB of GPU memory. Your GPU has {totalVramInGb}GB available"; } else { compatibility = ModelCompatibilityState.NotCompatible; - description = $"This model will not work on your device because it requires minimum {minimumSizeNeeded + 1}GB of dedicate GPU memory."; - if (vram == 0) + description = $"This model will not work on your device because it requires minimum {minimumSizeNeeded + 1}GB of GPU memory."; + if (totalVram == 0) { - description += " We could not find a dedicated GPU."; + description += " We could not find a compatible GPU."; } else { - description += $" Your GPU has {vramInGb}GB."; + description += $" Your GPU has {totalVramInGb}GB available."; } } } diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 171c2cbc..fa690e59 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -11,6 +11,9 @@ namespace AIDevGallery.Utils; internal static class DeviceUtils { + private static (ulong dedicated, ulong total)? _cachedVramInfo; + private static readonly object _vramLock = new(); + public static int GetBestDeviceId() { int deviceId = 0; @@ -61,54 +64,75 @@ public static int GetBestDeviceId() return deviceId; } - public static ulong GetVram() + private static (ulong dedicated, ulong total) GetVramInfo() { - nuint maxDedicatedVideoMemory = 0; - try + if (_cachedVramInfo.HasValue) { - DXGI_CREATE_FACTORY_FLAGS createFlags = 0; - Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); - IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; + return _cachedVramInfo.Value; + } - IDXGIAdapter1? selectedAdapter = null; + lock (_vramLock) + { + if (_cachedVramInfo.HasValue) + { + return _cachedVramInfo.Value; + } - var index = 0u; - do + nuint maxDedicatedVideoMemory = 0; + nuint maxTotalVideoMemory = 0; + + try { - var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); + DXGI_CREATE_FACTORY_FLAGS createFlags = 0; + Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); + IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; - if (result.Failed) - { - if (result != HRESULT.DXGI_ERROR_NOT_FOUND) - { - result.ThrowOnFailure(); - } + IDXGIAdapter1? selectedAdapter = null; - index = 0; - } - else + var index = 0u; + do { - DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); + var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); - if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) + if (result.Failed) { - maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; - selectedAdapter = dxgiAdapter1; + if (result != HRESULT.DXGI_ERROR_NOT_FOUND) + { + result.ThrowOnFailure(); + } + + index = 0; } + else + { + DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); - index++; - dxgiAdapter1 = null; + if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) + { + maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; + maxTotalVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory + dxgiAdapterDesc.SharedSystemMemory; + selectedAdapter = dxgiAdapter1; + } + + index++; + dxgiAdapter1 = null; + } } + while (index != 0); + } + catch (Exception) + { } - while (index != 0); - } - catch (Exception) - { - } - return maxDedicatedVideoMemory; + _cachedVramInfo = (maxDedicatedVideoMemory, maxTotalVideoMemory); + return _cachedVramInfo.Value; + } } + public static ulong GetVram() => GetVramInfo().dedicated; + + public static ulong GetTotalVram() => GetVramInfo().total; + public static bool IsArm64() { return System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; From 67e8857262ff008a8d7c836ecfe6f4bd53aae89d Mon Sep 17 00:00:00 2001 From: MillyWei Date: Sat, 29 Nov 2025 00:59:12 +0800 Subject: [PATCH 05/34] update --- AIDevGallery/Utils/DeviceUtils.cs | 187 ++++++++++++------------------ 1 file changed, 75 insertions(+), 112 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index fa690e59..14247a73 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -13,57 +13,27 @@ internal static class DeviceUtils { private static (ulong dedicated, ulong total)? _cachedVramInfo; private static readonly object _vramLock = new(); + private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; + private static readonly object _epDevicesLock = new(); public static int GetBestDeviceId() { - int deviceId = 0; - nuint maxDedicatedVideoMemory = 0; - try + var (deviceId, _, _) = EnumerateAdapters((desc, idx, maxVram) => { - DXGI_CREATE_FACTORY_FLAGS createFlags = 0; - Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); - IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; - - IDXGIAdapter1? selectedAdapter = null; - - var index = 0u; - do + if (desc.DedicatedVideoMemory > maxVram) { - var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); - - if (result.Failed) - { - if (result != HRESULT.DXGI_ERROR_NOT_FOUND) - { - result.ThrowOnFailure(); - } - - index = 0; - } - else - { - DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); - - if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) - { - maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; - selectedAdapter = dxgiAdapter1; - deviceId = (int)index; - } - - index++; - dxgiAdapter1 = null; - } + return ((int)idx, desc.DedicatedVideoMemory, true); } - while (index != 0); - } - catch (Exception) - { - } + return (0, maxVram, false); + }); return deviceId; } + public static ulong GetVram() => GetVramInfo().dedicated; + + public static ulong GetTotalVram() => GetVramInfo().total; + private static (ulong dedicated, ulong total) GetVramInfo() { if (_cachedVramInfo.HasValue) @@ -78,69 +48,87 @@ private static (ulong dedicated, ulong total) GetVramInfo() return _cachedVramInfo.Value; } - nuint maxDedicatedVideoMemory = 0; - nuint maxTotalVideoMemory = 0; - - try + var (_, maxDedicated, maxTotal) = EnumerateAdapters((desc, _, maxVram) => { - DXGI_CREATE_FACTORY_FLAGS createFlags = 0; - Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); - IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; + if (desc.DedicatedVideoMemory > maxVram) + { + var total = desc.DedicatedVideoMemory + desc.SharedSystemMemory; + return (0, desc.DedicatedVideoMemory, total); + } + return (0, maxVram, 0UL); + }); - IDXGIAdapter1? selectedAdapter = null; + _cachedVramInfo = (maxDedicated, maxTotal); + return _cachedVramInfo.Value; + } + } - var index = 0u; - do - { - var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); + private static (T result, nuint maxVram, ulong total) EnumerateAdapters(Func selector) + { + T result = default!; + nuint maxDedicatedVideoMemory = 0; + ulong totalMemory = 0; - if (result.Failed) - { - if (result != HRESULT.DXGI_ERROR_NOT_FOUND) - { - result.ThrowOnFailure(); - } + try + { + Windows.Win32.PInvoke.CreateDXGIFactory2(0, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); + IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; - index = 0; - } - else + var index = 0u; + while (true) + { + var enumResult = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); + + if (enumResult.Failed) + { + if (enumResult != HRESULT.DXGI_ERROR_NOT_FOUND) { - DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); + enumResult.ThrowOnFailure(); + } + break; + } - if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) - { - maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; - maxTotalVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory + dxgiAdapterDesc.SharedSystemMemory; - selectedAdapter = dxgiAdapter1; - } + var desc = dxgiAdapter1.GetDesc1(); + var (newResult, newMaxVram, newTotal) = selector(desc, index, maxDedicatedVideoMemory); - index++; - dxgiAdapter1 = null; - } + if (newMaxVram > maxDedicatedVideoMemory) + { + result = newResult; + maxDedicatedVideoMemory = newMaxVram; + totalMemory = newTotal; } - while (index != 0); - } - catch (Exception) - { - } - _cachedVramInfo = (maxDedicatedVideoMemory, maxTotalVideoMemory); - return _cachedVramInfo.Value; + index++; + } + } + catch (Exception) + { } + + return (result, maxDedicatedVideoMemory, totalMemory); } - public static ulong GetVram() => GetVramInfo().dedicated; + public static bool IsArm64() => + System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; - public static ulong GetTotalVram() => GetVramInfo().total; + public static bool HasNPU() => HasExecutionProvider(device => + device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); - public static bool IsArm64() + public static bool HasOpenVINO() => HasExecutionProvider(device => + device.EpName.Equals("OpenVINOExecutionProvider", StringComparison.OrdinalIgnoreCase)); + + private static bool HasExecutionProvider(Func predicate) { - return System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; + try + { + return GetEpDevices().Any(predicate); + } + catch + { + return false; + } } - private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; - private static readonly object _epDevicesLock = new(); - /// /// Gets the list of available ONNX Runtime Execution Provider devices. /// This method ensures that certified EPs (like OpenVINO, QNN, DML) are registered before querying, @@ -172,6 +160,7 @@ private static System.Collections.Generic.IReadOnlyList GetEpDevice } catch { + // Ignore registration errors } _cachedEpDevices = OrtEnv.Instance().GetEpDevices(); @@ -184,30 +173,4 @@ private static System.Collections.Generic.IReadOnlyList GetEpDevice return _cachedEpDevices; } } - - public static bool HasNPU() - { - try - { - return GetEpDevices().Any(device => - device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); - } - catch - { - return false; - } - } - - public static bool HasOpenVINO() - { - try - { - return GetEpDevices().Any(device => - device.EpName.Equals("OpenVINOExecutionProvider", StringComparison.OrdinalIgnoreCase)); - } - catch - { - return false; - } - } } \ No newline at end of file From a759ede336311feecfb9e140ff2725b7af7ae86a Mon Sep 17 00:00:00 2001 From: MillyWei Date: Sat, 29 Nov 2025 01:00:20 +0800 Subject: [PATCH 06/34] update --- AIDevGallery/Models/ModelCompatibility.cs | 2 +- AIDevGallery/Utils/DeviceUtils.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AIDevGallery/Models/ModelCompatibility.cs b/AIDevGallery/Models/ModelCompatibility.cs index 35296c4e..111e308d 100644 --- a/AIDevGallery/Models/ModelCompatibility.cs +++ b/AIDevGallery/Models/ModelCompatibility.cs @@ -91,7 +91,7 @@ public static ModelCompatibility GetModelCompatibility(ModelDetails modelDetails (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.DML) || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.GPU)) && !DeviceUtils.IsArm64()) { - var dedicatedVram = DeviceUtils.GetVram(); + var dedicatedVram = DeviceUtils.GetDedicatedVram(); var totalVram = DeviceUtils.GetTotalVram(); var minimumSizeNeeded = Math.Round((float)(modelDetails.Size / BytesInGB), 1); var totalVramInGb = Math.Round(totalVram / BytesInGB, 1); diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 14247a73..c67b329e 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -30,7 +30,7 @@ public static int GetBestDeviceId() return deviceId; } - public static ulong GetVram() => GetVramInfo().dedicated; + public static ulong GetDedicatedVram() => GetVramInfo().dedicated; public static ulong GetTotalVram() => GetVramInfo().total; From f689dc02f3c82a4bdb468a59b0a3692a5884a9ba Mon Sep 17 00:00:00 2001 From: MillyWei Date: Sat, 29 Nov 2025 01:05:32 +0800 Subject: [PATCH 07/34] update --- AIDevGallery/Utils/DeviceUtils.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index c67b329e..ff663213 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -22,15 +22,15 @@ public static int GetBestDeviceId() { if (desc.DedicatedVideoMemory > maxVram) { - return ((int)idx, desc.DedicatedVideoMemory, true); + return ((int)idx, desc.DedicatedVideoMemory, 0UL); } - return (0, maxVram, false); + return (0, maxVram, 0UL); }); return deviceId; } - public static ulong GetDedicatedVram() => GetVramInfo().dedicated; + public static ulong GetVram() => GetVramInfo().dedicated; public static ulong GetTotalVram() => GetVramInfo().total; From 72d43466650cced0f30ff2bdd18c233a8de22300 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Sat, 29 Nov 2025 01:07:08 +0800 Subject: [PATCH 08/34] update --- AIDevGallery/Utils/DeviceUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index ff663213..340d2de0 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -30,7 +30,7 @@ public static int GetBestDeviceId() return deviceId; } - public static ulong GetVram() => GetVramInfo().dedicated; + public static ulong GetDedicatedVram() => GetVramInfo().dedicated; public static ulong GetTotalVram() => GetVramInfo().total; From 1ee4fbe6da55e1bddd23c236b445eb4b3bc4db94 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 16:49:52 +0800 Subject: [PATCH 09/34] update --- AIDevGallery/Models/GenAIConfig.cs | 14 +++ AIDevGallery/Utils/UserAddedModelUtil.cs | 118 +++++++++++++++-------- 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/AIDevGallery/Models/GenAIConfig.cs b/AIDevGallery/Models/GenAIConfig.cs index 7a3c8eb0..a0177df0 100644 --- a/AIDevGallery/Models/GenAIConfig.cs +++ b/AIDevGallery/Models/GenAIConfig.cs @@ -29,6 +29,8 @@ internal class Decoder { [JsonPropertyName("session_options")] public required GenAISessionOptions SessionOptions { get; set; } + [JsonPropertyName("pipeline")] + public PipelineItem[]? Pipeline { get; set; } } internal class GenAISessionOptions @@ -37,6 +39,18 @@ internal class GenAISessionOptions public required ProviderOptions[] ProviderOptions { get; set; } } +internal class PipelineItem +{ + [JsonExtensionData] + public Dictionary? Stages { get; set; } +} + +internal class PipelineStage +{ + [JsonPropertyName("session_options")] + public GenAISessionOptions? SessionOptions { get; set; } +} + internal class ProviderOptions { [JsonExtensionData] diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 1c97b5e2..d8daa559 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -316,12 +316,6 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable DML > NPU > GPU > CPU - if (configContents.Contains("\"backend_path\": \"QnnHtp.dll\"", StringComparison.OrdinalIgnoreCase)) - { - return HardwareAccelerator.QNN; - } - var config = JsonSerializer.Deserialize(configContents, SourceGenerationContext.Default.GenAIConfig); if (config == null) { @@ -332,57 +326,99 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config bool hasNpu = false; bool hasCpu = false; - foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) + // Check all provider options from decoder-level and pipeline-level + var allProviderOptions = GetAllProviderOptions(config); + foreach (var provider in allProviderOptions) { - // Check QNN provider (highest priority) - if (provider.HasProvider("qnn")) + var accelerator = CheckProviderForAccelerator(provider, ref hasGpu, ref hasNpu, ref hasCpu); + if (accelerator.HasValue) { - return HardwareAccelerator.QNN; + return accelerator.Value; } + } - // Check DML provider - if (provider.HasProvider("dml")) + // Return based on priority: NPU > GPU > CPU + if (hasNpu) return HardwareAccelerator.NPU; + if (hasGpu) return HardwareAccelerator.GPU; + return HardwareAccelerator.CPU; + } + + private static IEnumerable GetAllProviderOptions(GenAIConfig config) + { + // Yield decoder-level provider options + foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) + { + yield return provider; + } + + // Yield pipeline-level provider options + if (config.Model.Decoder.Pipeline == null) + { + yield break; + } + + foreach (var pipelineItem in config.Model.Decoder.Pipeline) + { + if (pipelineItem.Stages == null) { - return HardwareAccelerator.DML; + continue; } - // Check OpenVINO provider - var openvinoOptions = provider.GetProviderOptions("OpenVINO"); - if (openvinoOptions != null) + foreach (var stageEntry in pipelineItem.Stages) { - if (openvinoOptions.TryGetValue("device_type", out var deviceType)) + PipelineStage? stage = null; + try { - var devType = deviceType.ToLowerInvariant(); - if (devType == "npu") hasNpu = true; - else if (devType == "gpu") hasGpu = true; - else if (devType == "cpu") hasCpu = true; + stage = JsonSerializer.Deserialize(stageEntry.Value.GetRawText()); + } + catch (JsonException) + { + // Skip stages that can't be deserialized + continue; } - } - // Check GPU providers - if (provider.HasProvider("cuda") || provider.HasProvider("tensorrt") || - provider.HasProvider("rocm") || provider.HasProvider("webgpu")) - { - hasGpu = true; + if (stage?.SessionOptions?.ProviderOptions != null) + { + foreach (var provider in stage.SessionOptions.ProviderOptions) + { + yield return provider; + } + } } + } + } - // Check VitisAI provider (typically FPGA/NPU) - if (provider.HasProvider("vitisai")) - { - hasNpu = true; - } + private static HardwareAccelerator? CheckProviderForAccelerator(ProviderOptions provider, ref bool hasGpu, ref bool hasNpu, ref bool hasCpu) + { + // Check QNN provider (highest priority) + if (provider.HasProvider("qnn")) + { + return HardwareAccelerator.QNN; + } - // Check CPU provider - if (provider.HasProvider("cpu")) - { - hasCpu = true; - } + // Check DML provider (high priority) + if (provider.HasProvider("dml")) + { + return HardwareAccelerator.DML; } - if (hasNpu) return HardwareAccelerator.NPU; - if (hasGpu) return HardwareAccelerator.GPU; - if (hasCpu) return HardwareAccelerator.CPU; - return HardwareAccelerator.CPU; + // Check OpenVINO provider + var openvinoOptions = provider.GetProviderOptions("OpenVINO"); + if (openvinoOptions != null && openvinoOptions.TryGetValue("device_type", out var deviceType)) + { + var devType = deviceType.ToLowerInvariant(); + if (devType == "npu") hasNpu = true; + else if (devType == "gpu") hasGpu = true; + else if (devType == "cpu") hasCpu = true; + } + + // Check CPU provider + if (provider.HasProvider("cpu")) + { + hasCpu = true; + } + + return null; } public static async Task AddModelFromLocalFilePath(string filepath, string name, List modelTypes) From 198746f967a5bfb1ca3b527e512aa7ffa4a73993 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 17:31:53 +0800 Subject: [PATCH 10/34] update --- AIDevGallery/Utils/UserAddedModelUtil.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index d8daa559..4287a071 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -345,13 +345,11 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config private static IEnumerable GetAllProviderOptions(GenAIConfig config) { - // Yield decoder-level provider options foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) { yield return provider; } - // Yield pipeline-level provider options if (config.Model.Decoder.Pipeline == null) { yield break; @@ -373,7 +371,6 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co } catch (JsonException) { - // Skip stages that can't be deserialized continue; } @@ -390,19 +387,16 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co private static HardwareAccelerator? CheckProviderForAccelerator(ProviderOptions provider, ref bool hasGpu, ref bool hasNpu, ref bool hasCpu) { - // Check QNN provider (highest priority) if (provider.HasProvider("qnn")) { return HardwareAccelerator.QNN; } - // Check DML provider (high priority) if (provider.HasProvider("dml")) { return HardwareAccelerator.DML; } - // Check OpenVINO provider var openvinoOptions = provider.GetProviderOptions("OpenVINO"); if (openvinoOptions != null && openvinoOptions.TryGetValue("device_type", out var deviceType)) { @@ -412,7 +406,11 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co else if (devType == "cpu") hasCpu = true; } - // Check CPU provider + if (provider.HasProvider("vitisai")) + { + hasNpu = true; + } + if (provider.HasProvider("cpu")) { hasCpu = true; From 34bd8df4a67137765fe82012c85dd041d86b4ca0 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 17:41:50 +0800 Subject: [PATCH 11/34] update --- AIDevGallery/Utils/UserAddedModelUtil.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 4287a071..60b39836 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -322,6 +322,7 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config throw new InvalidDataException("genai_config.json is not valid"); } + // Return based on priority: QNN > DML > NPU > GPU > CPU bool hasGpu = false; bool hasNpu = false; bool hasCpu = false; @@ -337,7 +338,6 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config } } - // Return based on priority: NPU > GPU > CPU if (hasNpu) return HardwareAccelerator.NPU; if (hasGpu) return HardwareAccelerator.GPU; return HardwareAccelerator.CPU; From a3fde392e4cbf665696e81b38964c16fb5a20419 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 18:06:53 +0800 Subject: [PATCH 12/34] update --- AIDevGallery/Models/ModelCompatibility.cs | 44 ++------ AIDevGallery/Utils/DeviceUtils.cs | 122 +++++++++++----------- 2 files changed, 71 insertions(+), 95 deletions(-) diff --git a/AIDevGallery/Models/ModelCompatibility.cs b/AIDevGallery/Models/ModelCompatibility.cs index 111e308d..c1c4c894 100644 --- a/AIDevGallery/Models/ModelCompatibility.cs +++ b/AIDevGallery/Models/ModelCompatibility.cs @@ -74,61 +74,37 @@ public static ModelCompatibility GetModelCompatibility(ModelDetails modelDetails description = "This model requires an NPU (Neural Processing Unit). No compatible NPU was detected on your device."; } } - else if (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.OpenVINO)) - { - // Specific OpenVINO EP check (can run on CPU/GPU/NPU) - if (DeviceUtils.HasOpenVINO()) - { - compatibility = ModelCompatibilityState.Compatible; - } - else - { - compatibility = ModelCompatibilityState.NotCompatible; - description = "This model requires OpenVINO Execution Provider. No compatible OpenVINO runtime was detected on your device."; - } - } else if ( (modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.DML) || modelDetails.HardwareAccelerators.Contains(HardwareAccelerator.GPU)) && !DeviceUtils.IsArm64()) { - var dedicatedVram = DeviceUtils.GetDedicatedVram(); - var totalVram = DeviceUtils.GetTotalVram(); + var vram = DeviceUtils.GetVram(); var minimumSizeNeeded = Math.Round((float)(modelDetails.Size / BytesInGB), 1); - var totalVramInGb = Math.Round(totalVram / BytesInGB, 1); - var isIntegratedGpu = dedicatedVram < BytesInGB; + var vramInGb = Math.Round(vram / BytesInGB, 1); // we want at least 2GB more than model size for good performance - if (modelDetails.Size + (2 * BytesInGB) < totalVram) + if (modelDetails.Size + (2 * BytesInGB) < vram) { - // Warn about potential performance issues on integrated GPUs for larger models - if (isIntegratedGpu && modelDetails.Size > 0.5 * BytesInGB) - { - compatibility = ModelCompatibilityState.NotRecomended; - description = $"This model can run on your integrated GPU, but performance may be significantly slower than on a dedicated GPU. Your system has {totalVramInGb}GB of shared GPU memory available."; - } - else - { - compatibility = ModelCompatibilityState.Compatible; - } + compatibility = ModelCompatibilityState.Compatible; } // we want at least 1GB more than model size for some breathing room - else if (modelDetails.Size + BytesInGB < totalVram) + else if (modelDetails.Size + BytesInGB < vram) { compatibility = ModelCompatibilityState.NotRecomended; - description = $"This model is not recommended for your device. We recommend minimum {minimumSizeNeeded + 2}GB of GPU memory. Your GPU has {totalVramInGb}GB available"; + description = $"This model is not recommended for your device. We recommend minimum {minimumSizeNeeded + 2}GB of dedicated GPU memory. Your GPU has {vramInGb}GB"; } else { compatibility = ModelCompatibilityState.NotCompatible; - description = $"This model will not work on your device because it requires minimum {minimumSizeNeeded + 1}GB of GPU memory."; - if (totalVram == 0) + description = $"This model will not work on your device because it requires minimum {minimumSizeNeeded + 1}GB of dedicate GPU memory."; + if (vram == 0) { - description += " We could not find a compatible GPU."; + description += " We could not find a dedicated GPU."; } else { - description += $" Your GPU has {totalVramInGb}GB available."; + description += $" Your GPU has {vramInGb}GB."; } } } diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 340d2de0..1b3bf632 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -11,112 +11,112 @@ namespace AIDevGallery.Utils; internal static class DeviceUtils { - private static (ulong dedicated, ulong total)? _cachedVramInfo; - private static readonly object _vramLock = new(); private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; private static readonly object _epDevicesLock = new(); public static int GetBestDeviceId() { - var (deviceId, _, _) = EnumerateAdapters((desc, idx, maxVram) => + int deviceId = 0; + nuint maxDedicatedVideoMemory = 0; + try { - if (desc.DedicatedVideoMemory > maxVram) - { - return ((int)idx, desc.DedicatedVideoMemory, 0UL); - } - return (0, maxVram, 0UL); - }); - - return deviceId; - } - - public static ulong GetDedicatedVram() => GetVramInfo().dedicated; + DXGI_CREATE_FACTORY_FLAGS createFlags = 0; + Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); + IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; - public static ulong GetTotalVram() => GetVramInfo().total; + IDXGIAdapter1? selectedAdapter = null; - private static (ulong dedicated, ulong total) GetVramInfo() - { - if (_cachedVramInfo.HasValue) - { - return _cachedVramInfo.Value; - } - - lock (_vramLock) - { - if (_cachedVramInfo.HasValue) + var index = 0u; + do { - return _cachedVramInfo.Value; - } + var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); - var (_, maxDedicated, maxTotal) = EnumerateAdapters((desc, _, maxVram) => - { - if (desc.DedicatedVideoMemory > maxVram) + if (result.Failed) { - var total = desc.DedicatedVideoMemory + desc.SharedSystemMemory; - return (0, desc.DedicatedVideoMemory, total); + if (result != HRESULT.DXGI_ERROR_NOT_FOUND) + { + result.ThrowOnFailure(); + } + + index = 0; } - return (0, maxVram, 0UL); - }); + else + { + DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); - _cachedVramInfo = (maxDedicated, maxTotal); - return _cachedVramInfo.Value; + if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) + { + maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; + selectedAdapter = dxgiAdapter1; + deviceId = (int)index; + } + + index++; + dxgiAdapter1 = null; + } + } + while (index != 0); } + catch (Exception) + { + } + + return deviceId; } - private static (T result, nuint maxVram, ulong total) EnumerateAdapters(Func selector) + public static ulong GetVram() { - T result = default!; nuint maxDedicatedVideoMemory = 0; - ulong totalMemory = 0; - try { - Windows.Win32.PInvoke.CreateDXGIFactory2(0, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); + DXGI_CREATE_FACTORY_FLAGS createFlags = 0; + Windows.Win32.PInvoke.CreateDXGIFactory2(createFlags, typeof(IDXGIFactory2).GUID, out object dxgiFactoryObj).ThrowOnFailure(); IDXGIFactory2? dxgiFactory = (IDXGIFactory2)dxgiFactoryObj; + IDXGIAdapter1? selectedAdapter = null; + var index = 0u; - while (true) + do { - var enumResult = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); + var result = dxgiFactory.EnumAdapters1(index, out IDXGIAdapter1? dxgiAdapter1); - if (enumResult.Failed) + if (result.Failed) { - if (enumResult != HRESULT.DXGI_ERROR_NOT_FOUND) + if (result != HRESULT.DXGI_ERROR_NOT_FOUND) { - enumResult.ThrowOnFailure(); + result.ThrowOnFailure(); } - break; + + index = 0; } + else + { + DXGI_ADAPTER_DESC1 dxgiAdapterDesc = dxgiAdapter1.GetDesc1(); - var desc = dxgiAdapter1.GetDesc1(); - var (newResult, newMaxVram, newTotal) = selector(desc, index, maxDedicatedVideoMemory); + if (selectedAdapter == null || dxgiAdapterDesc.DedicatedVideoMemory > maxDedicatedVideoMemory) + { + maxDedicatedVideoMemory = dxgiAdapterDesc.DedicatedVideoMemory; + selectedAdapter = dxgiAdapter1; + } - if (newMaxVram > maxDedicatedVideoMemory) - { - result = newResult; - maxDedicatedVideoMemory = newMaxVram; - totalMemory = newTotal; + index++; + dxgiAdapter1 = null; } - - index++; } + while (index != 0); } catch (Exception) { } - return (result, maxDedicatedVideoMemory, totalMemory); + return maxDedicatedVideoMemory; } - public static bool IsArm64() => System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; public static bool HasNPU() => HasExecutionProvider(device => device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); - public static bool HasOpenVINO() => HasExecutionProvider(device => - device.EpName.Equals("OpenVINOExecutionProvider", StringComparison.OrdinalIgnoreCase)); - private static bool HasExecutionProvider(Func predicate) { try From 8e16547fc3bdd32ad5104b5d47668fbcf167f710 Mon Sep 17 00:00:00 2001 From: Milly Wei Date: Wed, 3 Dec 2025 10:19:26 +0000 Subject: [PATCH 13/34] UPDATE --- AIDevGallery/Models/GenAIConfig.cs | 21 +++++++++++++---- AIDevGallery/Utils/AppUtils.cs | 4 ++-- AIDevGallery/Utils/DeviceUtils.cs | 1 + AIDevGallery/Utils/UserAddedModelUtil.cs | 30 ++++++++++++++++++++---- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/AIDevGallery/Models/GenAIConfig.cs b/AIDevGallery/Models/GenAIConfig.cs index a0177df0..08b0e6e0 100644 --- a/AIDevGallery/Models/GenAIConfig.cs +++ b/AIDevGallery/Models/GenAIConfig.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -58,16 +59,28 @@ internal class ProviderOptions public bool HasProvider(string name) { - if (ExtensionData == null) return false; + if (ExtensionData == null) + { + return false; + } + return ExtensionData.Keys.Any(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); } + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] public Dictionary? GetProviderOptions(string name) { - if (ExtensionData == null) return null; + if (ExtensionData == null) + { + return null; + } + var key = ExtensionData.Keys.FirstOrDefault(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (key == null) return null; - + if (key == null) + { + return null; + } + try { return JsonSerializer.Deserialize>(ExtensionData[key].GetRawText()); diff --git a/AIDevGallery/Utils/AppUtils.cs b/AIDevGallery/Utils/AppUtils.cs index 78114158..406a82db 100644 --- a/AIDevGallery/Utils/AppUtils.cs +++ b/AIDevGallery/Utils/AppUtils.cs @@ -285,10 +285,10 @@ public static string GetThemeAssetSuffix() { var accessibilitySettings = new AccessibilitySettings(); bool isHighContrast = accessibilitySettings.HighContrast; - if(isHighContrast) + if (isHighContrast) { string hcThemeName = accessibilitySettings.HighContrastScheme; - if(hcThemeName == "High Contrast White") + if (hcThemeName == "High Contrast White") { return ".light"; } diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 1b3bf632..7313fbf2 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -111,6 +111,7 @@ public static ulong GetVram() return maxDedicatedVideoMemory; } + public static bool IsArm64() => System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 60b39836..8c1e5b09 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -8,6 +8,7 @@ using Microsoft.UI.Xaml.Controls; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -314,6 +315,7 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable(String, JsonSerializerOptions)")] private static IEnumerable GetAllProviderOptions(GenAIConfig config) { foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) @@ -401,9 +412,18 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co if (openvinoOptions != null && openvinoOptions.TryGetValue("device_type", out var deviceType)) { var devType = deviceType.ToLowerInvariant(); - if (devType == "npu") hasNpu = true; - else if (devType == "gpu") hasGpu = true; - else if (devType == "cpu") hasCpu = true; + if (devType == "npu") + { + hasNpu = true; + } + else if (devType == "gpu") + { + hasGpu = true; + } + else if (devType == "cpu") + { + hasCpu = true; + } } if (provider.HasProvider("vitisai")) From 35e672a059b3defbf17dc92ad6b77cbe02ef7d6c Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 18:29:48 +0800 Subject: [PATCH 14/34] update --- AIDevGallery/Utils/UserAddedModelUtil.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 8c1e5b09..61b36c98 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -318,6 +318,11 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable Date: Wed, 3 Dec 2025 19:54:12 +0800 Subject: [PATCH 15/34] init --- AIDevGallery/Utils/DeviceUtils.cs | 20 ++ AIDevGallery/Utils/UserAddedModelUtil.cs | 92 ++++- docs/UIA_Spec.md | 405 +++++++++++++++++++++++ 3 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 docs/UIA_Spec.md diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 7313fbf2..91e16043 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -118,6 +118,26 @@ public static bool IsArm64() => public static bool HasNPU() => HasExecutionProvider(device => device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); + /// + /// Gets the list of available execution provider names on this device. + /// + /// A list of EP names (e.g., "OpenVINOExecutionProvider", "DmlExecutionProvider", etc.) + public static System.Collections.Generic.List GetAvailableExecutionProviders() + { + try + { + var epDevices = GetEpDevices(); + return epDevices + .Select(device => device.ExecutionProviderType) + .Distinct() + .ToList(); + } + catch + { + return new System.Collections.Generic.List(); + } + } + private static bool HasExecutionProvider(Func predicate) { try diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 61b36c98..44b1f5f5 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -53,10 +53,10 @@ public static async Task OpenAddLanguageModelFlow(XamlRoot root) } HardwareAccelerator accelerator = HardwareAccelerator.CPU; + string configContents = string.Empty; try { - string configContents = string.Empty; configContents = await File.ReadAllTextAsync(config); accelerator = GetHardwareAcceleratorFromConfig(configContents); } @@ -74,6 +74,36 @@ public static async Task OpenAddLanguageModelFlow(XamlRoot root) return; } + // Validate execution providers + var (isValid, unavailableProviders) = ValidateExecutionProviders(configContents); + if (!isValid) + { + var warningMessage = "This model requires execution providers that are not available on your device:\n\n" + + string.Join(", ", unavailableProviders) + + "\n\nThe model may fail to load or run. Do you want to add it anyway?"; + + ContentDialog warningDialog = new() + { + Title = "Incompatible Execution Providers", + Content = new TextBlock() + { + Text = warningMessage, + TextWrapping = TextWrapping.Wrap + }, + XamlRoot = root, + CloseButtonText = "Cancel", + PrimaryButtonText = "Add Anyway", + DefaultButton = ContentDialogButton.Close, + Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style + }; + + var warningResult = await warningDialog.ShowAsync(); + if (warningResult != ContentDialogResult.Primary) + { + return; + } + } + var nameTextBox = new TextBox() { Text = Path.GetFileName(folder.Path), @@ -444,6 +474,66 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co return null; } + /// + /// Validates that the execution providers specified in the genai_config.json are available on this device. + /// + /// A tuple with (isValid, unavailableProviders) + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] + private static (bool IsValid, List UnavailableProviders) ValidateExecutionProviders(string configContents) + { + var config = JsonSerializer.Deserialize(configContents, SourceGenerationContext.Default.GenAIConfig); + if (config == null) + { + return (false, new List { "Invalid genai_config.json" }); + } + + var availableEPs = DeviceUtils.GetAvailableExecutionProviders(); + var unavailableProviders = new List(); + + // Map provider names to their expected EP names + var providerMapping = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "qnn", "QNNExecutionProvider" }, + { "dml", "DmlExecutionProvider" }, + { "openvino", "OpenVINOExecutionProvider" }, + { "vitisai", "VitisAIExecutionProvider" }, + { "cuda", "CUDAExecutionProvider" }, + { "tensorrt", "TensorrtExecutionProvider" }, + { "cpu", "CPUExecutionProvider" } + }; + + var allProviderOptions = GetAllProviderOptions(config); + foreach (var provider in allProviderOptions) + { + if (provider.ExtensionData == null) + { + continue; + } + + foreach (var providerKey in provider.ExtensionData.Keys) + { + // Skip CPU as it's always available + if (providerKey.Equals("cpu", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (providerMapping.TryGetValue(providerKey, out var expectedEP)) + { + if (!availableEPs.Any(ep => ep.Equals(expectedEP, StringComparison.OrdinalIgnoreCase))) + { + if (!unavailableProviders.Contains(providerKey, StringComparer.OrdinalIgnoreCase)) + { + unavailableProviders.Add(providerKey); + } + } + } + } + } + + return (unavailableProviders.Count == 0, unavailableProviders); + } + public static async Task AddModelFromLocalFilePath(string filepath, string name, List modelTypes) { List validatedModelTypes = GetValidatedModelTypesForUploadedOnnxModel(filepath, modelTypes); diff --git a/docs/UIA_Spec.md b/docs/UIA_Spec.md new file mode 100644 index 00000000..8d335391 --- /dev/null +++ b/docs/UIA_Spec.md @@ -0,0 +1,405 @@ +# AI Dev Gallery 自动化测试与性能监控方案 - 执行摘要 + +> **文档版本**: 1.0 +> **最后更新**: 2025-11-27 +> **状态**: 待审批 +> **详细技术文档**: [Automated-Testing-and-PowerBI-Pipeline-Design.md](./Automated-Testing-and-PowerBI-Pipeline-Design.md) + +## 📋 目录 + +1. [项目目标](#1-项目目标) +2. [当前状态](#2-当前状态) +3. [方案对比与推荐](#3-方案对比与推荐) +4. [成本分析](#4-成本分析) +5. [实施计划](#5-实施计划) +6. [风险与缓解](#6-风险与缓解) +7. [决策建议](#7-决策建议) + +--- + +## 1. 项目目标 + +### 1.1 核心目标 +建立完整的自动化测试与性能监控体系,实现: +- ✅ **CI/CD 自动化测试**: 每次 PR 自动运行单元测试 +- ✅ **性能数据收集**: 自动记录启动时间、内存占用、模型加载时间等关键指标 +- ✅ **可视化 Dashboard**: 实时监控性能趋势,快速识别性能回归 + +### 1.2 预期收益 +| 收益项目 | 量化指标 | 说明 | +|---------|---------|------| +| **减少手动测试时间** | ~80% | 自动化替代人工测试 | +| **性能问题发现时间** | 从数天到数小时 | 实时监控自动告警 | +| **发布质量** | 提升 40%+ | 阻止有缺陷代码合并 | +| **开发者信心** | 显著提升 | 持续反馈机制 | + +--- + +## 2. 当前状态与技术选型 + +### 2.1 已完成工作 ✅ +- ✅ 测试项目 (`AIDevGallery.Tests`) 已建立 +- ✅ 性能收集器 (`PerformanceCollector`) 已实现 +- ✅ 部分测试用例已编写(单元测试、UI 测试、性能测试) + +### 2.2 待完成工作 🚧 +- ⏳ PR 自动化测试流程需要优化 +- ⏳ 性能测试覆盖率需要扩展 +- ❌ **数据存储方案未确定** ← 需要决策 +- ❌ **可视化 Dashboard 未实现** ← 需要决策 +- ❌ 性能回归通知机制未配置 + +### 2.3 技术栈选型 + +#### 已确定的技术选型 ✅ + +| 组件 | 选择方案 | 理由 | +|------|---------|------| +| **测试框架** | MSTest | • 项目当前已使用
• 与 Visual Studio 和 .NET 集成最好
• 原生支持 WinUI 3 测试容器 | +| **UI 自动化** | UI Automation (UIA3) + FlaUI | • WinAppDriver 已停止维护
• FlaUI 对 WinUI 3 支持优秀
• 无需额外服务器进程,CI 配置简单 | +| **性能数据收集** | 自定义 PerformanceCollector | • 使用 Stopwatch 记录时间
• 使用 Process API 记录内存
• 输出 JSON 格式,易于解析 | +| **CI/CD 平台** | GitHub Actions | • 与 GitHub 紧密集成
• 免费额度 2000 分钟/月
• 配置简单,生态丰富 | + +#### 待决策的技术选型 ⏳ + +| 组件 | 候选方案 | 决策依据 | +|------|---------|---------| +| **数据存储** | • 本地文件系统
• GitHub Repository
• Azure Blob Storage | 取决于是否有专用测试机和预算 | +| **Dashboard** | • Flask + Chart.js (自建)
• Grafana
• Power BI
• GitHub Pages | 取决于是否愿意开发和月度成本 | + +### 2.4 整体流程架构 + +```mermaid +graph TB + subgraph "阶段 1: 代码提交与 PR" + A[开发者提交 PR] -->|触发| B[GitHub Actions] + B -->|运行| C[单元测试] + C -->|通过| D[允许合并] + C -->|失败| E[阻止合并] + end + + subgraph "阶段 2: 主分支性能测试" + F[代码合并到 main] -->|定时触发| G[性能测试套件] + G -->|执行| H[PerformanceCollector] + H -->|记录| I[启动时间
内存占用
模型加载时间] + I -->|生成| J[JSON 数据文件] + end + + subgraph "阶段 3: 数据处理与存储" + J -->|上传/存储| K{存储方案选择} + K -->|方案 A| L[本地文件系统] + K -->|方案 B| M[GitHub Repo] + K -->|方案 C| N[Azure Blob] + end + + subgraph "阶段 4: 可视化与告警" + L --> O[Dashboard] + M --> O + N --> O + O -->|检测回归| P[Teams/Email 通知] + O -->|展示| Q[性能趋势图表] + end + + style A fill:#e1f5ff + style D fill:#c8e6c9 + style E fill:#ffcdd2 + style P fill:#fff9c4 + style Q fill:#c8e6c9 +``` + +### 2.5 数据流详解 + +**步骤 1: 测试执行** (在 CI 或专用测试机上) +``` +测试触发 → 性能测试运行 → PerformanceCollector 开始计时 + ↓ + 测试逻辑执行 (启动应用、加载模型、导航页面等) + ↓ + PerformanceCollector 记录指标 (时间、内存) + ↓ + 生成 JSON 文件 (包含元数据、环境信息、测量数据) +``` + +**步骤 2: 数据处理** +``` +JSON 文件 → 上传到存储 → 数据处理脚本 + ↓ + 转换为数据库格式 (SQLite/SQL) + ↓ + 检测性能回归 (对比历史数据) + ↓ + 如超过阈值 → 发送告警通知 +``` + +**步骤 3: 可视化展示** +``` +存储的数据 → Dashboard 读取 → 生成图表 + ↓ + • 启动时间趋势图 + • 内存占用对比图 + • 模型加载时间分析 + • 分支/提交对比视图 +``` + +### 2.6 关键性能指标定义 + +| 指标名称 | 单位 | 描述 | 目标值 | +|---------|------|------|-------| +| `StartupTime` | ms | 应用从启动到主窗口就绪 | < 2000ms | +| `MemoryUsage_Startup` | MB | 启动后内存占用 (Private Memory) | < 200MB | +| `ModelLoadTime` | ms | 加载 AI 模型所需时间 | < 5000ms | +| `InferenceTime` | ms | 模型首次推理耗时 (TTFT) | < 1000ms | +| `NavigationTime` | ms | 页面切换耗时 | < 500ms | + +**数据格式示例** (JSON Schema): +```json +{ + "Meta": { + "RunId": "1234567890", + "CommitHash": "a1b2c3d4", + "Branch": "main", + "Timestamp": "2025-11-27T10:30:00Z" + }, + "Environment": { + "OS": "Windows 10.0.22631", + "Platform": "X64", + "Configuration": "Release" + }, + "Measurements": [ + { + "Category": "Timing", + "Name": "StartupTime", + "Value": 1250.5, + "Unit": "ms" + } + ] +} +``` + +--- + +## 3. 方案对比与推荐 + +### 3.1 快速方案对比 + +| 方案 | 月度成本 | 一次性投入 | Dashboard 开发 | 推荐度 | 适用场景 | +|-----|---------|-----------|--------------|-------|---------| +| 专用机 + 本地存储 + 自建 Dashboard | **$0** | $500-1000 (硬件) | 需要 (2-3 天) | ⭐⭐⭐⭐ | **有预算购买测试机** | +| GitHub Actions + GitHub Pages | **$0** | $0 | 需要 (2-3 天) | ⭐⭐ | **预算有限,无测试机** | +| GitHub Actions + Azure + Power BI | $10-50 | $0 | **无需开发** | ⭐⭐⭐ | 有 Azure 订阅 | +| 专用机 + 本地 + Power BI Desktop | $0 | $500-1000 (硬件) | **无需开发** (仅本地) | ⭐⭐⭐⭐ | 有测试机 + 想用 Power BI | +| GitHub Actions + OneDrive (M365) + Power BI Pro | $10-30 | $0 | **无需开发** | ⭐⭐⭐ | 已有 M365 订阅 | +| 专用机+Azure+Power BI | $10-50 | $500-1000 (硬件) | 需要 (2-3 天) | ⭐⭐⭐⭐⭐ | 双重保障 | + +### 3.2 方案详细说明 + +#### 🏆 方案 A: 专用测试机 + 本地存储 + 自建 Dashboard + +**架构**: +``` +专用测试机 (Windows, 16GB RAM, 500GB SSD) +├── 定时任务运行性能测试 +├── 本地存储 JSON 数据 +├── SQLite 数据库 +├── Flask Web Dashboard (自建) +└── Teams/Email 通知 +``` + +**优点**: +- ✅ **零月度成本** (仅电费 ~$10-20/月) +- ✅ 完全可控,数据私有 +- ✅ 实时监控,无延迟 +- ✅ 环境稳定,易于调试 + +**缺点**: +- ❌ 需要购买硬件 ($500-1000) +- ❌ 需要开发 Dashboard (2-3 天) +- ❌ 需要维护物理设备 + +**推荐理由**: 长期成本最低,适合持续使用 (1 年后月均成本 < $100) + +--- + +#### 🏆 方案 B: GitHub Actions + GitHub Pages(零投入推荐) + +**架构**: +``` +GitHub Actions (CI/CD) +├── 定时运行性能测试 +├── 数据存储在 GitHub Repo (perf-data 分支) +├── Python 脚本生成静态 Dashboard +└── 发布到 GitHub Pages +``` + +**优点**: +- ✅ **完全免费** (零初期投入 + 零月费) +- ✅ 无需维护服务器 +- ✅ 数据永久保存 +- ✅ 可公开访问 + +**缺点**: +- ❌ 需要开发静态 Dashboard (2-3 天) +- ❌ Dashboard 交互能力有限 +- ❌ 依赖 GitHub Actions 稳定性 + +**推荐理由**: 适合预算有限或短期项目,快速启动 + +--- + +#### 方案 C: Azure + Power BI(企业级,有成本) + +**架构**: +``` +GitHub Actions +├── 上传数据到 Azure Blob Storage (~$1-5/月) +├── Power BI 连接 Azure Blob +└── Power BI Service 发布 (~$10/用户/月) +``` + +**优点**: +- ✅ **零开发成本** (Power BI 图形界面配置) +- ✅ 企业级可视化能力 +- ✅ 强大的交互和分析功能 + +**缺点**: +- ❌ 需要 Azure 订阅 (存储 + Power BI Pro) +- ❌ 月度成本 $10-50 + +**推荐理由**: 适合已有 Azure 订阅且需要高级分析的团队 + +--- + +#### 方案 A': 专用测试机 + Power BI Desktop(免费 Power BI) + +**架构**: +``` +专用测试机 +├── 本地存储 JSON 数据 +└── Power BI Desktop 读取本地文件夹(免费) + ├── 仅本地使用,无团队云端共享 + └── 可手动刷新或自动刷新 +``` + +**优点**: +- ✅ **零月度成本** +- ✅ **无需开发** (使用 Power BI 图形界面) +- ✅ 强大的可视化能力 + +**缺点**: +- ❌ 需要购买硬件 ($500-1000) +- ❌ 仅本地访问(团队需通过网络共享访问) +- ❌ 发布到云端需要 Power BI Pro + +**推荐理由**: 想要免费使用 Power BI 且有专用测试机的团队 + +--- + +## 5. 实施计划 + +### 5.1 时间线 (预计 6-8 周) + +```mermaid +gantt + title 自动化测试实施计划 + dateFormat YYYY-MM-DD + section 第一阶段 (Week 1-3) + 扩展测试覆盖 :a1, 2025-12-01, 10d + 验证数据收集 :a2, after a1, 4d + section 第二阶段 (Week 4-6) + 选择方案 :b1, 2025-12-15, 2d + 配置存储 :b2, after b1, 5d + 开发 Dashboard :b3, after b2, 8d + section 第三阶段 (Week 6-8) + 部署测试 :c1, 2026-01-05, 5d + 建立性能基线 :c2, after c1, 7d + 配置告警 :c3, after c2, 2d +``` + +### 5.2 里程碑检查点 + +| 阶段 | 里程碑 | 验收标准 | 责任人 | +|-----|-------|---------|-------| +| **第一阶段** | 测试覆盖完善 | • 90%+ 代码覆盖率
• 所有关键路径有测试 | QA Team | +| **第二阶段** | 基础设施就绪 | • 数据存储配置完成
• Dashboard 可访问 | Dev Team | +| **第三阶段** | 生产运行 | • 7 天稳定运行
• 性能基线建立 | DevOps | + +--- + +## 6. 风险与缓解 + +### 6.1 主要风险 + +| 风险 | 影响 | 概率 | 缓解措施 | +|------|------|------|---------| +| **测试机硬件故障** | 高 | 低 | • 定期备份数据到云端
• 准备备用硬件 | +| **Dashboard 开发延期** | 中 | 中 | • 使用简化版先上线
• 考虑使用 Power BI 快速启动 | +| **性能测试不稳定** | 高 | 中 | • 建立重试机制
• 环境隔离 | +| **团队学习曲线** | 低 | 高 | • 提供培训文档
• 指定专人负责 | + +### 6.2 技术风险 + +| 技术挑战 | 解决方案 | +|---------|---------| +| UI 测试 flaky | 使用 FlaUI + 增加等待时间 + 重试机制 | +| 数据量增长 | 定期归档旧数据 + 数据压缩 | +| Dashboard 性能 | 使用数据聚合 + 缓存机制 | + +--- + +## 7. 决策建议 + +### 7.1 决策矩阵 + +**如果团队情况是...** + +| 条件 | 推荐方案 | 原因 | +|------|---------|------| +| ✅ 有预算购买测试机 | **方案 A** | 长期成本最低,完全可控 | +| ✅ 完全零预算 | **方案 B** | 零投入,依赖 GitHub | +| ✅ 已有 Azure 订阅 | **方案 C** | 快速启动,企业级功能 | +| ✅ 有测试机 + 想用 Power BI | **方案 A'** | 免费 Power BI,强大可视化 | +| ✅ 已有 M365 + 想用 Power BI | **方案 B'** | 利用现有订阅,云端共享 | +| ✅ 技术团队强 + 想要自定义 | **方案 A** | 高度灵活,完全掌控 | +| ✅ 想快速启动(不想开发) | **方案 C / A' / B'** | 使用 Power BI,零开发 | + +### 7.2 推荐优先级 + +#### 首选方案(强烈推荐): +``` +🥇 方案 A: 专用测试机 + 自建 Dashboard + └─ 适合: 有硬件预算 + 长期使用 + 技术团队强 + └─ 优势: 零月费 + 完全可控 + 实时监控 +``` + +#### 备选方案: +``` +🥈 方案 B: GitHub Actions + GitHub Pages + └─ 适合: 零预算 + 快速启动 + └─ 优势: 完全免费 + 无需维护硬件 + +🥉 方案 A': 专用测试机 + Power BI Desktop + └─ 适合: 有测试机 + 想用 Power BI 但不想开发 + └─ 优势: 零月费 + Power BI 可视化 + 无需开发 +``` + +### 7.3 需要立即决策的问题 + +**请管理层决策以下问题**: + +1. ⭐ **是否批准购买专用测试机?** ($500-1000 一次性投入) + - [ ] 是 → 选择方案 A 或 A' + - [ ] 否 → 选择方案 B 或 B' 或 C + +2. ⭐ **是否愿意投入开发时间构建自定义 Dashboard?** (2-3 天) + - [ ] 是 → 方案 A 或 B + - [ ] 否 → 方案 C / A' / B' (使用 Power BI) + +3. ⭐ **是否接受月度云服务成本?** ($10-50/月) + - [ ] 是 → 方案 C 或 B' + - [ ] 否 → 方案 A / A' / B + +4. **预期项目时间线?** + - [ ] 4-6 周 (推荐) + - [ ] 加急 (2-3 周) → 选择无需开发 Dashboard 的方案 + +--- \ No newline at end of file From 49cb78e32bfe0fba35c121357837296e709b280f Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 21:42:34 +0800 Subject: [PATCH 16/34] update --- .../Pages/Scenarios/ScenarioPage.xaml.cs | 24 ++++----- .../Samples/SharedCode/WinMLHelpers.cs | 7 +-- AIDevGallery/Utils/DeviceUtils.cs | 2 +- AIDevGallery/Utils/ExecutionProviderNames.cs | 51 +++++++++++++++++++ AIDevGallery/Utils/UserAddedModelUtil.cs | 15 +++--- 5 files changed, 75 insertions(+), 24 deletions(-) create mode 100644 AIDevGallery/Utils/ExecutionProviderNames.cs diff --git a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs index 5536e82f..4feca2ce 100644 --- a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs +++ b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs @@ -142,38 +142,38 @@ private static async Task> GetSupportedHardwareAccelerators() switch (epName) { - case "VitisAIExecutionProvider": - supportedHardwareAccelerators.Add(new([HardwareAccelerator.VitisAI, HardwareAccelerator.NPU], "VitisAIExecutionProvider", "VitisAI", "NPU")); + case ExecutionProviderNames.VitisAI: + supportedHardwareAccelerators.Add(new([HardwareAccelerator.VitisAI, HardwareAccelerator.NPU], ExecutionProviderNames.VitisAI, "VitisAI", "NPU")); break; - case "OpenVINOExecutionProvider": + case ExecutionProviderNames.OpenVINO: if (epDeviceTypes.Contains("CPU")) { - supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.CPU], "OpenVINOExecutionProvider", "OpenVINO", "CPU")); + supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.CPU], ExecutionProviderNames.OpenVINO, "OpenVINO", "CPU")); } if (epDeviceTypes.Contains("GPU")) { - supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.GPU], "OpenVINOExecutionProvider", "OpenVINO", "GPU")); + supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.GPU], ExecutionProviderNames.OpenVINO, "OpenVINO", "GPU")); } if (epDeviceTypes.Contains("NPU")) { - supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.NPU], "OpenVINOExecutionProvider", "OpenVINO", "NPU")); + supportedHardwareAccelerators.Add(new([HardwareAccelerator.OpenVINO, HardwareAccelerator.NPU], ExecutionProviderNames.OpenVINO, "OpenVINO", "NPU")); } break; - case "QNNExecutionProvider": - supportedHardwareAccelerators.Add(new([HardwareAccelerator.QNN, HardwareAccelerator.NPU], "QNNExecutionProvider", "QNN", "NPU")); + case ExecutionProviderNames.QNN: + supportedHardwareAccelerators.Add(new([HardwareAccelerator.QNN, HardwareAccelerator.NPU], ExecutionProviderNames.QNN, "QNN", "NPU")); break; - case "DmlExecutionProvider": - supportedHardwareAccelerators.Add(new([HardwareAccelerator.DML, HardwareAccelerator.GPU], "DmlExecutionProvider", "DML", "GPU")); + case ExecutionProviderNames.DML: + supportedHardwareAccelerators.Add(new([HardwareAccelerator.DML, HardwareAccelerator.GPU], ExecutionProviderNames.DML, "DML", "GPU")); break; - case "NvTensorRTRTXExecutionProvider": - supportedHardwareAccelerators.Add(new([HardwareAccelerator.NvTensorRT, HardwareAccelerator.GPU], "NvTensorRTRTXExecutionProvider", "NvTensorRT", "GPU")); + case ExecutionProviderNames.NvTensorRTRTX: + supportedHardwareAccelerators.Add(new([HardwareAccelerator.NvTensorRT, HardwareAccelerator.GPU], ExecutionProviderNames.NvTensorRTRTX, "NvTensorRT", "GPU")); break; } } diff --git a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs index 2e76c189..604ca973 100644 --- a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs +++ b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using AIDevGallery.Utils; namespace AIDevGallery.Samples.SharedCode; @@ -28,16 +29,16 @@ public static bool AppendExecutionProviderFromEpName(this SessionOptions session Dictionary epOptions = new(StringComparer.OrdinalIgnoreCase); switch (epName) { - case "DmlExecutionProvider": + case ExecutionProviderNames.DML: // Configure performance mode for Dml EP // Dml some times have multiple devices which cause exception, we pick the first one here sessionOptions.AppendExecutionProvider(environment, [devices[0]], epOptions); return true; - case "OpenVINOExecutionProvider": + case ExecutionProviderNames.OpenVINO: var device = devices.Where(d => d.HardwareDevice.Type.ToString().Equals(deviceType, StringComparison.Ordinal)).FirstOrDefault(); sessionOptions.AppendExecutionProvider(environment, [device], epOptions); return true; - case "QNNExecutionProvider": + case ExecutionProviderNames.QNN: // Configure performance mode for QNN EP epOptions["htp_performance_mode"] = "high_performance"; break; diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 91e16043..a8b0ad7a 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -128,7 +128,7 @@ public static System.Collections.Generic.List GetAvailableExecutionProvi { var epDevices = GetEpDevices(); return epDevices - .Select(device => device.ExecutionProviderType) + .Select(device => device.EpName) .Distinct() .ToList(); } diff --git a/AIDevGallery/Utils/ExecutionProviderNames.cs b/AIDevGallery/Utils/ExecutionProviderNames.cs new file mode 100644 index 00000000..1710506b --- /dev/null +++ b/AIDevGallery/Utils/ExecutionProviderNames.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AIDevGallery.Utils; + +/// +/// Provides standard execution provider names used by ONNX Runtime. +/// These constants match the EpName values returned by OrtEpDevice. +/// +internal static class ExecutionProviderNames +{ + /// + /// CPU Execution Provider - always available + /// + public const string CPU = "CPUExecutionProvider"; + + /// + /// DirectML Execution Provider - for DirectX 12 GPU acceleration on Windows + /// + public const string DML = "DmlExecutionProvider"; + + /// + /// Qualcomm Neural Network (QNN) Execution Provider - for Qualcomm NPU + /// + public const string QNN = "QNNExecutionProvider"; + + /// + /// OpenVINO Execution Provider - supports CPU, GPU, and NPU on Intel hardware + /// + public const string OpenVINO = "OpenVINOExecutionProvider"; + + /// + /// Vitis AI Execution Provider - for AMD/Xilinx NPU acceleration + /// + public const string VitisAI = "VitisAIExecutionProvider"; + + /// + /// CUDA Execution Provider - for NVIDIA GPU acceleration + /// + public const string CUDA = "CUDAExecutionProvider"; + + /// + /// TensorRT Execution Provider - for optimized inference on NVIDIA GPUs + /// + public const string TensorRT = "TensorrtExecutionProvider"; + + /// + /// NVIDIA TensorRT RTX Execution Provider + /// + public const string NvTensorRTRTX = "NvTensorRTRTXExecutionProvider"; +} diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 44b1f5f5..1288568d 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -74,7 +74,6 @@ public static async Task OpenAddLanguageModelFlow(XamlRoot root) return; } - // Validate execution providers var (isValid, unavailableProviders) = ValidateExecutionProviders(configContents); if (!isValid) { @@ -493,13 +492,13 @@ private static (bool IsValid, List UnavailableProviders) ValidateExecuti // Map provider names to their expected EP names var providerMapping = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "qnn", "QNNExecutionProvider" }, - { "dml", "DmlExecutionProvider" }, - { "openvino", "OpenVINOExecutionProvider" }, - { "vitisai", "VitisAIExecutionProvider" }, - { "cuda", "CUDAExecutionProvider" }, - { "tensorrt", "TensorrtExecutionProvider" }, - { "cpu", "CPUExecutionProvider" } + { "qnn", ExecutionProviderNames.QNN }, + { "dml", ExecutionProviderNames.DML }, + { "openvino", ExecutionProviderNames.OpenVINO }, + { "vitisai", ExecutionProviderNames.VitisAI }, + { "cuda", ExecutionProviderNames.CUDA }, + { "tensorrt", ExecutionProviderNames.TensorRT }, + { "cpu", ExecutionProviderNames.CPU } }; var allProviderOptions = GetAllProviderOptions(config); From 64c4bae9a79613b3acc2821249802693e714f332 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 21:53:23 +0800 Subject: [PATCH 17/34] update --- AIDevGallery/Utils/DeviceUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index a8b0ad7a..9357e807 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -128,7 +128,7 @@ public static System.Collections.Generic.List GetAvailableExecutionProvi { var epDevices = GetEpDevices(); return epDevices - .Select(device => device.EpName) + .Select(device => device.EpName.ToLowerInvariant()) .Distinct() .ToList(); } From 79b6a4b010caf9013ff5b0c0b88fe02713a692b7 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Wed, 3 Dec 2025 23:00:06 +0800 Subject: [PATCH 18/34] revert --- AIDevGallery/Utils/DeviceUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 9357e807..a8b0ad7a 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -128,7 +128,7 @@ public static System.Collections.Generic.List GetAvailableExecutionProvi { var epDevices = GetEpDevices(); return epDevices - .Select(device => device.EpName.ToLowerInvariant()) + .Select(device => device.EpName) .Distinct() .ToList(); } From 94d8687de40098de32e22128bb78b52e705661d7 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 00:25:34 +0800 Subject: [PATCH 19/34] update --- AIDevGallery/Utils/UserAddedModelUtil.cs | 48 ++++++------------------ 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 1288568d..a79886de 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -31,8 +31,8 @@ public static async Task OpenAddLanguageModelFlow(XamlRoot root) if (folder != null) { - var files = Directory.GetFiles(folder.Path); - var config = files.Where(r => Path.GetFileName(r) == "genai_config.json").FirstOrDefault(); + var config = Directory.GetFiles(folder.Path) + .FirstOrDefault(r => Path.GetFileName(r) == "genai_config.json"); if (string.IsNullOrEmpty(config) || App.ModelCache.Models.Any(m => m.Path == folder.Path)) { @@ -250,17 +250,10 @@ public static async Task OpenAddModelFlow(XamlRoot targetRoot, List GetValidatedModelTypesForUploadedOnnxModel(string modelFilepath, List modelTypes) { - List validatedModelTypes = new(); - - foreach (var (type, models) in ModelDetailsHelper.GetModelDetailsForModelTypes(modelTypes)) - { - if (ValidateUserAddedModelDimensionsForModelTypeModelDetails(models, modelFilepath)) - { - validatedModelTypes.Add(type); - } - } - - return validatedModelTypes; + return ModelDetailsHelper.GetModelDetailsForModelTypes(modelTypes) + .Where(pair => ValidateUserAddedModelDimensionsForModelTypeModelDetails(pair.models, modelFilepath)) + .Select(pair => pair.type) + .ToList(); } private static bool ValidateUserAddedModelDimensionsForModelTypeModelDetails(List modelDetailsList, string modelFilepath) @@ -269,21 +262,11 @@ private static bool ValidateUserAddedModelDimensionsForModelTypeModelDetails(Lis sessionOptions.RegisterOrtExtensions(); using InferenceSession inferenceSession = new(modelFilepath, sessionOptions); - List inputDimensions = new(); - List outputDimensions = new(); - - inputDimensions.AddRange(inferenceSession.InputMetadata.Select(kvp => kvp.Value.Dimensions)); - outputDimensions.AddRange(inferenceSession.OutputMetadata.Select(kvp => kvp.Value.Dimensions)); + var inputDimensions = inferenceSession.InputMetadata.Select(kvp => kvp.Value.Dimensions).ToList(); + var outputDimensions = inferenceSession.OutputMetadata.Select(kvp => kvp.Value.Dimensions).ToList(); - foreach (ModelDetails modelDetails in modelDetailsList) - { - if (ValidateUserAddedModelAgainstModelDimensions(inputDimensions, outputDimensions, modelDetails)) - { - return true; - } - } - - return false; + return modelDetailsList.Any(modelDetails => + ValidateUserAddedModelAgainstModelDimensions(inputDimensions, outputDimensions, modelDetails)); } private static bool ValidateUserAddedModelAgainstModelDimensions(List inputDimensions, List outputDimensions, ModelDetails modelDetails) @@ -334,15 +317,7 @@ private static bool CompareDimension(int[] dimensionA, int[] dimensionB) public static bool IsModelsDetailsListUploadCompatible(this IEnumerable modelDetailsList) { - foreach (ModelDetails modelDetails in modelDetailsList) - { - if (modelDetails.InputDimensions != null && modelDetails.OutputDimensions != null) - { - return true; - } - } - - return false; + return modelDetailsList.Any(m => m.InputDimensions != null && m.OutputDimensions != null); } public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string configContents) @@ -489,7 +464,6 @@ private static (bool IsValid, List UnavailableProviders) ValidateExecuti var availableEPs = DeviceUtils.GetAvailableExecutionProviders(); var unavailableProviders = new List(); - // Map provider names to their expected EP names var providerMapping = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "qnn", ExecutionProviderNames.QNN }, From b12b8e75b1157c0bdc06a7f44b2a14200bdd63d2 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 00:28:51 +0800 Subject: [PATCH 20/34] update --- AIDevGallery/Utils/UserAddedModelUtil.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index a79886de..8dff959c 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -251,8 +251,8 @@ public static async Task OpenAddModelFlow(XamlRoot targetRoot, List GetValidatedModelTypesForUploadedOnnxModel(string modelFilepath, List modelTypes) { return ModelDetailsHelper.GetModelDetailsForModelTypes(modelTypes) - .Where(pair => ValidateUserAddedModelDimensionsForModelTypeModelDetails(pair.models, modelFilepath)) - .Select(pair => pair.type) + .Where(pair => ValidateUserAddedModelDimensionsForModelTypeModelDetails(pair.Value, modelFilepath)) + .Select(pair => pair.Key) .ToList(); } From bc370c5bce7281111805d2f7bdfd992ea438e57c Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 00:37:02 +0800 Subject: [PATCH 21/34] update --- docs/UIA_Spec.md | 405 ----------------------------------------------- 1 file changed, 405 deletions(-) delete mode 100644 docs/UIA_Spec.md diff --git a/docs/UIA_Spec.md b/docs/UIA_Spec.md deleted file mode 100644 index 8d335391..00000000 --- a/docs/UIA_Spec.md +++ /dev/null @@ -1,405 +0,0 @@ -# AI Dev Gallery 自动化测试与性能监控方案 - 执行摘要 - -> **文档版本**: 1.0 -> **最后更新**: 2025-11-27 -> **状态**: 待审批 -> **详细技术文档**: [Automated-Testing-and-PowerBI-Pipeline-Design.md](./Automated-Testing-and-PowerBI-Pipeline-Design.md) - -## 📋 目录 - -1. [项目目标](#1-项目目标) -2. [当前状态](#2-当前状态) -3. [方案对比与推荐](#3-方案对比与推荐) -4. [成本分析](#4-成本分析) -5. [实施计划](#5-实施计划) -6. [风险与缓解](#6-风险与缓解) -7. [决策建议](#7-决策建议) - ---- - -## 1. 项目目标 - -### 1.1 核心目标 -建立完整的自动化测试与性能监控体系,实现: -- ✅ **CI/CD 自动化测试**: 每次 PR 自动运行单元测试 -- ✅ **性能数据收集**: 自动记录启动时间、内存占用、模型加载时间等关键指标 -- ✅ **可视化 Dashboard**: 实时监控性能趋势,快速识别性能回归 - -### 1.2 预期收益 -| 收益项目 | 量化指标 | 说明 | -|---------|---------|------| -| **减少手动测试时间** | ~80% | 自动化替代人工测试 | -| **性能问题发现时间** | 从数天到数小时 | 实时监控自动告警 | -| **发布质量** | 提升 40%+ | 阻止有缺陷代码合并 | -| **开发者信心** | 显著提升 | 持续反馈机制 | - ---- - -## 2. 当前状态与技术选型 - -### 2.1 已完成工作 ✅ -- ✅ 测试项目 (`AIDevGallery.Tests`) 已建立 -- ✅ 性能收集器 (`PerformanceCollector`) 已实现 -- ✅ 部分测试用例已编写(单元测试、UI 测试、性能测试) - -### 2.2 待完成工作 🚧 -- ⏳ PR 自动化测试流程需要优化 -- ⏳ 性能测试覆盖率需要扩展 -- ❌ **数据存储方案未确定** ← 需要决策 -- ❌ **可视化 Dashboard 未实现** ← 需要决策 -- ❌ 性能回归通知机制未配置 - -### 2.3 技术栈选型 - -#### 已确定的技术选型 ✅ - -| 组件 | 选择方案 | 理由 | -|------|---------|------| -| **测试框架** | MSTest | • 项目当前已使用
• 与 Visual Studio 和 .NET 集成最好
• 原生支持 WinUI 3 测试容器 | -| **UI 自动化** | UI Automation (UIA3) + FlaUI | • WinAppDriver 已停止维护
• FlaUI 对 WinUI 3 支持优秀
• 无需额外服务器进程,CI 配置简单 | -| **性能数据收集** | 自定义 PerformanceCollector | • 使用 Stopwatch 记录时间
• 使用 Process API 记录内存
• 输出 JSON 格式,易于解析 | -| **CI/CD 平台** | GitHub Actions | • 与 GitHub 紧密集成
• 免费额度 2000 分钟/月
• 配置简单,生态丰富 | - -#### 待决策的技术选型 ⏳ - -| 组件 | 候选方案 | 决策依据 | -|------|---------|---------| -| **数据存储** | • 本地文件系统
• GitHub Repository
• Azure Blob Storage | 取决于是否有专用测试机和预算 | -| **Dashboard** | • Flask + Chart.js (自建)
• Grafana
• Power BI
• GitHub Pages | 取决于是否愿意开发和月度成本 | - -### 2.4 整体流程架构 - -```mermaid -graph TB - subgraph "阶段 1: 代码提交与 PR" - A[开发者提交 PR] -->|触发| B[GitHub Actions] - B -->|运行| C[单元测试] - C -->|通过| D[允许合并] - C -->|失败| E[阻止合并] - end - - subgraph "阶段 2: 主分支性能测试" - F[代码合并到 main] -->|定时触发| G[性能测试套件] - G -->|执行| H[PerformanceCollector] - H -->|记录| I[启动时间
内存占用
模型加载时间] - I -->|生成| J[JSON 数据文件] - end - - subgraph "阶段 3: 数据处理与存储" - J -->|上传/存储| K{存储方案选择} - K -->|方案 A| L[本地文件系统] - K -->|方案 B| M[GitHub Repo] - K -->|方案 C| N[Azure Blob] - end - - subgraph "阶段 4: 可视化与告警" - L --> O[Dashboard] - M --> O - N --> O - O -->|检测回归| P[Teams/Email 通知] - O -->|展示| Q[性能趋势图表] - end - - style A fill:#e1f5ff - style D fill:#c8e6c9 - style E fill:#ffcdd2 - style P fill:#fff9c4 - style Q fill:#c8e6c9 -``` - -### 2.5 数据流详解 - -**步骤 1: 测试执行** (在 CI 或专用测试机上) -``` -测试触发 → 性能测试运行 → PerformanceCollector 开始计时 - ↓ - 测试逻辑执行 (启动应用、加载模型、导航页面等) - ↓ - PerformanceCollector 记录指标 (时间、内存) - ↓ - 生成 JSON 文件 (包含元数据、环境信息、测量数据) -``` - -**步骤 2: 数据处理** -``` -JSON 文件 → 上传到存储 → 数据处理脚本 - ↓ - 转换为数据库格式 (SQLite/SQL) - ↓ - 检测性能回归 (对比历史数据) - ↓ - 如超过阈值 → 发送告警通知 -``` - -**步骤 3: 可视化展示** -``` -存储的数据 → Dashboard 读取 → 生成图表 - ↓ - • 启动时间趋势图 - • 内存占用对比图 - • 模型加载时间分析 - • 分支/提交对比视图 -``` - -### 2.6 关键性能指标定义 - -| 指标名称 | 单位 | 描述 | 目标值 | -|---------|------|------|-------| -| `StartupTime` | ms | 应用从启动到主窗口就绪 | < 2000ms | -| `MemoryUsage_Startup` | MB | 启动后内存占用 (Private Memory) | < 200MB | -| `ModelLoadTime` | ms | 加载 AI 模型所需时间 | < 5000ms | -| `InferenceTime` | ms | 模型首次推理耗时 (TTFT) | < 1000ms | -| `NavigationTime` | ms | 页面切换耗时 | < 500ms | - -**数据格式示例** (JSON Schema): -```json -{ - "Meta": { - "RunId": "1234567890", - "CommitHash": "a1b2c3d4", - "Branch": "main", - "Timestamp": "2025-11-27T10:30:00Z" - }, - "Environment": { - "OS": "Windows 10.0.22631", - "Platform": "X64", - "Configuration": "Release" - }, - "Measurements": [ - { - "Category": "Timing", - "Name": "StartupTime", - "Value": 1250.5, - "Unit": "ms" - } - ] -} -``` - ---- - -## 3. 方案对比与推荐 - -### 3.1 快速方案对比 - -| 方案 | 月度成本 | 一次性投入 | Dashboard 开发 | 推荐度 | 适用场景 | -|-----|---------|-----------|--------------|-------|---------| -| 专用机 + 本地存储 + 自建 Dashboard | **$0** | $500-1000 (硬件) | 需要 (2-3 天) | ⭐⭐⭐⭐ | **有预算购买测试机** | -| GitHub Actions + GitHub Pages | **$0** | $0 | 需要 (2-3 天) | ⭐⭐ | **预算有限,无测试机** | -| GitHub Actions + Azure + Power BI | $10-50 | $0 | **无需开发** | ⭐⭐⭐ | 有 Azure 订阅 | -| 专用机 + 本地 + Power BI Desktop | $0 | $500-1000 (硬件) | **无需开发** (仅本地) | ⭐⭐⭐⭐ | 有测试机 + 想用 Power BI | -| GitHub Actions + OneDrive (M365) + Power BI Pro | $10-30 | $0 | **无需开发** | ⭐⭐⭐ | 已有 M365 订阅 | -| 专用机+Azure+Power BI | $10-50 | $500-1000 (硬件) | 需要 (2-3 天) | ⭐⭐⭐⭐⭐ | 双重保障 | - -### 3.2 方案详细说明 - -#### 🏆 方案 A: 专用测试机 + 本地存储 + 自建 Dashboard - -**架构**: -``` -专用测试机 (Windows, 16GB RAM, 500GB SSD) -├── 定时任务运行性能测试 -├── 本地存储 JSON 数据 -├── SQLite 数据库 -├── Flask Web Dashboard (自建) -└── Teams/Email 通知 -``` - -**优点**: -- ✅ **零月度成本** (仅电费 ~$10-20/月) -- ✅ 完全可控,数据私有 -- ✅ 实时监控,无延迟 -- ✅ 环境稳定,易于调试 - -**缺点**: -- ❌ 需要购买硬件 ($500-1000) -- ❌ 需要开发 Dashboard (2-3 天) -- ❌ 需要维护物理设备 - -**推荐理由**: 长期成本最低,适合持续使用 (1 年后月均成本 < $100) - ---- - -#### 🏆 方案 B: GitHub Actions + GitHub Pages(零投入推荐) - -**架构**: -``` -GitHub Actions (CI/CD) -├── 定时运行性能测试 -├── 数据存储在 GitHub Repo (perf-data 分支) -├── Python 脚本生成静态 Dashboard -└── 发布到 GitHub Pages -``` - -**优点**: -- ✅ **完全免费** (零初期投入 + 零月费) -- ✅ 无需维护服务器 -- ✅ 数据永久保存 -- ✅ 可公开访问 - -**缺点**: -- ❌ 需要开发静态 Dashboard (2-3 天) -- ❌ Dashboard 交互能力有限 -- ❌ 依赖 GitHub Actions 稳定性 - -**推荐理由**: 适合预算有限或短期项目,快速启动 - ---- - -#### 方案 C: Azure + Power BI(企业级,有成本) - -**架构**: -``` -GitHub Actions -├── 上传数据到 Azure Blob Storage (~$1-5/月) -├── Power BI 连接 Azure Blob -└── Power BI Service 发布 (~$10/用户/月) -``` - -**优点**: -- ✅ **零开发成本** (Power BI 图形界面配置) -- ✅ 企业级可视化能力 -- ✅ 强大的交互和分析功能 - -**缺点**: -- ❌ 需要 Azure 订阅 (存储 + Power BI Pro) -- ❌ 月度成本 $10-50 - -**推荐理由**: 适合已有 Azure 订阅且需要高级分析的团队 - ---- - -#### 方案 A': 专用测试机 + Power BI Desktop(免费 Power BI) - -**架构**: -``` -专用测试机 -├── 本地存储 JSON 数据 -└── Power BI Desktop 读取本地文件夹(免费) - ├── 仅本地使用,无团队云端共享 - └── 可手动刷新或自动刷新 -``` - -**优点**: -- ✅ **零月度成本** -- ✅ **无需开发** (使用 Power BI 图形界面) -- ✅ 强大的可视化能力 - -**缺点**: -- ❌ 需要购买硬件 ($500-1000) -- ❌ 仅本地访问(团队需通过网络共享访问) -- ❌ 发布到云端需要 Power BI Pro - -**推荐理由**: 想要免费使用 Power BI 且有专用测试机的团队 - ---- - -## 5. 实施计划 - -### 5.1 时间线 (预计 6-8 周) - -```mermaid -gantt - title 自动化测试实施计划 - dateFormat YYYY-MM-DD - section 第一阶段 (Week 1-3) - 扩展测试覆盖 :a1, 2025-12-01, 10d - 验证数据收集 :a2, after a1, 4d - section 第二阶段 (Week 4-6) - 选择方案 :b1, 2025-12-15, 2d - 配置存储 :b2, after b1, 5d - 开发 Dashboard :b3, after b2, 8d - section 第三阶段 (Week 6-8) - 部署测试 :c1, 2026-01-05, 5d - 建立性能基线 :c2, after c1, 7d - 配置告警 :c3, after c2, 2d -``` - -### 5.2 里程碑检查点 - -| 阶段 | 里程碑 | 验收标准 | 责任人 | -|-----|-------|---------|-------| -| **第一阶段** | 测试覆盖完善 | • 90%+ 代码覆盖率
• 所有关键路径有测试 | QA Team | -| **第二阶段** | 基础设施就绪 | • 数据存储配置完成
• Dashboard 可访问 | Dev Team | -| **第三阶段** | 生产运行 | • 7 天稳定运行
• 性能基线建立 | DevOps | - ---- - -## 6. 风险与缓解 - -### 6.1 主要风险 - -| 风险 | 影响 | 概率 | 缓解措施 | -|------|------|------|---------| -| **测试机硬件故障** | 高 | 低 | • 定期备份数据到云端
• 准备备用硬件 | -| **Dashboard 开发延期** | 中 | 中 | • 使用简化版先上线
• 考虑使用 Power BI 快速启动 | -| **性能测试不稳定** | 高 | 中 | • 建立重试机制
• 环境隔离 | -| **团队学习曲线** | 低 | 高 | • 提供培训文档
• 指定专人负责 | - -### 6.2 技术风险 - -| 技术挑战 | 解决方案 | -|---------|---------| -| UI 测试 flaky | 使用 FlaUI + 增加等待时间 + 重试机制 | -| 数据量增长 | 定期归档旧数据 + 数据压缩 | -| Dashboard 性能 | 使用数据聚合 + 缓存机制 | - ---- - -## 7. 决策建议 - -### 7.1 决策矩阵 - -**如果团队情况是...** - -| 条件 | 推荐方案 | 原因 | -|------|---------|------| -| ✅ 有预算购买测试机 | **方案 A** | 长期成本最低,完全可控 | -| ✅ 完全零预算 | **方案 B** | 零投入,依赖 GitHub | -| ✅ 已有 Azure 订阅 | **方案 C** | 快速启动,企业级功能 | -| ✅ 有测试机 + 想用 Power BI | **方案 A'** | 免费 Power BI,强大可视化 | -| ✅ 已有 M365 + 想用 Power BI | **方案 B'** | 利用现有订阅,云端共享 | -| ✅ 技术团队强 + 想要自定义 | **方案 A** | 高度灵活,完全掌控 | -| ✅ 想快速启动(不想开发) | **方案 C / A' / B'** | 使用 Power BI,零开发 | - -### 7.2 推荐优先级 - -#### 首选方案(强烈推荐): -``` -🥇 方案 A: 专用测试机 + 自建 Dashboard - └─ 适合: 有硬件预算 + 长期使用 + 技术团队强 - └─ 优势: 零月费 + 完全可控 + 实时监控 -``` - -#### 备选方案: -``` -🥈 方案 B: GitHub Actions + GitHub Pages - └─ 适合: 零预算 + 快速启动 - └─ 优势: 完全免费 + 无需维护硬件 - -🥉 方案 A': 专用测试机 + Power BI Desktop - └─ 适合: 有测试机 + 想用 Power BI 但不想开发 - └─ 优势: 零月费 + Power BI 可视化 + 无需开发 -``` - -### 7.3 需要立即决策的问题 - -**请管理层决策以下问题**: - -1. ⭐ **是否批准购买专用测试机?** ($500-1000 一次性投入) - - [ ] 是 → 选择方案 A 或 A' - - [ ] 否 → 选择方案 B 或 B' 或 C - -2. ⭐ **是否愿意投入开发时间构建自定义 Dashboard?** (2-3 天) - - [ ] 是 → 方案 A 或 B - - [ ] 否 → 方案 C / A' / B' (使用 Power BI) - -3. ⭐ **是否接受月度云服务成本?** ($10-50/月) - - [ ] 是 → 方案 C 或 B' - - [ ] 否 → 方案 A / A' / B - -4. **预期项目时间线?** - - [ ] 4-6 周 (推荐) - - [ ] 加急 (2-3 周) → 选择无需开发 Dashboard 的方案 - ---- \ No newline at end of file From 7ddedc4b346ccbfc421203ad1737771916d3ce89 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 4 Dec 2025 01:05:49 +0800 Subject: [PATCH 22/34] update --- AIDevGallery/Samples/SharedCode/WinMLHelpers.cs | 2 +- AIDevGallery/Utils/DeviceUtils.cs | 2 +- AIDevGallery/Utils/ExecutionProviderNames.cs | 2 +- AIDevGallery/Utils/UserAddedModelUtil.cs | 5 ++++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs index 604ca973..946ee27a 100644 --- a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs +++ b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using AIDevGallery.Utils; using Microsoft.ML.OnnxRuntime; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using AIDevGallery.Utils; namespace AIDevGallery.Samples.SharedCode; diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index a8b0ad7a..60214c06 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -11,8 +11,8 @@ namespace AIDevGallery.Utils; internal static class DeviceUtils { - private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; private static readonly object _epDevicesLock = new(); + private static System.Collections.Generic.IReadOnlyList? _cachedEpDevices; public static int GetBestDeviceId() { diff --git a/AIDevGallery/Utils/ExecutionProviderNames.cs b/AIDevGallery/Utils/ExecutionProviderNames.cs index 1710506b..c727c949 100644 --- a/AIDevGallery/Utils/ExecutionProviderNames.cs +++ b/AIDevGallery/Utils/ExecutionProviderNames.cs @@ -48,4 +48,4 @@ internal static class ExecutionProviderNames /// NVIDIA TensorRT RTX Execution Provider ///
public const string NvTensorRTRTX = "NvTensorRTRTXExecutionProvider"; -} +} \ No newline at end of file diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 8dff959c..338efd6d 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -21,6 +21,7 @@ namespace AIDevGallery.Utils; internal static class UserAddedModelUtil { + [RequiresDynamicCode("Calls AIDevGallery.Utils.UserAddedModelUtil.ValidateExecutionProviders(String)")] public static async Task OpenAddLanguageModelFlow(XamlRoot root) { var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); @@ -265,7 +266,7 @@ private static bool ValidateUserAddedModelDimensionsForModelTypeModelDetails(Lis var inputDimensions = inferenceSession.InputMetadata.Select(kvp => kvp.Value.Dimensions).ToList(); var outputDimensions = inferenceSession.OutputMetadata.Select(kvp => kvp.Value.Dimensions).ToList(); - return modelDetailsList.Any(modelDetails => + return modelDetailsList.Any(modelDetails => ValidateUserAddedModelAgainstModelDimensions(inputDimensions, outputDimensions, modelDetails)); } @@ -320,6 +321,7 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable m.InputDimensions != null && m.OutputDimensions != null); } + [RequiresDynamicCode("Calls AIDevGallery.Utils.UserAddedModelUtil.GetAllProviderOptions(GenAIConfig)")] public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string configContents) { if (configContents.Contains(""""backend_path": "QnnHtp.dll"""", StringComparison.OrdinalIgnoreCase)) @@ -405,6 +407,7 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co } } + [RequiresDynamicCode("Calls AIDevGallery.Models.ProviderOptions.GetProviderOptions(String)")] private static HardwareAccelerator? CheckProviderForAccelerator(ProviderOptions provider, ref bool hasGpu, ref bool hasNpu, ref bool hasCpu) { if (provider.HasProvider("qnn")) From 6c2e7d852b67f9acf5188a26a36876a01ded3133 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 4 Dec 2025 01:31:30 +0800 Subject: [PATCH 23/34] format --- AIDevGallery/Models/GenAIConfig.cs | 4 +--- AIDevGallery/Utils/SourceGenerationContext.cs | 2 ++ AIDevGallery/Utils/UserAddedModelUtil.cs | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/AIDevGallery/Models/GenAIConfig.cs b/AIDevGallery/Models/GenAIConfig.cs index 08b0e6e0..eecf2194 100644 --- a/AIDevGallery/Models/GenAIConfig.cs +++ b/AIDevGallery/Models/GenAIConfig.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -67,7 +66,6 @@ public bool HasProvider(string name) return ExtensionData.Keys.Any(k => k.Equals(name, StringComparison.OrdinalIgnoreCase)); } - [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] public Dictionary? GetProviderOptions(string name) { if (ExtensionData == null) @@ -83,7 +81,7 @@ public bool HasProvider(string name) try { - return JsonSerializer.Deserialize>(ExtensionData[key].GetRawText()); + return JsonSerializer.Deserialize(ExtensionData[key].GetRawText(), AIDevGallery.Utils.SourceGenerationContext.Default.DictionaryStringString); } catch { diff --git a/AIDevGallery/Utils/SourceGenerationContext.cs b/AIDevGallery/Utils/SourceGenerationContext.cs index 6386d30a..31a8bdb8 100644 --- a/AIDevGallery/Utils/SourceGenerationContext.cs +++ b/AIDevGallery/Utils/SourceGenerationContext.cs @@ -9,6 +9,8 @@ namespace AIDevGallery.Utils; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(GenAIConfig))] +[JsonSerializable(typeof(PipelineStage))] +[JsonSerializable(typeof(Dictionary))] internal partial class SourceGenerationContext : JsonSerializerContext { } \ No newline at end of file diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 338efd6d..493c60e6 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -8,7 +8,6 @@ using Microsoft.UI.Xaml.Controls; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -21,7 +20,6 @@ namespace AIDevGallery.Utils; internal static class UserAddedModelUtil { - [RequiresDynamicCode("Calls AIDevGallery.Utils.UserAddedModelUtil.ValidateExecutionProviders(String)")] public static async Task OpenAddLanguageModelFlow(XamlRoot root) { var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow); @@ -321,7 +319,6 @@ public static bool IsModelsDetailsListUploadCompatible(this IEnumerable m.InputDimensions != null && m.OutputDimensions != null); } - [RequiresDynamicCode("Calls AIDevGallery.Utils.UserAddedModelUtil.GetAllProviderOptions(GenAIConfig)")] public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string configContents) { if (configContents.Contains(""""backend_path": "QnnHtp.dll"""", StringComparison.OrdinalIgnoreCase)) @@ -364,7 +361,6 @@ public static HardwareAccelerator GetHardwareAcceleratorFromConfig(string config return HardwareAccelerator.CPU; } - [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] private static IEnumerable GetAllProviderOptions(GenAIConfig config) { foreach (var provider in config.Model.Decoder.SessionOptions.ProviderOptions) @@ -389,7 +385,7 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co PipelineStage? stage = null; try { - stage = JsonSerializer.Deserialize(stageEntry.Value.GetRawText()); + stage = JsonSerializer.Deserialize(stageEntry.Value.GetRawText(), SourceGenerationContext.Default.PipelineStage); } catch (JsonException) { @@ -407,7 +403,6 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co } } - [RequiresDynamicCode("Calls AIDevGallery.Models.ProviderOptions.GetProviderOptions(String)")] private static HardwareAccelerator? CheckProviderForAccelerator(ProviderOptions provider, ref bool hasGpu, ref bool hasNpu, ref bool hasCpu) { if (provider.HasProvider("qnn")) @@ -455,7 +450,6 @@ private static IEnumerable GetAllProviderOptions(GenAIConfig co /// Validates that the execution providers specified in the genai_config.json are available on this device. /// /// A tuple with (isValid, unavailableProviders) - [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions)")] private static (bool IsValid, List UnavailableProviders) ValidateExecutionProviders(string configContents) { var config = JsonSerializer.Deserialize(configContents, SourceGenerationContext.Default.GenAIConfig); From 77bb382e29a381c8bf1371076452daa91ab4fb46 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 4 Dec 2025 01:40:41 +0800 Subject: [PATCH 24/34] format --- AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs index 442074f9..db2a812b 100644 --- a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs +++ b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs @@ -193,7 +193,13 @@ private async void HandleModelSelectionChanged(List selectedModel VisualStateManager.GoToState(this, "PageLoading", true); modelDetails.Clear(); - selectedModels.ForEach(modelDetails.Add); + foreach (var model in selectedModels) + { + if (model != null) + { + modelDetails.Add(model); + } + } // temporary fix EP dropdown list for useradded local languagemodel if (selectedModels.Any(m => m != null && m.IsOnnxModel() && string.IsNullOrEmpty(m.ParameterSize) && m.Id.StartsWith("useradded-local-languagemodel", System.StringComparison.InvariantCultureIgnoreCase) == false)) From d8cf6fbcc912bb7656b68c570d84808f2b058efd Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 4 Dec 2025 01:53:37 +0800 Subject: [PATCH 25/34] format --- AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs index db2a812b..98667e01 100644 --- a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs +++ b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs @@ -197,7 +197,7 @@ private async void HandleModelSelectionChanged(List selectedModel { if (model != null) { - modelDetails.Add(model); + modelDetails.Add(model!); } } From b961651520a660c1c84da6949c035a652ed8de17 Mon Sep 17 00:00:00 2001 From: "Milly Wei (from Dev Box)" Date: Thu, 4 Dec 2025 01:58:31 +0800 Subject: [PATCH 26/34] format --- AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs index 98667e01..73bad359 100644 --- a/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs +++ b/AIDevGallery/Pages/Scenarios/ScenarioPage.xaml.cs @@ -322,7 +322,7 @@ private void LoadSample(Sample? sampleToLoad) // TODO: don't load sample if model is not cached, but still let code to be seen // this would probably be handled in the SampleContainer - _ = SampleContainer.LoadSampleAsync(sample, [.. modelDetails], App.AppData.WinMLSampleOptions); + _ = SampleContainer.LoadSampleAsync(sample, modelDetails.Where(m => m != null).Select(m => m!).ToList(), App.AppData.WinMLSampleOptions); _ = App.AppData.AddMru( new MostRecentlyUsedItem() { From f799a2ec988c1e708cc5246ab3487768569444bb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:55:03 +0800 Subject: [PATCH 27/34] Initial plan (#525) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> From f368dc70a878405c7c1552083d3647111c800765 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 13:53:12 +0800 Subject: [PATCH 28/34] Consolidate EP device retrieval to use DeviceUtils --- AIDevGallery/Samples/SharedCode/WinMLHelpers.cs | 11 +++++------ AIDevGallery/Utils/DeviceUtils.cs | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs index 604ca973..2f37c3a6 100644 --- a/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs +++ b/AIDevGallery/Samples/SharedCode/WinMLHelpers.cs @@ -13,7 +13,7 @@ namespace AIDevGallery.Samples.SharedCode; internal static class WinMLHelpers { - public static bool AppendExecutionProviderFromEpName(this SessionOptions sessionOptions, string epName, string? deviceType, OrtEnv? environment = null) + public static bool AppendExecutionProviderFromEpName(this SessionOptions sessionOptions, string epName, string? deviceType) { if (epName == "CPU") { @@ -21,8 +21,8 @@ public static bool AppendExecutionProviderFromEpName(this SessionOptions session return true; } - environment ??= OrtEnv.Instance(); - var epDeviceMap = GetEpDeviceMap(environment); + var environment = OrtEnv.Instance(); + var epDeviceMap = GetEpDeviceMap(); if (epDeviceMap.TryGetValue(epName, out var devices)) { @@ -106,10 +106,9 @@ public static bool AppendExecutionProviderFromEpName(this SessionOptions session return null; } - public static Dictionary> GetEpDeviceMap(OrtEnv? environment = null) + public static Dictionary> GetEpDeviceMap() { - environment ??= OrtEnv.Instance(); - IReadOnlyList epDevices = environment.GetEpDevices(); + IReadOnlyList epDevices = DeviceUtils.GetEpDevices(); Dictionary> epDeviceMap = new(StringComparer.OrdinalIgnoreCase); foreach (OrtEpDevice device in epDevices) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index a8b0ad7a..eb6bf9ba 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -156,7 +156,7 @@ private static bool HasExecutionProvider(Func predicate) /// as OrtEnv.GetEpDevices() only returns already-registered providers. /// Results are cached to avoid repeated registration overhead. /// - private static System.Collections.Generic.IReadOnlyList GetEpDevices() + public static System.Collections.Generic.IReadOnlyList GetEpDevices() { if (_cachedEpDevices != null) { From 9d80bfcddce81a9bcc84b08de8768b13af17de22 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 14:04:38 +0800 Subject: [PATCH 29/34] fix ci error --- AIDevGallery/Utils/DeviceUtils.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 0321c696..ede72576 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -156,6 +156,7 @@ private static bool HasExecutionProvider(Func predicate) /// as OrtEnv.GetEpDevices() only returns already-registered providers. /// Results are cached to avoid repeated registration overhead. /// + /// A read-only list of available ONNX Runtime Execution Provider devices. public static System.Collections.Generic.IReadOnlyList GetEpDevices() { if (_cachedEpDevices != null) From 143059bbbcb18492de509170daad4ef23909e59c Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 18:02:23 +0800 Subject: [PATCH 30/34] remove GetAvailableExecutionProviders --- AIDevGallery/Utils/DeviceUtils.cs | 20 -------------------- AIDevGallery/Utils/UserAddedModelUtil.cs | 5 ++++- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index ede72576..05bcc995 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -118,26 +118,6 @@ public static bool IsArm64() => public static bool HasNPU() => HasExecutionProvider(device => device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); - /// - /// Gets the list of available execution provider names on this device. - /// - /// A list of EP names (e.g., "OpenVINOExecutionProvider", "DmlExecutionProvider", etc.) - public static System.Collections.Generic.List GetAvailableExecutionProviders() - { - try - { - var epDevices = GetEpDevices(); - return epDevices - .Select(device => device.EpName) - .Distinct() - .ToList(); - } - catch - { - return new System.Collections.Generic.List(); - } - } - private static bool HasExecutionProvider(Func predicate) { try diff --git a/AIDevGallery/Utils/UserAddedModelUtil.cs b/AIDevGallery/Utils/UserAddedModelUtil.cs index 493c60e6..9d859a85 100644 --- a/AIDevGallery/Utils/UserAddedModelUtil.cs +++ b/AIDevGallery/Utils/UserAddedModelUtil.cs @@ -458,7 +458,10 @@ private static (bool IsValid, List UnavailableProviders) ValidateExecuti return (false, new List { "Invalid genai_config.json" }); } - var availableEPs = DeviceUtils.GetAvailableExecutionProviders(); + var availableEPs = DeviceUtils.GetEpDevices() + .Select(device => device.EpName) + .Distinct() + .ToList(); var unavailableProviders = new List(); var providerMapping = new Dictionary(StringComparer.OrdinalIgnoreCase) From 7fe2493be30851589d5743585e3b9d36ccadf7a8 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 18:12:54 +0800 Subject: [PATCH 31/34] Add log to GetEpDevices --- AIDevGallery/Utils/DeviceUtils.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 05bcc995..a0e467e5 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -160,15 +160,18 @@ public static System.Collections.Generic.IReadOnlyList GetEpDevices { catalog.EnsureAndRegisterCertifiedAsync().GetAwaiter().GetResult(); } - catch + catch (Exception ex) { - // Ignore registration errors + // Log but continue + Telemetry.TelemetryFactory.Get().LogException("GetEpDevices_RegistrationFailed", ex); } _cachedEpDevices = OrtEnv.Instance().GetEpDevices(); } - catch + catch (Exception ex) { + // Log the failure to get EP devices - this could indicate ONNX Runtime initialization issues + Telemetry.TelemetryFactory.Get().LogException("GetEpDevices_Failed", ex); _cachedEpDevices = System.Array.Empty(); } From 5809535cd9cf0f1f884733f80550583b20d5fff7 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 20:03:29 +0800 Subject: [PATCH 32/34] use winml --- AIDevGallery/Utils/DeviceUtils.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index a0e467e5..362b1c7b 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -3,7 +3,6 @@ using Microsoft.ML.OnnxRuntime; using System; -using System.Linq; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dxgi; @@ -115,14 +114,13 @@ public static ulong GetVram() public static bool IsArm64() => System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; - public static bool HasNPU() => HasExecutionProvider(device => - device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); - - private static bool HasExecutionProvider(Func predicate) + public static bool HasNPU() { try { - return GetEpDevices().Any(predicate); + SessionOptions options = new(); + options.SetEpSelectionPolicy(ExecutionProviderDevicePolicy.PREFER_NPU); + return true; } catch { From e35ea03226efab0351845745093f00f22adfb149 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 20:08:12 +0800 Subject: [PATCH 33/34] CA2000 --- AIDevGallery/Utils/DeviceUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 362b1c7b..26da516c 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -118,7 +118,7 @@ public static bool HasNPU() { try { - SessionOptions options = new(); + using SessionOptions options = new(); options.SetEpSelectionPolicy(ExecutionProviderDevicePolicy.PREFER_NPU); return true; } From c9720ee7dda3047606df4289a24618ee9d868c25 Mon Sep 17 00:00:00 2001 From: MillyWei Date: Thu, 4 Dec 2025 20:19:47 +0800 Subject: [PATCH 34/34] revert --- AIDevGallery/Utils/DeviceUtils.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/AIDevGallery/Utils/DeviceUtils.cs b/AIDevGallery/Utils/DeviceUtils.cs index 26da516c..a0e467e5 100644 --- a/AIDevGallery/Utils/DeviceUtils.cs +++ b/AIDevGallery/Utils/DeviceUtils.cs @@ -3,6 +3,7 @@ using Microsoft.ML.OnnxRuntime; using System; +using System.Linq; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dxgi; @@ -114,13 +115,14 @@ public static ulong GetVram() public static bool IsArm64() => System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.Arm64; - public static bool HasNPU() + public static bool HasNPU() => HasExecutionProvider(device => + device.HardwareDevice.Type.ToString().Equals("NPU", StringComparison.OrdinalIgnoreCase)); + + private static bool HasExecutionProvider(Func predicate) { try { - using SessionOptions options = new(); - options.SetEpSelectionPolicy(ExecutionProviderDevicePolicy.PREFER_NPU); - return true; + return GetEpDevices().Any(predicate); } catch {