Skip to content

Commit 2ecf80a

Browse files
committed
Make framework execution contracts declaration-owned
1 parent 726880b commit 2ecf80a

9 files changed

Lines changed: 417 additions & 46 deletions

File tree

docs/source/decorators.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,7 @@ Dtype policy
5353
Direct calls default to preserving the input dtype. Hosts can pass an object
5454
implementing ``DtypeConversionConfig`` to select native or explicit output dtype
5555
behavior.
56+
57+
``wrap_dtype_preserving_callable(func, memory_type)`` exposes the dtype and
58+
slice-control wrapper as a public integration boundary for host registries that
59+
already own their memory and processing contracts.

docs/source/gpu_features.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ identity.
1111
Before importing TensorFlow or JAX, their declarations apply coexistence-safe
1212
defaults with ``setdefault``: TensorFlow memory growth is enabled and JAX GPU
1313
preallocation is disabled. An environment value supplied by the host is never
14-
overwritten.
14+
overwritten. Hosts that import optional frameworks themselves should call
15+
``MemoryType.prepare_import()`` first. ArrayBridge warns when it encounters an
16+
already-loaded framework whose import-time defaults were absent.
1517

1618
JAX float64 output requires x64 mode to be enabled before import. ArrayBridge
1719
raises instead of silently returning float32 when a caller requests float64

src/arraybridge/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
from .utils import _ensure_module, _get_device_id, _supports_dlpack
2828

2929
DtypeConversion = _decorators.DtypeConversion
30+
SliceBySliceRuntimeParameter = _decorators.SliceBySliceRuntimeParameter
3031
memory_types = _decorators.memory_types
32+
wrap_dtype_preserving_callable = _decorators.wrap_dtype_preserving_callable
3133
for _memory_type in MemoryType:
3234
globals()[_memory_type.value] = getattr(_decorators, _memory_type.value)
3335

@@ -44,6 +46,8 @@
4446
# Decorators
4547
"memory_types",
4648
"DtypeConversion",
49+
"SliceBySliceRuntimeParameter",
50+
"wrap_dtype_preserving_callable",
4751
# Stack utilities
4852
"stack_slices",
4953
"unstack_slices",

src/arraybridge/array_operations.py

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
FromNumpy = Callable[[Any, Any, int], Any]
1111
StackArrays = Callable[[Sequence[Any], Any], Any]
1212
ScaleDtype = Callable[[Any, Any, Any], Any]
13+
DtypeName = Callable[[Any], str]
14+
CastArray = Callable[[Any, Any, Any], Any]
15+
LogicalAnd = Callable[[Any, Any, Any], Any]
1316

1417
_SCALING_RANGES: dict[str, float | tuple[float, float]] = {
1518
"uint8": 255.0,
@@ -21,9 +24,25 @@
2124

2225

2326
def _dtype_name(dtype: Any) -> str:
27+
declared_name = getattr(dtype, "name", None)
28+
if declared_name is not None:
29+
return str(declared_name)
2430
return getattr(dtype, "__name__", str(dtype).rsplit(".", maxsplit=1)[-1])
2531

2632

33+
def _numpy_dtype_name(dtype: Any) -> str:
34+
return np.dtype(dtype).name
35+
36+
37+
def _torch_dtype_name(dtype: Any) -> str:
38+
return str(dtype).rsplit(".", maxsplit=1)[-1]
39+
40+
41+
def _tensorflow_dtype_name(dtype: Any) -> str:
42+
numpy_dtype = getattr(dtype, "as_numpy_dtype", dtype)
43+
return np.dtype(numpy_dtype).name
44+
45+
2746
def _scaled_values(result: Any, result_min: Any, result_max: Any, target_dtype: Any) -> Any:
2847
normalized = (result - result_min) / (result_max - result_min)
2948
range_info = _SCALING_RANGES.get(_dtype_name(target_dtype))
@@ -59,6 +78,15 @@ def _numpy_stack(values: Sequence[Any], module: Any) -> Any:
5978
return module.stack(values, axis=0)
6079

6180

81+
def _numpy_cast(data: Any, dtype: Any, module: Any) -> Any:
82+
del module
83+
return data.astype(dtype, copy=False)
84+
85+
86+
def _module_logical_and(left: Any, right: Any, module: Any) -> Any:
87+
return module.logical_and(left, right)
88+
89+
6290
def _numpy_scale(result: Any, target_dtype: Any, module: Any) -> Any:
6391
if not hasattr(result, "dtype"):
6492
return result
@@ -129,6 +157,10 @@ def _torch_stack(values: Sequence[Any], module: Any) -> Any:
129157
return module.stack(tuple(values), dim=0)
130158

131159

160+
def _torch_cast(data: Any, dtype: Any, module: Any) -> Any:
161+
return data.to(dtype=_mapped_dtype(dtype, module))
162+
163+
132164
def _mapped_dtype(target_dtype: Any, module: Any) -> Any:
133165
try:
134166
dtype_name = np.dtype(target_dtype).name
@@ -173,6 +205,10 @@ def _tensorflow_stack(values: Sequence[Any], module: Any) -> Any:
173205
return module.stack(tuple(values), axis=0)
174206

175207

208+
def _tensorflow_cast(data: Any, dtype: Any, module: Any) -> Any:
209+
return module.cast(data, _mapped_dtype(dtype, module))
210+
211+
176212
def _tensorflow_scale(result: Any, target_dtype: Any, module: Any) -> Any:
177213
if not hasattr(result, "dtype"):
178214
return result
@@ -205,6 +241,14 @@ def _jax_stack(values: Sequence[Any], module: Any) -> Any:
205241
return module.numpy.stack(tuple(values), axis=0)
206242

207243

244+
def _jax_cast(data: Any, dtype: Any, module: Any) -> Any:
245+
return data.astype(_mapped_dtype(dtype, module.numpy))
246+
247+
248+
def _jax_logical_and(left: Any, right: Any, module: Any) -> Any:
249+
return module.numpy.logical_and(left, right)
250+
251+
208252
def _jax_scale(result: Any, target_dtype: Any, module: Any) -> Any:
209253
if not hasattr(result, "dtype"):
210254
return result
@@ -254,17 +298,18 @@ def _pyclesperanto_stack(values: Sequence[Any], module: Any) -> Any:
254298
return result
255299

256300

301+
def _pyclesperanto_cast(data: Any, dtype: Any, module: Any) -> Any:
302+
return module.push(module.pull(data).astype(dtype, copy=False))
303+
304+
305+
def _pyclesperanto_logical_and(left: Any, right: Any, module: Any) -> Any:
306+
return module.push(np.logical_and(module.pull(left), module.pull(right)))
307+
308+
257309
def _pyclesperanto_scale(result: Any, target_dtype: Any, module: Any) -> Any:
258310
if not hasattr(result, "dtype"):
259311
return result
260-
target_is_int = target_dtype in {
261-
np.uint8,
262-
np.uint16,
263-
np.uint32,
264-
np.int8,
265-
np.int16,
266-
np.int32,
267-
}
312+
target_is_int = np.issubdtype(np.dtype(target_dtype), np.integer)
268313
if not (np.issubdtype(result.dtype, np.floating) and target_is_int):
269314
return module.push(module.pull(result).astype(target_dtype))
270315
result_min = float(module.minimum_of_all_pixels(result))
@@ -300,6 +345,9 @@ class ArrayOperations:
300345
from_numpy: FromNumpy
301346
stack: StackArrays
302347
scale_dtype: ScaleDtype
348+
dtype_name: DtypeName = _numpy_dtype_name
349+
cast: CastArray = _numpy_cast
350+
logical_and: LogicalAnd = _module_logical_and
303351

304352

305353
NUMPY_OPERATIONS = ArrayOperations(
@@ -319,22 +367,30 @@ class ArrayOperations:
319367
from_numpy=_torch_from_numpy,
320368
stack=_torch_stack,
321369
scale_dtype=_torch_scale,
370+
dtype_name=_torch_dtype_name,
371+
cast=_torch_cast,
322372
)
323373
TENSORFLOW_OPERATIONS = ArrayOperations(
324374
to_numpy=_tensorflow_to_numpy,
325375
from_numpy=_tensorflow_from_numpy,
326376
stack=_tensorflow_stack,
327377
scale_dtype=_tensorflow_scale,
378+
dtype_name=_tensorflow_dtype_name,
379+
cast=_tensorflow_cast,
328380
)
329381
JAX_OPERATIONS = ArrayOperations(
330382
to_numpy=_jax_to_numpy,
331383
from_numpy=_jax_from_numpy,
332384
stack=_jax_stack,
333385
scale_dtype=_jax_scale,
386+
cast=_jax_cast,
387+
logical_and=_jax_logical_and,
334388
)
335389
PYCLESPERANTO_OPERATIONS = ArrayOperations(
336390
to_numpy=_pyclesperanto_to_numpy,
337391
from_numpy=_pyclesperanto_from_numpy,
338392
stack=_pyclesperanto_stack,
339393
scale_dtype=_pyclesperanto_scale,
394+
cast=_pyclesperanto_cast,
395+
logical_and=_pyclesperanto_logical_and,
340396
)

0 commit comments

Comments
 (0)