1use crate::spatial::octree::Octree;
11use crate::storage::data_structures::NodePoint;
12use glam::Vec3;
13use std::collections::HashMap;
14
15pub struct Token {
19 pub id: u64,
20 pub position: Vec3,
22 pub time_step: u64,
25}
26
27pub struct AttentionResult {
29 pub query_id: u64,
30 pub attended: Vec<(u64, f32)>,
34}
35
36pub struct AttentionStats {
38 pub n_queries: usize,
39 pub mean_attended: f32,
40 pub min_attended: usize,
41 pub max_attended: usize,
42 pub total_pairs: usize,
44 pub attended_pairs: usize,
46 pub sparsity: f32,
48}
49
50fn to_node_point(t: &Token) -> NodePoint {
53 NodePoint {
54 id: t.id,
55 x: t.position.x,
56 y: t.position.y,
57 z: t.position.z,
58 }
59}
60
61fn build_key_octree(queries: &[Token], keys: &[Token]) -> Octree {
65 use crate::spatial::octree::BoundingBox;
66
67 let all: Vec<Vec3> = queries
68 .iter()
69 .chain(keys.iter())
70 .map(|t| t.position)
71 .collect();
72
73 let mut min = all[0];
74 let mut max = all[0];
75 for &p in &all {
76 min = min.min(p);
77 max = max.max(p);
78 }
79
80 let span = (max - min).length().max(1.0);
82 let pad = Vec3::splat(span * 0.25);
83 let bounds = BoundingBox::new(min - pad, max + pad);
84
85 let mut octree = Octree::new(bounds);
86 for key in keys {
87 octree.insert(to_node_point(key));
88 }
89 octree
90}
91
92fn softmax_weights(neg_dist_sq: &[f32]) -> Vec<f32> {
93 let max = neg_dist_sq
94 .iter()
95 .cloned()
96 .fold(f32::NEG_INFINITY, f32::max);
97 let exps: Vec<f32> = neg_dist_sq.iter().map(|&v| (v - max).exp()).collect();
98 let sum: f32 = exps.iter().sum();
99 exps.iter().map(|&e| e / sum).collect()
100}
101
102pub fn sparse_attention(
114 queries: &[Token],
115 keys: &[Token],
116 k: usize,
117 temperature: f32,
118) -> Vec<AttentionResult> {
119 if queries.is_empty() {
120 return Vec::new();
121 }
122 if keys.is_empty() {
123 return queries
124 .iter()
125 .map(|q| AttentionResult {
126 query_id: q.id,
127 attended: Vec::new(),
128 })
129 .collect();
130 }
131
132 let octree = build_key_octree(queries, keys);
133
134 let key_time: HashMap<u64, u64> = keys.iter().map(|t| (t.id, t.time_step)).collect();
135 let temp = temperature.max(1e-6);
136
137 queries
138 .iter()
139 .map(|query| {
140 let neighbors = octree.query_knn(query.position, k);
141
142 let causal: Vec<(u64, f32)> = neighbors
144 .into_iter()
145 .filter(|(np, _)| {
146 key_time
147 .get(&np.id)
148 .is_some_and(|&ts| ts <= query.time_step)
149 })
150 .map(|(np, dist_sq)| (np.id, dist_sq))
151 .collect();
152
153 if causal.is_empty() {
154 return AttentionResult {
155 query_id: query.id,
156 attended: Vec::new(),
157 };
158 }
159
160 let neg_dists: Vec<f32> = causal.iter().map(|(_, d)| -d / temp).collect();
161 let weights = softmax_weights(&neg_dists);
162
163 let attended = causal
164 .iter()
165 .zip(weights)
166 .map(|((id, _), w)| (*id, w))
167 .collect();
168
169 AttentionResult {
170 query_id: query.id,
171 attended,
172 }
173 })
174 .collect()
175}
176
177pub fn attention_stats(results: &[AttentionResult], n_keys: usize) -> AttentionStats {
179 let n_queries = results.len();
180 if n_queries == 0 {
181 return AttentionStats {
182 n_queries: 0,
183 mean_attended: 0.0,
184 min_attended: 0,
185 max_attended: 0,
186 total_pairs: 0,
187 attended_pairs: 0,
188 sparsity: 1.0,
189 };
190 }
191
192 let counts: Vec<usize> = results.iter().map(|r| r.attended.len()).collect();
193 let attended_pairs: usize = counts.iter().sum();
194 let min_attended = *counts.iter().min().unwrap();
195 let max_attended = *counts.iter().max().unwrap();
196 let mean_attended = attended_pairs as f32 / n_queries as f32;
197 let total_pairs = n_queries * n_keys;
198 let sparsity = if total_pairs == 0 {
199 1.0
200 } else {
201 1.0 - attended_pairs as f32 / total_pairs as f32
202 };
203
204 AttentionStats {
205 n_queries,
206 mean_attended,
207 min_attended,
208 max_attended,
209 total_pairs,
210 attended_pairs,
211 sparsity,
212 }
213}
214
215#[cfg(test)]
218mod tests {
219 use super::*;
220 use glam::Vec3;
221
222 fn tok(id: u64, x: f32, y: f32, z: f32, t: u64) -> Token {
223 Token {
224 id,
225 position: Vec3::new(x, y, z),
226 time_step: t,
227 }
228 }
229
230 #[test]
231 fn test_empty_queries_returns_empty() {
232 let result = sparse_attention(&[], &[tok(0, 0.0, 0.0, 0.0, 0)], 3, 1.0);
233 assert!(result.is_empty());
234 }
235
236 #[test]
237 fn test_empty_keys_returns_empty_attended() {
238 let queries = vec![tok(0, 0.0, 0.0, 0.0, 0)];
239 let result = sparse_attention(&queries, &[], 3, 1.0);
240 assert_eq!(result.len(), 1);
241 assert!(result[0].attended.is_empty());
242 }
243
244 #[test]
245 fn test_single_pair_weight_is_one() {
246 let queries = vec![tok(0, 0.0, 0.0, 0.0, 5)];
247 let keys = vec![tok(1, 0.1, 0.0, 0.0, 3)];
248 let result = sparse_attention(&queries, &keys, 1, 1.0);
249 assert_eq!(result[0].attended.len(), 1);
250 let (id, w) = result[0].attended[0];
251 assert_eq!(id, 1);
252 assert!(
253 (w - 1.0).abs() < 1e-5,
254 "single key must get weight 1.0, got {w}"
255 );
256 }
257
258 #[test]
259 fn test_causal_mask_excludes_future_keys() {
260 let queries = vec![tok(0, 0.0, 0.0, 0.0, 2)];
262 let keys = vec![tok(1, 0.1, 0.0, 0.0, 1), tok(2, 0.2, 0.0, 0.0, 3)];
263 let result = sparse_attention(&queries, &keys, 5, 1.0);
264 let ids: Vec<u64> = result[0].attended.iter().map(|(id, _)| *id).collect();
265 assert!(ids.contains(&1), "past key must be attended");
266 assert!(!ids.contains(&2), "future key must be masked");
267 }
268
269 #[test]
270 fn test_same_time_step_is_allowed() {
271 let queries = vec![tok(0, 0.0, 0.0, 0.0, 3)];
272 let keys = vec![tok(1, 0.1, 0.0, 0.0, 3)];
273 let result = sparse_attention(&queries, &keys, 1, 1.0);
274 assert_eq!(
275 result[0].attended.len(),
276 1,
277 "equal time_step should not be masked"
278 );
279 }
280
281 #[test]
282 fn test_k_limits_attended_count() {
283 let queries = vec![tok(0, 0.5, 0.5, 0.5, 10)];
284 let keys: Vec<Token> = (1u64..=8)
285 .map(|i| tok(i, i as f32 * 0.1, 0.0, 0.0, i))
286 .collect();
287 let result = sparse_attention(&queries, &keys, 3, 1.0);
288 assert!(
289 result[0].attended.len() <= 3,
290 "attended count must not exceed k"
291 );
292 }
293
294 #[test]
295 fn test_weights_sum_to_one() {
296 let queries = vec![tok(0, 0.0, 0.0, 0.0, 10)];
297 let keys: Vec<Token> = (1u64..=5)
298 .map(|i| tok(i, i as f32 * 0.2, 0.0, 0.0, i))
299 .collect();
300 let result = sparse_attention(&queries, &keys, 5, 1.0);
301 let sum: f32 = result[0].attended.iter().map(|(_, w)| w).sum();
302 assert!(
303 (sum - 1.0).abs() < 1e-5,
304 "weights must sum to 1.0, got {sum}"
305 );
306 }
307
308 #[test]
309 fn test_closer_token_gets_higher_weight() {
310 let queries = vec![tok(0, 0.0, 0.0, 0.0, 10)];
311 let keys = vec![
312 tok(1, 0.1, 0.0, 0.0, 1), tok(2, 5.0, 0.0, 0.0, 2), ];
315 let result = sparse_attention(&queries, &keys, 5, 1.0);
316 let w1 = result[0]
317 .attended
318 .iter()
319 .find(|(id, _)| *id == 1)
320 .map(|(_, w)| *w)
321 .expect("close key must be attended");
322 let w2 = result[0]
323 .attended
324 .iter()
325 .find(|(id, _)| *id == 2)
326 .map(|(_, w)| *w)
327 .expect("far key must be attended");
328 assert!(
329 w1 > w2,
330 "closer token must have higher weight ({w1:.4} vs {w2:.4})"
331 );
332 }
333
334 #[test]
335 fn test_attention_stats_fields() {
336 let queries: Vec<Token> = (0..4).map(|i| tok(i, i as f32, 0.0, 0.0, 10)).collect();
337 let keys: Vec<Token> = (10..14)
338 .map(|i| tok(i, (i - 10) as f32 * 0.5, 0.0, 0.0, 5))
339 .collect();
340 let results = sparse_attention(&queries, &keys, 2, 1.0);
341 let stats = attention_stats(&results, keys.len());
342 assert_eq!(stats.n_queries, 4);
343 assert_eq!(stats.total_pairs, 16);
344 assert!(stats.sparsity >= 0.0 && stats.sparsity <= 1.0);
345 assert!(stats.attended_pairs <= stats.total_pairs);
346 }
347}