dynamo_memory/nixl.rs
1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! NIXL registration wrapper for storage types.
5
6mod agent;
7mod config;
8
9use super::{MemoryDescriptor, StorageKind};
10use std::any::Any;
11use std::fmt;
12use std::sync::Arc;
13
14pub use agent::NixlAgent;
15pub use config::NixlBackendConfig;
16
17pub use nixl_sys::{
18 Agent, MemType, NotificationMap, OptArgs, RegistrationHandle, XferDescList, XferOp,
19 XferRequest, is_stub,
20};
21pub use serde::{Deserialize, Serialize};
22
23/// Trait for storage types that can be registered with NIXL.
24pub trait NixlCompatible {
25 /// Get parameters needed for NIXL registration.
26 ///
27 /// Returns (ptr, size, mem_type, device_id)
28 fn nixl_params(&self) -> (*const u8, usize, MemType, u64);
29}
30
31/// Combined trait for memory that can be registered with NIXL.
32///
33/// This supertrait enables type erasure via `Arc<dyn NixlMemory>`.
34/// Any type implementing both `MemoryDescriptor` and `NixlCompatible`
35/// automatically implements this trait via the blanket implementation.
36pub trait NixlMemory: MemoryDescriptor + NixlCompatible {}
37
38// Blanket impl - any type with both traits automatically implements NixlMemory
39impl<T: MemoryDescriptor + NixlCompatible + ?Sized> NixlMemory for T {}
40
41/// NIXL descriptor containing registration information.
42///
43/// This struct holds the information needed to describe a memory region
44/// to NIXL for transfer operations.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct NixlDescriptor {
47 /// Base address of the memory region.
48 pub addr: u64,
49 /// Size of the memory region in bytes.
50 pub size: usize,
51 /// Type of memory (host, device, etc.).
52 pub mem_type: MemType,
53 /// Device identifier (GPU index for device memory, 0 for host memory).
54 pub device_id: u64,
55}
56
57impl nixl_sys::MemoryRegion for NixlDescriptor {
58 unsafe fn as_ptr(&self) -> *const u8 {
59 self.addr as *const u8
60 }
61
62 fn size(&self) -> usize {
63 self.size
64 }
65}
66
67impl nixl_sys::NixlDescriptor for NixlDescriptor {
68 fn mem_type(&self) -> MemType {
69 self.mem_type
70 }
71
72 fn device_id(&self) -> u64 {
73 self.device_id
74 }
75}
76
77/// View trait for accessing registration information without unwrapping.
78pub trait RegisteredView {
79 /// Get the name of the NIXL agent that registered this memory.
80 fn agent_name(&self) -> &str;
81
82 /// Get the NIXL descriptor for this registered memory.
83 fn descriptor(&self) -> NixlDescriptor;
84}
85
86/// Wrapper for storage that has been registered with NIXL.
87///
88/// This wrapper ensures proper drop order: the registration handle is
89/// dropped before the storage, ensuring deregistration happens before
90/// the memory is freed.
91pub struct NixlRegistered<S: NixlCompatible> {
92 storage: S,
93 handle: Option<RegistrationHandle>,
94 agent_name: String,
95}
96
97impl<S: NixlCompatible> Drop for NixlRegistered<S> {
98 fn drop(&mut self) {
99 // Explicitly drop the registration handle first
100 drop(self.handle.take());
101 // Storage drops naturally after
102 }
103}
104
105impl<S: NixlCompatible + fmt::Debug> fmt::Debug for NixlRegistered<S> {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 f.debug_struct("NixlRegistered")
108 .field("storage", &self.storage)
109 .field("agent_name", &self.agent_name)
110 .field("handle", &self.handle.is_some())
111 .finish()
112 }
113}
114
115impl<S: MemoryDescriptor + NixlCompatible + 'static> MemoryDescriptor for NixlRegistered<S> {
116 fn addr(&self) -> usize {
117 self.storage.addr()
118 }
119
120 fn size(&self) -> usize {
121 self.storage.size()
122 }
123
124 fn storage_kind(&self) -> StorageKind {
125 self.storage.storage_kind()
126 }
127
128 fn as_any(&self) -> &dyn Any {
129 self
130 }
131
132 fn nixl_descriptor(&self) -> Option<NixlDescriptor> {
133 Some(self.descriptor())
134 }
135}
136
137impl<S: MemoryDescriptor + NixlCompatible> RegisteredView for NixlRegistered<S> {
138 fn agent_name(&self) -> &str {
139 &self.agent_name
140 }
141
142 fn descriptor(&self) -> NixlDescriptor {
143 let (ptr, size, mem_type, device_id) = self.storage.nixl_params();
144 NixlDescriptor {
145 addr: ptr as u64,
146 size,
147 mem_type,
148 device_id,
149 }
150 }
151}
152
153impl<S: MemoryDescriptor + NixlCompatible> NixlRegistered<S> {
154 /// Get a reference to the underlying storage.
155 pub fn storage(&self) -> &S {
156 &self.storage
157 }
158
159 /// Get a mutable reference to the underlying storage.
160 pub fn storage_mut(&mut self) -> &mut S {
161 &mut self.storage
162 }
163
164 /// Check if the registration handle is still valid.
165 pub fn is_registered(&self) -> bool {
166 self.handle.is_some()
167 }
168
169 /// Consume this wrapper and return the underlying storage.
170 ///
171 /// This will deregister the storage from NIXL.
172 pub fn into_storage(mut self) -> S {
173 drop(self.handle.take());
174 let mut this = std::mem::ManuallyDrop::new(self);
175 unsafe {
176 let storage = std::ptr::read(&this.storage);
177 std::ptr::drop_in_place(&mut this.agent_name);
178 storage
179 }
180 }
181}
182
183/// Register storage with a NIXL agent.
184///
185/// This consumes the storage and returns a `NixlRegistered` wrapper that
186/// manages the registration lifetime. The registration handle will be
187/// automatically dropped when the wrapper is dropped, ensuring proper
188/// cleanup order.
189///
190/// # Arguments
191/// * `storage` - The storage to register (consumed)
192/// * `agent` - The NIXL agent to register with
193/// * `opt` - Optional arguments for registration
194///
195/// # Returns
196/// A `NixlRegistered` wrapper containing the storage and registration handle.
197pub fn register_with_nixl<S>(
198 storage: S,
199 agent: &Agent,
200 opt: Option<&OptArgs>,
201) -> std::result::Result<NixlRegistered<S>, S>
202where
203 S: MemoryDescriptor + NixlCompatible,
204{
205 // let storage_kind = storage.storage_kind();
206
207 // // Determine if registration is needed based on storage type and available backends
208 // let should_register = match storage_kind {
209 // StorageKind::System | StorageKind::Pinned => {
210 // // System/Pinned memory needs UCX for remote transfers
211 // agent.has_backend("UCX") || agent.has_backend("POSIX")
212 // }
213 // StorageKind::Device(_) => {
214 // // Device memory needs UCX for remote transfers OR GDS for direct disk transfers
215 // agent.has_backend("UCX") || agent.has_backend("GDS_MT")
216 // }
217 // StorageKind::Disk(_) => {
218 // // Disk storage needs POSIX for regular I/O OR GDS for GPU direct I/O
219 // agent.has_backend("POSIX") || agent.has_backend("GDS_MT")
220 // } // StorageKind::Object(_) => {
221 // // // Object storage is always registered via NIXL's OBJ plugin
222 // // agent.has_backend("OBJ")
223 // // }
224 // };
225
226 // this is not true for our future object storage. so let's rethink this.
227 // for object, if there is no device_id or device_id is 0, then we need to register
228 // alternatively, the object storage holds it's own internal metadata but does not
229 // expose as a nixl descriptor, thus ObjectStorag will by default like all other storage
230 // types have a None for nixl_descriptor(), and we will use the internal
231 if storage.nixl_descriptor().is_some() {
232 return Ok(NixlRegistered {
233 storage,
234 handle: None,
235 agent_name: agent.name().to_string(),
236 });
237 }
238
239 // Get NIXL parameters
240 let (ptr, size, mem_type, device_id) = storage.nixl_params();
241
242 // Create a NIXL descriptor for registration
243 let descriptor = NixlDescriptor {
244 addr: ptr as u64,
245 size,
246 mem_type,
247 device_id,
248 };
249
250 match agent.register_memory(&descriptor, opt) {
251 Ok(handle) => Ok(NixlRegistered {
252 storage,
253 handle: Some(handle),
254 agent_name: agent.name().to_string(),
255 }),
256 Err(_) => Err(storage),
257 }
258}
259
260// =============================================================================
261// Arc<dyn NixlMemory> support
262// =============================================================================
263
264impl NixlCompatible for Arc<dyn NixlMemory + Send + Sync> {
265 fn nixl_params(&self) -> (*const u8, usize, MemType, u64) {
266 (**self).nixl_params()
267 }
268}
269
270impl MemoryDescriptor for Arc<dyn NixlMemory + Send + Sync> {
271 fn addr(&self) -> usize {
272 (**self).addr()
273 }
274
275 fn size(&self) -> usize {
276 (**self).size()
277 }
278
279 fn storage_kind(&self) -> StorageKind {
280 (**self).storage_kind()
281 }
282
283 fn as_any(&self) -> &dyn Any {
284 (**self).as_any()
285 }
286
287 fn nixl_descriptor(&self) -> Option<NixlDescriptor> {
288 (**self).nixl_descriptor()
289 }
290}
291
292// =============================================================================
293// Extension trait for ergonomic API
294// =============================================================================
295
296/// Extension trait providing ergonomic `.register()` method for NIXL registration.
297///
298/// This trait is automatically implemented for all types that implement both
299/// `MemoryDescriptor` and `NixlCompatible`. Import this trait to use the
300/// method syntax:
301///
302///
303pub trait NixlRegisterExt: MemoryDescriptor + NixlCompatible + Sized {
304 /// Get this memory as NIXL-registered.
305 ///
306 /// This operation is idempotent - it's a no-op if the memory is already registered.
307 ///
308 /// # Arguments
309 /// * `agent` - The NIXL agent to register with
310 /// * `opt` - Optional arguments for registration
311 ///
312 /// # Returns
313 /// A `NixlRegistered` wrapper on success, or the original storage on failure.
314 fn register(
315 self,
316 agent: &NixlAgent,
317 opt: Option<&OptArgs>,
318 ) -> std::result::Result<NixlRegistered<Self>, Self> {
319 register_with_nixl(self, agent, opt)
320 }
321}
322
323// Blanket impl for all compatible types
324impl<T: MemoryDescriptor + NixlCompatible + Sized> NixlRegisterExt for T {}