Skip to main content

optirs_learned/transformer/architecture/
positional_encoding.rs

1use std::fmt::Debug;
2// Positional encoding mechanisms for transformer optimization
3//
4// This module implements various positional encoding strategies used in the
5// transformer optimizer to provide position information to the attention mechanisms.
6
7use scirs2_core::ndarray::{s, Array1, Array2};
8use scirs2_core::numeric::Float;
9
10use super::super::TransformerOptimizerConfig;
11use crate::error::{OptimError, Result};
12
13/// Types of positional encoding
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PositionalEncodingType {
16    /// Sinusoidal position encoding
17    Sinusoidal,
18    /// Learned position embedding
19    Learned,
20    /// Rotary position embedding (RoPE)
21    Rotary,
22    /// Relative position encoding
23    Relative,
24    /// ALiBi (Attention with Linear Biases)
25    ALiBi,
26}
27
28/// Positional encoder for transformer inputs
29#[derive(Debug, Clone)]
30pub struct PositionalEncoder<T: Float + Debug + Send + Sync + 'static> {
31    /// Encoding type
32    encoding_type: PositionalEncodingType,
33
34    /// Cached encodings
35    cached_encodings: Option<Array2<T>>,
36
37    /// Maximum sequence length
38    max_seqlen: usize,
39
40    /// Model dimension
41    modeldim: usize,
42
43    /// Learned position embeddings (if applicable)
44    position_embeddings: Option<Array2<T>>,
45
46    /// ALiBi slopes (if applicable)
47    alibi_slopes: Option<Array1<T>>,
48}
49
50impl<T: Float + Debug + Default + Clone + Send + Sync + 'static> PositionalEncoder<T> {
51    /// Create new positional encoder
52    pub fn new(config: &TransformerOptimizerConfig) -> Result<Self> {
53        let max_seqlen = config.max_sequence_length;
54        let modeldim = config.modeldim;
55
56        let mut cached_encodings = None;
57        let mut position_embeddings = None;
58        let mut alibi_slopes = None;
59
60        match config.pos_encoding_type {
61            PositionalEncodingType::Sinusoidal => {
62                cached_encodings = Some(Self::sinusoidal_table(max_seqlen, modeldim));
63            }
64            PositionalEncodingType::Learned => {
65                // Initialize learnable position embeddings
66                let mut rng = scirs2_core::random::thread_rng();
67                let mut embeddings = Array2::zeros((max_seqlen, modeldim));
68
69                // Xavier initialization
70                let bound = (6.0 / (max_seqlen + modeldim) as f64).sqrt();
71                for elem in embeddings.iter_mut() {
72                    *elem = scirs2_core::numeric::NumCast::from(
73                        (rng.random::<f64>() - 0.5) * 2.0 * bound,
74                    )
75                    .unwrap_or_else(|| T::zero());
76                }
77                position_embeddings = Some(embeddings);
78            }
79            PositionalEncodingType::ALiBi => {
80                // Initialize ALiBi slopes
81                let numheads = config.numheads.max(1);
82                let mut slopes = Array1::zeros(numheads);
83
84                for h in 0..numheads {
85                    slopes[h] = scirs2_core::numeric::NumCast::from(
86                        2.0_f64.powf(-8.0 * (h + 1) as f64 / numheads as f64),
87                    )
88                    .unwrap_or_else(|| T::zero());
89                }
90                alibi_slopes = Some(slopes);
91            }
92            _ => {
93                // Rotary and relative encodings modify the attention scores
94                // rather than the inputs, but a sinusoidal table is still
95                // precomputed so `compute_sinusoidal_position` stays usable.
96                cached_encodings = Some(Self::sinusoidal_table(max_seqlen, modeldim));
97            }
98        }
99
100        Ok(Self {
101            encoding_type: config.pos_encoding_type,
102            cached_encodings,
103            max_seqlen,
104            modeldim,
105            position_embeddings,
106            alibi_slopes,
107        })
108    }
109
110    /// Angular frequency for dimension `i` of a `modeldim`-wide sinusoidal
111    /// encoding: `1 / 10000^(2 * floor(i / 2) / d)`.
112    ///
113    /// The `floor(i / 2)` is what makes `(2k, 2k+1)` a genuine
114    /// `(sin, cos)` pair at the *same* frequency, as in Vaswani et al. (2017).
115    /// Using `i` directly - as the previous implementation did - gave the sine
116    /// and cosine halves of each pair different frequencies, so the encoding
117    /// was not a rotation and adjacent dimensions carried unrelated phases.
118    fn inverse_frequency(i: usize, modeldim: usize) -> f64 {
119        let exponent = 2.0 * ((i / 2) as f64) / modeldim.max(1) as f64;
120        1.0 / 10000.0_f64.powf(exponent)
121    }
122
123    /// Build the full sinusoidal encoding table.
124    fn sinusoidal_table(max_seqlen: usize, modeldim: usize) -> Array2<T> {
125        let mut encodings = Array2::zeros((max_seqlen, modeldim));
126
127        for pos in 0..max_seqlen {
128            for i in 0..modeldim {
129                let angle = pos as f64 * Self::inverse_frequency(i, modeldim);
130                let value = if i % 2 == 0 { angle.sin() } else { angle.cos() };
131                encodings[[pos, i]] =
132                    scirs2_core::numeric::NumCast::from(value).unwrap_or_else(|| T::zero());
133            }
134        }
135
136        encodings
137    }
138
139    /// Encode input with positional information
140    pub fn encode(&self, input: &Array2<T>) -> Result<Array2<T>> {
141        let (seq_len, modeldim) = input.dim();
142
143        if seq_len > self.max_seqlen {
144            return Err(OptimError::InvalidConfig(format!(
145                "Sequence length {} exceeds maximum {}",
146                seq_len, self.max_seqlen
147            )));
148        }
149
150        if modeldim != self.modeldim {
151            return Err(OptimError::InvalidConfig(format!(
152                "Model dimension {} doesn't match expected {}",
153                modeldim, self.modeldim
154            )));
155        }
156
157        let mut output = input.clone();
158
159        match self.encoding_type {
160            PositionalEncodingType::Sinusoidal => {
161                if let Some(ref encodings) = self.cached_encodings {
162                    let pos_enc = encodings.slice(s![..seq_len, ..]);
163                    output = output + pos_enc;
164                }
165            }
166            PositionalEncodingType::Learned => {
167                if let Some(ref embeddings) = self.position_embeddings {
168                    let pos_emb = embeddings.slice(s![..seq_len, ..]);
169                    output = output + pos_emb;
170                }
171            }
172            PositionalEncodingType::Rotary => {
173                // Rotary position embedding (RoPE) doesn't add to input,
174                // it modifies attention computation
175                // For now, just return input unchanged
176            }
177            PositionalEncodingType::Relative => {
178                // Relative position encoding doesn't add to input,
179                // it modifies attention computation
180                // For now, just return input unchanged
181            }
182            PositionalEncodingType::ALiBi => {
183                // ALiBi doesn't add to input, it modifies attention scores
184                // For now, just return input unchanged
185            }
186        }
187
188        Ok(output)
189    }
190
191    /// Get ALiBi slopes for attention bias calculation
192    pub fn get_alibi_slopes(&self) -> Option<&Array1<T>> {
193        self.alibi_slopes.as_ref()
194    }
195
196    /// Get encoding type
197    pub fn encoding_type(&self) -> PositionalEncodingType {
198        self.encoding_type
199    }
200
201    /// Get maximum sequence length
202    pub fn max_sequence_length(&self) -> usize {
203        self.max_seqlen
204    }
205
206    /// Get model dimension
207    pub fn model_dimension(&self) -> usize {
208        self.modeldim
209    }
210
211    /// Update position embeddings (for learned encoding)
212    pub fn update_embeddings(&mut self, new_embeddings: Array2<T>) -> Result<()> {
213        match self.encoding_type {
214            PositionalEncodingType::Learned => {
215                let (pos_len, model_dim) = new_embeddings.dim();
216                if pos_len != self.max_seqlen || model_dim != self.modeldim {
217                    return Err(OptimError::InvalidConfig(
218                        "New embeddings dimensions don't match encoder configuration".to_string(),
219                    ));
220                }
221                self.position_embeddings = Some(new_embeddings);
222                Ok(())
223            }
224            _ => Err(OptimError::InvalidConfig(
225                "Position embeddings can only be updated for learned encoding type".to_string(),
226            )),
227        }
228    }
229
230    /// Compute sinusoidal encoding for a specific position
231    pub fn compute_sinusoidal_position(&self, position: usize) -> Result<Array1<T>> {
232        if position >= self.max_seqlen {
233            return Err(OptimError::InvalidConfig(
234                "Position exceeds maximum sequence length".to_string(),
235            ));
236        }
237
238        let mut encoding = Array1::zeros(self.modeldim);
239        for i in 0..self.modeldim {
240            let angle = position as f64 * Self::inverse_frequency(i, self.modeldim);
241            let value = if i % 2 == 0 { angle.sin() } else { angle.cos() };
242            encoding[i] = scirs2_core::numeric::NumCast::from(value).unwrap_or_else(|| T::zero());
243        }
244
245        Ok(encoding)
246    }
247
248    /// Apply ALiBi bias to attention scores
249    pub fn apply_alibi_bias(
250        &self,
251        attention_scores: &mut Array2<T>,
252        head_idx: usize,
253    ) -> Result<()> {
254        if self.encoding_type != PositionalEncodingType::ALiBi {
255            return Ok(()); // No-op for non-ALiBi encoding
256        }
257
258        if let Some(ref slopes) = self.alibi_slopes {
259            if head_idx >= slopes.len() {
260                return Err(OptimError::InvalidConfig(
261                    "Head index exceeds number of ALiBi slopes".to_string(),
262                ));
263            }
264
265            let slope = slopes[head_idx];
266            let (rows, cols) = attention_scores.dim();
267
268            for i in 0..rows {
269                for j in 0..cols {
270                    let distance: T =
271                        scirs2_core::numeric::NumCast::from((i as i64 - j as i64).abs() as f64)
272                            .unwrap_or_else(|| T::zero());
273                    attention_scores[[i, j]] = attention_scores[[i, j]] - slope * distance;
274                }
275            }
276        }
277
278        Ok(())
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::transformer::TransformerOptimizerConfig;
286
287    fn config(encoding: PositionalEncodingType) -> TransformerOptimizerConfig {
288        TransformerOptimizerConfig {
289            modeldim: 8,
290            numheads: 2,
291            max_sequence_length: 16,
292            pos_encoding_type: encoding,
293            ..Default::default()
294        }
295    }
296
297    #[test]
298    fn sinusoidal_pairs_share_a_frequency() {
299        let encoder = PositionalEncoder::<f64>::new(&config(PositionalEncodingType::Sinusoidal))
300            .expect("encoder creation");
301        let encoding = encoder
302            .compute_sinusoidal_position(3)
303            .expect("position encoding");
304
305        // d = 8; dimension pair (2, 3) shares the frequency 1 / 10000^(2/8).
306        let omega = 1.0 / 10000.0_f64.powf(2.0 * 1.0 / 8.0);
307        assert!((encoding[2] - (3.0 * omega).sin()).abs() < 1e-12);
308        assert!((encoding[3] - (3.0 * omega).cos()).abs() < 1e-12);
309
310        // sin^2 + cos^2 == 1 holds exactly for a genuine (sin, cos) pair.
311        assert!((encoding[2] * encoding[2] + encoding[3] * encoding[3] - 1.0).abs() < 1e-12);
312    }
313
314    #[test]
315    fn position_zero_is_alternating_zero_one() {
316        let encoder = PositionalEncoder::<f64>::new(&config(PositionalEncodingType::Sinusoidal))
317            .expect("encoder creation");
318        let encoding = encoder
319            .compute_sinusoidal_position(0)
320            .expect("position encoding");
321        for i in 0..8 {
322            let expected = if i % 2 == 0 { 0.0 } else { 1.0 };
323            assert!((encoding[i] - expected).abs() < 1e-12, "dim {i}");
324        }
325    }
326
327    #[test]
328    fn encode_adds_the_cached_table() {
329        let encoder = PositionalEncoder::<f64>::new(&config(PositionalEncodingType::Sinusoidal))
330            .expect("encoder creation");
331        let input = Array2::<f64>::zeros((4, 8));
332        let encoded = encoder.encode(&input).expect("encode");
333        let reference = encoder
334            .compute_sinusoidal_position(2)
335            .expect("position encoding");
336        for i in 0..8 {
337            assert!((encoded[[2, i]] - reference[i]).abs() < 1e-12);
338        }
339    }
340
341    #[test]
342    fn alibi_bias_penalizes_distance() {
343        let encoder = PositionalEncoder::<f64>::new(&config(PositionalEncodingType::ALiBi))
344            .expect("encoder creation");
345        let mut scores = Array2::<f64>::zeros((4, 4));
346        encoder
347            .apply_alibi_bias(&mut scores, 0)
348            .expect("alibi bias");
349        assert_eq!(scores[[0, 0]], 0.0);
350        assert!(
351            scores[[0, 3]] < scores[[0, 1]],
352            "farther must be penalized more"
353        );
354    }
355
356    #[test]
357    fn oversized_sequences_are_rejected() {
358        let encoder = PositionalEncoder::<f64>::new(&config(PositionalEncodingType::Sinusoidal))
359            .expect("encoder creation");
360        let input = Array2::<f64>::zeros((64, 8));
361        assert!(encoder.encode(&input).is_err());
362    }
363}