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.
- gradbridge-0.1.0/LICENSE +15 -0
- gradbridge-0.1.0/PKG-INFO +116 -0
- gradbridge-0.1.0/README.md +75 -0
- gradbridge-0.1.0/pyproject.toml +50 -0
- gradbridge-0.1.0/setup.cfg +4 -0
- gradbridge-0.1.0/src/gradbridge/__init__.py +129 -0
- gradbridge-0.1.0/src/gradbridge/_check.py +148 -0
- gradbridge-0.1.0/src/gradbridge/_convert.py +455 -0
- gradbridge-0.1.0/src/gradbridge/_core.py +291 -0
- gradbridge-0.1.0/src/gradbridge/_fd.py +95 -0
- gradbridge-0.1.0/src/gradbridge/_tf_native.py +173 -0
- gradbridge-0.1.0/src/gradbridge/_tf_op.cc +108 -0
- gradbridge-0.1.0/src/gradbridge/jax.py +340 -0
- gradbridge-0.1.0/src/gradbridge/numpy.py +142 -0
- gradbridge-0.1.0/src/gradbridge/tf.py +223 -0
- gradbridge-0.1.0/src/gradbridge/torch.py +541 -0
- gradbridge-0.1.0/src/gradbridge/warp.py +333 -0
- gradbridge-0.1.0/src/gradbridge.egg-info/PKG-INFO +116 -0
- gradbridge-0.1.0/src/gradbridge.egg-info/SOURCES.txt +24 -0
- gradbridge-0.1.0/src/gradbridge.egg-info/dependency_links.txt +1 -0
- gradbridge-0.1.0/src/gradbridge.egg-info/requires.txt +22 -0
- gradbridge-0.1.0/src/gradbridge.egg-info/top_level.txt +1 -0
- gradbridge-0.1.0/tests/test_advanced.py +444 -0
- gradbridge-0.1.0/tests/test_gpu.py +696 -0
- gradbridge-0.1.0/tests/test_gpu_advanced.py +254 -0
- gradbridge-0.1.0/tests/test_matrix.py +193 -0
gradbridge-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](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
|
+
[](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,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)
|