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        let mut result = crate::ops::gather_scatter::select_add::<u8>(tensor, dim, indices, value);
370        // Clamp to 0/1: select_add sums u8 values, but bool OR saturates at 1
371        let storage: &mut [u8] = result.storage_mut();
372        for v in storage.iter_mut() {
373            if *v > 1 {
374                *v = 1;
375            }
376        }
377        result
378    }
379
380    fn bool_transpose(tensor: BoolTensor<Flex>) -> BoolTensor<Flex> {
381        let ndims = tensor.layout().num_dims();
382        if ndims < 2 {
383            return tensor;
384        }
385        tensor.transpose(ndims - 2, ndims - 1)
386    }
387
388    fn bool_repeat_dim(tensor: BoolTensor<Flex>, dim: usize, times: usize) -> BoolTensor<Flex> {
389        crate::ops::repeat_dim::repeat_dim(tensor, dim, times)
390    }
391
392    async fn bool_argwhere(tensor: BoolTensor<Flex>, out_dtype: IntDType) -> IntTensor<Flex> {
393        let tensor = tensor.to_contiguous();
394        let shape = tensor.layout().shape().clone();
395        let ndims = shape.num_dims();
396        let data: &[u8] = tensor.storage();
397        let n = shape.num_elements();
398
399        let count = data[..n].iter().filter(|&&v| v != 0).count();
400        let mut coords: Vec<isize> = Vec::with_capacity(count * ndims);
401        let strides = crate::layout::contiguous_strides_usize(&shape);
402
403        for (flat_idx, &val) in data[..n].iter().enumerate() {
404            if val != 0 {
405                let mut remaining = flat_idx;
406                for &s in &strides {
407                    coords.push((remaining / s) as isize);
408                    remaining %= s;
409                }
410            }
411        }
412
413        let out_shape = Shape::from(vec![count, ndims]);
414        let result = FlexTensor::new(
415            Bytes::from_elems(coords),
416            Layout::contiguous(out_shape),
417            crate::ops::INDEX_DTYPE,
418        );
419        if result.dtype() != DType::from(out_dtype) {
420            Flex::int_cast(result, out_dtype)
421        } else {
422            result
423        }
424    }
425}
426
427/// Boolean binary operation type.
428#[derive(Clone, Copy)]
429enum BoolBinaryOp {
430    And,
431    Or,
432    Xor,
433}
434
435fn bool_binary_op_simd(lhs: FlexTensor, rhs: FlexTensor, op: BoolBinaryOp) -> FlexTensor {
436    use crate::strided_index::StridedIter;
437
438    debug_assert_eq!(lhs.dtype(), rhs.dtype(), "bool_binary_op: dtype mismatch");
439
440    // Broadcast to a common shape before dispatching. The scalar/SIMD helpers
441    // below assume equal-length operands; without this, mismatched shapes
442    // either silently keep the lhs shape or OOB-panic inside the helpers.
443    let (mut lhs, mut rhs) = crate::ops::expand::broadcast_binary(lhs, rhs);
444
445    // Preserve the input bool dtype (taken from lhs; rhs is assumed to match
446    // in dtype, checked above).
447    let out_dtype = burn_std::BoolDType::from(lhs.dtype());
448    let shape = lhs.layout().shape().clone();
449    let l_offsets = lhs.layout().contiguous_offsets();
450    let r_offsets = rhs.layout().contiguous_offsets();
451
452    // Fast path 1: lhs is unique and contiguous at offset 0 -> in-place on lhs
453    if lhs.is_unique()
454        && let (Some((0, l_end)), Some((r_start, r_end))) = (l_offsets, r_offsets)
455    {
456        let rhs_storage: &[u8] = rhs.bytes();
457        let r_slice = &rhs_storage[r_start..r_end];
458        let lhs_storage: &mut [u8] = lhs.storage_mut();
459        let l_slice = &mut lhs_storage[..l_end];
460
461        match op {
462            BoolBinaryOp::And => crate::simd::bool_and_inplace_u8(l_slice, r_slice),
463            BoolBinaryOp::Or => crate::simd::bool_or_inplace_u8(l_slice, r_slice),
464            BoolBinaryOp::Xor => crate::simd::bool_xor_inplace_u8(l_slice, r_slice),
465        }
466        return lhs;
467    }
468
469    // Fast path 2: rhs is unique and contiguous at offset 0 -> in-place on rhs
470    // (And/Or/Xor are commutative, so we can swap operands)
471    if rhs.is_unique()
472        && let (Some((l_start, l_end)), Some((0, r_end))) = (l_offsets, r_offsets)
473    {
474        let lhs_storage: &[u8] = lhs.bytes();
475        let l_slice = &lhs_storage[l_start..l_end];
476        let rhs_storage: &mut [u8] = rhs.storage_mut();
477        let r_slice = &mut rhs_storage[..r_end];
478
479        match op {
480            BoolBinaryOp::And => crate::simd::bool_and_inplace_u8(r_slice, l_slice),
481            BoolBinaryOp::Or => crate::simd::bool_or_inplace_u8(r_slice, l_slice),
482            BoolBinaryOp::Xor => crate::simd::bool_xor_inplace_u8(r_slice, l_slice),
483        }
484        return rhs;
485    }
486
487    // Allocating path: neither tensor is suitable for in-place
488    let lhs_storage: &[u8] = lhs.bytes();
489    let rhs_storage: &[u8] = rhs.bytes();
490
491    let result: Vec<u8> = match (l_offsets, r_offsets) {
492        (Some((l_start, l_end)), Some((r_start, r_end))) => {
493            let l_slice = &lhs_storage[l_start..l_end];
494            let r_slice = &rhs_storage[r_start..r_end];
495            let mut out = vec![0u8; l_slice.len()];
496            match op {
497                BoolBinaryOp::And => crate::simd::bool_and_u8(l_slice, r_slice, &mut out),
498                BoolBinaryOp::Or => crate::simd::bool_or_u8(l_slice, r_slice, &mut out),
499                BoolBinaryOp::Xor => crate::simd::bool_xor_u8(l_slice, r_slice, &mut out),
500            }
501            out
502        }
503        _ => {
504            let lhs_iter = StridedIter::new(lhs.layout());
505            let rhs_iter = StridedIter::new(rhs.layout());
506            match op {
507                BoolBinaryOp::And => lhs_iter
508                    .zip(rhs_iter)
509                    .map(|(li, ri)| lhs_storage[li] & rhs_storage[ri])
510                    .collect(),
511                BoolBinaryOp::Or => lhs_iter
512                    .zip(rhs_iter)
513                    .map(|(li, ri)| lhs_storage[li] | rhs_storage[ri])
514                    .collect(),
515                BoolBinaryOp::Xor => lhs_iter
516                    .zip(rhs_iter)
517                    .map(|(li, ri)| lhs_storage[li] ^ rhs_storage[ri])
518                    .collect(),
519            }
520        }
521    };
522
523    crate::ops::comparison::make_bool_tensor(result, shape, out_dtype)
524}
525
526// Tests kept here exercise flex-specific dtype storage selection via
527// explicit IntDType/FloatDType. Plain bool ops, bool-to-int/float
528// casts, and negative-stride (flipped) bool coverage have been migrated
529// to crates/burn-backend-tests/tests/tensor/bool/ops/{logical,cast}.rs
530// so they run against every backend. When adding new tests, keep them
531// here only if they probe flex dtype dispatch; otherwise add them
532// there.
533#[cfg(test)]
534mod tests {
535    use alloc::vec;
536    use burn_backend::TensorData;
537    use burn_backend::ops::BoolTensorOps;
538    use burn_std::{FloatDType, IntDType};
539
540    use crate::{Flex, FlexTensor};
541
542    #[test]
543    fn test_bool_into_int_u8() {
544        let t = FlexTensor::from_data(TensorData::from([true, false, true]));
545        let result = Flex::bool_into_int(t, IntDType::U8);
546        assert_eq!(result.dtype(), burn_backend::DType::U8);
547        let data: Vec<u8> = result.into_data().to_vec().unwrap();
548        assert_eq!(data, vec![1u8, 0, 1]);
549    }
550
551    #[test]
552    fn test_bool_into_float_f64() {
553        let t = FlexTensor::from_data(TensorData::from([true, false, true]));
554        let result = Flex::bool_into_float(t, FloatDType::F64);
555        assert_eq!(result.dtype(), burn_backend::DType::F64);
556        let data: Vec<f64> = result.into_data().to_vec().unwrap();
557        assert_eq!(data, vec![1.0f64, 0.0, 1.0]);
558    }
559}