This is an automated email from the ASF dual-hosted git repository.

jerry-024 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-vector-index.git


The following commit(s) were added to refs/heads/main by this push:
     new b5b4a77  fix: bound IVF-Flat serialization allocations (#77)
b5b4a77 is described below

commit b5b4a77f2a6a980c7602a7fc532472fb32ae117e
Author: jerry <[email protected]>
AuthorDate: Thu Aug 13 17:49:50 2026 +0800

    fix: bound IVF-Flat serialization allocations (#77)
---
 core/src/ivfflat_io.rs | 147 +++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 129 insertions(+), 18 deletions(-)

diff --git a/core/src/ivfflat_io.rs b/core/src/ivfflat_io.rs
index 10ddb5c..99a5a91 100644
--- a/core/src/ivfflat_io.rs
+++ b/core/src/ivfflat_io.rs
@@ -38,14 +38,30 @@ pub const IVFFLAT_HEADER_SIZE: usize = 64;
 const FLAG_DELTA_IDS: u32 = 1 << 0;
 const REQUIRED_FLAGS: u32 = FLAG_DELTA_IDS;
 const SUPPORTED_FLAGS: u32 = REQUIRED_FLAGS;
+const IVFFLAT_WRITE_BUFFER_SIZE: usize = 64 * 1024 * 1024;
 // Raw-vector scan cost scales with both rows and dimension. Below this amount,
 // Rayon scheduling and list-local heap merging outweigh the saved CPU time.
 const PARALLEL_FLAT_SCAN_MIN_COMPONENTS: usize = 1024 * 1024;
 
 pub fn write_ivfflat_index(index: &IVFFlatIndex, out: &mut dyn SeekWrite) -> 
io::Result<()> {
+    write_ivfflat_index_with_buffer_limit(index, out, 
IVFFLAT_WRITE_BUFFER_SIZE)
+}
+
+fn write_ivfflat_index_with_buffer_limit(
+    index: &IVFFlatIndex,
+    out: &mut dyn SeekWrite,
+    buffer_limit: usize,
+) -> io::Result<()> {
     let d = index.d;
     let nlist = index.nlist;
     validate_index_shape(index)?;
+    let bytes_per_vector = d.checked_mul(size_of::<f32>()).ok_or_else(|| {
+        io::Error::new(
+            io::ErrorKind::InvalidInput,
+            "IVF-FLAT bytes per vector overflow",
+        )
+    })?;
+    let mut write_buffer = Vec::new();
     let d_i32 = usize_to_i32(d, "dimension")?;
     let nlist_i32 = usize_to_i32(nlist, "nlist")?;
     let total_vectors = index.ids.iter().try_fold(0i64, |sum, ids| {
@@ -85,7 +101,12 @@ pub fn write_ivfflat_index(index: &IVFFlatIndex, out: &mut 
dyn SeekWrite) -> io:
     write_u32_le(out, FLAG_DELTA_IDS)?;
     out.write_all(&[0u8; 32])?;
 
-    write_f32_slice(out, &index.quantizer_centroids)?;
+    write_f32_slice(
+        out,
+        &index.quantizer_centroids,
+        &mut write_buffer,
+        buffer_limit,
+    )?;
 
     let offset_table_size = nlist.checked_mul(16).ok_or_else(|| {
         io::Error::new(
@@ -114,15 +135,7 @@ pub fn write_ivfflat_index(index: &IVFFlatIndex, out: &mut 
dyn SeekWrite) -> io:
         if count > 0 {
             let id_bytes_len = sorted_lists[list_id].1.len();
             list_id_bytes_lens[list_id] = usize_to_i32(id_bytes_len, "delta ID 
section")?;
-            let vector_bytes = checked_list_bytes(
-                count,
-                d.checked_mul(4).ok_or_else(|| {
-                    io::Error::new(
-                        io::ErrorKind::InvalidInput,
-                        "IVF-FLAT bytes per vector overflow",
-                    )
-                })?,
-            )?;
+            let vector_bytes = checked_list_bytes(count, bytes_per_vector)?;
             let list_bytes = 12usize
                 .checked_add(id_bytes_len)
                 .and_then(|len| len.checked_add(vector_bytes))
@@ -150,11 +163,15 @@ pub fn write_ivfflat_index(index: &IVFFlatIndex, out: 
&mut dyn SeekWrite) -> io:
         write_i64_le(out, index.ids[list_id][order[0]])?;
         write_i32_le(out, id_bytes.len() as i32)?;
         out.write_all(&id_bytes)?;
-        let mut sorted_vectors = Vec::with_capacity(order.len() * d);
-        for idx in order {
-            sorted_vectors.extend_from_slice(&index.vectors[list_id][idx * 
d..(idx + 1) * d]);
-        }
-        write_f32_slice(out, &sorted_vectors)?;
+        let vectors = &index.vectors[list_id];
+        write_f32_iter(
+            out,
+            order
+                .into_iter()
+                .flat_map(|idx| vectors[idx * d..(idx + 1) * d].iter()),
+            &mut write_buffer,
+            buffer_limit,
+        )?;
     }
 
     Ok(())
@@ -1139,9 +1156,41 @@ fn write_i64_le(out: &mut dyn SeekWrite, v: i64) -> 
io::Result<()> {
     out.write_all(&v.to_le_bytes())
 }
 
-fn write_f32_slice(out: &mut dyn SeekWrite, data: &[f32]) -> io::Result<()> {
-    let bytes: Vec<u8> = data.iter().flat_map(|f| f.to_le_bytes()).collect();
-    out.write_all(&bytes)
+fn write_f32_slice(
+    out: &mut dyn SeekWrite,
+    data: &[f32],
+    buffer: &mut Vec<u8>,
+    buffer_limit: usize,
+) -> io::Result<()> {
+    write_f32_iter(out, data.iter(), buffer, buffer_limit)
+}
+
+fn write_f32_iter<'a>(
+    out: &mut dyn SeekWrite,
+    data: impl Iterator<Item = &'a f32>,
+    buffer: &mut Vec<u8>,
+    buffer_limit: usize,
+) -> io::Result<()> {
+    let buffer_limit = buffer_limit.max(1);
+    buffer.clear();
+    for value in data {
+        let bytes = value.to_le_bytes();
+        let mut offset = 0;
+        while offset < bytes.len() {
+            let len = (buffer_limit - buffer.len()).min(bytes.len() - offset);
+            buffer.extend_from_slice(&bytes[offset..offset + len]);
+            offset += len;
+            if buffer.len() == buffer_limit {
+                out.write_all(buffer)?;
+                buffer.clear();
+            }
+        }
+    }
+    if !buffer.is_empty() {
+        out.write_all(buffer)?;
+        buffer.clear();
+    }
+    Ok(())
 }
 
 fn validate_positive_i32(val: i32, field: &str) -> io::Result<i32> {
@@ -1418,6 +1467,68 @@ mod tests {
         bytes
     }
 
+    struct MaxWriteWriter {
+        bytes: Vec<u8>,
+        max_write: usize,
+    }
+
+    impl SeekWrite for MaxWriteWriter {
+        fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
+            self.max_write = self.max_write.max(buf.len());
+            self.bytes.extend_from_slice(buf);
+            Ok(())
+        }
+
+        fn pos(&self) -> u64 {
+            self.bytes.len() as u64
+        }
+    }
+
+    #[test]
+    fn ivfflat_chunked_writer_preserves_format_and_bounds() {
+        const TEST_BUDGET: usize = 64;
+
+        let mut index = IVFFlatIndex::new(3, 1, MetricType::L2);
+        index.quantizer_centroids = vec![1.0, 2.0, 3.0];
+        index.ids[0] = vec![50, 10, 40, 20, 30, 60, 0];
+        index.vectors[0] = index.ids[0]
+            .iter()
+            .flat_map(|&id| [id as f32, id as f32 + 0.25, id as f32 + 0.5])
+            .collect();
+
+        let expected_bytes = serialized_flat_index(&index);
+        let mut chunked = MaxWriteWriter {
+            bytes: Vec::new(),
+            max_write: 0,
+        };
+        write_ivfflat_index_with_buffer_limit(&index, &mut chunked, 
TEST_BUDGET).unwrap();
+
+        assert_eq!(chunked.bytes, expected_bytes);
+        assert!(chunked.max_write <= TEST_BUDGET);
+
+        let mut reader = 
IVFFlatIndexReader::open(Cursor::new(chunked.bytes)).unwrap();
+        let (ids, vectors) = reader.read_inverted_list(0).unwrap();
+        assert_eq!(ids, vec![0, 10, 20, 30, 40, 50, 60]);
+        assert_eq!(
+            vectors,
+            ids.iter()
+                .flat_map(|&id| [id as f32, id as f32 + 0.25, id as f32 + 0.5])
+                .collect::<Vec<_>>()
+        );
+
+        let wide_dimension = TEST_BUDGET / size_of::<f32>() + 1;
+        let mut wide_index = IVFFlatIndex::new(wide_dimension, 1, 
MetricType::L2);
+        wide_index.quantizer_centroids = vec![0.0; wide_dimension];
+        wide_index.ids[0] = vec![1];
+        wide_index.vectors[0] = vec![1.0; wide_dimension];
+        let mut wide = MaxWriteWriter {
+            bytes: Vec::new(),
+            max_write: 0,
+        };
+        write_ivfflat_index_with_buffer_limit(&wide_index, &mut wide, 
TEST_BUDGET).unwrap();
+        assert!(wide.max_write <= TEST_BUDGET);
+    }
+
     #[test]
     fn ivfflat_streamed_list_reader_matches_full_payload() {
         let index = balanced_flat_index(8, 1, 257);

Reply via email to