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::{ModelArchitecture, 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 = if self.config.architecture == ModelArchitecture::Decoder {
565 crate::autograd::attention_causal(
566 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
567 )
568 } else {
569 crate::autograd::attention(
570 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
571 )
572 };
573
574 head_q_tensors.push(q_tensor);
575 head_k_tensors.push(k_tensor);
576 head_v_tensors.push(v_tensor);
577 head_outputs.push(attn_out);
578 }
579
580 let mut concat_output = vec![0.0; seq_len * q_dim];
582 for (h, head_out) in head_outputs.iter().enumerate() {
583 let hd = head_out.data();
584 let hdata = hd.as_slice().expect("contiguous attention output");
585 for s in 0..seq_len {
586 let src_base = s * head_dim;
587 let dst_base = s * q_dim + h * head_dim;
588 concat_output[dst_base..dst_base + head_dim]
589 .copy_from_slice(&hdata[src_base..src_base + head_dim]);
590 }
591 }
592
593 let mut concat_tensor = Tensor::from_vec(concat_output, requires_grad);
594
595 if requires_grad {
596 let backward_op = Rc::new(AttentionBlockBackward {
597 q: q.clone(),
598 k: k.clone(),
599 v: v.clone(),
600 head_q_tensors,
601 head_k_tensors,
602 head_v_tensors,
603 head_outputs,
604 head_kv_indices,
605 seq_len,
606 head_dim,
607 q_dim,
608 kv_hidden_size,
609 result_grad: concat_tensor.grad_cell(),
610 });
611 concat_tensor.set_backward_op(backward_op);
612 }
613
614 let result = matmul_nt(&concat_tensor, &self.w_o, seq_len, q_dim, hidden_size);
616 contract_post_attention!(result.data().as_slice().unwrap_or(&[]));
617 result
618 }
619
620 pub fn forward_with_lora(
633 &self,
634 x: &Tensor,
635 seq_len: usize,
636 lora_a_q: &Tensor,
637 lora_b_q: &Tensor,
639 lora_a_v: &Tensor,
640 lora_b_v: &Tensor,
641 lora_rank: usize,
642 lora_scale: f32,
643 ) -> Tensor {
644 contract_pre_lora_forward!();
645 let hidden_size = self.config.hidden_size;
646 let num_heads = self.config.num_attention_heads;
647 let num_kv_heads = self.config.num_kv_heads;
648 let head_dim = self.config.head_dim();
649 let q_dim = self.config.q_dim();
650 let kv_hidden_size = num_kv_heads * head_dim;
651
652 let q_base = matmul_nt(x, &self.w_q, seq_len, hidden_size, q_dim);
663 let q_mid = crate::autograd::matmul_nt(x, lora_a_q, seq_len, hidden_size, lora_rank);
664 let q_lora = crate::autograd::matmul_nt(&q_mid, lora_b_q, seq_len, lora_rank, q_dim);
665 let q = crate::autograd::add_scaled(&q_base, &q_lora, lora_scale);
666
667 let k = matmul_nt(x, &self.w_k, seq_len, hidden_size, kv_hidden_size);
669
670 let v_base = matmul_nt(x, &self.w_v, seq_len, hidden_size, kv_hidden_size);
672 let v_mid = crate::autograd::matmul_nt(x, lora_a_v, seq_len, hidden_size, lora_rank);
673 let v_lora =
674 crate::autograd::matmul_nt(&v_mid, lora_b_v, seq_len, lora_rank, kv_hidden_size);
675 let v = crate::autograd::add_scaled(&v_base, &v_lora, lora_scale);
676
677 let broadcast_bias = |bias: &Tensor| -> Tensor {
691 let bd = bias.data();
692 let b = bd.as_slice().expect("contiguous bias");
693 let mut out = Vec::with_capacity(seq_len * b.len());
694 for _ in 0..seq_len {
695 out.extend_from_slice(b);
696 }
697 Tensor::from_vec(out, false)
698 };
699 let q = if let Some(ref b_q) = self.b_q {
700 crate::autograd::add_scaled(&q, &broadcast_bias(b_q), 1.0)
701 } else {
702 q
703 };
704 let k = if let Some(ref b_k) = self.b_k {
705 crate::autograd::add_scaled(&k, &broadcast_bias(b_k), 1.0)
706 } else {
707 k
708 };
709 let v = if let Some(ref b_v) = self.b_v {
710 crate::autograd::add_scaled(&v, &broadcast_bias(b_v), 1.0)
711 } else {
712 v
713 };
714
715 let q = if let Some(ref qn) = self.q_norm {
717 apply_qk_norm(&q, qn, seq_len, num_heads, head_dim)
718 } else {
719 q
720 };
721 let k = if let Some(ref kn) = self.k_norm {
722 apply_qk_norm(&k, kn, seq_len, num_kv_heads, head_dim)
723 } else {
724 k
725 };
726
727 let (q, k) = if self.config.rope_theta > 0.0 {
730 (
731 apply_rope(&q, seq_len, num_heads, head_dim, self.config.rope_theta),
732 apply_rope(&k, seq_len, num_kv_heads, head_dim, self.config.rope_theta),
733 )
734 } else {
735 (q, k)
736 };
737
738 let requires_grad = q.requires_grad() || k.requires_grad() || v.requires_grad();
739 let heads_per_kv = num_heads / num_kv_heads;
740
741 let q_data = q.data();
743 let q_slice = q_data.as_slice().expect("contiguous Q");
744 let k_data = k.data();
745 let k_slice = k_data.as_slice().expect("contiguous K");
746 let v_data = v.data();
747 let v_slice = v_data.as_slice().expect("contiguous V");
748
749 let mut head_q_tensors = Vec::with_capacity(num_heads);
751 let mut head_k_tensors = Vec::with_capacity(num_heads);
752 let mut head_v_tensors = Vec::with_capacity(num_heads);
753 let mut head_outputs = Vec::with_capacity(num_heads);
754 let mut head_kv_indices = Vec::with_capacity(num_heads);
755
756 for h in 0..num_heads {
757 let kv_h = h / heads_per_kv;
758 head_kv_indices.push(kv_h);
759
760 let mut q_head = Vec::with_capacity(seq_len * head_dim);
762 for s in 0..seq_len {
763 let start = s * q_dim + h * head_dim;
764 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
765 }
766
767 let mut k_head = Vec::with_capacity(seq_len * head_dim);
768 for s in 0..seq_len {
769 let start = s * kv_hidden_size + kv_h * head_dim;
770 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
771 }
772
773 let mut v_head = Vec::with_capacity(seq_len * head_dim);
774 for s in 0..seq_len {
775 let start = s * kv_hidden_size + kv_h * head_dim;
776 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
777 }
778
779 let q_tensor = Tensor::from_vec(q_head, requires_grad);
780 let k_tensor = Tensor::from_vec(k_head, requires_grad);
781 let v_tensor = Tensor::from_vec(v_head, requires_grad);
782
783 let attn_out = if self.config.architecture == ModelArchitecture::Decoder {
789 crate::autograd::attention_causal(
790 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
791 )
792 } else {
793 crate::autograd::attention(
794 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
795 )
796 };
797
798 head_q_tensors.push(q_tensor);
799 head_k_tensors.push(k_tensor);
800 head_v_tensors.push(v_tensor);
801 head_outputs.push(attn_out);
802 }
803
804 let mut concat_output = vec![0.0; seq_len * q_dim];
806 for (h, head_out) in head_outputs.iter().enumerate() {
807 let hd = head_out.data();
808 let hdata = hd.as_slice().expect("contiguous attention output");
809 for s in 0..seq_len {
810 let src_base = s * head_dim;
811 let dst_base = s * q_dim + h * head_dim;
812 concat_output[dst_base..dst_base + head_dim]
813 .copy_from_slice(&hdata[src_base..src_base + head_dim]);
814 }
815 }
816
817 let mut concat_tensor = Tensor::from_vec(concat_output, requires_grad);
818
819 if requires_grad {
820 let backward_op = Rc::new(AttentionBlockBackward {
821 q: q.clone(),
822 k: k.clone(),
823 v: v.clone(),
824 head_q_tensors,
825 head_k_tensors,
826 head_v_tensors,
827 head_outputs,
828 head_kv_indices,
829 seq_len,
830 head_dim,
831 q_dim,
832 kv_hidden_size,
833 result_grad: concat_tensor.grad_cell(),
834 });
835 concat_tensor.set_backward_op(backward_op);
836 }
837
838 let result = matmul_nt(&concat_tensor, &self.w_o, seq_len, q_dim, hidden_size);
840 contract_post_lora_forward!(result);
841 result
842 }
843
844 pub fn parameters(&self) -> Vec<&Tensor> {
846 let mut params = vec![&self.w_q, &self.w_k, &self.w_v, &self.w_o];
847 if let Some(ref b) = self.b_q {
848 params.push(b);
849 }
850 if let Some(ref b) = self.b_k {
851 params.push(b);
852 }
853 if let Some(ref b) = self.b_v {
854 params.push(b);
855 }
856 params
857 }
858
859 pub fn parameters_mut(&mut self) -> Vec<&mut Tensor> {
861 let mut params = vec![&mut self.w_q, &mut self.w_k, &mut self.w_v, &mut self.w_o];
862 if let Some(ref mut b) = self.b_q {
863 params.push(b);
864 }
865 if let Some(ref mut b) = self.b_k {
866 params.push(b);
867 }
868 if let Some(ref mut b) = self.b_v {
869 params.push(b);
870 }
871 params
872 }
873
874 pub fn has_biases(&self) -> bool {
876 self.b_q.is_some()
877 }
878
879 pub fn named_parameters(&self, prefix: &str) -> Vec<(String, &Tensor)> {
881 let mut params = vec![
882 (format!("{prefix}.q_proj.weight"), &self.w_q),
883 (format!("{prefix}.k_proj.weight"), &self.w_k),
884 (format!("{prefix}.v_proj.weight"), &self.w_v),
885 (format!("{prefix}.o_proj.weight"), &self.w_o),
886 ];
887 if let Some(ref b) = self.b_q {
888 params.push((format!("{prefix}.q_proj.bias"), b));
889 }
890 if let Some(ref b) = self.b_k {
891 params.push((format!("{prefix}.k_proj.bias"), b));
892 }
893 if let Some(ref b) = self.b_v {
894 params.push((format!("{prefix}.v_proj.bias"), b));
895 }
896 params
897 }
898
899 pub fn set_named_parameter(&mut self, suffix: &str, value: Tensor) -> bool {
910 match suffix {
911 "self_attn.q_proj.weight" => {
912 self.w_q = value;
913 true
914 }
915 "self_attn.k_proj.weight" => {
916 self.w_k = value;
917 true
918 }
919 "self_attn.v_proj.weight" => {
920 self.w_v = value;
921 true
922 }
923 "self_attn.o_proj.weight" => {
924 self.w_o = value;
925 true
926 }
927 "self_attn.q_proj.bias" if self.b_q.is_some() => {
928 self.b_q = Some(value);
929 true
930 }
931 "self_attn.k_proj.bias" if self.b_k.is_some() => {
932 self.b_k = Some(value);
933 true
934 }
935 "self_attn.v_proj.bias" if self.b_v.is_some() => {
936 self.b_v = Some(value);
937 true
938 }
939 _ => false,
940 }
941 }
942}
943
944pub struct LoRAProjection {
949 pub base_weight: Tensor,
951 pub lora_a: Tensor,
953 pub lora_b: Tensor,
955 pub d_in: usize,
957 pub d_out: usize,
959 pub rank: usize,
961 pub scale: f32,
963}
964
965impl LoRAProjection {
966 pub fn new(base_weight: Tensor, d_in: usize, d_out: usize, rank: usize, alpha: f32) -> Self {
975 assert_eq!(base_weight.len(), d_in * d_out, "Base weight size mismatch");
976
977 let mut base_weight = base_weight;
979 base_weight.set_requires_grad(false);
980
981 let lora_a = Tensor::from_vec(
983 (0..d_in * rank).map(|i| (i as f32 * 0.123).sin() * 0.01).collect(),
984 true, );
986
987 let lora_b = Tensor::zeros(rank * d_out, true);
989
990 Self { base_weight, lora_a, lora_b, d_in, d_out, rank, scale: alpha / rank as f32 }
991 }
992
993 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
1004 let base_out = matmul(x, &self.base_weight, seq_len, self.d_in, self.d_out);
1006
1007 let lora_intermediate = matmul(x, &self.lora_a, seq_len, self.d_in, self.rank);
1010
1011 let lora_out = matmul(&lora_intermediate, &self.lora_b, seq_len, self.rank, self.d_out);
1013
1014 crate::autograd::add_scaled(&base_out, &lora_out, self.scale)
1017 }
1018
1019 pub fn lora_params(&self) -> Vec<&Tensor> {
1021 vec![&self.lora_a, &self.lora_b]
1022 }
1023
1024 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
1026 vec![&mut self.lora_a, &mut self.lora_b]
1027 }
1028}
1029
1030pub struct MultiHeadAttentionWithLoRA {
1034 pub config: TransformerConfig,
1036 pub q_proj: LoRAProjection,
1038 pub k_proj: LoRAProjection,
1040 pub v_proj: LoRAProjection,
1042 pub o_proj: LoRAProjection,
1044}
1045
1046impl MultiHeadAttentionWithLoRA {
1047 pub fn from_attention(attn: &MultiHeadAttention, rank: usize, alpha: f32) -> Self {
1054 let hidden_size = attn.config.hidden_size;
1055 let q_dim = attn.config.q_dim();
1056 let kv_hidden_size = attn.config.num_kv_heads * attn.config.head_dim();
1057
1058 Self {
1059 config: attn.config.clone(),
1060 q_proj: LoRAProjection::new(attn.w_q.clone(), hidden_size, q_dim, rank, alpha),
1061 k_proj: LoRAProjection::new(attn.w_k.clone(), hidden_size, kv_hidden_size, rank, alpha),
1062 v_proj: LoRAProjection::new(attn.w_v.clone(), hidden_size, kv_hidden_size, rank, alpha),
1063 o_proj: LoRAProjection::new(attn.w_o.clone(), q_dim, hidden_size, rank, alpha),
1064 }
1065 }
1066
1067 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
1071 let num_heads = self.config.num_attention_heads;
1072 let num_kv_heads = self.config.num_kv_heads;
1073 let head_dim = self.config.head_dim();
1074 let q_dim = self.config.q_dim();
1075 let kv_hidden_size = num_kv_heads * head_dim;
1076
1077 let q = self.q_proj.forward(x, seq_len);
1079 let k = self.k_proj.forward(x, seq_len);
1080 let v = self.v_proj.forward(x, seq_len);
1081
1082 let mut attn_outputs = Vec::with_capacity(num_heads * seq_len * head_dim);
1084 let heads_per_kv = num_heads / num_kv_heads;
1085
1086 let q_data = q.data();
1088 let q_slice = q_data.as_slice().expect("contiguous Q tensor");
1089 let k_data = k.data();
1090 let k_slice = k_data.as_slice().expect("contiguous K tensor");
1091 let v_data = v.data();
1092 let v_slice = v_data.as_slice().expect("contiguous V tensor");
1093
1094 for h in 0..num_heads {
1095 let kv_h = h / heads_per_kv;
1096
1097 let mut q_head = Vec::with_capacity(seq_len * head_dim);
1099 for s in 0..seq_len {
1100 let start = s * q_dim + h * head_dim;
1101 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
1102 }
1103
1104 let mut k_head = Vec::with_capacity(seq_len * head_dim);
1105 for s in 0..seq_len {
1106 let start = s * kv_hidden_size + kv_h * head_dim;
1107 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
1108 }
1109
1110 let mut v_head = Vec::with_capacity(seq_len * head_dim);
1111 for s in 0..seq_len {
1112 let start = s * kv_hidden_size + kv_h * head_dim;
1113 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
1114 }
1115
1116 let q_tensor = Tensor::from_vec(q_head, false);
1118 let k_tensor = Tensor::from_vec(k_head, false);
1119 let v_tensor = Tensor::from_vec(v_head, false);
1120
1121 let attn_out = if self.config.architecture == ModelArchitecture::Decoder {
1127 crate::autograd::attention_causal(
1128 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
1129 )
1130 } else {
1131 crate::autograd::attention(
1132 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
1133 )
1134 };
1135
1136 attn_outputs.extend_from_slice(
1137 attn_out.data().as_slice().expect("contiguous attention output"),
1138 );
1139 }
1140
1141 let mut concat_output = vec![0.0; seq_len * q_dim];
1143 for h in 0..num_heads {
1144 for s in 0..seq_len {
1145 let src_idx = h * seq_len * head_dim + s * head_dim;
1146 let dst_idx = s * q_dim + h * head_dim;
1147 concat_output[dst_idx..dst_idx + head_dim]
1148 .copy_from_slice(&attn_outputs[src_idx..src_idx + head_dim]);
1149 }
1150 }
1151
1152 let concat_tensor = Tensor::from_vec(concat_output, true);
1153
1154 self.o_proj.forward(&concat_tensor, seq_len)
1156 }
1157
1158 pub fn lora_params(&self) -> Vec<&Tensor> {
1160 let mut params = Vec::new();
1161 params.extend(self.q_proj.lora_params());
1162 params.extend(self.k_proj.lora_params());
1163 params.extend(self.v_proj.lora_params());
1164 params.extend(self.o_proj.lora_params());
1165 params
1166 }
1167
1168 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
1170 let mut params = Vec::new();
1171 params.extend(self.q_proj.lora_params_mut());
1172 params.extend(self.k_proj.lora_params_mut());
1173 params.extend(self.v_proj.lora_params_mut());
1174 params.extend(self.o_proj.lora_params_mut());
1175 params
1176 }
1177
1178 pub fn lora_param_count(&self) -> usize {
1180 let hidden = self.config.hidden_size;
1182 let kv_hidden = self.config.num_kv_heads * self.config.head_dim();
1183 let rank = self.q_proj.rank;
1184
1185 (hidden * rank + rank * hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * hidden) }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1216 fn beat_qlora_composed_forward_equivalence() {
1217 let mut config = TransformerConfig::tiny();
1218 config.use_bias = true;
1219 let hidden = config.hidden_size;
1220 let q_dim = config.q_dim();
1221 let kv_dim = config.num_kv_heads * config.head_dim();
1222 let seq = 3usize;
1223 let rank = 4usize;
1224 let scale = 8.0f32 / rank as f32; let mut attn = MultiHeadAttention::new(&config);
1227
1228 let mk_bias = |n: usize, amp: f32| {
1231 Tensor::from_vec((0..n).map(|i| amp * (((i % 7) as f32) - 3.0)).collect(), true)
1232 };
1233 attn.b_q = Some(mk_bias(q_dim, 0.05));
1234 attn.b_k = Some(mk_bias(kv_dim, 0.03));
1235 attn.b_v = Some(mk_bias(kv_dim, 0.04));
1236
1237 let mk = |rows: usize, cols: usize, amp: f32, ph: f32| -> Vec<f32> {
1240 (0..rows * cols).map(|i| amp * ((i as f32).mul_add(0.017, ph)).sin()).collect()
1241 };
1242 let a_q = mk(rank, hidden, 0.10, 0.0);
1243 let b_q = mk(q_dim, rank, 0.12, 1.0);
1244 let a_v = mk(rank, hidden, 0.09, 2.0);
1245 let b_v = mk(kv_dim, rank, 0.11, 3.0);
1246
1247 let x = Tensor::from_vec(
1248 (0..seq * hidden).map(|i| (i as f32).mul_add(0.023, -0.5).cos() * 0.4).collect(),
1249 true,
1250 );
1251
1252 let out_lora = attn.forward_with_lora(
1254 &x,
1255 seq,
1256 &Tensor::from_vec(a_q.clone(), true),
1257 &Tensor::from_vec(b_q.clone(), true),
1258 &Tensor::from_vec(a_v.clone(), true),
1259 &Tensor::from_vec(b_v.clone(), true),
1260 rank,
1261 scale,
1262 );
1263
1264 let merge = |w: &Tensor, a: &[f32], b: &[f32], out: usize| -> Vec<f32> {
1267 let wd = w.data();
1268 let mut m = wd.as_slice().expect("contiguous w").to_vec();
1269 for o in 0..out {
1270 for i in 0..hidden {
1271 let mut d = 0.0f32;
1272 for k in 0..rank {
1273 d += b[o * rank + k] * a[k * hidden + i];
1274 }
1275 m[o * hidden + i] += scale * d;
1276 }
1277 }
1278 m
1279 };
1280 attn.w_q = Tensor::from_vec(merge(&attn.w_q, &a_q, &b_q, q_dim), true);
1281 attn.w_v = Tensor::from_vec(merge(&attn.w_v, &a_v, &b_v, kv_dim), true);
1282 let out_merged = attn.forward(&x, seq);
1283
1284 let ld = out_lora.data();
1285 let ls = ld.as_slice().expect("contiguous lora out");
1286 let md = out_merged.data();
1287 let ms = md.as_slice().expect("contiguous merged out");
1288 assert_eq!(ls.len(), ms.len(), "output shape mismatch");
1289 let max_abs = ls.iter().zip(ms).map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
1290 assert!(
1291 max_abs < 1e-4,
1292 "FALSIFY-QLORA-COMPOSED-FORWARD-001: composed forward_with_lora (base + \
1293 scale·B@A + bias) diverges from the LoRA-merged forward by max|Δ|={max_abs:.6} \
1294 — a dropped Q/K/V bias, a wrong LoRA scale, or a transpose in the composition \
1295 (the #2260 bias-drop class, now with nonzero LoRA + biases)."
1296 );
1297 println!(
1298 "BEAT-QLORA-COMPOSED-FORWARD: forward_with_lora ≡ merged forward — max|Δ|={max_abs:.2e}"
1299 );
1300 }
1301
1302 #[test]
1306 fn falsify_pmat805_rope_backward_matches_finite_difference() {
1307 let seq_len = 3usize;
1308 let num_heads = 2usize;
1309 let head_dim = 4usize; let total = seq_len * num_heads * head_dim;
1311 let theta = 10000.0f32;
1312
1313 let x_data: Vec<f32> = (0..total).map(|i| ((i as f32 * 0.37).sin() * 1.5) + 0.1).collect();
1315 let w: Vec<f32> = (0..total).map(|i| ((i as f32 * 0.21).cos() * 0.8) - 0.05).collect();
1316
1317 let x = Tensor::from_vec(x_data.clone(), true);
1319 let y = apply_rope(&x, seq_len, num_heads, head_dim, theta);
1320 y.set_grad(Array1::from(w.clone()));
1321 y.backward_op().expect("rope must have a backward op").backward();
1322 let analytical = x.grad().expect("x must have grad after rope backward").to_vec();
1323
1324 let h = 1e-3f32;
1326 let loss = |xv: &[f32]| -> f32 {
1327 let t = Tensor::from_vec(xv.to_vec(), false);
1328 let r = apply_rope(&t, seq_len, num_heads, head_dim, theta);
1329 r.data().iter().zip(&w).map(|(a, b)| a * b).sum()
1330 };
1331 for j in 0..total {
1332 let mut xp = x_data.clone();
1333 let mut xm = x_data.clone();
1334 xp[j] += h;
1335 xm[j] -= h;
1336 let numerical = (loss(&xp) - loss(&xm)) / (2.0 * h);
1337 let diff = (analytical[j] - numerical).abs();
1338 assert!(
1339 diff < 1e-2,
1340 "FALSIFIED: RoPE grad[{j}] analytical={} numerical={} diff={diff}",
1341 analytical[j],
1342 numerical
1343 );
1344 }
1345 }
1346
1347 #[test]
1348 fn test_multi_head_attention_tiny() {
1349 let config = TransformerConfig::tiny();
1350 let attn = MultiHeadAttention::new(&config);
1351 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1352 let output = attn.forward(&x, 2);
1353 assert_eq!(output.len(), 2 * config.hidden_size);
1354 }
1355
1356 #[test]
1357 fn test_multi_head_attention_parameters() {
1358 let config = TransformerConfig::tiny();
1359 let attn = MultiHeadAttention::new(&config);
1360 let params = attn.parameters();
1361 assert_eq!(params.len(), 4); }
1363
1364 #[test]
1365 fn test_attention_longer_sequence() {
1366 let config = TransformerConfig::tiny();
1367 let attn = MultiHeadAttention::new(&config);
1368 let x = Tensor::from_vec(vec![0.1; 8 * config.hidden_size], true);
1369 let output = attn.forward(&x, 8);
1370 assert_eq!(output.len(), 8 * config.hidden_size);
1371 }
1372
1373 #[test]
1374 fn test_attention_weight_sizes() {
1375 let config = TransformerConfig::tiny();
1376 let attn = MultiHeadAttention::new(&config);
1377 let kv_hidden = config.num_kv_heads * config.head_dim();
1378 assert_eq!(attn.w_q.len(), config.hidden_size * config.hidden_size);
1379 assert_eq!(attn.w_k.len(), config.hidden_size * kv_hidden);
1380 assert_eq!(attn.w_v.len(), config.hidden_size * kv_hidden);
1381 assert_eq!(attn.w_o.len(), config.hidden_size * config.hidden_size);
1382 }
1383
1384 #[test]
1385 fn test_multi_head_attention_from_params_success() {
1386 let config = TransformerConfig::tiny();
1387 let hidden_size = config.hidden_size;
1388 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1389
1390 let mut params = HashMap::new();
1391 params.insert(
1392 "attn.q_proj.weight".to_string(),
1393 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1394 );
1395 params.insert(
1396 "attn.k_proj.weight".to_string(),
1397 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1398 );
1399 params.insert(
1400 "attn.v_proj.weight".to_string(),
1401 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1402 );
1403 params.insert(
1404 "attn.o_proj.weight".to_string(),
1405 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1406 );
1407
1408 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1409 assert!(attn.is_some());
1410 let attn = attn.expect("operation should succeed");
1411 assert_eq!(attn.w_q.len(), hidden_size * hidden_size);
1412 }
1413
1414 #[test]
1415 fn test_multi_head_attention_from_params_missing_key() {
1416 let config = TransformerConfig::tiny();
1417 let hidden_size = config.hidden_size;
1418
1419 let mut params = HashMap::new();
1420 params.insert(
1421 "attn.q_proj.weight".to_string(),
1422 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1423 );
1424 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1427 assert!(attn.is_none());
1428 }
1429
1430 #[test]
1431 fn test_attention_projections_backward() {
1432 let config = TransformerConfig::tiny();
1435 let attn = MultiHeadAttention::new(&config);
1436 let hidden_size = config.hidden_size;
1437 let seq_len = 2;
1438
1439 let x = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1440
1441 let mut q = crate::autograd::matmul(&x, &attn.w_q, seq_len, hidden_size, hidden_size);
1443 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1444 crate::autograd::backward(&mut q, Some(grad_out));
1445
1446 assert!(attn.w_q.grad().is_some());
1447 let grad_q = attn.w_q.grad().expect("gradient should be available");
1448 assert!(grad_q.iter().all(|&v| v.is_finite()));
1449 }
1450
1451 #[test]
1452 fn test_output_projection_backward() {
1453 let config = TransformerConfig::tiny();
1455 let attn = MultiHeadAttention::new(&config);
1456 let hidden_size = config.hidden_size;
1457 let seq_len = 2;
1458
1459 let concat_out = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1461
1462 let mut output =
1464 crate::autograd::matmul(&concat_out, &attn.w_o, seq_len, hidden_size, hidden_size);
1465
1466 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1467 crate::autograd::backward(&mut output, Some(grad_out));
1468
1469 assert!(attn.w_o.grad().is_some());
1470 let grad_o = attn.w_o.grad().expect("gradient should be available");
1471 assert!(grad_o.iter().all(|&v| v.is_finite()));
1472 let sum: f32 = grad_o.iter().map(|v| v.abs()).sum();
1473 assert!(sum > 0.0, "Output projection gradient should not be all zero");
1474 }
1475
1476 #[test]
1482 #[ignore = "apply_rope() severs autograd chain — needs backward op (ENT-272)"]
1483 fn test_attention_full_forward_qkv_gradients() {
1484 let config = TransformerConfig::tiny();
1485 let attn = MultiHeadAttention::new(&config);
1486 let hidden_size = config.hidden_size;
1487 let seq_len = 3;
1488
1489 let x_data: Vec<f32> =
1492 (0..seq_len * hidden_size).map(|i| ((i as f32) * 0.17).sin() * 0.5).collect();
1493 let x = Tensor::from_vec(x_data, true);
1494 let mut output = attn.forward(&x, seq_len);
1495
1496 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1497 crate::autograd::backward(&mut output, Some(grad_out));
1498
1499 for (name, param) in
1501 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1502 {
1503 assert!(
1504 param.grad().is_some(),
1505 "ALB-038: {name} must have gradient after full attention forward"
1506 );
1507 let grad = param.grad().expect("gradient available");
1508 assert!(grad.iter().all(|&v| v.is_finite()), "ALB-038: {name} gradient must be finite");
1509 assert!(
1510 grad.iter().any(|&v| v.abs() > 1e-10),
1511 "ALB-038: {name} gradient must be non-zero"
1512 );
1513 }
1514
1515 assert!(x.grad().is_some(), "ALB-038: input x must have gradient");
1517 }
1518
1519 #[test]
1524 fn test_lora_projection_new() {
1525 let d_in = 32;
1526 let d_out = 16;
1527 let rank = 4;
1528 let alpha = 8.0;
1529
1530 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1531 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1532
1533 assert_eq!(lora.d_in, d_in);
1534 assert_eq!(lora.d_out, d_out);
1535 assert_eq!(lora.rank, rank);
1536 assert!((lora.scale - 2.0).abs() < 1e-6); assert_eq!(lora.lora_a.len(), d_in * rank);
1538 assert_eq!(lora.lora_b.len(), rank * d_out);
1539 }
1540
1541 #[test]
1542 fn test_lora_projection_forward() {
1543 let d_in = 32;
1544 let d_out = 16;
1545 let rank = 4;
1546 let alpha = 8.0;
1547 let seq_len = 2;
1548
1549 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1550 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1551
1552 let x = Tensor::from_vec(vec![0.1; seq_len * d_in], false);
1553 let output = lora.forward(&x, seq_len);
1554
1555 assert_eq!(output.len(), seq_len * d_out);
1556 assert!(output.data().iter().all(|&v| v.is_finite()));
1558 }
1559
1560 #[test]
1561 fn test_lora_projection_params() {
1562 let d_in = 32;
1563 let d_out = 16;
1564 let rank = 4;
1565
1566 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1567 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1568
1569 let params = lora.lora_params();
1570 assert_eq!(params.len(), 2); }
1572
1573 #[test]
1574 fn test_lora_projection_params_mut() {
1575 let d_in = 32;
1576 let d_out = 16;
1577 let rank = 4;
1578
1579 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1580 let mut lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1581
1582 let params = lora.lora_params_mut();
1583 assert_eq!(params.len(), 2);
1584 }
1585
1586 #[test]
1587 #[should_panic(expected = "Base weight size mismatch")]
1588 fn test_lora_projection_size_mismatch() {
1589 let d_in = 32;
1590 let d_out = 16;
1591 let rank = 4;
1592
1593 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out + 1], false);
1595 let _ = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1596 }
1597
1598 #[test]
1603 fn test_mha_with_lora_creation() {
1604 let config = TransformerConfig::tiny();
1605 let attn = MultiHeadAttention::new(&config);
1606 let rank = 4;
1607 let alpha = 8.0;
1608
1609 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, alpha);
1610
1611 assert_eq!(lora_attn.q_proj.rank, rank);
1612 assert_eq!(lora_attn.k_proj.rank, rank);
1613 assert_eq!(lora_attn.v_proj.rank, rank);
1614 assert_eq!(lora_attn.o_proj.rank, rank);
1615 }
1616
1617 #[test]
1618 fn test_mha_with_lora_forward() {
1619 let config = TransformerConfig::tiny();
1620 let attn = MultiHeadAttention::new(&config);
1621 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1622
1623 let seq_len = 2;
1624 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1625 let output = lora_attn.forward(&x, seq_len);
1626
1627 assert_eq!(output.len(), seq_len * config.hidden_size);
1628 assert!(output.data().iter().all(|&v| v.is_finite()));
1630 }
1631
1632 #[test]
1633 fn test_mha_with_lora_params() {
1634 let config = TransformerConfig::tiny();
1635 let attn = MultiHeadAttention::new(&config);
1636 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1637
1638 let params = lora_attn.lora_params();
1639 assert_eq!(params.len(), 8);
1641 }
1642
1643 #[test]
1644 fn test_mha_with_lora_params_mut() {
1645 let config = TransformerConfig::tiny();
1646 let attn = MultiHeadAttention::new(&config);
1647 let mut lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1648
1649 let params = lora_attn.lora_params_mut();
1650 assert_eq!(params.len(), 8);
1651 }
1652
1653 #[test]
1654 fn test_mha_with_lora_param_count() {
1655 let config = TransformerConfig::tiny();
1656 let attn = MultiHeadAttention::new(&config);
1657 let rank = 4;
1658 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, 8.0);
1659
1660 let param_count = lora_attn.lora_param_count();
1661
1662 let hidden = config.hidden_size;
1664 let kv_hidden = config.num_kv_heads * config.head_dim();
1665 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);
1671 assert!(param_count > 0);
1672 }
1673
1674 #[test]
1675 fn test_mha_with_lora_longer_sequence() {
1676 let config = TransformerConfig::tiny();
1677 let attn = MultiHeadAttention::new(&config);
1678 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1679
1680 let seq_len = 8;
1681 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1682 let output = lora_attn.forward(&x, seq_len);
1683
1684 assert_eq!(output.len(), seq_len * config.hidden_size);
1685 }
1686
1687 #[test]
1688 fn test_parameters_mut() {
1689 let config = TransformerConfig::tiny();
1690 let mut attn = MultiHeadAttention::new(&config);
1691
1692 let params = attn.parameters_mut();
1693 assert_eq!(params.len(), 4);
1694 }
1695
1696 #[test]
1721 fn falsify_a1e_from_params_rejects_wrong_shape_q_weight() {
1722 let config = TransformerConfig::tiny();
1723 let hidden_size = config.hidden_size;
1724 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1725
1726 let mut params = HashMap::new();
1727 params.insert("attn.q_proj.weight".to_string(), Tensor::from_vec(vec![0.1; 50], true));
1729 params.insert(
1731 "attn.k_proj.weight".to_string(),
1732 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1733 );
1734 params.insert(
1735 "attn.v_proj.weight".to_string(),
1736 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1737 );
1738 params.insert(
1739 "attn.o_proj.weight".to_string(),
1740 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1741 );
1742
1743 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1744 assert!(
1746 attn.is_none(),
1747 "FALSIFY-A1e: PMAT-331 fix — from_params MUST reject wrong-shape q_proj"
1748 );
1749 }
1750
1751 #[test]
1756 fn falsify_a2e_gqa_init_correct_kv_dimensions() {
1757 let mut config = TransformerConfig::tiny();
1758 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1761 let head_dim = config.head_dim();
1762 let kv_hidden = config.num_kv_heads * head_dim; assert_eq!(
1766 attn.w_q.len(),
1767 config.hidden_size * config.hidden_size,
1768 "FALSIFY-A2e: Q projection must be hidden*hidden"
1769 );
1770
1771 assert_eq!(
1773 attn.w_k.len(),
1774 config.hidden_size * kv_hidden,
1775 "FALSIFY-A2e: K projection must use num_kv_heads, not num_heads"
1776 );
1777
1778 assert_eq!(
1780 attn.w_v.len(),
1781 config.hidden_size * kv_hidden,
1782 "FALSIFY-A2e: V projection must use num_kv_heads, not num_heads"
1783 );
1784
1785 assert_eq!(
1787 attn.w_o.len(),
1788 config.hidden_size * config.hidden_size,
1789 "FALSIFY-A2e: O projection must be hidden*hidden"
1790 );
1791
1792 assert!(
1794 attn.w_k.len() < attn.w_q.len(),
1795 "FALSIFY-A2e: For GQA, K weight must be smaller than Q weight"
1796 );
1797 }
1798
1799 #[test]
1804 fn falsify_a3e_gqa_forward_correct_output_dims() {
1805 let mut config = TransformerConfig::tiny();
1806 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1809 let seq_len = 3;
1810 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1811 let output = attn.forward(&x, seq_len);
1812
1813 assert_eq!(
1814 output.len(),
1815 seq_len * config.hidden_size,
1816 "FALSIFY-A3e: GQA output must be seq_len * hidden_size, not seq_len * kv_hidden"
1817 );
1818 }
1819
1820 #[test]
1824 fn falsify_a4e_init_produces_valid_attention_weights() {
1825 let config = TransformerConfig::tiny();
1826 let attn = MultiHeadAttention::new(&config);
1827
1828 for (name, w) in
1829 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1830 {
1831 let data = w.data();
1832 let slice = data.as_slice().expect("data as slice");
1833
1834 let nan_count = slice.iter().filter(|v| v.is_nan()).count();
1836 assert_eq!(nan_count, 0, "FALSIFY-A4e: {name} init must not contain NaN");
1837
1838 let inf_count = slice.iter().filter(|v| v.is_infinite()).count();
1840 assert_eq!(inf_count, 0, "FALSIFY-A4e: {name} init must not contain Inf");
1841
1842 let min = slice.iter().copied().fold(f32::INFINITY, f32::min);
1844 let max = slice.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1845 assert!(
1846 (max - min).abs() > 1e-6,
1847 "FALSIFY-A4e: {name} init values are constant ({min}..{max}) — degenerate weight"
1848 );
1849 }
1850 }
1851
1852 #[test]
1857 fn falsify_a5e_forward_produces_finite_output() {
1858 let config = TransformerConfig::tiny();
1859 let attn = MultiHeadAttention::new(&config);
1860 let seq_len = 4;
1861 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1862 let output = attn.forward(&x, seq_len);
1863
1864 let data = output.data();
1865 let nan_count = data.iter().filter(|v| v.is_nan()).count();
1866 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
1867 assert_eq!(nan_count, 0, "FALSIFY-A5e: Attention output must not contain NaN");
1868 assert_eq!(inf_count, 0, "FALSIFY-A5e: Attention output must not contain Inf");
1869 }
1870
1871 #[test]
1888 fn falsify_gq_001e_output_shape() {
1889 for (num_heads, num_kv_heads) in [(2, 2), (4, 2), (4, 1), (2, 1)] {
1890 let mut config = TransformerConfig::tiny();
1891 config.num_attention_heads = num_heads;
1892 config.num_kv_heads = num_kv_heads;
1893
1894 let attn = MultiHeadAttention::new(&config);
1895 let seq_len = 3;
1896 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1897 let output = attn.forward(&x, seq_len);
1898
1899 assert_eq!(
1900 output.len(),
1901 seq_len * config.hidden_size,
1902 "FALSIFIED GQ-001e: output len mismatch for heads={num_heads},kv={num_kv_heads}"
1903 );
1904 }
1905 }
1906
1907 #[test]
1909 fn falsify_gq_002e_mha_degeneration() {
1910 let config = TransformerConfig::tiny(); assert_eq!(config.num_attention_heads, config.num_kv_heads);
1912
1913 let attn = MultiHeadAttention::new(&config);
1914 let seq_len = 4;
1915 let x = Tensor::from_vec(
1916 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.37).sin()).collect(),
1917 true,
1918 );
1919 let output = attn.forward(&x, seq_len);
1920
1921 let data = output.data();
1922 for (i, v) in data.iter().enumerate() {
1923 assert!(v.is_finite(), "FALSIFIED GQ-002e: MHA output[{i}] = {v} (not finite)");
1924 }
1925 }
1926
1927 #[test]
1929 fn falsify_gq_004e_head_divisibility() {
1930 for (nh, nkv) in [(2, 1), (2, 2), (4, 1), (4, 2), (4, 4), (8, 2), (8, 4)] {
1932 let mut config = TransformerConfig::tiny();
1933 config.num_attention_heads = nh;
1934 config.num_kv_heads = nkv;
1935 assert_eq!(nh % nkv, 0, "FALSIFIED GQ-004e: test config has invalid head ratio");
1936 let attn = MultiHeadAttention::new(&config);
1938 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1939 let _ = attn.forward(&x, 2);
1940 }
1941 }
1942
1943 #[test]
1945 fn falsify_gq_006e_mqa_boundary() {
1946 let mut config = TransformerConfig::tiny();
1947 config.num_attention_heads = 4;
1948 config.num_kv_heads = 1;
1949 config.hidden_size = 64;
1951
1952 let attn = MultiHeadAttention::new(&config);
1953 let seq_len = 3;
1954 let x = Tensor::from_vec(
1955 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.73).cos()).collect(),
1956 true,
1957 );
1958 let output = attn.forward(&x, seq_len);
1959
1960 assert_eq!(
1961 output.len(),
1962 seq_len * config.hidden_size,
1963 "FALSIFIED GQ-006e: MQA output size wrong"
1964 );
1965
1966 let data = output.data();
1968 for (i, v) in data.iter().enumerate() {
1969 assert!(v.is_finite(), "FALSIFIED GQ-006e: MQA output[{i}] = {v} (not finite)");
1970 }
1971 }
1972
1973 mod gq_proptest_falsify {
1974 use super::*;
1975 use proptest::prelude::*;
1976
1977 proptest! {
1979 #![proptest_config(ProptestConfig::with_cases(50))]
1980
1981 #[test]
1982 fn falsify_gq_001e_prop_output_shape(
1983 config_idx in 0..4usize,
1984 seq_len in 2..=6usize,
1985 seed in 0..500u32,
1986 ) {
1987 let configs: [(usize, usize); 4] = [
1988 (2, 2), (2, 1), (4, 2), (4, 1),
1989 ];
1990 let (num_heads, num_kv_heads) = configs[config_idx];
1991 let mut config = TransformerConfig::tiny();
1992 config.num_attention_heads = num_heads;
1993 config.num_kv_heads = num_kv_heads;
1994
1995 let attn = MultiHeadAttention::new(&config);
1996 let data: Vec<f32> = (0..seq_len * config.hidden_size)
1997 .map(|i| ((i as f32 + seed as f32) * 0.37).sin())
1998 .collect();
1999 let x = Tensor::from_vec(data, true);
2000 let output = attn.forward(&x, seq_len);
2001
2002 prop_assert_eq!(
2003 output.len(),
2004 seq_len * config.hidden_size,
2005 "FALSIFIED GQ-001e-prop: output len mismatch"
2006 );
2007
2008 for v in output.data() {
2010 prop_assert!(
2011 v.is_finite(),
2012 "FALSIFIED GQ-001e-prop: non-finite output"
2013 );
2014 }
2015 }
2016 }
2017
2018 proptest! {
2020 #![proptest_config(ProptestConfig::with_cases(30))]
2021
2022 #[test]
2023 fn falsify_gq_006e_prop_mqa_boundary(
2024 seed in 0..500u32,
2025 seq_len in 2..=5usize,
2026 ) {
2027 let mut config = TransformerConfig::tiny();
2028 config.num_attention_heads = 4;
2029 config.num_kv_heads = 1;
2030 config.hidden_size = 64;
2031
2032 let attn = MultiHeadAttention::new(&config);
2033 let data: Vec<f32> = (0..seq_len * config.hidden_size)
2034 .map(|i| ((i as f32 + seed as f32) * 0.73).cos())
2035 .collect();
2036 let x = Tensor::from_vec(data, true);
2037 let output = attn.forward(&x, seq_len);
2038
2039 prop_assert_eq!(
2040 output.len(),
2041 seq_len * config.hidden_size,
2042 "FALSIFIED GQ-006e-prop: MQA output len mismatch"
2043 );
2044
2045 for v in output.data() {
2046 prop_assert!(
2047 v.is_finite(),
2048 "FALSIFIED GQ-006e-prop: non-finite MQA output"
2049 );
2050 }
2051 }
2052 }
2053 }
2054
2055 #[test]
2056 fn test_attention_from_params_with_biases() {
2057 let config = TransformerConfig::tiny();
2058 let hidden_size = config.hidden_size;
2059 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2060
2061 let mut params = HashMap::new();
2062 params.insert(
2063 "attn.q_proj.weight".to_string(),
2064 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2065 );
2066 params.insert(
2067 "attn.k_proj.weight".to_string(),
2068 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2069 );
2070 params.insert(
2071 "attn.v_proj.weight".to_string(),
2072 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2073 );
2074 params.insert(
2075 "attn.o_proj.weight".to_string(),
2076 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2077 );
2078 params.insert(
2079 "attn.q_proj.bias".to_string(),
2080 Tensor::from_vec(vec![0.01; hidden_size], true),
2081 );
2082 params.insert(
2083 "attn.k_proj.bias".to_string(),
2084 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2085 );
2086 params.insert(
2087 "attn.v_proj.bias".to_string(),
2088 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2089 );
2090
2091 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
2092 assert!(attn.is_some());
2093 let attn = attn.expect("should load with biases");
2094 assert!(attn.has_biases());
2095 assert_eq!(attn.parameters().len(), 7);
2096 }
2097
2098 #[test]
2099 fn test_attention_named_parameters_with_biases() {
2100 let config = TransformerConfig::tiny();
2101 let hidden_size = config.hidden_size;
2102 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2103
2104 let mut params = HashMap::new();
2105 params.insert(
2106 "attn.q_proj.weight".to_string(),
2107 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2108 );
2109 params.insert(
2110 "attn.k_proj.weight".to_string(),
2111 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2112 );
2113 params.insert(
2114 "attn.v_proj.weight".to_string(),
2115 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2116 );
2117 params.insert(
2118 "attn.o_proj.weight".to_string(),
2119 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2120 );
2121 params.insert(
2122 "attn.q_proj.bias".to_string(),
2123 Tensor::from_vec(vec![0.01; hidden_size], true),
2124 );
2125 params.insert(
2126 "attn.k_proj.bias".to_string(),
2127 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2128 );
2129 params.insert(
2130 "attn.v_proj.bias".to_string(),
2131 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2132 );
2133
2134 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
2135 let named = attn.named_parameters("attn");
2136 assert_eq!(named.len(), 7);
2137 let names: Vec<&str> = named.iter().map(|(n, _)| n.as_str()).collect();
2138 assert!(names.contains(&"attn.q_proj.bias"));
2139 assert!(names.contains(&"attn.k_proj.bias"));
2140 assert!(names.contains(&"attn.v_proj.bias"));
2141 }
2142
2143 #[test]
2144 fn test_attention_forward_with_biases() {
2145 let config = TransformerConfig::tiny();
2146 let hidden_size = config.hidden_size;
2147 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2148
2149 let mut params = HashMap::new();
2150 params.insert(
2151 "attn.q_proj.weight".to_string(),
2152 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2153 );
2154 params.insert(
2155 "attn.k_proj.weight".to_string(),
2156 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2157 );
2158 params.insert(
2159 "attn.v_proj.weight".to_string(),
2160 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2161 );
2162 params.insert(
2163 "attn.o_proj.weight".to_string(),
2164 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2165 );
2166 params
2167 .insert("attn.q_proj.bias".to_string(), Tensor::from_vec(vec![0.5; hidden_size], true));
2168 params.insert(
2169 "attn.k_proj.bias".to_string(),
2170 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2171 );
2172 params.insert(
2173 "attn.v_proj.bias".to_string(),
2174 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2175 );
2176
2177 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
2178 let x = Tensor::from_vec(vec![0.1; 2 * hidden_size], false);
2179 let output = attn.forward(&x, 2);
2180 assert_eq!(output.len(), 2 * hidden_size);
2181 assert!(output.data().iter().all(|v| v.is_finite()));
2182 }
2183}