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)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub enum DeviceType {
27 /// CPU using system memory.
28 Cpu,
29 /// Backend-reported GPU.
30 Gpu,
31 /// Backend-reported integrated GPU.
32 Igpu,
33 /// Host-memory accelerator (BLAS/AMX/...).
34 Accel,
35 /// A device-type value this binding does not recognize — reported by a
36 /// runtime newer than the header this binding was generated against.
37 /// Distinguish such devices by [`Device::device_id`] / [`Device::name`],
38 /// not by this axis.
39 Unknown,
40}
41
42impl DeviceType {
43 fn from_raw(raw: sys::transcribe_device_type) -> Self {
44 use sys::transcribe_device_type as T;
45 match raw {
46 T::TRANSCRIBE_DEVICE_TYPE_CPU => DeviceType::Cpu,
47 T::TRANSCRIBE_DEVICE_TYPE_GPU => DeviceType::Gpu,
48 T::TRANSCRIBE_DEVICE_TYPE_IGPU => DeviceType::Igpu,
49 T::TRANSCRIBE_DEVICE_TYPE_ACCEL => DeviceType::Accel,
50 _ => DeviceType::Unknown,
51 }
52 }
53}
54
55/// One registered compute device.
56#[derive(Debug, Clone)]
57pub struct Device {
58 /// ggml device name, e.g. "Metal".
59 pub name: String,
60 /// Human-readable description, e.g. "Apple M4 Max".
61 pub description: String,
62 /// Classified vendor kind: "cpu", "accel", "metal", "vulkan", "cuda",
63 /// "rocm", "sycl", "gpu", or "unknown".
64 pub kind: String,
65 /// The CPU/GPU/IGPU/ACCEL axis, orthogonal to [`Device::kind`].
66 pub device_type: DeviceType,
67 /// Stable hardware id when the backend reports one (PCI bus id for PCI
68 /// devices), or `None` (e.g. Metal).
69 pub device_id: Option<String>,
70 /// Reported device memory capacity in bytes, or 0 if unreported.
71 pub memory_total: u64,
72 /// Available device memory in bytes — a snapshot at the time this was
73 /// queried, or 0 if unreported. Re-query (via [`devices`] or
74 /// [`crate::Model::device`]) to refresh it; the value is backend-defined
75 /// and not comparable across device kinds.
76 pub memory_free: u64,
77 /// Registry index when this device came from [`devices`]. This is useful
78 /// for display only; pass the [`Device`] itself to [`ModelOptions`] for
79 /// exact selection. Registry indices are not stable across processes.
80 pub index: Option<usize>,
81 pub(crate) handle: sys::transcribe_device_t,
82}
83
84// Device handles are immutable process-lifetime registry entries. Backend
85// registration must finish before enumeration, so sharing a handle is safe.
86unsafe impl Send for Device {}
87unsafe impl Sync for Device {}
88
89impl PartialEq for Device {
90 fn eq(&self, other: &Self) -> bool {
91 self.handle == other.handle
92 }
93}
94
95impl Eq for Device {}
96
97impl Device {
98 /// Build a [`Device`] from the raw FFI struct filled by the library.
99 /// `index` is the registry index when the device came from enumeration,
100 /// or `None` when it came from a loaded model.
101 pub(crate) fn from_raw(
102 raw: &sys::transcribe_device_info,
103 handle: sys::transcribe_device_t,
104 index: Option<usize>,
105 ) -> Device {
106 Device {
107 name: owned_str(raw.name),
108 description: owned_str(raw.description),
109 kind: owned_str(raw.kind),
110 device_type: DeviceType::from_raw(raw.device_type),
111 device_id: owned_opt_str(raw.device_id),
112 memory_total: raw.memory_total,
113 memory_free: raw.memory_free,
114 index,
115 handle,
116 }
117 }
118}
119
120/// Load backend modules from `dir` (dynamic builds) and register their
121/// devices. A no-op in static builds. Call once, before the first model load.
122///
123/// Errors with [`crate::Error::Backend`] if, after loading, the process has no
124/// registered compute device (a dynamic build pointed at a directory with no
125/// usable modules). This call is idempotent per directory and NOT retryable in
126/// the same process.
127///
128/// This mutates the native process-global device registry. Complete every
129/// backend-init call before other threads enumerate devices, query backend
130/// availability, or load models; the native registry does not support racing
131/// registration against those operations.
132pub fn init_backends(dir: impl AsRef<Path>) -> Result<()> {
133 let dir = dir.as_ref();
134 // Pass the path bytes through faithfully (Unix) / reject non-UTF-8 (Windows),
135 // matching model loading — never lossily mangle a path with to_string_lossy.
136 let c_dir = CString::new(crate::model::path_bytes(dir)?)?;
137 let status = unsafe { sys::transcribe_init_backends(c_dir.as_ptr()) };
138 check(status, "init_backends")
139}
140
141/// Load the backend modules the way the build expects, with no caller-supplied
142/// path.
143///
144/// - **Compiled-in builds** (the default static build, or a plain `shared`
145/// build): a no-op returning `Ok(())` — the backends are already registered.
146/// - **`dynamic-backends` builds**: resolves the directory containing the
147/// loaded libtranscribe and scans only that directory. Ship the backend
148/// modules next to libtranscribe for this helper to find them.
149///
150/// If your app uses a different layout, call [`init_backends`] with that
151/// resolved module directory instead. Like [`init_backends`], this is
152/// idempotent and must run once before the first model load. It must also
153/// complete before other threads enumerate devices or query backend
154/// availability; see [`init_backends`] for the registry-ordering contract.
155pub fn init_backends_default() -> Result<()> {
156 let status = unsafe { sys::transcribe_init_backends_default() };
157 check(status, "init_backends_default")
158}
159
160/// Backend kinds allowed to register in this process. A backend outside the
161/// mask never runs any code, so a broken GPU driver can be kept out of a
162/// worker entirely. CPU is always allowed; `TRANSCRIBE_BACKENDS` can only
163/// narrow the mask.
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub struct BackendMask(u32);
167
168impl BackendMask {
169 /// CPU plus host-memory accelerators (BLAS). Always implied.
170 pub const CPU: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_CPU);
171 /// Apple Metal.
172 pub const METAL: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_METAL);
173 /// Vulkan.
174 pub const VULKAN: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_VULKAN);
175 /// NVIDIA CUDA.
176 pub const CUDA: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_CUDA);
177 /// AMD ROCm / HIP.
178 pub const ROCM: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_ROCM);
179 /// Every backend without a dedicated bit (SYCL, OpenCL, RPC, ...).
180 pub const OTHER: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_OTHER);
181 /// Everything. The default.
182 pub const ALL: BackendMask = BackendMask(sys::TRANSCRIBE_BACKEND_MASK_ALL);
183
184 /// The smallest mask that can serve a model-load `backend` request
185 /// ([`Backend::Auto`] needs everything).
186 pub const fn for_backend(backend: Backend) -> BackendMask {
187 match backend {
188 Backend::Auto => BackendMask::ALL,
189 Backend::Cpu | Backend::CpuAccel => BackendMask::CPU,
190 Backend::Metal => BackendMask::METAL.union(BackendMask::CPU),
191 Backend::Vulkan => BackendMask::VULKAN.union(BackendMask::CPU),
192 Backend::Cuda => BackendMask::CUDA.union(BackendMask::CPU),
193 Backend::Rocm => BackendMask::ROCM.union(BackendMask::CPU),
194 }
195 }
196
197 /// The raw `TRANSCRIBE_BACKEND_MASK_*` bits.
198 pub const fn bits(self) -> u32 {
199 self.0
200 }
201
202 /// A mask from raw `TRANSCRIBE_BACKEND_MASK_*` bits.
203 pub const fn from_bits(bits: u32) -> BackendMask {
204 BackendMask(bits)
205 }
206
207 /// Both masks' backends.
208 pub const fn union(self, other: BackendMask) -> BackendMask {
209 BackendMask(self.0 | other.0)
210 }
211
212 /// Whether every backend in `other` is in `self`.
213 pub const fn contains(self, other: BackendMask) -> bool {
214 self.0 & other.0 == other.0
215 }
216}
217
218impl Default for BackendMask {
219 fn default() -> Self {
220 BackendMask::ALL
221 }
222}
223
224impl std::ops::BitOr for BackendMask {
225 type Output = BackendMask;
226 fn bitor(self, rhs: BackendMask) -> BackendMask {
227 self.union(rhs)
228 }
229}
230
231impl std::ops::BitOrAssign for BackendMask {
232 fn bitor_assign(&mut self, rhs: BackendMask) {
233 *self = self.union(rhs);
234 }
235}
236
237/// [`init_backends`] / [`init_backends_default`] (`dir` = `None`) with an
238/// allowed-backend mask. The mask is fixed at first backend registration;
239/// call this first, once per process. A later call with a different
240/// effective mask returns [`crate::Error::Backend`].
241///
242/// ```no_run
243/// use transcribe_cpp::{init_backends_with, Backend, BackendMask};
244/// // A CPU fallback worker: never let the GPU driver load.
245/// init_backends_with(None::<&std::path::Path>, BackendMask::for_backend(Backend::Cpu))?;
246/// # Ok::<(), transcribe_cpp::Error>(())
247/// ```
248pub fn init_backends_with(dir: Option<impl AsRef<Path>>, allowed: BackendMask) -> Result<()> {
249 let c_dir = match dir {
250 Some(dir) => Some(CString::new(crate::model::path_bytes(dir.as_ref())?)?),
251 None => None,
252 };
253 let mut params: sys::transcribe_backend_init_params = unsafe { std::mem::zeroed() };
254 unsafe { sys::transcribe_backend_init_params_init(&mut params) };
255 params.artifact_dir = c_dir.as_ref().map_or(std::ptr::null(), |d| d.as_ptr());
256 params.allowed_backends = allowed.bits();
257 let status = unsafe { sys::transcribe_init_backends_ex(¶ms) };
258 check(status, "init_backends_with")
259}
260
261/// The effective mask: [`init_backends_with`]'s (ALL until then) narrowed
262/// by `TRANSCRIBE_BACKENDS`.
263pub fn allowed_backends() -> BackendMask {
264 BackendMask(unsafe { sys::transcribe_allowed_backends() })
265}
266
267/// The number of compute devices currently registered.
268///
269/// Do not race this query with [`init_backends`] or [`init_backends_default`].
270pub fn device_count() -> usize {
271 let n = unsafe { sys::transcribe_device_count() };
272 n.max(0) as usize
273}
274
275/// Every registered compute device.
276///
277/// Do not race enumeration with [`init_backends`] or
278/// [`init_backends_default`]. Finish backend registration before sharing
279/// devices across threads.
280pub fn devices() -> Vec<Device> {
281 let mut out = Vec::with_capacity(device_count());
282 for i in 0..device_count() as i32 {
283 let handle = unsafe { sys::transcribe_device_get(i) };
284 if handle.is_null() {
285 continue;
286 }
287 let mut raw: sys::transcribe_device_info = unsafe { std::mem::zeroed() };
288 unsafe { sys::transcribe_device_info_init(&mut raw) };
289 let status = unsafe { sys::transcribe_device_get_info(handle, &mut raw) };
290 if status == sys::transcribe_status::TRANSCRIBE_OK {
291 out.push(Device::from_raw(&raw, handle, Some(i as usize)));
292 }
293 }
294 out
295}
296
297/// Whether a backend request can be satisfied by some registered device. This
298/// is the probe to turn `Backend::Vulkan` on a machine without Vulkan into a
299/// clear error instead of a failed model load. Do not race this query with
300/// [`init_backends`] or [`init_backends_default`].
301pub fn backend_available(backend: Backend) -> bool {
302 unsafe { sys::transcribe_backend_available(backend.to_raw()) }
303}