Inference Monitoring
Real-time audit logging and explainability for APR format models.
Overview
The inference monitoring module provides comprehensive tracing for model predictions, enabling:
- Decision Path Tracing: Capture exactly how a model reached its decision
- Audit Logging: Tamper-evident logs for safety-critical applications
- Explainability: Human-readable explanations for predictions
- Provenance Graphs: Reconstruct incident causality chains
This follows the Toyota Way principle of 現地現物 (Genchi Genbutsu) - every decision is traceable to ground truth.
Decision Paths
Each model type has a specific path representation:
| Model Type | Path Type | Key Information |
|---|---|---|
| Linear/Logistic | LinearPath | Feature contributions, intercept, logit |
| Decision Tree | TreePath | Splits, thresholds, leaf info |
| Random Forest | ForestPath | Per-tree predictions, aggregation |
| KNN | KNNPath | Neighbor indices, distances, votes |
| Neural Network | NeuralPath | Gradients, activations, attention |
Example: Linear Path
#![allow(unused)] fn main() { use entrenar::monitor::inference::{LinearPath, DecisionPath}; // Create a linear path with feature contributions let path = LinearPath::new( vec![0.8, -0.3, 0.5], // contributions: weight * feature 0.1, // intercept 1.1, // logit (sum of contributions + intercept) 0.75, // output (e.g., sigmoid(logit)) ).with_probability(0.75); // Get human-readable explanation println!("{}", path.explain()); }
Output:
Prediction: 0.7500 (probability: 75.0%)
Top contributing features:
- feature[0]: +0.8000
- feature[2]: +0.5000
- feature[1]: -0.3000
Intercept: 0.1000
Collector Strategies
Three collector strategies for different use cases:
RingCollector (Real-Time)
Fixed-size ring buffer with O(1) operations. Ideal for games and drones.
- Target: <100ns per trace
- Memory: Bounded, configurable size
- Use Case: Real-time systems where only recent traces matter
#![allow(unused)] fn main() { use entrenar::monitor::inference::{RingCollector, LinearPath}; // Create ring collector with capacity 64 let collector: RingCollector<LinearPath, 64> = RingCollector::new(); // After recording traces, get recent ones let recent = collector.recent(10); // Last 10 traces let all = collector.all(); // All traces (oldest first) }
StreamCollector (Persistent)
Write-through to file or network. Supports binary, JSON, and JSON Lines formats.
- Target: <1µs per trace
- Memory: Configurable buffer before flush
- Use Case: Persistent logging, audit trails
#![allow(unused)] fn main() { use entrenar::monitor::inference::{StreamCollector, StreamFormat, TreePath}; use std::fs::File; let file = File::create("traces.jsonl")?; let collector = StreamCollector::<TreePath, _>::new(file, StreamFormat::JsonLines) .with_flush_threshold(100); // Flush every 100 traces }
HashChainCollector (Safety-Critical)
SHA-256 hash chain for tamper-evident audit trails. Each entry's hash includes the previous hash.
- Target: <10µs per entry
- Security: Cryptographic integrity verification
- Use Case: Autonomous vehicles, medical devices, regulatory compliance
#![allow(unused)] fn main() { use entrenar::monitor::inference::{HashChainCollector, LinearPath}; let mut collector: HashChainCollector<LinearPath> = HashChainCollector::new(); // Record traces... // Verify chain integrity let verification = collector.verify_chain(); assert!(verification.valid); println!("Verified {} entries", verification.entries_verified); }
InferenceMonitor
High-level wrapper that combines model, collector, and optional safety checks:
#![allow(unused)] fn main() { use entrenar::monitor::inference::{ InferenceMonitor, RingCollector, LinearPath, Explainable, }; // Model must implement Explainable trait let model = MyLinearModel::new(weights, intercept); let collector: RingCollector<LinearPath, 64> = RingCollector::new(); let mut monitor = InferenceMonitor::new(model, collector) .with_latency_budget_ns(10_000_000); // 10ms budget // Predict with automatic tracing let outputs = monitor.predict(&input_features, 1); // Access traces let traces = monitor.collector().recent(1); println!("{}", traces[0].explain()); }
Implementing Explainable
To use inference monitoring, your model must implement Explainable:
#![allow(unused)] fn main() { use entrenar::monitor::inference::{Explainable, LinearPath}; struct MyModel { weights: Vec<f32>, intercept: f32, } impl Explainable for MyModel { type Path = LinearPath; fn predict_explained(&self, x: &[f32], n_samples: usize) -> (Vec<f32>, Vec<Self::Path>) { let features_per_sample = x.len() / n_samples; let mut outputs = Vec::with_capacity(n_samples); let mut paths = Vec::with_capacity(n_samples); for i in 0..n_samples { let sample = &x[i * features_per_sample..(i + 1) * features_per_sample]; // Compute contributions let contributions: Vec<f32> = self.weights .iter() .zip(sample) .map(|(w, x)| w * x) .collect(); let logit: f32 = contributions.iter().sum::<f32>() + self.intercept; let output = sigmoid(logit); let path = LinearPath::new(contributions, self.intercept, logit, output) .with_probability(output); outputs.push(output); paths.push(path); } (outputs, paths) } fn explain_one(&self, sample: &[f32]) -> Self::Path { self.predict_explained(sample, 1).1.into_iter().next().unwrap() } } }
Safety Andon
Automatic quality monitoring based on Safety Integrity Levels (SIL):
#![allow(unused)] fn main() { use entrenar::monitor::inference::{SafetyAndon, SafetyIntegrityLevel}; let mut andon = SafetyAndon::new(SafetyIntegrityLevel::SIL2) .with_min_confidence(0.7) // Alert if confidence < 70% .with_low_confidence_threshold(3); // Alert after 3 consecutive low-confidence // Check each trace andon.check_trace(&trace, latency_budget_ns); // Review alerts for alert in andon.history() { println!("[{:?}] {}", alert.level, alert.message); } }
Safety Integrity Levels
| Level | Min Confidence | Max Latency | Use Case |
|---|---|---|---|
| QM | 50% | 100ms | Quality management |
| SIL1 | 60% | 50ms | Low risk |
| SIL2 | 70% | 20ms | Medium risk |
| SIL3 | 80% | 10ms | High risk |
| SIL4 | 90% | 5ms | Critical systems |
Counterfactual Explanations
Answer "What would need to change to flip the decision?"
#![allow(unused)] fn main() { use entrenar::monitor::inference::Counterfactual; let feature_names = vec!["income".into(), "debt_ratio".into(), "score".into()]; let counterfactual = Counterfactual::new( vec![0.3, 0.8, 0.2], // Original input (denied) 0, // Original decision 0.3, // Original confidence vec![0.6, 0.5, 0.4], // Counterfactual input (approved) 1, // Alternative decision 0.8, // Alternative confidence ).with_feature_names(&feature_names); println!("{}", counterfactual.explain()); // Output: // The decision would have been 1 if: // - income: 0.3000 → 0.6000 (+0.3000) // - debt_ratio: 0.8000 → 0.5000 (-0.3000) // - score: 0.2000 → 0.4000 (+0.2000) }
Provenance Graphs
Reconstruct causal chains for incident analysis:
#![allow(unused)] fn main() { use entrenar::monitor::inference::{ ProvenanceGraph, ProvenanceNode, ProvenanceEdge, CausalRelation, IncidentReconstructor, }; let mut graph = ProvenanceGraph::new(); // Build provenance chain: Input -> Transform -> Inference -> Action let camera = graph.add_node(ProvenanceNode::Input { source: "front_camera".into(), timestamp_ns: 1000, hash: 0xdeadbeef, }); let preprocess = graph.add_node(ProvenanceNode::Transform { operation: "normalize".into(), input_refs: vec![camera], }); let detection = graph.add_node(ProvenanceNode::Inference { model_id: "detector".into(), model_version: "v2.1".into(), confidence: 0.3, // Low confidence! output: 0.0, }); // Connect nodes graph.add_edge(ProvenanceEdge { from: camera, to: preprocess, relation: CausalRelation::DataFlow, timestamp_ns: 1100, }); // Reconstruct incident path and identify anomalies let reconstructor = IncidentReconstructor::new(&graph); let path = reconstructor.reconstruct_path(detection, 10); let anomalies = reconstructor.identify_anomalies(&path, 0.7); for anomaly in &anomalies { println!("Node {}: {}", anomaly.node_id, anomaly.description); } }
Serialization
Binary Format (APRT)
Compact binary format with magic number 0x41505254 ("APRT"):
#![allow(unused)] fn main() { use entrenar::monitor::inference::{TraceSerializer, TraceFormat, PathType}; let serializer = TraceSerializer::new(TraceFormat::Binary); let bytes = serializer.serialize(&trace, PathType::Linear)?; // Deserialize let restored: DecisionTrace<LinearPath> = serializer.deserialize(&bytes)?; }
JSON Format
Human-readable JSON for debugging and integration:
#![allow(unused)] fn main() { let serializer = TraceSerializer::new(TraceFormat::Json); let json = serializer.serialize(&trace, PathType::Linear)?; println!("{}", String::from_utf8_lossy(&json)); }
Performance Targets
| Collector | Target Latency | Measured |
|---|---|---|
| RingCollector | <100ns | ~50ns |
| StreamCollector | <1µs | ~500ns |
| HashChainCollector | <10µs | ~5µs |
Running the Example
cargo run --example inference_monitor
This demonstrates all components working together: collectors, safety monitoring, counterfactuals, provenance graphs, and serialization.