Skip to content

Commit 40837ae

Browse files
committed
Add TensorFlow array API backend
1 parent 66a6357 commit 40837ae

18 files changed

Lines changed: 3063 additions & 20 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Array API Tests (TensorFlow)
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
array-api-tests-tensorflow:
7+
uses: ./.github/workflows/array-api-tests.yml
8+
with:
9+
package-name: tensorflow
10+
python-versions: '[''3.10'', ''3.13'']'
11+
pytest-extra-args: -n 2
12+
extra-env-vars: |
13+
ARRAY_API_TESTS_XFAIL_MARK=skip

meson.build

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ sources_raw = {
6161
'src/array_api_compat/torch/fft.py',
6262
'src/array_api_compat/torch/linalg.py',
6363
],
64+
65+
'array_api_compat/tensorflow': [
66+
'src/array_api_compat/tensorflow/__init__.py',
67+
'src/array_api_compat/tensorflow/_aliases.py',
68+
'src/array_api_compat/tensorflow/_info.py',
69+
'src/array_api_compat/tensorflow/_typing.py',
70+
'src/array_api_compat/tensorflow/fft.py',
71+
'src/array_api_compat/tensorflow/linalg.py',
72+
],
6473
}
6574

6675
sources = {}

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ numpy = ["numpy>=1.22"]
3838
pytorch = ["torch"]
3939
sparse = ["sparse>=0.15.1"]
4040
ndonnx = ["ndonnx"]
41+
tensorflow = ["tensorflow"]
4142
docs = [
4243
"furo",
4344
"linkify-it-py",
@@ -55,6 +56,7 @@ dev = [
5556
"pytest",
5657
"torch",
5758
"sparse>=0.15.1",
59+
"tensorflow",
5860
]
5961

6062

@@ -105,7 +107,7 @@ warn_unused_ignores = true
105107
warn_unreachable = true
106108

107109
[[tool.mypy.overrides]]
108-
module = ["cupy.*", "cupy_backends.*", "dask.*", "jax.*", "ndonnx.*", "sparse.*", "torch.*"]
110+
module = ["cupy.*", "cupy_backends.*", "dask.*", "jax.*", "ndonnx.*", "sparse.*", "tensorflow.*", "torch.*"]
109111
ignore_missing_imports = true
110112

111113

src/array_api_compat/common/_helpers.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import numpy as np
3838
import numpy.typing as npt
3939
import sparse
40+
import tensorflow as tf
4041
import torch
4142

4243
# TODO: import from typing (requires Python >=3.13)
@@ -51,6 +52,7 @@
5152
| jax.Array
5253
| ndx.Array
5354
| sparse.SparseArray
55+
| tf.Tensor
5456
| torch.Tensor
5557
| SupportsArrayNamespace[Any]
5658
)
@@ -168,6 +170,29 @@ def is_torch_array(x: object) -> TypeIs[torch.Tensor]:
168170
return _issubclass_fast(cls, "torch", "Tensor")
169171

170172

173+
def is_tensorflow_array(x: object) -> TypeIs[tf.Tensor]:
174+
"""
175+
Return True if `x` is a TensorFlow tensor.
176+
177+
This function does not import TensorFlow if it has not already been
178+
imported and is therefore cheap to use.
179+
180+
See Also
181+
--------
182+
183+
array_namespace
184+
is_array_api_obj
185+
is_numpy_array
186+
is_cupy_array
187+
is_torch_array
188+
is_dask_array
189+
is_jax_array
190+
is_pydata_sparse_array
191+
"""
192+
cls = cast(Hashable, type(x))
193+
return _issubclass_fast(cls, "tensorflow", "Tensor")
194+
195+
171196
def is_ndonnx_array(x: object) -> TypeIs[ndx.Array]:
172197
"""
173198
Return True if `x` is a ndonnx Array.
@@ -308,6 +333,7 @@ def _is_array_api_cls(cls: type) -> bool:
308333
or _issubclass_fast(cls, "numpy", "generic")
309334
or _issubclass_fast(cls, "cupy", "ndarray")
310335
or _issubclass_fast(cls, "torch", "Tensor")
336+
or _issubclass_fast(cls, "tensorflow", "Tensor")
311337
or _issubclass_fast(cls, "dask.array", "Array")
312338
or _issubclass_fast(cls, "sparse", "SparseArray")
313339
# TODO: drop support for jax<0.4.32 which didn't have __array_namespace__
@@ -387,6 +413,30 @@ def is_torch_namespace(xp: Namespace) -> bool:
387413
return xp.__name__ in {"torch", _compat_module_name() + ".torch"}
388414

389415

416+
@lru_cache(100)
417+
def is_tensorflow_namespace(xp: Namespace) -> bool:
418+
"""
419+
Returns True if `xp` is a TensorFlow namespace.
420+
421+
This includes both TensorFlow itself and the version wrapped by
422+
array-api-compat.
423+
424+
See Also
425+
--------
426+
427+
array_namespace
428+
is_numpy_namespace
429+
is_cupy_namespace
430+
is_torch_namespace
431+
is_ndonnx_namespace
432+
is_dask_namespace
433+
is_jax_namespace
434+
is_pydata_sparse_namespace
435+
is_array_api_strict_namespace
436+
"""
437+
return xp.__name__ in {"tensorflow", _compat_module_name() + ".tensorflow"}
438+
439+
390440
def is_ndonnx_namespace(xp: Namespace) -> bool:
391441
"""
392442
Returns True if `xp` is an NDONNX namespace.
@@ -551,6 +601,14 @@ def _cls_to_namespace(
551601
import torch as xp # type: ignore[no-redef]
552602
return xp, None
553603

604+
if _issubclass_fast(cls_, "tensorflow", "Tensor"):
605+
if _use_compat:
606+
_check_api_version(api_version)
607+
from .. import tensorflow as xp # type: ignore[no-redef]
608+
else:
609+
import tensorflow as xp # type: ignore[no-redef]
610+
return xp, None
611+
554612
if _issubclass_fast(cls_, "dask.array", "Array"):
555613
if _use_compat:
556614
_check_api_version(api_version)
@@ -790,6 +848,8 @@ def device(x: _ArrayApiObj, /) -> Device:
790848
return x_device()
791849
else:
792850
return x_device
851+
elif is_tensorflow_array(x):
852+
return x.device
793853
elif is_pydata_sparse_array(x):
794854
# `sparse` will gain `.device`, so check for this first.
795855
x_device = getattr(x, "device", None)
@@ -851,6 +911,20 @@ def _torch_to_device(
851911
return x.to(device)
852912

853913

914+
def _tensorflow_to_device(
915+
x: tf.Tensor,
916+
device: str,
917+
/,
918+
stream: int | Any | None = None,
919+
) -> tf.Tensor:
920+
if stream is not None:
921+
raise ValueError("The stream argument to to_device() is not supported")
922+
import tensorflow as tf
923+
924+
with tf.device(device):
925+
return tf.identity(x)
926+
927+
854928
def to_device(x: Array, device: Device, /, *, stream: int | Any | None = None) -> Array:
855929
"""
856930
Copy the array from the device on which it currently resides to the specified ``device``.
@@ -911,6 +985,8 @@ def to_device(x: Array, device: Device, /, *, stream: int | Any | None = None) -
911985
return _cupy_to_device(x, device, stream=stream)
912986
elif is_torch_array(x):
913987
return _torch_to_device(x, device, stream=stream)
988+
elif is_tensorflow_array(x):
989+
return _tensorflow_to_device(x, device, stream=stream)
914990
elif is_dask_array(x):
915991
if stream is not None:
916992
raise ValueError("The stream argument to to_device() is not supported")
@@ -965,6 +1041,7 @@ def _is_writeable_cls(cls: type) -> bool | None:
9651041
or _issubclass_fast(cls, "jax", "Array")
9661042
or _issubclass_fast(cls, "jax.core", "Tracer") # see is_jax_array for limitations
9671043
or _issubclass_fast(cls, "sparse", "SparseArray")
1044+
or _issubclass_fast(cls, "tensorflow", "Tensor")
9681045
):
9691046
return False
9701047
if _is_array_api_cls(cls):
@@ -998,6 +1075,7 @@ def _is_lazy_cls(cls: type) -> bool | None:
9981075
or _issubclass_fast(cls, "numpy", "generic")
9991076
or _issubclass_fast(cls, "cupy", "ndarray")
10001077
or _issubclass_fast(cls, "torch", "Tensor")
1078+
or _issubclass_fast(cls, "tensorflow", "Tensor")
10011079
or _issubclass_fast(cls, "sparse", "SparseArray")
10021080
):
10031081
return False
@@ -1083,6 +1161,8 @@ def is_lazy_array(x: object) -> TypeGuard[_ArrayApiObj]:
10831161
"is_numpy_namespace",
10841162
"is_torch_array",
10851163
"is_torch_namespace",
1164+
"is_tensorflow_array",
1165+
"is_tensorflow_namespace",
10861166
"is_ndonnx_array",
10871167
"is_ndonnx_namespace",
10881168
"is_pydata_sparse_array",

0 commit comments

Comments
 (0)