entrenar/monitor/inference/
mod.rs1pub mod collector;
35pub mod counterfactual;
36pub mod path;
37pub mod provenance;
38pub mod safety_andon;
39pub mod serialization;
40pub mod trace;
41
42#[cfg(test)]
43mod tests;
44
45pub use collector::{
47 ChainEntry, ChainVerification, HashChainCollector, RingCollector, StreamCollector,
48 StreamFormat, TraceCollector,
49};
50pub use counterfactual::{Counterfactual, FeatureChange};
51pub use path::{
52 DecisionPath, ForestPath, KNNPath, LeafInfo, LinearPath, NeuralPath, PathError, TreePath,
53 TreeSplit,
54};
55pub use provenance::{
56 Anomaly, AttackPath, CausalRelation, IncidentReconstructor, NodeId, ProvenanceEdge,
57 ProvenanceGraph, ProvenanceNode,
58};
59pub use safety_andon::{EmergencyCondition, SafetyAndon, SafetyIntegrityLevel};
60pub use serialization::{
61 PathType, SerializationError, TraceFormat, TraceSerializer, APRT_MAGIC, APRT_VERSION,
62};
63pub use trace::DecisionTrace;
64
65use std::time::Instant;
66
67#[inline]
69pub fn monotonic_ns() -> u64 {
70 static START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
72 let start = START.get_or_init(Instant::now);
73 start.elapsed().as_nanos() as u64
74}
75
76#[inline]
78pub fn fnv1a_hash(data: &[u8]) -> u64 {
79 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
80 const FNV_PRIME: u64 = 0x100000001b3;
81
82 let mut hash = FNV_OFFSET;
83 for byte in data {
84 hash ^= u64::from(*byte);
85 hash = hash.wrapping_mul(FNV_PRIME);
86 }
87 hash
88}
89
90#[inline]
92pub fn hash_features(features: &[f32]) -> u64 {
93 let bytes: &[u8] = bytemuck::cast_slice(features);
94 fnv1a_hash(bytes)
95}
96
97pub use aprender::explainable::path::Explainable;
100
101pub struct InferenceMonitor<M, C>
105where
106 M: Explainable,
107 C: TraceCollector<M::Path>,
108{
109 model: M,
110 collector: C,
111 andon: Option<SafetyAndon>,
112 latency_budget_ns: u64,
113 sequence: u64,
114}
115
116impl<M, C> InferenceMonitor<M, C>
117where
118 M: Explainable,
119 C: TraceCollector<M::Path>,
120{
121 pub fn new(model: M, collector: C) -> Self {
123 Self {
124 model,
125 collector,
126 andon: None,
127 latency_budget_ns: 10_000_000, sequence: 0,
129 }
130 }
131
132 pub fn with_andon(mut self, andon: SafetyAndon) -> Self {
134 self.andon = Some(andon);
135 self
136 }
137
138 pub fn with_latency_budget_ns(mut self, budget: u64) -> Self {
140 self.latency_budget_ns = budget;
141 self
142 }
143
144 pub fn predict(&mut self, x: &[f32], n_samples: usize) -> Vec<f32> {
146 let start = Instant::now();
147 let timestamp_ns = monotonic_ns();
148
149 let (outputs, paths) = self.model.predict_explained(x, n_samples);
150
151 let elapsed_ns = start.elapsed().as_nanos() as u64;
152
153 let features_per_sample = x.len() / n_samples;
154
155 for (i, (output, path)) in outputs.iter().zip(paths.into_iter()).enumerate() {
156 let sample_start = i * features_per_sample;
157 let sample_end = sample_start + features_per_sample;
158 let sample_features = &x[sample_start..sample_end];
159
160 let trace = DecisionTrace {
161 timestamp_ns,
162 sequence: self.sequence,
163 input_hash: hash_features(sample_features),
164 path,
165 output: *output,
166 latency_ns: elapsed_ns,
167 };
168
169 self.sequence += 1;
170
171 if let Some(andon) = &mut self.andon {
173 andon.check_trace(&trace, self.latency_budget_ns);
174 }
175
176 self.collector.record(trace);
177 }
178
179 outputs
180 }
181
182 pub fn collector(&self) -> &C {
184 &self.collector
185 }
186
187 pub fn collector_mut(&mut self) -> &mut C {
189 &mut self.collector
190 }
191
192 pub fn model(&self) -> &M {
194 &self.model
195 }
196
197 pub fn sequence(&self) -> u64 {
199 self.sequence
200 }
201}