Open Neural Network Exchange for AI model interoperability and machine learning frameworks
—
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.
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
"""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 backendsHelper 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
"""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()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()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