lance-arrow 9.0.0

Arrow Extension for Lance
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors

//! bfloat16 support for Apache Arrow.

use std::fmt::Formatter;
use std::slice;

use arrow_array::{Array, FixedSizeBinaryArray, builder::BooleanBufferBuilder};
use arrow_buffer::{Buffer, MutableBuffer};
use arrow_data::ArrayData;
use arrow_schema::{ArrowError, DataType, Field as ArrowField};
use half::bf16;

use crate::{ARROW_EXT_NAME_KEY, FloatArray};

/// The name of the bfloat16 extension in Arrow metadata
pub const BFLOAT16_EXT_NAME: &str = "lance.bfloat16";

/// Check whether the given field is a bfloat16 field
///
/// A field is a bfloat16 field if it has a data type of `FixedSizeBinary(2)` and the metadata
/// contains the bfloat16 extension name.
pub fn is_bfloat16_field(field: &ArrowField) -> bool {
    field.data_type() == &DataType::FixedSizeBinary(2)
        && field
            .metadata()
            .get(ARROW_EXT_NAME_KEY)
            .map(|name| name == BFLOAT16_EXT_NAME)
            .unwrap_or_default()
}

/// The bfloat16 data type
///
/// This implements the [`ArrowFloatType`](crate::floats::ArrowFloatType) trait for bfloat16 values.
#[derive(Debug)]
pub struct BFloat16Type {}

/// An array of bfloat16 values
///
/// Note that bfloat16 is not the same thing as fp16 which is supported natively by arrow-rs.
#[derive(Clone)]
pub struct BFloat16Array {
    inner: FixedSizeBinaryArray,
}

impl std::fmt::Debug for BFloat16Array {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "BFloat16Array\n[\n")?;
        from_arrow::print_long_array(&self.inner, f, |array, i, f| {
            if array.is_null(i) {
                write!(f, "null")
            } else {
                let binary_values = array.value(i);
                let value =
                    bf16::from_bits(u16::from_le_bytes([binary_values[0], binary_values[1]]));
                write!(f, "{:?}", value)
            }
        })?;
        write!(f, "]")
    }
}

impl BFloat16Array {
    pub fn from_iter_values(iter: impl IntoIterator<Item = bf16>) -> Self {
        let values: Vec<bf16> = iter.into_iter().collect();
        values.into()
    }

    pub fn len(&self) -> usize {
        self.inner.len()
    }

    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    pub fn is_null(&self, i: usize) -> bool {
        self.inner.is_null(i)
    }

    pub fn null_count(&self) -> usize {
        self.inner.null_count()
    }

    pub fn iter(&self) -> BFloat16Iter<'_> {
        BFloat16Iter {
            array: self,
            index: 0,
        }
    }

    pub fn value(&self, i: usize) -> bf16 {
        assert!(
            i < self.len(),
            "Trying to access an element at index {} from a BFloat16Array of length {}",
            i,
            self.len()
        );
        // Safety:
        // `i < self.len()
        unsafe { self.value_unchecked(i) }
    }

    /// # Safety
    /// Caller must ensure that `i < self.len()`
    pub unsafe fn value_unchecked(&self, i: usize) -> bf16 {
        let binary_value = self.inner.value_unchecked(i);
        bf16::from_bits(u16::from_le_bytes([binary_value[0], binary_value[1]]))
    }

    pub fn into_inner(self) -> FixedSizeBinaryArray {
        self.inner
    }
}

impl FromIterator<Option<bf16>> for BFloat16Array {
    fn from_iter<I: IntoIterator<Item = Option<bf16>>>(iter: I) -> Self {
        let mut buffer = MutableBuffer::new(10);
        // No null buffer builder :(
        let mut nulls = BooleanBufferBuilder::new(10);
        let mut len = 0;

        for maybe_value in iter {
            if let Some(value) = maybe_value {
                let bytes = value.to_le_bytes();
                buffer.extend(bytes);
            } else {
                buffer.extend([0u8, 0u8]);
            }
            nulls.append(maybe_value.is_some());
            len += 1;
        }

        let null_buffer = nulls.finish();
        let num_valid = null_buffer.count_set_bits();
        let null_buffer = if num_valid == len {
            None
        } else {
            Some(null_buffer.into_inner())
        };

        let array_data = ArrayData::builder(DataType::FixedSizeBinary(2))
            .len(len)
            .add_buffer(buffer.into())
            .null_bit_buffer(null_buffer);
        // SAFETY: the value buffer contains exactly `2 * len` bytes (two bytes
        // pushed per iteration of the loop above, including the zero-fill for
        // null slots), which matches the `FixedSizeBinary(2)` storage layout.
        // The null bit buffer, when present, has `len` bits appended above, so
        // its length covers the array's logical range.
        let array_data = unsafe { array_data.build_unchecked() };
        Self {
            inner: FixedSizeBinaryArray::from(array_data),
        }
    }
}

impl FromIterator<bf16> for BFloat16Array {
    fn from_iter<I: IntoIterator<Item = bf16>>(iter: I) -> Self {
        Self::from_iter_values(iter)
    }
}

impl From<Vec<bf16>> for BFloat16Array {
    fn from(data: Vec<bf16>) -> Self {
        let len = data.len();
        // Zero-copy: `bf16` is `#[repr(transparent)]` over `u16` and derives
        // `bytemuck::Pod`, so `cast_vec` reinterprets the allocation in place —
        // no per-element copy or heap alloc. The crate-root `compile_error!`
        // pins `target_endian = "little"`, so the resulting bytes match the
        // `FixedSizeBinary(2)` on-disk order Lance writes elsewhere.
        let raw: Vec<u16> = bytemuck::cast_vec(data);
        let array_data = ArrayData::builder(DataType::FixedSizeBinary(2))
            .len(len)
            .add_buffer(Buffer::from_vec(raw));
        // SAFETY: the value buffer contains exactly `2 * len` bytes — one
        // `u16` per element after the layout-compatible cast — matching the
        // `FixedSizeBinary(2)` storage layout. No null buffer is attached, so
        // every element is logically valid.
        let array_data = unsafe { array_data.build_unchecked() };
        Self {
            inner: FixedSizeBinaryArray::from(array_data),
        }
    }
}

impl TryFrom<FixedSizeBinaryArray> for BFloat16Array {
    type Error = ArrowError;

    fn try_from(value: FixedSizeBinaryArray) -> Result<Self, Self::Error> {
        if value.value_length() == 2 {
            Ok(Self { inner: value })
        } else {
            Err(ArrowError::InvalidArgumentError(
                "FixedSizeBinaryArray must have a value length of 2".to_string(),
            ))
        }
    }
}

impl PartialEq<Self> for BFloat16Array {
    fn eq(&self, other: &Self) -> bool {
        self.inner.eq(&other.inner)
    }
}

pub struct BFloat16Iter<'a> {
    array: &'a BFloat16Array,
    index: usize,
}

impl<'a> Iterator for BFloat16Iter<'a> {
    type Item = Option<bf16>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.index >= self.array.len() {
            return None;
        }
        let i = self.index;
        self.index += 1;
        if self.array.is_null(i) {
            Some(None)
        } else {
            Some(Some(self.array.value(i)))
        }
    }
}

/// Methods that are lifted from arrow-rs temporarily until they are made public.
mod from_arrow {
    use arrow_array::Array;

    /// Helper function for printing potentially long arrays.
    pub(super) fn print_long_array<A, F>(
        array: &A,
        f: &mut std::fmt::Formatter,
        print_item: F,
    ) -> std::fmt::Result
    where
        A: Array,
        F: Fn(&A, usize, &mut std::fmt::Formatter) -> std::fmt::Result,
    {
        let head = std::cmp::min(10, array.len());

        for i in 0..head {
            if array.is_null(i) {
                writeln!(f, "  null,")?;
            } else {
                write!(f, "  ")?;
                print_item(array, i, f)?;
                writeln!(f, ",")?;
            }
        }
        if array.len() > 10 {
            if array.len() > 20 {
                writeln!(f, "  ...{} elements...,", array.len() - 20)?;
            }

            let tail = std::cmp::max(head, array.len() - 10);

            for i in tail..array.len() {
                if array.is_null(i) {
                    writeln!(f, "  null,")?;
                } else {
                    write!(f, "  ")?;
                    print_item(array, i, f)?;
                    writeln!(f, ",")?;
                }
            }
        }
        Ok(())
    }
}

impl FloatArray<BFloat16Type> for FixedSizeBinaryArray {
    type FloatType = BFloat16Type;

    /// Returns the underlying `bf16` values as a borrowed slice.
    ///
    /// # Preconditions
    ///
    /// - `value_length()` must be 2 (the `FixedSizeBinary(2)` storage shape
    ///   used by [`BFloat16Array`]). Asserted at entry.
    /// - The value buffer must be at least 2-byte aligned. Lance's in-tree
    ///   constructors always satisfy this: value buffers are built either via
    ///   `MutableBuffer` (aligned to arrow-buffer's `ALIGNMENT` constant, ≥32
    ///   bytes) or via `Buffer::from_vec::<u16>` (aligned to `align_of::<u16>()`
    ///   == 2); both meet `bf16`'s 2-byte requirement. Externally-built
    ///   `FixedSizeBinaryArray`s arriving via FFI, IPC, or
    ///   `Buffer::from_custom_allocation` are not required by arrow-rs to be
    ///   aligned beyond a single byte; passing one to this method violates the
    ///   precondition. A `debug_assert` below catches such inputs in debug and
    ///   test builds.
    ///
    /// # Endianness
    ///
    /// `lance-arrow` is gated on `target_endian = "little"` at the crate root,
    /// so this method always returns values in the same byte order Lance writes
    /// (see [`BFloat16Array::value`] and the [`FromIterator`] impls).
    fn as_slice(&self) -> &[bf16] {
        assert_eq!(
            self.value_length(),
            2,
            "BFloat16 arrays must use FixedSizeBinary(2) storage"
        );
        debug_assert_eq!(
            (self.value_data().as_ptr() as usize) % std::mem::align_of::<bf16>(),
            0,
            "BFloat16 value buffer must be at least 2-byte aligned"
        );
        // SAFETY:
        // - The assert above pins `value_size == 2`, so `value_data().len() / 2`
        //   equals the array's logical element count.
        //   `FixedSizeBinaryArray::From<ArrayData>` constructs its value buffer
        //   as `buffers[0].slice_with_length(offset * 2, len * 2)` (arrow-array
        //   `fixed_size_binary_array.rs`), so `value_data()` already returns
        //   the offset-adjusted slice. Do not replace `value_data()` with an
        //   accessor that returns the un-sliced backing buffer.
        // - `bf16` is `#[repr(transparent)]` over `u16` (size 2, alignment 2);
        //   every `u16` bit pattern is a valid `bf16`, so any byte content
        //   yields a defined value — never UB.
        // - Alignment is the caller's responsibility per the precondition
        //   documented above. The `debug_assert_eq!` immediately preceding this
        //   block catches violations in debug and test builds only — release
        //   builds rely on callers honoring the precondition. arrow-rs
        //   declares `FixedSizeBinary(n)`'s
        //   `BufferSpec::FixedWidth { alignment: align_of::<u8>() == 1 }`
        //   (arrow-data `data.rs`), so arrow-rs alone does not guarantee
        //   2-byte alignment. Lance's in-tree construction paths build value
        //   buffers via `MutableBuffer` (arrow-buffer `ALIGNMENT` constant,
        //   ≥32 bytes) or `Buffer::from_vec::<u16>` (2-byte aligned), both of
        //   which satisfy `bf16`'s 2-byte requirement.
        // - The returned slice borrows from `self`; the underlying ref-counted,
        //   immutable Arrow buffer cannot be mutated or freed for the slice's
        //   lifetime.
        unsafe {
            slice::from_raw_parts(
                self.value_data().as_ptr() as *const bf16,
                self.value_data().len() / 2,
            )
        }
    }

    fn from_values(values: Vec<bf16>) -> Self {
        BFloat16Array::from(values).into_inner()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_basics() {
        let values: Vec<f32> = vec![1.0, 2.0, 3.0];
        let values: Vec<bf16> = values.iter().map(|v| bf16::from_f32(*v)).collect();

        let array = BFloat16Array::from_iter_values(values.clone());
        let array2 = BFloat16Array::from(values.clone());
        assert_eq!(array, array2);
        assert_eq!(array.len(), 3);

        // Pin the raw little-endian bytes emitted by `From<Vec<bf16>>` (rewritten to
        // reinterpret the Vec via `bytemuck::cast_vec`), so a layout/byte-order
        // regression is caught directly rather than only through Debug formatting.
        // bf16 is the high 16 bits of the f32: 1.0->0x3F80, 2.0->0x4000, 3.0->0x4040.
        let inner = array2.clone().into_inner();
        let raw_bytes: Vec<u8> = (0..inner.len())
            .flat_map(|i| inner.value(i).to_vec())
            .collect();
        assert_eq!(raw_bytes, vec![0x80, 0x3F, 0x00, 0x40, 0x40, 0x40]);

        let expected_fmt = "BFloat16Array\n[\n  1.0,\n  2.0,\n  3.0,\n]";
        assert_eq!(expected_fmt, format!("{:?}", array));

        for (expected, value) in values.iter().zip(array.iter()) {
            assert_eq!(Some(*expected), value);
        }

        for (expected, value) in values.as_slice().iter().zip(array2.iter()) {
            assert_eq!(Some(*expected), value);
        }

        let arrow_array = array.into_inner();
        assert_eq!(arrow_array.as_slice(), values.as_slice());
    }

    #[test]
    fn test_nulls() {
        let values: Vec<Option<bf16>> =
            vec![Some(bf16::from_f32(1.0)), None, Some(bf16::from_f32(3.0))];
        let array = BFloat16Array::from_iter(values.clone());
        assert_eq!(array.len(), 3);
        assert_eq!(array.null_count(), 1);

        let expected_fmt = "BFloat16Array\n[\n  1.0,\n  null,\n  3.0,\n]";
        assert_eq!(expected_fmt, format!("{:?}", array));

        for (expected, value) in values.iter().zip(array.iter()) {
            assert_eq!(*expected, value);
        }
    }
}