Decorator System
arraybridge provides a powerful decorator system for automatic memory type conversion at function boundaries. This enables clean, declarative APIs and reduces boilerplate code.
Overview
The decorator system allows you to:
Declare input and output memory types
Automatically convert function arguments
Handle multiple array arguments
Specify GPU devices
Enable OOM recovery
Create framework-specific functions
Available Decorators
Framework-Specific Decorators
arraybridge provides decorators for each supported framework:
@numpy(): Convert to/from NumPy@cupy(): Convert to/from CuPy@torch(): Convert to/from PyTorch@tensorflow(): Convert to/from TensorFlow@jax(): Convert to/from JAX
Generic Decorator
@memory_types(): Generic decorator with full control
Basic Usage
Simple Decorator
from arraybridge import torch
import numpy as np
@torch()
def process_with_pytorch(x):
"""Function processes data as PyTorch tensor."""
return x * 2 + 1
# Pass NumPy, get PyTorch back
result = process_with_pytorch(np.array([1, 2, 3]))
print(type(result)) # <class 'torch.Tensor'>
Specifying Input/Output Types
from arraybridge import torch
@torch(input_type='numpy', output_type='numpy')
def public_api(x):
"""Public API: NumPy in, NumPy out (PyTorch internally)."""
# x is automatically converted to PyTorch
result = x.pow(2).sum()
# result is automatically converted back to NumPy
return result
# Users only see NumPy
np_result = public_api(np.array([1, 2, 3]))
GPU Device Selection
from arraybridge import cupy
@cupy(gpu_id=0)
def process_on_gpu0(x):
"""Process on GPU 0 using CuPy."""
return x @ x.T
@cupy(gpu_id=1)
def process_on_gpu1(x):
"""Process on GPU 1 using CuPy."""
return x @ x.T
Decorator Parameters
Common Parameters
All decorators support these parameters:
input_type: Source memory type for conversion (default: auto-detect)output_type: Target memory type for output (default: decorator’s framework)gpu_id: GPU device ID (default: 0 for GPU frameworks, None for CPU)oom_recovery: Enable OOM recovery (default: False)clear_cuda_cache: Clear CUDA cache on OOM (default: False)
Example with All Parameters
from arraybridge import torch
@torch(
input_type='numpy',
output_type='cupy',
gpu_id=0,
oom_recovery=True,
clear_cuda_cache=True
)
def complex_operation(x):
"""
- Input: NumPy → converted to PyTorch
- Process: PyTorch operations
- Output: PyTorch → converted to CuPy
- OOM recovery enabled
"""
return x.pow(2).sum()
Multi-Argument Functions
Automatic Conversion of All Arguments
from arraybridge import torch
@torch(input_type='numpy', gpu_id=0)
def matrix_multiply(a, b):
"""Both arguments converted to PyTorch."""
return a @ b
# Both NumPy arrays converted automatically
result = matrix_multiply(
np.array([[1, 2]]),
np.array([[3], [4]])
)
Mixed Argument Types
from arraybridge import torch
@torch(input_type='numpy')
def weighted_sum(x, weight=2.0):
"""x is converted, weight is left as-is."""
return x * weight
result = weighted_sum(np.array([1, 2, 3]), weight=3.0)
Keyword Arguments
from arraybridge import cupy
@cupy(gpu_id=0)
def process_with_kwargs(data, scale=1.0, offset=0.0):
"""Converts data, preserves scalar kwargs."""
return (data * scale) + offset
result = process_with_kwargs(
np.random.rand(100),
scale=2.0,
offset=1.0
)
Framework-Specific Decorators
@numpy
Convert to NumPy arrays (CPU-only).
from arraybridge import numpy
@numpy(input_type='torch', output_type='numpy')
def process_as_numpy(x):
"""Process PyTorch input as NumPy."""
return x + 1
# PyTorch → NumPy → NumPy
result = process_as_numpy(torch.tensor([1, 2, 3]))
@cupy
Convert to CuPy arrays (GPU-only).
from arraybridge import cupy
import cupyx.scipy.ndimage as ndi
@cupy(gpu_id=0, input_type='numpy')
def gpu_filter(image):
"""GPU-accelerated image filtering."""
return ndi.gaussian_filter(image, sigma=2.0)
# Automatically uses GPU
result = gpu_filter(np.random.rand(512, 512))
@torch
Convert to PyTorch tensors (CPU or GPU).
from arraybridge import torch
import torch.nn.functional as F
@torch(gpu_id=0, input_type='numpy')
def neural_network(x):
"""PyTorch neural network operations."""
return F.relu(x) @ x.T
result = neural_network(np.random.rand(100, 100))
@tensorflow
Convert to TensorFlow tensors (CPU or GPU).
from arraybridge import tensorflow
import tensorflow as tf
@tensorflow(gpu_id=0, input_type='numpy')
def tf_operation(x):
"""TensorFlow operations."""
return tf.matmul(x, x, transpose_b=True)
result = tf_operation(np.random.rand(100, 100))
@jax
Convert to JAX arrays (CPU or GPU).
from arraybridge import jax
import jax.numpy as jnp
@jax(gpu_id=0, input_type='numpy')
def jax_fft(x):
"""JAX FFT operation."""
return jnp.fft.fft2(x)
result = jax_fft(np.random.rand(256, 256))
@memory_types (Generic)
The generic decorator provides maximum flexibility.
from arraybridge.decorators import memory_types
@memory_types(
input_type='numpy',
output_type='cupy',
gpu_id=0
)
def flexible_function(x):
"""Can specify any input/output combination."""
return x * 2
Advanced Features
OOM Recovery
Enable automatic out-of-memory recovery:
from arraybridge import torch
@torch(gpu_id=0, oom_recovery=True, clear_cuda_cache=True)
def memory_intensive(x):
"""Automatically handles OOM errors."""
# Will retry with cache clearing if OOM occurs
return x @ x.T @ x
# Won't crash on OOM
result = memory_intensive(torch.rand(10000, 10000))
See Advanced Topics for more on OOM recovery.
Chaining Decorators
Decorators can be chained for complex pipelines:
from arraybridge import torch, numpy
import functools
@numpy(output_type='numpy')
def final_output(x):
"""Ensure final output is NumPy."""
return x
@torch(gpu_id=0, input_type='numpy')
def stage2(x):
"""Second stage on GPU."""
return x.pow(2)
@numpy(input_type='torch')
def stage1(x):
"""First stage as NumPy."""
return x + 1
# Use functools.compose or manual chaining
def pipeline(x):
return final_output(stage2(stage1(x)))
Return Value Handling
Decorators handle different return types:
from arraybridge import torch
@torch(output_type='numpy')
def returns_tuple(x):
"""Returns tuple of arrays."""
return x * 2, x + 1
# Both arrays in tuple are converted
result1, result2 = returns_tuple(np.array([1, 2, 3]))
@torch(output_type='numpy')
def returns_dict(x):
"""Returns dict of arrays."""
return {'doubled': x * 2, 'incremented': x + 1}
# All arrays in dict are converted
results = returns_dict(np.array([1, 2, 3]))
Class Methods
Decorators work with class methods:
from arraybridge import torch
class ImageProcessor:
@torch(gpu_id=0, input_type='numpy', output_type='numpy')
def process(self, image):
"""Process image on GPU."""
return image * 2
@staticmethod
@torch(gpu_id=0)
def static_process(image):
"""Static method with decorator."""
return image + 1
@classmethod
@torch(gpu_id=0)
def class_process(cls, image):
"""Class method with decorator."""
return image.mean()
processor = ImageProcessor()
result = processor.process(np.random.rand(512, 512))
Performance Considerations
Decorator Overhead
Decorators add minimal overhead:
Type detection: ~0.001 ms
Conversion: depends on strategy (see Conversion System)
Function call: normal Python overhead
Optimization Tips
Avoid Nested Decorated Calls:
# Bad: Repeated conversions @torch() def outer(x): return inner(x) @torch() def inner(x): return x * 2 # Good: Single conversion @torch() def combined(x): return _inner(x) def _inner(x): """No decorator - already correct type.""" return x * 2
Use Specific Input Types:
# Slower: Auto-detection @torch() def process(x): return x * 2 # Faster: Explicit input type @torch(input_type='numpy') def process(x): return x * 2
Reuse GPU Data:
# Convert once, use multiple decorated functions gpu_data = convert_memory(data, 'numpy', 'torch', gpu_id=0) @torch() def process1(x): return x * 2 @torch() def process2(x): return x + 1 # Both use already-converted data result1 = process1(gpu_data) result2 = process2(gpu_data)
Common Patterns
Pattern: Clean Public API
from arraybridge import torch
class ModelWrapper:
"""Public API accepts NumPy, uses PyTorch internally."""
@torch(input_type='numpy', output_type='numpy', gpu_id=0)
def predict(self, x):
"""Prediction with automatic conversion."""
# x is PyTorch tensor here
return self.model(x)
@torch(input_type='numpy', gpu_id=0)
def train(self, x, y):
"""Training with automatic conversion."""
loss = self.loss_fn(self.model(x), y)
loss.backward()
return float(loss)
Pattern: Multi-GPU Processing
from arraybridge import torch
class MultiGPUProcessor:
def __init__(self, num_gpus=4):
self.num_gpus = num_gpus
def process_batch(self, batch):
"""Distribute batch across GPUs."""
results = []
for i, data in enumerate(batch):
gpu_id = i % self.num_gpus
result = self._process_on_gpu(data, gpu_id)
results.append(result)
return results
def _process_on_gpu(self, data, gpu_id):
"""Dynamic GPU selection."""
@torch(gpu_id=gpu_id, input_type='numpy', output_type='numpy')
def process(x):
return x.sum()
return process(data)
Pattern: Framework Abstraction
from arraybridge import torch, cupy
def create_processor(framework='torch'):
"""Factory function for framework-specific processors."""
if framework == 'torch':
@torch(gpu_id=0)
def process(x):
return x @ x.T
elif framework == 'cupy':
@cupy(gpu_id=0)
def process(x):
return x @ x.T
return process
# User chooses framework
processor = create_processor('torch')
result = processor(np.random.rand(100, 100))
Error Handling
Decorator Error Handling
from arraybridge import torch, MemoryConversionError
@torch(input_type='numpy', gpu_id=0)
def safe_process(x):
"""Decorator handles conversion errors."""
try:
return x.pow(2)
except Exception as e:
print(f"Processing error: {e}")
return x
# Conversion errors raised before function execution
try:
result = safe_process(invalid_data)
except MemoryConversionError as e:
print(f"Conversion failed: {e}")
Fallback Logic
def robust_process(data):
"""Try GPU, fallback to CPU."""
try:
@torch(gpu_id=0, input_type='numpy')
def gpu_process(x):
return x @ x.T
return gpu_process(data)
except Exception:
@torch(gpu_id=None, input_type='numpy')
def cpu_process(x):
return x @ x.T
return cpu_process(data)
Testing with Decorators
Unit Testing
import pytest
from arraybridge import torch
@torch(input_type='numpy', output_type='numpy')
def add_one(x):
return x + 1
def test_add_one():
"""Test decorated function."""
input_data = np.array([1, 2, 3])
result = add_one(input_data)
expected = np.array([2, 3, 4])
np.testing.assert_array_equal(result, expected)
Mocking
from unittest.mock import patch
from arraybridge import torch
@torch()
def process(x):
return x * 2
def test_with_mock():
"""Test with mocked conversion."""
with patch('arraybridge.convert_memory') as mock_convert:
mock_convert.return_value = torch.tensor([2, 4, 6])
result = process(np.array([1, 2, 3]))
assert mock_convert.called
API Reference
See API Reference for complete decorator signatures.
Next Steps
Learn about Conversion System for conversion details
Explore GPU Features for device management
Check Advanced Topics for OOM recovery
Review Examples for more examples