Skip to main content

turboquant/
quantize.rs

1//! Core quantize / dequantize pipeline.
2//!
3//! Wires together [`codebook`], [`rotation`], and [`packed`] to provide the
4//! full TurboQuant quantization and dequantization path.
5//!
6//! ## Quantize path
7//!
8//! ```text
9//! Input: &[f32]  (key or value vector, length d)
10//! 1. scale = L2-norm(x)
11//! 2. x_normalized = x / scale
12//! 3. x_rotated = rotate(x_normalized)        // WHT + sign-flips
13//! 4. indices[i] = nearest_centroid(x_rotated[i], codebook)
14//! 5. packed = PackedBlock::new(bits, scale, indices)
15//! Output: PackedBlock
16//! ```
17//!
18//! ## Dequantize path
19//!
20//! ```text
21//! Input: PackedBlock
22//! 1. indices = block.unpack(dim)
23//! 2. x_rotated[i] = codebook.centroids[indices[i]]
24//! 3. x_normalized = inverse_rotate(x_rotated)
25//! 4. x = x_normalized * scale
26//! Output: Vec<f32>
27//! ```
28
29use half::f16;
30
31use crate::codebook::{get_codebook, nearest_centroid, Codebook};
32use crate::error::{check_values_match, Result};
33use crate::packed::{PackedBlock, TurboQuantConfig};
34use crate::rotation::{generate_sign_pattern, rotate, RotationOrder};
35
36// ---------------------------------------------------------------------------
37// Named constants (no magic numbers)
38// ---------------------------------------------------------------------------
39
40/// Minimum norm below which a vector is treated as zero to avoid division
41/// by near-zero values.
42const MIN_NORM: f32 = 1e-10;
43
44// ---------------------------------------------------------------------------
45// Pure Operation helpers (logic only, no calls to other project functions)
46// ---------------------------------------------------------------------------
47
48/// Computes the L2 norm of a vector.
49///
50/// Pure Operation: arithmetic only.
51pub fn l2_norm(data: &[f32]) -> f32 {
52    data.iter().map(|&x| x * x).sum::<f32>().sqrt()
53}
54
55/// Divides every element of `data` by `norm`, in place.
56///
57/// If `norm` is below [`MIN_NORM`], all elements are set to zero.
58///
59/// Pure Operation: arithmetic only.
60pub fn normalize_inplace(data: &mut [f32], norm: f32) {
61    if norm < MIN_NORM {
62        for v in data.iter_mut() {
63            *v = 0.0;
64        }
65    } else {
66        let inv = 1.0 / norm;
67        for v in data.iter_mut() {
68            *v *= inv;
69        }
70    }
71}
72
73/// Multiplies every element of `data` by `factor`, in place.
74///
75/// Pure Operation: arithmetic only.
76pub fn scale_inplace(data: &mut [f32], factor: f32) {
77    for v in data.iter_mut() {
78        *v *= factor;
79    }
80}
81
82/// Maps each f32 coordinate to its nearest centroid index using binary search
83/// on the codebook boundaries.
84///
85/// Pure Operation: iterates over coordinates, delegates each lookup to
86/// [`nearest_centroid`] which is a pure leaf function.
87pub fn quantize_coordinates(rotated: &[f32], codebook: &Codebook) -> Vec<u8> {
88    rotated
89        .iter()
90        .map(|&v| nearest_centroid(v as f64, codebook))
91        .collect()
92}
93
94/// Maps centroid indices back to their f32 centroid values.
95///
96/// Pure Operation: index lookup only.
97pub fn lookup_centroids(indices: &[u8], codebook: &Codebook) -> Vec<f32> {
98    indices
99        .iter()
100        .map(|&idx| codebook.centroids[idx as usize] as f32)
101        .collect()
102}
103
104/// Maps centroid indices into a caller-provided buffer, avoiding allocation.
105///
106/// Hot-path variant of [`lookup_centroids`]: reuses `out` across repeated
107/// calls to eliminate per-key allocations in attention loops.
108///
109/// Pure Operation: index lookup only.
110pub fn lookup_centroids_into(indices: &[u8], codebook: &Codebook, out: &mut Vec<f32>) {
111    out.clear();
112    out.extend(
113        indices
114            .iter()
115            .map(|&idx| codebook.centroids[idx as usize] as f32),
116    );
117}
118
119/// Selects the scale factor: zero if the norm is negligible, otherwise the norm
120/// converted to f16.
121///
122/// Pure Operation: comparison and external conversion only, no project calls.
123fn select_scale(norm: f32) -> f16 {
124    let effective = if norm < MIN_NORM { 0.0 } else { norm };
125    f16::from_f32(effective)
126}
127
128// ---------------------------------------------------------------------------
129// Integration: quantize_vec
130// ---------------------------------------------------------------------------
131
132/// Quantizes a floating-point vector into a packed [`PackedBlock`].
133///
134/// Pure Integration: orchestrates `check_values_match`, `get_codebook`,
135/// `generate_sign_pattern`, `l2_norm`, `normalize_inplace` (handles zero-norm
136/// internally), `rotate`, `quantize_coordinates`, `select_scale`, and
137/// `PackedBlock::new`.
138///
139/// # Errors
140///
141/// Returns [`TurboQuantError::DimensionMismatch`] if `data.len() != config.dim`.
142pub fn quantize_vec(config: &TurboQuantConfig, data: &[f32]) -> Result<PackedBlock> {
143    check_values_match(data.len(), config.dim)?;
144
145    let codebook = get_codebook(config.bits, config.dim)?;
146    let sign_pattern = generate_sign_pattern(config.dim, config.rotation_seed);
147    let norm = l2_norm(data);
148
149    let mut working = data.to_vec();
150    normalize_inplace(&mut working, norm);
151    rotate(&mut working, &sign_pattern, RotationOrder::Forward)?;
152
153    let indices = quantize_coordinates(&working, &codebook);
154    let scale = select_scale(norm);
155
156    Ok(PackedBlock::new(config.bits, scale, &indices))
157}
158
159/// Quantizes a floating-point vector into a packed [`PackedBlock`] using
160/// pre-fetched codebook and sign pattern.
161///
162/// Hot-path variant of [`quantize_vec`]: avoids repeated codebook allocation
163/// and sign-pattern generation when quantizing many vectors with the same
164/// config (e.g. in batch KV-cache insertion during prefill).
165///
166/// Integration: orchestrates `check_values_match`, `l2_norm`,
167/// `normalize_inplace`, `rotate`, `quantize_coordinates`, `select_scale`,
168/// and `PackedBlock::new` -- all using caller-provided codebook and sign
169/// pattern.
170///
171/// # Errors
172///
173/// Returns [`TurboQuantError::DimensionMismatch`] if `data.len() != config.dim`.
174pub fn quantize_vec_with_codebook(
175    config: &TurboQuantConfig,
176    data: &[f32],
177    codebook: &Codebook,
178    sign_pattern: &[f32],
179) -> Result<PackedBlock> {
180    check_values_match(data.len(), config.dim)?;
181
182    let norm = l2_norm(data);
183
184    let mut working = data.to_vec();
185    normalize_inplace(&mut working, norm);
186    rotate(&mut working, sign_pattern, RotationOrder::Forward)?;
187
188    let indices = quantize_coordinates(&working, codebook);
189    let scale = select_scale(norm);
190
191    Ok(PackedBlock::new(config.bits, scale, &indices))
192}
193
194// ---------------------------------------------------------------------------
195// Integration: dequantize_vec
196// ---------------------------------------------------------------------------
197
198/// Dequantizes a [`PackedBlock`] back into a floating-point vector.
199///
200/// Integration: unpacks indices, looks up centroids, applies inverse rotation,
201/// and scales by the stored norm.
202///
203/// # Errors
204///
205/// Returns an error if the inverse rotation fails (should not happen if the
206/// block was produced by [`quantize_vec`] with valid config).
207pub fn dequantize_vec(config: &TurboQuantConfig, block: &PackedBlock) -> Result<Vec<f32>> {
208    let codebook = get_codebook(config.bits, config.dim)?;
209    let sign_pattern = generate_sign_pattern(config.dim, config.rotation_seed);
210    dequantize_vec_with_codebook(config, block, &codebook, &sign_pattern)
211}
212
213/// Dequantizes a [`PackedBlock`] using a pre-fetched codebook and sign pattern.
214///
215/// Hot-path variant: avoids repeated codebook allocation and sign-pattern
216/// generation when dequantizing many blocks with the same config (e.g. in
217/// attention score computation).
218///
219/// Integration: unpacks indices, looks up centroids, applies inverse rotation,
220/// and scales by the stored norm.
221///
222/// # Errors
223///
224/// Returns an error if the inverse rotation fails.
225pub fn dequantize_vec_with_codebook(
226    config: &TurboQuantConfig,
227    block: &PackedBlock,
228    codebook: &Codebook,
229    sign_pattern: &[f32],
230) -> Result<Vec<f32>> {
231    let indices = block.unpack(config.dim);
232    let mut reconstructed = lookup_centroids(&indices, codebook);
233
234    rotate(&mut reconstructed, sign_pattern, RotationOrder::Inverse)?;
235
236    let scale = block.scale.to_f32();
237    scale_inplace(&mut reconstructed, scale);
238
239    Ok(reconstructed)
240}
241
242/// Dequantizes a [`PackedBlock`] into a caller-provided buffer, avoiding
243/// allocation on the hot path.
244///
245/// Uses pre-fetched codebook and sign pattern, plus caller-owned scratch
246/// buffers for indices and output.  Designed for tight loops (attention
247/// score / weighted value computation).
248///
249/// Integration: unpacks indices, looks up centroids, applies inverse rotation,
250/// and scales by the stored norm -- all into caller-provided buffers.
251///
252/// # Errors
253///
254/// Returns an error if the inverse rotation fails.
255pub fn dequantize_into_with_codebook(
256    config: &TurboQuantConfig,
257    block: &PackedBlock,
258    codebook: &Codebook,
259    sign_pattern: &[f32],
260    scratch: &mut DequantScratch,
261) -> Result<()> {
262    block.unpack_into(config.dim, &mut scratch.indices);
263    lookup_centroids_into(&scratch.indices, codebook, &mut scratch.values);
264    rotate(&mut scratch.values, sign_pattern, RotationOrder::Inverse)?;
265    scale_inplace(&mut scratch.values, block.scale.to_f32());
266    Ok(())
267}
268
269/// Pre-allocated scratch buffers for hot-path dequantization.
270///
271/// Avoids per-key heap allocation in `dequantize_into_with_codebook`.
272pub struct DequantScratch {
273    /// Buffer for unpacked indices.
274    pub(crate) indices: Vec<u8>,
275    /// Buffer for reconstructed f32 values.
276    pub(crate) values: Vec<f32>,
277}
278
279impl DequantScratch {
280    /// Creates scratch buffers pre-allocated for the given dimension.
281    pub fn new(dim: usize) -> Self {
282        Self {
283            indices: Vec::with_capacity(dim),
284            values: Vec::with_capacity(dim),
285        }
286    }
287}
288
289// ---------------------------------------------------------------------------
290// Integration: dequantize_rotated
291// ---------------------------------------------------------------------------
292
293/// Dequantizes a [`PackedBlock`] but *skips* the inverse rotation.
294///
295/// Returns the reconstructed vector in the **rotated domain**.  This is used
296/// by the attention optimization (Phase A6) where queries are pre-rotated so
297/// that the dot product can be computed directly in rotated space.
298///
299/// Integration: unpacks indices, looks up centroids, scales -- but does NOT
300/// call `inverse_rotate`.
301///
302/// # Errors
303///
304/// Returns an error if the codebook lookup fails.
305// qual:api -- public API for rotated-domain attention optimization
306pub fn dequantize_rotated(config: &TurboQuantConfig, block: &PackedBlock) -> Result<Vec<f32>> {
307    let codebook = get_codebook(config.bits, config.dim)?;
308
309    let indices = block.unpack(config.dim);
310    let mut reconstructed = lookup_centroids(&indices, &codebook);
311
312    let scale = block.scale.to_f32();
313    scale_inplace(&mut reconstructed, scale);
314
315    Ok(reconstructed)
316}
317
318// ---------------------------------------------------------------------------
319// Unit tests
320// ---------------------------------------------------------------------------
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::packed::{BITS_TQ2, BITS_TQ3, BITS_TQ4};
326    use crate::test_utils::pseudo_random_vec;
327
328    /// Test dimension for unit tests.
329    const TEST_DIM: usize = 64;
330    /// Small dimension for block-construction tests.
331    const TEST_SMALL_DIM: usize = 8;
332    /// Test seed for reproducible sign patterns.
333    const TEST_SEED: u64 = 42;
334    /// Seed for 3-bit roundtrip test.
335    const TEST_SEED_3BIT: u64 = 12345;
336    /// Seed for 4-bit roundtrip test.
337    const TEST_SEED_4BIT: u64 = 54321;
338    /// Seed for determinism test.
339    const TEST_SEED_DETERM: u64 = 99999;
340    /// Seed for rotated-domain test.
341    const TEST_SEED_ROTATED: u64 = 77777;
342    /// Tolerance for floating-point comparisons.
343    const FLOAT_EPSILON: f32 = 1e-6;
344    /// Tolerance for norm comparisons (f16 introduces rounding).
345    const NORM_EPSILON: f32 = 0.02;
346    /// Maximum acceptable relative error for a single-vector roundtrip.
347    /// Individual vectors can have much higher error than the aggregate MSE;
348    /// the proper quality gate is in mse_validation.rs.
349    const MAX_SINGLE_VEC_RELATIVE_ERROR: f32 = 1.0;
350    /// Scale value for known-norm test (||[3,4]|| = 5).
351    const TEST_SCALE_VALUE: f32 = 5.0;
352    /// Norm value used in normalize and scale tests.
353    const TEST_NORM_VALUE: f32 = 2.0;
354    /// Constant value A for block scale tests.
355    const TEST_CONST_VAL_A: f32 = 2.5;
356    /// Constant value B for block scale tests.
357    const TEST_CONST_VAL_B: f32 = 3.0;
358
359    // -- l2_norm --------------------------------------------------------------
360
361    #[test]
362    fn l2_norm_of_unit_vector() {
363        let mut v = vec![0.0_f32; TEST_DIM];
364        v[0] = 1.0;
365        let norm = l2_norm(&v);
366        assert!((norm - 1.0).abs() < FLOAT_EPSILON);
367    }
368
369    #[test]
370    fn l2_norm_of_zero_vector() {
371        let v = vec![0.0_f32; TEST_DIM];
372        let norm = l2_norm(&v);
373        assert!(norm < FLOAT_EPSILON);
374    }
375
376    #[test]
377    fn l2_norm_of_known_vector() {
378        // [3, 4] -> norm = 5
379        let v = vec![3.0_f32, 4.0];
380        let norm = l2_norm(&v);
381        assert!((norm - TEST_SCALE_VALUE).abs() < FLOAT_EPSILON);
382    }
383
384    // -- normalize_inplace ----------------------------------------------------
385
386    #[test]
387    fn normalize_inplace_unit_result() {
388        let mut v = vec![3.0_f32, 4.0];
389        normalize_inplace(&mut v, TEST_SCALE_VALUE);
390        assert!((v[0] - 0.6).abs() < FLOAT_EPSILON);
391        assert!((v[1] - 0.8).abs() < FLOAT_EPSILON);
392    }
393
394    #[test]
395    fn normalize_inplace_zero_norm_gives_zeros() {
396        let mut v = vec![1.0_f32, 2.0, 3.0];
397        normalize_inplace(&mut v, 0.0);
398        for &val in &v {
399            assert!(val.abs() < FLOAT_EPSILON);
400        }
401    }
402
403    // -- scale_inplace --------------------------------------------------------
404
405    #[test]
406    fn scale_inplace_doubles() {
407        let mut v = vec![1.0_f32, 2.0, 3.0];
408        scale_inplace(&mut v, TEST_NORM_VALUE);
409        assert!((v[0] - 2.0).abs() < FLOAT_EPSILON);
410        assert!((v[1] - 4.0).abs() < FLOAT_EPSILON);
411        assert!((v[2] - 6.0).abs() < FLOAT_EPSILON);
412    }
413
414    // -- values_match (from error.rs) / is_zero_norm --------------------------
415
416    #[test]
417    fn values_match_true() {
418        assert!(crate::error::values_match(128, 128));
419    }
420
421    #[test]
422    fn values_match_false() {
423        assert!(!crate::error::values_match(64, 128));
424    }
425
426    #[test]
427    fn select_scale_zero_for_tiny_norm() {
428        assert_eq!(select_scale(1e-11).to_f32(), 0.0);
429    }
430
431    #[test]
432    fn select_scale_preserves_normal_norm() {
433        assert!((select_scale(1.0).to_f32() - 1.0).abs() < FLOAT_EPSILON);
434    }
435
436    // -- quantize_coordinates / lookup_centroids roundtrip ---------------------
437
438    #[test]
439    fn quantize_lookup_roundtrip_preserves_structure() {
440        let codebook = get_codebook(BITS_TQ3, TEST_DIM).unwrap();
441        // Use centroid values directly; quantize should map them back.
442        let coords: Vec<f32> = codebook.centroids.iter().map(|&c| c as f32).collect();
443        let indices = quantize_coordinates(&coords, &codebook);
444        let recovered = lookup_centroids(&indices, &codebook);
445        // Each recovered value should be exactly the centroid.
446        for (i, (&orig, &rec)) in coords.iter().zip(recovered.iter()).enumerate() {
447            assert!(
448                (orig - rec).abs() < 0.01,
449                "mismatch at index {i}: orig={orig}, rec={rec}"
450            );
451        }
452    }
453
454    // -- PackedBlock construction ---------------------------------------------
455
456    #[test]
457    fn packed_block_tq3() {
458        let indices = vec![0u8; TEST_DIM];
459        let block = PackedBlock::new(BITS_TQ3, f16::from_f32(1.0), &indices);
460        assert_eq!(block.bits, BITS_TQ3);
461    }
462
463    #[test]
464    fn packed_block_tq4() {
465        let indices = vec![0u8; TEST_DIM];
466        let block = PackedBlock::new(BITS_TQ4, f16::from_f32(1.0), &indices);
467        assert_eq!(block.bits, BITS_TQ4);
468    }
469
470    // -- quantize rejects dimension mismatch ----------------------------------
471
472    #[test]
473    fn quantize_vec_rejects_wrong_dimension() {
474        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM).unwrap();
475        let data = vec![1.0_f32; TEST_DIM + 1];
476        assert!(quantize_vec(&config, &data).is_err());
477    }
478
479    // -- quantize/dequantize roundtrip ----------------------------------------
480
481    #[test]
482    fn quantize_dequantize_roundtrip_3bit() {
483        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM)
484            .unwrap()
485            .with_seed(TEST_SEED);
486        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_3BIT);
487        let block = quantize_vec(&config, &data).unwrap();
488        let recovered = dequantize_vec(&config, &block).unwrap();
489
490        let orig_norm = l2_norm(&data);
491        let err_norm = l2_norm(
492            &data
493                .iter()
494                .zip(recovered.iter())
495                .map(|(&a, &b)| a - b)
496                .collect::<Vec<_>>(),
497        );
498        let relative_error = err_norm / orig_norm;
499        // Single-vector relative error can be high; the aggregate MSE check
500        // (mse_validation.rs) is the real quality gate.  Here we just verify
501        // the pipeline produces a reasonable reconstruction.
502        assert!(
503            relative_error < MAX_SINGLE_VEC_RELATIVE_ERROR,
504            "relative error too large: {relative_error}"
505        );
506    }
507
508    #[test]
509    fn quantize_dequantize_roundtrip_4bit() {
510        let config = TurboQuantConfig::new(BITS_TQ4, TEST_DIM)
511            .unwrap()
512            .with_seed(TEST_SEED);
513        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_4BIT);
514        let block = quantize_vec(&config, &data).unwrap();
515        let recovered = dequantize_vec(&config, &block).unwrap();
516
517        let orig_norm = l2_norm(&data);
518        let err_norm = l2_norm(
519            &data
520                .iter()
521                .zip(recovered.iter())
522                .map(|(&a, &b)| a - b)
523                .collect::<Vec<_>>(),
524        );
525        let relative_error = err_norm / orig_norm;
526        assert!(
527            relative_error < MAX_SINGLE_VEC_RELATIVE_ERROR,
528            "relative error too large: {relative_error}"
529        );
530    }
531
532    // -- zero vector ----------------------------------------------------------
533
534    #[test]
535    fn quantize_zero_vector_does_not_panic() {
536        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM)
537            .unwrap()
538            .with_seed(TEST_SEED);
539        let data = vec![0.0_f32; TEST_DIM];
540        let block = quantize_vec(&config, &data).unwrap();
541        let recovered = dequantize_vec(&config, &block).unwrap();
542        let recovered_norm = l2_norm(&recovered);
543        assert!(
544            recovered_norm < NORM_EPSILON,
545            "recovered norm should be near zero, got {recovered_norm}"
546        );
547    }
548
549    // -- determinism ----------------------------------------------------------
550
551    #[test]
552    fn quantize_is_deterministic() {
553        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM)
554            .unwrap()
555            .with_seed(TEST_SEED);
556        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_DETERM);
557
558        let block_a = quantize_vec(&config, &data).unwrap();
559        let block_b = quantize_vec(&config, &data).unwrap();
560
561        let rec_a = dequantize_vec(&config, &block_a).unwrap();
562        let rec_b = dequantize_vec(&config, &block_b).unwrap();
563
564        assert_eq!(rec_a, rec_b);
565    }
566
567    // -- dequantize_rotated differs from dequantize ---------------------------
568
569    #[test]
570    fn dequantize_rotated_differs_from_full() {
571        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM)
572            .unwrap()
573            .with_seed(TEST_SEED);
574        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_ROTATED);
575        let block = quantize_vec(&config, &data).unwrap();
576
577        let full = dequantize_vec(&config, &block).unwrap();
578        let rotated = dequantize_rotated(&config, &block).unwrap();
579
580        // They should differ in coordinates...
581        assert_ne!(full, rotated);
582        // ...but have approximately the same norm (rotation preserves norm).
583        let full_norm = l2_norm(&full);
584        let rotated_norm = l2_norm(&rotated);
585        assert!(
586            (full_norm - rotated_norm).abs() < NORM_EPSILON,
587            "norms should be approximately equal: full={full_norm}, rotated={rotated_norm}"
588        );
589    }
590
591    // -- PackedBlock scale and size -------------------------------------------
592
593    #[test]
594    fn packed_block_scale_tq3() {
595        let block = PackedBlock::new(
596            BITS_TQ3,
597            f16::from_f32(TEST_CONST_VAL_A),
598            &[0u8; TEST_SMALL_DIM],
599        );
600        assert!((block.scale.to_f32() - TEST_CONST_VAL_A).abs() < 0.01);
601    }
602
603    #[test]
604    fn packed_block_scale_tq4() {
605        let block = PackedBlock::new(
606            BITS_TQ4,
607            f16::from_f32(TEST_CONST_VAL_B),
608            &[0u8; TEST_SMALL_DIM],
609        );
610        assert!((block.scale.to_f32() - TEST_CONST_VAL_B).abs() < 0.01);
611    }
612
613    // -- PackedBlock::size_bytes ----------------------------------------------
614
615    #[test]
616    fn packed_block_size_bytes_tq3() {
617        let config = TurboQuantConfig::new(BITS_TQ3, TEST_DIM)
618            .unwrap()
619            .with_seed(TEST_SEED);
620        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_3BIT);
621        let block = quantize_vec(&config, &data).unwrap();
622
623        // size_bytes = 2 (scale) + packed data length
624        assert!(block.size_bytes() > 2);
625    }
626
627    #[test]
628    fn packed_block_size_bytes_tq4() {
629        let config = TurboQuantConfig::new(BITS_TQ4, TEST_DIM)
630            .unwrap()
631            .with_seed(TEST_SEED);
632        let data = pseudo_random_vec(TEST_DIM, TEST_SEED_4BIT);
633        let block = quantize_vec(&config, &data).unwrap();
634
635        assert!(block.size_bytes() > 2);
636    }
637
638    // -----------------------------------------------------------------------
639    // PolarQuant block size verification tests
640    // -----------------------------------------------------------------------
641
642    /// Dimension for block-size verification tests.
643    const BLOCK_SIZE_DIM: usize = 128;
644
645    /// Bytes per f16 scale field.
646    const SCALE_BYTES: usize = 2;
647
648    /// Expected packed size for 2-bit polar, d=128:
649    /// packed_indices = 128 / 4 = 32 bytes, + 2 scale = 34 bytes.
650    const TQ2_D128_EXPECTED_BYTES: usize = 34;
651
652    /// Expected packed size for 3-bit polar, d=128:
653    /// packed_indices = 128 * 3 / 8 = 48 bytes, + 2 scale = 50 bytes.
654    const TQ3_D128_EXPECTED_BYTES: usize = 50;
655
656    /// Expected packed size for 4-bit polar, d=128:
657    /// packed_indices = 128 / 2 = 64 bytes, + 2 scale = 66 bytes.
658    const TQ4_D128_EXPECTED_BYTES: usize = 66;
659
660    /// Seed for block-size verification tests.
661    const BLOCK_SIZE_SEED: u64 = 42;
662
663    /// Seed for 2-bit block-size test data.
664    const BLOCK_SIZE_DATA_SEED_2: u64 = 20001;
665
666    /// Seed for 3-bit block-size test data.
667    const BLOCK_SIZE_DATA_SEED_3: u64 = 30001;
668
669    /// Seed for 4-bit block-size test data.
670    const BLOCK_SIZE_DATA_SEED_4: u64 = 40001;
671
672    #[test]
673    fn polar_block_size_2bit_d128() {
674        let config = TurboQuantConfig::new(BITS_TQ2, BLOCK_SIZE_DIM)
675            .unwrap()
676            .with_seed(BLOCK_SIZE_SEED);
677        let data = pseudo_random_vec(BLOCK_SIZE_DIM, BLOCK_SIZE_DATA_SEED_2);
678        let block = quantize_vec(&config, &data).unwrap();
679
680        assert_eq!(
681            block.size_bytes(),
682            TQ2_D128_EXPECTED_BYTES,
683            "2-bit polar block for d={BLOCK_SIZE_DIM}: expected {TQ2_D128_EXPECTED_BYTES} bytes, \
684             got {} (scale={SCALE_BYTES}, packed={})",
685            block.size_bytes(),
686            block.size_bytes() - SCALE_BYTES
687        );
688    }
689
690    #[test]
691    fn polar_block_size_3bit_d128() {
692        let config = TurboQuantConfig::new(BITS_TQ3, BLOCK_SIZE_DIM)
693            .unwrap()
694            .with_seed(BLOCK_SIZE_SEED);
695        let data = pseudo_random_vec(BLOCK_SIZE_DIM, BLOCK_SIZE_DATA_SEED_3);
696        let block = quantize_vec(&config, &data).unwrap();
697
698        assert_eq!(
699            block.size_bytes(),
700            TQ3_D128_EXPECTED_BYTES,
701            "3-bit polar block for d={BLOCK_SIZE_DIM}: expected {TQ3_D128_EXPECTED_BYTES} bytes, \
702             got {} (scale={SCALE_BYTES}, packed={})",
703            block.size_bytes(),
704            block.size_bytes() - SCALE_BYTES
705        );
706    }
707
708    #[test]
709    fn polar_block_size_4bit_d128() {
710        let config = TurboQuantConfig::new(BITS_TQ4, BLOCK_SIZE_DIM)
711            .unwrap()
712            .with_seed(BLOCK_SIZE_SEED);
713        let data = pseudo_random_vec(BLOCK_SIZE_DIM, BLOCK_SIZE_DATA_SEED_4);
714        let block = quantize_vec(&config, &data).unwrap();
715
716        assert_eq!(
717            block.size_bytes(),
718            TQ4_D128_EXPECTED_BYTES,
719            "4-bit polar block for d={BLOCK_SIZE_DIM}: expected {TQ4_D128_EXPECTED_BYTES} bytes, \
720             got {} (scale={SCALE_BYTES}, packed={})",
721            block.size_bytes(),
722            block.size_bytes() - SCALE_BYTES
723        );
724    }
725}