# Inference Plugin Documentation The Inference Plugin is a core component of the VIPR framework that provides a flexible, step-based workflow system for machine learning inference pipelines. ## Overview The Inference Plugin implements a generic, extensible workflow for ML/AI inference with the following characteristics: - **Domain-agnostic design**: Designed to support diverse ML models and data types - **Step-based architecture**: Six configurable steps that can be customized - **Hook and filter system**: Extensibility points at each step - **Type-safe**: Full type checking with beartype - **Configuration-driven**: YAML-based configuration for all components The plugin integrates with the VIPR Dynamic Hooks and Filters Extension, allowing hooks and filters to be configured via YAML files rather than requiring code changes. This provides maximum flexibility for adapting workflows to different use cases. ## Data Flow The inference workflow processes data through a standardized 6-step pipeline. Each step transforms data in a standardized way, ensuring type safety and extensibility. ### Step Input/Output Contracts ``` LoadDataStep: Input: config parameters (dict) Output: DataSet(x, y, errors, metadata) LoadModelStep: Input: config parameters (dict) Output: model instance (Any) NormalizeStep: Input: DataSet (original) Output: DataSet (normalized with transformed errors via filters) Note: Normalization is implemented via INFERENCE_NORMALIZE_PRE_FILTER. Filters can transform both data and errors. See LogNormalizer, MinMaxNormalizer, ZScoreNormalizer examples. PreprocessStep: Input: DataSet (model available via self.app.inference.model) Output: DataSet (preprocessed with transformed errors via filters) Note: Preprocessing is implemented via INFERENCE_PREPROCESS_PRE_FILTER. Filters can transform both data and errors (e.g., reflectorch interpolation). PredictionStep: Input: DataSet (model available via self.app.inference.model) Output: prediction results (dict) PostprocessStep: Input: prediction results (dict) Output: final results (Any) ``` ## DataSet Transfer Object The {py:class}`vipr.plugins.inference.dataset.DataSet` class implements the Transfer Object Pattern for immutable data transport between pipeline steps. **Key features:** - **Immutable**: Arrays are read-only after creation - **Type-safe**: Full Pydantic validation with beartype - **Functional updates**: Use `copy_with_updates()` for modifications **See:** {py:class}`vipr.plugins.inference.dataset.DataSet` for complete API reference and usage examples. ### Extension Points Per Step Each step provides 6 extension points for customization: ``` AbstractInferenceStep.run(): ├── runPrePreFilterHook() ◄── Hook 1: Before any processing ├── runPreFilter() ◄── Filter 1: Transform input ├── runPostPreFilterHook() ◄── Hook 2: After input filter ├── execute() ◄── Core step logic ├── runPrePostFilterHook() ◄── Hook 3: Before output filter ├── runPostFilter() ◄── Filter 2: Transform output └── runPostPostFilterHook() ◄── Hook 4: After all processing ``` ## Architecture ### Core Components 1. **{py:class}`vipr.plugins.inference.base_inference.BaseInference`** Abstract base providing shared functionality for inference implementations 2. **{py:class}`vipr.plugins.inference.inference.Inference`** Main orchestrator implementing the 6-step workflow with complete hook/filter system 3. **{py:class}`vipr.plugins.inference.abstract_step.AbstractInferenceStep`** Base class for workflow steps with hooks/filters 4. **Workflow Steps:** - {py:class}`vipr.plugins.inference.steps.load_data_step.LoadDataInferenceStep` - Loads input data - {py:class}`vipr.plugins.inference.steps.load_model_step.LoadModelInferenceStep` - Loads ML models - {py:class}`vipr.plugins.inference.steps.normalize_step.NormalizeInferenceStep` - Normalizes data - {py:class}`vipr.plugins.inference.steps.preprocess_step.PreprocessInferenceStep` - Preprocesses data - {py:class}`vipr.plugins.inference.steps.prediction_step.PredictionInferenceStep` - Executes prediction - {py:class}`vipr.plugins.inference.steps.postprocess_step.PostprocessInferenceStep` - Post-processes results ## Extension Points The inference workflow uses two extension mechanisms: ### Handler-Based Steps Steps that use pluggable handler implementations: | Step | Handler Type | Interface | Purpose | |------|-------------|-----------|---------| | LoadDataStep | `data_loader` | {py:class}`vipr.interfaces.data_loader.DataLoaderInterface` | Load data from various sources | | LoadModelStep | `model_loader` | {py:class}`vipr.interfaces.model_loader.ModelLoaderInterface` | Load ML models | | PredictionStep | `predictor` | {py:class}`vipr.interfaces.predictor.PredictorInterface` | Execute predictions | | PostprocessStep | `postprocessor` | {py:class}`vipr.interfaces.postprocessor.PostprocessorInterface` | Process results | ### Filter-Based Steps Steps that use filter transformations (no handlers): | Step | Primary Filter | Purpose | |------|---------------|---------| | NormalizeStep | `INFERENCE_NORMALIZE_PRE_FILTER` | Normalize data and transform errors (dy/dx) | | PreprocessStep | `INFERENCE_PREPROCESS_PRE_FILTER` | Preprocess data (e.g., interpolation with error transformation) | **Note:** Filter-based steps provide DataSet-aware transformation. See normalizer examples (LogNormalizer, MinMaxNormalizer, ZScoreNormalizer) and preprocessing filters (e.g., Reflectorch interpolation). ### Handler Implementation **Base classes:** {py:class}`vipr.handlers.data_loader.DataLoaderHandler`, {py:class}`vipr.handlers.model_loader.ModelLoaderHandler`, {py:class}`vipr.handlers.predictor.PredictorHandler` **Pattern:** 1. Inherit from appropriate base handler 2. Implement interface method (`_load_data`, `_load_model`, `_predict`) 3. Return correct type ({py:class}`vipr.plugins.inference.dataset.DataSet` for data loaders) **Complete examples:** See [Reflectometry Handler Patterns](../external/reflectometry/handlers.md) ## Hook and Filter System The Inference Plugin provides a comprehensive system for extending functionality through hooks and filters. These can be registered either programmatically in your plugin code or through configuration using the Dynamic Hooks and Filters Extension. ### Available Hooks **Global Workflow Hooks:** See {py:class}`vipr.plugins.inference.base_inference.BaseInference` for hook constants: - `INFERENCE_START_HOOK`: Triggered at workflow start - `INFERENCE_COMPLETE_HOOK`: Triggered at workflow completion **Step-Level Hooks:** See {py:class}`vipr.plugins.inference.abstract_step.AbstractInferenceStep` for all step hook constants. Where `` is: `LOAD_DATA`, `LOAD_MODEL`, `NORMALIZE`, `PREPROCESS`, `PREDICTION`, or `POSTPROCESS` - `INFERENCE__PRE_PRE_FILTER_HOOK` - Before PRE_FILTER execution - `INFERENCE__POST_PRE_FILTER_HOOK` - After PRE_FILTER, before execute() - `INFERENCE__PRE_POST_FILTER_HOOK` - After execute(), before POST_FILTER - `INFERENCE__POST_POST_FILTER_HOOK` - After POST_FILTER execution **Execution Order:** ``` 1. PRE_PRE_FILTER_HOOK ← Hook before input transformation 2. PRE_FILTER ← Filter transforms input 3. POST_PRE_FILTER_HOOK ← Hook after input transformation 4. execute() ← Core step logic 5. PRE_POST_FILTER_HOOK ← Hook before output transformation 6. POST_FILTER ← Filter transforms output 7. POST_POST_FILTER_HOOK ← Hook after output transformation ``` ### Available Filters **Step-Level Filters** (for each step): - `INFERENCE__PRE_FILTER`: Transform input data before execute() - `INFERENCE__POST_FILTER`: Transform output data after execute() ### Registering Hooks and Filters You can register hooks and filters in two ways: #### 1. Programmatically (in your plugin) ```python # In your plugin def _post_setup(self, app): # Register a hook app.hook.register('INFERENCE_START_HOOK', self.on_inference_start) # Register a filter app.filter.register('INFERENCE_NORMALIZE_PRE_FILTER', self.custom_normalize) def on_inference_start(self, app): app.log.info("Custom inference starting...") def custom_normalize(self, data: DataSet, **kwargs) -> DataSet: # Custom normalization logic return data.copy_with_updates( x=normalized_x, y=normalized_y, metadata={**data.metadata, 'normalized': True} ) ``` #### 2. Via Configuration (using Dynamic Hooks and Filters Extension) The recommended approach for maximum flexibility is to use the Dynamic Hooks and Filters Extension, which allows you to configure hooks and filters in your YAML file. > **TODO**: Dynamic Hooks and Filters Extension documentation will be migrated soon. ## Configuration ### Basic Configuration Structure ```yaml vipr: # Step configurations load_data: handler: csv_loader # Handler to use parameters: # Handler-specific parameters file_path: data/input.csv delimiter: ',' load_model: handler: pytorch_loader parameters: model_path: models/my_model.pt device: cuda normalize: handler: standard_normalizer parameters: method: zscore preprocess: handler: default_preprocessor parameters: remove_outliers: true prediction: handler: default_predictor parameters: batch_size: 32 postprocess: handler: default_postprocessor parameters: format: json # Dynamic hooks and filters can be configured here # See Dynamic Hooks and Filters Extension documentation for details hooks: {} filters: {} ``` ### Dynamic Hook/Filter Configuration The Inference Plugin integrates with the `dynamic_hooks_filters` extension for configuration-based hook and filter registration. This allows you to define hooks and filters in your YAML configuration instead of hardcoding them in plugin code. > **Note**: For detailed information about configuring dynamic hooks and filters, please refer to the [Dynamic Hooks and Filters Extension Documentation](../extensions/dynamic_hooks_filters.md). ## Usage Example ### Creating a Controller ```python from cement import Controller, ex class MLController(Controller): class Meta: label = 'ml' stacked_on = 'base' stacked_type = 'nested' @ex(help='Run ML inference') def predict(self): # The inference workflow is already available as self.app.inference # (registered by the inference plugin during app setup) # Run inference workflow result = self.app.inference.run() # Access intermediate results if needed self.app.log.info(f"Model used: {self.app.inference.model}") self.app.log.info(f"Preprocessed data shape: {self.app.inference.preprocessed_data[0].shape}") # Return final result self.app.log.info(f"Prediction complete") return result ``` ### Running via CLI ```bash # With configuration file vipr --config config/ml_config.yaml ml predict ```