folx 0.2.3__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: folx
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Forward Laplacian for JAX
5
5
  Home-page: https://github.com/microsoft/folx
6
6
  License: MIT
@@ -28,6 +28,7 @@ Requires-Dist: jax
28
28
  Requires-Dist: jaxlib
29
29
  Requires-Dist: jaxtyping
30
30
  Requires-Dist: numpy
31
+ Requires-Dist: parameterized
31
32
  Requires-Dist: pytest
32
33
  Project-URL: Repository, https://github.com/microsoft/folx
33
34
  Description-Content-Type: text/markdown
folx-0.2.4/folx/ad.py ADDED
@@ -0,0 +1,104 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ import jax.flatten_util as jfu
4
+ import jax.tree_util as jtu
5
+
6
+
7
+ def is_tree_complex(tree):
8
+ leaves = jtu.tree_leaves(tree)
9
+ return any(jnp.iscomplexobj(leaf) for leaf in leaves)
10
+
11
+
12
+ def vjp_rc(fun, *primals: jax.Array):
13
+ def real_fun(*primals):
14
+ return jnp.real(fun(*primals))
15
+
16
+ def imag_fun(*primals):
17
+ return jnp.imag(fun(*primals))
18
+
19
+ _, vjp_r = jax.vjp(real_fun, *primals)
20
+ _, vjp_i = jax.vjp(imag_fun, *primals)
21
+
22
+ def vjp(*tangents: jax.Array):
23
+ real_tangents = jtu.tree_map(jnp.real, tangents)
24
+ imag_tangents = jtu.tree_map(jnp.imag, tangents)
25
+
26
+ # letters: v=vector, j=jacobian, r=real, i=imag
27
+ vr_jr = vjp_r(*real_tangents)
28
+ vi_jr = vjp_r(*imag_tangents)
29
+ vr_ji = vjp_i(*real_tangents)
30
+ vi_ji = vjp_i(*imag_tangents)
31
+
32
+ result = jtu.tree_map(
33
+ lambda vr_jr, vi_jr, vr_ji, vi_ji: vr_jr - vi_ji + 1j * (vr_ji + vi_jr),
34
+ vr_jr,
35
+ vi_jr,
36
+ vr_ji,
37
+ vi_ji,
38
+ )
39
+ return result
40
+
41
+ return vjp
42
+
43
+
44
+ def vjp(fun, *primals: jax.Array):
45
+ out, vjp = jax.vjp(fun, *primals)
46
+ if is_tree_complex(primals):
47
+ if is_tree_complex(out):
48
+ # C -> C
49
+ return vjp
50
+ else:
51
+ # C -> R
52
+ return vjp
53
+ else:
54
+ if is_tree_complex(out):
55
+ # R -> C
56
+ return vjp_rc(fun, *primals)
57
+ else:
58
+ # R -> R
59
+ return vjp
60
+
61
+
62
+ def jacrev(f):
63
+ # Similar to jax.jacrev but works with complex inputs and outputs.
64
+ # A crucial difference is that we do not preserve the structure of the output but
65
+ # always flatten it to a 1D array.
66
+ def jacfun(*primals):
67
+ flat_primals, unravel = jfu.ravel_pytree(primals)
68
+
69
+ def flat_f(x):
70
+ return jfu.ravel_pytree(f(*unravel(x)))[0]
71
+
72
+ out = flat_f(flat_primals)
73
+
74
+ result = jax.vmap(vjp(flat_f, flat_primals))(
75
+ jnp.eye(out.size, dtype=out.dtype)
76
+ )[0]
77
+ result = jax.vmap(unravel, out_axes=0)(result)
78
+ if len(primals) == 1:
79
+ return result[0]
80
+ return result
81
+
82
+ return jacfun
83
+
84
+
85
+ def jacfwd(f):
86
+ # Similar to jax.jacfwd but works with complex inputs and outputs.
87
+ # A crucial difference is that we do not preserve the structure of the input but
88
+ # always flatten it to a 1D array.
89
+ def jacfun(*primals):
90
+ flat_primals, unravel = jfu.ravel_pytree(primals)
91
+
92
+ def jvp_fun(s):
93
+ return jax.jvp(f, primals, unravel(s))[1]
94
+
95
+ eye = jnp.eye(flat_primals.size, dtype=flat_primals.dtype)
96
+ J = jax.vmap(jvp_fun, out_axes=-1)(eye)
97
+ return J
98
+
99
+ return jacfun
100
+
101
+
102
+ def hessian(f):
103
+ # Similar to jax.hessian but works with complex inputs and outputs.
104
+ return jacfwd(jacrev(f))
@@ -151,7 +151,7 @@ class FwdJacobian(NamedTuple):
151
151
  if idx is None:
152
152
  return self.dense_array
153
153
  if len(idx) == 0:
154
- return jnp.zeros((*self.data_shape, 0))
154
+ return jnp.zeros((*self.data_shape, 0), dtype=self.data.dtype)
155
155
  if self.x0_idx is None:
156
156
  return self.data[idx]
157
157
  return self.materialize_for_idx(self.get_index_mask(idx), len(idx))
@@ -225,6 +225,17 @@ class FwdJacobian(NamedTuple):
225
225
  def astype(self, dtype):
226
226
  return FwdJacobian(self.data.astype(dtype), self.x0_idx)
227
227
 
228
+ @property
229
+ def real(self):
230
+ return FwdJacobian(self.data.real, self.x0_idx)
231
+
232
+ @property
233
+ def imag(self):
234
+ return FwdJacobian(self.data.imag, self.x0_idx)
235
+
236
+ def conj(self):
237
+ return FwdJacobian(self.data.conj(), self.x0_idx)
238
+
228
239
 
229
240
  class FwdLaplArray(NamedTuple):
230
241
  """
@@ -265,12 +276,8 @@ class FwdLaplArray(NamedTuple):
265
276
  return FwdLaplArray(self.x, self.jacobian.as_dense, self.laplacian)
266
277
 
267
278
  def astype(self, dtype):
268
- if dtype in (
269
- jnp.float16,
270
- jnp.float32,
271
- jnp.float64,
272
- jnp.complex64,
273
- jnp.complex128,
279
+ if jax.dtypes.issubdtype(dtype, jnp.floating) or jax.dtypes.issubdtype(
280
+ dtype, jnp.complexfloating
274
281
  ):
275
282
  return FwdLaplArray(
276
283
  self.x.astype(dtype),
@@ -280,6 +287,21 @@ class FwdLaplArray(NamedTuple):
280
287
  # If we convert to integer or boolean we drop the derivatives
281
288
  return self.x.astype(dtype)
282
289
 
290
+ @property
291
+ def dtype(self):
292
+ return self.x.dtype
293
+
294
+ @property
295
+ def real(self):
296
+ return FwdLaplArray(self.x.real, self.jacobian.real, self.laplacian.real)
297
+
298
+ @property
299
+ def imag(self):
300
+ return FwdLaplArray(self.x.imag, self.jacobian.imag, self.laplacian.imag)
301
+
302
+ def conj(self):
303
+ return FwdLaplArray(self.x.conj(), self.jacobian.conj(), self.laplacian.conj())
304
+
283
305
 
284
306
  def IS_LPL_ARR(x):
285
307
  return isinstance(x, FwdLaplArray)
@@ -1,6 +1,8 @@
1
1
  import jax
2
2
  import jax.numpy as jnp
3
3
 
4
+ from folx.ad import is_tree_complex
5
+
4
6
  from .api import Array, ExtraArgs, FwdLaplArgs, MergeFn, JAC_DIM
5
7
  from .utils import trace_of_product
6
8
 
@@ -56,4 +58,9 @@ def slogdet_jac_hessian_jac(
56
58
  return None, elementwise(*x)
57
59
 
58
60
  signs, flat_out = jax.lax.scan(scan_wrapper, None, (A_inv, J))[1]
59
- return signs.reshape(leading_dims), flat_out.reshape(leading_dims)
61
+ sign_out, log_abs_out = signs.reshape(leading_dims), flat_out.reshape(leading_dims)
62
+
63
+ if is_tree_complex(A):
64
+ # this is not the real Tr(JHJ^T) but a cached value we use later to compute the Tr(JHJ^T)
65
+ return log_abs_out, log_abs_out.real
66
+ return sign_out, log_abs_out.real
@@ -34,6 +34,7 @@ from .utils import (
34
34
  trace_of_product,
35
35
  vmap_sequences_and_squeeze,
36
36
  )
37
+ from .ad import hessian, jacrev
37
38
 
38
39
 
39
40
  def general_jac_hessian_jac(
@@ -56,8 +57,8 @@ def general_jac_hessian_jac(
56
57
  grad_2d = jnp.concatenate([x.T for x in grads_2d], axis=0)
57
58
  D, K = grad_2d.shape
58
59
  if K > D:
59
- # jax.hessian uses Fwd on Reverse AD
60
- flat_hessian = jax.hessian(flat_fn)(flat_x)
60
+ # Fwd on Reverse AD
61
+ flat_hessian = hessian(flat_fn)(flat_x)
61
62
  flat_out = trace_of_product(flat_hessian, grad_2d @ grad_2d.T)
62
63
  elif D > K:
63
64
  # Directly copmute the trace of tr(HJJ^T)=tr(J^THJ)
@@ -79,7 +80,7 @@ def general_jac_hessian_jac(
79
80
  @functools.partial(jax.vmap, in_axes=-1, out_axes=-1)
80
81
  def hvp(tangent):
81
82
  def jacobian(x):
82
- return jax.jacrev(flat_fn)(x)
83
+ return jacrev(flat_fn)(x)
83
84
 
84
85
  return jax.jvp(jacobian, (flat_x,), (tangent,))[1]
85
86
 
@@ -115,7 +116,9 @@ def off_diag_jac_hessian_jac(
115
116
  return unravel(flat_out)
116
117
 
117
118
 
118
- def mul_jac_hessian_jac(fn: ForwardFn, args: FwdLaplArgs, shared_idx: Array | None):
119
+ def dot_product_jac_hessian_jac(
120
+ fn: ForwardFn, args: FwdLaplArgs, shared_idx: Array | None
121
+ ):
119
122
  # For a dot product we know that the hessian looks like this:
120
123
  # [0, I]
121
124
  # [I, 0]
@@ -321,7 +324,7 @@ def vmapped_jac_hessian_jac(
321
324
  if custom_jac_hessian_jac is not None:
322
325
  result = custom_jac_hessian_jac(args, extra_args, merge, materialize_idx)
323
326
  elif FunctionFlags.MULTIPLICATION in flags:
324
- result = mul_jac_hessian_jac(merged_fn, args, materialize_idx)
327
+ result = dot_product_jac_hessian_jac(merged_fn, args, materialize_idx)
325
328
  elif FunctionFlags.LINEAR_IN_ONE in flags:
326
329
  result = off_diag_jac_hessian_jac(merged_fn, args, materialize_idx)
327
330
  else:
@@ -161,8 +161,8 @@ def eval_jaxpr_with_forward_laplacian(
161
161
 
162
162
  def eval_laplacian(eqn: core.JaxprEqn, invals):
163
163
  subfuns, params = eqn.primitive.get_bind_params(eqn.params)
164
- fn = get_laplacian(eqn.primitive, True)
165
164
  with LoggingPrefix(f'({summarize(eqn.source_info)})'):
165
+ fn = get_laplacian(eqn.primitive, True)
166
166
  return fn(
167
167
  (*subfuns, *invals), params, sparsity_threshold=sparsity_threshold
168
168
  )
@@ -219,12 +219,18 @@ def init_forward_laplacian_state(
219
219
  x_flat, unravel = ravel(x)
220
220
  jac = jtu.tree_map(jnp.ones_like, x)
221
221
  jac_idx = unravel(np.arange(x_flat.shape[0]))
222
+ if jnp.iscomplexobj(x_flat):
223
+ logging.info(
224
+ '[folx] Found complex input. This is not well supported, results might be wrong.'
225
+ )
222
226
  if sparsity:
223
227
  jac = jtu.tree_map(
224
- lambda j, i: FwdJacobian(j[None], np.array(i)[None]), jac, jac_idx
228
+ lambda j, i: FwdJacobian(j.astype(x_flat.dtype)[None], np.array(i)[None]),
229
+ jac,
230
+ jac_idx,
225
231
  )
226
232
  else:
227
- jac = jax.vmap(unravel)(jnp.eye(len(x_flat)))
233
+ jac = jax.vmap(unravel)(jnp.eye(len(x_flat), dtype=x_flat.dtype))
228
234
  jac = jtu.tree_map(FwdJacobian.from_dense, jac)
229
235
  lapl_x = jtu.tree_map(jnp.zeros_like, x)
230
236
  return jtu.tree_map(FwdLaplArray, x, jac, lapl_x)
@@ -29,6 +29,7 @@ from .utils import (
29
29
  get_jacobian_for_reduction,
30
30
  np_concatenate_brdcast,
31
31
  )
32
+ from .ad import vjp
32
33
 
33
34
  R = TypeVar('R', bound=PyTree[Array])
34
35
 
@@ -115,7 +116,7 @@ def sparse_diag_jvp(
115
116
  ):
116
117
  # If we have elementwise functions, we can just compute the full jacobian and
117
118
  # do the operations a bit faster.
118
- jac = jax.grad(lambda x: jnp.sum(fwd(x)))(laplace_args.x[0]) # type: ignore
119
+ jac = vjp(fwd, laplace_args.x[0])(jnp.ones_like(y))[0]
119
120
  grad_y = jac * laplace_args.jacobian[0].data
120
121
  lapl_y = jac * laplace_args.laplacian[0]
121
122
  else:
@@ -373,7 +374,7 @@ def dense_elementwise_jvp(
373
374
  if y.shape != laplace_args.x[0].shape:
374
375
  return dense_split_jvp(fwd, laplace_args)
375
376
 
376
- jac = jax.grad(lambda x: jnp.sum(fwd(x)))(laplace_args.x[0]) # type: ignore
377
+ jac = vjp(fwd, laplace_args.x[0])(jnp.ones_like(y))[0]
377
378
  grad_y = jac * laplace_args.dense_jacobian[0]
378
379
  lapl_y = jac * laplace_args.laplacian[0]
379
380
  return y, grad_y, lapl_y
@@ -7,16 +7,23 @@ import jax.numpy as jnp
7
7
  import jax.tree_util as jtu
8
8
  from jax.core import Primitive
9
9
 
10
+ from folx.ad import is_tree_complex
11
+
10
12
  from .api import (
11
13
  Array,
12
14
  ArrayOrFwdLaplArray,
13
15
  ForwardLaplacian,
14
16
  FunctionFlags,
17
+ FwdJacobian,
15
18
  FwdLaplArray,
16
19
  PyTree,
17
20
  )
18
21
  from .custom_hessian import slogdet_jac_hessian_jac
19
- from .wrapper import wrap_forward_laplacian, warp_without_fwd_laplacian
22
+ from .wrapper import (
23
+ wrap_elementwise,
24
+ wrap_forward_laplacian,
25
+ warp_without_fwd_laplacian,
26
+ )
20
27
 
21
28
  R = TypeVar('R', bound=PyTree[Array])
22
29
  P = ParamSpec('P')
@@ -119,7 +126,10 @@ def dtype_conversion(
119
126
  def slogdet(x):
120
127
  # We only need this custom slog det to avoid a jax bug
121
128
  # https://github.com/google/jax/issues/17379
122
- return jnp.linalg.slogdet(x)
129
+ # We explictily decompose this here as newer version will return
130
+ # SlogDetResult which is a NamedTuple and does not combine nicely with regular tuples.
131
+ sign, logdet = jnp.linalg.slogdet(x)
132
+ return sign, logdet
123
133
 
124
134
 
125
135
  def slogdet_jvp(primals, tangents):
@@ -136,10 +146,18 @@ def slogdet_jvp(primals, tangents):
136
146
 
137
147
  jacobians = jnp.linalg.inv(primals)
138
148
 
139
- def custom_jvp(jacobian, tangent):
140
- return (jnp.zeros(()), jnp.vdot(jacobian.T, tangent))
149
+ def custom_jvp(jacobian, tangent, sign):
150
+ jac_dot_tangent = jnp.vdot(jacobian.T.conj(), tangent)
151
+ if jac_dot_tangent.dtype in (jnp.complex64, jnp.complex128):
152
+ # this is not the real jvp but a cached value to ease the Tr(JHJ^T) computation
153
+ sign_jvp = jac_dot_tangent
154
+ log_det_jvp = jac_dot_tangent.real
155
+ else:
156
+ sign_jvp = jnp.zeros(())
157
+ log_det_jvp = jac_dot_tangent
158
+ return (sign_jvp, log_det_jvp)
141
159
 
142
- y_tangent = jax.vmap(custom_jvp)(jacobians, tangents)
160
+ y_tangent = jax.vmap(custom_jvp)(jacobians, tangents, sign)
143
161
 
144
162
  y, y_tangent = jtu.tree_map(lambda x: x.reshape(*batch_shape), (y, y_tangent))
145
163
  return y, y_tangent
@@ -158,15 +176,44 @@ def slogdet_wrapper(
158
176
  )
159
177
  sign, logdet = fwd_lapl_fn(x, {}, sparsity_threshold=0)
160
178
  # Remove the jacobian of the sign
161
- sign = warp_without_fwd_laplacian(lambda x: x)((sign,), {}, sparsity_threshold=0)
179
+ if jax.dtypes.issubdtype(sign.dtype, jnp.complexfloating):
180
+ sign_jac = sign.jacobian.data
181
+ sign_jac_flat = sign_jac.reshape(-1, *sign.shape).imag
182
+ sign_jac_dot = jnp.einsum('i...,i...->...', sign_jac_flat, sign_jac_flat)
183
+ sign = FwdLaplArray(
184
+ sign.x,
185
+ FwdJacobian(1.0j * sign.x * sign_jac.imag, x0_idx=sign.jacobian.x0_idx),
186
+ sign.x * (1.0j * sign.laplacian.imag - sign_jac_dot),
187
+ )
188
+ if jax.dtypes.issubdtype(sign.dtype, jnp.floating):
189
+ sign = sign.x
162
190
  return sign, logdet
163
191
 
164
192
 
193
+ def abs_wrapper(
194
+ x: tuple[ArrayOrFwdLaplArray],
195
+ kwargs: dict[str, Any],
196
+ sparsity_threshold: int,
197
+ ):
198
+ if not is_tree_complex(x):
199
+ return wrap_forward_laplacian(
200
+ jax.lax.abs, flags=FunctionFlags.LINEAR, in_axes=()
201
+ )(x, kwargs, sparsity_threshold)
202
+ import folx # we must import folx here to avoid circular imports
203
+
204
+ return folx.forward_laplacian(
205
+ lambda x: jnp.sqrt((x * x.conj()).real),
206
+ sparsity_threshold=sparsity_threshold,
207
+ disable_jit=True,
208
+ )(x[0])
209
+
210
+
165
211
  _LAPLACE_FN_REGISTRY: dict[Primitive | str, ForwardLaplacian] = {
212
+ jax.lax.conj_p: wrap_elementwise(jnp.conj),
213
+ jax.lax.imag_p: wrap_elementwise(jnp.imag),
214
+ jax.lax.real_p: wrap_elementwise(jnp.real),
166
215
  jax.lax.dot_general_p: dot_general,
167
- jax.lax.abs_p: wrap_forward_laplacian(
168
- jax.lax.abs, flags=FunctionFlags.LINEAR, in_axes=()
169
- ),
216
+ jax.lax.abs_p: abs_wrapper,
170
217
  jax.lax.neg_p: wrap_forward_laplacian(
171
218
  jax.lax.neg, flags=FunctionFlags.LINEAR, in_axes=()
172
219
  ),
@@ -266,11 +313,11 @@ _LAPLACE_FN_REGISTRY: dict[Primitive | str, ForwardLaplacian] = {
266
313
  flags=FunctionFlags.INDEXING | FunctionFlags.SCATTER,
267
314
  name='scatter',
268
315
  ),
269
- jax.lax.scatter_add_p: wrap_forward_laplacian(
270
- jax.lax.scatter_add_p.bind,
271
- flags=FunctionFlags.LINEAR | FunctionFlags.SCATTER,
272
- name='scatter_add',
273
- ),
316
+ # jax.lax.scatter_add_p: wrap_forward_laplacian(
317
+ # jax.lax.scatter_add_p.bind,
318
+ # flags=FunctionFlags.LINEAR | FunctionFlags.SCATTER,
319
+ # name='scatter_add',
320
+ # ),
274
321
  jax.lax.stop_gradient_p: warp_without_fwd_laplacian(jax.lax.stop_gradient),
275
322
  jax.lax.eq_p: warp_without_fwd_laplacian(jax.lax.eq),
276
323
  jax.lax.lt_p: warp_without_fwd_laplacian(jax.lax.lt),
@@ -14,6 +14,7 @@ from .api import (
14
14
  ForwardLaplacian,
15
15
  ForwardLaplacianFns,
16
16
  FunctionFlags,
17
+ FwdJacobian,
17
18
  FwdLaplArgs,
18
19
  FwdLaplArray,
19
20
  MergeFn,
@@ -120,7 +121,12 @@ def wrap_forward_laplacian(
120
121
  lapl_y = tree_add(
121
122
  lapl_y, lapl_fns.jac_hessian_jac_trace(laplace_args, sparsity_threshold)
122
123
  )
123
- return jax.tree_util.tree_map(FwdLaplArray, y, grad_y, lapl_y)
124
+ return jax.tree_util.tree_map(
125
+ lambda x, jac, lapl: FwdLaplArray(x, jac, lapl).astype(x.dtype),
126
+ y,
127
+ grad_y,
128
+ lapl_y,
129
+ )
124
130
 
125
131
  return new_fn
126
132
 
@@ -139,3 +145,25 @@ def warp_without_fwd_laplacian(fn) -> ForwardLaplacian:
139
145
  return fn(*args, **kwargs)
140
146
 
141
147
  return wrapped
148
+
149
+
150
+ def wrap_elementwise(fn) -> ForwardLaplacian:
151
+ """
152
+ Decorator that applies the function directly to the data, jacobian and laplacian.
153
+ """
154
+
155
+ def wrapped(args, kwargs, sparsity_threshold: int):
156
+ assert len(args) == 1
157
+
158
+ def inner(x: ArrayOrFwdLaplArray):
159
+ if isinstance(x, FwdLaplArray):
160
+ return FwdLaplArray(
161
+ fn(x.x),
162
+ FwdJacobian(fn(x.jacobian.data), x.jacobian.x0_idx),
163
+ fn(x.laplacian),
164
+ )
165
+ return fn(x)
166
+
167
+ return inner(args[0])
168
+
169
+ return wrapped
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = 'folx'
3
- version = '0.2.3'
3
+ version = '0.2.4'
4
4
  description = 'Forward Laplacian for JAX'
5
5
  authors = [
6
6
  "Nicholas Gao <n.gao@tum.de>",
@@ -37,6 +37,7 @@ jaxlib = "*"
37
37
  jaxtyping = "*"
38
38
  numpy = "*"
39
39
  pytest = "*"
40
+ parameterized = "*"
40
41
 
41
42
  [build-system]
42
43
  requires = ["poetry-core>=1.0.0"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes