Skip to content

Commit

Permalink
Merge pull request #526 from SciML/linsolve
Browse files Browse the repository at this point in the history
DifferentialEquations.jl v7 documentation: new linsolve, preconditioners, and more code optimization tutorials
  • Loading branch information
ChrisRackauckas authored Jan 9, 2022
2 parents 48822ac + d27c154 commit a23b283
Show file tree
Hide file tree
Showing 15 changed files with 1,421 additions and 547 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "DiffEqDocs"
uuid = "d91efeb5-c178-44a6-a2ee-51685df7c78e"
authors = ["Chris Rackauckas <[email protected]>"]
version = "6.19.0"
version = "7.0.0"

[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
Expand Down
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ makedocs(modules=[DiffEqBase,SciMLBase,DiffEqProblemLibrary,ODEProblemLibrary,SD
"DifferentialEquations.jl: Scientific Machine Learning (SciML) Enabled Simulation and Estimation" => "index.md",
"Tutorials" => Any[
"tutorials/ode_example.md",
"tutorials/faster_ode_example.md",
"tutorials/advanced_ode_example.md",
"tutorials/sde_example.md",
"tutorials/rode_example.md",
Expand Down
41 changes: 36 additions & 5 deletions docs/src/basics/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ This page is a compilation of frequently asked questions and answers.

## [Stability and Divergence of ODE Solves](@id faq_stability)

For guidelines on debugging ODE solve issues, see
For guidelines on debugging ODE solve issues, see
[PSA: How to help yourself debug differential equation solving issues](https://discourse.julialang.org/t/psa-how-to-help-yourself-debug-differential-equation-solving-issues/62489).

#### My model is reporting unstable results. What can I do?

First of all, don't panic. You may have experienced one of the following warnings:

> dt <= dtmin. Aborting. There is either an error in your model specification or the true solution is unstable.
>
>
> NaN dt detected. Likely a NaN value in the state, parameters, or derivative value caused this outcome.
>
>
> Instability detected. Aborting
These are all pointing to a similar behavior: for some reason or another, the
Expand Down Expand Up @@ -214,6 +214,37 @@ DistributedArrays require parallel linear solves to really matter, and thus are
only recommended when you have a problem that cannot fit into memory or are using
a stiff solver with a Krylov method for the linear solves.

### Note About Setting Up Your Julia Installation for Speed: BLAS Choices

Julia uses an underlying BLAS implementation for its matrix multiplications
and factorizations. This library is automatically multithreaded and accelerates
the internal linear algebra of DifferentialEquations.jl. However, for optimality,
you should make sure that the number of BLAS threads that you are using matches
the number of physical cores and not the number of logical cores. See
[this issue for more details](https://github.com/JuliaLang/julia/issues/33409).

To check the number of BLAS threads, use:

```julia
ccall((:openblas_get_num_threads64_, Base.libblas_name), Cint, ())
```

If I want to set this directly to 4 threads, I would use:

```julia
using LinearAlgebra
LinearAlgebra.BLAS.set_num_threads(4)
```

Additionally, in some cases Intel's MKL might be a faster BLAS than the standard
BLAS that ships with Julia (OpenBLAS). To switch your BLAS implementation, you
can use [MKL.jl](https://github.com/JuliaComputing/MKL.jl) which will accelerate
the linear algebra routines. This is done via:

```julia
using MKL
```

#### My ODE is solving really slow

First, check for bugs. These solvers go through a ton of convergence tests and
Expand Down Expand Up @@ -491,8 +522,8 @@ sol = solve(prob,Rosenbrock23(autodiff=false))
and it will use a numerical differentiation fallback (DiffEqDiffTools.jl) to
calculate Jacobians.

We could use `get_tmp` and `dualcache` functions from
[PreallocationTools.jl](https://github.com/SciML/PreallocationTools.jl)
We could use `get_tmp` and `dualcache` functions from
[PreallocationTools.jl](https://github.com/SciML/PreallocationTools.jl)
to solve this issue, e.g.,

```julia
Expand Down
234 changes: 69 additions & 165 deletions docs/src/features/linear_nonlinear.md
Original file line number Diff line number Diff line change
@@ -1,191 +1,95 @@
# [Specifying (Non)Linear Solvers](@id linear_nonlinear)
# [Specifying (Non)Linear Solvers and Preconditioners](@id linear_nonlinear)

One of the key features of DifferentialEquations.jl is its flexibility. Keeping
with this trend, many of the native Julia solvers provided by DifferentialEquations.jl
allow you to choose the method for linear and nonlinear solving. This section
details how to make that choice.

## Linear Solvers: `linsolve` Specification

For differential equation integrators which use linear solvers, an argument
to the method `linsolve` determines the linear solver which is used. The signature
is:

```julia
linsolve! = linsolve(Val{:init},f,x;kwargs...)
linsolve!(x,A,b,matrix_updated=false;kwargs...)
```

This is an in-place function which updates `x` by solving `Ax=b`. The user should
specify the function `linsolve(Val{:init},f,x)` which returns a `linsolve!` function.
The setting `matrix_updated` determines whether the matrix `A` has changed from the
last call. This can be used to smartly cache factorizations.

Note that `linsolve!` needs to accept splatted keyword arguments. The possible arguments
passed to the linear solver are as follows:

- `Pl`, a pre-specified left preconditioner which utilizes the internal adaptive norm estimates
- `Pr`, a pre-specified right preconditioner which utilizes the internal adaptive norm estimates
- `tol`, a linear solver tolerance specified from the ODE solver's implicit handling

### Pre-Built Linear Solver Choices

The following choices of pre-built linear solvers exist:
!!! note

- DefaultLinSolve
- LinSolveFactorize
- LinSolveGPUFactorize
- LinSolveGMRES
- LinSolveCG
- LinSolveBiCGStabl
- LinSolveChebyshev
- LinSolveMINRES
- LinSolveIterativeSolvers

Additionally, by adding [Pardiso.jl](https://github.com/JuliaSparse/Pardiso.jl) the following exist:

- MKLPardisoFactorize
- PardisoFactorize
- PardisoIterate

### DefaultLinSolve

The default linear solver is `DefaultLinSolve`. This method is adaptive, and
automatically chooses an LU factorization choose for dense and sparse
arrays, and is compatible with GPU-based arrays. When the Jacobian is an
`AbstractDiffEqOperator`, i.e. is matrix-free, `DefaultLinSolve` defaults to
using a `gmres` iterative solver.

### Basic linsolve method choice: Factorization by LinSolveFactorize

The easiest way to specify a `linsolve` is by a `factorization` function which
generates a type on which `\` (or `A_ldiv_B!`) is called. This is done through
the helper function `LinSolveFactorize` which makes the appropriate function.
For example, the `Rosenbrock23` takes in a `linsolve` function, which we can
choose to be a QR-factorization from the standard library `LinearAlgebra` by:

```julia
Rosenbrock23(linsolve=LinSolveFactorize(qr!))
```
We highly recommend looking at the [Solving Large Stiff Equations](@id stiff)
tutorial which goes through these options in a real-world example.

LinSolveFactorize takes in a function which returns an object that can `\`.
Direct methods like `qr!` will automatically cache the factorization,
making it efficient for small dense problems.
!!! warning

However, for large sparse problems, you can let `\` be an iterative method. For
example, using PETSc.jl, we can define our factorization function to be:
These options do not apply to the Sundials differential equation solvers
(`CVODE_BDF`, `CVODE_Adams`, `ARKODE`, and `IDA`). For complete descriptions
of similar functionality for Sundials, see the
[Sundials ODE solver documentation](@id ode_solve_sundials) and
[Sundials DAE solver documentation](@id dae_solve_sundials).

```julia
linsolve = LinSolveFactorize((A) -> KSP(A, ksp_type="gmres", ksp_rtol=1e-6))
```

This function creates a `KSP` type which makes `\` perform the GMRES iterative
method provided by PETSc.jl. Thus if we pass this function into the algorithm
as the factorization method, all internal linear solves will happen by PETSc.jl.

### GPU offloading of factorization with LinSolveGPUFactorize

If one has a problem with a sufficiently large Jacobian (~100x100) and a
sufficiently powerful GPU, it can make sense to offload the factorization
and backpropogation steps to the GPU. For this, the `LinSolveGPUFactorize`
linear solver is provided. It works similarly to `LinSolveFactorize`, but
the matrix is automatically sent to the GPU as a `CuArray` and the `ldiv!`
is performed against a CUDA QR factorization of the matrix.

Note that this method requires that you have done `using CuArrays` in your
script. A working installation of CuArrays.jl is required, which requires
an installation of CUDA Toolkit.

### [IterativeSolvers.jl-Based Methods](@id iterativesolvers-jl)

The signature for `LinSolveIterativeSolvers` is:

```julia
LinSolveIterativeSolvers(generate_iterator,args...;
Pl=IterativeSolvers.Identity(),
Pr=IterativeSolvers.Identity(),
kwargs...)
```

where `Pl` is the left preconditioner, `Pr` is the right preconditioner, and
the other `args...` and `kwargs...` are passed into the iterative solver
chosen in `generate_iterator` which designates the construction of an iterator
from IterativeSolvers.jl. For example, using `gmres_iterable!` would make a
version that uses `IterativeSolvers.gmres`. The following are aliases to common
choices:

- LinSolveGMRES -- GMRES
- LinSolveCG -- CG (Conjugate Gradient)
- LinSolveBiCGStabl -- BiCGStabl Stabilized Bi-Conjugate Gradient
- LinSolveChebyshev -- Chebyshev
- LinSolveMINRES -- MINRES

which all have the same arguments as `LinSolveIterativeSolvers` except with
`generate_iterator` pre-specified.

### Implementing Your Own LinSolve: How LinSolveFactorize Was Created
## Linear Solvers: `linsolve` Specification

In order to make your own `linsolve` functions, let's look at how the `LinSolveFactorize`
function is created. For example, for an LU-Factorization, we would like to use
`lufact!` to do our linear solving. We can directly write this as:
For linear solvers, DifferentialEquations.jl uses
[LinearSolve.jl](https://github.com/SciML/LinearSolve.jl). Any
[LinearSolve.jl algorithm](http://linearsolve.sciml.ai/dev/solvers/solvers/)
can be used as the linear solver simply by passing the algorithm choice to
linsole. For example, the following tells `TRBDF2` to use [KLU.jl](https://github.com/JuliaSparse/KLU.jl)

```julia
using LinearAlgebra
function linsolve!(::Type{Val{:init}},f,u0; kwargs...)
function _linsolve!(x,A,b,update_matrix=false; kwargs...)
_A = lu(A)
ldiv!(x,_A,b)
end
end
TRBDF2(linsolve = KLUFactorization())
```

This initialization function returns a linear solving function
that always computes the LU-factorization and then does the solving.
This method works fine and you can pass it to the methods like
Many choices exist, including GPU offloading, so consult the
[LinearSole.jl documentation](http://linearsolve.sciml.ai/dev/) for more details
on the choices.

## Preconditioners: `precs` Specification

Any [LinearSolve.jl-compatible preconditioner](http://linearsolve.sciml.ai/dev/basics/Preconditioners/)
can be used as a left or right preconditioner. Preconditioners are specified by
the `Pl,Pr = precs(W,du,u,p,t,newW,Plprev,Prprev,solverdata)` function where
the arguments are defined as:

- `W`: the current Jacobian of the nonlinear system. Specified as either
``I - gamma*J`` or ``I/gamma - J`` depending on the algorithm. This will
commonly be a `WOperator` type defined by OrdinaryDiffEq.jl. It is a lazy
representation of the operator. Users can construct the W-matrix on demand
by calling `convert(AbstractMatrix,W)` to receive an `AbstractMatrix` matching
the `jac_prototype`.
- `du`: the current ODE derivative
- `u`: the current ODE state
- `p`: the ODE parameters
- `t`: the current ODE time
- `newW`: a `Bool` which specifies whether the `W` matrix has been updated since
the last call to `precs`. It is recommended that this is checked to only
update the preconditioner when `newW == true`.
- `Plprev`: the previous `Pl`.
- `Prprev`: the previous `Pr`.
- `solverdata`: Optional extra data the solvers can give to the `precs` function.
Solver-dependent and subject to change.

The return is a tuple `(Pl,Pr)` of the LinearSolve.jl-compatible preconditioners.
To specify one-sided preconditioning, simply return `nothing` for the preconditioner
which is not used.

Additionally, `precs` must supply the dispatch:

```julia
Rosenbrock23(linsolve=linsolve!)
Pl,Pr = precs(W,du,u,p,t,::Nothing,::Nothing,::Nothing,solverdata)
```

and it will work, but this method does not cache `_A`, the factorization. This
means that, even if `A` has not changed, it will re-factorize the matrix.
which is used in the solver setup phase in order to construct the integrator
type with the preconditioners `(Pl,Pr)`.

To change this, we can instead create a call-overloaded type. The generalized form
of this is:
The default is `precs=DEFAULT_PRECS` where the default preconditioner function
is defined as:

```julia
mutable struct LinSolveFactorize{F}
factorization::F
A
end
LinSolveFactorize(factorization) = LinSolveFactorize(factorization,nothing)
function (p::LinSolveFactorize)(x,A,b,matrix_updated=false)
if matrix_updated
p.A = p.factorization(A)
end
A_ldiv_B!(x,p.A,b)
end
function (p::LinSolveFactorize)(::Type{Val{:init}},f,u0_prototype)
LinSolveFactorize(p.factorization,nothing)
end
linsolve = LinSolveFactorize(lufact!)
DEFAULT_PRECS(W,du,u,p,t,newW,Plprev,Prprev,solverdata) = nothing,nothing
```

`LinSolveFactorize` is a type which holds the factorization method and the pre-factorized
matrix. When `linsolve` is passed to the ODE/SDE/etc. solver, it will use the function
`linsolve(Val{:init},f,u0_prototype)` to create a `LinSolveFactorize` object which holds
the factorization method and a cache for holding a factorized matrix. Then
## Nonlinear Solvers: `nlsolve` Specification

```julia
function (p::LinSolveFactorize)(x,A,b,matrix_updated=false)
if matrix_updated
p.A = p.factorization(A)
end
A_ldiv_B!(x,p.A,b)
end
```
All of the Julia-based implicit solvers (OrdinaryDiffEq.jl, StochasticDiffEq.jl, etc.)
allow for choosing the nonlinear solver that is used to handle the implicit system.
While fully modifiable and customizable, most users should stick to the pre-defined
nonlinear solver choices. These are:

is what's used in the solver's internal loop. If `matrix_updated` is true, it
will re-compute the factorization. Otherwise it just solves the linear system
with the cached factorization. This general idea of using a call-overloaded
type can be employed to do many other things.
- `NLNewton(; κ=1//100, max_iter=10, fast_convergence_cutoff=1//5, new_W_dt_cutoff=1//5)`: A quasi-Newton method. The default
- `NLAnderson(; κ=1//100, max_iter=10, max_history::Int=5, aa_start::Int=1, droptol=nothing, fast_convergence_cutoff=1//5)`:
Anderson acceleration. While more stable than functional iteration, this method
is less stable than Newton's method but does not require a Jacobian.
- `NLFunctional(; κ=1//100, max_iter=10, fast_convergence_cutoff=1//5)`: This method
is the least stable but does not require Jacobians. Should only be used for
non-stiff ODEs.
32 changes: 31 additions & 1 deletion docs/src/features/performance_overloads.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
# [DiffEqFunctions (Jacobians, Gradients, etc.) and Jacobian Types](@id performance_overloads)
# [Jacobians, Gradients, etc.](@id performance_overloads)

The DiffEq ecosystem provides an extensive interface for declaring extra functions
associated with the differential equation's data. In traditional libraries there
is usually only one option: the Jacobian. However, we allow for a large array
of pre-computed functions to speed up the calculations. This is offered via the
`DiffEqFunction` types which can be passed to the problems.

## Built-In Jacobian Options

!!! warning

This subsection on Jacobian options only applies to the Julia-based solvers
(OrdinaryDiffEq.jl, StochasticDiffEq.jl, etc.). Wrappers of traditional
C/Fortran codes, such as Sundials.jl, always default to finite differencing
using the in-solver code.

All applicable stiff differential equation solvers in the Julia ecosystem
(OrdinaryDiffEq.jl, StochasticDiffEq.jl, DelayDiffEq.jl, etc.) take the following
arguments for handling the automatic Jacobian construction with the following defaults:

- `chunk_size`: The chunk size used with ForwardDiff.jl. Defaults to `Val{0}()`
and thus uses the internal ForwardDiff.jl algorithm for the choice.
- `autodiff`: Specifies whether to use automatic differentiation via
[ForwardDiff.jl](https://github.com/JuliaDiff/ForwardDiff.jl) or finite
differencing via [FiniteDiff.jl](https://github.com/JuliaDiff/FiniteDiff.jl).
Defaults to `Val{true}()` for automatic differentation.
- `standardtag`: Specifies whether to use package-specific tags instead of the
ForwardDiff default function-specific tags. For more information see
[this blog post](https://www.stochasticlifestyle.com/improved-forwarddiff-jl-stacktraces-with-package-tags/).
Defaults to `Val{true}()`.
- `concrete_jac`: Specifies whether a Jacobian should be constructed. Defalts to
`nothing`, which means it will be chosen true/false depending on circumstances
of the solver, such as whether a Krylov subspace method is used for `linsolve`.
- `diff_type`: The type of differentiation used in FiniteDiff.jl if `autodiff=false`.
Defalts to `Val{:forward}`, with altnerative choices of `Val{:central}` and
`Val{:complex}`.

## Function Type Definitions

### Function Choice Definitions
Expand Down
Loading

0 comments on commit a23b283

Please sign in to comment.