LLM for Unity  v3.0.1
Create characters in Unity with LLMs!
Loading...
Searching...
No Matches
LLMUnitySetup.cs
Go to the documentation of this file.
1
3using UnityEditor;
4using System.IO;
5using UnityEngine;
6using System.Threading.Tasks;
7using System;
8using System.IO.Compression;
9using System.Collections.Generic;
10using UnityEngine.Networking;
11using System.Text.RegularExpressions;
12using UndreamAI.LlamaLib;
13
16namespace LLMUnity
17{
19 public sealed class FloatAttribute : PropertyAttribute
20 {
21 public float Min { get; private set; }
22 public float Max { get; private set; }
23
24 public FloatAttribute(float min, float max)
25 {
26 Min = min;
27 Max = max;
28 }
29 }
30 public sealed class IntAttribute : PropertyAttribute
31 {
32 public int Min { get; private set; }
33 public int Max { get; private set; }
34
35 public IntAttribute(int min, int max)
36 {
37 Min = min;
38 Max = max;
39 }
40 }
41
42 [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
43 public class DynamicRangeAttribute : PropertyAttribute
44 {
45 public readonly string minVariable;
46 public readonly string maxVariable;
47 public bool intOrFloat;
48
49 public DynamicRangeAttribute(string minVariable, string maxVariable, bool intOrFloat)
50 {
51 this.minVariable = minVariable;
52 this.maxVariable = maxVariable;
53 this.intOrFloat = intOrFloat;
54 }
55 }
56
57 public class LLMAttribute : PropertyAttribute {}
58 public class LocalRemoteAttribute : PropertyAttribute {}
59 public class RemoteAttribute : PropertyAttribute {}
60 public class LocalAttribute : PropertyAttribute {}
61 public class ModelAttribute : PropertyAttribute {}
62 public class ModelExtrasAttribute : PropertyAttribute {}
63 public class ChatAttribute : PropertyAttribute {}
64 public class LLMUnityAttribute : PropertyAttribute {}
65
66 public class AdvancedAttribute : PropertyAttribute {}
67 public class LLMAdvancedAttribute : AdvancedAttribute {}
68 public class ModelAdvancedAttribute : AdvancedAttribute {}
69 public class ChatAdvancedAttribute : AdvancedAttribute {}
70
71
72 public class LLMUnityException : Exception
73 {
74 public LLMUnityException(string message = "") : base(message) {}
75 }
76
77 [Serializable]
78 public struct StringPair
79 {
80 public string source;
81 public string target;
82 }
83
84 [Serializable]
85 public class ListStringPair
86 {
87 public List<StringPair> pairs;
88 }
90
95 public class LLMUnitySetup
96 {
97 // DON'T CHANGE! the version is autocompleted with a GitHub action
99 public static string Version = "v3.0.1";
101 public static string LlamaLibVersion = "v2.0.2";
103 public static string LlamaLibReleaseURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}";
105 public static string libraryName = $"LlamaLib-{LlamaLibVersion}";
107 public static string libraryPath = GetAssetPath(libraryName);
109 public static string LlamaLibURL = $"{LlamaLibReleaseURL}/{libraryName}.zip";
111 public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity");
113 public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models");
115 public static string LLMManagerPath = GetAssetPath("LLMManager.json");
116
118 [HideInInspector]
119 public static readonly Dictionary<string, (string, string, string)[]> modelOptions = new Dictionary<string, (string, string, string)[]>()
120 {
121 {"Large models (more than 10B)", new(string, string, string)[]
122 {
123 ("Gemma 3 12B", "https://huggingface.co/lmstudio-community/gemma-3-12b-it-GGUF/resolve/main/gemma-3-12b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
124 ("Phi 4 14B", "https://huggingface.co/bartowski/phi-4-GGUF/resolve/main/phi-4-Q4_K_M.gguf", null),
125 ("Qwen 3 14B", "https://huggingface.co/unsloth/Qwen3-14B-GGUF/resolve/main/Qwen3-14B-Q4_K_M.gguf", null),
126 ("DeepSeek R1 Distill Qwen 14B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-14B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf", null),
127 }},
128 {"Medium models (up to 10B)", new(string, string, string)[]
129 {
130 ("Llama 3.1 8B", "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", "https://huggingface.co/meta-llama/Meta-Llama-3.1-8B/blob/main/LICENSE"),
131 ("Qwen 3 8B", "https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf", null),
132 ("DeepSeek R1 Distill Llama 8B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Llama-8B-GGUF/resolve/main/DeepSeek-R1-Distill-Llama-8B-Q4_K_M.gguf", null),
133 ("DeepSeek R1 Distill Qwen 7B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-7B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-7B-Q4_K_M.gguf", null),
134 ("Gemma 2 9B it", "https://huggingface.co/bartowski/gemma-2-9b-it-GGUF/resolve/main/gemma-2-9b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
135 ("Mistral 7B Instruct v0.2", "https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf", null),
136 ("OpenHermes 2.5 7B", "https://huggingface.co/TheBloke/OpenHermes-2.5-Mistral-7B-GGUF/resolve/main/openhermes-2.5-mistral-7b.Q4_K_M.gguf", null),
137 }},
138 {"Small models (up to 5B)", new(string, string, string)[]
139 {
140 ("Llama 3.2 3B", "https://huggingface.co/hugging-quants/Llama-3.2-3B-Instruct-Q4_K_M-GGUF/resolve/main/llama-3.2-3b-instruct-q4_k_m.gguf", "https://huggingface.co/meta-llama/Llama-3.2-1B/blob/main/LICENSE.txt"),
141 ("Gemma 3 4B", "https://huggingface.co/lmstudio-community/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
142 ("Phi 4 4B", "https://huggingface.co/bartowski/microsoft_Phi-4-mini-instruct-GGUF/resolve/main/microsoft_Phi-4-mini-instruct-Q4_K_M.gguf", null),
143 ("Qwen 3 4B", "https://huggingface.co/unsloth/Qwen3-4B-GGUF/resolve/main/Qwen3-4B-Q4_K_M.gguf", null),
144 }},
145 {"Tiny models (up to 2B)", new(string, string, string)[]
146 {
147 ("Llama 3.2 1B", "https://huggingface.co/hugging-quants/Llama-3.2-1B-Instruct-Q4_K_M-GGUF/resolve/main/llama-3.2-1b-instruct-q4_k_m.gguf", "https://huggingface.co/meta-llama/Llama-3.2-1B/blob/main/LICENSE.txt"),
148 ("Gemma 3 1B", "https://huggingface.co/lmstudio-community/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q4_K_M.gguf", "https://ai.google.dev/gemma/terms"),
149 ("Qwen 3 1.7B", "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf", null),
150 ("Qwen 3 0.6B", "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_K_M.gguf", null),
151 ("DeepSeek R1 Distill Qwen 1.5B", "https://huggingface.co/lmstudio-community/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q4_K_M.gguf", null),
152 }},
153 {"RAG models", new(string, string, string)[]
154 {
155 ("All MiniLM L12 v2", "https://huggingface.co/leliuga/all-MiniLM-L12-v2-GGUF/resolve/main/all-MiniLM-L12-v2.Q4_K_M.gguf", null),
156 ("BGE large en v1.5", "https://huggingface.co/CompendiumLabs/bge-large-en-v1.5-gguf/resolve/main/bge-large-en-v1.5-q4_k_m.gguf", null),
157 ("BGE base en v1.5", "https://huggingface.co/CompendiumLabs/bge-base-en-v1.5-gguf/resolve/main/bge-base-en-v1.5-q4_k_m.gguf", null),
158 ("BGE small en v1.5", "https://huggingface.co/CompendiumLabs/bge-small-en-v1.5-gguf/resolve/main/bge-small-en-v1.5-q4_k_m.gguf", null),
159 }},
160 };
161
163 [LLMUnity] public static DebugModeType DebugMode = DebugModeType.All;
164 static string DebugModeKey = "DebugMode";
165 public static bool CUBLAS = false;
166 static string CUBLASKey = "CUBLAS";
167 static List<Action<string>> errorCallbacks = new List<Action<string>>();
168 static readonly object lockObject = new object();
169 static Dictionary<string, Task> androidExtractTasks = new Dictionary<string, Task>();
170
171 public enum DebugModeType
172 {
173 Debug,
174 All,
175 Warning,
176 Error,
177 None
178 }
179
180 public static void Log(string message)
181 {
182 if ((int)DebugMode > (int)DebugModeType.All) return;
183 Debug.Log(message);
184 }
185
186 public static void LogWarning(string message)
187 {
188 if ((int)DebugMode > (int)DebugModeType.Warning) return;
189 Debug.LogWarning(message);
190 }
191
192 public static void LogError(string message, bool throwException = false)
193 {
194 if ((int)DebugMode > (int)DebugModeType.Error) return;
195 Debug.LogError(message);
196 foreach (Action<string> errorCallback in errorCallbacks) errorCallback(message);
197 if (throwException) throw new LLMUnityException(message);
198 }
199
200 static void LoadPlayerPrefs()
201 {
202 DebugMode = (DebugModeType)PlayerPrefs.GetInt(DebugModeKey, (int)DebugModeType.All);
203 CUBLAS = PlayerPrefs.GetInt(CUBLASKey, 0) == 1;
204 }
205
206 public static void SetDebugMode(DebugModeType newDebugMode)
207 {
208 if (DebugMode == newDebugMode) return;
209 DebugMode = newDebugMode;
210 PlayerPrefs.SetInt(DebugModeKey, (int)DebugMode);
211 PlayerPrefs.Save();
212 }
213
214#if UNITY_EDITOR
215 public static void SetCUBLAS(bool value)
216 {
217 if (CUBLAS == value) return;
218 CUBLAS = value;
219 PlayerPrefs.SetInt(CUBLASKey, value ? 1 : 0);
220 PlayerPrefs.Save();
221 }
222
223#endif
224
225 public static string GetAssetPath(string relPath = "")
226 {
227 string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath;
228 return Path.Combine(assetsDir, relPath).Replace('\\', '/');
229 }
230
231 public static string GetDownloadAssetPath(string relPath = "")
232 {
233 string assetsDir = Application.streamingAssetsPath;
234
235 bool isVisionOS = false;
236#if UNITY_2022_3_OR_NEWER
237 isVisionOS = Application.platform == RuntimePlatform.VisionOS;
238#endif
239 if (Application.platform == RuntimePlatform.Android || Application.platform == RuntimePlatform.IPhonePlayer || isVisionOS)
240 {
241 assetsDir = Application.persistentDataPath;
242 }
243 return Path.Combine(assetsDir, relPath).Replace('\\', '/');
244 }
245
246 static void InitializeOnLoadCommon()
247 {
248#if UNITY_EDITOR || !((UNITY_ANDROID || UNITY_IOS || UNITY_VISIONOS))
249 LlamaLib.baseLibraryPath = Path.Combine(libraryPath, LlamaLib.GetPlatform(), "native");
250#endif
251 }
252
253#if UNITY_EDITOR
254 [InitializeOnLoadMethod]
255 static async Task InitializeOnLoad()
256 {
257 LoadPlayerPrefs();
258 LlamaLib.libraryExclusion = new List<string>(){CUBLAS ? "tinyblas" : "cublas"};
259 InitializeOnLoadCommon();
260 await DownloadLibrary();
261 }
262
263#else
264 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
265 static void InitializeOnLoad()
266 {
267 InitializeOnLoadCommon();
268 }
269
270#endif
271
272 static Dictionary<string, ResumingWebClient> downloadClients = new Dictionary<string, ResumingWebClient>();
273
274 public static void CancelDownload(string savePath)
275 {
276 if (!downloadClients.ContainsKey(savePath)) return;
277 downloadClients[savePath].CancelDownloadAsync();
278 downloadClients.Remove(savePath);
279 }
280
281 public static async Task DownloadFile(
282 string fileUrl, string savePath, bool overwrite = false,
283 Action<string> callback = null, Action<float> progressCallback = null
284 )
285 {
286 if (File.Exists(savePath) && !overwrite)
287 {
288 Log($"File already exists at: {savePath}");
289 }
290 else
291 {
292 Log($"Downloading {fileUrl} to {savePath}...");
293 string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath));
294
295 ResumingWebClient client = new ResumingWebClient();
296 downloadClients[savePath] = client;
297 await client.DownloadFileTaskAsyncResume(new Uri(fileUrl), tmpPath, !overwrite, progressCallback);
298 downloadClients.Remove(savePath);
299#if UNITY_EDITOR
300 AssetDatabase.StartAssetEditing();
301#endif
302 Directory.CreateDirectory(Path.GetDirectoryName(savePath));
303 File.Move(tmpPath, savePath);
304#if UNITY_EDITOR
305 AssetDatabase.StopAssetEditing();
306#endif
307 Log($"Download complete!");
308 }
309
310 progressCallback?.Invoke(1f);
311 callback?.Invoke(savePath);
312 }
313
314 public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024 * 1024)
315 {
316 Task extractionTask;
317 lock (lockObject)
318 {
319 if (!androidExtractTasks.TryGetValue(assetName, out extractionTask))
320 {
321#if !UNITY_EDITOR && UNITY_ANDROID
322 extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize);
323#else
324 extractionTask = Task.CompletedTask;
325#endif
326 androidExtractTasks[assetName] = extractionTask;
327 }
328 }
329 await extractionTask;
330 }
331
332 public static async Task AndroidExtractFileOnce(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024 * 1024)
333 {
334 string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName;
335 string target = GetAssetPath(assetName);
336 if (!overwrite && File.Exists(target))
337 {
338 if (log) Log($"File {target} already exists");
339 return;
340 }
341
342 Log($"Extracting {source} to {target}");
343
344 // UnityWebRequest to read the file from StreamingAssets
345 UnityWebRequest www = UnityWebRequest.Get(source);
346 // Send the request and await its completion
347 var operation = www.SendWebRequest();
348
349 while (!operation.isDone) await Task.Delay(1);
350 if (www.result != UnityWebRequest.Result.Success)
351 {
352 LogError("Failed to load file from StreamingAssets: " + www.error);
353 }
354 else
355 {
356 byte[] buffer = new byte[chunkSize];
357 using (Stream responseStream = new MemoryStream(www.downloadHandler.data))
358 using (FileStream fileStream = new FileStream(target, FileMode.Create, FileAccess.Write))
359 {
360 int bytesRead;
361 while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
362 {
363 await fileStream.WriteAsync(buffer, 0, bytesRead);
364 }
365 }
366 }
367 }
368
369 public static async Task AndroidExtractAsset(string path, bool overwrite = false)
370 {
371 if (Application.platform != RuntimePlatform.Android) return;
372 await AndroidExtractFile(Path.GetFileName(path), overwrite);
373 }
374
375 public static string GetFullPath(string path)
376 {
377 return Path.GetFullPath(path).Replace('\\', '/');
378 }
379
380 public static bool IsSubPath(string childPath, string parentPath)
381 {
382 return GetFullPath(childPath).StartsWith(GetFullPath(parentPath), StringComparison.OrdinalIgnoreCase);
383 }
384
385 public static string RelativePath(string fullPath, string basePath)
386 {
387 // Get the full paths and replace backslashes with forward slashes (or vice versa)
388 string fullParentPath = GetFullPath(basePath).TrimEnd('/');
389 string fullChildPath = GetFullPath(fullPath);
390
391 string relativePath = fullChildPath;
392 if (fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase))
393 {
394 relativePath = fullChildPath.Substring(fullParentPath.Length);
395 while (relativePath.StartsWith("/")) relativePath = relativePath.Substring(1);
396 }
397 return relativePath;
398 }
399
400 public static string SearchDirectory(string directory, string targetFileName)
401 {
402 string[] files = Directory.GetFiles(directory, targetFileName);
403 if (files.Length > 0) return files[0];
404 string[] subdirectories = Directory.GetDirectories(directory);
405 foreach (var subdirectory in subdirectories)
406 {
407 string result = SearchDirectory(subdirectory, targetFileName);
408 if (result != null) return result;
409 }
410 return null;
411 }
412
413#if UNITY_EDITOR
414
415 [HideInInspector] public static float libraryProgress = 1;
416
417 public static void CreateEmptyFile(string path)
418 {
419 File.Create(path).Dispose();
420 }
421
422 static void ExtractInsideDirectory(string zipPath, string extractPath, string prefix = "", bool overwrite = true)
423 {
424 using (ZipArchive archive = ZipFile.OpenRead(zipPath))
425 {
426 foreach (ZipArchiveEntry entry in archive.Entries)
427 {
428 if (string.IsNullOrEmpty(entry.Name))
429 continue; // Skip directories
430
431 string destinationPath;
432 if (!String.IsNullOrEmpty(prefix))
433 {
434 string normalizedPath = entry.FullName.Replace('\\', '/');
435 if (!normalizedPath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
436 continue;
437 destinationPath = Path.Combine(extractPath, normalizedPath.Substring(prefix.Length));
438 }
439 else
440 {
441 destinationPath = Path.Combine(extractPath, entry.FullName);
442 }
443
444 Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
445 entry.ExtractToFile(destinationPath, overwrite);
446 }
447 }
448 }
449
450 static async Task DownloadAndExtractInsideDirectory(string url, string path, string setupDir)
451 {
452 string urlName = Path.GetFileName(url);
453 string setupFile = Path.Combine(setupDir, urlName + ".complete");
454 if (File.Exists(setupFile)) return;
455
456 string zipPath = Path.Combine(Application.temporaryCachePath, urlName);
457 await DownloadFile(url, zipPath, true, null, SetLibraryProgress);
458
459 AssetDatabase.StartAssetEditing();
460 ExtractInsideDirectory(zipPath, path, $"{libraryName}/runtimes/");
461 CreateEmptyFile(setupFile);
462 AssetDatabase.StopAssetEditing();
463
464 File.Delete(zipPath);
465 }
466
467 static void DeleteEarlierVersions()
468 {
469 List<string> assetPathSubDirs = new List<string>();
470 foreach (string dir in new string[] { GetAssetPath(), Path.Combine(Application.dataPath, "Plugins", "Android") })
471 {
472 if (Directory.Exists(dir)) assetPathSubDirs.AddRange(Directory.GetDirectories(dir));
473 }
474
475 List<Regex> versionRegexes = new List<Regex> { new Regex("undreamai-(.+)-llamacpp"), new Regex("LlamaLib-(.+)") };
476 foreach (string assetPathSubDir in assetPathSubDirs)
477 {
478 foreach (Regex regex in versionRegexes)
479 {
480 Match match = regex.Match(Path.GetFileName(assetPathSubDir));
481 if (match.Success)
482 {
483 string version = match.Groups[1].Value;
484 if (version != LlamaLibVersion)
485 {
486 Debug.Log($"Deleting other LLMUnity version folder: {assetPathSubDir}");
487 Directory.Delete(assetPathSubDir, true);
488 if (File.Exists(assetPathSubDir + ".meta")) File.Delete(assetPathSubDir + ".meta");
489 }
490 }
491 }
492 }
493 }
494
495 static async Task DownloadLibrary()
496 {
497 if (libraryProgress < 1) return;
498 libraryProgress = 0;
499
500 try
501 {
502 DeleteEarlierVersions();
503
504 string setupDir = Path.Combine(libraryPath, "setup");
505 Directory.CreateDirectory(setupDir);
506
507 // setup LlamaLib in StreamingAssets
508 await DownloadAndExtractInsideDirectory(LlamaLibURL, libraryPath, setupDir);
509 }
510 catch (Exception e)
511 {
512 LogError(e.Message);
513 }
514
515 libraryProgress = 1;
516 }
517
518 private static void SetLibraryProgress(float progress)
519 {
520 libraryProgress = Math.Min(0.99f, progress);
521 }
522
523 public static string AddAsset(string assetPath)
524 {
525 if (!File.Exists(assetPath))
526 {
527 LogError($"{assetPath} does not exist!");
528 return null;
529 }
530 string assetDir = GetAssetPath();
531 if (IsSubPath(assetPath, assetDir)) return RelativePath(assetPath, assetDir);
532
533 string filename = Path.GetFileName(assetPath);
534 string fullPath = GetAssetPath(filename);
535 AssetDatabase.StartAssetEditing();
536 foreach (string path in new string[] { fullPath, fullPath + ".meta" })
537 {
538 if (File.Exists(path)) File.Delete(path);
539 }
540 File.Copy(assetPath, fullPath);
541 AssetDatabase.StopAssetEditing();
542 return filename;
543 }
544
545#endif
547
549 public static void AddErrorCallBack(Action<string> callback)
550 {
551 errorCallbacks.Add(callback);
552 }
553
555 public static void RemoveErrorCallBack(Action<string> callback)
556 {
557 errorCallbacks.Remove(callback);
558 }
559
561 public static void ClearErrorCallBacks()
562 {
563 errorCallbacks.Clear();
564 }
565
566 public static int GetMaxFreqKHz(int cpuId)
567 {
568 string[] paths = new string[]
569 {
570 $"/sys/devices/system/cpu/cpufreq/stats/cpu{cpuId}/time_in_state",
571 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/stats/time_in_state",
572 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/cpuinfo_max_freq"
573 };
574
575 foreach (var path in paths)
576 {
577 if (!File.Exists(path)) continue;
578
579 int maxFreqKHz = 0;
580 using (StreamReader sr = new StreamReader(path))
581 {
582 string line;
583 while ((line = sr.ReadLine()) != null)
584 {
585 string[] parts = line.Split(' ');
586 if (parts.Length > 0 && int.TryParse(parts[0], out int freqKHz))
587 {
588 if (freqKHz > maxFreqKHz)
589 {
590 maxFreqKHz = freqKHz;
591 }
592 }
593 }
594 }
595 if (maxFreqKHz != 0) return maxFreqKHz;
596 }
597 return -1;
598 }
599
600 public static bool IsSmtCpu(int cpuId)
601 {
602 string[] paths = new string[]
603 {
604 $"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list",
605 $"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list"
606 };
607
608 foreach (var path in paths)
609 {
610 if (!File.Exists(path)) continue;
611 using (StreamReader sr = new StreamReader(path))
612 {
613 string line;
614 while ((line = sr.ReadLine()) != null)
615 {
616 if (line.Contains(",") || line.Contains("-"))
617 {
618 return true;
619 }
620 }
621 }
622 }
623 return false;
624 }
625
630 public static int AndroidGetNumBigCores()
631 {
632 int maxFreqKHzMin = int.MaxValue;
633 int maxFreqKHzMax = 0;
634 List<int> cpuMaxFreqKHz = new List<int>();
635 List<bool> cpuIsSmtCpu = new List<bool>();
636
637 try
638 {
639 string cpuPath = "/sys/devices/system/cpu/";
640 int coreIndex;
641 if (Directory.Exists(cpuPath))
642 {
643 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
644 {
645 string dirName = Path.GetFileName(cpuDir);
646 if (!dirName.StartsWith("cpu")) continue;
647 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
648
649 int maxFreqKHz = GetMaxFreqKHz(coreIndex);
650 cpuMaxFreqKHz.Add(maxFreqKHz);
651 if (maxFreqKHz > maxFreqKHzMax) maxFreqKHzMax = maxFreqKHz;
652 if (maxFreqKHz < maxFreqKHzMin) maxFreqKHzMin = maxFreqKHz;
653 cpuIsSmtCpu.Add(IsSmtCpu(coreIndex));
654 }
655 }
656 }
657 catch (Exception e)
658 {
659 LogError(e.Message);
660 }
661
662 int numBigCores = 0;
663 int numCores = SystemInfo.processorCount;
664 int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2;
665 if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores;
666 else
667 {
668 for (int i = 0; i < cpuMaxFreqKHz.Count; i++)
669 {
670 if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++;
671 }
672 }
673
674 if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2;
675 else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount);
676
677 return numBigCores;
678 }
679
685 {
686 List<int> capacities = new List<int>();
687 int minCapacity = int.MaxValue;
688 try
689 {
690 string cpuPath = "/sys/devices/system/cpu/";
691 int coreIndex;
692 if (Directory.Exists(cpuPath))
693 {
694 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
695 {
696 string dirName = Path.GetFileName(cpuDir);
697 if (!dirName.StartsWith("cpu")) continue;
698 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
699
700 string capacityPath = Path.Combine(cpuDir, "cpu_capacity");
701 if (!File.Exists(capacityPath)) break;
702
703 int capacity = int.Parse(File.ReadAllText(capacityPath).Trim());
704 capacities.Add(capacity);
705 if (minCapacity > capacity) minCapacity = capacity;
706 }
707 }
708 }
709 catch (Exception e)
710 {
711 LogError(e.Message);
712 }
713
714 int numBigCores = 0;
715 foreach (int capacity in capacities)
716 {
717 if (capacity >= 2 * minCapacity) numBigCores++;
718 }
719
720 if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount;
721 return numBigCores;
722 }
723 }
724}
Class implementing helper functions for setup and process management.
static int AndroidGetNumBigCoresCapacity()
Calculates the number of big cores in Android similarly to Unity (https://docs.unity3d....
static string LLMManagerPath
Path of file with build information for runtime.
static string LlamaLibReleaseURL
LlamaLib release url.
static string libraryPath
LlamaLib path.
static void AddErrorCallBack(Action< string > callback)
Add callback function to call for error logs.
static void RemoveErrorCallBack(Action< string > callback)
Remove callback function added for error logs.
static void ClearErrorCallBacks()
Remove all callback function added for error logs.
static string modelDownloadPath
Model download path.
static string LlamaLibVersion
LlamaLib version.
static int AndroidGetNumBigCores()
Calculates the number of big cores in Android similarly to ncnn (https://github.com/Tencent/ncnn)
static string Version
LLM for Unity version.
static string libraryName
LlamaLib name.
static readonly Dictionary< string,(string, string, string)[]> modelOptions
Default models for download.
static string LLMUnityStore
LLMnity store path.
static string LlamaLibURL
LlamaLib url.