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";
115 public static string LLMUnityStore = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"LLMUnity");
122 [HideInInspector]
public static readonly Dictionary<string, (string, string, string)[]>
modelOptions =
new Dictionary<
string, (
string,
string,
string)[]>()
124 {
"Medium models",
new(string, string, string)[]
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),
131 {
"Small models",
new(string, string, string)[]
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),
136 {
"Tiny models",
new(string, string, string)[]
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),
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>();
152 public enum DebugModeType
160 public static void Log(
string message)
162 if ((
int)DebugMode > (
int)DebugModeType.All)
return;
166 public static void LogWarning(
string message)
168 if ((
int)DebugMode > (
int)DebugModeType.Warning)
return;
169 Debug.LogWarning(message);
172 public static void LogError(
string message)
174 if ((
int)DebugMode > (
int)DebugModeType.Error)
return;
175 Debug.LogError(message);
176 foreach (Callback<string> errorCallback
in errorCallbacks) errorCallback(message);
179 static void LoadPlayerPrefs()
181 DebugMode = (DebugModeType)PlayerPrefs.GetInt(DebugModeKey, (
int)DebugModeType.All);
182 FullLlamaLib = PlayerPrefs.GetInt(FullLlamaLibKey, 0) == 1;
185 public static void SetDebugMode(DebugModeType newDebugMode)
187 if (DebugMode == newDebugMode)
return;
188 DebugMode = newDebugMode;
189 PlayerPrefs.SetInt(DebugModeKey, (
int)DebugMode);
194 public static void SetFullLlamaLib(
bool value)
196 if (FullLlamaLib == value)
return;
197 FullLlamaLib = value;
198 PlayerPrefs.SetInt(FullLlamaLibKey, value ? 1 : 0);
200 _ = DownloadLibrary();
205 public static string GetAssetPath(
string relPath =
"")
208 string assetsDir = Application.platform == RuntimePlatform.Android ? Application.persistentDataPath : Application.streamingAssetsPath;
209 return Path.Combine(assetsDir, relPath).Replace(
'\\',
'/');
213 [InitializeOnLoadMethod]
214 static async Task InitializeOnLoad()
217 await DownloadLibrary();
221 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
222 void InitializeOnLoad()
229 static Dictionary<string, ResumingWebClient> downloadClients =
new Dictionary<string, ResumingWebClient>();
231 public static void CancelDownload(
string savePath)
233 if (!downloadClients.ContainsKey(savePath))
return;
234 downloadClients[savePath].CancelDownloadAsync();
235 downloadClients.Remove(savePath);
238 public static async Task DownloadFile(
239 string fileUrl,
string savePath,
bool overwrite =
false,
240 Callback<string> callback =
null, Callback<float> progressCallback =
null
243 if (File.Exists(savePath) && !overwrite)
245 Log($
"File already exists at: {savePath}");
249 Log($
"Downloading {fileUrl} to {savePath}...");
250 string tmpPath = Path.Combine(Application.temporaryCachePath, Path.GetFileName(savePath));
252 ResumingWebClient client =
new ResumingWebClient();
253 downloadClients[savePath] = client;
254 await client.DownloadFileTaskAsyncResume(
new Uri(fileUrl), tmpPath, !overwrite, progressCallback);
255 downloadClients.Remove(savePath);
257 AssetDatabase.StartAssetEditing();
259 Directory.CreateDirectory(Path.GetDirectoryName(savePath));
260 File.Move(tmpPath, savePath);
262 AssetDatabase.StopAssetEditing();
264 Log($
"Download complete!");
267 progressCallback?.Invoke(1f);
268 callback?.Invoke(savePath);
271 public static async Task AndroidExtractFile(
string assetName,
bool overwrite =
false,
bool log =
true,
int chunkSize = 1024*1024)
276 if (!androidExtractTasks.TryGetValue(assetName, out extractionTask))
278 extractionTask = AndroidExtractFileOnce(assetName, overwrite, log, chunkSize);
279 androidExtractTasks[assetName] = extractionTask;
282 await extractionTask;
285 public static async Task AndroidExtractFileOnce(
string assetName,
bool overwrite =
false,
bool log =
true,
int chunkSize = 1024*1024)
287 string source =
"jar:file://" + Application.dataPath +
"!/assets/" + assetName;
288 string target = GetAssetPath(assetName);
289 if (!overwrite && File.Exists(target))
291 if (log) Log($
"File {target} already exists");
295 Log($
"Extracting {source} to {target}");
298 UnityWebRequest www = UnityWebRequest.Get(source);
300 var operation = www.SendWebRequest();
302 while (!operation.isDone) await Task.Delay(1);
303 if (www.result != UnityWebRequest.Result.Success)
305 LogError(
"Failed to load file from StreamingAssets: " + www.error);
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))
314 while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
316 await fileStream.WriteAsync(buffer, 0, bytesRead);
322 public static async Task AndroidExtractAsset(
string path,
bool overwrite =
false)
324 if (Application.platform != RuntimePlatform.Android)
return;
325 await AndroidExtractFile(Path.GetFileName(path), overwrite);
328 public static string GetFullPath(
string path)
330 return Path.GetFullPath(path).Replace(
'\\',
'/');
333 public static bool IsSubPath(
string childPath,
string parentPath)
335 return GetFullPath(childPath).StartsWith(GetFullPath(parentPath), StringComparison.OrdinalIgnoreCase);
338 public static string RelativePath(
string fullPath,
string basePath)
341 string fullParentPath = GetFullPath(basePath).TrimEnd(
'/');
342 string fullChildPath = GetFullPath(fullPath);
344 string relativePath = fullChildPath;
345 if (fullChildPath.StartsWith(fullParentPath, StringComparison.OrdinalIgnoreCase))
347 relativePath = fullChildPath.Substring(fullParentPath.Length);
348 while (relativePath.StartsWith(
"/")) relativePath = relativePath.Substring(1);
355 [HideInInspector]
public static float libraryProgress = 1;
357 public static void CreateEmptyFile(
string path)
359 File.Create(path).Dispose();
362 static void ExtractInsideDirectory(
string zipPath,
string extractPath,
bool overwrite =
true)
364 using (ZipArchive archive = ZipFile.OpenRead(zipPath))
366 foreach (ZipArchiveEntry entry
in archive.Entries)
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);
376 static async Task DownloadAndExtractInsideDirectory(
string url,
string path,
string setupDir)
378 string urlName = Path.GetFileName(url);
379 string setupFile = Path.Combine(setupDir, urlName +
".complete");
380 if (File.Exists(setupFile))
return;
382 string zipPath = Path.Combine(Application.temporaryCachePath, urlName);
383 await DownloadFile(url, zipPath,
true,
null, SetLibraryProgress);
385 AssetDatabase.StartAssetEditing();
386 ExtractInsideDirectory(zipPath, path);
387 CreateEmptyFile(setupFile);
388 AssetDatabase.StopAssetEditing();
390 File.Delete(zipPath);
393 static async Task DownloadLibrary()
395 if (libraryProgress < 1)
return;
400 string setupDir = Path.Combine(
libraryPath,
"setup");
401 Directory.CreateDirectory(setupDir);
407 AssetDatabase.StartAssetEditing();
408 string androidDir = Path.Combine(
libraryPath,
"android");
409 if (Directory.Exists(androidDir))
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");
418 AssetDatabase.StopAssetEditing();
431 private static void SetLibraryProgress(
float progress)
433 libraryProgress = Math.Min(0.99f, progress);
436 public static string AddAsset(
string assetPath)
438 if (!File.Exists(assetPath))
440 LogError($
"{assetPath} does not exist!");
443 string assetDir = GetAssetPath();
444 if (IsSubPath(assetPath, assetDir))
return RelativePath(assetPath, assetDir);
446 string filename = Path.GetFileName(assetPath);
447 string fullPath = GetAssetPath(filename);
448 AssetDatabase.StartAssetEditing();
449 foreach (
string path
in new string[] {fullPath, fullPath +
".meta"})
451 if (File.Exists(path)) File.Delete(path);
453 File.Copy(assetPath, fullPath);
454 AssetDatabase.StopAssetEditing();
464 errorCallbacks.Add(callback);
470 errorCallbacks.Remove(callback);
476 errorCallbacks.Clear();
479 public static int GetMaxFreqKHz(
int cpuId)
481 string[] paths =
new string[]
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"
488 foreach (var path
in paths)
490 if (!File.Exists(path))
continue;
493 using (StreamReader sr =
new StreamReader(path))
496 while ((line = sr.ReadLine()) !=
null)
498 string[] parts = line.Split(
' ');
499 if (parts.Length > 0 &&
int.TryParse(parts[0], out
int freqKHz))
501 if (freqKHz > maxFreqKHz)
503 maxFreqKHz = freqKHz;
508 if (maxFreqKHz != 0)
return maxFreqKHz;
513 public static bool IsSmtCpu(
int cpuId)
515 string[] paths =
new string[]
517 $
"/sys/devices/system/cpu/cpu{cpuId}/topology/core_cpus_list",
518 $
"/sys/devices/system/cpu/cpu{cpuId}/topology/thread_siblings_list"
521 foreach (var path
in paths)
523 if (!File.Exists(path))
continue;
524 using (StreamReader sr =
new StreamReader(path))
527 while ((line = sr.ReadLine()) !=
null)
529 if (line.Contains(
",") || line.Contains(
"-"))
545 int maxFreqKHzMin =
int.MaxValue;
546 int maxFreqKHzMax = 0;
547 List<int> cpuMaxFreqKHz =
new List<int>();
548 List<bool> cpuIsSmtCpu =
new List<bool>();
552 string cpuPath =
"/sys/devices/system/cpu/";
554 if (Directory.Exists(cpuPath))
556 foreach (
string cpuDir
in Directory.GetDirectories(cpuPath))
558 string dirName = Path.GetFileName(cpuDir);
559 if (!dirName.StartsWith(
"cpu"))
continue;
560 if (!
int.TryParse(dirName.Substring(3), out coreIndex))
continue;
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));
576 int numCores = SystemInfo.processorCount;
577 int maxFreqKHzMedium = (maxFreqKHzMin + maxFreqKHzMax) / 2;
578 if (maxFreqKHzMedium == maxFreqKHzMax) numBigCores = numCores;
581 for (
int i = 0; i < cpuMaxFreqKHz.Count; i++)
583 if (cpuIsSmtCpu[i] || cpuMaxFreqKHz[i] >= maxFreqKHzMedium) numBigCores++;
587 if (numBigCores == 0) numBigCores = SystemInfo.processorCount / 2;
588 else numBigCores = Math.Min(numBigCores, SystemInfo.processorCount);
599 List<int> capacities =
new List<int>();
600 int minCapacity =
int.MaxValue;
603 string cpuPath =
"/sys/devices/system/cpu/";
605 if (Directory.Exists(cpuPath))
607 foreach (
string cpuDir
in Directory.GetDirectories(cpuPath))
609 string dirName = Path.GetFileName(cpuDir);
610 if (!dirName.StartsWith(
"cpu"))
continue;
611 if (!
int.TryParse(dirName.Substring(3), out coreIndex))
continue;
613 string capacityPath = Path.Combine(cpuDir,
"cpu_capacity");
614 if (!File.Exists(capacityPath))
break;
616 int capacity =
int.Parse(File.ReadAllText(capacityPath).Trim());
617 capacities.Add(capacity);
618 if (minCapacity > capacity) minCapacity = capacity;
628 foreach (
int capacity
in capacities)
630 if (capacity >= 2 * minCapacity) numBigCores++;
633 if (numBigCores == 0 || numBigCores > SystemInfo.processorCount) numBigCores = SystemInfo.processorCount;