Skip to content

Commit 5338238

Browse files
triandcoLuQQiu
andauthored
feat: int8 support for distance functions (#3605)
Using [AllMiniLM12V2](https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2) to generate embeddings, I found that with a tiny bit of transformation, I can store the embedding as int8 to massively reduce its storage requirement and turn it into f32 during distance calculation (using cosine and dot) with marginal change to the outcome. I would like to add support for int8 by converting FixedSizeList type of Int8Array to Float32Array during calculation. The conversion is done with a ``convert_to_floating_point`` on the ``FixedSizeListArrayExt`` trait. In theory, this can also work with other types like uint8, int32, int64. This would make a big difference in storage requirement on the compatible models. --------- Co-authored-by: LuQQiu <luqiujob@gmail.com>
1 parent e0d3179 commit 5338238

11 files changed

Lines changed: 377 additions & 10 deletions

File tree

rust/lance-arrow/src/lib.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use arrow_array::{
1313
GenericListArray, OffsetSizeTrait, PrimitiveArray, RecordBatch, StructArray, UInt32Array,
1414
UInt8Array,
1515
};
16+
use arrow_array::{Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array};
1617
use arrow_buffer::MutableBuffer;
1718
use arrow_data::ArrayDataBuilder;
1819
use arrow_schema::{ArrowError, DataType, Field, FieldRef, Fields, IntervalUnit, Schema};
@@ -235,6 +236,10 @@ pub trait FixedSizeListArrayExt {
235236
/// assert_eq!(sampled.values().len(), 160);
236237
/// ```
237238
fn sample(&self, n: usize) -> Result<FixedSizeListArray>;
239+
240+
/// Ensure the [FixedSizeListArray] of Float16, Float32, Float64,
241+
/// Int8, Int16, Int32, Int64, UInt8, UInt32 type to its closest floating point type.
242+
fn convert_to_floating_point(&self) -> Result<FixedSizeListArray>;
238243
}
239244

240245
impl FixedSizeListArrayExt for FixedSizeListArray {
@@ -253,6 +258,136 @@ impl FixedSizeListArrayExt for FixedSizeListArray {
253258
let chosen = (0..self.len() as u32).choose_multiple(&mut rng, n);
254259
take(self, &UInt32Array::from(chosen), None).map(|arr| arr.as_fixed_size_list().clone())
255260
}
261+
262+
fn convert_to_floating_point(&self) -> Result<FixedSizeListArray> {
263+
match self.data_type() {
264+
DataType::FixedSizeList(field, size) => match field.data_type() {
265+
DataType::Float16 | DataType::Float32 | DataType::Float64 => Ok(self.clone()),
266+
DataType::Int8 => Ok(Self::new(
267+
Arc::new(arrow_schema::Field::new(
268+
field.name(),
269+
DataType::Float32,
270+
field.is_nullable(),
271+
)),
272+
*size,
273+
Arc::new(Float32Array::from_iter_values(
274+
self.values()
275+
.as_any()
276+
.downcast_ref::<Int8Array>()
277+
.ok_or(ArrowError::ParseError(
278+
"Fail to cast primitive array to Int8Type".to_string(),
279+
))?
280+
.into_iter()
281+
.filter_map(|x| x.map(|y| y as f32)),
282+
)),
283+
self.nulls().cloned(),
284+
)),
285+
DataType::Int16 => Ok(Self::new(
286+
Arc::new(arrow_schema::Field::new(
287+
field.name(),
288+
DataType::Float32,
289+
field.is_nullable(),
290+
)),
291+
*size,
292+
Arc::new(Float32Array::from_iter_values(
293+
self.values()
294+
.as_any()
295+
.downcast_ref::<Int16Array>()
296+
.ok_or(ArrowError::ParseError(
297+
"Fail to cast primitive array to Int8Type".to_string(),
298+
))?
299+
.into_iter()
300+
.filter_map(|x| x.map(|y| y as f32)),
301+
)),
302+
self.nulls().cloned(),
303+
)),
304+
DataType::Int32 => Ok(Self::new(
305+
Arc::new(arrow_schema::Field::new(
306+
field.name(),
307+
DataType::Float32,
308+
field.is_nullable(),
309+
)),
310+
*size,
311+
Arc::new(Float32Array::from_iter_values(
312+
self.values()
313+
.as_any()
314+
.downcast_ref::<Int32Array>()
315+
.ok_or(ArrowError::ParseError(
316+
"Fail to cast primitive array to Int8Type".to_string(),
317+
))?
318+
.into_iter()
319+
.filter_map(|x| x.map(|y| y as f32)),
320+
)),
321+
self.nulls().cloned(),
322+
)),
323+
DataType::Int64 => Ok(Self::new(
324+
Arc::new(arrow_schema::Field::new(
325+
field.name(),
326+
DataType::Float64,
327+
field.is_nullable(),
328+
)),
329+
*size,
330+
Arc::new(Float64Array::from_iter_values(
331+
self.values()
332+
.as_any()
333+
.downcast_ref::<Int64Array>()
334+
.ok_or(ArrowError::ParseError(
335+
"Fail to cast primitive array to Int8Type".to_string(),
336+
))?
337+
.into_iter()
338+
.filter_map(|x| x.map(|y| y as f64)),
339+
)),
340+
self.nulls().cloned(),
341+
)),
342+
DataType::UInt8 => Ok(Self::new(
343+
Arc::new(arrow_schema::Field::new(
344+
field.name(),
345+
DataType::Float64,
346+
field.is_nullable(),
347+
)),
348+
*size,
349+
Arc::new(Float64Array::from_iter_values(
350+
self.values()
351+
.as_any()
352+
.downcast_ref::<UInt8Array>()
353+
.ok_or(ArrowError::ParseError(
354+
"Fail to cast primitive array to Int8Type".to_string(),
355+
))?
356+
.into_iter()
357+
.filter_map(|x| x.map(|y| y as f64)),
358+
)),
359+
self.nulls().cloned(),
360+
)),
361+
DataType::UInt32 => Ok(Self::new(
362+
Arc::new(arrow_schema::Field::new(
363+
field.name(),
364+
DataType::Float64,
365+
field.is_nullable(),
366+
)),
367+
*size,
368+
Arc::new(Float64Array::from_iter_values(
369+
self.values()
370+
.as_any()
371+
.downcast_ref::<UInt32Array>()
372+
.ok_or(ArrowError::ParseError(
373+
"Fail to cast primitive array to Int8Type".to_string(),
374+
))?
375+
.into_iter()
376+
.filter_map(|x| x.map(|y| y as f64)),
377+
)),
378+
self.nulls().cloned(),
379+
)),
380+
data_type => Err(ArrowError::ParseError(format!(
381+
"Expect either floating type or integer got {:?}",
382+
data_type
383+
))),
384+
},
385+
data_type => Err(ArrowError::ParseError(format!(
386+
"Expect either FixedSizeList got {:?}",
387+
data_type
388+
))),
389+
}
390+
}
256391
}
257392

258393
/// Force downcast of an [`Array`], such as an [`ArrayRef`], to
@@ -412,6 +547,14 @@ pub trait RecordBatchExt {
412547
/// Replace a column (specified by name) and return the new [`RecordBatch`].
413548
fn replace_column_by_name(&self, name: &str, column: Arc<dyn Array>) -> Result<RecordBatch>;
414549

550+
/// Replace a column schema (specified by name) and return the new [`RecordBatch`].
551+
fn replace_column_schema_by_name(
552+
&self,
553+
name: &str,
554+
new_data_type: DataType,
555+
column: Arc<dyn Array>,
556+
) -> Result<RecordBatch>;
557+
415558
/// Get (potentially nested) column by qualified name.
416559
fn column_by_qualified_name(&self, name: &str) -> Option<&ArrayRef>;
417560

@@ -519,6 +662,37 @@ impl RecordBatchExt for RecordBatch {
519662
Self::try_new(self.schema(), columns)
520663
}
521664

665+
fn replace_column_schema_by_name(
666+
&self,
667+
name: &str,
668+
new_data_type: DataType,
669+
column: Arc<dyn Array>,
670+
) -> Result<RecordBatch> {
671+
let fields = self
672+
.schema()
673+
.fields()
674+
.iter()
675+
.map(|x| {
676+
if x.name() != name {
677+
x.clone()
678+
} else {
679+
let new_field = Field::new(name, new_data_type.clone(), x.is_nullable());
680+
Arc::new(new_field)
681+
}
682+
})
683+
.collect::<Vec<_>>();
684+
let schema = Schema::new_with_metadata(fields, self.schema().metadata.clone());
685+
let mut columns = self.columns().to_vec();
686+
let field_i = self
687+
.schema()
688+
.fields()
689+
.iter()
690+
.position(|f| f.name() == name)
691+
.ok_or_else(|| ArrowError::SchemaError(format!("Field {} does not exist", name)))?;
692+
columns[field_i] = column;
693+
Self::try_new(Arc::new(schema), columns)
694+
}
695+
522696
fn column_by_qualified_name(&self, name: &str) -> Option<&ArrayRef> {
523697
let split = name.split('.').collect::<Vec<_>>();
524698
if split.is_empty() {

rust/lance-index/src/vector/residual.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ pub(crate) fn compute_residual(
136136
(DataType::Float64, DataType::Float64) => {
137137
do_compute_residual::<Float64Type>(centroids, vectors, distance_type, partitions)
138138
}
139+
(DataType::Float32, DataType::Int8) => {
140+
do_compute_residual::<Float32Type>(
141+
centroids,
142+
&vectors.convert_to_floating_point()?,
143+
distance_type,
144+
partitions)
145+
}
139146
_ => Err(Error::Index {
140147
message: format!(
141148
"Compute residual vector: centroids and vector type mismatch: centroid: {}, vector: {}",
@@ -181,7 +188,16 @@ impl Transformer for ResidualTransform {
181188
compute_residual(&self.centroids, original_vectors, None, Some(part_ids_ref))?;
182189

183190
// Replace original column with residual column.
184-
let batch = batch.replace_column_by_name(&self.vec_col, Arc::new(residual_arr))?;
191+
let batch = if residual_arr.data_type() != original.data_type() {
192+
batch.replace_column_schema_by_name(
193+
&self.vec_col,
194+
residual_arr.data_type().clone(),
195+
Arc::new(residual_arr),
196+
)?
197+
} else {
198+
batch.replace_column_by_name(&self.vec_col, Arc::new(residual_arr))?
199+
};
200+
185201
Ok(batch)
186202
}
187203
}

rust/lance-index/src/vector/transform.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ impl Transformer for KeepFiniteVectors {
142142
DataType::Float32 => is_all_finite::<Float32Type>(&data),
143143
DataType::Float64 => is_all_finite::<Float64Type>(&data),
144144
DataType::UInt8 => data.null_count() == 0,
145+
DataType::Int8 => data.null_count() == 0,
145146
_ => false,
146147
};
147148
if is_valid {

rust/lance-linalg/src/distance/cosine.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ use std::sync::Arc;
1111

1212
use arrow_array::{
1313
cast::AsArray,
14-
types::{Float16Type, Float32Type, Float64Type},
14+
types::{Float16Type, Float32Type, Float64Type, Int8Type},
1515
Array, FixedSizeListArray, Float32Array,
1616
};
1717
use arrow_schema::DataType;
1818
use half::{bf16, f16};
19-
use lance_arrow::{ArrowFloatType, FloatArray};
19+
use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
2020
#[cfg(feature = "fp16kernels")]
2121
use lance_core::utils::cpu::SimdSupport;
2222
use lance_core::utils::cpu::FP16_SIMD_SUPPORT;
@@ -320,6 +320,14 @@ pub fn cosine_distance_arrow_batch(
320320
DataType::Float16 => do_cosine_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
321321
DataType::Float32 => do_cosine_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
322322
DataType::Float64 => do_cosine_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
323+
DataType::Int8 => do_cosine_distance_arrow_batch::<Float32Type>(
324+
&from
325+
.as_primitive::<Int8Type>()
326+
.into_iter()
327+
.map(|x| x.unwrap() as f32)
328+
.collect(),
329+
&to.convert_to_floating_point()?,
330+
),
323331
_ => Err(Error::InvalidArgumentError(format!(
324332
"Unsupported data type {:?}",
325333
from.data_type()

rust/lance-linalg/src/distance/dot.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ use std::ops::AddAssign;
88
use std::sync::Arc;
99

1010
use crate::Error;
11-
use arrow_array::types::{Float16Type, Float64Type};
11+
use arrow_array::types::{Float16Type, Float64Type, Int8Type};
1212
use arrow_array::{cast::AsArray, types::Float32Type, Array, FixedSizeListArray, Float32Array};
1313
use arrow_schema::DataType;
1414
use half::{bf16, f16};
15-
use lance_arrow::{ArrowFloatType, FloatArray};
15+
use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
1616
#[cfg(feature = "fp16kernels")]
1717
use lance_core::utils::cpu::SimdSupport;
1818
use lance_core::utils::cpu::FP16_SIMD_SUPPORT;
@@ -278,6 +278,14 @@ pub fn dot_distance_arrow_batch(
278278
DataType::Float16 => do_dot_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
279279
DataType::Float32 => do_dot_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
280280
DataType::Float64 => do_dot_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
281+
DataType::Int8 => do_dot_distance_arrow_batch::<Float32Type>(
282+
&from
283+
.as_primitive::<Int8Type>()
284+
.into_iter()
285+
.map(|x| x.unwrap() as f32)
286+
.collect(),
287+
&to.convert_to_floating_point()?,
288+
),
281289
_ => Err(Error::InvalidArgumentError(format!(
282290
"Unsupported data type: {:?}",
283291
from.data_type()

rust/lance-linalg/src/distance/l2.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ use std::sync::Arc;
1010

1111
use arrow_array::{
1212
cast::AsArray,
13-
types::{Float16Type, Float32Type, Float64Type},
13+
types::{Float16Type, Float32Type, Float64Type, Int8Type},
1414
Array, FixedSizeListArray, Float32Array,
1515
};
1616
use arrow_schema::DataType;
1717
use half::{bf16, f16};
18-
use lance_arrow::{ArrowFloatType, FloatArray};
18+
use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
1919
#[cfg(feature = "fp16kernels")]
2020
use lance_core::utils::cpu::SimdSupport;
2121
use lance_core::utils::cpu::FP16_SIMD_SUPPORT;
@@ -293,6 +293,14 @@ pub fn l2_distance_arrow_batch(
293293
DataType::Float16 => do_l2_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
294294
DataType::Float32 => do_l2_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
295295
DataType::Float64 => do_l2_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
296+
DataType::Int8 => do_l2_distance_arrow_batch::<Float32Type>(
297+
&from
298+
.as_primitive::<Int8Type>()
299+
.into_iter()
300+
.map(|x| x.unwrap() as f32)
301+
.collect(),
302+
&to.convert_to_floating_point()?,
303+
),
296304
_ => Err(Error::ComputeError(format!(
297305
"Unsupported data type: {}",
298306
from.data_type()

rust/lance-linalg/src/kmeans.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use arrow_array::{ArrowNumericType, UInt8Array};
2323
use arrow_ord::sort::sort_to_indices;
2424
use arrow_schema::{ArrowError, DataType};
2525
use bitvec::prelude::*;
26+
use lance_arrow::FixedSizeListArrayExt;
2627
use log::{info, warn};
2728
use num_traits::{AsPrimitive, Float, FromPrimitive, Num, Zero};
2829
use rand::prelude::*;
@@ -720,6 +721,15 @@ pub fn compute_partitions_arrow_array(
720721
centroids.value_length(),
721722
distance_type,
722723
)),
724+
(DataType::Float32, DataType::Int8) => Ok(compute_partitions::<
725+
Float32Type,
726+
KMeansAlgoFloat<Float32Type>,
727+
>(
728+
centroids.values().as_primitive(),
729+
vectors.convert_to_floating_point()?.values().as_primitive(),
730+
centroids.value_length(),
731+
distance_type,
732+
)),
723733
(DataType::Float64, DataType::Float64) => Ok(compute_partitions::<
724734
Float64Type,
725735
KMeansAlgoFloat<Float64Type>,
@@ -736,7 +746,7 @@ pub fn compute_partitions_arrow_array(
736746
distance_type,
737747
)),
738748
_ => Err(ArrowError::InvalidArgumentError(
739-
"Centroids and vectors have different types".to_string(),
749+
"Centroids and vectors have incompatible types".to_string(),
740750
)),
741751
}
742752
}

rust/lance-testing/src/datagen.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use std::{iter::repeat_with, ops::Range};
99

1010
use arrow_array::types::ArrowPrimitiveType;
1111
use arrow_array::{
12-
Float32Array, Int32Array, PrimitiveArray, RecordBatch, RecordBatchIterator, RecordBatchReader,
12+
Float32Array, Int32Array, Int8Array, PrimitiveArray, RecordBatch, RecordBatchIterator,
13+
RecordBatchReader,
1314
};
1415
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
1516
use lance_arrow::{fixed_size_list_type, ArrowFloatType, FixedSizeListArrayExt};
@@ -222,6 +223,13 @@ pub fn generate_random_array(n: usize) -> Float32Array {
222223
Float32Array::from_iter_values(repeat_with(|| rng.gen::<f32>()).take(n))
223224
}
224225

226+
/// Create a random float32 array where each element is uniformly
227+
/// distributed between [0..1]
228+
pub fn generate_random_int8_array(n: usize) -> Int8Array {
229+
let mut rng = rand::thread_rng();
230+
Int8Array::from_iter_values(repeat_with(|| rng.gen::<i8>()).take(n))
231+
}
232+
225233
/// Create a random primitive array where each element is uniformly distributed a
226234
/// given range.
227235
pub fn generate_random_array_with_range<T: ArrowPrimitiveType>(

0 commit comments

Comments
 (0)