1use bitpacking::{BitPacker, BitPacker4x};
2use half::f16;
3use thiserror::Error;
4
5const BITPACK_GROUP_SIZE: usize = BitPacker4x::BLOCK_LEN; const DIRECTORY_SENTINEL: u8 = 0xFF;
10
11pub const MAX_BLOCK_ENTRIES: usize = 4096;
15
16const HEADER_SIZE: usize = 16;
17const DIRECTORY_ENTRY_SIZE: usize = 8; fn fill_relative_group(
23 offsets: &[u32],
24 min_offset: u32,
25 last_relative: u32,
26 out: &mut [u32; BITPACK_GROUP_SIZE],
27) {
28 for (r, val) in out.iter_mut().zip(
29 offsets
30 .iter()
31 .map(|&o| o - min_offset)
32 .chain(std::iter::repeat(last_relative)),
33 ) {
34 *r = val;
35 }
36}
37
38pub const DIRECTORY_PREFIX: &str = "~";
54
55#[derive(Debug, Clone, Error)]
58pub enum SparsePostingBlockError {
59 #[error("block must have at least one entry")]
60 EmptyEntries,
61 #[error("block has {count} entries, max is {MAX_BLOCK_ENTRIES}")]
62 TooManyEntries { count: usize },
63 #[error("directory: max_offsets len ({offsets}) != max_weights len ({weights})")]
64 MismatchedLengths { offsets: usize, weights: usize },
65 #[error("expected at least {HEADER_SIZE} header bytes, got {len}")]
66 TruncatedHeader { len: usize },
67 #[error("expected {expected} body bytes, got {actual}")]
68 TruncatedBody { expected: usize, actual: usize },
69 #[error("invalid bits_per_delta: {value} (expected 0..=32 or 0xFF for directory)")]
70 InvalidBitsPerDelta { value: u8 },
71}
72
73#[derive(Debug, Clone, Copy)]
93pub struct PostingBlockHeader {
94 pub num_entries: u16,
96 pub bits_per_delta: u8,
99 pub min_offset: u32,
101 pub max_offset: u32,
103 pub max_weight: f32,
106}
107
108impl PostingBlockHeader {
109 pub fn is_directory(&self) -> bool {
110 self.bits_per_delta == DIRECTORY_SENTINEL
111 }
112}
113
114#[derive(Debug, Clone)]
115struct Decompressed {
116 offsets: Vec<u32>,
117 values: Vec<f32>,
118}
119
120#[derive(Debug, Clone)]
128enum PostingBody {
129 Encoded(Vec<u8>),
130 Decoded(Decompressed),
131}
132
133#[derive(Debug, Clone)]
164pub struct SparsePostingBlock {
165 pub header: PostingBlockHeader,
167 body: PostingBody,
168}
169
170impl SparsePostingBlock {
171 pub fn from_sorted_entries(entries: &[(u32, f32)]) -> Result<Self, SparsePostingBlockError> {
173 if entries.is_empty() {
174 return Err(SparsePostingBlockError::EmptyEntries);
175 }
176 if entries.len() > MAX_BLOCK_ENTRIES {
177 return Err(SparsePostingBlockError::TooManyEntries {
178 count: entries.len(),
179 });
180 }
181
182 let n = entries.len();
183 debug_assert!(
184 entries.is_sorted_by_key(|e| e.0),
185 "from_sorted_entries: offsets must be monotonically non-decreasing"
186 );
187 let min_offset = entries[0].0;
188 let max_offset = entries[n - 1].0;
189 let max_weight = entries
190 .iter()
191 .map(|(_, v)| *v)
192 .fold(0.0f32, f32::max)
193 .max(f32::MIN_POSITIVE);
194
195 let (offsets, values): (Vec<u32>, Vec<f32>) = entries.iter().copied().unzip();
196
197 let packer = BitPacker4x::new();
202 let num_groups = n.div_ceil(BITPACK_GROUP_SIZE);
203 let last_relative = max_offset - min_offset;
204 let mut max_bits = 0u8;
205 let mut rel_group = [0u32; BITPACK_GROUP_SIZE];
206 for g in 0..num_groups {
207 let start = g * BITPACK_GROUP_SIZE;
208 let group_offsets = &offsets[start..n.min(start + BITPACK_GROUP_SIZE)];
209 fill_relative_group(group_offsets, min_offset, last_relative, &mut rel_group);
210 let initial = if g == 0 {
211 0
212 } else {
213 offsets[start - 1] - min_offset
214 };
215 max_bits = max_bits.max(packer.num_bits_sorted(initial, &rel_group));
216 }
217
218 Ok(SparsePostingBlock {
219 header: PostingBlockHeader {
220 min_offset,
221 max_offset,
222 max_weight,
223 num_entries: n as u16,
224 bits_per_delta: max_bits,
225 },
226 body: PostingBody::Decoded(Decompressed { offsets, values }),
227 })
228 }
229
230 pub fn len(&self) -> usize {
231 self.header.num_entries as usize
232 }
233
234 pub fn is_empty(&self) -> bool {
235 self.header.num_entries == 0
236 }
237
238 pub fn decode(&mut self) -> (&[u32], &[f32]) {
248 if let PostingBody::Encoded(ref raw) = self.body {
249 if !self.is_directory() {
250 let decoded = Self::decompress_raw(
251 raw,
252 self.header.num_entries as usize,
253 self.header.bits_per_delta,
254 self.header.min_offset,
255 );
256 self.body = PostingBody::Decoded(decoded);
257 }
258 }
259 match &self.body {
260 PostingBody::Decoded(d) => (&d.offsets, &d.values),
261 PostingBody::Encoded(_) => (&[], &[]),
262 }
263 }
264
265 pub fn offsets(&mut self) -> &[u32] {
268 self.decode().0
269 }
270
271 pub fn values(&mut self) -> &[f32] {
274 self.decode().1
275 }
276
277 fn decompress_raw(
278 raw_body: &[u8],
279 num_entries: usize,
280 bits_per_delta: u8,
281 min_offset: u32,
282 ) -> Decompressed {
283 let mut offsets = Vec::new();
284 Self::decompress_offsets_from_body(
285 raw_body,
286 num_entries,
287 bits_per_delta,
288 min_offset,
289 &mut offsets,
290 );
291
292 let weight_start = Self::body_weight_offset(num_entries, bits_per_delta);
293 let weight_bytes = &raw_body[weight_start..weight_start + num_entries * 2];
294 let values: Vec<f32> = weight_bytes
295 .chunks_exact(2)
296 .map(|b| f16::from_le_bytes([b[0], b[1]]).to_f32())
297 .collect();
298
299 Decompressed { offsets, values }
300 }
301
302 fn decompress_offsets_from_body(
305 raw_body: &[u8],
306 num_entries: usize,
307 bits_per_delta: u8,
308 min_offset: u32,
309 buf: &mut Vec<u32>,
310 ) {
311 let packer = BitPacker4x::new();
312 let num_groups = num_entries.div_ceil(BITPACK_GROUP_SIZE);
313 let packed_group_bytes = (BITPACK_GROUP_SIZE * (bits_per_delta as usize)).div_ceil(8);
314
315 let padded_len = num_groups * BITPACK_GROUP_SIZE;
316 buf.clear();
317 buf.resize(padded_len, 0);
318
319 let mut byte_offset = 0;
320 let mut initial = 0u32;
321
322 for g in 0..num_groups {
323 let group_end = byte_offset + packed_group_bytes;
324 let group = &mut buf[g * BITPACK_GROUP_SIZE..(g + 1) * BITPACK_GROUP_SIZE];
325 packer.decompress_sorted(
326 initial,
327 &raw_body[byte_offset..group_end],
328 group,
329 bits_per_delta,
330 );
331 initial = group[BITPACK_GROUP_SIZE - 1];
332 for offset in group.iter_mut() {
333 *offset += min_offset;
334 }
335 byte_offset = group_end;
336 }
337
338 buf.truncate(num_entries);
339 }
340
341 fn body_weight_offset(num_entries: usize, bits_per_delta: u8) -> usize {
343 let num_groups = num_entries.div_ceil(BITPACK_GROUP_SIZE);
344 let packed_group_bytes = (BITPACK_GROUP_SIZE * (bits_per_delta as usize)).div_ceil(8);
345 num_groups * packed_group_bytes
346 }
347
348 pub fn serialize(&self) -> Vec<u8> {
352 let data = match &self.body {
353 PostingBody::Encoded(raw) => {
354 let mut buf = Vec::with_capacity(HEADER_SIZE + raw.len());
355 self.write_header(&mut buf);
356 buf.extend_from_slice(raw);
357 return buf;
358 }
359 PostingBody::Decoded(d) => d,
360 };
361 let n = data.offsets.len();
362 let packer = BitPacker4x::new();
363 let last_relative = self.header.max_offset - self.header.min_offset;
364
365 let num_groups = n.div_ceil(BITPACK_GROUP_SIZE);
366 let packed_group_bytes =
367 (BITPACK_GROUP_SIZE * (self.header.bits_per_delta as usize)).div_ceil(8);
368
369 let mut buf = Vec::with_capacity(self.serialized_size());
370 self.write_header(&mut buf);
371
372 let mut packed = [0u8; BITPACK_GROUP_SIZE * 4];
376 let mut rel_group = [0u32; BITPACK_GROUP_SIZE];
377 for g in 0..num_groups {
378 let start = g * BITPACK_GROUP_SIZE;
379 let group_offsets = &data.offsets[start..n.min(start + BITPACK_GROUP_SIZE)];
380 fill_relative_group(
381 group_offsets,
382 self.header.min_offset,
383 last_relative,
384 &mut rel_group,
385 );
386 let initial = if g == 0 {
387 0
388 } else {
389 data.offsets[start - 1] - self.header.min_offset
390 };
391 packed[..packed_group_bytes].fill(0);
392 packer.compress_sorted(
393 initial,
394 &rel_group,
395 &mut packed[..packed_group_bytes],
396 self.header.bits_per_delta,
397 );
398 buf.extend_from_slice(&packed[..packed_group_bytes]);
399 }
400
401 for &v in &data.values {
402 buf.extend_from_slice(&f16::from_f32(v).to_le_bytes());
403 }
404
405 buf
406 }
407
408 pub fn serialized_size(&self) -> usize {
411 HEADER_SIZE
412 + Self::expected_body_size(self.header.num_entries as usize, self.header.bits_per_delta)
413 }
414
415 pub fn deserialize(bytes: &[u8]) -> Result<Self, SparsePostingBlockError> {
421 if bytes.len() < HEADER_SIZE {
422 return Err(SparsePostingBlockError::TruncatedHeader { len: bytes.len() });
423 }
424
425 let num_entries = u16::from_le_bytes([bytes[0], bytes[1]]);
426 let bits_per_delta = bytes[2];
427 let min_offset = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
429 let max_offset = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
430 let max_weight = f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
431
432 if bits_per_delta > 32 && bits_per_delta != DIRECTORY_SENTINEL {
433 return Err(SparsePostingBlockError::InvalidBitsPerDelta {
434 value: bits_per_delta,
435 });
436 }
437
438 let expected_body = Self::expected_body_size(num_entries as usize, bits_per_delta);
439 let actual_body = bytes.len() - HEADER_SIZE;
440 if actual_body < expected_body {
441 return Err(SparsePostingBlockError::TruncatedBody {
442 expected: expected_body,
443 actual: actual_body,
444 });
445 }
446
447 Ok(SparsePostingBlock {
448 header: PostingBlockHeader {
449 min_offset,
450 max_offset,
451 max_weight,
452 num_entries,
453 bits_per_delta,
454 },
455 body: PostingBody::Encoded(bytes[HEADER_SIZE..HEADER_SIZE + expected_body].to_vec()),
456 })
457 }
458
459 fn expected_body_size(num_entries: usize, bits_per_delta: u8) -> usize {
461 if bits_per_delta == DIRECTORY_SENTINEL {
462 num_entries * DIRECTORY_ENTRY_SIZE
463 } else {
464 Self::body_weight_offset(num_entries, bits_per_delta) + num_entries * 2
465 }
466 }
467
468 fn write_header(&self, buf: &mut Vec<u8>) {
469 buf.extend_from_slice(&self.header.num_entries.to_le_bytes());
470 buf.push(self.header.bits_per_delta);
471 buf.push(0); buf.extend_from_slice(&self.header.min_offset.to_le_bytes());
473 buf.extend_from_slice(&self.header.max_offset.to_le_bytes());
474 buf.extend_from_slice(&self.header.max_weight.to_le_bytes());
475 }
476
477 pub fn peek_header(bytes: &[u8]) -> Result<PostingBlockHeader, SparsePostingBlockError> {
485 if bytes.len() < HEADER_SIZE {
486 return Err(SparsePostingBlockError::TruncatedHeader { len: bytes.len() });
487 }
488 Ok(PostingBlockHeader {
489 num_entries: u16::from_le_bytes([bytes[0], bytes[1]]),
490 bits_per_delta: bytes[2],
491 min_offset: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
492 max_offset: u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]),
493 max_weight: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]),
494 })
495 }
496
497 pub fn decompress_offsets_into(bytes: &[u8], hdr: &PostingBlockHeader, buf: &mut Vec<u32>) {
500 debug_assert!(
501 !hdr.is_directory(),
502 "decompress_offsets_into called on directory block"
503 );
504 Self::decompress_offsets_from_body(
505 &bytes[HEADER_SIZE..],
506 hdr.num_entries as usize,
507 hdr.bits_per_delta,
508 hdr.min_offset,
509 buf,
510 );
511 }
512
513 pub fn raw_weight_bytes<'a>(bytes: &'a [u8], hdr: &PostingBlockHeader) -> &'a [u8] {
516 debug_assert!(
517 !hdr.is_directory(),
518 "raw_weight_bytes called on directory block"
519 );
520 let n = hdr.num_entries as usize;
521 let w_start = Self::weight_byte_offset(hdr);
522 &bytes[w_start..w_start + n * 2]
523 }
524
525 pub fn read_value_at(bytes: &[u8], hdr: &PostingBlockHeader, index: usize) -> f32 {
528 debug_assert!(
529 !hdr.is_directory(),
530 "read_value_at called on directory block"
531 );
532 debug_assert!(index < hdr.num_entries as usize);
533 let byte_pos = Self::weight_byte_offset(hdr) + index * 2;
534 f16::from_le_bytes([bytes[byte_pos], bytes[byte_pos + 1]]).to_f32()
535 }
536
537 pub fn decompress_values_into(bytes: &[u8], hdr: &PostingBlockHeader, buf: &mut Vec<f32>) {
540 debug_assert!(
541 !hdr.is_directory(),
542 "decompress_values_into called on directory block"
543 );
544 let n = hdr.num_entries as usize;
545 buf.clear();
546 buf.resize(n, 0.0);
547
548 let w_start = Self::weight_byte_offset(hdr);
549 let f16_bytes = &bytes[w_start..w_start + n * 2];
550 convert_f16_to_f32(f16_bytes, buf);
551 }
552
553 fn weight_byte_offset(hdr: &PostingBlockHeader) -> usize {
556 HEADER_SIZE + Self::body_weight_offset(hdr.num_entries as usize, hdr.bits_per_delta)
557 }
558
559 pub fn is_directory(&self) -> bool {
560 self.header.bits_per_delta == DIRECTORY_SENTINEL
561 }
562}
563
564#[derive(Debug, Clone)]
582pub struct DirectoryBlock(SparsePostingBlock);
583
584impl DirectoryBlock {
585 pub fn new(max_offsets: &[u32], max_weights: &[f32]) -> Result<Self, SparsePostingBlockError> {
590 if max_offsets.len() != max_weights.len() {
591 return Err(SparsePostingBlockError::MismatchedLengths {
592 offsets: max_offsets.len(),
593 weights: max_weights.len(),
594 });
595 }
596 if max_offsets.len() > u16::MAX as usize {
597 return Err(SparsePostingBlockError::TooManyEntries {
598 count: max_offsets.len(),
599 });
600 }
601 let n = max_offsets.len();
602 let dim_max = max_weights.iter().copied().fold(0.0f32, f32::max);
603
604 let mut raw_body = Vec::with_capacity(n * 8);
605 for i in 0..n {
606 raw_body.extend_from_slice(&max_offsets[i].to_le_bytes());
607 raw_body.extend_from_slice(&max_weights[i].to_le_bytes());
608 }
609
610 Ok(DirectoryBlock(SparsePostingBlock {
611 header: PostingBlockHeader {
612 min_offset: max_offsets.first().copied().unwrap_or(0),
613 max_offset: max_offsets.last().copied().unwrap_or(0),
614 max_weight: dim_max,
615 num_entries: n as u16,
616 bits_per_delta: DIRECTORY_SENTINEL,
617 },
618 body: PostingBody::Encoded(raw_body),
619 }))
620 }
621
622 pub fn from_block(block: SparsePostingBlock) -> Result<Self, SparsePostingBlock> {
625 if block.is_directory() {
626 Ok(DirectoryBlock(block))
627 } else {
628 Err(block)
629 }
630 }
631
632 pub fn dim_max_weight(&self) -> f32 {
634 self.0.header.max_weight
635 }
636
637 pub fn num_blocks(&self) -> usize {
639 self.0.header.num_entries as usize
640 }
641
642 pub fn entries(&self) -> (Vec<u32>, Vec<f32>) {
644 let raw = match &self.0.body {
645 PostingBody::Encoded(raw) => raw.as_slice(),
646 PostingBody::Decoded(_) => return (Vec::new(), Vec::new()),
650 };
651 let n = self.0.header.num_entries as usize;
652 let mut max_offsets = Vec::with_capacity(n);
653 let mut max_weights = Vec::with_capacity(n);
654 for i in 0..n {
655 let pos = i * 8;
656 max_offsets.push(u32::from_le_bytes([
657 raw[pos],
658 raw[pos + 1],
659 raw[pos + 2],
660 raw[pos + 3],
661 ]));
662 max_weights.push(f32::from_le_bytes([
663 raw[pos + 4],
664 raw[pos + 5],
665 raw[pos + 6],
666 raw[pos + 7],
667 ]));
668 }
669 (max_offsets, max_weights)
670 }
671
672 pub fn into_block(self) -> SparsePostingBlock {
675 self.0
676 }
677}
678
679#[derive(Debug, Clone)]
693pub struct Directory {
694 max_offsets: Vec<u32>,
695 max_weights: Vec<f32>,
696 dim_max_weight: f32,
697}
698
699impl Directory {
700 pub fn new(
702 max_offsets: Vec<u32>,
703 max_weights: Vec<f32>,
704 ) -> Result<Self, SparsePostingBlockError> {
705 if max_offsets.len() != max_weights.len() {
706 return Err(SparsePostingBlockError::MismatchedLengths {
707 offsets: max_offsets.len(),
708 weights: max_weights.len(),
709 });
710 }
711 if max_offsets.is_empty() {
712 return Err(SparsePostingBlockError::EmptyEntries);
713 }
714 let dim_max_weight = max_weights.iter().copied().fold(0.0f32, f32::max);
715 Ok(Directory {
716 max_offsets,
717 max_weights,
718 dim_max_weight,
719 })
720 }
721
722 pub fn from_parts(
724 parts: impl IntoIterator<Item = DirectoryBlock>,
725 ) -> Result<Self, SparsePostingBlockError> {
726 let mut max_offsets = Vec::new();
727 let mut max_weights = Vec::new();
728 for part in parts {
729 let (o, w) = part.entries();
730 max_offsets.extend(o);
731 max_weights.extend(w);
732 }
733 Self::new(max_offsets, max_weights)
734 }
735
736 pub fn into_parts(self, max_entries_per_part: usize) -> Vec<DirectoryBlock> {
742 let cap = max_entries_per_part.max(1).min(u16::MAX as usize);
743 self.max_offsets
744 .chunks(cap)
745 .zip(self.max_weights.chunks(cap))
746 .map(|(o, w)| DirectoryBlock::new(o, w).expect("chunk from valid directory"))
747 .collect()
748 }
749
750 pub fn max_offsets(&self) -> &[u32] {
751 &self.max_offsets
752 }
753
754 pub fn max_weights(&self) -> &[f32] {
755 &self.max_weights
756 }
757
758 pub fn dim_max_weight(&self) -> f32 {
760 self.dim_max_weight
761 }
762
763 pub fn num_blocks(&self) -> usize {
765 self.max_offsets.len()
766 }
767
768 pub fn max_entries_for_block_size(max_block_size_bytes: usize) -> usize {
775 const ARROW_OVERHEAD_ESTIMATE: usize = 256;
776 max_block_size_bytes.saturating_sub(HEADER_SIZE + ARROW_OVERHEAD_ESTIMATE)
777 / DIRECTORY_ENTRY_SIZE
778 }
779}
780
781pub fn convert_f16_to_f32(f16_bytes: &[u8], out: &mut [f32]) {
786 #[cfg(target_arch = "aarch64")]
787 {
788 convert_f16_to_f32_neon(f16_bytes, out);
789 return;
790 }
791 #[cfg(target_arch = "x86_64")]
792 {
793 if is_x86_feature_detected!("avx512f") {
794 unsafe { convert_f16_to_f32_avx512(f16_bytes, out) };
797 return;
798 }
799 if is_x86_feature_detected!("f16c") {
800 unsafe { convert_f16_to_f32_f16c(f16_bytes, out) };
803 return;
804 }
805 }
806 #[allow(unreachable_code)]
807 convert_f16_to_f32_scalar(f16_bytes, out);
808}
809
810pub fn convert_f16_to_f32_scalar(f16_bytes: &[u8], out: &mut [f32]) {
812 for (o, chunk) in out.iter_mut().zip(f16_bytes.chunks_exact(2)) {
813 *o = f16::from_le_bytes([chunk[0], chunk[1]]).to_f32();
814 }
815}
816
817#[cfg(target_arch = "aarch64")]
824fn convert_f16_to_f32_neon(f16_bytes: &[u8], out: &mut [f32]) {
825 use std::arch::aarch64::*;
826
827 let n = out.len();
828 let chunks = n / 8;
829
830 unsafe {
834 let sign_mask = vdupq_n_u32(0x8000);
835 let nosign_mask = vdupq_n_u32(0x7FFF);
836 let bias = vdupq_n_u32(0x3800_0000); for c in 0..chunks {
839 let base = c * 8;
840 let byte_base = base * 2;
841
842 let h8 = vld1q_u16(f16_bytes.as_ptr().add(byte_base) as *const u16);
843 let lo = vmovl_u16(vget_low_u16(h8));
844 let hi = vmovl_u16(vget_high_u16(h8));
845
846 macro_rules! cvt {
847 ($h:expr, $off:expr) => {{
848 let sign = vshlq_n_u32::<16>(vandq_u32($h, sign_mask));
849 let nosign = vshlq_n_u32::<13>(vandq_u32($h, nosign_mask));
850 let bits = vorrq_u32(sign, vaddq_u32(nosign, bias));
851 vst1q_f32(
852 out.as_mut_ptr().add(base + $off),
853 vreinterpretq_f32_u32(bits),
854 );
855 }};
856 }
857 cvt!(lo, 0);
858 cvt!(hi, 4);
859 }
860 }
861
862 let rem_start = chunks * 8;
863 convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
864}
865
866#[cfg(target_arch = "x86_64")]
868#[target_feature(enable = "avx512f")]
869unsafe fn convert_f16_to_f32_avx512(f16_bytes: &[u8], out: &mut [f32]) {
870 use std::arch::x86_64::*;
871
872 let n = out.len();
873 let chunks = n / 16;
874
875 for c in 0..chunks {
876 let base = c * 16;
877 let byte_base = base * 2;
878 let h16 = _mm256_loadu_si256(f16_bytes.as_ptr().add(byte_base) as *const __m256i);
879 let f16_out = _mm512_cvtph_ps(h16);
880 _mm512_storeu_ps(out.as_mut_ptr().add(base), f16_out);
881 }
882
883 let rem_start = chunks * 16;
884 convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
885}
886
887#[cfg(target_arch = "x86_64")]
889#[target_feature(enable = "f16c")]
890unsafe fn convert_f16_to_f32_f16c(f16_bytes: &[u8], out: &mut [f32]) {
891 use std::arch::x86_64::*;
892
893 let n = out.len();
894 let chunks = n / 8;
895
896 for c in 0..chunks {
899 let base = c * 8;
900 let byte_base = base * 2;
901 let h8 = _mm_loadu_si128(f16_bytes.as_ptr().add(byte_base) as *const __m128i);
902 let f8 = _mm256_cvtph_ps(h8);
903 _mm256_storeu_ps(out.as_mut_ptr().add(base), f8);
904 }
905
906 let rem_start = chunks * 8;
907 convert_f16_to_f32_scalar(&f16_bytes[rem_start * 2..], &mut out[rem_start..]);
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913
914 const F16_TOL: f32 = 1e-3;
915
916 fn make_block(entries: &[(u32, f32)]) -> SparsePostingBlock {
917 SparsePostingBlock::from_sorted_entries(entries).expect("make_block: invalid entries")
918 }
919
920 fn sequential_entries(start: u32, step: u32, count: usize, weight: f32) -> Vec<(u32, f32)> {
921 (0..count)
922 .map(|i| (start + step * i as u32, weight))
923 .collect()
924 }
925
926 fn assert_approx(actual: f32, expected: f32, tol: f32) {
927 assert!(
928 (actual - expected).abs() <= tol,
929 "expected {expected} +/- {tol}, got {actual}"
930 );
931 }
932
933 fn assert_roundtrip_offsets(entries: &[(u32, f32)]) {
934 let mut block = make_block(entries);
935 let bytes = block.serialize();
936 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
937 assert_eq!(restored.offsets(), block.offsets());
938 }
939
940 fn assert_roundtrip_values(entries: &[(u32, f32)]) {
941 let mut block = make_block(entries);
942 let bytes = block.serialize();
943 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
944 for (i, (&orig, &rest)) in block
945 .values()
946 .iter()
947 .zip(restored.values().iter())
948 .enumerate()
949 {
950 assert!(
951 (rest - orig).abs() <= F16_TOL,
952 "entry {i}: expected {orig} +/- {F16_TOL}, got {rest}"
953 );
954 }
955 }
956
957 #[test]
958 fn roundtrip_at_boundary_sizes() {
959 for count in [1, 3, 127, 128, 129, 255, 256, 512, MAX_BLOCK_ENTRIES] {
962 let entries = sequential_entries(0, 1, count, 0.5);
963 assert_roundtrip_offsets(&entries);
964 assert_roundtrip_values(&entries);
965 }
966 }
967
968 #[test]
969 fn padding_does_not_inflate_bits_per_delta() {
970 let entries = sequential_entries(0, 1, 129, 0.5);
975 let block = make_block(&entries);
976 assert_eq!(block.header.bits_per_delta, 1);
977
978 let single = make_block(&[(42, 0.5)]);
980 assert_eq!(single.header.bits_per_delta, 0);
981 }
982
983 #[test]
984 fn roundtrip_large_deltas() {
985 let entries = vec![(0, 0.5), (1_000_000, 0.8), (2_000_000, 0.3)];
986 assert_roundtrip_offsets(&entries);
987 assert_roundtrip_values(&entries);
988 }
989
990 #[test]
991 fn roundtrip_tiny_weights() {
992 let entries = vec![(0, 0.001), (1, 1.0)];
993 let mut block = make_block(&entries);
994 let bytes = block.serialize();
995 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
996 assert_eq!(restored.offsets(), block.offsets());
997 assert_approx(restored.values()[1], 1.0, F16_TOL);
998 assert!(restored.values()[0] < 0.01);
999 }
1000
1001 #[test]
1002 fn header_fields() {
1003 let entries = vec![(10, 0.5), (20, 0.9), (30, 0.2)];
1004 let block = make_block(&entries);
1005 let bytes = block.serialize();
1006 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1007 assert_eq!(restored.header.min_offset, 10);
1008 assert_eq!(restored.header.max_offset, 30);
1009 assert_eq!(restored.header.max_weight, 0.9);
1010 assert_eq!(restored.offsets().len(), 3);
1011 }
1012
1013 #[test]
1014 fn peek_header_matches() {
1015 let entries = sequential_entries(100, 5, 200, 0.42);
1016 let block = make_block(&entries);
1017 let bytes = block.serialize();
1018 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1019 assert_eq!(hdr.num_entries, 200);
1020 assert_eq!(hdr.min_offset, 100);
1021 assert_eq!(hdr.max_offset, 100 + 5 * 199);
1022 }
1023
1024 #[test]
1025 fn raw_weight_bytes_length() {
1026 let entries = sequential_entries(0, 1, 200, 0.5);
1027 let block = make_block(&entries);
1028 let bytes = block.serialize();
1029 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1030 let wb = SparsePostingBlock::raw_weight_bytes(&bytes, &hdr);
1031 assert_eq!(wb.len(), 200 * 2);
1032 }
1033
1034 #[test]
1035 fn serialized_size_matches_actual() {
1036 for count in [1, 3, 127, 128, 129, 255, 256, 257, 512, 1024] {
1037 let entries = sequential_entries(0, 1, count, 0.5);
1038 let block = make_block(&entries);
1039 let bytes = block.serialize();
1040 assert_eq!(
1041 block.serialized_size(),
1042 bytes.len(),
1043 "serialized_size mismatch for count={count}"
1044 );
1045 }
1046 }
1047
1048 #[test]
1049 fn directory_block_roundtrip() {
1050 let max_offsets = vec![100, 500, 1000];
1051 let max_weights = vec![0.9, 0.7, 0.5];
1052 let dir = DirectoryBlock::new(&max_offsets, &max_weights).unwrap();
1053 assert_eq!(dir.dim_max_weight(), 0.9);
1054 assert_eq!(dir.num_blocks(), 3);
1055
1056 let block = dir.into_block();
1057 assert!(block.is_directory());
1058 let bytes = block.serialize();
1059
1060 let restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1061 assert!(restored.is_directory());
1062 let dir2 = DirectoryBlock::from_block(restored).unwrap();
1063 let (offsets, weights) = dir2.entries();
1064 assert_eq!(offsets, max_offsets);
1065 assert_eq!(weights, max_weights);
1066 }
1067
1068 #[test]
1069 fn directory_from_block_rejects_posting_block() {
1070 let entries = vec![(0, 1.0), (5, 0.5)];
1071 let block = make_block(&entries);
1072 assert!(!block.is_directory());
1073 let err = DirectoryBlock::from_block(block).unwrap_err();
1074 assert!(!err.is_directory());
1075 }
1076
1077 #[test]
1078 fn deserialize_too_short_returns_err() {
1079 assert!(SparsePostingBlock::deserialize(&[0u8; 15]).is_err());
1080 assert!(SparsePostingBlock::deserialize(&[]).is_err());
1081 }
1082
1083 #[test]
1084 fn deserialize_truncated_body_returns_err() {
1085 let entries = sequential_entries(0, 1, 200, 0.5);
1086 let block = make_block(&entries);
1087 let bytes = block.serialize();
1088
1089 let truncated = &bytes[..bytes.len() - 1];
1090 let err = SparsePostingBlock::deserialize(truncated).unwrap_err();
1091 assert!(
1092 matches!(err, SparsePostingBlockError::TruncatedBody { .. }),
1093 "expected TruncatedBody, got {err:?}"
1094 );
1095 }
1096
1097 #[test]
1098 fn deserialize_truncated_directory_body_returns_err() {
1099 let dir = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
1100 let bytes = dir.into_block().serialize();
1101
1102 let truncated = &bytes[..HEADER_SIZE + 3 * 8 - 1];
1103 let err = SparsePostingBlock::deserialize(truncated).unwrap_err();
1104 assert!(
1105 matches!(err, SparsePostingBlockError::TruncatedBody { .. }),
1106 "expected TruncatedBody, got {err:?}"
1107 );
1108 }
1109
1110 #[test]
1111 fn deserialize_header_only_data_block_returns_err() {
1112 let entries = sequential_entries(0, 1, 200, 0.5);
1113 let block = make_block(&entries);
1114 let bytes = block.serialize();
1115
1116 let err = SparsePostingBlock::deserialize(&bytes[..HEADER_SIZE]).unwrap_err();
1117 assert!(matches!(err, SparsePostingBlockError::TruncatedBody { .. }));
1118 }
1119
1120 #[test]
1121 fn deserialize_extra_trailing_bytes_ignored() {
1122 let entries = sequential_entries(0, 1, 50, 0.5);
1123 let mut block = make_block(&entries);
1124 let mut bytes = block.serialize();
1125 bytes.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
1126
1127 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1128 assert_eq!(restored.offsets(), block.offsets());
1129 }
1130
1131 #[test]
1132 fn quantization_precision_random() {
1133 use std::collections::hash_map::DefaultHasher;
1134 use std::hash::{Hash, Hasher};
1135
1136 fn cheap_rng(seed: u64, i: usize) -> f32 {
1137 let mut h = DefaultHasher::new();
1138 seed.hash(&mut h);
1139 i.hash(&mut h);
1140 let bits = h.finish();
1141 (bits % 1000) as f32 / 1000.0 * 0.99 + 0.01
1142 }
1143
1144 let entries: Vec<(u32, f32)> = (0..256)
1145 .map(|i| (i as u32 * 7, cheap_rng(12345, i)))
1146 .collect();
1147
1148 let mut block = make_block(&entries);
1149 let bytes = block.serialize();
1150 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1151
1152 for (i, (&orig, &rest)) in block
1153 .values()
1154 .iter()
1155 .zip(restored.values().iter())
1156 .enumerate()
1157 {
1158 assert!(
1159 (rest - orig).abs() <= F16_TOL,
1160 "entry {i}: expected {orig} +/- {F16_TOL}, got {rest}"
1161 );
1162 }
1163 }
1164
1165 #[test]
1168 fn from_sorted_entries_empty_returns_error() {
1169 let err = SparsePostingBlock::from_sorted_entries(&[]).unwrap_err();
1170 assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
1171 }
1172
1173 #[test]
1174 fn from_sorted_entries_too_many_returns_error() {
1175 let entries: Vec<(u32, f32)> = (0..MAX_BLOCK_ENTRIES + 1)
1176 .map(|i| (i as u32, 0.5))
1177 .collect();
1178 let err = SparsePostingBlock::from_sorted_entries(&entries).unwrap_err();
1179 assert!(
1180 matches!(err, SparsePostingBlockError::TooManyEntries { count } if count == MAX_BLOCK_ENTRIES + 1)
1181 );
1182 }
1183
1184 #[test]
1185 fn from_sorted_entries_at_max_succeeds() {
1186 let entries: Vec<(u32, f32)> = (0..MAX_BLOCK_ENTRIES).map(|i| (i as u32, 0.5)).collect();
1187 let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1188 assert_eq!(block.len(), MAX_BLOCK_ENTRIES);
1189 }
1190
1191 #[test]
1192 fn directory_new_mismatched_lengths_returns_error() {
1193 let err = DirectoryBlock::new(&[1, 2, 3], &[0.5, 0.5]).unwrap_err();
1194 assert!(matches!(
1195 err,
1196 SparsePostingBlockError::MismatchedLengths {
1197 offsets: 3,
1198 weights: 2,
1199 }
1200 ));
1201 }
1202
1203 #[test]
1206 fn directory_block_offsets_values_return_empty() {
1207 let dir = DirectoryBlock::new(&[100], &[0.5]).unwrap();
1208 let mut block = dir.into_block();
1209 assert!(block.is_directory());
1210 assert_eq!(block.offsets(), &[] as &[u32]);
1211 assert_eq!(block.values(), &[] as &[f32]);
1212 }
1213
1214 fn make_dir_data(n: usize) -> (Vec<u32>, Vec<f32>) {
1217 let offsets: Vec<u32> = (0..n).map(|i| (i as u32 + 1) * 100).collect();
1218 let weights: Vec<f32> = (0..n).map(|i| 0.1 + 0.001 * i as f32).collect();
1219 (offsets, weights)
1220 }
1221
1222 #[test]
1223 fn directory_into_parts_single_part() {
1224 let (offsets, weights) = make_dir_data(10);
1225 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1226 let parts = dir.into_parts(100);
1227 assert_eq!(parts.len(), 1);
1228 assert_eq!(parts[0].num_blocks(), 10);
1229 let (o, w) = parts[0].entries();
1230 assert_eq!(o, offsets);
1231 assert_eq!(w, weights);
1232 }
1233
1234 #[test]
1235 fn directory_into_parts_exact_split() {
1236 let (offsets, weights) = make_dir_data(100);
1237 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1238 let parts = dir.into_parts(50);
1239 assert_eq!(parts.len(), 2);
1240 assert_eq!(parts[0].num_blocks(), 50);
1241 assert_eq!(parts[1].num_blocks(), 50);
1242
1243 let merged = Directory::from_parts(parts).unwrap();
1244 assert_eq!(merged.max_offsets(), &offsets[..]);
1245 assert_eq!(merged.max_weights(), &weights[..]);
1246 }
1247
1248 #[test]
1249 fn directory_into_parts_uneven_split() {
1250 let (offsets, weights) = make_dir_data(105);
1251 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1252 let parts = dir.into_parts(50);
1253 assert_eq!(parts.len(), 3);
1254 assert_eq!(parts[0].num_blocks(), 50);
1255 assert_eq!(parts[1].num_blocks(), 50);
1256 assert_eq!(parts[2].num_blocks(), 5);
1257
1258 let merged = Directory::from_parts(parts).unwrap();
1259 assert_eq!(merged.max_offsets(), &offsets[..]);
1260 assert_eq!(merged.max_weights(), &weights[..]);
1261 }
1262
1263 #[test]
1264 fn directory_into_parts_one_per_part() {
1265 let (offsets, weights) = make_dir_data(5);
1266 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1267 let parts = dir.into_parts(1);
1268 assert_eq!(parts.len(), 5);
1269 for (i, part) in parts.iter().enumerate() {
1270 assert_eq!(part.num_blocks(), 1);
1271 let (o, w) = part.entries();
1272 assert_eq!(o, vec![offsets[i]]);
1273 assert_eq!(w, vec![weights[i]]);
1274 }
1275
1276 let merged = Directory::from_parts(parts).unwrap();
1277 assert_eq!(merged.max_offsets(), &offsets[..]);
1278 assert_eq!(merged.max_weights(), &weights[..]);
1279 }
1280
1281 #[test]
1282 fn directory_into_parts_single_entry() {
1283 let dir = Directory::new(vec![42], vec![0.5]).unwrap();
1284 let parts = dir.into_parts(100);
1285 assert_eq!(parts.len(), 1);
1286 let (o, w) = parts[0].entries();
1287 assert_eq!(o, vec![42]);
1288 assert_eq!(w, vec![0.5]);
1289 }
1290
1291 #[test]
1292 fn directory_roundtrip_through_serialize() {
1293 let (offsets, weights) = make_dir_data(250);
1294 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1295 let parts = dir.into_parts(100);
1296 assert_eq!(parts.len(), 3);
1297
1298 let restored_parts: Vec<DirectoryBlock> = parts
1299 .into_iter()
1300 .map(|p| {
1301 let bytes = p.into_block().serialize();
1302 let block = SparsePostingBlock::deserialize(&bytes).unwrap();
1303 assert!(block.is_directory());
1304 DirectoryBlock::from_block(block).unwrap()
1305 })
1306 .collect();
1307
1308 let merged = Directory::from_parts(restored_parts).unwrap();
1309 assert_eq!(merged.max_offsets(), &offsets[..]);
1310 assert_eq!(merged.max_weights(), &weights[..]);
1311 }
1312
1313 #[test]
1314 fn directory_large_partitioned() {
1315 let n = 10_000;
1316 let (offsets, weights) = make_dir_data(n);
1317 let max_per_part = Directory::max_entries_for_block_size(16384);
1318
1319 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1320 let parts = dir.into_parts(max_per_part);
1321 assert!(parts.len() > 1, "should produce multiple parts at 16KiB");
1322 for part in &parts {
1323 let block = part.clone().into_block();
1324 assert!(
1325 block.serialized_size() <= 16384,
1326 "part serialized size {} exceeds 16KiB",
1327 block.serialized_size()
1328 );
1329 }
1330
1331 let merged = Directory::from_parts(parts).unwrap();
1332 assert_eq!(merged.max_offsets(), &offsets[..]);
1333 assert_eq!(merged.max_weights(), &weights[..]);
1334 }
1335
1336 #[test]
1337 fn directory_exceeds_u16_entries() {
1338 let n = 70_000; let (offsets, weights) = make_dir_data(n);
1340 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1341 assert_eq!(dir.num_blocks(), n);
1342 assert_eq!(dir.max_offsets().len(), n);
1343
1344 let parts = dir.into_parts(10_000);
1345 assert_eq!(parts.len(), 7);
1346
1347 let restored: Vec<DirectoryBlock> = parts
1349 .into_iter()
1350 .map(|p| {
1351 let bytes = p.into_block().serialize();
1352 let block = SparsePostingBlock::deserialize(&bytes).unwrap();
1353 assert!(block.is_directory());
1354 DirectoryBlock::from_block(block).unwrap()
1355 })
1356 .collect();
1357
1358 let merged = Directory::from_parts(restored).unwrap();
1359 assert_eq!(merged.num_blocks(), n);
1360 assert_eq!(merged.max_offsets(), &offsets[..]);
1361 assert_eq!(merged.max_weights(), &weights[..]);
1362 }
1363
1364 #[test]
1365 fn directory_into_parts_zero_clamps_to_one() {
1366 let (offsets, weights) = make_dir_data(3);
1367 let dir = Directory::new(offsets.clone(), weights.clone()).unwrap();
1368 let parts = dir.into_parts(0);
1369 assert_eq!(parts.len(), 3);
1370 for part in &parts {
1371 assert_eq!(part.num_blocks(), 1);
1372 }
1373 let merged = Directory::from_parts(parts).unwrap();
1374 assert_eq!(merged.max_offsets(), &offsets[..]);
1375 assert_eq!(merged.max_weights(), &weights[..]);
1376 }
1377
1378 #[test]
1379 fn directory_from_parts_preserves_dim_max() {
1380 let parts = vec![
1381 DirectoryBlock::new(&[10, 20], &[0.3, 0.5]).unwrap(),
1382 DirectoryBlock::new(&[30, 40], &[0.9, 0.1]).unwrap(),
1383 DirectoryBlock::new(&[50], &[0.6]).unwrap(),
1384 ];
1385 let merged = Directory::from_parts(parts).unwrap();
1386 assert_eq!(merged.dim_max_weight(), 0.9);
1387 assert_eq!(merged.num_blocks(), 5);
1388 }
1389
1390 #[test]
1391 fn directory_max_entries_for_block_size() {
1392 const OVERHEAD: usize = HEADER_SIZE + 256;
1393 assert_eq!(
1394 Directory::max_entries_for_block_size(16384),
1395 (16384 - OVERHEAD) / DIRECTORY_ENTRY_SIZE
1396 );
1397 assert_eq!(
1398 Directory::max_entries_for_block_size(512 * 1024),
1399 (512 * 1024 - OVERHEAD) / DIRECTORY_ENTRY_SIZE
1400 );
1401 assert_eq!(Directory::max_entries_for_block_size(OVERHEAD), 0);
1402 assert_eq!(Directory::max_entries_for_block_size(0), 0);
1403 }
1404
1405 #[test]
1406 fn directory_from_parts_single() {
1407 let part = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
1408 let dir = Directory::from_parts(vec![part]).unwrap();
1409 assert_eq!(dir.max_offsets(), &[10, 20, 30]);
1410 assert_eq!(dir.max_weights(), &[0.5, 0.9, 0.2]);
1411 }
1412
1413 #[test]
1414 fn directory_from_parts_empty_returns_error() {
1415 let err = Directory::from_parts(vec![]).unwrap_err();
1416 assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
1417 }
1418
1419 #[test]
1420 fn directory_new_empty_returns_error() {
1421 let err = Directory::new(vec![], vec![]).unwrap_err();
1422 assert!(matches!(err, SparsePostingBlockError::EmptyEntries));
1423 }
1424
1425 #[test]
1426 fn directory_new_mismatched_returns_error() {
1427 let err = Directory::new(vec![1, 2, 3], vec![0.5]).unwrap_err();
1428 assert!(matches!(
1429 err,
1430 SparsePostingBlockError::MismatchedLengths { .. }
1431 ));
1432 }
1433
1434 #[test]
1435 fn directory_prefix_constant() {
1436 assert_eq!(DIRECTORY_PREFIX, "~");
1437 }
1438
1439 #[test]
1442 fn len_and_is_empty() {
1443 let block1 = make_block(&[(0, 1.0)]);
1444 assert_eq!(block1.len(), 1);
1445 assert!(!block1.is_empty());
1446
1447 let block200 = make_block(&sequential_entries(0, 1, 200, 0.5));
1448 assert_eq!(block200.len(), 200);
1449 assert!(!block200.is_empty());
1450 }
1451
1452 #[test]
1455 fn roundtrip_high_offsets() {
1456 let base = u32::MAX - 1000;
1457 let entries: Vec<(u32, f32)> = (0..10).map(|i| (base + i * 100, 0.5)).collect();
1458 assert_roundtrip_offsets(&entries);
1459 assert_roundtrip_values(&entries);
1460 }
1461
1462 #[test]
1463 fn roundtrip_u32_max_single() {
1464 let entries = vec![(u32::MAX, 0.42)];
1465 assert_roundtrip_offsets(&entries);
1466 assert_roundtrip_values(&entries);
1467 }
1468
1469 #[test]
1472 fn roundtrip_varied_deltas() {
1473 let entries = vec![
1474 (0, 0.1),
1475 (1, 0.2),
1476 (100, 0.3),
1477 (101, 0.4),
1478 (10_000, 0.5),
1479 (10_001, 0.6),
1480 (1_000_000, 0.7),
1481 ];
1482 assert_roundtrip_offsets(&entries);
1483 assert_roundtrip_values(&entries);
1484 }
1485
1486 #[test]
1489 fn serialize_deserialize_serialize_is_stable() {
1490 for count in [1, 3, 127, 128, 129, 255, 256, 512] {
1491 let entries = sequential_entries(0, 7, count, 0.5);
1492 let block = make_block(&entries);
1493 let bytes1 = block.serialize();
1494 let restored = SparsePostingBlock::deserialize(&bytes1).unwrap();
1495 let bytes2 = restored.serialize();
1496 assert_eq!(
1497 bytes1, bytes2,
1498 "double-serialize mismatch for count={count}"
1499 );
1500 }
1501 }
1502
1503 #[test]
1506 fn raw_weight_bytes_content_correct() {
1507 let entries: Vec<(u32, f32)> = (0..5).map(|i| (i * 10, 0.1 * (i as f32 + 1.0))).collect();
1508 let block = make_block(&entries);
1509 let bytes = block.serialize();
1510 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1511 let wb = SparsePostingBlock::raw_weight_bytes(&bytes, &hdr);
1512 assert_eq!(wb.len(), 5 * 2);
1513
1514 for i in 0..5 {
1515 let f = f16::from_le_bytes([wb[i * 2], wb[i * 2 + 1]]).to_f32();
1516 assert_approx(f, entries[i].1, F16_TOL);
1517 }
1518 }
1519
1520 #[test]
1523 fn peek_header_directory_is_directory() {
1524 let dir = DirectoryBlock::new(&[10, 20, 30], &[0.5, 0.9, 0.2]).unwrap();
1525 let bytes = dir.into_block().serialize();
1526 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1527 assert_eq!(hdr.bits_per_delta, DIRECTORY_SENTINEL);
1528 }
1529
1530 #[test]
1533 fn directory_single_entry() {
1534 let dir = DirectoryBlock::new(&[42], &[0.99]).unwrap();
1535 assert_eq!(dir.num_blocks(), 1);
1536 assert_approx(dir.dim_max_weight(), 0.99, 1e-6);
1537 let (offsets, weights) = dir.entries();
1538 assert_eq!(offsets, vec![42]);
1539 assert_eq!(weights, vec![0.99]);
1540 }
1541
1542 #[test]
1545 fn convert_f16_to_f32_empty() {
1546 let mut out = vec![];
1547 convert_f16_to_f32(&[], &mut out);
1548 assert!(out.is_empty());
1549 }
1550
1551 #[test]
1552 fn convert_f16_to_f32_odd_trailing_byte_ignored() {
1553 let val = f16::from_f32(0.5);
1554 let mut input = val.to_le_bytes().to_vec();
1555 input.push(0xAB); let mut out = vec![0.0; 2];
1557 convert_f16_to_f32(&input, &mut out);
1558 assert_approx(out[0], 0.5, F16_TOL);
1559 assert_eq!(out[1], 0.0); }
1561
1562 #[test]
1565 fn convert_f16_simd_matches_scalar() {
1566 for n in [1, 3, 7, 8, 9, 15, 16, 17, 31, 63, 64, 100, 256, 1000] {
1568 let f16_bytes: Vec<u8> = (0..n)
1569 .flat_map(|i| {
1570 let val = 0.01 * (i as f32 + 1.0);
1571 f16::from_f32(val).to_le_bytes()
1572 })
1573 .collect();
1574
1575 let mut scalar_out = vec![0.0f32; n];
1576 let mut simd_out = vec![0.0f32; n];
1577
1578 convert_f16_to_f32_scalar(&f16_bytes, &mut scalar_out);
1579 convert_f16_to_f32(&f16_bytes, &mut simd_out);
1580
1581 for i in 0..n {
1582 assert!(
1583 (scalar_out[i] - simd_out[i]).abs() <= f32::EPSILON,
1584 "mismatch at n={n}, i={i}: scalar={} simd={}",
1585 scalar_out[i],
1586 simd_out[i],
1587 );
1588 }
1589 }
1590 }
1591
1592 #[test]
1595 fn zero_copy_offsets_at_boundary_sizes() {
1596 for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257, 512] {
1597 let entries = sequential_entries(10, 3, count, 0.5);
1598 let mut block = make_block(&entries);
1599 let bytes = block.serialize();
1600 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1601
1602 let mut buf = Vec::new();
1603 SparsePostingBlock::decompress_offsets_into(&bytes, &hdr, &mut buf);
1604 assert_eq!(buf.as_slice(), block.offsets(), "count={count}");
1605 }
1606 }
1607
1608 #[test]
1609 fn zero_copy_values_at_boundary_sizes() {
1610 for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257, 512] {
1611 let entries = sequential_entries(0, 1, count, 0.7);
1612 let mut block = make_block(&entries);
1613 let bytes = block.serialize();
1614 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1615
1616 let mut buf = Vec::new();
1617 SparsePostingBlock::decompress_values_into(&bytes, &hdr, &mut buf);
1618 for (i, (&a, &b)) in buf.iter().zip(block.values().iter()).enumerate() {
1619 assert!((a - b).abs() <= F16_TOL, "count={count}, i={i}: {a} vs {b}");
1620 }
1621 }
1622 }
1623
1624 #[test]
1625 fn read_value_at_boundary_sizes() {
1626 for count in [1, 2, 63, 127, 128, 129, 130, 255, 256, 257] {
1627 let entries: Vec<(u32, f32)> = (0..count)
1628 .map(|i| (i as u32 * 5, 0.1 + 0.001 * i as f32))
1629 .collect();
1630 let mut block = make_block(&entries);
1631 let bytes = block.serialize();
1632 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1633
1634 for i in 0..count {
1635 let v = SparsePostingBlock::read_value_at(&bytes, &hdr, i);
1636 assert_approx(v, block.values()[i], F16_TOL);
1637 }
1638 }
1639 }
1640}
1641
1642#[cfg(all(test, feature = "testing"))]
1643mod proptests {
1644 use super::*;
1645 use proptest::prelude::*;
1646
1647 fn arb_weight() -> impl Strategy<Value = f32> {
1648 (10u16..1000).prop_map(|weight| f32::from(weight) / 1000.0)
1650 }
1651
1652 fn arb_entries(max_count: usize) -> impl Strategy<Value = Vec<(u32, f32)>> {
1653 (1..=max_count)
1654 .prop_flat_map(|n| {
1655 (
1656 proptest::collection::vec(0u32..u32::MAX / 2, n),
1657 proptest::collection::vec(arb_weight(), n),
1658 )
1659 })
1660 .prop_map(|(mut offsets, weights)| {
1661 offsets.sort();
1662 offsets.dedup();
1663 let n = offsets.len().min(weights.len());
1664 offsets.into_iter().zip(weights).take(n).collect::<Vec<_>>()
1665 })
1666 .prop_filter("need at least one entry", |v| !v.is_empty())
1667 }
1668
1669 proptest! {
1670 #[test]
1671 fn serialize_deserialize_serialize_byte_identical(entries in arb_entries(512)) {
1672 let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1673 let bytes1 = block.serialize();
1674 let restored = SparsePostingBlock::deserialize(&bytes1).unwrap();
1675 let bytes2 = restored.serialize();
1676 prop_assert_eq!(&bytes1, &bytes2);
1677 }
1678
1679 #[test]
1680 fn roundtrip_offsets_always_match(entries in arb_entries(512)) {
1681 let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1682 let bytes = block.serialize();
1683 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1684 prop_assert_eq!(restored.offsets(), block.offsets());
1685 }
1686
1687 #[test]
1688 fn roundtrip_values_within_f16_tolerance(entries in arb_entries(512)) {
1689 let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1690 let bytes = block.serialize();
1691 let mut restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1692 for (i, (&orig, &rest)) in block
1693 .values()
1694 .iter()
1695 .zip(restored.values().iter())
1696 .enumerate()
1697 {
1698 let diff = (orig - rest).abs();
1699 prop_assert!(
1700 diff <= 1e-3,
1701 "entry {}: expected {} ± 1e-3, got {} (diff={})",
1702 i, orig, rest, diff
1703 );
1704 }
1705 }
1706
1707 #[test]
1708 fn zero_copy_matches_lazy_offsets(entries in arb_entries(512)) {
1709 let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1710 let bytes = block.serialize();
1711 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1712 let mut buf = Vec::new();
1713 SparsePostingBlock::decompress_offsets_into(&bytes, &hdr, &mut buf);
1714 prop_assert_eq!(buf.as_slice(), block.offsets());
1715 }
1716
1717 #[test]
1718 fn zero_copy_matches_lazy_values(entries in arb_entries(512)) {
1719 let mut block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1720 let bytes = block.serialize();
1721 let hdr = SparsePostingBlock::peek_header(&bytes).unwrap();
1722 let mut buf = Vec::new();
1723 SparsePostingBlock::decompress_values_into(&bytes, &hdr, &mut buf);
1724 for (i, (&a, &b)) in buf.iter().zip(block.values().iter()).enumerate() {
1725 let diff = (a - b).abs();
1726 prop_assert!(
1727 diff <= 1e-3,
1728 "entry {}: zero-copy {} vs lazy {} (diff={})",
1729 i, a, b, diff
1730 );
1731 }
1732 }
1733
1734 #[test]
1735 fn serialized_size_always_matches(entries in arb_entries(512)) {
1736 let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1737 let actual = block.serialize().len();
1738 prop_assert_eq!(block.serialized_size(), actual);
1739 }
1740
1741 #[test]
1742 fn serialized_size_survives_roundtrip(entries in arb_entries(512)) {
1743 let block = SparsePostingBlock::from_sorted_entries(&entries).unwrap();
1744 let size_before = block.serialized_size();
1745 let bytes = block.serialize();
1746 let restored = SparsePostingBlock::deserialize(&bytes).unwrap();
1747 let size_after = restored.serialized_size();
1748 prop_assert_eq!(size_before, bytes.len());
1749 prop_assert_eq!(size_after, bytes.len());
1750 prop_assert_eq!(size_before, size_after);
1751 }
1752 }
1753}