llama_cpp_4/rpc/backend.rs
1//! RPC backend for distributed inference
2
3use crate::rpc::error::RpcError;
4use llama_cpp_sys_4 as sys;
5use std::ffi::CString;
6use std::ptr::NonNull;
7
8/// RPC backend for distributed inference across multiple machines
9pub struct RpcBackend {
10 backend: NonNull<sys::ggml_backend>,
11 endpoint: String,
12 device: u32,
13}
14
15impl RpcBackend {
16 /// Initialize a new RPC backend for the given endpoint and remote device.
17 ///
18 /// # Arguments
19 /// * `endpoint` - The RPC server endpoint (e.g., "127.0.0.1:50052")
20 /// * `device` - Index of the device to use on the remote server. A single
21 /// endpoint can expose several devices; pass `0` for the first.
22 ///
23 /// # Errors
24 ///
25 /// Returns [`RpcError::StringConversion`] if `endpoint` contains an interior
26 /// NUL, or [`RpcError::InitializationFailed`] if llama.cpp could not reach
27 /// the endpoint or the device index is out of range.
28 ///
29 /// # Example
30 /// ```no_run
31 /// use llama_cpp_4::rpc::RpcBackend;
32 ///
33 /// let backend = RpcBackend::init("127.0.0.1:50052", 0)?;
34 /// # Ok::<(), llama_cpp_4::rpc::RpcError>(())
35 /// ```
36 pub fn init(endpoint: &str, device: u32) -> Result<Self, RpcError> {
37 let c_endpoint = CString::new(endpoint)?;
38
39 // SAFETY: the arguments are either scalars or pointers that outlive the call, and
40 // the library owns whatever it returns.
41 let backend = unsafe { sys::ggml_backend_rpc_init(c_endpoint.as_ptr(), device) };
42
43 NonNull::new(backend)
44 .map(|ptr| Self {
45 backend: ptr,
46 endpoint: endpoint.to_string(),
47 device,
48 })
49 .ok_or_else(|| RpcError::InitializationFailed {
50 endpoint: endpoint.to_string(),
51 })
52 }
53
54 /// Check if a backend is an RPC backend
55 #[must_use]
56 pub fn is_rpc(&self) -> bool {
57 // SAFETY: the arguments are either scalars or pointers that outlive the call, and
58 // the library owns whatever it returns.
59 unsafe { sys::ggml_backend_is_rpc(self.backend.as_ptr()) }
60 }
61
62 /// Get the buffer type for this RPC backend
63 #[must_use]
64 pub fn buffer_type(&self) -> Option<NonNull<sys::ggml_backend_buffer_type>> {
65 let c_endpoint = CString::new(self.endpoint.as_str()).ok()?;
66 let buffer_type =
67 // SAFETY: the arguments are either scalars or pointers that outlive the call, and
68 // the library owns whatever it returns.
69 unsafe { sys::ggml_backend_rpc_buffer_type(c_endpoint.as_ptr(), self.device) };
70 NonNull::new(buffer_type)
71 }
72
73 /// Query the available memory on the remote device
74 ///
75 /// Returns (`free_memory`, `total_memory`) in bytes.
76 ///
77 /// # Errors
78 ///
79 /// Returns [`RpcError::MemoryQueryFailed`] when the server reports a total
80 /// of zero, which is how an unreachable endpoint surfaces here.
81 pub fn get_device_memory(&self) -> Result<(usize, usize), RpcError> {
82 let c_endpoint = CString::new(self.endpoint.as_str())?;
83
84 let mut free: usize = 0;
85 let mut total: usize = 0;
86
87 // SAFETY: the arguments are either scalars or pointers that outlive the call, and
88 // the library owns whatever it returns.
89 unsafe {
90 sys::ggml_backend_rpc_get_device_memory(
91 c_endpoint.as_ptr(),
92 self.device,
93 std::ptr::from_mut(&mut free),
94 std::ptr::from_mut(&mut total),
95 );
96 }
97
98 if total == 0 {
99 Err(RpcError::MemoryQueryFailed)
100 } else {
101 Ok((free, total))
102 }
103 }
104
105 /// Get the endpoint this backend is connected to
106 #[must_use]
107 pub fn endpoint(&self) -> &str {
108 &self.endpoint
109 }
110
111 /// Index of the remote device this backend is bound to.
112 #[must_use]
113 pub fn device(&self) -> u32 {
114 self.device
115 }
116
117 /// Get the raw backend pointer for FFI calls.
118 ///
119 /// The pointer is owned by this `RpcBackend` and is freed on drop; do not
120 /// free it, and do not use it after this value goes out of scope.
121 #[must_use]
122 pub fn as_ptr(&self) -> NonNull<sys::ggml_backend> {
123 self.backend
124 }
125}
126
127impl Drop for RpcBackend {
128 fn drop(&mut self) {
129 // SAFETY: this runs once from `Drop` on a non-null handle, and nothing reads it
130 // afterwards.
131 unsafe {
132 sys::ggml_backend_free(self.backend.as_ptr());
133 }
134 }
135}
136
137// Safety: RpcBackend can be sent between threads
138unsafe impl Send for RpcBackend {}
139// Safety: RpcBackend can be shared between threads (the C API is thread-safe)
140unsafe impl Sync for RpcBackend {}
141
142impl std::fmt::Debug for RpcBackend {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 f.debug_struct("RpcBackend")
145 .field("endpoint", &self.endpoint)
146 .field("device", &self.device)
147 .field("is_rpc", &self.is_rpc())
148 // `backend` is an opaque llama.cpp pointer with nothing useful to show.
149 .finish_non_exhaustive()
150 }
151}