From 6e530f6444b85981e2e6b292b17cec752bb3b289 Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 28 Jun 2026 11:28:47 -0400 Subject: [PATCH 1/2] docs: document symbolic factorization reuse for sparse matrices Add a "Reusing the Symbolic Factorization with Sparse Matrices" section to the caching interface tutorial. Explains reuse_symbolic/check_pattern, and the key gotcha that in-place edits to A's values are not seen by the cache: the change must be signaled with `linsolve.A = A` (or, internally, isfresh). Addresses SciML/LinearSolve.jl#1060. Co-Authored-By: Chris Rackauckas --- docs/src/tutorials/caching_interface.md | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/src/tutorials/caching_interface.md b/docs/src/tutorials/caching_interface.md index f07ee6421..93a7d0beb 100644 --- a/docs/src/tutorials/caching_interface.md +++ b/docs/src/tutorials/caching_interface.md @@ -59,3 +59,78 @@ The advantage of course with import LinearSolve.jl in this form is that it is efficient while being agnostic to the linear solver. One can easily swap in iterative solvers, sparse solvers, etc. and it will do all the tricks like caching the symbolic factorization if the sparsity pattern is unchanged. + +## Reusing the Symbolic Factorization with Sparse Matrices + +For sparse factorizations such as `UMFPACKFactorization` and `KLUFactorization`, +the most expensive part of a solve is often the *symbolic* factorization: the +analysis of the sparsity pattern that determines the fill-in and elimination +ordering. The subsequent *numeric* factorization, which computes the actual `L` +and `U` factor values, is comparatively cheap. + +A very common case (for example, the Newton steps of a nonlinear solve or the +time steps of an implicit ODE/PDE integrator) is that `A` keeps the **same +sparsity pattern** across solves while only its stored values change. In that +case, the symbolic factorization can be reused and only the numeric +factorization needs to be redone. This is controlled by the `reuse_symbolic` +keyword (default `true`) on the sparse factorization algorithms: + +```julia +solver = LS.UMFPACKFactorization(; reuse_symbolic = true) +``` + +With `reuse_symbolic = true`, when a new `A` with an unchanged pattern is given, +LinearSolve.jl reuses the cached symbolic factorization and performs only the +numeric refactorization. By default it also runs a `check_pattern` pass to +confirm the pattern really is unchanged; if your pattern is guaranteed constant, +you can set `check_pattern = false` to skip that check for a little extra speed. + +### Updating the values of `A` + +The caching interface tracks whether the factorization is stale through the +assignment `linsolve.A = A`. **Mutating the stored values of `A` in place does +not, on its own, tell the cache that `A` has changed**, so the next `solve!` +will silently reuse the old factorization and return a stale (wrong) result: + +```julia +linsolve.A.nzval .*= 2 # in-place edit: NOT seen by the cache +LS.solve!(linsolve) # reuses the old factorization -> wrong answer +``` + +The supported way to signal that the values changed is to assign `A` back into +the cache. Reassigning the *same* (now-mutated) matrix object is fine and still +reuses the symbolic factorization when the pattern is unchanged: + +```@example sparsereuse +import LinearSolve as LS +import LinearAlgebra as LA +import SparseArrays + +n = 100 +A = SparseArrays.sprand(n, n, 0.05) + 10.0 * LA.I +b = rand(n) +prob = LS.LinearProblem(A, b) + +linsolve = LS.init(prob, LS.UMFPACKFactorization()) +sol1 = LS.solve!(linsolve) + +# Update the values of A (same sparsity pattern), then signal the change: +A.nzval .*= 2 +linsolve.A = A # triggers refactorization; symbolic part is reused +sol2 = LS.solve!(linsolve) + +sol2.u +``` + +Because the pattern is unchanged, `sol2` reuses the cached symbolic +factorization and only redoes the (cheaper) numeric factorization. + +!!! note + + Internally, the cache uses an `isfresh` flag to mark the factorization as + stale, and assigning `linsolve.A = A` sets it. If you must edit `A`'s values + in place and want to avoid touching the matrix field, you can equivalently + set `linsolve.isfresh = true` directly. Prefer `linsolve.A = A`, as + `isfresh` is an internal detail. Either way, `reuse_symbolic = true` ensures + the symbolic factorization is reused as long as the sparsity pattern is the + same. From 37528335d6d041a2908298f9e12eb6baedcde63c Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 28 Jun 2026 11:44:12 -0400 Subject: [PATCH 2/2] docs: clarify reuse_symbolic is on by default, no need to set it Co-Authored-By: Chris Rackauckas --- docs/src/tutorials/caching_interface.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/src/tutorials/caching_interface.md b/docs/src/tutorials/caching_interface.md index 93a7d0beb..cdc5344bf 100644 --- a/docs/src/tutorials/caching_interface.md +++ b/docs/src/tutorials/caching_interface.md @@ -73,17 +73,19 @@ time steps of an implicit ODE/PDE integrator) is that `A` keeps the **same sparsity pattern** across solves while only its stored values change. In that case, the symbolic factorization can be reused and only the numeric factorization needs to be redone. This is controlled by the `reuse_symbolic` -keyword (default `true`) on the sparse factorization algorithms: +keyword on the sparse factorization algorithms, which **defaults to `true`** — +so you get this behavior automatically and do not need to set it: ```julia -solver = LS.UMFPACKFactorization(; reuse_symbolic = true) +solver = LS.UMFPACKFactorization() # reuse_symbolic = true by default ``` -With `reuse_symbolic = true`, when a new `A` with an unchanged pattern is given, -LinearSolve.jl reuses the cached symbolic factorization and performs only the -numeric refactorization. By default it also runs a `check_pattern` pass to -confirm the pattern really is unchanged; if your pattern is guaranteed constant, -you can set `check_pattern = false` to skip that check for a little extra speed. +When a new `A` with an unchanged pattern is given, LinearSolve.jl reuses the +cached symbolic factorization and performs only the numeric refactorization. By +default it also runs a `check_pattern` pass to confirm the pattern really is +unchanged; if your pattern is guaranteed constant, you can set +`check_pattern = false` to skip that check for a little extra speed. Set +`reuse_symbolic = false` only if the sparsity pattern may change between solves. ### Updating the values of `A`