perf: rework the carré du champ inner loop; retire the porting plan - #26
Merged
Merged
Conversation
The port is complete and the repo now diverges from upstream, so the phased plan has nothing left to track. Scope lives in README.md; the places where the Julia deliberately does not match the Python live in docs/src/upstream-bugs.md, which is where the one substantive cross-reference (the fixture-correction guard) now points.
Profiling a 3000-5000 point cloud puts the time in two places, neither of them where the einsums are: from_point_cloud is ~50% Arpack and ~44% markov_chain (almost all of that tune_kernel's exp sweep), and every differential operator is dominated by carre_du_champ_knn -- ~93% of the lie_bracket build. carre_du_champ_knn now does its per-point work in a point-last layout. The neighbour gather becomes a contiguous column copy instead of a strided walk over an (n, F) array, and the rank-k update accumulates into a dense H x F buffer rather than the strided cdc[p, :, :] view, which had been forcing LinearAlgebra's generic matmul fallback once per point. Only that buffer is scattered back, and the 1/2rho scaling moves to a single vectorised pass over the contiguous axis. Two thresholds fell out of measurement rather than taste: BLAS beats a hand rolled rank-1 accumulation only above ~2^11 flops, and the mean-centring wants opposite loop orders for wide and narrow tensors. The point loop and the epsilon sweep are both embarrassingly parallel, so both are threaded -- the cdc one gated on F*H*k so small tensors don't pay for a spawn, which otherwise cost more than it saved. Neither split changes results. Also skips exp terms that underflow to exactly zero (bit-identical, worth only a few percent) and hoists a squared-distance matrix markov_chain built twice. At n=4000, single-threaded, against cold geometry: grad -30%, d(1) -20%, laplacian(1) -42%, hessian -10%, levi_civita -8%, lie_bracket -35%. With 8 threads lie_bracket is 0.020s against a 0.065s baseline. Full suite green at both 1 and 8 threads, parity fixtures and doctests included.
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: dropping
PORTING_PLAN.mdnow that the port is done, and a measured performance pass over the two places the time actually goes.Retiring the porting plan
The port is complete and the repo diverges from upstream from here, so the phased plan has nothing left to track. Scope lives in
README.md; the places where the Julia deliberately does not match the Python live indocs/src/upstream-bugs.md, which is where the one substantive cross-reference (the fixture-correction guard) now points.Where the time goes
Profiling a 3000–5000 point cloud found the hot spots are not the einsums:
from_point_cloudis ~50% Arpackeigsand ~44%markov_chain— almost all of the latter istune_kernel's exp sweep (80 ε × 60k entries, run twice).carre_du_champ_knn, not the OMEinsum contractions. Forlie_bracketit was ~93% of the build.carre_du_champ_knnThe per-point work now runs in a point-last layout. The neighbour gather becomes a contiguous column copy instead of a strided walk over an
(n, F)array, and the rank-k update accumulates into a denseH×Fbuffer rather than the stridedcdc[p, :, :]view — which had been forcing LinearAlgebra's generic matmul fallback once per point. Only that buffer is scattered back, and the1/2ρscaling moves to a single vectorised pass over the contiguous axis.Two thresholds in here fell out of measurement rather than taste, and both are commented as such: BLAS beats a hand-rolled rank-1 accumulation only above ~2^11 flops (below that the per-point
mul!call overhead dominates), and the mean-centring wants opposite loop orders for wide and narrow tensors.Threading
The cdc point loop and the ε sweep are both embarrassingly parallel, so both are threaded. The cdc split is gated on
F*H*k— ungated, spawning for small tensors cost more than it saved. Neither split changes results.Also included: skipping exp terms that underflow to exactly zero (bit-identical, worth only a few percent), and hoisting a squared-distance matrix
markov_chainwas building twice.Results
n=4000, cold geometry per rep, against a stashed baseline in the same session:
gradd(1)laplacian(1)hessianlevi_civitalie_bracketTesting
Full suite green at both 1 and 8 threads — 2473 tests, parity fixtures and doctests included. That is the real gate on the
muladdand loop-reordering changes, since they perturb floating-point association.Notes for review
LinearOperator._weak::Union{Nothing,AbstractMatrix},GammaCache::Any) are per-operator caches hit once per big matrix; tightening them buys nothing measurable, so they are untouched.eigscall is deliberately pinned to a fixedv0and a tight tolerance, and the comment there explains why. Deliberately out of scope.