Skip to main content

fib_quant/
rope.rs

1//! RoPE-aware bit allocation for KV-cache quantization.
2//!
3//! Provides CPU-side, deterministic infrastructure for assigning variable
4//! bit-widths to RoPE frequency blocks.  The greedy energy heuristic is
5//! experimental and inspired by block-level frequency analysis; it does
6//! **not** claim equivalence with any published algorithm.
7
8/// A contiguous pair of head dimensions forming one RoPE frequency block.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct RopeBlock {
11    /// Zero-based index of this block within the head.
12    pub block_index: usize,
13    /// Inclusive start dimension (always even).
14    pub dim_start: usize,
15    /// Exclusive end dimension (`dim_start + 2`).
16    pub dim_end: usize,
17}
18
19/// Partition `head_dim` into contiguous 2-D RoPE blocks.
20///
21/// Each block covers `[2k, 2k+2)` for `k in 0..head_dim/2`.  When `head_dim`
22/// is odd the single trailing dimension is silently ignored; only
23/// `head_dim & !1` dimensions are covered.
24pub fn rope_blocks(head_dim: usize) -> Vec<RopeBlock> {
25    (0..head_dim / 2)
26        .map(|k| RopeBlock {
27            block_index: k,
28            dim_start: 2 * k,
29            dim_end: 2 * k + 2,
30        })
31        .collect()
32}
33
34/// Energy estimate for one RoPE block.
35#[derive(Debug, Clone, Copy)]
36pub struct RopeBlockEnergy {
37    pub block: RopeBlock,
38    /// Mean squared value across all key vectors that cover this block.
39    pub energy: f32,
40}
41
42/// Compute per-block energy from a set of key vectors.
43///
44/// For each block covering `[dim_start, dim_end)`, only key vectors whose
45/// length is `>= dim_end` contribute.  Vectors that are too short to reach a
46/// block are silently skipped for that block.  If no vector covers a block its
47/// energy is `0.0`.
48pub fn rope_block_energies(keys: &[Vec<f32>], head_dim: usize) -> Vec<RopeBlockEnergy> {
49    rope_blocks(head_dim)
50        .into_iter()
51        .map(|block| {
52            let mut sum_sq = 0.0f64;
53            let mut count = 0usize;
54            for key in keys {
55                if key.len() < block.dim_end {
56                    continue;
57                }
58                for &v in &key[block.dim_start..block.dim_end] {
59                    sum_sq += (v as f64) * (v as f64);
60                }
61                count += 1;
62            }
63            let energy = if count == 0 {
64                0.0f32
65            } else {
66                (sum_sq / ((count * 2) as f64)) as f32
67            };
68            RopeBlockEnergy { block, energy }
69        })
70        .collect()
71}
72
73/// Bit-width assignment for every RoPE block in a head.
74#[derive(Debug, Clone)]
75pub struct RopeBitAllocation {
76    /// Bit width assigned to block `i`.
77    pub bits_per_block: Vec<u8>,
78    /// Sum of `bits_per_block`.
79    pub total_bits: usize,
80}
81
82/// Greedily allocate bits across RoPE blocks.
83///
84/// Initializes each block to `min_bits`, then spends the remaining budget one
85/// bit at a time on the highest-energy block that has not yet reached
86/// `max_bits`.  Equal-energy ties are broken by lower `block_index`.
87///
88/// Edge cases:
89/// * If `total_bits < min_bits * n_blocks` the minimum floor is applied and
90///   `RopeBitAllocation::total_bits` is set to `min_bits * n_blocks`.
91/// * If `max_bits < min_bits`, `max_bits` is clamped up to `min_bits`; all
92///   blocks receive exactly `min_bits`.
93/// * An empty `energies` slice returns an empty allocation with `total_bits = 0`.
94pub fn allocate_rope_bits(
95    energies: &[RopeBlockEnergy],
96    min_bits: u8,
97    max_bits: u8,
98    total_bits: usize,
99) -> RopeBitAllocation {
100    let n = energies.len();
101    if n == 0 {
102        return RopeBitAllocation {
103            bits_per_block: vec![],
104            total_bits: 0,
105        };
106    }
107
108    let effective_max = max_bits.max(min_bits);
109    let mut alloc: Vec<u8> = vec![min_bits; n];
110    let min_total = (min_bits as usize) * n;
111
112    if total_bits <= min_total {
113        return RopeBitAllocation {
114            bits_per_block: alloc,
115            total_bits: min_total,
116        };
117    }
118
119    let mut remaining = total_bits - min_total;
120    while remaining > 0 {
121        // Pick the eligible block with the highest energy; lower block_index
122        // breaks ties deterministically.
123        let best = energies
124            .iter()
125            .enumerate()
126            .filter(|(i, _)| alloc[*i] < effective_max)
127            .max_by(|(ia, a), (ib, b)| {
128                a.energy
129                    .partial_cmp(&b.energy)
130                    .unwrap_or(std::cmp::Ordering::Equal)
131                    .then(ib.cmp(ia)) // lower index wins on tie
132            });
133        match best {
134            None => break, // all blocks at cap
135            Some((idx, _)) => {
136                alloc[idx] += 1;
137                remaining -= 1;
138            }
139        }
140    }
141
142    let actual_total: usize = alloc.iter().map(|&b| b as usize).sum();
143    RopeBitAllocation {
144        bits_per_block: alloc,
145        total_bits: actual_total,
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn rope_blocks_even_head_dim_returns_2d_spans() {
155        let blocks = rope_blocks(8);
156        assert_eq!(blocks.len(), 4);
157        assert_eq!(
158            blocks[0],
159            RopeBlock {
160                block_index: 0,
161                dim_start: 0,
162                dim_end: 2
163            }
164        );
165        assert_eq!(
166            blocks[1],
167            RopeBlock {
168                block_index: 1,
169                dim_start: 2,
170                dim_end: 4
171            }
172        );
173        assert_eq!(
174            blocks[2],
175            RopeBlock {
176                block_index: 2,
177                dim_start: 4,
178                dim_end: 6
179            }
180        );
181        assert_eq!(
182            blocks[3],
183            RopeBlock {
184                block_index: 3,
185                dim_start: 6,
186                dim_end: 8
187            }
188        );
189    }
190
191    #[test]
192    fn rope_blocks_odd_head_dim_ignores_trailing_dim() {
193        // head_dim=9 → 4 blocks; dimension index 8 is the trailing odd dim, not covered
194        let blocks = rope_blocks(9);
195        assert_eq!(blocks.len(), 4);
196        assert_eq!(blocks.last().unwrap().dim_end, 8);
197    }
198
199    #[test]
200    fn rope_block_energies_rank_high_energy_block_first() {
201        // Block 1 ([2,4)) has energy 100, block 0 ([0,2)) is zero
202        let keys = vec![vec![0.0f32, 0.0, 10.0, 10.0], vec![0.0f32, 0.0, 10.0, 10.0]];
203        let energies = rope_block_energies(&keys, 4);
204        assert_eq!(energies.len(), 2);
205        assert!(energies[1].energy > energies[0].energy);
206        assert_eq!(energies[0].energy, 0.0);
207        assert!((energies[1].energy - 100.0f32).abs() < 1e-4);
208    }
209
210    #[test]
211    fn rope_block_energies_empty_keys_are_zero() {
212        let energies = rope_block_energies(&[], 8);
213        assert_eq!(energies.len(), 4);
214        for be in &energies {
215            assert_eq!(be.energy, 0.0);
216        }
217    }
218
219    #[test]
220    fn allocate_rope_bits_honors_min_max_and_budget() {
221        // 4 blocks, min=2 max=4, budget=12; base=8, 4 extra bits to spread
222        let blocks = rope_blocks(8);
223        let energies: Vec<RopeBlockEnergy> = blocks
224            .iter()
225            .map(|&block| RopeBlockEnergy { block, energy: 1.0 })
226            .collect();
227        let alloc = allocate_rope_bits(&energies, 2, 4, 12);
228        assert_eq!(alloc.total_bits, 12);
229        for &b in &alloc.bits_per_block {
230            assert!((2..=4).contains(&b), "got {b}");
231        }
232    }
233
234    #[test]
235    fn allocate_rope_bits_prefers_high_energy_blocks() {
236        let blocks = rope_blocks(8);
237        let energies: Vec<RopeBlockEnergy> = blocks
238            .iter()
239            .enumerate()
240            .map(|(i, &block)| RopeBlockEnergy {
241                block,
242                energy: if i == 3 { 100.0 } else { 1.0 },
243            })
244            .collect();
245        // Exactly one extra bit above the min-total floor
246        let alloc = allocate_rope_bits(&energies, 2, 8, 2 * 4 + 1);
247        assert_eq!(
248            alloc.bits_per_block[3], 3,
249            "extra bit must go to highest-energy block"
250        );
251        assert_eq!(alloc.bits_per_block[0], 2);
252    }
253
254    #[test]
255    fn allocate_rope_bits_tie_breaks_by_block_index() {
256        let blocks = rope_blocks(8);
257        // All blocks have identical energy
258        let energies: Vec<RopeBlockEnergy> = blocks
259            .iter()
260            .map(|&block| RopeBlockEnergy { block, energy: 5.0 })
261            .collect();
262        // Exactly one extra bit; must land on block 0 (lowest index)
263        let alloc = allocate_rope_bits(&energies, 2, 8, 2 * 4 + 1);
264        assert_eq!(
265            alloc.bits_per_block[0], 3,
266            "tie must break to lower block_index"
267        );
268        assert_eq!(alloc.bits_per_block[1], 2);
269        assert_eq!(alloc.bits_per_block[2], 2);
270        assert_eq!(alloc.bits_per_block[3], 2);
271    }
272}