Mayo commited on
Commit
e5a161e
·
unverified ·
1 Parent(s): 8310b2d

refactor: koharu-runtime

Browse files
.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.9.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
- "csv",
4120
  "flate2",
4121
- "futures",
4122
  "koharu-http",
4123
- "libloading 0.9.0",
4124
- "num_cpus",
4125
- "once_cell",
4126
- "rayon",
4127
- "reqwest 0.13.2",
4128
  "serde_json",
 
4129
  "tempfile",
4130
  "tokio",
4131
- "tracing",
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 = "0.8.9"
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
- koharu_llm::safe::runtime::ensure_dylibs(&runtime_dir)
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
- let runtime_dir = koharu_llm::runtime_dir();
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={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-{LLAMA_CPP_TAG}.tar.gz"));
131
- let source_root = cache_dir.join(format!("llama.cpp-{LLAMA_CPP_TAG}"));
 
 
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(SOURCE_URL)
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
- static LIBRARIES: OnceCell<LoadedLibraries> = OnceCell::new();
 
58
 
59
- pub(crate) fn is_initialized() -> bool {
60
- LIBRARIES.get().is_some()
61
- }
62
 
63
- pub(crate) fn initialize(dir: &Path) -> Result<()> {
64
- let canonical_dir = canonical_dir(dir)?;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- if let Some(libraries) = LIBRARIES.get() {
67
- if libraries.path == canonical_dir {
 
 
 
 
68
  return Ok(());
69
  }
70
-
71
  bail!(
72
  "koharu-llm is already initialized with `{}` and cannot be reinitialized with `{}`",
73
- libraries.path.display(),
74
- canonical_dir.display()
75
  );
76
  }
77
 
78
- let load_plan = crate::safe::runtime::load_plan(&canonical_dir)?;
79
- let libraries = load_libraries(&canonical_dir, &load_plan)?;
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 canonical_dir(dir: &Path) -> Result<PathBuf> {
90
- if dir.exists() {
91
- dir.canonicalize().with_context(|| {
92
- format!(
93
- "failed to canonicalize runtime directory `{}`",
94
- dir.display()
95
- )
96
- })
97
- } else {
98
- bail!(
99
- "runtime directory `{}` does not exist; call `koharu_llm::runtime::ensure_dylibs` first",
100
- dir.display()
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
- auxiliary,
147
- llama: llama.ok_or_else(|| anyhow!("core llama runtime library was not loaded"))?,
148
- ggml: ggml.ok_or_else(|| anyhow!("core ggml runtime library was not loaded"))?,
149
- ggml_base: ggml_base
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 load_library(path: &Path) -> Result<Library> {
156
- unsafe { Library::new(path) }.with_context(|| format!("failed to load `{}`", path.display()))
 
 
 
 
 
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 `{}` is not valid UTF-8", dir.display()))?;
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 `koharu_llm::runtime::initialize` first",
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
- let runtime_dir = koharu_llm::runtime_dir();
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 dylib;
2
- mod zip;
 
 
3
 
4
- pub use dylib::{ensure_dylibs, preload_dylibs};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-runtime/cuda", "koharu-ml/cuda"]
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::{facade, runtime_dir as llm_runtime_dir};
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
- ensure_dylibs(LIB_ROOT.to_path_buf()).await?;
112
- koharu_llm::safe::runtime::ensure_dylibs(LLAMA_RUNTIME_ROOT.as_path())
113
  .await
114
- .context("Failed to ensure llama.cpp runtime libraries")?;
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 crate::nvidia::driver_version() {
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
- if !cpu && cuda_is_available() {
175
- ensure_dylibs(LIB_ROOT.to_path_buf())
176
- .await
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
- "cudnn"
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
- "cudnn"
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
  }