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}