Skip to main content

optirs_learned/transformer_based_optimizer/
architecture.rs

1// Core transformer architecture implementation
2
3use super::attention::MultiHeadAttention;
4use super::config::TransformerArchConfig;
5use super::feedforward::{FeedForwardCache, FeedForwardNetwork};
6use super::layers::{
7    DropoutLayer, EmbeddingLayer, LayerNormalization, OutputProjection, ResidualConnections,
8};
9use crate::error::Result;
10use scirs2_core::ndarray::{Array2, Array3};
11use scirs2_core::numeric::Float;
12use std::fmt::Debug;
13
14/// Activations cached during a training forward pass so that
15/// [`TransformerArchitecture::backward`] can compute gradients without
16/// recomputing the forward pass.
17#[derive(Debug, Clone)]
18pub struct ArchitectureCache<
19    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
20> {
21    /// Raw input to the embedding projection
22    pub embedding_input: Array2<T>,
23
24    /// Per-layer caches, in forward order
25    pub layers: Vec<LayerCache<T>>,
26
27    /// Input to the final layer normalization
28    pub final_norm_input: Array2<T>,
29
30    /// Input to the output projection
31    pub projection_input: Array2<T>,
32}
33
34/// Activations cached for a single transformer layer.
35#[derive(Debug, Clone)]
36pub struct LayerCache<
37    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
38> {
39    /// Input to the layer
40    pub input: Array2<T>,
41
42    /// Output of the attention sub-layer including its residual
43    pub attention_residual: Array2<T>,
44
45    /// Input to the feed-forward normalization
46    pub ff_norm_input: Array2<T>,
47
48    /// Activations of the feed-forward sub-layer
49    pub feed_forward: FeedForwardCache<T>,
50}
51
52/// Core transformer architecture
53pub struct TransformerArchitecture<
54    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
55> {
56    /// Transformer layers
57    layers: Vec<TransformerLayer<T>>,
58
59    /// Input embedding
60    input_embedding: EmbeddingLayer<T>,
61
62    /// Output projection
63    output_projection: OutputProjection<T>,
64
65    /// Layer normalization
66    layer_norm: LayerNormalization<T>,
67
68    /// Dropout for regularization
69    dropout: DropoutLayer,
70
71    /// Architecture configuration
72    config: TransformerArchConfig,
73}
74
75impl<T: Float + Debug + 'static + scirs2_core::ndarray::ScalarOperand + Send + Sync>
76    TransformerArchitecture<T>
77{
78    /// Create new transformer architecture
79    pub fn new(config: TransformerArchConfig) -> Result<Self> {
80        let mut layers = Vec::new();
81
82        for layer_idx in 0..config.num_layers {
83            let layer = TransformerLayer::new(
84                config.model_dimension,
85                config.num_attention_heads,
86                config.feedforward_dimension,
87                config.dropout_rate,
88                config.use_pre_norm,
89                layer_idx,
90            )?;
91            layers.push(layer);
92        }
93
94        let input_embedding = EmbeddingLayer::new(
95            config.model_dimension,
96            config.model_dimension, // Vocab size equals model dimension for continuous inputs
97        )?;
98
99        let output_projection =
100            OutputProjection::new(config.model_dimension, config.model_dimension)?;
101
102        let layer_norm = LayerNormalization::new(config.model_dimension)?;
103        let dropout = DropoutLayer::new(config.dropout_rate);
104
105        Ok(Self {
106            layers,
107            input_embedding,
108            output_projection,
109            layer_norm,
110            dropout,
111            config,
112        })
113    }
114
115    /// Forward pass through the transformer
116    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
117        // Input embedding
118        let mut hidden_states = self.input_embedding.forward(input)?;
119
120        // Apply input dropout
121        hidden_states = self.dropout.forward(&hidden_states);
122
123        // Forward through transformer layers
124        for layer in &mut self.layers {
125            hidden_states = layer.forward(&hidden_states)?;
126        }
127
128        // Final layer normalization
129        hidden_states = self.layer_norm.forward(&hidden_states)?;
130
131        // Output projection
132        let output = self.output_projection.forward(&hidden_states)?;
133
134        Ok(output)
135    }
136
137    /// Get number of parameters
138    pub fn parameter_count(&self) -> usize {
139        let mut count = 0;
140
141        count += self.input_embedding.parameter_count();
142        count += self.output_projection.parameter_count();
143        count += self.layer_norm.parameter_count();
144
145        for layer in &self.layers {
146            count += layer.parameter_count();
147        }
148
149        count
150    }
151
152    /// Reset all layers
153    pub fn reset(&mut self) -> Result<()> {
154        self.input_embedding.reset()?;
155        self.output_projection.reset()?;
156        self.layer_norm.reset()?;
157
158        for layer in &mut self.layers {
159            layer.reset()?;
160        }
161
162        Ok(())
163    }
164
165    /// Get configuration
166    pub fn get_config(&self) -> &TransformerArchConfig {
167        &self.config
168    }
169
170    /// Switch every dropout in the architecture between training and inference.
171    pub fn set_training(&mut self, training: bool) {
172        self.dropout.set_training(training);
173        for layer in &mut self.layers {
174            layer.set_training(training);
175        }
176    }
177
178    /// Forward pass that records the activations needed for [`Self::backward`].
179    ///
180    /// Dropout is bypassed so that the cached activations correspond exactly to
181    /// the values the gradients are computed against.
182    pub fn forward_with_cache(
183        &mut self,
184        input: &Array2<T>,
185    ) -> Result<(Array2<T>, ArchitectureCache<T>)> {
186        let embedding_input = input.clone();
187        let mut hidden_states = self.input_embedding.forward(input)?;
188
189        let mut layer_caches = Vec::with_capacity(self.layers.len());
190        for layer in &mut self.layers {
191            let (next, cache) = layer.forward_with_cache(&hidden_states)?;
192            layer_caches.push(cache);
193            hidden_states = next;
194        }
195
196        let final_norm_input = hidden_states.clone();
197        let projection_input = self.layer_norm.forward(&hidden_states)?;
198        let output = self.output_projection.forward(&projection_input)?;
199
200        Ok((
201            output,
202            ArchitectureCache {
203                embedding_input,
204                layers: layer_caches,
205                final_norm_input,
206                projection_input,
207            },
208        ))
209    }
210
211    /// Backward pass over the readout, feed-forward and embedding parameters.
212    ///
213    /// Gradients flow exactly through the output projection, the final layer
214    /// normalization, each layer's feed-forward sub-layer (with its own layer
215    /// normalization) and the input projection. The attention sub-layer is
216    /// treated as a first-order (residual-path) pass-through: its identity
217    /// branch carries the gradient exactly, while the attention Jacobian itself
218    /// is not differentiated, so the attention projections stay frozen. This is
219    /// the first-order approximation the module documents; end-to-end
220    /// differentiation of the attention weights is not implemented.
221    ///
222    /// Returns the total squared gradient magnitude that was applied, which is
223    /// zero exactly when the incoming gradient is zero.
224    pub fn backward(
225        &mut self,
226        cache: &ArchitectureCache<T>,
227        grad_output: &Array2<T>,
228        learning_rate: T,
229    ) -> Result<T> {
230        let mut grad =
231            self.output_projection
232                .backward(&cache.projection_input, grad_output, learning_rate)?;
233        grad = self
234            .layer_norm
235            .backward(&cache.final_norm_input, &grad, learning_rate)?;
236
237        for (layer, layer_cache) in self.layers.iter_mut().rev().zip(cache.layers.iter().rev()) {
238            grad = layer.backward(layer_cache, &grad, learning_rate)?;
239        }
240
241        let grad_embedding =
242            self.input_embedding
243                .backward(&cache.embedding_input, &grad, learning_rate)?;
244
245        Ok(grad_embedding
246            .iter()
247            .map(|&x| x * x)
248            .fold(T::zero(), |a, b| a + b))
249    }
250}
251
252/// Individual transformer layer
253pub struct TransformerLayer<
254    T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static,
255> {
256    /// Multi-head self-attention
257    self_attention: MultiHeadAttention<T>,
258
259    /// Feed-forward network
260    feed_forward: FeedForwardNetwork<T>,
261
262    /// Layer normalization (pre-norm style)
263    pre_norm1: LayerNormalization<T>,
264    pre_norm2: LayerNormalization<T>,
265
266    /// Post-norm layers (if not using pre-norm)
267    post_norm1: Option<LayerNormalization<T>>,
268    post_norm2: Option<LayerNormalization<T>>,
269
270    /// Residual connections
271    residual_connections: ResidualConnections<T>,
272
273    /// Layer-specific dropout
274    dropout: DropoutLayer,
275
276    /// Layer configuration
277    use_pre_norm: bool,
278    layer_index: usize,
279}
280
281impl<T: Float + Debug + scirs2_core::ndarray::ScalarOperand + Send + Sync + 'static>
282    TransformerLayer<T>
283{
284    /// Create new transformer layer
285    pub fn new(
286        model_dimension: usize,
287        num_attention_heads: usize,
288        feedforward_dimension: usize,
289        dropout_rate: f64,
290        use_pre_norm: bool,
291        layer_index: usize,
292    ) -> Result<Self> {
293        let self_attention = MultiHeadAttention::new(
294            num_attention_heads,
295            model_dimension,
296            model_dimension / num_attention_heads,
297        )?;
298
299        let feed_forward = FeedForwardNetwork::new(
300            model_dimension,
301            feedforward_dimension,
302            super::config::ActivationFunction::ReLU,
303        )?;
304
305        let pre_norm1 = LayerNormalization::new(model_dimension)?;
306        let pre_norm2 = LayerNormalization::new(model_dimension)?;
307
308        let (post_norm1, post_norm2) = if !use_pre_norm {
309            (
310                Some(LayerNormalization::new(model_dimension)?),
311                Some(LayerNormalization::new(model_dimension)?),
312            )
313        } else {
314            (None, None)
315        };
316
317        let residual_connections = ResidualConnections::new(model_dimension);
318        let dropout = DropoutLayer::new(dropout_rate);
319
320        Ok(Self {
321            self_attention,
322            feed_forward,
323            pre_norm1,
324            pre_norm2,
325            post_norm1,
326            post_norm2,
327            residual_connections,
328            dropout,
329            use_pre_norm,
330            layer_index,
331        })
332    }
333
334    /// Forward pass through the layer
335    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
336        if self.use_pre_norm {
337            self.forward_pre_norm(input)
338        } else {
339            self.forward_post_norm(input)
340        }
341    }
342
343    /// Forward pass with pre-normalization
344    fn forward_pre_norm(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
345        // Self-attention sub-layer with pre-norm
346        let normed_input = self.pre_norm1.forward(input)?;
347        let attention_output =
348            self.self_attention
349                .forward(&normed_input, &normed_input, &normed_input)?;
350        let attention_output = self.dropout.forward(&attention_output);
351        let attention_residual = self.residual_connections.add(input, &attention_output)?;
352
353        // Feed-forward sub-layer with pre-norm
354        let normed_attention = self.pre_norm2.forward(&attention_residual)?;
355        let ff_output = self.feed_forward.forward(&normed_attention)?;
356        let ff_output = self.dropout.forward(&ff_output);
357        let ff_residual = self
358            .residual_connections
359            .add(&attention_residual, &ff_output)?;
360
361        Ok(ff_residual)
362    }
363
364    /// Forward pass with post-normalization
365    fn forward_post_norm(&mut self, input: &Array2<T>) -> Result<Array2<T>> {
366        // Self-attention sub-layer with post-norm
367        let attention_output = self.self_attention.forward(input, input, input)?;
368        let attention_output = self.dropout.forward(&attention_output);
369        let attention_residual = self.residual_connections.add(input, &attention_output)?;
370        let attention_normed = if let Some(ref mut post_norm1) = self.post_norm1 {
371            post_norm1.forward(&attention_residual)?
372        } else {
373            attention_residual
374        };
375
376        // Feed-forward sub-layer with post-norm
377        let ff_output = self.feed_forward.forward(&attention_normed)?;
378        let ff_output = self.dropout.forward(&ff_output);
379        let ff_residual = self
380            .residual_connections
381            .add(&attention_normed, &ff_output)?;
382        let ff_normed = if let Some(ref mut post_norm2) = self.post_norm2 {
383            post_norm2.forward(&ff_residual)?
384        } else {
385            ff_residual
386        };
387
388        Ok(ff_normed)
389    }
390
391    /// Get number of parameters in this layer
392    pub fn parameter_count(&self) -> usize {
393        let mut count = 0;
394
395        count += self.self_attention.parameter_count();
396        count += self.feed_forward.parameter_count();
397        count += self.pre_norm1.parameter_count();
398        count += self.pre_norm2.parameter_count();
399
400        if let Some(ref post_norm1) = self.post_norm1 {
401            count += post_norm1.parameter_count();
402        }
403
404        if let Some(ref post_norm2) = self.post_norm2 {
405            count += post_norm2.parameter_count();
406        }
407
408        count
409    }
410
411    /// Reset layer parameters
412    pub fn reset(&mut self) -> Result<()> {
413        self.self_attention.reset()?;
414        self.feed_forward.reset()?;
415        self.pre_norm1.reset()?;
416        self.pre_norm2.reset()?;
417
418        if let Some(ref mut post_norm1) = self.post_norm1 {
419            post_norm1.reset()?;
420        }
421
422        if let Some(ref mut post_norm2) = self.post_norm2 {
423            post_norm2.reset()?;
424        }
425
426        Ok(())
427    }
428
429    /// Get layer index
430    pub fn get_layer_index(&self) -> usize {
431        self.layer_index
432    }
433
434    /// Get attention weights (for visualization/analysis)
435    pub fn get_attention_weights(&self) -> Option<Array3<T>> {
436        self.self_attention.get_attention_weights()
437    }
438
439    /// Switch this layer's dropout between training and inference mode.
440    pub fn set_training(&mut self, training: bool) {
441        self.dropout.set_training(training);
442        self.feed_forward.set_training(training);
443    }
444
445    /// Forward pass that records the activations needed by [`Self::backward`].
446    ///
447    /// Always uses the pre-norm ordering for the cached path so the backward
448    /// pass has a single, well-defined graph to follow, and bypasses dropout.
449    pub fn forward_with_cache(&mut self, input: &Array2<T>) -> Result<(Array2<T>, LayerCache<T>)> {
450        let normed_input = self.pre_norm1.forward(input)?;
451        let attention_output =
452            self.self_attention
453                .forward(&normed_input, &normed_input, &normed_input)?;
454        let attention_residual = self.residual_connections.add(input, &attention_output)?;
455
456        let ff_norm_input = attention_residual.clone();
457        let normed_attention = self.pre_norm2.forward(&attention_residual)?;
458        let (ff_output, feed_forward) = self.feed_forward.forward_with_cache(&normed_attention)?;
459        let ff_residual = self
460            .residual_connections
461            .add(&attention_residual, &ff_output)?;
462
463        Ok((
464            ff_residual,
465            LayerCache {
466                input: input.clone(),
467                attention_residual,
468                ff_norm_input,
469                feed_forward,
470            },
471        ))
472    }
473
474    /// Backward pass for the feed-forward sub-layer and the residual path.
475    pub fn backward(
476        &mut self,
477        cache: &LayerCache<T>,
478        grad_output: &Array2<T>,
479        learning_rate: T,
480    ) -> Result<Array2<T>> {
481        // The FF residual adds the sub-layer output to its input, so the
482        // incoming gradient reaches both branches.
483        let grad_ff =
484            self.feed_forward
485                .backward(&cache.feed_forward, grad_output, learning_rate)?;
486        let grad_ff_norm =
487            self.pre_norm2
488                .backward(&cache.ff_norm_input, &grad_ff, learning_rate)?;
489        let grad_attention_residual = grad_output + &grad_ff_norm;
490
491        // First-order treatment of the attention sub-layer: only the identity
492        // branch of its residual connection carries gradient.
493        let _ = &cache.attention_residual;
494        Ok(grad_attention_residual)
495    }
496}
497
498/// Architecture statistics
499#[derive(Debug, Clone)]
500pub struct ArchitectureStats {
501    pub total_parameters: usize,
502    pub total_layers: usize,
503    pub model_dimension: usize,
504    pub attention_heads: usize,
505    pub memory_usage_mb: f64,
506}
507
508impl<T: Float + Debug + 'static + scirs2_core::ndarray::ScalarOperand + Send + Sync>
509    TransformerArchitecture<T>
510{
511    /// Get architecture statistics
512    pub fn get_stats(&self) -> ArchitectureStats {
513        let total_parameters = self.parameter_count();
514        let memory_usage_mb =
515            (total_parameters * std::mem::size_of::<T>()) as f64 / (1024.0 * 1024.0);
516
517        ArchitectureStats {
518            total_parameters,
519            total_layers: self.config.num_layers,
520            model_dimension: self.config.model_dimension,
521            attention_heads: self.config.num_attention_heads,
522            memory_usage_mb,
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use crate::transformer_based_optimizer::config::TransformerArchConfig;
531
532    fn create_test_config() -> TransformerArchConfig {
533        TransformerArchConfig {
534            model_dimension: 128,
535            num_layers: 2,
536            num_attention_heads: 4,
537            feedforward_dimension: 256,
538            dropout_rate: 0.1,
539            use_pre_norm: true,
540            enable_residual_connections: true,
541        }
542    }
543
544    #[test]
545    fn test_transformer_architecture_creation() {
546        let config = create_test_config();
547        let architecture = TransformerArchitecture::<f32>::new(config);
548        assert!(architecture.is_ok());
549
550        let arch = architecture.expect("TransformerArchitecture::new should succeed");
551        assert_eq!(arch.layers.len(), 2);
552        assert_eq!(arch.config.model_dimension, 128);
553    }
554
555    #[test]
556    fn test_transformer_layer_creation() {
557        let layer = TransformerLayer::<f32>::new(128, 4, 256, 0.1, true, 0);
558        assert!(layer.is_ok());
559
560        let l = layer.expect("TransformerLayer::new should succeed");
561        assert_eq!(l.layer_index, 0);
562        assert!(l.use_pre_norm);
563    }
564
565    #[test]
566    fn test_forward_pass() {
567        let config = create_test_config();
568        let mut architecture = TransformerArchitecture::<f32>::new(config)
569            .expect("TransformerArchitecture::new should succeed");
570
571        let input = Array2::<f32>::zeros((4, 128)); // (batch_size * seq_length, model_dimension) = (4, 128)
572        let result = architecture.forward(&input);
573        assert!(result.is_ok());
574
575        let output = result.expect("forward should succeed");
576        assert_eq!(output.shape(), &[4, 128]);
577    }
578
579    #[test]
580    fn test_parameter_count() {
581        let config = create_test_config();
582        let architecture = TransformerArchitecture::<f32>::new(config)
583            .expect("TransformerArchitecture::new should succeed");
584
585        let param_count = architecture.parameter_count();
586        assert!(param_count > 0);
587
588        let stats = architecture.get_stats();
589        assert_eq!(stats.total_parameters, param_count);
590        assert_eq!(stats.total_layers, 2);
591    }
592}