gradbridge 0.1.0__tar.gz

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,15 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: gradbridge
3
+ Version: 0.1.0
4
+ Summary: Gradients that cross framework boundaries: PyTorch, JAX, TensorFlow, NVIDIA Warp and NumPy.
5
+ Author: Anthony Angeles
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/aneangel/gradbridge
8
+ Project-URL: Issues, https://github.com/aneangel/gradbridge/issues
9
+ Project-URL: Benchmarks, https://github.com/aneangel/gradbridge/blob/main/benchmarks/RESULTS.md
10
+ Keywords: autodiff,gradients,pytorch,jax,tensorflow,warp,interop,dlpack
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Intended Audience :: Science/Research
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.24
25
+ Provides-Extra: torch
26
+ Requires-Dist: torch>=2.4; extra == "torch"
27
+ Provides-Extra: jax
28
+ Requires-Dist: jax>=0.4.31; extra == "jax"
29
+ Provides-Extra: tf
30
+ Requires-Dist: tensorflow>=2.15; extra == "tf"
31
+ Provides-Extra: warp
32
+ Requires-Dist: warp-lang>=1.5; extra == "warp"
33
+ Provides-Extra: all
34
+ Requires-Dist: torch>=2.4; extra == "all"
35
+ Requires-Dist: jax>=0.4.31; extra == "all"
36
+ Requires-Dist: tensorflow>=2.15; extra == "all"
37
+ Requires-Dist: warp-lang>=1.5; extra == "all"
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest>=7; extra == "test"
40
+ Dynamic: license-file
41
+
42
+ # gradbridge
43
+
44
+ [![ci](https://github.com/aneangel/gradbridge/actions/workflows/ci.yml/badge.svg)](https://github.com/aneangel/gradbridge/actions/workflows/ci.yml)
45
+
46
+ **Gradients that cross framework boundaries.** Wrap a function written in PyTorch, JAX,
47
+ TensorFlow, NVIDIA Warp or NumPy, then call it — and differentiate through it — from any of
48
+ the others. On the GPU, data stays on the device.
49
+
50
+ ## Getting started
51
+
52
+ ```bash
53
+ pip install "gradbridge[torch,jax]" # pick the frameworks you use: torch, jax, tf, warp
54
+ pip install "gradbridge[all]" # or all four
55
+ ```
56
+
57
+ gradbridge itself only requires NumPy and Python 3.10+; each framework adapter is imported
58
+ when you first use it.
59
+
60
+ ## Quick example
61
+
62
+ Call a JAX function from PyTorch, and backpropagate through it:
63
+
64
+ ```python
65
+ import gradbridge as gb
66
+ import jax.numpy as jnp
67
+ import torch
68
+
69
+ physics = gb.to_torch(gb.from_jax(lambda x: jnp.sin(x) ** 2)) # JAX code...
70
+
71
+ x = torch.randn(3, requires_grad=True)
72
+ physics(x).sum().backward() # ...PyTorch gradients
73
+ print(x.grad) # 2 sin(x) cos(x)
74
+ ```
75
+
76
+ Every combination works the same way: a **source** wraps a function
77
+ (`from_torch`, `from_jax`, `from_tf`, `from_warp`, `from_numpy`) and a **consumer** exposes it
78
+ to another framework (`to_torch`, `to_jax`, `to_tf`, `to_warp`, `to_numpy`). Gradients flow
79
+ through `jit`, `torch.compile`, `vmap` and higher-order derivatives where the frameworks allow.
80
+
81
+ More complete examples:
82
+
83
+ - [Warp simulation optimized with JAX](https://github.com/aneangel/gradbridge/blob/main/examples/warp_sim_in_jax.py)
84
+ - [PyTorch policy trained through JAX dynamics and a TensorFlow reward](https://github.com/aneangel/gradbridge/blob/main/examples/mixed_pipeline.py)
85
+ - [Training through a Newton physics simulation from PyTorch](https://github.com/aneangel/gradbridge/blob/main/examples/newton_ball_torch.py)
86
+ — reproduces Newton's native training loop bit for bit
87
+
88
+ ## Known limitations
89
+
90
+ - **Bridge coarse functions.** Each call costs a few tenths of a millisecond and nothing fuses
91
+ across the boundary: wrap a simulator rollout, a model or a loss, not single operations.
92
+ - **NumPy functions run on the host**, so their inputs and outputs are copied off the GPU.
93
+ - **Calling other frameworks from TensorFlow needs a C++ compiler** for zero-copy; without one,
94
+ gradbridge warns once and falls back to `tf.py_function` (host copies).
95
+ - **Second-order derivatives** need a source that supports them: PyTorch, JAX and TensorFlow do;
96
+ Warp is first-order only; NumPy uses finite differences beyond your VJP.
97
+ - **Forward mode** is available for the JAX and NumPy consumers only.
98
+ - **Warp buffers you write yourself** between forward and backward are invisible to
99
+ gradbridge; pass `cache_residuals=False` to `to_torch` in that case.
100
+ - **Import PyTorch (or `gradbridge.torch`) before TensorFlow** if you use `torch.compile`:
101
+ the two bundle conflicting LLVM builds.
102
+ - **Tested on one GPU** (NVIDIA RTX 5090) and on CPU; multi-GPU placement is not validated yet.
103
+
104
+ Details and workarounds: [docs/limitations.md](https://github.com/aneangel/gradbridge/blob/main/docs/limitations.md).
105
+
106
+ ## Learn more
107
+
108
+ - [User guide](https://github.com/aneangel/gradbridge/blob/main/docs/guide.md): what works with
109
+ each framework, options, NumPy functions, gradient checking, and the `__gradbridge__` protocol
110
+ - [Design](https://github.com/aneangel/gradbridge/blob/main/docs/design.md): how it works, how data
111
+ moves between frameworks on the GPU, and performance
112
+ - [Benchmarks](https://github.com/aneangel/gradbridge/blob/main/benchmarks/RESULTS.md)
113
+
114
+ ## License
115
+
116
+ Apache-2.0
@@ -0,0 +1,75 @@
1
+ # gradbridge
2
+
3
+ [![ci](https://github.com/aneangel/gradbridge/actions/workflows/ci.yml/badge.svg)](https://github.com/aneangel/gradbridge/actions/workflows/ci.yml)
4
+
5
+ **Gradients that cross framework boundaries.** Wrap a function written in PyTorch, JAX,
6
+ TensorFlow, NVIDIA Warp or NumPy, then call it — and differentiate through it — from any of
7
+ the others. On the GPU, data stays on the device.
8
+
9
+ ## Getting started
10
+
11
+ ```bash
12
+ pip install "gradbridge[torch,jax]" # pick the frameworks you use: torch, jax, tf, warp
13
+ pip install "gradbridge[all]" # or all four
14
+ ```
15
+
16
+ gradbridge itself only requires NumPy and Python 3.10+; each framework adapter is imported
17
+ when you first use it.
18
+
19
+ ## Quick example
20
+
21
+ Call a JAX function from PyTorch, and backpropagate through it:
22
+
23
+ ```python
24
+ import gradbridge as gb
25
+ import jax.numpy as jnp
26
+ import torch
27
+
28
+ physics = gb.to_torch(gb.from_jax(lambda x: jnp.sin(x) ** 2)) # JAX code...
29
+
30
+ x = torch.randn(3, requires_grad=True)
31
+ physics(x).sum().backward() # ...PyTorch gradients
32
+ print(x.grad) # 2 sin(x) cos(x)
33
+ ```
34
+
35
+ Every combination works the same way: a **source** wraps a function
36
+ (`from_torch`, `from_jax`, `from_tf`, `from_warp`, `from_numpy`) and a **consumer** exposes it
37
+ to another framework (`to_torch`, `to_jax`, `to_tf`, `to_warp`, `to_numpy`). Gradients flow
38
+ through `jit`, `torch.compile`, `vmap` and higher-order derivatives where the frameworks allow.
39
+
40
+ More complete examples:
41
+
42
+ - [Warp simulation optimized with JAX](https://github.com/aneangel/gradbridge/blob/main/examples/warp_sim_in_jax.py)
43
+ - [PyTorch policy trained through JAX dynamics and a TensorFlow reward](https://github.com/aneangel/gradbridge/blob/main/examples/mixed_pipeline.py)
44
+ - [Training through a Newton physics simulation from PyTorch](https://github.com/aneangel/gradbridge/blob/main/examples/newton_ball_torch.py)
45
+ — reproduces Newton's native training loop bit for bit
46
+
47
+ ## Known limitations
48
+
49
+ - **Bridge coarse functions.** Each call costs a few tenths of a millisecond and nothing fuses
50
+ across the boundary: wrap a simulator rollout, a model or a loss, not single operations.
51
+ - **NumPy functions run on the host**, so their inputs and outputs are copied off the GPU.
52
+ - **Calling other frameworks from TensorFlow needs a C++ compiler** for zero-copy; without one,
53
+ gradbridge warns once and falls back to `tf.py_function` (host copies).
54
+ - **Second-order derivatives** need a source that supports them: PyTorch, JAX and TensorFlow do;
55
+ Warp is first-order only; NumPy uses finite differences beyond your VJP.
56
+ - **Forward mode** is available for the JAX and NumPy consumers only.
57
+ - **Warp buffers you write yourself** between forward and backward are invisible to
58
+ gradbridge; pass `cache_residuals=False` to `to_torch` in that case.
59
+ - **Import PyTorch (or `gradbridge.torch`) before TensorFlow** if you use `torch.compile`:
60
+ the two bundle conflicting LLVM builds.
61
+ - **Tested on one GPU** (NVIDIA RTX 5090) and on CPU; multi-GPU placement is not validated yet.
62
+
63
+ Details and workarounds: [docs/limitations.md](https://github.com/aneangel/gradbridge/blob/main/docs/limitations.md).
64
+
65
+ ## Learn more
66
+
67
+ - [User guide](https://github.com/aneangel/gradbridge/blob/main/docs/guide.md): what works with
68
+ each framework, options, NumPy functions, gradient checking, and the `__gradbridge__` protocol
69
+ - [Design](https://github.com/aneangel/gradbridge/blob/main/docs/design.md): how it works, how data
70
+ moves between frameworks on the GPU, and performance
71
+ - [Benchmarks](https://github.com/aneangel/gradbridge/blob/main/benchmarks/RESULTS.md)
72
+
73
+ ## License
74
+
75
+ Apache-2.0
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gradbridge"
7
+ version = "0.1.0"
8
+ description = "Gradients that cross framework boundaries: PyTorch, JAX, TensorFlow, NVIDIA Warp and NumPy."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Anthony Angeles" }]
13
+ dependencies = ["numpy>=1.24"]
14
+ keywords = ["autodiff", "gradients", "pytorch", "jax", "tensorflow", "warp", "interop", "dlpack"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: POSIX :: Linux",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Intended Audience :: Science/Research",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/aneangel/gradbridge"
30
+ Issues = "https://github.com/aneangel/gradbridge/issues"
31
+ Benchmarks = "https://github.com/aneangel/gradbridge/blob/main/benchmarks/RESULTS.md"
32
+
33
+ [project.optional-dependencies]
34
+ torch = ["torch>=2.4"]
35
+ jax = ["jax>=0.4.31"]
36
+ tf = ["tensorflow>=2.15"]
37
+ warp = ["warp-lang>=1.5"]
38
+ all = ["torch>=2.4", "jax>=0.4.31", "tensorflow>=2.15", "warp-lang>=1.5"]
39
+ test = ["pytest>=7"]
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
43
+
44
+ [tool.setuptools.package-data]
45
+ # Compiled on first use against the installed TensorFlow (see gradbridge/_tf_native.py).
46
+ gradbridge = ["_tf_op.cc"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ filterwarnings = ["ignore::DeprecationWarning", "ignore::FutureWarning"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,129 @@
1
+ """gradbridge: gradients that cross framework boundaries.
2
+
3
+ Wrap a function from one framework, use it -- and differentiate through it --
4
+ from any other::
5
+
6
+ import gradbridge as gb
7
+
8
+ f = gb.from_jax(jax_fn) # JAX function -> protocol object
9
+ torch_f = gb.to_torch(f) # ...callable from PyTorch
10
+ torch_f(x).sum().backward() # gradients flow back into JAX
11
+
12
+ Sources: from_torch, from_jax, from_tf, from_numpy, from_warp
13
+ Consumers: to_torch, to_jax, to_tf, to_numpy, to_warp
14
+
15
+ Framework adapters are imported lazily, so only the frameworks you use need to
16
+ be installed.
17
+ """
18
+
19
+ from ._check import CompareReport, GradcheckReport, compare, gradcheck
20
+ from ._convert import HostCopyWarning
21
+ from ._core import DiffFunction, MaxOrderError, Spec, as_diff_function
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "DiffFunction",
27
+ "Spec",
28
+ "MaxOrderError",
29
+ "HostCopyWarning",
30
+ "as_diff_function",
31
+ "from_torch",
32
+ "from_jax",
33
+ "from_tf",
34
+ "from_numpy",
35
+ "from_warp",
36
+ "to_torch",
37
+ "to_jax",
38
+ "to_tf",
39
+ "to_numpy",
40
+ "to_warp",
41
+ "convert",
42
+ "gradcheck",
43
+ "compare",
44
+ "GradcheckReport",
45
+ "CompareReport",
46
+ ]
47
+
48
+
49
+ def from_torch(fn, **kwargs):
50
+ """Wrap a PyTorch function. See :func:`gradbridge.torch.from_torch`."""
51
+ from .torch import from_torch as _f
52
+
53
+ return _f(fn, **kwargs)
54
+
55
+
56
+ def from_jax(fn, **kwargs):
57
+ """Wrap a JAX function. See :func:`gradbridge.jax.from_jax`."""
58
+ from .jax import from_jax as _f
59
+
60
+ return _f(fn, **kwargs)
61
+
62
+
63
+ def from_tf(fn, **kwargs):
64
+ """Wrap a TensorFlow function. See :func:`gradbridge.tf.from_tf`."""
65
+ from .tf import from_tf as _f
66
+
67
+ return _f(fn, **kwargs)
68
+
69
+
70
+ def from_numpy(fn, **kwargs):
71
+ """Wrap a NumPy function. See :func:`gradbridge.numpy.from_numpy`."""
72
+ from .numpy import from_numpy as _f
73
+
74
+ return _f(fn, **kwargs)
75
+
76
+
77
+ def from_warp(fn, **kwargs):
78
+ """Wrap a function that launches Warp kernels. See :func:`gradbridge.warp.from_warp`."""
79
+ from .warp import from_warp as _f
80
+
81
+ return _f(fn, **kwargs)
82
+
83
+
84
+ def to_torch(fn, **kwargs):
85
+ """Expose a gradbridge function to PyTorch. See :func:`gradbridge.torch.to_torch`."""
86
+ from .torch import to_torch as _f
87
+
88
+ return _f(fn, **kwargs)
89
+
90
+
91
+ def to_jax(fn, **kwargs):
92
+ """Expose a gradbridge function to JAX. See :func:`gradbridge.jax.to_jax`."""
93
+ from .jax import to_jax as _f
94
+
95
+ return _f(fn, **kwargs)
96
+
97
+
98
+ def to_tf(fn, **kwargs):
99
+ """Expose a gradbridge function to TensorFlow. See :func:`gradbridge.tf.to_tf`."""
100
+ from .tf import to_tf as _f
101
+
102
+ return _f(fn, **kwargs)
103
+
104
+
105
+ def to_numpy(fn, **kwargs):
106
+ """Expose a gradbridge function to NumPy. See :func:`gradbridge.numpy.to_numpy`."""
107
+ from .numpy import to_numpy as _f
108
+
109
+ return _f(fn, **kwargs)
110
+
111
+
112
+ def to_warp(fn, **kwargs):
113
+ """Expose a gradbridge function to Warp. See :func:`gradbridge.warp.to_warp`."""
114
+ from .warp import to_warp as _f
115
+
116
+ return _f(fn, **kwargs)
117
+
118
+
119
+ _SOURCES = {"torch": from_torch, "jax": from_jax, "tf": from_tf, "tensorflow": from_tf, "numpy": from_numpy, "warp": from_warp}
120
+ _CONSUMERS = {"torch": to_torch, "jax": to_jax, "tf": to_tf, "tensorflow": to_tf, "numpy": to_numpy, "warp": to_warp}
121
+
122
+
123
+ def convert(fn, *, src: str, dst: str, src_options: dict = None, dst_options: dict = None):
124
+ """One-step bridge: ``convert(jax_fn, src="jax", dst="torch")``."""
125
+ try:
126
+ source, consumer = _SOURCES[src], _CONSUMERS[dst]
127
+ except KeyError as e:
128
+ raise ValueError(f"unknown framework {e.args[0]!r}; choose from {sorted(_SOURCES)}") from None
129
+ return consumer(source(fn, **(src_options or {})), **(dst_options or {}))
@@ -0,0 +1,148 @@
1
+ """Gradient checking that works the same way for every framework.
2
+
3
+ ``gradcheck`` compares a function's analytic VJP (any order) against central
4
+ finite differences. ``compare`` checks that two implementations -- say the same
5
+ model in JAX and in PyTorch -- agree on values and gradients.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import List, Optional
12
+
13
+ import numpy as np
14
+
15
+ from . import _convert, _fd
16
+ from ._core import as_diff_function
17
+
18
+
19
+ @dataclass
20
+ class InputCheck:
21
+ index: int
22
+ shape: tuple
23
+ max_abs_err: float
24
+ max_rel_err: float
25
+ ok: bool
26
+
27
+
28
+ @dataclass
29
+ class GradcheckReport:
30
+ name: str
31
+ order: int
32
+ inputs: List[InputCheck] = field(default_factory=list)
33
+ note: str = ""
34
+
35
+ @property
36
+ def passed(self) -> bool:
37
+ return all(c.ok for c in self.inputs)
38
+
39
+ def __bool__(self) -> bool:
40
+ return self.passed
41
+
42
+ def __str__(self) -> str:
43
+ status = "PASSED" if self.passed else "FAILED"
44
+ lines = [f"gradcheck '{self.name}' (order {self.order}): {status}"]
45
+ if self.note:
46
+ lines.append(f" {self.note}")
47
+ lines.append(f" {'input':>5} {'shape':<14} {'max abs err':>12} {'max rel err':>12} ok")
48
+ for c in self.inputs:
49
+ lines.append(
50
+ f" {c.index:>5} {str(c.shape):<14} {c.max_abs_err:>12.3e} {c.max_rel_err:>12.3e} {'yes' if c.ok else 'NO'}"
51
+ )
52
+ return "\n".join(lines)
53
+
54
+
55
+ def _np(x):
56
+ return np.asarray(_convert.to_numpy(x))
57
+
58
+
59
+ def _random_like(rng, arrays):
60
+ return [
61
+ rng.standard_normal(np.shape(a)).astype(a.dtype) if np.issubdtype(a.dtype, np.floating) else np.zeros_like(a)
62
+ for a in arrays
63
+ ]
64
+
65
+
66
+ def gradcheck(
67
+ fn,
68
+ *xs,
69
+ order: int = 1,
70
+ eps: Optional[float] = None,
71
+ rtol: float = 1e-4,
72
+ atol: float = 1e-6,
73
+ seed: int = 0,
74
+ raise_on_fail: bool = False,
75
+ ) -> GradcheckReport:
76
+ """Check ``fn``'s order-``order`` reverse-mode derivative against finite differences.
77
+
78
+ Use float64 inputs: float32 finite differences are too noisy for tight
79
+ tolerances. Order 2 checks the VJP-of-VJP (Hessian-vector products), the
80
+ same way ``torch.autograd.gradgradcheck`` does.
81
+ """
82
+ dfn = as_diff_function(fn)
83
+ rng = np.random.default_rng(seed)
84
+ args = [_np(x) for x in xs]
85
+ target = dfn
86
+ for _ in range(order - 1):
87
+ outs = [_np(o) for o in target.eval(*args)]
88
+ args = args + _random_like(rng, outs)
89
+ target = target.vjp_function
90
+
91
+ outs = [_np(o) for o in target.eval(*args)]
92
+ cts = _random_like(rng, outs)
93
+ analytic = [_np(g) for g in target.vjp_function.eval(*args, *cts)]
94
+ numeric = _fd.fd_vjp(target, *args, cts=cts, eps=eps)
95
+
96
+ report = GradcheckReport(name=dfn.name, order=order)
97
+ if any(a.dtype == np.float32 for a in args):
98
+ report.note = "float32 inputs: finite differences are noisy, prefer float64"
99
+ for i, (a, n, x) in enumerate(zip(analytic, numeric, args)):
100
+ a = a.astype(np.float64).reshape(np.shape(x))
101
+ n = np.asarray(n, dtype=np.float64)
102
+ abs_err = float(np.max(np.abs(a - n))) if a.size else 0.0
103
+ denom = np.maximum(np.abs(n), atol)
104
+ rel_err = float(np.max(np.abs(a - n) / denom)) if a.size else 0.0
105
+ ok = bool(np.allclose(a, n, rtol=rtol, atol=atol))
106
+ report.inputs.append(InputCheck(i, tuple(np.shape(x)), abs_err, rel_err, ok))
107
+ if raise_on_fail and not report.passed:
108
+ raise AssertionError(str(report))
109
+ return report
110
+
111
+
112
+ @dataclass
113
+ class CompareReport:
114
+ name_a: str
115
+ name_b: str
116
+ max_value_err: float
117
+ max_grad_err: float
118
+ passed: bool
119
+
120
+ def __bool__(self) -> bool:
121
+ return self.passed
122
+
123
+ def __str__(self) -> str:
124
+ status = "MATCH" if self.passed else "MISMATCH"
125
+ return (
126
+ f"compare '{self.name_a}' vs '{self.name_b}': {status} "
127
+ f"(max value err {self.max_value_err:.3e}, max grad err {self.max_grad_err:.3e})"
128
+ )
129
+
130
+
131
+ def compare(fn_a, fn_b, *xs, rtol: float = 1e-5, atol: float = 1e-7, seed: int = 0) -> CompareReport:
132
+ """Check that two implementations agree on outputs and on VJPs at ``xs``."""
133
+ a, b = as_diff_function(fn_a), as_diff_function(fn_b)
134
+ rng = np.random.default_rng(seed)
135
+ args = [_np(x) for x in xs]
136
+ oa = [_np(o) for o in a.eval(*args)]
137
+ ob = [_np(o) for o in b.eval(*args)]
138
+ if len(oa) != len(ob) or any(x.shape != y.shape for x, y in zip(oa, ob)):
139
+ return CompareReport(a.name, b.name, float("inf"), float("inf"), False)
140
+ val_err = max((float(np.max(np.abs(x - y))) if x.size else 0.0) for x, y in zip(oa, ob)) if oa else 0.0
141
+ cts = _random_like(rng, oa)
142
+ ga = [_np(g) for g in a.vjp_function.eval(*args, *cts)]
143
+ gb = [_np(g) for g in b.vjp_function.eval(*args, *cts)]
144
+ grad_err = max((float(np.max(np.abs(x - y))) if np.size(x) else 0.0) for x, y in zip(ga, gb)) if ga else 0.0
145
+ ok = all(np.allclose(x, y, rtol=rtol, atol=atol) for x, y in zip(oa, ob)) and all(
146
+ np.allclose(x, y, rtol=rtol, atol=atol) for x, y in zip(ga, gb)
147
+ )
148
+ return CompareReport(a.name, b.name, val_err, grad_err, ok)