fix: validate unsigned AVX2 take indices - #9036
Conversation
|
I wonder if we its better to just always mask it out instead of checking for better perf? do we even allow take indices larger than |
4460fad to
688f070
Compare
|
so I asked codex to do another pass and it found this: DetailsThe unsafe audit is not clean: I found one remaining UB path. P1: incorrect mask packing for u64 indices and 32-bit valuesAt vortex-array/src/arrays/primitive/compute/take/avx2.rs:367, each 64-bit comparison mask is represented as two identical [m0, m0, m1, m1, m2, m2, m3, m3] But _mm_shuffle_epi32::<0x55> selects dword 1 four times. After unpacking, the gather mask becomes: [m0, m0, m2, m2] It should be: [m0, m1, m2, m3] _mm256_mask_i64gather_epi32 pairs four i64 offsets with four i32 mask lanes, as confirmed by the official Rust The likely correction is 0b11_01_11_01 (0xDD), which selects dwords 1 and 3 from each half. We should add a direct mask- Other unsafe boundaries
The index-slice transmute, unaligned loads/stores, deferred set_len, and signed gather offsets are otherwise sound once the I guess I need to fix these too? |
688f070 to
58f87d4
Compare
|
im going to try this on my machine at home that actually has avx2 and make sure everything here is correct |
0696e95 to
f27ef98
Compare
|
Follow-up on reducing the validity mask once after the AVX2 loop. (This is an implementation detail for the new changes, not a comparison to the current buggy develop. I will make a comment with that comparison soon) Local benchmark on an AMD Ryzen 9 7950X, pinned to CPU 0. Each run used 200 Divan samples; values below are the median of 3 runs.
Optimized LLVM IR diff (abridged; SSA names normalized)- %all_valid = phi i32 [ -1, %entry ], [ %next_bits, %loop ]
+ %all_valid = phi <4 x i64> [ splat (i64 -1), %entry ], [ %next_mask, %loop ]
%lane_mask = sext <8 x i1> %valid to <8 x i32>
- %mask_bytes = bitcast <8 x i32> %lane_mask to <32 x i8>
- %sign_bits = icmp slt <32 x i8> %mask_bytes, zeroinitializer
- %mask_bits = bitcast <32 x i1> %sign_bits to i32
- %next_bits = and i32 %all_valid, %mask_bits
+ %mask_vec = bitcast <8 x i32> %lane_mask to <4 x i64>
+ %next_mask = and <4 x i64> %all_valid, %mask_vec
+ ; Reduced once after the loop.
+ %mask_bytes = bitcast <4 x i64> %all_valid.lcssa to <32 x i8>
+ %invalid = icmp sgt <32 x i8> %mask_bytes, splat (i8 -1)
+ %mask_bits = bitcast <32 x i1> %invalid to i32x86-64 assembly diff (u32/u32 hot loop)- vpcmpgtd ymm3, ymm0, ymm3
- vpmovmskb r9d, ymm3
- and r9d, ecx
+ vpcmpgtd ymm4, ymm0, ymm4
+ vpand ymm1, ymm1, ymm4
vpgatherdd ymm5, dword ptr [r13 + 4*ymm3], ymm4
- vpcmpgtd ymm3, ymm0, ymm3
- vpmovmskb ecx, ymm3
- and ecx, r9d
+ vpcmpgtd ymm4, ymm0, ymm4
+ vpand ymm1, ymm1, ymm4
vpgatherdd ymm5, dword ptr [r13 + 4*ymm3], ymm4
jne .Lloop
+ vpmovmskb ecx, ymm1
cmp ecx, -1 |
|
Comparison against the actual buggy develop base ( Local benchmark on an AMD Ryzen 9 7950X, pinned to CPU 0: 1M
Paired deltas ranged from 2.1% faster to 1.0% slower, so this is effectively performance-neutral. The correctness fix is not itself a performance optimization; accumulating the validity mask in SIMD keeps the added safety check from causing a measurable regression. Optimized LLVM IR diff (u32/u32 hot loop, abridged; SSA names normalized)- ; Signed inclusive comparison (incorrect for high-bit u32 and index == len).
- %valid = icmp sle <8 x i32> %indices, %len_splat
+ ; Sign-bit bias maps unsigned ordering to AVX2's signed comparison.
+ %biased_indices = xor <8 x i32> %indices, splat (i32 -2147483648)
+ %valid = icmp sgt <8 x i32> %biased_len_splat, %biased_indices
%mask = sext <8 x i1> %valid to <8 x i32>
+ %mask64 = bitcast <8 x i32> %mask to <4 x i64>
%values = tail call <8 x i32> @llvm.x86.avx2.gather.d.d.256(
<8 x i32> zeroinitializer, ptr %src, <8 x i32> %indices,
<8 x i32> %mask, i8 4)
store <8 x i32> %values, ptr %dst
+ %all_valid.next = and <4 x i64> %all_valid, %mask64
+ ; Reduced once after the SIMD loop.
+ %mask_bytes = bitcast <4 x i64> %all_valid.lcssa to <32 x i8>
+ %nonnegative = icmp sgt <32 x i8> %mask_bytes, splat (i8 -1)
+ %invalid_bits = bitcast <32 x i1> %nonnegative to i32
+ %all_valid = icmp eq i32 %invalid_bits, 0
+ br i1 %all_valid, label %remainder, label %panicx86-64 assembly diff (u32/u32 hot loop, abridged; registers normalized) vmovdqu ymm_indices, [indices]
- vpcmpgtd ymm_mask, ymm_indices, ymm_len
- vpxor ymm_mask, ymm_mask, ymm_all_ones
+ vpxor ymm_biased_indices, ymm_indices, ymm_sign_bit
+ vpcmpgtd ymm_mask, ymm_biased_len, ymm_biased_indices
+ vpand ymm_all_valid, ymm_all_valid, ymm_mask
vpgatherdd ymm_values, [values + 4*ymm_indices], ymm_mask
vmovdqu [output], ymm_values
jne .Lloop
+ vpmovmskb ecx, ymm_all_valid
+ cmp ecx, -1
+ jne .LpanicThe loop is unrolled twice, so the Codegen command: |
Merging this PR will improve performance by 26.41%
Performance Changes
Tip Curious why this is faster? Comment Comparing Footnotes
|
| #[doc(hidden)] | ||
| unsafe fn take_avx2<V: Copy, I: UnsignedPType>(buffer: &[V], indices: &[I]) -> Buffer<V> { | ||
| if buffer.is_empty() { | ||
| return Buffer::zeroed(indices.len()); |
There was a problem hiding this comment.
this was somewhat questionable in the first place, I don't think this behavior is what we want?
Use unsigned SIMD comparisons and masked gathers for bounds safety, retain scalar fallbacks for unsupported address ranges, and reduce validity masks once after the hot loop. Add regression coverage and a focused primitive-take benchmark. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
f27ef98 to
38430b8
Compare
robert3005
left a comment
There was a problem hiding this comment.
I'm no expert on UB but this look correct now
Summary
This is actually not as large of a change as it looks, and nothing changes in the hot path (there's just a bit more validation). The rest of the diff comes from the regression tests.