Mayo commited on
refactor: koharu-runtime
Browse files- .cargo/config.toml +1 -0
- Cargo.lock +6 -32
- Cargo.toml +1 -1
- koharu-llm/Cargo.toml +2 -2
- koharu-llm/bin/llama.rs +1 -8
- koharu-llm/bin/llm.rs +6 -8
- koharu-llm/build.rs +10 -11
- koharu-llm/src/lib.rs +0 -8
- koharu-llm/src/model.rs +3 -9
- koharu-llm/src/safe/mod.rs +0 -1
- koharu-llm/src/safe/runtime.rs +0 -884
- koharu-llm/src/sys/mod.rs +63 -89
- koharu-llm/tests/llm.rs +1 -3
- koharu-runtime/Cargo.toml +5 -12
- koharu-runtime/src/archive.rs +263 -0
- koharu-runtime/src/cuda.rs +281 -0
- koharu-runtime/src/dylib.rs +0 -394
- koharu-runtime/src/lib.rs +106 -3
- koharu-runtime/src/llama.rs +259 -0
- koharu-runtime/src/loader.rs +79 -0
- koharu-runtime/src/zip.rs +0 -226
- koharu/Cargo.toml +1 -3
- koharu/src/app.rs +10 -30
- koharu/src/lib.rs +0 -1
- koharu/src/nvidia.rs +0 -103
- koharu/tauri.linux.conf.json +7 -8
- koharu/tauri.windows.conf.json +7 -8
- scripts/dev.ts +0 -34
.cargo/config.toml
CHANGED
|
@@ -3,5 +3,6 @@
|
|
| 3 |
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
| 4 |
TS_RS_EXPORT_DIR = { value = "ui/lib/generated/protocol", relative = true }
|
| 5 |
TS_RS_LARGE_INT = "number"
|
|
|
|
| 6 |
# CUDA 13.1 requires C++17
|
| 7 |
NVCC_PREPEND_FLAGS = "-std=c++17 -Xcompiler /Zc:preprocessor"
|
|
|
|
| 3 |
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
| 4 |
TS_RS_EXPORT_DIR = { value = "ui/lib/generated/protocol", relative = true }
|
| 5 |
TS_RS_LARGE_INT = "number"
|
| 6 |
+
LLAMA_CPP_TAG = "b8233"
|
| 7 |
# CUDA 13.1 requires C++17
|
| 8 |
NVCC_PREPEND_FLAGS = "-std=c++17 -Xcompiler /Zc:preprocessor"
|
Cargo.lock
CHANGED
|
@@ -1297,27 +1297,6 @@ dependencies = [
|
|
| 1297 |
"syn 2.0.117",
|
| 1298 |
]
|
| 1299 |
|
| 1300 |
-
[[package]]
|
| 1301 |
-
name = "csv"
|
| 1302 |
-
version = "1.4.0"
|
| 1303 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1304 |
-
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
| 1305 |
-
dependencies = [
|
| 1306 |
-
"csv-core",
|
| 1307 |
-
"itoa",
|
| 1308 |
-
"ryu",
|
| 1309 |
-
"serde_core",
|
| 1310 |
-
]
|
| 1311 |
-
|
| 1312 |
-
[[package]]
|
| 1313 |
-
name = "csv-core"
|
| 1314 |
-
version = "0.1.13"
|
| 1315 |
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1316 |
-
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
| 1317 |
-
dependencies = [
|
| 1318 |
-
"memchr",
|
| 1319 |
-
]
|
| 1320 |
-
|
| 1321 |
[[package]]
|
| 1322 |
name = "ctor"
|
| 1323 |
version = "0.2.9"
|
|
@@ -3921,7 +3900,6 @@ dependencies = [
|
|
| 3921 |
"koharu-rpc",
|
| 3922 |
"koharu-runtime",
|
| 3923 |
"koharu-types",
|
| 3924 |
-
"libloading 0.9.0",
|
| 3925 |
"once_cell",
|
| 3926 |
"rfd",
|
| 3927 |
"serde_json",
|
|
@@ -3971,11 +3949,11 @@ dependencies = [
|
|
| 3971 |
"hf-hub",
|
| 3972 |
"keyring",
|
| 3973 |
"koharu-http",
|
|
|
|
| 3974 |
"koharu-types",
|
| 3975 |
"libloading 0.8.9",
|
| 3976 |
"minijinja",
|
| 3977 |
"num_cpus",
|
| 3978 |
-
"once_cell",
|
| 3979 |
"proc-macro2",
|
| 3980 |
"quote",
|
| 3981 |
"reqwest 0.13.2",
|
|
@@ -4010,7 +3988,7 @@ dependencies = [
|
|
| 4010 |
"keyring",
|
| 4011 |
"koharu-http",
|
| 4012 |
"koharu-types",
|
| 4013 |
-
"libloading 0.
|
| 4014 |
"minijinja",
|
| 4015 |
"num_cpus",
|
| 4016 |
"objc2",
|
|
@@ -4116,19 +4094,15 @@ name = "koharu-runtime"
|
|
| 4116 |
version = "0.40.1"
|
| 4117 |
dependencies = [
|
| 4118 |
"anyhow",
|
| 4119 |
-
"
|
| 4120 |
"flate2",
|
| 4121 |
-
"futures",
|
| 4122 |
"koharu-http",
|
| 4123 |
-
"libloading 0.
|
| 4124 |
-
"num_cpus",
|
| 4125 |
-
"once_cell",
|
| 4126 |
-
"rayon",
|
| 4127 |
-
"reqwest 0.13.2",
|
| 4128 |
"serde_json",
|
|
|
|
| 4129 |
"tempfile",
|
| 4130 |
"tokio",
|
| 4131 |
-
"
|
| 4132 |
"zip 8.2.0",
|
| 4133 |
]
|
| 4134 |
|
|
|
|
| 1297 |
"syn 2.0.117",
|
| 1298 |
]
|
| 1299 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1300 |
[[package]]
|
| 1301 |
name = "ctor"
|
| 1302 |
version = "0.2.9"
|
|
|
|
| 3900 |
"koharu-rpc",
|
| 3901 |
"koharu-runtime",
|
| 3902 |
"koharu-types",
|
|
|
|
| 3903 |
"once_cell",
|
| 3904 |
"rfd",
|
| 3905 |
"serde_json",
|
|
|
|
| 3949 |
"hf-hub",
|
| 3950 |
"keyring",
|
| 3951 |
"koharu-http",
|
| 3952 |
+
"koharu-runtime",
|
| 3953 |
"koharu-types",
|
| 3954 |
"libloading 0.8.9",
|
| 3955 |
"minijinja",
|
| 3956 |
"num_cpus",
|
|
|
|
| 3957 |
"proc-macro2",
|
| 3958 |
"quote",
|
| 3959 |
"reqwest 0.13.2",
|
|
|
|
| 3988 |
"keyring",
|
| 3989 |
"koharu-http",
|
| 3990 |
"koharu-types",
|
| 3991 |
+
"libloading 0.8.9",
|
| 3992 |
"minijinja",
|
| 3993 |
"num_cpus",
|
| 3994 |
"objc2",
|
|
|
|
| 4094 |
version = "0.40.1"
|
| 4095 |
dependencies = [
|
| 4096 |
"anyhow",
|
| 4097 |
+
"dirs",
|
| 4098 |
"flate2",
|
|
|
|
| 4099 |
"koharu-http",
|
| 4100 |
+
"libloading 0.8.9",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4101 |
"serde_json",
|
| 4102 |
+
"tar",
|
| 4103 |
"tempfile",
|
| 4104 |
"tokio",
|
| 4105 |
+
"windows-sys 0.61.2",
|
| 4106 |
"zip 8.2.0",
|
| 4107 |
]
|
| 4108 |
|
Cargo.toml
CHANGED
|
@@ -77,7 +77,7 @@ flate2 = { version = "1.0", default-features = false, features = [
|
|
| 77 |
csv = "1.3"
|
| 78 |
tempfile = "3.24"
|
| 79 |
once_cell = "1.21"
|
| 80 |
-
libloading = "0.9"
|
| 81 |
num_cpus = "1.17"
|
| 82 |
rustfft = "6.4"
|
| 83 |
cudarc = { version = "0.19.4", features = [
|
|
|
|
| 77 |
csv = "1.3"
|
| 78 |
tempfile = "3.24"
|
| 79 |
once_cell = "1.21"
|
| 80 |
+
libloading = "0.8.9"
|
| 81 |
num_cpus = "1.17"
|
| 82 |
rustfft = "6.4"
|
| 83 |
cudarc = { version = "0.19.4", features = [
|
koharu-llm/Cargo.toml
CHANGED
|
@@ -30,11 +30,11 @@ flate2 = { workspace = true }
|
|
| 30 |
futures = { workspace = true }
|
| 31 |
hf-hub = { workspace = true }
|
| 32 |
koharu-http = { workspace = true }
|
|
|
|
| 33 |
koharu-types = { workspace = true }
|
| 34 |
-
libloading =
|
| 35 |
minijinja = { workspace = true }
|
| 36 |
num_cpus = { workspace = true }
|
| 37 |
-
once_cell = { workspace = true }
|
| 38 |
reqwest = { workspace = true }
|
| 39 |
serde = { workspace = true }
|
| 40 |
serde_json = { workspace = true }
|
|
|
|
| 30 |
futures = { workspace = true }
|
| 31 |
hf-hub = { workspace = true }
|
| 32 |
koharu-http = { workspace = true }
|
| 33 |
+
koharu-runtime = { workspace = true }
|
| 34 |
koharu-types = { workspace = true }
|
| 35 |
+
libloading = { workspace = true }
|
| 36 |
minijinja = { workspace = true }
|
| 37 |
num_cpus = { workspace = true }
|
|
|
|
| 38 |
reqwest = { workspace = true }
|
| 39 |
serde = { workspace = true }
|
| 40 |
serde_json = { workspace = true }
|
koharu-llm/bin/llama.rs
CHANGED
|
@@ -160,20 +160,13 @@ fn main() -> Result<()> {
|
|
| 160 |
list_devices,
|
| 161 |
} = Args::parse();
|
| 162 |
|
| 163 |
-
let runtime_dir = dirs::cache_dir()
|
| 164 |
-
.unwrap_or_else(std::env::temp_dir)
|
| 165 |
-
.join("Koharu")
|
| 166 |
-
.join("llama.cpp")
|
| 167 |
-
.join(env!("KOHARU_LLM_LLAMA_CPP_TAG"));
|
| 168 |
tokio::runtime::Builder::new_current_thread()
|
| 169 |
.enable_all()
|
| 170 |
.build()
|
| 171 |
.with_context(|| "unable to create tokio runtime")?
|
| 172 |
.block_on(async {
|
| 173 |
-
|
| 174 |
.await
|
| 175 |
-
.context("failed to download runtime libraries")?;
|
| 176 |
-
koharu_llm::safe::runtime::initialize(&runtime_dir)
|
| 177 |
.context("failed to initialize runtime libraries")?;
|
| 178 |
Ok::<(), anyhow::Error>(())
|
| 179 |
})?;
|
|
|
|
| 160 |
list_devices,
|
| 161 |
} = Args::parse();
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
tokio::runtime::Builder::new_current_thread()
|
| 164 |
.enable_all()
|
| 165 |
.build()
|
| 166 |
.with_context(|| "unable to create tokio runtime")?
|
| 167 |
.block_on(async {
|
| 168 |
+
koharu_runtime::initialize()
|
| 169 |
.await
|
|
|
|
|
|
|
| 170 |
.context("failed to initialize runtime libraries")?;
|
| 171 |
Ok::<(), anyhow::Error>(())
|
| 172 |
})?;
|
koharu-llm/bin/llm.rs
CHANGED
|
@@ -10,11 +10,11 @@ struct Args {
|
|
| 10 |
#[arg(
|
| 11 |
long,
|
| 12 |
default_value = r#"「ãˆã€ãƒžã‚¸ã§!?ã€
|
| 13 |
-
俺ã€ç”°ä¸
|
| 14 |
-
「やã°ã„ã€
|
| 15 |
-
ãã®çž¬é–“ã€ä¿ºã®æ‰‹ã‹ã‚‰é’白ã„å…‰ãŒæ”¾ãŸã‚ŒãŸã€‚
|
| 16 |
-
『レベルアップã—ã¾ã—ãŸã€‚新スã‚
|
| 17 |
-
é
|
| 18 |
「ã“ã®ä¸–界ã§ã€ä¿ºã¯æœ€å¼·ã«ãªã‚‹!ã€
|
| 19 |
ã“ã†ã—ã¦ä¿ºã®ç•°ä¸–界ライフãŒå§‹ã¾ã£ãŸã€‚美少女ã¨ã®å‡ºä¼šã„ã‚‚ã€ã‚‚ã¡ã‚ã‚“å¾…ã£ã¦ã„る。"#
|
| 20 |
)]
|
|
@@ -71,9 +71,7 @@ fn init_tracing() {
|
|
| 71 |
}
|
| 72 |
|
| 73 |
async fn initialize_runtime() -> anyhow::Result<()> {
|
| 74 |
-
|
| 75 |
-
koharu_llm::safe::runtime::ensure_dylibs(&runtime_dir).await?;
|
| 76 |
-
koharu_llm::safe::runtime::initialize(&runtime_dir)?;
|
| 77 |
Ok(())
|
| 78 |
}
|
| 79 |
|
|
|
|
| 10 |
#[arg(
|
| 11 |
long,
|
| 12 |
default_value = r#"「ãˆã€ãƒžã‚¸ã§!?ã€
|
| 13 |
+
俺ã€ç”°ä¸å¤ªéƒŽã¯æ°—ãŒã¤ãã¨è¦‹çŸ¥ã‚‰ã¬æ£®ã®ä¸ã«ã„ãŸã€‚ç›®ã®å‰ã«ã¯å·¨å¤§ãªé”ç£ãŒç‰™ã‚’剥ã„ã¦ã„る。
|
| 14 |
+
「やã°ã„ã€æ»ã¬!ã€
|
| 15 |
+
ãã®çž¬é–“ã€ä¿ºã®æ‰‹ã‹ã‚‰é’白ã„å…‰ãŒæ”¾ãŸã‚ŒãŸã€‚é”ç£ã¯ä¸€çž¬ã§æ¶ˆæ»…ã™ã‚‹ã€‚
|
| 16 |
+
『レベルアップã—ã¾ã—ãŸã€‚新スã‚ãƒ«ã€Œçˆ†ç‚Žé”æ³•ã€ã‚’ç¿’å¾—ã€
|
| 17 |
+
é ã®ä¸ã«è¬Žã®å£°ãŒéŸ¿ã。ã©ã†ã‚„ら俺ã€ãƒãƒ¼ãƒˆèƒ½åŠ›ã‚’æ‰‹ã«å…¥ã‚ŒãŸã‚‰ã—ã„。
|
| 18 |
「ã“ã®ä¸–界ã§ã€ä¿ºã¯æœ€å¼·ã«ãªã‚‹!ã€
|
| 19 |
ã“ã†ã—ã¦ä¿ºã®ç•°ä¸–界ライフãŒå§‹ã¾ã£ãŸã€‚美少女ã¨ã®å‡ºä¼šã„ã‚‚ã€ã‚‚ã¡ã‚ã‚“å¾…ã£ã¦ã„る。"#
|
| 20 |
)]
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
async fn initialize_runtime() -> anyhow::Result<()> {
|
| 74 |
+
koharu_runtime::initialize().await?;
|
|
|
|
|
|
|
| 75 |
Ok(())
|
| 76 |
}
|
| 77 |
|
koharu-llm/build.rs
CHANGED
|
@@ -11,10 +11,6 @@ use reqwest::blocking::Client;
|
|
| 11 |
use syn::{FnArg, ImplItem, Item, Pat, Signature};
|
| 12 |
use tar::Archive;
|
| 13 |
|
| 14 |
-
const LLAMA_CPP_TAG: &str = "b8233";
|
| 15 |
-
const LLAMA_CPP_COMMIT: &str = "c5a778891ba0ddbd4cbb507c823f970595b1adc2";
|
| 16 |
-
const SOURCE_URL: &str = "https://github.com/ggml-org/llama.cpp/archive/refs/tags/b8233.tar.gz";
|
| 17 |
-
|
| 18 |
const GGML_FUNCTIONS: &[&str] = &[
|
| 19 |
"ggml_backend_dev_count",
|
| 20 |
"ggml_backend_dev_get",
|
|
@@ -45,6 +41,7 @@ const MTMD_FUNCTIONS: &[&str] = &["mtmd_.*", "mtmd_helper_.*"];
|
|
| 45 |
|
| 46 |
fn main() {
|
| 47 |
println!("cargo:rerun-if-changed=build.rs");
|
|
|
|
| 48 |
|
| 49 |
if let Err(err) = run() {
|
| 50 |
panic!("{err:#}");
|
|
@@ -54,8 +51,9 @@ fn main() {
|
|
| 54 |
fn run() -> Result<()> {
|
| 55 |
validate_target()?;
|
| 56 |
|
|
|
|
| 57 |
let out_dir = PathBuf::from(env::var("OUT_DIR").context("missing OUT_DIR")?);
|
| 58 |
-
let source_root = ensure_source_tree(&out_dir)?;
|
| 59 |
let header_path = write_wrapper_header(&out_dir)?;
|
| 60 |
let include_dirs = include_dirs(&source_root);
|
| 61 |
|
|
@@ -106,8 +104,7 @@ fn run() -> Result<()> {
|
|
| 106 |
],
|
| 107 |
)?;
|
| 108 |
|
| 109 |
-
println!("cargo:rustc-env=KOHARU_LLM_LLAMA_CPP_TAG={
|
| 110 |
-
println!("cargo:rustc-env=KOHARU_LLM_LLAMA_CPP_COMMIT={LLAMA_CPP_COMMIT}");
|
| 111 |
|
| 112 |
Ok(())
|
| 113 |
}
|
|
@@ -125,10 +122,12 @@ fn validate_target() -> Result<()> {
|
|
| 125 |
Ok(())
|
| 126 |
}
|
| 127 |
|
| 128 |
-
fn ensure_source_tree(out_dir: &Path) -> Result<PathBuf> {
|
| 129 |
let cache_dir = out_dir.join("llama.cpp-source");
|
| 130 |
-
let tarball_path = cache_dir.join(format!("llama.cpp-{
|
| 131 |
-
let source_root = cache_dir.join(format!("llama.cpp-{
|
|
|
|
|
|
|
| 132 |
|
| 133 |
if source_root.join("include/llama.h").exists() {
|
| 134 |
return Ok(source_root);
|
|
@@ -142,7 +141,7 @@ fn ensure_source_tree(out_dir: &Path) -> Result<PathBuf> {
|
|
| 142 |
.build()
|
| 143 |
.context("failed to build reqwest client")?;
|
| 144 |
let mut response = client
|
| 145 |
-
.get(
|
| 146 |
.send()
|
| 147 |
.context("failed to download llama.cpp source tarball")?
|
| 148 |
.error_for_status()
|
|
|
|
| 11 |
use syn::{FnArg, ImplItem, Item, Pat, Signature};
|
| 12 |
use tar::Archive;
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
const GGML_FUNCTIONS: &[&str] = &[
|
| 15 |
"ggml_backend_dev_count",
|
| 16 |
"ggml_backend_dev_get",
|
|
|
|
| 41 |
|
| 42 |
fn main() {
|
| 43 |
println!("cargo:rerun-if-changed=build.rs");
|
| 44 |
+
println!("cargo:rerun-if-env-changed=LLAMA_CPP_TAG");
|
| 45 |
|
| 46 |
if let Err(err) = run() {
|
| 47 |
panic!("{err:#}");
|
|
|
|
| 51 |
fn run() -> Result<()> {
|
| 52 |
validate_target()?;
|
| 53 |
|
| 54 |
+
let llama_cpp_tag = env::var("LLAMA_CPP_TAG").context("missing LLAMA_CPP_TAG")?;
|
| 55 |
let out_dir = PathBuf::from(env::var("OUT_DIR").context("missing OUT_DIR")?);
|
| 56 |
+
let source_root = ensure_source_tree(&out_dir, &llama_cpp_tag)?;
|
| 57 |
let header_path = write_wrapper_header(&out_dir)?;
|
| 58 |
let include_dirs = include_dirs(&source_root);
|
| 59 |
|
|
|
|
| 104 |
],
|
| 105 |
)?;
|
| 106 |
|
| 107 |
+
println!("cargo:rustc-env=KOHARU_LLM_LLAMA_CPP_TAG={llama_cpp_tag}");
|
|
|
|
| 108 |
|
| 109 |
Ok(())
|
| 110 |
}
|
|
|
|
| 122 |
Ok(())
|
| 123 |
}
|
| 124 |
|
| 125 |
+
fn ensure_source_tree(out_dir: &Path, llama_cpp_tag: &str) -> Result<PathBuf> {
|
| 126 |
let cache_dir = out_dir.join("llama.cpp-source");
|
| 127 |
+
let tarball_path = cache_dir.join(format!("llama.cpp-{llama_cpp_tag}.tar.gz"));
|
| 128 |
+
let source_root = cache_dir.join(format!("llama.cpp-{llama_cpp_tag}"));
|
| 129 |
+
let source_url =
|
| 130 |
+
format!("https://github.com/ggml-org/llama.cpp/archive/refs/tags/{llama_cpp_tag}.tar.gz");
|
| 131 |
|
| 132 |
if source_root.join("include/llama.h").exists() {
|
| 133 |
return Ok(source_root);
|
|
|
|
| 141 |
.build()
|
| 142 |
.context("failed to build reqwest client")?;
|
| 143 |
let mut response = client
|
| 144 |
+
.get(&source_url)
|
| 145 |
.send()
|
| 146 |
.context("failed to download llama.cpp source tarball")?
|
| 147 |
.error_for_status()
|
koharu-llm/src/lib.rs
CHANGED
|
@@ -15,14 +15,6 @@ pub use language::{Language, language_from_tag, supported_locales};
|
|
| 15 |
pub use model::{GenerateOptions, Llm};
|
| 16 |
pub use prompt::{ChatMessage, ChatRole};
|
| 17 |
|
| 18 |
-
pub fn runtime_dir() -> PathBuf {
|
| 19 |
-
dirs::cache_dir()
|
| 20 |
-
.unwrap_or_else(std::env::temp_dir)
|
| 21 |
-
.join("Koharu")
|
| 22 |
-
.join("llama.cpp")
|
| 23 |
-
.join(env!("KOHARU_LLM_LLAMA_CPP_TAG"))
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
#[derive(
|
| 27 |
Debug,
|
| 28 |
Clone,
|
|
|
|
| 15 |
pub use model::{GenerateOptions, Llm};
|
| 16 |
pub use prompt::{ChatMessage, ChatRole};
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
#[derive(
|
| 19 |
Debug,
|
| 20 |
Clone,
|
koharu-llm/src/model.rs
CHANGED
|
@@ -59,7 +59,6 @@ impl Default for GenerateOptions {
|
|
| 59 |
|
| 60 |
impl Llm {
|
| 61 |
pub async fn load(id: ModelId, cpu: bool) -> Result<Self> {
|
| 62 |
-
ensure_runtime_initialized()?;
|
| 63 |
let model_path = id.get().await?;
|
| 64 |
|
| 65 |
tokio::task::spawn_blocking(move || Self::load_from_path(id, cpu, model_path))
|
|
@@ -68,6 +67,9 @@ impl Llm {
|
|
| 68 |
}
|
| 69 |
|
| 70 |
fn load_from_path(id: ModelId, cpu: bool, model_path: PathBuf) -> Result<Self> {
|
|
|
|
|
|
|
|
|
|
| 71 |
LOGGING_READY.call_once(|| {
|
| 72 |
send_logs_to_tracing(LogOptions::default().with_logs_enabled(true));
|
| 73 |
});
|
|
@@ -239,14 +241,6 @@ impl Llm {
|
|
| 239 |
}
|
| 240 |
}
|
| 241 |
|
| 242 |
-
fn ensure_runtime_initialized() -> Result<()> {
|
| 243 |
-
anyhow::ensure!(
|
| 244 |
-
crate::sys::is_initialized(),
|
| 245 |
-
"llama.cpp runtime is not initialized; initialize it before calling `koharu_llm::Llm::load`"
|
| 246 |
-
);
|
| 247 |
-
Ok(())
|
| 248 |
-
}
|
| 249 |
-
|
| 250 |
fn model_params(cpu: bool, backend: &LlamaBackend) -> LlamaModelParams {
|
| 251 |
if !cpu && backend.supports_gpu_offload() {
|
| 252 |
LlamaModelParams::default().with_n_gpu_layers(DEFAULT_GPU_LAYERS)
|
|
|
|
| 59 |
|
| 60 |
impl Llm {
|
| 61 |
pub async fn load(id: ModelId, cpu: bool) -> Result<Self> {
|
|
|
|
| 62 |
let model_path = id.get().await?;
|
| 63 |
|
| 64 |
tokio::task::spawn_blocking(move || Self::load_from_path(id, cpu, model_path))
|
|
|
|
| 67 |
}
|
| 68 |
|
| 69 |
fn load_from_path(id: ModelId, cpu: bool, model_path: PathBuf) -> Result<Self> {
|
| 70 |
+
crate::sys::initialize()
|
| 71 |
+
.context("failed to initialize llama.cpp runtime bindings")?;
|
| 72 |
+
|
| 73 |
LOGGING_READY.call_once(|| {
|
| 74 |
send_logs_to_tracing(LogOptions::default().with_logs_enabled(true));
|
| 75 |
});
|
|
|
|
| 241 |
}
|
| 242 |
}
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
fn model_params(cpu: bool, backend: &LlamaBackend) -> LlamaModelParams {
|
| 245 |
if !cpu && backend.supports_gpu_offload() {
|
| 246 |
LlamaModelParams::default().with_n_gpu_layers(DEFAULT_GPU_LAYERS)
|
koharu-llm/src/safe/mod.rs
CHANGED
|
@@ -27,7 +27,6 @@ pub mod llama_backend;
|
|
| 27 |
pub mod llama_batch;
|
| 28 |
pub mod model;
|
| 29 |
pub mod mtmd;
|
| 30 |
-
pub mod runtime;
|
| 31 |
pub mod sampling;
|
| 32 |
pub mod timing;
|
| 33 |
pub mod token;
|
|
|
|
| 27 |
pub mod llama_batch;
|
| 28 |
pub mod model;
|
| 29 |
pub mod mtmd;
|
|
|
|
| 30 |
pub mod sampling;
|
| 31 |
pub mod timing;
|
| 32 |
pub mod token;
|
koharu-llm/src/safe/runtime.rs
DELETED
|
@@ -1,884 +0,0 @@
|
|
| 1 |
-
use std::env;
|
| 2 |
-
use std::ffi::OsStr;
|
| 3 |
-
use std::fs;
|
| 4 |
-
use std::io;
|
| 5 |
-
use std::path::{Path, PathBuf};
|
| 6 |
-
|
| 7 |
-
use anyhow::{Context, Result, bail};
|
| 8 |
-
use flate2::read::GzDecoder;
|
| 9 |
-
use futures::StreamExt;
|
| 10 |
-
use libloading::Library;
|
| 11 |
-
use tokio::io::AsyncWriteExt;
|
| 12 |
-
use zip::read::ZipArchive;
|
| 13 |
-
|
| 14 |
-
const LLAMA_CPP_TAG: &str = env!("KOHARU_LLM_LLAMA_CPP_TAG");
|
| 15 |
-
const RELEASE_BASE_URL: &str = "https://github.com/ggml-org/llama.cpp/releases/download";
|
| 16 |
-
const STAMP_FILE: &str = ".koharu-llm-runtime-stamp";
|
| 17 |
-
const DOWNLOADS_DIR: &str = ".downloads";
|
| 18 |
-
|
| 19 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 20 |
-
enum ArchiveKind {
|
| 21 |
-
Zip,
|
| 22 |
-
TarGz,
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
#[allow(dead_code)]
|
| 26 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 27 |
-
enum Backend {
|
| 28 |
-
Cuda,
|
| 29 |
-
Vulkan,
|
| 30 |
-
Default,
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 34 |
-
pub(crate) enum CoreLibraryKind {
|
| 35 |
-
GgmlBase,
|
| 36 |
-
Ggml,
|
| 37 |
-
Llama,
|
| 38 |
-
Mtmd,
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
#[derive(Debug, Clone, PartialEq, Eq)]
|
| 42 |
-
pub(crate) struct LoadStep {
|
| 43 |
-
pub(crate) path: PathBuf,
|
| 44 |
-
pub(crate) core: Option<CoreLibraryKind>,
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 48 |
-
struct ReleaseAsset {
|
| 49 |
-
name: &'static str,
|
| 50 |
-
kind: ArchiveKind,
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
#[derive(Debug, Clone, Copy)]
|
| 54 |
-
struct Manifest {
|
| 55 |
-
id: &'static str,
|
| 56 |
-
support_libraries: &'static [&'static str],
|
| 57 |
-
support_prefixes: &'static [&'static str],
|
| 58 |
-
core_ggml_base: &'static str,
|
| 59 |
-
core_ggml: &'static str,
|
| 60 |
-
core_llama: &'static str,
|
| 61 |
-
core_mtmd: &'static str,
|
| 62 |
-
plugin_prefixes: &'static [&'static str],
|
| 63 |
-
library_extension: &'static str,
|
| 64 |
-
assets: &'static [ReleaseAsset],
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 68 |
-
struct TargetTriple {
|
| 69 |
-
os: &'static str,
|
| 70 |
-
arch: &'static str,
|
| 71 |
-
env: &'static str,
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
const WINDOWS_CUDA_ASSETS: &[ReleaseAsset] = &[
|
| 75 |
-
ReleaseAsset {
|
| 76 |
-
name: "llama-b8233-bin-win-cuda-13.1-x64.zip",
|
| 77 |
-
kind: ArchiveKind::Zip,
|
| 78 |
-
},
|
| 79 |
-
ReleaseAsset {
|
| 80 |
-
name: "cudart-llama-bin-win-cuda-13.1-x64.zip",
|
| 81 |
-
kind: ArchiveKind::Zip,
|
| 82 |
-
},
|
| 83 |
-
];
|
| 84 |
-
|
| 85 |
-
const WINDOWS_VULKAN_ASSETS: &[ReleaseAsset] = &[ReleaseAsset {
|
| 86 |
-
name: "llama-b8233-bin-win-vulkan-x64.zip",
|
| 87 |
-
kind: ArchiveKind::Zip,
|
| 88 |
-
}];
|
| 89 |
-
|
| 90 |
-
const LINUX_VULKAN_ASSETS: &[ReleaseAsset] = &[ReleaseAsset {
|
| 91 |
-
name: "llama-b8233-bin-ubuntu-vulkan-x64.tar.gz",
|
| 92 |
-
kind: ArchiveKind::TarGz,
|
| 93 |
-
}];
|
| 94 |
-
|
| 95 |
-
const MACOS_ARM64_ASSETS: &[ReleaseAsset] = &[ReleaseAsset {
|
| 96 |
-
name: "llama-b8233-bin-macos-arm64.tar.gz",
|
| 97 |
-
kind: ArchiveKind::TarGz,
|
| 98 |
-
}];
|
| 99 |
-
|
| 100 |
-
const WINDOWS_CUDA_MANIFEST: Manifest = Manifest {
|
| 101 |
-
id: "windows-cuda13-x64",
|
| 102 |
-
support_libraries: &["cudart64_13.dll", "cublasLt64_13.dll", "cublas64_13.dll"],
|
| 103 |
-
support_prefixes: &["libomp"],
|
| 104 |
-
core_ggml_base: "ggml-base.dll",
|
| 105 |
-
core_ggml: "ggml.dll",
|
| 106 |
-
core_llama: "llama.dll",
|
| 107 |
-
core_mtmd: "mtmd.dll",
|
| 108 |
-
plugin_prefixes: &["ggml-"],
|
| 109 |
-
library_extension: ".dll",
|
| 110 |
-
assets: WINDOWS_CUDA_ASSETS,
|
| 111 |
-
};
|
| 112 |
-
|
| 113 |
-
const WINDOWS_VULKAN_MANIFEST: Manifest = Manifest {
|
| 114 |
-
id: "windows-vulkan-x64",
|
| 115 |
-
support_libraries: &[],
|
| 116 |
-
support_prefixes: &["libomp"],
|
| 117 |
-
core_ggml_base: "ggml-base.dll",
|
| 118 |
-
core_ggml: "ggml.dll",
|
| 119 |
-
core_llama: "llama.dll",
|
| 120 |
-
core_mtmd: "mtmd.dll",
|
| 121 |
-
plugin_prefixes: &["ggml-"],
|
| 122 |
-
library_extension: ".dll",
|
| 123 |
-
assets: WINDOWS_VULKAN_ASSETS,
|
| 124 |
-
};
|
| 125 |
-
|
| 126 |
-
const LINUX_VULKAN_MANIFEST: Manifest = Manifest {
|
| 127 |
-
id: "linux-vulkan-x64",
|
| 128 |
-
support_libraries: &[],
|
| 129 |
-
support_prefixes: &[],
|
| 130 |
-
core_ggml_base: "libggml-base.so",
|
| 131 |
-
core_ggml: "libggml.so",
|
| 132 |
-
core_llama: "libllama.so",
|
| 133 |
-
core_mtmd: "libmtmd.so",
|
| 134 |
-
plugin_prefixes: &["libggml-"],
|
| 135 |
-
library_extension: ".so",
|
| 136 |
-
assets: LINUX_VULKAN_ASSETS,
|
| 137 |
-
};
|
| 138 |
-
|
| 139 |
-
const MACOS_ARM64_MANIFEST: Manifest = Manifest {
|
| 140 |
-
id: "macos-arm64",
|
| 141 |
-
support_libraries: &[],
|
| 142 |
-
support_prefixes: &[],
|
| 143 |
-
core_ggml_base: "libggml-base.dylib",
|
| 144 |
-
core_ggml: "libggml.dylib",
|
| 145 |
-
core_llama: "libllama.dylib",
|
| 146 |
-
core_mtmd: "libmtmd.dylib",
|
| 147 |
-
plugin_prefixes: &["libggml-"],
|
| 148 |
-
library_extension: ".dylib",
|
| 149 |
-
assets: MACOS_ARM64_ASSETS,
|
| 150 |
-
};
|
| 151 |
-
|
| 152 |
-
pub async fn ensure_dylibs(path: impl AsRef<Path>) -> Result<()> {
|
| 153 |
-
let root = path.as_ref().to_path_buf();
|
| 154 |
-
let manifest = detect_manifest()?;
|
| 155 |
-
let runtime_dir = resolve_runtime_dir(&root, manifest)?;
|
| 156 |
-
|
| 157 |
-
tokio::fs::create_dir_all(&runtime_dir)
|
| 158 |
-
.await
|
| 159 |
-
.with_context(|| {
|
| 160 |
-
format!(
|
| 161 |
-
"failed to create runtime directory `{}`",
|
| 162 |
-
runtime_dir.display()
|
| 163 |
-
)
|
| 164 |
-
})?;
|
| 165 |
-
|
| 166 |
-
if stamp_matches(&runtime_dir, manifest)? && core_libraries_present(&runtime_dir, manifest) {
|
| 167 |
-
return Ok(());
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
let downloads_dir = root.join(DOWNLOADS_DIR);
|
| 171 |
-
tokio::fs::create_dir_all(&downloads_dir)
|
| 172 |
-
.await
|
| 173 |
-
.with_context(|| {
|
| 174 |
-
format!(
|
| 175 |
-
"failed to create download cache `{}`",
|
| 176 |
-
downloads_dir.display()
|
| 177 |
-
)
|
| 178 |
-
})?;
|
| 179 |
-
|
| 180 |
-
for asset in manifest.assets {
|
| 181 |
-
let archive_path = download_asset(asset, &downloads_dir).await?;
|
| 182 |
-
let extract_root = runtime_dir.clone();
|
| 183 |
-
let asset_name = asset.name;
|
| 184 |
-
tokio::task::spawn_blocking(move || extract_archive(asset, &archive_path, &extract_root))
|
| 185 |
-
.await
|
| 186 |
-
.with_context(|| format!("failed to join extraction task for `{asset_name}`"))??;
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
write_stamp(&runtime_dir, manifest)?;
|
| 190 |
-
Ok(())
|
| 191 |
-
}
|
| 192 |
-
|
| 193 |
-
pub fn initialize(path: impl AsRef<Path>) -> Result<()> {
|
| 194 |
-
let manifest = detect_manifest()?;
|
| 195 |
-
let runtime_dir = resolve_runtime_dir(path.as_ref(), manifest)?;
|
| 196 |
-
crate::sys::initialize(&runtime_dir)
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
pub(crate) fn load_plan(dir: &Path) -> Result<Vec<LoadStep>> {
|
| 200 |
-
let manifest = manifest_for_runtime_dir(dir)?;
|
| 201 |
-
let mut steps = Vec::new();
|
| 202 |
-
|
| 203 |
-
for library_name in manifest.support_libraries {
|
| 204 |
-
let library_path = dir.join(library_name);
|
| 205 |
-
if library_path.exists() {
|
| 206 |
-
steps.push(LoadStep {
|
| 207 |
-
path: library_path,
|
| 208 |
-
core: None,
|
| 209 |
-
});
|
| 210 |
-
}
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
steps.extend(scan_prefixed_libraries(
|
| 214 |
-
dir,
|
| 215 |
-
manifest,
|
| 216 |
-
manifest.support_prefixes,
|
| 217 |
-
core_library_names(manifest),
|
| 218 |
-
manifest.support_libraries,
|
| 219 |
-
)?);
|
| 220 |
-
|
| 221 |
-
steps.push(required_core_step(
|
| 222 |
-
dir,
|
| 223 |
-
manifest.core_ggml_base,
|
| 224 |
-
CoreLibraryKind::GgmlBase,
|
| 225 |
-
)?);
|
| 226 |
-
steps.push(required_core_step(
|
| 227 |
-
dir,
|
| 228 |
-
manifest.core_ggml,
|
| 229 |
-
CoreLibraryKind::Ggml,
|
| 230 |
-
)?);
|
| 231 |
-
|
| 232 |
-
steps.extend(scan_prefixed_libraries(
|
| 233 |
-
dir,
|
| 234 |
-
manifest,
|
| 235 |
-
manifest.plugin_prefixes,
|
| 236 |
-
core_library_names(manifest),
|
| 237 |
-
manifest.support_libraries,
|
| 238 |
-
)?);
|
| 239 |
-
|
| 240 |
-
steps.push(required_core_step(
|
| 241 |
-
dir,
|
| 242 |
-
manifest.core_llama,
|
| 243 |
-
CoreLibraryKind::Llama,
|
| 244 |
-
)?);
|
| 245 |
-
steps.push(required_core_step(
|
| 246 |
-
dir,
|
| 247 |
-
manifest.core_mtmd,
|
| 248 |
-
CoreLibraryKind::Mtmd,
|
| 249 |
-
)?);
|
| 250 |
-
|
| 251 |
-
Ok(steps)
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
fn detect_manifest() -> Result<&'static Manifest> {
|
| 255 |
-
let target = current_target();
|
| 256 |
-
validate_target(target)?;
|
| 257 |
-
let backend = detect_backend(target)?;
|
| 258 |
-
select_manifest(target.os, target.arch, target.env, backend)
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
fn current_target() -> TargetTriple {
|
| 262 |
-
TargetTriple {
|
| 263 |
-
os: env::consts::OS,
|
| 264 |
-
arch: env::consts::ARCH,
|
| 265 |
-
env: target_env(),
|
| 266 |
-
}
|
| 267 |
-
}
|
| 268 |
-
|
| 269 |
-
fn target_env() -> &'static str {
|
| 270 |
-
#[cfg(target_env = "msvc")]
|
| 271 |
-
{
|
| 272 |
-
"msvc"
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
#[cfg(not(target_env = "msvc"))]
|
| 276 |
-
{
|
| 277 |
-
""
|
| 278 |
-
}
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
fn validate_target(target: TargetTriple) -> Result<()> {
|
| 282 |
-
match (target.os, target.arch, target.env) {
|
| 283 |
-
("windows", "x86_64", "msvc") | ("linux", "x86_64", _) | ("macos", "aarch64", _) => Ok(()),
|
| 284 |
-
_ => bail!(
|
| 285 |
-
"unsupported koharu-llm target: target_os={}, target_arch={}, target_env={}",
|
| 286 |
-
target.os,
|
| 287 |
-
target.arch,
|
| 288 |
-
target.env
|
| 289 |
-
),
|
| 290 |
-
}
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
fn detect_backend(target: TargetTriple) -> Result<Backend> {
|
| 294 |
-
let has_cuda_driver = matches!(
|
| 295 |
-
(target.os, target.arch, target.env),
|
| 296 |
-
("windows", "x86_64", "msvc")
|
| 297 |
-
) && windows_has_cuda_driver();
|
| 298 |
-
detect_backend_for_target(target, has_cuda_driver)
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
fn detect_backend_for_target(target: TargetTriple, has_cuda_driver: bool) -> Result<Backend> {
|
| 302 |
-
match (target.os, target.arch, target.env) {
|
| 303 |
-
("windows", "x86_64", "msvc") => Ok(if has_cuda_driver {
|
| 304 |
-
Backend::Cuda
|
| 305 |
-
} else {
|
| 306 |
-
Backend::Vulkan
|
| 307 |
-
}),
|
| 308 |
-
("linux", "x86_64", _) => Ok(Backend::Vulkan),
|
| 309 |
-
("macos", "aarch64", _) => Ok(Backend::Default),
|
| 310 |
-
_ => bail!(
|
| 311 |
-
"unsupported koharu-llm target/backend detection: target_os={}, target_arch={}, target_env={}",
|
| 312 |
-
target.os,
|
| 313 |
-
target.arch,
|
| 314 |
-
target.env
|
| 315 |
-
),
|
| 316 |
-
}
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
fn windows_has_cuda_driver() -> bool {
|
| 320 |
-
unsafe { Library::new("nvcuda.dll") }.is_ok()
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
fn select_manifest(
|
| 324 |
-
target_os: &str,
|
| 325 |
-
target_arch: &str,
|
| 326 |
-
target_env: &str,
|
| 327 |
-
backend: Backend,
|
| 328 |
-
) -> Result<&'static Manifest> {
|
| 329 |
-
match (target_os, target_arch, target_env, backend) {
|
| 330 |
-
("windows", "x86_64", "msvc", Backend::Cuda) => Ok(&WINDOWS_CUDA_MANIFEST),
|
| 331 |
-
("windows", "x86_64", "msvc", Backend::Vulkan) => Ok(&WINDOWS_VULKAN_MANIFEST),
|
| 332 |
-
("linux", "x86_64", _, Backend::Vulkan) => Ok(&LINUX_VULKAN_MANIFEST),
|
| 333 |
-
("macos", "aarch64", _, Backend::Default) => Ok(&MACOS_ARM64_MANIFEST),
|
| 334 |
-
_ => bail!(
|
| 335 |
-
"unsupported koharu-llm target/backend combination: target_os={target_os}, target_arch={target_arch}, target_env={target_env}, backend={backend:?}"
|
| 336 |
-
),
|
| 337 |
-
}
|
| 338 |
-
}
|
| 339 |
-
|
| 340 |
-
fn resolve_runtime_dir(root: &Path, manifest: &Manifest) -> Result<PathBuf> {
|
| 341 |
-
if is_runtime_dir(root, manifest)? {
|
| 342 |
-
Ok(root.to_path_buf())
|
| 343 |
-
} else {
|
| 344 |
-
Ok(root.join(manifest.id))
|
| 345 |
-
}
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
fn is_runtime_dir(dir: &Path, manifest: &Manifest) -> Result<bool> {
|
| 349 |
-
if dir.file_name().and_then(OsStr::to_str) == Some(manifest.id) {
|
| 350 |
-
return Ok(true);
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
if let Some(existing_manifest) = manifest_from_stamp(dir)? {
|
| 354 |
-
return Ok(existing_manifest.id == manifest.id);
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
-
Ok(core_libraries_present(dir, manifest))
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
fn manifest_for_runtime_dir(dir: &Path) -> Result<&'static Manifest> {
|
| 361 |
-
if let Some(manifest) = manifest_from_stamp(dir)? {
|
| 362 |
-
return Ok(manifest);
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
if let Some(manifest) = dir
|
| 366 |
-
.file_name()
|
| 367 |
-
.and_then(OsStr::to_str)
|
| 368 |
-
.and_then(manifest_by_id)
|
| 369 |
-
{
|
| 370 |
-
return Ok(manifest);
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
let manifest = detect_manifest()?;
|
| 374 |
-
if core_libraries_present(dir, manifest) {
|
| 375 |
-
return Ok(manifest);
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
bail!(
|
| 379 |
-
"failed to determine the koharu-llm runtime manifest for `{}`",
|
| 380 |
-
dir.display()
|
| 381 |
-
)
|
| 382 |
-
}
|
| 383 |
-
|
| 384 |
-
fn manifest_from_stamp(dir: &Path) -> Result<Option<&'static Manifest>> {
|
| 385 |
-
let stamp_path = dir.join(STAMP_FILE);
|
| 386 |
-
if !stamp_path.exists() {
|
| 387 |
-
return Ok(None);
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
let contents = fs::read_to_string(&stamp_path)
|
| 391 |
-
.with_context(|| format!("failed to read `{}`", stamp_path.display()))?;
|
| 392 |
-
Ok(parse_manifest_from_stamp(&contents))
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
fn parse_manifest_from_stamp(contents: &str) -> Option<&'static Manifest> {
|
| 396 |
-
let mut lines = contents.lines();
|
| 397 |
-
let _tag = lines.next()?;
|
| 398 |
-
let id = lines.next()?;
|
| 399 |
-
manifest_by_id(id)
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
fn manifest_by_id(id: &str) -> Option<&'static Manifest> {
|
| 403 |
-
if id == WINDOWS_CUDA_MANIFEST.id {
|
| 404 |
-
Some(&WINDOWS_CUDA_MANIFEST)
|
| 405 |
-
} else if id == WINDOWS_VULKAN_MANIFEST.id {
|
| 406 |
-
Some(&WINDOWS_VULKAN_MANIFEST)
|
| 407 |
-
} else if id == LINUX_VULKAN_MANIFEST.id {
|
| 408 |
-
Some(&LINUX_VULKAN_MANIFEST)
|
| 409 |
-
} else if id == MACOS_ARM64_MANIFEST.id {
|
| 410 |
-
Some(&MACOS_ARM64_MANIFEST)
|
| 411 |
-
} else {
|
| 412 |
-
None
|
| 413 |
-
}
|
| 414 |
-
}
|
| 415 |
-
|
| 416 |
-
fn core_libraries_present(dir: &Path, manifest: &Manifest) -> bool {
|
| 417 |
-
core_library_names(manifest)
|
| 418 |
-
.iter()
|
| 419 |
-
.all(|name| dir.join(name).exists())
|
| 420 |
-
}
|
| 421 |
-
|
| 422 |
-
fn core_library_names(manifest: &Manifest) -> [&'static str; 4] {
|
| 423 |
-
[
|
| 424 |
-
manifest.core_ggml_base,
|
| 425 |
-
manifest.core_ggml,
|
| 426 |
-
manifest.core_llama,
|
| 427 |
-
manifest.core_mtmd,
|
| 428 |
-
]
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
fn required_core_step(
|
| 432 |
-
dir: &Path,
|
| 433 |
-
library_name: &'static str,
|
| 434 |
-
core: CoreLibraryKind,
|
| 435 |
-
) -> Result<LoadStep> {
|
| 436 |
-
let path = dir.join(library_name);
|
| 437 |
-
if !path.exists() {
|
| 438 |
-
bail!(
|
| 439 |
-
"required runtime library `{}` is missing from `{}`; run `koharu_llm::runtime::ensure_dylibs` first",
|
| 440 |
-
library_name,
|
| 441 |
-
dir.display()
|
| 442 |
-
);
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
Ok(LoadStep {
|
| 446 |
-
path,
|
| 447 |
-
core: Some(core),
|
| 448 |
-
})
|
| 449 |
-
}
|
| 450 |
-
|
| 451 |
-
fn scan_prefixed_libraries(
|
| 452 |
-
dir: &Path,
|
| 453 |
-
manifest: &Manifest,
|
| 454 |
-
prefixes: &[&str],
|
| 455 |
-
core_names: [&str; 4],
|
| 456 |
-
explicit_support_names: &[&str],
|
| 457 |
-
) -> Result<Vec<LoadStep>> {
|
| 458 |
-
let mut paths = Vec::new();
|
| 459 |
-
|
| 460 |
-
for entry in fs::read_dir(dir)
|
| 461 |
-
.with_context(|| format!("failed to read runtime directory `{}`", dir.display()))?
|
| 462 |
-
{
|
| 463 |
-
let entry = entry.context("failed to read runtime directory entry")?;
|
| 464 |
-
let path = entry.path();
|
| 465 |
-
if !path.is_file() {
|
| 466 |
-
continue;
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
let Some(file_name) = path.file_name().and_then(OsStr::to_str) else {
|
| 470 |
-
continue;
|
| 471 |
-
};
|
| 472 |
-
if !file_name.ends_with(manifest.library_extension) {
|
| 473 |
-
continue;
|
| 474 |
-
}
|
| 475 |
-
if core_names.contains(&file_name) || explicit_support_names.contains(&file_name) {
|
| 476 |
-
continue;
|
| 477 |
-
}
|
| 478 |
-
if prefixes.iter().any(|prefix| file_name.starts_with(prefix)) {
|
| 479 |
-
paths.push(LoadStep { path, core: None });
|
| 480 |
-
}
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
paths.sort_by(|left, right| left.path.cmp(&right.path));
|
| 484 |
-
Ok(paths)
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
fn release_asset_url(asset_name: &str) -> String {
|
| 488 |
-
format!("{RELEASE_BASE_URL}/{LLAMA_CPP_TAG}/{asset_name}")
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
async fn download_asset(asset: &ReleaseAsset, downloads_dir: &Path) -> Result<PathBuf> {
|
| 492 |
-
let archive_path = downloads_dir.join(asset.name);
|
| 493 |
-
if archive_path.exists() {
|
| 494 |
-
return Ok(archive_path);
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
let partial_path = downloads_dir.join(format!("{}.partial", asset.name));
|
| 498 |
-
let url = release_asset_url(asset.name);
|
| 499 |
-
let response = reqwest::Client::builder()
|
| 500 |
-
.user_agent("koharu-llm-runtime")
|
| 501 |
-
.build()
|
| 502 |
-
.context("failed to build reqwest client")?
|
| 503 |
-
.get(&url)
|
| 504 |
-
.send()
|
| 505 |
-
.await
|
| 506 |
-
.with_context(|| format!("failed to download `{url}`"))?
|
| 507 |
-
.error_for_status()
|
| 508 |
-
.with_context(|| format!("download failed for `{url}`"))?;
|
| 509 |
-
|
| 510 |
-
let mut file = tokio::fs::File::create(&partial_path)
|
| 511 |
-
.await
|
| 512 |
-
.with_context(|| format!("failed to create `{}`", partial_path.display()))?;
|
| 513 |
-
let mut stream = response.bytes_stream();
|
| 514 |
-
|
| 515 |
-
while let Some(chunk) = stream.next().await {
|
| 516 |
-
let chunk = chunk.with_context(|| format!("failed while downloading `{url}`"))?;
|
| 517 |
-
file.write_all(&chunk)
|
| 518 |
-
.await
|
| 519 |
-
.with_context(|| format!("failed to write `{}`", partial_path.display()))?;
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
file.flush()
|
| 523 |
-
.await
|
| 524 |
-
.with_context(|| format!("failed to flush `{}`", partial_path.display()))?;
|
| 525 |
-
drop(file);
|
| 526 |
-
|
| 527 |
-
tokio::fs::rename(&partial_path, &archive_path)
|
| 528 |
-
.await
|
| 529 |
-
.with_context(|| format!("failed to finalize `{}`", archive_path.display()))?;
|
| 530 |
-
|
| 531 |
-
Ok(archive_path)
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
fn extract_archive(asset: &ReleaseAsset, archive_path: &Path, output_dir: &Path) -> Result<()> {
|
| 535 |
-
match asset.kind {
|
| 536 |
-
ArchiveKind::Zip => extract_zip_archive(archive_path, output_dir),
|
| 537 |
-
ArchiveKind::TarGz => extract_tar_archive(archive_path, output_dir),
|
| 538 |
-
}
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
fn extract_zip_archive(archive_path: &Path, output_dir: &Path) -> Result<()> {
|
| 542 |
-
let file = fs::File::open(archive_path)
|
| 543 |
-
.with_context(|| format!("failed to open `{}`", archive_path.display()))?;
|
| 544 |
-
let mut archive = ZipArchive::new(file)
|
| 545 |
-
.with_context(|| format!("failed to read zip archive `{}`", archive_path.display()))?;
|
| 546 |
-
|
| 547 |
-
for index in 0..archive.len() {
|
| 548 |
-
let mut entry = archive.by_index(index).with_context(|| {
|
| 549 |
-
format!(
|
| 550 |
-
"failed to read zip entry {index} from `{}`",
|
| 551 |
-
archive_path.display()
|
| 552 |
-
)
|
| 553 |
-
})?;
|
| 554 |
-
if entry.is_dir() {
|
| 555 |
-
continue;
|
| 556 |
-
}
|
| 557 |
-
|
| 558 |
-
let Some(file_name) = Path::new(entry.name())
|
| 559 |
-
.file_name()
|
| 560 |
-
.and_then(OsStr::to_str)
|
| 561 |
-
.map(ToOwned::to_owned)
|
| 562 |
-
else {
|
| 563 |
-
continue;
|
| 564 |
-
};
|
| 565 |
-
if !looks_like_runtime_library(&file_name) {
|
| 566 |
-
continue;
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
let out_path = output_dir.join(&file_name);
|
| 570 |
-
let mut out_file = fs::File::create(&out_path)
|
| 571 |
-
.with_context(|| format!("failed to create `{}`", out_path.display()))?;
|
| 572 |
-
io::copy(&mut entry, &mut out_file)
|
| 573 |
-
.with_context(|| format!("failed to extract `{}`", out_path.display()))?;
|
| 574 |
-
}
|
| 575 |
-
|
| 576 |
-
Ok(())
|
| 577 |
-
}
|
| 578 |
-
|
| 579 |
-
fn extract_tar_archive(archive_path: &Path, output_dir: &Path) -> Result<()> {
|
| 580 |
-
let file = fs::File::open(archive_path)
|
| 581 |
-
.with_context(|| format!("failed to open `{}`", archive_path.display()))?;
|
| 582 |
-
let decoder = GzDecoder::new(file);
|
| 583 |
-
let mut archive = tar::Archive::new(decoder);
|
| 584 |
-
let mut aliases = Vec::new();
|
| 585 |
-
|
| 586 |
-
for entry in archive.entries().with_context(|| {
|
| 587 |
-
format!(
|
| 588 |
-
"failed to read tar entries from `{}`",
|
| 589 |
-
archive_path.display()
|
| 590 |
-
)
|
| 591 |
-
})? {
|
| 592 |
-
let mut entry = entry.context("failed to read tar entry")?;
|
| 593 |
-
let path = entry.path().context("failed to read tar entry path")?;
|
| 594 |
-
let Some(file_name) = path
|
| 595 |
-
.file_name()
|
| 596 |
-
.and_then(OsStr::to_str)
|
| 597 |
-
.map(ToOwned::to_owned)
|
| 598 |
-
else {
|
| 599 |
-
continue;
|
| 600 |
-
};
|
| 601 |
-
if !looks_like_runtime_library(&file_name) {
|
| 602 |
-
continue;
|
| 603 |
-
}
|
| 604 |
-
|
| 605 |
-
let entry_type = entry.header().entry_type();
|
| 606 |
-
if entry_type.is_symlink() {
|
| 607 |
-
let Some(target_name) = entry
|
| 608 |
-
.link_name()
|
| 609 |
-
.context("failed to read tar symlink target")?
|
| 610 |
-
.and_then(|target| target.file_name().map(ToOwned::to_owned))
|
| 611 |
-
.and_then(|name| name.to_str().map(ToOwned::to_owned))
|
| 612 |
-
else {
|
| 613 |
-
continue;
|
| 614 |
-
};
|
| 615 |
-
aliases.push((output_dir.join(file_name), output_dir.join(target_name)));
|
| 616 |
-
continue;
|
| 617 |
-
}
|
| 618 |
-
|
| 619 |
-
if !entry_type.is_file() {
|
| 620 |
-
continue;
|
| 621 |
-
}
|
| 622 |
-
|
| 623 |
-
let out_path = output_dir.join(&file_name);
|
| 624 |
-
let mut out_file = fs::File::create(&out_path)
|
| 625 |
-
.with_context(|| format!("failed to create `{}`", out_path.display()))?;
|
| 626 |
-
io::copy(&mut entry, &mut out_file)
|
| 627 |
-
.with_context(|| format!("failed to extract `{}`", out_path.display()))?;
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
materialize_aliases(&aliases)?;
|
| 631 |
-
Ok(())
|
| 632 |
-
}
|
| 633 |
-
|
| 634 |
-
fn materialize_aliases(aliases: &[(PathBuf, PathBuf)]) -> Result<()> {
|
| 635 |
-
let mut pending = aliases.to_vec();
|
| 636 |
-
|
| 637 |
-
while !pending.is_empty() {
|
| 638 |
-
let mut progressed = false;
|
| 639 |
-
let mut next = Vec::new();
|
| 640 |
-
|
| 641 |
-
for (alias_path, target_path) in pending {
|
| 642 |
-
if alias_path.exists() {
|
| 643 |
-
progressed = true;
|
| 644 |
-
continue;
|
| 645 |
-
}
|
| 646 |
-
if !target_path.exists() {
|
| 647 |
-
next.push((alias_path, target_path));
|
| 648 |
-
continue;
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
match fs::hard_link(&target_path, &alias_path) {
|
| 652 |
-
Ok(()) => {}
|
| 653 |
-
Err(_) => {
|
| 654 |
-
fs::copy(&target_path, &alias_path).with_context(|| {
|
| 655 |
-
format!(
|
| 656 |
-
"failed to materialize alias `{}` from `{}`",
|
| 657 |
-
alias_path.display(),
|
| 658 |
-
target_path.display()
|
| 659 |
-
)
|
| 660 |
-
})?;
|
| 661 |
-
}
|
| 662 |
-
}
|
| 663 |
-
progressed = true;
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
if !progressed {
|
| 667 |
-
let unresolved = next
|
| 668 |
-
.into_iter()
|
| 669 |
-
.map(|(alias, target)| format!("{} -> {}", alias.display(), target.display()))
|
| 670 |
-
.collect::<Vec<_>>()
|
| 671 |
-
.join(", ");
|
| 672 |
-
bail!("failed to materialize runtime library aliases: {unresolved}");
|
| 673 |
-
}
|
| 674 |
-
|
| 675 |
-
pending = next;
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
Ok(())
|
| 679 |
-
}
|
| 680 |
-
|
| 681 |
-
fn looks_like_runtime_library(file_name: &str) -> bool {
|
| 682 |
-
file_name.ends_with(".dll")
|
| 683 |
-
|| file_name.ends_with(".so")
|
| 684 |
-
|| file_name.contains(".so.")
|
| 685 |
-
|| file_name.ends_with(".dylib")
|
| 686 |
-
|| file_name.contains(".dylib.")
|
| 687 |
-
}
|
| 688 |
-
|
| 689 |
-
fn stamp_matches(root: &Path, manifest: &Manifest) -> Result<bool> {
|
| 690 |
-
let stamp_path = root.join(STAMP_FILE);
|
| 691 |
-
if !stamp_path.exists() {
|
| 692 |
-
return Ok(false);
|
| 693 |
-
}
|
| 694 |
-
|
| 695 |
-
let contents = fs::read_to_string(&stamp_path)
|
| 696 |
-
.with_context(|| format!("failed to read `{}`", stamp_path.display()))?;
|
| 697 |
-
Ok(contents == manifest_stamp(manifest))
|
| 698 |
-
}
|
| 699 |
-
|
| 700 |
-
fn write_stamp(root: &Path, manifest: &Manifest) -> Result<()> {
|
| 701 |
-
let stamp_path = root.join(STAMP_FILE);
|
| 702 |
-
fs::write(&stamp_path, manifest_stamp(manifest))
|
| 703 |
-
.with_context(|| format!("failed to write `{}`", stamp_path.display()))?;
|
| 704 |
-
Ok(())
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
fn manifest_stamp(manifest: &Manifest) -> String {
|
| 708 |
-
let mut stamp = format!("{LLAMA_CPP_TAG}\n{}\n", manifest.id);
|
| 709 |
-
for asset in manifest.assets {
|
| 710 |
-
stamp.push_str(asset.name);
|
| 711 |
-
stamp.push('\n');
|
| 712 |
-
}
|
| 713 |
-
stamp
|
| 714 |
-
}
|
| 715 |
-
|
| 716 |
-
#[cfg(test)]
|
| 717 |
-
mod tests {
|
| 718 |
-
use super::*;
|
| 719 |
-
|
| 720 |
-
fn touch(path: &Path) {
|
| 721 |
-
fs::write(path, b"ok").unwrap();
|
| 722 |
-
}
|
| 723 |
-
|
| 724 |
-
#[test]
|
| 725 |
-
fn selects_supported_manifests() {
|
| 726 |
-
assert_eq!(
|
| 727 |
-
select_manifest("windows", "x86_64", "msvc", Backend::Cuda)
|
| 728 |
-
.unwrap()
|
| 729 |
-
.id,
|
| 730 |
-
WINDOWS_CUDA_MANIFEST.id
|
| 731 |
-
);
|
| 732 |
-
assert_eq!(
|
| 733 |
-
select_manifest("windows", "x86_64", "msvc", Backend::Vulkan)
|
| 734 |
-
.unwrap()
|
| 735 |
-
.id,
|
| 736 |
-
WINDOWS_VULKAN_MANIFEST.id
|
| 737 |
-
);
|
| 738 |
-
assert_eq!(
|
| 739 |
-
select_manifest("linux", "x86_64", "", Backend::Vulkan)
|
| 740 |
-
.unwrap()
|
| 741 |
-
.id,
|
| 742 |
-
LINUX_VULKAN_MANIFEST.id
|
| 743 |
-
);
|
| 744 |
-
assert_eq!(
|
| 745 |
-
select_manifest("macos", "aarch64", "", Backend::Default)
|
| 746 |
-
.unwrap()
|
| 747 |
-
.id,
|
| 748 |
-
MACOS_ARM64_MANIFEST.id
|
| 749 |
-
);
|
| 750 |
-
}
|
| 751 |
-
|
| 752 |
-
#[test]
|
| 753 |
-
fn detects_backends_for_supported_targets() {
|
| 754 |
-
let windows = TargetTriple {
|
| 755 |
-
os: "windows",
|
| 756 |
-
arch: "x86_64",
|
| 757 |
-
env: "msvc",
|
| 758 |
-
};
|
| 759 |
-
let linux = TargetTriple {
|
| 760 |
-
os: "linux",
|
| 761 |
-
arch: "x86_64",
|
| 762 |
-
env: "",
|
| 763 |
-
};
|
| 764 |
-
let macos = TargetTriple {
|
| 765 |
-
os: "macos",
|
| 766 |
-
arch: "aarch64",
|
| 767 |
-
env: "",
|
| 768 |
-
};
|
| 769 |
-
|
| 770 |
-
assert_eq!(
|
| 771 |
-
detect_backend_for_target(windows, true).unwrap(),
|
| 772 |
-
Backend::Cuda
|
| 773 |
-
);
|
| 774 |
-
assert_eq!(
|
| 775 |
-
detect_backend_for_target(windows, false).unwrap(),
|
| 776 |
-
Backend::Vulkan
|
| 777 |
-
);
|
| 778 |
-
assert_eq!(
|
| 779 |
-
detect_backend_for_target(linux, true).unwrap(),
|
| 780 |
-
Backend::Vulkan
|
| 781 |
-
);
|
| 782 |
-
assert_eq!(
|
| 783 |
-
detect_backend_for_target(macos, true).unwrap(),
|
| 784 |
-
Backend::Default
|
| 785 |
-
);
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
#[test]
|
| 789 |
-
fn rejects_unsupported_manifest() {
|
| 790 |
-
assert!(select_manifest("linux", "x86_64", "", Backend::Cuda).is_err());
|
| 791 |
-
assert!(select_manifest("windows", "aarch64", "msvc", Backend::Cuda).is_err());
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
#[test]
|
| 795 |
-
fn resolves_manifest_from_stamp() {
|
| 796 |
-
assert_eq!(
|
| 797 |
-
parse_manifest_from_stamp(&manifest_stamp(&WINDOWS_VULKAN_MANIFEST))
|
| 798 |
-
.unwrap()
|
| 799 |
-
.id,
|
| 800 |
-
WINDOWS_VULKAN_MANIFEST.id
|
| 801 |
-
);
|
| 802 |
-
}
|
| 803 |
-
|
| 804 |
-
#[test]
|
| 805 |
-
fn manifest_stamp_is_stable() {
|
| 806 |
-
let stamp = manifest_stamp(&WINDOWS_CUDA_MANIFEST);
|
| 807 |
-
assert!(stamp.contains(LLAMA_CPP_TAG));
|
| 808 |
-
assert!(stamp.contains("windows-cuda13-x64"));
|
| 809 |
-
assert!(stamp.contains("llama-b8233-bin-win-cuda-13.1-x64.zip"));
|
| 810 |
-
}
|
| 811 |
-
|
| 812 |
-
#[test]
|
| 813 |
-
fn materializes_aliases_by_copy() {
|
| 814 |
-
let tempdir = tempfile::tempdir().unwrap();
|
| 815 |
-
let target = tempdir.path().join("libllama.so.0.0.8233");
|
| 816 |
-
let alias = tempdir.path().join("libllama.so");
|
| 817 |
-
|
| 818 |
-
touch(&target);
|
| 819 |
-
materialize_aliases(&[(alias.clone(), target.clone())]).unwrap();
|
| 820 |
-
|
| 821 |
-
assert!(alias.exists());
|
| 822 |
-
assert_eq!(fs::read(alias).unwrap(), fs::read(target).unwrap());
|
| 823 |
-
}
|
| 824 |
-
|
| 825 |
-
#[test]
|
| 826 |
-
fn load_plan_orders_core_and_plugins() {
|
| 827 |
-
let tempdir = tempfile::tempdir().unwrap();
|
| 828 |
-
let root = tempdir.path();
|
| 829 |
-
let manifest = &WINDOWS_CUDA_MANIFEST;
|
| 830 |
-
|
| 831 |
-
for core in core_library_names(manifest) {
|
| 832 |
-
touch(&root.join(core));
|
| 833 |
-
}
|
| 834 |
-
for support in manifest.support_libraries {
|
| 835 |
-
touch(&root.join(support));
|
| 836 |
-
}
|
| 837 |
-
touch(&root.join("libomp140.x86_64.dll"));
|
| 838 |
-
touch(&root.join("ggml-cuda.dll"));
|
| 839 |
-
touch(&root.join("ggml-rpc.dll"));
|
| 840 |
-
write_stamp(root, manifest).unwrap();
|
| 841 |
-
|
| 842 |
-
let steps = load_plan(root).unwrap();
|
| 843 |
-
let names = steps
|
| 844 |
-
.iter()
|
| 845 |
-
.map(|step| {
|
| 846 |
-
step.path
|
| 847 |
-
.file_name()
|
| 848 |
-
.unwrap()
|
| 849 |
-
.to_string_lossy()
|
| 850 |
-
.into_owned()
|
| 851 |
-
})
|
| 852 |
-
.collect::<Vec<_>>();
|
| 853 |
-
|
| 854 |
-
assert_eq!(names[0], "cudart64_13.dll");
|
| 855 |
-
assert_eq!(names[1], "cublasLt64_13.dll");
|
| 856 |
-
assert_eq!(names[2], "cublas64_13.dll");
|
| 857 |
-
assert_eq!(names[3], "libomp140.x86_64.dll");
|
| 858 |
-
assert_eq!(names[4], "ggml-base.dll");
|
| 859 |
-
assert_eq!(names[5], "ggml.dll");
|
| 860 |
-
assert!(names[6..names.len() - 2].contains(&"ggml-cuda.dll".to_string()));
|
| 861 |
-
assert_eq!(names[names.len() - 2], "llama.dll");
|
| 862 |
-
assert_eq!(names[names.len() - 1], "mtmd.dll");
|
| 863 |
-
}
|
| 864 |
-
|
| 865 |
-
#[tokio::test]
|
| 866 |
-
#[ignore]
|
| 867 |
-
async fn downloads_runtime_assets() -> Result<()> {
|
| 868 |
-
let tempdir = tempfile::tempdir()?;
|
| 869 |
-
ensure_dylibs(tempdir.path()).await?;
|
| 870 |
-
let manifest = detect_manifest()?;
|
| 871 |
-
let runtime_dir = resolve_runtime_dir(tempdir.path(), manifest)?;
|
| 872 |
-
assert!(core_libraries_present(&runtime_dir, manifest));
|
| 873 |
-
Ok(())
|
| 874 |
-
}
|
| 875 |
-
|
| 876 |
-
#[tokio::test]
|
| 877 |
-
#[ignore]
|
| 878 |
-
async fn initializes_runtime_assets() -> Result<()> {
|
| 879 |
-
let tempdir = tempfile::tempdir()?;
|
| 880 |
-
ensure_dylibs(tempdir.path()).await?;
|
| 881 |
-
initialize(tempdir.path())?;
|
| 882 |
-
Ok(())
|
| 883 |
-
}
|
| 884 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
koharu-llm/src/sys/mod.rs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
| 1 |
use std::ffi::CString;
|
| 2 |
use std::path::{Path, PathBuf};
|
|
|
|
| 3 |
|
| 4 |
use anyhow::{Context, Result, anyhow, bail};
|
| 5 |
-
use libloading::Library;
|
| 6 |
-
use once_cell::sync::OnceCell;
|
| 7 |
-
|
| 8 |
-
use crate::safe::runtime::{CoreLibraryKind, LoadStep};
|
| 9 |
|
| 10 |
#[allow(warnings)]
|
| 11 |
mod generated {
|
|
@@ -17,28 +14,24 @@ mod generated {
|
|
| 17 |
#[allow(warnings)]
|
| 18 |
pub mod llama {
|
| 19 |
use super::types::*;
|
| 20 |
-
|
| 21 |
include!(concat!(env!("OUT_DIR"), "/llama_loader.rs"));
|
| 22 |
}
|
| 23 |
|
| 24 |
#[allow(warnings)]
|
| 25 |
pub mod ggml {
|
| 26 |
use super::types::*;
|
| 27 |
-
|
| 28 |
include!(concat!(env!("OUT_DIR"), "/ggml_loader.rs"));
|
| 29 |
}
|
| 30 |
|
| 31 |
#[allow(warnings)]
|
| 32 |
pub mod ggml_base {
|
| 33 |
use super::types::*;
|
| 34 |
-
|
| 35 |
include!(concat!(env!("OUT_DIR"), "/ggml_base_loader.rs"));
|
| 36 |
}
|
| 37 |
|
| 38 |
#[allow(warnings)]
|
| 39 |
pub mod mtmd {
|
| 40 |
use super::types::*;
|
| 41 |
-
|
| 42 |
include!(concat!(env!("OUT_DIR"), "/mtmd_loader.rs"));
|
| 43 |
}
|
| 44 |
}
|
|
@@ -47,37 +40,56 @@ pub use generated::types::*;
|
|
| 47 |
|
| 48 |
struct LoadedLibraries {
|
| 49 |
path: PathBuf,
|
| 50 |
-
auxiliary: Vec<Library>,
|
| 51 |
llama: generated::llama::llama,
|
| 52 |
ggml: generated::ggml::ggml,
|
| 53 |
ggml_base: generated::ggml_base::ggml_base,
|
| 54 |
mtmd: generated::mtmd::mtmd,
|
| 55 |
}
|
| 56 |
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
}
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
return Ok(());
|
| 69 |
}
|
| 70 |
-
|
| 71 |
bail!(
|
| 72 |
"koharu-llm is already initialized with `{}` and cannot be reinitialized with `{}`",
|
| 73 |
-
|
| 74 |
-
|
| 75 |
);
|
| 76 |
}
|
| 77 |
|
| 78 |
-
let
|
| 79 |
-
|
| 80 |
-
register_backends(&libraries.ggml, &canonical_dir)?;
|
| 81 |
|
| 82 |
LIBRARIES
|
| 83 |
.set(libraries)
|
|
@@ -86,80 +98,44 @@ pub(crate) fn initialize(dir: &Path) -> Result<()> {
|
|
| 86 |
Ok(())
|
| 87 |
}
|
| 88 |
|
| 89 |
-
fn
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
fn load_libraries(dir: &Path, load_plan: &[LoadStep]) -> Result<LoadedLibraries> {
|
| 106 |
-
let mut auxiliary = Vec::new();
|
| 107 |
-
let mut llama = None;
|
| 108 |
-
let mut ggml = None;
|
| 109 |
-
let mut ggml_base = None;
|
| 110 |
-
let mut mtmd = None;
|
| 111 |
-
|
| 112 |
-
for step in load_plan {
|
| 113 |
-
let library = load_library(&step.path)?;
|
| 114 |
-
|
| 115 |
-
match step.core {
|
| 116 |
-
Some(CoreLibraryKind::Llama) => {
|
| 117 |
-
llama = Some(
|
| 118 |
-
unsafe { generated::llama::llama::from_library(library) }
|
| 119 |
-
.with_context(|| format!("failed to bind `{}`", step.path.display()))?,
|
| 120 |
-
);
|
| 121 |
-
}
|
| 122 |
-
Some(CoreLibraryKind::Ggml) => {
|
| 123 |
-
ggml = Some(
|
| 124 |
-
unsafe { generated::ggml::ggml::from_library(library) }
|
| 125 |
-
.with_context(|| format!("failed to bind `{}`", step.path.display()))?,
|
| 126 |
-
);
|
| 127 |
-
}
|
| 128 |
-
Some(CoreLibraryKind::GgmlBase) => {
|
| 129 |
-
ggml_base = Some(
|
| 130 |
-
unsafe { generated::ggml_base::ggml_base::from_library(library) }
|
| 131 |
-
.with_context(|| format!("failed to bind `{}`", step.path.display()))?,
|
| 132 |
-
);
|
| 133 |
-
}
|
| 134 |
-
Some(CoreLibraryKind::Mtmd) => {
|
| 135 |
-
mtmd = Some(
|
| 136 |
-
unsafe { generated::mtmd::mtmd::from_library(library) }
|
| 137 |
-
.with_context(|| format!("failed to bind `{}`", step.path.display()))?,
|
| 138 |
-
);
|
| 139 |
-
}
|
| 140 |
-
None => auxiliary.push(library),
|
| 141 |
-
}
|
| 142 |
-
}
|
| 143 |
|
| 144 |
Ok(LoadedLibraries {
|
| 145 |
path: dir.to_path_buf(),
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
.ok_or_else(|| anyhow!("core ggml-base runtime library was not loaded"))?,
|
| 151 |
-
mtmd: mtmd.ok_or_else(|| anyhow!("core mtmd runtime library was not loaded"))?,
|
| 152 |
})
|
| 153 |
}
|
| 154 |
|
| 155 |
-
fn
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
fn register_backends(ggml: &generated::ggml::ggml, dir: &Path) -> Result<()> {
|
| 160 |
let dir = dir
|
| 161 |
.to_str()
|
| 162 |
-
.ok_or_else(|| anyhow!("runtime directory
|
| 163 |
let dir = CString::new(dir).context("runtime directory contains an interior null byte")?;
|
| 164 |
|
| 165 |
unsafe {
|
|
@@ -171,12 +147,11 @@ fn register_backends(ggml: &generated::ggml::ggml, dir: &Path) -> Result<()> {
|
|
| 171 |
|
| 172 |
fn libraries() -> &'static LoadedLibraries {
|
| 173 |
LIBRARIES.get().expect(
|
| 174 |
-
"koharu-llm runtime libraries are not initialized; call `
|
| 175 |
)
|
| 176 |
}
|
| 177 |
|
| 178 |
fn llama_lib() -> &'static generated::llama::llama {
|
| 179 |
-
let _ = libraries().auxiliary.len();
|
| 180 |
&libraries().llama
|
| 181 |
}
|
| 182 |
|
|
@@ -195,7 +170,6 @@ fn mtmd_lib() -> &'static generated::mtmd::mtmd {
|
|
| 195 |
#[allow(warnings)]
|
| 196 |
mod wrappers {
|
| 197 |
use super::*;
|
| 198 |
-
|
| 199 |
include!(concat!(env!("OUT_DIR"), "/wrappers.rs"));
|
| 200 |
}
|
| 201 |
|
|
|
|
| 1 |
use std::ffi::CString;
|
| 2 |
use std::path::{Path, PathBuf};
|
| 3 |
+
use std::sync::OnceLock;
|
| 4 |
|
| 5 |
use anyhow::{Context, Result, anyhow, bail};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
#[allow(warnings)]
|
| 8 |
mod generated {
|
|
|
|
| 14 |
#[allow(warnings)]
|
| 15 |
pub mod llama {
|
| 16 |
use super::types::*;
|
|
|
|
| 17 |
include!(concat!(env!("OUT_DIR"), "/llama_loader.rs"));
|
| 18 |
}
|
| 19 |
|
| 20 |
#[allow(warnings)]
|
| 21 |
pub mod ggml {
|
| 22 |
use super::types::*;
|
|
|
|
| 23 |
include!(concat!(env!("OUT_DIR"), "/ggml_loader.rs"));
|
| 24 |
}
|
| 25 |
|
| 26 |
#[allow(warnings)]
|
| 27 |
pub mod ggml_base {
|
| 28 |
use super::types::*;
|
|
|
|
| 29 |
include!(concat!(env!("OUT_DIR"), "/ggml_base_loader.rs"));
|
| 30 |
}
|
| 31 |
|
| 32 |
#[allow(warnings)]
|
| 33 |
pub mod mtmd {
|
| 34 |
use super::types::*;
|
|
|
|
| 35 |
include!(concat!(env!("OUT_DIR"), "/mtmd_loader.rs"));
|
| 36 |
}
|
| 37 |
}
|
|
|
|
| 40 |
|
| 41 |
struct LoadedLibraries {
|
| 42 |
path: PathBuf,
|
|
|
|
| 43 |
llama: generated::llama::llama,
|
| 44 |
ggml: generated::ggml::ggml,
|
| 45 |
ggml_base: generated::ggml_base::ggml_base,
|
| 46 |
mtmd: generated::mtmd::mtmd,
|
| 47 |
}
|
| 48 |
|
| 49 |
+
#[cfg(target_os = "windows")]
|
| 50 |
+
const LIB_NAMES: [&str; 4] = ["ggml-base.dll", "ggml.dll", "llama.dll", "mtmd.dll"];
|
| 51 |
|
| 52 |
+
#[cfg(target_os = "linux")]
|
| 53 |
+
const LIB_NAMES: [&str; 4] = ["libggml-base.so", "libggml.so", "libllama.so", "libmtmd.so"];
|
|
|
|
| 54 |
|
| 55 |
+
#[cfg(target_os = "macos")]
|
| 56 |
+
const LIB_NAMES: [&str; 4] = [
|
| 57 |
+
"libggml-base.dylib",
|
| 58 |
+
"libggml.dylib",
|
| 59 |
+
"libllama.dylib",
|
| 60 |
+
"libmtmd.dylib",
|
| 61 |
+
];
|
| 62 |
+
|
| 63 |
+
static LIBRARIES: OnceLock<LoadedLibraries> = OnceLock::new();
|
| 64 |
+
|
| 65 |
+
pub(crate) fn initialize() -> Result<()> {
|
| 66 |
+
let runtime_dir = koharu_runtime::llama_runtime_dir()
|
| 67 |
+
.context("failed to resolve the llama runtime directory; call `koharu_runtime::initialize()` first")?;
|
| 68 |
+
|
| 69 |
+
if !runtime_dir.exists() {
|
| 70 |
+
bail!(
|
| 71 |
+
"runtime directory `{}` does not exist; call `koharu_runtime::initialize()` first",
|
| 72 |
+
runtime_dir.display()
|
| 73 |
+
);
|
| 74 |
+
}
|
| 75 |
|
| 76 |
+
let dir = runtime_dir.canonicalize().with_context(|| {
|
| 77 |
+
format!("failed to canonicalize `{}`", runtime_dir.display())
|
| 78 |
+
})?;
|
| 79 |
+
|
| 80 |
+
if let Some(existing) = LIBRARIES.get() {
|
| 81 |
+
if existing.path == dir {
|
| 82 |
return Ok(());
|
| 83 |
}
|
|
|
|
| 84 |
bail!(
|
| 85 |
"koharu-llm is already initialized with `{}` and cannot be reinitialized with `{}`",
|
| 86 |
+
existing.path.display(),
|
| 87 |
+
dir.display()
|
| 88 |
);
|
| 89 |
}
|
| 90 |
|
| 91 |
+
let libraries = load_libraries(&dir)?;
|
| 92 |
+
register_backends(&libraries.ggml, &dir)?;
|
|
|
|
| 93 |
|
| 94 |
LIBRARIES
|
| 95 |
.set(libraries)
|
|
|
|
| 98 |
Ok(())
|
| 99 |
}
|
| 100 |
|
| 101 |
+
fn load_libraries(dir: &Path) -> Result<LoadedLibraries> {
|
| 102 |
+
let [ggml_base_name, ggml_name, llama_name, mtmd_name] = LIB_NAMES;
|
| 103 |
+
|
| 104 |
+
let ggml_base = load_and_bind(ggml_base_name, |lib| unsafe {
|
| 105 |
+
generated::ggml_base::ggml_base::from_library(lib)
|
| 106 |
+
})?;
|
| 107 |
+
let ggml = load_and_bind(ggml_name, |lib| unsafe {
|
| 108 |
+
generated::ggml::ggml::from_library(lib)
|
| 109 |
+
})?;
|
| 110 |
+
let llama = load_and_bind(llama_name, |lib| unsafe {
|
| 111 |
+
generated::llama::llama::from_library(lib)
|
| 112 |
+
})?;
|
| 113 |
+
let mtmd = load_and_bind(mtmd_name, |lib| unsafe {
|
| 114 |
+
generated::mtmd::mtmd::from_library(lib)
|
| 115 |
+
})?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
Ok(LoadedLibraries {
|
| 118 |
path: dir.to_path_buf(),
|
| 119 |
+
llama,
|
| 120 |
+
ggml,
|
| 121 |
+
ggml_base,
|
| 122 |
+
mtmd,
|
|
|
|
|
|
|
| 123 |
})
|
| 124 |
}
|
| 125 |
|
| 126 |
+
fn load_and_bind<T>(
|
| 127 |
+
name: &str,
|
| 128 |
+
bind: impl FnOnce(libloading::Library) -> std::result::Result<T, libloading::Error>,
|
| 129 |
+
) -> Result<T> {
|
| 130 |
+
let library = koharu_runtime::load_library_by_name(name)
|
| 131 |
+
.with_context(|| format!("failed to load `{name}`"))?;
|
| 132 |
+
bind(library).with_context(|| format!("failed to bind `{name}`"))
|
| 133 |
}
|
| 134 |
|
| 135 |
fn register_backends(ggml: &generated::ggml::ggml, dir: &Path) -> Result<()> {
|
| 136 |
let dir = dir
|
| 137 |
.to_str()
|
| 138 |
+
.ok_or_else(|| anyhow!("runtime directory is not valid UTF-8"))?;
|
| 139 |
let dir = CString::new(dir).context("runtime directory contains an interior null byte")?;
|
| 140 |
|
| 141 |
unsafe {
|
|
|
|
| 147 |
|
| 148 |
fn libraries() -> &'static LoadedLibraries {
|
| 149 |
LIBRARIES.get().expect(
|
| 150 |
+
"koharu-llm runtime libraries are not initialized; call `koharu_runtime::initialize()` first",
|
| 151 |
)
|
| 152 |
}
|
| 153 |
|
| 154 |
fn llama_lib() -> &'static generated::llama::llama {
|
|
|
|
| 155 |
&libraries().llama
|
| 156 |
}
|
| 157 |
|
|
|
|
| 170 |
#[allow(warnings)]
|
| 171 |
mod wrappers {
|
| 172 |
use super::*;
|
|
|
|
| 173 |
include!(concat!(env!("OUT_DIR"), "/wrappers.rs"));
|
| 174 |
}
|
| 175 |
|
koharu-llm/tests/llm.rs
CHANGED
|
@@ -5,9 +5,7 @@ use strum::IntoEnumIterator;
|
|
| 5 |
use koharu_llm::{GenerateOptions, Language, Llm, ModelId};
|
| 6 |
|
| 7 |
async fn initialize_runtime() -> anyhow::Result<()> {
|
| 8 |
-
|
| 9 |
-
koharu_llm::safe::runtime::ensure_dylibs(&runtime_dir).await?;
|
| 10 |
-
koharu_llm::safe::runtime::initialize(&runtime_dir)?;
|
| 11 |
Ok(())
|
| 12 |
}
|
| 13 |
|
|
|
|
| 5 |
use koharu_llm::{GenerateOptions, Language, Llm, ModelId};
|
| 6 |
|
| 7 |
async fn initialize_runtime() -> anyhow::Result<()> {
|
| 8 |
+
koharu_runtime::initialize().await?;
|
|
|
|
|
|
|
| 9 |
Ok(())
|
| 10 |
}
|
| 11 |
|
koharu-runtime/Cargo.toml
CHANGED
|
@@ -14,23 +14,16 @@ publish.workspace = true
|
|
| 14 |
[dependencies]
|
| 15 |
koharu-http = { workspace = true }
|
| 16 |
anyhow = { workspace = true }
|
|
|
|
| 17 |
serde_json = { workspace = true }
|
| 18 |
zip = { workspace = true }
|
| 19 |
-
rayon = { workspace = true }
|
| 20 |
-
tracing = { workspace = true }
|
| 21 |
-
reqwest = { workspace = true }
|
| 22 |
tokio = { workspace = true }
|
| 23 |
-
futures = { workspace = true }
|
| 24 |
-
once_cell = { workspace = true }
|
| 25 |
libloading = { workspace = true }
|
| 26 |
-
num_cpus = { workspace = true }
|
| 27 |
-
csv = { workspace = true }
|
| 28 |
flate2 = { workspace = true }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
[dev-dependencies]
|
| 31 |
tempfile = { workspace = true }
|
| 32 |
-
|
| 33 |
-
[features]
|
| 34 |
-
default = ["cuda", "cudnn"]
|
| 35 |
-
cuda = []
|
| 36 |
-
cudnn = []
|
|
|
|
| 14 |
[dependencies]
|
| 15 |
koharu-http = { workspace = true }
|
| 16 |
anyhow = { workspace = true }
|
| 17 |
+
dirs = { workspace = true }
|
| 18 |
serde_json = { workspace = true }
|
| 19 |
zip = { workspace = true }
|
|
|
|
|
|
|
|
|
|
| 20 |
tokio = { workspace = true }
|
|
|
|
|
|
|
| 21 |
libloading = { workspace = true }
|
|
|
|
|
|
|
| 22 |
flate2 = { workspace = true }
|
| 23 |
+
tar = "0.4"
|
| 24 |
+
|
| 25 |
+
[target.'cfg(target_os = "windows")'.dependencies]
|
| 26 |
+
windows-sys = { workspace = true }
|
| 27 |
|
| 28 |
[dev-dependencies]
|
| 29 |
tempfile = { workspace = true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
koharu-runtime/src/archive.rs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::ffi::OsStr;
|
| 2 |
+
use std::fs;
|
| 3 |
+
use std::io::{self, Write};
|
| 4 |
+
use std::path::{Path, PathBuf};
|
| 5 |
+
|
| 6 |
+
use anyhow::{Context, Result, bail};
|
| 7 |
+
use flate2::read::GzDecoder;
|
| 8 |
+
use koharu_http::download;
|
| 9 |
+
|
| 10 |
+
const RUNTIME_LIB_EXTENSIONS: &[&str] = &[".dll", ".so", ".dylib"];
|
| 11 |
+
|
| 12 |
+
pub(crate) async fn download_cached(
|
| 13 |
+
url: &str,
|
| 14 |
+
file_name: &str,
|
| 15 |
+
downloads_dir: &Path,
|
| 16 |
+
) -> Result<PathBuf> {
|
| 17 |
+
let archive_path = downloads_dir.join(file_name);
|
| 18 |
+
if archive_path.exists() {
|
| 19 |
+
return Ok(archive_path);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
let partial_path = downloads_dir.join(format!("{file_name}.partial"));
|
| 23 |
+
let bytes = download::bytes(url)
|
| 24 |
+
.await
|
| 25 |
+
.with_context(|| format!("failed to download `{url}`"))?;
|
| 26 |
+
|
| 27 |
+
let mut file = fs::File::create(&partial_path)
|
| 28 |
+
.with_context(|| format!("failed to create `{}`", partial_path.display()))?;
|
| 29 |
+
file.write_all(&bytes)
|
| 30 |
+
.with_context(|| format!("failed to write `{}`", partial_path.display()))?;
|
| 31 |
+
file.flush()?;
|
| 32 |
+
|
| 33 |
+
fs::rename(&partial_path, &archive_path)
|
| 34 |
+
.with_context(|| format!("failed to finalize `{}`", archive_path.display()))?;
|
| 35 |
+
|
| 36 |
+
Ok(archive_path)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// Extract files matching known runtime library extensions from a zip archive.
|
| 40 |
+
pub(crate) fn extract_zip(archive_path: &Path, output_dir: &Path) -> Result<()> {
|
| 41 |
+
let file = fs::File::open(archive_path)
|
| 42 |
+
.with_context(|| format!("failed to open `{}`", archive_path.display()))?;
|
| 43 |
+
let mut archive = zip::ZipArchive::new(file)
|
| 44 |
+
.with_context(|| format!("failed to read zip `{}`", archive_path.display()))?;
|
| 45 |
+
|
| 46 |
+
for index in 0..archive.len() {
|
| 47 |
+
let mut entry = archive.by_index(index)?;
|
| 48 |
+
if entry.is_dir() {
|
| 49 |
+
continue;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
let Some(file_name) = Path::new(entry.name())
|
| 53 |
+
.file_name()
|
| 54 |
+
.and_then(OsStr::to_str)
|
| 55 |
+
.map(ToOwned::to_owned)
|
| 56 |
+
else {
|
| 57 |
+
continue;
|
| 58 |
+
};
|
| 59 |
+
if !looks_like_runtime_library(&file_name) {
|
| 60 |
+
continue;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
let out_path = output_dir.join(file_name);
|
| 64 |
+
let mut out_file = fs::File::create(&out_path)
|
| 65 |
+
.with_context(|| format!("failed to create `{}`", out_path.display()))?;
|
| 66 |
+
io::copy(&mut entry, &mut out_file)
|
| 67 |
+
.with_context(|| format!("failed to extract `{}`", out_path.display()))?;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
Ok(())
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/// Extract specific files by name from a zip archive (case-insensitive match on basename).
|
| 74 |
+
pub(crate) fn extract_zip_selected(
|
| 75 |
+
archive_path: &Path,
|
| 76 |
+
output_dir: &Path,
|
| 77 |
+
wanted_names: &[&str],
|
| 78 |
+
) -> Result<()> {
|
| 79 |
+
let file = fs::File::open(archive_path)
|
| 80 |
+
.with_context(|| format!("failed to open `{}`", archive_path.display()))?;
|
| 81 |
+
let mut archive = zip::ZipArchive::new(file)
|
| 82 |
+
.with_context(|| format!("failed to read zip `{}`", archive_path.display()))?;
|
| 83 |
+
|
| 84 |
+
for index in 0..archive.len() {
|
| 85 |
+
let mut entry = archive.by_index(index)?;
|
| 86 |
+
if entry.is_dir() {
|
| 87 |
+
continue;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
let entry_name = Path::new(entry.name())
|
| 91 |
+
.file_name()
|
| 92 |
+
.and_then(OsStr::to_str)
|
| 93 |
+
.unwrap_or(entry.name());
|
| 94 |
+
if !wanted_names
|
| 95 |
+
.iter()
|
| 96 |
+
.any(|name| entry_name.eq_ignore_ascii_case(name))
|
| 97 |
+
{
|
| 98 |
+
continue;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
let out_path = output_dir.join(entry_name);
|
| 102 |
+
let mut out_file = fs::File::create(&out_path)
|
| 103 |
+
.with_context(|| format!("failed to create `{}`", out_path.display()))?;
|
| 104 |
+
io::copy(&mut entry, &mut out_file)
|
| 105 |
+
.with_context(|| format!("failed to extract `{}`", out_path.display()))?;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
Ok(())
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/// Extract files matching known runtime library extensions from a tar.gz archive.
|
| 112 |
+
pub(crate) fn extract_tar_gz(archive_path: &Path, output_dir: &Path) -> Result<()> {
|
| 113 |
+
let file = fs::File::open(archive_path)
|
| 114 |
+
.with_context(|| format!("failed to open `{}`", archive_path.display()))?;
|
| 115 |
+
let mut archive = tar::Archive::new(GzDecoder::new(file));
|
| 116 |
+
let mut aliases = Vec::new();
|
| 117 |
+
|
| 118 |
+
for entry in archive
|
| 119 |
+
.entries()
|
| 120 |
+
.with_context(|| format!("failed to read tar `{}`", archive_path.display()))?
|
| 121 |
+
{
|
| 122 |
+
let mut entry = entry.context("failed to read tar entry")?;
|
| 123 |
+
let path = entry.path().context("failed to read tar entry path")?;
|
| 124 |
+
let Some(file_name) = path
|
| 125 |
+
.file_name()
|
| 126 |
+
.and_then(OsStr::to_str)
|
| 127 |
+
.map(ToOwned::to_owned)
|
| 128 |
+
else {
|
| 129 |
+
continue;
|
| 130 |
+
};
|
| 131 |
+
if !looks_like_runtime_library(&file_name) {
|
| 132 |
+
continue;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
let entry_type = entry.header().entry_type();
|
| 136 |
+
if entry_type.is_symlink() {
|
| 137 |
+
let Some(target_name) = entry
|
| 138 |
+
.link_name()
|
| 139 |
+
.context("failed to read tar symlink target")?
|
| 140 |
+
.and_then(|t| t.file_name().map(ToOwned::to_owned))
|
| 141 |
+
.and_then(|n| n.to_str().map(ToOwned::to_owned))
|
| 142 |
+
else {
|
| 143 |
+
continue;
|
| 144 |
+
};
|
| 145 |
+
aliases.push((output_dir.join(file_name), output_dir.join(target_name)));
|
| 146 |
+
continue;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if !entry_type.is_file() {
|
| 150 |
+
continue;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
let out_path = output_dir.join(&file_name);
|
| 154 |
+
let mut out_file = fs::File::create(&out_path)
|
| 155 |
+
.with_context(|| format!("failed to create `{}`", out_path.display()))?;
|
| 156 |
+
io::copy(&mut entry, &mut out_file)
|
| 157 |
+
.with_context(|| format!("failed to extract `{}`", out_path.display()))?;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
materialize_aliases(&aliases)
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
fn looks_like_runtime_library(file_name: &str) -> bool {
|
| 164 |
+
RUNTIME_LIB_EXTENSIONS
|
| 165 |
+
.iter()
|
| 166 |
+
.any(|ext| file_name.ends_with(ext) || file_name.contains(&format!("{ext}.")))
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
fn materialize_aliases(aliases: &[(PathBuf, PathBuf)]) -> Result<()> {
|
| 170 |
+
let mut pending = aliases.to_vec();
|
| 171 |
+
|
| 172 |
+
while !pending.is_empty() {
|
| 173 |
+
let mut progressed = false;
|
| 174 |
+
let mut next = Vec::new();
|
| 175 |
+
|
| 176 |
+
for (alias_path, target_path) in pending {
|
| 177 |
+
if alias_path.exists() {
|
| 178 |
+
progressed = true;
|
| 179 |
+
continue;
|
| 180 |
+
}
|
| 181 |
+
if !target_path.exists() {
|
| 182 |
+
next.push((alias_path, target_path));
|
| 183 |
+
continue;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
fs::hard_link(&target_path, &alias_path)
|
| 187 |
+
.or_else(|_| fs::copy(&target_path, &alias_path).map(|_| ()))
|
| 188 |
+
.with_context(|| {
|
| 189 |
+
format!(
|
| 190 |
+
"failed to create alias `{}` -> `{}`",
|
| 191 |
+
alias_path.display(),
|
| 192 |
+
target_path.display()
|
| 193 |
+
)
|
| 194 |
+
})?;
|
| 195 |
+
progressed = true;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
if !progressed {
|
| 199 |
+
let unresolved: Vec<_> = next
|
| 200 |
+
.iter()
|
| 201 |
+
.map(|(a, t)| format!("{} -> {}", a.display(), t.display()))
|
| 202 |
+
.collect();
|
| 203 |
+
bail!("unresolvable aliases: {}", unresolved.join(", "));
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
pending = next;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
Ok(())
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
#[cfg(test)]
|
| 213 |
+
mod tests {
|
| 214 |
+
use std::io::Write;
|
| 215 |
+
|
| 216 |
+
use super::*;
|
| 217 |
+
|
| 218 |
+
#[test]
|
| 219 |
+
fn materializes_aliases_by_copy() {
|
| 220 |
+
let tempdir = tempfile::tempdir().unwrap();
|
| 221 |
+
let target = tempdir.path().join("libllama.so.0.0.8233");
|
| 222 |
+
let alias = tempdir.path().join("libllama.so");
|
| 223 |
+
|
| 224 |
+
fs::write(&target, b"ok").unwrap();
|
| 225 |
+
materialize_aliases(&[(alias.clone(), target.clone())]).unwrap();
|
| 226 |
+
|
| 227 |
+
assert!(alias.exists());
|
| 228 |
+
assert_eq!(fs::read(&alias).unwrap(), fs::read(&target).unwrap());
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
#[test]
|
| 232 |
+
fn extract_zip_selected_filters_by_name() {
|
| 233 |
+
let tempdir = tempfile::tempdir().unwrap();
|
| 234 |
+
let archive_path = tempdir.path().join("test.zip");
|
| 235 |
+
let output_dir = tempdir.path().join("out");
|
| 236 |
+
fs::create_dir_all(&output_dir).unwrap();
|
| 237 |
+
|
| 238 |
+
let file = fs::File::create(&archive_path).unwrap();
|
| 239 |
+
let mut zip = zip::ZipWriter::new(file);
|
| 240 |
+
let options = zip::write::SimpleFileOptions::default();
|
| 241 |
+
zip.start_file("bin/cudart64_13.dll", options).unwrap();
|
| 242 |
+
zip.write_all(b"cuda").unwrap();
|
| 243 |
+
zip.start_file("bin/ignored.txt", options).unwrap();
|
| 244 |
+
zip.write_all(b"ignore").unwrap();
|
| 245 |
+
zip.finish().unwrap();
|
| 246 |
+
|
| 247 |
+
extract_zip_selected(&archive_path, &output_dir, &["cudart64_13.dll"]).unwrap();
|
| 248 |
+
|
| 249 |
+
assert_eq!(
|
| 250 |
+
fs::read(output_dir.join("cudart64_13.dll")).unwrap(),
|
| 251 |
+
b"cuda"
|
| 252 |
+
);
|
| 253 |
+
assert!(!output_dir.join("ignored.txt").exists());
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
#[test]
|
| 257 |
+
fn runtime_library_detection() {
|
| 258 |
+
assert!(looks_like_runtime_library("ggml.dll"));
|
| 259 |
+
assert!(looks_like_runtime_library("libllama.so.0.0.8233"));
|
| 260 |
+
assert!(looks_like_runtime_library("libggml-metal.0.9.7.dylib"));
|
| 261 |
+
assert!(!looks_like_runtime_library("README.md"));
|
| 262 |
+
}
|
| 263 |
+
}
|
koharu-runtime/src/cuda.rs
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::fmt;
|
| 2 |
+
use std::path::Path;
|
| 3 |
+
|
| 4 |
+
use anyhow::{Context, Result, anyhow, bail};
|
| 5 |
+
use koharu_http::http::http_client;
|
| 6 |
+
use libloading::Library;
|
| 7 |
+
|
| 8 |
+
use crate::archive;
|
| 9 |
+
use crate::loader::{add_runtime_search_path, preload_library};
|
| 10 |
+
|
| 11 |
+
const CUDA_SUCCESS: i32 = 0;
|
| 12 |
+
const CUDA_13_1_DRIVER_VERSION: i32 = 13010;
|
| 13 |
+
|
| 14 |
+
type CuInit = unsafe extern "C" fn(flags: u32) -> i32;
|
| 15 |
+
type CuDriverGetVersion = unsafe extern "C" fn(driver_version: *mut i32) -> i32;
|
| 16 |
+
|
| 17 |
+
// ── Public driver API ────────────────────────────────────────────────
|
| 18 |
+
|
| 19 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
| 20 |
+
pub struct CudaDriverVersion {
|
| 21 |
+
raw: i32,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
impl CudaDriverVersion {
|
| 25 |
+
pub const fn from_raw(raw: i32) -> Self {
|
| 26 |
+
Self { raw }
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
pub const fn raw(self) -> i32 {
|
| 30 |
+
self.raw
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
pub const fn major(self) -> i32 {
|
| 34 |
+
self.raw / 1000
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
pub const fn minor(self) -> i32 {
|
| 38 |
+
(self.raw % 1000) / 10
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
pub const fn supports_cuda_13_1(self) -> bool {
|
| 42 |
+
self.raw >= CUDA_13_1_DRIVER_VERSION
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
impl fmt::Display for CudaDriverVersion {
|
| 47 |
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 48 |
+
write!(f, "{}.{}", self.major(), self.minor())
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
pub fn driver_version() -> Result<CudaDriverVersion> {
|
| 53 |
+
let lib_name = if cfg!(target_os = "windows") {
|
| 54 |
+
"nvcuda.dll"
|
| 55 |
+
} else {
|
| 56 |
+
"libcuda.so"
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
unsafe {
|
| 60 |
+
let library = Library::new(lib_name)
|
| 61 |
+
.with_context(|| format!("failed to load NVIDIA driver library {lib_name}"))?;
|
| 62 |
+
let cu_init = *library
|
| 63 |
+
.get::<CuInit>(b"cuInit\0")
|
| 64 |
+
.context("failed to load cuInit from NVIDIA driver")?;
|
| 65 |
+
let cu_driver_get_version = *library
|
| 66 |
+
.get::<CuDriverGetVersion>(b"cuDriverGetVersion\0")
|
| 67 |
+
.context("failed to load cuDriverGetVersion from NVIDIA driver")?;
|
| 68 |
+
|
| 69 |
+
let status = cu_init(0);
|
| 70 |
+
if status != CUDA_SUCCESS {
|
| 71 |
+
bail!("cuInit failed with CUDA driver error code {status}");
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
let mut raw = 0;
|
| 75 |
+
let status = cu_driver_get_version(&mut raw);
|
| 76 |
+
if status != CUDA_SUCCESS {
|
| 77 |
+
bail!("cuDriverGetVersion failed with CUDA driver error code {status}");
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
Ok(CudaDriverVersion::from_raw(raw))
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
pub(crate) fn is_available() -> bool {
|
| 85 |
+
#[cfg(target_os = "windows")]
|
| 86 |
+
return unsafe { Library::new("nvcuda.dll") }.is_ok();
|
| 87 |
+
|
| 88 |
+
#[cfg(target_os = "linux")]
|
| 89 |
+
return unsafe { Library::new("libcuda.so.1") }.is_ok();
|
| 90 |
+
|
| 91 |
+
#[allow(unreachable_code)]
|
| 92 |
+
false
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// ── Wheel installation ───────────────────────────────────────────────
|
| 96 |
+
|
| 97 |
+
#[allow(dead_code)]
|
| 98 |
+
struct CudaWheel {
|
| 99 |
+
name: &'static str,
|
| 100 |
+
windows_dylibs: &'static [&'static str],
|
| 101 |
+
linux_dylibs: &'static [&'static str],
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
const WHEELS: &[CudaWheel] = &[
|
| 105 |
+
CudaWheel {
|
| 106 |
+
name: "nvidia-cuda-runtime/13.1.80",
|
| 107 |
+
windows_dylibs: &["cudart64_13.dll"],
|
| 108 |
+
linux_dylibs: &["libcudart.so.13"],
|
| 109 |
+
},
|
| 110 |
+
CudaWheel {
|
| 111 |
+
name: "nvidia-cublas/13.2.1.1",
|
| 112 |
+
windows_dylibs: &["cublasLt64_13.dll", "cublas64_13.dll"],
|
| 113 |
+
linux_dylibs: &["libcublasLt.so.13", "libcublas.so.13"],
|
| 114 |
+
},
|
| 115 |
+
CudaWheel {
|
| 116 |
+
name: "nvidia-cufft/12.1.0.78",
|
| 117 |
+
windows_dylibs: &["cufft64_12.dll"],
|
| 118 |
+
linux_dylibs: &["libcufft.so.12"],
|
| 119 |
+
},
|
| 120 |
+
CudaWheel {
|
| 121 |
+
name: "nvidia-curand/10.4.1.81",
|
| 122 |
+
windows_dylibs: &["curand64_10.dll"],
|
| 123 |
+
linux_dylibs: &["libcurand.so.10"],
|
| 124 |
+
},
|
| 125 |
+
];
|
| 126 |
+
|
| 127 |
+
fn platform_tags() -> Result<&'static [&'static str]> {
|
| 128 |
+
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
| 129 |
+
return Ok(&["win_amd64"]);
|
| 130 |
+
|
| 131 |
+
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
| 132 |
+
return Ok(&["manylinux_2_27_x86_64", "manylinux_2_17_x86_64"]);
|
| 133 |
+
|
| 134 |
+
#[cfg(not(any(
|
| 135 |
+
all(target_os = "windows", target_arch = "x86_64"),
|
| 136 |
+
all(target_os = "linux", target_arch = "x86_64")
|
| 137 |
+
)))]
|
| 138 |
+
bail!(
|
| 139 |
+
"CUDA wheels unsupported on {}/{}",
|
| 140 |
+
std::env::consts::OS,
|
| 141 |
+
std::env::consts::ARCH
|
| 142 |
+
)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
impl CudaWheel {
|
| 146 |
+
fn dylibs(&self) -> &'static [&'static str] {
|
| 147 |
+
#[cfg(target_os = "windows")]
|
| 148 |
+
return self.windows_dylibs;
|
| 149 |
+
|
| 150 |
+
#[cfg(target_os = "linux")]
|
| 151 |
+
return self.linux_dylibs;
|
| 152 |
+
|
| 153 |
+
#[allow(unreachable_code)]
|
| 154 |
+
&[]
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
fn source_id() -> Result<String> {
|
| 159 |
+
let wheels: Vec<&str> = WHEELS.iter().map(|w| w.name).collect();
|
| 160 |
+
Ok(format!(
|
| 161 |
+
"cuda;platform={};wheels={}",
|
| 162 |
+
platform_tags()?.join(","),
|
| 163 |
+
wheels.join(",")
|
| 164 |
+
))
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
pub(crate) async fn ensure_ready(root: &Path, downloads_dir: &Path) -> Result<()> {
|
| 168 |
+
let install_dir = root.join("cuda");
|
| 169 |
+
let source_id = source_id()?;
|
| 170 |
+
|
| 171 |
+
if !crate::is_up_to_date(&install_dir, &source_id) {
|
| 172 |
+
crate::reset_dir(&install_dir)?;
|
| 173 |
+
|
| 174 |
+
let tags = platform_tags()?;
|
| 175 |
+
for wheel in WHEELS {
|
| 176 |
+
let (url, filename) = select_wheel(wheel.name, tags).await?;
|
| 177 |
+
let archive = archive::download_cached(&url, &filename, downloads_dir).await?;
|
| 178 |
+
archive::extract_zip_selected(&archive, &install_dir, wheel.dylibs())?;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
crate::mark_installed(&install_dir, &source_id)?;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
add_runtime_search_path(&install_dir)?;
|
| 185 |
+
for wheel in WHEELS {
|
| 186 |
+
for dylib in wheel.dylibs() {
|
| 187 |
+
let path = install_dir.join(dylib);
|
| 188 |
+
if path.exists() {
|
| 189 |
+
preload_library(&path)?;
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
Ok(())
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
async fn select_wheel(package: &str, tags: &[&str]) -> Result<(String, String)> {
|
| 198 |
+
let (dist, version) = package
|
| 199 |
+
.split_once('/')
|
| 200 |
+
.ok_or_else(|| anyhow!("invalid wheel package `{package}`"))?;
|
| 201 |
+
|
| 202 |
+
let meta_url = format!("https://pypi.org/pypi/{dist}/{version}/json");
|
| 203 |
+
let json: serde_json::Value = http_client()
|
| 204 |
+
.get(&meta_url)
|
| 205 |
+
.send()
|
| 206 |
+
.await
|
| 207 |
+
.with_context(|| format!("failed to fetch `{meta_url}`"))?
|
| 208 |
+
.json()
|
| 209 |
+
.await
|
| 210 |
+
.with_context(|| format!("failed to parse metadata for `{dist}`"))?;
|
| 211 |
+
|
| 212 |
+
let files = json
|
| 213 |
+
.get("urls")
|
| 214 |
+
.and_then(|v| v.as_array())
|
| 215 |
+
.ok_or_else(|| anyhow!("bad PyPI json for `{dist}`"))?;
|
| 216 |
+
|
| 217 |
+
for file in files {
|
| 218 |
+
let filename = file.get("filename").and_then(|v| v.as_str()).unwrap_or("");
|
| 219 |
+
let url = file.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
| 220 |
+
|
| 221 |
+
if filename.ends_with(".whl") && tags.iter().any(|tag| filename.contains(tag)) {
|
| 222 |
+
return Ok((url.to_string(), filename.to_string()));
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
bail!("no wheel found for `{dist}` {version} on {tags:?}")
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
#[cfg(test)]
|
| 230 |
+
mod tests {
|
| 231 |
+
use super::*;
|
| 232 |
+
|
| 233 |
+
#[test]
|
| 234 |
+
fn source_id_includes_platform() {
|
| 235 |
+
let id = source_id().unwrap();
|
| 236 |
+
assert!(id.contains("cuda"));
|
| 237 |
+
assert!(id.contains("platform="));
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
#[test]
|
| 241 |
+
fn wheels_have_dylibs_for_current_platform() {
|
| 242 |
+
for wheel in WHEELS {
|
| 243 |
+
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
| 244 |
+
assert!(!wheel.dylibs().is_empty(), "{} has no dylibs", wheel.name);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
#[test]
|
| 249 |
+
fn preload_order_follows_wheel_declaration() {
|
| 250 |
+
let tempdir = tempfile::tempdir().unwrap();
|
| 251 |
+
let root = tempdir.path();
|
| 252 |
+
|
| 253 |
+
for wheel in WHEELS {
|
| 254 |
+
for dylib in wheel.dylibs() {
|
| 255 |
+
std::fs::write(root.join(dylib), b"ok").unwrap();
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
let all_dylibs: Vec<&str> =
|
| 260 |
+
WHEELS.iter().flat_map(|w| w.dylibs().iter().copied()).collect();
|
| 261 |
+
for dylib in &all_dylibs {
|
| 262 |
+
assert!(root.join(dylib).exists());
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
#[test]
|
| 267 |
+
fn parses_major_minor_from_driver_version() {
|
| 268 |
+
let version = CudaDriverVersion::from_raw(13010);
|
| 269 |
+
assert_eq!(version.major(), 13);
|
| 270 |
+
assert_eq!(version.minor(), 1);
|
| 271 |
+
assert_eq!(version.to_string(), "13.1");
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
#[test]
|
| 275 |
+
fn checks_cuda_13_1_threshold() {
|
| 276 |
+
assert!(CudaDriverVersion::from_raw(13010).supports_cuda_13_1());
|
| 277 |
+
assert!(CudaDriverVersion::from_raw(13020).supports_cuda_13_1());
|
| 278 |
+
assert!(!CudaDriverVersion::from_raw(13000).supports_cuda_13_1());
|
| 279 |
+
assert!(!CudaDriverVersion::from_raw(12080).supports_cuda_13_1());
|
| 280 |
+
}
|
| 281 |
+
}
|
koharu-runtime/src/dylib.rs
DELETED
|
@@ -1,394 +0,0 @@
|
|
| 1 |
-
use std::{
|
| 2 |
-
fs,
|
| 3 |
-
io::{BufWriter, Read, Write},
|
| 4 |
-
path::{Path, PathBuf},
|
| 5 |
-
sync::Arc,
|
| 6 |
-
};
|
| 7 |
-
|
| 8 |
-
use anyhow::{Context, Result};
|
| 9 |
-
use futures::stream::{self, StreamExt, TryStreamExt};
|
| 10 |
-
use koharu_http::download;
|
| 11 |
-
use koharu_http::http::http_client;
|
| 12 |
-
use once_cell::sync::OnceCell;
|
| 13 |
-
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
| 14 |
-
use tokio::task;
|
| 15 |
-
use tracing::debug;
|
| 16 |
-
|
| 17 |
-
use crate::zip::fetch_record;
|
| 18 |
-
|
| 19 |
-
/// Keep handles to loaded dynamic libraries alive for process lifetime
|
| 20 |
-
static DYLIB_HANDLES: OnceCell<Vec<libloading::Library>> = OnceCell::new();
|
| 21 |
-
|
| 22 |
-
#[derive(Clone, Copy)]
|
| 23 |
-
struct DylibSpec {
|
| 24 |
-
/// Basename as shipped inside the wheel/archive
|
| 25 |
-
archive_filename: &'static str,
|
| 26 |
-
/// Basename to write locally (may differ to accommodate preload expectations)
|
| 27 |
-
alias_filename: &'static str,
|
| 28 |
-
/// Whether we should preload the library eagerly
|
| 29 |
-
preload: bool,
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
#[allow(unused)]
|
| 33 |
-
const fn dylib(name: &'static str) -> DylibSpec {
|
| 34 |
-
DylibSpec {
|
| 35 |
-
archive_filename: name,
|
| 36 |
-
alias_filename: name,
|
| 37 |
-
preload: true,
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
#[allow(unused)]
|
| 42 |
-
const fn dylib_with_alias(
|
| 43 |
-
archive_filename: &'static str,
|
| 44 |
-
alias_filename: &'static str,
|
| 45 |
-
) -> DylibSpec {
|
| 46 |
-
DylibSpec {
|
| 47 |
-
archive_filename,
|
| 48 |
-
alias_filename,
|
| 49 |
-
preload: true,
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
#[allow(unused)]
|
| 54 |
-
const fn skip_preload(spec: DylibSpec) -> DylibSpec {
|
| 55 |
-
DylibSpec {
|
| 56 |
-
preload: false,
|
| 57 |
-
..spec
|
| 58 |
-
}
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
/// CUDA packages to pull wheels for
|
| 62 |
-
pub const PACKAGES: &[&str] = &[
|
| 63 |
-
#[cfg(feature = "cuda")]
|
| 64 |
-
"nvidia-cuda-runtime/13.1.80",
|
| 65 |
-
#[cfg(feature = "cuda")]
|
| 66 |
-
"nvidia-cublas/13.2.1.1",
|
| 67 |
-
#[cfg(feature = "cuda")]
|
| 68 |
-
"nvidia-cufft/12.1.0.78",
|
| 69 |
-
#[cfg(feature = "cuda")]
|
| 70 |
-
"nvidia-curand/10.4.1.81",
|
| 71 |
-
#[cfg(feature = "cudnn")]
|
| 72 |
-
"nvidia-cudnn-cu13/9.19.0.56",
|
| 73 |
-
];
|
| 74 |
-
|
| 75 |
-
/// Hard-coded load list by platform
|
| 76 |
-
#[cfg(target_os = "windows")]
|
| 77 |
-
const DYLIBS: &[DylibSpec] = &[
|
| 78 |
-
// Core CUDA runtime and BLAS/FFT
|
| 79 |
-
#[cfg(feature = "cuda")]
|
| 80 |
-
dylib("cudart64_13.dll"),
|
| 81 |
-
#[cfg(feature = "cuda")]
|
| 82 |
-
dylib("cublasLt64_13.dll"),
|
| 83 |
-
#[cfg(feature = "cuda")]
|
| 84 |
-
dylib("cublas64_13.dll"),
|
| 85 |
-
#[cfg(feature = "cuda")]
|
| 86 |
-
dylib("cufft64_12.dll"),
|
| 87 |
-
#[cfg(feature = "cuda")]
|
| 88 |
-
dylib("curand64_10.dll"),
|
| 89 |
-
// cuDNN core and dependency chain (graph -> ops -> adv/cnn)
|
| 90 |
-
#[cfg(feature = "cudnn")]
|
| 91 |
-
dylib("cudnn64_9.dll"),
|
| 92 |
-
#[cfg(feature = "cudnn")]
|
| 93 |
-
dylib("cudnn_graph64_9.dll"),
|
| 94 |
-
#[cfg(feature = "cudnn")]
|
| 95 |
-
dylib("cudnn_ops64_9.dll"),
|
| 96 |
-
#[cfg(feature = "cudnn")]
|
| 97 |
-
dylib("cudnn_heuristic64_9.dll"),
|
| 98 |
-
#[cfg(feature = "cudnn")]
|
| 99 |
-
dylib("cudnn_adv64_9.dll"),
|
| 100 |
-
#[cfg(feature = "cudnn")]
|
| 101 |
-
dylib("cudnn_cnn64_9.dll"),
|
| 102 |
-
// cuDNN engine packs (may require NVRTC/NVJITLINK; load last, ignore failures)
|
| 103 |
-
#[cfg(feature = "cudnn")]
|
| 104 |
-
dylib("cudnn_engines_precompiled64_9.dll"),
|
| 105 |
-
#[cfg(feature = "cudnn")]
|
| 106 |
-
dylib("cudnn_engines_runtime_compiled64_9.dll"),
|
| 107 |
-
];
|
| 108 |
-
|
| 109 |
-
#[cfg(target_os = "macos")]
|
| 110 |
-
const DYLIBS: &[DylibSpec] = &[];
|
| 111 |
-
|
| 112 |
-
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
| 113 |
-
const DYLIBS: &[DylibSpec] = &[
|
| 114 |
-
// Core CUDA runtime and BLAS/FFT (sonames)
|
| 115 |
-
#[cfg(feature = "cuda")]
|
| 116 |
-
dylib("libcudart.so.13"),
|
| 117 |
-
#[cfg(feature = "cuda")]
|
| 118 |
-
dylib("libcublasLt.so.13"),
|
| 119 |
-
#[cfg(feature = "cuda")]
|
| 120 |
-
dylib("libcublas.so.13"),
|
| 121 |
-
#[cfg(feature = "cuda")]
|
| 122 |
-
dylib("libcufft.so.12"),
|
| 123 |
-
#[cfg(feature = "cuda")]
|
| 124 |
-
dylib("libcurand.so.10"),
|
| 125 |
-
// cuDNN core and dependency chain
|
| 126 |
-
#[cfg(feature = "cudnn")]
|
| 127 |
-
dylib("libcudnn.so.9"),
|
| 128 |
-
#[cfg(feature = "cudnn")]
|
| 129 |
-
dylib("libcudnn_graph.so.9"),
|
| 130 |
-
#[cfg(feature = "cudnn")]
|
| 131 |
-
dylib("libcudnn_ops.so.9"),
|
| 132 |
-
#[cfg(feature = "cudnn")]
|
| 133 |
-
dylib("libcudnn_heuristic.so.9"),
|
| 134 |
-
#[cfg(feature = "cudnn")]
|
| 135 |
-
dylib("libcudnn_adv.so.9"),
|
| 136 |
-
#[cfg(feature = "cudnn")]
|
| 137 |
-
dylib("libcudnn_cnn.so.9"),
|
| 138 |
-
// cuDNN engine packs
|
| 139 |
-
#[cfg(feature = "cudnn")]
|
| 140 |
-
dylib("libcudnn_engines_precompiled.so.9"),
|
| 141 |
-
#[cfg(feature = "cudnn")]
|
| 142 |
-
dylib("libcudnn_engines_runtime_compiled.so.9"),
|
| 143 |
-
];
|
| 144 |
-
|
| 145 |
-
pub async fn ensure_dylibs(path: impl AsRef<Path>) -> Result<()> {
|
| 146 |
-
let path = path.as_ref().to_owned();
|
| 147 |
-
|
| 148 |
-
fs::create_dir_all(&path)?;
|
| 149 |
-
|
| 150 |
-
let platform_tags = current_platform_tags()?;
|
| 151 |
-
debug!("ensure_dylibs: start -> {}", path.display());
|
| 152 |
-
|
| 153 |
-
let out_dir = Arc::new(path);
|
| 154 |
-
let packages: Vec<String> = PACKAGES.iter().map(|s| s.to_string()).collect();
|
| 155 |
-
|
| 156 |
-
let fetches = packages.into_iter().map(|pkg| {
|
| 157 |
-
let out_dir = Arc::clone(&out_dir);
|
| 158 |
-
async move { fetch_and_extract(pkg.as_str(), platform_tags, out_dir).await }
|
| 159 |
-
});
|
| 160 |
-
|
| 161 |
-
stream::iter(fetches)
|
| 162 |
-
.buffer_unordered(num_cpus::get())
|
| 163 |
-
.try_collect::<Vec<_>>()
|
| 164 |
-
.await?;
|
| 165 |
-
|
| 166 |
-
debug!("ensure_dylibs: done");
|
| 167 |
-
Ok(())
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
/// Preload runtime dynamic libraries with a dependency-friendly order.
|
| 171 |
-
/// Keeps the library handles alive for the process lifetime.
|
| 172 |
-
pub fn preload_dylibs(dir: impl AsRef<Path>) -> Result<()> {
|
| 173 |
-
let dir = dir.as_ref();
|
| 174 |
-
|
| 175 |
-
let mut libs = Vec::new();
|
| 176 |
-
|
| 177 |
-
// Load exactly in our hard-coded order; skip names that are not present.
|
| 178 |
-
for spec in DYLIBS {
|
| 179 |
-
let path = dir.join(spec.alias_filename);
|
| 180 |
-
if !path.exists() {
|
| 181 |
-
continue;
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
if !spec.preload {
|
| 185 |
-
continue;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
unsafe {
|
| 189 |
-
match libloading::Library::new(&path) {
|
| 190 |
-
Ok(lib) => libs.push(lib),
|
| 191 |
-
Err(err) => {
|
| 192 |
-
anyhow::bail!("preload_dylibs: failed {}: {}", path.display(), err);
|
| 193 |
-
}
|
| 194 |
-
}
|
| 195 |
-
}
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
DYLIB_HANDLES
|
| 199 |
-
.set(libs)
|
| 200 |
-
.map_err(|_| anyhow::anyhow!("preload_dylibs: already initialized"))?;
|
| 201 |
-
Ok(())
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
fn wanted_spec(path: &str) -> Option<&'static DylibSpec> {
|
| 205 |
-
let base = Path::new(path)
|
| 206 |
-
.file_name()
|
| 207 |
-
.and_then(|s| s.to_str())
|
| 208 |
-
.unwrap_or(path);
|
| 209 |
-
|
| 210 |
-
DYLIBS
|
| 211 |
-
.iter()
|
| 212 |
-
.find(|&want| base.eq_ignore_ascii_case(want.archive_filename))
|
| 213 |
-
.map(|v| v as _)
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
fn current_platform_tags() -> Result<&'static [&'static str]> {
|
| 217 |
-
if cfg!(target_os = "windows") {
|
| 218 |
-
Ok(&["win_amd64"])
|
| 219 |
-
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
|
| 220 |
-
Ok(&["manylinux_2_27_x86_64", "manylinux_2_17_x86_64"])
|
| 221 |
-
} else if cfg!(target_os = "macos") {
|
| 222 |
-
Ok(&["macosx_13_0_universal2"])
|
| 223 |
-
} else {
|
| 224 |
-
anyhow::bail!("unsupported platform for runtime bundling");
|
| 225 |
-
}
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
async fn fetch_and_extract(pkg: &str, platform_tags: &[&str], out_dir: Arc<PathBuf>) -> Result<()> {
|
| 229 |
-
// 1) Query PyPI JSON
|
| 230 |
-
let meta_url = format!("https://pypi.org/pypi/{pkg}/json");
|
| 231 |
-
let resp = http_client()
|
| 232 |
-
.get(&meta_url)
|
| 233 |
-
.send()
|
| 234 |
-
.await
|
| 235 |
-
.context("failed to fetch package metadata")?;
|
| 236 |
-
let json: serde_json::Value = resp
|
| 237 |
-
.json()
|
| 238 |
-
.await
|
| 239 |
-
.context("failed to parse package metadata")?;
|
| 240 |
-
|
| 241 |
-
// 2) Choose a wheel
|
| 242 |
-
let files = json
|
| 243 |
-
.get("urls")
|
| 244 |
-
.and_then(|v| v.as_array())
|
| 245 |
-
.ok_or_else(|| anyhow::anyhow!("bad json: urls"))?;
|
| 246 |
-
let mut chosen: Option<(String, String)> = None; // (url, filename)
|
| 247 |
-
for f in files {
|
| 248 |
-
let filename = f.get("filename").and_then(|v| v.as_str()).unwrap_or("");
|
| 249 |
-
let file_url = f.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
| 250 |
-
|
| 251 |
-
if !filename.ends_with(".whl") {
|
| 252 |
-
continue;
|
| 253 |
-
}
|
| 254 |
-
if platform_tags.iter().any(|tag| filename.contains(tag)) {
|
| 255 |
-
chosen = Some((file_url.to_string(), filename.to_string()));
|
| 256 |
-
break;
|
| 257 |
-
}
|
| 258 |
-
}
|
| 259 |
-
let (wheel_url, wheel_name) = chosen.ok_or_else(|| anyhow::anyhow!("no suitable wheel"))?;
|
| 260 |
-
debug!("{pkg}: selected wheel {wheel_name}");
|
| 261 |
-
|
| 262 |
-
// 3) Use RECORD to check local dylibs; download only if needed
|
| 263 |
-
let entries = fetch_record(&wheel_url)
|
| 264 |
-
.await
|
| 265 |
-
.context("failed to fetch RECORD")?;
|
| 266 |
-
|
| 267 |
-
// Fast path: existence + size-only check; no hashing.
|
| 268 |
-
// If size is None and file exists, treat as OK (no further verification).
|
| 269 |
-
let needs_download = entries
|
| 270 |
-
.par_iter()
|
| 271 |
-
.filter_map(|e| wanted_spec(&e.path).map(|spec| (spec.alias_filename, e.size)))
|
| 272 |
-
.any(|(base, rec_size)| {
|
| 273 |
-
let local = out_dir.as_ref().join(base);
|
| 274 |
-
if !local.exists() {
|
| 275 |
-
return true;
|
| 276 |
-
}
|
| 277 |
-
match (local.metadata(), rec_size) {
|
| 278 |
-
(Ok(meta), Some(sz)) => meta.len() != sz,
|
| 279 |
-
_ => false,
|
| 280 |
-
}
|
| 281 |
-
});
|
| 282 |
-
|
| 283 |
-
if needs_download {
|
| 284 |
-
debug!("{pkg}: downloading {wheel_name}...");
|
| 285 |
-
let bytes = download::bytes(&wheel_url)
|
| 286 |
-
.await
|
| 287 |
-
.context("failed to download wheel")?;
|
| 288 |
-
let out = Arc::clone(&out_dir);
|
| 289 |
-
|
| 290 |
-
task::spawn_blocking(move || extract_from_wheel(&bytes, out.as_ref()))
|
| 291 |
-
.await?
|
| 292 |
-
.context("failed to extract dylibs")?;
|
| 293 |
-
debug!("{pkg}: download and extract complete");
|
| 294 |
-
Ok(())
|
| 295 |
-
} else {
|
| 296 |
-
debug!("{pkg}: {wheel_name} runtime libs are up-to-date");
|
| 297 |
-
Ok(())
|
| 298 |
-
}
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
fn extract_from_wheel(bytes: &[u8], out_dir: &Path) -> Result<()> {
|
| 302 |
-
// First, list target entries to extract
|
| 303 |
-
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
|
| 304 |
-
let mut targets: Vec<(String, String)> = Vec::new(); // (full archive path, output basename)
|
| 305 |
-
for i in 0..archive.len() {
|
| 306 |
-
let file = archive.by_index(i)?;
|
| 307 |
-
if let Some(spec) = wanted_spec(file.name()) {
|
| 308 |
-
targets.push((file.name().to_owned(), spec.alias_filename.to_owned()));
|
| 309 |
-
}
|
| 310 |
-
}
|
| 311 |
-
drop(archive);
|
| 312 |
-
|
| 313 |
-
if targets.is_empty() {
|
| 314 |
-
anyhow::bail!("no runtime libraries found in wheel");
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
let results: Result<Vec<(String, u64)>> = targets
|
| 318 |
-
.par_iter()
|
| 319 |
-
.map(|(full_name, base_name)| -> Result<(String, u64)> {
|
| 320 |
-
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes))?;
|
| 321 |
-
let mut file = zip.by_name(full_name)?;
|
| 322 |
-
|
| 323 |
-
let out_path = out_dir.join(base_name);
|
| 324 |
-
let out = fs::File::create(&out_path)?;
|
| 325 |
-
// Preallocate to uncompressed size if known to reduce fragmentation.
|
| 326 |
-
let _ = out.set_len(file.size());
|
| 327 |
-
|
| 328 |
-
// Buffered copy with large chunk size
|
| 329 |
-
let mut writer = BufWriter::with_capacity(8 * 1024 * 1024, out);
|
| 330 |
-
let mut buf = vec![0u8; 8 * 1024 * 1024];
|
| 331 |
-
let mut written: u64 = 0;
|
| 332 |
-
loop {
|
| 333 |
-
let n = file.read(&mut buf)?;
|
| 334 |
-
if n == 0 {
|
| 335 |
-
break;
|
| 336 |
-
}
|
| 337 |
-
writer.write_all(&buf[..n])?;
|
| 338 |
-
written += n as u64;
|
| 339 |
-
}
|
| 340 |
-
writer.flush()?;
|
| 341 |
-
Ok((base_name.clone(), written))
|
| 342 |
-
})
|
| 343 |
-
.collect();
|
| 344 |
-
|
| 345 |
-
let results = results?;
|
| 346 |
-
let _total_bytes: u64 = results.iter().map(|(_, w)| *w).sum();
|
| 347 |
-
debug!(
|
| 348 |
-
"extract: copied {} libraries into {}",
|
| 349 |
-
results.len(),
|
| 350 |
-
out_dir.display()
|
| 351 |
-
);
|
| 352 |
-
|
| 353 |
-
Ok(())
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
#[cfg(test)]
|
| 357 |
-
mod tests {
|
| 358 |
-
use super::*;
|
| 359 |
-
|
| 360 |
-
#[tokio::test]
|
| 361 |
-
#[ignore]
|
| 362 |
-
async fn test_skip_download_if_up_to_date() -> Result<()> {
|
| 363 |
-
let temp_dir = tempfile::tempdir()?;
|
| 364 |
-
let out_dir = temp_dir.path();
|
| 365 |
-
|
| 366 |
-
let t0 = std::time::Instant::now();
|
| 367 |
-
ensure_dylibs(out_dir).await?;
|
| 368 |
-
|
| 369 |
-
let elapsed = t0.elapsed();
|
| 370 |
-
println!("Elapsed time: {:?}", elapsed);
|
| 371 |
-
|
| 372 |
-
let t1 = std::time::Instant::now();
|
| 373 |
-
ensure_dylibs(out_dir).await?;
|
| 374 |
-
|
| 375 |
-
let elapsed = t1.elapsed();
|
| 376 |
-
println!("Elapsed time: {:?}", elapsed);
|
| 377 |
-
|
| 378 |
-
assert!(elapsed < t0.elapsed());
|
| 379 |
-
|
| 380 |
-
Ok(())
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
#[tokio::test]
|
| 384 |
-
#[ignore]
|
| 385 |
-
async fn test_preload_dylibs() -> Result<()> {
|
| 386 |
-
let temp_dir = std::env::temp_dir();
|
| 387 |
-
let out_dir = temp_dir.join("cuda_rt_test_dylibs");
|
| 388 |
-
|
| 389 |
-
ensure_dylibs(&out_dir).await?;
|
| 390 |
-
preload_dylibs(&out_dir)?;
|
| 391 |
-
|
| 392 |
-
Ok(())
|
| 393 |
-
}
|
| 394 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
koharu-runtime/src/lib.rs
CHANGED
|
@@ -1,4 +1,107 @@
|
|
| 1 |
-
mod
|
| 2 |
-
mod
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mod archive;
|
| 2 |
+
mod cuda;
|
| 3 |
+
mod llama;
|
| 4 |
+
mod loader;
|
| 5 |
|
| 6 |
+
use std::env;
|
| 7 |
+
use std::fs;
|
| 8 |
+
use std::path::{Path, PathBuf};
|
| 9 |
+
|
| 10 |
+
use anyhow::{Context, Result};
|
| 11 |
+
|
| 12 |
+
pub use cuda::{CudaDriverVersion, driver_version as cuda_driver_version};
|
| 13 |
+
pub use loader::load_library_by_name;
|
| 14 |
+
|
| 15 |
+
const DOWNLOADS_DIR: &str = ".downloads";
|
| 16 |
+
const INSTALLED_MARKER: &str = ".installed";
|
| 17 |
+
|
| 18 |
+
pub async fn initialize() -> Result<()> {
|
| 19 |
+
initialize_with_root(&runtime_root()).await
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
async fn initialize_with_root(root: &Path) -> Result<()> {
|
| 23 |
+
fs::create_dir_all(root)
|
| 24 |
+
.with_context(|| format!("failed to create runtime root `{}`", root.display()))?;
|
| 25 |
+
let downloads_dir = root.join(DOWNLOADS_DIR);
|
| 26 |
+
fs::create_dir_all(&downloads_dir)
|
| 27 |
+
.with_context(|| format!("failed to create `{}`", downloads_dir.display()))?;
|
| 28 |
+
|
| 29 |
+
if cuda::is_available() {
|
| 30 |
+
cuda::ensure_ready(root, &downloads_dir).await?;
|
| 31 |
+
}
|
| 32 |
+
llama::ensure_ready(root, &downloads_dir).await?;
|
| 33 |
+
|
| 34 |
+
Ok(())
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
#[doc(hidden)]
|
| 38 |
+
pub fn llama_runtime_dir() -> Result<PathBuf> {
|
| 39 |
+
llama::runtime_dir(&runtime_root())
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
fn runtime_root() -> PathBuf {
|
| 43 |
+
if let Some(path) = env::var_os("KOHARU_RUNTIME_ROOT") {
|
| 44 |
+
return PathBuf::from(path);
|
| 45 |
+
}
|
| 46 |
+
dirs::data_local_dir()
|
| 47 |
+
.unwrap_or_else(env::temp_dir)
|
| 48 |
+
.join("Koharu")
|
| 49 |
+
.join("runtime")
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
fn is_up_to_date(install_dir: &Path, source_id: &str) -> bool {
|
| 53 |
+
matches!(
|
| 54 |
+
fs::read_to_string(install_dir.join(INSTALLED_MARKER)),
|
| 55 |
+
Ok(content) if content == source_id
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
fn reset_dir(dir: &Path) -> Result<()> {
|
| 60 |
+
if dir.exists() {
|
| 61 |
+
fs::remove_dir_all(dir)
|
| 62 |
+
.with_context(|| format!("failed to reset `{}`", dir.display()))?;
|
| 63 |
+
}
|
| 64 |
+
fs::create_dir_all(dir).with_context(|| format!("failed to create `{}`", dir.display()))?;
|
| 65 |
+
Ok(())
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
fn mark_installed(install_dir: &Path, source_id: &str) -> Result<()> {
|
| 69 |
+
fs::write(install_dir.join(INSTALLED_MARKER), source_id)
|
| 70 |
+
.with_context(|| format!("failed to write marker in `{}`", install_dir.display()))
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#[cfg(test)]
|
| 74 |
+
mod tests {
|
| 75 |
+
use super::*;
|
| 76 |
+
|
| 77 |
+
#[tokio::test]
|
| 78 |
+
#[ignore]
|
| 79 |
+
async fn initializes_llama_runtime_into_shared_root() -> Result<()> {
|
| 80 |
+
let tempdir = tempfile::tempdir()?;
|
| 81 |
+
initialize_with_root(tempdir.path()).await?;
|
| 82 |
+
let dir = llama::runtime_dir(tempdir.path())?;
|
| 83 |
+
assert!(dir.exists());
|
| 84 |
+
Ok(())
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
#[tokio::test]
|
| 88 |
+
#[ignore]
|
| 89 |
+
async fn repeated_basename_loads_succeed_after_runtime_initialize() -> Result<()> {
|
| 90 |
+
let tempdir = tempfile::tempdir()?;
|
| 91 |
+
initialize_with_root(tempdir.path()).await?;
|
| 92 |
+
let dir = llama::runtime_dir(tempdir.path())?;
|
| 93 |
+
|
| 94 |
+
let lib_name = fs::read_dir(&dir)?
|
| 95 |
+
.filter_map(|e| e.ok())
|
| 96 |
+
.filter_map(|e| {
|
| 97 |
+
let name = e.file_name().to_string_lossy().into_owned();
|
| 98 |
+
name.contains("llama").then_some(name)
|
| 99 |
+
})
|
| 100 |
+
.next()
|
| 101 |
+
.ok_or_else(|| anyhow::anyhow!("no llama library found"))?;
|
| 102 |
+
|
| 103 |
+
let _first = load_library_by_name(&lib_name)?;
|
| 104 |
+
let _second = load_library_by_name(&lib_name)?;
|
| 105 |
+
Ok(())
|
| 106 |
+
}
|
| 107 |
+
}
|
koharu-runtime/src/llama.rs
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::path::{Path, PathBuf};
|
| 2 |
+
|
| 3 |
+
use anyhow::{Result, bail};
|
| 4 |
+
use libloading::Library;
|
| 5 |
+
|
| 6 |
+
use crate::archive;
|
| 7 |
+
use crate::loader::{add_runtime_search_path, preload_library};
|
| 8 |
+
|
| 9 |
+
const LLAMA_CPP_TAG: &str = env!("LLAMA_CPP_TAG");
|
| 10 |
+
const RELEASE_BASE_URL: &str = "https://github.com/ggml-org/llama.cpp/releases/download";
|
| 11 |
+
|
| 12 |
+
#[derive(Debug, Clone, Copy)]
|
| 13 |
+
#[allow(dead_code)]
|
| 14 |
+
enum LlamaRuntime {
|
| 15 |
+
WindowsCuda13X64,
|
| 16 |
+
WindowsVulkanX64,
|
| 17 |
+
LinuxVulkanX64,
|
| 18 |
+
MacosArm64,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
impl LlamaRuntime {
|
| 22 |
+
#[allow(clippy::needless_return)]
|
| 23 |
+
fn detect() -> Result<Self> {
|
| 24 |
+
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
| 25 |
+
if unsafe { Library::new("nvcuda.dll") }.is_ok() {
|
| 26 |
+
return Ok(Self::WindowsCuda13X64);
|
| 27 |
+
} else {
|
| 28 |
+
return Ok(Self::WindowsVulkanX64);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
| 32 |
+
return Ok(Self::LinuxVulkanX64);
|
| 33 |
+
|
| 34 |
+
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
| 35 |
+
return Ok(Self::MacosArm64);
|
| 36 |
+
|
| 37 |
+
#[cfg(not(any(
|
| 38 |
+
all(target_os = "windows", target_arch = "x86_64"),
|
| 39 |
+
all(target_os = "linux", target_arch = "x86_64"),
|
| 40 |
+
all(target_os = "macos", target_arch = "aarch64")
|
| 41 |
+
)))]
|
| 42 |
+
bail!(
|
| 43 |
+
"unsupported platform: os={}, arch={}",
|
| 44 |
+
std::env::consts::OS,
|
| 45 |
+
std::env::consts::ARCH
|
| 46 |
+
)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
fn id(self) -> &'static str {
|
| 50 |
+
match self {
|
| 51 |
+
Self::WindowsCuda13X64 => "windows-cuda13-x64",
|
| 52 |
+
Self::WindowsVulkanX64 => "windows-vulkan-x64",
|
| 53 |
+
Self::LinuxVulkanX64 => "linux-vulkan-x64",
|
| 54 |
+
Self::MacosArm64 => "macos-arm64",
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
fn assets(self) -> &'static [&'static str] {
|
| 59 |
+
match self {
|
| 60 |
+
Self::WindowsCuda13X64 => &[
|
| 61 |
+
"llama-b8233-bin-win-cuda-13.1-x64.zip",
|
| 62 |
+
"cudart-llama-bin-win-cuda-13.1-x64.zip",
|
| 63 |
+
],
|
| 64 |
+
Self::WindowsVulkanX64 => &["llama-b8233-bin-win-vulkan-x64.zip"],
|
| 65 |
+
Self::LinuxVulkanX64 => &["llama-b8233-bin-ubuntu-vulkan-x64.tar.gz"],
|
| 66 |
+
Self::MacosArm64 => &["llama-b8233-bin-macos-arm64.tar.gz"],
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
fn libraries(self) -> &'static [&'static str] {
|
| 71 |
+
match self {
|
| 72 |
+
Self::WindowsCuda13X64 => &[
|
| 73 |
+
"cudart64_13.dll",
|
| 74 |
+
"cublasLt64_13.dll",
|
| 75 |
+
"cublas64_13.dll",
|
| 76 |
+
"libomp140.x86_64.dll",
|
| 77 |
+
"ggml-base.dll",
|
| 78 |
+
"ggml.dll",
|
| 79 |
+
"ggml-cpu-alderlake.dll",
|
| 80 |
+
"ggml-cpu-cannonlake.dll",
|
| 81 |
+
"ggml-cpu-cascadelake.dll",
|
| 82 |
+
"ggml-cpu-cooperlake.dll",
|
| 83 |
+
"ggml-cpu-haswell.dll",
|
| 84 |
+
"ggml-cpu-icelake.dll",
|
| 85 |
+
"ggml-cpu-ivybridge.dll",
|
| 86 |
+
"ggml-cpu-piledriver.dll",
|
| 87 |
+
"ggml-cpu-sandybridge.dll",
|
| 88 |
+
"ggml-cpu-sapphirerapids.dll",
|
| 89 |
+
"ggml-cpu-skylakex.dll",
|
| 90 |
+
"ggml-cpu-sse42.dll",
|
| 91 |
+
"ggml-cpu-x64.dll",
|
| 92 |
+
"ggml-cpu-zen4.dll",
|
| 93 |
+
"ggml-cuda.dll",
|
| 94 |
+
"ggml-rpc.dll",
|
| 95 |
+
"llama.dll",
|
| 96 |
+
"mtmd.dll",
|
| 97 |
+
],
|
| 98 |
+
Self::WindowsVulkanX64 => &[
|
| 99 |
+
"libomp140.x86_64.dll",
|
| 100 |
+
"ggml-base.dll",
|
| 101 |
+
"ggml.dll",
|
| 102 |
+
"ggml-cpu-alderlake.dll",
|
| 103 |
+
"ggml-cpu-cannonlake.dll",
|
| 104 |
+
"ggml-cpu-cascadelake.dll",
|
| 105 |
+
"ggml-cpu-cooperlake.dll",
|
| 106 |
+
"ggml-cpu-haswell.dll",
|
| 107 |
+
"ggml-cpu-icelake.dll",
|
| 108 |
+
"ggml-cpu-ivybridge.dll",
|
| 109 |
+
"ggml-cpu-piledriver.dll",
|
| 110 |
+
"ggml-cpu-sandybridge.dll",
|
| 111 |
+
"ggml-cpu-sapphirerapids.dll",
|
| 112 |
+
"ggml-cpu-skylakex.dll",
|
| 113 |
+
"ggml-cpu-sse42.dll",
|
| 114 |
+
"ggml-cpu-x64.dll",
|
| 115 |
+
"ggml-cpu-zen4.dll",
|
| 116 |
+
"ggml-rpc.dll",
|
| 117 |
+
"ggml-vulkan.dll",
|
| 118 |
+
"llama.dll",
|
| 119 |
+
"mtmd.dll",
|
| 120 |
+
],
|
| 121 |
+
Self::LinuxVulkanX64 => &[
|
| 122 |
+
"libggml-base.so",
|
| 123 |
+
"libggml.so",
|
| 124 |
+
"libggml-cpu-alderlake.so",
|
| 125 |
+
"libggml-cpu-cannonlake.so",
|
| 126 |
+
"libggml-cpu-cascadelake.so",
|
| 127 |
+
"libggml-cpu-cooperlake.so",
|
| 128 |
+
"libggml-cpu-haswell.so",
|
| 129 |
+
"libggml-cpu-icelake.so",
|
| 130 |
+
"libggml-cpu-ivybridge.so",
|
| 131 |
+
"libggml-cpu-piledriver.so",
|
| 132 |
+
"libggml-cpu-sandybridge.so",
|
| 133 |
+
"libggml-cpu-sapphirerapids.so",
|
| 134 |
+
"libggml-cpu-skylakex.so",
|
| 135 |
+
"libggml-cpu-sse42.so",
|
| 136 |
+
"libggml-cpu-x64.so",
|
| 137 |
+
"libggml-cpu-zen4.so",
|
| 138 |
+
"libggml-rpc.so",
|
| 139 |
+
"libggml-vulkan.so",
|
| 140 |
+
"libllama.so",
|
| 141 |
+
"libmtmd.so",
|
| 142 |
+
],
|
| 143 |
+
Self::MacosArm64 => &[
|
| 144 |
+
"libggml-base.dylib",
|
| 145 |
+
"libggml.dylib",
|
| 146 |
+
"libggml-blas.dylib",
|
| 147 |
+
"libggml-cpu.dylib",
|
| 148 |
+
"libggml-metal.dylib",
|
| 149 |
+
"libggml-rpc.dylib",
|
| 150 |
+
"libllama.dylib",
|
| 151 |
+
"libmtmd.dylib",
|
| 152 |
+
],
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
fn install_dir(self, root: &Path) -> PathBuf {
|
| 157 |
+
root.join("llama.cpp").join(LLAMA_CPP_TAG).join(self.id())
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
fn source_id(self) -> String {
|
| 161 |
+
format!("llama-{LLAMA_CPP_TAG}-{}", self.id())
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
fn is_zip_asset(asset: &str) -> bool {
|
| 165 |
+
asset.ends_with(".zip")
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
async fn install(self, install_dir: &Path, downloads_dir: &Path) -> Result<()> {
|
| 169 |
+
for asset in self.assets() {
|
| 170 |
+
let url = format!("{RELEASE_BASE_URL}/{LLAMA_CPP_TAG}/{asset}");
|
| 171 |
+
let archive = archive::download_cached(&url, asset, downloads_dir).await?;
|
| 172 |
+
if Self::is_zip_asset(asset) {
|
| 173 |
+
archive::extract_zip(&archive, install_dir)?;
|
| 174 |
+
} else {
|
| 175 |
+
archive::extract_tar_gz(&archive, install_dir)?;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
for lib in self.libraries() {
|
| 180 |
+
if !install_dir.join(lib).exists() {
|
| 181 |
+
bail!(
|
| 182 |
+
"required library `{lib}` missing from `{}`",
|
| 183 |
+
install_dir.display()
|
| 184 |
+
);
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
Ok(())
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
pub(crate) async fn ensure_ready(root: &Path, downloads_dir: &Path) -> Result<()> {
|
| 193 |
+
let runtime = LlamaRuntime::detect()?;
|
| 194 |
+
let install_dir = runtime.install_dir(root);
|
| 195 |
+
let source_id = runtime.source_id();
|
| 196 |
+
|
| 197 |
+
if !crate::is_up_to_date(&install_dir, &source_id) {
|
| 198 |
+
crate::reset_dir(&install_dir)?;
|
| 199 |
+
runtime.install(&install_dir, downloads_dir).await?;
|
| 200 |
+
crate::mark_installed(&install_dir, &source_id)?;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
add_runtime_search_path(&install_dir)?;
|
| 204 |
+
for lib in runtime.libraries() {
|
| 205 |
+
preload_library(&install_dir.join(lib))?;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
Ok(())
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
pub(crate) fn runtime_dir(root: &Path) -> Result<PathBuf> {
|
| 212 |
+
Ok(LlamaRuntime::detect()?.install_dir(root))
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
#[cfg(test)]
|
| 216 |
+
mod tests {
|
| 217 |
+
use std::fs;
|
| 218 |
+
use std::path::Path;
|
| 219 |
+
|
| 220 |
+
use super::*;
|
| 221 |
+
|
| 222 |
+
fn touch(path: &Path) {
|
| 223 |
+
fs::write(path, b"ok").unwrap();
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
#[test]
|
| 227 |
+
fn detect_returns_a_variant_for_current_platform() {
|
| 228 |
+
let runtime = LlamaRuntime::detect().unwrap();
|
| 229 |
+
assert!(!runtime.id().is_empty());
|
| 230 |
+
assert!(!runtime.assets().is_empty());
|
| 231 |
+
assert!(!runtime.libraries().is_empty());
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
#[test]
|
| 235 |
+
fn install_dir_includes_tag_and_id() {
|
| 236 |
+
let root = Path::new("/tmp/rt");
|
| 237 |
+
let dir = LlamaRuntime::WindowsVulkanX64.install_dir(root);
|
| 238 |
+
assert!(dir.ends_with(
|
| 239 |
+
Path::new("llama.cpp")
|
| 240 |
+
.join(LLAMA_CPP_TAG)
|
| 241 |
+
.join("windows-vulkan-x64")
|
| 242 |
+
));
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
#[test]
|
| 246 |
+
fn preload_order_matches_libraries() {
|
| 247 |
+
let tempdir = tempfile::tempdir().unwrap();
|
| 248 |
+
let root = tempdir.path();
|
| 249 |
+
let rt = LlamaRuntime::WindowsCuda13X64;
|
| 250 |
+
|
| 251 |
+
for lib in rt.libraries() {
|
| 252 |
+
touch(&root.join(lib));
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
let paths: Vec<PathBuf> = rt.libraries().iter().map(|l| root.join(l)).collect();
|
| 256 |
+
assert!(paths.iter().all(|p| p.exists()));
|
| 257 |
+
assert_eq!(paths.len(), rt.libraries().len());
|
| 258 |
+
}
|
| 259 |
+
}
|
koharu-runtime/src/loader.rs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::ffi::OsStr;
|
| 2 |
+
use std::path::Path;
|
| 3 |
+
|
| 4 |
+
use anyhow::{Context, Result, bail};
|
| 5 |
+
use libloading::Library;
|
| 6 |
+
|
| 7 |
+
pub(crate) fn add_runtime_search_path(path: &Path) -> Result<()> {
|
| 8 |
+
#[cfg(not(target_os = "windows"))]
|
| 9 |
+
{
|
| 10 |
+
let _ = path;
|
| 11 |
+
Ok(())
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
#[cfg(target_os = "windows")]
|
| 15 |
+
{
|
| 16 |
+
use std::os::windows::ffi::OsStrExt;
|
| 17 |
+
use windows_sys::Win32::System::LibraryLoader::{
|
| 18 |
+
AddDllDirectory, LOAD_LIBRARY_SEARCH_SYSTEM32, LOAD_LIBRARY_SEARCH_USER_DIRS,
|
| 19 |
+
SetDefaultDllDirectories,
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
let path = path
|
| 23 |
+
.canonicalize()
|
| 24 |
+
.with_context(|| format!("failed to canonicalize `{}`", path.display()))?;
|
| 25 |
+
let wide = path
|
| 26 |
+
.as_os_str()
|
| 27 |
+
.encode_wide()
|
| 28 |
+
.chain(std::iter::once(0))
|
| 29 |
+
.collect::<Vec<_>>();
|
| 30 |
+
unsafe {
|
| 31 |
+
if SetDefaultDllDirectories(
|
| 32 |
+
LOAD_LIBRARY_SEARCH_USER_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32,
|
| 33 |
+
) == 0
|
| 34 |
+
{
|
| 35 |
+
bail!(
|
| 36 |
+
"failed to set default DLL directories: {}",
|
| 37 |
+
std::io::Error::last_os_error()
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
if AddDllDirectory(wide.as_ptr()).is_null() {
|
| 41 |
+
bail!(
|
| 42 |
+
"failed to add DLL directory: {}",
|
| 43 |
+
std::io::Error::last_os_error()
|
| 44 |
+
);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
Ok(())
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
pub(crate) fn preload_library(path: &Path) -> Result<()> {
|
| 52 |
+
let path = path
|
| 53 |
+
.canonicalize()
|
| 54 |
+
.with_context(|| format!("failed to canonicalize `{}`", path.display()))?;
|
| 55 |
+
let library = load_library(path.as_os_str())
|
| 56 |
+
.with_context(|| format!("failed to preload `{}`", path.display()))?;
|
| 57 |
+
std::mem::forget(library);
|
| 58 |
+
Ok(())
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
pub fn load_library_by_name(name: &str) -> Result<Library> {
|
| 62 |
+
load_library(OsStr::new(name)).with_context(|| format!("failed to load `{name}`"))
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
fn load_library(target: &OsStr) -> Result<Library> {
|
| 66 |
+
#[cfg(target_os = "windows")]
|
| 67 |
+
{
|
| 68 |
+
let library = unsafe { Library::new(target) }?;
|
| 69 |
+
Ok(library)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
#[cfg(not(target_os = "windows"))]
|
| 73 |
+
{
|
| 74 |
+
use libloading::os::unix::{Library as UnixLibrary, RTLD_GLOBAL, RTLD_NOW};
|
| 75 |
+
|
| 76 |
+
let library = unsafe { UnixLibrary::open(Some(target), RTLD_NOW | RTLD_GLOBAL) }?;
|
| 77 |
+
Ok(library.into())
|
| 78 |
+
}
|
| 79 |
+
}
|
koharu-runtime/src/zip.rs
DELETED
|
@@ -1,226 +0,0 @@
|
|
| 1 |
-
use std::io;
|
| 2 |
-
|
| 3 |
-
use anyhow::Result;
|
| 4 |
-
use flate2::read::DeflateDecoder;
|
| 5 |
-
#[cfg(test)]
|
| 6 |
-
use koharu_http::http::http_client;
|
| 7 |
-
use koharu_http::range;
|
| 8 |
-
|
| 9 |
-
// ZIP signatures and constants
|
| 10 |
-
const SIG_EOCD: [u8; 4] = [0x50, 0x4b, 0x05, 0x06]; // End Of Central Directory
|
| 11 |
-
const SIG_CFH: [u8; 4] = [0x50, 0x4b, 0x01, 0x02]; // Central Directory File Header
|
| 12 |
-
const SIG_LFH: [u8; 4] = [0x50, 0x4b, 0x03, 0x04]; // Local File Header
|
| 13 |
-
const EOCD_MIN_LEN: usize = 22; // Minimum EOCD size
|
| 14 |
-
const CFH_FIXED_LEN: usize = 46; // CFH fixed-length portion
|
| 15 |
-
const LFH_FIXED_LEN: u64 = 30; // LFH fixed-length portion
|
| 16 |
-
|
| 17 |
-
// EOCD field offsets (relative to EOCD start)
|
| 18 |
-
const EOCD_OFF_CD_SIZE: usize = 12; // u32: size of central directory
|
| 19 |
-
const EOCD_OFF_CD_OFFSET: usize = 16; // u32: offset of central directory
|
| 20 |
-
|
| 21 |
-
// CFH offsets (relative to CFH start)
|
| 22 |
-
const CFH_OFF_COMP_METHOD: usize = 10; // u16
|
| 23 |
-
const CFH_OFF_COMP_SIZE: usize = 20; // u32
|
| 24 |
-
const CFH_OFF_NAME_LEN: usize = 28; // u16
|
| 25 |
-
const CFH_OFF_EXTRA_LEN: usize = 30; // u16
|
| 26 |
-
const CFH_OFF_COMMENT_LEN: usize = 32; // u16
|
| 27 |
-
const CFH_OFF_LFH_OFFSET: usize = 42; // u32
|
| 28 |
-
|
| 29 |
-
// LFH offsets (relative to LFH start)
|
| 30 |
-
const LFH_OFF_NAME_LEN: usize = 26; // u16
|
| 31 |
-
const LFH_OFF_EXTRA_LEN: usize = 28; // u16
|
| 32 |
-
|
| 33 |
-
#[inline]
|
| 34 |
-
fn le_u16(bytes: &[u8], off: usize) -> u16 {
|
| 35 |
-
u16::from_le_bytes([bytes[off], bytes[off + 1]])
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
#[inline]
|
| 39 |
-
fn le_u32(bytes: &[u8], off: usize) -> u32 {
|
| 40 |
-
u32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
#[allow(unused)]
|
| 44 |
-
#[derive(Debug, Clone)]
|
| 45 |
-
pub struct RecordEntry {
|
| 46 |
-
pub path: String,
|
| 47 |
-
pub hash: Option<String>, // e.g., Some("sha256-<urlsafe_b64>") or None
|
| 48 |
-
pub size: Option<u64>,
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
pub async fn fetch_record(url: &str) -> Result<Vec<RecordEntry>> {
|
| 52 |
-
// 1) Locate EOCD and central directory
|
| 53 |
-
let (cd_offset, cd_size) = http_zip_eocd_and_cd(url).await?;
|
| 54 |
-
let cd_bytes = range::get_range(url, cd_offset, cd_offset + cd_size - 1).await?;
|
| 55 |
-
let (lh_off, comp_size, comp_method) = parse_central_directory_for_record(&cd_bytes)?;
|
| 56 |
-
|
| 57 |
-
// 2) Read local header to compute exact data offset
|
| 58 |
-
let lh_fixed = range::get_range(url, lh_off, lh_off + LFH_FIXED_LEN - 1).await?;
|
| 59 |
-
if lh_fixed.len() < LFH_FIXED_LEN as usize || lh_fixed[0..4] != SIG_LFH {
|
| 60 |
-
anyhow::bail!("bad local file header");
|
| 61 |
-
}
|
| 62 |
-
let name_len = le_u16(&lh_fixed, LFH_OFF_NAME_LEN) as u64;
|
| 63 |
-
let extra_len = le_u16(&lh_fixed, LFH_OFF_EXTRA_LEN) as u64;
|
| 64 |
-
let data_off = lh_off + LFH_FIXED_LEN + name_len + extra_len;
|
| 65 |
-
let data_end = data_off + comp_size as u64 - 1;
|
| 66 |
-
|
| 67 |
-
// 3) Fetch and decode RECORD (deflate only, method 8)
|
| 68 |
-
let comp = range::get_range(url, data_off, data_end).await?;
|
| 69 |
-
if comp_method != 8 {
|
| 70 |
-
anyhow::bail!("RECORD compression method unsupported: {comp_method}");
|
| 71 |
-
}
|
| 72 |
-
let mut dec = DeflateDecoder::new(std::io::Cursor::new(comp));
|
| 73 |
-
let mut rec_csv = Vec::new();
|
| 74 |
-
io::copy(&mut dec, &mut rec_csv)?;
|
| 75 |
-
|
| 76 |
-
// 4) Parse CSV for all entries
|
| 77 |
-
parse_record_csv(&rec_csv)
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
fn parse_central_directory_for_record(cd: &[u8]) -> Result<(u64, u32, u16)> {
|
| 81 |
-
let mut i = 0usize;
|
| 82 |
-
while i + CFH_FIXED_LEN <= cd.len() {
|
| 83 |
-
if cd[i..i + 4] != SIG_CFH {
|
| 84 |
-
i += 1;
|
| 85 |
-
continue;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
let comp_method = le_u16(cd, i + CFH_OFF_COMP_METHOD);
|
| 89 |
-
let comp_size = le_u32(cd, i + CFH_OFF_COMP_SIZE);
|
| 90 |
-
let name_len = le_u16(cd, i + CFH_OFF_NAME_LEN);
|
| 91 |
-
let extra_len = le_u16(cd, i + CFH_OFF_EXTRA_LEN);
|
| 92 |
-
let comment_len = le_u16(cd, i + CFH_OFF_COMMENT_LEN);
|
| 93 |
-
let lh_off = le_u32(cd, i + CFH_OFF_LFH_OFFSET) as u64;
|
| 94 |
-
|
| 95 |
-
let name_start = i + CFH_FIXED_LEN;
|
| 96 |
-
let name_end = name_start + name_len as usize;
|
| 97 |
-
if name_end > cd.len() {
|
| 98 |
-
anyhow::bail!("bad central directory entry");
|
| 99 |
-
}
|
| 100 |
-
let name = &cd[name_start..name_end];
|
| 101 |
-
let name_str = std::str::from_utf8(name).unwrap_or("");
|
| 102 |
-
if name_str.ends_with("/RECORD") && name_str.contains(".dist-info/") {
|
| 103 |
-
if comp_size == u32::MAX {
|
| 104 |
-
anyhow::bail!("zip64 not supported for RECORD");
|
| 105 |
-
}
|
| 106 |
-
return Ok((lh_off, comp_size, comp_method));
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
i = name_end + extra_len as usize + comment_len as usize;
|
| 110 |
-
}
|
| 111 |
-
anyhow::bail!("RECORD not found")
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
async fn http_zip_eocd_and_cd(url: &str) -> Result<(u64, u64)> {
|
| 115 |
-
// Fetch last ~70KiB to find EOCD (max comment is 64KiB; add slack)
|
| 116 |
-
let tail = range::get_tail(url, 70 * 1024).await?;
|
| 117 |
-
let mut found = None;
|
| 118 |
-
for i in (0..=tail.len().saturating_sub(EOCD_MIN_LEN)).rev() {
|
| 119 |
-
if tail[i..i + 4] == SIG_EOCD {
|
| 120 |
-
found = Some(i);
|
| 121 |
-
break;
|
| 122 |
-
}
|
| 123 |
-
}
|
| 124 |
-
let pos = found.ok_or_else(|| anyhow::anyhow!("zip EOCD not found"))?;
|
| 125 |
-
let eocd = &tail[pos..];
|
| 126 |
-
if eocd.len() < EOCD_MIN_LEN {
|
| 127 |
-
anyhow::bail!("truncated EOCD");
|
| 128 |
-
}
|
| 129 |
-
let cd_size = le_u32(eocd, EOCD_OFF_CD_SIZE) as u64;
|
| 130 |
-
let cd_off = le_u32(eocd, EOCD_OFF_CD_OFFSET) as u64;
|
| 131 |
-
Ok((cd_off, cd_size))
|
| 132 |
-
}
|
| 133 |
-
|
| 134 |
-
fn parse_record_csv(csv_bytes: &[u8]) -> Result<Vec<RecordEntry>> {
|
| 135 |
-
let mut rdr = csv::ReaderBuilder::new()
|
| 136 |
-
.has_headers(false)
|
| 137 |
-
.from_reader(csv_bytes);
|
| 138 |
-
let mut out = Vec::new();
|
| 139 |
-
for rec in rdr.records() {
|
| 140 |
-
let r = rec?;
|
| 141 |
-
if r.is_empty() {
|
| 142 |
-
continue;
|
| 143 |
-
}
|
| 144 |
-
let path = r.get(0).unwrap_or("").to_string();
|
| 145 |
-
let hash = r.get(1).and_then(|s| {
|
| 146 |
-
if s.is_empty() {
|
| 147 |
-
None
|
| 148 |
-
} else {
|
| 149 |
-
Some(s.to_string())
|
| 150 |
-
}
|
| 151 |
-
});
|
| 152 |
-
let size = r.get(2).and_then(|s| s.parse::<u64>().ok());
|
| 153 |
-
out.push(RecordEntry { path, hash, size });
|
| 154 |
-
}
|
| 155 |
-
Ok(out)
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
#[cfg(test)]
|
| 159 |
-
mod tests {
|
| 160 |
-
use super::*;
|
| 161 |
-
|
| 162 |
-
#[tokio::test]
|
| 163 |
-
async fn test_pip_wheels() -> Result<()> {
|
| 164 |
-
async fn pick_wheel_url(pkg: &str, version: &str, tag: &str) -> Result<String> {
|
| 165 |
-
let meta_url = format!("https://pypi.org/pypi/{pkg}/{version}/json");
|
| 166 |
-
let resp = http_client().get(&meta_url).send().await?;
|
| 167 |
-
let json: serde_json::Value = resp.json().await?;
|
| 168 |
-
let urls = json
|
| 169 |
-
.get("urls")
|
| 170 |
-
.and_then(|v| v.as_array())
|
| 171 |
-
.ok_or_else(|| anyhow::anyhow!("bad json: urls"))?;
|
| 172 |
-
|
| 173 |
-
urls.iter()
|
| 174 |
-
.filter_map(|f| {
|
| 175 |
-
let filename = f.get("filename").and_then(|v| v.as_str())?;
|
| 176 |
-
let file_url = f.get("url").and_then(|v| v.as_str())?;
|
| 177 |
-
if !filename.ends_with(".whl") {
|
| 178 |
-
return None;
|
| 179 |
-
}
|
| 180 |
-
let matches = if tag == "manylinux" {
|
| 181 |
-
filename.contains("manylinux") && filename.contains("x86_64")
|
| 182 |
-
} else {
|
| 183 |
-
filename.contains(tag)
|
| 184 |
-
};
|
| 185 |
-
if matches {
|
| 186 |
-
Some(file_url.to_string())
|
| 187 |
-
} else {
|
| 188 |
-
None
|
| 189 |
-
}
|
| 190 |
-
})
|
| 191 |
-
.next()
|
| 192 |
-
.ok_or_else(|| anyhow::anyhow!("no suitable wheel for tag {tag}"))
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
// Pin versions that are known to publish both win_amd64 and manylinux wheels.
|
| 196 |
-
// The latest PyPI release may only have a source tarball (e.g. cublas 12.9.2.10).
|
| 197 |
-
for (pkg, version) in [
|
| 198 |
-
("nvidia-cuda-runtime-cu12", "12.9.79"),
|
| 199 |
-
("nvidia-cublas-cu12", "12.9.1.4"),
|
| 200 |
-
] {
|
| 201 |
-
for tag in ["win_amd64", "manylinux"] {
|
| 202 |
-
let url = pick_wheel_url(pkg, version, tag).await?;
|
| 203 |
-
let entries = fetch_record(&url).await?;
|
| 204 |
-
assert!(
|
| 205 |
-
!entries.is_empty(),
|
| 206 |
-
"{} {}: record entries should not be empty",
|
| 207 |
-
pkg,
|
| 208 |
-
tag
|
| 209 |
-
);
|
| 210 |
-
assert!(
|
| 211 |
-
entries.iter().any(|e| e.path.ends_with("/RECORD")),
|
| 212 |
-
"{} {}: RECORD file should be present",
|
| 213 |
-
pkg,
|
| 214 |
-
tag
|
| 215 |
-
);
|
| 216 |
-
assert!(
|
| 217 |
-
entries.iter().any(|e| e.size.is_some()),
|
| 218 |
-
"{} {}: some RECORD entries should have sizes",
|
| 219 |
-
pkg,
|
| 220 |
-
tag
|
| 221 |
-
);
|
| 222 |
-
}
|
| 223 |
-
}
|
| 224 |
-
Ok(())
|
| 225 |
-
}
|
| 226 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
koharu/Cargo.toml
CHANGED
|
@@ -40,7 +40,6 @@ tauri-plugin-updater = { workspace = true }
|
|
| 40 |
tokio = { workspace = true }
|
| 41 |
tracing = { workspace = true }
|
| 42 |
tracing-subscriber = { workspace = true }
|
| 43 |
-
libloading = { workspace = true }
|
| 44 |
|
| 45 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 46 |
koharu-llm = { workspace = true }
|
|
@@ -55,8 +54,7 @@ koharu-llm = { workspace = true }
|
|
| 55 |
koharu-llm = { workspace = true }
|
| 56 |
|
| 57 |
[features]
|
| 58 |
-
cuda = ["koharu-
|
| 59 |
-
cudnn = ["koharu-runtime/cudnn", "koharu-ml/cudnn"]
|
| 60 |
metal = ["koharu-ml/metal"]
|
| 61 |
|
| 62 |
[build-dependencies]
|
|
|
|
| 40 |
tokio = { workspace = true }
|
| 41 |
tracing = { workspace = true }
|
| 42 |
tracing-subscriber = { workspace = true }
|
|
|
|
| 43 |
|
| 44 |
[target.'cfg(target_os = "windows")'.dependencies]
|
| 45 |
koharu-llm = { workspace = true }
|
|
|
|
| 54 |
koharu-llm = { workspace = true }
|
| 55 |
|
| 56 |
[features]
|
| 57 |
+
cuda = ["koharu-ml/cuda"]
|
|
|
|
| 58 |
metal = ["koharu-ml/metal"]
|
| 59 |
|
| 60 |
[build-dependencies]
|
koharu/src/app.rs
CHANGED
|
@@ -8,12 +8,11 @@ use tauri::{Manager, WebviewWindowBuilder};
|
|
| 8 |
use tokio::{net::TcpListener, sync::RwLock};
|
| 9 |
use tracing_subscriber::fmt::format::FmtSpan;
|
| 10 |
|
| 11 |
-
use koharu_llm::
|
| 12 |
use koharu_ml::{cuda_is_available, device};
|
| 13 |
use koharu_pipeline::AppResources;
|
| 14 |
use koharu_renderer::facade::Renderer;
|
| 15 |
use koharu_rpc::{SharedResources, server};
|
| 16 |
-
use koharu_runtime::{ensure_dylibs, preload_dylibs};
|
| 17 |
use koharu_types::State;
|
| 18 |
|
| 19 |
static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
|
|
@@ -21,8 +20,6 @@ static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
|
|
| 21 |
.map(|path| path.join("Koharu"))
|
| 22 |
.unwrap_or_default()
|
| 23 |
});
|
| 24 |
-
static LIB_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("libs"));
|
| 25 |
-
static LLAMA_RUNTIME_ROOT: Lazy<PathBuf> = Lazy::new(llm_runtime_dir);
|
| 26 |
static MODEL_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("models"));
|
| 27 |
|
| 28 |
#[derive(Parser)]
|
|
@@ -108,24 +105,14 @@ fn initialize(headless: bool, _debug: bool) -> Result<()> {
|
|
| 108 |
}
|
| 109 |
|
| 110 |
async fn prefetch() -> Result<()> {
|
| 111 |
-
|
| 112 |
-
koharu_llm::safe::runtime::ensure_dylibs(LLAMA_RUNTIME_ROOT.as_path())
|
| 113 |
.await
|
| 114 |
-
.context("Failed to
|
| 115 |
koharu_ml::facade::prefetch().await?;
|
| 116 |
|
| 117 |
Ok(())
|
| 118 |
}
|
| 119 |
|
| 120 |
-
async fn initialize_llama_runtime() -> Result<()> {
|
| 121 |
-
koharu_llm::safe::runtime::ensure_dylibs(LLAMA_RUNTIME_ROOT.as_path())
|
| 122 |
-
.await
|
| 123 |
-
.context("Failed to ensure llama.cpp runtime libraries")?;
|
| 124 |
-
koharu_llm::safe::runtime::initialize(LLAMA_RUNTIME_ROOT.as_path())
|
| 125 |
-
.context("Failed to initialize llama.cpp runtime libraries")?;
|
| 126 |
-
Ok(())
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
fn warning(headless: bool, title: &str, description: &str) {
|
| 130 |
tracing::warn!("{description}");
|
| 131 |
|
|
@@ -144,7 +131,7 @@ async fn build_resources(cpu: bool, headless: bool) -> Result<AppResources> {
|
|
| 144 |
let mut cpu = cpu;
|
| 145 |
|
| 146 |
if !cpu && cuda_is_available() {
|
| 147 |
-
match
|
| 148 |
Ok(version) if version.supports_cuda_13_1() => {
|
| 149 |
tracing::info!("NVIDIA driver reports CUDA {version} support");
|
| 150 |
}
|
|
@@ -171,29 +158,21 @@ async fn build_resources(cpu: bool, headless: bool) -> Result<AppResources> {
|
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
.context("Failed to ensure dynamic libraries")?;
|
| 178 |
-
preload_dylibs(LIB_ROOT.to_path_buf()).context("Failed to preload dynamic libraries")?;
|
| 179 |
|
|
|
|
| 180 |
#[cfg(target_os = "windows")]
|
| 181 |
{
|
| 182 |
if let Err(err) = crate::windows::register_khr() {
|
| 183 |
tracing::warn!(?err, "Failed to register .khr file association");
|
| 184 |
}
|
| 185 |
-
|
| 186 |
-
crate::windows::add_dll_directory(&LIB_ROOT).context("Failed to add DLL directory")?;
|
| 187 |
}
|
| 188 |
|
| 189 |
-
tracing::info!(
|
| 190 |
-
"CUDA is available, loaded dynamic libraries from {:?}",
|
| 191 |
-
*LIB_ROOT
|
| 192 |
-
);
|
| 193 |
}
|
| 194 |
|
| 195 |
-
initialize_llama_runtime().await?;
|
| 196 |
-
|
| 197 |
let ml = Arc::new(
|
| 198 |
koharu_ml::facade::Model::new(cpu)
|
| 199 |
.await
|
|
@@ -313,3 +292,4 @@ pub async fn run() -> Result<()> {
|
|
| 313 |
|
| 314 |
Ok(())
|
| 315 |
}
|
|
|
|
|
|
| 8 |
use tokio::{net::TcpListener, sync::RwLock};
|
| 9 |
use tracing_subscriber::fmt::format::FmtSpan;
|
| 10 |
|
| 11 |
+
use koharu_llm::facade;
|
| 12 |
use koharu_ml::{cuda_is_available, device};
|
| 13 |
use koharu_pipeline::AppResources;
|
| 14 |
use koharu_renderer::facade::Renderer;
|
| 15 |
use koharu_rpc::{SharedResources, server};
|
|
|
|
| 16 |
use koharu_types::State;
|
| 17 |
|
| 18 |
static APP_ROOT: Lazy<PathBuf> = Lazy::new(|| {
|
|
|
|
| 20 |
.map(|path| path.join("Koharu"))
|
| 21 |
.unwrap_or_default()
|
| 22 |
});
|
|
|
|
|
|
|
| 23 |
static MODEL_ROOT: Lazy<PathBuf> = Lazy::new(|| APP_ROOT.join("models"));
|
| 24 |
|
| 25 |
#[derive(Parser)]
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
async fn prefetch() -> Result<()> {
|
| 108 |
+
koharu_runtime::initialize()
|
|
|
|
| 109 |
.await
|
| 110 |
+
.context("Failed to initialize runtime packages")?;
|
| 111 |
koharu_ml::facade::prefetch().await?;
|
| 112 |
|
| 113 |
Ok(())
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
fn warning(headless: bool, title: &str, description: &str) {
|
| 117 |
tracing::warn!("{description}");
|
| 118 |
|
|
|
|
| 131 |
let mut cpu = cpu;
|
| 132 |
|
| 133 |
if !cpu && cuda_is_available() {
|
| 134 |
+
match koharu_runtime::cuda_driver_version() {
|
| 135 |
Ok(version) if version.supports_cuda_13_1() => {
|
| 136 |
tracing::info!("NVIDIA driver reports CUDA {version} support");
|
| 137 |
}
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
|
| 161 |
+
koharu_runtime::initialize()
|
| 162 |
+
.await
|
| 163 |
+
.context("Failed to initialize runtime packages")?;
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
if !cpu && cuda_is_available() {
|
| 166 |
#[cfg(target_os = "windows")]
|
| 167 |
{
|
| 168 |
if let Err(err) = crate::windows::register_khr() {
|
| 169 |
tracing::warn!(?err, "Failed to register .khr file association");
|
| 170 |
}
|
|
|
|
|
|
|
| 171 |
}
|
| 172 |
|
| 173 |
+
tracing::info!("CUDA is available and runtime packages were initialized");
|
|
|
|
|
|
|
|
|
|
| 174 |
}
|
| 175 |
|
|
|
|
|
|
|
| 176 |
let ml = Arc::new(
|
| 177 |
koharu_ml::facade::Model::new(cpu)
|
| 178 |
.await
|
|
|
|
| 292 |
|
| 293 |
Ok(())
|
| 294 |
}
|
| 295 |
+
|
koharu/src/lib.rs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
pub mod app;
|
| 2 |
pub mod assets;
|
| 3 |
-
pub mod nvidia;
|
| 4 |
pub mod version;
|
| 5 |
#[cfg(target_os = "windows")]
|
| 6 |
pub mod windows;
|
|
|
|
| 1 |
pub mod app;
|
| 2 |
pub mod assets;
|
|
|
|
| 3 |
pub mod version;
|
| 4 |
#[cfg(target_os = "windows")]
|
| 5 |
pub mod windows;
|
koharu/src/nvidia.rs
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
use std::fmt;
|
| 2 |
-
|
| 3 |
-
use anyhow::{Context, Result};
|
| 4 |
-
|
| 5 |
-
const CUDA_SUCCESS: i32 = 0;
|
| 6 |
-
const CUDA_13_1_DRIVER_VERSION: i32 = 13010;
|
| 7 |
-
|
| 8 |
-
type CuInit = unsafe extern "C" fn(flags: u32) -> i32;
|
| 9 |
-
type CuDriverGetVersion = unsafe extern "C" fn(driver_version: *mut i32) -> i32;
|
| 10 |
-
|
| 11 |
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
| 12 |
-
pub struct CudaDriverVersion {
|
| 13 |
-
raw: i32,
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
impl CudaDriverVersion {
|
| 17 |
-
pub const fn from_raw(raw: i32) -> Self {
|
| 18 |
-
Self { raw }
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
pub const fn raw(self) -> i32 {
|
| 22 |
-
self.raw
|
| 23 |
-
}
|
| 24 |
-
|
| 25 |
-
pub const fn major(self) -> i32 {
|
| 26 |
-
self.raw / 1000
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
pub const fn minor(self) -> i32 {
|
| 30 |
-
(self.raw % 1000) / 10
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
pub const fn supports_cuda_13_1(self) -> bool {
|
| 34 |
-
self.raw >= CUDA_13_1_DRIVER_VERSION
|
| 35 |
-
}
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
impl fmt::Display for CudaDriverVersion {
|
| 39 |
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 40 |
-
write!(f, "{}.{}", self.major(), self.minor())
|
| 41 |
-
}
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
pub fn supports_cuda_13_1() -> Result<bool> {
|
| 45 |
-
Ok(driver_version()?.supports_cuda_13_1())
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
pub fn driver_version() -> Result<CudaDriverVersion> {
|
| 49 |
-
let lib_name = if cfg!(target_os = "windows") {
|
| 50 |
-
"nvcuda.dll"
|
| 51 |
-
} else {
|
| 52 |
-
"libcuda.so"
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
unsafe {
|
| 56 |
-
let library = libloading::Library::new(lib_name)
|
| 57 |
-
.with_context(|| format!("Failed to load NVIDIA driver library {lib_name}"))?;
|
| 58 |
-
let cu_init = *library
|
| 59 |
-
.get::<CuInit>(b"cuInit\0")
|
| 60 |
-
.context("Failed to load cuInit from NVIDIA driver")?;
|
| 61 |
-
let cu_driver_get_version = *library
|
| 62 |
-
.get::<CuDriverGetVersion>(b"cuDriverGetVersion\0")
|
| 63 |
-
.context("Failed to load cuDriverGetVersion from NVIDIA driver")?;
|
| 64 |
-
|
| 65 |
-
let init_status = cu_init(0);
|
| 66 |
-
ensure_cuda_success(init_status, "cuInit")?;
|
| 67 |
-
|
| 68 |
-
let mut raw = 0;
|
| 69 |
-
let version_status = cu_driver_get_version(&mut raw);
|
| 70 |
-
ensure_cuda_success(version_status, "cuDriverGetVersion")?;
|
| 71 |
-
|
| 72 |
-
Ok(CudaDriverVersion::from_raw(raw))
|
| 73 |
-
}
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
fn ensure_cuda_success(code: i32, op: &str) -> Result<()> {
|
| 77 |
-
if code == CUDA_SUCCESS {
|
| 78 |
-
return Ok(());
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
anyhow::bail!("{op} failed with CUDA driver error code {code}")
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
#[cfg(test)]
|
| 85 |
-
mod tests {
|
| 86 |
-
use super::*;
|
| 87 |
-
|
| 88 |
-
#[test]
|
| 89 |
-
fn parses_major_minor_from_driver_version() {
|
| 90 |
-
let version = CudaDriverVersion::from_raw(13010);
|
| 91 |
-
assert_eq!(version.major(), 13);
|
| 92 |
-
assert_eq!(version.minor(), 1);
|
| 93 |
-
assert_eq!(version.to_string(), "13.1");
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
#[test]
|
| 97 |
-
fn checks_cuda_13_1_threshold() {
|
| 98 |
-
assert!(CudaDriverVersion::from_raw(13010).supports_cuda_13_1());
|
| 99 |
-
assert!(CudaDriverVersion::from_raw(13020).supports_cuda_13_1());
|
| 100 |
-
assert!(!CudaDriverVersion::from_raw(13000).supports_cuda_13_1());
|
| 101 |
-
assert!(!CudaDriverVersion::from_raw(12080).supports_cuda_13_1());
|
| 102 |
-
}
|
| 103 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
koharu/tauri.linux.conf.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
{
|
| 2 |
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
| 3 |
-
"identifier": "Koharu",
|
| 4 |
-
"build": {
|
| 5 |
-
"features": [
|
| 6 |
-
"cuda"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
| 3 |
+
"identifier": "Koharu",
|
| 4 |
+
"build": {
|
| 5 |
+
"features": [
|
| 6 |
+
"cuda"
|
| 7 |
+
]
|
| 8 |
+
}
|
| 9 |
+
}
|
|
|
koharu/tauri.windows.conf.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
{
|
| 2 |
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
| 3 |
-
"identifier": "Koharu",
|
| 4 |
-
"build": {
|
| 5 |
-
"features": [
|
| 6 |
-
"cuda"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
| 3 |
+
"identifier": "Koharu",
|
| 4 |
+
"build": {
|
| 5 |
+
"features": [
|
| 6 |
+
"cuda"
|
| 7 |
+
]
|
| 8 |
+
}
|
| 9 |
+
}
|
|
|
scripts/dev.ts
CHANGED
|
@@ -62,37 +62,6 @@ async function setupCuda() {
|
|
| 62 |
)
|
| 63 |
}
|
| 64 |
|
| 65 |
-
async function setupCudnn() {
|
| 66 |
-
const cudnnRoot = 'C:/Program Files/NVIDIA/CUDNN'
|
| 67 |
-
const versions = await readdir(cudnnRoot).catch(() => [])
|
| 68 |
-
|
| 69 |
-
sortVersionsDesc(versions)
|
| 70 |
-
|
| 71 |
-
for (const version of versions) {
|
| 72 |
-
if (version.startsWith('v')) {
|
| 73 |
-
const binPath = path.join(cudnnRoot, version, 'bin')
|
| 74 |
-
|
| 75 |
-
if (await pathExists(binPath)) {
|
| 76 |
-
const versions = await readdir(binPath)
|
| 77 |
-
|
| 78 |
-
sortVersionsDesc(versions)
|
| 79 |
-
|
| 80 |
-
for (const version of versions) {
|
| 81 |
-
const fullPath = path.join(binPath, version, 'x64')
|
| 82 |
-
process.env.PATH = `${fullPath}${path.delimiter}${process.env.PATH}`
|
| 83 |
-
|
| 84 |
-
console.log(`Added cuDNN to PATH: ${fullPath}`)
|
| 85 |
-
return
|
| 86 |
-
}
|
| 87 |
-
}
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
throw new Error(
|
| 91 |
-
'cuDNN not found. Please install cuDNN from https://developer.nvidia.com/rdp/cudnn-download',
|
| 92 |
-
)
|
| 93 |
-
}
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
async function setupCl() {
|
| 97 |
const vsRoot = 'C:/Program Files/Microsoft Visual Studio'
|
| 98 |
const vsVersions = await readdir(vsRoot).catch(() => [])
|
|
@@ -129,9 +98,6 @@ async function dev() {
|
|
| 129 |
await checkNvcc()
|
| 130 |
})
|
| 131 |
|
| 132 |
-
// Setup cuDNN path
|
| 133 |
-
await setupCudnn()
|
| 134 |
-
|
| 135 |
// Setup cl.exe path
|
| 136 |
await setupCl()
|
| 137 |
}
|
|
|
|
| 62 |
)
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
async function setupCl() {
|
| 66 |
const vsRoot = 'C:/Program Files/Microsoft Visual Studio'
|
| 67 |
const vsVersions = await readdir(vsRoot).catch(() => [])
|
|
|
|
| 98 |
await checkNvcc()
|
| 99 |
})
|
| 100 |
|
|
|
|
|
|
|
|
|
|
| 101 |
// Setup cl.exe path
|
| 102 |
await setupCl()
|
| 103 |
}
|