Skip to content

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 ​

julia
using OptimalControl
using MadNLPGPU
using CUDA
using CUDSS

println("CUDA.functional() = ", CUDA.functional())
CUDA.functional() = false

Check 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:

julia
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
end
Abstract 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:

julia
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 functional

Completion fills in the rest — the first match containing :gpu is the same method:

julia
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 functional

Solver 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:

julia
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:

julia
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.CUDSSSolver
julia
modeler = 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: MumpsSolver

On 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:

julia
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:31

Running them is what needs the hardware:

julia
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 functional

What 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
julia> OptimalControl.ADNLP{GPU}()
TypeError: in ADNLP, in P, expected P<:CPU, got Type{GPU}

Same for Ipopt:

julia
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
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:

julia
parameter = CUDA.functional() ? :gpu : :cpu
println("parameter = ", parameter)
parameter = cpu

On 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):

julia
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.")
end
No functional GPU on the machine that built this page: no timing to compare.

See also ​