GPU
GPU support runs through ExaModels.jl and MadNLPGPU.jl, NVIDIA GPUs only, via CUDA.jl.
What you are reading depends on the machine that built this page
Every block below is executed when the documentation is built. With a functional CUDA device you are reading real GPU output; without one, you are reading the failure this exact code really produces. The first block says which of the two it is.
Prerequisites
using OptimalControl
using MadNLPGPU
using CUDA
using CUDSS
println("CUDA.functional() = ", CUDA.functional())CUDA.functional() = falseCheck CUDA.functional() before assuming a :gpu solve will actually run on the device.
All three, and CUDSS is the one you will forget
The GPU solvers need MadNLPGPU, CUDA and CUDSS, loaded together. CUDSS (the sparse linear solver on the GPU) is a weak dependency of MadNLPGPU: using MadNLPGPU does not load it. Load only the first two, and the GPU solve fails with an ExtensionError that reports Missing CUDSS, with the hint using CUDSS.
ExaModels needs no using of its own: it comes with OptimalControl, and :exa works without it. Importing it explicitly would also bring its objective and constraint into scope, which collide with the accessors of the same name. If you need ExaModels' own API, import it qualified: using ExaModels: ExaModels.
The problem
:exa, the only GPU-capable modeler, needs a problem written with @def. The dynamics and the path constraints can be written in vector form, as here, or coordinate by coordinate:
ocp = @def begin
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
v ∈ R, variable
x(0) == [0, 1]
x(1) == [0, -1]
ẋ(t) == [x₂(t), u(t)]
0 ≤ x₁(t) + v^2 ≤ 1.1
-10 ≤ u(t) ≤ 10
1 ≤ v ≤ 2
∫(u(t)^2 + v) → min
endAbstract definition:
t ∈ [0, 1], time
x ∈ R², state
u ∈ R, control
v ∈ R, variable
x(0) == [0, 1]
x(1) == [0, -1]
ẋ(t) == [x₂(t), u(t)]
0 ≤ x₁(t) + v ^ 2 ≤ 1.1
-10 ≤ u(t) ≤ 10
1 ≤ v ≤ 2
∫(u(t) ^ 2 + v) → min
The (autonomous) optimal control problem is of the form:
minimize J(x, u, v) = ∫ f⁰(x(t), u(t), v) dt, over [0, 1]
subject to
ẋ(t) = f(x(t), u(t), v), t in [0, 1] a.e.,
ψ₋ ≤ ψ(x(t), u(t), v) ≤ ψ₊,
ϕ₋ ≤ ϕ(x(0), x(1), v) ≤ ϕ₊,
u₋ ≤ u(t) ≤ u₊,
v₋ ≤ v ≤ v₊,
where x(t) ∈ R², u(t) ∈ R and v ∈ R.Descriptive mode
The :gpu parameter token selects GPU-optimized defaults:
try
global sol = solve(ocp, :exa, :madnlp, :gpu; grid_size=100, display=false)
println("objective = ", objective(sol))
println("iterations = ", iterations(sol))
catch e
println("GPU solve failed — no functional device on this machine.")
println("CUDA.functional() = ", CUDA.functional())
println("Exception: ", first(sprint(showerror, e), 400))
end┌ Warning: CUDA is loaded but not functional. GPU backend may not work properly.
└ @ CTSolversCUDA ~/.julia/packages/CTSolvers/uc2u1/ext/CTSolversCUDA.jl:31
GPU solve failed — no functional device on this machine.
CUDA.functional() = false
Exception: CUDA driver not functionalCompletion fills in the rest — the first match containing :gpu is the same method:
try
global sol = solve(ocp, :gpu; grid_size=100, display=false)
println("objective = ", objective(sol))
catch e
println("Exception: ", first(sprint(showerror, e), 400))
end┌ Warning: CUDA is loaded but not functional. GPU backend may not work properly.
└ @ CTSolversCUDA ~/.julia/packages/CTSolvers/uc2u1/ext/CTSolversCUDA.jl:31
Exception: CUDA driver not functionalSolver verbosity is a separate concern: display=false above silences the OptimalControl-level report, and the underlying solver takes its own options — print_level=MadNLP.ERROR for MadNLP, which needs using MadNLP in scope. See Options.
What :gpu changes
describe(:gpu) lists every strategy with a GPU variant. It needs nothing GPU-specific and runs on CPU alone:
describe(:gpu)GPU (parameter)
├─ id: :gpu
├─ hierarchy: GPU → AbstractStrategyParameter
├─ description: GPU-based computation
│
└─ used by strategies (5):
├─ :di (AbstractADBackend) → DifferentiationInterface{O}
├─ :exa (AbstractNLPModeler) → Exa{GPU}
├─ :madncl (AbstractNLPSolver) → MadNCL{GPU}
├─ :madnlp (AbstractNLPSolver) → MadNLP{GPU}
└─ :sciml (AbstractIntegrator) → SciML{O, OP, OT}Only :exa, :madnlp and :madncl have one. The parameter changes the defaults of their options. Constructing the strategies does not touch the device, so this runs anywhere:
modeler = OptimalControl.Exa{GPU}()
solver = OptimalControl.MadNLP{GPU}()
println("Exa{GPU} backend: ", options(modeler)[:backend])
println("MadNLP{GPU} linear_solver: ", options(solver)[:linear_solver])Exa{GPU} backend: CUDACore.CUDAKernels.CUDABackend(false, false)
MadNLP{GPU} linear_solver: MadNLPGPUCUDAExt.CUDSSSolvermodeler = OptimalControl.Exa() # same as OptimalControl.Exa{CPU}()
solver = OptimalControl.MadNLP() # same as OptimalControl.MadNLP{CPU}()
println("Exa{CPU} backend: ", options(modeler)[:backend])
println("MadNLP{CPU} linear_solver: ", options(solver)[:linear_solver])Exa{CPU} backend: nothing
MadNLP{CPU} linear_solver: MumpsSolverOn the GPU, ExaModels evaluates the model with a CUDA backend, and MadNLP factorizes its linear systems with cuDSS instead of MUMPS.
Explicit mode
Constructing the components does not touch the device, so this block runs anywhere:
discretizer = OptimalControl.Collocation(; grid_size=100, scheme=:midpoint)
modeler = OptimalControl.Exa{GPU}()
solver = OptimalControl.MadNLP{GPU}()┌ Warning: CUDA is loaded but not functional. GPU backend may not work properly.
└ @ CTSolversCUDA ~/.julia/packages/CTSolvers/uc2u1/ext/CTSolversCUDA.jl:31Running them is what needs the hardware:
try
global sol = solve(ocp; discretizer=discretizer, modeler=modeler, solver=solver)
println("objective = ", objective(sol))
catch e
println("Exception: ", first(sprint(showerror, e), 400))
end▫ This is OptimalControl 2.2.1-beta, solving with: collocation → exa → madnlp (gpu)
📦 Configuration:
├─ Discretizer: collocation (grid_size = 100, scheme = midpoint)
├─ Modeler: exa (backend = CUDACore.CUDAKernels.CUDABackend(false, false) [gpu-dependent])
└─ Solver: madnlp (linear_solver = MadNLPGPUCUDAExt.CUDSSSolver [gpu-dependent])
▫ Exception: CUDA driver not functionalWhat combinations work
Only :exa × {:madnlp, :madncl} on :gpu — the two :gpu entries of methods() (see Choosing a method). Everything else is a compile-time or runtime error. These are type-system and routing errors, not hardware-dependent ones, so they raise identically on every machine:
ADNLP's parameter is constrained to <:CPU:
julia> OptimalControl.ADNLP{GPU}()
TypeError: in ADNLP, in P, expected P<:CPU, got Type{GPU}Same for Ipopt:
julia> OptimalControl.Ipopt{GPU}()
TypeError: in Ipopt, in P, expected P<:CPU, got Type{GPU}Descriptively, the same combinations fail earlier still — no entry in methods() carries :adnlp together with :gpu, so completion cannot resolve the description at all:
julia> solve(ocp, :adnlp, :gpu)
AmbiguousDescription → _complete_description, strategy_builders.jl:260
│
│ cannot find matching description
│
│ Diagnostic No complete match — no description contains all symbols
│ Requested (:adnlp, :gpu)
│ Available (:collocation, :adnlp, :ipopt, :cpu)
│ (:collocation, :adnlp, :madnlp, :cpu)
│ (:collocation, :adnlp, :uno, :cpu)
│ (:collocation, :adnlp, :madncl, :cpu)
│ (:collocation, :adnlp, :knitro, :cpu)
│ (:collocation, :exa, :ipopt, :cpu)
│ (:collocation, :exa, :madnlp, :cpu)
│ (:collocation, :exa, :uno, :cpu)
│ (:collocation, :exa, :madncl, :cpu)
│ (:collocation, :exa, :knitro, :cpu)
│ (:collocation, :exa, :madnlp, :gpu)
│ (:collocation, :exa, :madncl, :gpu)
│
│ Context description completion
│ Hint Try one of the closest matches:
│ (:collocation, :exa, :madnlp, :gpu)
│ (:collocation, :exa, :madncl, :gpu)
│ (:collocation, :adnlp, :uno, :cpu)
│ (:collocation, :adnlp, :madnlp, :cpu)
│ (:collocation, :adnlp, :madncl, :cpu)
└─Performance notes
GPU solving is beneficial for:
large-scale problems: thousands of variables and constraints, for instance fine grids;
dense computations: problems with many nonlinear constraints;
repeated solves: the GPU initialization overhead is paid once.
For small problems, CPU solving is usually faster.
The idiomatic guard is CUDA.functional(): pick the parameter, then solve:
parameter = CUDA.functional() ? :gpu : :cpu
println("parameter = ", parameter)parameter = cpuOn a machine with a functional GPU, this block compares the same method on CPU and GPU, on a fine grid (each solve is run once first, so that compilation is not timed):
using MadNLP
if CUDA.functional()
for p in (:cpu, :gpu)
solve(ocp, :exa, :madnlp, p; grid_size=1000, display=false, print_level=MadNLP.ERROR)
t = @elapsed solve(ocp, :exa, :madnlp, p; grid_size=1000, display=false, print_level=MadNLP.ERROR)
println(p, " at grid_size=1000: ", round(t; digits=3), " s")
end
else
println("No functional GPU on the machine that built this page: no timing to compare.")
endNo functional GPU on the machine that built this page: no timing to compare.See also
Solve overview — CPU solving basics.
Choosing a method — the full method list, GPU entries included.
Explicit mode — typed components in general.
The same
:cpu/:gpudistinction applies toFlow; see Flows overview.