You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

NativeApi.Load.cs 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. using LLama.Exceptions;
  2. using System;
  3. using System.IO;
  4. using System.Runtime.InteropServices;
  5. using System.Text.Json;
  6. using System.Collections.Generic;
  7. namespace LLama.Native
  8. {
  9. public static partial class NativeApi
  10. {
  11. static NativeApi()
  12. {
  13. // Overwrite the Dll import resolver for this assembly. The resolver gets
  14. // called by the runtime every time that a call into a DLL is required. The
  15. // resolver returns the loaded DLL handle. This allows us to take control of
  16. // which llama.dll is used.
  17. SetDllImportResolver();
  18. // Immediately make a call which requires loading the llama DLL. This method call
  19. // can't fail unless the DLL hasn't been loaded.
  20. try
  21. {
  22. llama_empty_call();
  23. }
  24. catch (DllNotFoundException)
  25. {
  26. throw new RuntimeError("The native library cannot be correctly loaded. It could be one of the following reasons: \n" +
  27. "1. No LLamaSharp backend was installed. Please search LLamaSharp.Backend and install one of them. \n" +
  28. "2. You are using a device with only CPU but installed cuda backend. Please install cpu backend instead. \n" +
  29. "3. One of the dependency of the native library is missed. Please use `ldd` on linux, `dumpbin` on windows and `otool`" +
  30. "to check if all the dependency of the native library is satisfied. Generally you could find the libraries under your output folder.\n" +
  31. "4. Try to compile llama.cpp yourself to generate a libllama library, then use `LLama.Native.NativeLibraryConfig.WithLibrary` " +
  32. "to specify it at the very beginning of your code. For more informations about compilation, please refer to LLamaSharp repo on github.\n");
  33. }
  34. // Init llama.cpp backend
  35. llama_backend_init();
  36. }
  37. #if NET5_0_OR_GREATER
  38. private static IntPtr _loadedLlamaHandle;
  39. private static IntPtr _loadedLlavaSharedHandle;
  40. #endif
  41. private static void SetDllImportResolver()
  42. {
  43. // NativeLibrary is not available on older runtimes. We'll have to depend on
  44. // the normal runtime dll resolution there.
  45. #if NET5_0_OR_GREATER
  46. NativeLibrary.SetDllImportResolver(typeof(NativeApi).Assembly, (name, _, _) =>
  47. {
  48. if (name == "llama")
  49. {
  50. // If we've already loaded llama return the handle that was loaded last time.
  51. if (_loadedLlamaHandle != IntPtr.Zero)
  52. return _loadedLlamaHandle;
  53. // Try to load a preferred library, based on CPU feature detection
  54. _loadedLlamaHandle = TryLoadLibraries(LibraryName.Llama);
  55. return _loadedLlamaHandle;
  56. }
  57. if (name == "llava_shared")
  58. {
  59. // If we've already loaded llava return the handle that was loaded last time.
  60. if (_loadedLlavaSharedHandle != IntPtr.Zero)
  61. return _loadedLlavaSharedHandle;
  62. // Try to load a preferred library, based on CPU feature detection
  63. _loadedLlavaSharedHandle = TryLoadLibraries(LibraryName.LlavaShared);
  64. return _loadedLlavaSharedHandle;
  65. }
  66. // Return null pointer to indicate that nothing was loaded.
  67. return IntPtr.Zero;
  68. });
  69. #endif
  70. }
  71. private static void Log(string message, LLamaLogLevel level)
  72. {
  73. if (!enableLogging)
  74. return;
  75. if ((int)level > (int)logLevel)
  76. return;
  77. var fg = Console.ForegroundColor;
  78. var bg = Console.BackgroundColor;
  79. try
  80. {
  81. ConsoleColor color;
  82. string levelPrefix;
  83. if (level == LLamaLogLevel.Debug)
  84. {
  85. color = ConsoleColor.Cyan;
  86. levelPrefix = "[Debug]";
  87. }
  88. else if (level == LLamaLogLevel.Info)
  89. {
  90. color = ConsoleColor.Green;
  91. levelPrefix = "[Info]";
  92. }
  93. else if (level == LLamaLogLevel.Error)
  94. {
  95. color = ConsoleColor.Red;
  96. levelPrefix = "[Error]";
  97. }
  98. else
  99. {
  100. color = ConsoleColor.Yellow;
  101. levelPrefix = "[UNK]";
  102. }
  103. Console.ForegroundColor = color;
  104. Console.WriteLine($"{loggingPrefix} {levelPrefix} {message}");
  105. }
  106. finally
  107. {
  108. Console.ForegroundColor = fg;
  109. Console.BackgroundColor = bg;
  110. }
  111. }
  112. #region CUDA version
  113. private static int GetCudaMajorVersion()
  114. {
  115. string? cudaPath;
  116. string version = "";
  117. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  118. {
  119. cudaPath = Environment.GetEnvironmentVariable("CUDA_PATH");
  120. if (cudaPath is null)
  121. {
  122. return -1;
  123. }
  124. version = GetCudaVersionFromPath(cudaPath);
  125. }
  126. else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
  127. {
  128. // Try the default first
  129. cudaPath = "/usr/local/bin/cuda";
  130. version = GetCudaVersionFromPath(cudaPath);
  131. if (string.IsNullOrEmpty(version))
  132. {
  133. cudaPath = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
  134. if (cudaPath is null)
  135. {
  136. return -1;
  137. }
  138. foreach (var path in cudaPath.Split(':'))
  139. {
  140. version = GetCudaVersionFromPath(Path.Combine(path, ".."));
  141. if (string.IsNullOrEmpty(version))
  142. {
  143. break;
  144. }
  145. }
  146. }
  147. }
  148. if (string.IsNullOrEmpty(version))
  149. return -1;
  150. version = version.Split('.')[0];
  151. if (int.TryParse(version, out var majorVersion))
  152. return majorVersion;
  153. return -1;
  154. }
  155. private static string GetCudaVersionFromPath(string cudaPath)
  156. {
  157. try
  158. {
  159. string json = File.ReadAllText(Path.Combine(cudaPath, cudaVersionFile));
  160. using (JsonDocument document = JsonDocument.Parse(json))
  161. {
  162. JsonElement root = document.RootElement;
  163. JsonElement cublasNode = root.GetProperty("libcublas");
  164. JsonElement versionNode = cublasNode.GetProperty("version");
  165. if (versionNode.ValueKind == JsonValueKind.Undefined)
  166. {
  167. return string.Empty;
  168. }
  169. return versionNode.GetString() ?? "";
  170. }
  171. }
  172. catch (Exception)
  173. {
  174. return string.Empty;
  175. }
  176. }
  177. #endregion
  178. #if NET6_0_OR_GREATER
  179. private static IEnumerable<string> GetLibraryTryOrder(NativeLibraryConfig.Description configuration)
  180. {
  181. var loadingName = configuration.Library.GetLibraryName();
  182. Log($"Loading library: '{loadingName}'", LLamaLogLevel.Debug);
  183. // Get platform specific parts of the path (e.g. .so/.dll/.dylib, libName prefix or not)
  184. GetPlatformPathParts(out var platform, out var os, out var ext, out var libPrefix);
  185. Log($"Detected OS Platform: '{platform}'", LLamaLogLevel.Info);
  186. Log($"Detected OS string: '{os}'", LLamaLogLevel.Debug);
  187. Log($"Detected extension string: '{ext}'", LLamaLogLevel.Debug);
  188. Log($"Detected prefix string: '{libPrefix}'", LLamaLogLevel.Debug);
  189. if (configuration.UseCuda && (platform == OSPlatform.Windows || platform == OSPlatform.Linux))
  190. {
  191. var cudaVersion = GetCudaMajorVersion();
  192. Log($"Detected cuda major version {cudaVersion}.", LLamaLogLevel.Info);
  193. if (cudaVersion == -1 && !configuration.AllowFallback)
  194. {
  195. // if check skipped, we just try to load cuda libraries one by one.
  196. if (configuration.SkipCheck)
  197. {
  198. yield return GetCudaLibraryPath(loadingName, "cuda12");
  199. yield return GetCudaLibraryPath(loadingName, "cuda11");
  200. }
  201. else
  202. {
  203. throw new RuntimeError("Configured to load a cuda library but no cuda detected on your device.");
  204. }
  205. }
  206. else if (cudaVersion == 11)
  207. {
  208. yield return GetCudaLibraryPath(loadingName, "cuda11");
  209. }
  210. else if (cudaVersion == 12)
  211. {
  212. yield return GetCudaLibraryPath(loadingName, "cuda12");
  213. }
  214. else if (cudaVersion > 0)
  215. {
  216. throw new RuntimeError($"Cuda version {cudaVersion} hasn't been supported by LLamaSharp, please open an issue for it.");
  217. }
  218. // otherwise no cuda detected but allow fallback
  219. }
  220. // Add the CPU/Metal libraries
  221. if (platform == OSPlatform.OSX)
  222. {
  223. // On Mac it's very simple, there's no AVX to consider.
  224. yield return GetMacLibraryPath(loadingName);
  225. }
  226. else
  227. {
  228. if (configuration.AllowFallback)
  229. {
  230. // Try all of the AVX levels we can support.
  231. if (configuration.AvxLevel >= NativeLibraryConfig.AvxLevel.Avx512)
  232. yield return GetAvxLibraryPath(loadingName, NativeLibraryConfig.AvxLevel.Avx512);
  233. if (configuration.AvxLevel >= NativeLibraryConfig.AvxLevel.Avx2)
  234. yield return GetAvxLibraryPath(loadingName, NativeLibraryConfig.AvxLevel.Avx2);
  235. if (configuration.AvxLevel >= NativeLibraryConfig.AvxLevel.Avx)
  236. yield return GetAvxLibraryPath(loadingName, NativeLibraryConfig.AvxLevel.Avx);
  237. yield return GetAvxLibraryPath(loadingName, NativeLibraryConfig.AvxLevel.None);
  238. }
  239. else
  240. {
  241. // Fallback is not allowed - use the exact specified AVX level
  242. yield return GetAvxLibraryPath(loadingName, configuration.AvxLevel);
  243. }
  244. }
  245. }
  246. private static string GetMacLibraryPath(string libraryName)
  247. {
  248. GetPlatformPathParts(out _, out var os, out var fileExtension, out var libPrefix);
  249. return $"runtimes/{os}/native/{libPrefix}{libraryName}{fileExtension}";
  250. }
  251. /// <summary>
  252. /// Given a CUDA version and some path parts, create a complete path to the library file
  253. /// </summary>
  254. /// <param name="libraryName">Library being loaded (e.g. "llama")</param>
  255. /// <param name="cuda">CUDA version (e.g. "cuda11")</param>
  256. /// <returns></returns>
  257. private static string GetCudaLibraryPath(string libraryName, string cuda)
  258. {
  259. GetPlatformPathParts(out _, out var os, out var fileExtension, out var libPrefix);
  260. return $"runtimes/{os}/native/{cuda}/{libPrefix}{libraryName}{fileExtension}";
  261. }
  262. /// <summary>
  263. /// Given an AVX level and some path parts, create a complete path to the library file
  264. /// </summary>
  265. /// <param name="libraryName">Library being loaded (e.g. "llama")</param>
  266. /// <param name="avx"></param>
  267. /// <returns></returns>
  268. private static string GetAvxLibraryPath(string libraryName, NativeLibraryConfig.AvxLevel avx)
  269. {
  270. GetPlatformPathParts(out _, out var os, out var fileExtension, out var libPrefix);
  271. var avxStr = NativeLibraryConfig.AvxLevelToString(avx);
  272. if (!string.IsNullOrEmpty(avxStr))
  273. avxStr += "/";
  274. return $"runtimes/{os}/native/{avxStr}{libPrefix}{libraryName}{fileExtension}";
  275. }
  276. private static void GetPlatformPathParts(out OSPlatform platform, out string os, out string fileExtension, out string libPrefix)
  277. {
  278. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  279. {
  280. platform = OSPlatform.Windows;
  281. os = "win-x64";
  282. fileExtension = ".dll";
  283. libPrefix = "";
  284. return;
  285. }
  286. if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
  287. {
  288. platform = OSPlatform.Linux;
  289. os = "linux-x64";
  290. fileExtension = ".so";
  291. libPrefix = "lib";
  292. return;
  293. }
  294. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
  295. {
  296. platform = OSPlatform.OSX;
  297. fileExtension = ".dylib";
  298. os = System.Runtime.Intrinsics.Arm.ArmBase.Arm64.IsSupported
  299. ? "osx-arm64"
  300. : "osx-x64";
  301. libPrefix = "lib";
  302. }
  303. else
  304. {
  305. throw new RuntimeError("Your operating system is not supported, please open an issue in LLamaSharp.");
  306. }
  307. }
  308. #endif
  309. /// <summary>
  310. /// Try to load libllama/llava_shared, using CPU feature detection to try and load a more specialised DLL if possible
  311. /// </summary>
  312. /// <returns>The library handle to unload later, or IntPtr.Zero if no library was loaded</returns>
  313. private static IntPtr TryLoadLibraries(LibraryName lib)
  314. {
  315. #if NET6_0_OR_GREATER
  316. var configuration = NativeLibraryConfig.CheckAndGatherDescription(lib);
  317. enableLogging = configuration.Logging;
  318. logLevel = configuration.LogLevel;
  319. // Set the flag to ensure the NativeLibraryConfig can no longer be modified
  320. NativeLibraryConfig.LibraryHasLoaded = true;
  321. // Show the configuration we're working with
  322. Log(configuration.ToString(), LLamaLogLevel.Info);
  323. // If a specific path is requested, load that or immediately fail
  324. if (!string.IsNullOrEmpty(configuration.Path))
  325. {
  326. if (!NativeLibrary.TryLoad(configuration.Path, out var handle))
  327. throw new RuntimeError($"Failed to load the native library [{configuration.Path}] you specified.");
  328. Log($"Successfully loaded the library [{configuration.Path}] specified by user", LLamaLogLevel.Info);
  329. return handle;
  330. }
  331. // Get a list of locations to try loading (in order of preference)
  332. var libraryTryLoadOrder = GetLibraryTryOrder(configuration);
  333. foreach (var libraryPath in libraryTryLoadOrder)
  334. {
  335. var fullPath = TryFindPath(libraryPath);
  336. Log($"Trying '{fullPath}'", LLamaLogLevel.Debug);
  337. var result = TryLoad(fullPath);
  338. if (result != IntPtr.Zero)
  339. {
  340. Log($"Loaded '{fullPath}'", LLamaLogLevel.Info);
  341. return result;
  342. }
  343. Log($"Failed Loading '{fullPath}'", LLamaLogLevel.Info);
  344. }
  345. if (!configuration.AllowFallback)
  346. {
  347. throw new RuntimeError("Failed to load the library that match your rule, please" +
  348. " 1) check your rule." +
  349. " 2) try to allow fallback." +
  350. " 3) or open an issue if it's expected to be successful.");
  351. }
  352. #endif
  353. Log($"No library was loaded before calling native apis. " +
  354. $"This is not an error under netstandard2.0 but needs attention with net6 or higher.", LLamaLogLevel.Warning);
  355. return IntPtr.Zero;
  356. #if NET6_0_OR_GREATER
  357. // Try to load a DLL from the path.
  358. // Returns null if nothing is loaded.
  359. static IntPtr TryLoad(string path)
  360. {
  361. if (NativeLibrary.TryLoad(path, out var handle))
  362. return handle;
  363. return IntPtr.Zero;
  364. }
  365. // Try to find the given file in any of the possible search paths
  366. string TryFindPath(string filename)
  367. {
  368. // Try the configured search directories in the configuration
  369. foreach (var path in configuration.SearchDirectories)
  370. {
  371. var candidate = Path.Combine(path, filename);
  372. if (File.Exists(candidate))
  373. return candidate;
  374. }
  375. // Try a few other possible paths
  376. var possiblePathPrefix = new[] {
  377. AppDomain.CurrentDomain.BaseDirectory,
  378. Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? ""
  379. };
  380. foreach (var path in possiblePathPrefix)
  381. {
  382. var candidate = Path.Combine(path, filename);
  383. if (File.Exists(candidate))
  384. return candidate;
  385. }
  386. return filename;
  387. }
  388. #endif
  389. }
  390. internal const string libraryName = "llama";
  391. internal const string llavaLibraryName = "llava_shared";
  392. private const string cudaVersionFile = "version.json";
  393. private const string loggingPrefix = "[LLamaSharp Native]";
  394. private static bool enableLogging = false;
  395. private static LLamaLogLevel logLevel = LLamaLogLevel.Info;
  396. }
  397. }