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/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .interpreter import forward_laplacian
|
|
2
|
+
from .operators import (
|
|
3
|
+
ForwardLaplacianOperator,
|
|
4
|
+
LaplacianOperator,
|
|
5
|
+
LoopLaplacianOperator,
|
|
6
|
+
ParallelLaplacianOperator,
|
|
7
|
+
)
|
|
8
|
+
from .vmap import batched_vmap
|
|
9
|
+
from .wrapper import wrap_forward_laplacian, warp_without_fwd_laplacian
|
|
10
|
+
from .wrapped_functions import deregister_function, register_function
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"batched_vmap",
|
|
15
|
+
"forward_laplacian",
|
|
16
|
+
"ForwardLaplacianOperator",
|
|
17
|
+
"LaplacianOperator",
|
|
18
|
+
"LoopLaplacianOperator",
|
|
19
|
+
"ParallelLaplacianOperator",
|
|
20
|
+
"wrap_forward_laplacian",
|
|
21
|
+
"warp_without_fwd_laplacian",
|
|
22
|
+
"deregister_function",
|
|
23
|
+
"register_function",
|
|
24
|
+
]
|
folx/api.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
from enum import IntFlag
|
|
2
|
+
from typing import Any, Callable, NamedTuple, Protocol, TypeAlias, TypeVar
|
|
3
|
+
|
|
4
|
+
import jax
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
from jax import core
|
|
9
|
+
from jaxtyping import Array, PyTree
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=PyTree[Array])
|
|
12
|
+
R = TypeVar("R", bound=PyTree[Array])
|
|
13
|
+
|
|
14
|
+
ExtraArgs = tuple[Array, ...]
|
|
15
|
+
Arrays = tuple[Array, ...]
|
|
16
|
+
|
|
17
|
+
JAC_DIM = 0 # should be either 0 or -1. TODO: switching is not support.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FwdJacobian(NamedTuple):
|
|
21
|
+
"""
|
|
22
|
+
Represents the Jacobian of a tensor with respect to the function's initial arguments.
|
|
23
|
+
The Jacobian may either be sparse or dense. So, for a function f: R^n -> R^m, the
|
|
24
|
+
Jacobian is an n x m matrix. If the Jacobian is dense, we also store it as such.
|
|
25
|
+
However, it might be that the Jacobian is sparse, e.g., if f(x)=x. In such a case
|
|
26
|
+
the Jacobian tensor is mostly sparse along the last dimension. Instead of explicitly
|
|
27
|
+
storing all of the zeros, we store the non-zero elements and to which of the n inputs
|
|
28
|
+
it depends on. So, instead of storing a n xm matrix, we store a k x m matrix, where
|
|
29
|
+
k is maximum number of elements any element in m depends on. Additionally we store
|
|
30
|
+
the index tensor which has shape k x m and contains integer indices between 0 and n.
|
|
31
|
+
|
|
32
|
+
Note that we always compute sparsity patterns at compile time for efficiency reasons.
|
|
33
|
+
This means that x0_idx is a numpy array and not a jax array.
|
|
34
|
+
|
|
35
|
+
A few notes:
|
|
36
|
+
- If sparsity patterns are modified in jax functions, we have to disable omnistaging.
|
|
37
|
+
- Materializing the dense array is expensive and should be avoided if possible.
|
|
38
|
+
- As we do not explicitly keep track of m, it might be that two dense Jacobians differ
|
|
39
|
+
in the last dimension. This is not a problem as we can always pad the smaller one.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
data: Array # shape (k, ...)
|
|
43
|
+
x0_idx: npt.NDArray[np.int32] | None = None # integer array of same shape as data
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def weak(self) -> bool:
|
|
47
|
+
return self.x0_idx is not None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def unique_idx(self):
|
|
51
|
+
"""
|
|
52
|
+
Returns an array containing the indices in that the Jacobian depends on.
|
|
53
|
+
"""
|
|
54
|
+
if self.x0_idx is not None:
|
|
55
|
+
return np.unique(self.x0_idx)
|
|
56
|
+
else:
|
|
57
|
+
return np.arange(self.data.shape[JAC_DIM])
|
|
58
|
+
|
|
59
|
+
def materialize_for_idx(self, idx, max_idx: int | None = None):
|
|
60
|
+
"""
|
|
61
|
+
Materializes the Jacobian for the given indices. If max_idx is not None, the
|
|
62
|
+
resulting Jacobian will have shape (..., max_idx). Otherwise, it will have
|
|
63
|
+
shape (..., max(idx) + 1). The latter only works if the idx tensor is a numpy
|
|
64
|
+
array.
|
|
65
|
+
"""
|
|
66
|
+
assert self.weak
|
|
67
|
+
from .utils import broadcast_except, compact_repeated_dims_except
|
|
68
|
+
|
|
69
|
+
# If we have static indices, we can do some optimization as we can statically
|
|
70
|
+
# analyze the indices on whether they are identical along certain axes.
|
|
71
|
+
# If so we can reduce these dimensions to enable coalesced memory access.
|
|
72
|
+
if isinstance(idx, np.ndarray):
|
|
73
|
+
idx = compact_repeated_dims_except(idx, axis=JAC_DIM)[0]
|
|
74
|
+
|
|
75
|
+
# Broadcast to ensure shape compatibility
|
|
76
|
+
x, idx = broadcast_except((self.data, idx), axis=JAC_DIM)
|
|
77
|
+
|
|
78
|
+
# If we just broadcasted idx we should reduce these dims again.
|
|
79
|
+
if isinstance(idx, np.ndarray):
|
|
80
|
+
idx, copied_axes = compact_repeated_dims_except(idx, axis=JAC_DIM)
|
|
81
|
+
else:
|
|
82
|
+
copied_axes = ()
|
|
83
|
+
|
|
84
|
+
indexed_axes = np.setdiff1d(np.arange(idx.ndim), (*copied_axes, JAC_DIM))
|
|
85
|
+
new_order = (*indexed_axes, JAC_DIM, *copied_axes)
|
|
86
|
+
inv_order = tuple(np.argsort(new_order))
|
|
87
|
+
|
|
88
|
+
x = jnp.transpose(x, new_order)
|
|
89
|
+
idx = np.transpose(idx, new_order)
|
|
90
|
+
idx = idx[(..., *([0] * len(copied_axes)))] # remove copied axes
|
|
91
|
+
|
|
92
|
+
x_shape = (*x.shape[: len(indexed_axes)], -1, *x.shape[len(indexed_axes) + 1 :])
|
|
93
|
+
x = x.reshape(
|
|
94
|
+
np.prod(x.shape[: len(indexed_axes)], dtype=int), *x.shape[len(indexed_axes) :]
|
|
95
|
+
)
|
|
96
|
+
idx = idx.reshape(
|
|
97
|
+
np.prod(idx.shape[: len(indexed_axes)], dtype=int), *idx.shape[len(indexed_axes) :]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@jax.vmap
|
|
101
|
+
def aggregate(x, indices):
|
|
102
|
+
return jax.ops.segment_sum(x, indices, max_idx)
|
|
103
|
+
|
|
104
|
+
result = aggregate(x, idx).reshape(x_shape)
|
|
105
|
+
result = jnp.transpose(result, inv_order)
|
|
106
|
+
return result
|
|
107
|
+
|
|
108
|
+
def get_index_mask(self, outputs):
|
|
109
|
+
"""
|
|
110
|
+
Returns the index mask for the given outputs. The index mask is an array of
|
|
111
|
+
shape broadcast(outputs, x0_idx) that contains the index of the output that
|
|
112
|
+
each element in the Jacobian depends on. If an element does not depend on any
|
|
113
|
+
output, the index is set to -1.
|
|
114
|
+
"""
|
|
115
|
+
assert self.weak
|
|
116
|
+
from .utils import broadcast_except
|
|
117
|
+
|
|
118
|
+
outputs, mask = broadcast_except((outputs, self.mask), axis=JAC_DIM)
|
|
119
|
+
|
|
120
|
+
og_shape = mask.shape[1:]
|
|
121
|
+
flat_mask = mask.reshape(-1, np.prod(og_shape, dtype=int)).T
|
|
122
|
+
flat_outputs = outputs.reshape(-1, np.prod(og_shape, dtype=int)).T
|
|
123
|
+
|
|
124
|
+
@jax.vmap
|
|
125
|
+
def get_indices(mask, out_mask):
|
|
126
|
+
matching = mask[..., None] == out_mask
|
|
127
|
+
indices = jnp.argmax(matching, axis=-1)
|
|
128
|
+
indices = jnp.where(jnp.any(matching, axis=-1), indices, -1)
|
|
129
|
+
return indices
|
|
130
|
+
|
|
131
|
+
if isinstance(outputs, np.ndarray):
|
|
132
|
+
with core.new_main(core.EvalTrace, dynamic=True):
|
|
133
|
+
result = np.asarray(get_indices(flat_mask, flat_outputs), dtype=int).T
|
|
134
|
+
else:
|
|
135
|
+
result = get_indices(flat_mask, flat_outputs).T
|
|
136
|
+
return result.reshape(mask.shape)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def data_shape(self):
|
|
140
|
+
return tuple(self.data.shape[i] for i in range(self.data.ndim) if i != JAC_DIM)
|
|
141
|
+
|
|
142
|
+
def construct_jac_for(self, idx):
|
|
143
|
+
"""
|
|
144
|
+
Constructs the Jacobian for the given indices. If the Jacobian is dense, this
|
|
145
|
+
is just a simple indexing operation. If the Jacobian is sparse, we have to
|
|
146
|
+
materialize it first.
|
|
147
|
+
If idx is None we return the dense matrix.
|
|
148
|
+
"""
|
|
149
|
+
if idx is None:
|
|
150
|
+
return self.dense_array
|
|
151
|
+
if len(idx) == 0:
|
|
152
|
+
return jnp.zeros((*self.data_shape, 0))
|
|
153
|
+
if self.x0_idx is None:
|
|
154
|
+
return self.data[idx]
|
|
155
|
+
return self.materialize_for_idx(self.get_index_mask(idx), len(idx))
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def dense_array(self) -> Array:
|
|
159
|
+
"""
|
|
160
|
+
Returns the dense Jacobian. If the Jacobian is sparse, we materialize it first.
|
|
161
|
+
"""
|
|
162
|
+
if self.x0_idx is None:
|
|
163
|
+
return self.data
|
|
164
|
+
ext_idx = (..., *((None,) * len(self.data_shape))) # this is for mypy
|
|
165
|
+
return self.construct_jac_for(np.arange(self.max_n + 1)[ext_idx])
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def max_n(self) -> int:
|
|
169
|
+
if self.x0_idx is not None:
|
|
170
|
+
return int(np.max(self.x0_idx))
|
|
171
|
+
return self.data.shape[JAC_DIM]
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def as_dense(self):
|
|
175
|
+
return FwdJacobian.from_dense(self.dense_array)
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def dense_or_sparse(self) -> Array:
|
|
179
|
+
return self.data
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def sparse(self) -> Array:
|
|
183
|
+
assert self.weak
|
|
184
|
+
return self.data
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def mask(self) -> np.ndarray:
|
|
188
|
+
if self.x0_idx is not None:
|
|
189
|
+
return self.x0_idx
|
|
190
|
+
else:
|
|
191
|
+
ext_idx = (..., *((None,) * len(self.data_shape))) # this is for mypy
|
|
192
|
+
return (
|
|
193
|
+
np.ones(self.data.shape, dtype=np.int32)
|
|
194
|
+
* np.arange(self.data.shape[JAC_DIM], dtype=np.int32)[ext_idx]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def ndim(self) -> int:
|
|
199
|
+
return self.data.ndim
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_dense(cls, array):
|
|
203
|
+
return cls(array, None)
|
|
204
|
+
|
|
205
|
+
def __add__(self, other):
|
|
206
|
+
assert isinstance(other, FwdJacobian)
|
|
207
|
+
# If any is not weak we just add dense jacobians
|
|
208
|
+
if not self.weak or not other.weak:
|
|
209
|
+
from .utils import add_jacobians
|
|
210
|
+
return FwdJacobian(add_jacobians(self.as_dense.data, other.as_dense.data))
|
|
211
|
+
# If both are weak, we can keep them sparse
|
|
212
|
+
if (other.x0_idx == self.x0_idx).all():
|
|
213
|
+
return FwdJacobian(self.data + other.data, np.broadcast_arrays(self.x0_idx, other.x0_idx)[0]) # type: ignore
|
|
214
|
+
return FwdJacobian(
|
|
215
|
+
data=jnp.concatenate((self.data, other.data), axis=JAC_DIM),
|
|
216
|
+
x0_idx=np.concatenate((self.x0_idx, other.x0_idx), axis=JAC_DIM), # type: ignore
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class FwdLaplArray(NamedTuple):
|
|
221
|
+
"""
|
|
222
|
+
Represents a triplet of a tensor, its Jacobian and its Laplacian with respect to
|
|
223
|
+
the function's initial arguments.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
x: Array
|
|
227
|
+
jacobian: FwdJacobian
|
|
228
|
+
laplacian: Array
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def shape(self):
|
|
232
|
+
return self.x.shape
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def ndim(self):
|
|
236
|
+
return self.x.ndim
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def dense_jacobian(self):
|
|
240
|
+
return self.jacobian.dense_array
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def is_jacobian_weak(self):
|
|
244
|
+
return self.jacobian.weak
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def sparse_jacobian(self):
|
|
248
|
+
return self.jacobian.sparse
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def jacobian_mask(self):
|
|
252
|
+
return self.jacobian.mask
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def dense(self):
|
|
256
|
+
return FwdLaplArray(self.x, self.jacobian.as_dense, self.laplacian)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def IS_LPL_ARR(x):
|
|
260
|
+
return isinstance(x, FwdLaplArray)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def IS_LEAF(x):
|
|
264
|
+
return isinstance(x, (FwdLaplArray, Array))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
FwdLaplArrays = tuple[FwdLaplArray, ...]
|
|
268
|
+
ArrayOrFwdLaplArray: TypeAlias = Array | FwdLaplArray
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class FwdLaplArgs(NamedTuple):
|
|
272
|
+
"""
|
|
273
|
+
Utility class that represents a tuple of tensors, their Jacobians and their
|
|
274
|
+
Laplacians with respect to the function's initial arguments.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
arrays: FwdLaplArrays
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def x(self) -> Arrays:
|
|
281
|
+
return tuple(a.x for a in self.arrays)
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def jacobian(self) -> tuple[FwdJacobian, ...]:
|
|
285
|
+
return tuple(a.jacobian for a in self.arrays)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def dense_jacobian(self) -> Arrays:
|
|
289
|
+
return tuple(a.dense_jacobian for a in self.arrays)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def sparse_jacobian(self) -> Arrays:
|
|
293
|
+
return tuple(a.sparse_jacobian for a in self.arrays)
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def jacobian_mask(self):
|
|
297
|
+
return tuple(a.jacobian_mask for a in self.arrays)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def all_jacobian_weak(self) -> bool:
|
|
301
|
+
return all(a.is_jacobian_weak for a in self.arrays)
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def any_jacobian_weak(self) -> bool:
|
|
305
|
+
return any(a.is_jacobian_weak for a in self.arrays)
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def dense(self):
|
|
309
|
+
return FwdLaplArgs(tuple(a.dense for a in self.arrays))
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def laplacian(self) -> Arrays:
|
|
313
|
+
return tuple(a.laplacian for a in self.arrays)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def one_hot_sparse_jacobian(self):
|
|
317
|
+
jacobians = self.sparse_jacobian
|
|
318
|
+
return tuple(
|
|
319
|
+
tuple(
|
|
320
|
+
jacobians[j]
|
|
321
|
+
if i == j
|
|
322
|
+
else jnp.zeros((jacobians[i].shape[0], *jacobians[j].shape[1:]), dtype=jacobians[j].dtype)
|
|
323
|
+
for j in range(len(jacobians))
|
|
324
|
+
)
|
|
325
|
+
for i in range(len(jacobians))
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def __len__(self) -> int:
|
|
329
|
+
return len(self.arrays)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
Axes = Any
|
|
333
|
+
|
|
334
|
+
ArrayOrArrays: TypeAlias = Array | tuple[Array, ...] | list[Array]
|
|
335
|
+
ForwardFn = Callable[..., ArrayOrArrays]
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class MergeFn(Protocol):
|
|
339
|
+
def __call__(self, args: Arrays, extra: ExtraArgs) -> Arrays:
|
|
340
|
+
...
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ForwardLaplacianFns(NamedTuple):
|
|
344
|
+
forward: ForwardFn
|
|
345
|
+
jvp: Callable[[FwdLaplArgs, dict[str, Any]], tuple[ArrayOrArrays, FwdJacobian, ArrayOrArrays]]
|
|
346
|
+
jac_hessian_jac_trace: Callable[[FwdLaplArgs, int], ArrayOrArrays]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class JvpFn(Protocol):
|
|
350
|
+
def __call__(self, primals: Arrays, tangents: Arrays) -> tuple[Array, Array]:
|
|
351
|
+
...
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class CustomTraceJacHessianJac(Protocol):
|
|
355
|
+
def __call__(self, args: FwdLaplArgs, extra_args: ExtraArgs, merge: MergeFn, materialize_idx: Array) -> PyTree[Array]:
|
|
356
|
+
...
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class ForwardLaplacian(Protocol):
|
|
360
|
+
def __call__(self, *args: ArrayOrFwdLaplArray, sparsity_threshold: int , **kwargs) -> PyTree[ArrayOrFwdLaplArray]:
|
|
361
|
+
...
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class FunctionFlags(IntFlag):
|
|
365
|
+
GENERAL = 0
|
|
366
|
+
LINEAR_IN_FIRST = 1
|
|
367
|
+
LINEAR_IN_ONE = 2 | LINEAR_IN_FIRST
|
|
368
|
+
LINEAR = 4 | LINEAR_IN_ONE
|
|
369
|
+
REDUCTION = 8
|
|
370
|
+
MULTIPLICATION = 16 | LINEAR_IN_ONE
|
|
371
|
+
DOT_PRODUCT = 32 | REDUCTION | MULTIPLICATION
|
|
372
|
+
INDEXING = 64 | LINEAR
|
|
373
|
+
SCATTER = 128
|
|
374
|
+
JOIN_JVP = 256
|
folx/custom_hessian.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import jax
|
|
2
|
+
import jax.numpy as jnp
|
|
3
|
+
|
|
4
|
+
from .api import (
|
|
5
|
+
Array,
|
|
6
|
+
ExtraArgs,
|
|
7
|
+
FwdLaplArgs,
|
|
8
|
+
MergeFn,
|
|
9
|
+
JAC_DIM
|
|
10
|
+
)
|
|
11
|
+
from .utils import trace_of_product
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def slogdet_jac_hessian_jac(args: FwdLaplArgs, extra_args: ExtraArgs, merge: MergeFn, materialize_idx: Array | None):
|
|
15
|
+
# For slogdet we know how to compute the determinant faster.
|
|
16
|
+
# We can use the fact that the jacobian of logdet is A^-1.
|
|
17
|
+
# Thus, the hessian is A^-1 (x) A^-T. Where (x) is the kronecker product.
|
|
18
|
+
# We can now reformulate this to (A^-1 (x) I)(A^-1 (x) I)^T.
|
|
19
|
+
# If one wants to compute the product vec(M)(A^-1 (x) I), this can be
|
|
20
|
+
# efficiently evaluated as vec(MA^-1). As we multiply the Hessian from
|
|
21
|
+
# both sides with the jacobian tr(JHJ^T), this can be efficiently be done
|
|
22
|
+
# as tr(J@A^-1 @ A^-1^T@J^T) where the inner @ is the outer product.
|
|
23
|
+
assert len(args.x) == 1
|
|
24
|
+
A = args.x[0]
|
|
25
|
+
A_inv = jnp.linalg.inv(A)
|
|
26
|
+
J = args.jacobian[0].construct_jac_for(materialize_idx)
|
|
27
|
+
J = jnp.moveaxis(J, JAC_DIM, -1)
|
|
28
|
+
leading_dims = A.shape[:-2]
|
|
29
|
+
x0_dim = J.shape[-1]
|
|
30
|
+
|
|
31
|
+
def elementwise(A_inv, J):
|
|
32
|
+
# Naive implementation
|
|
33
|
+
# @functools.partial(jax.vmap, in_axes=(-1, None), out_axes=-1)
|
|
34
|
+
# @functools.partial(jax.vmap, in_axes=(None, -1), out_axes=-1)
|
|
35
|
+
# def inner(v1, v2):
|
|
36
|
+
# A_inv_v = A_inv@v1
|
|
37
|
+
# v_A_inv = v2.T@A_inv.T
|
|
38
|
+
# return -v_A_inv.reshape(-1)@A_inv_v.reshape(-1)
|
|
39
|
+
# vHv = inner(J, J)
|
|
40
|
+
# trace = jnp.trace(vHv)
|
|
41
|
+
|
|
42
|
+
# We can do better and compute the trace more efficiently.
|
|
43
|
+
A_inv_J = jnp.einsum("ij,jdk->idk", A_inv, J)
|
|
44
|
+
trace = -trace_of_product(
|
|
45
|
+
jnp.transpose(A_inv_J, (1, 0, 2)).reshape(-1, x0_dim), A_inv_J.reshape(-1, x0_dim)
|
|
46
|
+
)
|
|
47
|
+
return jnp.zeros(()), trace
|
|
48
|
+
|
|
49
|
+
A_inv = A_inv.reshape(-1, *A.shape[-2:])
|
|
50
|
+
J = J.reshape(-1, *J.shape[-3:])
|
|
51
|
+
|
|
52
|
+
# We can either use vmap or scan. Scan is slightly slower but uses less memory.
|
|
53
|
+
# Here we assume that we will in general encounter larger determinants rather than many.
|
|
54
|
+
# signs, flat_out = jax.vmap(elementwise)(A_inv, J)
|
|
55
|
+
def scan_wrapper(_, x):
|
|
56
|
+
return None, elementwise(*x)
|
|
57
|
+
|
|
58
|
+
signs, flat_out = jax.lax.scan(scan_wrapper, None, (A_inv, J))[1]
|
|
59
|
+
return signs.reshape(leading_dims), flat_out.reshape(leading_dims)
|