Skip to main content

lance_index/vector/v3/
shuffler.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Shuffler is a component that takes a stream of record batches and shuffles them into
5//! the corresponding IVF partitions.
6
7use std::sync::Arc;
8
9use arrow::{array::AsArray, compute::sort_to_indices};
10use arrow_array::{RecordBatch, UInt32Array};
11use arrow_schema::Schema;
12use futures::{future::try_join_all, prelude::*};
13use lance_arrow::{RecordBatchExt, SchemaExt};
14use lance_core::{
15    Error, Result,
16    cache::LanceCache,
17    utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu},
18};
19use lance_encoding::decoder::{DecoderPlugins, FilterExpression};
20use lance_file::reader::{FileReader, FileReaderOptions};
21use lance_file::writer::FileWriter;
22use lance_io::{
23    object_store::ObjectStore,
24    scheduler::{ScanScheduler, SchedulerConfig},
25    stream::{RecordBatchStream, RecordBatchStreamAdapter},
26    utils::CachedFileSize,
27};
28use object_store::path::Path;
29
30use crate::vector::{LOSS_METADATA_KEY, PART_ID_COLUMN};
31
32#[async_trait::async_trait]
33/// A reader that can read the shuffled partitions.
34pub trait ShuffleReader: Send + Sync {
35    /// Read a partition by partition_id
36    /// will return Ok(None) if partition_size is 0
37    /// check reader.partition_size(partition_id) before calling this function
38    async fn read_partition(
39        &self,
40        partition_id: usize,
41    ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>>;
42
43    /// Get the size of the partition by partition_id
44    fn partition_size(&self, partition_id: usize) -> Result<usize>;
45
46    /// Get the total loss,
47    /// if the loss is not available, return None,
48    /// in such case, the caller should sum up the losses from each batch's metadata.
49    /// Must be called after all partitions are read.
50    fn total_loss(&self) -> Option<f64>;
51}
52
53#[async_trait::async_trait]
54/// A shuffler that can shuffle the incoming stream of record batches into IVF partitions.
55/// Returns a IvfShuffleReader that can be used to read the shuffled partitions.
56pub trait Shuffler: Send + Sync {
57    /// Shuffle the incoming stream of record batches into IVF partitions.
58    /// Returns a IvfShuffleReader that can be used to read the shuffled partitions.
59    async fn shuffle(
60        &self,
61        data: Box<dyn RecordBatchStream + Unpin + 'static>,
62    ) -> Result<Box<dyn ShuffleReader>>;
63}
64
65pub struct IvfShuffler {
66    object_store: Arc<ObjectStore>,
67    output_dir: Path,
68    num_partitions: usize,
69
70    // options
71    precomputed_shuffle_buffers: Option<Vec<String>>,
72    progress: Arc<dyn crate::progress::IndexBuildProgress>,
73}
74
75impl IvfShuffler {
76    pub fn new(output_dir: Path, num_partitions: usize) -> Self {
77        Self {
78            object_store: Arc::new(ObjectStore::local()),
79            output_dir,
80            num_partitions,
81            precomputed_shuffle_buffers: None,
82            progress: crate::progress::noop_progress(),
83        }
84    }
85
86    pub fn with_progress(mut self, progress: Arc<dyn crate::progress::IndexBuildProgress>) -> Self {
87        self.progress = progress;
88        self
89    }
90
91    pub fn with_precomputed_shuffle_buffers(
92        mut self,
93        precomputed_shuffle_buffers: Option<Vec<String>>,
94    ) -> Self {
95        self.precomputed_shuffle_buffers = precomputed_shuffle_buffers;
96        self
97    }
98}
99
100#[async_trait::async_trait]
101impl Shuffler for IvfShuffler {
102    async fn shuffle(
103        &self,
104        data: Box<dyn RecordBatchStream + Unpin + 'static>,
105    ) -> Result<Box<dyn ShuffleReader>> {
106        let num_partitions = self.num_partitions;
107        let mut partition_sizes = vec![0; num_partitions];
108        let schema = data.schema().without_column(PART_ID_COLUMN);
109        let mut writers = stream::iter(0..num_partitions)
110            .map(|partition_id| {
111                let part_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
112                let spill_path = self.output_dir.child(format!("ivf_{}.spill", partition_id));
113                let object_store = self.object_store.clone();
114                let schema = schema.clone();
115                async move {
116                    let writer = object_store.create(&part_path).await?;
117                    let file_writer = FileWriter::try_new(
118                        writer,
119                        lance_core::datatypes::Schema::try_from(&schema)?,
120                        Default::default(),
121                    )?
122                    .with_page_metadata_spill(object_store.clone(), spill_path);
123                    Result::Ok(file_writer)
124                }
125            })
126            .buffered(self.object_store.io_parallelism())
127            .try_collect::<Vec<_>>()
128            .await?;
129        let mut parallel_sort_stream = data
130            .map(|batch| {
131                spawn_cpu(move || {
132                    let batch = batch?;
133
134                    let loss = batch
135                        .metadata()
136                        .get(LOSS_METADATA_KEY)
137                        .map(|s| s.parse::<f64>().unwrap_or_default())
138                        .unwrap_or_default();
139
140                    let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
141
142                    let indices = sort_to_indices(&part_ids, None, None)?;
143                    let batch = batch.take(&indices)?;
144
145                    let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
146                    let batch = batch.drop_column(PART_ID_COLUMN)?;
147
148                    let mut partition_buffers = vec![Vec::new(); num_partitions];
149
150                    let mut start = 0;
151                    while start < batch.num_rows() {
152                        let part_id: u32 = part_ids.value(start);
153                        let mut end = start + 1;
154                        while end < batch.num_rows() && part_ids.value(end) == part_id {
155                            end += 1;
156                        }
157
158                        let part_batches = &mut partition_buffers[part_id as usize];
159                        part_batches.push(batch.slice(start, end - start));
160                        start = end;
161                    }
162
163                    Ok::<(Vec<Vec<RecordBatch>>, f64), Error>((partition_buffers, loss))
164                })
165            })
166            .buffered(get_num_compute_intensive_cpus());
167
168        let mut total_loss = 0.0;
169        let mut counter: u64 = 0;
170        while let Some(shuffled) = parallel_sort_stream.next().await {
171            let (shuffled, loss) = shuffled?;
172            total_loss += loss;
173
174            let mut futs = Vec::new();
175            for (part_id, (writer, batches)) in writers.iter_mut().zip(shuffled.iter()).enumerate()
176            {
177                if !batches.is_empty() {
178                    partition_sizes[part_id] += batches.iter().map(|b| b.num_rows()).sum::<usize>();
179                    futs.push(writer.write_batches(batches.iter()));
180                }
181            }
182            try_join_all(futs).await?;
183
184            counter += 1;
185            self.progress.stage_progress("shuffle", counter).await?;
186        }
187
188        // finish all writers
189        for writer in writers.iter_mut() {
190            writer.finish().await?;
191        }
192
193        Ok(Box::new(IvfShufflerReader::new(
194            self.object_store.clone(),
195            self.output_dir.clone(),
196            partition_sizes,
197            total_loss,
198        )))
199    }
200}
201
202pub struct IvfShufflerReader {
203    scheduler: Arc<ScanScheduler>,
204    output_dir: Path,
205    partition_sizes: Vec<usize>,
206    loss: f64,
207}
208
209impl IvfShufflerReader {
210    pub fn new(
211        object_store: Arc<ObjectStore>,
212        output_dir: Path,
213        partition_sizes: Vec<usize>,
214        loss: f64,
215    ) -> Self {
216        let scheduler_config = SchedulerConfig::max_bandwidth(&object_store);
217        let scheduler = ScanScheduler::new(object_store, scheduler_config);
218        Self {
219            scheduler,
220            output_dir,
221            partition_sizes,
222            loss,
223        }
224    }
225}
226
227#[async_trait::async_trait]
228impl ShuffleReader for IvfShufflerReader {
229    async fn read_partition(
230        &self,
231        partition_id: usize,
232    ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
233        if partition_id >= self.partition_sizes.len() {
234            return Ok(None);
235        }
236
237        let partition_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
238
239        let reader = FileReader::try_open(
240            self.scheduler
241                .open_file(&partition_path, &CachedFileSize::unknown())
242                .await?,
243            None,
244            Arc::<DecoderPlugins>::default(),
245            &LanceCache::no_cache(),
246            FileReaderOptions::default(),
247        )
248        .await?;
249        let schema: Schema = reader.schema().as_ref().into();
250        Ok(Some(Box::new(RecordBatchStreamAdapter::new(
251            Arc::new(schema),
252            reader.read_stream(
253                lance_io::ReadBatchParams::RangeFull,
254                u32::MAX,
255                16,
256                FilterExpression::no_filter(),
257            )?,
258        ))))
259    }
260
261    fn partition_size(&self, partition_id: usize) -> Result<usize> {
262        Ok(self.partition_sizes.get(partition_id).copied().unwrap_or(0))
263    }
264
265    fn total_loss(&self) -> Option<f64> {
266        Some(self.loss)
267    }
268}
269
270pub struct EmptyReader;
271
272#[async_trait::async_trait]
273impl ShuffleReader for EmptyReader {
274    async fn read_partition(
275        &self,
276        _partition_id: usize,
277    ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
278        Ok(None)
279    }
280
281    fn partition_size(&self, _partition_id: usize) -> Result<usize> {
282        Ok(0)
283    }
284
285    fn total_loss(&self) -> Option<f64> {
286        None
287    }
288}