LLM for Unity  v2.2.5
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;
11
15namespace LLMUnity
16{
18 public sealed class FloatAttribute : PropertyAttribute
19 {
20 public float Min { get; private set; }
21 public float Max { get; private set; }
22
23 public FloatAttribute(float min, float max)
24 {
25 Min = min;
26 Max = max;
27 }
28 }
29 public sealed class IntAttribute : PropertyAttribute
30 {
31 public int Min { get; private set; }
32 public int Max { get; private set; }
33
34 public IntAttribute(int min, int max)
35 {
36 Min = min;
37 Max = max;
38 }
39 }
40
41 [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
42 public class DynamicRangeAttribute : PropertyAttribute
43 {
44 public readonly string minVariable;
45 public readonly string maxVariable;
46 public bool intOrFloat;
47
48 public DynamicRangeAttribute(string minVariable, string maxVariable, bool intOrFloat)
49 {
50 this.minVariable = minVariable;
51 this.maxVariable = maxVariable;
52 this.intOrFloat = intOrFloat;
53 }
54 }
55
56 public class LLMAttribute : PropertyAttribute {}
57 public class LLMAdvancedAttribute : PropertyAttribute {}
58 public class LocalRemoteAttribute : PropertyAttribute {}
59 public class RemoteAttribute : PropertyAttribute {}
60 public class LocalAttribute : PropertyAttribute {}
61 public class ModelAttribute : PropertyAttribute {}
62 public class ModelDownloadAttribute : ModelAttribute {}
63 public class ModelDownloadAdvancedAttribute : ModelAdvancedAttribute {}
64 public class ModelAdvancedAttribute : PropertyAttribute {}
65 public class ModelExtrasAttribute : PropertyAttribute {}
66 public class ChatAttribute : PropertyAttribute {}
67 public class ChatAdvancedAttribute : PropertyAttribute {}
68 public class LLMUnityAttribute : PropertyAttribute {}
69
70 public class NotImplementedException : Exception
71 {
72 public NotImplementedException() : base("The method needs to be implemented by subclasses.") {}
73 }
74
75 public delegate void EmptyCallback();
76 public delegate void Callback<T>(T message);
77 public delegate Task TaskCallback<T>(T message);
78 public delegate T2 ContentCallback<T, T2>(T message);
79 public delegate void ActionCallback(string source, string target);
80
81 [Serializable]
82 public struct StringPair
83 {
84 public string source;
85 public string target;
86 }
87
88 [Serializable]
89 public class ListStringPair
90 {
91 public List<StringPair> pairs;
92 }
94
99 public class LLMUnitySetup
100 {
101 // DON'T CHANGE! the version is autocompleted with a GitHub action
103 public static string Version = "v2.2.5";
105 public static string LlamaLibVersion = "v1.1.12";
107 public static string LlamaLibReleaseURL = $"https://github.com/undreamai/LlamaLib/releases/download/{LlamaLibVersion}";
109 public static string LlamaLibURL = $"{LlamaLibReleaseURL}/undreamai-{LlamaLibVersion}-llamacpp.zip";
111 public static string LlamaLibExtensionURL = $"{LlamaLibReleaseURL}/undreamai-{LlamaLibVersion}-llamacpp-full.zip";
113 public static string libraryPath = GetAssetPath(Path.GetFileName(LlamaLibURL).Replace(".zip", ""));
115 public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LLMUnity");
117 public static string modelDownloadPath = Path.Combine(LLMUnityStore, "models");
119 public static string LLMManagerPath = GetAssetPath("LLMManager.json");
120
122 [HideInInspector] public static readonly Dictionary<string, (string, string, string)[]> modelOptions = new Dictionary<string, (string, string, string)[]>()
123 {
124 {"Medium models", new(string, string, string)[]
125 {
126 ("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?download=true", "https://huggingface.co/meta-llama/Meta-Llama-3.1-8B/blob/main/LICENSE"),
127 ("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?download=true", null),
128 ("Gemma 2 9B it", "https://huggingface.co/bartowski/gemma-2-9b-it-GGUF/resolve/main/gemma-2-9b-it-Q4_K_M.gguf?download=true", "https://ai.google.dev/gemma/terms"),
129 ("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?download=true", null),
130 }},
131 {"Small models", new(string, string, string)[]
132 {
133 ("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", null),
134 ("Phi 3.5 4B", "https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF/resolve/main/Phi-3.5-mini-instruct-Q4_K_M.gguf", null),
135 }},
136 {"Tiny models", new(string, string, string)[]
137 {
138 ("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", null),
139 ("Qwen 2 0.5B", "https://huggingface.co/Qwen/Qwen2-0.5B-Instruct-GGUF/resolve/main/qwen2-0_5b-instruct-q4_k_m.gguf?download=true", null),
140 }},
141 };
142
144 [LLMUnity] public static DebugModeType DebugMode = DebugModeType.All;
145 static string DebugModeKey = "DebugMode";
146 public static bool FullLlamaLib = false;
147 static string FullLlamaLibKey = "FullLlamaLib";
148 static List<Callback<string>> errorCallbacks = new List<Callback<string>>();
149 static readonly object lockObject = new object();
150 static Dictionary<string, Task> androidExtractTasks = new Dictionary<string, Task>();
151
152 public enum DebugModeType
153 {
154 All,
155 Warning,
156 Error,
157 None
158 }
159
160 public static void Log(string message)
161 {
162 if ((int)DebugMode > (int)DebugModeType.All) return;
163 Debug.Log(message);
164 }
165
166 public static void LogWarning(string message)
167 {
168 if ((int)DebugMode > (int)DebugModeType.Warning) return;
169 Debug.LogWarning(message);
170 }
171
172 public static void LogError(string message)
173 {
174 if ((int)DebugMode > (int)DebugModeType.Error) return;
175 Debug.LogError(message);
176 foreach (Callback<string> errorCallback in errorCallbacks) errorCallback(message);
177 }
178
179 static void LoadPlayerPrefs()
180 {
181 DebugMode = (DebugModeType)PlayerPrefs.GetInt(DebugModeKey, (int)DebugModeType.All);
182 FullLlamaLib = PlayerPrefs.GetInt(FullLlamaLibKey, 0) == 1;
183 }
184
185 public static void SetDebugMode(DebugModeType newDebugMode)
186 {
187 if (DebugMode == newDebugMode) return;
188 DebugMode = newDebugMode;
189 PlayerPrefs.SetInt(DebugModeKey, (int)DebugMode);
190 PlayerPrefs.Save();
191 }
192
193#if UNITY_EDITOR
194 public static void SetFullLlamaLib(bool value)
195 {
196 if (FullLlamaLib == value) return;
197 FullLlamaLib = value;
198 PlayerPrefs.SetInt(FullLlamaLibKey, value ? 1 : 0);
199 PlayerPrefs.Save();
200 _ = DownloadLibrary();
201 }
202
203#endif
204
205 public static string GetAssetPath(string relPath = "")
206 {
207 // Path to store llm server binaries and models
208 string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath;
209 return Path.Combine(assetsDir, relPath).Replace('\\', '/');
210 }
211
212#if UNITY_EDITOR
213 [InitializeOnLoadMethod]
214 static async Task InitializeOnLoad()
215 {
216 LoadPlayerPrefs();
217 await DownloadLibrary();
218 }
219
220#else
221 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
222 void InitializeOnLoad()
223 {
224 LoadPlayerPrefs();
225 }
226
227#endif
228
229 static Dictionary<string, ResumingWebClient> downloadClients = new Dictionary<string, ResumingWebClient>();
230
231 public static void CancelDownload(string savePath)
232 {
233 if (!downloadClients.ContainsKey(savePath)) return;
234 downloadClients[savePath].CancelDownloadAsync();
235 downloadClients.Remove(savePath);
236 }
237
238 public static async Task DownloadFile(
239 string fileUrl, string savePath, bool overwrite = false,
240 Callback<string> callback = null, Callback<float> progressCallback = null
241 )
242 {
243 if (File.Exists(savePath) && !overwrite)
244 {
245 Log($"File already exists at: {savePath}");
246 }
247 else
248 {
249 Log($"Downloading {fileUrl} to {savePath}...");
250 string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath));
251
252 ResumingWebClient client = new ResumingWebClient();
253 downloadClients[savePath] = client;
254 await client.DownloadFileTaskAsyncResume(new Uri(fileUrl), tmpPath, !overwrite, progressCallback);
255 downloadClients.Remove(savePath);
256#if UNITY_EDITOR
257 AssetDatabase.StartAssetEditing();
258#endif
259 Directory.CreateDirectory(Path.GetDirectoryName(savePath));
260 File.Move(tmpPath, savePath);
261#if UNITY_EDITOR
262 AssetDatabase.StopAssetEditing();
263#endif
264 Log($"Download complete!");
265 }
266
267 progressCallback?.Invoke(1f);
268 callback?.Invoke(savePath);
269 }
270
271 public static async Task AndroidExtractFile(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024)
272 {
273 Task extractionTask;
274 lock (lockObject)
275 {
276 if (!androidExtractTasks.TryGetValue(assetName, out extractionTask))
277 {
278 extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize);
279 androidExtractTasks[assetName] = extractionTask;
280 }
281 }
282 await extractionTask;
283 }
284
285 public static async Task AndroidExtractFileOnce(string assetName, bool overwrite = false, bool log = true, int chunkSize = 1024*1024)
286 {
287 string source = "jar:file://" + Application.dataPath + "!/assets/" + assetName;
288 string target = GetAssetPath(assetName);
289 if (!overwrite && File.Exists(target))
290 {
291 if (log) Log($"File {target} already exists");
292 return;
293 }
294
295 Log($"Extracting {source} to {target}");
296
297 // UnityWebRequest to read the file from StreamingAssets
298 UnityWebRequest www = UnityWebRequest.Get(source);
299 // Send the request and await its completion
300 var operation = www.SendWebRequest();
301
302 while (!operation.isDone) await Task.Delay(1);
303 if (www.result != UnityWebRequest.Result.Success)
304 {
305 LogError("Failed to load file from StreamingAssets: " + www.error);
306 }
307 else
308 {
309 byte[] buffer = new byte[chunkSize];
310 using (Stream responseStream = new MemoryStream(www.downloadHandler.data))
311 using (FileStream fileStream = new FileStream(target, FileMode.Create, FileAccess.Write))
312 {
313 int bytesRead;
314 while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
315 {
316 await fileStream.WriteAsync(buffer, 0, bytesRead);
317 }
318 }
319 }
320 }
321
322 public static async Task AndroidExtractAsset(string path, bool overwrite = false)
323 {
324 if (Application.platform != RuntimePlatform.Android) return;
325 await AndroidExtractFile(Path.GetFileName(path), overwrite);
326 }
327
328 public static string GetFullPath(string path)
329 {
330 return Path.GetFullPath(path).Replace('\\', '/');
331 }
332
333 public static bool IsSubPath(string childPath, string parentPath)
334 {
335 return GetFullPath(childPath).StartsWith(GetFullPath(parentPath), StringComparison.OrdinalIgnoreCase);
336 }
337
338 public static string RelativePath(string fullPath, string basePath)
339 {
340 // Get the full paths and replace backslashes with forward slashes (or vice versa)
341 string fullParentPath = GetFullPath(basePath).TrimEnd('/');
342 string fullChildPath = GetFullPath(fullPath);
343
344 string relativePath = fullChildPath;
345 if (fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase))
346 {
347 relativePath = fullChildPath.Substring(fullParentPath.Length);
348 while (relativePath.StartsWith("/")) relativePath = relativePath.Substring(1);
349 }
350 return relativePath;
351 }
352
353#if UNITY_EDITOR
354
355 [HideInInspector] public static float libraryProgress = 1;
356
357 public static void CreateEmptyFile(string path)
358 {
359 File.Create(path).Dispose();
360 }
361
362 static void ExtractInsideDirectory(string zipPath, string extractPath, bool overwrite = true)
363 {
364 using (ZipArchive archive = ZipFile.OpenRead(zipPath))
365 {
366 foreach (ZipArchiveEntry entry in archive.Entries)
367 {
368 if (string.IsNullOrEmpty(entry.Name)) continue;
369 string destinationPath = Path.Combine(extractPath, entry.FullName);
370 Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
371 entry.ExtractToFile(destinationPath, overwrite);
372 }
373 }
374 }
375
376 static async Task DownloadAndExtractInsideDirectory(string url, string path, string setupDir)
377 {
378 string urlName = Path.GetFileName(url);
379 string setupFile = Path.Combine(setupDir, urlName + ".complete");
380 if (File.Exists(setupFile)) return;
381
382 string zipPath = Path.Combine(Application.temporaryCachePath, urlName);
383 await DownloadFile(url, zipPath, true, null, SetLibraryProgress);
384
385 AssetDatabase.StartAssetEditing();
386 ExtractInsideDirectory(zipPath, path);
387 CreateEmptyFile(setupFile);
388 AssetDatabase.StopAssetEditing();
389
390 File.Delete(zipPath);
391 }
392
393 static async Task DownloadLibrary()
394 {
395 if (libraryProgress < 1) return;
396 libraryProgress = 0;
397
398 try
399 {
400 string setupDir = Path.Combine(libraryPath, "setup");
401 Directory.CreateDirectory(setupDir);
402
403 // setup LlamaLib in StreamingAssets
404 await DownloadAndExtractInsideDirectory(LlamaLibURL, libraryPath, setupDir);
405
406 // setup LlamaLib in Plugins for Android
407 AssetDatabase.StartAssetEditing();
408 string androidDir = Path.Combine(libraryPath, "android");
409 if (Directory.Exists(androidDir))
410 {
411 string androidPluginsDir = Path.Combine(Application.dataPath, "Plugins", "Android");
412 Directory.CreateDirectory(androidPluginsDir);
413 string pluginDir = Path.Combine(androidPluginsDir, Path.GetFileName(libraryPath));
414 if (Directory.Exists(pluginDir)) Directory.Delete(pluginDir, true);
415 Directory.Move(androidDir, pluginDir);
416 if (File.Exists(androidDir + ".meta")) File.Delete(androidDir + ".meta");
417 }
418 AssetDatabase.StopAssetEditing();
419
420 // setup LlamaLib extras in StreamingAssets
421 if (FullLlamaLib) await DownloadAndExtractInsideDirectory(LlamaLibExtensionURL, libraryPath, setupDir);
422 }
423 catch (Exception e)
424 {
425 LogError(e.Message);
426 }
427
428 libraryProgress = 1;
429 }
430
431 private static void SetLibraryProgress(float progress)
432 {
433 libraryProgress = Math.Min(0.99f, progress);
434 }
435
436 public static string AddAsset(string assetPath)
437 {
438 if (!File.Exists(assetPath))
439 {
440 LogError($"{assetPath} does not exist!");
441 return null;
442 }
443 string assetDir = GetAssetPath();
444 if (IsSubPath(assetPath, assetDir)) return RelativePath(assetPath, assetDir);
445
446 string filename = Path.GetFileName(assetPath);
447 string fullPath = GetAssetPath(filename);
448 AssetDatabase.StartAssetEditing();
449 foreach (string path in new string[] {fullPath, fullPath + ".meta"})
450 {
451 if (File.Exists(path)) File.Delete(path);
452 }
453 File.Copy(assetPath, fullPath);
454 AssetDatabase.StopAssetEditing();
455 return filename;
456 }
457
458#endif
460
462 public static void AddErrorCallBack(Callback<string> callback)
463 {
464 errorCallbacks.Add(callback);
465 }
466
468 public static void RemoveErrorCallBack(Callback<string> callback)
469 {
470 errorCallbacks.Remove(callback);
471 }
472
474 public static void ClearErrorCallBacks()
475 {
476 errorCallbacks.Clear();
477 }
478
479 public static int GetMaxFreqKHz(int cpuId)
480 {
481 string[] paths = new string[]
482 {
483 $"/sys/devices/system/cpu/cpufreq/stats/cpu{cpuId}/time_in_state",
484 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/stats/time_in_state",
485 $"/sys/devices/system/cpu/cpu{cpuId}/cpufreq/cpuinfo_max_freq"
486 };
487
488 foreach (var path in paths)
489 {
490 if (!File.Exists(path)) continue;
491
492 int maxFreqKHz = 0;
493 using (StreamReader sr = new StreamReader(path))
494 {
495 string line;
496 while ((line = sr.ReadLine()) != null)
497 {
498 string[] parts = line.Split(' ');
499 if (parts.Length > 0 && int.TryParse(parts[0], out int freqKHz))
500 {
501 if (freqKHz > maxFreqKHz)
502 {
503 maxFreqKHz = freqKHz;
504 }
505 }
506 }
507 }
508 if (maxFreqKHz != 0) return maxFreqKHz;
509 }
510 return -1;
511 }
512
513 public static bool IsSmtCpu(int cpuId)
514 {
515 string[] paths = new string[]
516 {
517 $"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list",
518 $"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list"
519 };
520
521 foreach (var path in paths)
522 {
523 if (!File.Exists(path)) continue;
524 using (StreamReader sr = new StreamReader(path))
525 {
526 string line;
527 while ((line = sr.ReadLine()) != null)
528 {
529 if (line.Contains(",") || line.Contains("-"))
530 {
531 return true;
532 }
533 }
534 }
535 }
536 return false;
537 }
538
543 public static int AndroidGetNumBigCores()
544 {
545 int maxFreqKHzMin = int.MaxValue;
546 int maxFreqKHzMax = 0;
547 List<int> cpuMaxFreqKHz = new List<int>();
548 List<bool> cpuIsSmtCpu = new List<bool>();
549
550 try
551 {
552 string cpuPath = "/sys/devices/system/cpu/";
553 int coreIndex;
554 if (Directory.Exists(cpuPath))
555 {
556 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
557 {
558 string dirName = Path.GetFileName(cpuDir);
559 if (!dirName.StartsWith("cpu")) continue;
560 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
561
562 int maxFreqKHz = GetMaxFreqKHz(coreIndex);
563 cpuMaxFreqKHz.Add(maxFreqKHz);
564 if (maxFreqKHz > maxFreqKHzMax) maxFreqKHzMax = maxFreqKHz;
565 if (maxFreqKHz < maxFreqKHzMin) maxFreqKHzMin = maxFreqKHz;
566 cpuIsSmtCpu.Add(IsSmtCpu(coreIndex));
567 }
568 }
569 }
570 catch (Exception e)
571 {
572 LogError(e.Message);
573 }
574
575 int numBigCores = 0;
576 int numCores = SystemInfo.processorCount;
577 int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2;
578 if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores;
579 else
580 {
581 for (int i = 0; i < cpuMaxFreqKHz.Count; i++)
582 {
583 if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++;
584 }
585 }
586
587 if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2;
588 else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount);
589
590 return numBigCores;
591 }
592
598 {
599 List<int> capacities = new List<int>();
600 int minCapacity = int.MaxValue;
601 try
602 {
603 string cpuPath = "/sys/devices/system/cpu/";
604 int coreIndex;
605 if (Directory.Exists(cpuPath))
606 {
607 foreach (string cpuDir in Directory.GetDirectories(cpuPath))
608 {
609 string dirName = Path.GetFileName(cpuDir);
610 if (!dirName.StartsWith("cpu")) continue;
611 if (!int.TryParse(dirName.Substring(3), out coreIndex)) continue;
612
613 string capacityPath = Path.Combine(cpuDir, "cpu_capacity");
614 if (!File.Exists(capacityPath)) break;
615
616 int capacity = int.Parse(File.ReadAllText(capacityPath).Trim());
617 capacities.Add(capacity);
618 if (minCapacity > capacity) minCapacity = capacity;
619 }
620 }
621 }
622 catch (Exception e)
623 {
624 LogError(e.Message);
625 }
626
627 int numBigCores = 0;
628 foreach (int capacity in capacities)
629 {
630 if (capacity >= 2 * minCapacity) numBigCores++;
631 }
632
633 if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount;
634 return numBigCores;
635 }
636 }
637}
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 void RemoveErrorCallBack(Callback< string > callback)
Remove callback function added for error logs.
static string LLMManagerPath
Path of file with build information for runtime.
static string LlamaLibReleaseURL
LlamaLib release url.
static string libraryPath
LlamaLib path.
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 readonly Dictionary< string,(string, string, string)[]> modelOptions
Default models for download.
static string LLMUnityStore
LLMnity store path.
static string LlamaLibExtensionURL
LlamaLib extension url.
static string LlamaLibURL
LlamaLib url.
static void AddErrorCallBack(Callback< string > callback)
Add callback function to call for error logs.