Skip to content

Fix SNES NonlinearPDE class when using line search - #3507

Merged
jhale merged 10 commits into
mainfrom
jhale/fix-snes-linesearch
Nov 15, 2024
Merged

Fix SNES NonlinearPDE class when using line search#3507
jhale merged 10 commits into
mainfrom
jhale/fix-snes-linesearch

Conversation

@jhale

@jhale jhale commented Nov 13, 2024

Copy link
Copy Markdown
Member

Thanks to @jonas-heinzmann for clearly identifying and explaining this issue and @michalhabera for help creating this fix.

When using a linesearch routine SNES needs to calculate (multiple times) the norm of the residual evaluated at a vector w equal to the current solution plus some proposed scaling factor times the Newton increment. To do this SNES calls the user-defined residual evaluation routine passing w. Our current implementation of the residual evaluation routine overwrites the residual form state u with w but does not replace u with the current solution x before returning. This leads to a failure of the linesearch.

This patch fixes this behaviour by preserving the internal state of the residual.

@jhale jhale changed the title Fix SNES NonlinearPDE class when using line search. Fix SNES NonlinearPDE class when using line search Nov 13, 2024
@jhale

jhale commented Nov 13, 2024

Copy link
Copy Markdown
Member Author

Failing line search in parallel run https://petsc.org/release/manualpages/SNES/SNES_DIVERGED_LINE_SEARCH

@jhale

jhale commented Nov 13, 2024

Copy link
Copy Markdown
Member Author

This works but seems unsatisfactory. A point from Jonas:

"...the pointer to the PETSc solution vector is linked directly to the pointer of the solution vector of Dolfinx, while in fact both instead should be independent, i.e. the vector at which Dolfinx assembles the forms and the solution vector PETSc works must not be the same to allow for these temporary solution vectors in PETSc."

@michalhabera

Copy link
Copy Markdown
Contributor

Looking at this again, I think a simpler solution would be to pass a new independent vector to a call to snes.solve().

@jorgensd

Copy link
Copy Markdown
Member

Could we move this implementation into the dolfinx namespace?

It is rather hard for users to see how to use PETSC snes as they have to browse the tests.

@jonas-heinzmann

jonas-heinzmann commented Nov 14, 2024

Copy link
Copy Markdown

To elaborate a bit more on the PETSc side of things to help the discussion:

In my understanding, the SNES solver object usually only works with a solution vector $X$ at which it needs to evaluate the function $F(X)$ as well as the Jacobian $J(X)$.
To me it seems that with the previously proposed SNESProblem class setup, the PETSc vector $X$ is somehow directly linked to self.u in the dolfinx namespace such that we can assemble $a(X)$ and $L(X)$.

This is not an issue unless we want to properly use the SNESLinesearch routines:
They work with another temporary vector $W$ at which the line search routines need to evaluate $F(W)$, while they explicitly rely on $X$ as an unchanged reference point to (iteratively) test different step sizes $\lambda_i$, i.e.

$$W_{i+1} = X + \lambda_{i+1} \Delta X$$

See for instance here.
However, once the line search routines call the SNESProblem.F method passing in $W$, since dolfinx wants to assemble $L(X)$, we need to explicitly overwrite $X \leftarrow W$, which then causes $X$ to change in the PETSc namespace, meaning that the line searches can't rely on a fixed reference point to test different step lengths $\lambda$.
This means that the line searches (for iteration $i>1$) get

$$W_{i+1} = W_i + \lambda_{i+1} \Delta X$$

instead of the expected

$$W_{i+1} = X + \lambda_{i+1} \Delta X$$

which causes them to fail when they try to fit an ansatz e.g. to $||F||_2$ along the step length.
When one looks at the output of the line search routines, this typically means that they shrink the step size $\lambda$ further and further, until the minimum step size is reaches and then exit.
As long as the line search only needs a single evaluation of $F(W)$, everything appears to work since in that case it does not matter if the reference point is not consistent due to $X\leftarrow W$.

So perhaps another idea to fix the issue would be to somehow 'unlink' u in the dolfinx namespace from X in the PETSc namespace?
Meaning that the solution vector at which dolfinx assembles $a$ and $L$ are independent of the vectors in the PETSc namespace.
Then the SNESProblem.F method could assemble the forms at whatever point PETSc passes in, without us interfering with the solution vectors of PETSc.

@jonas-heinzmann

Copy link
Copy Markdown

I think this was also the problem in this post on the discourse group.
There, one can also see the behavior I mentioned earlier of the line search routine shrinking the step size further and further, until the minimum is reached and then exiting.

@jhale

jhale commented Nov 14, 2024

Copy link
Copy Markdown
Member Author

"So perhaps another idea to fix the issue would be to somehow 'unlink' u in the dolfinx namespace from X in the PETSc namespace?"

Agreed, I think that is the best solution and I think it's more consistent with the design of DOLFINx and SNES. We should also consider adjusting our own NewtonSolver if we make the same link there.

@michalhabera

Copy link
Copy Markdown
Contributor

Thanks @jonas-heinzmann for noticing this.

I'd suggest following patch. I've checked and all other uses of SNES (e.g. test_petsc_nonlinear_assembler.py) do allocate new solution vector (unrelated to any function in forms),

diff --git a/python/test/unit/nls/test_newton.py b/python/test/unit/nls/test_newton.py
index ba56dbcdf1..8ef044f515 100644
--- a/python/test/unit/nls/test_newton.py
+++ b/python/test/unit/nls/test_newton.py
@@ -224,13 +224,16 @@ class TestNLS:
         snes.getKSP().setTolerances(rtol=1.0e-9)
         snes.getKSP().getPC().setType("lu")
 
-        snes.solve(None, u.x.petsc_vec)
+        x = u.x.petsc_vec.copy()
+        x.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
+
+        snes.solve(None, x)
         assert snes.getConvergedReason() > 0
         assert snes.getIterationNumber() < 6
 
         # Modify boundary condition and solve again
         u_bc.x.array[:] = 0.6
-        snes.solve(None, u.x.petsc_vec)
+        snes.solve(None, x)
         assert snes.getConvergedReason() > 0
         assert snes.getIterationNumber() < 6
         # print(snes.getIterationNumber())

@jhale

jhale commented Nov 14, 2024

Copy link
Copy Markdown
Member Author

I think this is fixed - @jonas-heinzmann can you try this out?

In short, users should (almost) never pass the same vector memory to snes.solve as used in the specifying the residual or Jacobian form.

@jorgensd We can bring NonlinearPDE_SNESProblem out to the user API in another PR.

I also took a look at our NewtonSolver. In our current NonlinearPDEProblem class we assume that x and u share the same memory - infact without this the solver will not converge. I would propose for consistency with SNES (that I believe has the right design here) that we remove this implicit coupling in our examples.

@jonas-heinzmann

Copy link
Copy Markdown

This seems to do the trick, thanks a lot!

@jhale
jhale added this pull request to the merge queue Nov 15, 2024
Merged via the queue into main with commit ca76505 Nov 15, 2024
@jhale
jhale deleted the jhale/fix-snes-linesearch branch November 15, 2024 07:16
schnellerhase pushed a commit to schnellerhase/fenics-dolfinx that referenced this pull request Dec 28, 2024
* Fix SNES NonlinearPDE class when using line search.

* Remove comments.

* Removing typing.

* Works in parallel, needs reviewing (end of day).

* Not entirely satisfactory...

* Patch from Michal.

* Add a comment

* Ruff.
jorgensd added a commit to METHODS-Group/ProximalGalerkin that referenced this pull request Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants