Fix GMRES restart residual reconstruction - #586
Conversation
7894c26 to
fd9a7f9
Compare
|
Reviewed this change. The math is right — the old restart kept only the One thing I caught while going back over it, and have now fixed in the branch: the reconstruction has to happen before the solution update, not after. With a preconditioner active the update reuses Non-blocking nits, if you want them:
Numerically I replicated the algorithm and it matches scipy's restarted GMRES to ~1e-10 with and without a right preconditioner (the old code sat at 1e-1 … 1e-6 and stagnated/breakdown'd). Default config is unaffected since |
|
Overall looks good. Can probably clean up the z assignments a bit z[i:i+1] = and see if it is easier to enumerate(zip(...)) over the c and s terms. |
b0eb448 to
80463c5
Compare
|
Done — switched the cascade to slice assignment and for k, (c, s) in enumerate(zip(self._cs[j::-1], self._sn[j::-1])):
i = j - k
z[i:i + 2] = (c*z[i] - s*z[i + 1], s*z[i] + c*z[i + 1])Reads a bit cleaner, and it's still the same backward cascade (applies |
I think it ends up a bit uglier in practice due to the extra line breaks. Maybe just revert and just tweak the assignment. |
| # preconditioner is active. The residual is | ||
| # r_m = beta[j+1]*V_{j+2}*(G^T e_{j+2}), i.e. a combination of all | ||
| # of the Arnoldi vectors v_0..v_{j+1}; stash the result in v[j+1], | ||
| # which survives the solution update. |
There was a problem hiding this comment.
Aim for single line comments.
80463c5 to
16845d1
Compare
The restarted-GMRES restart was reconstructing the residual as being
parallel to the last Arnoldi vector only, dropping the components in the
earlier Krylov directions. The correct residual is a combination of all
the Arnoldi vectors, obtained by cascading the Givens rotations back
through the basis (r_m = beta[j+1]*V_{j+2}*(G^T e_{j+2})). The old form
had the correct magnitude but wrong direction, causing restarted GMRES
(restart < linear-max-iter) to stagnate or break down instead of
converging.
16845d1 to
9f85c5a
Compare
|
Agreed on both — reverted the for i in range(j, -1, -1):
c, s = self._cs[i], self._sn[i]
z[i:i + 2] = (c*z[i] - s*z[i + 1], s*z[i] + c*z[i + 1])Also condensed that comment block down to a single line. :) |
| z[j + 1] = 1.0 | ||
| for i in range(j, -1, -1): | ||
| c, s = self._cs[i], self._sn[i] | ||
| z[i:i + 2] = (c*z[i] - s*z[i + 1], s*z[i] + c*z[i + 1]) |
| for i in range(j, -1, -1): | ||
| c, s = self._cs[i], self._sn[i] | ||
| z[i:i + 2] = (c*z[i] - s*z[i + 1], s*z[i] + c*z[i + 1]) | ||
| z *= np.copysign(1.0, self._beta[j + 1]) |
There was a problem hiding this comment.
Do we need copysign here or will np.sign work?
| y = np.linalg.solve(self._H[:j + 1, :j + 1], self._beta[:j + 1]) | ||
|
|
||
| # Determine if a restart is needed after this cycle | ||
| will_restart = not (err < rtol or h_jp1_j < self._breakdown_tol |
There was a problem hiding this comment.
Given we not this in one if later on, we may want to remove the not and rename the variable accordingly.
Summary
Fixes the restarted-GMRES residual reconstruction in
pyfr/integrators/implicit/krylov/gmres.py. Closes #585.The restart was reconstructing the residual as parallel to the last Arnoldi vector only, which has the correct magnitude but the wrong direction. In exact arithmetic the residual after a GMRES cycle is
i.e. a linear combination of all the Arnoldi vectors
v_0 … v_m, with coefficients cascading through every Givens rotation — not justv_m. Dropping the earlier components caused restarted GMRES (restart < linear-max-iter) to stagnate or break down instead of converging.The fix
Cascade the Givens rotations back through the Krylov basis to reconstruct the full residual direction. It reuses the already-stored
cs/snrotations and Krylov vectors, so it costs no extra matvec:The one subtlety worth calling out: the reconstruction has to happen before the solution update, not after. The preconditioned solution update reuses
v[0]as a scratch register forM⁻¹(V y), which would destroy the first Arnoldi vector that the cascade needs as a source.Validation
Replicating the algorithm in NumPy (MGS/CGS Arnoldi + incremental Givens) and comparing against
scipy.sparse.linalg.gmreson a well-conditioned SPD matrix (cond ≈ 8), targetingrtol = 1e-10, with and without a right preconditioner:mNotes
solver-gmres restartdefaults to0(single cycle), so the buggy path only triggers whenrestart < linear-max-iter.cgsvsmgs) and of preconditioning.