transcribe_cpp/backend.rs
1//! Backend module loading and compute-device discovery.
2//!
3//! In a static (or plain `shared`) build the compiled-in backends are already
4//! registered and [`init_backends`] is a harmless no-op. In a `dynamic-backends`
5//! build the compute backends are loadable modules; a host points the library at
6//! the provider directory ONCE, before the first model load — use
7//! [`init_backends_default`] when the modules sit next to libtranscribe, or
8//! [`init_backends`] with an explicit bundled provider directory. See the C
9//! header's backend-module section for the degradation contract.
10
11use std::ffi::CString;
12use std::path::Path;
13
14use transcribe_cpp_sys as sys;
15
16use crate::error::{check, Result};
17use crate::result::{owned_opt_str, owned_str};
18use crate::types::Backend;
19
20/// ggml's vendor-agnostic class for a compute device, orthogonal to
21/// [`Device::kind`] (which carries the vendor). Backends report this
22/// classification themselves, so use it as a runtime hint rather than a
23/// portable hardware-memory taxonomy.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum DeviceType {
26 /// CPU using system memory.
27 Cpu,
28 /// Backend-reported GPU.
29 Gpu,
30 /// Backend-reported integrated GPU.
31 Igpu,
32 /// Host-memory accelerator (BLAS/AMX/...).
33 Accel,
34 /// A device-type value this binding does not recognize — reported by a
35 /// runtime newer than the header this binding was generated against.
36 /// Distinguish such devices by [`Device::device_id`] / [`Device::name`],
37 /// not by this axis.
38 Unknown,
39}
40
41impl DeviceType {
42 fn from_raw(raw: sys::transcribe_device_type) -> Self {
43 use sys::transcribe_device_type as T;
44 match raw {
45 T::TRANSCRIBE_DEVICE_TYPE_CPU => DeviceType::Cpu,
46 T::TRANSCRIBE_DEVICE_TYPE_GPU => DeviceType::Gpu,
47 T::TRANSCRIBE_DEVICE_TYPE_IGPU => DeviceType::Igpu,
48 T::TRANSCRIBE_DEVICE_TYPE_ACCEL => DeviceType::Accel,
49 _ => DeviceType::Unknown,
50 }
51 }
52}
53
54/// One registered compute device.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Device {
57 /// ggml device name, e.g. "Metal".
58 pub name: String,
59 /// Human-readable description, e.g. "Apple M4 Max".
60 pub description: String,
61 /// Classified vendor kind: "cpu", "accel", "metal", "vulkan", "cuda",
62 /// "sycl", "gpu", or "unknown".
63 pub kind: String,
64 /// The CPU/GPU/IGPU/ACCEL axis, orthogonal to [`Device::kind`].
65 pub device_type: DeviceType,
66 /// Stable hardware id when the backend reports one (PCI bus id for PCI
67 /// devices), or `None` (e.g. Metal).
68 pub device_id: Option<String>,
69 /// Reported device memory capacity in bytes, or 0 if unreported.
70 pub memory_total: u64,
71 /// Available device memory in bytes — a snapshot at the time this was
72 /// queried, or 0 if unreported. Re-query (via [`devices`] or
73 /// [`crate::Model::device`]) to refresh it; the value is backend-defined
74 /// and not comparable across device kinds.
75 pub memory_free: u64,
76 /// Registry index of this device — the value to pass as
77 /// [`ModelOptions::gpu_device`](crate::ModelOptions) to select it (0
78 /// means auto: discrete GPUs are probed before integrated). `None` when
79 /// this `Device` came from [`crate::Model::device`], since
80 /// `transcribe_model_get_device` does not expose an index; correlate such
81 /// a device back to [`devices`] by `device_id` / `name` instead.
82 /// Order-dependent and not stable across driver updates or hosts.
83 pub index: Option<usize>,
84}
85
86impl Device {
87 /// Build a [`Device`] from the raw FFI struct filled by the library.
88 /// `index` is the registry index when the device came from enumeration,
89 /// or `None` when it came from `transcribe_model_get_device`.
90 pub(crate) fn from_raw(raw: &sys::transcribe_backend_device, index: Option<usize>) -> Device {
91 Device {
92 name: owned_str(raw.name),
93 description: owned_str(raw.description),
94 kind: owned_str(raw.kind),
95 device_type: DeviceType::from_raw(raw.device_type),
96 device_id: owned_opt_str(raw.device_id),
97 memory_total: raw.memory_total,
98 memory_free: raw.memory_free,
99 index,
100 }
101 }
102}
103
104/// Load backend modules from `dir` (dynamic builds) and register their
105/// devices. A no-op in static builds. Call once, before the first model load.
106///
107/// Errors with [`crate::Error::Backend`] if, after loading, the process has no
108/// registered compute device (a dynamic build pointed at a directory with no
109/// usable modules). This call is idempotent per directory and NOT retryable in
110/// the same process.
111pub fn init_backends(dir: impl AsRef<Path>) -> Result<()> {
112 let dir = dir.as_ref();
113 // Pass the path bytes through faithfully (Unix) / reject non-UTF-8 (Windows),
114 // matching model loading — never lossily mangle a path with to_string_lossy.
115 let c_dir = CString::new(crate::model::path_bytes(dir)?)?;
116 let status = unsafe { sys::transcribe_init_backends(c_dir.as_ptr()) };
117 check(status, "init_backends")
118}
119
120/// Load the backend modules the way the build expects, with no caller-supplied
121/// path.
122///
123/// - **Compiled-in builds** (the default static build, or a plain `shared`
124/// build): a no-op returning `Ok(())` — the backends are already registered.
125/// - **`dynamic-backends` builds**: resolves the directory containing the
126/// loaded libtranscribe and scans only that directory. Ship the backend
127/// modules next to libtranscribe for this helper to find them.
128///
129/// If your app uses a different layout, call [`init_backends`] with that
130/// resolved module directory instead. Like [`init_backends`], this is
131/// idempotent and must run once before the first model load.
132pub fn init_backends_default() -> Result<()> {
133 let status = unsafe { sys::transcribe_init_backends_default() };
134 check(status, "init_backends_default")
135}
136
137/// The number of compute devices currently registered.
138pub fn device_count() -> usize {
139 let n = unsafe { sys::transcribe_backend_device_count() };
140 n.max(0) as usize
141}
142
143/// Every registered compute device.
144pub fn devices() -> Vec<Device> {
145 let mut out = Vec::with_capacity(device_count());
146 for i in 0..device_count() as i32 {
147 let mut raw: sys::transcribe_backend_device = unsafe { std::mem::zeroed() };
148 unsafe { sys::transcribe_backend_device_init(&mut raw) };
149 let status = unsafe { sys::transcribe_get_backend_device(i, &mut raw) };
150 if status == sys::transcribe_status::TRANSCRIBE_OK {
151 out.push(Device::from_raw(&raw, Some(i as usize)));
152 }
153 }
154 out
155}
156
157/// Whether a backend request can be satisfied by some registered device. This
158/// is the probe to turn `Backend::Vulkan` on a machine without Vulkan into a
159/// clear error instead of a failed model load.
160pub fn backend_available(backend: Backend) -> bool {
161 unsafe { sys::transcribe_backend_available(backend.to_raw()) }
162}