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.

NativeLibraryConfig.cs 23 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using LLama.Abstractions;
  5. using Microsoft.Extensions.Logging;
  6. namespace LLama.Native
  7. {
  8. #if NET6_0_OR_GREATER
  9. /// <summary>
  10. /// Allows configuration of the native llama.cpp libraries to load and use.
  11. /// All configuration must be done before using **any** other LLamaSharp methods!
  12. /// </summary>
  13. public sealed partial class NativeLibraryConfig
  14. {
  15. private string? _libraryPath;
  16. private bool _useCuda = true;
  17. private AvxLevel _avxLevel;
  18. private bool _allowFallback = true;
  19. private bool _skipCheck = false;
  20. /// <summary>
  21. /// search directory -> priority level, 0 is the lowest.
  22. /// </summary>
  23. private readonly List<string> _searchDirectories = new List<string>();
  24. internal INativeLibrarySelectingPolicy SelectingPolicy { get; private set; } = new DefaultNativeLibrarySelectingPolicy();
  25. #region configurators
  26. /// <summary>
  27. /// Load a specified native library as backend for LLamaSharp.
  28. /// When this method is called, all the other configurations will be ignored.
  29. /// </summary>
  30. /// <param name="libraryPath">The full path to the native library to load.</param>
  31. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  32. public NativeLibraryConfig WithLibrary(string? libraryPath)
  33. {
  34. ThrowIfLoaded();
  35. _libraryPath = libraryPath;
  36. return this;
  37. }
  38. /// <summary>
  39. /// Configure whether to use cuda backend if possible. Default is true.
  40. /// </summary>
  41. /// <param name="enable"></param>
  42. /// <returns></returns>
  43. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  44. public NativeLibraryConfig WithCuda(bool enable = true)
  45. {
  46. ThrowIfLoaded();
  47. _useCuda = enable;
  48. return this;
  49. }
  50. /// <summary>
  51. /// Configure the prefferred avx support level of the backend.
  52. /// Default value is detected automatically due to your operating system.
  53. /// </summary>
  54. /// <param name="level"></param>
  55. /// <returns></returns>
  56. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  57. public NativeLibraryConfig WithAvx(AvxLevel level)
  58. {
  59. ThrowIfLoaded();
  60. _avxLevel = level;
  61. return this;
  62. }
  63. /// <summary>
  64. /// Configure whether to allow fallback when there's no match for preferred settings. Default is true.
  65. /// </summary>
  66. /// <param name="enable"></param>
  67. /// <returns></returns>
  68. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  69. public NativeLibraryConfig WithAutoFallback(bool enable = true)
  70. {
  71. ThrowIfLoaded();
  72. _allowFallback = enable;
  73. return this;
  74. }
  75. /// <summary>
  76. /// Whether to skip the check when you don't allow fallback. This option
  77. /// may be useful under some complex conditions. For example, you're sure
  78. /// you have your cublas configured but LLamaSharp take it as invalid by mistake. Default is false;
  79. /// </summary>
  80. /// <param name="enable"></param>
  81. /// <returns></returns>
  82. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  83. public NativeLibraryConfig SkipCheck(bool enable = true)
  84. {
  85. ThrowIfLoaded();
  86. _skipCheck = enable;
  87. return this;
  88. }
  89. /// <summary>
  90. /// Add self-defined search directories. Note that the file stucture of the added
  91. /// directories must be the same as the default directory. Besides, the directory
  92. /// won't be used recursively.
  93. /// </summary>
  94. /// <param name="directories"></param>
  95. /// <returns></returns>
  96. public NativeLibraryConfig WithSearchDirectories(IEnumerable<string> directories)
  97. {
  98. ThrowIfLoaded();
  99. _searchDirectories.AddRange(directories);
  100. return this;
  101. }
  102. /// <summary>
  103. /// Add self-defined search directories. Note that the file stucture of the added
  104. /// directories must be the same as the default directory. Besides, the directory
  105. /// won't be used recursively.
  106. /// </summary>
  107. /// <param name="directory"></param>
  108. /// <returns></returns>
  109. public NativeLibraryConfig WithSearchDirectory(string directory)
  110. {
  111. ThrowIfLoaded();
  112. _searchDirectories.Add(directory);
  113. return this;
  114. }
  115. /// <summary>
  116. /// Set the policy which decides how to select the desired native libraries and order them by priority.
  117. /// By default we use <see cref="DefaultNativeLibrarySelectingPolicy"/>.
  118. /// </summary>
  119. /// <param name="policy"></param>
  120. /// <returns></returns>
  121. public NativeLibraryConfig WithSelectingPolicy(INativeLibrarySelectingPolicy policy)
  122. {
  123. ThrowIfLoaded();
  124. SelectingPolicy = policy;
  125. return this;
  126. }
  127. #endregion
  128. internal Description CheckAndGatherDescription()
  129. {
  130. if (_allowFallback && _skipCheck)
  131. throw new ArgumentException("Cannot skip the check when fallback is allowed.");
  132. var path = _libraryPath;
  133. return new Description(
  134. path,
  135. NativeLibraryName,
  136. _useCuda,
  137. _avxLevel,
  138. _allowFallback,
  139. _skipCheck,
  140. _searchDirectories.Concat(new[] { "./" }).ToArray()
  141. );
  142. }
  143. internal static string AvxLevelToString(AvxLevel level)
  144. {
  145. return level switch
  146. {
  147. AvxLevel.None => string.Empty,
  148. AvxLevel.Avx => "avx",
  149. AvxLevel.Avx2 => "avx2",
  150. AvxLevel.Avx512 => "avx512",
  151. _ => throw new ArgumentException($"Unknown AvxLevel '{level}'")
  152. };
  153. }
  154. /// <summary>
  155. /// Private constructor prevents new instances of this class being created
  156. /// </summary>
  157. private NativeLibraryConfig(NativeLibraryName nativeLibraryName)
  158. {
  159. NativeLibraryName = nativeLibraryName;
  160. // Automatically detect the highest supported AVX level
  161. if (System.Runtime.Intrinsics.X86.Avx.IsSupported)
  162. _avxLevel = AvxLevel.Avx;
  163. if (System.Runtime.Intrinsics.X86.Avx2.IsSupported)
  164. _avxLevel = AvxLevel.Avx2;
  165. if (CheckAVX512())
  166. _avxLevel = AvxLevel.Avx512;
  167. }
  168. private static bool CheckAVX512()
  169. {
  170. if (!System.Runtime.Intrinsics.X86.X86Base.IsSupported)
  171. return false;
  172. // ReSharper disable UnusedVariable (ebx is used when < NET8)
  173. var (_, ebx, ecx, _) = System.Runtime.Intrinsics.X86.X86Base.CpuId(7, 0);
  174. // ReSharper restore UnusedVariable
  175. var vnni = (ecx & 0b_1000_0000_0000) != 0;
  176. #if NET8_0_OR_GREATER
  177. var f = System.Runtime.Intrinsics.X86.Avx512F.IsSupported;
  178. var bw = System.Runtime.Intrinsics.X86.Avx512BW.IsSupported;
  179. var vbmi = System.Runtime.Intrinsics.X86.Avx512Vbmi.IsSupported;
  180. #else
  181. var f = (ebx & (1 << 16)) != 0;
  182. var bw = (ebx & (1 << 30)) != 0;
  183. var vbmi = (ecx & 0b_0000_0000_0010) != 0;
  184. #endif
  185. return vnni && vbmi && bw && f;
  186. }
  187. /// <summary>
  188. /// The description of the native library configurations that's already specified.
  189. /// </summary>
  190. /// <param name="Path"></param>
  191. /// <param name="Library"></param>
  192. /// <param name="UseCuda"></param>
  193. /// <param name="AvxLevel"></param>
  194. /// <param name="AllowFallback"></param>
  195. /// <param name="SkipCheck"></param>
  196. /// <param name="SearchDirectories"></param>
  197. public record Description(string? Path, NativeLibraryName Library, bool UseCuda, AvxLevel AvxLevel, bool AllowFallback, bool SkipCheck,
  198. string[] SearchDirectories)
  199. {
  200. /// <inheritdoc/>
  201. public override string ToString()
  202. {
  203. string avxLevelString = AvxLevel switch
  204. {
  205. AvxLevel.None => "NoAVX",
  206. AvxLevel.Avx => "AVX",
  207. AvxLevel.Avx2 => "AVX2",
  208. AvxLevel.Avx512 => "AVX512",
  209. _ => "Unknown"
  210. };
  211. string searchDirectoriesString = "{ " + string.Join(", ", SearchDirectories) + " }";
  212. return $"NativeLibraryConfig Description:\n" +
  213. $"- LibraryName: {Library}\n" +
  214. $"- Path: '{Path}'\n" +
  215. $"- PreferCuda: {UseCuda}\n" +
  216. $"- PreferredAvxLevel: {avxLevelString}\n" +
  217. $"- AllowFallback: {AllowFallback}\n" +
  218. $"- SkipCheck: {SkipCheck}\n" +
  219. $"- SearchDirectories and Priorities: {searchDirectoriesString}";
  220. }
  221. }
  222. }
  223. #endif
  224. public sealed partial class NativeLibraryConfig
  225. {
  226. /// <summary>
  227. /// Set configurations for all the native libraries, including LLama and LLava
  228. /// </summary>
  229. [Obsolete("Please use NativeLibraryConfig.All instead, or set configurations for NativeLibraryConfig.LLama and NativeLibraryConfig.LLavaShared respectively.")]
  230. public static NativeLibraryConfigContainer Instance => All;
  231. /// <summary>
  232. /// Set configurations for all the native libraries, including LLama and LLava
  233. /// </summary>
  234. public static NativeLibraryConfigContainer All { get; }
  235. /// <summary>
  236. /// Configuration for LLama native library
  237. /// </summary>
  238. public static NativeLibraryConfig LLama { get; }
  239. /// <summary>
  240. /// Configuration for LLava native library
  241. /// </summary>
  242. public static NativeLibraryConfig LLava { get; }
  243. /// <summary>
  244. /// The current version.
  245. /// </summary>
  246. public static string CurrentVersion => VERSION; // This should be changed before publishing new version. TODO: any better approach?
  247. private const string COMMIT_HASH = "f7001c";
  248. private const string VERSION = "master";
  249. /// <summary>
  250. /// Get the llama.cpp commit hash of the current version.
  251. /// </summary>
  252. /// <returns></returns>
  253. public static string GetNativeLibraryCommitHash() => COMMIT_HASH;
  254. static NativeLibraryConfig()
  255. {
  256. LLama = new(NativeLibraryName.Llama);
  257. LLava = new(NativeLibraryName.LlavaShared);
  258. All = new(LLama, LLava);
  259. }
  260. #if NETSTANDARD2_0
  261. private NativeLibraryConfig(NativeLibraryName nativeLibraryName)
  262. {
  263. NativeLibraryName = nativeLibraryName;
  264. }
  265. #endif
  266. /// <summary>
  267. /// Check if the native library has already been loaded. Configuration cannot be modified if this is true.
  268. /// </summary>
  269. public bool LibraryHasLoaded { get; internal set; }
  270. internal NativeLibraryName NativeLibraryName { get; }
  271. internal NativeLogConfig.LLamaLogCallback? LogCallback { get; private set; } = null;
  272. private void ThrowIfLoaded()
  273. {
  274. if (LibraryHasLoaded)
  275. throw new InvalidOperationException("The library has already loaded, you can't change the configurations. " +
  276. "Please finish the configuration setting before any call to LLamaSharp native APIs." +
  277. "Please use NativeLibraryConfig.DryRun if you want to see whether it's loaded " +
  278. "successfully but still have chance to modify the configurations.");
  279. }
  280. /// <summary>
  281. /// Set the log callback that will be used for all llama.cpp log messages
  282. /// </summary>
  283. /// <param name="callback"></param>
  284. /// <exception cref="NotImplementedException"></exception>
  285. public NativeLibraryConfig WithLogCallback(NativeLogConfig.LLamaLogCallback? callback)
  286. {
  287. ThrowIfLoaded();
  288. LogCallback = callback;
  289. return this;
  290. }
  291. /// <summary>
  292. /// Set the log callback that will be used for all llama.cpp log messages
  293. /// </summary>
  294. /// <param name="logger"></param>
  295. /// <exception cref="NotImplementedException"></exception>
  296. public NativeLibraryConfig WithLogCallback(ILogger? logger)
  297. {
  298. ThrowIfLoaded();
  299. // Redirect to llama_log_set. This will wrap the logger in a delegate and bind that as the log callback instead.
  300. NativeLogConfig.llama_log_set(logger);
  301. return this;
  302. }
  303. /// <summary>
  304. /// Try to load the native library with the current configurations,
  305. /// but do not actually set it to <see cref="NativeApi"/>.
  306. ///
  307. /// You can still modify the configuration after this calling but only before any call from <see cref="NativeApi"/>.
  308. /// </summary>
  309. /// <param name="loadedLibrary">
  310. /// The loaded livrary. When the loading failed, this will be null.
  311. /// However if you are using .NET standard2.0, this will never return null.
  312. /// </param>
  313. /// <returns>Whether the running is successful.</returns>
  314. public bool DryRun(out INativeLibrary? loadedLibrary)
  315. {
  316. LogCallback?.Invoke(LLamaLogLevel.Debug, $"Beginning dry run for {this.NativeLibraryName.GetLibraryName()}...");
  317. return NativeLibraryUtils.TryLoadLibrary(this, out loadedLibrary) != IntPtr.Zero;
  318. }
  319. }
  320. /// <summary>
  321. /// A class to set same configurations to multiple libraries at the same time.
  322. /// </summary>
  323. public sealed class NativeLibraryConfigContainer
  324. {
  325. private NativeLibraryConfig[] _configs;
  326. internal NativeLibraryConfigContainer(params NativeLibraryConfig[] configs)
  327. {
  328. _configs = configs;
  329. }
  330. #region configurators
  331. #if NET6_0_OR_GREATER
  332. /// <summary>
  333. /// Load a specified native library as backend for LLamaSharp.
  334. /// When this method is called, all the other configurations will be ignored.
  335. /// </summary>
  336. /// <param name="llamaPath">The full path to the llama library to load.</param>
  337. /// <param name="llavaPath">The full path to the llava library to load.</param>
  338. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  339. public NativeLibraryConfigContainer WithLibrary(string? llamaPath, string? llavaPath)
  340. {
  341. foreach(var config in _configs)
  342. {
  343. if(config.NativeLibraryName == NativeLibraryName.Llama && llamaPath is not null)
  344. {
  345. config.WithLibrary(llamaPath);
  346. }
  347. if(config.NativeLibraryName == NativeLibraryName.LlavaShared && llavaPath is not null)
  348. {
  349. config.WithLibrary(llavaPath);
  350. }
  351. }
  352. return this;
  353. }
  354. /// <summary>
  355. /// Configure whether to use cuda backend if possible.
  356. /// </summary>
  357. /// <param name="enable"></param>
  358. /// <returns></returns>
  359. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  360. public NativeLibraryConfigContainer WithCuda(bool enable = true)
  361. {
  362. foreach(var config in _configs)
  363. {
  364. config.WithCuda(enable);
  365. }
  366. return this;
  367. }
  368. /// <summary>
  369. /// Configure the prefferred avx support level of the backend.
  370. /// </summary>
  371. /// <param name="level"></param>
  372. /// <returns></returns>
  373. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  374. public NativeLibraryConfigContainer WithAvx(AvxLevel level)
  375. {
  376. foreach (var config in _configs)
  377. {
  378. config.WithAvx(level);
  379. }
  380. return this;
  381. }
  382. /// <summary>
  383. /// Configure whether to allow fallback when there's no match for preferred settings.
  384. /// </summary>
  385. /// <param name="enable"></param>
  386. /// <returns></returns>
  387. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  388. public NativeLibraryConfigContainer WithAutoFallback(bool enable = true)
  389. {
  390. foreach (var config in _configs)
  391. {
  392. config.WithAutoFallback(enable);
  393. }
  394. return this;
  395. }
  396. /// <summary>
  397. /// Whether to skip the check when you don't allow fallback. This option
  398. /// may be useful under some complex conditions. For example, you're sure
  399. /// you have your cublas configured but LLamaSharp take it as invalid by mistake.
  400. /// </summary>
  401. /// <param name="enable"></param>
  402. /// <returns></returns>
  403. /// <exception cref="InvalidOperationException">Thrown if `LibraryHasLoaded` is true.</exception>
  404. public NativeLibraryConfigContainer SkipCheck(bool enable = true)
  405. {
  406. foreach (var config in _configs)
  407. {
  408. config.SkipCheck(enable);
  409. }
  410. return this;
  411. }
  412. /// <summary>
  413. /// Add self-defined search directories. Note that the file stucture of the added
  414. /// directories must be the same as the default directory. Besides, the directory
  415. /// won't be used recursively.
  416. /// </summary>
  417. /// <param name="directories"></param>
  418. /// <returns></returns>
  419. public NativeLibraryConfigContainer WithSearchDirectories(IEnumerable<string> directories)
  420. {
  421. foreach (var config in _configs)
  422. {
  423. config.WithSearchDirectories(directories);
  424. }
  425. return this;
  426. }
  427. /// <summary>
  428. /// Add self-defined search directories. Note that the file stucture of the added
  429. /// directories must be the same as the default directory. Besides, the directory
  430. /// won't be used recursively.
  431. /// </summary>
  432. /// <param name="directory"></param>
  433. /// <returns></returns>
  434. public NativeLibraryConfigContainer WithSearchDirectory(string directory)
  435. {
  436. foreach (var config in _configs)
  437. {
  438. config.WithSearchDirectory(directory);
  439. }
  440. return this;
  441. }
  442. /// <summary>
  443. /// Set the policy which decides how to select the desired native libraries and order them by priority.
  444. /// By default we use <see cref="DefaultNativeLibrarySelectingPolicy"/>.
  445. /// </summary>
  446. /// <param name="policy"></param>
  447. /// <returns></returns>
  448. public NativeLibraryConfigContainer WithSelectingPolicy(INativeLibrarySelectingPolicy policy)
  449. {
  450. foreach (var config in _configs)
  451. {
  452. config.WithSelectingPolicy(policy);
  453. }
  454. return this;
  455. }
  456. #endif
  457. /// <summary>
  458. /// Set the log callback that will be used for all llama.cpp log messages
  459. /// </summary>
  460. /// <param name="callback"></param>
  461. /// <exception cref="NotImplementedException"></exception>
  462. public NativeLibraryConfigContainer WithLogCallback(NativeLogConfig.LLamaLogCallback? callback)
  463. {
  464. foreach (var config in _configs)
  465. {
  466. config.WithLogCallback(callback);
  467. }
  468. return this;
  469. }
  470. /// <summary>
  471. /// Set the log callback that will be used for all llama.cpp log messages
  472. /// </summary>
  473. /// <param name="logger"></param>
  474. /// <exception cref="NotImplementedException"></exception>
  475. public NativeLibraryConfigContainer WithLogCallback(ILogger? logger)
  476. {
  477. foreach (var config in _configs)
  478. {
  479. config.WithLogCallback(logger);
  480. }
  481. return this;
  482. }
  483. #endregion
  484. /// <summary>
  485. /// Try to load the native library with the current configurations,
  486. /// but do not actually set it to <see cref="NativeApi"/>.
  487. ///
  488. /// You can still modify the configuration after this calling but only before any call from <see cref="NativeApi"/>.
  489. /// </summary>
  490. /// <returns>Whether the running is successful.</returns>
  491. public bool DryRun(out INativeLibrary? loadedLLamaNativeLibrary, out INativeLibrary? loadedLLavaNativeLibrary)
  492. {
  493. bool success = true;
  494. foreach(var config in _configs)
  495. {
  496. success &= config.DryRun(out var loadedLibrary);
  497. if(config.NativeLibraryName == NativeLibraryName.Llama)
  498. {
  499. loadedLLamaNativeLibrary = loadedLibrary;
  500. }
  501. else if(config.NativeLibraryName == NativeLibraryName.LlavaShared)
  502. {
  503. loadedLLavaNativeLibrary = loadedLibrary;
  504. }
  505. else
  506. {
  507. throw new Exception("Unknown native library config during the dry run.");
  508. }
  509. }
  510. loadedLLamaNativeLibrary = loadedLLavaNativeLibrary = null;
  511. return success;
  512. }
  513. }
  514. /// <summary>
  515. /// The name of the native library
  516. /// </summary>
  517. public enum NativeLibraryName
  518. {
  519. /// <summary>
  520. /// The native library compiled from llama.cpp.
  521. /// </summary>
  522. Llama,
  523. /// <summary>
  524. /// The native library compiled from the LLaVA example of llama.cpp.
  525. /// </summary>
  526. LlavaShared
  527. }
  528. internal static class LibraryNameExtensions
  529. {
  530. public static string GetLibraryName(this NativeLibraryName name)
  531. {
  532. switch (name)
  533. {
  534. case NativeLibraryName.Llama:
  535. return NativeApi.libraryName;
  536. case NativeLibraryName.LlavaShared:
  537. return NativeApi.llavaLibraryName;
  538. default:
  539. throw new ArgumentOutOfRangeException(nameof(name), name, null);
  540. }
  541. }
  542. }
  543. }