GPU support
Every performance-critical path in TwoDG is written once, as KernelAbstractions kernels over plain arrays, and runs unchanged on CPU threads or a GPU. CUDA.jl is deliberately not a dependency: load it yourself and pass a device array type.
using TwoDG, CUDA
sol = solve(prob, RK4(); dt = compute_dt(prob), tfinal = 1.0, ArrayT = CuArray)ArrayT = CuArray works on DGProblem (the whole RK4 time loop), HDGProblem with GMRES (batched local solves, assembly, Krylov trace iterations, and recovery), and CGProblem with ConjugateGradient/GMRES (the matrix-free iteration). Any other KernelAbstractions backend array type works the same way.
What moves to the device — and what doesn't
Device: DG residuals and RK4 stages (DGContext + pointwise fluxes), LDG gradient/viscous kernels, HDG batched GEMMs and in-kernel LU (HDGBatch), the HDG trace matvec and block-Jacobi preconditioner (HDGSystem), the CG stiffness operator and Jacobi preconditioner.
CPU (by design): mesh generation and high-order node projection, ReferenceElement construction, one-time geometry precomputation, source-term evaluation (user closures), sparse direct factorizations (Direct — sparse LU/Cholesky is a poor GPU fit), and plotting. Setup runs once; the device gets what iterates.
The same applies in 3D — see 3D in TwoDG for measured 3D speedups and the quadrature/precision guidance that goes with them.
Working with device data directly
The low-level structs are Adapt-able — everything is a plain array field:
using Adapt
ctx = DGContext(master, mesh; T = Float32)
dctx = adapt(CuArray, ctx) # whole geometry cache on the GPU
phys_d = adapt(CuArray, phys) # DGPhysics: equation + BCs + fluxes
u_d = CuArray(Float32.(u0))
rk4_ka!(inviscid_residual!, dctx, phys_d, u_d, 0f0, dt, nstep)Precision
All solver structs are eltype-parametric; T = Float32 runs end to end in single precision, which matters on consumer GPUs (FP64 throughput is typically 1/32–1/64 of FP32). Empirical guidance from the smoke benchmarks (examples/dg/run_ka_cuda.jl):
- Explicit DG: Float32 is the right choice — residual noise matches the CPU's Float32 noise, and throughput wins grow with mesh size.
- HDG GMRES: use Float64 on the GPU. Single-precision Krylov iterations stagnate before tight tolerances on large trace systems; the matvec is memory-bound, so consumer-card FP64 throughput is not the bottleneck.
- CG conjugate gradients: Float64 matches the direct solve to ~1e-14; Float32 converges to ~1e-4 solution accuracy.
Small meshes are launch-overhead-bound — the GPU pays off at scale (tens of thousands of elements), and for one-shot 2D solves the CPU sparse direct path is often still the fastest option. Benchmark before assuming.
Running the smoke benchmark
examples/dg/run_ka_cuda.jl exercises DG, LDG, HDG, and CG on the GPU with correctness assertions and timings. CUDA.jl is best installed once into a shared environment:
julia -e 'using Pkg; Pkg.activate("cuda"; shared=true); Pkg.add("CUDA")'
JULIA_LOAD_PATH='@;@cuda;@v#.#;@stdlib' julia --project=. examples/dg/run_ka_cuda.jl