1#[derive(Debug, Clone)]
49pub enum AttnError {
50 DimensionMismatch {
52 op: String,
53 expected: String,
54 got: String,
55 },
56 EmptyInput,
58 InvalidConfig(String),
60}
61
62impl std::fmt::Display for AttnError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Self::DimensionMismatch { op, expected, got } => {
66 write!(
67 f,
68 "DimensionMismatch in {op}: expected {expected}, got {got}"
69 )
70 }
71 Self::EmptyInput => write!(f, "EmptyInput: sequence length is 0"),
72 Self::InvalidConfig(msg) => write!(f, "InvalidConfig: {msg}"),
73 }
74 }
75}
76
77impl std::error::Error for AttnError {}
78
79#[derive(Debug, Clone)]
85pub struct AttentionMatrix {
86 pub values: Vec<f64>,
88 pub rows: usize,
90 pub cols: usize,
92}
93
94impl AttentionMatrix {
95 pub fn zeros(rows: usize, cols: usize) -> Self {
97 Self {
98 values: vec![0.0; rows * cols],
99 rows,
100 cols,
101 }
102 }
103
104 #[inline]
106 pub fn get(&self, row: usize, col: usize) -> f64 {
107 if row < self.rows && col < self.cols {
108 self.values[row * self.cols + col]
109 } else {
110 0.0
111 }
112 }
113
114 #[inline]
116 pub fn set(&mut self, row: usize, col: usize, v: f64) {
117 if row < self.rows && col < self.cols {
118 self.values[row * self.cols + col] = v;
119 }
120 }
121
122 pub fn matmul(a: &AttentionMatrix, b: &AttentionMatrix) -> Result<AttentionMatrix, AttnError> {
126 if a.cols != b.rows {
127 return Err(AttnError::DimensionMismatch {
128 op: "AttentionMatrix::matmul".to_string(),
129 expected: format!("b.rows == {}", a.cols),
130 got: format!("b.rows == {}", b.rows),
131 });
132 }
133 let m = a.rows;
134 let k = a.cols;
135 let n = b.cols;
136 let mut out = AttentionMatrix::zeros(m, n);
137 for i in 0..m {
138 for p in 0..k {
139 let a_val = a.values[i * k + p];
140 if a_val == 0.0 {
141 continue;
142 }
143 for j in 0..n {
144 out.values[i * n + j] += a_val * b.values[p * n + j];
145 }
146 }
147 }
148 Ok(out)
149 }
150
151 pub fn transpose(&self) -> AttentionMatrix {
153 let mut out = AttentionMatrix::zeros(self.cols, self.rows);
154 for r in 0..self.rows {
155 for c in 0..self.cols {
156 out.values[c * self.rows + r] = self.values[r * self.cols + c];
157 }
158 }
159 out
160 }
161
162 pub fn softmax_rows(&self) -> AttentionMatrix {
164 let mut out = AttentionMatrix::zeros(self.rows, self.cols);
165 for r in 0..self.rows {
166 let start = r * self.cols;
167 let end = start + self.cols;
168 let row = &self.values[start..end];
169 let max_val = row.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
170 let exps: Vec<f64> = row.iter().map(|x| (x - max_val).exp()).collect();
171 let sum: f64 = exps.iter().sum();
172 let denom = if sum == 0.0 { 1.0 } else { sum };
173 for (c, exp_val) in exps.iter().enumerate() {
174 out.values[start + c] = exp_val / denom;
175 }
176 }
177 out
178 }
179
180 fn add_pos_enc(&self, pos_enc: &AttentionMatrix) -> Result<AttentionMatrix, AttnError> {
184 if self.cols != pos_enc.cols {
185 return Err(AttnError::DimensionMismatch {
186 op: "add_pos_enc".to_string(),
187 expected: format!("pos_enc.cols == {}", self.cols),
188 got: format!("pos_enc.cols == {}", pos_enc.cols),
189 });
190 }
191 let seq_len = self.rows.min(pos_enc.rows);
192 let mut out = self.clone();
193 for r in 0..seq_len {
194 for c in 0..self.cols {
195 out.values[r * self.cols + c] += pos_enc.values[r * pos_enc.cols + c];
196 }
197 }
198 Ok(out)
199 }
200
201 fn hconcat(mats: &[AttentionMatrix]) -> Result<AttentionMatrix, AttnError> {
205 if mats.is_empty() {
206 return Ok(AttentionMatrix::zeros(0, 0));
207 }
208 let rows = mats[0].rows;
209 let total_cols: usize = mats.iter().map(|m| m.cols).sum();
210 for m in mats.iter().skip(1) {
211 if m.rows != rows {
212 return Err(AttnError::DimensionMismatch {
213 op: "hconcat".to_string(),
214 expected: format!("rows == {rows}"),
215 got: format!("rows == {}", m.rows),
216 });
217 }
218 }
219 let mut out = AttentionMatrix::zeros(rows, total_cols);
220 let mut col_offset = 0usize;
221 for m in mats {
222 for r in 0..rows {
223 for c in 0..m.cols {
224 out.values[r * total_cols + col_offset + c] = m.values[r * m.cols + c];
225 }
226 }
227 col_offset += m.cols;
228 }
229 Ok(out)
230 }
231}
232
233#[derive(Debug, Clone)]
239pub struct AttentionConfig {
240 pub num_heads: usize,
242 pub head_dim: usize,
244 pub dropout_rate: f64,
246 pub use_causal_mask: bool,
249}
250
251impl AttentionConfig {
252 pub fn model_dim(&self) -> usize {
254 self.num_heads * self.head_dim
255 }
256}
257
258#[derive(Debug, Clone)]
266pub struct AttentionHead {
267 pub query_proj: AttentionMatrix,
269 pub key_proj: AttentionMatrix,
271 pub value_proj: AttentionMatrix,
273}
274
275#[derive(Debug, Clone)]
281pub struct AttentionOutput {
282 pub output: AttentionMatrix,
285 pub attention_weights: Vec<AttentionMatrix>,
288 pub head_outputs: Vec<AttentionMatrix>,
291}
292
293#[derive(Debug, Clone)]
302pub struct PositionalEncoding {
303 pub max_seq_len: usize,
305 pub encoding_dim: usize,
307 pub encodings: AttentionMatrix,
309}
310
311impl PositionalEncoding {
312 pub fn new(max_seq_len: usize, encoding_dim: usize) -> Self {
314 let mut enc = AttentionMatrix::zeros(max_seq_len, encoding_dim);
315 for pos in 0..max_seq_len {
316 for i in 0..encoding_dim {
317 let half_i = (i / 2) as f64;
318 let denom = 10000_f64.powf(2.0 * half_i / encoding_dim.max(1) as f64);
319 let angle = pos as f64 / denom;
320 let v = if i % 2 == 0 { angle.sin() } else { angle.cos() };
321 enc.set(pos, i, v);
322 }
323 }
324 Self {
325 max_seq_len,
326 encoding_dim,
327 encodings: enc,
328 }
329 }
330
331 pub fn slice(&self, seq_len: usize) -> AttentionMatrix {
333 let n = seq_len.min(self.max_seq_len);
334 let mut out = AttentionMatrix::zeros(n, self.encoding_dim);
335 for r in 0..n {
336 for c in 0..self.encoding_dim {
337 out.values[r * self.encoding_dim + c] =
338 self.encodings.values[r * self.encoding_dim + c];
339 }
340 }
341 out
342 }
343}
344
345#[derive(Debug, Clone)]
351pub struct AttnStats {
352 pub num_heads: usize,
354 pub head_dim: usize,
356 pub model_dim: usize,
358 pub forward_count: u64,
360 pub max_seq_len: usize,
362}
363
364pub struct AttentionMechanism {
372 pub config: AttentionConfig,
374 pub heads: Vec<AttentionHead>,
376 pub output_proj: AttentionMatrix,
378 pub pos_enc: PositionalEncoding,
380 pub forward_count: u64,
382}
383
384impl AttentionMechanism {
385 pub fn new(config: AttentionConfig, max_seq_len: usize) -> Self {
390 let model_dim = config.model_dim();
391 let head_dim = config.head_dim;
392 let init_val = if model_dim > 0 {
393 1.0 / (model_dim as f64).sqrt()
394 } else {
395 0.0
396 };
397
398 let make_proj = |rows: usize, cols: usize| {
399 let mut m = AttentionMatrix::zeros(rows, cols);
400 for v in m.values.iter_mut() {
401 *v = init_val;
402 }
403 m
404 };
405
406 let heads: Vec<AttentionHead> = (0..config.num_heads)
407 .map(|_| AttentionHead {
408 query_proj: make_proj(head_dim, model_dim),
409 key_proj: make_proj(head_dim, model_dim),
410 value_proj: make_proj(head_dim, model_dim),
411 })
412 .collect();
413
414 let output_proj = make_proj(model_dim, model_dim);
415 let pos_enc = PositionalEncoding::new(max_seq_len, model_dim);
416
417 Self {
418 config,
419 heads,
420 output_proj,
421 pos_enc,
422 forward_count: 0,
423 }
424 }
425
426 pub fn stats(&self) -> AttnStats {
428 AttnStats {
429 num_heads: self.config.num_heads,
430 head_dim: self.config.head_dim,
431 model_dim: self.config.model_dim(),
432 forward_count: self.forward_count,
433 max_seq_len: self.pos_enc.max_seq_len,
434 }
435 }
436
437 pub fn scaled_dot_product(
451 &self,
452 q: &AttentionMatrix,
453 k: &AttentionMatrix,
454 v: &AttentionMatrix,
455 mask: Option<&AttentionMatrix>,
456 ) -> Result<(AttentionMatrix, AttentionMatrix), AttnError> {
457 let seq_len = q.rows;
458 let scale = if self.config.head_dim > 0 {
459 (self.config.head_dim as f64).sqrt()
460 } else {
461 1.0
462 };
463
464 let k_t = k.transpose();
466 let mut scores = AttentionMatrix::matmul(q, &k_t)?;
467
468 for r in 0..seq_len {
470 for c in 0..seq_len {
471 let idx = r * seq_len + c;
472 scores.values[idx] /= scale;
473 if let Some(m) = mask {
474 if m.get(r, c) == 1.0 {
475 scores.values[idx] = -1e9;
476 }
477 }
478 }
479 }
480
481 let weights = scores.softmax_rows();
483
484 let output = AttentionMatrix::matmul(&weights, v)?;
486
487 Ok((output, weights))
488 }
489
490 pub fn causal_mask(seq_len: usize) -> AttentionMatrix {
492 let mut m = AttentionMatrix::zeros(seq_len, seq_len);
493 for i in 0..seq_len {
494 for j in (i + 1)..seq_len {
495 m.set(i, j, 1.0);
496 }
497 }
498 m
499 }
500
501 pub fn forward(&mut self, input: &AttentionMatrix) -> Result<AttentionOutput, AttnError> {
509 let seq_len = input.rows;
510 if seq_len == 0 {
511 return Err(AttnError::EmptyInput);
512 }
513 let model_dim = self.config.model_dim();
514 if input.cols != model_dim {
515 return Err(AttnError::DimensionMismatch {
516 op: "forward".to_string(),
517 expected: format!("input.cols == {model_dim}"),
518 got: format!("input.cols == {}", input.cols),
519 });
520 }
521
522 let pos_slice = self.pos_enc.slice(seq_len);
524 let x = input.add_pos_enc(&pos_slice)?;
525
526 let mask_opt: Option<AttentionMatrix> = if self.config.use_causal_mask {
527 Some(Self::causal_mask(seq_len))
528 } else {
529 None
530 };
531
532 let mut head_out_list: Vec<AttentionMatrix> = Vec::with_capacity(self.config.num_heads);
533 let mut weight_list: Vec<AttentionMatrix> = Vec::with_capacity(self.config.num_heads);
534
535 for head in &self.heads {
536 let wq_t = head.query_proj.transpose();
538 let wk_t = head.key_proj.transpose();
539 let wv_t = head.value_proj.transpose();
540
541 let q = AttentionMatrix::matmul(&x, &wq_t)?;
542 let k = AttentionMatrix::matmul(&x, &wk_t)?;
543 let v = AttentionMatrix::matmul(&x, &wv_t)?;
544
545 let (h_out, h_weights) = self.scaled_dot_product(&q, &k, &v, mask_opt.as_ref())?;
546
547 head_out_list.push(h_out);
548 weight_list.push(h_weights);
549 }
550
551 let concat = AttentionMatrix::hconcat(&head_out_list)?;
553
554 let wo_t = self.output_proj.transpose();
556 let final_output = AttentionMatrix::matmul(&concat, &wo_t)?;
557
558 self.forward_count += 1;
559
560 Ok(AttentionOutput {
561 output: final_output,
562 attention_weights: weight_list,
563 head_outputs: head_out_list,
564 })
565 }
566
567 pub fn attention_entropy(weights: &AttentionMatrix) -> Vec<f64> {
571 (0..weights.rows)
572 .map(|r| {
573 let start = r * weights.cols;
574 let end = start + weights.cols;
575 weights.values[start..end]
576 .iter()
577 .map(|&w| -w * (w + 1e-10_f64).ln())
578 .sum()
579 })
580 .collect()
581 }
582
583 pub fn peak_attention(weights: &AttentionMatrix) -> Vec<usize> {
585 (0..weights.rows)
586 .map(|r| {
587 let start = r * weights.cols;
588 let end = start + weights.cols;
589 weights.values[start..end]
590 .iter()
591 .enumerate()
592 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
593 .map(|(i, _)| i)
594 .unwrap_or(0)
595 })
596 .collect()
597 }
598}
599
600#[derive(Debug, Clone)]
606pub struct SimpleAttentionConfig {
607 pub num_heads: usize,
609 pub head_dim: usize,
611 pub dropout_rate: f64,
614 pub causal_mask: bool,
617 pub scale: Option<f64>,
619}
620
621#[derive(Debug, Clone)]
623pub struct SimpleAttentionOutput {
624 pub output: Vec<Vec<f64>>,
626 pub attention_weights: Vec<Vec<f64>>,
628}
629
630#[derive(Debug, Clone, Default)]
632pub struct SimpleAttentionStats {
633 pub total_calls: u64,
635 pub total_tokens: u64,
637 pub avg_seq_len: f64,
639}
640
641pub struct SimpleAttentionMechanism {
646 config: SimpleAttentionConfig,
647 stats: SimpleAttentionStats,
648}
649
650impl SimpleAttentionMechanism {
651 pub fn new(config: SimpleAttentionConfig) -> Self {
653 Self {
654 config,
655 stats: SimpleAttentionStats::default(),
656 }
657 }
658
659 pub fn stats(&self) -> &SimpleAttentionStats {
661 &self.stats
662 }
663
664 pub fn attend(
674 &mut self,
675 queries: &[Vec<f64>],
676 keys: &[Vec<f64>],
677 values: &[Vec<f64>],
678 ) -> SimpleAttentionOutput {
679 let seq_len = queries.len();
680
681 self.stats.total_calls += 1;
682 self.stats.total_tokens += seq_len as u64;
683 let n = self.stats.total_calls as f64;
684 self.stats.avg_seq_len += (seq_len as f64 - self.stats.avg_seq_len) / n;
685
686 if seq_len == 0 {
687 return SimpleAttentionOutput {
688 output: vec![],
689 attention_weights: vec![],
690 };
691 }
692
693 let scale = self
694 .config
695 .scale
696 .unwrap_or_else(|| 1.0 / (self.config.head_dim as f64).sqrt());
697
698 let causal = if self.config.causal_mask {
699 Some(causal_mask(seq_len))
700 } else {
701 None
702 };
703 let mask_ref = causal.as_deref();
704
705 let num_heads = self.config.num_heads;
706 let head_dim = self.config.head_dim;
707 let d_model = num_heads * head_dim;
708
709 let mut head_outputs: Vec<Vec<Vec<f64>>> = Vec::with_capacity(num_heads);
710 let mut weight_sum: Vec<Vec<f64>> = vec![vec![0.0; seq_len]; seq_len];
711
712 for h in 0..num_heads {
713 let col_start = h * head_dim;
714 let col_end = col_start + head_dim;
715
716 let q_h = slice_cols(queries, col_start, col_end);
717 let k_h = slice_cols(keys, col_start, col_end);
718 let v_h = slice_cols(values, col_start, col_end);
719
720 let out_h = scaled_dot_product_attention(&q_h, &k_h, &v_h, scale, mask_ref);
721
722 for (i, row) in weight_sum.iter_mut().enumerate().take(seq_len) {
723 for (j, cell) in row.iter_mut().enumerate().take(seq_len) {
724 *cell += out_h.attention_weights[i].get(j).copied().unwrap_or(0.0);
725 }
726 }
727
728 head_outputs.push(out_h.output);
729 }
730
731 let n_heads_f = num_heads as f64;
732 let attention_weights: Vec<Vec<f64>> = weight_sum
733 .iter()
734 .map(|row| row.iter().map(|w| w / n_heads_f).collect())
735 .collect();
736
737 let mut output = vec![vec![0.0; d_model]; seq_len];
738 for (h, head_out) in head_outputs.iter().enumerate() {
739 let col_start = h * head_dim;
740 for (i, row) in head_out.iter().enumerate() {
741 for (j, val) in row.iter().enumerate() {
742 output[i][col_start + j] = *val;
743 }
744 }
745 }
746
747 SimpleAttentionOutput {
748 output,
749 attention_weights,
750 }
751 }
752}
753
754pub fn scaled_dot_product_attention(
764 queries: &[Vec<f64>],
765 keys: &[Vec<f64>],
766 values: &[Vec<f64>],
767 scale: f64,
768 mask: Option<&[Vec<bool>]>,
769) -> SimpleAttentionOutput {
770 let seq_len = queries.len();
771 if seq_len == 0 {
772 return SimpleAttentionOutput {
773 output: vec![],
774 attention_weights: vec![],
775 };
776 }
777
778 let k_t = transpose(keys);
779 let mut scores = matmul(queries, &k_t);
780
781 let safe_scale = if scale.abs() < 1e-12 { 1.0 } else { scale };
782 for (i, row) in scores.iter_mut().enumerate().take(seq_len) {
783 for (j, cell) in row.iter_mut().enumerate().take(seq_len) {
784 *cell /= safe_scale;
785 if let Some(m) = mask {
786 if m.get(i).and_then(|r| r.get(j)).copied().unwrap_or(false) {
787 *cell = -1e9;
788 }
789 }
790 }
791 }
792
793 let attention_weights: Vec<Vec<f64>> = scores.iter().map(|row| softmax_1d(row)).collect();
794 let output = matmul(&attention_weights, values);
795
796 SimpleAttentionOutput {
797 output,
798 attention_weights,
799 }
800}
801
802pub fn softmax_1d(logits: &[f64]) -> Vec<f64> {
806 if logits.is_empty() {
807 return vec![];
808 }
809
810 let max_val = logits.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
811
812 let exps: Vec<f64> = logits.iter().map(|x| (x - max_val).exp()).collect();
813 let sum: f64 = exps.iter().sum();
814
815 if sum == 0.0 {
816 let n = logits.len() as f64;
817 return vec![1.0 / n; logits.len()];
818 }
819
820 exps.iter().map(|e| e / sum).collect()
821}
822
823pub fn matmul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
827 let m = a.len();
828 if m == 0 || b.is_empty() {
829 return vec![];
830 }
831
832 let k = b.len();
833 let n = b.first().map(|r| r.len()).unwrap_or(0);
834
835 let mut result = vec![vec![0.0; n]; m];
836 for i in 0..m {
837 let a_row = &a[i];
838 let a_len = a_row.len().min(k);
839 for p in 0..a_len {
840 let a_val = a_row[p];
841 if a_val == 0.0 {
842 continue;
843 }
844 let b_row = &b[p];
845 let b_len = b_row.len().min(n);
846 for j in 0..b_len {
847 result[i][j] += a_val * b_row[j];
848 }
849 }
850 }
851 result
852}
853
854pub fn transpose(m: &[Vec<f64>]) -> Vec<Vec<f64>> {
856 let rows = m.len();
857 if rows == 0 {
858 return vec![];
859 }
860
861 let cols = m.iter().map(|r| r.len()).max().unwrap_or(0);
862 if cols == 0 {
863 return vec![];
864 }
865
866 let mut out = vec![vec![0.0; rows]; cols];
867 for (i, row) in m.iter().enumerate() {
868 for (j, val) in row.iter().enumerate() {
869 out[j][i] = *val;
870 }
871 }
872 out
873}
874
875pub fn causal_mask(seq_len: usize) -> Vec<Vec<bool>> {
879 (0..seq_len)
880 .map(|i| (0..seq_len).map(|j| j > i).collect())
881 .collect()
882}
883
884fn slice_cols(m: &[Vec<f64>], col_start: usize, col_end: usize) -> Vec<Vec<f64>> {
887 m.iter()
888 .map(|row| {
889 (col_start..col_end)
890 .map(|c| row.get(c).copied().unwrap_or(0.0))
891 .collect()
892 })
893 .collect()
894}
895
896#[cfg(test)]
901mod tests {
902 use crate::attention_mechanism::{
903 causal_mask, matmul, scaled_dot_product_attention, softmax_1d, transpose, AttentionConfig,
904 AttentionMatrix, AttentionMechanism, AttnError, PositionalEncoding, SimpleAttentionConfig,
905 SimpleAttentionMechanism,
906 };
907
908 #[test]
911 fn softmax_sums_to_one_uniform() {
912 let logits = vec![1.0, 2.0, 3.0, 4.0];
913 let result = softmax_1d(&logits);
914 let sum: f64 = result.iter().sum();
915 assert!((sum - 1.0).abs() < 1e-12, "softmax sum = {sum}");
916 }
917
918 #[test]
919 fn softmax_sums_to_one_negative_values() {
920 let logits = vec![-100.0, -50.0, -1.0];
921 let result = softmax_1d(&logits);
922 let sum: f64 = result.iter().sum();
923 assert!((sum - 1.0).abs() < 1e-12, "softmax sum = {sum}");
924 }
925
926 #[test]
927 fn softmax_numerical_stability_large_values() {
928 let logits = vec![1e308, 1e308 + 1.0, 1e308 + 2.0];
929 let result = softmax_1d(&logits);
930 let sum: f64 = result.iter().sum();
931 assert!((sum - 1.0).abs() < 1e-12, "softmax sum = {sum}");
932 assert!(result.iter().all(|v| v.is_finite()));
933 }
934
935 #[test]
936 fn softmax_numerical_stability_very_negative() {
937 let logits = vec![-1e308, -1e308, -1e308];
938 let result = softmax_1d(&logits);
939 let sum: f64 = result.iter().sum();
940 assert!((sum - 1.0).abs() < 1e-9, "softmax sum = {sum}");
941 }
942
943 #[test]
944 fn softmax_single_element() {
945 let result = softmax_1d(&[42.0]);
946 assert!((result[0] - 1.0).abs() < 1e-15);
947 }
948
949 #[test]
950 fn softmax_empty() {
951 assert!(softmax_1d(&[]).is_empty());
952 }
953
954 #[test]
955 fn softmax_monotone_order() {
956 let logits = vec![1.0, 3.0, 2.0];
957 let result = softmax_1d(&logits);
958 assert!(result[1] > result[2]);
959 assert!(result[2] > result[0]);
960 }
961
962 #[test]
965 fn matmul_identity() {
966 let a = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
967 let b = vec![vec![5.0, 6.0], vec![7.0, 8.0]];
968 let c = matmul(&a, &b);
969 assert!((c[0][0] - 5.0).abs() < 1e-15);
970 assert!((c[0][1] - 6.0).abs() < 1e-15);
971 assert!((c[1][0] - 7.0).abs() < 1e-15);
972 assert!((c[1][1] - 8.0).abs() < 1e-15);
973 }
974
975 #[test]
976 fn matmul_known_values() {
977 let a = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
978 let b = vec![vec![5.0, 6.0], vec![7.0, 8.0]];
979 let c = matmul(&a, &b);
980 assert!((c[0][0] - 19.0).abs() < 1e-12);
981 assert!((c[0][1] - 22.0).abs() < 1e-12);
982 assert!((c[1][0] - 43.0).abs() < 1e-12);
983 assert!((c[1][1] - 50.0).abs() < 1e-12);
984 }
985
986 #[test]
987 fn matmul_non_square() {
988 let a = vec![vec![1.0, 0.0, 2.0], vec![0.0, 3.0, 1.0]];
989 let b = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![2.0, 3.0]];
990 let c = matmul(&a, &b);
991 assert!((c[0][0] - 5.0).abs() < 1e-12);
992 assert!((c[0][1] - 6.0).abs() < 1e-12);
993 assert!((c[1][0] - 2.0).abs() < 1e-12);
994 assert!((c[1][1] - 6.0).abs() < 1e-12);
995 }
996
997 #[test]
998 fn matmul_empty_returns_empty() {
999 let empty: Vec<Vec<f64>> = vec![];
1000 assert!(matmul(&empty, &empty).is_empty());
1001 }
1002
1003 #[test]
1006 fn transpose_square() {
1007 let m = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
1008 let t = transpose(&m);
1009 assert!((t[0][0] - 1.0).abs() < 1e-15);
1010 assert!((t[0][1] - 3.0).abs() < 1e-15);
1011 assert!((t[1][0] - 2.0).abs() < 1e-15);
1012 assert!((t[1][1] - 4.0).abs() < 1e-15);
1013 }
1014
1015 #[test]
1016 fn transpose_rectangular() {
1017 let m = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1018 let t = transpose(&m);
1019 assert_eq!(t.len(), 3);
1020 assert_eq!(t[0].len(), 2);
1021 assert!((t[1][1] - 5.0).abs() < 1e-15);
1022 assert!((t[2][0] - 3.0).abs() < 1e-15);
1023 }
1024
1025 #[test]
1026 fn transpose_double_returns_original() {
1027 let m = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1028 let tt = transpose(&transpose(&m));
1029 for (r, row) in m.iter().enumerate() {
1030 for (c, val) in row.iter().enumerate() {
1031 assert!((tt[r][c] - val).abs() < 1e-15);
1032 }
1033 }
1034 }
1035
1036 #[test]
1037 fn transpose_empty() {
1038 let empty: Vec<Vec<f64>> = vec![];
1039 assert!(transpose(&empty).is_empty());
1040 }
1041
1042 #[test]
1045 fn causal_mask_upper_triangle_masked() {
1046 let mask = causal_mask(4);
1047 for (i, row) in mask.iter().enumerate() {
1048 for (j, &masked) in row.iter().enumerate().take(i + 1) {
1049 assert!(!masked, "position ({i},{j}) should NOT be masked");
1050 }
1051 }
1052 for (i, row) in mask.iter().enumerate() {
1053 for (j, &masked) in row.iter().enumerate().skip(i + 1) {
1054 assert!(masked, "position ({i},{j}) should be masked");
1055 }
1056 }
1057 }
1058
1059 #[test]
1060 fn causal_mask_size_one() {
1061 let mask = causal_mask(1);
1062 assert_eq!(mask.len(), 1);
1063 assert!(!mask[0][0]);
1064 }
1065
1066 #[test]
1067 fn causal_mask_dimensions() {
1068 let n = 6;
1069 let mask = causal_mask(n);
1070 assert_eq!(mask.len(), n);
1071 assert!(mask.iter().all(|row| row.len() == n));
1072 }
1073
1074 #[test]
1077 fn sdp_output_shape() {
1078 let q = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
1079 let k = q.clone();
1080 let v = q.clone();
1081 let out = scaled_dot_product_attention(&q, &k, &v, 1.0, None);
1082 assert_eq!(out.output.len(), 3);
1083 assert_eq!(out.output[0].len(), 2);
1084 assert_eq!(out.attention_weights.len(), 3);
1085 assert_eq!(out.attention_weights[0].len(), 3);
1086 }
1087
1088 #[test]
1089 fn sdp_attention_weights_sum_to_one_per_row() {
1090 let q = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]];
1091 let k = q.clone();
1092 let v = q.clone();
1093 let out = scaled_dot_product_attention(&q, &k, &v, 1.0, None);
1094 for (i, row) in out.attention_weights.iter().enumerate() {
1095 let s: f64 = row.iter().sum();
1096 assert!((s - 1.0).abs() < 1e-12, "row {i} sums to {s}");
1097 }
1098 }
1099
1100 #[test]
1101 fn sdp_causal_mask_suppresses_future() {
1102 let q = vec![vec![1.0], vec![1.0], vec![1.0]];
1103 let k = q.clone();
1104 let v = vec![vec![10.0], vec![20.0], vec![30.0]];
1105 let mask = causal_mask(3);
1106 let out = scaled_dot_product_attention(&q, &k, &v, 1.0, Some(&mask));
1107 assert!(out.attention_weights[0][1] < 1e-6);
1108 assert!(out.attention_weights[0][2] < 1e-6);
1109 assert!(out.attention_weights[2][0] > 1e-6);
1110 assert!(out.attention_weights[2][1] > 1e-6);
1111 }
1112
1113 #[test]
1114 fn sdp_single_token() {
1115 let q = vec![vec![1.0, 2.0, 3.0]];
1116 let k = q.clone();
1117 let v = vec![vec![5.0, 6.0, 7.0]];
1118 let out = scaled_dot_product_attention(&q, &k, &v, 1.0, None);
1119 assert_eq!(out.output.len(), 1);
1120 assert!((out.attention_weights[0][0] - 1.0).abs() < 1e-12);
1121 assert!((out.output[0][0] - 5.0).abs() < 1e-12);
1122 }
1123
1124 fn make_simple(heads: usize, head_dim: usize, causal: bool) -> SimpleAttentionMechanism {
1127 SimpleAttentionMechanism::new(SimpleAttentionConfig {
1128 num_heads: heads,
1129 head_dim,
1130 dropout_rate: 0.0,
1131 causal_mask: causal,
1132 scale: None,
1133 })
1134 }
1135
1136 #[test]
1137 fn simple_attend_output_shape() {
1138 let mut attn = make_simple(2, 4, false);
1139 let d_model = 8;
1140 let seq_len = 5;
1141 let q = vec![vec![1.0; d_model]; seq_len];
1142 let out = attn.attend(&q, &q, &q);
1143 assert_eq!(out.output.len(), seq_len);
1144 assert_eq!(out.output[0].len(), d_model);
1145 assert_eq!(out.attention_weights.len(), seq_len);
1146 }
1147
1148 #[test]
1149 fn simple_attend_stats_tracking() {
1150 let mut attn = make_simple(1, 4, false);
1151 let q = vec![vec![1.0; 4]; 3];
1152 attn.attend(&q, &q, &q);
1153 attn.attend(&q, &q, &q);
1154 assert_eq!(attn.stats().total_calls, 2);
1155 assert_eq!(attn.stats().total_tokens, 6);
1156 }
1157
1158 #[test]
1161 fn attn_matrix_zeros() {
1162 let m = AttentionMatrix::zeros(3, 4);
1163 assert_eq!(m.rows, 3);
1164 assert_eq!(m.cols, 4);
1165 assert!(m.values.iter().all(|&v| v == 0.0));
1166 }
1167
1168 #[test]
1169 fn attn_matrix_get_set() {
1170 let mut m = AttentionMatrix::zeros(2, 3);
1171 m.set(0, 1, 7.0);
1172 assert!((m.get(0, 1) - 7.0).abs() < 1e-15);
1173 assert_eq!(m.get(0, 0), 0.0);
1174 assert_eq!(m.get(10, 10), 0.0);
1176 m.set(10, 10, 99.0);
1177 }
1178
1179 #[test]
1180 fn attn_matrix_matmul_correct() {
1181 let mut a = AttentionMatrix::zeros(2, 2);
1182 a.set(0, 0, 1.0);
1183 a.set(0, 1, 2.0);
1184 a.set(1, 0, 3.0);
1185 a.set(1, 1, 4.0);
1186 let mut b = AttentionMatrix::zeros(2, 2);
1187 b.set(0, 0, 5.0);
1188 b.set(0, 1, 6.0);
1189 b.set(1, 0, 7.0);
1190 b.set(1, 1, 8.0);
1191 let c = AttentionMatrix::matmul(&a, &b).expect("test: should succeed");
1192 assert!((c.get(0, 0) - 19.0).abs() < 1e-12);
1193 assert!((c.get(0, 1) - 22.0).abs() < 1e-12);
1194 assert!((c.get(1, 0) - 43.0).abs() < 1e-12);
1195 assert!((c.get(1, 1) - 50.0).abs() < 1e-12);
1196 }
1197
1198 #[test]
1199 fn attn_matrix_matmul_dim_mismatch() {
1200 let a = AttentionMatrix::zeros(2, 3);
1201 let b = AttentionMatrix::zeros(2, 2); let result = AttentionMatrix::matmul(&a, &b);
1203 assert!(matches!(result, Err(AttnError::DimensionMismatch { .. })));
1204 }
1205
1206 #[test]
1207 fn attn_matrix_transpose() {
1208 let mut m = AttentionMatrix::zeros(2, 3);
1209 m.set(0, 0, 1.0);
1210 m.set(0, 1, 2.0);
1211 m.set(0, 2, 3.0);
1212 m.set(1, 0, 4.0);
1213 m.set(1, 1, 5.0);
1214 m.set(1, 2, 6.0);
1215 let t = m.transpose();
1216 assert_eq!(t.rows, 3);
1217 assert_eq!(t.cols, 2);
1218 assert!((t.get(0, 0) - 1.0).abs() < 1e-15);
1219 assert!((t.get(1, 0) - 2.0).abs() < 1e-15);
1220 assert!((t.get(2, 1) - 6.0).abs() < 1e-15);
1221 }
1222
1223 #[test]
1224 fn attn_matrix_softmax_rows_sums_to_one() {
1225 let mut m = AttentionMatrix::zeros(3, 4);
1226 for r in 0..3 {
1227 for c in 0..4 {
1228 m.set(r, c, ((r * 4 + c) as f64) * 0.5);
1229 }
1230 }
1231 let s = m.softmax_rows();
1232 for r in 0..3 {
1233 let row_sum: f64 = (0..4).map(|c| s.get(r, c)).sum();
1234 assert!((row_sum - 1.0).abs() < 1e-12, "row {r} sum = {row_sum}");
1235 }
1236 }
1237
1238 #[test]
1241 fn pos_enc_shape() {
1242 let pe = PositionalEncoding::new(64, 8);
1243 assert_eq!(pe.encodings.rows, 64);
1244 assert_eq!(pe.encodings.cols, 8);
1245 }
1246
1247 #[test]
1248 fn pos_enc_position_zero_even_dims_zero() {
1249 let pe = PositionalEncoding::new(10, 8);
1251 for i in 0..4 {
1252 let val = pe.encodings.get(0, i * 2);
1253 assert!(val.abs() < 1e-12, "PE[0][{i}*2] = {val}");
1254 }
1255 }
1256
1257 #[test]
1258 fn pos_enc_position_zero_odd_dims_one() {
1259 let pe = PositionalEncoding::new(10, 8);
1261 for i in 0..4 {
1262 let val = pe.encodings.get(0, i * 2 + 1);
1263 assert!((val - 1.0).abs() < 1e-12, "PE[0][{i}*2+1] = {val}");
1264 }
1265 }
1266
1267 #[test]
1268 fn pos_enc_slice_correct_rows() {
1269 let pe = PositionalEncoding::new(64, 8);
1270 let sliced = pe.slice(5);
1271 assert_eq!(sliced.rows, 5);
1272 assert_eq!(sliced.cols, 8);
1273 }
1274
1275 #[test]
1276 fn pos_enc_values_bounded() {
1277 let pe = PositionalEncoding::new(100, 16);
1279 for v in &pe.encodings.values {
1280 assert!(
1281 *v >= -1.0 - 1e-12 && *v <= 1.0 + 1e-12,
1282 "PE value out of bounds: {v}"
1283 );
1284 }
1285 }
1286
1287 fn make_attn(
1290 heads: usize,
1291 head_dim: usize,
1292 causal: bool,
1293 max_len: usize,
1294 ) -> AttentionMechanism {
1295 AttentionMechanism::new(
1296 AttentionConfig {
1297 num_heads: heads,
1298 head_dim,
1299 dropout_rate: 0.0,
1300 use_causal_mask: causal,
1301 },
1302 max_len,
1303 )
1304 }
1305
1306 #[test]
1307 fn attn_config_model_dim() {
1308 let cfg = AttentionConfig {
1309 num_heads: 4,
1310 head_dim: 8,
1311 dropout_rate: 0.0,
1312 use_causal_mask: false,
1313 };
1314 assert_eq!(cfg.model_dim(), 32);
1315 }
1316
1317 #[test]
1318 fn attn_forward_output_shape() {
1319 let mut attn = make_attn(2, 4, false, 64);
1320 let input = AttentionMatrix::zeros(3, 8);
1321 let out = attn.forward(&input).expect("test: should succeed");
1322 assert_eq!(out.output.rows, 3);
1323 assert_eq!(out.output.cols, 8);
1324 assert_eq!(out.attention_weights.len(), 2);
1325 assert_eq!(out.head_outputs.len(), 2);
1326 }
1327
1328 #[test]
1329 fn attn_forward_weight_shape() {
1330 let mut attn = make_attn(3, 4, false, 32);
1331 let input = AttentionMatrix::zeros(5, 12);
1332 let out = attn.forward(&input).expect("test: should succeed");
1333 for w in &out.attention_weights {
1334 assert_eq!(w.rows, 5);
1335 assert_eq!(w.cols, 5);
1336 }
1337 }
1338
1339 #[test]
1340 fn attn_forward_weights_sum_to_one() {
1341 let mut attn = make_attn(2, 4, false, 64);
1342 let mut input = AttentionMatrix::zeros(4, 8);
1343 for i in 0..4 {
1344 for j in 0..8 {
1345 input.set(i, j, (i * 8 + j) as f64 * 0.01);
1346 }
1347 }
1348 let out = attn.forward(&input).expect("test: should succeed");
1349 for (h, w) in out.attention_weights.iter().enumerate() {
1350 for r in 0..w.rows {
1351 let sum: f64 = (0..w.cols).map(|c| w.get(r, c)).sum();
1352 assert!((sum - 1.0).abs() < 1e-10, "head {h} row {r} sum = {sum}");
1353 }
1354 }
1355 }
1356
1357 #[test]
1358 fn attn_forward_increments_count() {
1359 let mut attn = make_attn(1, 4, false, 16);
1360 let input = AttentionMatrix::zeros(2, 4);
1361 attn.forward(&input).expect("test: should succeed");
1362 attn.forward(&input).expect("test: should succeed");
1363 assert_eq!(attn.forward_count, 2);
1364 }
1365
1366 #[test]
1367 fn attn_forward_stats() {
1368 let attn = make_attn(2, 4, false, 32);
1369 let s = attn.stats();
1370 assert_eq!(s.num_heads, 2);
1371 assert_eq!(s.head_dim, 4);
1372 assert_eq!(s.model_dim, 8);
1373 assert_eq!(s.forward_count, 0);
1374 assert_eq!(s.max_seq_len, 32);
1375 }
1376
1377 #[test]
1378 fn attn_forward_empty_input_error() {
1379 let mut attn = make_attn(1, 4, false, 16);
1380 let empty = AttentionMatrix::zeros(0, 4);
1381 let result = attn.forward(&empty);
1382 assert!(matches!(result, Err(AttnError::EmptyInput)));
1383 }
1384
1385 #[test]
1386 fn attn_forward_dim_mismatch_error() {
1387 let mut attn = make_attn(2, 4, false, 16);
1388 let bad = AttentionMatrix::zeros(3, 6);
1390 let result = attn.forward(&bad);
1391 assert!(matches!(result, Err(AttnError::DimensionMismatch { .. })));
1392 }
1393
1394 #[test]
1395 fn attn_forward_causal_mask() {
1396 let mut attn = make_attn(1, 4, true, 16);
1397 let mut input = AttentionMatrix::zeros(4, 4);
1398 for i in 0..4 {
1399 for j in 0..4 {
1400 input.set(i, j, 1.0);
1401 }
1402 }
1403 let out = attn.forward(&input).expect("test: should succeed");
1404 let w = &out.attention_weights[0];
1405 for j in 1..4 {
1407 assert!(w.get(0, j) < 1e-5, "causal: w[0][{j}] = {}", w.get(0, j));
1408 }
1409 }
1410
1411 #[test]
1412 fn attn_causal_mask_matrix() {
1413 let m = AttentionMechanism::causal_mask(4);
1414 assert_eq!(m.rows, 4);
1415 assert_eq!(m.cols, 4);
1416 for i in 0..4 {
1417 for j in 0..=i {
1418 assert_eq!(m.get(i, j), 0.0, "({i},{j}) should be 0.0");
1419 }
1420 for j in (i + 1)..4 {
1421 assert_eq!(m.get(i, j), 1.0, "({i},{j}) should be 1.0");
1422 }
1423 }
1424 }
1425
1426 #[test]
1427 fn attn_entropy_non_negative() {
1428 let mut attn = make_attn(1, 4, false, 16);
1429 let input = AttentionMatrix::zeros(3, 4);
1430 let out = attn.forward(&input).expect("test: should succeed");
1431 let h = AttentionMechanism::attention_entropy(&out.attention_weights[0]);
1432 assert_eq!(h.len(), 3);
1433 assert!(
1434 h.iter().all(|&e| e >= 0.0),
1435 "entropy must be non-negative: {h:?}"
1436 );
1437 }
1438
1439 #[test]
1440 fn attn_entropy_uniform_distribution_is_max() {
1441 let mut uniform = AttentionMatrix::zeros(1, 4);
1443 for c in 0..4 {
1444 uniform.set(0, c, 0.25);
1445 }
1446 let h = AttentionMechanism::attention_entropy(&uniform);
1447 let expected = (4_f64).ln();
1448 assert!(
1449 (h[0] - expected).abs() < 0.01,
1450 "entropy = {}, expected ≈ {expected}",
1451 h[0]
1452 );
1453 }
1454
1455 #[test]
1456 fn attn_peak_attention_argmax() {
1457 let mut m = AttentionMatrix::zeros(2, 4);
1458 m.set(0, 2, 1.0); m.set(1, 0, 1.0); let peaks = AttentionMechanism::peak_attention(&m);
1461 assert_eq!(peaks[0], 2);
1462 assert_eq!(peaks[1], 0);
1463 }
1464
1465 #[test]
1466 fn attn_head_count_in_output() {
1467 let num_heads = 4;
1468 let mut attn = make_attn(num_heads, 4, false, 32);
1469 let input = AttentionMatrix::zeros(3, 16);
1470 let out = attn.forward(&input).expect("test: should succeed");
1471 assert_eq!(out.attention_weights.len(), num_heads);
1472 assert_eq!(out.head_outputs.len(), num_heads);
1473 }
1474
1475 #[test]
1476 fn attn_scaled_dot_product_output_shape() {
1477 let attn = make_attn(1, 4, false, 16);
1478 let q = AttentionMatrix::zeros(3, 4);
1479 let k = AttentionMatrix::zeros(3, 4);
1480 let v = AttentionMatrix::zeros(3, 4);
1481 let (out, weights) = attn
1482 .scaled_dot_product(&q, &k, &v, None)
1483 .expect("test: should succeed");
1484 assert_eq!(out.rows, 3);
1485 assert_eq!(out.cols, 4);
1486 assert_eq!(weights.rows, 3);
1487 assert_eq!(weights.cols, 3);
1488 }
1489
1490 #[test]
1491 fn attn_error_display_empty_input() {
1492 let e = AttnError::EmptyInput;
1493 let s = e.to_string();
1494 assert!(s.contains("EmptyInput"));
1495 }
1496
1497 #[test]
1498 fn attn_error_display_dim_mismatch() {
1499 let e = AttnError::DimensionMismatch {
1500 op: "test".to_string(),
1501 expected: "4".to_string(),
1502 got: "8".to_string(),
1503 };
1504 let s = e.to_string();
1505 assert!(s.contains("DimensionMismatch"));
1506 }
1507
1508 #[test]
1509 fn attn_error_display_invalid_config() {
1510 let e = AttnError::InvalidConfig("num_heads must be > 0".to_string());
1511 let s = e.to_string();
1512 assert!(s.contains("InvalidConfig"));
1513 }
1514
1515 #[test]
1516 fn attn_forward_large_sequence() {
1517 let mut attn = make_attn(2, 8, false, 256);
1518 let input = AttentionMatrix::zeros(32, 16);
1519 let out = attn.forward(&input).expect("test: should succeed");
1520 assert_eq!(out.output.rows, 32);
1521 assert_eq!(out.output.cols, 16);
1522 }
1523
1524 #[test]
1525 fn attn_head_output_dim() {
1526 let mut attn = make_attn(3, 5, false, 32);
1527 let input = AttentionMatrix::zeros(4, 15);
1528 let out = attn.forward(&input).expect("test: should succeed");
1529 for h in &out.head_outputs {
1530 assert_eq!(h.rows, 4);
1531 assert_eq!(h.cols, 5);
1532 }
1533 }
1534}