Summary
docs/src/solve/gpu.md is the only page in docs/ that opts out of execution
(Draft = true, plain ```julia fences instead of ```@example). It reads as
reference prose, not as tested output, and it's currently annotated as such. This issue is
about (1) making the page genuinely executable everywhere, using a pattern already proven
elsewhere in these docs, and (2) a follow-up, cross-repo question about whether we also want
the deployed dev/stable docs to show real GPU numbers rather than a graceful CPU fallback.
Part 1 — make gpu.md executable (self-contained, no CI change needed)
We don't need a GPU to make this page @example-based. docs/src/getting-started/guided-tour.md:376-386
already does exactly this, today, on plain ubuntu-latest:
try
global sol_gpu = solve(goddard, :gpu; grid_size=1000, display=false)
println("GPU solve succeeded — a functional GPU is available.")
catch e
println("GPU solve failed, as expected without a functional GPU.")
println("CUDA.functional() = ", CUDA.functional())
println("Exception: ", first(sprint(showerror, e), 400))
end
That block is a real @example, executed at every build, on every runner — it just prints a
different (still true) story depending on whether CUDA is functional. gpu.md should adopt
the same pattern instead of Draft = true:
- Drop the
@meta Draft = true block and the "This page doesn't execute" admonition.
- Convert the
julia fences under "Prerequisites", "Descriptive mode", "Explicit mode" and
"Performance notes" to @example gpu (shared session) blocks.
- Wrap the actual
:gpu solve calls in the same CUDA.functional() ? … : … /
try/catch style as guided-tour.md, so the page is honest about what ran where.
docs/Project.toml already depends on CUDA, MadNLPGPU and ExaModels (used by
guided-tour.md and others already), so no dependency work is needed for this part.
- The "What combinations work" section (
OptimalControl.ADNLP{GPU}() → TypeError, etc.) is
actually CPU-safe to execute for real right now — those are type-system errors, not
hardware-dependent ones — and should move from prose to executed @example/@repl blocks
wrapped in try/catch showing the real exception, same spirit as everything else on the
page.
This part is low-risk, self-contained in this repo, and doesn't depend on anything below.
Part 2 — real GPU numbers in the deployed dev/stable docs (cross-repo, needs a decision)
Part 1 makes every build honest, but on ubuntu-latest (what Documentation.yml uses today)
the deployed page will always show the CPU-fallback branch — never real GPU output — because
GitHub-hosted runners have no CUDA device. If we want the live dev/stable docs to show actual
GPU numbers, the Documenter build itself has to run on a CUDA-capable machine at least for the
runs that get deployed.
What's actually available, and a naming wrinkle worth resolving first
The org has two self-hosted GPU boxes, not one, and they are not interchangeable/renamed
versions of each other — see Handbook/WORKFLOWS.md:151-173:
kkt — the original self-hosted GPU runner.
occidata — added 2026-08-24, "for heavier GPU workloads", alongside kkt, not a
replacement (per the Handbook). It runs as a SLURM job
(CTActions/.github/workflows/occidata-runner-maintenance.yml,
occidata-runner-watchdog.yml: sbatch /projects/ctb/gha-runner/runner.slurm on
occidata-cluster.irit.fr), watched by a daily cron (15 3 * * *) that restarts it if
offline — i.e. it can legitimately be queued or down for up to ~24h before anything notices.
OptimalControl's own CI is still on kkt only — .github/workflows/CI.yml has
test-gpu-kkt (runs_on: '[["kkt"]]', label run ci kkt-runner), no occidata job, no
occidata-runner label defined in this repo (gh label list confirms). Meanwhile
CTParser.jl, CTDirect.jl have already moved their GPU CI to occidata only, CTFlows.jl
runs both, and Handbook/WORKFLOWS.md's own summary table (row occidata-runner) still only
lists CTFlows.jl (CTLie planned) — i.e. the Handbook table itself is already lagging the actual
per-repo CI.yml files. Worth deciding explicitly, not by drift:
- Does OptimalControl's GPU-backed docs build target
kkt, occidata, or both?
- Should this be the moment OptimalControl's GPU test CI also picks up
occidata
(matching CTParser/CTDirect), independent of the docs question?
I don't have a strong opinion on kkt vs occidata from here — whichever box is expected to
carry heavier/longer jobs reliably is probably right for a full Documenter build (it's a bigger
job than a test suite run), which points at occidata, but that's a call for whoever owns
those machines.
Reliability constraint this drives the design
Given the SLURM-queue + daily-watchdog behavior above, a GPU self-hosted box should not be
the sole path to publishing docs — Documentation.yml deploys on every push to main (dev)
and every version tag (stable); if the only build path depends on a box that can be queued or
silently offline for hours, doc publishing stalls with it.
Proposed shape: a job that attempts the GPU runner first (with a bounded timeout to absorb
SLURM queueing) and falls back to the current ubuntu-latest build automatically if it
doesn't complete in time — for both the main push (dev) and tag (stable) triggers. This needs
changes in the reusable CTActions/.github/workflows/documentation.yml (currently a single
hardcoded runs-on: ubuntu-latest job) plus the calling Documentation.yml here. PR-preview
builds (run documentation label) can stay ubuntu-latest-only — no need for real GPU numbers
on every PR iteration, and it keeps preview builds fast.
This part touches a shared reusable workflow used by every package in the org
(documentation.yml), so it should be scoped and reviewed as its own piece of work, not bundled
silently into a docs content PR.
Suggested split
References
Summary
docs/src/solve/gpu.mdis the only page indocs/that opts out of execution(
Draft = true, plain```juliafences instead of```@example). It reads asreference prose, not as tested output, and it's currently annotated as such. This issue is
about (1) making the page genuinely executable everywhere, using a pattern already proven
elsewhere in these docs, and (2) a follow-up, cross-repo question about whether we also want
the deployed dev/stable docs to show real GPU numbers rather than a graceful CPU fallback.
Part 1 — make
gpu.mdexecutable (self-contained, no CI change needed)We don't need a GPU to make this page
@example-based.docs/src/getting-started/guided-tour.md:376-386already does exactly this, today, on plain
ubuntu-latest:That block is a real
@example, executed at every build, on every runner — it just prints adifferent (still true) story depending on whether CUDA is functional.
gpu.mdshould adoptthe same pattern instead of
Draft = true:@meta Draft = trueblock and the "This page doesn't execute" admonition.juliafences under "Prerequisites", "Descriptive mode", "Explicit mode" and"Performance notes" to
@example gpu(shared session) blocks.:gpusolve calls in the sameCUDA.functional() ? … : …/try/catchstyle asguided-tour.md, so the page is honest about what ran where.docs/Project.tomlalready depends onCUDA,MadNLPGPUandExaModels(used byguided-tour.mdand others already), so no dependency work is needed for this part.OptimalControl.ADNLP{GPU}()→TypeError, etc.) isactually CPU-safe to execute for real right now — those are type-system errors, not
hardware-dependent ones — and should move from prose to executed
@example/@replblockswrapped in
try/catchshowing the real exception, same spirit as everything else on thepage.
This part is low-risk, self-contained in this repo, and doesn't depend on anything below.
Part 2 — real GPU numbers in the deployed dev/stable docs (cross-repo, needs a decision)
Part 1 makes every build honest, but on
ubuntu-latest(whatDocumentation.ymluses today)the deployed page will always show the CPU-fallback branch — never real GPU output — because
GitHub-hosted runners have no CUDA device. If we want the live dev/stable docs to show actual
GPU numbers, the Documenter build itself has to run on a CUDA-capable machine at least for the
runs that get deployed.
What's actually available, and a naming wrinkle worth resolving first
The org has two self-hosted GPU boxes, not one, and they are not interchangeable/renamed
versions of each other — see
Handbook/WORKFLOWS.md:151-173:kkt— the original self-hosted GPU runner.occidata— added 2026-08-24, "for heavier GPU workloads", alongsidekkt, not areplacement (per the Handbook). It runs as a SLURM job
(
CTActions/.github/workflows/occidata-runner-maintenance.yml,occidata-runner-watchdog.yml:sbatch /projects/ctb/gha-runner/runner.slurmonoccidata-cluster.irit.fr), watched by a daily cron (15 3 * * *) that restarts it ifoffline — i.e. it can legitimately be queued or down for up to ~24h before anything notices.
OptimalControl's own CI is still on
kktonly —.github/workflows/CI.ymlhastest-gpu-kkt(runs_on: '[["kkt"]]', labelrun ci kkt-runner), nooccidatajob, nooccidata-runnerlabel defined in this repo (gh label listconfirms). MeanwhileCTParser.jl,CTDirect.jlhave already moved their GPU CI tooccidataonly,CTFlows.jlruns both, and
Handbook/WORKFLOWS.md's own summary table (rowoccidata-runner) still onlylists CTFlows.jl (CTLie planned) — i.e. the Handbook table itself is already lagging the actual
per-repo
CI.ymlfiles. Worth deciding explicitly, not by drift:kkt,occidata, or both?occidata(matching CTParser/CTDirect), independent of the docs question?
I don't have a strong opinion on
kktvsoccidatafrom here — whichever box is expected tocarry heavier/longer jobs reliably is probably right for a full Documenter build (it's a bigger
job than a test suite run), which points at
occidata, but that's a call for whoever ownsthose machines.
Reliability constraint this drives the design
Given the SLURM-queue + daily-watchdog behavior above, a GPU self-hosted box should not be
the sole path to publishing docs —
Documentation.ymldeploys on every push tomain(dev)and every version tag (stable); if the only build path depends on a box that can be queued or
silently offline for hours, doc publishing stalls with it.
Proposed shape: a job that attempts the GPU runner first (with a bounded timeout to absorb
SLURM queueing) and falls back to the current
ubuntu-latestbuild automatically if itdoesn't complete in time — for both the
mainpush (dev) and tag (stable) triggers. This needschanges in the reusable
CTActions/.github/workflows/documentation.yml(currently a singlehardcoded
runs-on: ubuntu-latestjob) plus the callingDocumentation.ymlhere. PR-previewbuilds (
run documentationlabel) can stayubuntu-latest-only — no need for real GPU numberson every PR iteration, and it keeps preview builds fast.
This part touches a shared reusable workflow used by every package in the org
(
documentation.yml), so it should be scoped and reviewed as its own piece of work, not bundledsilently into a docs content PR.
Suggested split
gpu.mdto be fully@example-based, followingguided-tour.md'sCUDA.functional()/try-catchpattern. No CI change required.kktvsoccidata(or both) as the target for a GPU-backed docs build,and whether to also bring OptimalControl's GPU test CI onto
occidatawhile we're at it.CTActions+ here): add a GPU-attempt-with-CPU-fallback job tothe reusable
documentation.yml, wired for themain-push and tag triggers here.Handbook/WORKFLOWS.md§3.1/§3.2 once the above lands, since itsoccidata-runneradoption table is already out of sync with
CTParser.jl/CTDirect.jl's actualCI.yml.References
docs/src/solve/gpu.md(page in question)docs/src/getting-started/guided-tour.md:376-386(the graceful-degradation pattern to reuse).github/workflows/CI.yml,.github/workflows/Documentation.yml(this repo)CTActions/.github/workflows/documentation.yml,ci.yml,occidata-runner-watchdog.yml,occidata-runner-maintenance.ymlHandbook/WORKFLOWS.md:151-224(label/runner conventions, current adoption table)Handbook/philosophy/testing.md:122-133(on_gpu_runner()capability-gating precedent)