Skip to main content

csm_core_lib/
bundle_simd.rs

1#![allow(clippy::needless_range_loop)]
2//! SIMD-optimized operations for BundleAccumulator.
3
4/// AVX2-optimized bit-packing for bundle finalize.
5#[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
6#[inline]
7#[target_feature(enable = "avx2")]
8/// # SAFETY
9/// Caller must ensure AVX2 is supported.
10pub(crate) unsafe fn finalize_simd_avx2(counts: &[i32; 10240], threshold: i32) -> [u128; 80] {
11    use std::arch::x86_64::{
12        _mm256_castsi256_ps, _mm256_cmpgt_epi32, _mm256_loadu_si256, _mm256_movemask_ps,
13        _mm256_set1_epi32,
14    };
15    let mut data = [0u128; 80];
16    let threshold_vec = _mm256_set1_epi32(threshold);
17
18    for i in 0..80 {
19        let offset = i * 128;
20        let mut word_low = 0u64;
21        let mut word_high = 0u64;
22        for j in 0..8 {
23            // SAFETY: counts is [i32; 10240]. offset + j * 8 + 8 is within bounds.
24            let packed = unsafe {
25                let ptr = counts.as_ptr().add(offset + j * 8);
26                let chunk = _mm256_loadu_si256(ptr.cast());
27                let mask = _mm256_cmpgt_epi32(chunk, threshold_vec);
28                _mm256_movemask_ps(_mm256_castsi256_ps(mask)) as u64
29            };
30            word_low |= packed << (j * 8);
31        }
32        for j in 0..8 {
33            // SAFETY: offset + 64 + j * 8 + 8 is within bounds.
34            let packed = unsafe {
35                let ptr = counts.as_ptr().add(offset + 64 + j * 8);
36                let chunk = _mm256_loadu_si256(ptr.cast());
37                let mask = _mm256_cmpgt_epi32(chunk, threshold_vec);
38                _mm256_movemask_ps(_mm256_castsi256_ps(mask)) as u64
39            };
40            word_high |= packed << (j * 8);
41        }
42        data[i] = (word_low as u128) | ((word_high as u128) << 64);
43    }
44    data
45}
46
47/// ARM NEON-optimized bit-packing for bundle finalize.
48#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
49#[inline]
50#[target_feature(enable = "neon")]
51/// # SAFETY
52/// Caller must ensure NEON is supported.
53pub(crate) unsafe fn finalize_simd_neon(counts: &[i32; 10240], threshold: i32) -> [u128; 80] {
54    use std::arch::aarch64::{vaddvq_u32, vandq_u32, vcgtq_s32, vdupq_n_s32, vld1q_s32};
55    let mut data = [0u128; 80];
56    // SAFETY: weights is [u32; 4], which is 16 bytes. vld1q_u32 loads 16 bytes.
57    let weights = unsafe {
58        let w = [1u32, 2, 4, 8];
59        std::arch::aarch64::vld1q_u32(w.as_ptr())
60    };
61
62    for i in 0..80 {
63        let offset = i * 128;
64        let mut word_low = 0u64;
65        let mut word_high = 0u64;
66        for j in 0..16 {
67            // SAFETY: counts is [i32; 10240]. offset + j * 4 + 4 is within bounds.
68            let packed = unsafe {
69                let ptr = counts.as_ptr().add(offset + j * 4);
70                let chunk = vld1q_s32(ptr);
71                let mask = vcgtq_s32(chunk, vdupq_n_s32(threshold));
72                let weighted = vandq_u32(mask, weights);
73                vaddvq_u32(weighted) as u64
74            };
75            word_low |= packed << (j * 4);
76        }
77        for j in 0..16 {
78            // SAFETY: offset + 64 + j * 4 + 4 is within bounds.
79            let packed = unsafe {
80                let ptr = counts.as_ptr().add(offset + 64 + j * 4);
81                let chunk = vld1q_s32(ptr);
82                let mask = vcgtq_s32(chunk, vdupq_n_s32(threshold));
83                let weighted = vandq_u32(mask, weights);
84                vaddvq_u32(weighted) as u64
85            };
86            word_high |= packed << (j * 4);
87        }
88        data[i] = (word_low as u128) | ((word_high as u128) << 64);
89    }
90    data
91}
92
93/// AVX2-optimized count accumulation for bundle add.
94#[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
95#[inline]
96#[target_feature(enable = "avx2")]
97/// # SAFETY
98/// Caller must ensure AVX2 is supported.
99pub(crate) unsafe fn update_counts_simd_avx2(
100    counts: &mut [i32; 10240],
101    hv: &[u128; 80],
102    sign: i32,
103) {
104    use std::arch::x86_64::{
105        _mm256_add_epi32, _mm256_and_si256, _mm256_cmpeq_epi32, _mm256_loadu_si256,
106        _mm256_set_epi32, _mm256_set1_epi32, _mm256_storeu_si256,
107    };
108
109    let sign_vec = _mm256_set1_epi32(sign);
110    // Bit selector for expanding bits to i32 lanes.
111    // lane 0: 2^0, lane 1: 2^1, ..., lane 7: 2^7
112    let bit_selector = _mm256_set_epi32(128, 64, 32, 16, 8, 4, 2, 1);
113
114    for i in 0..80 {
115        let word_ptr = &hv[i] as *const u128 as *const u8;
116        // SAFETY: counts is [i32; 10240], i * 128 is within bounds.
117        let counts_ptr = unsafe { counts.as_mut_ptr().add(i * 128) };
118
119        // Unroll by 2 to process 16 bits (2 bytes) per iteration, improving ILP.
120        for j in (0..16).step_by(2) {
121            // SAFETY: hv[i] is u128 (16 bytes), j and j+1 are 0..16.
122            let byte0 = unsafe { *word_ptr.add(j) };
123            let byte1 = unsafe { *word_ptr.add(j + 1) };
124
125            if byte0 != 0 {
126                let byte_vec = _mm256_set1_epi32(i32::from(byte0));
127                let mask =
128                    _mm256_cmpeq_epi32(_mm256_and_si256(byte_vec, bit_selector), bit_selector);
129                let increment = _mm256_and_si256(mask, sign_vec);
130
131                let target_ptr = unsafe { counts_ptr.add(j * 8) };
132                let current_counts = unsafe { _mm256_loadu_si256(target_ptr.cast()) };
133                let new_counts = _mm256_add_epi32(current_counts, increment);
134                unsafe { _mm256_storeu_si256(target_ptr.cast(), new_counts) };
135            }
136
137            if byte1 != 0 {
138                let byte_vec = _mm256_set1_epi32(i32::from(byte1));
139                let mask =
140                    _mm256_cmpeq_epi32(_mm256_and_si256(byte_vec, bit_selector), bit_selector);
141                let increment = _mm256_and_si256(mask, sign_vec);
142
143                let target_ptr = unsafe { counts_ptr.add((j + 1) * 8) };
144                let current_counts = unsafe { _mm256_loadu_si256(target_ptr.cast()) };
145                let new_counts = _mm256_add_epi32(current_counts, increment);
146                unsafe { _mm256_storeu_si256(target_ptr.cast(), new_counts) };
147            }
148        }
149    }
150}
151
152/// ARM NEON-optimized count accumulation for bundle add.
153#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
154#[inline]
155#[target_feature(enable = "neon")]
156/// # SAFETY
157/// Caller must ensure NEON is supported.
158pub(crate) unsafe fn update_counts_simd_neon(
159    counts: &mut [i32; 10240],
160    hv: &[u128; 80],
161    sign: i32,
162) {
163    for i in 0..80 {
164        let word_ptr = &hv[i] as *const u128 as *const u8;
165        // SAFETY: counts is [i32; 10240], i * 128 is within bounds.
166        let counts_ptr = unsafe { counts.as_mut_ptr().add(i * 128) };
167
168        for j in 0..16 {
169            // SAFETY: hv[i] is u128 (16 bytes), j is 0..16.
170            let byte = unsafe { *word_ptr.add(j) } as i32;
171            if byte == 0 {
172                continue;
173            }
174
175            for k in 0..8 {
176                if (byte & (1 << k)) != 0 {
177                    // SAFETY: counts_ptr + j * 8 + k is within bounds.
178                    unsafe { *counts_ptr.add(j * 8 + k) += sign };
179                }
180            }
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
188    use super::*;
189    use crate::hyperdim::HVec10240;
190    use rand::Rng;
191
192    fn finalize_scalar(counts: &[i32; 10240], threshold: i32) -> [u128; 80] {
193        let mut data = [0u128; 80];
194        for i in 0..80 {
195            let offset = i * 128;
196            for j in 0..128 {
197                if counts[offset + j] > threshold {
198                    data[i] |= 1u128 << j;
199                }
200            }
201        }
202        data
203    }
204
205    fn make_test_counts(seed: u64) -> [i32; 10240] {
206        use rand::RngExt;
207        use rand::SeedableRng;
208        let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
209        let mut counts = [0i32; 10240];
210        for i in 0..10240 {
211            counts[i] = rng.random_range(-10..10);
212        }
213        counts
214    }
215
216    #[test]
217    #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
218    fn test_finalize_simd_avx2_consistency() {
219        if is_x86_feature_detected!("avx2") {
220            for seed in 0..10 {
221                let counts = make_test_counts(seed);
222                for threshold in [-2, -1, 0, 1, 2] {
223                    let scalar = finalize_scalar(&counts, threshold);
224                    // SAFETY: AVX2 is checked above.
225                    let simd = unsafe { finalize_simd_avx2(&counts, threshold) };
226                    assert_eq!(simd, scalar);
227                }
228            }
229        }
230    }
231
232    #[test]
233    #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
234    fn test_finalize_simd_neon_consistency() {
235        for seed in 0..10 {
236            let counts = make_test_counts(seed);
237            for threshold in [-2, -1, 0, 1, 2] {
238                let scalar = finalize_scalar(&counts, threshold);
239                // SAFETY: NEON is always available on aarch64.
240                let simd = unsafe { finalize_simd_neon(&counts, threshold) };
241                assert_eq!(simd, scalar);
242            }
243        }
244    }
245
246    fn update_counts_scalar(counts: &mut [i32; 10240], hv: &[u128; 80], sign: i32) {
247        for i in 0..80 {
248            let mut val = hv[i];
249            let offset = i * 128;
250            for j in 0..128 {
251                if (val & 1) != 0 {
252                    counts[offset + j] += sign;
253                }
254                val >>= 1;
255            }
256        }
257    }
258
259    #[test]
260    #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
261    fn test_update_counts_simd_avx2_consistency() {
262        if is_x86_feature_detected!("avx2") {
263            let mut counts_scalar = [0i32; 10240];
264            let mut counts_simd = [0i32; 10240];
265            let mut hvs = Vec::new();
266            for i in 0..10 {
267                hvs.push(HVec10240::new_seeded(i).data);
268            }
269            for hv in &hvs {
270                update_counts_scalar(&mut counts_scalar, hv, 1);
271                // SAFETY: AVX2 is checked above.
272                unsafe { update_counts_simd_avx2(&mut counts_simd, hv, 1) };
273            }
274            assert_eq!(counts_scalar, counts_simd);
275            for hv in &hvs {
276                update_counts_scalar(&mut counts_scalar, hv, -1);
277                // SAFETY: AVX2 is checked above.
278                unsafe { update_counts_simd_avx2(&mut counts_simd, hv, -1) };
279            }
280            assert_eq!(counts_scalar, counts_simd);
281        }
282    }
283
284    #[test]
285    #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
286    fn test_update_counts_simd_neon_consistency() {
287        let mut counts_scalar = [0i32; 10240];
288        let mut counts_simd = [0i32; 10240];
289        let mut hvs = Vec::new();
290        for i in 0..10 {
291            hvs.push(HVec10240::new_seeded(i).data);
292        }
293        for hv in &hvs {
294            update_counts_scalar(&mut counts_scalar, hv, 1);
295            // SAFETY: NEON is always available on aarch64.
296            unsafe { update_counts_simd_neon(&mut counts_simd, hv, 1) };
297        }
298        assert_eq!(counts_scalar, counts_simd);
299        for hv in &hvs {
300            update_counts_scalar(&mut counts_scalar, hv, -1);
301            // SAFETY: NEON is always available on aarch64.
302            unsafe { update_counts_simd_neon(&mut counts_simd, hv, -1) };
303        }
304        assert_eq!(counts_scalar, counts_simd);
305    }
306}