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}