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