Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<OnnxRuntimeVersion>1.23.2</OnnxRuntimeVersion>
<OnnxRuntimeVersion>1.28.0</OnnxRuntimeVersion>
</PropertyGroup>
</Project>
8 changes: 7 additions & 1 deletion src/OneWare.Core/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@ protected virtual AvaloniaObject CreateShell()

settingsService.RegisterSettingCategory("Languages", 0, "FluentIcons.ProofreadLanguageRegular");

var onnxRuntimeOptions = OnnxRuntimeBootstrapper.GetOnnxRuntimeOptions(paths);

settingsService.RegisterSetting("Tools", "ONNX Runtime", OnnxRuntimeBootstrapper.SettingSelectedRuntimeKey,
new ComboBoxSetting("Runtime (restart required)", "onnxruntime-builtin", OnnxRuntimeBootstrapper.GetOnnxRuntimeOptions(paths).Cast<object>().ToArray())
new ComboBoxSetting("Runtime (restart required)", "onnxruntime-builtin", onnxRuntimeOptions.Cast<object>().ToArray())
{
MarkdownDocumentation = """
**This setting requires a restart to be effective**
Expand All @@ -168,6 +170,10 @@ protected virtual AvaloniaObject CreateShell()
"""
});

// The selected runtime can be uninstalled or removed as unsupported between sessions.
if (!onnxRuntimeOptions.Contains(settingsService.GetSettingValue<string>(OnnxRuntimeBootstrapper.SettingSelectedRuntimeKey)))
settingsService.SetSettingValue(OnnxRuntimeBootstrapper.SettingSelectedRuntimeKey, "onnxruntime-builtin");

var onnxRuntimeBootstrapper = Services.Resolve<OnnxRuntimeBootstrapper>();
var selectedRuntime = settingsService.GetSettingValue<string>(OnnxRuntimeBootstrapper.SettingSelectedRuntimeKey);
var initialExecutionProviders = onnxRuntimeBootstrapper.GetOnnxExecutionProviders(selectedRuntime);
Expand Down
160 changes: 147 additions & 13 deletions src/OneWare.Core/Services/OnnxRuntimeBootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.ML.OnnxRuntime;
using OneWare.Essentials.Enums;
using OneWare.Essentials.Helpers;
using OneWare.Essentials.Services;
Expand All @@ -16,6 +17,41 @@ public class OnnxRuntimeBootstrapper

public const string SettingOpenVinoDeviceKey = "OnnxRuntime_OpenVinoDevice";

/// <summary>
/// Runtimes from the frozen combined-build packages. They ship an onnxruntime older than the
/// managed assembly, which is not ABI compatible, and were superseded by Windows ML and the
/// plugin execution providers. Stale installations are removed instead of being loaded.
/// </summary>
private static readonly string[] LegacyIncompatibleRuntimes =
[
"onnxruntime-directml",
"onnxruntime-openvino",
"onnxruntime-qnn"
];

/// <summary>
/// Native libraries that live next to onnxruntime and are loaded dynamically by it at runtime.
/// They are preloaded by absolute path so the OS loader resolves them from the side-loaded
/// runtime directory instead of the application directory.
/// </summary>
private static readonly string[] SiblingDependencyLibraries =
[
"onnxruntime_providers_shared",
"DirectML",
"Microsoft.Windows.AI.MachineLearning"
];

/// <summary>
/// Plugin execution providers are additive: they do not replace onnxruntime itself but are
/// registered against it via <see cref="OrtEnv.RegisterExecutionProviderLibrary"/>.
/// Maps the plugin library base name to its ONNX Runtime registration name.
/// </summary>
private static readonly (string LibraryBaseName, string RegistrationName)[] PluginExecutionProviders =
[
("onnxruntime_providers_openvino_plugin", "OpenVINOExecutionProvider"),
("onnxruntime_providers_qnn", "QNNExecutionProvider")
];

private readonly ILogger _logger;
private readonly IPaths _paths;
private static readonly Lock ResolverSync = new();
Expand All @@ -25,6 +61,17 @@ public class OnnxRuntimeBootstrapper

public string SelectedRuntime { get; private set; } = "onnxruntime-builtin";

/// <summary>
/// ONNX Runtime registration name of the side-loaded plugin execution provider, if the selected
/// runtime is a plugin execution provider rather than a full runtime.
/// </summary>
public string? PluginExecutionProviderName { get; private set; }

/// <summary>
/// Absolute path to the side-loaded plugin execution provider library.
/// </summary>
public string? PluginExecutionProviderLibraryPath { get; private set; }

public OnnxRuntimeBootstrapper(IPaths paths, ILogger logger)
{
_paths = paths;
Expand All @@ -40,7 +87,8 @@ public static string[] GetOnnxRuntimeOptions(IPaths paths)
options.AddRange(Directory.GetDirectories(paths.OnnxRuntimesDirectory)
.Select(Path.GetFileName)
.Where(x => !string.IsNullOrWhiteSpace(x))!
.Cast<string>());
.Cast<string>()
.Where(x => !LegacyIncompatibleRuntimes.Contains(x, StringComparer.OrdinalIgnoreCase)));
}
catch
{
Expand All @@ -66,17 +114,17 @@ public OnnxExecutionProvider[] GetOnnxExecutionProviders(string? runtimeName)
if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
executionProviders.Add(OnnxExecutionProvider.CoreMl);
break;
case "onnxruntime-directml":
case "onnxruntime-windowsml":
executionProviders.Add(OnnxExecutionProvider.DirectMl);
break;
case "onnxruntime-nvidia":
executionProviders.Add(OnnxExecutionProvider.Cuda);
executionProviders.Add(OnnxExecutionProvider.TensorRt);
break;
case "onnxruntime-openvino":
case "onnxruntime-ep-openvino":
executionProviders.Add(OnnxExecutionProvider.OpenVino);
break;
case "onnxruntime-qnn":
case "onnxruntime-ep-qnn":
executionProviders.Add(OnnxExecutionProvider.Qnn);
break;
}
Expand All @@ -92,7 +140,18 @@ public void Initialize()
{
// We don't use settings service here because it is not loaded at this state
var selectedRuntime = ReadStringSetting(SettingSelectedRuntimeKey)?.Trim() ?? "no-runtime";


RemoveLegacyRuntimes();

if (LegacyIncompatibleRuntimes.Contains(selectedRuntime, StringComparer.OrdinalIgnoreCase))
{
_logger.LogWarning(
"ONNX runtime '{Runtime}' is no longer supported and was removed. Falling back to the built-in runtime.",
selectedRuntime);
SelectedRuntime = "onnxruntime-builtin";
return;
}

var selectedRuntimeRoot = Path.Combine(_paths.OnnxRuntimesDirectory, selectedRuntime);
var runtimeRootToLoad = CreateSessionRuntimeCopy(selectedRuntime, selectedRuntimeRoot) ?? selectedRuntimeRoot;

Expand Down Expand Up @@ -120,6 +179,29 @@ public void Initialize()
}
}

/// <summary>
/// Deletes installations of runtimes that are no longer ABI compatible with the bundled managed
/// assembly, so they cannot be side-loaded and no longer show up as a selectable runtime.
/// </summary>
private void RemoveLegacyRuntimes()
{
foreach (var legacyRuntime in LegacyIncompatibleRuntimes)
{
var path = Path.Combine(_paths.OnnxRuntimesDirectory, legacyRuntime);
if (!Directory.Exists(path)) continue;

try
{
Directory.Delete(path, true);
_logger.LogInformation("Removed unsupported ONNX runtime '{Runtime}'.", legacyRuntime);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to remove unsupported ONNX runtime '{Runtime}'.", legacyRuntime);
}
}
}

private string? CreateSessionRuntimeCopy(string runtimeName, string sourceRootPath)
{
if (string.IsNullOrWhiteSpace(runtimeName) || !Directory.Exists(sourceRootPath))
Expand Down Expand Up @@ -157,15 +239,9 @@ private bool TryLoadFromNativeDirectory(string nativeDirectory)
{
if (!Directory.Exists(nativeDirectory)) return false;

var fileNames = GetOnnxRuntimeFileNameCandidates();
var providersShared = PlatformHelper.GetLibraryFileName("onnxruntime_providers_shared");

// Ensure provider shared library can be resolved before loading onnxruntime itself.
var providerSharedPath = Path.Combine(nativeDirectory, providersShared);
if (File.Exists(providerSharedPath))
_ = NativeLibrary.TryLoad(providerSharedPath, out _);
PreloadSiblingDependencies(nativeDirectory);

foreach (var fileName in fileNames.Distinct(StringComparer.OrdinalIgnoreCase))
foreach (var fileName in GetOnnxRuntimeFileNameCandidates().Distinct(StringComparer.OrdinalIgnoreCase))
{
var fullPath = Path.Combine(nativeDirectory, fileName);
if (!File.Exists(fullPath)) continue;
Expand All @@ -177,9 +253,64 @@ private bool TryLoadFromNativeDirectory(string nativeDirectory)
return true;
}

return TryDetectPluginExecutionProvider(nativeDirectory);
}

/// <summary>
/// Detects a plugin execution provider in the given directory. Plugin providers ship without
/// onnxruntime itself and are registered against the active runtime once ONNX Runtime is
/// initialized, so nothing is loaded here beyond recording the library path.
/// </summary>
private bool TryDetectPluginExecutionProvider(string nativeDirectory)
{
foreach (var (libraryBaseName, registrationName) in PluginExecutionProviders)
{
foreach (var candidate in BuildLibraryFileCandidates(libraryBaseName))
{
var fullPath = Path.Combine(nativeDirectory, candidate);
if (!File.Exists(fullPath)) continue;

PluginExecutionProviderName = registrationName;
PluginExecutionProviderLibraryPath = fullPath;

// Preloading by absolute path lets the loader resolve the provider's own dependencies
// (OpenVINO/QNN runtime libraries) from the side-loaded directory.
if (!NativeLibrary.TryLoad(fullPath, out _))
_logger.LogDebug("Failed to preload plugin execution provider '{Path}'.", fullPath);

_logger.LogInformation("Found ONNX Runtime plugin execution provider {Provider} at {Path}",
registrationName, fullPath);
return true;
}
}

return false;
}

/// <summary>
/// Preloads native libraries that sit next to onnxruntime and are resolved dynamically by it.
/// Loading them by absolute path first makes the loader reuse the already loaded module instead
/// of searching the application directory, where the side-loaded copies do not exist.
/// </summary>
private void PreloadSiblingDependencies(string nativeDirectory)
{
foreach (var libraryBaseName in SiblingDependencyLibraries)
{
// Not using BuildLibraryFileCandidates: names like "Microsoft.Windows.AI.MachineLearning"
// already contain dots and would be treated as having a file extension.
var platformFileName = PlatformHelper.GetLibraryFileName(libraryBaseName);

foreach (var candidate in new[] { platformFileName, $"lib{platformFileName}" })
{
var fullPath = Path.Combine(nativeDirectory, candidate);
if (!File.Exists(fullPath)) continue;

if (!NativeLibrary.TryLoad(fullPath, out _))
_logger.LogDebug("Failed to preload ONNX Runtime dependency '{Path}'.", fullPath);
}
}
}

private IEnumerable<string> EnumerateNativeSearchDirectories(string rootPath)
{
var directories = new List<string>();
Expand Down Expand Up @@ -240,6 +371,9 @@ private void ConfigureOnnxRuntimeDllImportResolver(string? nativeDirectory, IntP

try
{
// Our resolver takes over native lookup, so ONNX Runtime must not install its own.
OrtEnv.DisableDllImportResolver = true;

var onnxAssembly = typeof(Microsoft.ML.OnnxRuntime.InferenceSession).Assembly;
NativeLibrary.SetDllImportResolver(onnxAssembly, ResolveOnnxRuntimeNativeLibrary);
_onnxResolverRegistered = true;
Expand Down
67 changes: 66 additions & 1 deletion src/OneWare.Core/Services/OnnxRuntimeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public class OnnxRuntimeService : IOnnxRuntimeService
private readonly ISettingsService _settingsService;
private readonly OnnxRuntimeBootstrapper _bootstrapper;
private readonly ILogger _logger;
private readonly Lock _pluginRegistrationSync = new();
private bool _pluginRegistrationAttempted;
private bool _pluginRegistered;

public OnnxRuntimeService(OnnxRuntimeBootstrapper bootstrapper, ILogger logger, ISettingsService settingsService)
{
Expand Down Expand Up @@ -110,7 +113,15 @@ public SessionOptions CreateSessionOptions(OnnxExecutionProvider? providerOverri
break;

case OnnxExecutionProvider.OpenVino:
so.AppendExecutionProvider_OpenVINO(GetOpenVinoDevice());
if (!TryAppendPluginExecutionProvider(so, "OpenVINOExecutionProvider",
new Dictionary<string, string> { ["device_type"] = GetOpenVinoDevice() }))
so.AppendExecutionProvider_OpenVINO(GetOpenVinoDevice());
break;

case OnnxExecutionProvider.Qnn:
if (!TryAppendPluginExecutionProvider(so, "QNNExecutionProvider",
new Dictionary<string, string>()))
so.AppendExecutionProvider("QNN");
break;
}
}
Expand All @@ -124,6 +135,60 @@ public SessionOptions CreateSessionOptions(OnnxExecutionProvider? providerOverri
return so;
}

/// <summary>
/// Registers a side-loaded plugin execution provider with ONNX Runtime and appends the devices it
/// exposes to the session options. Plugin providers replaced the frozen combined-build packages
/// (OpenVINO, QNN) and are additive to the active runtime.
/// </summary>
private bool TryAppendPluginExecutionProvider(SessionOptions sessionOptions, string providerName,
Dictionary<string, string> providerOptions)
{
if (!string.Equals(_bootstrapper.PluginExecutionProviderName, providerName, StringComparison.Ordinal))
return false;

if (!EnsurePluginExecutionProviderRegistered()) return false;

var env = OrtEnv.Instance();
var devices = env.GetEpDevices()
.Where(x => string.Equals(x.EpName, providerName, StringComparison.Ordinal))
.ToArray();

if (devices.Length == 0)
{
_logger.LogWarning("Plugin execution provider '{Provider}' did not expose any devices.", providerName);
return false;
}

sessionOptions.AppendExecutionProvider(env, devices, providerOptions);
return true;
}

private bool EnsurePluginExecutionProviderRegistered()
{
lock (_pluginRegistrationSync)
{
if (_pluginRegistrationAttempted) return _pluginRegistered;
_pluginRegistrationAttempted = true;

var name = _bootstrapper.PluginExecutionProviderName;
var path = _bootstrapper.PluginExecutionProviderLibraryPath;
if (name == null || path == null) return false;

try
{
OrtEnv.Instance().RegisterExecutionProviderLibrary(name, path);
_pluginRegistered = true;
_logger.LogInformation("Registered ONNX Runtime plugin execution provider '{Provider}'.", name);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to register ONNX Runtime plugin execution provider '{Provider}'.", name);
}

return _pluginRegistered;
}
}

private string GetOpenVinoDevice()
{
var configuredDevice = _settingsService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public override Task<PackageInstallerResult> InstallAsync(PackageInstallContext
var recommendedExecutionProvider = runtime switch
{
"onnxruntime-nvidia" => OnnxExecutionProvider.Cuda,
"onnxruntime-directml" => OnnxExecutionProvider.DirectMl,
"onnxruntime-openvino" => OnnxExecutionProvider.OpenVino,
"onnxruntime-qnn" => OnnxExecutionProvider.Qnn,
"onnxruntime-windowsml" => OnnxExecutionProvider.DirectMl,
"onnxruntime-ep-openvino" => OnnxExecutionProvider.OpenVino,
"onnxruntime-ep-qnn" => OnnxExecutionProvider.Qnn,
_ => OnnxExecutionProvider.Cpu
};
settingsService.SetSettingValue("OnnxRuntime_SelectedExecutionProvider", recommendedExecutionProvider);
Expand Down
Loading
Loading