Skip to main content

dynamo_memory/
disk.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Disk-backed memory storage using memory-mapped files.
5
6use super::{MemoryDescriptor, Result, StorageError, StorageKind, nixl::NixlDescriptor};
7use std::any::Any;
8use std::path::{Path, PathBuf};
9
10use core::ffi::c_char;
11use nix::fcntl::{FallocateFlags, fallocate};
12use nix::unistd::unlink;
13use std::ffi::CString;
14use std::os::fd::BorrowedFd;
15
16const DISK_CACHE_KEY: &str = "DYN_KVBM_DISK_CACHE_DIR";
17const DEFAULT_DISK_CACHE_DIR: &str = "/tmp/";
18
19/// Disk-backed storage using memory-mapped files with O_DIRECT support.
20#[derive(Debug)]
21pub struct DiskStorage {
22    /// File descriptor for the backing file.
23    fd: u64,
24    /// Path to the backing file.
25    path: PathBuf,
26    /// Size of the storage in bytes.
27    size: usize,
28    /// Whether the file has been unlinked from the filesystem.
29    unlinked: bool,
30}
31
32impl DiskStorage {
33    /// Creates a new disk storage of the given size in the default cache directory.
34    pub fn new(size: usize) -> Result<Self> {
35        // We need to open our file with some special flags that aren't supported by the tempfile crate.
36        // Instead, we'll use the mkostemp function to create a temporary file with the correct flags.
37
38        let specified_dir =
39            std::env::var(DISK_CACHE_KEY).unwrap_or_else(|_| DEFAULT_DISK_CACHE_DIR.to_string());
40        let file_path = Path::new(&specified_dir).join("dynamo-kvbm-disk-cache-XXXXXX");
41
42        Self::new_at(file_path, size)
43    }
44
45    /// Creates a new disk storage at the specified path with the given size.
46    pub fn new_at(path: impl AsRef<Path>, len: usize) -> Result<Self> {
47        if len == 0 {
48            return Err(StorageError::AllocationFailed(
49                "zero-sized allocations are not supported".into(),
50            ));
51        }
52
53        let file_path = path.as_ref().to_path_buf();
54
55        if !file_path.exists() {
56            let parent = file_path.parent().ok_or_else(|| {
57                StorageError::AllocationFailed(format!(
58                    "disk cache path {} has no parent directory",
59                    file_path.display()
60                ))
61            })?;
62            std::fs::create_dir_all(parent).map_err(|e| {
63                StorageError::AllocationFailed(format!(
64                    "failed to create disk cache directory {}: {e}",
65                    parent.display()
66                ))
67            })?;
68        }
69
70        tracing::debug!("Allocating disk cache file at {}", file_path.display());
71
72        let path_str = file_path.to_str().ok_or_else(|| {
73            StorageError::AllocationFailed(format!(
74                "disk cache path {} is not valid UTF-8",
75                file_path.display()
76            ))
77        })?;
78        let is_template = path_str.contains("XXXXXX");
79
80        let (raw_fd, actual_path) = if is_template {
81            // Template path - use mkostemp to generate unique filename
82            let template = CString::new(path_str).unwrap();
83            let mut template_bytes = template.into_bytes_with_nul();
84
85            let fd = unsafe {
86                nix::libc::mkostemp(
87                    template_bytes.as_mut_ptr() as *mut c_char,
88                    nix::libc::O_RDWR | nix::libc::O_DIRECT,
89                )
90            };
91
92            if fd == -1 {
93                return Err(StorageError::AllocationFailed(format!(
94                    "mkostemp failed: {}",
95                    std::io::Error::last_os_error()
96                )));
97            }
98
99            // Extract the actual path created by mkostemp
100            let actual = PathBuf::from(
101                CString::from_vec_with_nul(template_bytes)
102                    .unwrap()
103                    .to_str()
104                    .unwrap(),
105            );
106
107            (fd, actual)
108        } else {
109            // Specific path - use open with O_CREAT
110            let path_cstr = CString::new(path_str).unwrap();
111            let fd = unsafe {
112                nix::libc::open(
113                    path_cstr.as_ptr(),
114                    nix::libc::O_CREAT | nix::libc::O_RDWR | nix::libc::O_DIRECT,
115                    0o644,
116                )
117            };
118
119            if fd == -1 {
120                return Err(StorageError::AllocationFailed(format!(
121                    "open failed: {}",
122                    std::io::Error::last_os_error()
123                )));
124            }
125
126            (fd, file_path)
127        };
128
129        // We need to use fallocate to actually allocate the storage and create the blocks on disk.
130        unsafe {
131            fallocate(
132                BorrowedFd::borrow_raw(raw_fd),
133                FallocateFlags::empty(),
134                0,
135                len as i64,
136            )
137            .map_err(|e| {
138                StorageError::AllocationFailed(format!("Failed to allocate temp file: {}", e))
139            })?
140        };
141
142        Ok(Self {
143            fd: raw_fd as u64,
144            path: actual_path,
145            size: len,
146            unlinked: false,
147        })
148    }
149
150    /// Returns the file descriptor of the backing file.
151    pub fn fd(&self) -> u64 {
152        self.fd
153    }
154
155    /// Returns the path to the backing file.
156    pub fn path(&self) -> &Path {
157        self.path.as_path()
158    }
159
160    /// Unlinks the backing file from the filesystem.
161    /// This means that when this process terminates, the file will be automatically deleted by the OS.
162    /// Unfortunately, GDS requires that files we try to register must be linked.
163    /// To get around this, we unlink the file only after we've registered it with NIXL.
164    pub fn unlink(&mut self) -> Result<()> {
165        if self.unlinked {
166            return Ok(());
167        }
168
169        unlink(self.path.as_path())
170            .map_err(|e| StorageError::AllocationFailed(format!("Failed to unlink file: {}", e)))?;
171        self.unlinked = true;
172        Ok(())
173    }
174
175    /// Returns whether the backing file has been unlinked from the filesystem.
176    pub fn unlinked(&self) -> bool {
177        self.unlinked
178    }
179}
180
181impl Drop for DiskStorage {
182    fn drop(&mut self) {
183        let _ = self.unlink();
184        if let Err(e) = nix::unistd::close(self.fd as std::os::fd::RawFd) {
185            tracing::debug!("failed to close disk cache fd {}: {e}", self.fd);
186        }
187    }
188}
189
190impl MemoryDescriptor for DiskStorage {
191    fn addr(&self) -> usize {
192        0
193    }
194
195    fn size(&self) -> usize {
196        self.size
197    }
198
199    fn storage_kind(&self) -> StorageKind {
200        StorageKind::Disk(self.fd)
201    }
202
203    fn as_any(&self) -> &dyn Any {
204        self
205    }
206    fn nixl_descriptor(&self) -> Option<NixlDescriptor> {
207        None
208    }
209}
210
211// Support for NIXL registration
212impl super::nixl::NixlCompatible for DiskStorage {
213    fn nixl_params(&self) -> (*const u8, usize, nixl_sys::MemType, u64) {
214        #[cfg(unix)]
215        {
216            // Use file descriptor as device_id for MemType::File
217            (
218                std::ptr::null(),
219                self.size,
220                nixl_sys::MemType::File,
221                self.fd,
222            )
223        }
224
225        #[cfg(not(unix))]
226        {
227            // On non-Unix systems, we can't get the file descriptor easily
228            // Return device_id as 0 - registration will fail on these systems
229            (
230                self.mmap.as_ptr(),
231                self.mmap.len(),
232                nixl_sys::MemType::File,
233                0,
234            )
235        }
236    }
237}
238
239// mod mmap {
240//     use super::*;
241
242//     #[cfg(unix)]
243//     use std::os::unix::io::AsRawFd;
244
245//     use memmap2::{MmapMut, MmapOptions};
246//     use std::fs::{File, OpenOptions};
247//     use tempfile::NamedTempFile;
248
249//     /// Disk-backed storage using memory-mapped files.
250//     #[derive(Debug)]
251//     pub struct MemMappedFileStorage {
252//         _file: File, // Keep file alive for the lifetime of the mmap
253//         mmap: MmapMut,
254//         path: PathBuf,
255//         #[cfg(unix)]
256//         fd: i32,
257//     }
258
259//     unsafe impl Send for MemMappedFileStorage {}
260//     unsafe impl Sync for MemMappedFileStorage {}
261
262//     impl MemMappedFileStorage {
263//         /// Create new disk storage with a temporary file.
264//         pub fn new_temp(len: usize) -> Result<Self> {
265//             if len == 0 {
266//                 return Err(StorageError::AllocationFailed(
267//                     "zero-sized allocations are not supported".into(),
268//                 ));
269//             }
270
271//             // Create temporary file
272//             let temp_file = NamedTempFile::new()?;
273//             let path = temp_file.path().to_path_buf();
274//             let file = temp_file.into_file();
275
276//             // Set file size
277//             file.set_len(len as u64)?;
278
279//             #[cfg(unix)]
280//             let fd = file.as_raw_fd();
281
282//             // Memory map the file
283//             let mmap = unsafe { MmapOptions::new().len(len).map_mut(&file)? };
284
285//             Ok(Self {
286//                 _file: file,
287//                 mmap,
288//                 path,
289//                 #[cfg(unix)]
290//                 fd,
291//             })
292//         }
293
294//         /// Create new disk storage with a specific file path.
295//         pub fn new_at(path: impl AsRef<Path>, len: usize) -> Result<Self> {
296//             if len == 0 {
297//                 return Err(StorageError::AllocationFailed(
298//                     "zero-sized allocations are not supported".into(),
299//                 ));
300//             }
301
302//             let path = path.as_ref().to_path_buf();
303
304//             // Create or open file
305//             let file = OpenOptions::new()
306//                 .read(true)
307//                 .write(true)
308//                 .create(true)
309//                 .open(&path)?;
310
311//             // Set file size
312//             file.set_len(len as u64)?;
313
314//             #[cfg(unix)]
315//             let fd = file.as_raw_fd();
316
317//             // Memory map the file
318//             let mmap = unsafe { MmapOptions::new().len(len).map_mut(&file)? };
319
320//             Ok(Self {
321//                 _file: file,
322//                 mmap,
323//                 path,
324//                 #[cfg(unix)]
325//                 fd,
326//             })
327//         }
328
329//         /// Get the path to the backing file.
330//         pub fn path(&self) -> &Path {
331//             &self.path
332//         }
333
334//         /// Get the file descriptor (Unix only).
335//         #[cfg(unix)]
336//         pub fn fd(&self) -> i32 {
337//             self.fd
338//         }
339
340//         /// Get a pointer to the memory-mapped region.
341//         ///
342//         /// # Safety
343//         /// The caller must ensure the pointer is not used after this storage is dropped.
344//         pub unsafe fn as_ptr(&self) -> *const u8 {
345//             self.mmap.as_ptr()
346//         }
347
348//         /// Get a mutable pointer to the memory-mapped region.
349//         ///
350//         /// # Safety
351//         /// The caller must ensure the pointer is not used after this storage is dropped
352//         /// and that there are no other references to this memory.
353//         pub unsafe fn as_mut_ptr(&mut self) -> *mut u8 {
354//             self.mmap.as_mut_ptr()
355//         }
356//     }
357
358//     impl MemoryDescriptor for MemMappedFileStorage {
359//         fn addr(&self) -> usize {
360//             self.mmap.as_ptr() as usize
361//         }
362
363//         fn size(&self) -> usize {
364//             self.mmap.len()
365//         }
366
367//         fn storage_kind(&self) -> StorageKind {
368//             StorageKind::Disk
369//         }
370
371//         fn as_any(&self) -> &dyn Any {
372//             self
373//         }
374//     }
375
376//     // Support for NIXL registration
377//     impl super::super::registered::NixlCompatible for MemMappedFileStorage {
378//         fn nixl_params(&self) -> (*const u8, usize, nixl_sys::MemType, u64) {
379//             #[cfg(unix)]
380//             {
381//                 // Use file descriptor as device_id for MemType::File
382//                 (
383//                     self.mmap.as_ptr(),
384//                     self.mmap.len(),
385//                     nixl_sys::MemType::File,
386//                     self.fd as u64,
387//                 )
388//             }
389
390//             #[cfg(not(unix))]
391//             {
392//                 // On non-Unix systems, we can't get the file descriptor easily
393//                 // Return device_id as 0 - registration will fail on these systems
394//                 (
395//                     self.mmap.as_ptr(),
396//                     self.mmap.len(),
397//                     nixl_sys::MemType::File,
398//                     0,
399//                 )
400//             }
401//         }
402//     }
403// }