Inference Monitoring - Entrenar - Training & Optimization Library
Description: A comprehensive guide to building neural network training systems with autograd, optimizers, LoRA/QLoRA, and quantization using EXTREME TDD methodology

# 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.