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