Faster solve!: BLAS-friendly AIC layout + leaner filament kernels (5.4-8.0x) - #259
Merged
Conversation
`BodyAerodynamics.AIC` was `(3, n_panels, n_panels)`, so each component slice
`AIC[k, :, :]` had `stride1 == 3`. That is not a BLAS layout, so the three
`mul!` calls in the gamma loop silently fell back to the generic Julia
matmul — over 247 iterations that was ~80% of `solve!`.
Storing it as `(n_panels, n_panels, 3)` makes every slice contiguous and the
products dispatch to `gemv`:
before after speedup
INVISCID n=60 5.92 ms 1.72 ms 3.43x
INVISCID n=120 26.10 ms 4.89 ms 5.34x
POLAR_VECTORS n=60 6.44 ms 2.11 ms 3.06x
POLAR_VECTORS n=120 21.06 ms 6.05 ms 3.48x
Also swaps the AIC build loop nest to `jring` outer so the writes are
unit-stride and the filament tuple hoists, replaces the hand-rolled triple
loop in `update_effective_angle_of_attack!` with three `mul!` calls, and
drops the AIC-as-shape-template for `f_body_3D`/`m_body_3D`, which no longer
has a 3xn slice to borrow.
Allocation counts are unchanged and `calc_forces! is zero-alloc` still holds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The three induced-velocity kernels run n^2 times per `solve!`, and each was recomputing quantities it either already had or did not need: - `r0` and `|r0|` are `reinit!`-maintained on every `BoundFilament`, but `velocity_3D_trailing_vortex!` rebuilt both from `x1`/`x2` each call and `velocity_3D_bound_vortex!` re-normed the stored vector. - The core-radius cutoff only needs `|r_perp|`, and `r_perp` is a projection onto `r0` (or onto `Vf`), so its length is `|r1.r0|/|r0|` — no need to form the vector. It is still built inside the core branch, where it is used. - `r1 x r2`, the `r1/|r1| - r2/|r2|` difference and `r2 x r0` were computed before the branch that selects between the regular and core-radius forms, but each is read on only one side of it. Bit-identical output: max deviation 0.0 over 28800 calls against the previous implementation, and the wing coefficients are unchanged. INVISCID n=60 1724.0 us -> 1173.3 us 1.47x INVISCID n=120 4889.0 us -> 3277.7 us 1.49x POLAR_VECTORS n=60 2106.2 us -> 1428.4 us 1.47x POLAR_VECTORS n=120 6047.6 us -> 3908.3 us 1.55x The kernels keep their `MVector` scratch. An `SVector` rewrite is 1.14x faster again for `Float64` but 1.2-1.4x *slower* for `ForwardDiff.Dual`, which `linearize` depends on, so it is not worth the trade. Also fixes two stale defaults in the `Solver` docstring: `type_initial_gamma_distribution` is `ZEROS`, not `ELLIPTIC`, and `core_radius_fraction` is `0.05`, not `1e-20`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Solver docstring carried the SolverSettings defaults rather than its own and left core_radius_fraction undescribed. 0.05 is the upstream VSM default, attributed there to Damiani et al. (2019). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
core_radius_fraction 1e-20 -> 0.05 and type_initial_gamma_distribution ELLIPTIC -> ZEROS. 0.05 is the upstream awegroup/Vortex-Step-Method default, attributed there to Damiani et al. (2019), "A Vortex Step Method for Nonlinear Airfoil Polar Data as Implemented in KiteAeroDyn". At 1e-20 the Biot-Savart singularity guard never engaged. Coefficients are unchanged for well-separated geometry: the guard triggers only when a control point falls within 5% of a filament length of a bound vortex, and that ratio is 0.5*n/AR for a wing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Julia 1.12.7 precompiles the whole test environment before running any test, so MakieControlPlots and GLMakie load ahead of the Sys.islinux() guard in runtests.jl. Only Linux sets up xvfb, so GLMakie fails to open a GL context there: "GLFWError (FORMAT_UNAVAILABLE): NSGL: Failed to find a suitable pixel format". Same runner image, same GLMakie/GLFW versions as the last green run on 1.12.6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AIC is an internal buffer; its only accessor, calculate_AIC_matrices!, is documented under private functions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes to the
solve!hot path. Combined effect at n=120: 8.0x (inviscid) and 5.4x (polars).1. AIC stored component-last
BodyAerodynamics.AICwas(3, n_panels, n_panels), so each component sliceAIC[k, :, :]hadstride1 == 3. That is not a valid BLAS layout, so the threemul!calls inupdate_gamma_candidate!silently fell back to the generic Julia matmul instead ofgemv.With the default
relaxation_factor = 0.03the gamma loop runs ~247 iterations, so those three matvecs were ~80% of totalsolve!time. Isolated, the strided slice was 22-26x slower than the same matrix made contiguous.Storing
AICas(n_panels, n_panels, 3)makes every component slice contiguous.Also in this commit:
jringouter, so the writes are unit-stride and the filament tuple hoists out of the inner loop.update_effective_angle_of_attack!becomes threemul!calls.f_body_3D/m_body_3Dno longer borrow a3 x nslice ofAICas aLazyBufferCacheshape template (there isn't one any more); they ask for the size directly.AICis an internal buffer (its only accessor,calculate_AIC_matrices!, is listed under private functions), so this is not a breaking change; internal code readingAIC[k, i, j]becomesAIC[i, j, k].2. Redundant work removed from the filament kernels
The three induced-velocity kernels run n^2 times per
solve!, and each recomputed things it already had or did not need:r0and|r0|arereinit!-maintained on everyBoundFilament, butvelocity_3D_trailing_vortex!rebuilt both fromx1/x2each call andvelocity_3D_bound_vortex!re-normed the stored vector.|r_perp|, andr_perpis a projection ontor0(orVf), so its length is|r1.r0|/|r0|— the vector is now built only inside the core branch that actually uses it.r1 x r2, ther1/|r1| - r2/|r2|difference, andr2 x r0were computed before the branch selecting between the regular and core-radius forms, but each is read on only one side of it.Bit-identical: max deviation 0.0 over 28800 calls against the previous implementation.
The kernels keep their
MVectorscratch. AnSVectorrewrite is 1.14x faster again forFloat64but 1.2-1.4x slower forForwardDiff.Dual(measured 0.84x at N=4, 0.69x at N=12), whichlinearizedepends on — so it was not worth the trade.Numbers
Allocation counts unchanged throughout (13-14).
3. Docstring fixes
The
Solverdocstring listedtype_initial_gamma_distributionasELLIPTIC(it isZEROS) andcore_radius_fractionas1e-20(it is0.05).Testing
Zero failures, run after both changes. The geometry/IO groups were left to CI.
🤖 Generated with Claude Code
3.
SolverSettingsdefaults aligned withSolverSolverandSolverSettingsdisagreed on two defaults, andSolver(settings)passed the divergence straight through, so the same nominal default meant
different things depending on how you configured the solve:
SolverSolverSettings(before)core_radius_fraction0.051e-20type_initial_gamma_distributionZEROSELLIPTICSolverSettingsnow uses0.05/ZEROS.0.05is the upstreamawegroup/Vortex-Step-Methoddefault, attributed there to Damiani et al. (2019), A Vortex Step Method for
Nonlinear Airfoil Polar Data as Implemented in KiteAeroDyn; upstream likewise
defaults
gamma_initial_distribution_typeto"zero".This moves no numbers. At
1e-20the Biot-Savart singularity guard simplynever engaged. The branch fires when
perp_dist/‖r0‖ < core_radius_fraction,and for a wing that ratio is
0.5·n/AR, so triggering needsn < 0.1·AR—under 2 panels on an AR-20 wing. Verified on an arc wing (60° half-arc, R=8),
where bound filaments are genuinely non-collinear:
dCL = 0.0andmax‖Δγ‖/max‖γ‖ = 0.0at α = 5°/10°/20°, n = 40/80. The fix matters fordegenerate geometry (near-touching panels, deformed or multi-body layouts), not
for coefficient accuracy. Every settings file in
data/sets both keysexplicitly, so none of them change.
Also documented
core_radius_fractionin both docstrings (it had an emptydescription in
Solver) and corrected three YAML comments that called it afraction of chord — it is a fraction of filament length, i.e. panel width.
Tests
Full suite green on this branch: 6131 pass, 0 fail, 10m09.7s (the single
brokenis a pre-existing@test_brokenin Kite Geometry Tests).4. CI: keep test-env precompilation lazy on macOS and Windows
Unrelated to the changes above, but it was blocking this PR and affects
mainequally, so it is fixed here rather than split out.
macOS-aarch64 and Windows-x64 began failing on every branch with:
Isolated by diffing the last green
mainrun against a failing one — only theJulia patch version differed:
test/runtests.jlalready skips plotting off Linux, and the Aug 13 macOS logprints
Skipping plotting tests on Darwin: GLMakie needs a display.and passes.But that guard runs at test time; 1.12.7 precompiles the whole test
environment first, so
MakieControlPlots(top-levelusing GLMakie) loads andfails before the guard is reached.
CI.ymlonly provisions xvfb on Linux.Fix — keep precompilation lazy where there is no display, so the existing guard
is reached before GLMakie is ever loaded:
macOS now reports
6060 pass, 1 brokenin 4m41s (was 15m43s failing). The71-test gap versus Linux's 6131 is exactly the Linux-only plotting testsets
(58 Plotting + 13 Airfoil skin), so nothing is silently skipped.
Trade-off worth knowing: those two jobs no longer get an up-front precompile of
the test environment, so a genuine precompile error in a non-plotting test dep
would surface as a load error mid-suite rather than cleanly before it starts.
Fixing it at the source instead would mean moving
MakieControlPlotsout oftest/Project.tomlinto a Linux-only job with its own environment.All 9 checks green.