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,149 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GPU memory cleanup utilities for different frameworks.
|
|
3
|
+
|
|
4
|
+
This module provides unified GPU memory cleanup functions for PyTorch, CuPy,
|
|
5
|
+
TensorFlow, JAX, and pyclesperanto. The cleanup functions are designed to be called
|
|
6
|
+
after processing steps to free up GPU memory that's no longer needed.
|
|
7
|
+
|
|
8
|
+
REFACTORED: Uses enum-driven metaprogramming to eliminate 67% of code duplication.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import gc
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from arraybridge.framework_config import _FRAMEWORK_CONFIG
|
|
16
|
+
from arraybridge.types import MemoryType
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_cleanup_function(mem_type: MemoryType):
|
|
27
|
+
"""
|
|
28
|
+
Factory function that creates a cleanup function for a specific memory type.
|
|
29
|
+
|
|
30
|
+
This single factory replaces 6 nearly-identical cleanup functions.
|
|
31
|
+
"""
|
|
32
|
+
config = _FRAMEWORK_CONFIG[mem_type]
|
|
33
|
+
framework_name = config['import_name']
|
|
34
|
+
display_name = config['display_name']
|
|
35
|
+
|
|
36
|
+
# CPU memory type - no cleanup needed
|
|
37
|
+
if config['cleanup_ops'] is None:
|
|
38
|
+
def cleanup(device_id: Optional[int] = None) -> None:
|
|
39
|
+
"""No-op cleanup for CPU memory type."""
|
|
40
|
+
logger.debug(f"🔥 GPU CLEANUP: No-op for {display_name} (CPU memory type)")
|
|
41
|
+
|
|
42
|
+
cleanup.__name__ = f"cleanup_{framework_name}_gpu"
|
|
43
|
+
cleanup.__doc__ = f"No-op cleanup for {display_name} (CPU memory type)."
|
|
44
|
+
return cleanup
|
|
45
|
+
|
|
46
|
+
# GPU memory type - generate cleanup function
|
|
47
|
+
def cleanup(device_id: Optional[int] = None) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Clean up {display_name} GPU memory.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
device_id: Optional GPU device ID. If None, cleans all devices.
|
|
53
|
+
"""
|
|
54
|
+
framework = globals().get(framework_name)
|
|
55
|
+
|
|
56
|
+
if framework is None:
|
|
57
|
+
logger.debug(f"{display_name} not available, skipping cleanup")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
# Check GPU availability
|
|
62
|
+
gpu_check_expr = config['gpu_check'].format(mod=framework_name)
|
|
63
|
+
try:
|
|
64
|
+
gpu_available = eval(gpu_check_expr, {framework_name: framework})
|
|
65
|
+
except Exception:
|
|
66
|
+
gpu_available = False
|
|
67
|
+
|
|
68
|
+
if not gpu_available:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Execute cleanup operations
|
|
72
|
+
if device_id is not None and config['device_context'] is not None:
|
|
73
|
+
# Clean specific device with context
|
|
74
|
+
device_ctx_expr = config['device_context'].format(
|
|
75
|
+
device_id=device_id, mod=framework_name
|
|
76
|
+
)
|
|
77
|
+
device_ctx = eval(device_ctx_expr, {framework_name: framework})
|
|
78
|
+
|
|
79
|
+
with device_ctx:
|
|
80
|
+
# Execute cleanup operations
|
|
81
|
+
cleanup_expr = config['cleanup_ops'].format(mod=framework_name)
|
|
82
|
+
exec(cleanup_expr, {framework_name: framework, 'gc': gc})
|
|
83
|
+
|
|
84
|
+
logger.debug(f"🔥 GPU CLEANUP: Cleared {display_name} for device {device_id}")
|
|
85
|
+
else:
|
|
86
|
+
# Clean all devices (no device context)
|
|
87
|
+
cleanup_expr = config['cleanup_ops'].format(mod=framework_name)
|
|
88
|
+
exec(cleanup_expr, {framework_name: framework, 'gc': gc})
|
|
89
|
+
logger.debug(f"🔥 GPU CLEANUP: Cleared {display_name} for all devices")
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Failed to cleanup {display_name} GPU memory: {e}")
|
|
93
|
+
|
|
94
|
+
# Set proper function name and docstring
|
|
95
|
+
cleanup.__name__ = f"cleanup_{framework_name}_gpu"
|
|
96
|
+
cleanup.__doc__ = cleanup.__doc__.format(display_name=display_name)
|
|
97
|
+
|
|
98
|
+
return cleanup
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Auto-generate all cleanup functions
|
|
102
|
+
for mem_type in MemoryType:
|
|
103
|
+
cleanup_func = _create_cleanup_function(mem_type)
|
|
104
|
+
globals()[cleanup_func.__name__] = cleanup_func
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Auto-generate cleanup registry
|
|
108
|
+
MEMORY_TYPE_CLEANUP_REGISTRY = {
|
|
109
|
+
mem_type.value: globals()[f"cleanup_{_FRAMEWORK_CONFIG[mem_type]['import_name']}_gpu"]
|
|
110
|
+
for mem_type in MemoryType
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def cleanup_all_gpu_frameworks(device_id: Optional[int] = None) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Clean up GPU memory for all available frameworks.
|
|
117
|
+
|
|
118
|
+
This function calls cleanup for all GPU frameworks that are currently loaded.
|
|
119
|
+
It's safe to call even if some frameworks aren't available.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
device_id: Optional GPU device ID. If None, cleans all devices.
|
|
123
|
+
"""
|
|
124
|
+
logger.debug(f"🔥 GPU CLEANUP: Starting cleanup for all GPU frameworks (device_id={device_id})")
|
|
125
|
+
|
|
126
|
+
# Only cleanup GPU memory types (those with cleanup operations)
|
|
127
|
+
for mem_type, config in _FRAMEWORK_CONFIG.items():
|
|
128
|
+
if config['cleanup_ops'] is not None:
|
|
129
|
+
cleanup_func = MEMORY_TYPE_CLEANUP_REGISTRY[mem_type.value]
|
|
130
|
+
cleanup_func(device_id)
|
|
131
|
+
|
|
132
|
+
logger.debug("🔥 GPU CLEANUP: Completed cleanup for all GPU frameworks")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Export all cleanup functions and utilities
|
|
139
|
+
__all__ = [
|
|
140
|
+
'cleanup_all_gpu_frameworks',
|
|
141
|
+
'MEMORY_TYPE_CLEANUP_REGISTRY',
|
|
142
|
+
'cleanup_numpy_gpu', # noqa: F822
|
|
143
|
+
'cleanup_cupy_gpu', # noqa: F822
|
|
144
|
+
'cleanup_torch_gpu', # noqa: F822
|
|
145
|
+
'cleanup_tensorflow_gpu', # noqa: F822
|
|
146
|
+
'cleanup_jax_gpu', # noqa: F822
|
|
147
|
+
'cleanup_pyclesperanto_gpu', # noqa: F822
|
|
148
|
+
]
|
|
149
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GPU Out of Memory (OOM) recovery utilities.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive OOM detection and cache clearing for all supported
|
|
5
|
+
GPU frameworks in OpenHCS.
|
|
6
|
+
|
|
7
|
+
REFACTORED: Uses enum-driven metaprogramming to eliminate 71% of code duplication.
|
|
8
|
+
All OOM patterns and cache clearing operations are defined in framework_ops.py.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import gc
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from arraybridge.framework_ops import _FRAMEWORK_OPS
|
|
16
|
+
from arraybridge.types import MemoryType
|
|
17
|
+
from arraybridge.utils import optional_import
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_oom_error(e: Exception, memory_type: str) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Detect Out of Memory errors for all GPU frameworks.
|
|
25
|
+
|
|
26
|
+
Auto-generated from framework_ops.py OOM patterns.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
e: Exception to check
|
|
30
|
+
memory_type: Memory type string (e.g., 'torch', 'cupy')
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if exception is an OOM error for the given framework
|
|
34
|
+
"""
|
|
35
|
+
# Find the MemoryType enum for this memory_type string
|
|
36
|
+
mem_type_enum = None
|
|
37
|
+
for mt in MemoryType:
|
|
38
|
+
if mt.value == memory_type:
|
|
39
|
+
mem_type_enum = mt
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
if mem_type_enum is None:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
ops = _FRAMEWORK_OPS[mem_type_enum]
|
|
46
|
+
error_str = str(e).lower()
|
|
47
|
+
|
|
48
|
+
# Check framework-specific exception types
|
|
49
|
+
for exc_type_expr in ops['oom_exception_types']:
|
|
50
|
+
try:
|
|
51
|
+
# Import the module and get the exception type
|
|
52
|
+
mod_name = ops['import_name']
|
|
53
|
+
mod = optional_import(mod_name)
|
|
54
|
+
if mod is None:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# Evaluate the exception type expression
|
|
58
|
+
exc_type_str = exc_type_expr.format(mod='mod')
|
|
59
|
+
# Extract the attribute path
|
|
60
|
+
# (e.g., 'mod.cuda.OutOfMemoryError' -> ['cuda', 'OutOfMemoryError'])
|
|
61
|
+
parts = exc_type_str.split('.')[1:] # Skip 'mod'
|
|
62
|
+
exc_type = mod
|
|
63
|
+
for part in parts:
|
|
64
|
+
if hasattr(exc_type, part):
|
|
65
|
+
exc_type = getattr(exc_type, part)
|
|
66
|
+
else:
|
|
67
|
+
exc_type = None
|
|
68
|
+
break
|
|
69
|
+
|
|
70
|
+
if exc_type is not None and isinstance(e, exc_type):
|
|
71
|
+
return True
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# String-based detection using framework-specific patterns
|
|
76
|
+
return any(pattern in error_str for pattern in ops['oom_string_patterns'])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _clear_cache_for_memory_type(memory_type: str, device_id: Optional[int] = None):
|
|
80
|
+
"""
|
|
81
|
+
Clear GPU cache for specific memory type.
|
|
82
|
+
|
|
83
|
+
Auto-generated from framework_ops.py cache clearing operations.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
memory_type: Memory type string (e.g., 'torch', 'cupy')
|
|
87
|
+
device_id: GPU device ID (optional, currently unused but kept for API compatibility)
|
|
88
|
+
"""
|
|
89
|
+
# Find the MemoryType enum for this memory_type string
|
|
90
|
+
mem_type_enum = None
|
|
91
|
+
for mt in MemoryType:
|
|
92
|
+
if mt.value == memory_type:
|
|
93
|
+
mem_type_enum = mt
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if mem_type_enum is None:
|
|
97
|
+
logger.warning(f"Unknown memory type for cache clearing: {memory_type}")
|
|
98
|
+
gc.collect()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
ops = _FRAMEWORK_OPS[mem_type_enum]
|
|
102
|
+
|
|
103
|
+
# Get the module
|
|
104
|
+
mod_name = ops['import_name']
|
|
105
|
+
mod = optional_import(mod_name)
|
|
106
|
+
|
|
107
|
+
if mod is None:
|
|
108
|
+
logger.warning(f"Module {mod_name} not available for cache clearing")
|
|
109
|
+
gc.collect()
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
# Execute cache clearing operations
|
|
113
|
+
cache_clear_expr = ops['oom_clear_cache']
|
|
114
|
+
if cache_clear_expr:
|
|
115
|
+
try:
|
|
116
|
+
# Execute cache clear directly (device context handled by the operations themselves)
|
|
117
|
+
exec(cache_clear_expr.format(mod=mod_name), {mod_name: mod, 'gc': gc})
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.warning(f"Failed to clear cache for {memory_type}: {e}")
|
|
120
|
+
|
|
121
|
+
# Always trigger Python garbage collection
|
|
122
|
+
gc.collect()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _execute_with_oom_recovery(func_callable, memory_type: str, max_retries: int = 2):
|
|
126
|
+
"""
|
|
127
|
+
Execute function with automatic OOM recovery.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
func_callable: Function to execute
|
|
131
|
+
memory_type: Memory type from MemoryType enum
|
|
132
|
+
max_retries: Maximum number of retry attempts
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Function result
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
Original exception if not OOM or retries exhausted
|
|
139
|
+
"""
|
|
140
|
+
for attempt in range(max_retries + 1):
|
|
141
|
+
try:
|
|
142
|
+
return func_callable()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
if not _is_oom_error(e, memory_type) or attempt == max_retries:
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
# Clear cache and retry
|
|
148
|
+
_clear_cache_for_memory_type(memory_type)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared slice-by-slice processing logic for all memory types.
|
|
3
|
+
|
|
4
|
+
This module provides a single implementation of slice-by-slice processing
|
|
5
|
+
that works for all memory types, eliminating duplication across dtype wrappers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from arraybridge.converters import detect_memory_type
|
|
9
|
+
from arraybridge.stack_utils import stack_slices, unstack_slices
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def process_slices(image, func, args, kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Process a 3D array slice-by-slice using the provided function.
|
|
15
|
+
|
|
16
|
+
This function handles:
|
|
17
|
+
- Unstacking 3D arrays into 2D slices
|
|
18
|
+
- Processing each slice independently
|
|
19
|
+
- Handling functions that return tuples (main output + special outputs)
|
|
20
|
+
- Stacking results back into 3D arrays
|
|
21
|
+
- Combining special outputs from all slices
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
image: 3D array to process
|
|
25
|
+
func: Function to apply to each slice
|
|
26
|
+
args: Positional arguments to pass to func
|
|
27
|
+
kwargs: Keyword arguments to pass to func
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Processed 3D array, or tuple of (processed_3d_array, special_outputs...)
|
|
31
|
+
if func returns tuples
|
|
32
|
+
"""
|
|
33
|
+
# Detect memory type and use proper OpenHCS utilities
|
|
34
|
+
memory_type = detect_memory_type(image)
|
|
35
|
+
gpu_id = 0 # Default GPU ID for slice processing
|
|
36
|
+
|
|
37
|
+
# Unstack 3D array into 2D slices
|
|
38
|
+
slices_2d = unstack_slices(image, memory_type, gpu_id)
|
|
39
|
+
|
|
40
|
+
# Process each slice and handle special outputs
|
|
41
|
+
main_outputs = []
|
|
42
|
+
special_outputs_list = []
|
|
43
|
+
|
|
44
|
+
for slice_2d in slices_2d:
|
|
45
|
+
slice_result = func(slice_2d, *args, **kwargs)
|
|
46
|
+
|
|
47
|
+
# Check if result is a tuple (indicating special outputs)
|
|
48
|
+
if isinstance(slice_result, tuple):
|
|
49
|
+
main_outputs.append(slice_result[0]) # First element is main output
|
|
50
|
+
special_outputs_list.append(slice_result[1:]) # Rest are special outputs
|
|
51
|
+
else:
|
|
52
|
+
main_outputs.append(slice_result) # Single output
|
|
53
|
+
|
|
54
|
+
# Stack main outputs back into 3D array
|
|
55
|
+
result = stack_slices(main_outputs, memory_type, gpu_id)
|
|
56
|
+
|
|
57
|
+
# If we have special outputs, combine them and return tuple
|
|
58
|
+
if special_outputs_list:
|
|
59
|
+
# Combine special outputs from all slices
|
|
60
|
+
combined_special_outputs = []
|
|
61
|
+
num_special_outputs = len(special_outputs_list[0])
|
|
62
|
+
|
|
63
|
+
for i in range(num_special_outputs):
|
|
64
|
+
# Collect the i-th special output from all slices
|
|
65
|
+
special_output_values = [slice_outputs[i] for slice_outputs in special_outputs_list]
|
|
66
|
+
combined_special_outputs.append(special_output_values)
|
|
67
|
+
|
|
68
|
+
# Return tuple: (stacked_main_output, combined_special_output1, # noqa: E501
|
|
69
|
+
# combined_special_output2, ...)
|
|
70
|
+
return (result, *combined_special_outputs)
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stack utilities module for OpenHCS.
|
|
3
|
+
|
|
4
|
+
This module provides functions for stacking 2D slices into a 3D array
|
|
5
|
+
and unstacking a 3D array into 2D slices, with explicit memory type handling.
|
|
6
|
+
|
|
7
|
+
This module enforces Clause 278 — Mandatory 3D Output Enforcement:
|
|
8
|
+
All functions must return a 3D array of shape [Z, Y, X], even when operating
|
|
9
|
+
on a single 2D slice. No logic may check, coerce, or infer rank at unstack time.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from arraybridge.converters import detect_memory_type
|
|
16
|
+
from arraybridge.framework_config import _FRAMEWORK_CONFIG
|
|
17
|
+
from arraybridge.types import GPU_MEMORY_TYPES, MemoryType
|
|
18
|
+
from arraybridge.utils import optional_import
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# 🔍 MEMORY CONVERSION LOGGING: Test log to verify logger is working
|
|
23
|
+
logger.debug("🔄 STACK_UTILS: Module loaded - memory conversion logging enabled")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_2d(data: Any) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Check if data is a 2D array.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data: Data to check
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
True if data is 2D, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
# Check if data has a shape attribute
|
|
37
|
+
if not hasattr(data, 'shape'):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Check if shape has length 2
|
|
41
|
+
return len(data.shape) == 2
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_3d(data: Any) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Check if data is a 3D array.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data: Data to check
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if data is 3D, False otherwise
|
|
53
|
+
"""
|
|
54
|
+
# Check if data has a shape attribute
|
|
55
|
+
if not hasattr(data, 'shape'):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Check if shape has length 3
|
|
59
|
+
return len(data.shape) == 3
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _enforce_gpu_device_requirements(memory_type: str, gpu_id: int) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Enforce GPU device requirements.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
memory_type: The memory type
|
|
68
|
+
gpu_id: The GPU device ID
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If gpu_id is negative
|
|
72
|
+
"""
|
|
73
|
+
# For GPU memory types, validate gpu_id
|
|
74
|
+
if memory_type in {mem_type.value for mem_type in GPU_MEMORY_TYPES}:
|
|
75
|
+
if gpu_id < 0:
|
|
76
|
+
raise ValueError(f"Invalid GPU device ID: {gpu_id}. Must be a non-negative integer.")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# NOTE: Allocation operations now defined in framework_config.py
|
|
80
|
+
# This eliminates the scattered _ALLOCATION_OPS dict
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _allocate_stack_array(
|
|
84
|
+
memory_type: str, stack_shape: tuple, first_slice: Any, gpu_id: int
|
|
85
|
+
) -> Any:
|
|
86
|
+
"""
|
|
87
|
+
Allocate a 3D array for stacking slices using framework config.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
memory_type: The target memory type
|
|
91
|
+
stack_shape: The shape of the stack (Z, Y, X)
|
|
92
|
+
first_slice: The first slice (used for dtype inference)
|
|
93
|
+
gpu_id: The GPU device ID
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Pre-allocated array or None for pyclesperanto
|
|
97
|
+
"""
|
|
98
|
+
# Convert string to enum
|
|
99
|
+
mem_type = MemoryType(memory_type)
|
|
100
|
+
config = _FRAMEWORK_CONFIG[mem_type]
|
|
101
|
+
allocate_expr = config['allocate_stack']
|
|
102
|
+
|
|
103
|
+
# Check if allocation is None (pyclesperanto uses custom stacking)
|
|
104
|
+
if allocate_expr is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# Import the module
|
|
108
|
+
mod = optional_import(mem_type.value)
|
|
109
|
+
if mod is None:
|
|
110
|
+
raise ValueError(f"{mem_type.value} is required for memory type {memory_type}")
|
|
111
|
+
|
|
112
|
+
# Handle dtype conversion if needed
|
|
113
|
+
needs_conversion = config['needs_dtype_conversion']
|
|
114
|
+
if callable(needs_conversion):
|
|
115
|
+
# It's a callable that determines if conversion is needed
|
|
116
|
+
needs_conversion = needs_conversion(first_slice, detect_memory_type)
|
|
117
|
+
|
|
118
|
+
if needs_conversion:
|
|
119
|
+
from arraybridge.converters import convert_memory
|
|
120
|
+
first_slice_source_type = detect_memory_type(first_slice)
|
|
121
|
+
sample_converted = convert_memory( # noqa: F841 (used in eval)
|
|
122
|
+
data=first_slice,
|
|
123
|
+
source_type=first_slice_source_type,
|
|
124
|
+
target_type=memory_type,
|
|
125
|
+
gpu_id=gpu_id
|
|
126
|
+
)
|
|
127
|
+
dtype = sample_converted.dtype # noqa: F841 (used in eval)
|
|
128
|
+
else:
|
|
129
|
+
dtype = first_slice.dtype if hasattr(first_slice, 'dtype') else None # noqa: F841 (used in eval)
|
|
130
|
+
|
|
131
|
+
# Set up local variables for eval
|
|
132
|
+
np = optional_import("numpy") # noqa: F841 (used in eval)
|
|
133
|
+
cupy = mod if mem_type == MemoryType.CUPY else None # noqa: F841 (used in eval)
|
|
134
|
+
torch = mod if mem_type == MemoryType.TORCH else None # noqa: F841 (used in eval)
|
|
135
|
+
tf = mod if mem_type == MemoryType.TENSORFLOW else None # noqa: F841 (used in eval)
|
|
136
|
+
jnp = optional_import("jax.numpy") if mem_type == MemoryType.JAX else None # noqa: F841 (used in eval)
|
|
137
|
+
|
|
138
|
+
# Execute allocation with context if needed
|
|
139
|
+
allocate_context = config.get('allocate_context')
|
|
140
|
+
if allocate_context:
|
|
141
|
+
context = eval(allocate_context)
|
|
142
|
+
with context:
|
|
143
|
+
return eval(allocate_expr)
|
|
144
|
+
else:
|
|
145
|
+
return eval(allocate_expr)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def stack_slices(slices: list[Any], memory_type: str, gpu_id: int) -> Any:
|
|
149
|
+
"""
|
|
150
|
+
Stack 2D slices into a 3D array with the specified memory type.
|
|
151
|
+
|
|
152
|
+
STRICT VALIDATION: Assumes all slices are 2D arrays.
|
|
153
|
+
No automatic handling of improper inputs.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
slices: List of 2D slices (numpy arrays, cupy arrays, torch tensors, etc.)
|
|
157
|
+
memory_type: The memory type to use for the stacked array (REQUIRED)
|
|
158
|
+
gpu_id: The target GPU device ID (REQUIRED)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A 3D array with the specified memory type of shape [Z, Y, X]
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
ValueError: If memory_type is not supported or slices is empty
|
|
165
|
+
ValueError: If gpu_id is negative for GPU memory types
|
|
166
|
+
ValueError: If slices are not 2D arrays
|
|
167
|
+
MemoryConversionError: If conversion fails
|
|
168
|
+
"""
|
|
169
|
+
if not slices:
|
|
170
|
+
raise ValueError("Cannot stack empty list of slices")
|
|
171
|
+
|
|
172
|
+
# Verify all slices are 2D
|
|
173
|
+
for i, slice_data in enumerate(slices):
|
|
174
|
+
if not _is_2d(slice_data):
|
|
175
|
+
raise ValueError(f"Slice at index {i} is not a 2D array. All slices must be 2D.")
|
|
176
|
+
|
|
177
|
+
# Analyze input types for conversion planning (minimal logging)
|
|
178
|
+
input_types = [detect_memory_type(slice_data) for slice_data in slices]
|
|
179
|
+
unique_input_types = set(input_types)
|
|
180
|
+
memory_type not in unique_input_types or len(unique_input_types) > 1
|
|
181
|
+
|
|
182
|
+
# Check GPU requirements
|
|
183
|
+
_enforce_gpu_device_requirements(memory_type, gpu_id)
|
|
184
|
+
|
|
185
|
+
# Pre-allocate the final 3D array to avoid intermediate list and final stack operation
|
|
186
|
+
first_slice = slices[0]
|
|
187
|
+
stack_shape = (len(slices), first_slice.shape[0], first_slice.shape[1])
|
|
188
|
+
|
|
189
|
+
# Create pre-allocated result array in target memory type using enum dispatch
|
|
190
|
+
result = _allocate_stack_array(memory_type, stack_shape, first_slice, gpu_id)
|
|
191
|
+
|
|
192
|
+
# Convert each slice and assign to result array
|
|
193
|
+
conversion_count = 0
|
|
194
|
+
|
|
195
|
+
# Check for custom stack handler (pyclesperanto)
|
|
196
|
+
mem_type = MemoryType(memory_type)
|
|
197
|
+
config = _FRAMEWORK_CONFIG[mem_type]
|
|
198
|
+
stack_handler = config.get('stack_handler')
|
|
199
|
+
|
|
200
|
+
if stack_handler:
|
|
201
|
+
# Use custom stack handler
|
|
202
|
+
mod = optional_import(mem_type.value)
|
|
203
|
+
result = stack_handler(slices, memory_type, gpu_id, mod)
|
|
204
|
+
else:
|
|
205
|
+
# Standard stacking logic
|
|
206
|
+
for i, slice_data in enumerate(slices):
|
|
207
|
+
source_type = detect_memory_type(slice_data)
|
|
208
|
+
|
|
209
|
+
# Track conversions for batch logging
|
|
210
|
+
if source_type != memory_type:
|
|
211
|
+
conversion_count += 1
|
|
212
|
+
|
|
213
|
+
# Direct conversion
|
|
214
|
+
if source_type == memory_type:
|
|
215
|
+
converted_data = slice_data
|
|
216
|
+
else:
|
|
217
|
+
from arraybridge.converters import convert_memory
|
|
218
|
+
converted_data = convert_memory(
|
|
219
|
+
data=slice_data,
|
|
220
|
+
source_type=source_type,
|
|
221
|
+
target_type=memory_type,
|
|
222
|
+
gpu_id=gpu_id
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Assign converted slice using framework-specific handler if available
|
|
226
|
+
assign_handler = config.get('assign_slice')
|
|
227
|
+
if assign_handler:
|
|
228
|
+
# Custom assignment (JAX immutability)
|
|
229
|
+
result = assign_handler(result, i, converted_data)
|
|
230
|
+
else:
|
|
231
|
+
# Standard assignment
|
|
232
|
+
result[i] = converted_data
|
|
233
|
+
|
|
234
|
+
# 🔍 MEMORY CONVERSION LOGGING: Only log when conversions happen or issues occur
|
|
235
|
+
if conversion_count > 0:
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"🔄 STACK_SLICES: Converted {conversion_count}/{len(slices)} "
|
|
238
|
+
f"slices to {memory_type}"
|
|
239
|
+
)
|
|
240
|
+
# Silent success for no-conversion cases to reduce log pollution
|
|
241
|
+
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def unstack_slices(
|
|
246
|
+
array: Any, memory_type: str, gpu_id: int, validate_slices: bool = True
|
|
247
|
+
) -> list[Any]:
|
|
248
|
+
"""
|
|
249
|
+
Split a 3D array into 2D slices along axis 0 and convert to the specified memory type.
|
|
250
|
+
|
|
251
|
+
STRICT VALIDATION: Input must be a 3D array. No automatic handling of improper inputs.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
array: 3D array to split - MUST BE 3D
|
|
255
|
+
memory_type: The memory type to use for the output slices (REQUIRED)
|
|
256
|
+
gpu_id: The target GPU device ID (REQUIRED)
|
|
257
|
+
validate_slices: If True, validates that each extracted slice is 2D
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of 2D slices in the specified memory type
|
|
261
|
+
|
|
262
|
+
Raises:
|
|
263
|
+
ValueError: If array is not 3D
|
|
264
|
+
ValueError: If validate_slices is True and any extracted slice is not 2D
|
|
265
|
+
ValueError: If gpu_id is negative for GPU memory types
|
|
266
|
+
ValueError: If memory_type is not supported
|
|
267
|
+
MemoryConversionError: If conversion fails
|
|
268
|
+
"""
|
|
269
|
+
# Detect input type and check if conversion is needed
|
|
270
|
+
input_type = detect_memory_type(array)
|
|
271
|
+
getattr(array, 'shape', 'unknown')
|
|
272
|
+
|
|
273
|
+
# Verify the array is 3D - fail loudly if not
|
|
274
|
+
if not _is_3d(array):
|
|
275
|
+
raise ValueError(f"Array must be 3D, got shape {getattr(array, 'shape', 'unknown')}")
|
|
276
|
+
|
|
277
|
+
# Check GPU requirements
|
|
278
|
+
_enforce_gpu_device_requirements(memory_type, gpu_id)
|
|
279
|
+
|
|
280
|
+
# Convert to target memory type
|
|
281
|
+
source_type = input_type # Reuse already detected type
|
|
282
|
+
|
|
283
|
+
# Direct conversion
|
|
284
|
+
if source_type == memory_type:
|
|
285
|
+
# No conversion needed - silent success to reduce log pollution
|
|
286
|
+
pass
|
|
287
|
+
else:
|
|
288
|
+
# Convert and log the conversion
|
|
289
|
+
from arraybridge.converters import convert_memory
|
|
290
|
+
logger.debug(f"🔄 UNSTACK_SLICES: Converting array - {source_type} → {memory_type}")
|
|
291
|
+
array = convert_memory(
|
|
292
|
+
data=array,
|
|
293
|
+
source_type=source_type,
|
|
294
|
+
target_type=memory_type,
|
|
295
|
+
gpu_id=gpu_id
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Extract slices along axis 0 (already in the target memory type)
|
|
299
|
+
slices = [array[i] for i in range(array.shape[0])]
|
|
300
|
+
|
|
301
|
+
# Validate that all extracted slices are 2D if requested
|
|
302
|
+
if validate_slices:
|
|
303
|
+
for i, slice_data in enumerate(slices):
|
|
304
|
+
if not _is_2d(slice_data):
|
|
305
|
+
raise ValueError(
|
|
306
|
+
f"Extracted slice at index {i} is not 2D. "
|
|
307
|
+
f"This indicates a malformed 3D array."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# 🔍 MEMORY CONVERSION LOGGING: Only log conversions or issues
|
|
311
|
+
if source_type != memory_type:
|
|
312
|
+
logger.debug(f"🔄 UNSTACK_SLICES: Converted and extracted {len(slices)} slices")
|
|
313
|
+
elif len(slices) == 0:
|
|
314
|
+
logger.warning("🔄 UNSTACK_SLICES: No slices extracted (empty array)")
|
|
315
|
+
# Silent success for no-conversion cases to reduce log pollution
|
|
316
|
+
|
|
317
|
+
return slices
|