Hi everyone,
I’m running an ensemble of small (2–6 state) stiff/DAE thermal RC-network models (ModelingToolkit-built, many independent “buildings” of the same model structure with their own physical parameters) and trying to parallelize the ensemble solve on GPU via DiffEqGPU.jl using EnsembleGPUArray.
Setup: MTK-built RC network → structural_simplify(...; split=false) → ODEProblem → EnsembleProblem → solve(..., Rodas5P(autodiff=AutoFiniteDiff()), EnsembleGPUArray(CUDA.CUDABackend()); trajectories=n, saveat=...).
Symptom: a single trajectory (n=1) solves within a ~3s. The moment I batch ≥2 trajectories, cost jumps dramatically — e.g. ~300 - 2000s real execution time for 6 trajectories.
MWE (2-state analog: one differential + one algebraic state, small per-trajectory forcing table, partly vibe-coded to be honest):
using OrdinaryDiffEq, DiffEqGPU, CUDA, StaticArrays
const N_TIMEPOINTS = 24
const DT = 3600.0f0
const ncases = 2
function make_table(n)
SMatrix{N_TIMEPOINTS, n, Float32}(Float32.(5 .+ 10 .* rand(N_TIMEPOINTS, n)))
end
const TABLE = make_table(ncases)
function lookup(t, case_id)
normalized_t = t / DT
lower_idx = clamp(floor(Int, normalized_t) + 1, 1, N_TIMEPOINTS - 1)
frac = normalized_t - floor(normalized_t)
col = Int(case_id)
tbl = TABLE
return tbl[lower_idx, col] + frac * (tbl[lower_idx + 1, col] - tbl[lower_idx, col])
end
function thermal_rhs!(dT, T, p, t)
R1, C1, Cond, case_id = p[1], p[2], p[3], p[4]
Tout = lookup(t, case_id)
dT[1] = (Tout - T[1]) / (R1 * C1) # differential state
dT[2] = Cond * (T[1] - T[2]) # algebraic constraint
end
Mass_mat = [1.0 0.0; 0.0 0.0]
u0 = Float32[20.0, 20.0]
tspan = (0.0, 24 * 3600.0 * 30)
p0 = Float32[1.0, 1000.0, 5.0, 1.0]
func = ODEFunction(thermal_rhs!, mass_matrix = Mass_mat)
prob = ODEProblem(func, u0, tspan, p0)
function assign_case(prob, ctx)
case_id = Float32(getfield(ctx, :sim_id))
newp = copy(prob.p)
newp[4] = Float32(case_id)
remake(prob; p = newp)
end
ensprob = EnsembleProblem(prob; prob_func = assign_case, safetycopy = false)
println("Starting single solve.")
@time sol1 = solve(ensprob, Rodas5P(autodiff = AutoFiniteDiff()),
EnsembleGPUArray(CUDA.CUDABackend());
trajectories = 1, saveat = 3600.0f0)
println("Single done, converged: $(sol1.converged), steps: ", sol1.stats.naccept + sol1.stats.nreject)
@time sol6 = solve(ensprob, Rodas5P(autodiff = AutoFiniteDiff()),
EnsembleGPUArray(CUDA.CUDABackend());
trajectories = ncases, saveat = 3600.0f0)
println("$(ncases) cases done, converged: $(sol6.converged)")
On my machine n=1 finishes in ~3 s (~11 M allocations); n=2 ranges between ~22 and 90s (140 / 550 M allocations), n=10 gives 50 - 100s (270 - 520 M allocations) and n=100 gives ~280 s (1.3 G allocations). I am aware that compared to CPU parallelized versions, this is likely to only produce comparable results once I scale the problem size up to a few thousand, however these high execution times were making me sceptical about the feasibility of scaling this up further. Where is this extreme jump between a single case and 2 cases coming from? Is this expected/known behaviour? Is there any way to improve these runtimes?
I am still rather new to Julia and GPU computing in general, so I’d be very grateful for any pointers you might have! I am working with Julia 1.12.6, DiffEqGPU 3.15.4 and CUDA.jl 6.2.1.