Skip to content

Commit 9eb20ba

Browse files
chore(array): use fearless_simd for AVX2 1- and 2-byte filter compress (#10324)
Signed-off-by: Claude <noreply@anthropic.com>
1 parent 5d5f429 commit 9eb20ba

6 files changed

Lines changed: 146 additions & 139 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vortex-array/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ bytes = { workspace = true }
3131
cfg-if = { workspace = true }
3232
cudarc = { workspace = true, optional = true }
3333
enum-iterator = { workspace = true }
34+
fearless_simd = { workspace = true }
35+
fearless_simd_macros = { workspace = true }
3436
flatbuffers = { workspace = true }
3537
futures = { workspace = true, features = ["alloc", "async-await", "std"] }
3638
goldenfile = { workspace = true, optional = true }
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Portable `fearless_simd` compress kernels for 1- and 2-byte elements.
5+
//!
6+
//! Each sub-word of 8 lanes is compacted with one 16-byte `swizzle_dyn` driven by a byte-index
7+
//! lookup table. Dispatch wraps the whole mask walk, so the per-word body inlines into one
8+
//! target-feature function rather than being called once per mask word.
9+
10+
use std::ptr;
11+
12+
use fearless_simd::Level;
13+
use fearless_simd::Simd;
14+
use fearless_simd::dispatch;
15+
use fearless_simd::prelude::*;
16+
use fearless_simd::u8x16;
17+
use fearless_simd_macros::simd;
18+
use vortex_mask::MaskValues;
19+
20+
use super::super::slice::for_each_mask_word;
21+
use super::super::slice::low_bits_mask;
22+
use super::bulk_copy;
23+
use super::compress_lut;
24+
use super::compress_tail;
25+
26+
static IDX_LUT_8: [[u8; 16]; 256] = compress_lut::<256, 16>(8, 1);
27+
static IDX_LUT_16: [[u8; 16]; 256] = compress_lut::<256, 16>(8, 2);
28+
29+
/// Compact one mask word of `ELEM`-byte elements, `LANES` elements per shuffle.
30+
///
31+
/// # Safety
32+
///
33+
/// The pointer contract of [`filter_slice_by_bitmap`](super::filter_slice_by_bitmap) /
34+
/// [`filter_slice_mut_by_bitmap`](super::filter_slice_mut_by_bitmap) must hold.
35+
#[expect(
36+
clippy::cast_possible_truncation,
37+
reason = "deliberate submask narrowing"
38+
)]
39+
#[allow(clippy::too_many_arguments)]
40+
#[simd]
41+
unsafe fn compress_word<S: Simd, const IN_PLACE: bool, const ELEM: usize, const LANES: usize>(
42+
simd: S,
43+
idx_lut: &[[u8; 16]],
44+
src: *const u8,
45+
dst: *mut u8,
46+
word: u64,
47+
word_start: usize,
48+
word_len: usize,
49+
mut write_pos: usize,
50+
) -> usize {
51+
if word == 0 {
52+
return write_pos;
53+
}
54+
if word == low_bits_mask(word_len) {
55+
// SAFETY: forwarded from the caller contract.
56+
unsafe { bulk_copy::<IN_PLACE>(src, dst, word_start, word_len, write_pos, ELEM) };
57+
return write_pos + word_len;
58+
}
59+
60+
// Empty chunks still store garbage that the next chunk overwrites; branching here
61+
// regresses masks near the density crossover.
62+
let mut sub = 0;
63+
while sub + LANES <= word_len {
64+
let m = ((word >> sub) & low_bits_mask(LANES)) as usize;
65+
// SAFETY: the chunk holds `LANES` in-bounds source elements.
66+
let chunk_ptr = unsafe { src.add((word_start + sub) * ELEM) };
67+
// Materializing the bytes ends the source read before an overlapping in-place store.
68+
let bytes: [u8; 16] = if ELEM * LANES == 16 {
69+
// SAFETY: see above; the chunk is exactly 16 bytes.
70+
unsafe { chunk_ptr.cast::<[u8; 16]>().read_unaligned() }
71+
} else {
72+
// SAFETY: see above; the chunk is exactly 8 bytes.
73+
let half = unsafe { chunk_ptr.cast::<[u8; 8]>().read_unaligned() };
74+
let mut bytes = [0u8; 16];
75+
bytes[..8].copy_from_slice(&half);
76+
bytes
77+
};
78+
let packed = u8x16::from_slice(simd, &bytes)
79+
.swizzle_dyn(u8x16::from_slice(simd, &idx_lut[m]))
80+
.to_array();
81+
// SAFETY: out-of-place output has vector slack. In-place, the store ends within the
82+
// source chunk already loaded, and later stores overwrite trailing garbage.
83+
unsafe {
84+
ptr::copy_nonoverlapping(packed.as_ptr(), dst.add(write_pos * ELEM), ELEM * LANES)
85+
};
86+
write_pos += m.count_ones() as usize;
87+
sub += LANES;
88+
}
89+
90+
if sub < word_len {
91+
let bits = (word >> sub) & low_bits_mask(word_len - sub);
92+
// SAFETY: forwarded from the caller contract.
93+
write_pos =
94+
unsafe { compress_tail::<IN_PLACE>(src, dst, bits, word_start + sub, write_pos, ELEM) };
95+
}
96+
97+
write_pos
98+
}
99+
100+
/// Generate a mask-walking entry point for one element width.
101+
macro_rules! generic_compress_kernel {
102+
($walk_fn:ident,elem_size: $elem_size:literal,lanes: $lanes:literal,idx_lut: $idx_lut:ident) => {
103+
/// # Safety
104+
///
105+
/// The pointer contract of [`filter_slice_by_bitmap`](super::filter_slice_by_bitmap) /
106+
/// [`filter_slice_mut_by_bitmap`](super::filter_slice_mut_by_bitmap) must hold.
107+
pub(super) unsafe fn $walk_fn<const IN_PLACE: bool>(
108+
src: *const u8,
109+
dst: *mut u8,
110+
mask: &MaskValues,
111+
) -> usize {
112+
dispatch!(Level::new(), simd => {
113+
let mut write_pos = 0;
114+
for_each_mask_word(mask, |word, word_start, word_len| {
115+
// SAFETY: forwarded from the caller contract.
116+
write_pos = unsafe {
117+
compress_word::<_, IN_PLACE, $elem_size, $lanes>(
118+
simd, &$idx_lut, src, dst, word, word_start, word_len, write_pos,
119+
)
120+
};
121+
});
122+
write_pos
123+
})
124+
}
125+
};
126+
}
127+
128+
generic_compress_kernel!(compress_generic_8, elem_size: 1, lanes: 8, idx_lut: IDX_LUT_8);
129+
generic_compress_kernel!(compress_generic_16, elem_size: 2, lanes: 8, idx_lut: IDX_LUT_16);

‎vortex-array/src/arrays/filter/execute/simd_compress/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ use vortex_buffer::BufferAllocatorRef;
3131
use vortex_buffer::BufferMut;
3232
use vortex_mask::MaskValues;
3333

34+
#[cfg(all(target_arch = "x86_64", not(miri)))]
35+
mod generic;
3436
#[cfg(all(target_arch = "aarch64", not(miri)))]
3537
mod neon;
3638
#[cfg(test)]

‎vortex-array/src/arrays/filter/execute/simd_compress/tests.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ fn declines_sparse_and_short_masks() {
105105
assert!(filter_slice_by_bitmap(&values[..32], short).is_none());
106106
}
107107

108-
// AVX-512 machines need direct coverage of the otherwise-unselected AVX2 tier.
108+
// AVX-512 machines need direct coverage of the otherwise-unselected AVX2-tier kernels.
109109
#[cfg(all(target_arch = "x86_64", not(miri)))]
110110
#[test]
111111
fn avx2_kernels_match_scalar() {
@@ -152,14 +152,14 @@ fn avx2_kernels_match_scalar() {
152152
let u32_values: Vec<u32> = (0..len as u32).collect();
153153
let u64_values: Vec<u64> = (0..len as u64).collect();
154154
check_kernel(
155-
x86::compress_pshufb_epi8::<false>,
156-
x86::compress_pshufb_epi8::<true>,
155+
generic::compress_generic_8::<false>,
156+
generic::compress_generic_8::<true>,
157157
&u8_values,
158158
mask,
159159
);
160160
check_kernel(
161-
x86::compress_pshufb_epi16::<false>,
162-
x86::compress_pshufb_epi16::<true>,
161+
generic::compress_generic_16::<false>,
162+
generic::compress_generic_16::<true>,
163163
&u16_values,
164164
mask,
165165
);

‎vortex-array/src/arrays/filter/execute/simd_compress/x86.rs‎

Lines changed: 6 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4-
//! AVX-512 `vpcompress`, AVX2 `vpermd`, and 128-bit `pshufb` compress kernels.
4+
//! AVX-512 `vpcompress` and AVX2 `vpermd` compress kernels. The 1- and 2-byte AVX2 paths use the
5+
//! portable kernels in [`generic`](super::generic).
56
//!
67
//! See the [module docs](super) for how these fit the shared dispatch.
78
8-
use std::arch::x86_64::__m128i;
9-
use std::arch::x86_64::_mm_loadl_epi64;
10-
use std::arch::x86_64::_mm_loadu_si128;
11-
use std::arch::x86_64::_mm_shuffle_epi8;
12-
use std::arch::x86_64::_mm_storel_epi64;
13-
use std::arch::x86_64::_mm_storeu_si128;
149
use std::arch::x86_64::_mm256_loadu_si256;
1510
use std::arch::x86_64::_mm256_maskload_epi32;
1611
use std::arch::x86_64::_mm256_maskload_epi64;
@@ -45,8 +40,8 @@ use super::super::slice::for_each_mask_word;
4540
use super::super::slice::low_bits_mask;
4641
use super::Kernel;
4742
use super::bulk_copy;
48-
use super::compress_lut;
49-
use super::compress_tail;
43+
use super::generic::compress_generic_8;
44+
use super::generic::compress_generic_16;
5045

5146
/// Choose the widest available kernel above its benchmarked density crossover.
5247
///
@@ -59,8 +54,8 @@ pub(super) fn select_kernel<T, const IN_PLACE: bool>(mask: &MaskValues) -> Optio
5954
4 if avx512f() => (compress_avx512_epi32::<IN_PLACE> as Kernel, 0.25),
6055
8 if avx512f() => (compress_avx512_epi64::<IN_PLACE> as Kernel, 0.30),
6156
// AVX-512F without VBMI2 (e.g. Skylake-X) falls through to these too.
62-
1 if avx2() => (compress_pshufb_epi8::<IN_PLACE> as Kernel, 0.15),
63-
2 if avx2() => (compress_pshufb_epi16::<IN_PLACE> as Kernel, 0.25),
57+
1 if avx2() => (compress_generic_8::<IN_PLACE> as Kernel, 0.15),
58+
2 if avx2() => (compress_generic_16::<IN_PLACE> as Kernel, 0.25),
6459
4 if avx2() => (compress_avx2_epi32::<IN_PLACE> as Kernel, 0.25),
6560
8 if avx2() => (compress_avx2_epi64::<IN_PLACE> as Kernel, 0.45),
6661
_ => return None,
@@ -451,126 +446,3 @@ avx2_compress_kernel!(
451446
maskload: _mm256_maskload_epi64,
452447
maskstore: _mm256_maskstore_epi64
453448
);
454-
455-
/// Byte-index rows for `pshufb`, which always indexes a full 16-byte register even though only
456-
/// the low 8 (1-byte elements) or all 16 (2-byte elements) bytes hold lanes.
457-
static SHUF_LUT_8: [[u8; 16]; 256] = compress_lut::<256, 16>(8, 1);
458-
static SHUF_LUT_16: [[u8; 16]; 256] = compress_lut::<256, 16>(8, 2);
459-
460-
/// Generate an AVX2 `pshufb` kernel for 1- or 2-byte elements.
461-
macro_rules! pshufb_compress_kernel {
462-
(
463-
$word_fn:ident,
464-
$walk_fn:ident,elem_size:
465-
$elem_size:literal,idx_lut:
466-
$idx_lut:ident,load:
467-
$load:ident,store:
468-
$store:ident
469-
) => {
470-
/// # Safety
471-
///
472-
/// The CPU must support AVX2 and the pointer contract of
473-
/// [`filter_slice_by_bitmap`](super::filter_slice_by_bitmap) /
474-
/// [`filter_slice_mut_by_bitmap`](super::filter_slice_mut_by_bitmap) must hold.
475-
#[expect(
476-
clippy::cast_possible_truncation,
477-
reason = "deliberate submask narrowing"
478-
)]
479-
#[target_feature(enable = "avx2")]
480-
#[inline]
481-
unsafe fn $word_fn<const IN_PLACE: bool>(
482-
src: *const u8,
483-
dst: *mut u8,
484-
word: u64,
485-
word_start: usize,
486-
word_len: usize,
487-
mut write_pos: usize,
488-
) -> usize {
489-
if word == 0 {
490-
return write_pos;
491-
}
492-
if word == low_bits_mask(word_len) {
493-
// SAFETY: forwarded from the caller contract.
494-
unsafe {
495-
bulk_copy::<IN_PLACE>(src, dst, word_start, word_len, write_pos, $elem_size)
496-
};
497-
return write_pos + word_len;
498-
}
499-
500-
// Empty chunks still store garbage that the next chunk overwrites; branching here
501-
// regresses masks near the density crossover.
502-
let mut sub = 0;
503-
while sub + 8 <= word_len {
504-
let m = ((word >> sub) & low_bits_mask(8)) as usize;
505-
// SAFETY: the chunk holds 8 in-bounds source elements.
506-
let chunk = unsafe { $load(src.add((word_start + sub) * $elem_size).cast()) };
507-
// SAFETY: every LUT row is 16 bytes.
508-
let idx = unsafe { _mm_loadu_si128($idx_lut[m].as_ptr().cast()) };
509-
// SAFETY: out-of-place output has vector slack. In-place, the store ends within
510-
// the source chunk already loaded, and later stores overwrite trailing garbage.
511-
unsafe {
512-
$store(
513-
dst.add(write_pos * $elem_size).cast::<__m128i>(),
514-
_mm_shuffle_epi8(chunk, idx),
515-
)
516-
};
517-
write_pos += m.count_ones() as usize;
518-
sub += 8;
519-
}
520-
521-
if sub < word_len {
522-
let bits = (word >> sub) & low_bits_mask(word_len - sub);
523-
// SAFETY: forwarded from the caller contract.
524-
write_pos = unsafe {
525-
compress_tail::<IN_PLACE>(
526-
src,
527-
dst,
528-
bits,
529-
word_start + sub,
530-
write_pos,
531-
$elem_size,
532-
)
533-
};
534-
}
535-
536-
write_pos
537-
}
538-
539-
/// # Safety
540-
///
541-
/// The CPU must support AVX2 and the pointer contract of
542-
/// [`filter_slice_by_bitmap`](super::filter_slice_by_bitmap) /
543-
/// [`filter_slice_mut_by_bitmap`](super::filter_slice_mut_by_bitmap) must hold.
544-
#[target_feature(enable = "avx2")]
545-
pub(super) unsafe fn $walk_fn<const IN_PLACE: bool>(
546-
src: *const u8,
547-
dst: *mut u8,
548-
mask: &MaskValues,
549-
) -> usize {
550-
let mut write_pos = 0;
551-
for_each_mask_word(mask, |word, word_start, word_len| {
552-
// SAFETY: forwarded from the caller contract.
553-
write_pos = unsafe {
554-
$word_fn::<IN_PLACE>(src, dst, word, word_start, word_len, write_pos)
555-
};
556-
});
557-
write_pos
558-
}
559-
};
560-
}
561-
562-
pshufb_compress_kernel!(
563-
compress_word_pshufb_epi8, compress_pshufb_epi8,
564-
elem_size: 1,
565-
idx_lut: SHUF_LUT_8,
566-
load: _mm_loadl_epi64,
567-
store: _mm_storel_epi64
568-
);
569-
570-
pshufb_compress_kernel!(
571-
compress_word_pshufb_epi16, compress_pshufb_epi16,
572-
elem_size: 2,
573-
idx_lut: SHUF_LUT_16,
574-
load: _mm_loadu_si128,
575-
store: _mm_storeu_si128
576-
);

0 commit comments

Comments
 (0)