1use crate::autograd::{matmul, matmul_nt, BackwardOp};
6use crate::Tensor;
7use ndarray::Array1;
8use std::cell::RefCell;
9use std::collections::HashMap;
10use std::rc::Rc;
11
12use super::config::TransformerConfig;
13
14fn add_bias(x: &Tensor, bias: &Tensor, seq_len: usize) -> Tensor {
17 let xd = x.data();
18 let x_slice = xd.as_slice().expect("contiguous projection");
19 let bd = bias.data();
20 let b_slice = bd.as_slice().expect("contiguous bias");
21 let dim = b_slice.len();
22 let mut out = Vec::with_capacity(x_slice.len());
23 for s in 0..seq_len {
24 let base = s * dim;
25 for d in 0..dim {
26 out.push(x_slice[base + d] + b_slice[d]);
27 }
28 }
29 Tensor::from_vec(out, x.requires_grad())
30}
31
32fn apply_qk_norm(
37 x: &Tensor,
38 norm_weight: &Tensor,
39 seq_len: usize,
40 num_heads: usize,
41 head_dim: usize,
42) -> Tensor {
43 let xd = x.data();
44 let x_slice = xd.as_slice().expect("contiguous qk");
45 let wd = norm_weight.data();
46 let w_slice = wd.as_slice().expect("contiguous norm weight");
47 let total_dim = num_heads * head_dim;
48 let eps = 1e-6_f32;
49 let mut out = vec![0.0f32; seq_len * total_dim];
50
51 for s in 0..seq_len {
52 for h in 0..num_heads {
53 let offset = s * total_dim + h * head_dim;
54 let mut sum_sq = 0.0f32;
56 for d in 0..head_dim {
57 let v = x_slice[offset + d];
58 sum_sq += v * v;
59 }
60 let rms = (sum_sq / head_dim as f32 + eps).sqrt();
61 let inv_rms = 1.0 / rms;
62 for d in 0..head_dim {
63 out[offset + d] = x_slice[offset + d] * inv_rms * w_slice[d];
64 }
65 }
66 }
67
68 Tensor::from_vec(out, x.requires_grad())
69}
70
71fn apply_rope(
80 x: &Tensor,
81 seq_len: usize,
82 num_heads: usize,
83 head_dim: usize,
84 rope_theta: f32,
85) -> Tensor {
86 let xd = x.data();
87 let x_slice = xd.as_slice().expect("contiguous qk for rope");
88 let total_dim = num_heads * head_dim;
89 let half_dim = head_dim / 2;
90 let mut out = vec![0.0f32; seq_len * total_dim];
91
92 let inv_freq: Vec<f32> =
94 (0..half_dim).map(|i| 1.0 / rope_theta.powf(2.0 * i as f32 / head_dim as f32)).collect();
95
96 for pos in 0..seq_len {
97 for h in 0..num_heads {
98 let offset = pos * total_dim + h * head_dim;
99 for i in 0..half_dim {
100 let freq = pos as f32 * inv_freq[i];
101 let cos_f = freq.cos();
102 let sin_f = freq.sin();
103 let x_first = x_slice[offset + i];
105 let x_second = x_slice[offset + i + half_dim];
106 out[offset + i] = x_first * cos_f - x_second * sin_f;
108 out[offset + i + half_dim] = x_second * cos_f + x_first * sin_f;
109 }
110 }
111 }
112
113 let requires_grad = x.requires_grad();
114 let mut result = Tensor::from_vec(out, requires_grad);
115
116 if requires_grad {
122 let backward_op = Rc::new(RopeBackward {
123 x: x.clone(),
124 inv_freq,
125 seq_len,
126 num_heads,
127 head_dim,
128 half_dim,
129 total_dim,
130 result_grad: result.grad_cell(),
131 });
132 result.set_backward_op(backward_op);
133 }
134
135 contract_post_rope!(result.data().as_slice().unwrap_or(&[]));
136 result
137}
138
139struct RopeBackward {
149 x: Tensor,
150 inv_freq: Vec<f32>,
151 seq_len: usize,
152 num_heads: usize,
153 head_dim: usize,
154 half_dim: usize,
155 total_dim: usize,
156 result_grad: Rc<RefCell<Option<Array1<f32>>>>,
157}
158
159impl BackwardOp for RopeBackward {
160 fn backward(&self) {
161 if !self.x.requires_grad() {
162 return;
163 }
164 let Some(grad_out) = self.result_grad.borrow().as_ref().cloned() else { return };
165 let go = grad_out.as_slice().expect("rope grad contiguous");
166 let mut grad_x = vec![0.0f32; self.seq_len * self.total_dim];
167
168 for pos in 0..self.seq_len {
169 for h in 0..self.num_heads {
170 let offset = pos * self.total_dim + h * self.head_dim;
171 for i in 0..self.half_dim {
172 let freq = pos as f32 * self.inv_freq[i];
173 let cos_f = freq.cos();
174 let sin_f = freq.sin();
175 let g_first = go[offset + i];
176 let g_second = go[offset + i + self.half_dim];
177 grad_x[offset + i] = g_first * cos_f + g_second * sin_f;
179 grad_x[offset + i + self.half_dim] = -g_first * sin_f + g_second * cos_f;
180 }
181 }
182 }
183
184 self.x.accumulate_grad(Array1::from(grad_x));
185 if let Some(op) = self.x.backward_op() {
186 op.backward();
187 }
188 }
189}
190
191struct AttentionBlockBackward {
199 q: Tensor,
200 k: Tensor,
201 v: Tensor,
202 head_q_tensors: Vec<Tensor>,
203 head_k_tensors: Vec<Tensor>,
204 head_v_tensors: Vec<Tensor>,
205 head_outputs: Vec<Tensor>,
206 head_kv_indices: Vec<usize>,
207 seq_len: usize,
208 head_dim: usize,
209 q_dim: usize,
210 kv_hidden_size: usize,
211 result_grad: Rc<RefCell<Option<Array1<f32>>>>,
212}
213
214impl BackwardOp for AttentionBlockBackward {
215 fn backward(&self) {
216 let Some(grad_out) = self.result_grad.borrow().as_ref().cloned() else { return };
217 let go = grad_out.as_slice().expect("grad contiguous");
218 let h = self.head_dim;
219
220 split_and_backward_heads(go, &self.head_outputs, self.seq_len, h, self.q_dim);
222
223 scatter_head_grads_q(&self.q, &self.head_q_tensors, self.seq_len, h, self.q_dim);
225 scatter_head_grads_kv(
226 &self.k,
227 &self.head_k_tensors,
228 &self.head_kv_indices,
229 self.seq_len,
230 h,
231 self.kv_hidden_size,
232 );
233 scatter_head_grads_kv(
234 &self.v,
235 &self.head_v_tensors,
236 &self.head_kv_indices,
237 self.seq_len,
238 h,
239 self.kv_hidden_size,
240 );
241
242 for proj in [&self.q, &self.k, &self.v] {
244 if let Some(op) = proj.backward_op() {
245 op.backward();
246 }
247 }
248 }
249}
250
251fn split_and_backward_heads(
253 go: &[f32],
254 head_outputs: &[Tensor],
255 seq_len: usize,
256 head_dim: usize,
257 q_dim: usize,
258) {
259 for (head_idx, head_out) in head_outputs.iter().enumerate() {
260 let mut grad_head = vec![0.0_f32; seq_len * head_dim];
261 for s in 0..seq_len {
262 let src_base = s * q_dim + head_idx * head_dim;
263 let dst_base = s * head_dim;
264 grad_head[dst_base..dst_base + head_dim]
265 .copy_from_slice(&go[src_base..src_base + head_dim]);
266 }
267 head_out.accumulate_grad(Array1::from(grad_head));
268 if let Some(op) = head_out.backward_op() {
269 op.backward();
270 }
271 }
272}
273
274fn scatter_head_grads_q(
276 q: &Tensor,
277 head_q_tensors: &[Tensor],
278 seq_len: usize,
279 head_dim: usize,
280 q_dim: usize,
281) {
282 if !q.requires_grad() {
283 return;
284 }
285 let mut grad_q = vec![0.0_f32; seq_len * q_dim];
286 for (head_idx, head_q) in head_q_tensors.iter().enumerate() {
287 if let Some(hgrad) = head_q.grad() {
288 let hg = hgrad.as_slice().expect("contiguous");
289 for s in 0..seq_len {
290 let src_base = s * head_dim;
291 let dst_base = s * q_dim + head_idx * head_dim;
292 for d in 0..head_dim {
293 grad_q[dst_base + d] += hg[src_base + d];
294 }
295 }
296 }
297 }
298 q.accumulate_grad(Array1::from(grad_q));
299}
300
301fn scatter_head_grads_kv(
303 target: &Tensor,
304 head_tensors: &[Tensor],
305 kv_indices: &[usize],
306 seq_len: usize,
307 head_dim: usize,
308 kv_hidden_size: usize,
309) {
310 if !target.requires_grad() {
311 return;
312 }
313 let mut grad = vec![0.0_f32; seq_len * kv_hidden_size];
314 for (head_idx, head_t) in head_tensors.iter().enumerate() {
315 let kv_h = kv_indices[head_idx];
316 if let Some(hgrad) = head_t.grad() {
317 let hg = hgrad.as_slice().expect("contiguous");
318 for s in 0..seq_len {
319 let src_base = s * head_dim;
320 let dst_base = s * kv_hidden_size + kv_h * head_dim;
321 for d in 0..head_dim {
322 grad[dst_base + d] += hg[src_base + d];
323 }
324 }
325 }
326 }
327 target.accumulate_grad(Array1::from(grad));
328}
329
330pub struct MultiHeadAttention {
332 config: TransformerConfig,
334 pub w_q: Tensor,
336 pub w_k: Tensor,
338 pub w_v: Tensor,
340 pub w_o: Tensor,
342 pub b_q: Option<Tensor>,
344 pub b_k: Option<Tensor>,
346 pub b_v: Option<Tensor>,
348 pub q_norm: Option<Tensor>,
350 pub k_norm: Option<Tensor>,
352}
353
354impl MultiHeadAttention {
355 pub fn new(config: &TransformerConfig) -> Self {
372 use super::init::{get_init_seed, rand_normal_seeded};
373 let hidden_size = config.hidden_size;
374 let q_dim = config.q_dim();
375 let kv_hidden_size = config.num_kv_heads * config.head_dim();
376 let seed = get_init_seed();
377
378 let (b_q, b_k, b_v) = if config.use_bias {
379 (
380 Some(Tensor::from_vec(vec![0.0_f32; q_dim], true)),
381 Some(Tensor::from_vec(vec![0.0_f32; kv_hidden_size], true)),
382 Some(Tensor::from_vec(vec![0.0_f32; kv_hidden_size], true)),
383 )
384 } else {
385 (None, None, None)
386 };
387
388 Self {
390 config: config.clone(),
391 w_q: Tensor::from_vec(rand_normal_seeded(q_dim * hidden_size, seed, "w_q"), true),
392 w_k: Tensor::from_vec(
393 rand_normal_seeded(kv_hidden_size * hidden_size, seed, "w_k"),
394 true,
395 ),
396 w_v: Tensor::from_vec(
397 rand_normal_seeded(kv_hidden_size * hidden_size, seed, "w_v"),
398 true,
399 ),
400 w_o: Tensor::from_vec(rand_normal_seeded(hidden_size * q_dim, seed, "w_o"), true),
401 b_q,
402 b_k,
403 b_v,
404 q_norm: None,
405 k_norm: None,
406 }
407 }
408
409 pub fn from_params(
420 config: &TransformerConfig,
421 params: &HashMap<String, Tensor>,
422 prefix: &str,
423 ) -> Option<Self> {
424 let w_q = params.get(&format!("{prefix}.q_proj.weight"))?.clone();
425 let w_k = params.get(&format!("{prefix}.k_proj.weight"))?.clone();
426 let w_v = params.get(&format!("{prefix}.v_proj.weight"))?.clone();
427 let w_o = params.get(&format!("{prefix}.o_proj.weight"))?.clone();
428
429 let hidden = config.hidden_size;
430 let q_dim = config.q_dim();
431 let kv_hidden = config.num_kv_heads * config.head_dim();
432
433 let checks: &[(&str, &Tensor, usize)] = &[
436 ("q_proj", &w_q, q_dim * hidden),
437 ("k_proj", &w_k, kv_hidden * hidden),
438 ("v_proj", &w_v, kv_hidden * hidden),
439 ("o_proj", &w_o, hidden * q_dim),
440 ];
441 for &(name, tensor, expected) in checks {
442 if tensor.len() != expected {
443 eprintln!(
444 "[PMAT-331] {prefix}.{name}: shape mismatch — got {} elements, expected {expected}",
445 tensor.len()
446 );
447 return None;
448 }
449 }
450
451 let b_q = params.get(&format!("{prefix}.q_proj.bias")).cloned();
453 let b_k = params.get(&format!("{prefix}.k_proj.bias")).cloned();
454 let b_v = params.get(&format!("{prefix}.v_proj.bias")).cloned();
455
456 let q_norm = params.get(&format!("{prefix}.q_norm.weight")).cloned();
458 let k_norm = params.get(&format!("{prefix}.k_norm.weight")).cloned();
459
460 Some(Self { config: config.clone(), w_q, w_k, w_v, w_o, b_q, b_k, b_v, q_norm, k_norm })
461 }
462
463 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
472 contract_pre_attention!(x.data());
473 let hidden_size = self.config.hidden_size;
474 let num_heads = self.config.num_attention_heads;
475 let num_kv_heads = self.config.num_kv_heads;
476 let head_dim = self.config.head_dim();
477 let q_dim = self.config.q_dim();
478 let kv_hidden_size = num_kv_heads * head_dim;
479
480 let mut q = matmul_nt(x, &self.w_q, seq_len, hidden_size, q_dim);
482 let mut k = matmul_nt(x, &self.w_k, seq_len, hidden_size, kv_hidden_size);
483 let mut v = matmul_nt(x, &self.w_v, seq_len, hidden_size, kv_hidden_size);
484
485 if let Some(ref b_q) = self.b_q {
487 q = add_bias(&q, b_q, seq_len);
488 }
489 if let Some(ref b_k) = self.b_k {
490 k = add_bias(&k, b_k, seq_len);
491 }
492 if let Some(ref b_v) = self.b_v {
493 v = add_bias(&v, b_v, seq_len);
494 }
495
496 if let Some(ref qn) = self.q_norm {
498 q = apply_qk_norm(&q, qn, seq_len, num_heads, head_dim);
499 }
500 if let Some(ref kn) = self.k_norm {
501 k = apply_qk_norm(&k, kn, seq_len, num_kv_heads, head_dim);
502 }
503
504 if self.config.rope_theta > 0.0 {
507 q = apply_rope(&q, seq_len, num_heads, head_dim, self.config.rope_theta);
508 k = apply_rope(&k, seq_len, num_kv_heads, head_dim, self.config.rope_theta);
509 }
510
511 let requires_grad = q.requires_grad() || k.requires_grad() || v.requires_grad();
512 let heads_per_kv = num_heads / num_kv_heads;
513
514 let q_data = q.data();
517 let q_slice = q_data.as_slice().expect("contiguous Q");
518 let k_data = k.data();
519 let k_slice = k_data.as_slice().expect("contiguous K");
520 let v_data = v.data();
521 let v_slice = v_data.as_slice().expect("contiguous V");
522
523 let mut head_q_tensors = Vec::with_capacity(num_heads);
525 let mut head_k_tensors = Vec::with_capacity(num_heads);
526 let mut head_v_tensors = Vec::with_capacity(num_heads);
527 let mut head_outputs = Vec::with_capacity(num_heads);
528 let mut head_kv_indices = Vec::with_capacity(num_heads);
529
530 for h in 0..num_heads {
531 let kv_h = h / heads_per_kv;
532 head_kv_indices.push(kv_h);
533
534 let mut q_head = Vec::with_capacity(seq_len * head_dim);
538 for s in 0..seq_len {
539 let start = s * q_dim + h * head_dim;
540 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
541 }
542
543 let mut k_head = Vec::with_capacity(seq_len * head_dim);
544 for s in 0..seq_len {
545 let start = s * kv_hidden_size + kv_h * head_dim;
546 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
547 }
548
549 let mut v_head = Vec::with_capacity(seq_len * head_dim);
550 for s in 0..seq_len {
551 let start = s * kv_hidden_size + kv_h * head_dim;
552 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
553 }
554
555 let q_tensor = Tensor::from_vec(q_head, requires_grad);
556 let k_tensor = Tensor::from_vec(k_head, requires_grad);
557 let v_tensor = Tensor::from_vec(v_head, requires_grad);
558
559 let attn_out = crate::autograd::attention(
560 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
561 );
562
563 head_q_tensors.push(q_tensor);
564 head_k_tensors.push(k_tensor);
565 head_v_tensors.push(v_tensor);
566 head_outputs.push(attn_out);
567 }
568
569 let mut concat_output = vec![0.0; seq_len * q_dim];
571 for (h, head_out) in head_outputs.iter().enumerate() {
572 let hd = head_out.data();
573 let hdata = hd.as_slice().expect("contiguous attention output");
574 for s in 0..seq_len {
575 let src_base = s * head_dim;
576 let dst_base = s * q_dim + h * head_dim;
577 concat_output[dst_base..dst_base + head_dim]
578 .copy_from_slice(&hdata[src_base..src_base + head_dim]);
579 }
580 }
581
582 let mut concat_tensor = Tensor::from_vec(concat_output, requires_grad);
583
584 if requires_grad {
585 let backward_op = Rc::new(AttentionBlockBackward {
586 q: q.clone(),
587 k: k.clone(),
588 v: v.clone(),
589 head_q_tensors,
590 head_k_tensors,
591 head_v_tensors,
592 head_outputs,
593 head_kv_indices,
594 seq_len,
595 head_dim,
596 q_dim,
597 kv_hidden_size,
598 result_grad: concat_tensor.grad_cell(),
599 });
600 concat_tensor.set_backward_op(backward_op);
601 }
602
603 let result = matmul_nt(&concat_tensor, &self.w_o, seq_len, q_dim, hidden_size);
605 contract_post_attention!(result.data().as_slice().unwrap_or(&[]));
606 result
607 }
608
609 pub fn forward_with_lora(
622 &self,
623 x: &Tensor,
624 seq_len: usize,
625 lora_a_q: &Tensor,
626 lora_b_q: &Tensor,
628 lora_a_v: &Tensor,
629 lora_b_v: &Tensor,
630 lora_rank: usize,
631 lora_scale: f32,
632 ) -> Tensor {
633 contract_pre_lora_forward!();
634 let hidden_size = self.config.hidden_size;
635 let num_heads = self.config.num_attention_heads;
636 let num_kv_heads = self.config.num_kv_heads;
637 let head_dim = self.config.head_dim();
638 let q_dim = self.config.q_dim();
639 let kv_hidden_size = num_kv_heads * head_dim;
640
641 let q_base = matmul_nt(x, &self.w_q, seq_len, hidden_size, q_dim);
652 let q_mid = crate::autograd::matmul_nt(x, lora_a_q, seq_len, hidden_size, lora_rank);
653 let q_lora = crate::autograd::matmul_nt(&q_mid, lora_b_q, seq_len, lora_rank, q_dim);
654 let q = crate::autograd::add_scaled(&q_base, &q_lora, lora_scale);
655
656 let k = matmul_nt(x, &self.w_k, seq_len, hidden_size, kv_hidden_size);
658
659 let v_base = matmul_nt(x, &self.w_v, seq_len, hidden_size, kv_hidden_size);
661 let v_mid = crate::autograd::matmul_nt(x, lora_a_v, seq_len, hidden_size, lora_rank);
662 let v_lora =
663 crate::autograd::matmul_nt(&v_mid, lora_b_v, seq_len, lora_rank, kv_hidden_size);
664 let v = crate::autograd::add_scaled(&v_base, &v_lora, lora_scale);
665
666 let q = if let Some(ref qn) = self.q_norm {
668 apply_qk_norm(&q, qn, seq_len, num_heads, head_dim)
669 } else {
670 q
671 };
672 let k = if let Some(ref kn) = self.k_norm {
673 apply_qk_norm(&k, kn, seq_len, num_kv_heads, head_dim)
674 } else {
675 k
676 };
677
678 let (q, k) = if self.config.rope_theta > 0.0 {
681 (
682 apply_rope(&q, seq_len, num_heads, head_dim, self.config.rope_theta),
683 apply_rope(&k, seq_len, num_kv_heads, head_dim, self.config.rope_theta),
684 )
685 } else {
686 (q, k)
687 };
688
689 let requires_grad = q.requires_grad() || k.requires_grad() || v.requires_grad();
690 let heads_per_kv = num_heads / num_kv_heads;
691
692 let q_data = q.data();
694 let q_slice = q_data.as_slice().expect("contiguous Q");
695 let k_data = k.data();
696 let k_slice = k_data.as_slice().expect("contiguous K");
697 let v_data = v.data();
698 let v_slice = v_data.as_slice().expect("contiguous V");
699
700 let mut head_q_tensors = Vec::with_capacity(num_heads);
702 let mut head_k_tensors = Vec::with_capacity(num_heads);
703 let mut head_v_tensors = Vec::with_capacity(num_heads);
704 let mut head_outputs = Vec::with_capacity(num_heads);
705 let mut head_kv_indices = Vec::with_capacity(num_heads);
706
707 for h in 0..num_heads {
708 let kv_h = h / heads_per_kv;
709 head_kv_indices.push(kv_h);
710
711 let mut q_head = Vec::with_capacity(seq_len * head_dim);
713 for s in 0..seq_len {
714 let start = s * q_dim + h * head_dim;
715 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
716 }
717
718 let mut k_head = Vec::with_capacity(seq_len * head_dim);
719 for s in 0..seq_len {
720 let start = s * kv_hidden_size + kv_h * head_dim;
721 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
722 }
723
724 let mut v_head = Vec::with_capacity(seq_len * head_dim);
725 for s in 0..seq_len {
726 let start = s * kv_hidden_size + kv_h * head_dim;
727 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
728 }
729
730 let q_tensor = Tensor::from_vec(q_head, requires_grad);
731 let k_tensor = Tensor::from_vec(k_head, requires_grad);
732 let v_tensor = Tensor::from_vec(v_head, requires_grad);
733
734 let attn_out = crate::autograd::attention(
735 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
736 );
737
738 head_q_tensors.push(q_tensor);
739 head_k_tensors.push(k_tensor);
740 head_v_tensors.push(v_tensor);
741 head_outputs.push(attn_out);
742 }
743
744 let mut concat_output = vec![0.0; seq_len * q_dim];
746 for (h, head_out) in head_outputs.iter().enumerate() {
747 let hd = head_out.data();
748 let hdata = hd.as_slice().expect("contiguous attention output");
749 for s in 0..seq_len {
750 let src_base = s * head_dim;
751 let dst_base = s * q_dim + h * head_dim;
752 concat_output[dst_base..dst_base + head_dim]
753 .copy_from_slice(&hdata[src_base..src_base + head_dim]);
754 }
755 }
756
757 let mut concat_tensor = Tensor::from_vec(concat_output, requires_grad);
758
759 if requires_grad {
760 let backward_op = Rc::new(AttentionBlockBackward {
761 q: q.clone(),
762 k: k.clone(),
763 v: v.clone(),
764 head_q_tensors,
765 head_k_tensors,
766 head_v_tensors,
767 head_outputs,
768 head_kv_indices,
769 seq_len,
770 head_dim,
771 q_dim,
772 kv_hidden_size,
773 result_grad: concat_tensor.grad_cell(),
774 });
775 concat_tensor.set_backward_op(backward_op);
776 }
777
778 let result = matmul_nt(&concat_tensor, &self.w_o, seq_len, q_dim, hidden_size);
780 contract_post_lora_forward!(result);
781 result
782 }
783
784 pub fn parameters(&self) -> Vec<&Tensor> {
786 let mut params = vec![&self.w_q, &self.w_k, &self.w_v, &self.w_o];
787 if let Some(ref b) = self.b_q {
788 params.push(b);
789 }
790 if let Some(ref b) = self.b_k {
791 params.push(b);
792 }
793 if let Some(ref b) = self.b_v {
794 params.push(b);
795 }
796 params
797 }
798
799 pub fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
801 let mut params = vec![&mut self.w_q, &mut self.w_k, &mut self.w_v, &mut self.w_o];
802 if let Some(ref mut b) = self.b_q {
803 params.push(b);
804 }
805 if let Some(ref mut b) = self.b_k {
806 params.push(b);
807 }
808 if let Some(ref mut b) = self.b_v {
809 params.push(b);
810 }
811 params
812 }
813
814 pub fn has_biases(&self) -> bool {
816 self.b_q.is_some()
817 }
818
819 pub fn named_parameters(&self, prefix: &str) -> Vec<(String, &Tensor)> {
821 let mut params = vec![
822 (format!("{prefix}.q_proj.weight"), &self.w_q),
823 (format!("{prefix}.k_proj.weight"), &self.w_k),
824 (format!("{prefix}.v_proj.weight"), &self.w_v),
825 (format!("{prefix}.o_proj.weight"), &self.w_o),
826 ];
827 if let Some(ref b) = self.b_q {
828 params.push((format!("{prefix}.q_proj.bias"), b));
829 }
830 if let Some(ref b) = self.b_k {
831 params.push((format!("{prefix}.k_proj.bias"), b));
832 }
833 if let Some(ref b) = self.b_v {
834 params.push((format!("{prefix}.v_proj.bias"), b));
835 }
836 params
837 }
838
839 pub fn set_named_parameter(&mut self, suffix: &str, value: Tensor) -> bool {
850 match suffix {
851 "self_attn.q_proj.weight" => {
852 self.w_q = value;
853 true
854 }
855 "self_attn.k_proj.weight" => {
856 self.w_k = value;
857 true
858 }
859 "self_attn.v_proj.weight" => {
860 self.w_v = value;
861 true
862 }
863 "self_attn.o_proj.weight" => {
864 self.w_o = value;
865 true
866 }
867 "self_attn.q_proj.bias" => {
868 if self.b_q.is_some() {
869 self.b_q = Some(value);
870 true
871 } else {
872 false
873 }
874 }
875 "self_attn.k_proj.bias" => {
876 if self.b_k.is_some() {
877 self.b_k = Some(value);
878 true
879 } else {
880 false
881 }
882 }
883 "self_attn.v_proj.bias" => {
884 if self.b_v.is_some() {
885 self.b_v = Some(value);
886 true
887 } else {
888 false
889 }
890 }
891 _ => false,
892 }
893 }
894}
895
896pub struct LoRAProjection {
901 pub base_weight: Tensor,
903 pub lora_a: Tensor,
905 pub lora_b: Tensor,
907 pub d_in: usize,
909 pub d_out: usize,
911 pub rank: usize,
913 pub scale: f32,
915}
916
917impl LoRAProjection {
918 pub fn new(base_weight: Tensor, d_in: usize, d_out: usize, rank: usize, alpha: f32) -> Self {
927 assert_eq!(base_weight.len(), d_in * d_out, "Base weight size mismatch");
928
929 let mut base_weight = base_weight;
931 base_weight.set_requires_grad(false);
932
933 let lora_a = Tensor::from_vec(
935 (0..d_in * rank).map(|i| (i as f32 * 0.123).sin() * 0.01).collect(),
936 true, );
938
939 let lora_b = Tensor::zeros(rank * d_out, true);
941
942 Self { base_weight, lora_a, lora_b, d_in, d_out, rank, scale: alpha / rank as f32 }
943 }
944
945 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
956 let base_out = matmul(x, &self.base_weight, seq_len, self.d_in, self.d_out);
958
959 let lora_intermediate = matmul(x, &self.lora_a, seq_len, self.d_in, self.rank);
962
963 let lora_out = matmul(&lora_intermediate, &self.lora_b, seq_len, self.rank, self.d_out);
965
966 crate::autograd::add_scaled(&base_out, &lora_out, self.scale)
969 }
970
971 pub fn lora_params(&self) -> Vec<&Tensor> {
973 vec![&self.lora_a, &self.lora_b]
974 }
975
976 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
978 vec![&mut self.lora_a, &mut self.lora_b]
979 }
980}
981
982pub struct MultiHeadAttentionWithLoRA {
986 pub config: TransformerConfig,
988 pub q_proj: LoRAProjection,
990 pub k_proj: LoRAProjection,
992 pub v_proj: LoRAProjection,
994 pub o_proj: LoRAProjection,
996}
997
998impl MultiHeadAttentionWithLoRA {
999 pub fn from_attention(attn: &MultiHeadAttention, rank: usize, alpha: f32) -> Self {
1006 let hidden_size = attn.config.hidden_size;
1007 let q_dim = attn.config.q_dim();
1008 let kv_hidden_size = attn.config.num_kv_heads * attn.config.head_dim();
1009
1010 Self {
1011 config: attn.config.clone(),
1012 q_proj: LoRAProjection::new(attn.w_q.clone(), hidden_size, q_dim, rank, alpha),
1013 k_proj: LoRAProjection::new(attn.w_k.clone(), hidden_size, kv_hidden_size, rank, alpha),
1014 v_proj: LoRAProjection::new(attn.w_v.clone(), hidden_size, kv_hidden_size, rank, alpha),
1015 o_proj: LoRAProjection::new(attn.w_o.clone(), q_dim, hidden_size, rank, alpha),
1016 }
1017 }
1018
1019 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
1023 let num_heads = self.config.num_attention_heads;
1024 let num_kv_heads = self.config.num_kv_heads;
1025 let head_dim = self.config.head_dim();
1026 let q_dim = self.config.q_dim();
1027 let kv_hidden_size = num_kv_heads * head_dim;
1028
1029 let q = self.q_proj.forward(x, seq_len);
1031 let k = self.k_proj.forward(x, seq_len);
1032 let v = self.v_proj.forward(x, seq_len);
1033
1034 let mut attn_outputs = Vec::with_capacity(num_heads * seq_len * head_dim);
1036 let heads_per_kv = num_heads / num_kv_heads;
1037
1038 let q_data = q.data();
1040 let q_slice = q_data.as_slice().expect("contiguous Q tensor");
1041 let k_data = k.data();
1042 let k_slice = k_data.as_slice().expect("contiguous K tensor");
1043 let v_data = v.data();
1044 let v_slice = v_data.as_slice().expect("contiguous V tensor");
1045
1046 for h in 0..num_heads {
1047 let kv_h = h / heads_per_kv;
1048
1049 let mut q_head = Vec::with_capacity(seq_len * head_dim);
1051 for s in 0..seq_len {
1052 let start = s * q_dim + h * head_dim;
1053 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
1054 }
1055
1056 let mut k_head = Vec::with_capacity(seq_len * head_dim);
1057 for s in 0..seq_len {
1058 let start = s * kv_hidden_size + kv_h * head_dim;
1059 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
1060 }
1061
1062 let mut v_head = Vec::with_capacity(seq_len * head_dim);
1063 for s in 0..seq_len {
1064 let start = s * kv_hidden_size + kv_h * head_dim;
1065 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
1066 }
1067
1068 let q_tensor = Tensor::from_vec(q_head, false);
1070 let k_tensor = Tensor::from_vec(k_head, false);
1071 let v_tensor = Tensor::from_vec(v_head, false);
1072
1073 let attn_out = crate::autograd::attention(
1074 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
1075 );
1076
1077 attn_outputs.extend_from_slice(
1078 attn_out.data().as_slice().expect("contiguous attention output"),
1079 );
1080 }
1081
1082 let mut concat_output = vec![0.0; seq_len * q_dim];
1084 for h in 0..num_heads {
1085 for s in 0..seq_len {
1086 let src_idx = h * seq_len * head_dim + s * head_dim;
1087 let dst_idx = s * q_dim + h * head_dim;
1088 concat_output[dst_idx..dst_idx + head_dim]
1089 .copy_from_slice(&attn_outputs[src_idx..src_idx + head_dim]);
1090 }
1091 }
1092
1093 let concat_tensor = Tensor::from_vec(concat_output, true);
1094
1095 self.o_proj.forward(&concat_tensor, seq_len)
1097 }
1098
1099 pub fn lora_params(&self) -> Vec<&Tensor> {
1101 let mut params = Vec::new();
1102 params.extend(self.q_proj.lora_params());
1103 params.extend(self.k_proj.lora_params());
1104 params.extend(self.v_proj.lora_params());
1105 params.extend(self.o_proj.lora_params());
1106 params
1107 }
1108
1109 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
1111 let mut params = Vec::new();
1112 params.extend(self.q_proj.lora_params_mut());
1113 params.extend(self.k_proj.lora_params_mut());
1114 params.extend(self.v_proj.lora_params_mut());
1115 params.extend(self.o_proj.lora_params_mut());
1116 params
1117 }
1118
1119 pub fn lora_param_count(&self) -> usize {
1121 let hidden = self.config.hidden_size;
1123 let kv_hidden = self.config.num_kv_heads * self.config.head_dim();
1124 let rank = self.q_proj.rank;
1125
1126 (hidden * rank + rank * hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * hidden) }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use super::*;
1140
1141 #[test]
1145 fn falsify_pmat805_rope_backward_matches_finite_difference() {
1146 let seq_len = 3usize;
1147 let num_heads = 2usize;
1148 let head_dim = 4usize; let total = seq_len * num_heads * head_dim;
1150 let theta = 10000.0f32;
1151
1152 let x_data: Vec<f32> =
1154 (0..total).map(|i| ((i as f32 * 0.37).sin() * 1.5) + 0.1).collect();
1155 let w: Vec<f32> = (0..total).map(|i| ((i as f32 * 0.21).cos() * 0.8) - 0.05).collect();
1156
1157 let x = Tensor::from_vec(x_data.clone(), true);
1159 let y = apply_rope(&x, seq_len, num_heads, head_dim, theta);
1160 y.set_grad(Array1::from(w.clone()));
1161 y.backward_op().expect("rope must have a backward op").backward();
1162 let analytical = x.grad().expect("x must have grad after rope backward").to_vec();
1163
1164 let h = 1e-3f32;
1166 let loss = |xv: &[f32]| -> f32 {
1167 let t = Tensor::from_vec(xv.to_vec(), false);
1168 let r = apply_rope(&t, seq_len, num_heads, head_dim, theta);
1169 r.data().iter().zip(&w).map(|(a, b)| a * b).sum()
1170 };
1171 for j in 0..total {
1172 let mut xp = x_data.clone();
1173 let mut xm = x_data.clone();
1174 xp[j] += h;
1175 xm[j] -= h;
1176 let numerical = (loss(&xp) - loss(&xm)) / (2.0 * h);
1177 let diff = (analytical[j] - numerical).abs();
1178 assert!(
1179 diff < 1e-2,
1180 "FALSIFIED: RoPE grad[{j}] analytical={} numerical={} diff={diff}",
1181 analytical[j],
1182 numerical
1183 );
1184 }
1185 }
1186
1187 #[test]
1188 fn test_multi_head_attention_tiny() {
1189 let config = TransformerConfig::tiny();
1190 let attn = MultiHeadAttention::new(&config);
1191 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1192 let output = attn.forward(&x, 2);
1193 assert_eq!(output.len(), 2 * config.hidden_size);
1194 }
1195
1196 #[test]
1197 fn test_multi_head_attention_parameters() {
1198 let config = TransformerConfig::tiny();
1199 let attn = MultiHeadAttention::new(&config);
1200 let params = attn.parameters();
1201 assert_eq!(params.len(), 4); }
1203
1204 #[test]
1205 fn test_attention_longer_sequence() {
1206 let config = TransformerConfig::tiny();
1207 let attn = MultiHeadAttention::new(&config);
1208 let x = Tensor::from_vec(vec![0.1; 8 * config.hidden_size], true);
1209 let output = attn.forward(&x, 8);
1210 assert_eq!(output.len(), 8 * config.hidden_size);
1211 }
1212
1213 #[test]
1214 fn test_attention_weight_sizes() {
1215 let config = TransformerConfig::tiny();
1216 let attn = MultiHeadAttention::new(&config);
1217 let kv_hidden = config.num_kv_heads * config.head_dim();
1218 assert_eq!(attn.w_q.len(), config.hidden_size * config.hidden_size);
1219 assert_eq!(attn.w_k.len(), config.hidden_size * kv_hidden);
1220 assert_eq!(attn.w_v.len(), config.hidden_size * kv_hidden);
1221 assert_eq!(attn.w_o.len(), config.hidden_size * config.hidden_size);
1222 }
1223
1224 #[test]
1225 fn test_multi_head_attention_from_params_success() {
1226 let config = TransformerConfig::tiny();
1227 let hidden_size = config.hidden_size;
1228 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1229
1230 let mut params = HashMap::new();
1231 params.insert(
1232 "attn.q_proj.weight".to_string(),
1233 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1234 );
1235 params.insert(
1236 "attn.k_proj.weight".to_string(),
1237 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1238 );
1239 params.insert(
1240 "attn.v_proj.weight".to_string(),
1241 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1242 );
1243 params.insert(
1244 "attn.o_proj.weight".to_string(),
1245 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1246 );
1247
1248 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1249 assert!(attn.is_some());
1250 let attn = attn.expect("operation should succeed");
1251 assert_eq!(attn.w_q.len(), hidden_size * hidden_size);
1252 }
1253
1254 #[test]
1255 fn test_multi_head_attention_from_params_missing_key() {
1256 let config = TransformerConfig::tiny();
1257 let hidden_size = config.hidden_size;
1258
1259 let mut params = HashMap::new();
1260 params.insert(
1261 "attn.q_proj.weight".to_string(),
1262 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1263 );
1264 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1267 assert!(attn.is_none());
1268 }
1269
1270 #[test]
1271 fn test_attention_projections_backward() {
1272 let config = TransformerConfig::tiny();
1275 let attn = MultiHeadAttention::new(&config);
1276 let hidden_size = config.hidden_size;
1277 let seq_len = 2;
1278
1279 let x = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1280
1281 let mut q = crate::autograd::matmul(&x, &attn.w_q, seq_len, hidden_size, hidden_size);
1283 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1284 crate::autograd::backward(&mut q, Some(grad_out));
1285
1286 assert!(attn.w_q.grad().is_some());
1287 let grad_q = attn.w_q.grad().expect("gradient should be available");
1288 assert!(grad_q.iter().all(|&v| v.is_finite()));
1289 }
1290
1291 #[test]
1292 fn test_output_projection_backward() {
1293 let config = TransformerConfig::tiny();
1295 let attn = MultiHeadAttention::new(&config);
1296 let hidden_size = config.hidden_size;
1297 let seq_len = 2;
1298
1299 let concat_out = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1301
1302 let mut output =
1304 crate::autograd::matmul(&concat_out, &attn.w_o, seq_len, hidden_size, hidden_size);
1305
1306 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1307 crate::autograd::backward(&mut output, Some(grad_out));
1308
1309 assert!(attn.w_o.grad().is_some());
1310 let grad_o = attn.w_o.grad().expect("gradient should be available");
1311 assert!(grad_o.iter().all(|&v| v.is_finite()));
1312 let sum: f32 = grad_o.iter().map(|v| v.abs()).sum();
1313 assert!(sum > 0.0, "Output projection gradient should not be all zero");
1314 }
1315
1316 #[test]
1322 #[ignore = "apply_rope() severs autograd chain — needs backward op (ENT-272)"]
1323 fn test_attention_full_forward_qkv_gradients() {
1324 let config = TransformerConfig::tiny();
1325 let attn = MultiHeadAttention::new(&config);
1326 let hidden_size = config.hidden_size;
1327 let seq_len = 3;
1328
1329 let x_data: Vec<f32> =
1332 (0..seq_len * hidden_size).map(|i| ((i as f32) * 0.17).sin() * 0.5).collect();
1333 let x = Tensor::from_vec(x_data, true);
1334 let mut output = attn.forward(&x, seq_len);
1335
1336 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1337 crate::autograd::backward(&mut output, Some(grad_out));
1338
1339 for (name, param) in
1341 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1342 {
1343 assert!(
1344 param.grad().is_some(),
1345 "ALB-038: {name} must have gradient after full attention forward"
1346 );
1347 let grad = param.grad().expect("gradient available");
1348 assert!(grad.iter().all(|&v| v.is_finite()), "ALB-038: {name} gradient must be finite");
1349 assert!(
1350 grad.iter().any(|&v| v.abs() > 1e-10),
1351 "ALB-038: {name} gradient must be non-zero"
1352 );
1353 }
1354
1355 assert!(x.grad().is_some(), "ALB-038: input x must have gradient");
1357 }
1358
1359 #[test]
1364 fn test_lora_projection_new() {
1365 let d_in = 32;
1366 let d_out = 16;
1367 let rank = 4;
1368 let alpha = 8.0;
1369
1370 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1371 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1372
1373 assert_eq!(lora.d_in, d_in);
1374 assert_eq!(lora.d_out, d_out);
1375 assert_eq!(lora.rank, rank);
1376 assert!((lora.scale - 2.0).abs() < 1e-6); assert_eq!(lora.lora_a.len(), d_in * rank);
1378 assert_eq!(lora.lora_b.len(), rank * d_out);
1379 }
1380
1381 #[test]
1382 fn test_lora_projection_forward() {
1383 let d_in = 32;
1384 let d_out = 16;
1385 let rank = 4;
1386 let alpha = 8.0;
1387 let seq_len = 2;
1388
1389 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1390 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1391
1392 let x = Tensor::from_vec(vec![0.1; seq_len * d_in], false);
1393 let output = lora.forward(&x, seq_len);
1394
1395 assert_eq!(output.len(), seq_len * d_out);
1396 assert!(output.data().iter().all(|&v| v.is_finite()));
1398 }
1399
1400 #[test]
1401 fn test_lora_projection_params() {
1402 let d_in = 32;
1403 let d_out = 16;
1404 let rank = 4;
1405
1406 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1407 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1408
1409 let params = lora.lora_params();
1410 assert_eq!(params.len(), 2); }
1412
1413 #[test]
1414 fn test_lora_projection_params_mut() {
1415 let d_in = 32;
1416 let d_out = 16;
1417 let rank = 4;
1418
1419 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1420 let mut lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1421
1422 let params = lora.lora_params_mut();
1423 assert_eq!(params.len(), 2);
1424 }
1425
1426 #[test]
1427 #[should_panic(expected = "Base weight size mismatch")]
1428 fn test_lora_projection_size_mismatch() {
1429 let d_in = 32;
1430 let d_out = 16;
1431 let rank = 4;
1432
1433 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out + 1], false);
1435 let _ = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1436 }
1437
1438 #[test]
1443 fn test_mha_with_lora_creation() {
1444 let config = TransformerConfig::tiny();
1445 let attn = MultiHeadAttention::new(&config);
1446 let rank = 4;
1447 let alpha = 8.0;
1448
1449 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, alpha);
1450
1451 assert_eq!(lora_attn.q_proj.rank, rank);
1452 assert_eq!(lora_attn.k_proj.rank, rank);
1453 assert_eq!(lora_attn.v_proj.rank, rank);
1454 assert_eq!(lora_attn.o_proj.rank, rank);
1455 }
1456
1457 #[test]
1458 fn test_mha_with_lora_forward() {
1459 let config = TransformerConfig::tiny();
1460 let attn = MultiHeadAttention::new(&config);
1461 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1462
1463 let seq_len = 2;
1464 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1465 let output = lora_attn.forward(&x, seq_len);
1466
1467 assert_eq!(output.len(), seq_len * config.hidden_size);
1468 assert!(output.data().iter().all(|&v| v.is_finite()));
1470 }
1471
1472 #[test]
1473 fn test_mha_with_lora_params() {
1474 let config = TransformerConfig::tiny();
1475 let attn = MultiHeadAttention::new(&config);
1476 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1477
1478 let params = lora_attn.lora_params();
1479 assert_eq!(params.len(), 8);
1481 }
1482
1483 #[test]
1484 fn test_mha_with_lora_params_mut() {
1485 let config = TransformerConfig::tiny();
1486 let attn = MultiHeadAttention::new(&config);
1487 let mut lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1488
1489 let params = lora_attn.lora_params_mut();
1490 assert_eq!(params.len(), 8);
1491 }
1492
1493 #[test]
1494 fn test_mha_with_lora_param_count() {
1495 let config = TransformerConfig::tiny();
1496 let attn = MultiHeadAttention::new(&config);
1497 let rank = 4;
1498 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, 8.0);
1499
1500 let param_count = lora_attn.lora_param_count();
1501
1502 let hidden = config.hidden_size;
1504 let kv_hidden = config.num_kv_heads * config.head_dim();
1505 let expected = (hidden * rank + rank * hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * hidden); assert_eq!(param_count, expected);
1511 assert!(param_count > 0);
1512 }
1513
1514 #[test]
1515 fn test_mha_with_lora_longer_sequence() {
1516 let config = TransformerConfig::tiny();
1517 let attn = MultiHeadAttention::new(&config);
1518 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1519
1520 let seq_len = 8;
1521 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1522 let output = lora_attn.forward(&x, seq_len);
1523
1524 assert_eq!(output.len(), seq_len * config.hidden_size);
1525 }
1526
1527 #[test]
1528 fn test_parameters_mut() {
1529 let config = TransformerConfig::tiny();
1530 let mut attn = MultiHeadAttention::new(&config);
1531
1532 let params = attn.parameters_mut();
1533 assert_eq!(params.len(), 4);
1534 }
1535
1536 #[test]
1561 fn falsify_a1e_from_params_rejects_wrong_shape_q_weight() {
1562 let config = TransformerConfig::tiny();
1563 let hidden_size = config.hidden_size;
1564 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1565
1566 let mut params = HashMap::new();
1567 params.insert("attn.q_proj.weight".to_string(), Tensor::from_vec(vec![0.1; 50], true));
1569 params.insert(
1571 "attn.k_proj.weight".to_string(),
1572 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1573 );
1574 params.insert(
1575 "attn.v_proj.weight".to_string(),
1576 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1577 );
1578 params.insert(
1579 "attn.o_proj.weight".to_string(),
1580 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1581 );
1582
1583 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1584 assert!(
1586 attn.is_none(),
1587 "FALSIFY-A1e: PMAT-331 fix — from_params MUST reject wrong-shape q_proj"
1588 );
1589 }
1590
1591 #[test]
1596 fn falsify_a2e_gqa_init_correct_kv_dimensions() {
1597 let mut config = TransformerConfig::tiny();
1598 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1601 let head_dim = config.head_dim();
1602 let kv_hidden = config.num_kv_heads * head_dim; assert_eq!(
1606 attn.w_q.len(),
1607 config.hidden_size * config.hidden_size,
1608 "FALSIFY-A2e: Q projection must be hidden*hidden"
1609 );
1610
1611 assert_eq!(
1613 attn.w_k.len(),
1614 config.hidden_size * kv_hidden,
1615 "FALSIFY-A2e: K projection must use num_kv_heads, not num_heads"
1616 );
1617
1618 assert_eq!(
1620 attn.w_v.len(),
1621 config.hidden_size * kv_hidden,
1622 "FALSIFY-A2e: V projection must use num_kv_heads, not num_heads"
1623 );
1624
1625 assert_eq!(
1627 attn.w_o.len(),
1628 config.hidden_size * config.hidden_size,
1629 "FALSIFY-A2e: O projection must be hidden*hidden"
1630 );
1631
1632 assert!(
1634 attn.w_k.len() < attn.w_q.len(),
1635 "FALSIFY-A2e: For GQA, K weight must be smaller than Q weight"
1636 );
1637 }
1638
1639 #[test]
1644 fn falsify_a3e_gqa_forward_correct_output_dims() {
1645 let mut config = TransformerConfig::tiny();
1646 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1649 let seq_len = 3;
1650 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1651 let output = attn.forward(&x, seq_len);
1652
1653 assert_eq!(
1654 output.len(),
1655 seq_len * config.hidden_size,
1656 "FALSIFY-A3e: GQA output must be seq_len * hidden_size, not seq_len * kv_hidden"
1657 );
1658 }
1659
1660 #[test]
1664 fn falsify_a4e_init_produces_valid_attention_weights() {
1665 let config = TransformerConfig::tiny();
1666 let attn = MultiHeadAttention::new(&config);
1667
1668 for (name, w) in
1669 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1670 {
1671 let data = w.data();
1672 let slice = data.as_slice().expect("data as slice");
1673
1674 let nan_count = slice.iter().filter(|v| v.is_nan()).count();
1676 assert_eq!(nan_count, 0, "FALSIFY-A4e: {name} init must not contain NaN");
1677
1678 let inf_count = slice.iter().filter(|v| v.is_infinite()).count();
1680 assert_eq!(inf_count, 0, "FALSIFY-A4e: {name} init must not contain Inf");
1681
1682 let min = slice.iter().copied().fold(f32::INFINITY, f32::min);
1684 let max = slice.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1685 assert!(
1686 (max - min).abs() > 1e-6,
1687 "FALSIFY-A4e: {name} init values are constant ({min}..{max}) — degenerate weight"
1688 );
1689 }
1690 }
1691
1692 #[test]
1697 fn falsify_a5e_forward_produces_finite_output() {
1698 let config = TransformerConfig::tiny();
1699 let attn = MultiHeadAttention::new(&config);
1700 let seq_len = 4;
1701 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1702 let output = attn.forward(&x, seq_len);
1703
1704 let data = output.data();
1705 let nan_count = data.iter().filter(|v| v.is_nan()).count();
1706 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
1707 assert_eq!(nan_count, 0, "FALSIFY-A5e: Attention output must not contain NaN");
1708 assert_eq!(inf_count, 0, "FALSIFY-A5e: Attention output must not contain Inf");
1709 }
1710
1711 #[test]
1728 fn falsify_gq_001e_output_shape() {
1729 for (num_heads, num_kv_heads) in [(2, 2), (4, 2), (4, 1), (2, 1)] {
1730 let mut config = TransformerConfig::tiny();
1731 config.num_attention_heads = num_heads;
1732 config.num_kv_heads = num_kv_heads;
1733
1734 let attn = MultiHeadAttention::new(&config);
1735 let seq_len = 3;
1736 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1737 let output = attn.forward(&x, seq_len);
1738
1739 assert_eq!(
1740 output.len(),
1741 seq_len * config.hidden_size,
1742 "FALSIFIED GQ-001e: output len mismatch for heads={num_heads},kv={num_kv_heads}"
1743 );
1744 }
1745 }
1746
1747 #[test]
1749 fn falsify_gq_002e_mha_degeneration() {
1750 let config = TransformerConfig::tiny(); assert_eq!(config.num_attention_heads, config.num_kv_heads);
1752
1753 let attn = MultiHeadAttention::new(&config);
1754 let seq_len = 4;
1755 let x = Tensor::from_vec(
1756 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.37).sin()).collect(),
1757 true,
1758 );
1759 let output = attn.forward(&x, seq_len);
1760
1761 let data = output.data();
1762 for (i, v) in data.iter().enumerate() {
1763 assert!(v.is_finite(), "FALSIFIED GQ-002e: MHA output[{i}] = {v} (not finite)");
1764 }
1765 }
1766
1767 #[test]
1769 fn falsify_gq_004e_head_divisibility() {
1770 for (nh, nkv) in [(2, 1), (2, 2), (4, 1), (4, 2), (4, 4), (8, 2), (8, 4)] {
1772 let mut config = TransformerConfig::tiny();
1773 config.num_attention_heads = nh;
1774 config.num_kv_heads = nkv;
1775 assert_eq!(nh % nkv, 0, "FALSIFIED GQ-004e: test config has invalid head ratio");
1776 let attn = MultiHeadAttention::new(&config);
1778 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1779 let _ = attn.forward(&x, 2);
1780 }
1781 }
1782
1783 #[test]
1785 fn falsify_gq_006e_mqa_boundary() {
1786 let mut config = TransformerConfig::tiny();
1787 config.num_attention_heads = 4;
1788 config.num_kv_heads = 1;
1789 config.hidden_size = 64;
1791
1792 let attn = MultiHeadAttention::new(&config);
1793 let seq_len = 3;
1794 let x = Tensor::from_vec(
1795 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.73).cos()).collect(),
1796 true,
1797 );
1798 let output = attn.forward(&x, seq_len);
1799
1800 assert_eq!(
1801 output.len(),
1802 seq_len * config.hidden_size,
1803 "FALSIFIED GQ-006e: MQA output size wrong"
1804 );
1805
1806 let data = output.data();
1808 for (i, v) in data.iter().enumerate() {
1809 assert!(v.is_finite(), "FALSIFIED GQ-006e: MQA output[{i}] = {v} (not finite)");
1810 }
1811 }
1812
1813 mod gq_proptest_falsify {
1814 use super::*;
1815 use proptest::prelude::*;
1816
1817 proptest! {
1819 #![proptest_config(ProptestConfig::with_cases(50))]
1820
1821 #[test]
1822 fn falsify_gq_001e_prop_output_shape(
1823 config_idx in 0..4usize,
1824 seq_len in 2..=6usize,
1825 seed in 0..500u32,
1826 ) {
1827 let configs: [(usize, usize); 4] = [
1828 (2, 2), (2, 1), (4, 2), (4, 1),
1829 ];
1830 let (num_heads, num_kv_heads) = configs[config_idx];
1831 let mut config = TransformerConfig::tiny();
1832 config.num_attention_heads = num_heads;
1833 config.num_kv_heads = num_kv_heads;
1834
1835 let attn = MultiHeadAttention::new(&config);
1836 let data: Vec<f32> = (0..seq_len * config.hidden_size)
1837 .map(|i| ((i as f32 + seed as f32) * 0.37).sin())
1838 .collect();
1839 let x = Tensor::from_vec(data, true);
1840 let output = attn.forward(&x, seq_len);
1841
1842 prop_assert_eq!(
1843 output.len(),
1844 seq_len * config.hidden_size,
1845 "FALSIFIED GQ-001e-prop: output len mismatch"
1846 );
1847
1848 for v in output.data() {
1850 prop_assert!(
1851 v.is_finite(),
1852 "FALSIFIED GQ-001e-prop: non-finite output"
1853 );
1854 }
1855 }
1856 }
1857
1858 proptest! {
1860 #![proptest_config(ProptestConfig::with_cases(30))]
1861
1862 #[test]
1863 fn falsify_gq_006e_prop_mqa_boundary(
1864 seed in 0..500u32,
1865 seq_len in 2..=5usize,
1866 ) {
1867 let mut config = TransformerConfig::tiny();
1868 config.num_attention_heads = 4;
1869 config.num_kv_heads = 1;
1870 config.hidden_size = 64;
1871
1872 let attn = MultiHeadAttention::new(&config);
1873 let data: Vec<f32> = (0..seq_len * config.hidden_size)
1874 .map(|i| ((i as f32 + seed as f32) * 0.73).cos())
1875 .collect();
1876 let x = Tensor::from_vec(data, true);
1877 let output = attn.forward(&x, seq_len);
1878
1879 prop_assert_eq!(
1880 output.len(),
1881 seq_len * config.hidden_size,
1882 "FALSIFIED GQ-006e-prop: MQA output len mismatch"
1883 );
1884
1885 for v in output.data() {
1886 prop_assert!(
1887 v.is_finite(),
1888 "FALSIFIED GQ-006e-prop: non-finite MQA output"
1889 );
1890 }
1891 }
1892 }
1893 }
1894
1895 #[test]
1896 fn test_attention_from_params_with_biases() {
1897 let config = TransformerConfig::tiny();
1898 let hidden_size = config.hidden_size;
1899 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1900
1901 let mut params = HashMap::new();
1902 params.insert(
1903 "attn.q_proj.weight".to_string(),
1904 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1905 );
1906 params.insert(
1907 "attn.k_proj.weight".to_string(),
1908 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1909 );
1910 params.insert(
1911 "attn.v_proj.weight".to_string(),
1912 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1913 );
1914 params.insert(
1915 "attn.o_proj.weight".to_string(),
1916 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1917 );
1918 params.insert(
1919 "attn.q_proj.bias".to_string(),
1920 Tensor::from_vec(vec![0.01; hidden_size], true),
1921 );
1922 params.insert(
1923 "attn.k_proj.bias".to_string(),
1924 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
1925 );
1926 params.insert(
1927 "attn.v_proj.bias".to_string(),
1928 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
1929 );
1930
1931 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1932 assert!(attn.is_some());
1933 let attn = attn.expect("should load with biases");
1934 assert!(attn.has_biases());
1935 assert_eq!(attn.parameters().len(), 7);
1936 }
1937
1938 #[test]
1939 fn test_attention_named_parameters_with_biases() {
1940 let config = TransformerConfig::tiny();
1941 let hidden_size = config.hidden_size;
1942 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1943
1944 let mut params = HashMap::new();
1945 params.insert(
1946 "attn.q_proj.weight".to_string(),
1947 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1948 );
1949 params.insert(
1950 "attn.k_proj.weight".to_string(),
1951 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1952 );
1953 params.insert(
1954 "attn.v_proj.weight".to_string(),
1955 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1956 );
1957 params.insert(
1958 "attn.o_proj.weight".to_string(),
1959 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1960 );
1961 params.insert(
1962 "attn.q_proj.bias".to_string(),
1963 Tensor::from_vec(vec![0.01; hidden_size], true),
1964 );
1965 params.insert(
1966 "attn.k_proj.bias".to_string(),
1967 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
1968 );
1969 params.insert(
1970 "attn.v_proj.bias".to_string(),
1971 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
1972 );
1973
1974 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
1975 let named = attn.named_parameters("attn");
1976 assert_eq!(named.len(), 7);
1977 let names: Vec<&str> = named.iter().map(|(n, _)| n.as_str()).collect();
1978 assert!(names.contains(&"attn.q_proj.bias"));
1979 assert!(names.contains(&"attn.k_proj.bias"));
1980 assert!(names.contains(&"attn.v_proj.bias"));
1981 }
1982
1983 #[test]
1984 fn test_attention_forward_with_biases() {
1985 let config = TransformerConfig::tiny();
1986 let hidden_size = config.hidden_size;
1987 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1988
1989 let mut params = HashMap::new();
1990 params.insert(
1991 "attn.q_proj.weight".to_string(),
1992 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1993 );
1994 params.insert(
1995 "attn.k_proj.weight".to_string(),
1996 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1997 );
1998 params.insert(
1999 "attn.v_proj.weight".to_string(),
2000 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2001 );
2002 params.insert(
2003 "attn.o_proj.weight".to_string(),
2004 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2005 );
2006 params
2007 .insert("attn.q_proj.bias".to_string(), Tensor::from_vec(vec![0.5; hidden_size], true));
2008 params.insert(
2009 "attn.k_proj.bias".to_string(),
2010 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2011 );
2012 params.insert(
2013 "attn.v_proj.bias".to_string(),
2014 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2015 );
2016
2017 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
2018 let x = Tensor::from_vec(vec![0.1; 2 * hidden_size], false);
2019 let output = attn.forward(&x, 2);
2020 assert_eq!(output.len(), 2 * hidden_size);
2021 assert!(output.data().iter().all(|v| v.is_finite()));
2022 }
2023}