1use crate::algorithms::natural_grad::softmax;
9use ndarray::{Array1, Array2, ArrayView2, Axis};
10
11pub fn matmul(a: &[f32], a_rows: usize, a_cols: usize, b: &[f32], b_cols: usize) -> Vec<f32> {
15 assert_eq!(a.len(), a_rows * a_cols, "matmul: a dimensions mismatch");
16 assert_eq!(b.len(), a_cols * b_cols, "matmul: b dimensions mismatch");
17
18 let mut c = vec![0.0f32; a_rows * b_cols];
19 matmul_into(a, a_rows, a_cols, b, b_cols, &mut c);
20 c
21}
22
23pub fn matmul_into(
27 a: &[f32],
28 a_rows: usize,
29 a_cols: usize,
30 b: &[f32],
31 b_cols: usize,
32 c: &mut [f32],
33) {
34 assert_eq!(
35 a.len(),
36 a_rows * a_cols,
37 "matmul_into: a dimensions mismatch"
38 );
39 assert_eq!(
40 b.len(),
41 a_cols * b_cols,
42 "matmul_into: b dimensions mismatch"
43 );
44 assert_eq!(
45 c.len(),
46 a_rows * b_cols,
47 "matmul_into: c dimensions mismatch"
48 );
49
50 unsafe {
53 matrixmultiply::sgemm(
54 a_rows,
55 a_cols,
56 b_cols,
57 1.0,
58 a.as_ptr(),
59 a_cols as isize,
60 1,
61 b.as_ptr(),
62 b_cols as isize,
63 1,
64 0.0,
65 c.as_mut_ptr(),
66 b_cols as isize,
67 1,
68 );
69 }
70}
71
72pub fn cross_entropy_loss(probs: &[f32], target: usize) -> f32 {
76 assert!(!probs.is_empty(), "cross_entropy_loss: empty distribution");
77 assert!(
78 target < probs.len(),
79 "cross_entropy_loss: target out of bounds"
80 );
81 -probs[target].max(1e-30).ln()
82}
83
84pub fn cross_entropy_from_logits(logits: &[f32], target: usize) -> f32 {
89 assert!(
90 !logits.is_empty(),
91 "cross_entropy_from_logits: empty logits"
92 );
93 assert!(
94 target < logits.len(),
95 "cross_entropy_from_logits: target out of bounds"
96 );
97 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
98 let log_sum_exp = logits.iter().map(|&l| (l - max).exp()).sum::<f32>().ln() + max;
99 -logits[target] + log_sum_exp
100}
101
102pub fn cross_entropy_logits_grad(logits: &[f32], target: usize) -> Vec<f32> {
106 let mut probs = softmax(logits);
107 probs[target] -= 1.0;
108 probs
109}
110
111pub fn one_hot(class: usize, classes: usize) -> Vec<f32> {
113 assert!(class < classes, "one_hot: class out of bounds");
114 let mut v = vec![0.0f32; classes];
115 v[class] = 1.0;
116 v
117}
118
119pub fn relu(x: f32) -> f32 {
121 x.max(0.0)
122}
123
124pub fn relu_grad(x: f32) -> f32 {
126 if x > 0.0 {
127 1.0
128 } else {
129 0.0
130 }
131}
132
133struct XorShift32 {
135 state: u32,
136}
137
138impl XorShift32 {
139 fn new(seed: u32) -> Self {
140 Self {
141 state: seed.wrapping_add(0x9e37_79b9),
142 }
143 }
144
145 fn next(&mut self) -> u32 {
146 let mut x = self.state;
147 x ^= x << 13;
148 x ^= x >> 17;
149 x ^= x << 5;
150 self.state = x;
151 x
152 }
153
154 fn uniform_f32(&mut self, scale: f32) -> f32 {
156 let u = self.next();
157 let normalised = (u as f32 / u32::MAX as f32) * 2.0 - 1.0;
158 normalised * scale
159 }
160}
161
162pub struct MlpForward {
164 pub z1: Vec<f32>,
166 pub hidden: Vec<f32>,
168 pub logits: Vec<f32>,
170}
171
172pub struct MlpForwardBatch {
174 pub z1: Array2<f32>,
176 pub hidden: Array2<f32>,
178 pub logits: Array2<f32>,
180}
181
182#[derive(Debug, Clone)]
184pub struct MlpGradients {
185 pub dw1: Vec<f32>,
187 pub db1: Vec<f32>,
189 pub dw2: Vec<f32>,
191 pub db2: Vec<f32>,
193}
194
195#[derive(Debug, Clone)]
201pub struct MlpClassifier {
202 input_dim: usize,
203 hidden_dim: usize,
204 output_dim: usize,
205 pub w1: Vec<f32>,
207 pub b1: Vec<f32>,
209 pub w2: Vec<f32>,
211 pub b2: Vec<f32>,
213}
214
215impl MlpClassifier {
216 pub fn new(input_dim: usize, hidden_dim: usize, output_dim: usize, seed: u32) -> Self {
218 let mut rng = XorShift32::new(seed);
219
220 let scale1 = (2.0 / input_dim as f32).sqrt() * 0.1;
221 let mut w1 = vec![0.0f32; input_dim * hidden_dim];
222 for v in w1.iter_mut() {
223 *v = rng.uniform_f32(scale1);
224 }
225 let mut b1 = vec![0.0f32; hidden_dim];
226 for v in b1.iter_mut() {
227 *v = rng.uniform_f32(0.01);
228 }
229
230 let scale2 = (2.0 / hidden_dim as f32).sqrt() * 0.1;
231 let mut w2 = vec![0.0f32; hidden_dim * output_dim];
232 for v in w2.iter_mut() {
233 *v = rng.uniform_f32(scale2);
234 }
235 let mut b2 = vec![0.0f32; output_dim];
236 for v in b2.iter_mut() {
237 *v = rng.uniform_f32(0.01);
238 }
239
240 Self {
241 input_dim,
242 hidden_dim,
243 output_dim,
244 w1,
245 b1,
246 w2,
247 b2,
248 }
249 }
250
251 pub fn forward(&self, x: &[f32]) -> MlpForward {
253 assert_eq!(x.len(), self.input_dim, "forward: input dimension mismatch");
254
255 let z1 = matmul(x, 1, self.input_dim, &self.w1, self.hidden_dim)
256 .iter()
257 .zip(self.b1.iter())
258 .map(|(&z, &b)| z + b)
259 .collect::<Vec<_>>();
260 let hidden = z1.iter().map(|&z| relu(z)).collect::<Vec<_>>();
261 let logits = matmul(&hidden, 1, self.hidden_dim, &self.w2, self.output_dim)
262 .iter()
263 .zip(self.b2.iter())
264 .map(|(&z, &b)| z + b)
265 .collect::<Vec<_>>();
266
267 MlpForward { z1, hidden, logits }
268 }
269
270 pub fn forward_hidden(&self, x: &[f32]) -> Vec<f32> {
274 assert_eq!(
275 x.len(),
276 self.input_dim,
277 "forward_hidden: input dimension mismatch"
278 );
279
280 matmul(x, 1, self.input_dim, &self.w1, self.hidden_dim)
281 .iter()
282 .zip(self.b1.iter())
283 .map(|(&z, &b)| relu(z + b))
284 .collect()
285 }
286
287 pub fn predict(&self, x: &[f32]) -> usize {
289 let fwd = self.forward(x);
290 fwd.logits
291 .iter()
292 .enumerate()
293 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
294 .map(|(i, _)| i)
295 .unwrap_or(0)
296 }
297
298 pub fn probabilities(&self, x: &[f32]) -> Vec<f32> {
300 let fwd = self.forward(x);
301 softmax(&fwd.logits)
302 }
303
304 pub fn loss(&self, x: &[f32], target: usize) -> f32 {
306 let fwd = self.forward(x);
307 cross_entropy_from_logits(&fwd.logits, target)
308 }
309
310 pub fn backward(&self, x: &[f32], target: usize) -> MlpGradients {
312 assert_eq!(
313 x.len(),
314 self.input_dim,
315 "backward: input dimension mismatch"
316 );
317 assert!(
318 target < self.output_dim,
319 "backward: target class out of bounds"
320 );
321
322 let fwd = self.forward(x);
323 let dlogits = cross_entropy_logits_grad(&fwd.logits, target);
324
325 let mut dw2 = vec![0.0f32; self.hidden_dim * self.output_dim];
327 for (i, &h_i) in fwd.hidden.iter().enumerate() {
328 for (k, &dlogit_k) in dlogits.iter().enumerate() {
329 dw2[i * self.output_dim + k] = h_i * dlogit_k;
330 }
331 }
332 let db2 = dlogits.clone();
333
334 let mut dh = vec![0.0f32; self.hidden_dim];
336 for (i, dh_i) in dh.iter_mut().enumerate() {
337 let mut acc = 0.0f32;
338 for (k, &dlogit_k) in dlogits.iter().enumerate() {
339 acc += dlogit_k * self.w2[i * self.output_dim + k];
340 }
341 *dh_i = acc;
342 }
343
344 let dz1: Vec<f32> = dh
346 .iter()
347 .zip(fwd.z1.iter())
348 .map(|(&dh_i, &z1_i)| dh_i * relu_grad(z1_i))
349 .collect();
350
351 let mut dw1 = vec![0.0f32; self.input_dim * self.hidden_dim];
353 for j in 0..self.input_dim {
354 for i in 0..self.hidden_dim {
355 dw1[j * self.hidden_dim + i] = x[j] * dz1[i];
356 }
357 }
358 let db1 = dz1;
359
360 MlpGradients { dw1, db1, dw2, db2 }
361 }
362
363 pub fn apply_sgd(&mut self, grad: &MlpGradients, lr: f32) {
365 for i in 0..self.w1.len() {
366 self.w1[i] -= lr * grad.dw1[i];
367 }
368 for i in 0..self.b1.len() {
369 self.b1[i] -= lr * grad.db1[i];
370 }
371 for i in 0..self.w2.len() {
372 self.w2[i] -= lr * grad.dw2[i];
373 }
374 for i in 0..self.b2.len() {
375 self.b2[i] -= lr * grad.db2[i];
376 }
377 }
378
379 pub fn forward_batch(&self, x: &[f32], batch_size: usize) -> MlpForwardBatch {
383 assert_eq!(
384 x.len(),
385 batch_size * self.input_dim,
386 "forward_batch: input dimension mismatch"
387 );
388
389 let x_arr = Array2::from_shape_vec((batch_size, self.input_dim), x.to_vec())
390 .expect("forward_batch: invalid input shape");
391 let w1_view = ArrayView2::from_shape((self.input_dim, self.hidden_dim), &self.w1).unwrap();
392 let b1_view = ArrayView2::from_shape((1, self.hidden_dim), &self.b1).unwrap();
393 let z1 = x_arr.dot(&w1_view) + &b1_view;
394
395 let hidden = z1.mapv(|v| relu(v));
396
397 let w2_view = ArrayView2::from_shape((self.hidden_dim, self.output_dim), &self.w2).unwrap();
398 let b2_view = ArrayView2::from_shape((1, self.output_dim), &self.b2).unwrap();
399 let logits = hidden.dot(&w2_view) + &b2_view;
400
401 MlpForwardBatch { z1, hidden, logits }
402 }
403
404 pub fn loss_batch(&self, x: &[f32], targets: &[usize], batch_size: usize) -> f32 {
406 assert_eq!(
407 targets.len(),
408 batch_size,
409 "loss_batch: target count mismatch"
410 );
411 let fwd = self.forward_batch(x, batch_size);
412 let mut total = 0.0f32;
413 for (logits_row, &target) in fwd.logits.axis_iter(Axis(0)).zip(targets.iter()) {
414 total += cross_entropy_from_logits(logits_row.as_slice().unwrap(), target);
415 }
416 total / batch_size as f32
417 }
418
419 pub fn backward_batch(
426 &self,
427 x: &[f32],
428 targets: &[usize],
429 batch_size: usize,
430 ) -> (MlpGradients, Vec<f32>, f32) {
431 assert_eq!(
432 x.len(),
433 batch_size * self.input_dim,
434 "backward_batch: input dimension mismatch"
435 );
436 assert_eq!(
437 targets.len(),
438 batch_size,
439 "backward_batch: target count mismatch"
440 );
441 for &target in targets {
442 assert!(
443 target < self.output_dim,
444 "backward_batch: target class out of bounds"
445 );
446 }
447
448 let fwd = self.forward_batch(x, batch_size);
449 let batch_f = batch_size as f32;
450
451 let mut dlogits = Array2::zeros((batch_size, self.output_dim));
453 let mut total_loss = 0.0f32;
454 for ((mut row, logits_row), &target) in dlogits
455 .axis_iter_mut(Axis(0))
456 .zip(fwd.logits.axis_iter(Axis(0)))
457 .zip(targets.iter())
458 {
459 let probs = softmax(logits_row.as_slice().unwrap());
460 total_loss += -probs[target].max(1e-30).ln();
461 let mut shifted = Array1::from_vec(probs);
462 shifted[target] -= 1.0;
463 row.assign(&shifted);
464 }
465 let avg_loss = total_loss / batch_f;
466
467 let dw2 = fwd.hidden.t().dot(&dlogits) / batch_f;
469 let db2 = dlogits.mean_axis(Axis(0)).unwrap();
470
471 let w2_view = ArrayView2::from_shape((self.hidden_dim, self.output_dim), &self.w2).unwrap();
473 let dh = dlogits.dot(&w2_view.t());
474
475 let dz1 = dh * fwd.z1.mapv(|v| relu_grad(v));
477
478 let w1_view = ArrayView2::from_shape((self.input_dim, self.hidden_dim), &self.w1).unwrap();
481 let dx = dz1.dot(&w1_view.t());
482
483 let x_arr = Array2::from_shape_vec((batch_size, self.input_dim), x.to_vec())
485 .expect("backward_batch: invalid input shape");
486 let dw1 = x_arr.t().dot(&dz1) / batch_f;
487 let db1 = dz1.mean_axis(Axis(0)).unwrap();
488
489 (
490 MlpGradients {
491 dw1: dw1.into_raw_vec(),
492 db1: db1.into_raw_vec(),
493 dw2: dw2.into_raw_vec(),
494 db2: db2.into_raw_vec(),
495 },
496 dx.into_raw_vec(),
497 avg_loss,
498 )
499 }
500
501 pub fn flatten_params(&self) -> Vec<f32> {
504 let mut params =
505 Vec::with_capacity(self.w1.len() + self.b1.len() + self.w2.len() + self.b2.len());
506 params.extend_from_slice(&self.w1);
507 params.extend_from_slice(&self.b1);
508 params.extend_from_slice(&self.w2);
509 params.extend_from_slice(&self.b2);
510 params
511 }
512
513 pub fn flatten_grad(&self, grad: &MlpGradients) -> Vec<f32> {
516 let mut flat =
517 Vec::with_capacity(grad.dw1.len() + grad.db1.len() + grad.dw2.len() + grad.db2.len());
518 flat.extend_from_slice(&grad.dw1);
519 flat.extend_from_slice(&grad.db1);
520 flat.extend_from_slice(&grad.dw2);
521 flat.extend_from_slice(&grad.db2);
522 flat
523 }
524
525 pub fn load_flat_params(&mut self, params: &[f32]) {
527 let w1_len = self.w1.len();
528 let b1_len = self.b1.len();
529 let w2_len = self.w2.len();
530 let b2_len = self.b2.len();
531 let expected = w1_len + b1_len + w2_len + b2_len;
532 assert_eq!(params.len(), expected, "load_flat_params: size mismatch");
533
534 let mut off = 0;
535 self.w1.copy_from_slice(¶ms[off..off + w1_len]);
536 off += w1_len;
537 self.b1.copy_from_slice(¶ms[off..off + b1_len]);
538 off += b1_len;
539 self.w2.copy_from_slice(¶ms[off..off + w2_len]);
540 off += w2_len;
541 self.b2.copy_from_slice(¶ms[off..off + b2_len]);
542 }
543}
544
545#[derive(Debug, Clone)]
547pub struct EmbeddingMlpGradients {
548 pub dembedding: Vec<f32>,
550 pub mlp: MlpGradients,
552}
553
554#[derive(Debug, Clone)]
561pub struct EmbeddingMlpClassifier {
562 vocab_size: usize,
563 embed_dim: usize,
564 n_context_tokens: usize,
565 continuous_dim: usize,
566 rope_theta: Option<f32>,
569 pub embedding: Vec<f32>,
571 pub mlp: MlpClassifier,
574}
575
576fn apply_rope_in_place(vec: &mut [f32], embed_dim: usize, position: usize, theta: f32) {
581 assert_eq!(
582 vec.len(),
583 embed_dim,
584 "apply_rope_in_place: dimension mismatch"
585 );
586 assert!(
587 embed_dim % 2 == 0,
588 "apply_rope_in_place: embed_dim must be even"
589 );
590
591 let ln_theta = theta.ln();
592 let pos_f = position as f32;
593 let embed_dim_f = embed_dim as f32;
594
595 for pair in 0..embed_dim / 2 {
596 let d0 = pair * 2;
597 let d1 = d0 + 1;
598 let freq = (-2.0f32 * pair as f32 * ln_theta / embed_dim_f).exp();
599 let angle = pos_f * freq;
600 let (cos_a, sin_a) = (angle.cos(), angle.sin());
601 let v0 = vec[d0];
602 let v1 = vec[d1];
603 vec[d0] = v0 * cos_a - v1 * sin_a;
604 vec[d1] = v0 * sin_a + v1 * cos_a;
605 }
606}
607
608fn apply_rope_inv_in_place(vec: &mut [f32], embed_dim: usize, position: usize, theta: f32) {
610 assert_eq!(
611 vec.len(),
612 embed_dim,
613 "apply_rope_inv_in_place: dimension mismatch"
614 );
615 assert!(
616 embed_dim % 2 == 0,
617 "apply_rope_inv_in_place: embed_dim must be even"
618 );
619
620 let ln_theta = theta.ln();
621 let pos_f = position as f32;
622 let embed_dim_f = embed_dim as f32;
623
624 for pair in 0..embed_dim / 2 {
625 let d0 = pair * 2;
626 let d1 = d0 + 1;
627 let freq = (-2.0f32 * pair as f32 * ln_theta / embed_dim_f).exp();
628 let angle = pos_f * freq;
629 let (cos_a, sin_a) = (angle.cos(), angle.sin());
630 let v0 = vec[d0];
631 let v1 = vec[d1];
632 vec[d0] = v0 * cos_a + v1 * sin_a;
633 vec[d1] = -v0 * sin_a + v1 * cos_a;
634 }
635}
636
637impl EmbeddingMlpClassifier {
638 pub fn new(
640 vocab_size: usize,
641 embed_dim: usize,
642 n_context_tokens: usize,
643 continuous_dim: usize,
644 hidden_dim: usize,
645 output_dim: usize,
646 seed: u32,
647 rope_theta: Option<f32>,
648 ) -> Self {
649 if let Some(theta) = rope_theta {
650 assert!(
651 theta > 0.0,
652 "EmbeddingMlpClassifier: rope_theta must be positive"
653 );
654 assert!(
655 embed_dim % 2 == 0,
656 "EmbeddingMlpClassifier: RoPE requires even embed_dim"
657 );
658 }
659
660 let input_dim = n_context_tokens * embed_dim + continuous_dim;
661 let mut rng = XorShift32::new(seed);
662
663 let scale = (2.0 / embed_dim as f32).sqrt() * 0.1;
664 let mut embedding = vec![0.0f32; vocab_size * embed_dim];
665 for v in embedding.iter_mut() {
666 *v = rng.uniform_f32(scale);
667 }
668
669 let mlp = MlpClassifier::new(input_dim, hidden_dim, output_dim, seed.wrapping_add(1));
670
671 Self {
672 vocab_size,
673 embed_dim,
674 n_context_tokens,
675 continuous_dim,
676 rope_theta,
677 embedding,
678 mlp,
679 }
680 }
681
682 pub fn input_dim(&self) -> usize {
684 self.n_context_tokens * self.embed_dim + self.continuous_dim
685 }
686
687 fn fill_input(&self, token_ids: &[u32], continuous: Option<&[f32]>, input: &mut [f32]) {
688 assert_eq!(
689 token_ids.len(),
690 self.n_context_tokens,
691 "fill_input: token count mismatch"
692 );
693 if let Some(cont) = continuous {
694 assert_eq!(
695 cont.len(),
696 self.continuous_dim,
697 "fill_input: continuous dimension mismatch"
698 );
699 }
700
701 let mut off = 0;
702 for (t, &tid) in token_ids.iter().enumerate() {
703 let idx = (tid as usize).min(self.vocab_size - 1);
704 let src = &self.embedding[idx * self.embed_dim..(idx + 1) * self.embed_dim];
705 input[off..off + self.embed_dim].copy_from_slice(src);
706
707 if let Some(theta) = self.rope_theta {
710 let position = self.n_context_tokens - t;
711 apply_rope_in_place(
712 &mut input[off..off + self.embed_dim],
713 self.embed_dim,
714 position,
715 theta,
716 );
717 }
718
719 off += self.embed_dim;
720 }
721 if let Some(cont) = continuous {
722 input[off..off + self.continuous_dim].copy_from_slice(cont);
723 }
724 }
725
726 fn fill_batch_input(
727 &self,
728 token_ids: &[u32],
729 continuous: Option<&[f32]>,
730 batch_size: usize,
731 input: &mut [f32],
732 ) {
733 let input_dim = self.input_dim();
734 assert_eq!(
735 token_ids.len(),
736 batch_size * self.n_context_tokens,
737 "fill_batch_input: token count mismatch"
738 );
739 assert_eq!(
740 input.len(),
741 batch_size * input_dim,
742 "fill_batch_input: input buffer size mismatch"
743 );
744
745 for b in 0..batch_size {
746 let tok_start = b * self.n_context_tokens;
747 let cont = continuous.map(|c| {
748 let start = b * self.continuous_dim;
749 &c[start..start + self.continuous_dim]
750 });
751 self.fill_input(
752 &token_ids[tok_start..tok_start + self.n_context_tokens],
753 cont,
754 &mut input[b * input_dim..(b + 1) * input_dim],
755 );
756 }
757 }
758
759 pub fn forward(&self, token_ids: &[u32], continuous: Option<&[f32]>) -> MlpForward {
761 let mut input = vec![0.0f32; self.input_dim()];
762 self.fill_input(token_ids, continuous, &mut input);
763 self.mlp.forward(&input)
764 }
765
766 pub fn predict(&self, token_ids: &[u32], continuous: Option<&[f32]>) -> usize {
768 let fwd = self.forward(token_ids, continuous);
769 fwd.logits
770 .iter()
771 .enumerate()
772 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
773 .map(|(i, _)| i)
774 .unwrap_or(0)
775 }
776
777 pub fn loss(&self, token_ids: &[u32], continuous: Option<&[f32]>, target: usize) -> f32 {
779 let fwd = self.forward(token_ids, continuous);
780 cross_entropy_from_logits(&fwd.logits, target)
781 }
782
783 pub fn forward_batch(
785 &self,
786 token_ids: &[u32],
787 continuous: Option<&[f32]>,
788 batch_size: usize,
789 ) -> MlpForwardBatch {
790 let mut input = vec![0.0f32; batch_size * self.input_dim()];
791 self.fill_batch_input(token_ids, continuous, batch_size, &mut input);
792 self.mlp.forward_batch(&input, batch_size)
793 }
794
795 pub fn backward_batch(
801 &self,
802 token_ids: &[u32],
803 continuous: Option<&[f32]>,
804 targets: &[usize],
805 batch_size: usize,
806 ) -> (EmbeddingMlpGradients, f32) {
807 assert_eq!(
808 token_ids.len(),
809 batch_size * self.n_context_tokens,
810 "backward_batch: token count mismatch"
811 );
812 assert_eq!(
813 targets.len(),
814 batch_size,
815 "backward_batch: target count mismatch"
816 );
817 if let Some(cont) = continuous {
818 assert_eq!(
819 cont.len(),
820 batch_size * self.continuous_dim,
821 "backward_batch: continuous size mismatch"
822 );
823 }
824
825 let mut input = vec![0.0f32; batch_size * self.input_dim()];
826 self.fill_batch_input(token_ids, continuous, batch_size, &mut input);
827
828 let (mlp_grad, dx, loss) = self.mlp.backward_batch(&input, targets, batch_size);
829
830 let mut dembedding = vec![0.0f32; self.embedding.len()];
834 let input_dim = self.input_dim();
835 for b in 0..batch_size {
836 let dx_row = &dx[b * input_dim..(b + 1) * input_dim];
837 let tok_start = b * self.n_context_tokens;
838 for (t, &tid) in token_ids[tok_start..tok_start + self.n_context_tokens]
839 .iter()
840 .enumerate()
841 {
842 let idx = (tid as usize).min(self.vocab_size - 1);
843 let mut dx_emb = dx_row[t * self.embed_dim..(t + 1) * self.embed_dim].to_vec();
844 if let Some(theta) = self.rope_theta {
845 let position = self.n_context_tokens - t;
846 apply_rope_inv_in_place(&mut dx_emb, self.embed_dim, position, theta);
847 }
848 let dst = &mut dembedding[idx * self.embed_dim..(idx + 1) * self.embed_dim];
849 for (d, g) in dst.iter_mut().zip(dx_emb.iter()) {
850 *d += *g;
851 }
852 }
853 }
854
855 let inv_b = 1.0 / batch_size as f32;
856 for v in dembedding.iter_mut() {
857 *v *= inv_b;
858 }
859
860 (
861 EmbeddingMlpGradients {
862 dembedding,
863 mlp: mlp_grad,
864 },
865 loss,
866 )
867 }
868
869 pub fn apply_sgd(&mut self, grad: &EmbeddingMlpGradients, lr: f32) {
871 for i in 0..self.embedding.len() {
872 self.embedding[i] -= lr * grad.dembedding[i];
873 }
874 self.mlp.apply_sgd(&grad.mlp, lr);
875 }
876}
877
878#[cfg(test)]
879mod tests {
880 use super::*;
881
882 #[test]
883 fn matmul_identity() {
884 let a = vec![1.0f32, 2.0, 3.0, 4.0];
885 let i = vec![1.0f32, 0.0, 0.0, 1.0];
887 let c = matmul(&a, 2, 2, &i, 2);
888 assert_eq!(c, a);
889 }
890
891 #[test]
892 fn matmul_small() {
893 let a = vec![1.0f32, 2.0, 3.0, 4.0];
896 let b = vec![5.0f32, 6.0, 7.0, 8.0];
897 let c = matmul(&a, 2, 2, &b, 2);
898 assert_eq!(c, vec![19.0, 22.0, 43.0, 50.0]);
899 }
900
901 #[test]
902 fn cross_entropy_decreases_with_target_probability() {
903 let p = vec![0.1f32, 0.8, 0.1];
904 assert!(cross_entropy_loss(&p, 1) < cross_entropy_loss(&p, 0));
905 }
906
907 #[test]
908 fn mlp_learns_xor() {
909 let examples: Vec<(Vec<f32>, usize)> = vec![
911 (vec![0.0, 0.0], 0),
912 (vec![0.0, 1.0], 1),
913 (vec![1.0, 0.0], 1),
914 (vec![1.0, 1.0], 0),
915 ];
916
917 let mut model = MlpClassifier::new(2, 8, 2, 7);
918 let lr = 0.2;
919
920 for _ in 0..2000 {
921 for (x, y) in &examples {
922 let grad = model.backward(x, *y);
923 model.apply_sgd(&grad, lr);
924 }
925 }
926
927 for (x, y) in &examples {
928 assert_eq!(
929 model.predict(x),
930 *y,
931 "failed XOR input {:?}, logits {:?}",
932 x,
933 model.forward(x).logits
934 );
935 }
936 }
937
938 #[test]
939 fn flatten_and_load_round_trip() {
940 let model = MlpClassifier::new(3, 4, 2, 13);
941 let params = model.flatten_params();
942 let mut restored = MlpClassifier::new(3, 4, 2, 99);
943 restored.load_flat_params(¶ms);
944 assert_eq!(model.w1, restored.w1);
945 assert_eq!(model.b1, restored.b1);
946 assert_eq!(model.w2, restored.w2);
947 assert_eq!(model.b2, restored.b2);
948 }
949
950 #[test]
951 fn backward_batch_matches_per_example_average() {
952 let input_dim = 4;
953 let hidden_dim = 5;
954 let output_dim = 3;
955 let model = MlpClassifier::new(input_dim, hidden_dim, output_dim, 21);
956 let batch_size = 3;
957
958 let inputs: Vec<f32> = (0..input_dim * batch_size)
959 .map(|i| (i as f32) * 0.13 - 0.4)
960 .collect();
961 let targets = vec![0, 2, 1];
962
963 let (grad_batch, _dx, loss_batch) = model.backward_batch(&inputs, &targets, batch_size);
964
965 let mut dw1 = vec![0.0f32; grad_batch.dw1.len()];
966 let mut db1 = vec![0.0f32; grad_batch.db1.len()];
967 let mut dw2 = vec![0.0f32; grad_batch.dw2.len()];
968 let mut db2 = vec![0.0f32; grad_batch.db2.len()];
969 let mut loss_sum = 0.0f32;
970
971 for b in 0..batch_size {
972 let x = &inputs[b * input_dim..(b + 1) * input_dim];
973 let g = model.backward(x, targets[b]);
974 for (a, v) in dw1.iter_mut().zip(g.dw1.iter()) {
975 *a += v;
976 }
977 for (a, v) in db1.iter_mut().zip(g.db1.iter()) {
978 *a += v;
979 }
980 for (a, v) in dw2.iter_mut().zip(g.dw2.iter()) {
981 *a += v;
982 }
983 for (a, v) in db2.iter_mut().zip(g.db2.iter()) {
984 *a += v;
985 }
986 loss_sum += model.loss(x, targets[b]);
987 }
988
989 let inv = 1.0 / batch_size as f32;
990 for v in dw1.iter_mut() {
991 *v *= inv;
992 }
993 for v in db1.iter_mut() {
994 *v *= inv;
995 }
996 for v in dw2.iter_mut() {
997 *v *= inv;
998 }
999 for v in db2.iter_mut() {
1000 *v *= inv;
1001 }
1002
1003 let tol = 1e-5;
1004 for (a, b) in grad_batch.dw1.iter().zip(dw1.iter()) {
1005 assert!((a - b).abs() < tol, "dw1 mismatch: {} vs {}", a, b);
1006 }
1007 for (a, b) in grad_batch.db1.iter().zip(db1.iter()) {
1008 assert!((a - b).abs() < tol, "db1 mismatch: {} vs {}", a, b);
1009 }
1010 for (a, b) in grad_batch.dw2.iter().zip(dw2.iter()) {
1011 assert!((a - b).abs() < tol, "dw2 mismatch: {} vs {}", a, b);
1012 }
1013 for (a, b) in grad_batch.db2.iter().zip(db2.iter()) {
1014 assert!((a - b).abs() < tol, "db2 mismatch: {} vs {}", a, b);
1015 }
1016 assert!((loss_batch - loss_sum * inv).abs() < tol);
1017 }
1018
1019 #[test]
1020 fn embedding_mlp_learns_xor_with_continuous() {
1021 let examples: Vec<(u32, u32, usize)> = vec![(0, 0, 0), (0, 1, 1), (1, 0, 1), (1, 1, 0)];
1025
1026 let mut model = EmbeddingMlpClassifier::new(2, 8, 2, 0, 16, 2, 7, None);
1027 let lr = 0.2;
1028
1029 for _ in 0..8000 {
1030 for &(t1, t2, y) in &examples {
1031 let grad = model.backward_batch(&[t1, t2], None, &[y], 1);
1032 model.apply_sgd(&grad.0, lr);
1033 }
1034 }
1035
1036 for &(t1, t2, y) in &examples {
1037 assert_eq!(
1038 model.predict(&[t1, t2], None),
1039 y,
1040 "failed XOR input ({}, {})",
1041 t1,
1042 t2
1043 );
1044 }
1045 }
1046
1047 #[test]
1048 fn embedding_mlp_batch_matches_single_example() {
1049 let vocab_size = 5;
1050 let embed_dim = 3;
1051 let n_context = 2;
1052 let continuous_dim = 4;
1053 let model = EmbeddingMlpClassifier::new(
1054 vocab_size,
1055 embed_dim,
1056 n_context,
1057 continuous_dim,
1058 6,
1059 3,
1060 11,
1061 None,
1062 );
1063
1064 let token_ids = vec![0u32, 2, 4, 1];
1065 let continuous: Vec<f32> = (0..(2 * continuous_dim) as i32)
1066 .map(|i| i as f32 * 0.1 - 0.5)
1067 .collect();
1068 let targets = vec![0, 2];
1069
1070 let (batch_grad, batch_loss) =
1071 model.backward_batch(&token_ids, Some(&continuous), &targets, 2);
1072
1073 let mut dembedding = vec![0.0f32; vocab_size * embed_dim];
1075 let mut dw1 = vec![0.0f32; model.mlp.w1.len()];
1076 let mut db1 = vec![0.0f32; model.mlp.b1.len()];
1077 let mut dw2 = vec![0.0f32; model.mlp.w2.len()];
1078 let mut db2 = vec![0.0f32; model.mlp.b2.len()];
1079 let mut loss_sum = 0.0f32;
1080
1081 for b in 0..2 {
1082 let t = &token_ids[b * n_context..(b + 1) * n_context];
1083 let c = &continuous[b * continuous_dim..(b + 1) * continuous_dim];
1084 let (g, loss) = model.backward_batch(t, Some(c), &[targets[b]], 1);
1085 loss_sum += loss;
1086
1087 for (a, v) in dembedding.iter_mut().zip(g.dembedding.iter()) {
1088 *a += v;
1089 }
1090 for (a, v) in dw1.iter_mut().zip(g.mlp.dw1.iter()) {
1091 *a += v;
1092 }
1093 for (a, v) in db1.iter_mut().zip(g.mlp.db1.iter()) {
1094 *a += v;
1095 }
1096 for (a, v) in dw2.iter_mut().zip(g.mlp.dw2.iter()) {
1097 *a += v;
1098 }
1099 for (a, v) in db2.iter_mut().zip(g.mlp.db2.iter()) {
1100 *a += v;
1101 }
1102 }
1103
1104 let inv = 1.0 / 2.0;
1105 for v in dembedding.iter_mut() {
1106 *v *= inv;
1107 }
1108 for v in dw1.iter_mut() {
1109 *v *= inv;
1110 }
1111 for v in db1.iter_mut() {
1112 *v *= inv;
1113 }
1114 for v in dw2.iter_mut() {
1115 *v *= inv;
1116 }
1117 for v in db2.iter_mut() {
1118 *v *= inv;
1119 }
1120
1121 let tol = 1e-5;
1122 for (a, b) in batch_grad.dembedding.iter().zip(dembedding.iter()) {
1123 assert!((a - b).abs() < tol, "dembedding mismatch: {} vs {}", a, b);
1124 }
1125 for (a, b) in batch_grad.mlp.dw1.iter().zip(dw1.iter()) {
1126 assert!((a - b).abs() < tol, "dw1 mismatch: {} vs {}", a, b);
1127 }
1128 for (a, b) in batch_grad.mlp.db1.iter().zip(db1.iter()) {
1129 assert!((a - b).abs() < tol, "db1 mismatch: {} vs {}", a, b);
1130 }
1131 for (a, b) in batch_grad.mlp.dw2.iter().zip(dw2.iter()) {
1132 assert!((a - b).abs() < tol, "dw2 mismatch: {} vs {}", a, b);
1133 }
1134 for (a, b) in batch_grad.mlp.db2.iter().zip(db2.iter()) {
1135 assert!((a - b).abs() < tol, "db2 mismatch: {} vs {}", a, b);
1136 }
1137 assert!((batch_loss - loss_sum * inv).abs() < tol);
1138 }
1139
1140 #[test]
1141 fn rope_rotates_differently_by_position() {
1142 let mut v1 = vec![1.0f32, 0.0, 0.0, 1.0];
1143 let mut v2 = v1.clone();
1144 apply_rope_in_place(&mut v1, 4, 1, 10000.0);
1145 apply_rope_in_place(&mut v2, 4, 2, 10000.0);
1146 assert!(v1.iter().zip(v2.iter()).any(|(a, b)| (a - b).abs() > 1e-6));
1148 }
1149
1150 #[test]
1151 fn rope_inverse_recovers_original() {
1152 let original = vec![0.3f32, -0.7, 1.2, 0.4];
1153 let mut rotated = original.clone();
1154 apply_rope_in_place(&mut rotated, 4, 3, 10000.0);
1155 let mut recovered = rotated.clone();
1156 apply_rope_inv_in_place(&mut recovered, 4, 3, 10000.0);
1157
1158 let tol = 1e-5;
1159 for (a, b) in original.iter().zip(recovered.iter()) {
1160 assert!((a - b).abs() < tol, "RoPE inverse mismatch: {} vs {}", a, b);
1161 }
1162 }
1163
1164 #[test]
1165 fn embedding_mlp_batch_with_rope_matches_per_example() {
1166 let vocab_size = 5;
1167 let embed_dim = 4;
1168 let n_context = 2;
1169 let continuous_dim = 0;
1170 let model = EmbeddingMlpClassifier::new(
1171 vocab_size,
1172 embed_dim,
1173 n_context,
1174 continuous_dim,
1175 6,
1176 3,
1177 11,
1178 Some(10000.0),
1179 );
1180
1181 let token_ids = vec![0u32, 2, 4, 1];
1182 let targets = vec![0, 2];
1183
1184 let (batch_grad, batch_loss) = model.backward_batch(&token_ids, None, &targets, 2);
1185
1186 let mut dembedding = vec![0.0f32; vocab_size * embed_dim];
1188 let mut dw1 = vec![0.0f32; model.mlp.w1.len()];
1189 let mut db1 = vec![0.0f32; model.mlp.b1.len()];
1190 let mut dw2 = vec![0.0f32; model.mlp.w2.len()];
1191 let mut db2 = vec![0.0f32; model.mlp.b2.len()];
1192 let mut loss_sum = 0.0f32;
1193
1194 for b in 0..2 {
1195 let t = &token_ids[b * n_context..(b + 1) * n_context];
1196 let (g, loss) = model.backward_batch(t, None, &[targets[b]], 1);
1197 loss_sum += loss;
1198
1199 for (a, v) in dembedding.iter_mut().zip(g.dembedding.iter()) {
1200 *a += v;
1201 }
1202 for (a, v) in dw1.iter_mut().zip(g.mlp.dw1.iter()) {
1203 *a += v;
1204 }
1205 for (a, v) in db1.iter_mut().zip(g.mlp.db1.iter()) {
1206 *a += v;
1207 }
1208 for (a, v) in dw2.iter_mut().zip(g.mlp.dw2.iter()) {
1209 *a += v;
1210 }
1211 for (a, v) in db2.iter_mut().zip(g.mlp.db2.iter()) {
1212 *a += v;
1213 }
1214 }
1215
1216 let inv = 1.0 / 2.0;
1217 for v in dembedding.iter_mut() {
1218 *v *= inv;
1219 }
1220 for v in dw1.iter_mut() {
1221 *v *= inv;
1222 }
1223 for v in db1.iter_mut() {
1224 *v *= inv;
1225 }
1226 for v in dw2.iter_mut() {
1227 *v *= inv;
1228 }
1229 for v in db2.iter_mut() {
1230 *v *= inv;
1231 }
1232
1233 let tol = 1e-5;
1234 for (a, b) in batch_grad.dembedding.iter().zip(dembedding.iter()) {
1235 assert!((a - b).abs() < tol, "dembedding mismatch: {} vs {}", a, b);
1236 }
1237 for (a, b) in batch_grad.mlp.dw1.iter().zip(dw1.iter()) {
1238 assert!((a - b).abs() < tol, "dw1 mismatch: {} vs {}", a, b);
1239 }
1240 for (a, b) in batch_grad.mlp.db1.iter().zip(db1.iter()) {
1241 assert!((a - b).abs() < tol, "db1 mismatch: {} vs {}", a, b);
1242 }
1243 for (a, b) in batch_grad.mlp.dw2.iter().zip(dw2.iter()) {
1244 assert!((a - b).abs() < tol, "dw2 mismatch: {} vs {}", a, b);
1245 }
1246 for (a, b) in batch_grad.mlp.db2.iter().zip(db2.iter()) {
1247 assert!((a - b).abs() < tol, "db2 mismatch: {} vs {}", a, b);
1248 }
1249 assert!((batch_loss - loss_sum * inv).abs() < tol);
1250 }
1251}