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 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)