-
Notifications
You must be signed in to change notification settings - Fork 414
/
Copy pathNativeLibraryUtils.cs
271 lines (232 loc) · 12.7 KB
/
NativeLibraryUtils.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
using LLama.Abstractions;
using LLama.Exceptions;
using System;
using System.Collections.Generic;
using System.IO;
namespace LLama.Native
{
internal static class NativeLibraryUtils
{
/// <summary>
/// Try to load libllama/llava_shared, using CPU feature detection to try and load a more specialised DLL if possible
/// </summary>
/// <returns>The library handle to unload later, or IntPtr.Zero if no library was loaded</returns>
internal static IntPtr TryLoadLibrary(NativeLibraryConfig config, out INativeLibrary? loadedLibrary)
{
#if NET6_0_OR_GREATER
var description = config.CheckAndGatherDescription();
var systemInfo = SystemInfo.Get();
Log($"Loading library: '{config.NativeLibraryName.GetLibraryName()}'", LLamaLogLevel.Debug, config.LogCallback);
// Get platform specific parts of the path (e.g. .so/.dll/.dylib, libName prefix or not)
NativeLibraryUtils.GetPlatformPathParts(systemInfo.OSPlatform, out var os, out var ext, out var libPrefix);
Log($"Detected OS Platform: '{systemInfo.OSPlatform}'", LLamaLogLevel.Info, config.LogCallback);
Log($"Detected OS string: '{os}'", LLamaLogLevel.Debug, config.LogCallback);
Log($"Detected extension string: '{ext}'", LLamaLogLevel.Debug, config.LogCallback);
Log($"Detected prefix string: '{libPrefix}'", LLamaLogLevel.Debug, config.LogCallback);
// Set the flag to ensure this config can no longer be modified
config.LibraryHasLoaded = true;
// Show the configuration we're working with
Log(description.ToString(), LLamaLogLevel.Info, config.LogCallback);
// Get the libraries ordered by priority from the selecting policy.
var libraries = config.SelectingPolicy.Apply(description, systemInfo, config.LogCallback);
// Try to load the libraries
foreach (var library in libraries)
{
// Prepare the local library file and get the path.
var paths = library.Prepare(systemInfo, config.LogCallback);
foreach (var path in paths)
{
Log($"Got relative library path '{path}' from local with {library.Metadata}, trying to load it...", LLamaLogLevel.Debug, config.LogCallback);
// After the llama.cpp binaries have been split up (PR #10256), we need to load the dependencies manually.
// It can't be done automatically on Windows, because the dependencies can be in different folders (for example, ggml-cuda.dll from the cuda12 folder, and ggml-cpu.dll from the avx2 folder)
// It can't be done automatically on Linux, because Linux uses the environment variable "LD_LIBRARY_PATH" to automatically load dependencies, and LD_LIBRARY_PATH can only be
// set before running LLamaSharp, but we only know which folders to search in when running LLamaSharp (decided by the NativeLibrary).
// Get the directory of the current runtime
string? currentRuntimeDirectory = Path.GetDirectoryName(path);
// If we failed to get the directory of the current runtime, log it and continue on to the next library
if (currentRuntimeDirectory == null)
{
Log($"Failed to get the directory of the current runtime from path '{path}'", LLamaLogLevel.Error, config.LogCallback);
continue;
}
// List which will hold all paths to dependencies to load
var dependencyPaths = new List<string>();
// We should always load ggml-base from the current runtime directory
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-base{ext}"));
// If the library has metadata, we can check if we need to load additional dependencies
if (library.Metadata != null)
{
if (systemInfo.OSPlatform == OSPlatform.OSX)
{
// On OSX, we should load the CPU backend from the current directory
// ggml-cpu
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-cpu{ext}"));
// ggml-metal (only supported on osx-arm64)
if (os == "osx-arm64")
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-metal{ext}"));
// ggml-blas (osx-x64, osx-x64-rosetta2 and osx-arm64 all have blas)
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-blas{ext}"));
}
else
{
// On other platforms (Windows, Linux), we need to load the CPU backend from the specified AVX level directory
// We are using the AVX level supplied by NativeLibraryConfig, which automatically detects the highest supported AVX level for us
if (os == "linux-arm64"){
dependencyPaths.Add(Path.Combine(
$"runtimes/{os}/native",
$"{libPrefix}ggml-cpu{ext}"
));
}
else{
// ggml-cpu
dependencyPaths.Add(Path.Combine(
$"runtimes/{os}/native/{NativeLibraryConfig.AvxLevelToString(library.Metadata.AvxLevel)}",
$"{libPrefix}ggml-cpu{ext}"
));
// ggml-cuda
if (library.Metadata.UseCuda)
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-cuda{ext}"));
// ggml-vulkan
if (library.Metadata.UseVulkan)
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml-vulkan{ext}"));
}
}
}
// And finally, we can add ggml
dependencyPaths.Add(Path.Combine(currentRuntimeDirectory, $"{libPrefix}ggml{ext}"));
// Now, we will loop through our dependencyPaths and try to load them one by one
foreach (var dependencyPath in dependencyPaths)
{
// Try to load the dependency
var dependencyResult = TryLoad(dependencyPath, description.SearchDirectories, config.LogCallback);
// If we successfully loaded the library, log it
if (dependencyResult != IntPtr.Zero)
{
Log($"Successfully loaded dependency '{dependencyPath}'", LLamaLogLevel.Info, config.LogCallback);
}
else
{
Log($"Failed loading dependency '{dependencyPath}'", LLamaLogLevel.Info, config.LogCallback);
}
}
// Try to load the main library
var result = TryLoad(path, description.SearchDirectories, config.LogCallback);
// If we successfully loaded the library, return the handle
if (result != IntPtr.Zero)
{
loadedLibrary = library;
return result;
}
}
}
// If fallback is allowed, we will make the last try (the default system loading) when calling the native api.
// Otherwise we throw an exception here.
if (!description.AllowFallback)
{
throw new RuntimeError("Failed to load the native library. Please check the log for more information.");
}
loadedLibrary = null;
#else
loadedLibrary = new UnknownNativeLibrary();
#endif
Log($"No library was loaded before calling native apis. " +
$"This is not an error under netstandard2.0 but needs attention with net6 or higher.", LLamaLogLevel.Warning, config.LogCallback);
return IntPtr.Zero;
#if NET6_0_OR_GREATER
// Try to load a DLL from the path.
// Returns null if nothing is loaded.
static IntPtr TryLoad(string path, IEnumerable<string> searchDirectories, NativeLogConfig.LLamaLogCallback? logCallback)
{
var fullPath = TryFindPath(path, searchDirectories);
Log($"Found full path file '{fullPath}' for relative path '{path}'", LLamaLogLevel.Debug, logCallback);
if (NativeLibrary.TryLoad(fullPath, out var handle))
{
Log($"Successfully loaded '{fullPath}'", LLamaLogLevel.Info, logCallback);
return handle;
}
Log($"Failed Loading '{fullPath}'", LLamaLogLevel.Info, logCallback);
return IntPtr.Zero;
}
#endif
}
// Try to find the given file in any of the possible search paths
private static string TryFindPath(string filename, IEnumerable<string> searchDirectories)
{
// Try the configured search directories in the configuration
foreach (var path in searchDirectories)
{
var candidate = Path.Combine(path, filename);
if (File.Exists(candidate))
return candidate;
}
// Try a few other possible paths
var possiblePathPrefix = new[] {
AppDomain.CurrentDomain.BaseDirectory,
Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ""
};
foreach (var path in possiblePathPrefix)
{
var candidate = Path.Combine(path, filename);
if (File.Exists(candidate))
return candidate;
}
return filename;
}
private static void Log(string message, LLamaLogLevel level, NativeLogConfig.LLamaLogCallback? logCallback)
{
if (!message.EndsWith("\n"))
message += "\n";
logCallback?.Invoke(level, message);
}
#if NET6_0_OR_GREATER
public static void GetPlatformPathParts(OSPlatform platform, out string os, out string fileExtension, out string libPrefix)
{
if (platform == OSPlatform.Windows)
{
os = "win-x64";
fileExtension = ".dll";
libPrefix = "";
return;
}
if (platform == OSPlatform.Linux)
{
if(System.Runtime.Intrinsics.Arm.ArmBase.Arm64.IsSupported){
// linux arm64
os = "linux-arm64";
fileExtension = ".so";
libPrefix = "lib";
return;
}
if(RuntimeInformation.RuntimeIdentifier.ToLower().StartsWith("alpine"))
{
// alpine linux distro
os = "linux-musl-x64";
fileExtension = ".so";
libPrefix = "lib";
return;
}
else
{
// other linux distro
os = "linux-x64";
fileExtension = ".so";
libPrefix = "lib";
return;
}
}
if (platform == OSPlatform.OSX)
{
fileExtension = ".dylib";
os = System.Runtime.Intrinsics.Arm.ArmBase.Arm64.IsSupported
? "osx-arm64"
: "osx-x64";
libPrefix = "lib";
}
else
{
throw new RuntimeError("Your operating system is not supported, please open an issue in LLamaSharp.");
}
}
#endif
}
}