1use std::fmt::Debug;
2use scirs2_core::ndarray::{s, Array1, Array2};
8use scirs2_core::numeric::Float;
9use scirs2_core::random::CoreRandom as Random;
10
11use super::super::TransformerOptimizerConfig;
12use super::attention::MultiHeadAttention;
13use crate::error::{OptimError, Result};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum ActivationFunction {
18 ReLU,
20 #[default]
22 GELU,
23 Swish,
25 GLU,
28 GeGLU,
31}
32
33impl ActivationFunction {
34 pub fn is_gated(self) -> bool {
36 matches!(self, ActivationFunction::GLU | ActivationFunction::GeGLU)
37 }
38
39 pub fn output_width(self, input_width: usize) -> usize {
41 if self.is_gated() {
42 input_width / 2
43 } else {
44 input_width
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct TransformerLayer<T: Float + Debug + Send + Sync + 'static> {
52 self_attention: MultiHeadAttention<T>,
54
55 cross_attention: Option<MultiHeadAttention<T>>,
57
58 feed_forward: FeedForwardNetwork<T>,
60
61 ln1: LayerNorm<T>,
63 ln2: LayerNorm<T>,
64 ln3: Option<LayerNorm<T>>, dropout1: DropoutLayer,
68 dropout2: DropoutLayer,
69 dropout3: Option<DropoutLayer>,
70
71 pre_layer_norm: bool,
73}
74
75#[derive(Debug, Clone)]
77pub struct FeedForwardNetwork<T: Float + Debug + Send + Sync + 'static> {
78 linear1: Array2<T>,
80
81 bias1: Array1<T>,
83
84 linear2: Array2<T>,
86
87 bias2: Array1<T>,
89
90 activation: ActivationFunction,
92
93 modeldim: usize,
95
96 ff_dim: usize,
98
99 dropout: DropoutLayer,
101}
102
103#[derive(Debug, Clone)]
105pub struct LayerNorm<T: Float + Debug + Send + Sync + 'static> {
106 gamma: Array1<T>,
108
109 beta: Array1<T>,
111
112 eps: T,
114
115 dim: usize,
117}
118
119#[derive(Debug, Clone)]
121pub struct DropoutLayer {
122 prob: f64,
124
125 training: bool,
128
129 rng_state: u64,
131}
132
133impl<
134 T: Float
135 + Debug
136 + Default
137 + Clone
138 + std::iter::Sum
139 + scirs2_core::ndarray::ScalarOperand
140 + Send
141 + Sync,
142 > TransformerLayer<T>
143{
144 pub fn new(config: &TransformerOptimizerConfig, _rng: &mut Random) -> Result<Self> {
146 let self_attention = MultiHeadAttention::new(config)?;
147 let cross_attention = if config.cross_attention {
148 Some(MultiHeadAttention::new(config)?)
149 } else {
150 None
151 };
152
153 let feed_forward = FeedForwardNetwork::new(config)?;
154
155 let ln1 = LayerNorm::new(config.modeldim, config.layer_norm_eps);
156 let ln2 = LayerNorm::new(config.modeldim, config.layer_norm_eps);
157 let ln3 = if config.cross_attention {
158 Some(LayerNorm::new(config.modeldim, config.layer_norm_eps))
159 } else {
160 None
161 };
162
163 let dropout1 = DropoutLayer::with_seed(config.attention_dropout, config.dropout_seed);
164 let dropout2 = DropoutLayer::with_seed(config.ff_dropout, config.dropout_seed ^ 0x9E37);
165 let dropout3 = if config.cross_attention {
166 Some(DropoutLayer::with_seed(
167 config.attention_dropout,
168 config.dropout_seed ^ 0x7F4A,
169 ))
170 } else {
171 None
172 };
173
174 Ok(Self {
175 self_attention,
176 cross_attention,
177 feed_forward,
178 ln1,
179 ln2,
180 ln3,
181 dropout1,
182 dropout2,
183 dropout3,
184 pre_layer_norm: config.pre_layer_norm,
185 })
186 }
187
188 pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
190 let mut x = input.clone();
191
192 let residual = x.clone();
194 if self.pre_layer_norm {
195 x = self.ln1.forward(&x)?;
196 }
197
198 x = self.self_attention.forward(&x, &x, &x)?;
199 x = self.dropout1.forward(&x);
200 x = x + &residual;
201
202 if !self.pre_layer_norm {
203 x = self.ln1.forward(&x)?;
204 }
205
206 if let Some(ref mut cross_attn) = self.cross_attention {
208 let residual = x.clone();
209 if self.pre_layer_norm {
210 if let Some(ref ln3) = self.ln3 {
211 x = ln3.forward(&x)?;
212 }
213 }
214
215 x = cross_attn.forward(&x, &x, &x)?;
216 if let Some(ref mut dropout3) = self.dropout3 {
217 x = dropout3.forward(&x);
218 }
219 x = x + &residual;
220
221 if !self.pre_layer_norm {
222 if let Some(ref ln3) = self.ln3 {
223 x = ln3.forward(&x)?;
224 }
225 }
226 }
227
228 let residual = x.clone();
230 if self.pre_layer_norm {
231 x = self.ln2.forward(&x)?;
232 }
233
234 x = self.feed_forward.forward(&x)?;
235 x = self.dropout2.forward(&x);
236 x = x + &residual;
237
238 if !self.pre_layer_norm {
239 x = self.ln2.forward(&x)?;
240 }
241
242 Ok(x)
243 }
244
245 pub fn get_attention_patterns(&self) -> Option<&scirs2_core::ndarray::Array3<T>> {
247 self.self_attention.get_attention_patterns()
248 }
249
250 pub fn set_training(&mut self, training: bool) {
252 self.dropout1.set_training(training);
253 self.dropout2.set_training(training);
254 if let Some(ref mut dropout3) = self.dropout3 {
255 dropout3.set_training(training);
256 }
257 self.feed_forward.set_training(training);
258 }
259
260 pub fn feed_forward_mut(&mut self) -> &mut FeedForwardNetwork<T> {
262 &mut self.feed_forward
263 }
264
265 pub fn self_attention(&self) -> &MultiHeadAttention<T> {
267 &self.self_attention
268 }
269}
270
271impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> FeedForwardNetwork<T> {
272 pub fn new(config: &TransformerOptimizerConfig) -> Result<Self> {
274 Self::with_dimensions(
275 config.modeldim,
276 config.ff_dim,
277 config.activation,
278 config.ff_dropout,
279 config.dropout_seed ^ 0x1357,
280 )
281 }
282
283 pub fn with_dimensions(
288 modeldim: usize,
289 ff_dim: usize,
290 activation: ActivationFunction,
291 dropout: f64,
292 dropout_seed: u64,
293 ) -> Result<Self> {
294 if modeldim == 0 || ff_dim == 0 {
295 return Err(OptimError::InvalidConfig(
296 "Feed-forward dimensions must be positive".to_string(),
297 ));
298 }
299 if activation.is_gated() && !ff_dim.is_multiple_of(2) {
300 return Err(OptimError::InvalidConfig(format!(
301 "Gated activation {activation:?} requires an even feed-forward dimension, got {ff_dim}"
302 )));
303 }
304
305 let hidden_out = activation.output_width(ff_dim);
306 let linear1 = Self::xavier(modeldim, ff_dim);
307 let linear2 = Self::xavier(hidden_out, modeldim);
308
309 Ok(Self {
310 linear1,
311 bias1: Array1::zeros(ff_dim),
312 linear2,
313 bias2: Array1::zeros(modeldim),
314 activation,
315 modeldim,
316 ff_dim,
317 dropout: DropoutLayer::with_seed(dropout, dropout_seed),
318 })
319 }
320
321 fn xavier(fan_in: usize, fan_out: usize) -> Array2<T> {
323 let mut rng = scirs2_core::random::thread_rng();
324 let bound = (6.0 / (fan_in + fan_out).max(1) as f64).sqrt();
325 let mut weights = Array2::zeros((fan_in, fan_out));
326 for elem in weights.iter_mut() {
327 *elem = scirs2_core::numeric::NumCast::from((rng.random::<f64>() - 0.5) * 2.0 * bound)
328 .unwrap_or_else(|| T::zero());
329 }
330 weights
331 }
332
333 pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
335 let x1 = Self::linear_transform(input, &self.linear1, &self.bias1)?;
336 let x2 = self.apply_activation(&x1);
337 let x3 = self.dropout.forward(&x2);
338 Self::linear_transform(&x3, &self.linear2, &self.bias2)
339 }
340
341 fn linear_transform(
342 input: &Array2<T>,
343 weights: &Array2<T>,
344 bias: &Array1<T>,
345 ) -> Result<Array2<T>> {
346 let (_seq_len, input_dim) = input.dim();
347 let (weight_in, weight_out) = weights.dim();
348
349 if input_dim != weight_in {
350 return Err(OptimError::InvalidConfig(format!(
351 "Input dimension {input_dim} doesn't match weight matrix input {weight_in}"
352 )));
353 }
354 if bias.len() != weight_out {
355 return Err(OptimError::InvalidConfig(format!(
356 "Bias dimension {} doesn't match output dimension {weight_out}",
357 bias.len()
358 )));
359 }
360
361 Ok(input.dot(weights) + bias)
362 }
363
364 fn gelu(x: T) -> T {
366 let sqrt_2_pi: T =
367 scirs2_core::numeric::NumCast::from(0.797_884_560_802_865).unwrap_or_else(|| T::one());
368 let coeff: T = scirs2_core::numeric::NumCast::from(0.044715).unwrap_or_else(|| T::zero());
369 let half: T = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
370 let inner = sqrt_2_pi * (x + coeff * x * x * x);
371 half * x * (T::one() + inner.tanh())
372 }
373
374 fn sigmoid(x: T) -> T {
376 if x >= T::zero() {
377 T::one() / (T::one() + (-x).exp())
378 } else {
379 let e = x.exp();
380 e / (T::one() + e)
381 }
382 }
383
384 fn apply_activation(&self, input: &Array2<T>) -> Array2<T> {
385 match self.activation {
386 ActivationFunction::ReLU => input.map(|&x| if x > T::zero() { x } else { T::zero() }),
387 ActivationFunction::GELU => input.map(|&x| Self::gelu(x)),
388 ActivationFunction::Swish => input.map(|&x| x * Self::sigmoid(x)),
391 ActivationFunction::GLU => self.gated(input, Self::sigmoid),
392 ActivationFunction::GeGLU => self.gated(input, Self::gelu),
393 }
394 }
395
396 fn gated(&self, input: &Array2<T>, gate: fn(T) -> T) -> Array2<T> {
398 let (rows, cols) = input.dim();
399 let half = cols / 2;
400 let mut output = Array2::zeros((rows, half));
401 for i in 0..rows {
402 for j in 0..half {
403 output[[i, j]] = input[[i, j]] * gate(input[[i, half + j]]);
404 }
405 }
406 output
407 }
408
409 pub fn set_activation(&mut self, activation: ActivationFunction) -> Result<()> {
412 if activation == self.activation {
413 return Ok(());
414 }
415 if activation.is_gated() && !self.ff_dim.is_multiple_of(2) {
416 return Err(OptimError::InvalidConfig(format!(
417 "Gated activation {activation:?} requires an even feed-forward dimension, got {}",
418 self.ff_dim
419 )));
420 }
421
422 let new_width = activation.output_width(self.ff_dim);
423 if new_width != self.linear2.nrows() {
424 self.linear2 = Self::xavier(new_width, self.modeldim);
425 }
426 self.activation = activation;
427 Ok(())
428 }
429
430 pub fn activation(&self) -> ActivationFunction {
432 self.activation
433 }
434
435 pub fn model_dim(&self) -> usize {
437 self.modeldim
438 }
439
440 pub fn hidden_dim(&self) -> usize {
442 self.ff_dim
443 }
444
445 pub fn set_training(&mut self, training: bool) {
447 self.dropout.set_training(training);
448 }
449}
450
451impl<T: Float + Debug + Default + Clone + std::iter::Sum + Send + Sync> LayerNorm<T> {
452 pub fn new(dim: usize, eps: f64) -> Self {
454 Self {
455 gamma: Array1::ones(dim),
456 beta: Array1::zeros(dim),
457 eps: scirs2_core::numeric::NumCast::from(eps).unwrap_or_else(|| T::zero()),
458 dim,
459 }
460 }
461
462 pub fn with_default_eps(dim: usize) -> Self {
464 Self::new(dim, 1e-6)
465 }
466
467 pub fn forward(&self, input: &Array2<T>) -> Result<Array2<T>> {
469 let (seq_len, input_dim) = input.dim();
470
471 if input_dim != self.dim {
472 return Err(OptimError::InvalidConfig(format!(
473 "Input dimension {} doesn't match layer norm dimension {}",
474 input_dim, self.dim
475 )));
476 }
477 if input_dim == 0 {
478 return Ok(Array2::zeros((seq_len, 0)));
479 }
480
481 let mut output = Array2::zeros((seq_len, input_dim));
482 let width: T =
483 scirs2_core::numeric::NumCast::from(input_dim as f64).unwrap_or_else(|| T::one());
484
485 for i in 0..seq_len {
486 let row = input.slice(s![i, ..]);
487
488 let mean = row.iter().cloned().sum::<T>() / width;
489 let variance = row
490 .iter()
491 .map(|&x| {
492 let diff = x - mean;
493 diff * diff
494 })
495 .sum::<T>()
496 / width;
497
498 let std = (variance + self.eps).sqrt();
499
500 for j in 0..input_dim {
501 let normalized = (input[[i, j]] - mean) / std;
502 output[[i, j]] = self.gamma[j] * normalized + self.beta[j];
503 }
504 }
505
506 Ok(output)
507 }
508
509 pub fn parameters(&self) -> (&Array1<T>, &Array1<T>) {
511 (&self.gamma, &self.beta)
512 }
513
514 pub fn dim(&self) -> usize {
516 self.dim
517 }
518
519 pub fn set_parameters(&mut self, gamma: Array1<T>, beta: Array1<T>) -> Result<()> {
521 if gamma.len() != self.dim || beta.len() != self.dim {
522 return Err(OptimError::InvalidConfig(format!(
523 "Parameter dimensions ({}, {}) don't match layer norm dimension {}",
524 gamma.len(),
525 beta.len(),
526 self.dim
527 )));
528 }
529 self.gamma = gamma;
530 self.beta = beta;
531 Ok(())
532 }
533}
534
535impl DropoutLayer {
536 pub fn new(prob: f64) -> Self {
538 Self::with_seed(prob, 0x243F_6A88_85A3_08D3)
539 }
540
541 pub fn with_seed(prob: f64, seed: u64) -> Self {
543 Self {
544 prob: prob.clamp(0.0, 1.0),
545 training: false,
546 rng_state: seed | 1,
548 }
549 }
550
551 fn next_f64(&mut self) -> f64 {
552 let mut x = self.rng_state;
553 x ^= x >> 12;
554 x ^= x << 25;
555 x ^= x >> 27;
556 self.rng_state = x;
557 let value = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
558 (value >> 11) as f64 / (1u64 << 53) as f64
559 }
560
561 pub fn forward<T: Float + Clone>(&mut self, input: &Array2<T>) -> Array2<T> {
565 if !self.training || self.prob <= 0.0 {
566 return input.clone();
567 }
568 if self.prob >= 1.0 {
569 return Array2::zeros(input.dim());
570 }
571
572 let keep_prob = 1.0 - self.prob;
573 let scale: T = scirs2_core::numeric::NumCast::from(1.0 / keep_prob).unwrap_or_else(T::one);
574
575 let mut output = input.clone();
576 for elem in output.iter_mut() {
577 if self.next_f64() < self.prob {
578 *elem = T::zero();
579 } else {
580 *elem = *elem * scale;
581 }
582 }
583 output
584 }
585
586 pub fn set_training(&mut self, training: bool) {
588 self.training = training;
589 }
590
591 pub fn is_training(&self) -> bool {
593 self.training
594 }
595
596 pub fn prob(&self) -> f64 {
598 self.prob
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn swish_is_stable_for_large_inputs() {
608 let input =
609 Array2::<f64>::from_shape_vec((1, 3), vec![-1000.0, 0.0, 1000.0]).expect("valid shape");
610 let mut ffn =
611 FeedForwardNetwork::<f64>::with_dimensions(3, 4, ActivationFunction::Swish, 0.0, 1)
612 .expect("ffn creation");
613 let activated = ffn.apply_activation(&input);
614 assert!(activated.iter().all(|v| v.is_finite()), "{activated:?}");
615 assert!((activated[[0, 2]] - 1000.0).abs() < 1e-6);
616 assert!(activated[[0, 0]].abs() < 1e-6);
617 ffn.set_training(true);
618 }
619
620 #[test]
621 fn gated_activations_halve_the_width() {
622 let ffn = FeedForwardNetwork::<f64>::with_dimensions(4, 8, ActivationFunction::GLU, 0.0, 1)
623 .expect("ffn creation");
624 let hidden = Array2::<f64>::from_shape_fn((2, 8), |(_, j)| j as f64 - 3.5);
625 let activated = ffn.apply_activation(&hidden);
626 assert_eq!(activated.shape(), &[2, 4]);
627 let expected = hidden[[0, 0]] * (1.0 / (1.0 + (-hidden[[0, 4]]).exp()));
629 assert!((activated[[0, 0]] - expected).abs() < 1e-12);
630 }
631
632 #[test]
633 fn gated_feed_forward_round_trips_to_model_dim() {
634 let mut ffn =
635 FeedForwardNetwork::<f64>::with_dimensions(4, 8, ActivationFunction::GeGLU, 0.0, 1)
636 .expect("ffn creation");
637 let input = Array2::<f64>::from_shape_fn((3, 4), |(i, j)| (i + j) as f64 * 0.1);
638 let output = ffn.forward(&input).expect("forward");
639 assert_eq!(output.shape(), &[3, 4]);
640 }
641
642 #[test]
643 fn switching_to_a_gated_activation_resizes_the_output_layer() {
644 let mut ffn =
645 FeedForwardNetwork::<f64>::with_dimensions(4, 8, ActivationFunction::GELU, 0.0, 1)
646 .expect("ffn creation");
647 ffn.set_activation(ActivationFunction::GLU)
648 .expect("activation switch");
649 let input = Array2::<f64>::ones((2, 4));
650 let output = ffn.forward(&input).expect("forward");
651 assert_eq!(output.shape(), &[2, 4]);
652 }
653
654 #[test]
655 fn odd_hidden_width_rejects_gated_activation() {
656 let result =
657 FeedForwardNetwork::<f64>::with_dimensions(4, 7, ActivationFunction::GLU, 0.0, 1);
658 assert!(result.is_err());
659 }
660
661 #[test]
662 fn dropout_is_identity_at_inference_and_active_in_training() {
663 let mut dropout = DropoutLayer::with_seed(0.5, 42);
664 let input = Array2::<f64>::ones((8, 8));
665
666 assert!(!dropout.is_training());
668 assert_eq!(dropout.forward(&input), input);
669
670 dropout.set_training(true);
671 let output = dropout.forward(&input);
672 let zeros = output.iter().filter(|&&v| v == 0.0).count();
673 assert!(zeros > 0, "training-mode dropout dropped nothing");
674 assert!(
675 zeros < input.len(),
676 "training-mode dropout dropped everything"
677 );
678 assert!(output.iter().all(|&v| v == 0.0 || (v - 2.0).abs() < 1e-12));
680 }
681
682 #[test]
683 fn dropout_is_reproducible_for_a_given_seed() {
684 let input = Array2::<f64>::ones((4, 4));
685 let run = || {
686 let mut dropout = DropoutLayer::with_seed(0.3, 7);
687 dropout.set_training(true);
688 dropout.forward(&input)
689 };
690 assert_eq!(run(), run());
691 }
692
693 #[test]
694 fn layer_norm_rejects_mismatched_width() {
695 let norm = LayerNorm::<f64>::with_default_eps(8);
696 let input = Array2::<f64>::zeros((2, 4));
697 assert!(norm.forward(&input).is_err());
698 }
699
700 #[test]
701 fn layer_norm_standardizes_rows() {
702 let norm = LayerNorm::<f64>::with_default_eps(4);
703 let input =
704 Array2::<f64>::from_shape_vec((1, 4), vec![1.0, 2.0, 3.0, 4.0]).expect("valid shape");
705 let output = norm.forward(&input).expect("forward");
706 let mean: f64 = output.row(0).iter().sum::<f64>() / 4.0;
707 assert!(mean.abs() < 1e-9, "mean {mean}");
708 }
709}