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" => {
928 if self.b_q.is_some() {
929 self.b_q = Some(value);
930 true
931 } else {
932 false
933 }
934 }
935 "self_attn.k_proj.bias" => {
936 if self.b_k.is_some() {
937 self.b_k = Some(value);
938 true
939 } else {
940 false
941 }
942 }
943 "self_attn.v_proj.bias" => {
944 if self.b_v.is_some() {
945 self.b_v = Some(value);
946 true
947 } else {
948 false
949 }
950 }
951 _ => false,
952 }
953 }
954}
955
956pub struct LoRAProjection {
961 pub base_weight: Tensor,
963 pub lora_a: Tensor,
965 pub lora_b: Tensor,
967 pub d_in: usize,
969 pub d_out: usize,
971 pub rank: usize,
973 pub scale: f32,
975}
976
977impl LoRAProjection {
978 pub fn new(base_weight: Tensor, d_in: usize, d_out: usize, rank: usize, alpha: f32) -> Self {
987 assert_eq!(base_weight.len(), d_in * d_out, "Base weight size mismatch");
988
989 let mut base_weight = base_weight;
991 base_weight.set_requires_grad(false);
992
993 let lora_a = Tensor::from_vec(
995 (0..d_in * rank).map(|i| (i as f32 * 0.123).sin() * 0.01).collect(),
996 true, );
998
999 let lora_b = Tensor::zeros(rank * d_out, true);
1001
1002 Self { base_weight, lora_a, lora_b, d_in, d_out, rank, scale: alpha / rank as f32 }
1003 }
1004
1005 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
1016 let base_out = matmul(x, &self.base_weight, seq_len, self.d_in, self.d_out);
1018
1019 let lora_intermediate = matmul(x, &self.lora_a, seq_len, self.d_in, self.rank);
1022
1023 let lora_out = matmul(&lora_intermediate, &self.lora_b, seq_len, self.rank, self.d_out);
1025
1026 crate::autograd::add_scaled(&base_out, &lora_out, self.scale)
1029 }
1030
1031 pub fn lora_params(&self) -> Vec<&Tensor> {
1033 vec![&self.lora_a, &self.lora_b]
1034 }
1035
1036 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
1038 vec![&mut self.lora_a, &mut self.lora_b]
1039 }
1040}
1041
1042pub struct MultiHeadAttentionWithLoRA {
1046 pub config: TransformerConfig,
1048 pub q_proj: LoRAProjection,
1050 pub k_proj: LoRAProjection,
1052 pub v_proj: LoRAProjection,
1054 pub o_proj: LoRAProjection,
1056}
1057
1058impl MultiHeadAttentionWithLoRA {
1059 pub fn from_attention(attn: &MultiHeadAttention, rank: usize, alpha: f32) -> Self {
1066 let hidden_size = attn.config.hidden_size;
1067 let q_dim = attn.config.q_dim();
1068 let kv_hidden_size = attn.config.num_kv_heads * attn.config.head_dim();
1069
1070 Self {
1071 config: attn.config.clone(),
1072 q_proj: LoRAProjection::new(attn.w_q.clone(), hidden_size, q_dim, rank, alpha),
1073 k_proj: LoRAProjection::new(attn.w_k.clone(), hidden_size, kv_hidden_size, rank, alpha),
1074 v_proj: LoRAProjection::new(attn.w_v.clone(), hidden_size, kv_hidden_size, rank, alpha),
1075 o_proj: LoRAProjection::new(attn.w_o.clone(), q_dim, hidden_size, rank, alpha),
1076 }
1077 }
1078
1079 pub fn forward(&self, x: &Tensor, seq_len: usize) -> Tensor {
1083 let num_heads = self.config.num_attention_heads;
1084 let num_kv_heads = self.config.num_kv_heads;
1085 let head_dim = self.config.head_dim();
1086 let q_dim = self.config.q_dim();
1087 let kv_hidden_size = num_kv_heads * head_dim;
1088
1089 let q = self.q_proj.forward(x, seq_len);
1091 let k = self.k_proj.forward(x, seq_len);
1092 let v = self.v_proj.forward(x, seq_len);
1093
1094 let mut attn_outputs = Vec::with_capacity(num_heads * seq_len * head_dim);
1096 let heads_per_kv = num_heads / num_kv_heads;
1097
1098 let q_data = q.data();
1100 let q_slice = q_data.as_slice().expect("contiguous Q tensor");
1101 let k_data = k.data();
1102 let k_slice = k_data.as_slice().expect("contiguous K tensor");
1103 let v_data = v.data();
1104 let v_slice = v_data.as_slice().expect("contiguous V tensor");
1105
1106 for h in 0..num_heads {
1107 let kv_h = h / heads_per_kv;
1108
1109 let mut q_head = Vec::with_capacity(seq_len * head_dim);
1111 for s in 0..seq_len {
1112 let start = s * q_dim + h * head_dim;
1113 q_head.extend_from_slice(&q_slice[start..start + head_dim]);
1114 }
1115
1116 let mut k_head = Vec::with_capacity(seq_len * head_dim);
1117 for s in 0..seq_len {
1118 let start = s * kv_hidden_size + kv_h * head_dim;
1119 k_head.extend_from_slice(&k_slice[start..start + head_dim]);
1120 }
1121
1122 let mut v_head = Vec::with_capacity(seq_len * head_dim);
1123 for s in 0..seq_len {
1124 let start = s * kv_hidden_size + kv_h * head_dim;
1125 v_head.extend_from_slice(&v_slice[start..start + head_dim]);
1126 }
1127
1128 let q_tensor = Tensor::from_vec(q_head, false);
1130 let k_tensor = Tensor::from_vec(k_head, false);
1131 let v_tensor = Tensor::from_vec(v_head, false);
1132
1133 let attn_out = if self.config.architecture == ModelArchitecture::Decoder {
1139 crate::autograd::attention_causal(
1140 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
1141 )
1142 } else {
1143 crate::autograd::attention(
1144 &q_tensor, &k_tensor, &v_tensor, seq_len, head_dim, seq_len, head_dim,
1145 )
1146 };
1147
1148 attn_outputs.extend_from_slice(
1149 attn_out.data().as_slice().expect("contiguous attention output"),
1150 );
1151 }
1152
1153 let mut concat_output = vec![0.0; seq_len * q_dim];
1155 for h in 0..num_heads {
1156 for s in 0..seq_len {
1157 let src_idx = h * seq_len * head_dim + s * head_dim;
1158 let dst_idx = s * q_dim + h * head_dim;
1159 concat_output[dst_idx..dst_idx + head_dim]
1160 .copy_from_slice(&attn_outputs[src_idx..src_idx + head_dim]);
1161 }
1162 }
1163
1164 let concat_tensor = Tensor::from_vec(concat_output, true);
1165
1166 self.o_proj.forward(&concat_tensor, seq_len)
1168 }
1169
1170 pub fn lora_params(&self) -> Vec<&Tensor> {
1172 let mut params = Vec::new();
1173 params.extend(self.q_proj.lora_params());
1174 params.extend(self.k_proj.lora_params());
1175 params.extend(self.v_proj.lora_params());
1176 params.extend(self.o_proj.lora_params());
1177 params
1178 }
1179
1180 pub fn lora_params_mut(&mut self) -> Vec<&mut Tensor> {
1182 let mut params = Vec::new();
1183 params.extend(self.q_proj.lora_params_mut());
1184 params.extend(self.k_proj.lora_params_mut());
1185 params.extend(self.v_proj.lora_params_mut());
1186 params.extend(self.o_proj.lora_params_mut());
1187 params
1188 }
1189
1190 pub fn lora_param_count(&self) -> usize {
1192 let hidden = self.config.hidden_size;
1194 let kv_hidden = self.config.num_kv_heads * self.config.head_dim();
1195 let rank = self.q_proj.rank;
1196
1197 (hidden * rank + rank * hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * kv_hidden) + (hidden * rank + rank * hidden) }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211
1212 #[test]
1228 fn beat_qlora_composed_forward_equivalence() {
1229 let mut config = TransformerConfig::tiny();
1230 config.use_bias = true;
1231 let hidden = config.hidden_size;
1232 let q_dim = config.q_dim();
1233 let kv_dim = config.num_kv_heads * config.head_dim();
1234 let seq = 3usize;
1235 let rank = 4usize;
1236 let scale = 8.0f32 / rank as f32; let mut attn = MultiHeadAttention::new(&config);
1239
1240 let mk_bias = |n: usize, amp: f32| {
1243 Tensor::from_vec((0..n).map(|i| amp * (((i % 7) as f32) - 3.0)).collect(), true)
1244 };
1245 attn.b_q = Some(mk_bias(q_dim, 0.05));
1246 attn.b_k = Some(mk_bias(kv_dim, 0.03));
1247 attn.b_v = Some(mk_bias(kv_dim, 0.04));
1248
1249 let mk = |rows: usize, cols: usize, amp: f32, ph: f32| -> Vec<f32> {
1252 (0..rows * cols).map(|i| amp * ((i as f32).mul_add(0.017, ph)).sin()).collect()
1253 };
1254 let a_q = mk(rank, hidden, 0.10, 0.0);
1255 let b_q = mk(q_dim, rank, 0.12, 1.0);
1256 let a_v = mk(rank, hidden, 0.09, 2.0);
1257 let b_v = mk(kv_dim, rank, 0.11, 3.0);
1258
1259 let x = Tensor::from_vec(
1260 (0..seq * hidden).map(|i| (i as f32).mul_add(0.023, -0.5).cos() * 0.4).collect(),
1261 true,
1262 );
1263
1264 let out_lora = attn.forward_with_lora(
1266 &x,
1267 seq,
1268 &Tensor::from_vec(a_q.clone(), true),
1269 &Tensor::from_vec(b_q.clone(), true),
1270 &Tensor::from_vec(a_v.clone(), true),
1271 &Tensor::from_vec(b_v.clone(), true),
1272 rank,
1273 scale,
1274 );
1275
1276 let merge = |w: &Tensor, a: &[f32], b: &[f32], out: usize| -> Vec<f32> {
1279 let wd = w.data();
1280 let mut m = wd.as_slice().expect("contiguous w").to_vec();
1281 for o in 0..out {
1282 for i in 0..hidden {
1283 let mut d = 0.0f32;
1284 for k in 0..rank {
1285 d += b[o * rank + k] * a[k * hidden + i];
1286 }
1287 m[o * hidden + i] += scale * d;
1288 }
1289 }
1290 m
1291 };
1292 attn.w_q = Tensor::from_vec(merge(&attn.w_q, &a_q, &b_q, q_dim), true);
1293 attn.w_v = Tensor::from_vec(merge(&attn.w_v, &a_v, &b_v, kv_dim), true);
1294 let out_merged = attn.forward(&x, seq);
1295
1296 let ld = out_lora.data();
1297 let ls = ld.as_slice().expect("contiguous lora out");
1298 let md = out_merged.data();
1299 let ms = md.as_slice().expect("contiguous merged out");
1300 assert_eq!(ls.len(), ms.len(), "output shape mismatch");
1301 let max_abs = ls.iter().zip(ms).map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
1302 assert!(
1303 max_abs < 1e-4,
1304 "FALSIFY-QLORA-COMPOSED-FORWARD-001: composed forward_with_lora (base + \
1305 scale·B@A + bias) diverges from the LoRA-merged forward by max|Δ|={max_abs:.6} \
1306 — a dropped Q/K/V bias, a wrong LoRA scale, or a transpose in the composition \
1307 (the #2260 bias-drop class, now with nonzero LoRA + biases)."
1308 );
1309 println!(
1310 "BEAT-QLORA-COMPOSED-FORWARD: forward_with_lora ≡ merged forward — max|Δ|={max_abs:.2e}"
1311 );
1312 }
1313
1314 #[test]
1318 fn falsify_pmat805_rope_backward_matches_finite_difference() {
1319 let seq_len = 3usize;
1320 let num_heads = 2usize;
1321 let head_dim = 4usize; let total = seq_len * num_heads * head_dim;
1323 let theta = 10000.0f32;
1324
1325 let x_data: Vec<f32> = (0..total).map(|i| ((i as f32 * 0.37).sin() * 1.5) + 0.1).collect();
1327 let w: Vec<f32> = (0..total).map(|i| ((i as f32 * 0.21).cos() * 0.8) - 0.05).collect();
1328
1329 let x = Tensor::from_vec(x_data.clone(), true);
1331 let y = apply_rope(&x, seq_len, num_heads, head_dim, theta);
1332 y.set_grad(Array1::from(w.clone()));
1333 y.backward_op().expect("rope must have a backward op").backward();
1334 let analytical = x.grad().expect("x must have grad after rope backward").to_vec();
1335
1336 let h = 1e-3f32;
1338 let loss = |xv: &[f32]| -> f32 {
1339 let t = Tensor::from_vec(xv.to_vec(), false);
1340 let r = apply_rope(&t, seq_len, num_heads, head_dim, theta);
1341 r.data().iter().zip(&w).map(|(a, b)| a * b).sum()
1342 };
1343 for j in 0..total {
1344 let mut xp = x_data.clone();
1345 let mut xm = x_data.clone();
1346 xp[j] += h;
1347 xm[j] -= h;
1348 let numerical = (loss(&xp) - loss(&xm)) / (2.0 * h);
1349 let diff = (analytical[j] - numerical).abs();
1350 assert!(
1351 diff < 1e-2,
1352 "FALSIFIED: RoPE grad[{j}] analytical={} numerical={} diff={diff}",
1353 analytical[j],
1354 numerical
1355 );
1356 }
1357 }
1358
1359 #[test]
1360 fn test_multi_head_attention_tiny() {
1361 let config = TransformerConfig::tiny();
1362 let attn = MultiHeadAttention::new(&config);
1363 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1364 let output = attn.forward(&x, 2);
1365 assert_eq!(output.len(), 2 * config.hidden_size);
1366 }
1367
1368 #[test]
1369 fn test_multi_head_attention_parameters() {
1370 let config = TransformerConfig::tiny();
1371 let attn = MultiHeadAttention::new(&config);
1372 let params = attn.parameters();
1373 assert_eq!(params.len(), 4); }
1375
1376 #[test]
1377 fn test_attention_longer_sequence() {
1378 let config = TransformerConfig::tiny();
1379 let attn = MultiHeadAttention::new(&config);
1380 let x = Tensor::from_vec(vec![0.1; 8 * config.hidden_size], true);
1381 let output = attn.forward(&x, 8);
1382 assert_eq!(output.len(), 8 * config.hidden_size);
1383 }
1384
1385 #[test]
1386 fn test_attention_weight_sizes() {
1387 let config = TransformerConfig::tiny();
1388 let attn = MultiHeadAttention::new(&config);
1389 let kv_hidden = config.num_kv_heads * config.head_dim();
1390 assert_eq!(attn.w_q.len(), config.hidden_size * config.hidden_size);
1391 assert_eq!(attn.w_k.len(), config.hidden_size * kv_hidden);
1392 assert_eq!(attn.w_v.len(), config.hidden_size * kv_hidden);
1393 assert_eq!(attn.w_o.len(), config.hidden_size * config.hidden_size);
1394 }
1395
1396 #[test]
1397 fn test_multi_head_attention_from_params_success() {
1398 let config = TransformerConfig::tiny();
1399 let hidden_size = config.hidden_size;
1400 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1401
1402 let mut params = HashMap::new();
1403 params.insert(
1404 "attn.q_proj.weight".to_string(),
1405 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1406 );
1407 params.insert(
1408 "attn.k_proj.weight".to_string(),
1409 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1410 );
1411 params.insert(
1412 "attn.v_proj.weight".to_string(),
1413 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1414 );
1415 params.insert(
1416 "attn.o_proj.weight".to_string(),
1417 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1418 );
1419
1420 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1421 assert!(attn.is_some());
1422 let attn = attn.expect("operation should succeed");
1423 assert_eq!(attn.w_q.len(), hidden_size * hidden_size);
1424 }
1425
1426 #[test]
1427 fn test_multi_head_attention_from_params_missing_key() {
1428 let config = TransformerConfig::tiny();
1429 let hidden_size = config.hidden_size;
1430
1431 let mut params = HashMap::new();
1432 params.insert(
1433 "attn.q_proj.weight".to_string(),
1434 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1435 );
1436 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1439 assert!(attn.is_none());
1440 }
1441
1442 #[test]
1443 fn test_attention_projections_backward() {
1444 let config = TransformerConfig::tiny();
1447 let attn = MultiHeadAttention::new(&config);
1448 let hidden_size = config.hidden_size;
1449 let seq_len = 2;
1450
1451 let x = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1452
1453 let mut q = crate::autograd::matmul(&x, &attn.w_q, seq_len, hidden_size, hidden_size);
1455 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1456 crate::autograd::backward(&mut q, Some(grad_out));
1457
1458 assert!(attn.w_q.grad().is_some());
1459 let grad_q = attn.w_q.grad().expect("gradient should be available");
1460 assert!(grad_q.iter().all(|&v| v.is_finite()));
1461 }
1462
1463 #[test]
1464 fn test_output_projection_backward() {
1465 let config = TransformerConfig::tiny();
1467 let attn = MultiHeadAttention::new(&config);
1468 let hidden_size = config.hidden_size;
1469 let seq_len = 2;
1470
1471 let concat_out = Tensor::from_vec(vec![0.1; seq_len * hidden_size], true);
1473
1474 let mut output =
1476 crate::autograd::matmul(&concat_out, &attn.w_o, seq_len, hidden_size, hidden_size);
1477
1478 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1479 crate::autograd::backward(&mut output, Some(grad_out));
1480
1481 assert!(attn.w_o.grad().is_some());
1482 let grad_o = attn.w_o.grad().expect("gradient should be available");
1483 assert!(grad_o.iter().all(|&v| v.is_finite()));
1484 let sum: f32 = grad_o.iter().map(|v| v.abs()).sum();
1485 assert!(sum > 0.0, "Output projection gradient should not be all zero");
1486 }
1487
1488 #[test]
1494 #[ignore = "apply_rope() severs autograd chain — needs backward op (ENT-272)"]
1495 fn test_attention_full_forward_qkv_gradients() {
1496 let config = TransformerConfig::tiny();
1497 let attn = MultiHeadAttention::new(&config);
1498 let hidden_size = config.hidden_size;
1499 let seq_len = 3;
1500
1501 let x_data: Vec<f32> =
1504 (0..seq_len * hidden_size).map(|i| ((i as f32) * 0.17).sin() * 0.5).collect();
1505 let x = Tensor::from_vec(x_data, true);
1506 let mut output = attn.forward(&x, seq_len);
1507
1508 let grad_out = ndarray::Array1::ones(seq_len * hidden_size);
1509 crate::autograd::backward(&mut output, Some(grad_out));
1510
1511 for (name, param) in
1513 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1514 {
1515 assert!(
1516 param.grad().is_some(),
1517 "ALB-038: {name} must have gradient after full attention forward"
1518 );
1519 let grad = param.grad().expect("gradient available");
1520 assert!(grad.iter().all(|&v| v.is_finite()), "ALB-038: {name} gradient must be finite");
1521 assert!(
1522 grad.iter().any(|&v| v.abs() > 1e-10),
1523 "ALB-038: {name} gradient must be non-zero"
1524 );
1525 }
1526
1527 assert!(x.grad().is_some(), "ALB-038: input x must have gradient");
1529 }
1530
1531 #[test]
1536 fn test_lora_projection_new() {
1537 let d_in = 32;
1538 let d_out = 16;
1539 let rank = 4;
1540 let alpha = 8.0;
1541
1542 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1543 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1544
1545 assert_eq!(lora.d_in, d_in);
1546 assert_eq!(lora.d_out, d_out);
1547 assert_eq!(lora.rank, rank);
1548 assert!((lora.scale - 2.0).abs() < 1e-6); assert_eq!(lora.lora_a.len(), d_in * rank);
1550 assert_eq!(lora.lora_b.len(), rank * d_out);
1551 }
1552
1553 #[test]
1554 fn test_lora_projection_forward() {
1555 let d_in = 32;
1556 let d_out = 16;
1557 let rank = 4;
1558 let alpha = 8.0;
1559 let seq_len = 2;
1560
1561 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1562 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, alpha);
1563
1564 let x = Tensor::from_vec(vec![0.1; seq_len * d_in], false);
1565 let output = lora.forward(&x, seq_len);
1566
1567 assert_eq!(output.len(), seq_len * d_out);
1568 assert!(output.data().iter().all(|&v| v.is_finite()));
1570 }
1571
1572 #[test]
1573 fn test_lora_projection_params() {
1574 let d_in = 32;
1575 let d_out = 16;
1576 let rank = 4;
1577
1578 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1579 let lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1580
1581 let params = lora.lora_params();
1582 assert_eq!(params.len(), 2); }
1584
1585 #[test]
1586 fn test_lora_projection_params_mut() {
1587 let d_in = 32;
1588 let d_out = 16;
1589 let rank = 4;
1590
1591 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out], false);
1592 let mut lora = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1593
1594 let params = lora.lora_params_mut();
1595 assert_eq!(params.len(), 2);
1596 }
1597
1598 #[test]
1599 #[should_panic(expected = "Base weight size mismatch")]
1600 fn test_lora_projection_size_mismatch() {
1601 let d_in = 32;
1602 let d_out = 16;
1603 let rank = 4;
1604
1605 let base_weight = Tensor::from_vec(vec![0.1; d_in * d_out + 1], false);
1607 let _ = LoRAProjection::new(base_weight, d_in, d_out, rank, 8.0);
1608 }
1609
1610 #[test]
1615 fn test_mha_with_lora_creation() {
1616 let config = TransformerConfig::tiny();
1617 let attn = MultiHeadAttention::new(&config);
1618 let rank = 4;
1619 let alpha = 8.0;
1620
1621 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, alpha);
1622
1623 assert_eq!(lora_attn.q_proj.rank, rank);
1624 assert_eq!(lora_attn.k_proj.rank, rank);
1625 assert_eq!(lora_attn.v_proj.rank, rank);
1626 assert_eq!(lora_attn.o_proj.rank, rank);
1627 }
1628
1629 #[test]
1630 fn test_mha_with_lora_forward() {
1631 let config = TransformerConfig::tiny();
1632 let attn = MultiHeadAttention::new(&config);
1633 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1634
1635 let seq_len = 2;
1636 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1637 let output = lora_attn.forward(&x, seq_len);
1638
1639 assert_eq!(output.len(), seq_len * config.hidden_size);
1640 assert!(output.data().iter().all(|&v| v.is_finite()));
1642 }
1643
1644 #[test]
1645 fn test_mha_with_lora_params() {
1646 let config = TransformerConfig::tiny();
1647 let attn = MultiHeadAttention::new(&config);
1648 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1649
1650 let params = lora_attn.lora_params();
1651 assert_eq!(params.len(), 8);
1653 }
1654
1655 #[test]
1656 fn test_mha_with_lora_params_mut() {
1657 let config = TransformerConfig::tiny();
1658 let attn = MultiHeadAttention::new(&config);
1659 let mut lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1660
1661 let params = lora_attn.lora_params_mut();
1662 assert_eq!(params.len(), 8);
1663 }
1664
1665 #[test]
1666 fn test_mha_with_lora_param_count() {
1667 let config = TransformerConfig::tiny();
1668 let attn = MultiHeadAttention::new(&config);
1669 let rank = 4;
1670 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, rank, 8.0);
1671
1672 let param_count = lora_attn.lora_param_count();
1673
1674 let hidden = config.hidden_size;
1676 let kv_hidden = config.num_kv_heads * config.head_dim();
1677 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);
1683 assert!(param_count > 0);
1684 }
1685
1686 #[test]
1687 fn test_mha_with_lora_longer_sequence() {
1688 let config = TransformerConfig::tiny();
1689 let attn = MultiHeadAttention::new(&config);
1690 let lora_attn = MultiHeadAttentionWithLoRA::from_attention(&attn, 4, 8.0);
1691
1692 let seq_len = 8;
1693 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], false);
1694 let output = lora_attn.forward(&x, seq_len);
1695
1696 assert_eq!(output.len(), seq_len * config.hidden_size);
1697 }
1698
1699 #[test]
1700 fn test_parameters_mut() {
1701 let config = TransformerConfig::tiny();
1702 let mut attn = MultiHeadAttention::new(&config);
1703
1704 let params = attn.parameters_mut();
1705 assert_eq!(params.len(), 4);
1706 }
1707
1708 #[test]
1733 fn falsify_a1e_from_params_rejects_wrong_shape_q_weight() {
1734 let config = TransformerConfig::tiny();
1735 let hidden_size = config.hidden_size;
1736 let kv_hidden_size = config.num_kv_heads * config.head_dim();
1737
1738 let mut params = HashMap::new();
1739 params.insert("attn.q_proj.weight".to_string(), Tensor::from_vec(vec![0.1; 50], true));
1741 params.insert(
1743 "attn.k_proj.weight".to_string(),
1744 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1745 );
1746 params.insert(
1747 "attn.v_proj.weight".to_string(),
1748 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
1749 );
1750 params.insert(
1751 "attn.o_proj.weight".to_string(),
1752 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
1753 );
1754
1755 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
1756 assert!(
1758 attn.is_none(),
1759 "FALSIFY-A1e: PMAT-331 fix — from_params MUST reject wrong-shape q_proj"
1760 );
1761 }
1762
1763 #[test]
1768 fn falsify_a2e_gqa_init_correct_kv_dimensions() {
1769 let mut config = TransformerConfig::tiny();
1770 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1773 let head_dim = config.head_dim();
1774 let kv_hidden = config.num_kv_heads * head_dim; assert_eq!(
1778 attn.w_q.len(),
1779 config.hidden_size * config.hidden_size,
1780 "FALSIFY-A2e: Q projection must be hidden*hidden"
1781 );
1782
1783 assert_eq!(
1785 attn.w_k.len(),
1786 config.hidden_size * kv_hidden,
1787 "FALSIFY-A2e: K projection must use num_kv_heads, not num_heads"
1788 );
1789
1790 assert_eq!(
1792 attn.w_v.len(),
1793 config.hidden_size * kv_hidden,
1794 "FALSIFY-A2e: V projection must use num_kv_heads, not num_heads"
1795 );
1796
1797 assert_eq!(
1799 attn.w_o.len(),
1800 config.hidden_size * config.hidden_size,
1801 "FALSIFY-A2e: O projection must be hidden*hidden"
1802 );
1803
1804 assert!(
1806 attn.w_k.len() < attn.w_q.len(),
1807 "FALSIFY-A2e: For GQA, K weight must be smaller than Q weight"
1808 );
1809 }
1810
1811 #[test]
1816 fn falsify_a3e_gqa_forward_correct_output_dims() {
1817 let mut config = TransformerConfig::tiny();
1818 config.num_kv_heads = 1; let attn = MultiHeadAttention::new(&config);
1821 let seq_len = 3;
1822 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1823 let output = attn.forward(&x, seq_len);
1824
1825 assert_eq!(
1826 output.len(),
1827 seq_len * config.hidden_size,
1828 "FALSIFY-A3e: GQA output must be seq_len * hidden_size, not seq_len * kv_hidden"
1829 );
1830 }
1831
1832 #[test]
1836 fn falsify_a4e_init_produces_valid_attention_weights() {
1837 let config = TransformerConfig::tiny();
1838 let attn = MultiHeadAttention::new(&config);
1839
1840 for (name, w) in
1841 [("w_q", &attn.w_q), ("w_k", &attn.w_k), ("w_v", &attn.w_v), ("w_o", &attn.w_o)]
1842 {
1843 let data = w.data();
1844 let slice = data.as_slice().expect("data as slice");
1845
1846 let nan_count = slice.iter().filter(|v| v.is_nan()).count();
1848 assert_eq!(nan_count, 0, "FALSIFY-A4e: {name} init must not contain NaN");
1849
1850 let inf_count = slice.iter().filter(|v| v.is_infinite()).count();
1852 assert_eq!(inf_count, 0, "FALSIFY-A4e: {name} init must not contain Inf");
1853
1854 let min = slice.iter().copied().fold(f32::INFINITY, f32::min);
1856 let max = slice.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1857 assert!(
1858 (max - min).abs() > 1e-6,
1859 "FALSIFY-A4e: {name} init values are constant ({min}..{max}) — degenerate weight"
1860 );
1861 }
1862 }
1863
1864 #[test]
1869 fn falsify_a5e_forward_produces_finite_output() {
1870 let config = TransformerConfig::tiny();
1871 let attn = MultiHeadAttention::new(&config);
1872 let seq_len = 4;
1873 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1874 let output = attn.forward(&x, seq_len);
1875
1876 let data = output.data();
1877 let nan_count = data.iter().filter(|v| v.is_nan()).count();
1878 let inf_count = data.iter().filter(|v| v.is_infinite()).count();
1879 assert_eq!(nan_count, 0, "FALSIFY-A5e: Attention output must not contain NaN");
1880 assert_eq!(inf_count, 0, "FALSIFY-A5e: Attention output must not contain Inf");
1881 }
1882
1883 #[test]
1900 fn falsify_gq_001e_output_shape() {
1901 for (num_heads, num_kv_heads) in [(2, 2), (4, 2), (4, 1), (2, 1)] {
1902 let mut config = TransformerConfig::tiny();
1903 config.num_attention_heads = num_heads;
1904 config.num_kv_heads = num_kv_heads;
1905
1906 let attn = MultiHeadAttention::new(&config);
1907 let seq_len = 3;
1908 let x = Tensor::from_vec(vec![0.1; seq_len * config.hidden_size], true);
1909 let output = attn.forward(&x, seq_len);
1910
1911 assert_eq!(
1912 output.len(),
1913 seq_len * config.hidden_size,
1914 "FALSIFIED GQ-001e: output len mismatch for heads={num_heads},kv={num_kv_heads}"
1915 );
1916 }
1917 }
1918
1919 #[test]
1921 fn falsify_gq_002e_mha_degeneration() {
1922 let config = TransformerConfig::tiny(); assert_eq!(config.num_attention_heads, config.num_kv_heads);
1924
1925 let attn = MultiHeadAttention::new(&config);
1926 let seq_len = 4;
1927 let x = Tensor::from_vec(
1928 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.37).sin()).collect(),
1929 true,
1930 );
1931 let output = attn.forward(&x, seq_len);
1932
1933 let data = output.data();
1934 for (i, v) in data.iter().enumerate() {
1935 assert!(v.is_finite(), "FALSIFIED GQ-002e: MHA output[{i}] = {v} (not finite)");
1936 }
1937 }
1938
1939 #[test]
1941 fn falsify_gq_004e_head_divisibility() {
1942 for (nh, nkv) in [(2, 1), (2, 2), (4, 1), (4, 2), (4, 4), (8, 2), (8, 4)] {
1944 let mut config = TransformerConfig::tiny();
1945 config.num_attention_heads = nh;
1946 config.num_kv_heads = nkv;
1947 assert_eq!(nh % nkv, 0, "FALSIFIED GQ-004e: test config has invalid head ratio");
1948 let attn = MultiHeadAttention::new(&config);
1950 let x = Tensor::from_vec(vec![0.1; 2 * config.hidden_size], true);
1951 let _ = attn.forward(&x, 2);
1952 }
1953 }
1954
1955 #[test]
1957 fn falsify_gq_006e_mqa_boundary() {
1958 let mut config = TransformerConfig::tiny();
1959 config.num_attention_heads = 4;
1960 config.num_kv_heads = 1;
1961 config.hidden_size = 64;
1963
1964 let attn = MultiHeadAttention::new(&config);
1965 let seq_len = 3;
1966 let x = Tensor::from_vec(
1967 (0..seq_len * config.hidden_size).map(|i| (i as f32 * 0.73).cos()).collect(),
1968 true,
1969 );
1970 let output = attn.forward(&x, seq_len);
1971
1972 assert_eq!(
1973 output.len(),
1974 seq_len * config.hidden_size,
1975 "FALSIFIED GQ-006e: MQA output size wrong"
1976 );
1977
1978 let data = output.data();
1980 for (i, v) in data.iter().enumerate() {
1981 assert!(v.is_finite(), "FALSIFIED GQ-006e: MQA output[{i}] = {v} (not finite)");
1982 }
1983 }
1984
1985 mod gq_proptest_falsify {
1986 use super::*;
1987 use proptest::prelude::*;
1988
1989 proptest! {
1991 #![proptest_config(ProptestConfig::with_cases(50))]
1992
1993 #[test]
1994 fn falsify_gq_001e_prop_output_shape(
1995 config_idx in 0..4usize,
1996 seq_len in 2..=6usize,
1997 seed in 0..500u32,
1998 ) {
1999 let configs: [(usize, usize); 4] = [
2000 (2, 2), (2, 1), (4, 2), (4, 1),
2001 ];
2002 let (num_heads, num_kv_heads) = configs[config_idx];
2003 let mut config = TransformerConfig::tiny();
2004 config.num_attention_heads = num_heads;
2005 config.num_kv_heads = num_kv_heads;
2006
2007 let attn = MultiHeadAttention::new(&config);
2008 let data: Vec<f32> = (0..seq_len * config.hidden_size)
2009 .map(|i| ((i as f32 + seed as f32) * 0.37).sin())
2010 .collect();
2011 let x = Tensor::from_vec(data, true);
2012 let output = attn.forward(&x, seq_len);
2013
2014 prop_assert_eq!(
2015 output.len(),
2016 seq_len * config.hidden_size,
2017 "FALSIFIED GQ-001e-prop: output len mismatch"
2018 );
2019
2020 for v in output.data() {
2022 prop_assert!(
2023 v.is_finite(),
2024 "FALSIFIED GQ-001e-prop: non-finite output"
2025 );
2026 }
2027 }
2028 }
2029
2030 proptest! {
2032 #![proptest_config(ProptestConfig::with_cases(30))]
2033
2034 #[test]
2035 fn falsify_gq_006e_prop_mqa_boundary(
2036 seed in 0..500u32,
2037 seq_len in 2..=5usize,
2038 ) {
2039 let mut config = TransformerConfig::tiny();
2040 config.num_attention_heads = 4;
2041 config.num_kv_heads = 1;
2042 config.hidden_size = 64;
2043
2044 let attn = MultiHeadAttention::new(&config);
2045 let data: Vec<f32> = (0..seq_len * config.hidden_size)
2046 .map(|i| ((i as f32 + seed as f32) * 0.73).cos())
2047 .collect();
2048 let x = Tensor::from_vec(data, true);
2049 let output = attn.forward(&x, seq_len);
2050
2051 prop_assert_eq!(
2052 output.len(),
2053 seq_len * config.hidden_size,
2054 "FALSIFIED GQ-006e-prop: MQA output len mismatch"
2055 );
2056
2057 for v in output.data() {
2058 prop_assert!(
2059 v.is_finite(),
2060 "FALSIFIED GQ-006e-prop: non-finite MQA output"
2061 );
2062 }
2063 }
2064 }
2065 }
2066
2067 #[test]
2068 fn test_attention_from_params_with_biases() {
2069 let config = TransformerConfig::tiny();
2070 let hidden_size = config.hidden_size;
2071 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2072
2073 let mut params = HashMap::new();
2074 params.insert(
2075 "attn.q_proj.weight".to_string(),
2076 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2077 );
2078 params.insert(
2079 "attn.k_proj.weight".to_string(),
2080 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2081 );
2082 params.insert(
2083 "attn.v_proj.weight".to_string(),
2084 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2085 );
2086 params.insert(
2087 "attn.o_proj.weight".to_string(),
2088 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2089 );
2090 params.insert(
2091 "attn.q_proj.bias".to_string(),
2092 Tensor::from_vec(vec![0.01; hidden_size], true),
2093 );
2094 params.insert(
2095 "attn.k_proj.bias".to_string(),
2096 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2097 );
2098 params.insert(
2099 "attn.v_proj.bias".to_string(),
2100 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2101 );
2102
2103 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn");
2104 assert!(attn.is_some());
2105 let attn = attn.expect("should load with biases");
2106 assert!(attn.has_biases());
2107 assert_eq!(attn.parameters().len(), 7);
2108 }
2109
2110 #[test]
2111 fn test_attention_named_parameters_with_biases() {
2112 let config = TransformerConfig::tiny();
2113 let hidden_size = config.hidden_size;
2114 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2115
2116 let mut params = HashMap::new();
2117 params.insert(
2118 "attn.q_proj.weight".to_string(),
2119 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2120 );
2121 params.insert(
2122 "attn.k_proj.weight".to_string(),
2123 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2124 );
2125 params.insert(
2126 "attn.v_proj.weight".to_string(),
2127 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2128 );
2129 params.insert(
2130 "attn.o_proj.weight".to_string(),
2131 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2132 );
2133 params.insert(
2134 "attn.q_proj.bias".to_string(),
2135 Tensor::from_vec(vec![0.01; hidden_size], true),
2136 );
2137 params.insert(
2138 "attn.k_proj.bias".to_string(),
2139 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2140 );
2141 params.insert(
2142 "attn.v_proj.bias".to_string(),
2143 Tensor::from_vec(vec![0.01; kv_hidden_size], true),
2144 );
2145
2146 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
2147 let named = attn.named_parameters("attn");
2148 assert_eq!(named.len(), 7);
2149 let names: Vec<&str> = named.iter().map(|(n, _)| n.as_str()).collect();
2150 assert!(names.contains(&"attn.q_proj.bias"));
2151 assert!(names.contains(&"attn.k_proj.bias"));
2152 assert!(names.contains(&"attn.v_proj.bias"));
2153 }
2154
2155 #[test]
2156 fn test_attention_forward_with_biases() {
2157 let config = TransformerConfig::tiny();
2158 let hidden_size = config.hidden_size;
2159 let kv_hidden_size = config.num_kv_heads * config.head_dim();
2160
2161 let mut params = HashMap::new();
2162 params.insert(
2163 "attn.q_proj.weight".to_string(),
2164 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2165 );
2166 params.insert(
2167 "attn.k_proj.weight".to_string(),
2168 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2169 );
2170 params.insert(
2171 "attn.v_proj.weight".to_string(),
2172 Tensor::from_vec(vec![0.1; hidden_size * kv_hidden_size], true),
2173 );
2174 params.insert(
2175 "attn.o_proj.weight".to_string(),
2176 Tensor::from_vec(vec![0.1; hidden_size * hidden_size], true),
2177 );
2178 params
2179 .insert("attn.q_proj.bias".to_string(), Tensor::from_vec(vec![0.5; hidden_size], true));
2180 params.insert(
2181 "attn.k_proj.bias".to_string(),
2182 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2183 );
2184 params.insert(
2185 "attn.v_proj.bias".to_string(),
2186 Tensor::from_vec(vec![0.5; kv_hidden_size], true),
2187 );
2188
2189 let attn = MultiHeadAttention::from_params(&config, ¶ms, "attn").expect("should load");
2190 let x = Tensor::from_vec(vec![0.1; 2 * hidden_size], false);
2191 let output = attn.forward(&x, 2);
2192 assert_eq!(output.len(), 2 * hidden_size);
2193 assert!(output.data().iter().all(|v| v.is_finite()));
2194 }
2195}