gradbridge 0.1.0__py3-none-any.whl
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/__init__.py +129 -0
- gradbridge/_check.py +148 -0
- gradbridge/_convert.py +455 -0
- gradbridge/_core.py +291 -0
- gradbridge/_fd.py +95 -0
- gradbridge/_tf_native.py +173 -0
- gradbridge/_tf_op.cc +108 -0
- gradbridge/jax.py +340 -0
- gradbridge/numpy.py +142 -0
- gradbridge/tf.py +223 -0
- gradbridge/torch.py +541 -0
- gradbridge/warp.py +333 -0
- gradbridge-0.1.0.dist-info/METADATA +116 -0
- gradbridge-0.1.0.dist-info/RECORD +17 -0
- gradbridge-0.1.0.dist-info/WHEEL +5 -0
- gradbridge-0.1.0.dist-info/licenses/LICENSE +15 -0
- gradbridge-0.1.0.dist-info/top_level.txt +1 -0
gradbridge/__init__.py
ADDED
|
@@ -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 {}))
|
gradbridge/_check.py
ADDED
|
@@ -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)
|