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.
@@ -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