Skip to main content

eredu_runtime/
inspection.rs

1//! Backend-neutral activation, target-state, and routed-expert observation contracts.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5/// One ordinary block output selected for a target/draft consumer.
6pub struct TargetStateTap<'a, T> {
7    /// Architecture block ordinal.
8    pub layer: usize,
9    /// Backend-native block output.
10    pub value: &'a T,
11}
12
13/// Ordered target-state capture without backend storage or family policy.
14#[derive(Debug, Clone)]
15pub struct TargetStateCapture<T> {
16    requested: Vec<usize>,
17    captured: BTreeMap<usize, T>,
18}
19
20impl<T> TargetStateCapture<T> {
21    /// Creates a capture plan with exact, ordered layer identities.
22    pub fn new(
23        requested: impl IntoIterator<Item = usize>,
24    ) -> Result<Self, TargetStateCaptureError> {
25        let requested = requested.into_iter().collect::<Vec<_>>();
26        if requested.is_empty() {
27            return Err(TargetStateCaptureError::Empty);
28        }
29        let mut unique = BTreeSet::new();
30        if let Some(duplicate) = requested.iter().find(|layer| !unique.insert(**layer)) {
31            return Err(TargetStateCaptureError::DuplicateRequest(*duplicate));
32        }
33        Ok(Self {
34            requested,
35            captured: BTreeMap::new(),
36        })
37    }
38
39    /// Returns whether this plan requests one block output.
40    pub fn wants(&self, layer: usize) -> bool {
41        self.requested.contains(&layer)
42    }
43
44    /// Captures one requested block output exactly once.
45    pub fn capture(&mut self, tap: TargetStateTap<'_, T>) -> Result<(), TargetStateCaptureError>
46    where
47        T: Clone,
48    {
49        if !self.wants(tap.layer) {
50            return Err(TargetStateCaptureError::Unrequested(tap.layer));
51        }
52        if self.captured.insert(tap.layer, tap.value.clone()).is_some() {
53            return Err(TargetStateCaptureError::DuplicateCapture(tap.layer));
54        }
55        Ok(())
56    }
57
58    /// Returns captured values in declared request order, rejecting omissions.
59    pub fn into_ordered(mut self) -> Result<Vec<T>, TargetStateCaptureError> {
60        let mut ordered = Vec::with_capacity(self.requested.len());
61        for layer in self.requested {
62            ordered.push(
63                self.captured
64                    .remove(&layer)
65                    .ok_or(TargetStateCaptureError::Missing(layer))?,
66            );
67        }
68        Ok(ordered)
69    }
70}
71
72/// Invalid target-state capture lifecycle.
73#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
74pub enum TargetStateCaptureError {
75    /// At least one state tap must be requested.
76    #[error("target-state capture requires at least one layer")]
77    Empty,
78    /// One layer was requested more than once.
79    #[error("target-state layer {0} was requested more than once")]
80    DuplicateRequest(usize),
81    /// A block emitted a state that was not requested.
82    #[error("target-state layer {0} was not requested")]
83    Unrequested(usize),
84    /// A requested state was captured more than once.
85    #[error("target-state layer {0} was captured more than once")]
86    DuplicateCapture(usize),
87    /// A requested block never emitted its output.
88    #[error("target-state layer {0} was not captured")]
89    Missing(usize),
90}
91
92/// Normalized routed-expert data emitted by architecture implementations.
93pub struct RoutingObservation<'a, T> {
94    /// Stable path-like name of the routed block.
95    pub path: &'a str,
96    /// Selected expert IDs shaped `[..., top_k]`.
97    pub selected_experts: &'a T,
98    /// Selected scores before optional top-k renormalization.
99    pub selected_scores: &'a T,
100    /// Final route weights applied to expert outputs.
101    pub coefficients: &'a T,
102    /// Combined routed expert contribution.
103    pub routed_output: &'a T,
104    /// Rank-local contribution before expert-parallel reduction.
105    pub local_routed_output: Option<&'a T>,
106    /// Globally reduced expert-parallel contribution.
107    pub reduced_routed_output: Option<&'a T>,
108    /// Shared-expert contribution when the architecture has one.
109    pub shared_output: Option<&'a T>,
110    /// Combined routed and shared contribution when reported separately.
111    pub combined_output: Option<&'a T>,
112    /// Total number of routed experts.
113    pub expert_count: i32,
114}
115
116/// Statically dispatched activation observation and intervention contract.
117pub trait ActivationObserver<T, E> {
118    /// Observes a named backend-native tensor.
119    fn observe(&mut self, path: &str, value: &T) -> Result<(), E>;
120
121    /// Optionally replaces an activation before it is consumed or returned.
122    fn intervene(&mut self, _path: &str, _value: &T) -> Result<Option<T>, E> {
123        Ok(None)
124    }
125
126    /// Observes normalized routed-expert decisions and contributions.
127    fn observe_routing(&mut self, _routing: RoutingObservation<'_, T>) -> Result<(), E> {
128        Ok(())
129    }
130}
131
132/// Observes an activation and applies an optional replacement without
133/// materializing its backend-native value.
134pub fn observe_and_intervene<T, E, O>(observer: &mut O, path: &str, value: &T) -> Result<T, E>
135where
136    T: Clone,
137    O: ActivationObserver<T, E> + ?Sized,
138{
139    observer.observe(path, value)?;
140    Ok(observer
141        .intervene(path, value)?
142        .unwrap_or_else(|| value.clone()))
143}
144
145/// Observes final model logits and applies an optional replacement.
146///
147/// Family and topology adapters must return this value, rather than merely
148/// reporting [`eredu_core::MODEL_LOGITS_OBSERVATION_PATH`], so final-output
149/// intervention has the same semantics as every other activation point.
150pub fn observe_model_logits<T, E, O>(observer: &mut O, logits: &T) -> Result<T, E>
151where
152    T: Clone,
153    O: ActivationObserver<T, E> + ?Sized,
154{
155    observe_and_intervene(observer, eredu_core::MODEL_LOGITS_OBSERVATION_PATH, logits)
156}
157
158/// Zero-sized observer used by the ordinary unobserved inference path.
159#[derive(Debug, Default, Clone, Copy)]
160pub struct NoopObserver;
161
162impl<T, E> ActivationObserver<T, E> for NoopObserver {
163    fn observe(&mut self, _path: &str, _value: &T) -> Result<(), E> {
164        Ok(())
165    }
166}
167
168impl<T, E, F> ActivationObserver<T, E> for F
169where
170    F: FnMut(&str, &T) -> Result<(), E>,
171{
172    fn observe(&mut self, path: &str, value: &T) -> Result<(), E> {
173        self(path, value)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn noop_observer_is_static_and_passthrough() {
183        fn observe<O: ActivationObserver<i32, ()>>(observer: &mut O) {
184            observer.observe("layer.output", &7).unwrap();
185            assert_eq!(observer.intervene("layer.output", &7).unwrap(), None);
186        }
187        observe(&mut NoopObserver);
188    }
189
190    #[test]
191    fn observed_activation_can_be_replaced_without_an_erased_hot_path() {
192        struct ReplacingObserver {
193            observed: Vec<String>,
194        }
195
196        impl ActivationObserver<i32, ()> for ReplacingObserver {
197            fn observe(&mut self, path: &str, _value: &i32) -> Result<(), ()> {
198                self.observed.push(path.into());
199                Ok(())
200            }
201
202            fn intervene(&mut self, path: &str, value: &i32) -> Result<Option<i32>, ()> {
203                Ok((path == "model.layers.0.output").then_some(value + 4))
204            }
205        }
206
207        let mut observer = ReplacingObserver {
208            observed: Vec::new(),
209        };
210        let output = observe_and_intervene(&mut observer, "model.layers.0.output", &3).unwrap();
211        assert_eq!(output, 7);
212        assert_eq!(observer.observed, ["model.layers.0.output"]);
213    }
214
215    #[test]
216    fn final_logits_observation_returns_the_intervention() {
217        struct ReplacingLogits;
218
219        impl ActivationObserver<i32, ()> for ReplacingLogits {
220            fn observe(&mut self, path: &str, value: &i32) -> Result<(), ()> {
221                assert_eq!(path, eredu_core::MODEL_LOGITS_OBSERVATION_PATH);
222                assert_eq!(*value, 3);
223                Ok(())
224            }
225
226            fn intervene(&mut self, path: &str, value: &i32) -> Result<Option<i32>, ()> {
227                assert_eq!(path, eredu_core::MODEL_LOGITS_OBSERVATION_PATH);
228                Ok(Some(value + 4))
229            }
230        }
231
232        assert_eq!(observe_model_logits(&mut ReplacingLogits, &3), Ok(7));
233    }
234
235    #[test]
236    fn target_states_are_captured_once_in_request_order() {
237        let mut capture = TargetStateCapture::new([5, 1, 3]).unwrap();
238        assert!(capture.wants(1));
239        assert!(!capture.wants(2));
240        capture
241            .capture(TargetStateTap {
242                layer: 1,
243                value: &10,
244            })
245            .unwrap();
246        capture
247            .capture(TargetStateTap {
248                layer: 5,
249                value: &50,
250            })
251            .unwrap();
252        capture
253            .capture(TargetStateTap {
254                layer: 3,
255                value: &30,
256            })
257            .unwrap();
258        assert_eq!(capture.into_ordered().unwrap(), [50, 10, 30]);
259    }
260
261    #[test]
262    fn target_state_capture_rejects_duplicates_omissions_and_unrequested_layers() {
263        assert_eq!(
264            TargetStateCapture::<i32>::new([2, 2]).unwrap_err(),
265            TargetStateCaptureError::DuplicateRequest(2)
266        );
267        let mut capture = TargetStateCapture::new([2]).unwrap();
268        assert_eq!(
269            capture
270                .capture(TargetStateTap {
271                    layer: 3,
272                    value: &7,
273                })
274                .unwrap_err(),
275            TargetStateCaptureError::Unrequested(3)
276        );
277        assert_eq!(
278            capture.into_ordered().unwrap_err(),
279            TargetStateCaptureError::Missing(2)
280        );
281    }
282}