use heapless::Vec as HVec;
pub const MAX_BINARY_SIZE: usize = 64;
#[derive(Debug, Clone)]
pub struct BinaryVector<const N: usize> {
pub data: HVec<u8, N>,
pub dim: usize,
pub threshold: i8,
}
impl<const N: usize> BinaryVector<N> {
pub fn from_i8(values: &[i8], threshold: i8) -> crate::Result<Self> {
let dim = values.len();
let num_bytes = (dim + 7) / 8;
if num_bytes > N {
return Err(crate::Error::BufferOverflow);
}
let mut data = HVec::new();
for chunk_idx in 0..(num_bytes) {
let mut byte = 0u8;
for bit_idx in 0..8 {
let val_idx = chunk_idx * 8 + bit_idx;
if val_idx < dim && values[val_idx] >= threshold {
byte |= 1 << bit_idx;
}
}
data.push(byte).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(Self { data, dim, threshold })
}
#[cfg(feature = "host-test")]
pub fn from_f32(values: &[f32], threshold: f32) -> crate::Result<Self> {
let i8_threshold = (threshold * 127.0) as i8;
let i8_values: heapless::Vec<i8, 512> = values
.iter()
.map(|&v| (v * 127.0).clamp(-128.0, 127.0) as i8)
.collect();
Self::from_i8(&i8_values, i8_threshold)
}
pub fn num_bytes(&self) -> usize {
self.data.len()
}
pub fn compression_ratio(&self) -> f32 {
self.dim as f32 / self.data.len() as f32
}
}
pub struct BinaryEmbedding<const VOCAB: usize, const DIM_BYTES: usize> {
data: HVec<u8, { 32 * 1024 }>, vocab_size: usize,
dim: usize,
bytes_per_embed: usize,
}
impl<const VOCAB: usize, const DIM_BYTES: usize> BinaryEmbedding<VOCAB, DIM_BYTES> {
pub fn random(vocab_size: usize, dim: usize, seed: u32) -> crate::Result<Self> {
let bytes_per_embed = (dim + 7) / 8;
let total_bytes = vocab_size * bytes_per_embed;
let mut data = HVec::new();
let mut rng_state = seed;
for _ in 0..total_bytes {
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let byte = ((rng_state >> 16) & 0xFF) as u8;
data.push(byte).map_err(|_| crate::Error::BufferOverflow)?;
}
Ok(Self {
data,
vocab_size,
dim,
bytes_per_embed,
})
}
pub fn lookup(&self, token_id: u16, output: &mut [u8]) -> crate::Result<()> {
let id = token_id as usize;
if id >= self.vocab_size {
return Err(crate::Error::InvalidModel("Token ID out of range"));
}
let start = id * self.bytes_per_embed;
let end = start + self.bytes_per_embed;
if output.len() < self.bytes_per_embed {
return Err(crate::Error::BufferOverflow);
}
output[..self.bytes_per_embed].copy_from_slice(&self.data[start..end]);
Ok(())
}
pub fn memory_size(&self) -> usize {
self.data.len()
}
pub fn compression_vs_int8(&self) -> f32 {
8.0 }
}
#[inline]
pub fn hamming_distance(a: &[u8], b: &[u8]) -> u32 {
debug_assert_eq!(a.len(), b.len());
let mut distance: u32 = 0;
let chunks = a.len() / 4;
for i in 0..chunks {
let idx = i * 4;
let xor0 = a[idx] ^ b[idx];
let xor1 = a[idx + 1] ^ b[idx + 1];
let xor2 = a[idx + 2] ^ b[idx + 2];
let xor3 = a[idx + 3] ^ b[idx + 3];
distance += popcount8(xor0) + popcount8(xor1) + popcount8(xor2) + popcount8(xor3);
}
for i in (chunks * 4)..a.len() {
distance += popcount8(a[i] ^ b[i]);
}
distance
}
#[inline]
pub fn hamming_similarity(a: &[u8], b: &[u8]) -> f32 {
let total_bits = (a.len() * 8) as f32;
let distance = hamming_distance(a, b) as f32;
1.0 - (distance / total_bits)
}
#[inline]
pub fn hamming_similarity_fixed(a: &[u8], b: &[u8]) -> u8 {
let total_bits = (a.len() * 8) as u32;
let matching_bits = total_bits - hamming_distance(a, b);
((matching_bits * 255) / total_bits) as u8
}
#[inline]
pub fn popcount8(x: u8) -> u32 {
const POPCOUNT_TABLE: [u8; 256] = [
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
];
POPCOUNT_TABLE[x as usize] as u32
}
#[inline]
pub fn xnor_popcount(a: &[u8], b: &[u8]) -> i32 {
debug_assert_eq!(a.len(), b.len());
let total_bits = (a.len() * 8) as i32;
let mut matching: i32 = 0;
for (&x, &y) in a.iter().zip(b.iter()) {
let xnor = !(x ^ y);
matching += popcount8(xnor) as i32;
}
2 * matching - total_bits
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binary_quantization() {
let values = [10i8, -5, 20, -10, 0, 15, -8, 30];
let binary = BinaryVector::<8>::from_i8(&values, 0).unwrap();
assert_eq!(binary.dim, 8);
assert_eq!(binary.num_bytes(), 1);
assert_eq!(binary.data[0], 0b10110101);
}
#[test]
fn test_hamming_distance() {
let a = [0b11110000u8, 0b10101010];
let b = [0b11110000u8, 0b10101010];
assert_eq!(hamming_distance(&a, &b), 0);
let c = [0b00001111u8, 0b01010101];
assert_eq!(hamming_distance(&a, &c), 16); }
#[test]
fn test_xnor_popcount() {
let a = [0b11111111u8];
let b = [0b11111111u8];
assert_eq!(xnor_popcount(&a, &b), 8);
let c = [0b00000000u8];
assert_eq!(xnor_popcount(&a, &c), -8);
}
#[test]
fn test_compression_ratio() {
let values = [0i8; 64];
let binary = BinaryVector::<8>::from_i8(&values, 0).unwrap();
assert_eq!(binary.compression_ratio(), 8.0);
}
}