1use crate::acceleration::cpu_kernels;
2use crate::algorithms::mlp::{cross_entropy_from_logits, MlpClassifier, MlpGradients};
3
4#[derive(Debug, Clone)]
10pub struct GraphAttentionClassifier {
11 vocab_size: usize,
12 embed_dim: usize,
13 num_neighbors: usize,
14 rope_theta: Option<f32>,
17 pub embedding: Vec<f32>,
19 pub w_q: Vec<f32>,
21 pub w_k: Vec<f32>,
23 pub w_v: Vec<f32>,
25 pub edge_weights_raw: Vec<f32>,
30 pub mlp: MlpClassifier,
32 geometric_attention_only: bool,
37}
38
39#[derive(Debug, Clone)]
41pub struct GraphAttentionGradients {
42 pub dembedding: Vec<f32>,
43 pub dw_q: Vec<f32>,
44 pub dw_k: Vec<f32>,
45 pub dw_v: Vec<f32>,
46 pub dedge_weights_raw: Vec<f32>,
47 pub mlp: MlpGradients,
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct TiebreakConfig {
53 pub min_max_weight: f32,
56 pub min_margin: f32,
59 pub max_fractal_dimension: f32,
65 pub fractal_window_size: usize,
68}
69
70impl Default for TiebreakConfig {
71 fn default() -> Self {
72 Self {
73 min_max_weight: 0.5,
74 min_margin: 0.1,
75 max_fractal_dimension: f32::INFINITY,
76 fractal_window_size: 8,
77 }
78 }
79}
80
81fn vec_matmul(v: &[f32], cols: usize, m: &[f32], out: usize) -> Vec<f32> {
83 assert_eq!(v.len(), cols);
84 assert_eq!(m.len(), cols * out);
85 let mut res = vec![0.0f32; out];
86 for k in 0..cols {
87 for j in 0..out {
88 res[j] += v[k] * m[k * out + j];
89 }
90 }
91 res
92}
93
94fn mat_t_vec(m: &[f32], rows: usize, cols: usize, v: &[f32]) -> Vec<f32> {
97 assert_eq!(m.len(), rows * cols);
98 assert_eq!(v.len(), rows);
99 let mut res = vec![0.0f32; cols];
100 for i in 0..rows {
101 for j in 0..cols {
102 res[j] += m[i * cols + j] * v[i];
103 }
104 }
105 res
106}
107
108fn softmax(scores: &mut [f32]) -> Vec<f32> {
109 cpu_kernels::softmax_in_place(scores);
110 scores.to_vec()
111}
112
113fn top_two(weights: &[f32]) -> (f32, f32) {
114 let mut max = f32::NEG_INFINITY;
115 let mut second = f32::NEG_INFINITY;
116 for &w in weights {
117 if w > max {
118 second = max;
119 max = w;
120 } else if w > second {
121 second = w;
122 }
123 }
124 (max, second)
125}
126
127fn xavier_init(rows: usize, cols: usize, rng: &mut impl FnMut() -> f32) -> Vec<f32> {
128 let scale = (2.0 / (rows + cols) as f32).sqrt();
129 (0..rows * cols).map(|_| rng() * scale).collect()
130}
131
132fn apply_rope_in_place(vec: &mut [f32], embed_dim: usize, position: usize, theta: f32) {
137 cpu_kernels::apply_rope_in_place(vec, embed_dim, position, theta);
138}
139
140fn apply_rope_inv_in_place(vec: &mut [f32], embed_dim: usize, position: usize, theta: f32) {
142 cpu_kernels::apply_rope_inv_in_place(vec, embed_dim, position, theta);
143}
144
145impl GraphAttentionClassifier {
146 pub fn new(
151 vocab_size: usize,
152 embed_dim: usize,
153 hidden_dim: usize,
154 output_dim: usize,
155 num_neighbors: usize,
156 seed: u32,
157 rope_theta: Option<f32>,
158 plasticity: bool,
159 ) -> Self {
160 if let Some(theta) = rope_theta {
161 assert!(
162 theta > 0.0,
163 "GraphAttentionClassifier: rope_theta must be positive"
164 );
165 assert!(
166 embed_dim % 2 == 0,
167 "GraphAttentionClassifier: RoPE requires even embed_dim"
168 );
169 }
170
171 let mut rng_state = seed;
172 let mut next_rand = || {
173 rng_state ^= rng_state << 13;
174 rng_state ^= rng_state >> 17;
175 rng_state ^= rng_state << 5;
176 (rng_state as f32 / u32::MAX as f32) - 0.5
177 };
178
179 let embedding = xavier_init(vocab_size, embed_dim, &mut next_rand);
180 let w_q = xavier_init(embed_dim, embed_dim, &mut next_rand);
181 let w_k = xavier_init(embed_dim, embed_dim, &mut next_rand);
182 let w_v = xavier_init(embed_dim, embed_dim, &mut next_rand);
183 let mlp = MlpClassifier::new(embed_dim, hidden_dim, output_dim, seed.wrapping_add(1));
184 let edge_weights_raw = if plasticity {
185 vec![0.0f32; vocab_size * num_neighbors.max(1)]
188 } else {
189 Vec::new()
190 };
191
192 Self {
193 vocab_size,
194 embed_dim,
195 num_neighbors,
196 rope_theta,
197 embedding,
198 w_q,
199 w_k,
200 w_v,
201 edge_weights_raw,
202 mlp,
203 geometric_attention_only: false,
204 }
205 }
206
207 pub fn set_geometric_attention_only(&mut self, enabled: bool) {
214 self.geometric_attention_only = enabled;
215 }
216
217 fn plasticity_enabled(&self) -> bool {
218 !self.edge_weights_raw.is_empty()
219 }
220
221 fn edge_weight(&self, token_idx: usize, nbr_pos: usize) -> f32 {
222 if !self.plasticity_enabled() {
223 return 1.0f32;
224 }
225 let idx = token_idx * self.num_neighbors + nbr_pos;
226 self.edge_weights_raw[idx.min(self.edge_weights_raw.len() - 1)].exp()
227 }
228
229 fn build_attended_indices(
233 &self,
234 j: usize,
235 n_context: usize,
236 nbrs: &[usize],
237 geometric_only: bool,
238 ) -> (Vec<usize>, usize) {
239 let mut attended = Vec::with_capacity(1 + n_context + self.num_neighbors);
240 attended.push(j);
241 if !geometric_only {
242 for k in 0..n_context {
243 if k != j {
244 attended.push(k);
245 }
246 }
247 }
248 for &nbr in nbrs.iter().take(self.num_neighbors) {
249 attended.push(nbr);
250 }
251 let n_context_attended = if geometric_only { 1 } else { n_context };
252 (attended, n_context_attended)
253 }
254
255 fn lookup(&self, token_id: u32) -> &[f32] {
256 let idx = (token_id as usize).min(self.vocab_size - 1);
257 &self.embedding[idx * self.embed_dim..(idx + 1) * self.embed_dim]
258 }
259
260 pub fn forward(&self, token_ids: &[u32], neighbors: &[&[usize]]) -> Vec<f32> {
266 let h = self.attend(token_ids, neighbors, self.geometric_attention_only);
267 let last = h.last().expect("empty context");
268 self.mlp.forward(last).logits
269 }
270
271 pub fn forward_hybrid(&self, token_ids: &[u32], neighbors: &[&[usize]]) -> Vec<f32> {
273 let h = self.attend(token_ids, neighbors, false);
274 let last = h.last().expect("empty context");
275 self.mlp.forward(last).logits
276 }
277
278 pub fn forward_geometric_only(&self, token_ids: &[u32], neighbors: &[&[usize]]) -> Vec<f32> {
280 let h = self.attend(token_ids, neighbors, true);
281 let last = h.last().expect("empty context");
282 self.mlp.forward(last).logits
283 }
284
285 pub fn forward_hidden(
290 &self,
291 token_ids: &[u32],
292 neighbors: &[&[usize]],
293 ) -> (Vec<f32>, Vec<f32>) {
294 let h = self.attend(token_ids, neighbors, self.geometric_attention_only);
295 let last = h.last().expect("empty context");
296 let fwd = self.mlp.forward(last);
297 (fwd.logits, fwd.hidden)
298 }
299
300 pub fn forward_hidden_only(&self, token_ids: &[u32], neighbors: &[&[usize]]) -> Vec<f32> {
304 let h = self.attend(token_ids, neighbors, self.geometric_attention_only);
305 let last = h.last().expect("empty context");
306 self.mlp.forward_hidden(last)
307 }
308
309 pub fn loss(&self, token_ids: &[u32], neighbors: &[&[usize]], target: usize) -> f32 {
311 let logits = self.forward(token_ids, neighbors);
312 cross_entropy_from_logits(&logits, target)
313 }
314
315 pub fn predict(&self, token_ids: &[u32], neighbors: &[&[usize]]) -> usize {
317 let logits = self.forward(token_ids, neighbors);
318 logits
319 .iter()
320 .enumerate()
321 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
322 .map(|(i, _)| i)
323 .unwrap_or(0)
324 }
325
326 pub(crate) fn attend(
328 &self,
329 token_ids: &[u32],
330 neighbors: &[&[usize]],
331 geometric_only: bool,
332 ) -> Vec<Vec<f32>> {
333 let n_context = token_ids.len();
334 assert_eq!(neighbors.len(), n_context);
335
336 let d = self.embed_dim;
337 let scale = (d as f32).sqrt();
338
339 let embeddings: Vec<Vec<f32>> =
341 token_ids.iter().map(|&t| self.lookup(t).to_vec()).collect();
342 let mut rotated_embeddings = embeddings.clone();
343 if let Some(theta) = self.rope_theta {
344 for (j, emb) in rotated_embeddings.iter_mut().enumerate() {
345 let position = n_context - j;
346 apply_rope_in_place(emb, d, position, theta);
347 }
348 }
349
350 let mut hiddens = Vec::with_capacity(n_context);
351
352 for j in 0..n_context {
353 let q = vec_matmul(&rotated_embeddings[j], d, &self.w_q, d);
354
355 let (attended_indices, n_context_attended) =
359 self.build_attended_indices(j, n_context, neighbors[j], geometric_only);
360
361 let mut scores = Vec::with_capacity(attended_indices.len());
362 let mut values = Vec::with_capacity(attended_indices.len());
363
364 for (pos, &idx) in attended_indices.iter().enumerate() {
365 let e_owned: Vec<f32>;
366 let e: &[f32] = if idx < n_context {
367 &rotated_embeddings[idx]
370 } else {
371 let dense_idx = idx.min(self.vocab_size - 1);
374 e_owned = self.lookup(dense_idx as u32).to_vec();
375 &e_owned
376 };
377 let k = vec_matmul(e, d, &self.w_k, d);
378 let v = vec_matmul(e, d, &self.w_v, d);
379
380 let mut score: f32 =
381 q.iter().zip(k.iter()).map(|(a, b)| a * b).sum::<f32>() / scale;
382 if pos >= n_context_attended {
385 let nbr_pos = pos - n_context_attended;
386 let edge_w = self.edge_weight(token_ids[j] as usize, nbr_pos);
387 score *= edge_w;
388 }
389 scores.push(score);
390 values.push(v);
391 }
392
393 let weights = softmax(&mut scores);
394 let mut context = vec![0.0f32; d];
395 for (w, v) in weights.iter().zip(values.iter()) {
396 for i in 0..d {
397 context[i] += w * v[i];
398 }
399 }
400
401 let h: Vec<f32> = embeddings[j]
403 .iter()
404 .zip(context.iter())
405 .map(|(e, c)| e + c)
406 .collect();
407 hiddens.push(h);
408 }
409
410 hiddens
411 }
412
413 pub fn attend_mixed(
420 &self,
421 token_ids: &[u32],
422 neighbors: &[&[usize]],
423 per_position_geometric: &[bool],
424 ) -> Vec<Vec<f32>> {
425 let n_context = token_ids.len();
426 assert_eq!(neighbors.len(), n_context);
427 assert_eq!(per_position_geometric.len(), n_context);
428
429 let d = self.embed_dim;
430 let scale = (d as f32).sqrt();
431
432 let embeddings: Vec<Vec<f32>> =
433 token_ids.iter().map(|&t| self.lookup(t).to_vec()).collect();
434 let mut rotated_embeddings = embeddings.clone();
435 if let Some(theta) = self.rope_theta {
436 for (j, emb) in rotated_embeddings.iter_mut().enumerate() {
437 let position = n_context - j;
438 apply_rope_in_place(emb, d, position, theta);
439 }
440 }
441
442 let mut hiddens = Vec::with_capacity(n_context);
443
444 for j in 0..n_context {
445 let q = vec_matmul(&rotated_embeddings[j], d, &self.w_q, d);
446 let geometric_only = per_position_geometric[j];
447 let (attended_indices, n_context_attended) =
448 self.build_attended_indices(j, n_context, neighbors[j], geometric_only);
449
450 let mut scores = Vec::with_capacity(attended_indices.len());
451 let mut values = Vec::with_capacity(attended_indices.len());
452
453 for (pos, &idx) in attended_indices.iter().enumerate() {
454 let e_owned: Vec<f32>;
455 let e: &[f32] = if idx < n_context {
456 &rotated_embeddings[idx]
457 } else {
458 let dense_idx = idx.min(self.vocab_size - 1);
459 e_owned = self.lookup(dense_idx as u32).to_vec();
460 &e_owned
461 };
462 let k = vec_matmul(e, d, &self.w_k, d);
463 let v = vec_matmul(e, d, &self.w_v, d);
464
465 let mut score: f32 =
466 q.iter().zip(k.iter()).map(|(a, b)| a * b).sum::<f32>() / scale;
467 if pos >= n_context_attended {
468 let nbr_pos = pos - n_context_attended;
469 let edge_w = self.edge_weight(token_ids[j] as usize, nbr_pos);
470 score *= edge_w;
471 }
472 scores.push(score);
473 values.push(v);
474 }
475
476 let weights = softmax(&mut scores);
477 let mut context = vec![0.0f32; d];
478 for (w, v) in weights.iter().zip(values.iter()) {
479 for i in 0..d {
480 context[i] += w * v[i];
481 }
482 }
483
484 let h: Vec<f32> = embeddings[j]
485 .iter()
486 .zip(context.iter())
487 .map(|(e, c)| e + c)
488 .collect();
489 hiddens.push(h);
490 }
491
492 hiddens
493 }
494
495 pub fn forward_mixed(
497 &self,
498 token_ids: &[u32],
499 neighbors: &[&[usize]],
500 per_position_geometric: &[bool],
501 ) -> Vec<f32> {
502 let h = self.attend_mixed(token_ids, neighbors, per_position_geometric);
503 let last = h.last().expect("empty context");
504 self.mlp.forward(last).logits
505 }
506
507 pub fn forward_tiebreak(
515 &self,
516 token_ids: &[u32],
517 neighbors: &[&[usize]],
518 config: &TiebreakConfig,
519 ) -> Vec<f32> {
520 let mask = self.tiebreak_mask(token_ids, neighbors, config);
521 self.forward_mixed(token_ids, neighbors, &mask)
522 }
523
524 pub fn tiebreak_mask(
532 &self,
533 token_ids: &[u32],
534 neighbors: &[&[usize]],
535 config: &TiebreakConfig,
536 ) -> Vec<bool> {
537 let n_context = token_ids.len();
538 assert_eq!(neighbors.len(), n_context);
539
540 let d = self.embed_dim;
541 let scale = (d as f32).sqrt();
542
543 let embeddings: Vec<Vec<f32>> =
544 token_ids.iter().map(|&t| self.lookup(t).to_vec()).collect();
545 let mut rotated_embeddings = embeddings.clone();
546 if let Some(theta) = self.rope_theta {
547 for (j, emb) in rotated_embeddings.iter_mut().enumerate() {
548 let position = n_context - j;
549 apply_rope_in_place(emb, d, position, theta);
550 }
551 }
552
553 let mut max_weights = Vec::with_capacity(n_context);
557 let mut attention_uncertain = Vec::with_capacity(n_context);
558 for j in 0..n_context {
559 let q = vec_matmul(&rotated_embeddings[j], d, &self.w_q, d);
560 let (attended, n_context_attended) =
561 self.build_attended_indices(j, n_context, neighbors[j], true);
562
563 let mut scores = Vec::with_capacity(attended.len());
564 for (pos, &idx) in attended.iter().enumerate() {
565 let e_owned: Vec<f32>;
566 let e: &[f32] = if idx < n_context {
567 &rotated_embeddings[idx]
568 } else {
569 let dense_idx = idx.min(self.vocab_size - 1);
570 e_owned = self.lookup(dense_idx as u32).to_vec();
571 &e_owned
572 };
573 let k = vec_matmul(e, d, &self.w_k, d);
574 let mut score: f32 =
575 q.iter().zip(k.iter()).map(|(a, b)| a * b).sum::<f32>() / scale;
576 if pos >= n_context_attended {
577 let nbr_pos = pos - n_context_attended;
578 let edge_w = self.edge_weight(token_ids[j] as usize, nbr_pos);
579 score *= edge_w;
580 }
581 scores.push(score);
582 }
583
584 let weights = softmax(&mut scores);
585 let (max, second) = top_two(&weights);
586 max_weights.push(max);
587 attention_uncertain
588 .push(max < config.min_max_weight || (max - second) < config.min_margin);
589 }
590
591 let mut mask = Vec::with_capacity(n_context);
595 for j in 0..n_context {
596 let start = j.saturating_sub(config.fractal_window_size.saturating_sub(1));
597 let window = &max_weights[start..=j];
598 let fractal_dim = if window.len() >= 4 {
599 super::fractal::box_counting_dimension_1d(window)
600 } else {
601 1.0
604 };
605 let uncertain = attention_uncertain[j] || fractal_dim > config.max_fractal_dimension;
606 mask.push(!uncertain);
607 }
608
609 mask
610 }
611
612 pub fn forward_batch(
614 &self,
615 token_ids: &[u32],
616 neighbors: &[Vec<Vec<usize>>],
617 batch_size: usize,
618 ) -> Vec<f32> {
619 let n_context = token_ids.len() / batch_size;
620 let mut h_matrix = Vec::with_capacity(batch_size * self.embed_dim);
621 for b in 0..batch_size {
622 let start = b * n_context;
623 let ids = &token_ids[start..start + n_context];
624 let nbr_refs: Vec<&[usize]> = neighbors[b].iter().map(|v| v.as_slice()).collect();
625 let h = self.attend(ids, &nbr_refs, self.geometric_attention_only);
626 h_matrix.extend_from_slice(h.last().unwrap());
627 }
628 self.mlp
629 .forward_batch(&h_matrix, batch_size)
630 .logits
631 .iter()
632 .copied()
633 .collect()
634 }
635
636 pub fn backward_batch(
642 &self,
643 token_ids: &[u32],
644 neighbors: &[Vec<Vec<usize>>],
645 targets: &[usize],
646 batch_size: usize,
647 ) -> (GraphAttentionGradients, f32) {
648 let n_context = token_ids.len() / batch_size;
649 let d = self.embed_dim;
650 let scale = (d as f32).sqrt();
651
652 let mut embeddings_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
654 let mut rotated_embeddings_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
655 let mut queries_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
656 let mut values_cache: Vec<Vec<Vec<Vec<f32>>>> = Vec::with_capacity(batch_size);
657 let mut scores_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
658 let mut weights_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
659 let mut hiddens_cache: Vec<Vec<Vec<f32>>> = Vec::with_capacity(batch_size);
660 let mut h_matrix: Vec<f32> = Vec::with_capacity(batch_size * d);
661
662 for b in 0..batch_size {
663 let start = b * n_context;
664 let ids = &token_ids[start..start + n_context];
665 let nbr_refs: Vec<&[usize]> = neighbors[b].iter().map(|v| v.as_slice()).collect();
666
667 let embeddings: Vec<Vec<f32>> = ids.iter().map(|&t| self.lookup(t).to_vec()).collect();
668 let mut rotated_embeddings = embeddings.clone();
669 if let Some(theta) = self.rope_theta {
670 for (j, emb) in rotated_embeddings.iter_mut().enumerate() {
671 let position = n_context - j;
672 apply_rope_in_place(emb, d, position, theta);
673 }
674 }
675
676 let mut queries = Vec::with_capacity(n_context);
677 let mut batch_values = Vec::with_capacity(n_context);
678 let mut batch_scores = Vec::with_capacity(n_context);
679 let mut batch_weights = Vec::with_capacity(n_context);
680 let mut hiddens = Vec::with_capacity(n_context);
681
682 for j in 0..n_context {
683 let q = vec_matmul(&rotated_embeddings[j], d, &self.w_q, d);
684
685 let (attended, n_context_attended) = self.build_attended_indices(
686 j,
687 n_context,
688 nbr_refs[j],
689 self.geometric_attention_only,
690 );
691
692 let mut scores = Vec::with_capacity(attended.len());
693 let mut values = Vec::with_capacity(attended.len());
694
695 for (pos, &idx) in attended.iter().enumerate() {
696 let e_owned: Vec<f32>;
697 let e: &[f32] = if idx < n_context {
698 &rotated_embeddings[idx]
699 } else {
700 let dense_idx = idx.min(self.vocab_size - 1);
701 e_owned = self.lookup(dense_idx as u32).to_vec();
702 &e_owned
703 };
704 let k = vec_matmul(e, d, &self.w_k, d);
705 let v = vec_matmul(e, d, &self.w_v, d);
706 let mut score = q.iter().zip(k.iter()).map(|(a, b)| a * b).sum::<f32>() / scale;
707 if pos >= n_context_attended {
708 let nbr_pos = pos - n_context_attended;
709 let edge_w = self.edge_weight(ids[j] as usize, nbr_pos);
710 score *= edge_w;
711 }
712 scores.push(score);
713 values.push(v);
714 }
715
716 let weights = softmax(&mut scores);
717 let mut context = vec![0.0f32; d];
718 for (w, v) in weights.iter().zip(values.iter()) {
719 for i in 0..d {
720 context[i] += w * v[i];
721 }
722 }
723
724 let h: Vec<f32> = embeddings[j]
725 .iter()
726 .zip(context.iter())
727 .map(|(e, c)| e + c)
728 .collect();
729
730 queries.push(q);
731 batch_values.push(values);
732 batch_scores.push(scores);
733 batch_weights.push(weights);
734 hiddens.push(h.clone());
735 if j == n_context - 1 {
736 h_matrix.extend_from_slice(&h);
737 }
738 }
739
740 embeddings_cache.push(embeddings);
741 rotated_embeddings_cache.push(rotated_embeddings);
742 queries_cache.push(queries);
743 values_cache.push(batch_values);
744 scores_cache.push(batch_scores);
745 weights_cache.push(batch_weights);
746 hiddens_cache.push(hiddens);
747 }
748
749 let (mlp_grad, dh_matrix, loss) = self.mlp.backward_batch(&h_matrix, targets, batch_size);
751
752 let mut dembedding = vec![0.0f32; self.vocab_size * d];
754 let mut dw_q = vec![0.0f32; d * d];
755 let mut dw_k = vec![0.0f32; d * d];
756 let mut dw_v = vec![0.0f32; d * d];
757 let mut dedge_weights_raw = vec![0.0f32; self.edge_weights_raw.len()];
758
759 for b in 0..batch_size {
760 let start = b * n_context;
761 let ids = &token_ids[start..start + n_context];
762
763 for j in 0..n_context {
764 let dh = if j == n_context - 1 {
766 &dh_matrix[b * d..(b + 1) * d]
767 } else {
768 &[] as &[f32]
769 };
770
771 if dh.is_empty() {
772 continue;
773 }
774 let dh: Vec<f32> = dh.to_vec();
775
776 let q = &queries_cache[b][j];
777 let values = &values_cache[b][j];
778 let weights = &weights_cache[b][j];
779
780 let (attended, n_context_attended) = self.build_attended_indices(
781 j,
782 n_context,
783 &neighbors[b][j],
784 self.geometric_attention_only,
785 );
786
787 let dcontext = dh.clone();
789
790 let mut dvalues: Vec<Vec<f32>> = Vec::with_capacity(attended.len());
793 let mut dscores = Vec::with_capacity(attended.len());
794 for (w, v) in weights.iter().zip(values.iter()) {
795 let dv: Vec<f32> = dcontext.iter().map(|&dc| w * dc).collect();
796 let dw: f32 = v
797 .iter()
798 .zip(dcontext.iter())
799 .map(|(vi, dci)| vi * dci)
800 .sum();
801 dvalues.push(dv);
802 dscores.push(dw);
803 }
804
805 let weighted_sum: f32 = weights
808 .iter()
809 .zip(dscores.iter())
810 .map(|(w, ds)| w * ds)
811 .sum();
812 let dscores: Vec<f32> = weights
813 .iter()
814 .zip(dscores.iter())
815 .map(|(w, ds)| w * (ds - weighted_sum))
816 .collect();
817
818 if self.plasticity_enabled() {
822 let scores = &scores_cache[b][j];
823 let src_idx = ids[j] as usize;
824 for (pos, (&ds, &score)) in dscores.iter().zip(scores.iter()).enumerate() {
825 if pos < n_context_attended {
826 continue;
827 }
828 let nbr_pos = pos - n_context_attended;
829 let edge_idx = src_idx * self.num_neighbors + nbr_pos;
830 dedge_weights_raw[edge_idx] += ds * score;
831 }
832 }
833
834 let mut dq = vec![0.0f32; d];
838 let mut dk_list: Vec<Vec<f32>> = Vec::with_capacity(attended.len());
839 for (ds, _v) in dscores.iter().zip(values.iter()) {
840 let idx = attended[dk_list.len()];
842 let e_owned: Vec<f32>;
843 let e: &[f32] = if idx < n_context {
844 &rotated_embeddings_cache[b][idx]
845 } else {
846 let dense_idx = idx.min(self.vocab_size - 1);
847 e_owned = self.lookup(dense_idx as u32).to_vec();
848 &e_owned
849 };
850 let k = vec_matmul(e, d, &self.w_k, d);
851 for i in 0..d {
852 dq[i] += ds * k[i] / scale;
853 }
854 let dk: Vec<f32> = q.iter().map(|&qi| ds * qi / scale).collect();
855 dk_list.push(dk);
856 }
857
858 for i in 0..d {
861 dembedding[ids[j] as usize * d + i] += dh[i];
862 }
863
864 let de_q_rotated = mat_t_vec(&self.w_q, d, d, &dq);
866 let mut de_q = de_q_rotated;
868 if let Some(theta) = self.rope_theta {
869 let position = n_context - j;
870 apply_rope_inv_in_place(&mut de_q, d, position, theta);
871 }
872 for i in 0..d {
873 dembedding[ids[j] as usize * d + i] += de_q[i];
874 }
875
876 for k in 0..d {
878 for l in 0..d {
879 dw_q[k * d + l] += rotated_embeddings_cache[b][j][k] * dq[l];
880 }
881 }
882
883 for (idx, (dv, dk)) in attended.iter().zip(dvalues.iter().zip(dk_list.iter())) {
889 let idx = *idx;
890 let is_context = idx < n_context;
891
892 let e_unrot_owned: Vec<f32>;
893 let e_for_proj: &[f32] = if is_context {
894 &rotated_embeddings_cache[b][idx]
895 } else {
896 let dense_idx = idx.min(self.vocab_size - 1);
897 e_unrot_owned = self.lookup(dense_idx as u32).to_vec();
898 &e_unrot_owned
899 };
900
901 let de_v_rotated = mat_t_vec(&self.w_v, d, d, dv);
902 let de_k_rotated = mat_t_vec(&self.w_k, d, d, dk);
903
904 let token_idx = if is_context {
905 ids[idx] as usize
906 } else {
907 idx.min(self.vocab_size - 1)
908 };
909
910 let mut de_v = de_v_rotated;
913 let mut de_k = de_k_rotated;
914 if is_context {
915 if let Some(theta) = self.rope_theta {
916 let position = n_context - idx;
917 apply_rope_inv_in_place(&mut de_v, d, position, theta);
918 apply_rope_inv_in_place(&mut de_k, d, position, theta);
919 }
920 }
921
922 for i in 0..d {
923 dembedding[token_idx * d + i] += de_v[i] + de_k[i];
924 }
925
926 for k in 0..d {
927 for l in 0..d {
928 dw_k[k * d + l] += e_for_proj[k] * dk[l];
929 dw_v[k * d + l] += e_for_proj[k] * dv[l];
930 }
931 }
932 }
933 }
934 }
935
936 let inv_b = 1.0 / batch_size as f32;
938 for v in dembedding.iter_mut() {
939 *v *= inv_b;
940 }
941 for v in dw_q.iter_mut() {
942 *v *= inv_b;
943 }
944 for v in dw_k.iter_mut() {
945 *v *= inv_b;
946 }
947 for v in dw_v.iter_mut() {
948 *v *= inv_b;
949 }
950 for v in dedge_weights_raw.iter_mut() {
951 *v *= inv_b;
952 }
953
954 (
955 GraphAttentionGradients {
956 dembedding,
957 dw_q,
958 dw_k,
959 dw_v,
960 dedge_weights_raw,
961 mlp: mlp_grad,
962 },
963 loss,
964 )
965 }
966
967 pub fn apply_sgd(&mut self, grad: &GraphAttentionGradients, lr: f32) {
969 for i in 0..self.embedding.len() {
970 self.embedding[i] -= lr * grad.dembedding[i];
971 }
972 for i in 0..self.w_q.len() {
973 self.w_q[i] -= lr * grad.dw_q[i];
974 }
975 for i in 0..self.w_k.len() {
976 self.w_k[i] -= lr * grad.dw_k[i];
977 }
978 for i in 0..self.w_v.len() {
979 self.w_v[i] -= lr * grad.dw_v[i];
980 }
981 for i in 0..self.edge_weights_raw.len() {
982 self.edge_weights_raw[i] -= lr * grad.dedge_weights_raw[i];
983 }
984 self.mlp.apply_sgd(&grad.mlp, lr);
985 }
986
987 pub fn flatten_params(&self) -> Vec<f32> {
990 let mut params = Vec::with_capacity(
991 self.embedding.len()
992 + self.w_q.len()
993 + self.w_k.len()
994 + self.w_v.len()
995 + self.edge_weights_raw.len()
996 + self.mlp.flatten_params().len(),
997 );
998 params.extend_from_slice(&self.embedding);
999 params.extend_from_slice(&self.w_q);
1000 params.extend_from_slice(&self.w_k);
1001 params.extend_from_slice(&self.w_v);
1002 params.extend_from_slice(&self.edge_weights_raw);
1003 params.extend_from_slice(&self.mlp.flatten_params());
1004 params
1005 }
1006
1007 pub fn load_flat_params(&mut self, params: &[f32]) {
1009 let embed_len = self.embedding.len();
1010 let wq_len = self.w_q.len();
1011 let wk_len = self.w_k.len();
1012 let wv_len = self.w_v.len();
1013 let edge_len = self.edge_weights_raw.len();
1014 let mlp_len = self.mlp.flatten_params().len();
1015 let expected = embed_len + wq_len + wk_len + wv_len + edge_len + mlp_len;
1016 assert_eq!(params.len(), expected, "load_flat_params: size mismatch");
1017
1018 let mut off = 0;
1019 self.embedding
1020 .copy_from_slice(¶ms[off..off + embed_len]);
1021 off += embed_len;
1022 self.w_q.copy_from_slice(¶ms[off..off + wq_len]);
1023 off += wq_len;
1024 self.w_k.copy_from_slice(¶ms[off..off + wk_len]);
1025 off += wk_len;
1026 self.w_v.copy_from_slice(¶ms[off..off + wv_len]);
1027 off += wv_len;
1028 self.edge_weights_raw
1029 .copy_from_slice(¶ms[off..off + edge_len]);
1030 off += edge_len;
1031 self.mlp.load_flat_params(¶ms[off..off + mlp_len]);
1032 }
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038
1039 #[test]
1040 fn forward_produces_logits() {
1041 let model = GraphAttentionClassifier::new(10, 8, 16, 3, 2, 1, None, false);
1042 let token_ids = vec![0u32, 1, 2];
1043 let neighbors_owned: Vec<Vec<usize>> = vec![vec![0], vec![1], vec![2]];
1044 let neighbors: Vec<&[usize]> = neighbors_owned.iter().map(|v| v.as_slice()).collect();
1045 let logits = model.forward(&token_ids, &neighbors);
1046 assert_eq!(logits.len(), 3);
1047 }
1048
1049 #[test]
1050 fn learns_simple_task() {
1051 let mut model = GraphAttentionClassifier::new(4, 8, 16, 2, 1, 7, None, false);
1054 let lr = 0.5;
1055
1056 for _ in 0..2000 {
1057 let examples: Vec<(Vec<u32>, Vec<Vec<usize>>, usize)> = vec![
1058 (vec![0, 0, 1], vec![vec![0], vec![1], vec![2]], 0),
1059 (vec![0, 1, 2], vec![vec![0], vec![1], vec![2]], 1),
1060 (vec![1, 1, 0], vec![vec![0], vec![1], vec![2]], 0),
1061 (vec![1, 2, 0], vec![vec![0], vec![1], vec![2]], 1),
1062 ];
1063 for (ids, nbrs, target) in examples {
1064 let (grad, _) = model.backward_batch(&ids, &[nbrs], &[target], 1);
1065 model.apply_sgd(&grad, lr);
1066 }
1067 }
1068
1069 let neighbors_owned: Vec<Vec<usize>> = vec![vec![0], vec![1], vec![2]];
1070 let neighbors: Vec<&[usize]> = neighbors_owned.iter().map(|v| v.as_slice()).collect();
1071 assert_eq!(model.predict(&[0, 0, 1], &neighbors), 0);
1072 assert_eq!(model.predict(&[0, 1, 2], &neighbors), 1);
1073 }
1074
1075 #[test]
1076 fn learns_with_rope() {
1077 let mut model = GraphAttentionClassifier::new(4, 8, 16, 2, 1, 7, Some(10000.0), false);
1079 let lr = 0.5;
1080
1081 for _ in 0..3000 {
1082 let examples: Vec<(Vec<u32>, Vec<Vec<usize>>, usize)> = vec![
1083 (vec![0, 0, 1], vec![vec![0], vec![1], vec![2]], 0),
1084 (vec![0, 1, 2], vec![vec![0], vec![1], vec![2]], 1),
1085 (vec![1, 1, 0], vec![vec![0], vec![1], vec![2]], 0),
1086 (vec![1, 2, 0], vec![vec![0], vec![1], vec![2]], 1),
1087 ];
1088 for (ids, nbrs, target) in examples {
1089 let (grad, _) = model.backward_batch(&ids, &[nbrs], &[target], 1);
1090 model.apply_sgd(&grad, lr);
1091 }
1092 }
1093
1094 let neighbors_owned: Vec<Vec<usize>> = vec![vec![0], vec![1], vec![2]];
1095 let neighbors: Vec<&[usize]> = neighbors_owned.iter().map(|v| v.as_slice()).collect();
1096 assert_eq!(model.predict(&[0, 0, 1], &neighbors), 0);
1097 assert_eq!(model.predict(&[0, 1, 2], &neighbors), 1);
1098 }
1099
1100 #[test]
1101 fn learns_simple_task_geometric_only() {
1102 let mut model = GraphAttentionClassifier::new(4, 8, 16, 2, 1, 7, None, false);
1106 model.set_geometric_attention_only(true);
1107 let lr = 0.5;
1108
1109 for _ in 0..2000 {
1110 let examples: Vec<(Vec<u32>, Vec<Vec<usize>>, usize)> = vec![
1111 (vec![0, 0, 1], vec![vec![0], vec![1], vec![2]], 0),
1112 (vec![0, 1, 2], vec![vec![0], vec![1], vec![2]], 1),
1113 (vec![1, 1, 0], vec![vec![0], vec![1], vec![2]], 0),
1114 (vec![1, 2, 0], vec![vec![0], vec![1], vec![2]], 1),
1115 ];
1116 for (ids, nbrs, target) in examples {
1117 let (grad, _) = model.backward_batch(&ids, &[nbrs], &[target], 1);
1118 model.apply_sgd(&grad, lr);
1119 }
1120 }
1121
1122 let neighbors_owned: Vec<Vec<usize>> = vec![vec![0], vec![1], vec![2]];
1123 let neighbors: Vec<&[usize]> = neighbors_owned.iter().map(|v| v.as_slice()).collect();
1124 assert_eq!(model.predict(&[0, 0, 1], &neighbors), 0);
1125 assert_eq!(model.predict(&[0, 1, 2], &neighbors), 1);
1126 }
1127}