Skip to main content

runtime/
precompute.rs

1//! Precomputed Static Tensors for Performance
2//!
3//! This module provides caching utilities for static tensors that don't change
4//! during inference. By precomputing these once at model initialization,
5//! we avoid redundant computation on every forward pass.
6//!
7//! Supported caches:
8//! - RoPECache: Precomputed rotary position embeddings (cos/sin frequencies)
9//! - CausalMaskCache: Precomputed causal attention masks
10//!
11//! These caches work with ALL model architectures:
12//! - RoPE: LLaMA, Qwen, Gemma, Mistral, Phi, DeepSeek, etc.
13//! - Causal mask: All autoregressive decoder models
14
15use anyhow::Result;
16use crate::tensor_core::{Tensor, Device, DataType};
17
18/// Precomputed RoPE (Rotary Position Embedding) frequencies
19///
20/// RoPE is used by most modern LLMs for position encoding.
21/// Computing cos/sin values for each position is expensive, so we
22/// precompute them once for max_seq_len positions.
23///
24/// # Example
25/// ```ignore
26/// let rope = RoPECache::new(128, 4096, 10000.0, &Device::CPU)?;
27/// let (cos, sin) = rope.get(seq_len)?;
28/// ```
29pub struct RoPECache {
30    /// Cosine values: [max_seq_len, head_dim/2]
31    cos: Tensor,
32    /// Sine values: [max_seq_len, head_dim/2]
33    sin: Tensor,
34    /// Head dimension
35    head_dim: usize,
36    /// Maximum sequence length
37    max_seq_len: usize,
38    /// RoPE base frequency (typically 10000.0)
39    base: f32,
40}
41
42impl RoPECache {
43    /// Create a new RoPE cache with precomputed frequencies
44    ///
45    /// # Arguments
46    /// * `head_dim` - Dimension of each attention head
47    /// * `max_seq_len` - Maximum sequence length to precompute
48    /// * `base` - RoPE base frequency (typically 10000.0 for LLaMA)
49    /// * `device` - Device to store tensors on
50    pub fn new(head_dim: usize, max_seq_len: usize, base: f32, device: &Device) -> Result<Self> {
51        let half_dim = head_dim / 2;
52
53        // Compute inverse frequencies: 1 / (base^(2i/d))
54        let mut inv_freq = vec![0.0f32; half_dim];
55        for i in 0..half_dim {
56            let exponent = (2.0 * i as f32) / head_dim as f32;
57            inv_freq[i] = 1.0 / base.powf(exponent);
58        }
59
60        // Compute position indices
61        let positions: Vec<f32> = (0..max_seq_len).map(|i| i as f32).collect();
62
63        // Compute angles: position * inv_freq for all positions
64        // Result shape: [max_seq_len, half_dim]
65        let mut cos_data = vec![0.0f32; max_seq_len * half_dim];
66        let mut sin_data = vec![0.0f32; max_seq_len * half_dim];
67
68        for pos in 0..max_seq_len {
69            for freq_idx in 0..half_dim {
70                let angle = positions[pos] * inv_freq[freq_idx];
71                cos_data[pos * half_dim + freq_idx] = angle.cos();
72                sin_data[pos * half_dim + freq_idx] = angle.sin();
73            }
74        }
75
76        let cos = Tensor::from_f32_slice(&cos_data, &[max_seq_len, half_dim], device)?;
77        let sin = Tensor::from_f32_slice(&sin_data, &[max_seq_len, half_dim], device)?;
78
79        Ok(Self {
80            cos,
81            sin,
82            head_dim,
83            max_seq_len,
84            base,
85        })
86    }
87
88    /// Get precomputed cos/sin for a specific sequence length
89    ///
90    /// Returns slices of the precomputed tensors for positions [0, seq_len)
91    pub fn get(&self, seq_len: usize) -> Result<(Tensor, Tensor)> {
92        if seq_len > self.max_seq_len {
93            return Err(anyhow::anyhow!(
94                "Requested seq_len {} exceeds max_seq_len {}",
95                seq_len,
96                self.max_seq_len
97            ));
98        }
99
100        // Return slices for the requested sequence length
101        let cos = self.cos.narrow(0, 0, seq_len)?;
102        let sin = self.sin.narrow(0, 0, seq_len)?;
103
104        Ok((cos, sin))
105    }
106
107    /// Get precomputed cos/sin for a range of positions
108    ///
109    /// Useful for KV cache scenarios where we only need positions [start, end)
110    pub fn get_range(&self, start: usize, end: usize) -> Result<(Tensor, Tensor)> {
111        if end > self.max_seq_len {
112            return Err(anyhow::anyhow!(
113                "Requested end {} exceeds max_seq_len {}",
114                end,
115                self.max_seq_len
116            ));
117        }
118        if start >= end {
119            return Err(anyhow::anyhow!(
120                "Invalid range: start {} >= end {}",
121                start,
122                end
123            ));
124        }
125
126        let len = end - start;
127        let cos = self.cos.narrow(0, start, len)?;
128        let sin = self.sin.narrow(0, start, len)?;
129
130        Ok((cos, sin))
131    }
132
133    /// Get the head dimension
134    pub fn head_dim(&self) -> usize {
135        self.head_dim
136    }
137
138    /// Get the maximum sequence length
139    pub fn max_seq_len(&self) -> usize {
140        self.max_seq_len
141    }
142
143    /// Get the RoPE base frequency
144    pub fn base(&self) -> f32 {
145        self.base
146    }
147}
148
149/// Precomputed causal attention mask
150///
151/// Causal masks ensure that position i can only attend to positions <= i.
152/// We precompute a mask for max_seq_len and slice as needed.
153pub struct CausalMaskCache {
154    /// Lower triangular mask: [max_seq_len, max_seq_len]
155    /// Value 0.0 means "attend", NEG_INFINITY means "don't attend"
156    mask: Tensor,
157    /// Maximum sequence length
158    max_seq_len: usize,
159}
160
161impl CausalMaskCache {
162    /// Create a new causal mask cache
163    ///
164    /// # Arguments
165    /// * `max_seq_len` - Maximum sequence length to precompute
166    /// * `device` - Device to store the mask on
167    pub fn new(max_seq_len: usize, device: &Device) -> Result<Self> {
168        // Create causal mask: 0 for positions to attend, NEG_INFINITY for masked
169        // This is the additive mask format used in attention computations
170        let mut mask_data = vec![0.0f32; max_seq_len * max_seq_len];
171
172        for i in 0..max_seq_len {
173            for j in 0..max_seq_len {
174                if j > i {
175                    // Position j comes after position i, mask it
176                    mask_data[i * max_seq_len + j] = f32::NEG_INFINITY;
177                }
178            }
179        }
180
181        let mask = Tensor::from_f32_slice(&mask_data, &[max_seq_len, max_seq_len], device)?;
182
183        Ok(Self { mask, max_seq_len })
184    }
185
186    /// Get causal mask for a specific sequence length
187    ///
188    /// Returns a [seq_len, seq_len] mask
189    pub fn get(&self, seq_len: usize) -> Result<Tensor> {
190        if seq_len > self.max_seq_len {
191            return Err(anyhow::anyhow!(
192                "Requested seq_len {} exceeds max_seq_len {}",
193                seq_len,
194                self.max_seq_len
195            ));
196        }
197
198        // Narrow both dimensions to get [seq_len, seq_len] slice
199        let mask = self.mask.narrow(0, 0, seq_len)?.narrow(1, 0, seq_len)?;
200
201        Ok(mask)
202    }
203
204    /// Get causal mask reshaped for attention broadcasting
205    ///
206    /// Returns mask with shape [1, 1, seq_len, seq_len] for broadcasting
207    /// over [batch, heads, seq, seq] attention scores
208    pub fn get_broadcast(&self, seq_len: usize) -> Result<Tensor> {
209        let mask = self.get(seq_len)?;
210        // Reshape to [1, 1, seq_len, seq_len] for broadcasting
211        mask.reshape(&[1, 1, seq_len, seq_len])
212    }
213
214    /// Get the maximum sequence length
215    pub fn max_seq_len(&self) -> usize {
216        self.max_seq_len
217    }
218}
219
220/// Precomputed sliding window attention mask
221///
222/// Each position can only attend to the previous `window_size` positions.
223/// Used by Mistral, Mixtral, and other sliding window attention models.
224pub struct SlidingWindowMaskCache {
225    /// Sliding window mask: [max_seq_len, max_seq_len]
226    mask: Tensor,
227    /// Maximum sequence length
228    max_seq_len: usize,
229    /// Window size
230    window_size: usize,
231}
232
233impl SlidingWindowMaskCache {
234    /// Create a new sliding window mask cache
235    ///
236    /// # Arguments
237    /// * `max_seq_len` - Maximum sequence length to precompute
238    /// * `window_size` - Number of positions to attend to
239    /// * `device` - Device to store the mask on
240    pub fn new(max_seq_len: usize, window_size: usize, device: &Device) -> Result<Self> {
241        let mut mask_data = vec![f32::NEG_INFINITY; max_seq_len * max_seq_len];
242
243        for i in 0..max_seq_len {
244            // Can attend to positions from max(0, i - window_size + 1) to i
245            let start = if i >= window_size { i - window_size + 1 } else { 0 };
246            for j in start..=i {
247                mask_data[i * max_seq_len + j] = 0.0;
248            }
249        }
250
251        let mask = Tensor::from_f32_slice(&mask_data, &[max_seq_len, max_seq_len], device)?;
252
253        Ok(Self {
254            mask,
255            max_seq_len,
256            window_size,
257        })
258    }
259
260    /// Get sliding window mask for a specific sequence length
261    pub fn get(&self, seq_len: usize) -> Result<Tensor> {
262        if seq_len > self.max_seq_len {
263            return Err(anyhow::anyhow!(
264                "Requested seq_len {} exceeds max_seq_len {}",
265                seq_len,
266                self.max_seq_len
267            ));
268        }
269
270        let mask = self.mask.narrow(0, 0, seq_len)?.narrow(1, 0, seq_len)?;
271        Ok(mask)
272    }
273
274    /// Get sliding window mask reshaped for attention broadcasting
275    pub fn get_broadcast(&self, seq_len: usize) -> Result<Tensor> {
276        let mask = self.get(seq_len)?;
277        mask.reshape(&[1, 1, seq_len, seq_len])
278    }
279
280    /// Get the window size
281    pub fn window_size(&self) -> usize {
282        self.window_size
283    }
284
285    /// Get the maximum sequence length
286    pub fn max_seq_len(&self) -> usize {
287        self.max_seq_len
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[test]
296    fn test_rope_cache_creation() {
297        let rope = RoPECache::new(64, 128, 10000.0, &Device::CPU).unwrap();
298        assert_eq!(rope.head_dim(), 64);
299        assert_eq!(rope.max_seq_len(), 128);
300    }
301
302    #[test]
303    fn test_rope_cache_get() {
304        let rope = RoPECache::new(64, 128, 10000.0, &Device::CPU).unwrap();
305        let (cos, sin) = rope.get(32).unwrap();
306        assert_eq!(cos.shape(), &[32, 32]); // half_dim = 64/2 = 32
307        assert_eq!(sin.shape(), &[32, 32]);
308    }
309
310    #[test]
311    fn test_causal_mask_cache() {
312        let cache = CausalMaskCache::new(64, &Device::CPU).unwrap();
313        let mask = cache.get(8).unwrap();
314        assert_eq!(mask.shape(), &[8, 8]);
315    }
316
317    #[test]
318    fn test_causal_mask_values() {
319        let cache = CausalMaskCache::new(4, &Device::CPU).unwrap();
320        let mask = cache.get(4).unwrap();
321        // Flatten to 1D to get values
322        let mask_flat = mask.reshape(&[16]).unwrap();
323        let values = mask_flat.to_vec_f32().unwrap();
324
325        // Position 0 can only attend to position 0
326        assert_eq!(values[0], 0.0);
327        assert!(values[1].is_infinite() && values[1] < 0.0);
328
329        // Position 3 can attend to all positions 0-3
330        assert_eq!(values[12], 0.0); // pos 3, attend to 0
331        assert_eq!(values[13], 0.0); // pos 3, attend to 1
332        assert_eq!(values[14], 0.0); // pos 3, attend to 2
333        assert_eq!(values[15], 0.0); // pos 3, attend to 3
334    }
335
336    #[test]
337    fn test_sliding_window_mask() {
338        let cache = SlidingWindowMaskCache::new(8, 3, &Device::CPU).unwrap();
339        let mask = cache.get(8).unwrap();
340        // Flatten to 1D to get values
341        let mask_flat = mask.reshape(&[64]).unwrap();
342        let values = mask_flat.to_vec_f32().unwrap();
343
344        // Position 5 can attend to positions 3, 4, 5 (window_size=3)
345        assert!(values[5 * 8 + 2].is_infinite()); // pos 5, can't attend to 2
346        assert_eq!(values[5 * 8 + 3], 0.0); // pos 5, can attend to 3
347        assert_eq!(values[5 * 8 + 4], 0.0); // pos 5, can attend to 4
348        assert_eq!(values[5 * 8 + 5], 0.0); // pos 5, can attend to 5
349    }
350}