|
| 1 | +--- |
| 2 | +priority: 10000000 |
| 3 | +author: "Avik Pal" |
| 4 | +title: "Ill-Conditioned Nonlinear System Work-Precision Diagrams (Krylov Methods)" |
| 5 | +--- |
| 6 | + |
| 7 | + |
| 8 | +# Setup |
| 9 | + |
| 10 | +Fetch required packages |
| 11 | + |
| 12 | +```julia |
| 13 | +using NonlinearSolve, LinearAlgebra, SparseArrays, DiffEqDevTools, |
| 14 | + CairoMakie, Symbolics, BenchmarkTools, PolyesterForwardDiff, LinearSolve, Sundials, |
| 15 | + Enzyme, SparseConnectivityTracer, DifferentiationInterface, SparseMatrixColorings |
| 16 | +import NLsolve, MINPACK, PETSc, RecursiveFactorization |
| 17 | + |
| 18 | +const RUS = RadiusUpdateSchemes; |
| 19 | +BenchmarkTools.DEFAULT_PARAMETERS.seconds = 0.2; |
| 20 | +``` |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | +Define a utility to timeout the benchmark after a certain time. |
| 26 | + |
| 27 | +```julia |
| 28 | +# Taken from ReTestItems.jl |
| 29 | +function timeout(f, timeout) |
| 30 | + cond = Threads.Condition() |
| 31 | + timer = Timer(timeout) do tm |
| 32 | + close(tm) |
| 33 | + ex = ErrorException("timed out after $timeout seconds") |
| 34 | + @lock cond notify(cond, ex; error=false) |
| 35 | + end |
| 36 | + Threads.@spawn begin |
| 37 | + try |
| 38 | + ret = $f() |
| 39 | + isopen(timer) && @lock cond notify(cond, ret) |
| 40 | + catch e |
| 41 | + isopen(timer) && @lock cond notify(cond, CapturedException(e, catch_backtrace()); error=true) |
| 42 | + finally |
| 43 | + close(timer) |
| 44 | + end |
| 45 | + end |
| 46 | + return @lock cond wait(cond) # will throw if we timeout |
| 47 | +end |
| 48 | + |
| 49 | +function get_ordering(x::AbstractMatrix) |
| 50 | + idxs = Vector{Int}(undef, size(x, 1)) |
| 51 | + placed = zeros(Bool, size(x, 1)) |
| 52 | + idx = 1 |
| 53 | + for j in size(x, 2):-1:1 |
| 54 | + row = view(x, :, j) |
| 55 | + idxs_row = sortperm(row; by = x -> isnan(x) ? Inf : (x == -1 ? Inf : x)) |
| 56 | + for i in idxs_row |
| 57 | + if !placed[i] && !isnan(row[i]) && row[i] ≠ -1 |
| 58 | + idxs[idx] = i |
| 59 | + placed[i] = true |
| 60 | + idx += 1 |
| 61 | + idx > length(idxs) && break |
| 62 | + end |
| 63 | + end |
| 64 | + idx > length(idxs) && break |
| 65 | + end |
| 66 | + return idxs |
| 67 | +end |
| 68 | +``` |
| 69 | + |
| 70 | +``` |
| 71 | +get_ordering (generic function with 1 method) |
| 72 | +``` |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | + |
| 77 | + |
| 78 | +# Brusselator |
| 79 | + |
| 80 | +Define the Brussletor problem. |
| 81 | + |
| 82 | +```julia |
| 83 | +brusselator_f(x, y) = (((x - 3 // 10) ^ 2 + (y - 6 // 10) ^ 2) ≤ 0.01) * 5 |
| 84 | + |
| 85 | +limit(a, N) = ifelse(a == N + 1, 1, ifelse(a == 0, N, a)) |
| 86 | + |
| 87 | +function init_brusselator_2d(xyd, N) |
| 88 | + N = length(xyd) |
| 89 | + u = zeros(N, N, 2) |
| 90 | + for I in CartesianIndices((N, N)) |
| 91 | + x = xyd[I[1]] |
| 92 | + y = xyd[I[2]] |
| 93 | + u[I, 1] = 22 * (y * (1 - y))^(3 / 2) |
| 94 | + u[I, 2] = 27 * (x * (1 - x))^(3 / 2) |
| 95 | + end |
| 96 | + return u |
| 97 | +end |
| 98 | + |
| 99 | +function generate_brusselator_problem(N::Int; sparsity = nothing, kwargs...) |
| 100 | + xyd_brusselator = range(0; stop = 1, length = N) |
| 101 | + |
| 102 | + function brusselator_2d_loop(du_, u_, p) |
| 103 | + A, B, α, δx = p |
| 104 | + α = α / δx ^ 2 |
| 105 | + |
| 106 | + du = reshape(du_, N, N, 2) |
| 107 | + u = reshape(u_, N, N, 2) |
| 108 | + |
| 109 | + @inbounds @simd for I in CartesianIndices((N, N)) |
| 110 | + i, j = Tuple(I) |
| 111 | + x, y = xyd_brusselator[I[1]], xyd_brusselator[I[2]] |
| 112 | + ip1, im1 = limit(i + 1, N), limit(i - 1, N) |
| 113 | + jp1, jm1 = limit(j + 1, N), limit(j - 1, N) |
| 114 | + |
| 115 | + du[i, j, 1] = α * (u[im1, j, 1] + u[ip1, j, 1] + u[i, jp1, 1] + u[i, jm1, 1] - |
| 116 | + 4u[i, j, 1]) + |
| 117 | + B + u[i, j, 1] ^ 2 * u[i, j, 2] - (A + 1) * u[i, j, 1] + |
| 118 | + brusselator_f(x, y) |
| 119 | + |
| 120 | + du[i, j, 2] = α * (u[im1, j, 2] + u[ip1, j, 2] + u[i, jp1, 2] + u[i, jm1, 2] - |
| 121 | + 4u[i, j, 2]) + |
| 122 | + A * u[i, j, 1] - u[i, j, 1] ^ 2 * u[i, j, 2] |
| 123 | + end |
| 124 | + return nothing |
| 125 | + end |
| 126 | + |
| 127 | + return NonlinearProblem( |
| 128 | + NonlinearFunction(brusselator_2d_loop; sparsity), |
| 129 | + vec(init_brusselator_2d(xyd_brusselator, N)), |
| 130 | + (3.4, 1.0, 10.0, step(xyd_brusselator)); |
| 131 | + kwargs... |
| 132 | + ) |
| 133 | +end |
| 134 | +``` |
| 135 | + |
| 136 | +``` |
| 137 | +generate_brusselator_problem (generic function with 1 method) |
| 138 | +``` |
| 139 | + |
| 140 | + |
| 141 | + |
| 142 | + |
| 143 | + |
| 144 | + |
| 145 | +# Jacobian-Free Newton / TR Krylov Methods |
| 146 | + |
| 147 | +In this section, we will benchmark jacobian-free nonlinear solvers with Krylov methods. We |
| 148 | +will use preconditioning from `AlgebraicMultigrid.jl` and `IncompleteLU.jl`. Unfortunately, |
| 149 | +our ability to use 3rd party software is limited here, since only `Sundials.jl` supports |
| 150 | +jacobian-free methods via `:GMRES`. |
| 151 | + |
| 152 | +```julia |
| 153 | +using AlgebraicMultigrid, IncompleteLU |
| 154 | + |
| 155 | +incompletelu(W, p = nothing) = ilu(W, τ = 50.0), LinearAlgebra.I |
| 156 | + |
| 157 | +function algebraicmultigrid(W, p = nothing) |
| 158 | + return aspreconditioner(ruge_stuben(convert(AbstractMatrix, W))), LinearAlgebra.I |
| 159 | +end |
| 160 | + |
| 161 | +function algebraicmultigrid_jacobi(W, p = nothing) |
| 162 | + A = convert(AbstractMatrix, W) |
| 163 | + Dinv = 1.0 ./ diag(A) # PETSc-style Jacobi: inverse of diagonal |
| 164 | + smoother = AlgebraicMultigrid.Jacobi(Dinv) |
| 165 | + |
| 166 | + Pl = aspreconditioner(AlgebraicMultigrid.ruge_stuben( |
| 167 | + A, |
| 168 | + presmoother = smoother, |
| 169 | + postsmoother = smoother |
| 170 | + )) |
| 171 | + return Pl, LinearAlgebra.I |
| 172 | +end |
| 173 | + |
| 174 | +Ns = 2 .^ (2:7) |
| 175 | +krylov_dim = 1000 |
| 176 | + |
| 177 | +solvers_scaling_jacobian_free = [ |
| 178 | + (; pkg = :nonlinearsolve, name = "Newton Krylov", alg = NewtonRaphson(; linsolve = KrylovJL_GMRES())), |
| 179 | + (; pkg = :nonlinearsolve, name = "Newton Krylov (ILU)", alg = NewtonRaphson(; linsolve = KrylovJL_GMRES(; precs = incompletelu), concrete_jac = true)), |
| 180 | + (; pkg = :nonlinearsolve, name = "Newton Krylov (AMG)", alg = NewtonRaphson(; linsolve = KrylovJL_GMRES(; precs = algebraicmultigrid), concrete_jac = true)), |
| 181 | + (; pkg = :nonlinearsolve, name = "Newton Krylov (AMG Jacobi)", alg = NewtonRaphson(; linsolve = KrylovJL_GMRES(; precs = algebraicmultigrid_jacobi), concrete_jac = true)), |
| 182 | + |
| 183 | + (; pkg = :nonlinearsolve, name = "TR Krylov", alg = TrustRegion(; linsolve = KrylovJL_GMRES())), |
| 184 | + (; pkg = :nonlinearsolve, name = "TR Krylov (ILU)", alg = TrustRegion(; linsolve = KrylovJL_GMRES(; precs = incompletelu), concrete_jac = true)), |
| 185 | + (; pkg = :nonlinearsolve, name = "TR Krylov (AMG)", alg = TrustRegion(; linsolve = KrylovJL_GMRES(; precs = algebraicmultigrid), concrete_jac = true)), |
| 186 | + (; pkg = :nonlinearsolve, name = "TR Krylov (AMG Jacobi)", alg = TrustRegion(; linsolve = KrylovJL_GMRES(; precs = algebraicmultigrid_jacobi), concrete_jac = true)), |
| 187 | + |
| 188 | + (; pkg = :wrapper, name = "Newton Krylov [Sundials]", alg = KINSOL(; linear_solver = :GMRES, maxsetupcalls=1, krylov_dim)), |
| 189 | + |
| 190 | + (; pkg = :wrapper, name = "Newton Krylov [PETSc]", alg = PETScSNES(; snes_type = "newtonls", snes_linesearch_type = "basic", ksp_type = "gmres", snes_mf = true, ksp_gmres_restart = krylov_dim)), |
| 191 | + (; pkg = :wrapper, name = "Newton Krylov (ILU) [PETSc]", alg = PETScSNES(; snes_type = "newtonls", snes_linesearch_type = "basic", ksp_type = "gmres", pc_type = "ilu", ksp_gmres_restart = krylov_dim, pc_factor_levels = 0, pc_factor_drop_tolerance = 50.0)), |
| 192 | + (; pkg = :wrapper, name = "Newton Krylov (AMG) [PETSc]", alg = PETScSNES(; snes_type = "newtonls", snes_linesearch_type = "basic", ksp_type = "gmres", pc_type = "gamg", ksp_gmres_restart = krylov_dim)), |
| 193 | + (; pkg = :wrapper, name = "Newton Krylov (AMG Jacobi) [PETSc]", alg = PETScSNES(; snes_type = "newtonls", snes_linesearch_type = "basic", ksp_type = "gmres", pc_type = "gamg", mg_levels_ksp_type = "richardson", mg_levels_pc_type = "jacobi", ksp_gmres_restart = krylov_dim)), |
| 194 | + |
| 195 | + (; pkg = :wrapper, name = "TR Krylov (Not Matrix Free) [PETSc]", alg = PETScSNES(; snes_type = "newtontr", ksp_type = "gmres", ksp_gmres_restart = krylov_dim)), |
| 196 | + (; pkg = :wrapper, name = "TR Krylov (ILU) [PETSc]", alg = PETScSNES(; snes_type = "newtontr", ksp_type = "gmres", pc_type = "ilu", ksp_gmres_restart = krylov_dim, pc_factor_levels = 0, pc_factor_drop_tolerance = 50.0)), |
| 197 | + (; pkg = :wrapper, name = "TR Krylov (AMG) [PETSc]", alg = PETScSNES(; snes_type = "newtontr", ksp_type = "gmres", pc_type = "gamg", ksp_gmres_restart = krylov_dim)), |
| 198 | + (; pkg = :wrapper, name = "TR Krylov (AMG Jacobi) [PETSc]", alg = PETScSNES(; snes_type = "newtontr", ksp_type = "gmres", pc_type = "gamg", mg_levels_ksp_type = "richardson", mg_levels_pc_type = "jacobi", ksp_gmres_restart = krylov_dim)), |
| 199 | +] |
| 200 | + |
| 201 | +gc_disabled = false |
| 202 | +runtimes_scaling = fill(-1.0, length(solvers_scaling_jacobian_free), length(Ns)) |
| 203 | + |
| 204 | +for (j, solver) in enumerate(solvers_scaling_jacobian_free) |
| 205 | + alg = solver.alg |
| 206 | + name = solver.name |
| 207 | + |
| 208 | + if !gc_disabled && alg isa PETScSNES |
| 209 | + GC.enable(false) |
| 210 | + global gc_disabled = true |
| 211 | + @info "Disabling GC for $(name)" |
| 212 | + end |
| 213 | + |
| 214 | + for (i, N) in enumerate(Ns) |
| 215 | + prob = generate_brusselator_problem(N; sparsity = TracerSparsityDetector()) |
| 216 | + |
| 217 | + if (j > 1 && runtimes_scaling[j - 1, i] == -1) |
| 218 | + # The last benchmark failed so skip this too |
| 219 | + runtimes_scaling[j, i] = NaN |
| 220 | + @warn "$(name): Would Have Timed out" |
| 221 | + else |
| 222 | + function benchmark_function() |
| 223 | + termination_condition = (alg isa PETScSNES || alg isa KINSOL) ? |
| 224 | + nothing : |
| 225 | + NonlinearSolveBase.AbsNormTerminationMode(Base.Fix1(maximum, abs)) |
| 226 | + # PETSc doesn't converge properly |
| 227 | + tol = alg isa PETScSNES ? 1e-6 : 1e-4 |
| 228 | + sol = solve(prob, alg; abstol=tol, reltol=tol, |
| 229 | + linsolve_kwargs = (; abstol = 1e-8, reltol = 1e-8), |
| 230 | + termination_condition) |
| 231 | + if SciMLBase.successful_retcode(sol) || norm(sol.resid, Inf) ≤ 1e-4 |
| 232 | + runtimes_scaling[j, i] = @belapsed solve($prob, $alg; |
| 233 | + abstol=$tol, reltol=$tol, |
| 234 | + linsolve_kwargs = (; abstol = 1e-8, reltol = 1e-8), |
| 235 | + termination_condition=$termination_condition) |
| 236 | + else |
| 237 | + runtimes_scaling[j, i] = NaN |
| 238 | + end |
| 239 | + @info "$(name): $(runtimes_scaling[j, i]) | $(norm(sol.resid, Inf)) | $(sol.retcode)" |
| 240 | + end |
| 241 | + |
| 242 | + timeout(benchmark_function, 600) |
| 243 | + |
| 244 | + if runtimes_scaling[j, i] == -1 |
| 245 | + @warn "$(name): Timed out" |
| 246 | + runtimes_scaling[j, i] = NaN |
| 247 | + end |
| 248 | + end |
| 249 | + end |
| 250 | + |
| 251 | + println() |
| 252 | +end |
| 253 | +``` |
| 254 | + |
| 255 | + |
| 256 | + |
| 257 | + |
| 258 | +Plot the results. |
| 259 | + |
| 260 | +```julia |
| 261 | +fig = begin |
| 262 | + ASPECT_RATIO = 0.7 |
| 263 | + WIDTH = 1200 |
| 264 | + HEIGHT = round(Int, WIDTH * ASPECT_RATIO) |
| 265 | + STROKEWIDTH = 2.5 |
| 266 | + |
| 267 | + successful_solvers = map(x -> any(isfinite, x), eachrow(runtimes_scaling)) |
| 268 | + solvers_scaling_jacobian_free = solvers_scaling_jacobian_free[successful_solvers] |
| 269 | + runtimes_scaling = runtimes_scaling[successful_solvers, :] |
| 270 | + |
| 271 | + cycle = Cycle([:marker], covary = true) |
| 272 | + colors = cgrad(:tableau_20, length(solvers_scaling_jacobian_free); categorical = true) |
| 273 | + theme = Theme(Lines = (cycle = cycle,), Scatter = (cycle = cycle,)) |
| 274 | + LINESTYLES = Dict( |
| 275 | + :none => :solid, |
| 276 | + :amg => :dot, |
| 277 | + :amg_jacobi => :dash, |
| 278 | + :ilu => :dashdot, |
| 279 | + ) |
| 280 | + |
| 281 | + Ns_ = Ns .^ 2 .* 2 |
| 282 | + |
| 283 | + with_theme(theme) do |
| 284 | + fig = Figure(; size = (WIDTH, HEIGHT)) |
| 285 | + |
| 286 | + ax = Axis(fig[1, 1:2], ylabel = L"Time ($s$)", xlabel = L"Problem Size ($N$)", |
| 287 | + xscale = log2, yscale = log2, xlabelsize = 22, ylabelsize = 22, |
| 288 | + xticklabelsize = 20, yticklabelsize = 20, xtickwidth = STROKEWIDTH, |
| 289 | + ytickwidth = STROKEWIDTH, spinewidth = STROKEWIDTH) |
| 290 | + |
| 291 | + idxs = get_ordering(runtimes_scaling) |
| 292 | + |
| 293 | + ls, scs, labels = [], [], [] |
| 294 | + for (i, solver) in zip(idxs, solvers_scaling_jacobian_free[idxs]) |
| 295 | + all(isnan, runtimes_scaling[i, :]) && continue |
| 296 | + precon = occursin("AMG Jacobi", solver.name) ? :amg_jacobi : occursin("AMG", solver.name) ? :amg : occursin("ILU", solver.name) ? :ilu : :none |
| 297 | + linestyle = LINESTYLES[precon] |
| 298 | + l = lines!(Ns_, runtimes_scaling[i, :]; linewidth = 5, color = colors[i], |
| 299 | + linestyle) |
| 300 | + sc = scatter!(Ns_, runtimes_scaling[i, :]; markersize = 16, strokewidth = 2, |
| 301 | + color = colors[i]) |
| 302 | + push!(ls, l) |
| 303 | + push!(scs, sc) |
| 304 | + push!(labels, solver.name) |
| 305 | + end |
| 306 | + |
| 307 | + axislegend(ax, [[l, sc] for (l, sc) in zip(ls, scs)], labels, |
| 308 | + "Successful Solvers"; |
| 309 | + framevisible=true, framewidth = STROKEWIDTH, orientation = :vertical, |
| 310 | + titlesize = 20, labelsize = 16, position = :lt, nbanks = 2, |
| 311 | + tellheight = true, tellwidth = false, patchsize = (40.0f0, 20.0f0)) |
| 312 | + |
| 313 | + axislegend(ax, [ |
| 314 | + LineElement(; linestyle = :solid, linewidth = 5), |
| 315 | + LineElement(; linestyle = :dot, linewidth = 5), |
| 316 | + LineElement(; linestyle = :dash, linewidth = 5), |
| 317 | + LineElement(; linestyle = :dashdot, linewidth = 5), |
| 318 | + ], ["No Preconditioning", "AMG", "AMG Jacobi", "Incomplete LU"], |
| 319 | + "Preconditioning"; framevisible=true, framewidth = STROKEWIDTH, |
| 320 | + orientation = :vertical, titlesize = 20, labelsize = 16, |
| 321 | + tellheight = true, tellwidth = true, patchsize = (40.0f0, 20.0f0), |
| 322 | + position = :rb) |
| 323 | + |
| 324 | + fig[0, :] = Label(fig, |
| 325 | + "Brusselator 2D: Scaling of Jacobian-Free Nonlinear Solvers with Problem Size", |
| 326 | + fontsize = 24, tellwidth = false, font = :bold) |
| 327 | + |
| 328 | + return fig |
| 329 | + end |
| 330 | +end |
| 331 | +``` |
| 332 | + |
| 333 | + |
| 334 | + |
| 335 | +```julia |
| 336 | +save("brusselator_krylov_methods_scaling.svg", fig) |
| 337 | +``` |
| 338 | + |
| 339 | +``` |
| 340 | +CairoMakie.Screen{SVG} |
| 341 | +``` |
| 342 | + |
| 343 | + |
0 commit comments