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