diskann_disk/search/provider/aligned_file_reader/reader/
linux.rs1use std::{
6 fs::OpenOptions,
7 os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
8};
9
10use diskann::{ANNError, ANNResult};
11use io_uring::IoUring;
12use libc;
13
14use crate::search::provider::aligned_file_reader::{
15 platform::IOContext, traits::AlignedFileReader, AlignedRead, A512,
16};
17
18pub const MAX_IO_CONCURRENCY: usize = 128;
19
20pub struct LinuxAlignedFileReader {
21 io_context: IOContext,
22}
23
24impl LinuxAlignedFileReader {
33 pub fn new(fname: &str) -> ANNResult<Self> {
34 let open_result = OpenOptions::new()
38 .read(true)
39 .custom_flags(libc::O_DIRECT)
40 .open(fname);
41
42 let file = match open_result {
43 Ok(file_handle) => file_handle,
44 Err(err) => {
45 return Err(ANNError::log_io_error(err));
46 }
47 };
48
49 let ring = IoUring::new(MAX_IO_CONCURRENCY as u32)?;
50 let fd = file.as_raw_fd();
51 ring.submitter().register_files(std::slice::from_ref(&fd))?;
52 let io_context = IOContext::new(file, ring);
53
54 Ok(LinuxAlignedFileReader { io_context })
55 }
56
57 fn submit_aligned_read(
58 aligned_read: &mut AlignedRead<u8, A512>,
59 ring: &mut IoUring,
60 identifier: u64,
61 ) -> Result<(), ANNError> {
62 let fixed_buffer = libc::iovec {
63 iov_base: aligned_read.aligned_buf_mut().as_mut_ptr() as _,
64 iov_len: aligned_read.aligned_buf_mut().len() as _,
65 };
66
67 let read = io_uring::opcode::Read::new(
68 io_uring::types::Fixed(0),
70 fixed_buffer.iov_base.cast::<u8>(),
71 fixed_buffer.iov_len as _,
72 )
73 .offset(aligned_read.offset())
74 .build()
75 .user_data(identifier);
76
77 unsafe {
81 ring.submission()
82 .push(&read)
83 .map_err(ANNError::log_push_error)?
84 };
85 Ok(())
86 }
87}
88
89impl AlignedFileReader for LinuxAlignedFileReader {
90 type Alignment = A512;
93
94 fn read(&mut self, read_requests: &mut [AlignedRead<u8, A512>]) -> ANNResult<()> {
96 let n_requests = read_requests.len();
97 let n_batches = n_requests.div_ceil(MAX_IO_CONCURRENCY);
98
99 let ring = &mut self.io_context.ring;
100
101 for batch_idx in 0..n_batches {
102 let batch_start = MAX_IO_CONCURRENCY * batch_idx;
104 let batch_size = std::cmp::min(n_requests - batch_start, MAX_IO_CONCURRENCY);
105
106 for j in 0..batch_size {
107 let read_id = j + batch_start;
108 let aligned_read = &mut read_requests[read_id];
109 Self::submit_aligned_read(aligned_read, ring, read_id as u64)?;
110 }
111
112 ring.submit_and_wait(batch_size)?;
114
115 for cqe in ring.completion() {
119 if cqe.result() < 0 {
120 return Err(ANNError::log_io_error(std::io::Error::from_raw_os_error(
121 cqe.result(),
122 )));
123 }
124 }
125 }
126
127 Ok(())
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use std::{
134 cmp::max,
135 fs::File,
136 io::{BufReader, Read, Seek, SeekFrom},
137 };
138
139 use bincode::deserialize_from;
140 use serde::{Deserialize, Serialize};
141
142 use super::*;
143 use diskann_quantization::alloc::{AlignedAllocator, Poly};
144 pub const TEST_INDEX_PATH: &str =
145 "../test_data/disk_index_misc/disk_index_siftsmall_learn_256pts_R4_L50_A1.2_aligned_reader_test.index";
146 pub const TRUTH_NODE_DATA_PATH: &str =
147 "../test_data/disk_index_misc/disk_index_node_data_aligned_reader_truth.bin";
148 const DEFAULT_DISK_SECTOR_LEN: usize = 4096;
149
150 #[derive(Debug, Serialize, Deserialize)]
151 struct NodeData {
152 num_neighbors: u32,
153 coordinates: Vec<f32>,
154 neighbors: Vec<u32>,
155 }
156
157 impl PartialEq for NodeData {
158 fn eq(&self, other: &Self) -> bool {
159 self.num_neighbors == other.num_neighbors
160 && self.coordinates == other.coordinates
161 && self.neighbors == other.neighbors
162 }
163 }
164
165 #[test]
166 fn test_new_aligned_file_reader() {
167 let result = LinuxAlignedFileReader::new(TEST_INDEX_PATH);
169 assert!(result.is_ok());
170 }
171
172 #[test]
173 fn test_read() {
174 let mut reader = LinuxAlignedFileReader::new(TEST_INDEX_PATH).unwrap();
175
176 let read_length = 512; let num_read = 10;
178 let mut aligned_mem =
179 Poly::broadcast(0u8, read_length * num_read, AlignedAllocator::A512).unwrap();
180
181 let mut mem_slices: Vec<&mut [u8]> = aligned_mem.chunks_mut(read_length).collect();
183
184 let mut aligned_reads: Vec<AlignedRead<'_, u8, A512>> = mem_slices
185 .iter_mut()
186 .enumerate()
187 .map(|(i, slice)| {
188 let offset = (i * read_length) as u64;
189 AlignedRead::new(offset, slice).unwrap()
190 })
191 .collect();
192
193 let result = reader.read(&mut aligned_reads);
194 assert!(result.is_ok());
195
196 let mut file = File::open(TEST_INDEX_PATH).unwrap();
198 for current_read in aligned_reads {
199 let mut expected = vec![0; current_read.aligned_buf().len()];
200 file.seek(SeekFrom::Start(current_read.offset())).unwrap();
201 file.read_exact(&mut expected).unwrap();
202
203 assert_eq!(
204 expected,
205 current_read.aligned_buf(),
206 "aligned_buf did not contain the expected data"
207 );
208 }
209 }
210
211 #[test]
217 fn many_batches_all_should_have_data() {
218 let mut reader = LinuxAlignedFileReader::new(TEST_INDEX_PATH).unwrap();
219
220 let read_length = 512;
221 let num_read = MAX_IO_CONCURRENCY * 100; let mut aligned_mem =
223 Poly::broadcast(0u8, read_length * num_read, AlignedAllocator::A512).unwrap();
224
225 let mut mem_slices: Vec<&mut [u8]> = aligned_mem.chunks_mut(read_length).collect();
227
228 let mut aligned_reads: Vec<AlignedRead<'_, u8, A512>> = mem_slices
230 .iter_mut()
231 .map(|slice| AlignedRead::new(0, slice).unwrap())
232 .collect();
233
234 let result = reader.read(&mut aligned_reads);
235
236 assert!(result.is_ok());
238
239 assert!(
242 !aligned_reads.iter().any(aligned_read_buffer_is_empty),
243 "Found uninitialized data that should have been read from disk"
244 );
245 }
246
247 fn aligned_read_buffer_is_empty(read: &AlignedRead<'_, u8, A512>) -> bool {
249 let max_value = read.aligned_buf().iter().fold(0, |acc, &x| max(acc, x));
250
251 max_value == 0
254 }
255
256 #[test]
257 fn test_read_disk_index_by_sector() {
258 let mut reader = LinuxAlignedFileReader::new(TEST_INDEX_PATH).unwrap();
259
260 let read_length = 512; let num_sector = 10;
262 let mut aligned_mem =
263 Poly::broadcast(0u8, read_length * num_sector, AlignedAllocator::A512).unwrap();
264
265 let mut mem_slices: Vec<&mut [u8]> = aligned_mem.chunks_mut(read_length).collect();
267
268 let mut aligned_reads: Vec<AlignedRead<'_, u8, A512>> = mem_slices
269 .iter_mut()
270 .enumerate()
271 .map(|(sector_id, slice)| {
272 let offset = (sector_id * read_length) as u64;
273 AlignedRead::new(offset, slice).unwrap()
274 })
275 .collect();
276
277 let result = reader.read(&mut aligned_reads);
278 assert!(result.is_ok());
279
280 aligned_reads.iter().for_each(|read| {
281 assert_eq!(read.aligned_buf().len(), 512);
282 });
283
284 let disk_layout_meta = reconstruct_disk_meta(aligned_reads[0].aligned_buf_mut());
285 assert!(disk_layout_meta.len() > 9);
286
287 let dims = disk_layout_meta[1];
288 let num_pts = disk_layout_meta[0];
289 let node_len = disk_layout_meta[3];
290 let max_num_nodes_per_sector = disk_layout_meta[4];
291
292 assert!(node_len * max_num_nodes_per_sector < DEFAULT_DISK_SECTOR_LEN as u64);
293
294 let num_nbrs_start = (dims as usize) * std::mem::size_of::<f32>();
295 let nbrs_buf_start = num_nbrs_start + std::mem::size_of::<u32>();
296
297 let mut node_data_array = Vec::with_capacity(max_num_nodes_per_sector as usize * 9);
298
299 (1..9).for_each(|sector_id| {
301 let sector_data = &mem_slices[sector_id];
302 for node_data in sector_data.chunks_exact(node_len as usize) {
303 let coordinates_end = (dims as usize) * std::mem::size_of::<f32>();
305 let coordinates = node_data[0..coordinates_end]
306 .chunks_exact(std::mem::size_of::<f32>())
307 .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
308 .collect();
309
310 let neighbors_num = u32::from_le_bytes(
312 node_data[num_nbrs_start..nbrs_buf_start]
313 .try_into()
314 .unwrap(),
315 );
316
317 let nbors_buf_end =
318 nbrs_buf_start + (neighbors_num as usize) * std::mem::size_of::<u32>();
319
320 let mut neighbors = Vec::new();
322 for nbors_data in node_data[nbrs_buf_start..nbors_buf_end]
323 .chunks_exact(std::mem::size_of::<u32>())
324 {
325 let nbors_id = u32::from_le_bytes(nbors_data.try_into().unwrap());
326 assert!(nbors_id < num_pts as u32);
327 neighbors.push(nbors_id);
328 }
329
330 node_data_array.push(NodeData {
332 num_neighbors: neighbors_num,
333 coordinates,
334 neighbors,
335 });
336 }
337 });
338
339 let node_data_truth_file = File::open(TRUTH_NODE_DATA_PATH).unwrap();
341 let reader = BufReader::new(node_data_truth_file);
342
343 let node_data_vec: Vec<NodeData> = deserialize_from(reader).unwrap();
344 for (node_from_node_data_file, node_from_disk_index) in
345 node_data_vec.iter().zip(node_data_array.iter())
346 {
347 assert_eq!(node_from_node_data_file, node_from_disk_index);
349 }
350 }
351
352 #[test]
353 fn test_read_fail_invalid_file() {
354 let reader = LinuxAlignedFileReader::new("/invalid_path");
355 assert!(reader.is_err());
356 }
357
358 #[test]
359 #[allow(clippy::read_zero_byte_vec)]
360 fn test_read_no_requests() {
361 let mut reader = LinuxAlignedFileReader::new(TEST_INDEX_PATH).unwrap();
362
363 let mut read_requests = Vec::<AlignedRead<u8, A512>>::new();
364 let result = reader.read(&mut read_requests);
365 assert!(result.is_ok());
366 }
367
368 fn reconstruct_disk_meta(buffer: &[u8]) -> Vec<u64> {
369 let size_of_u64 = std::mem::size_of::<u64>();
370
371 let num_values = buffer.len() / size_of_u64;
372 let mut disk_layout_meta = Vec::with_capacity(num_values);
373 let meta_data = &buffer[8..];
374
375 for chunk in meta_data.chunks_exact(size_of_u64) {
376 let value = u64::from_le_bytes(chunk.try_into().unwrap());
377 disk_layout_meta.push(value);
378 }
379
380 disk_layout_meta
381 }
382}