CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-onnx

Open Neural Network Exchange for AI model interoperability and machine learning frameworks

Pending
Overview
Eval results
Files

backend-integration.mddocs/

Backend Integration

Abstract interfaces for implementing ONNX model execution backends, enabling custom runtime integration and testing frameworks. This module provides the foundation for creating execution engines that can run ONNX models.

Capabilities

Backend Base Classes

Abstract base classes for implementing ONNX execution backends.

class Backend:
    """
    Abstract base class for ONNX execution backends.
    
    Provides interface for preparing and running ONNX models
    on various compute devices and runtimes.
    """
    
    @classmethod
    def is_compatible(cls, model, device="CPU", **kwargs):
        """
        Check if backend can execute the given model.

        Parameters:
        - model: ModelProto to check compatibility for
        - device: Target device ("CPU", "CUDA", etc.)
        - **kwargs: Additional backend-specific options

        Returns:
        bool: True if backend can execute the model, False otherwise
        """
    
    @classmethod  
    def prepare(cls, model, device="CPU", **kwargs):
        """
        Prepare model for execution on this backend.

        Parameters:
        - model: ModelProto to prepare
        - device: Target device for execution
        - **kwargs: Backend-specific preparation options

        Returns:
        BackendRep: Prepared model representation ready for execution

        Raises:
        BackendIsNotSupposedToImplementIt: If backend doesn't support preparation
        """
    
    @classmethod
    def run_model(cls, model, inputs, device="CPU", **kwargs):
        """
        Run model once with given inputs.

        Parameters:
        - model: ModelProto to execute
        - inputs: Input data as list of numpy arrays
        - device: Target device for execution
        - **kwargs: Execution options

        Returns:
        namedtuple: Output values with names corresponding to model outputs

        Raises:
        BackendIsNotSupposedToImplementIt: If backend doesn't support direct execution
        """
    
    @classmethod
    def run_node(cls, node, inputs, device="CPU", outputs_info=None, **kwargs):
        """
        Run a single node with given inputs.

        Parameters:
        - node: NodeProto to execute
        - inputs: Input data as list of numpy arrays
        - device: Target device for execution
        - outputs_info: Optional output type information
        - **kwargs: Execution options

        Returns:
        namedtuple: Output values

        Raises:
        BackendIsNotSupposedToImplementIt: If backend doesn't support node execution
        """
    
    @classmethod
    def supports_device(cls, device):
        """
        Check if backend supports the specified device.

        Parameters:
        - device: Device identifier to check

        Returns:
        bool: True if device is supported, False otherwise
        """

class BackendRep:
    """
    Backend representation of a prepared model.
    
    Contains the model in a backend-specific format
    optimized for repeated execution.
    """
    
    def run(self, inputs, **kwargs):
        """
        Execute the prepared model with given inputs.

        Parameters:
        - inputs: Input data as list of numpy arrays
        - **kwargs: Execution options

        Returns:
        namedtuple: Output values with names corresponding to model outputs
        """

Device Management

Classes and constants for device specification and management.

class Device:
    """
    Device specification for backend execution.
    
    Encapsulates device type and device-specific configuration.
    """
    
    def __init__(self, device_type, device_id=0):
        """
        Initialize device specification.

        Parameters:
        - device_type: Type of device (CPU, CUDA, etc.)
        - device_id: Device identifier for multi-device systems
        """

class DeviceType:
    """
    Constants for standard device types.
    """
    CPU = "CPU"
    CUDA = "CUDA" 
    # Additional device types may be defined by specific backends

Utility Functions

Helper functions for backend development and testing.

def namedtupledict(typename, field_names, *args, **kwargs):
    """
    Create a named tuple class with dictionary-like access.

    Parameters:
    - typename: Name for the named tuple class
    - field_names: Field names for the tuple
    - *args, **kwargs: Additional arguments for namedtuple creation

    Returns:
    type: Named tuple class with dict-like access methods
    """

Usage Examples

Implementing a Custom Backend

import onnx
from onnx import backend
import numpy as np
from collections import namedtuple

class MyCustomBackend(backend.Backend):
    """Example implementation of a custom ONNX backend."""
    
    @classmethod
    def is_compatible(cls, model, device="CPU", **kwargs):
        """Check if we can run this model."""
        
        # Only support CPU for this example
        if device != "CPU":
            return False
            
        # Check if all operators are supported
        supported_ops = {"Add", "Sub", "Mul", "Div", "Relu", "MatMul", "Conv"}
        
        for node in model.graph.node:
            if node.op_type not in supported_ops:
                print(f"Unsupported operator: {node.op_type}")
                return False
                
        return True
    
    @classmethod
    def prepare(cls, model, device="CPU", **kwargs):
        """Prepare model for execution."""
        
        if not cls.is_compatible(model, device, **kwargs):
            raise RuntimeError("Model is not compatible with this backend")
            
        # Create a backend representation
        return MyBackendRep(model, device)
    
    @classmethod
    def run_model(cls, model, inputs, device="CPU", **kwargs):
        """Run model directly (without preparation)."""
        
        # Prepare and run
        rep = cls.prepare(model, device, **kwargs)
        return rep.run(inputs, **kwargs)
    
    @classmethod
    def run_node(cls, node, inputs, device="CPU", **kwargs):
        """Run a single node."""
        
        # Simple node execution logic
        if node.op_type == "Add":
            result = inputs[0] + inputs[1]
        elif node.op_type == "Mul":
            result = inputs[0] * inputs[1]
        elif node.op_type == "Relu":
            result = np.maximum(0, inputs[0])
        else:
            raise NotImplementedError(f"Node {node.op_type} not implemented")
            
        # Return as named tuple
        OutputTuple = namedtuple('Output', [f'output_{i}' for i in range(len(node.output))])
        return OutputTuple(result)
    
    @classmethod
    def supports_device(cls, device):
        """Check device support."""
        return device == "CPU"

class MyBackendRep(backend.BackendRep):
    """Backend representation for prepared models."""
    
    def __init__(self, model, device):
        self.model = model
        self.device = device
        self._prepare_model()
    
    def _prepare_model(self):
        """Internal preparation logic."""
        print(f"Preparing model '{self.model.graph.name}' for {self.device}")
        # In a real backend, this would compile/optimize the model
        
    def run(self, inputs, **kwargs):
        """Execute the prepared model."""
        
        print(f"Executing model with {len(inputs)} inputs")
        
        # Simple execution simulation
        # In a real backend, this would use optimized execution
        current_values = {}
        
        # Set input values
        for i, input_info in enumerate(self.model.graph.input):
            current_values[input_info.name] = inputs[i]
            
        # Set initializer values
        for initializer in self.model.graph.initializer:
            from onnx import numpy_helper
            current_values[initializer.name] = numpy_helper.to_array(initializer)
        
        # Execute nodes in order
        for node in self.model.graph.node:
            node_inputs = [current_values[name] for name in node.input]
            node_result = MyCustomBackend.run_node(node, node_inputs)
            
            # Store outputs
            for i, output_name in enumerate(node.output):
                if hasattr(node_result, f'output_{i}'):
                    current_values[output_name] = getattr(node_result, f'output_{i}')
                else:
                    current_values[output_name] = node_result[i] if isinstance(node_result, (list, tuple)) else node_result
        
        # Collect final outputs
        outputs = []
        output_names = []
        for output_info in self.model.graph.output:
            outputs.append(current_values[output_info.name])
            output_names.append(output_info.name)
        
        # Return as named tuple
        OutputTuple = namedtuple('ModelOutput', output_names)
        return OutputTuple(*outputs)

# Test the custom backend
def test_custom_backend():
    """Test the custom backend implementation."""
    
    # Create a simple model for testing
    from onnx import helper, TensorProto
    
    X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [2, 2])
    Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [2, 2])
    
    # Create a simple Add node
    add_node = helper.make_node('Add', ['X', 'X'], ['Y'])
    
    graph = helper.make_graph([add_node], 'test_model', [X], [Y])
    model = helper.make_model(graph)
    
    # Test backend compatibility
    backend_impl = MyCustomBackend()
    
    if backend_impl.is_compatible(model):
        print("✓ Model is compatible with custom backend")
        
        # Test direct execution
        test_input = np.array([[1, 2], [3, 4]], dtype=np.float32)
        result = backend_impl.run_model(model, [test_input])
        print(f"Direct execution result: {result.Y}")
        
        # Test prepared execution
        rep = backend_impl.prepare(model)
        result2 = rep.run([test_input])
        print(f"Prepared execution result: {result2.Y}")
        
        print(f"Results match: {np.array_equal(result.Y, result2.Y)}")
        
    else:
        print("✗ Model is not compatible with custom backend")

# Run the test
test_custom_backend()

Backend Testing Framework

import onnx
from onnx import backend
import numpy as np

class BackendTester:
    """Framework for testing ONNX backend implementations."""
    
    def __init__(self, backend_class):
        self.backend = backend_class
        
    def test_operator_support(self, test_cases):
        """Test backend support for various operators."""
        
        results = {}
        
        for op_name, test_model in test_cases.items():
            try:
                is_compatible = self.backend.is_compatible(test_model)
                results[op_name] = {
                    'compatible': is_compatible,
                    'error': None
                }
                
                if is_compatible:
                    # Try to prepare the model
                    rep = self.backend.prepare(test_model)
                    results[op_name]['preparable'] = True
                else:
                    results[op_name]['preparable'] = False
                    
            except Exception as e:
                results[op_name] = {
                    'compatible': False,
                    'preparable': False,
                    'error': str(e)
                }
        
        return results
    
    def test_device_support(self, devices):
        """Test backend device support."""
        
        device_results = {}
        for device in devices:
            device_results[device] = self.backend.supports_device(device)
            
        return device_results
    
    def benchmark_execution(self, model, inputs, iterations=100):
        """Benchmark model execution performance."""
        
        import time
        
        # Test direct execution
        start_time = time.time()
        for _ in range(iterations):
            result = self.backend.run_model(model, inputs)
        direct_time = time.time() - start_time
        
        # Test prepared execution  
        rep = self.backend.prepare(model)
        start_time = time.time()
        for _ in range(iterations):
            result = rep.run(inputs)
        prepared_time = time.time() - start_time
        
        return {
            'direct_execution_time': direct_time,
            'prepared_execution_time': prepared_time,
            'speedup': direct_time / prepared_time if prepared_time > 0 else float('inf'),
            'iterations': iterations
        }

# Example usage with custom backend
def test_backend_comprehensive():
    """Comprehensive backend testing example."""
    
    # Create test models for different operators
    from onnx import helper, TensorProto
    
    test_cases = {}
    
    # Add test
    X = helper.make_tensor_value_info('X', TensorProto.FLOAT, [2, 2])
    Y = helper.make_tensor_value_info('Y', TensorProto.FLOAT, [2, 2])
    add_node = helper.make_node('Add', ['X', 'X'], ['Y'])
    add_graph = helper.make_graph([add_node], 'add_test', [X], [Y])
    test_cases['Add'] = helper.make_model(add_graph)
    
    # ReLU test
    relu_node = helper.make_node('Relu', ['X'], ['Y']) 
    relu_graph = helper.make_graph([relu_node], 'relu_test', [X], [Y])
    test_cases['Relu'] = helper.make_model(relu_graph)
    
    # Unsupported operator test
    unsupported_node = helper.make_node('LSTM', ['X'], ['Y'])
    unsupported_graph = helper.make_graph([unsupported_node], 'unsupported_test', [X], [Y])
    test_cases['LSTM'] = helper.make_model(unsupported_graph)
    
    # Run tests
    tester = BackendTester(MyCustomBackend)
    
    print("=== Operator Support Test ===")
    op_results = tester.test_operator_support(test_cases)
    for op, result in op_results.items():
        status = "✓" if result['compatible'] else "✗"
        print(f"{status} {op}: Compatible={result['compatible']}, Preparable={result.get('preparable', 'N/A')}")
        if result.get('error'):
            print(f"    Error: {result['error']}")
    
    print("\n=== Device Support Test ===")
    device_results = tester.test_device_support(['CPU', 'CUDA', 'OpenCL'])
    for device, supported in device_results.items():
        status = "✓" if supported else "✗"
        print(f"{status} {device}: {supported}")
    
    print("\n=== Performance Benchmark ===")
    test_input = np.array([[1, 2], [3, 4]], dtype=np.float32)
    benchmark_results = tester.benchmark_execution(test_cases['Add'], [test_input], iterations=10)
    
    print(f"Direct execution time: {benchmark_results['direct_execution_time']:.4f}s")
    print(f"Prepared execution time: {benchmark_results['prepared_execution_time']:.4f}s")
    print(f"Speedup: {benchmark_results['speedup']:.2f}x")

# Run comprehensive tests
test_backend_comprehensive()

Integration with Testing Frameworks

import onnx
from onnx import backend

def create_backend_test_suite(backend_class):
    """Create a test suite for backend validation."""
    
    class BackendTestSuite:
        def __init__(self):
            self.backend = backend_class
            
        def test_basic_compatibility(self):
            """Test basic backend functionality."""
            from onnx import helper, TensorProto
            
            # Create minimal model
            X = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1])
            Y = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1])
            identity_node = helper.make_node('Identity', ['input'], ['output'])
            graph = helper.make_graph([identity_node], 'identity', [X], [Y])
            model = helper.make_model(graph)
            
            # Test compatibility check
            assert hasattr(self.backend, 'is_compatible'), "Backend must implement is_compatible"
            
            # Test preparation
            if self.backend.is_compatible(model):
                rep = self.backend.prepare(model)
                assert hasattr(rep, 'run'), "BackendRep must implement run method"
                
        def test_device_support(self):
            """Test device support functionality."""
            assert hasattr(self.backend, 'supports_device'), "Backend must implement supports_device"
            
            # At minimum, should support CPU
            assert self.backend.supports_device('CPU'), "Backend should support CPU"
            
        def test_error_handling(self):
            """Test error handling for unsupported models."""
            from onnx import helper, TensorProto
            
            # Create model with unsupported operator
            X = helper.make_tensor_value_info('input', TensorProto.FLOAT, [1])
            Y = helper.make_tensor_value_info('output', TensorProto.FLOAT, [1])
            
            # Use a hypothetical unsupported operator
            unsupported_node = helper.make_node('UnsupportedOp', ['input'], ['output'])
            graph = helper.make_graph([unsupported_node], 'unsupported', [X], [Y])
            model = helper.make_model(graph)
            
            # Should either return False for is_compatible or raise appropriate exception
            try:
                compatible = self.backend.is_compatible(model)
                if compatible:
                    # If claiming compatibility, prepare should work
                    rep = self.backend.prepare(model)
            except Exception as e:
                # Acceptable if raises informative exception
                assert len(str(e)) > 0, "Exception should have descriptive message"
    
    return BackendTestSuite()

# Example usage
def validate_backend_implementation():
    """Validate that our backend implementation meets requirements."""
    
    test_suite = create_backend_test_suite(MyCustomBackend)
    
    try:
        test_suite.test_basic_compatibility()
        print("✓ Basic compatibility test passed")
    except Exception as e:
        print(f"✗ Basic compatibility test failed: {e}")
    
    try:
        test_suite.test_device_support()
        print("✓ Device support test passed")
    except Exception as e:
        print(f"✗ Device support test failed: {e}")
    
    try:
        test_suite.test_error_handling()
        print("✓ Error handling test passed")
    except Exception as e:
        print(f"✗ Error handling test failed: {e}")

# Validate our custom backend
validate_backend_implementation()

Install with Tessl CLI

npx tessl i tessl/pypi-onnx

docs

backend-integration.md

index.md

model-composition.md

model-construction.md

model-hub.md

model-io.md

model-validation.md

numpy-integration.md

operator-definitions.md

reference-implementation.md

shape-inference.md

text-processing.md

version-conversion.md

tile.json