Skip to main content

eredu_core/
attention.rs

1//! Architecture-neutral decoder layer schedules and attention geometry.
2
3use serde::{Deserialize, Serialize};
4use std::{collections::BTreeMap, num::NonZeroU32};
5
6/// Attention behavior for one decoder layer.
7#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum AttentionPolicy {
10    /// Attend to the complete causal prefix.
11    Full,
12    /// Attend to at most `window` positions, including the current token.
13    Sliding {
14        /// Exact positive number of visible positions.
15        window: NonZeroU32,
16    },
17}
18
19impl AttentionPolicy {
20    /// Creates a sliding policy from a positive window.
21    pub fn sliding(window: u32) -> Result<Self, LayerScheduleError> {
22        let window = NonZeroU32::new(window).ok_or(LayerScheduleError::ZeroWindow)?;
23        Ok(Self::Sliding { window })
24    }
25
26    /// Converts an optional signed runtime window into an exact attention policy.
27    pub fn from_sliding_window(window: Option<i32>) -> Result<Self, LayerScheduleError> {
28        match window {
29            None => Ok(Self::Full),
30            Some(window) if window <= 0 => Err(LayerScheduleError::ZeroWindow),
31            Some(window) => Self::sliding(window as u32),
32        }
33    }
34
35    /// Returns the exact signed runtime window, rejecting values outside `i32`.
36    pub fn sliding_window_i32(self) -> Result<Option<i32>, LayerScheduleError> {
37        self.window()
38            .map(|window| {
39                i32::try_from(window.get()).map_err(|_| LayerScheduleError::WindowOutOfRange {
40                    window: window.get(),
41                })
42            })
43            .transpose()
44    }
45
46    /// Returns the sliding window, or `None` for full attention.
47    pub const fn window(self) -> Option<NonZeroU32> {
48        match self {
49            Self::Full => None,
50            Self::Sliding { window } => Some(window),
51        }
52    }
53}
54
55/// Validated, ordered policy for every decoder layer.
56#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
57#[serde(transparent)]
58pub struct LayerSchedule<P> {
59    layers: Box<[P]>,
60}
61
62impl<P> LayerSchedule<P> {
63    /// Validates an exact ordered policy list against the decoder layer count.
64    pub fn new(layer_count: usize, layers: Vec<P>) -> Result<Self, LayerScheduleError> {
65        if layer_count == 0 {
66            return Err(LayerScheduleError::Empty);
67        }
68        if layers.len() != layer_count {
69            return Err(LayerScheduleError::LayerCount {
70                expected: layer_count,
71                actual: layers.len(),
72            });
73        }
74        Ok(Self {
75            layers: layers.into_boxed_slice(),
76        })
77    }
78
79    /// Returns the number of decoder layers represented by the schedule.
80    pub const fn len(&self) -> usize {
81        self.layers.len()
82    }
83    /// Returns whether the schedule contains no layers.
84    pub const fn is_empty(&self) -> bool {
85        self.layers.is_empty()
86    }
87    /// Returns one layer policy, with out-of-range indices reported as `None`.
88    pub fn get(&self, layer: usize) -> Option<&P> {
89        self.layers.get(layer)
90    }
91    /// Iterates over policies in architecture layer order.
92    pub fn iter(&self) -> impl ExactSizeIterator<Item = &P> + '_ {
93        self.layers.iter()
94    }
95}
96
97impl LayerSchedule<AttentionPolicy> {
98    /// Creates an all-full attention schedule.
99    pub fn all_full(layer_count: usize) -> Result<Self, LayerScheduleError> {
100        Self::new(layer_count, vec![AttentionPolicy::Full; layer_count])
101    }
102    /// Creates an all-sliding attention schedule.
103    pub fn all_sliding(layer_count: usize, window: u32) -> Result<Self, LayerScheduleError> {
104        Self::new(
105            layer_count,
106            vec![AttentionPolicy::sliding(window)?; layer_count],
107        )
108    }
109    /// Creates a schedule from a Boolean pattern where `true` means sliding.
110    pub fn from_sliding_pattern(
111        layer_count: usize,
112        pattern: &[bool],
113        window: Option<u32>,
114    ) -> Result<Self, LayerScheduleError> {
115        if pattern.len() != layer_count {
116            return Err(LayerScheduleError::LayerCount {
117                expected: layer_count,
118                actual: pattern.len(),
119            });
120        }
121        let policy = match (pattern.iter().any(|value| *value), window) {
122            (true, Some(window)) => Some(AttentionPolicy::sliding(window)?),
123            (true, None) => return Err(LayerScheduleError::MissingWindow),
124            (false, _) => None,
125        };
126        Self::new(
127            layer_count,
128            pattern
129                .iter()
130                .map(|enabled| {
131                    if *enabled {
132                        policy.expect("validated")
133                    } else {
134                        AttentionPolicy::Full
135                    }
136                })
137                .collect(),
138        )
139    }
140    /// Returns the number of full-attention layers.
141    pub fn full_layer_count(&self) -> usize {
142        self.layers
143            .iter()
144            .filter(|p| matches!(p, AttentionPolicy::Full))
145            .count()
146    }
147    /// Returns the number of sliding-attention layers.
148    pub fn sliding_layer_count(&self) -> usize {
149        self.len() - self.full_layer_count()
150    }
151    /// Counts sliding layers by exact window.
152    pub fn sliding_windows(&self) -> BTreeMap<NonZeroU32, usize> {
153        let mut result = BTreeMap::new();
154        for window in self.iter().copied().filter_map(AttentionPolicy::window) {
155            *result.entry(window).or_default() += 1;
156        }
157        result
158    }
159    /// Returns a stable representation suitable for fingerprints.
160    pub fn fingerprint_component(&self) -> String {
161        self.iter()
162            .map(|policy| match policy {
163                AttentionPolicy::Full => "f".into(),
164                AttentionPolicy::Sliding { window } => format!("s{}", window.get()),
165            })
166            .collect::<Vec<_>>()
167            .join(",")
168    }
169}
170
171/// Validation error for an ordered per-layer schedule.
172#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
173pub enum LayerScheduleError {
174    /// A decoder-layer schedule cannot be empty.
175    #[error("layer schedule must contain at least one layer")]
176    Empty,
177    /// The supplied policy count differs from decoder depth.
178    #[error("layer schedule has {actual} entries for {expected} decoder layers")]
179    LayerCount {
180        /// Decoder layer count.
181        expected: usize,
182        /// Supplied policy count.
183        actual: usize,
184    },
185    /// A sliding window was zero.
186    #[error("sliding attention window must be positive")]
187    ZeroWindow,
188    /// Sliding attention was enabled without a window.
189    #[error("sliding attention is enabled for at least one layer without a window")]
190    MissingWindow,
191    /// A window cannot be represented by runtime cache APIs.
192    #[error("sliding attention window {window} exceeds i32")]
193    WindowOutOfRange {
194        /// Unrepresentable window.
195        window: u32,
196    },
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    #[test]
203    fn validates_attention_schedule() {
204        let schedule = LayerSchedule::new(
205            3,
206            vec![
207                AttentionPolicy::Full,
208                AttentionPolicy::sliding(8).unwrap(),
209                AttentionPolicy::Full,
210            ],
211        )
212        .unwrap();
213        assert_eq!(schedule.fingerprint_component(), "f,s8,f");
214        assert_eq!(schedule.sliding_layer_count(), 1);
215        assert!(LayerSchedule::from_sliding_pattern(2, &[true], Some(4)).is_err());
216    }
217}