Skip to content
Draft
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
47 changes: 25 additions & 22 deletions LLama/Native/Load/NativeLibraryConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ namespace LLama.Native
/// </summary>
public sealed partial class NativeLibraryConfig
{
private string? _libraryPath;

private bool _useCuda = true;
private bool _useVulkan = true;
private AvxLevel _avxLevel;
Expand All @@ -29,20 +27,6 @@ public sealed partial class NativeLibraryConfig
internal INativeLibrarySelectingPolicy SelectingPolicy { get; private set; } = new DefaultNativeLibrarySelectingPolicy();

#region configurators
/// <summary>
/// Load a specified native library as backend for LLamaSharp.
/// When this method is called, all the other configurations will be ignored.
/// </summary>
/// <param name="libraryPath">The full path to the native library to load.</param>
/// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
public NativeLibraryConfig WithLibrary(string? libraryPath)
{
ThrowIfLoaded();

_libraryPath = libraryPath;
return this;
}

/// <summary>
/// Configure whether to use cuda backend if possible. Default is true.
/// </summary>
Expand Down Expand Up @@ -167,8 +151,7 @@ internal Description CheckAndGatherDescription()
if (_allowFallback && _skipCheck)
throw new ArgumentException("Cannot skip the check when fallback is allowed.");

var path = _libraryPath;

var path = LibraryPath;

return new Description(
path,
Expand Down Expand Up @@ -324,6 +307,25 @@ private NativeLibraryConfig(NativeLibraryName nativeLibraryName)

internal NativeLibraryName NativeLibraryName { get; }

/// <summary>
/// The full path to a specific native library to load, set by <see cref="WithLibrary"/>.
/// </summary>
internal string? LibraryPath { get; private set; }

/// <summary>
/// Load a specified native library as backend for LLamaSharp.
/// When this method is called, all the other configurations (that are available on the current target framework) will be ignored.
/// </summary>
/// <param name="libraryPath">The full path to the native library to load.</param>
/// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
public NativeLibraryConfig WithLibrary(string? libraryPath)
{
ThrowIfLoaded();

LibraryPath = libraryPath;
return this;
}

internal NativeLogConfig.LLamaLogCallback? LogCallback { get; private set; } = null;

private void ThrowIfLoaded()
Expand Down Expand Up @@ -370,8 +372,9 @@ public NativeLibraryConfig WithLogCallback(ILogger? logger)
/// You can still modify the configuration after this calling but only before any call from <see cref="NativeApi"/>.
/// </summary>
/// <param name="loadedLibrary">
/// The loaded livrary. When the loading failed, this will be null.
/// However if you are using .NET standard2.0, this will never return null.
/// The loaded livrary. When the loading failed, this will be null.
/// On .NET standard2.0, this will only be non-null if a specific library was configured with <see cref="WithLibrary"/>;
/// otherwise it will always be null since no automatic backend detection/loading is performed on that target framework.
/// </param>
/// <returns>Whether the running is successful.</returns>
public bool DryRun(out INativeLibrary? loadedLibrary)
Expand Down Expand Up @@ -407,10 +410,9 @@ public void ForEach(Action<NativeLibraryConfig> action)

#region configurators

#if NET6_0_OR_GREATER
/// <summary>
/// Load a specified native library as backend for LLamaSharp.
/// When this method is called, all the other configurations will be ignored.
/// When this method is called, all the other configurations (that are available on the current target framework) will be ignored.
/// </summary>
/// <param name="llamaPath">The full path to the llama library to load.</param>
/// <param name="mtmdPath">The full path to the mtmd library to load.</param>
Expand All @@ -432,6 +434,7 @@ public NativeLibraryConfigContainer WithLibrary(string? llamaPath, string? mtmdP
return this;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Configure whether to use cuda backend if possible.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions LLama/Native/Load/NativeLibraryUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,27 @@ internal static IntPtr TryLoadLibrary(NativeLibraryConfig config, out INativeLib
}
loadedLibrary = null;
#else
// netstandard2.0 doesn't have access to System.Runtime.InteropServices.NativeLibrary, so we can't
// do the full backend auto-detection that the NET6_0_OR_GREATER path above does. However, if the
// caller gave us an explicit path via NativeLibraryConfig.WithLibrary, we can still load exactly
// that file ourselves using a small platform-specific P/Invoke shim.
if (!string.IsNullOrEmpty(config.LibraryPath))
{
Log($"Loading library: '{config.NativeLibraryName.GetLibraryName()}' from explicit path '{config.LibraryPath}'", LLamaLogLevel.Debug, config.LogCallback);

// Set the flag to ensure this config can no longer be modified
config.LibraryHasLoaded = true;

if (PlatformNativeLibrary.TryLoad(config.LibraryPath!, out var handle))
{
Log($"Successfully loaded '{config.LibraryPath}'", LLamaLogLevel.Info, config.LogCallback);
loadedLibrary = new NativeLibraryFromPath(config.LibraryPath!);
return handle;
}

Log($"Failed loading '{config.LibraryPath}'", LLamaLogLevel.Info, config.LogCallback);
}

loadedLibrary = new UnknownNativeLibrary();
#endif

Expand Down
108 changes: 108 additions & 0 deletions LLama/Native/Load/PlatformNativeLibrary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#if !NET6_0_OR_GREATER
using System;
using System.Runtime.InteropServices;

namespace LLama.Native
{
/// <summary>
/// A minimal, explicit-path native library loader used as a stand-in for
/// <see cref="System.Runtime.InteropServices.NativeLibrary"/> on target frameworks
/// (e.g. netstandard2.0) where that API does not exist. Only the single operation
/// actually needed by <see cref="NativeLibraryUtils"/> - loading a library from a
/// known file path - is implemented.
/// </summary>
internal static class PlatformNativeLibrary
{
/// <summary>
/// Try to load a native library from an explicit file path.
/// </summary>
/// <param name="path">Full or relative path to the native library file.</param>
/// <param name="handle">The OS handle of the loaded library, or <see cref="IntPtr.Zero"/> if loading failed.</param>
/// <returns>True if the library was loaded successfully.</returns>
internal static bool TryLoad(string path, out IntPtr handle)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return Windows.TryLoad(path, out handle);

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Mac.TryLoad(path, out handle);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return Linux.TryLoad(path, out handle);

handle = IntPtr.Zero;
return false;
}

private static class Windows
{
internal static bool TryLoad(string path, out IntPtr handle)
{
handle = LoadLibraryW(path);
return handle != IntPtr.Zero;
}

[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "LoadLibraryW")]
private static extern IntPtr LoadLibraryW(string lpFileName);
}

private static class Mac
{
private const int RTLD_NOW = 2;

internal static bool TryLoad(string path, out IntPtr handle)
{
handle = dlopen(path, RTLD_NOW);
return handle != IntPtr.Zero;
}

// On macOS, dlopen is exported by libSystem, which "libdl.dylib" resolves to.
[DllImport("libdl.dylib", EntryPoint = "dlopen", CharSet = CharSet.Ansi)]
private static extern IntPtr dlopen(string path, int mode);
}

private static class Linux
{
private const int RTLD_NOW = 2;

internal static bool TryLoad(string path, out IntPtr handle)
{
// The native library that exports dlopen varies across distros/glibc versions
// (e.g. glibc >= 2.34 folded libdl into libc). Try each known candidate in turn,
// the way NativeLibrary.TryLoad's internal resolver would.
if (TryDlopen(path, RTLD_NOW, DlopenLibDl2, out handle)) return true;
if (TryDlopen(path, RTLD_NOW, DlopenLibDl, out handle)) return true;
if (TryDlopen(path, RTLD_NOW, DlopenLibC, out handle)) return true;

handle = IntPtr.Zero;
return false;
}

private static bool TryDlopen(string path, int mode, Func<string, int, IntPtr> dlopen, out IntPtr handle)
{
try
{
handle = dlopen(path, mode);
return handle != IntPtr.Zero;
}
catch (DllNotFoundException)
{
// The candidate native library that exports dlopen isn't present on this system, try the next one.
handle = IntPtr.Zero;
return false;
}
}

[DllImport("libdl.so.2", EntryPoint = "dlopen", CharSet = CharSet.Ansi)]
private static extern IntPtr DlopenLibDl2(string path, int mode);

[DllImport("libdl.so", EntryPoint = "dlopen", CharSet = CharSet.Ansi)]
private static extern IntPtr DlopenLibDl(string path, int mode);

// musl and glibc >= 2.34 export dlopen directly from libc.
[DllImport("libc.so.6", EntryPoint = "dlopen", CharSet = CharSet.Ansi)]
private static extern IntPtr DlopenLibC(string path, int mode);
}
}
}
#endif
5 changes: 3 additions & 2 deletions LLama/Native/Load/UnknownNativeLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
namespace LLama.Native
{
/// <summary>
/// When you are using .NET standard2.0, dynamic native library loading is not supported.
/// This class will be returned in <see cref="NativeLibraryConfig.DryRun(out INativeLibrary)"/>.
/// When you are using .NET standard2.0 and no explicit library path was set with
/// <see cref="NativeLibraryConfig.WithLibrary"/>, automatic native library loading is not supported.
/// This class will be returned in <see cref="NativeLibraryConfig.DryRun(out INativeLibrary)"/> in that case.
/// </summary>
public class UnknownNativeLibrary: INativeLibrary
{
Expand Down