Skip to main content

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 {}