arraybridge 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arraybridge/__init__.py +37 -0
- arraybridge/conversion_helpers.py +150 -0
- arraybridge/converters.py +61 -0
- arraybridge/decorators.py +396 -0
- arraybridge/dtype_scaling.py +157 -0
- arraybridge/exceptions.py +26 -0
- arraybridge/framework_config.py +459 -0
- arraybridge/framework_ops.py +15 -0
- arraybridge/gpu_cleanup.py +149 -0
- arraybridge/oom_recovery.py +148 -0
- arraybridge/slice_processing.py +73 -0
- arraybridge/stack_utils.py +317 -0
- arraybridge/types.py +70 -0
- arraybridge/utils.py +352 -0
- arraybridge-0.2.0.dist-info/METADATA +228 -0
- arraybridge-0.2.0.dist-info/RECORD +18 -0
- arraybridge-0.2.0.dist-info/WHEEL +4 -0
- arraybridge-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dtype scaling and conversion functions for different memory types.
|
|
3
|
+
|
|
4
|
+
This module provides framework-specific scaling functions that handle conversion
|
|
5
|
+
between floating point and integer dtypes with proper range scaling.
|
|
6
|
+
|
|
7
|
+
Uses enum-driven metaprogramming to eliminate 276 lines of duplication (82% reduction).
|
|
8
|
+
Pattern follows PR #38: pure data → eval() → single generic function.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from functools import partial
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from arraybridge.framework_config import _FRAMEWORK_CONFIG
|
|
16
|
+
from arraybridge.types import MemoryType
|
|
17
|
+
from arraybridge.utils import optional_import
|
|
18
|
+
|
|
19
|
+
# Scaling ranges for integer dtypes (shared across all memory types)
|
|
20
|
+
_SCALING_RANGES = {
|
|
21
|
+
'uint8': 255.0,
|
|
22
|
+
'uint16': 65535.0,
|
|
23
|
+
'uint32': 4294967295.0,
|
|
24
|
+
'int16': (65535.0, 32768.0), # (scale, offset)
|
|
25
|
+
'int32': (4294967295.0, 2147483648.0),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# NOTE: Framework-specific scaling operations now defined in framework_config.py
|
|
30
|
+
# This eliminates the scattered _FRAMEWORK_OPS dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _scale_generic(result, target_dtype, mem_type: MemoryType):
|
|
34
|
+
"""
|
|
35
|
+
Generic scaling function that works for all memory types using framework config.
|
|
36
|
+
|
|
37
|
+
This single function replaces 6 nearly-identical scaling functions.
|
|
38
|
+
"""
|
|
39
|
+
# Special case: pyclesperanto
|
|
40
|
+
if mem_type == MemoryType.PYCLESPERANTO:
|
|
41
|
+
return _scale_pyclesperanto(result, target_dtype)
|
|
42
|
+
|
|
43
|
+
config = _FRAMEWORK_CONFIG[mem_type]
|
|
44
|
+
ops = config['scaling_ops']
|
|
45
|
+
mod = optional_import(mem_type.value) # noqa: F841 (used in eval)
|
|
46
|
+
if mod is None:
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
if not hasattr(result, 'dtype'):
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
# Handle dtype mapping for frameworks that need it
|
|
53
|
+
target_dtype_mapped = target_dtype # noqa: F841 (used in eval)
|
|
54
|
+
if ops.get('needs_dtype_map'):
|
|
55
|
+
dtype_map = {
|
|
56
|
+
np.uint8: mod.uint8, np.int8: mod.int8, np.int16: mod.int16,
|
|
57
|
+
np.int32: mod.int32, np.int64: mod.int64, np.float16: mod.float16,
|
|
58
|
+
np.float32: mod.float32, np.float64: mod.float64,
|
|
59
|
+
}
|
|
60
|
+
target_dtype_mapped = dtype_map.get(target_dtype, mod.float32) # noqa: F841
|
|
61
|
+
|
|
62
|
+
# Extra imports (e.g., jax.numpy)
|
|
63
|
+
if 'extra_import' in ops:
|
|
64
|
+
jnp = optional_import(ops['extra_import']) # noqa: F841 (used in eval)
|
|
65
|
+
|
|
66
|
+
# Check if conversion needed (float → int)
|
|
67
|
+
result_is_float = eval(ops['check_float'])
|
|
68
|
+
target_is_int = eval(ops['check_int'])
|
|
69
|
+
|
|
70
|
+
if not (result_is_float and target_is_int):
|
|
71
|
+
# Direct conversion
|
|
72
|
+
return eval(ops['astype'])
|
|
73
|
+
|
|
74
|
+
# Get min/max
|
|
75
|
+
result_min = eval(ops['min']) # noqa: F841 (used in eval)
|
|
76
|
+
result_max = eval(ops['max']) # noqa: F841 (used in eval)
|
|
77
|
+
|
|
78
|
+
if result_max <= result_min:
|
|
79
|
+
# Constant image
|
|
80
|
+
return eval(ops['astype'])
|
|
81
|
+
|
|
82
|
+
# Normalize to [0, 1]
|
|
83
|
+
normalized = (result - result_min) / (result_max - result_min) # noqa: F841 (used in eval)
|
|
84
|
+
|
|
85
|
+
# Scale to target range
|
|
86
|
+
if hasattr(target_dtype, '__name__'):
|
|
87
|
+
dtype_name = target_dtype.__name__
|
|
88
|
+
else:
|
|
89
|
+
dtype_name = str(target_dtype).split('.')[-1]
|
|
90
|
+
|
|
91
|
+
if dtype_name in _SCALING_RANGES:
|
|
92
|
+
range_info = _SCALING_RANGES[dtype_name]
|
|
93
|
+
if isinstance(range_info, tuple):
|
|
94
|
+
scale_val, offset_val = range_info
|
|
95
|
+
result = normalized * scale_val - offset_val # noqa: F841 (used in eval)
|
|
96
|
+
else:
|
|
97
|
+
result = normalized * range_info # noqa: F841 (used in eval)
|
|
98
|
+
else:
|
|
99
|
+
result = normalized # noqa: F841 (used in eval)
|
|
100
|
+
|
|
101
|
+
# Convert dtype
|
|
102
|
+
return eval(ops['astype'])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _scale_pyclesperanto(result, target_dtype):
|
|
106
|
+
"""Scale pyclesperanto results (GPU operations require special handling)."""
|
|
107
|
+
cle = optional_import("pyclesperanto")
|
|
108
|
+
if cle is None or not hasattr(result, 'dtype'):
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
# Check if result is floating point and target is integer
|
|
112
|
+
result_is_float = np.issubdtype(result.dtype, np.floating)
|
|
113
|
+
target_is_int = target_dtype in [np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32]
|
|
114
|
+
|
|
115
|
+
if not (result_is_float and target_is_int):
|
|
116
|
+
# Direct conversion
|
|
117
|
+
return cle.push(cle.pull(result).astype(target_dtype))
|
|
118
|
+
|
|
119
|
+
# Get min/max
|
|
120
|
+
result_min = float(cle.minimum_of_all_pixels(result))
|
|
121
|
+
result_max = float(cle.maximum_of_all_pixels(result))
|
|
122
|
+
|
|
123
|
+
if result_max <= result_min:
|
|
124
|
+
# Constant image
|
|
125
|
+
return cle.push(cle.pull(result).astype(target_dtype))
|
|
126
|
+
|
|
127
|
+
# Normalize to [0, 1] using GPU operations
|
|
128
|
+
normalized = cle.subtract_image_from_scalar(result, scalar=result_min)
|
|
129
|
+
range_val = result_max - result_min
|
|
130
|
+
normalized = cle.multiply_image_and_scalar(normalized, scalar=1.0/range_val)
|
|
131
|
+
|
|
132
|
+
# Scale to target range
|
|
133
|
+
dtype_name = target_dtype.__name__
|
|
134
|
+
if dtype_name in _SCALING_RANGES:
|
|
135
|
+
range_info = _SCALING_RANGES[dtype_name]
|
|
136
|
+
if isinstance(range_info, tuple):
|
|
137
|
+
scale_val, offset_val = range_info
|
|
138
|
+
scaled = cle.multiply_image_and_scalar(normalized, scalar=scale_val)
|
|
139
|
+
scaled = cle.subtract_image_from_scalar(scaled, scalar=offset_val)
|
|
140
|
+
else:
|
|
141
|
+
scaled = cle.multiply_image_and_scalar(normalized, scalar=range_info)
|
|
142
|
+
else:
|
|
143
|
+
scaled = normalized
|
|
144
|
+
|
|
145
|
+
# Convert dtype
|
|
146
|
+
return cle.push(cle.pull(scaled).astype(target_dtype))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Auto-generate all scaling functions using partial application
|
|
150
|
+
_SCALING_FUNCTIONS_GENERATED = {
|
|
151
|
+
mem_type.value: partial(_scale_generic, mem_type=mem_type)
|
|
152
|
+
for mem_type in MemoryType
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Registry mapping memory type names to scaling functions (backward compatibility)
|
|
156
|
+
SCALING_FUNCTIONS = _SCALING_FUNCTIONS_GENERATED
|
|
157
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exceptions for arraybridge."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MemoryConversionError(Exception):
|
|
5
|
+
"""
|
|
6
|
+
Exception raised when memory conversion fails.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
source_type: The source memory type
|
|
10
|
+
target_type: The target memory type
|
|
11
|
+
method: The conversion method that was attempted
|
|
12
|
+
reason: The reason for the failure
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, source_type: str, target_type: str, method: str, reason: str):
|
|
16
|
+
self.source_type = source_type
|
|
17
|
+
self.target_type = target_type
|
|
18
|
+
self.method = method
|
|
19
|
+
self.reason = reason
|
|
20
|
+
|
|
21
|
+
message = (
|
|
22
|
+
f"Cannot convert from {source_type} to {target_type} using {method}. "
|
|
23
|
+
f"Reason: {reason}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
super().__init__(message)
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Single source of truth for ALL framework-specific behavior.
|
|
3
|
+
|
|
4
|
+
This module consolidates all framework-specific logic that was previously
|
|
5
|
+
scattered across utils.py, stack_utils.py, gpu_cleanup.py, dtype_scaling.py,
|
|
6
|
+
and framework_ops.py.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
- Framework handlers: Custom logic for special cases (pyclesperanto, JAX, TensorFlow)
|
|
10
|
+
- Unified config: Single _FRAMEWORK_CONFIG dict with all framework metadata
|
|
11
|
+
- Polymorphic dispatch: Handlers can be callables or eval expressions
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
from arraybridge.types import MemoryType
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================================
|
|
23
|
+
# FRAMEWORK HANDLERS - All special-case logic lives here
|
|
24
|
+
# ============================================================================
|
|
25
|
+
|
|
26
|
+
def _pyclesperanto_get_device_id(data: Any, mod: Any) -> int:
|
|
27
|
+
"""Get device ID for pyclesperanto array."""
|
|
28
|
+
try:
|
|
29
|
+
current_device = mod.get_device()
|
|
30
|
+
if hasattr(current_device, 'id'):
|
|
31
|
+
return current_device.id
|
|
32
|
+
devices = mod.list_available_devices()
|
|
33
|
+
for i, device in enumerate(devices):
|
|
34
|
+
if str(device) == str(current_device):
|
|
35
|
+
return i
|
|
36
|
+
return 0
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.warning(f"Failed to get device ID for pyclesperanto: {e}")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pyclesperanto_set_device(device_id: int, mod: Any) -> None:
|
|
43
|
+
"""Set device for pyclesperanto."""
|
|
44
|
+
devices = mod.list_available_devices()
|
|
45
|
+
if device_id >= len(devices):
|
|
46
|
+
raise ValueError(f"Device {device_id} not available. Available: {len(devices)}")
|
|
47
|
+
mod.select_device(device_id)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _pyclesperanto_move_to_device(data: Any, device_id: int, mod: Any, memory_type: str) -> Any:
|
|
51
|
+
"""Move pyclesperanto array to device."""
|
|
52
|
+
# Import here to avoid circular dependency
|
|
53
|
+
from arraybridge.utils import _get_device_id
|
|
54
|
+
|
|
55
|
+
current_device_id = _get_device_id(data, memory_type)
|
|
56
|
+
|
|
57
|
+
if current_device_id != device_id:
|
|
58
|
+
mod.select_device(device_id)
|
|
59
|
+
result = mod.create_like(data)
|
|
60
|
+
mod.copy(data, result)
|
|
61
|
+
return result
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _pyclesperanto_stack_slices(slices: list, memory_type: str, gpu_id: int, mod: Any) -> Any:
|
|
66
|
+
"""Stack slices using pyclesperanto's concatenate_along_z."""
|
|
67
|
+
from arraybridge.converters import convert_memory, detect_memory_type
|
|
68
|
+
|
|
69
|
+
converted_slices = []
|
|
70
|
+
conversion_count = 0
|
|
71
|
+
|
|
72
|
+
for slice_data in slices:
|
|
73
|
+
source_type = detect_memory_type(slice_data)
|
|
74
|
+
|
|
75
|
+
if source_type != memory_type:
|
|
76
|
+
conversion_count += 1
|
|
77
|
+
|
|
78
|
+
if source_type == memory_type:
|
|
79
|
+
converted_slices.append(slice_data)
|
|
80
|
+
else:
|
|
81
|
+
converted = convert_memory(slice_data, source_type, memory_type, gpu_id)
|
|
82
|
+
converted_slices.append(converted)
|
|
83
|
+
|
|
84
|
+
# Log batch conversion
|
|
85
|
+
if conversion_count > 0:
|
|
86
|
+
logger.debug(
|
|
87
|
+
f"🔄 MEMORY CONVERSION: Converted {conversion_count}/{len(slices)} slices "
|
|
88
|
+
f"to {memory_type} for pyclesperanto stacking"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return mod.concatenate_along_z(converted_slices)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _jax_assign_slice(result: Any, index: int, slice_data: Any) -> Any:
|
|
95
|
+
"""Assign slice to JAX array (immutable)."""
|
|
96
|
+
return result.at[index].set(slice_data)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _tensorflow_validate_dlpack(obj: Any, mod: Any) -> bool:
|
|
100
|
+
"""Validate TensorFlow DLPack support."""
|
|
101
|
+
# Check version
|
|
102
|
+
major, minor = map(int, mod.__version__.split('.')[:2])
|
|
103
|
+
if major < 2 or (major == 2 and minor < 12):
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"TensorFlow {mod.__version__} does not support stable DLPack. "
|
|
106
|
+
f"Version 2.12.0+ required. "
|
|
107
|
+
f"Clause 88 violation: Cannot infer DLPack capability."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Check GPU
|
|
111
|
+
device_str = obj.device.lower()
|
|
112
|
+
if "gpu" not in device_str:
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"TensorFlow tensor on CPU cannot use DLPack operations reliably. "
|
|
115
|
+
"Only GPU tensors are supported for DLPack operations. "
|
|
116
|
+
"Clause 88 violation: Cannot infer GPU capability."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Check module
|
|
120
|
+
if not hasattr(mod.experimental, "dlpack"):
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"TensorFlow installation missing experimental.dlpack module. "
|
|
123
|
+
"Clause 88 violation: Cannot infer DLPack capability."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _numpy_dtype_conversion_needed(first_slice: Any, detect_memory_type_func: Callable) -> bool:
|
|
130
|
+
"""Check if NumPy needs dtype conversion (only for torch sources)."""
|
|
131
|
+
source_type = detect_memory_type_func(first_slice)
|
|
132
|
+
return source_type == MemoryType.TORCH.value
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _torch_dtype_conversion_needed(first_slice: Any, detect_memory_type_func: Callable) -> bool:
|
|
136
|
+
"""Torch always needs dtype conversion to get correct torch dtype."""
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ============================================================================
|
|
141
|
+
# UNIFIED FRAMEWORK CONFIGURATION
|
|
142
|
+
# ============================================================================
|
|
143
|
+
|
|
144
|
+
_FRAMEWORK_CONFIG = {
|
|
145
|
+
MemoryType.NUMPY: {
|
|
146
|
+
# Metadata
|
|
147
|
+
'import_name': 'numpy',
|
|
148
|
+
'display_name': 'NumPy',
|
|
149
|
+
'is_gpu': False,
|
|
150
|
+
|
|
151
|
+
# Device operations
|
|
152
|
+
'get_device_id': None, # CPU
|
|
153
|
+
'set_device': None, # CPU
|
|
154
|
+
'move_to_device': None, # CPU
|
|
155
|
+
|
|
156
|
+
# Stack operations
|
|
157
|
+
'allocate_stack': 'np.empty(stack_shape, dtype=dtype)',
|
|
158
|
+
'allocate_context': None,
|
|
159
|
+
'needs_dtype_conversion': _numpy_dtype_conversion_needed, # Callable
|
|
160
|
+
'assign_slice': None, # Standard: result[i] = slice
|
|
161
|
+
'stack_handler': None, # Standard stacking
|
|
162
|
+
|
|
163
|
+
# Dtype scaling
|
|
164
|
+
'scaling_ops': {
|
|
165
|
+
'min': 'result.min()',
|
|
166
|
+
'max': 'result.max()',
|
|
167
|
+
'astype': 'result.astype(target_dtype)',
|
|
168
|
+
'check_float': 'np.issubdtype(result.dtype, np.floating)',
|
|
169
|
+
'check_int': 'target_dtype in [np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32]', # noqa: E501
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
# Conversion operations
|
|
173
|
+
'conversion_ops': {
|
|
174
|
+
'to_numpy': 'data',
|
|
175
|
+
'from_numpy': 'data',
|
|
176
|
+
'from_dlpack': None,
|
|
177
|
+
'move_to_device': 'data',
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
# DLPack
|
|
181
|
+
'supports_dlpack': False,
|
|
182
|
+
'validate_dlpack': None,
|
|
183
|
+
|
|
184
|
+
# GPU/Cleanup
|
|
185
|
+
'lazy_getter': None,
|
|
186
|
+
'gpu_check': None,
|
|
187
|
+
'stream_context': None,
|
|
188
|
+
'device_context': None,
|
|
189
|
+
'cleanup_ops': None,
|
|
190
|
+
'has_oom_recovery': False,
|
|
191
|
+
'oom_exception_types': [],
|
|
192
|
+
'oom_string_patterns': ['cannot allocate memory', 'memory exhausted'],
|
|
193
|
+
'oom_clear_cache': 'import gc; gc.collect()',
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
MemoryType.CUPY: {
|
|
197
|
+
# Metadata
|
|
198
|
+
'import_name': 'cupy',
|
|
199
|
+
'display_name': 'CuPy',
|
|
200
|
+
'is_gpu': True,
|
|
201
|
+
|
|
202
|
+
# Device operations (eval expressions)
|
|
203
|
+
'get_device_id': 'data.device.id',
|
|
204
|
+
'get_device_id_fallback': '0',
|
|
205
|
+
'set_device': '{mod}.cuda.Device(device_id).use()',
|
|
206
|
+
'move_to_device': 'data.copy() if data.device.id != device_id else data',
|
|
207
|
+
'move_context': '{mod}.cuda.Device(device_id)',
|
|
208
|
+
|
|
209
|
+
# Stack operations
|
|
210
|
+
'allocate_stack': 'cupy.empty(stack_shape, dtype=first_slice.dtype)',
|
|
211
|
+
'allocate_context': 'cupy.cuda.Device(gpu_id)',
|
|
212
|
+
'needs_dtype_conversion': False,
|
|
213
|
+
'assign_slice': None, # Standard
|
|
214
|
+
'stack_handler': None, # Standard
|
|
215
|
+
|
|
216
|
+
# Dtype scaling
|
|
217
|
+
'scaling_ops': {
|
|
218
|
+
'min': 'mod.min(result)',
|
|
219
|
+
'max': 'mod.max(result)',
|
|
220
|
+
'astype': 'result.astype(target_dtype)',
|
|
221
|
+
'check_float': 'mod.issubdtype(result.dtype, mod.floating)',
|
|
222
|
+
'check_int': 'not mod.issubdtype(target_dtype, mod.floating)',
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
# Conversion operations
|
|
226
|
+
'conversion_ops': {
|
|
227
|
+
'to_numpy': 'data.get()',
|
|
228
|
+
'from_numpy': '({mod}.cuda.Device(gpu_id), {mod}.array(data))[1]',
|
|
229
|
+
'from_dlpack': '{mod}.from_dlpack(data)',
|
|
230
|
+
'move_to_device': 'data if data.device.id == gpu_id else ({mod}.cuda.Device(gpu_id), {mod}.array(data))[1]', # noqa: E501
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
# DLPack
|
|
234
|
+
'supports_dlpack': True,
|
|
235
|
+
'validate_dlpack': None,
|
|
236
|
+
|
|
237
|
+
# GPU/Cleanup
|
|
238
|
+
'lazy_getter': '_get_cupy',
|
|
239
|
+
'gpu_check': '{mod} is not None and hasattr({mod}, "cuda")',
|
|
240
|
+
'stream_context': '{mod}.cuda.Stream()',
|
|
241
|
+
'device_context': '{mod}.cuda.Device({device_id})',
|
|
242
|
+
'cleanup_ops': '{mod}.get_default_memory_pool().free_all_blocks(); {mod}.get_default_pinned_memory_pool().free_all_blocks(); {mod}.cuda.runtime.deviceSynchronize()', # noqa: E501
|
|
243
|
+
'has_oom_recovery': True,
|
|
244
|
+
'oom_exception_types': ['{mod}.cuda.memory.OutOfMemoryError', '{mod}.cuda.runtime.CUDARuntimeError'], # noqa: E501
|
|
245
|
+
'oom_string_patterns': ['out of memory', 'cuda_error_out_of_memory'],
|
|
246
|
+
'oom_clear_cache': '{mod}.get_default_memory_pool().free_all_blocks(); {mod}.get_default_pinned_memory_pool().free_all_blocks(); {mod}.cuda.runtime.deviceSynchronize()', # noqa: E501
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
MemoryType.TORCH: {
|
|
250
|
+
# Metadata
|
|
251
|
+
'import_name': 'torch',
|
|
252
|
+
'display_name': 'PyTorch',
|
|
253
|
+
'is_gpu': True,
|
|
254
|
+
|
|
255
|
+
# Device operations
|
|
256
|
+
'get_device_id': 'data.device.index if data.is_cuda else None',
|
|
257
|
+
'get_device_id_fallback': 'None',
|
|
258
|
+
'set_device': None, # PyTorch handles device at tensor creation
|
|
259
|
+
'move_to_device': 'data.to(f"cuda:{device_id}") if (not data.is_cuda or data.device.index != device_id) else data', # noqa: E501
|
|
260
|
+
|
|
261
|
+
# Stack operations
|
|
262
|
+
'allocate_stack': 'torch.empty(stack_shape, dtype=sample_converted.dtype, device=sample_converted.device)', # noqa: E501
|
|
263
|
+
'allocate_context': None,
|
|
264
|
+
'needs_dtype_conversion': _torch_dtype_conversion_needed, # Callable
|
|
265
|
+
'assign_slice': None, # Standard
|
|
266
|
+
'stack_handler': None, # Standard
|
|
267
|
+
|
|
268
|
+
# Dtype scaling
|
|
269
|
+
'scaling_ops': {
|
|
270
|
+
'min': 'result.min()',
|
|
271
|
+
'max': 'result.max()',
|
|
272
|
+
'astype': 'result.to(target_dtype_mapped)',
|
|
273
|
+
'check_float': 'result.dtype in [mod.float16, mod.float32, mod.float64]',
|
|
274
|
+
'check_int': 'target_dtype_mapped in [mod.uint8, mod.int8, mod.int16, mod.int32, mod.int64]', # noqa: E501
|
|
275
|
+
'needs_dtype_map': True,
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
# Conversion operations
|
|
279
|
+
'conversion_ops': {
|
|
280
|
+
'to_numpy': 'data.cpu().numpy()',
|
|
281
|
+
'from_numpy': '{mod}.from_numpy(data).cuda(gpu_id)',
|
|
282
|
+
'from_dlpack': '{mod}.from_dlpack(data)',
|
|
283
|
+
'move_to_device': 'data if data.device.index == gpu_id else data.cuda(gpu_id)',
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
# DLPack
|
|
287
|
+
'supports_dlpack': True,
|
|
288
|
+
'validate_dlpack': None,
|
|
289
|
+
|
|
290
|
+
# GPU/Cleanup
|
|
291
|
+
'lazy_getter': '_get_torch',
|
|
292
|
+
'gpu_check': '{mod} is not None and hasattr({mod}, "cuda") and {mod}.cuda.is_available()',
|
|
293
|
+
'stream_context': '{mod}.cuda.Stream()',
|
|
294
|
+
'device_context': '{mod}.cuda.device({device_id})',
|
|
295
|
+
'cleanup_ops': '{mod}.cuda.empty_cache(); {mod}.cuda.synchronize()',
|
|
296
|
+
'has_oom_recovery': True,
|
|
297
|
+
'oom_exception_types': ['{mod}.cuda.OutOfMemoryError'],
|
|
298
|
+
'oom_string_patterns': ['out of memory', 'cuda_error_out_of_memory'],
|
|
299
|
+
'oom_clear_cache': '{mod}.cuda.empty_cache(); {mod}.cuda.synchronize()',
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
MemoryType.TENSORFLOW: {
|
|
303
|
+
# Metadata
|
|
304
|
+
'import_name': 'tensorflow',
|
|
305
|
+
'display_name': 'TensorFlow',
|
|
306
|
+
'is_gpu': True,
|
|
307
|
+
|
|
308
|
+
# Device operations
|
|
309
|
+
'get_device_id': 'int(data.device.lower().split(":")[-1]) if "gpu" in data.device.lower() else None', # noqa: E501
|
|
310
|
+
'get_device_id_fallback': 'None',
|
|
311
|
+
'set_device': None, # TensorFlow handles device at tensor creation
|
|
312
|
+
'move_to_device': '{mod}.identity(data)',
|
|
313
|
+
'move_context': '{mod}.device(f"/device:GPU:{device_id}")',
|
|
314
|
+
|
|
315
|
+
# Stack operations
|
|
316
|
+
'allocate_stack': 'tf.zeros(stack_shape, dtype=first_slice.dtype)', # TF doesn't have empty() # noqa: E501
|
|
317
|
+
'allocate_context': 'tf.device(f"/device:GPU:{gpu_id}")',
|
|
318
|
+
'needs_dtype_conversion': False,
|
|
319
|
+
'assign_slice': None, # Standard
|
|
320
|
+
'stack_handler': None, # Standard
|
|
321
|
+
|
|
322
|
+
# Dtype scaling
|
|
323
|
+
'scaling_ops': {
|
|
324
|
+
'min': 'mod.reduce_min(result)',
|
|
325
|
+
'max': 'mod.reduce_max(result)',
|
|
326
|
+
'astype': 'mod.cast(result, target_dtype_mapped)',
|
|
327
|
+
'check_float': 'result.dtype in [mod.float16, mod.float32, mod.float64]',
|
|
328
|
+
'check_int': 'target_dtype_mapped in [mod.uint8, mod.int8, mod.int16, mod.int32, mod.int64]', # noqa: E501
|
|
329
|
+
'needs_dtype_map': True,
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
# Conversion operations
|
|
333
|
+
'conversion_ops': {
|
|
334
|
+
'to_numpy': 'data.numpy()',
|
|
335
|
+
'from_numpy': '{mod}.convert_to_tensor(data)',
|
|
336
|
+
'from_dlpack': '{mod}.experimental.dlpack.from_dlpack(data)',
|
|
337
|
+
'move_to_device': 'data',
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
# DLPack
|
|
341
|
+
'supports_dlpack': True,
|
|
342
|
+
'validate_dlpack': _tensorflow_validate_dlpack, # Custom validation
|
|
343
|
+
|
|
344
|
+
# GPU/Cleanup
|
|
345
|
+
'lazy_getter': '_get_tensorflow',
|
|
346
|
+
'gpu_check': '{mod} is not None and {mod}.config.list_physical_devices("GPU")',
|
|
347
|
+
'stream_context': None, # TensorFlow manages streams internally
|
|
348
|
+
'device_context': '{mod}.device("/GPU:0")',
|
|
349
|
+
'cleanup_ops': None, # TensorFlow has no explicit cache clearing API
|
|
350
|
+
'has_oom_recovery': True,
|
|
351
|
+
'oom_exception_types': [
|
|
352
|
+
'{mod}.errors.ResourceExhaustedError',
|
|
353
|
+
'{mod}.errors.InvalidArgumentError',
|
|
354
|
+
],
|
|
355
|
+
'oom_string_patterns': ['out of memory', 'resource_exhausted'],
|
|
356
|
+
'oom_clear_cache': None, # TensorFlow has no explicit cache clearing API
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
MemoryType.JAX: {
|
|
360
|
+
# Metadata
|
|
361
|
+
'import_name': 'jax',
|
|
362
|
+
'display_name': 'JAX',
|
|
363
|
+
'is_gpu': True,
|
|
364
|
+
|
|
365
|
+
# Device operations
|
|
366
|
+
'get_device_id': 'int(str(data.device).lower().split(":")[-1]) if "gpu" in str(data.device).lower() else None', # noqa: E501
|
|
367
|
+
'get_device_id_fallback': 'None',
|
|
368
|
+
'set_device': None, # JAX handles device at array creation
|
|
369
|
+
'move_to_device': '{mod}.device_put(data, {mod}.devices("gpu")[device_id])',
|
|
370
|
+
|
|
371
|
+
# Stack operations
|
|
372
|
+
'allocate_stack': 'jnp.empty(stack_shape, dtype=first_slice.dtype)',
|
|
373
|
+
'allocate_context': None,
|
|
374
|
+
'needs_dtype_conversion': False,
|
|
375
|
+
'assign_slice': _jax_assign_slice, # Custom handler for immutability
|
|
376
|
+
'stack_handler': None, # Standard
|
|
377
|
+
|
|
378
|
+
# Dtype scaling
|
|
379
|
+
'scaling_ops': {
|
|
380
|
+
'min': 'jnp.min(result)',
|
|
381
|
+
'max': 'jnp.max(result)',
|
|
382
|
+
'astype': 'result.astype(target_dtype_mapped)',
|
|
383
|
+
'check_float': 'result.dtype in [jnp.float16, jnp.float32, jnp.float64]',
|
|
384
|
+
'check_int': 'target_dtype_mapped in [jnp.uint8, jnp.int8, jnp.int16, jnp.int32, jnp.int64]', # noqa: E501
|
|
385
|
+
'needs_dtype_map': True,
|
|
386
|
+
'extra_import': 'jax.numpy',
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
# Conversion operations
|
|
390
|
+
'conversion_ops': {
|
|
391
|
+
'to_numpy': 'np.asarray(data)',
|
|
392
|
+
'from_numpy': '{mod}.device_put(data, {mod}.devices()[gpu_id])',
|
|
393
|
+
'from_dlpack': '{mod}.dlpack.from_dlpack(data)',
|
|
394
|
+
'move_to_device': 'data',
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
# DLPack
|
|
398
|
+
'supports_dlpack': True,
|
|
399
|
+
'validate_dlpack': None,
|
|
400
|
+
|
|
401
|
+
# GPU/Cleanup
|
|
402
|
+
'lazy_getter': '_get_jax',
|
|
403
|
+
'gpu_check': '{mod} is not None and any(d.platform == "gpu" for d in {mod}.devices())',
|
|
404
|
+
'stream_context': None, # JAX/XLA manages streams internally
|
|
405
|
+
'device_context': '{mod}.default_device([d for d in {mod}.devices() if d.platform == "gpu"][0])', # noqa: E501
|
|
406
|
+
'cleanup_ops': '{mod}.clear_caches()',
|
|
407
|
+
'has_oom_recovery': True,
|
|
408
|
+
'oom_exception_types': [],
|
|
409
|
+
'oom_string_patterns': ['out of memory', 'oom when allocating', 'allocation failure'],
|
|
410
|
+
'oom_clear_cache': '{mod}.clear_caches()',
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
MemoryType.PYCLESPERANTO: {
|
|
414
|
+
# Metadata
|
|
415
|
+
'import_name': 'pyclesperanto',
|
|
416
|
+
'display_name': 'pyclesperanto',
|
|
417
|
+
'is_gpu': True,
|
|
418
|
+
|
|
419
|
+
# Device operations (custom handlers)
|
|
420
|
+
'get_device_id': _pyclesperanto_get_device_id, # Callable
|
|
421
|
+
'get_device_id_fallback': '0',
|
|
422
|
+
'set_device': _pyclesperanto_set_device, # Callable
|
|
423
|
+
'move_to_device': _pyclesperanto_move_to_device, # Callable
|
|
424
|
+
|
|
425
|
+
# Stack operations (custom handler)
|
|
426
|
+
'allocate_stack': None, # Uses concatenate_along_z
|
|
427
|
+
'allocate_context': None,
|
|
428
|
+
'needs_dtype_conversion': False,
|
|
429
|
+
'assign_slice': None, # Not used (custom stacking)
|
|
430
|
+
'stack_handler': _pyclesperanto_stack_slices, # Custom stacking
|
|
431
|
+
|
|
432
|
+
# Conversion operations
|
|
433
|
+
'conversion_ops': {
|
|
434
|
+
'to_numpy': '{mod}.pull(data)',
|
|
435
|
+
'from_numpy': '{mod}.push(data)',
|
|
436
|
+
'from_dlpack': None,
|
|
437
|
+
'move_to_device': 'data',
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
# Dtype scaling (custom implementation in dtype_scaling.py)
|
|
441
|
+
'scaling_ops': None, # Custom _scale_pyclesperanto function
|
|
442
|
+
|
|
443
|
+
# DLPack
|
|
444
|
+
'supports_dlpack': False,
|
|
445
|
+
'validate_dlpack': None,
|
|
446
|
+
|
|
447
|
+
# GPU/Cleanup
|
|
448
|
+
'lazy_getter': None,
|
|
449
|
+
'gpu_check': None, # pyclesperanto always uses GPU if available
|
|
450
|
+
'stream_context': None, # OpenCL manages streams internally
|
|
451
|
+
'device_context': None, # OpenCL device selection is global
|
|
452
|
+
'cleanup_ops': None, # pyclesperanto/OpenCL has no explicit cache clearing API
|
|
453
|
+
'has_oom_recovery': True,
|
|
454
|
+
'oom_exception_types': [],
|
|
455
|
+
'oom_string_patterns': ['cl_mem_object_allocation_failure', 'cl_out_of_resources', 'out of memory'], # noqa: E501
|
|
456
|
+
'oom_clear_cache': None, # pyclesperanto/OpenCL has no explicit cache clearing API
|
|
457
|
+
},
|
|
458
|
+
}
|
|
459
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework operations data for memory type system.
|
|
3
|
+
|
|
4
|
+
This module now imports from the unified framework_config.py.
|
|
5
|
+
All framework-specific operations are consolidated in a single source of truth.
|
|
6
|
+
|
|
7
|
+
DEPRECATED: This module is maintained for backward compatibility.
|
|
8
|
+
New code should import directly from framework_config.py.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from arraybridge.framework_config import _FRAMEWORK_CONFIG
|
|
12
|
+
|
|
13
|
+
# Re-export for backward compatibility
|
|
14
|
+
_FRAMEWORK_OPS = _FRAMEWORK_CONFIG
|
|
15
|
+
|