Skip to main content

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)]
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    /// "rocm", "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 when this device came from [`devices`]. This is useful
77    /// for display only; pass the [`Device`] itself to [`ModelOptions`] for
78    /// exact selection. Registry indices are not stable across processes.
79    pub index: Option<usize>,
80    pub(crate) handle: sys::transcribe_device_t,
81}
82
83// Device handles are immutable process-lifetime registry entries. Backend
84// registration must finish before enumeration, so sharing a handle is safe.
85unsafe impl Send for Device {}
86unsafe impl Sync for Device {}
87
88impl PartialEq for Device {
89    fn eq(&self, other: &Self) -> bool {
90        self.handle == other.handle
91    }
92}
93
94impl Eq for Device {}
95
96impl Device {
97    /// Build a [`Device`] from the raw FFI struct filled by the library.
98    /// `index` is the registry index when the device came from enumeration,
99    /// or `None` when it came from a loaded model.
100    pub(crate) fn from_raw(
101        raw: &sys::transcribe_device_info,
102        handle: sys::transcribe_device_t,
103        index: Option<usize>,
104    ) -> Device {
105        Device {
106            name: owned_str(raw.name),
107            description: owned_str(raw.description),
108            kind: owned_str(raw.kind),
109            device_type: DeviceType::from_raw(raw.device_type),
110            device_id: owned_opt_str(raw.device_id),
111            memory_total: raw.memory_total,
112            memory_free: raw.memory_free,
113            index,
114            handle,
115        }
116    }
117}
118
119/// Load backend modules from `dir` (dynamic builds) and register their
120/// devices. A no-op in static builds. Call once, before the first model load.
121///
122/// Errors with [`crate::Error::Backend`] if, after loading, the process has no
123/// registered compute device (a dynamic build pointed at a directory with no
124/// usable modules). This call is idempotent per directory and NOT retryable in
125/// the same process.
126///
127/// This mutates the native process-global device registry. Complete every
128/// backend-init call before other threads enumerate devices, query backend
129/// availability, or load models; the native registry does not support racing
130/// registration against those operations.
131pub fn init_backends(dir: impl AsRef<Path>) -> Result<()> {
132    let dir = dir.as_ref();
133    // Pass the path bytes through faithfully (Unix) / reject non-UTF-8 (Windows),
134    // matching model loading — never lossily mangle a path with to_string_lossy.
135    let c_dir = CString::new(crate::model::path_bytes(dir)?)?;
136    let status = unsafe { sys::transcribe_init_backends(c_dir.as_ptr()) };
137    check(status, "init_backends")
138}
139
140/// Load the backend modules the way the build expects, with no caller-supplied
141/// path.
142///
143/// - **Compiled-in builds** (the default static build, or a plain `shared`
144///   build): a no-op returning `Ok(())` — the backends are already registered.
145/// - **`dynamic-backends` builds**: resolves the directory containing the
146///   loaded libtranscribe and scans only that directory. Ship the backend
147///   modules next to libtranscribe for this helper to find them.
148///
149/// If your app uses a different layout, call [`init_backends`] with that
150/// resolved module directory instead. Like [`init_backends`], this is
151/// idempotent and must run once before the first model load. It must also
152/// complete before other threads enumerate devices or query backend
153/// availability; see [`init_backends`] for the registry-ordering contract.
154pub fn init_backends_default() -> Result<()> {
155    let status = unsafe { sys::transcribe_init_backends_default() };
156    check(status, "init_backends_default")
157}
158
159/// The number of compute devices currently registered.
160///
161/// Do not race this query with [`init_backends`] or [`init_backends_default`].
162pub fn device_count() -> usize {
163    let n = unsafe { sys::transcribe_device_count() };
164    n.max(0) as usize
165}
166
167/// Every registered compute device.
168///
169/// Do not race enumeration with [`init_backends`] or
170/// [`init_backends_default`]. Finish backend registration before sharing
171/// devices across threads.
172pub fn devices() -> Vec<Device> {
173    let mut out = Vec::with_capacity(device_count());
174    for i in 0..device_count() as i32 {
175        let handle = unsafe { sys::transcribe_device_get(i) };
176        if handle.is_null() {
177            continue;
178        }
179        let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
180        unsafe { sys::transcribe_device_info_init(&mut raw) };
181        let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
182        if status == sys::transcribe_status::TRANSCRIBE_OK {
183            out.push(Device::from_raw(&raw, handle, Some(i as usize)));
184        }
185    }
186    out
187}
188
189/// Whether a backend request can be satisfied by some registered device. This
190/// is the probe to turn `Backend::Vulkan` on a machine without Vulkan into a
191/// clear error instead of a failed model load. Do not race this query with
192/// [`init_backends`] or [`init_backends_default`].
193pub fn backend_available(backend: Backend) -> bool {
194    unsafe { sys::transcribe_backend_available(backend.to_raw()) }
195}