perf: upgrade INS.jl tesseract to v5.0.0 (native ν gradient, in-place forward, float32)#103
perf: upgrade INS.jl tesseract to v5.0.0 (native ν gradient, in-place forward, float32)#103agdestein wants to merge 4 commits into
Conversation
- Pin IncompressibleNavierStokes 5.0.0 (was 4.0.0). - ns_vjp: take grad_nu from the same Zygote pullback as grad_v0/grad_dt; v5's diffusion rrule provides the viscosity cotangent (exact, the term is linear in nu). Removes the finite-difference fallback and its two extra forward rollouts per VJP call (~25-36% of reported VJP time, which vjp_cost never requested). - ns_apply: forward-only rollouts use the in-place solve_unsteady stepper (RK44, same per-stage BC + projection as the projected RHS); 4-6x faster than the non-mutating AD-ready rollout at identical interiors. The VJP primal keeps the non-mutating path. - Build the Setup from Float32 coordinates and make the collocated<-> staggered helpers eltype-preserving. Previously the Float64 grid (and a bare 0.5 literal) silently promoted the state and the Zygote tape to Float64 after the first projection, despite Float32 casts at every interface; internal_dtype now truthfully float32. - Drop the redundant per-stage ghost wrapping in the RK4 rollout: create_right_hand_side applies periodic BCs to its input and projects its output, so the refresh was dead work (~19% of the rollout plus Zygote tape nodes); interiors are bitwise identical. Validated against v5.0.0 in 2D and 3D: in-place vs non-mutating forward agree to Float32 round-off (2e-4; 4e-14 in a Float64 A/B), grad_v0 and grad_dt match central FD to ~1e-3, grad_nu converges to the AD value as the FD step grows (noise-limited by Float32 loss quantization), and TGV kinetic-energy decay matches the analytic rate at the solver's known spatial error. Channel/obstacle code paths (excluded from experiments) are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gdif1Cu1pdCJENe2hNUaF1
CLA signatures confirmedAll contributors have signed the Contributor License Agreement. |
|
@PasteurBot recheck |
Following up on the invitation in agdestein/IncompressibleNavierStokes.jl#197 — thanks for including INS.jl! This first PR only corrects a few statements about the solver; the companion upgrade PR is #103. Suggested label: `benchmark:none` — strings and metadata only, no behavior change. ## Changes - **`INS_JL_NO_OBSTACLE` exclusion text.** The current text says the cylinder "cannot be represented in INS.jl" and that its "spectral/LU pressure projection is also periodic-only". Neither is accurate: - `psolver_direct` (LU) assembles the pressure Laplacian for any supported BC combination — it is the general-BC fallback that `default_psolver` selects for non-periodic setups. `psolver_transform` (DCT) supports Dirichlet BCs, as this repo's own channel code uses. Only `psolver_spectral` (FFT) is periodic-only. - Brinkman volume penalization is a momentum forcing term, and a *finite* penalization `−(χ/η)·u` added to the RHS before projection is smooth and stable. What diverges is this tesseract's *hard-zero masking after each projection* (an infinite-penalization clamp the projection cannot see). INS.jl also natively supports the channel configuration (`DirichletBC(f)` inflow, `PressureBC` outflow, periodic lateral BCs). The exclusion itself stays (that route isn't implemented in the tesseract); only the attribution is corrected. - **`discretization: FD` → `FV`.** INS.jl is a staggered finite-volume (energy-conserving) discretization; `FV` is among the documented options for this field. - **"CPU-only" description.** The wrapper runs on CPU, but the library's kernels are KernelAbstractions.jl and run on CUDA (including the spectral pressure solver and the differentiable path). Reworded so readers don't take the cost tables as a solver property; `uses_gpu: false` is kept as a property of this tesseract. No results change; the generated solver/results pages pick these up on the next docs build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Gdif1Cu1pdCJENe2hNUaF1 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Dion Häfner <dion.haefner@simulation.science>
Fork PRs (e.g. #103) fail benchmark CI because they cannot access the private mosaic/* GHCR packages: the read-only fork token is denied `packages: write` (build push fails) and cannot read the private package either (every image pull returns `manifest unknown`). Both the changed solver's head-SHA image and the unchanged solvers' base-SHA images are unreachable, so the benchmark run jobs fail with "no image found". Split benchmark CI along a trust boundary, mirroring the existing docs-publish workflow_run pattern: - benchmark.yml (on pull_request, fork context, read-only, no secrets) now only plans and hands off. `plan-gate` (renamed from the old `bench-ok` merge-gate job) enforces the label policy and seeds the `bench-ok` commit status: failure on a policy violation, immediate success when nothing will run, else pending. Seeds are best-effort so a fork token that can't write a status doesn't cascade. A new `handoff` job uploads the validated plan outputs as an artifact. - benchmark-execute.yml (new, on workflow_run of "Benchmark", trusted base-repo context) recovers + validates the handoff, then builds and pushes the changed solver image to private GHCR, runs the benchmarks (pulling the private images), renders the docs preview, and posts the authoritative `bench-ok` commit status plus the PR comment / RTD preview. Its `finalize` job always resolves a seeded pending — posting `error` if the handoff can't be recovered on a successful planner run — so the gate can never deadlock in `pending`. The status step is gated on the planner having concluded success, so it never clobbers a correct terminal status set by plan-gate. - build-tesseracts.yml gains `ref`/`image_sha` inputs so the trusted run builds the PR's HEAD and tags the image to match what the benchmark run pulls; the fork-tolerant `continue-on-error` push is removed (both remaining callers hold packages:write). - docs-publish.yml is retired: workflow_run cannot chain, so its RTD + PR-comment steps fold into benchmark-execute's finalize job. The reusable build/benchmark calls drop `secrets: inherit`: the jobs that build and run fork-authored solver code see only the ephemeral GITHUB_TOKEN, keeping RTD_TOKEN / the bot PAT out of reach. Branch protection keeps requiring the `bench-ok` context unchanged — it is now a commit status rather than a job, posted only by the two paths above, so a PR cannot merge until real benchmarks pass. workflow_run changes only take effect once on main, so the fork path must be verified by re-running a fork PR after merge. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📊 View the full benchmark resultsThe rendered docs preview has every plot for this run (forward accuracy, Status diff vs baseLegend · ✅ ok · 🟠 anom · ❌ fail · · missing · 🚫 excluded (permanent — out of score denominator) · ⚪ excluded (work-to-do) · * stale — result predates current benchmark run No status changes. · score 0.98 → 0.95 Full Mosaic statusMosaic statusLegend · ✅ ok · 🟠 anom · ❌ fail · · missing · 🚫 excluded (permanent — out of score denominator) · ⚪ excluded (work-to-do) · * stale — result predates current benchmark run Each solver is run against every experiment in the suite. ok = produced valid results; fail = crashed or returned invalid data; anom = ran successfully but tripped an automated quality check (e.g. poor gradient accuracy, outlier wall-clock time, or diverged optimisation). Thresholds are defined per-problem in the problem config.
Failures & anomalies
ns-3d-grid — 16 experiment(s)
ns-grid — 20 experiment(s)
structural-mesh — 10 experiment(s)
thermal-mesh — 14 experiment(s)
|
Follow-up to agdestein/IncompressibleNavierStokes.jl#197 and the companion wording PR (#102). This upgrades the INS.jl tesseract to the just-released v5.0.0 and fixes three things that skewed the reported numbers. Every INS.jl symbol used by this tesseract is unchanged in v5.0.0, so the upgrade itself is just the version pin.
Suggested label:
benchmark:solver— INS.jl numbers change; please re-run theins_jlcells.Changes that affect published numbers
INS.jl v5.0.0's
diffusionrrule provides the ν cotangent (the diffusive term is linear in ν, so it is exact).ns_vjpnow takesgrad_v0,grad_dt, andgrad_nufrom a single Zygote pullback. This removes two full forward rollouts from every VJP call — the FD fallback ran unconditionally, even thoughvjp_cost(jax.gradw.r.t.v0) never requested the ν gradient, inflating all reported INS.jl VJP times by ~25–36%.solve_unsteadyinstead of the AD-ready rollout.applyhas no gradient tape to build, so it now uses the library's in-place time stepper (RKMethods.RK44, which applies the same per-stage BC + projection as the projected RHS used for the VJP primal). Measured at the 2-D cost point (N=256, 100 steps, CPU): 2.49 s → 0.56 s, with interiors agreeing to round-off (≤4e-14 relative in a Float64 A/B test). The VJP primal keeps the non-mutating rollout — see the validation section for the cross-check.internal_dtype: float64 → float32).Previously the
Setupwas built from Float64 coordinates, and INS.jl's non-mutating operators allocate their outputs from the setup grid — so the first projection silently promoted the state (and the whole Zygote tape) to Float64, despite theFloat32casts at every interface: Float64 compute cost with Float32 I/O precision. (A bare0.5literal in the collocated↔staggered helpers had the same effect; they are now eltype-preserving.) Building the grid in Float32 makes forward + VJP run in single precision end-to-end, like the other float32 solvers. Note this will move INS.jl's FD-gradient-error numbers to the float32 noise floor of its peers — the previous best-in-class values partly reflected the hidden double precision.(If you'd rather keep INS.jl in genuine Float64 and disclose the dtype difference instead, that is a one-line revert of the
LinRangechange — happy to go either way.)Changes with identical results (pure speedup)
add_ghosts/strip_ghostswrapping in the RK4 rollout used for the VJP primal.create_right_hand_sideapplies the periodic BCs to its input and projects its output, so the ghost refresh between stages is redundant — the state's ghost entries go stale but are never read, and are stripped at the end. Bitwise-identical interiors; ~19% off the non-mutating rollout time plus fewer Zygote tape nodes.Not changed
Validation
Run against INS.jl v5.0.0 outside the container (calling the
ns_solver.jlfunctions directly), 2-D (N=32) and 3-D (N=16):applyvs non-mutating VJP-primal rollout: rel. diff 2.2e-4 (2-D) / 1.0e-4 (3-D) — Float32 round-off accumulation (the same A/B in Float64 gives ≤4e-14);grad_v0(random direction) andgrad_dtvs central finite differences: rel. err 1.1e-3–2.5e-3 (Float32 FD noise floor);grad_nuvs central FD: 3e-2 at ε=1% shrinking monotonically to 5.4e-3 at ε=20% — the FD is noise-limited by Float32 loss quantization (identical FD values at ε=5% and 10%) and converges toward the AD value; the upstream rrule is additionally verified exactly (linear in ν) with ChainRulesTestUtils;Expected effects of the
benchmark:solverre-run: forward times ↓ ~4–6× plus the float32 gain, VJP times ↓ ~25–36% plus the float32 gain, FD-gradient errors move to the float32 floor, physics/agreement results unchanged within tolerance.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gdif1Cu1pdCJENe2hNUaF1