folx 0.2__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.
- folx/__init__.py +24 -0
- folx/api.py +374 -0
- folx/custom_hessian.py +59 -0
- folx/hessian.py +346 -0
- folx/interpreter.py +221 -0
- folx/jvp.py +438 -0
- folx/operators.py +72 -0
- folx/tree_utils.py +81 -0
- folx/utils.py +519 -0
- folx/vmap.py +67 -0
- folx/wrapped_functions.py +303 -0
- folx/wrapper.py +130 -0
- folx-0.2.dist-info/LICENSE +21 -0
- folx-0.2.dist-info/METADATA +216 -0
- folx-0.2.dist-info/RECORD +16 -0
- folx-0.2.dist-info/WHEEL +4 -0
folx/hessian.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.flatten_util as jfu
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
import jax.tree_util as jtu
|
|
9
|
+
import jaxlib.xla_extension
|
|
10
|
+
import numpy as np
|
|
11
|
+
from jax import core
|
|
12
|
+
|
|
13
|
+
from .api import (
|
|
14
|
+
JAC_DIM,
|
|
15
|
+
Array,
|
|
16
|
+
Axes,
|
|
17
|
+
CustomTraceJacHessianJac,
|
|
18
|
+
ExtraArgs,
|
|
19
|
+
ForwardFn,
|
|
20
|
+
FunctionFlags,
|
|
21
|
+
FwdJacobian,
|
|
22
|
+
FwdLaplArgs,
|
|
23
|
+
FwdLaplArray,
|
|
24
|
+
MergeFn,
|
|
25
|
+
PyTree,
|
|
26
|
+
)
|
|
27
|
+
from .utils import (
|
|
28
|
+
add_vmap_jacobian_dim,
|
|
29
|
+
array_wise_flat_wrap,
|
|
30
|
+
flat_wrap,
|
|
31
|
+
get_reduced_jacobians,
|
|
32
|
+
jac_jacT,
|
|
33
|
+
trace_jac_jacT,
|
|
34
|
+
trace_of_product,
|
|
35
|
+
vmap_sequences_and_squeeze,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def general_jac_hessian_jac(fn: ForwardFn, args: FwdLaplArgs, materialize_idx: Array | None):
|
|
40
|
+
# It's conceptually easier to work with the flattened version of the
|
|
41
|
+
# Hessian, since we can then use einsum to compute the trace.
|
|
42
|
+
flat_fn = flat_wrap(fn, *args.x)
|
|
43
|
+
flat_x = jfu.ravel_pytree(args.x)[0]
|
|
44
|
+
# We have to decide on an order in which we execute tr(HJJ^T).
|
|
45
|
+
# H will be of shape NxDxD, J is DxK where N could potentially be D.
|
|
46
|
+
# We will do the following:
|
|
47
|
+
# if K >= D, we compute
|
|
48
|
+
# JJ^T first and then the trace.
|
|
49
|
+
# if D < K, we compute HJ first and then the trace.
|
|
50
|
+
# We should also flatten our gradient tensor to a 2D matrix where the first dimension
|
|
51
|
+
# is the x0 dim and the second dim is the input dim.
|
|
52
|
+
grads_2d = get_reduced_jacobians(*args.jacobian, idx=materialize_idx)
|
|
53
|
+
grad_2d = jnp.concatenate([x.T for x in grads_2d], axis=0)
|
|
54
|
+
D, K = grad_2d.shape
|
|
55
|
+
if K > D:
|
|
56
|
+
# jax.hessian uses Fwd on Reverse AD
|
|
57
|
+
flat_hessian = jax.hessian(flat_fn)(flat_x)
|
|
58
|
+
flat_out = trace_of_product(flat_hessian, grad_2d @ grad_2d.T)
|
|
59
|
+
elif D > K:
|
|
60
|
+
# Directly copmute the trace of tr(HJJ^T)=tr(J^THJ)
|
|
61
|
+
@functools.partial(jax.vmap, in_axes=-1, out_axes=-1)
|
|
62
|
+
def vhvp(tangent):
|
|
63
|
+
def vjp(x):
|
|
64
|
+
@functools.partial(jax.vmap, in_axes=(None, -1), out_axes=-1)
|
|
65
|
+
def jvp(x, tangent):
|
|
66
|
+
return jax.jvp(flat_fn, (x,), (tangent,))[1]
|
|
67
|
+
|
|
68
|
+
return jvp(x, grad_2d)
|
|
69
|
+
|
|
70
|
+
return jax.jvp(vjp, (flat_x,), (tangent,))[1]
|
|
71
|
+
|
|
72
|
+
flat_out = jnp.trace(vhvp(grad_2d), axis1=-2, axis2=-1)
|
|
73
|
+
else:
|
|
74
|
+
# Implementation where we compute HJ and then the trace via
|
|
75
|
+
# the sum of hadamard product
|
|
76
|
+
@functools.partial(jax.vmap, in_axes=-1, out_axes=-1)
|
|
77
|
+
def hvp(tangent):
|
|
78
|
+
def jacobian(x):
|
|
79
|
+
return jax.jacrev(flat_fn)(x)
|
|
80
|
+
|
|
81
|
+
return jax.jvp(jacobian, (flat_x,), (tangent,))[1]
|
|
82
|
+
|
|
83
|
+
HJ = hvp(grad_2d) # N x D x K
|
|
84
|
+
flat_out = trace_of_product(HJ, grad_2d) # N x D x K and D x K
|
|
85
|
+
|
|
86
|
+
# since f(x) and nabla f(x) should have the same structure, we can use the
|
|
87
|
+
# structure of f(x) to unravel the flat_out
|
|
88
|
+
unravel = jfu.ravel_pytree(fn(*args.x))[1]
|
|
89
|
+
return unravel(flat_out)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def off_diag_jac_hessian_jac(fn: ForwardFn, args: FwdLaplArgs, materialize_idx: Array | None):
|
|
93
|
+
# if we know that a function is linear in one arguments, it's hessian must be off diagonal
|
|
94
|
+
# thus we can safe some computation by only computing the off diagonal part of the hessian.
|
|
95
|
+
assert len(args) == 2, "Off diag hessian only supports 2 args at the moment."
|
|
96
|
+
|
|
97
|
+
def flat_arr(x: FwdLaplArray) -> Array:
|
|
98
|
+
return jfu.ravel_pytree(x.x)[0]
|
|
99
|
+
|
|
100
|
+
flat_fn = array_wise_flat_wrap(fn, *args.x)
|
|
101
|
+
|
|
102
|
+
def jac_lhs(lhs, rhs):
|
|
103
|
+
return jax.jacobian(flat_fn, argnums=0)(lhs, rhs)
|
|
104
|
+
|
|
105
|
+
hessian = jax.jacobian(jac_lhs, argnums=1)(flat_arr(args.arrays[0]), flat_arr(args.arrays[1]))
|
|
106
|
+
|
|
107
|
+
flat_out = 2 * trace_of_product(
|
|
108
|
+
hessian, jac_jacT(args.arrays[0].jacobian, args.arrays[1].jacobian, materialize_idx)
|
|
109
|
+
)
|
|
110
|
+
unravel = jfu.ravel_pytree(fn(*args.x))[1]
|
|
111
|
+
return unravel(flat_out)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def mul_jac_hessian_jac(fn: ForwardFn, args: FwdLaplArgs, shared_idx: Array | None):
|
|
115
|
+
# For a dot product we know that the hessian looks like this:
|
|
116
|
+
# [0, I]
|
|
117
|
+
# [I, 0]
|
|
118
|
+
# where I is the identity matrix of the same shape as the input.
|
|
119
|
+
assert len(args) == 2, "Dot product only supports two args."
|
|
120
|
+
flat_out = (
|
|
121
|
+
2 * trace_jac_jacT(args.arrays[0].jacobian, args.arrays[1].jacobian, shared_idx)[None]
|
|
122
|
+
)
|
|
123
|
+
unravel = jfu.ravel_pytree(fn(*args.x))[1]
|
|
124
|
+
return unravel(flat_out)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def remove_fill(arrs: np.ndarray, find_unique: bool = False):
|
|
128
|
+
"""
|
|
129
|
+
Remove the fill value from an array. As the tensors might not be shaped correctly
|
|
130
|
+
afterwards, we reduce all the leading dimensions by lists.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
- arrs: array to remove fill value from
|
|
134
|
+
Returns:
|
|
135
|
+
- arrs: nested lists of arrays without fill value
|
|
136
|
+
"""
|
|
137
|
+
if arrs.size == 0:
|
|
138
|
+
return arrs
|
|
139
|
+
if arrs[0].ndim >= 1:
|
|
140
|
+
return [remove_fill(x, find_unique=find_unique) for x in arrs]
|
|
141
|
+
if find_unique:
|
|
142
|
+
arrs = np.unique(arrs)
|
|
143
|
+
return arrs[arrs >= 0] # type: ignore
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def merge_and_populate(arrs: Sequence[np.ndarray], operation: Callable[[np.ndarray, np.ndarray], np.ndarray]):
|
|
147
|
+
"""
|
|
148
|
+
The arrays are assumed to be of the same shape. We look at the intersection of all arrays.
|
|
149
|
+
We then find the maximum intersection size and fill all arrays to that size.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
- arrs: list of arrays
|
|
153
|
+
Returns:
|
|
154
|
+
- arrs: np.ndarray where only intersections are kept and all arrays are filled to the same size.
|
|
155
|
+
"""
|
|
156
|
+
result = jtu.tree_map(
|
|
157
|
+
lambda *x: functools.reduce(operation, tuple(x[1:]), x[0]),
|
|
158
|
+
*arrs,
|
|
159
|
+
is_leaf=lambda x: isinstance(x, np.ndarray)
|
|
160
|
+
)
|
|
161
|
+
sizes = jtu.tree_map(lambda x: x.size, result, is_leaf=lambda x: isinstance(x, np.ndarray))
|
|
162
|
+
max_size = np.max(jtu.tree_leaves(sizes))
|
|
163
|
+
result = jtu.tree_map(
|
|
164
|
+
lambda x: np.concatenate([x, np.full(max_size - x.size, -1, dtype=x.dtype)]),
|
|
165
|
+
result,
|
|
166
|
+
is_leaf=lambda x: isinstance(x, np.ndarray),
|
|
167
|
+
)
|
|
168
|
+
return np.asarray(result, dtype=int)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def find_materialization_idx(lapl_args: FwdLaplArgs, in_axes, flags: FunctionFlags, threshold: int):
|
|
172
|
+
if not lapl_args.any_jacobian_weak:
|
|
173
|
+
return None
|
|
174
|
+
# TODO: Rewrite this!! This is quity messy and inefficient.
|
|
175
|
+
# it assumes that we're only interested in the last dimension.
|
|
176
|
+
with core.new_main(core.EvalTrace, dynamic=True):
|
|
177
|
+
vmap_seq, (inp,) = vmap_sequences_and_squeeze(
|
|
178
|
+
([j.mask for j in lapl_args.jacobian],),
|
|
179
|
+
([j for j in add_vmap_jacobian_dim(lapl_args, FwdLaplArgs(in_axes)).jacobian],),
|
|
180
|
+
)
|
|
181
|
+
max_size = np.max([np.sum(j.unique_idx >= 0, dtype=int) for j in lapl_args.jacobian])
|
|
182
|
+
# This can be quite memory intensive, so we try to do it on the GPU and
|
|
183
|
+
# if that fails we just use the CPU. On the CPU this takes quite some time.
|
|
184
|
+
# TODO: work on a more memory efficient implementation!
|
|
185
|
+
unique_fn = functools.partial(jnp.unique, size=max_size + 1, fill_value=-1)
|
|
186
|
+
|
|
187
|
+
def idx_fn(x):
|
|
188
|
+
return jtu.tree_map(unique_fn, x)
|
|
189
|
+
for s in vmap_seq[::-1]:
|
|
190
|
+
idx_fn = jax.vmap(idx_fn, in_axes=s)
|
|
191
|
+
try:
|
|
192
|
+
# This path is more memory intensive by using the GPU to find uniques but
|
|
193
|
+
# potentially fails if the arrays are too large.
|
|
194
|
+
# +1 because we need to accomodate the -1.
|
|
195
|
+
arrs = np.asarray(idx_fn(inp), dtype=int)
|
|
196
|
+
except jaxlib.xla_extension.XlaRuntimeError:
|
|
197
|
+
logging.info(
|
|
198
|
+
"Failed to find unique elements on GPU, falling back to CPU. This will be slow."
|
|
199
|
+
)
|
|
200
|
+
with jax.default_device(jax.devices("cpu")[0]):
|
|
201
|
+
arrs = np.asarray(idx_fn(inp), dtype=int)
|
|
202
|
+
filtered_arrs = remove_fill(arrs, False)
|
|
203
|
+
|
|
204
|
+
if FunctionFlags.LINEAR_IN_ONE in flags:
|
|
205
|
+
# For off diagonal Hessians we only need to look at the intersection between
|
|
206
|
+
# all arrays rather than their union.
|
|
207
|
+
idx = merge_and_populate(filtered_arrs, np.intersect1d) # type: ignore
|
|
208
|
+
else:
|
|
209
|
+
idx = merge_and_populate(filtered_arrs, np.union1d) # type: ignore
|
|
210
|
+
idx = np.moveaxis(idx, -1, JAC_DIM)
|
|
211
|
+
|
|
212
|
+
if idx.shape[JAC_DIM] >= max_size or idx.shape[JAC_DIM] > threshold:
|
|
213
|
+
idx = None
|
|
214
|
+
return idx
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def remove_zero_entries(lapl_args: FwdLaplArgs, materialize_idx: np.ndarray | None):
|
|
218
|
+
if materialize_idx is None:
|
|
219
|
+
return lapl_args, None, None
|
|
220
|
+
|
|
221
|
+
mask = (materialize_idx != -1).any(0)
|
|
222
|
+
if mask.sum() > 0.5 * mask.size:
|
|
223
|
+
# this is a heuristic to avoid having unnecessary indexing overhead for
|
|
224
|
+
# insufficiently sparse masks.
|
|
225
|
+
return lapl_args, materialize_idx, None
|
|
226
|
+
|
|
227
|
+
indices = np.where(mask)
|
|
228
|
+
new_mat_idx = materialize_idx[(slice(None), *indices)]
|
|
229
|
+
new_arrs = []
|
|
230
|
+
for arg in lapl_args.arrays:
|
|
231
|
+
brdcast_dims = np.where(np.array(arg.x.shape) == 1)[0]
|
|
232
|
+
idx = tuple(
|
|
233
|
+
0 if i in brdcast_dims else x
|
|
234
|
+
for i, x in enumerate(indices)
|
|
235
|
+
)
|
|
236
|
+
new_arrs.append(FwdLaplArray(
|
|
237
|
+
x=arg.x[idx],
|
|
238
|
+
jacobian=FwdJacobian(
|
|
239
|
+
data=arg.jacobian.data[(slice(None), *idx)],
|
|
240
|
+
x0_idx=arg.jacobian.x0_idx[(slice(None), *idx)], # type: ignore
|
|
241
|
+
),
|
|
242
|
+
laplacian=arg.laplacian[idx],
|
|
243
|
+
))
|
|
244
|
+
new_args = FwdLaplArgs(tuple(new_arrs))
|
|
245
|
+
return new_args, new_mat_idx, mask
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def vmapped_jac_hessian_jac(
|
|
249
|
+
fwd: ForwardFn,
|
|
250
|
+
flags: FunctionFlags,
|
|
251
|
+
custom_jac_hessian_jac: CustomTraceJacHessianJac | None,
|
|
252
|
+
extra_args: ExtraArgs,
|
|
253
|
+
in_axes: Axes,
|
|
254
|
+
extra_in_axes: Axes,
|
|
255
|
+
merge: MergeFn,
|
|
256
|
+
sparsity_threshold: int,
|
|
257
|
+
lapl_args: FwdLaplArgs,
|
|
258
|
+
) -> PyTree[Array]:
|
|
259
|
+
# Determine output structure
|
|
260
|
+
def merged_fn(*x: Array):
|
|
261
|
+
return fwd(*merge(x, extra_args))
|
|
262
|
+
out = merged_fn(*lapl_args.x)
|
|
263
|
+
unravel = jfu.ravel_pytree(out)[1]
|
|
264
|
+
|
|
265
|
+
materialize_idx = find_materialization_idx(lapl_args, in_axes, flags, sparsity_threshold)
|
|
266
|
+
if materialize_idx is None:
|
|
267
|
+
lapl_args = lapl_args.dense
|
|
268
|
+
if materialize_idx is not None and materialize_idx.shape[JAC_DIM] == 0:
|
|
269
|
+
return jnp.zeros(())
|
|
270
|
+
|
|
271
|
+
# If we do a dot product (not a hadamard product) we can check for empty hessian entries
|
|
272
|
+
if FunctionFlags.DOT_PRODUCT in flags and all(len(a) == 1 for a in in_axes):
|
|
273
|
+
lapl_args, materialize_idx, mask = remove_zero_entries(lapl_args, materialize_idx)
|
|
274
|
+
in_axes = jtu.tree_map(lambda _: -1, in_axes)
|
|
275
|
+
else:
|
|
276
|
+
mask = None
|
|
277
|
+
|
|
278
|
+
# Broadcast and flatten all arguments
|
|
279
|
+
vmap_seq, (lapl_args, extra_args) = vmap_sequences_and_squeeze(
|
|
280
|
+
(lapl_args, extra_args),
|
|
281
|
+
(add_vmap_jacobian_dim(lapl_args, FwdLaplArgs(in_axes)), extra_in_axes),
|
|
282
|
+
)
|
|
283
|
+
# Hessian computation
|
|
284
|
+
def hess_transform(args: FwdLaplArgs, extra_args: ExtraArgs, materialize_idx):
|
|
285
|
+
def merged_fn(*x):
|
|
286
|
+
return fwd(*merge(x, extra_args))
|
|
287
|
+
|
|
288
|
+
if custom_jac_hessian_jac is not None:
|
|
289
|
+
result = custom_jac_hessian_jac(args, extra_args, merge, materialize_idx)
|
|
290
|
+
elif FunctionFlags.MULTIPLICATION in flags:
|
|
291
|
+
result = mul_jac_hessian_jac(merged_fn, args, materialize_idx)
|
|
292
|
+
elif FunctionFlags.LINEAR_IN_ONE in flags:
|
|
293
|
+
result = off_diag_jac_hessian_jac(merged_fn, args, materialize_idx)
|
|
294
|
+
else:
|
|
295
|
+
result = general_jac_hessian_jac(merged_fn, args, materialize_idx)
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
# TODO: this implementation also assumes that we only reduce the last dimension.
|
|
299
|
+
for axes in vmap_seq[::-1]:
|
|
300
|
+
hess_transform = jax.vmap(
|
|
301
|
+
hess_transform, in_axes=(*axes, (None if materialize_idx is None else 1))
|
|
302
|
+
)
|
|
303
|
+
# flatten to 1D and then unravel to the original structure
|
|
304
|
+
if mask is None:
|
|
305
|
+
flat_out = jfu.ravel_pytree(hess_transform(lapl_args, extra_args, materialize_idx))[0]
|
|
306
|
+
else:
|
|
307
|
+
flat_out = hess_transform(lapl_args, extra_args, materialize_idx)
|
|
308
|
+
result = jnp.zeros_like(out).at[mask].set(flat_out) # type: ignore
|
|
309
|
+
flat_out = jfu.ravel_pytree(result)[0]
|
|
310
|
+
return unravel(flat_out)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_jacobian_hessian_jacobian_trace(
|
|
314
|
+
fwd: ForwardFn,
|
|
315
|
+
flags: FunctionFlags,
|
|
316
|
+
custom_jac_hessian_jac: CustomTraceJacHessianJac | None,
|
|
317
|
+
extra_args: ExtraArgs,
|
|
318
|
+
in_axes: Axes,
|
|
319
|
+
extra_in_axes: Axes,
|
|
320
|
+
merge: MergeFn,
|
|
321
|
+
):
|
|
322
|
+
def hessian_transform(args: FwdLaplArgs, sparsity_threshold: int):
|
|
323
|
+
if FunctionFlags.LINEAR in flags:
|
|
324
|
+
return jnp.zeros(())
|
|
325
|
+
elif FunctionFlags.LINEAR_IN_ONE in flags and len(args.arrays) == 1:
|
|
326
|
+
return jnp.zeros(())
|
|
327
|
+
elif (
|
|
328
|
+
FunctionFlags.LINEAR_IN_FIRST in flags
|
|
329
|
+
and jtu.tree_leaves(merge(args.x, extra_args))[0] is args.x[0]
|
|
330
|
+
and len(args.arrays) == 1
|
|
331
|
+
):
|
|
332
|
+
return jnp.zeros(())
|
|
333
|
+
else:
|
|
334
|
+
return vmapped_jac_hessian_jac(
|
|
335
|
+
fwd=fwd,
|
|
336
|
+
flags=flags,
|
|
337
|
+
custom_jac_hessian_jac=custom_jac_hessian_jac,
|
|
338
|
+
extra_args=extra_args,
|
|
339
|
+
in_axes=in_axes,
|
|
340
|
+
extra_in_axes=extra_in_axes,
|
|
341
|
+
merge=merge,
|
|
342
|
+
sparsity_threshold=sparsity_threshold,
|
|
343
|
+
lapl_args=args,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return hessian_transform
|
folx/interpreter.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Callable, ParamSpec, Sequence, TypeVar
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
import jax.tree_util as jtu
|
|
8
|
+
import numpy as np
|
|
9
|
+
from jax import core
|
|
10
|
+
from jax.util import safe_map
|
|
11
|
+
|
|
12
|
+
from .api import Array, ArrayOrFwdLaplArray, FwdJacobian, FwdLaplArray, PyTree
|
|
13
|
+
from .utils import extract_jacobian_mask, ravel
|
|
14
|
+
from .wrapped_functions import get_laplacian
|
|
15
|
+
|
|
16
|
+
R = TypeVar("R", bound=PyTree[Array])
|
|
17
|
+
P = ParamSpec("P")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JaxExprEnvironment:
|
|
21
|
+
# A simple environment that keeps track of the variables
|
|
22
|
+
# and frees them once they are no longer needed.
|
|
23
|
+
env: dict[core.Var, ArrayOrFwdLaplArray]
|
|
24
|
+
reference_counter: dict[core.Var, int]
|
|
25
|
+
|
|
26
|
+
def __init__(self, jaxpr: core.Jaxpr, consts: Sequence[Array], *args: ArrayOrFwdLaplArray):
|
|
27
|
+
self.env = {}
|
|
28
|
+
self.reference_counter = defaultdict(int)
|
|
29
|
+
for v in jaxpr.invars + jaxpr.constvars:
|
|
30
|
+
if isinstance(v, core.Literal):
|
|
31
|
+
continue
|
|
32
|
+
self.reference_counter[v] += 1
|
|
33
|
+
eqn: core.JaxprEqn
|
|
34
|
+
for eqn in jaxpr.eqns:
|
|
35
|
+
for v in eqn.invars:
|
|
36
|
+
if isinstance(v, core.Literal):
|
|
37
|
+
continue
|
|
38
|
+
self.reference_counter[v] += 1
|
|
39
|
+
for v in jaxpr.outvars:
|
|
40
|
+
if isinstance(v, core.Literal):
|
|
41
|
+
continue
|
|
42
|
+
self.reference_counter[v] = np.iinfo(np.int32).max
|
|
43
|
+
self.write_many(jaxpr.constvars, consts)
|
|
44
|
+
self.write_many(jaxpr.invars, args)
|
|
45
|
+
|
|
46
|
+
def read(self, var: core.Atom) -> ArrayOrFwdLaplArray:
|
|
47
|
+
if isinstance(var, core.Literal):
|
|
48
|
+
return var.val
|
|
49
|
+
self.reference_counter[var] -= 1
|
|
50
|
+
result = self.env[var]
|
|
51
|
+
if self.reference_counter[var] == 0:
|
|
52
|
+
del self.env[var]
|
|
53
|
+
del self.reference_counter[var]
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def write(self, var: core.Var, val: ArrayOrFwdLaplArray):
|
|
57
|
+
if self.reference_counter[var] > 0:
|
|
58
|
+
self.env[var] = val
|
|
59
|
+
|
|
60
|
+
def read_many(self, vars: Sequence[core.Atom]) -> list[ArrayOrFwdLaplArray]:
|
|
61
|
+
return safe_map(self.read, vars)
|
|
62
|
+
|
|
63
|
+
def write_many(self, vars: Sequence[core.Var], vals: Sequence[ArrayOrFwdLaplArray]):
|
|
64
|
+
return safe_map(self.write, vars, vals)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def eval_jaxpr_with_forward_laplacian(jaxpr: core.Jaxpr, consts, *args, sparsity_threshold: int):
|
|
68
|
+
enable_sparsity = sparsity_threshold > 0
|
|
69
|
+
env = JaxExprEnvironment(jaxpr, consts, *args)
|
|
70
|
+
|
|
71
|
+
def eval_scan(eqn: core.JaxprEqn, invals):
|
|
72
|
+
n_carry, n_const = eqn.params["num_carry"], eqn.params["num_consts"]
|
|
73
|
+
in_const, in_carry, in_inp = invals[:n_const], invals[n_const:n_carry+n_const], invals[n_const+n_carry:]
|
|
74
|
+
carry_merge = extract_jacobian_mask(in_carry)
|
|
75
|
+
assert all(isinstance(x, Array) for x in in_inp), "Scan does not support scanning over input depenedent tensors.\nPlease unroll the loop."
|
|
76
|
+
def wrapped(carry, x):
|
|
77
|
+
result = eval_jaxpr_with_forward_laplacian(
|
|
78
|
+
eqn.params['jaxpr'].jaxpr,
|
|
79
|
+
(),
|
|
80
|
+
*in_const,
|
|
81
|
+
*carry_merge(carry),
|
|
82
|
+
*x,
|
|
83
|
+
sparsity_threshold=sparsity_threshold
|
|
84
|
+
)
|
|
85
|
+
return result[:n_carry], result[n_carry:]
|
|
86
|
+
first_carry, first_y = wrapped(in_carry, jtu.tree_map(lambda x: x[0], in_inp))
|
|
87
|
+
# Check whether jacobian sparsity matches
|
|
88
|
+
for a, b in zip(in_carry, first_carry):
|
|
89
|
+
if type(a) != type(b):
|
|
90
|
+
raise TypeError(f"Type mismatch in scan: {type(a)} != {type(b)}")
|
|
91
|
+
if isinstance(a, FwdLaplArray):
|
|
92
|
+
if not np.all(a.jacobian.x0_idx == b.jacobian.x0_idx): # type: ignore
|
|
93
|
+
raise ValueError("Jacobian sparsity mismatch in scan.")
|
|
94
|
+
carry, y = jax.lax.scan(
|
|
95
|
+
wrapped, # type: ignore
|
|
96
|
+
in_carry,
|
|
97
|
+
in_inp,
|
|
98
|
+
length=eqn.params['length'],
|
|
99
|
+
reverse=eqn.params['reverse'],
|
|
100
|
+
unroll=eqn.params['unroll'],
|
|
101
|
+
)
|
|
102
|
+
carry = [
|
|
103
|
+
a._replace(jacobian=a.jacobian._replace(x0_idx=b.jacobian.x0_idx)) # type: ignore
|
|
104
|
+
if isinstance(a, FwdLaplArray) else a
|
|
105
|
+
for a, b in zip(carry, first_carry)
|
|
106
|
+
]
|
|
107
|
+
y = [
|
|
108
|
+
a._replace(jacobian=a.jacobian._replace(x0_idx=b.jacobian.x0_idx)) # type: ignore
|
|
109
|
+
if isinstance(a, FwdLaplArray) else a
|
|
110
|
+
for a, b in zip(y, first_y)
|
|
111
|
+
]
|
|
112
|
+
return *carry, *y
|
|
113
|
+
|
|
114
|
+
def eval_pjit(eqn: core.JaxprEqn, invals):
|
|
115
|
+
name = eqn.params["name"]
|
|
116
|
+
if fn := get_laplacian(name):
|
|
117
|
+
# TODO: this is a bit incomplete, e.g., kwargs?
|
|
118
|
+
outvals = fn(*invals, sparsity_threshold=sparsity_threshold)
|
|
119
|
+
if isinstance(outvals, (FwdLaplArray, Array)):
|
|
120
|
+
outvals = [outvals] # TODO: Figure out how to properly handle outvals
|
|
121
|
+
return outvals
|
|
122
|
+
sub_expr: core.ClosedJaxpr = eqn.params["jaxpr"]
|
|
123
|
+
return eval_jaxpr_with_forward_laplacian(
|
|
124
|
+
sub_expr.jaxpr, sub_expr.literals, *invals, sparsity_threshold=sparsity_threshold
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def eval_laplacian(eqn: core.JaxprEqn, invals):
|
|
128
|
+
fn = get_laplacian(eqn.primitive, True)
|
|
129
|
+
return fn(*invals, **eqn.params, sparsity_threshold=sparsity_threshold)
|
|
130
|
+
|
|
131
|
+
for eqn in jaxpr.eqns:
|
|
132
|
+
invals = env.read_many(eqn.invars)
|
|
133
|
+
# Eval expression
|
|
134
|
+
if all(not isinstance(x, FwdLaplArray) for x in invals):
|
|
135
|
+
subfuns, bind_params = eqn.primitive.get_bind_params(eqn.params)
|
|
136
|
+
# If non of the inputs were dependent on an FwdLaplArray,
|
|
137
|
+
# we can just use the regular primitive. This will avoid
|
|
138
|
+
# omnistaging. While this could cost us some memory and speed,
|
|
139
|
+
# it gives us access to more variables during tracing.
|
|
140
|
+
# https://github.com/google/jax/pull/3370
|
|
141
|
+
if all(not isinstance(x, core.Tracer) for x in invals) and enable_sparsity:
|
|
142
|
+
try:
|
|
143
|
+
with core.new_main(core.EvalTrace, dynamic=True):
|
|
144
|
+
outvals = eqn.primitive.bind(*subfuns, *invals, **bind_params)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logging.warning(
|
|
147
|
+
f"Could not perform operation {eqn.primitive.name} in eager execution despite it only depending on non-input dependent values. "
|
|
148
|
+
"We switch to tracing rather than eager execution. This may impact sparsity propagation.\n"
|
|
149
|
+
f"{e}"
|
|
150
|
+
)
|
|
151
|
+
outvals = eqn.primitive.bind(*subfuns, *invals, **bind_params)
|
|
152
|
+
else:
|
|
153
|
+
outvals = eqn.primitive.bind(*subfuns, *invals, **bind_params)
|
|
154
|
+
elif eqn.primitive.name == "scan":
|
|
155
|
+
outvals = eval_scan(eqn, invals)
|
|
156
|
+
elif eqn.primitive.name == "pjit":
|
|
157
|
+
outvals = eval_pjit(eqn, invals)
|
|
158
|
+
else:
|
|
159
|
+
outvals = eval_laplacian(eqn, invals)
|
|
160
|
+
|
|
161
|
+
# unify output
|
|
162
|
+
if not eqn.primitive.multiple_results:
|
|
163
|
+
outvals = [outvals]
|
|
164
|
+
# save output
|
|
165
|
+
env.write_many(eqn.outvars, outvals) # type: ignore
|
|
166
|
+
return env.read_many(jaxpr.outvars)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def init_forward_laplacian_state(*x: PyTree[Array], sparsity: bool) -> PyTree[FwdLaplArray]:
|
|
171
|
+
"""
|
|
172
|
+
Initialize forward Laplacian state from a PyTree of arrays.
|
|
173
|
+
"""
|
|
174
|
+
x_flat, unravel = ravel(x)
|
|
175
|
+
jac = jtu.tree_map(jnp.ones_like, x)
|
|
176
|
+
jac_idx = unravel(np.arange(x_flat.shape[0]))
|
|
177
|
+
if sparsity:
|
|
178
|
+
jac = jtu.tree_map(lambda j, i: FwdJacobian(j[None], np.array(i)[None]), jac, jac_idx)
|
|
179
|
+
else:
|
|
180
|
+
jac = jax.vmap(unravel)(jnp.eye(len(x_flat)))
|
|
181
|
+
jac = jtu.tree_map(FwdJacobian.from_dense, jac)
|
|
182
|
+
lapl_x = jtu.tree_map(jnp.zeros_like, x)
|
|
183
|
+
return jtu.tree_map(FwdLaplArray, x, jac, lapl_x)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def forward_laplacian(
|
|
187
|
+
fn: Callable[P, PyTree[Array]],
|
|
188
|
+
sparsity_threshold: int | float = 0,
|
|
189
|
+
disable_jit: bool = False
|
|
190
|
+
) -> Callable[P, PyTree[FwdLaplArray]]:
|
|
191
|
+
"""
|
|
192
|
+
This function takes a function and returns a function that computes the Laplacian of the function.
|
|
193
|
+
The returned function will be jitted by default as running it in eager execution will typically be a lot slower.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
- fn: function to compute the Laplacian of
|
|
197
|
+
- sparsity_threshold: threshold for sparsity propagation.
|
|
198
|
+
If the number of non-zero elements in the input is larger than this threshold,we will not propagate sparsity.
|
|
199
|
+
If the value is between 0 and 1, it will be interpreted as a fraction of the total number of elements.
|
|
200
|
+
If the value is larger than 1, it will be interpreted as an absolute number of elements.
|
|
201
|
+
If enabling sparsity, we recommend relatively large values like 0.6 as frequent materializations are slow.
|
|
202
|
+
"""
|
|
203
|
+
def wrapped(*args: P.args, **kwargs: P.kwargs):
|
|
204
|
+
closed_jaxpr = jax.make_jaxpr(fn)(*args, **kwargs)
|
|
205
|
+
flat_args = jtu.tree_leaves(args)
|
|
206
|
+
if 0 < sparsity_threshold < 1:
|
|
207
|
+
threshold = int(sparsity_threshold * sum(x.size for x in flat_args))
|
|
208
|
+
else:
|
|
209
|
+
threshold = int(sparsity_threshold)
|
|
210
|
+
lapl_args = init_forward_laplacian_state(*flat_args, sparsity=threshold > 0)
|
|
211
|
+
out = eval_jaxpr_with_forward_laplacian(
|
|
212
|
+
closed_jaxpr.jaxpr, closed_jaxpr.literals, *lapl_args, sparsity_threshold=threshold
|
|
213
|
+
)
|
|
214
|
+
if len(out) == 1:
|
|
215
|
+
return out[0]
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
if disable_jit:
|
|
219
|
+
return wrapped
|
|
220
|
+
|
|
221
|
+
return jax.jit(wrapped) # type: ignore
|