Skip to main content

csm_core_lib/
hyperdim.rs

1//! Hyperdimensional computing primitives
2//!
3//! Implements 10240-bit hypervectors using `[u128; 80]`.
4
5// Casts are intentional for HDC dimension math (10240-bit operations)
6#![allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
7
8use rand::RngExt;
9
10#[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
11use rayon::prelude::*;
12
13use crate::error::Result;
14
15pub use crate::hyperdim_batch::batch_cosine_similarity;
16pub use crate::hyperdim_binary::BHVec10240;
17pub use crate::hyperdim_ops::{Hypervector, bundle_word_scalar};
18
19// Import SIMD functions from extension module
20#[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
21use crate::hyperdim_simd::{and_simd_avx2, bind_simd_avx2, hamming_distance_simd_avx2};
22#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
23use crate::hyperdim_simd::{and_simd_neon, bind_simd_neon, hamming_distance_simd_neon};
24#[cfg(all(
25    not(target_arch = "wasm32"),
26    any(target_arch = "x86_64", target_arch = "x86")
27))]
28use crate::hyperdim_simd::{and_simd_x86, bind_simd_x86};
29
30#[cfg(all(target_arch = "x86_64", not(target_arch = "wasm32")))]
31use crate::hyperdim_simd_bundle::bundle_block_avx2;
32
33#[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
34use crate::hyperdim_simd_bundle::bundle_block_neon;
35
36/// 10240-bit hypervector (80 x 128-bit words)
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[must_use]
39pub struct HVec10240 {
40    pub data: [u128; 80],
41}
42
43impl HVec10240 {
44    pub const DIMENSION: usize = 10240;
45    pub const WORDS: usize = 80;
46
47    /// Create a new hypervector with all zeros
48    pub const fn zero() -> Self {
49        Self { data: [0u128; 80] }
50    }
51
52    /// Create a random hypervector (each bit has 50% probability)
53    ///
54    /// Performance Optimization: Uses `rng.fill()` for bulk data generation, reducing
55    /// per-word overhead and allowing the RNG to use vectorized memory-filling paths.
56    /// Expected speedup: ~15% for random generation.
57    pub fn random() -> Self {
58        let mut rng = rand::rng();
59        let mut data = [0u128; 80];
60        rng.fill(&mut data);
61        Self { data }
62    }
63
64    /// Create a deterministic random hypervector from a seed.
65    ///
66    /// Uses `rand::rngs::StdRng` for reproducibility across runs.
67    pub fn new_seeded(seed: u64) -> Self {
68        use rand::SeedableRng;
69        use rand::rngs::StdRng;
70        let mut rng = StdRng::seed_from_u64(seed);
71        let mut data = [0u128; 80];
72        rng.fill(&mut data);
73        Self { data }
74    }
75
76    /// Create a random sparse hypervector with given density
77    pub fn sparse(density: f32) -> Self {
78        let mut rng = rand::rng();
79        let mut data = [0u128; 80];
80        let bits_to_set = (Self::DIMENSION as f32 * density) as usize;
81
82        for _ in 0..bits_to_set {
83            let pos = rng.random_range(0..Self::DIMENSION);
84            let word = pos / 128;
85            let bit = pos % 128;
86            data[word] |= 1u128 << bit;
87        }
88
89        Self { data }
90    }
91
92    /// Set a specific bit in the hypervector.
93    ///
94    /// # Panics
95    /// Panics if `pos >= DIMENSION` (10240).
96    pub fn set_bit(&mut self, pos: usize) {
97        assert!(
98            pos < Self::DIMENSION,
99            "bit position {pos} out of range (max {})",
100            Self::DIMENSION
101        );
102        let word = pos / 128;
103        let bit = pos % 128;
104        self.data[word] |= 1u128 << bit;
105    }
106
107    /// Bundle (sum) multiple hypervectors using bit-sliced addition.
108    ///
109    /// This implementation is optimized for performance and memory efficiency:
110    /// 1. It uses word-parallel bit-sliced addition to count set bits across vectors.
111    /// 2. It eliminates the large heap-allocated counter array and bit-by-bit loops.
112    /// 3. It parallelizes over hypervector words rather than over vectors to minimize
113    ///    memory traffic and synchronization overhead.
114    #[allow(clippy::needless_range_loop)]
115    pub fn bundle(vectors: &[Self]) -> Result<Self> {
116        let num_vectors = vectors.len();
117        if num_vectors == 0 {
118            return Ok(Self::zero());
119        }
120        if num_vectors == 1 {
121            return Ok(vectors[0]);
122        }
123        if num_vectors == 2 {
124            #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
125            {
126                if is_x86_feature_detected!("avx2") {
127                    // SAFETY: AVX2 is detected at runtime.
128                    return Ok(Self {
129                        data: unsafe { and_simd_avx2(&vectors[0].data, &vectors[1].data) },
130                    });
131                } else {
132                    return Ok(Self {
133                        data: and_simd_x86(&vectors[0].data, &vectors[1].data),
134                    });
135                }
136            }
137
138            #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86"))]
139            {
140                return Ok(Self {
141                    data: and_simd_x86(&vectors[0].data, &vectors[1].data),
142                });
143            }
144
145            #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
146            {
147                // SAFETY: NEON is always available on aarch64.
148                return Ok(Self {
149                    data: unsafe { and_simd_neon(&vectors[0].data, &vectors[1].data) },
150                });
151            }
152
153            #[cfg(any(
154                target_arch = "wasm32",
155                all(
156                    not(target_arch = "wasm32"),
157                    not(any(
158                        target_arch = "x86_64",
159                        target_arch = "x86",
160                        target_arch = "aarch64"
161                    ))
162                )
163            ))]
164            {
165                let mut res = Self::zero();
166                for i in 0..80 {
167                    res.data[i] = vectors[0].data[i] & vectors[1].data[i];
168                }
169                return Ok(res);
170            }
171        }
172
173        let threshold = num_vectors / 2 + 1;
174        let num_planes = (usize::BITS - num_vectors.leading_zeros()) as usize;
175
176        #[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
177        // Performance Optimization: Use parallel bit-sliced addition for large batches (N >= 256).
178        // For AVX2, we process 2 words (256 bits) per task to match register width.
179        // For NEON, we process 1 word (128 bits) per task to match register width.
180        if num_vectors >= 256 {
181            #[cfg(target_arch = "x86_64")]
182            if is_x86_feature_detected!("avx2") {
183                let mut data = [0u128; 80];
184                data.par_chunks_mut(2).enumerate().for_each(|(i, chunk)| {
185                    // SAFETY: AVX2 is detected at runtime. Pointers to vectors and indices are within bounds.
186                    let res = unsafe {
187                        crate::hyperdim_simd_bundle::bundle_block_avx2_single(
188                            vectors,
189                            i * 2,
190                            threshold,
191                            num_planes,
192                        )
193                    };
194                    // SAFETY: chunk length is 2 (32 bytes), matching AVX2 256-bit block size.
195                    unsafe {
196                        std::arch::x86_64::_mm256_storeu_si256(chunk.as_mut_ptr().cast(), res);
197                    }
198                });
199                return Ok(Self { data });
200            }
201
202            #[cfg(target_arch = "aarch64")]
203            {
204                let mut data = [0u128; 80];
205                data.par_iter_mut().enumerate().for_each(|(i, word)| {
206                    // SAFETY: NEON is always available on aarch64. Pointers to vectors and indices are within bounds.
207                    let res = unsafe {
208                        crate::hyperdim_simd_bundle::bundle_block_neon_single(
209                            vectors, i, threshold, num_planes,
210                        )
211                    };
212                    // SAFETY: word is a single u128 (16 bytes), matching NEON 128-bit block size.
213                    unsafe {
214                        std::arch::aarch64::vst1q_u8(word as *mut u128 as *mut u8, res);
215                    }
216                });
217                return Ok(Self { data });
218            }
219
220            // Parallel scalar fallback
221            // Note: explicit gate for aarch64 to avoid unreachable code warnings
222            // because the NEON path above returns unconditionally.
223            #[cfg(not(target_arch = "aarch64"))]
224            {
225                let mut data = [0u128; 80];
226                data.par_iter_mut().enumerate().for_each(|(i, word)| {
227                    *word = bundle_word_scalar(vectors, i, threshold, num_planes);
228                });
229                return Ok(Self { data });
230            }
231        }
232
233        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
234        if is_x86_feature_detected!("avx2") {
235            // SAFETY: AVX2 is detected at runtime.
236            return Ok(Self {
237                data: unsafe { bundle_block_avx2(vectors, threshold, num_planes) },
238            });
239        }
240
241        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
242        {
243            // SAFETY: NEON is always available on aarch64.
244            return Ok(Self {
245                data: unsafe { bundle_block_neon(vectors, threshold, num_planes) },
246            });
247        }
248
249        #[cfg(not(all(not(target_arch = "wasm32"), target_arch = "aarch64")))]
250        {
251            let mut data = [0u128; 80];
252            #[allow(clippy::needless_range_loop)]
253            for i in 0..80 {
254                data[i] = bundle_word_scalar(vectors, i, threshold, num_planes);
255            }
256            Ok(Self { data })
257        }
258    }
259
260    /// XOR binding of two hypervectors.
261    pub fn bind(&self, other: &Self) -> Self {
262        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
263        {
264            // Runtime dispatch: AVX2 if available, else SSE fallback
265            if is_x86_feature_detected!("avx2") {
266                // SAFETY: AVX2 feature detected at runtime.
267                Self {
268                    data: unsafe { bind_simd_avx2(&self.data, &other.data) },
269                }
270            } else {
271                Self {
272                    data: bind_simd_x86(&self.data, &other.data),
273                }
274            }
275        }
276
277        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86"))]
278        {
279            Self {
280                data: bind_simd_x86(&self.data, &other.data),
281            }
282        }
283
284        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
285        {
286            // SAFETY: bind_simd_neon requires unsafe due to NEON intrinsics.
287            // The function is marked #[target_feature(enable = "neon")] which
288            // is always available on aarch64, making this call safe.
289            Self {
290                data: unsafe { bind_simd_neon(&self.data, &other.data) },
291            }
292        }
293
294        #[cfg(target_arch = "wasm32")]
295        {
296            let mut result = [0u128; 80];
297            for i in 0..80 {
298                result[i] = self.data[i] ^ other.data[i];
299            }
300            Self { data: result }
301        }
302
303        #[cfg(all(
304            not(target_arch = "wasm32"),
305            not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"))
306        ))]
307        {
308            let mut result = [0u128; 80];
309            for i in 0..80 {
310                result[i] = self.data[i] ^ other.data[i];
311            }
312            Self { data: result }
313        }
314    }
315
316    /// Cosine similarity between two hypervectors.
317    ///
318    /// Calculated as `1.0 - (HammingDistance / 5120.0)` for 10240-bit vectors.
319    #[must_use]
320    pub fn cosine_similarity(&self, other: &Self) -> f32 {
321        let distance = self.hamming_distance(other);
322        // Similarity = (Matches - Mismatches) / Dimension
323        // Similarity = (Dimension - 2 * HammingDistance) / Dimension
324        // Similarity = 1.0 - (2.0 * HammingDistance / 10240.0) = 1.0 - (HammingDistance / 5120.0)
325        1.0 - (distance as f32 / 5120.0)
326    }
327
328    /// Hamming distance
329    ///
330    /// Dispatches to optimized SIMD paths based on platform:
331    /// - x86_64: AVX2 (runtime detection) or unrolled scalar GPR popcount fallback
332    /// - aarch64: NEON
333    /// - Other: unrolled scalar GPR popcount
334    #[must_use]
335    pub fn hamming_distance(&self, other: &Self) -> u32 {
336        #[cfg(all(not(target_arch = "wasm32"), target_arch = "x86_64"))]
337        {
338            if is_x86_feature_detected!("avx2") {
339                // SAFETY: AVX2 feature detected at runtime.
340                unsafe { hamming_distance_simd_avx2(&self.data, &other.data) }
341            } else {
342                crate::hyperdim_simd::hamming_distance_optimized(&self.data, &other.data)
343            }
344        }
345
346        #[cfg(all(not(target_arch = "wasm32"), target_arch = "aarch64"))]
347        {
348            // SAFETY: aarch64 always has NEON.
349            unsafe { hamming_distance_simd_neon(&self.data, &other.data) }
350        }
351
352        #[cfg(any(
353            target_arch = "wasm32",
354            not(any(target_arch = "x86_64", target_arch = "aarch64"))
355        ))]
356        {
357            crate::hyperdim_simd::hamming_distance_optimized(&self.data, &other.data)
358        }
359    }
360
361    /// Permute the hypervector (cyclic rotation)
362    ///
363    /// Optimized implementation that eliminates modulo operations and branches
364    /// from the hot loop by splitting the rotation into two contiguous segments.
365    #[allow(clippy::needless_range_loop)]
366    pub fn permute(&self, shift: usize) -> Self {
367        let mut result = [0u128; 80];
368        let bit_shift = shift % 128;
369        let word_shift = (shift / 128) % 80;
370
371        // Optimized path for word-aligned rotations
372        if bit_shift == 0 {
373            let (left, right) = self.data.split_at(word_shift);
374            result[..80 - word_shift].copy_from_slice(right);
375            result[80 - word_shift..].copy_from_slice(left);
376            return Self { data: result };
377        }
378
379        let inv_bit_shift = 128 - bit_shift;
380
381        // Split cyclic rotation into two segments to eliminate modulo in the loop
382        // Segment 1: src1 from word_shift to 78, src2 from word_shift + 1 to 79
383        let limit = 79 - word_shift;
384        for i in 0..limit {
385            let src1 = i + word_shift;
386            let src2 = src1 + 1;
387            result[i] = (self.data[src1] << bit_shift) | (self.data[src2] >> inv_bit_shift);
388        }
389
390        // Handle the wrap-around word at the boundary of segment 1 and 2
391        // result[79 - word_shift] uses data[79] and data[0]
392        result[limit] = (self.data[79] << bit_shift) | (self.data[0] >> inv_bit_shift);
393
394        // Segment 2: src1 from 0 to word_shift - 1, src2 from 1 to word_shift
395        for i in limit + 1..80 {
396            let src1 = i + word_shift - 80;
397            let src2 = src1 + 1;
398            result[i] = (self.data[src1] << bit_shift) | (self.data[src2] >> inv_bit_shift);
399        }
400
401        Self { data: result }
402    }
403
404    /// Serialize to bytes
405    pub fn to_bytes(&self) -> Vec<u8> {
406        let mut bytes = Vec::with_capacity(1280);
407        #[cfg(target_endian = "little")]
408        {
409            // Performance Optimization: [u128; 80] is bit-compatible with [u8; 1280]
410            // on little-endian platforms. Using extend_from_slice with a casted
411            // byte reference avoids 80 bounds checks and word-by-word serialization.
412            // SAFETY: Alignment of u128 is stricter than u8. Pointers are valid.
413            let data_bytes: &[u8; 1280] = unsafe { &*(self.data.as_ptr() as *const [u8; 1280]) };
414            bytes.extend_from_slice(data_bytes);
415        }
416        #[cfg(not(target_endian = "little"))]
417        {
418            for word in &self.data {
419                bytes.extend_from_slice(&word.to_le_bytes());
420            }
421        }
422        bytes
423    }
424
425    /// Deserialize from bytes
426    #[allow(clippy::missing_const_for_fn)] // Result return prevents const
427    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
428        if bytes.len() != 1280 {
429            return Err(crate::error::MemoryError::InvalidDimension {
430                expected: 1280,
431                actual: bytes.len(),
432            });
433        }
434
435        #[allow(unused_mut)]
436        let mut data = [0u128; 80];
437        #[cfg(target_endian = "little")]
438        {
439            // Performance Optimization: Direct memcpy for little-endian platforms.
440            // Avoids 80 loop iterations and multiple bounds checks per word.
441            // SAFETY: bytes length is verified to be 1280. [u128; 80] is bit-compatible
442            // with [u8; 1280] on little-endian. Pointers are valid.
443            unsafe {
444                std::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_mut_ptr() as *mut u8, 1280);
445            }
446        }
447        #[cfg(not(target_endian = "little"))]
448        {
449            for i in 0..80 {
450                let mut word_bytes = [0u8; 16];
451                word_bytes.copy_from_slice(&bytes[i * 16..(i + 1) * 16]);
452                data[i] = u128::from_le_bytes(word_bytes);
453            }
454        }
455
456        Ok(Self { data })
457    }
458}
459
460// Serde impls are in hyperdim_serde.rs (LOC gate extraction)
461
462// Re-export BundleAccumulator from bundle module
463pub use crate::bundle::BundleAccumulator;