Skip to main content

burn_flex/ops/
bool.rs

1//! Bool tensor operations for the Flex backend.
2
3use alloc::vec;
4use alloc::vec::Vec;
5use burn_backend::{
6    DType, ExecutionError, TensorData,
7    ops::{BoolTensorOps, IntTensorOps},
8    tensor::{BoolTensor, Device, FloatTensor, IntTensor},
9};
10use burn_std::{Bytes, FloatDType, IntDType, Shape, Slice, bf16, f16};
11
12use crate::{Flex, FlexTensor, Layout};
13
14impl BoolTensorOps<Flex> for Flex {
15    fn bool_from_data(data: TensorData, _device: &Device<Flex>) -> BoolTensor<Flex> {
16        FlexTensor::from_data(data)
17    }
18
19    async fn bool_into_data(tensor: BoolTensor<Flex>) -> Result<TensorData, ExecutionError> {
20        Ok(tensor.into_data())
21    }
22
23    fn bool_to_device(tensor: BoolTensor<Flex>, _device: &Device<Flex>) -> BoolTensor<Flex> {
24        tensor
25    }
26
27    fn bool_cat(tensors: Vec<BoolTensor<Flex>>, dim: usize) -> BoolTensor<Flex> {
28        crate::ops::cat::cat(tensors, dim)
29    }
30
31    fn bool_reshape(tensor: BoolTensor<Flex>, shape: Shape) -> BoolTensor<Flex> {
32        tensor.reshape(shape)
33    }
34
35    fn bool_slice(tensor: BoolTensor<Flex>, slices: &[Slice]) -> BoolTensor<Flex> {
36        crate::ops::slice::slice(tensor, slices)
37    }
38
39    fn bool_empty(
40        shape: Shape,
41        _device: &Device<Flex>,
42        dtype: burn_std::BoolDType,
43    ) -> BoolTensor<Flex> {
44        FlexTensor::empty(shape, DType::from(dtype))
45    }
46
47    fn bool_slice_assign(
48        tensor: BoolTensor<Flex>,
49        slices: &[Slice],
50        value: BoolTensor<Flex>,
51    ) -> BoolTensor<Flex> {
52        crate::ops::slice::slice_assign(tensor, slices, value)
53    }
54
55    fn bool_into_int(tensor: BoolTensor<Flex>, out_dtype: burn_std::IntDType) -> IntTensor<Flex> {
56        let tensor = tensor.to_contiguous();
57        let shape = tensor.layout().shape().clone();
58        let out_dt = DType::from(out_dtype);
59        let bools = tensor.bytes();
60
61        macro_rules! convert {
62            ($int_ty:ty) => {{
63                let data: Vec<$int_ty> =
64                    bools.iter().map(|&x| if x != 0 { 1 } else { 0 }).collect();
65                FlexTensor::new(Bytes::from_elems(data), Layout::contiguous(shape), out_dt)
66            }};
67        }
68
69        match out_dtype {
70            IntDType::I64 => convert!(i64),
71            IntDType::I32 => convert!(i32),
72            IntDType::I16 => convert!(i16),
73            IntDType::I8 => convert!(i8),
74            IntDType::U64 => convert!(u64),
75            IntDType::U32 => convert!(u32),
76            IntDType::U16 => convert!(u16),
77            IntDType::U8 => convert!(u8),
78        }
79    }
80
81    fn bool_into_float(
82        tensor: BoolTensor<Flex>,
83        out_dtype: burn_std::FloatDType,
84    ) -> FloatTensor<Flex> {
85        let tensor = tensor.to_contiguous();
86        let shape = tensor.layout().shape().clone();
87        let out_dt = DType::from(out_dtype);
88        let bools = tensor.bytes();
89
90        match out_dtype {
91            FloatDType::F64 => {
92                let data: Vec<f64> = bools
93                    .iter()
94                    .map(|&x| if x != 0 { 1.0 } else { 0.0 })
95                    .collect();
96                FlexTensor::new(Bytes::from_elems(data), Layout::contiguous(shape), out_dt)
97            }
98            FloatDType::F32 | FloatDType::Flex32 => {
99                let data: Vec<f32> = bools
100                    .iter()
101                    .map(|&x| if x != 0 { 1.0 } else { 0.0 })
102                    .collect();
103                FlexTensor::new(Bytes::from_elems(data), Layout::contiguous(shape), out_dt)
104            }
105            FloatDType::F16 => {
106                let one = f16::from_f32(1.0);
107                let zero = f16::from_f32(0.0);
108                let data: Vec<f16> = bools
109                    .iter()
110                    .map(|&x| if x != 0 { one } else { zero })
111                    .collect();
112                FlexTensor::new(Bytes::from_elems(data), Layout::contiguous(shape), out_dt)
113            }
114            FloatDType::BF16 => {
115                let one = bf16::from_f32(1.0);
116                let zero = bf16::from_f32(0.0);
117                let data: Vec<bf16> = bools
118                    .iter()
119                    .map(|&x| if x != 0 { one } else { zero })
120                    .collect();
121                FlexTensor::new(Bytes::from_elems(data), Layout::contiguous(shape), out_dt)
122            }
123        }
124    }
125
126    fn bool_swap_dims(tensor: BoolTensor<Flex>, dim1: usize, dim2: usize) -> BoolTensor<Flex> {
127        tensor.transpose(dim1, dim2)
128    }
129
130    fn bool_permute(tensor: BoolTensor<Flex>, axes: &[usize]) -> BoolTensor<Flex> {
131        tensor.permute(axes)
132    }
133
134    fn bool_flip(tensor: BoolTensor<Flex>, axes: &[usize]) -> BoolTensor<Flex> {
135        crate::ops::flip::flip(tensor, axes)
136    }
137
138    fn bool_equal(lhs: BoolTensor<Flex>, rhs: BoolTensor<Flex>) -> BoolTensor<Flex> {
139        use crate::strided_index::StridedIter;
140
141        // Broadcast to a common shape before comparing. The contiguous fast
142        // path below uses `zip`, which silently truncates to the shorter
143        // operand; and the output shape is taken from lhs, so mismatched
144        // operands would otherwise produce a result vec shorter than the
145        // output layout claims.
146        let (lhs, rhs) = crate::ops::expand::broadcast_binary(lhs, rhs);
147
148        let out_dtype = burn_std::BoolDType::from(lhs.dtype());
149        let shape = lhs.layout().shape().clone();
150        let lhs_storage: &[u8] = lhs.bytes();
151        let rhs_storage: &[u8] = rhs.bytes();
152
153        let result: Vec<u8> = match (
154            lhs.layout().contiguous_offsets(),
155            rhs.layout().contiguous_offsets(),
156        ) {
157            (Some((l_start, l_end)), Some((r_start, r_end))) => {
158                let l_slice = &lhs_storage[l_start..l_end];
159                let r_slice = &rhs_storage[r_start..r_end];
160                l_slice
161                    .iter()
162                    .zip(r_slice)
163                    .map(|(&a, &b)| (a == b) as u8)
164                    .collect()
165            }
166            _ => {
167                let lhs_iter = StridedIter::new(lhs.layout());
168                let rhs_iter = StridedIter::new(rhs.layout());
169                lhs_iter
170                    .zip(rhs_iter)
171                    .map(|(li, ri)| (lhs_storage[li] == rhs_storage[ri]) as u8)
172                    .collect()
173            }
174        };
175
176        crate::ops::comparison::make_bool_tensor(result, shape, out_dtype)
177    }
178
179    fn bool_not(mut tensor: BoolTensor<Flex>) -> BoolTensor<Flex> {
180        use crate::strided_index::StridedIter;
181
182        debug_assert!(
183            matches!(
184                tensor.dtype(),
185                DType::Bool(burn_std::BoolStore::Native | burn_std::BoolStore::U8)
186            ),
187            "bool_not: only Bool(Native) and Bool(U8) are supported, got {:?}",
188            tensor.dtype()
189        );
190
191        // Fast path: in-place for unique, contiguous tensors at offset 0. This
192        // preserves the input tensor's dtype tag implicitly (the in-place SIMD
193        // ops flip bytes without touching the dtype tag).
194        if tensor.is_unique()
195            && tensor.layout().is_contiguous()
196            && tensor.layout().start_offset() == 0
197        {
198            let storage = tensor.storage_mut::<u8>();
199            crate::simd::bool_not_inplace_u8(storage);
200            return tensor;
201        }
202
203        // Allocating path for shared, non-contiguous, or offset tensors:
204        // preserve the input's bool dtype for the new tensor.
205        let out_dtype = burn_std::BoolDType::from(tensor.dtype());
206        let shape = tensor.layout().shape().clone();
207        let storage: &[u8] = tensor.bytes();
208
209        let result: Vec<u8> = match tensor.layout().contiguous_offsets() {
210            Some((start, end)) => {
211                let slice = &storage[start..end];
212                let mut out = vec![0u8; slice.len()];
213                crate::simd::bool_not_u8(slice, &mut out);
214                out
215            }
216            None => StridedIter::new(tensor.layout())
217                .map(|idx| (storage[idx] == 0) as u8)
218                .collect(),
219        };
220
221        crate::ops::comparison::make_bool_tensor(result, shape, out_dtype)
222    }
223
224    fn bool_and(lhs: BoolTensor<Flex>, rhs: BoolTensor<Flex>) -> BoolTensor<Flex> {
225        bool_binary_op_simd(lhs, rhs, BoolBinaryOp::And)
226    }
227
228    fn bool_or(lhs: BoolTensor<Flex>, rhs: BoolTensor<Flex>) -> BoolTensor<Flex> {
229        bool_binary_op_simd(lhs, rhs, BoolBinaryOp::Or)
230    }
231
232    fn bool_xor(lhs: BoolTensor<Flex>, rhs: BoolTensor<Flex>) -> BoolTensor<Flex> {
233        bool_binary_op_simd(lhs, rhs, BoolBinaryOp::Xor)
234    }
235
236    fn bool_expand(tensor: BoolTensor<Flex>, shape: Shape) -> BoolTensor<Flex> {
237        crate::ops::expand::expand(tensor, shape)
238    }
239
240    // Missing methods
241    fn bool_zeros(
242        shape: Shape,
243        device: &Device<Flex>,
244        dtype: burn_std::BoolDType,
245    ) -> BoolTensor<Flex> {
246        Self::bool_empty(shape, device, dtype)
247    }
248
249    fn bool_ones(
250        shape: Shape,
251        _device: &Device<Flex>,
252        dtype: burn_std::BoolDType,
253    ) -> BoolTensor<Flex> {
254        let num_elements = shape.num_elements();
255        let data = vec![1u8; num_elements];
256        crate::ops::comparison::make_bool_tensor(data, shape, dtype)
257    }
258
259    fn bool_mask_where(
260        tensor: BoolTensor<Flex>,
261        mask: BoolTensor<Flex>,
262        value: BoolTensor<Flex>,
263    ) -> BoolTensor<Flex> {
264        crate::ops::mask::mask_where_bool(tensor, mask, value)
265    }
266
267    fn bool_mask_fill(
268        tensor: BoolTensor<Flex>,
269        mask: BoolTensor<Flex>,
270        value: burn_backend::Scalar,
271    ) -> BoolTensor<Flex> {
272        let value: bool = value.elem();
273        crate::ops::mask::mask_fill_bool(tensor, mask, value)
274    }
275
276    fn bool_gather(
277        dim: usize,
278        tensor: BoolTensor<Flex>,
279        indices: IntTensor<Flex>,
280    ) -> BoolTensor<Flex> {
281        crate::ops::gather_scatter::gather_bool(tensor, dim, indices)
282    }
283
284    fn bool_scatter_or(
285        dim: usize,
286        tensor: BoolTensor<Flex>,
287        indices: IntTensor<Flex>,
288        value: BoolTensor<Flex>,
289    ) -> BoolTensor<Flex> {
290        crate::ops::gather_scatter::scatter_or(tensor, dim, indices, value)
291    }
292
293    fn bool_equal_elem(lhs: BoolTensor<Flex>, rhs: burn_backend::Scalar) -> BoolTensor<Flex> {
294        use crate::strided_index::StridedIter;
295
296        let out_dtype = burn_std::BoolDType::from(lhs.dtype());
297        let shape = lhs.layout().shape().clone();
298        let storage: &[u8] = lhs.bytes();
299        let rhs_bool: bool = rhs.elem();
300        let rhs_val = rhs_bool as u8;
301
302        let result: Vec<u8> = match lhs.layout().contiguous_offsets() {
303            Some((start, end)) => storage[start..end]
304                .iter()
305                .map(|&v| (v == rhs_val) as u8)
306                .collect(),
307            None => StridedIter::new(lhs.layout())
308                .map(|idx| (storage[idx] == rhs_val) as u8)
309                .collect(),
310        };
311
312        crate::ops::comparison::make_bool_tensor(result, shape, out_dtype)
313    }
314
315    fn bool_unfold(
316        tensor: BoolTensor<Flex>,
317        dim: usize,
318        size: usize,
319        step: usize,
320    ) -> BoolTensor<Flex> {
321        crate::ops::unfold::unfold_bool(tensor, dim, size, step)
322    }
323
324    fn bool_not_equal(lhs: BoolTensor<Flex>, rhs: BoolTensor<Flex>) -> BoolTensor<Flex> {
325        let out_dtype = burn_std::BoolDType::from(lhs.dtype());
326        crate::ops::comparison::bool_not_equal(lhs, rhs, out_dtype)
327    }
328
329    fn bool_not_equal_elem(lhs: BoolTensor<Flex>, rhs: burn_backend::Scalar) -> BoolTensor<Flex> {
330        let out_dtype = burn_std::BoolDType::from(lhs.dtype());
331        let rhs: bool = rhs.elem();
332        crate::ops::comparison::bool_not_equal_elem(lhs, rhs, out_dtype)
333    }
334
335    fn bool_any(tensor: BoolTensor<Flex>) -> BoolTensor<Flex> {
336        let out_dtype = burn_std::BoolDType::from(tensor.dtype());
337        crate::ops::comparison::any_bool(tensor, out_dtype)
338    }
339
340    fn bool_any_dim(tensor: BoolTensor<Flex>, dim: usize) -> BoolTensor<Flex> {
341        let out_dtype = burn_std::BoolDType::from(tensor.dtype());
342        crate::ops::comparison::any_bool_dim(tensor, dim, out_dtype)
343    }
344
345    fn bool_all(tensor: BoolTensor<Flex>) -> BoolTensor<Flex> {
346        let out_dtype = burn_std::BoolDType::from(tensor.dtype());
347        crate::ops::comparison::all_bool(tensor, out_dtype)
348    }
349
350    fn bool_all_dim(tensor: BoolTensor<Flex>, dim: usize) -> BoolTensor<Flex> {
351        let out_dtype = burn_std::BoolDType::from(tensor.dtype());
352        crate::ops::comparison::all_bool_dim(tensor, dim, out_dtype)
353    }
354
355    fn bool_select(
356        tensor: BoolTensor<Flex>,
357        dim: usize,
358        indices: IntTensor<Flex>,
359    ) -> BoolTensor<Flex> {
360        crate::ops::gather_scatter::select::<u8>(tensor, dim, indices)
361    }
362
363    fn bool_select_or(
364        tensor: BoolTensor<Flex>,
365        dim: usize,
366        indices: IntTensor<Flex>,
367        value: BoolTensor<Flex>,
368    ) -> BoolTensor<Flex> {
369        crate::ops::gather_scatter::select_or(tensor, dim, indices, value)
370    }
371
372    fn bool_transpose(tensor: BoolTensor<Flex>) -> BoolTensor<Flex> {
373        let ndims = tensor.layout().num_dims();
374        if ndims < 2 {
375            return tensor;
376        }
377        tensor.transpose(ndims - 2, ndims - 1)
378    }
379
380    fn bool_repeat_dim(tensor: BoolTensor<Flex>, dim: usize, times: usize) -> BoolTensor<Flex> {
381        crate::ops::repeat_dim::repeat_dim(tensor, dim, times)
382    }
383
384    async fn bool_argwhere(tensor: BoolTensor<Flex>, out_dtype: IntDType) -> IntTensor<Flex> {
385        let tensor = tensor.to_contiguous();
386        let shape = tensor.layout().shape().clone();
387        let ndims = shape.num_dims();
388        let data: &[u8] = tensor.storage();
389        let n = shape.num_elements();
390
391        let count = data[..n].iter().filter(|&&v| v != 0).count();
392        let mut coords: Vec<isize> = Vec::with_capacity(count * ndims);
393        let strides = crate::layout::contiguous_strides_usize(&shape);
394
395        for (flat_idx, &val) in data[..n].iter().enumerate() {
396            if val != 0 {
397                let mut remaining = flat_idx;
398                for &s in &strides {
399                    coords.push((remaining / s) as isize);
400                    remaining %= s;
401                }
402            }
403        }
404
405        let out_shape = Shape::from(vec![count, ndims]);
406        let result = FlexTensor::new(
407            Bytes::from_elems(coords),
408            Layout::contiguous(out_shape),
409            crate::ops::INDEX_DTYPE,
410        );
411        if result.dtype() != DType::from(out_dtype) {
412            Flex::int_cast(result, out_dtype)
413        } else {
414            result
415        }
416    }
417}
418
419/// Boolean binary operation type.
420#[derive(Clone, Copy)]
421enum BoolBinaryOp {
422    And,
423    Or,
424    Xor,
425}
426
427fn bool_binary_op_simd(lhs: FlexTensor, rhs: FlexTensor, op: BoolBinaryOp) -> FlexTensor {
428    use crate::strided_index::StridedIter;
429
430    debug_assert_eq!(lhs.dtype(), rhs.dtype(), "bool_binary_op: dtype mismatch");
431
432    // Broadcast to a common shape before dispatching. The scalar/SIMD helpers
433    // below assume equal-length operands; without this, mismatched shapes
434    // either silently keep the lhs shape or OOB-panic inside the helpers.
435    let (mut lhs, mut rhs) = crate::ops::expand::broadcast_binary(lhs, rhs);
436
437    // Preserve the input bool dtype (taken from lhs; rhs is assumed to match
438    // in dtype, checked above).
439    let out_dtype = burn_std::BoolDType::from(lhs.dtype());
440    let shape = lhs.layout().shape().clone();
441    let l_offsets = lhs.layout().contiguous_offsets();
442    let r_offsets = rhs.layout().contiguous_offsets();
443
444    // Fast path 1: lhs is unique and contiguous at offset 0 -> in-place on lhs
445    if lhs.is_unique()
446        && let (Some((0, l_end)), Some((r_start, r_end))) = (l_offsets, r_offsets)
447    {
448        let rhs_storage: &[u8] = rhs.bytes();
449        let r_slice = &rhs_storage[r_start..r_end];
450        let lhs_storage: &mut [u8] = lhs.storage_mut();
451        let l_slice = &mut lhs_storage[..l_end];
452
453        match op {
454            BoolBinaryOp::And => crate::simd::bool_and_inplace_u8(l_slice, r_slice),
455            BoolBinaryOp::Or => crate::simd::bool_or_inplace_u8(l_slice, r_slice),
456            BoolBinaryOp::Xor => crate::simd::bool_xor_inplace_u8(l_slice, r_slice),
457        }
458        return lhs;
459    }
460
461    // Fast path 2: rhs is unique and contiguous at offset 0 -> in-place on rhs
462    // (And/Or/Xor are commutative, so we can swap operands)
463    if rhs.is_unique()
464        && let (Some((l_start, l_end)), Some((0, r_end))) = (l_offsets, r_offsets)
465    {
466        let lhs_storage: &[u8] = lhs.bytes();
467        let l_slice = &lhs_storage[l_start..l_end];
468        let rhs_storage: &mut [u8] = rhs.storage_mut();
469        let r_slice = &mut rhs_storage[..r_end];
470
471        match op {
472            BoolBinaryOp::And => crate::simd::bool_and_inplace_u8(r_slice, l_slice),
473            BoolBinaryOp::Or => crate::simd::bool_or_inplace_u8(r_slice, l_slice),
474            BoolBinaryOp::Xor => crate::simd::bool_xor_inplace_u8(r_slice, l_slice),
475        }
476        return rhs;
477    }
478
479    // Allocating path: neither tensor is suitable for in-place
480    let lhs_storage: &[u8] = lhs.bytes();
481    let rhs_storage: &[u8] = rhs.bytes();
482
483    let result: Vec<u8> = match (l_offsets, r_offsets) {
484        (Some((l_start, l_end)), Some((r_start, r_end))) => {
485            let l_slice = &lhs_storage[l_start..l_end];
486            let r_slice = &rhs_storage[r_start..r_end];
487            let mut out = vec![0u8; l_slice.len()];
488            match op {
489                BoolBinaryOp::And => crate::simd::bool_and_u8(l_slice, r_slice, &mut out),
490                BoolBinaryOp::Or => crate::simd::bool_or_u8(l_slice, r_slice, &mut out),
491                BoolBinaryOp::Xor => crate::simd::bool_xor_u8(l_slice, r_slice, &mut out),
492            }
493            out
494        }
495        _ => {
496            // Strided/broadcast fallback: collapsed loop nest with an
497            // autovectorized bitwise inner loop. Separate zip_map calls
498            // per op keep the closures monomorphized. StridedIter only
499            // remains for layouts the nest can't handle (negative
500            // strides, rank > 8).
501            let zipped = match op {
502                BoolBinaryOp::And => crate::zip::zip_map(
503                    lhs_storage,
504                    lhs.layout(),
505                    rhs_storage,
506                    rhs.layout(),
507                    |a, b| a & b,
508                ),
509                BoolBinaryOp::Or => crate::zip::zip_map(
510                    lhs_storage,
511                    lhs.layout(),
512                    rhs_storage,
513                    rhs.layout(),
514                    |a, b| a | b,
515                ),
516                BoolBinaryOp::Xor => crate::zip::zip_map(
517                    lhs_storage,
518                    lhs.layout(),
519                    rhs_storage,
520                    rhs.layout(),
521                    |a, b| a ^ b,
522                ),
523            };
524            match zipped {
525                Some(result) => result,
526                None => {
527                    let lhs_iter = StridedIter::new(lhs.layout());
528                    let rhs_iter = StridedIter::new(rhs.layout());
529                    match op {
530                        BoolBinaryOp::And => lhs_iter
531                            .zip(rhs_iter)
532                            .map(|(li, ri)| lhs_storage[li] & rhs_storage[ri])
533                            .collect(),
534                        BoolBinaryOp::Or => lhs_iter
535                            .zip(rhs_iter)
536                            .map(|(li, ri)| lhs_storage[li] | rhs_storage[ri])
537                            .collect(),
538                        BoolBinaryOp::Xor => lhs_iter
539                            .zip(rhs_iter)
540                            .map(|(li, ri)| lhs_storage[li] ^ rhs_storage[ri])
541                            .collect(),
542                    }
543                }
544            }
545        }
546    };
547
548    crate::ops::comparison::make_bool_tensor(result, shape, out_dtype)
549}
550
551// Tests kept here exercise flex-specific dtype storage selection via
552// explicit IntDType/FloatDType. Plain bool ops, bool-to-int/float
553// casts, and negative-stride (flipped) bool coverage have been migrated
554// to crates/burn-backend-tests/tests/tensor/bool/ops/{logical,cast}.rs
555// so they run against every backend. When adding new tests, keep them
556// here only if they probe flex dtype dispatch; otherwise add them
557// there.
558#[cfg(test)]
559mod tests {
560    use alloc::vec;
561    use burn_backend::TensorData;
562    use burn_backend::ops::BoolTensorOps;
563    use burn_std::{FloatDType, IntDType};
564
565    use crate::{Flex, FlexTensor};
566
567    #[test]
568    fn test_bool_into_int_u8() {
569        let t = FlexTensor::from_data(TensorData::from([true, false, true]));
570        let result = Flex::bool_into_int(t, IntDType::U8);
571        assert_eq!(result.dtype(), burn_backend::DType::U8);
572        let data: Vec<u8> = result.into_data().try_into_vec().unwrap();
573        assert_eq!(data, vec![1u8, 0, 1]);
574    }
575
576    #[test]
577    fn test_bool_into_float_f64() {
578        let t = FlexTensor::from_data(TensorData::from([true, false, true]));
579        let result = Flex::bool_into_float(t, FloatDType::F64);
580        assert_eq!(result.dtype(), burn_backend::DType::F64);
581        let data: Vec<f64> = result.into_data().try_into_vec().unwrap();
582        assert_eq!(data, vec![1.0f64, 0.0, 1.0]);
583    }
584}