Skip to content

Commit 27d1036

Browse files
committed
ENH: testing.lazy_xp_function: torch.compile support
1 parent 647ce53 commit 27d1036

4 files changed

Lines changed: 126 additions & 54 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ errors = { unannotated-return = false }
146146

147147
[tool.pytest.ini_options]
148148
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
149-
filterwarnings = ["error"]
149+
filterwarnings = [
150+
"error",
151+
"ignore:.*torch.jit.script_method.*",
152+
"ignore:.*accumulated_recompile_limit reached.*",
153+
]
150154
log_cli_level = "INFO"
151155
markers = [
152156
"skip_xp_backend(library, /, *, reason=None): Skip test for a specific backend",

src/array_api_extra/_lib/_helpers.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import enum
56
import functools
67
import io
78
import math
@@ -28,14 +29,15 @@
2829

2930

3031
__all__ = [
32+
"JitLibrary",
3133
"asarrays",
34+
"autojit",
3235
"capabilities",
3336
"deprecated",
3437
"eager_shape",
3538
"in1d",
3639
"is_jax_jit_enabled",
3740
"is_python_scalar",
38-
"jax_autojit",
3941
"meta_namespace",
4042
"normalize_pad_width",
4143
"pickle_flatten",
@@ -487,20 +489,20 @@ def persistent_load(self, pid: Literal[0, 1]) -> object: # numpydoc ignore=GL08
487489

488490
class _AutoJITWrapper(Generic[T]): # numpydoc ignore=PR01
489491
"""
490-
Helper of :func:`jax_autojit`.
492+
Helper of :func:`autojit`.
491493
492494
Wrap arbitrary inputs and outputs of the jitted function and
493495
convert them to/from PyTrees.
494496
"""
495497

496498
_obj: Any
497499
_is_iter: bool
498-
_registered: ClassVar[bool] = False
500+
_registered: ClassVar[set[JitLibrary]] = set()
499501
__slots__: tuple[str, ...] = ("_is_iter", "_obj")
500502

501-
def __init__(self, obj: T) -> None: # numpydoc ignore=GL08
502-
self._register()
503-
if isinstance(obj, Iterator):
503+
def __init__(self, obj: T, jit_library: JitLibrary) -> None: # numpydoc ignore=GL08
504+
self._register(jit_library)
505+
if jit_library is JitLibrary.jax and isinstance(obj, Iterator):
504506
self._obj = list(obj)
505507
self._is_iter = True
506508
else:
@@ -513,24 +515,44 @@ def obj(self) -> T: # numpydoc ignore=RT01
513515
return iter(self._obj) if self._is_iter else self._obj
514516

515517
@classmethod
516-
def _register(cls) -> None: # numpydoc ignore=SS06
518+
def _register(cls, jit_library: JitLibrary) -> None: # numpydoc ignore=SS06,PR01
517519
"""
518520
Register upon first use instead of at import time, to avoid
519521
globally importing JAX.
520522
"""
521-
if not cls._registered:
523+
if jit_library in cls._registered:
524+
return
525+
526+
if jit_library is JitLibrary.jax:
522527
import jax
523528

524529
jax.tree_util.register_pytree_node(
525530
cls,
526531
lambda instance: pickle_flatten(instance, jax.Array), # pyright: ignore[reportUnknownArgumentType]
527532
lambda aux_data, children: pickle_unflatten(children, aux_data), # pyright: ignore[reportUnknownArgumentType]
528533
)
529-
cls._registered = True
534+
elif jit_library is JitLibrary.torch:
535+
import torch
536+
537+
torch.utils._pytree.register_pytree_node(
538+
cls,
539+
lambda instance: pickle_flatten(instance, torch.Tensor), # pyright: ignore[reportUnknownArgumentType]
540+
pickle_unflatten,
541+
)
542+
cls._registered.add(jit_library)
530543

531544

532-
def jax_autojit(
533-
func: Callable[P, T],
545+
class JitLibrary(enum.Enum):
546+
"""
547+
Enum for JIT libraries compatible with `autojit`.
548+
"""
549+
550+
jax = enum.auto()
551+
torch = enum.auto()
552+
553+
554+
def autojit(
555+
func: Callable[P, T], jit_library: JitLibrary
534556
) -> Callable[P, T]: # numpydoc ignore=PR01,RT01,SS03
535557
"""
536558
Wrap `func` with ``jax.jit``, with the following differences:
@@ -573,19 +595,26 @@ def f(x: Array, y: float, plus: bool) -> Array:
573595
``j1``, but on the flip side it means that it will be re-traced for every different
574596
value of ``y``, which likely makes it not fit for purpose in production.
575597
"""
576-
import jax
598+
if jit_library is JitLibrary.jax:
599+
import jax
600+
601+
jit_decorator = jax.jit
602+
elif jit_library is JitLibrary.torch:
603+
import torch
604+
605+
jit_decorator = functools.partial(torch.compile, fullgraph=True)
577606

578-
@jax.jit # type: ignore[untyped-decorator] # pyright: ignore[reportUntypedFunctionDecorator]
607+
@jit_decorator # type: ignore[untyped-decorator] # pyright: ignore[reportUntypedFunctionDecorator]
579608
def inner( # numpydoc ignore=GL08
580609
wargs: _AutoJITWrapper[Any],
581610
) -> _AutoJITWrapper[T]:
582611
args, kwargs = wargs.obj
583612
res = func(*args, **kwargs) # pyright: ignore[reportCallIssue]
584-
return _AutoJITWrapper(res)
613+
return _AutoJITWrapper(res, jit_library)
585614

586615
@functools.wraps(func)
587616
def outer(*args: P.args, **kwargs: P.kwargs) -> T: # numpydoc ignore=GL08
588-
wargs = _AutoJITWrapper((args, kwargs))
617+
wargs = _AutoJITWrapper((args, kwargs), jit_library)
589618
return inner(wargs).obj
590619

591620
return outer

src/array_api_extra/testing/_testing.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,6 @@
3737
"patch_lazy_xp_functions",
3838
]
3939

40-
41-
__all__ = [
42-
"assert_close",
43-
"assert_close_nulp",
44-
"assert_equal",
45-
"assert_less",
46-
"lazy_xp_function",
47-
"patch_lazy_xp_functions",
48-
]
49-
50-
5140
P = ParamSpec("P")
5241
T = TypeVar("T")
5342

@@ -83,6 +72,7 @@ def lazy_xp_function(
8372
*,
8473
allow_dask_compute: bool | int = False,
8574
jax_jit: bool = True,
75+
torch_compile: bool = True,
8676
static_argnums: _Deprecated = DEPRECATED,
8777
static_argnames: _Deprecated = DEPRECATED,
8878
) -> None: # numpydoc ignore=GL07
@@ -146,6 +136,8 @@ def lazy_xp_function(
146136
... return user_consumes(z)
147137
148138
Default: True.
139+
torch_compile : bool, optional
140+
TODO: proper docs.
149141
static_argnums : Deprecated
150142
Deprecated; ignored.
151143
static_argnames : Deprecated
@@ -238,6 +230,7 @@ def test_myfunc(xp):
238230
tags: dict[str, bool | int | type] = {
239231
"allow_dask_compute": allow_dask_compute,
240232
"jax_jit": jax_jit,
233+
"torch_compile": torch_compile,
241234
}
242235

243236
if isinstance(func, tuple):
@@ -444,7 +437,19 @@ def iter_tagged() -> Iterator[
444437
elif _compat.is_jax_namespace(xp):
445438
for target, name, attr, func, tags in iter_tagged():
446439
if tags["jax_jit"]:
447-
wrapped = _helpers.jax_autojit(func)
440+
wrapped = _helpers.autojit(func, _helpers.JitLibrary.jax)
441+
# If we're dealing with a staticmethod or classmethod, make
442+
# sure things stay that way.
443+
if isinstance(attr, staticmethod):
444+
wrapped = staticmethod(wrapped)
445+
elif isinstance(attr, classmethod):
446+
wrapped = classmethod(wrapped)
447+
temp_setattr(target, name, wrapped)
448+
449+
elif _compat.is_torch_namespace(xp):
450+
for target, name, attr, func, tags in iter_tagged():
451+
if tags["torch_compile"]:
452+
wrapped = _helpers.autojit(func, _helpers.JitLibrary.torch)
448453
# If we're dealing with a staticmethod or classmethod, make
449454
# sure things stay that way.
450455
if isinstance(attr, staticmethod):

tests/main/test_helpers.py

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from collections.abc import Iterator
2-
from typing import Generic, TypeVar, cast
1+
import functools
2+
from collections.abc import Callable, Iterator
3+
from types import ModuleType
4+
from typing import Generic, ParamSpec, Protocol, TypeVar, cast
35

46
import numpy as np
57
import pytest
@@ -8,11 +10,12 @@
810
from array_api_extra._lib._compat import array_namespace
911
from array_api_extra._lib._compat import device as get_device
1012
from array_api_extra._lib._helpers import (
13+
JitLibrary,
1114
asarrays,
15+
autojit,
1216
capabilities,
1317
eager_shape,
1418
in1d,
15-
jax_autojit,
1619
meta_namespace,
1720
ndindex,
1821
pickle_flatten,
@@ -23,6 +26,7 @@
2326

2427
from .conftest import np_compat
2528

29+
P = ParamSpec("P")
2630
T = TypeVar("T")
2731

2832
# FIXME calls xp.unique_values without size
@@ -345,41 +349,48 @@ def test_recursion(self):
345349
assert obj2[1] is obj2
346350

347351

348-
class TestJAXAutoJIT:
349-
def test_basic(self, jnp: ArrayNamespace):
350-
@jax_autojit
352+
class AutoJitFunc(Protocol):
353+
def __call__(
354+
self,
355+
func: Callable[P, T],
356+
) -> Callable[P, T]: ...
357+
358+
359+
class CheckAutoJIT:
360+
def test_basic(self, autojit_func: AutoJitFunc, xp: ArrayNamespace):
361+
@autojit_func
351362
def f(x: Array, k: object = False) -> Array:
352363
return x + 1 if k else x - 1
353364

354365
# Basic recognition of static_argnames
355-
assert_equal(f(jnp.asarray([1, 2])), jnp.asarray([0, 1]))
356-
assert_equal(f(jnp.asarray([1, 2]), False), jnp.asarray([0, 1]))
357-
assert_equal(f(jnp.asarray([1, 2]), True), jnp.asarray([2, 3]))
358-
assert_equal(f(jnp.asarray([1, 2]), 1), jnp.asarray([2, 3]))
366+
assert_equal(f(xp.asarray([1, 2])), xp.asarray([0, 1]))
367+
assert_equal(f(xp.asarray([1, 2]), False), xp.asarray([0, 1]))
368+
assert_equal(f(xp.asarray([1, 2]), True), xp.asarray([2, 3]))
369+
assert_equal(f(xp.asarray([1, 2]), 1), xp.asarray([2, 3]))
359370

360371
# static argument is not an ArrayLike
361-
assert_equal(f(jnp.asarray([1, 2]), "foo"), jnp.asarray([2, 3]))
372+
assert_equal(f(xp.asarray([1, 2]), "foo"), xp.asarray([2, 3]))
362373

363374
# static argument is not hashable, but serializable
364-
assert_equal(f(jnp.asarray([1, 2]), ["foo"]), jnp.asarray([2, 3]))
375+
assert_equal(f(xp.asarray([1, 2]), ["foo"]), xp.asarray([2, 3]))
365376

366-
def test_wrapper(self, jnp: ArrayNamespace):
367-
@jax_autojit
377+
def test_wrapper(self, autojit_func: AutoJitFunc, xp: ArrayNamespace):
378+
@autojit_func
368379
def f(w: Wrapper[Array]) -> Wrapper[Array]:
369380
return Wrapper(w.x + 1)
370381

371-
inp = Wrapper(jnp.asarray([1, 2]))
382+
inp = Wrapper(xp.asarray([1, 2]))
372383
out = f(inp).x
373-
assert_equal(out, jnp.asarray([2, 3]))
384+
assert_equal(out, xp.asarray([2, 3]))
374385

375-
def test_static_hashable(self, jnp: ArrayNamespace):
386+
def test_static_hashable(self, autojit_func: AutoJitFunc, xp: ArrayNamespace):
376387
"""Static argument/return value is hashable, but not serializable"""
377388

378389
class C:
379390
def __reduce__(self) -> object: # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride,reportImplicitOverride]
380391
raise Exception() # noqa: TRY002
381392

382-
@jax_autojit
393+
@autojit_func
383394
def f(x: object) -> object:
384395
return x
385396

@@ -388,17 +399,20 @@ def f(x: object) -> object:
388399
assert out is inp
389400

390401
# Serializable opaque input contains non-serializable object plus array
391-
winp = Wrapper((C(), jnp.asarray([1, 2])))
402+
winp = Wrapper((C(), xp.asarray([1, 2])))
392403
out = f(winp)
393404
assert isinstance(out, Wrapper)
394405
assert out.x[0] is winp.x[0]
395406
assert out.x[1] is not winp.x[1]
396407
assert_equal(out.x[1], winp.x[1])
397408

398-
def test_arraylikes_are_static(self):
409+
def test_arraylikes_are_static(
410+
self,
411+
autojit_func: AutoJitFunc,
412+
):
399413
pytest.importorskip("jax")
400414

401-
@jax_autojit
415+
@autojit_func
402416
def f(x: list[int]) -> list[int]:
403417
assert isinstance(x, list)
404418
assert x == [1, 2]
@@ -408,15 +422,35 @@ def f(x: list[int]) -> list[int]:
408422
assert isinstance(out, list)
409423
assert out == [3, 4]
410424

411-
def test_iterators(self, jnp: ArrayNamespace):
412-
@jax_autojit
425+
def test_iterators(self, autojit_func: AutoJitFunc, xp: ArrayNamespace):
426+
@autojit_func
413427
def f(x: Array) -> Iterator[Array]:
414428
return (x + i for i in range(2))
415429

416-
inp = jnp.asarray([1, 2])
430+
inp = xp.asarray([1, 2])
417431
out = f(inp)
418432
assert isinstance(out, Iterator)
419-
assert_equal(next(out), jnp.asarray([1, 2]))
420-
assert_equal(next(out), jnp.asarray([2, 3]))
433+
assert_equal(next(out), xp.asarray([1, 2]))
434+
assert_equal(next(out), xp.asarray([2, 3]))
421435
with pytest.raises(StopIteration):
422436
_ = next(out)
437+
438+
439+
class TestJAXAutoJit(CheckAutoJIT):
440+
@pytest.fixture
441+
def xp(self, jnp: ModuleType) -> ModuleType:
442+
return jnp
443+
444+
@pytest.fixture
445+
def autojit_func(self) -> AutoJitFunc:
446+
return functools.partial(autojit, jit_library=JitLibrary.jax)
447+
448+
449+
class TestTorchAutoJit(CheckAutoJIT):
450+
@pytest.fixture
451+
def xp(self, torch: ModuleType) -> ModuleType:
452+
return torch
453+
454+
@pytest.fixture
455+
def autojit_func(self) -> AutoJitFunc:
456+
return functools.partial(autojit, jit_library=JitLibrary.torch)

0 commit comments

Comments
 (0)