lean_ctx/core/
ort_execution_providers.rs1use std::path::Path;
10
11const PROVIDER_ENV: &str = "LEAN_CTX_ORT_EXECUTION_PROVIDER";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14enum ProviderPolicy {
15 Cpu,
16 Gpu,
17 Auto,
18}
19
20pub fn execution_providers() -> Vec<ort::ep::ExecutionProviderDispatch> {
22 match provider_policy() {
23 ProviderPolicy::Cpu => cpu_execution_providers(),
24 ProviderPolicy::Gpu => gpu_execution_providers(),
25 ProviderPolicy::Auto => {
26 if selected_runtime_looks_gpu() {
27 gpu_execution_providers()
28 } else {
29 tracing::debug!(
30 env = PROVIDER_ENV,
31 "ONNX Runtime GPU auto-detect did not find a GPU runtime; using CPU"
32 );
33 cpu_execution_providers()
34 }
35 }
36 }
37}
38
39pub fn execution_provider_status() -> String {
40 let policy = provider_policy_name();
41 let compiled = compiled_gpu_provider_names();
42 let compiled = if compiled.is_empty() {
43 "none".to_string()
44 } else {
45 compiled.join(",")
46 };
47 format!(
48 "ORT execution provider policy: {policy} (env {PROVIDER_ENV}; compiled GPU EPs: {compiled})"
49 )
50}
51
52fn policy_wants_gpu() -> bool {
55 if compiled_gpu_provider_names().is_empty() {
56 return false;
57 }
58 match provider_policy() {
59 ProviderPolicy::Cpu => false,
60 ProviderPolicy::Gpu => true,
61 ProviderPolicy::Auto => selected_runtime_looks_gpu(),
62 }
63}
64
65pub fn gpu_active() -> bool {
73 if !policy_wants_gpu() {
74 return false;
75 }
76 #[cfg(feature = "ort-cuda")]
80 {
81 cuda_runtime_available()
82 }
83 #[cfg(not(feature = "ort-cuda"))]
84 {
85 true
86 }
87}
88
89pub fn gpu_fallback_warning() -> Option<String> {
94 #[cfg(feature = "ort-cuda")]
95 {
96 if !policy_wants_gpu() || cuda_runtime_available() {
97 return None;
98 }
99 let detail = probe_cuda_runtime().err().unwrap_or_default();
100 Some(cuda_missing_message(&detail))
101 }
102 #[cfg(not(feature = "ort-cuda"))]
103 {
104 None
105 }
106}
107
108#[cfg(feature = "ort-cuda")]
110fn cuda_provider_lib_name() -> &'static str {
111 #[cfg(target_os = "windows")]
112 {
113 "onnxruntime_providers_cuda.dll"
114 }
115 #[cfg(target_os = "macos")]
116 {
117 "libonnxruntime_providers_cuda.dylib"
118 }
119 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
120 {
121 "libonnxruntime_providers_cuda.so"
122 }
123}
124
125#[cfg(feature = "ort-cuda")]
127fn cuda_provider_lib_path() -> Option<std::path::PathBuf> {
128 let dylib = crate::core::ort_environment::resolved_ort_dylib_path().ok()?;
129 Some(dylib.parent()?.join(cuda_provider_lib_name()))
130}
131
132#[cfg(feature = "ort-cuda")]
137fn probe_cuda_runtime() -> Result<(), String> {
138 let path = cuda_provider_lib_path()
139 .ok_or_else(|| "could not resolve the ORT CUDA provider library path".to_string())?;
140 if !path.exists() {
141 return Err(format!("{} not found", path.display()));
142 }
143 unsafe { libloading::Library::new(&path) }
147 .map(|_lib| ())
148 .or_else(|e| {
149 let err = e.to_string();
150 if err.contains("Provider_GetHost") {
154 Ok(())
155 } else {
156 Err(err)
157 }
158 })
159}
160
161#[cfg(feature = "ort-cuda")]
163fn cuda_runtime_available() -> bool {
164 static CACHE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
165 *CACHE.get_or_init(|| probe_cuda_runtime().is_ok())
166}
167
168#[cfg(feature = "ort-cuda")]
171fn cuda_missing_message(probe_err: &str) -> String {
172 format!(
173 "GPU requested (via {env}) but the CUDA runtime libraries required by ONNX Runtime \
174 could not be loaded — embedding is running on CPU. Loader error: {probe_err}\n\
175 ONNX Runtime 1.{ort_minor}.x needs CUDA 12 + cuDNN 9. Missing libraries typically \
176 include: libcudart.so.12, libcublas.so.12, libcublasLt.so.12, libcudnn.so.9, \
177 libcurand.so.10, libcufft.so.11.\n\
178 Install them on Ubuntu / WSL2:\n \
179 wget -O /tmp/cuda-keyring_1.1-1_all.deb https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb\n \
180 sudo dpkg -i /tmp/cuda-keyring_1.1-1_all.deb && rm -f /tmp/cuda-keyring_1.1-1_all.deb && sudo apt-get update\n \
181 sudo apt-get install -y cuda-cudart-12-8 libcublas-12-8 libcurand-12-8 libcufft-12-8\n \
182 python3 -m venv $HOME/.local/share/lean-ctx/cuda-libs\n \
183 $HOME/.local/share/lean-ctx/cuda-libs/bin/python -m pip install nvidia-cudnn-cu12==9.8.0.87\n \
184 # then ensure the loader can find them (if not already on the path):\n \
185 export LD_LIBRARY_PATH=$($HOME/.local/share/lean-ctx/cuda-libs/bin/python -c 'import pathlib, nvidia.cudnn; print(pathlib.Path(nvidia.cudnn.__file__).parent / '\''lib'\'')'):/usr/local/cuda-12.8/targets/x86_64-linux/lib:/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH\n\
186 To silence this and stay on CPU, set {env}=cpu.",
187 env = PROVIDER_ENV,
188 ort_minor = ort::MINOR_VERSION,
189 )
190}
191
192pub fn execution_provider_help() -> &'static str {
193 "By default lean-ctx auto-detects GPU runtimes from ORT_DYLIB_PATH and otherwise uses CPU. Set LEAN_CTX_ORT_EXECUTION_PROVIDER=cpu|gpu|auto to override."
194}
195
196fn cpu_execution_providers() -> Vec<ort::ep::ExecutionProviderDispatch> {
197 vec![ort::ep::CPU::default().build()]
198}
199
200pub fn gpu_execution_providers() -> Vec<ort::ep::ExecutionProviderDispatch> {
202 #[allow(unused_mut)]
203 let mut eps: Vec<ort::ep::ExecutionProviderDispatch> = Vec::new();
204 let compiled_gpu_count = compiled_gpu_provider_names().len();
205
206 #[cfg(feature = "ort-cuda")]
207 {
208 tracing::info!("Enabling CUDA execution provider for ONNX Runtime");
209 eps.push(ort::ep::CUDA::default().build());
210 }
211
212 #[cfg(feature = "ort-rocm")]
213 {
214 tracing::info!("Enabling ROCm execution provider for ONNX Runtime");
215 eps.push(ort::ep::ROCm::default().build());
216 }
217
218 #[cfg(feature = "ort-webgpu")]
219 {
220 tracing::info!("Enabling WebGPU execution provider for ONNX Runtime");
221 eps.push(ort::ep::WebGPU::default().build());
222 }
223 #[cfg(all(target_os = "windows", feature = "ort-directml"))]
224 {
225 tracing::info!("Enabling DirectML execution provider for ONNX Runtime");
226 eps.push(ort::ep::DirectML::default().build());
227 }
228
229 #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "ort-coreml"))]
230 {
231 tracing::info!("Enabling CoreML execution provider for ONNX Runtime");
232 eps.push(ort::ep::CoreML::default().build());
233 }
234
235 if compiled_gpu_count == 0 {
236 tracing::warn!(
237 "GPU execution provider requested, but this lean-ctx binary was built without ort-cuda/ort-rocm/etc.; using CPU only"
238 );
239 } else if eps.is_empty() {
240 tracing::debug!("No GPU execution providers configured — using CPU only");
241 }
242
243 eps.push(ort::ep::CPU::default().build());
244 eps
245}
246
247fn provider_policy() -> ProviderPolicy {
248 match std::env::var(PROVIDER_ENV) {
249 Ok(value) => provider_policy_from_value(&value),
250 Err(_) => ProviderPolicy::Auto,
251 }
252}
253
254fn provider_policy_name() -> &'static str {
255 match provider_policy() {
256 ProviderPolicy::Cpu => "cpu",
257 ProviderPolicy::Gpu => "gpu",
258 ProviderPolicy::Auto => "auto",
259 }
260}
261
262fn provider_policy_from_value(value: &str) -> ProviderPolicy {
263 match value.trim().to_lowercase().as_str() {
264 "gpu" | "cuda" | "rocm" | "webgpu" | "directml" | "coreml" => ProviderPolicy::Gpu,
265 "auto" => ProviderPolicy::Auto,
266 _ => ProviderPolicy::Cpu,
267 }
268}
269
270fn selected_runtime_looks_gpu() -> bool {
271 crate::core::ort_environment::resolved_ort_dylib_path()
272 .ok()
273 .as_deref()
274 .is_some_and(runtime_path_looks_gpu)
275}
276
277fn runtime_path_looks_gpu(path: &Path) -> bool {
278 let path_text = path.to_string_lossy().to_lowercase();
279 if path_text.contains("gpu") || path_text.contains("cuda") || path_text.contains("rocm") {
280 return true;
281 }
282 let Some(parent) = path.parent() else {
283 return false;
284 };
285 [
286 "libonnxruntime_providers_cuda.so",
287 "libonnxruntime_providers_rocm.so",
288 "onnxruntime_providers_cuda.dll",
289 "onnxruntime_providers_rocm.dll",
290 "libonnxruntime_providers_cuda.dylib",
291 "libonnxruntime_providers_rocm.dylib",
292 ]
293 .iter()
294 .any(|name| parent.join(name).exists())
295}
296
297fn compiled_gpu_provider_names() -> Vec<&'static str> {
298 let mut names = vec![
299 #[cfg(feature = "ort-cuda")]
300 "cuda",
301 #[cfg(feature = "ort-rocm")]
302 "rocm",
303 #[cfg(feature = "ort-webgpu")]
304 "webgpu",
305 #[cfg(all(target_os = "windows", feature = "ort-directml"))]
306 "directml",
307 #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "ort-coreml"))]
308 "coreml",
309 ];
310 let _ = &mut names; names
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn provider_policy_defaults_to_cpu_for_unknown_values() {
320 assert_eq!(provider_policy_from_value(""), ProviderPolicy::Cpu);
321 assert_eq!(provider_policy_from_value("bogus"), ProviderPolicy::Cpu);
322 assert_eq!(provider_policy_from_value("cpu"), ProviderPolicy::Cpu);
323 }
324
325 #[test]
326 fn provider_policy_accepts_gpu_and_auto_aliases() {
327 assert_eq!(provider_policy_from_value("gpu"), ProviderPolicy::Gpu);
328 assert_eq!(provider_policy_from_value("CUDA"), ProviderPolicy::Gpu);
329 assert_eq!(provider_policy_from_value("auto"), ProviderPolicy::Auto);
330 }
331}