Skip to content
This repository was archived by the owner on Nov 7, 2024. It is now read-only.

Commit db81bc0

Browse files
alewisChase Roberts
andauthored
Jax gmres (#707)
* Add linalg directory and initialization code * Delete linalg for merge * Jax GMRES and tests * Correct typing of ShapedArray * Correct typing in gmres.py * Refactor GMRES to use existing Arnoldi * Fix obsolete test from merge * Cleanups for PR * Correct docstring * Fix caching Co-authored-by: Chase Roberts <chaseriley@google.com>
1 parent 04d4e46 commit db81bc0

3 files changed

Lines changed: 464 additions & 120 deletions

File tree

tensornetwork/backends/jax/jax_backend.py

Lines changed: 183 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
# pylint: disable=abstract-method
2525

2626
_CACHED_MATVECS = {}
27+
_CACHED_FUNCTIONS = {}
2728

2829

2930
class JaxBackend(abstract_backend.AbstractBackend):
@@ -243,15 +244,15 @@ def eigs(self,
243244
which: Text = 'LR',
244245
maxiter: int = 20) -> Tuple[Tensor, List]:
245246
"""
246-
Implicitly restarted Arnoldi method for finding the lowest
247-
eigenvector-eigenvalue pairs of a linear operator `A`.
247+
Implicitly restarted Arnoldi method for finding the lowest
248+
eigenvector-eigenvalue pairs of a linear operator `A`.
248249
`A` is a function implementing the matrix-vector
249-
product.
250+
product.
250251
251252
WARNING: This routine uses jax.jit to reduce runtimes. jitting is triggered
252-
at the first invocation of `eigs`, and on any subsequent calls
253-
if the python `id` of `A` changes, even if the formal definition of `A`
254-
stays the same.
253+
at the first invocation of `eigs`, and on any subsequent calls
254+
if the python `id` of `A` changes, even if the formal definition of `A`
255+
stays the same.
255256
Example: the following will jit once at the beginning, and then never again:
256257
257258
```python
@@ -265,7 +266,7 @@ def A(H,x):
265266
res = eigs(A, [H],x) #jitting is triggerd only at `n=0`
266267
```
267268
268-
The following code triggers jitting at every iteration, which
269+
The following code triggers jitting at every iteration, which
269270
results in considerably reduced performance
270271
271272
```python
@@ -278,7 +279,7 @@ def A(H,x):
278279
x = jax.np.array(np.random.rand(10,10))
279280
res = eigs(A, [H],x) #jitting is triggerd at every step `n`
280281
```
281-
282+
282283
Args:
283284
A: A (sparse) implementation of a linear operator.
284285
Call signature of `A` is `res = A(vector, *args)`, where `vector`
@@ -293,13 +294,13 @@ def A(H,x):
293294
num_krylov_vecs: The number of iterations (number of krylov vectors).
294295
numeig: The number of eigenvector-eigenvalue pairs to be computed.
295296
tol: The desired precision of the eigenvalues. For the jax backend
296-
this has currently no effect, and precision of eigenvalues is not
297+
this has currently no effect, and precision of eigenvalues is not
297298
guaranteed. This feature may be added at a later point. To increase
298299
precision the caller can either increase `maxiter` or `num_krylov_vecs`.
299-
which: Flag for targetting different types of eigenvalues. Currently
300-
supported are `which = 'LR'` (larges real part) and `which = 'LM'`
300+
which: Flag for targetting different types of eigenvalues. Currently
301+
supported are `which = 'LR'` (larges real part) and `which = 'LM'`
301302
(larges magnitude).
302-
maxiter: Maximum number of restarts. For `maxiter=0` the routine becomes
303+
maxiter: Maximum number of restarts. For `maxiter=0` the routine becomes
303304
equivalent to a simple Arnoldi method.
304305
Returns:
305306
(eigvals, eigvecs)
@@ -326,11 +327,12 @@ def A(H,x):
326327
type(initial_state)))
327328
if A not in _CACHED_MATVECS:
328329
_CACHED_MATVECS[A] = libjax.tree_util.Partial(libjax.jit(A))
329-
if not hasattr(self, '_iram'):
330-
# pylint: disable=attribute-defined-outside-init
331-
self._iram = jitted_functions._implicitly_restarted_arnoldi(libjax)
332-
return self._iram(_CACHED_MATVECS[A], args, initial_state, num_krylov_vecs,
333-
numeig, which, tol, maxiter)
330+
if "imp_arnoldi" not in _CACHED_FUNCTIONS:
331+
imp_arnoldi = jitted_functions._implicitly_restarted_arnoldi(libjax)
332+
_CACHED_FUNCTIONS["imp_arnoldi"] = imp_arnoldi
333+
return _CACHED_FUNCTIONS["imp_arnoldi"](_CACHED_MATVECS[A], args,
334+
initial_state, num_krylov_vecs,
335+
numeig, which, tol, maxiter)
334336

335337
def eigsh_lanczos(
336338
self,
@@ -347,12 +349,12 @@ def eigsh_lanczos(
347349
reorthogonalize: Optional[bool] = False) -> Tuple[Tensor, List]:
348350
"""
349351
Lanczos method for finding the lowest eigenvector-eigenvalue pairs
350-
of a hermitian linear operator `A`. `A` is a function implementing
351-
the matrix-vector product.
352+
of a hermitian linear operator `A`. `A` is a function implementing
353+
the matrix-vector product.
352354
WARNING: This routine uses jax.jit to reduce runtimes. jitting is triggered
353-
at the first invocation of `eigsh_lanczos`, and on any subsequent calls
354-
if the python `id` of `A` changes, even if the formal definition of `A`
355-
stays the same.
355+
at the first invocation of `eigsh_lanczos`, and on any subsequent calls
356+
if the python `id` of `A` changes, even if the formal definition of `A`
357+
stays the same.
356358
Example: the following will jit once at the beginning, and then never again:
357359
358360
```python
@@ -366,7 +368,7 @@ def A(H,x):
366368
res = eigsh_lanczos(A, [H],x) #jitting is triggerd only at `n=0`
367369
```
368370
369-
The following code triggers jitting at every iteration, which
371+
The following code triggers jitting at every iteration, which
370372
results in considerably reduced performance
371373
372374
```python
@@ -379,7 +381,7 @@ def A(H,x):
379381
x = jax.np.array(np.random.rand(10,10))
380382
res = eigsh_lanczos(A, [H],x) #jitting is triggerd at every step `n`
381383
```
382-
384+
383385
Args:
384386
A: A (sparse) implementation of a linear operator.
385387
Call signature of `A` is `res = A(vector, *args)`, where `vector`
@@ -395,7 +397,7 @@ def A(H,x):
395397
numeig: The number of eigenvector-eigenvalue pairs to be computed.
396398
If `numeig > 1`, `reorthogonalize` has to be `True`.
397399
tol: The desired precision of the eigenvalues. For the jax backend
398-
this has currently no effect, and precision of eigenvalues is not
400+
this has currently no effect, and precision of eigenvalues is not
399401
guaranteed. This feature may be added at a later point.
400402
To increase precision the caller can increase `num_krylov_vecs`.
401403
delta: Stopping criterion for Lanczos iteration.
@@ -404,7 +406,7 @@ def A(H,x):
404406
is stopped. It means that an (approximate) invariant subspace has
405407
been found.
406408
ndiag: The tridiagonal Operator is diagonalized every `ndiag` iterations
407-
to check convergence. This has currently no effect for the jax backend,
409+
to check convergence. This has currently no effect for the jax backend,
408410
but may be added at a later point.
409411
reorthogonalize: If `True`, Krylov vectors are kept orthogonal by
410412
explicit orthogonalization (more costly than `reorthogonalize=False`)
@@ -433,12 +435,162 @@ def A(H,x):
433435
type(initial_state)))
434436
if A not in _CACHED_MATVECS:
435437
_CACHED_MATVECS[A] = libjax.tree_util.Partial(A)
436-
if not hasattr(self, '_jaxlan'):
437-
# pylint: disable=attribute-defined-outside-init
438-
self._jaxlan = jitted_functions._generate_jitted_eigsh_lanczos(libjax)
438+
if "eigsh_lanczos" not in _CACHED_FUNCTIONS:
439+
eigsh_lanczos = jitted_functions._generate_jitted_eigsh_lanczos(libjax)
440+
_CACHED_FUNCTIONS["eigsh_lanczos"] = eigsh_lanczos
441+
eigsh_lanczos = _CACHED_FUNCTIONS["eigsh_lanczos"]
442+
return eigsh_lanczos(_CACHED_MATVECS[A], args, initial_state,
443+
num_krylov_vecs, numeig, delta, reorthogonalize)
444+
445+
def gmres(self,
446+
A_mv: Callable,
447+
b: Tensor,
448+
A_args: Optional[List] = None,
449+
A_kwargs: Optional[dict] = None,
450+
x0: Optional[Tensor] = None,
451+
tol: float = 1E-05,
452+
atol: Optional[float] = None,
453+
num_krylov_vectors: Optional[int] = None,
454+
maxiter: Optional[int] = 1,
455+
M: Optional[Callable] = None
456+
) -> Tuple[Tensor, int]:
457+
""" GMRES solves the linear system A @ x = b for x given a vector `b` and
458+
a general (not necessarily symmetric/Hermitian) linear operator `A`.
459+
460+
As a Krylov method, GMRES does not require a concrete matrix representation
461+
of the n by n `A`, but only a function
462+
`vector1 = A_mv(vector0, *A_args, **A_kwargs)`
463+
prescribing a one-to-one linear map from vector0 to vector1 (that is,
464+
A must be square, and thus vector0 and vector1 the same size). If `A` is a
465+
dense matrix, or if it is a symmetric/Hermitian operator, a different
466+
linear solver will usually be preferable.
467+
468+
GMRES works by first constructing the Krylov basis
469+
K = (x0, A_mv@x0, A_mv@A_mv@x0, ..., (A_mv^num_krylov_vectors)@x_0) and then
470+
solving a certain dense linear system K @ q0 = q1 from whose solution x can
471+
be approximated. For `num_krylov_vectors = n` the solution is provably exact
472+
in infinite precision, but the expense is cubic in `num_krylov_vectors` so
473+
one is typically interested in the `num_krylov_vectors << n` case.
474+
The solution can in this case be repeatedly
475+
improved, to a point, by restarting the Arnoldi iterations each time
476+
`num_krylov_vectors` is reached. Unfortunately the optimal parameter choices
477+
balancing expense and accuracy are difficult to predict in advance, so
478+
applying this function requires a degree of experimentation.
479+
480+
In a tensor network code one is typically interested in A_mv implementing
481+
some tensor contraction. This implementation thus allows `b` and `x0` to be
482+
of whatever arbitrary, though identical, shape `b = A_mv(x0, ...)` expects.
483+
Reshaping to and from a matrix problem is handled internally.
484+
485+
The Jax backend version of GMRES uses a homemade implementation that, for
486+
now, is suboptimal for num_krylov_vecs ~ b.size.
487+
488+
For the same reason as described in eigsh_lancsoz, the function A_mv
489+
should be Jittable (or already Jitted) and, if at all possible, defined
490+
only once at the global scope. A new compilation will be triggered each
491+
time an A_mv with a new function signature is passed in, even if the
492+
'new' function is identical to the old one (function identity is
493+
undecidable).
494+
495+
496+
Args:
497+
A_mv : A function `v0 = A_mv(v, *A_args, **A_kwargs)` where `v0` and
498+
`v` have the same shape.
499+
b : The `b` in `A @ x = b`; it should be of the shape `A_mv`
500+
operates on.
501+
A_args : Positional arguments to `A_mv`, supplied to this interface
502+
as a list.
503+
Default: None.
504+
A_kwargs : In the other backends, keyword arguments to `A_mv`, supplied
505+
as a dictionary. However, the Jax backend does not support
506+
A_mv accepting
507+
keyword arguments since this causes problems with Jit.
508+
Therefore, an error is thrown if A_kwargs is specified.
509+
Default: None.
510+
x0 : An optional guess solution. Zeros are used by default.
511+
If `x0` is supplied, its shape and dtype must match those of
512+
`b`, or an
513+
error will be thrown.
514+
Default: zeros.
515+
tol, atol: Solution tolerance to achieve,
516+
norm(residual) <= max(tol*norm(b), atol).
517+
Default: tol=1E-05
518+
atol=tol
519+
num_krylov_vectors
520+
: Size of the Krylov space to build at each restart.
521+
Expense is cubic in this parameter. If supplied, it must be
522+
an integer in 0 < num_krylov_vectors <= b.size.
523+
Default: b.size.
524+
maxiter : The Krylov space will be repeatedly rebuilt up to this many
525+
times. Large values of this argument
526+
should be used only with caution, since especially for nearly
527+
symmetric matrices and small `num_krylov_vectors` convergence
528+
might well freeze at a value significantly larger than `tol`.
529+
Default: 1
530+
M : Inverse of the preconditioner of A; see the docstring for
531+
`scipy.sparse.linalg.gmres`. This is unsupported in the Jax
532+
backend, and NotImplementedError will be raised if it is
533+
supplied.
534+
Default: None.
535+
536+
537+
Raises:
538+
ValueError: -if `x0` is supplied but its shape differs from that of `b`.
539+
-if num_krylov_vectors is 0 or exceeds b.size.
540+
-if tol or atol was negative.
541+
NotImplementedError: - If M is supplied.
542+
- If A_kwargs is supplied.
543+
544+
Returns:
545+
x : The converged solution. It has the same shape as `b`.
546+
info : 0 if convergence was achieved, the number of restarts otherwise.
547+
"""
439548

440-
return self._jaxlan(_CACHED_MATVECS[A], args, initial_state,
441-
num_krylov_vecs, numeig, delta, reorthogonalize)
549+
if x0 is not None and x0.shape != b.shape:
550+
errstring = (f"If x0 is supplied, its shape, {x0.shape}, must match b's"
551+
f", {b.shape}.")
552+
raise ValueError(errstring)
553+
if x0 is not None and x0.dtype != b.dtype:
554+
errstring = (f"If x0 is supplied, its dtype, {x0.dtype}, must match b's"
555+
f", {b.dtype}.")
556+
raise ValueError(errstring)
557+
if num_krylov_vectors is None:
558+
num_krylov_vectors = b.size
559+
if num_krylov_vectors <= 0 or num_krylov_vectors > b.size:
560+
errstring = (f"num_krylov_vectors must be in "
561+
f"0 < {num_krylov_vectors} <= {b.size}.")
562+
raise ValueError(errstring)
563+
if tol < 0:
564+
raise ValueError(f"tol = {tol} must be positive.")
565+
if atol is None:
566+
atol = tol
567+
if atol < 0:
568+
raise ValueError(f"atol = {atol} must be positive.")
569+
570+
if M is not None:
571+
raise NotImplementedError("M is not supported by the Jax backend.")
572+
if A_kwargs is not None:
573+
raise NotImplementedError("A_kwargs is not supported by the Jax backend.")
574+
575+
if A_args is None:
576+
A_args = []
577+
578+
if x0 is None:
579+
x0 = self.zeros(b.shape, b.dtype)
580+
581+
if A_mv not in _CACHED_MATVECS:
582+
_CACHED_MATVECS[A_mv] = libjax.tree_util.Partial(A_mv)
583+
if "gmres_f" not in _CACHED_FUNCTIONS:
584+
_CACHED_FUNCTIONS["gmres_f"] = jitted_functions.gmres_wrapper(libjax)
585+
gmres_f = _CACHED_FUNCTIONS["gmres_f"]
586+
x, _, n_iter, converged = gmres_f(_CACHED_MATVECS[A_mv], A_args, b,
587+
x0, tol, atol, num_krylov_vectors,
588+
maxiter)
589+
if converged:
590+
info = 0
591+
else:
592+
info = n_iter
593+
return x, info
442594

443595
def conj(self, tensor: Tensor) -> Tensor:
444596
return jnp.conj(tensor)

0 commit comments

Comments
 (0)