Add batched simplices_containing query and Rust default_loss for LearnerND hot loops#13
Merged
Merged
Conversation
Profiling LearnerND with the Rust backend (adaptive PR #493) showed only 14-25% of runtime left inside this extension; two of the remaining Python hotspots are triangulation-shaped and move here: - Triangulation.simplices_containing(point, simplex=None, candidates=None, eps=1e-8): all simplices containing a point in one call. Instead of one barycentric solve per neighbouring simplex (the loop LearnerND.tell_pending runs in Python today, ~14 checks per point in 2D, ~70 in 3D), it solves once for a simplex known to contain the point (the hint, or locate_point), reduces it to the face the point lies on, and looks up the simplices containing that face in the facet index. - default_loss(simplex, values, value_scale=None): the LearnerND default loss (embedded simplex volume) taking the scaled vertex/value arrays directly, signature-compatible with loss_per_simplex. End-to-end with both wired into LearnerND (examples/learnernd_batched_apis.py): 1.18x in 2D and 1.39x in 3D on top of the Rust backend, with identical sampled points.
adaptive 1.5.0 ships the automatic Rust-backend selection, so the usage section now leads with `pip install "adaptive[rust]"` (monkey-patching is only needed for < 1.5.0) and the example points at the release instead of the PR branch. Re-measured all tables against 1.5.0: standalone numbers are unchanged within noise; LearnerND's pure-Python path got faster in 1.5.0, so the honest end-to-end ratio is now 3.3x (was 3.7x). Adds the batched-API numbers (1.17x 2D / 1.40x 3D on top of the backend) to the performance section.
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.
Summary
Profiling
LearnerNDwith the Rust backend active (shipped in adaptive 1.5.0, adaptive#493) shows that only ~14% (2D) / ~25% (3D) of the remaining runtime is spent inside this extension — roughly half is LearnerND's own Python. Two of the remaining hotspots are triangulation-shaped rather than learner-shaped, so they belong here:sphere_of_fire, 1500 pts)learnerND.py)tell_pending's neighbour loop: for every pending point, adaptive loops over all simplices sharing a vertex with the containing simplex and callstri.point_in_simplexone at a time (~14 checks per point in 2D, ~70 in 3D, ~96% misses) — a barycentric solve plus FFI round-trip per check.default_loss: a Python wrapper that tuple-splices vertices and values before callingsimplex_volume_in_embedding; the wrapper costs 3-7x the Rust math it wraps.New APIs
Triangulation.simplices_containing(point, simplex=None, candidates=None, eps=1e-8)All simplices containing
point, as a sorted list of tuples. Instead of one solve per neighbour, it solves once for a simplex known to contain the point (thesimplexhint when given and valid — mapping directly ontotell_pending'ssimplexargument — elselocate_point), reduces it to the face the point actually lies on, and returns the simplices containing that face via the existing facet index. A stale/wrong hint safely falls back to locating the point. Passingcandidatesinstead filters those throughpoint_in_simplex, preserving the original per-candidate semantics. Error parity withpoint_in_simplex(ZeroDivisionError/numpy.linalg.LinAlgError).default_loss(simplex, values, value_scale=None)The
LearnerNDdefault loss (embedded simplex volume) taking the scaled vertex/value arrays directly, with bulk-copy fast paths for 1-D/2-D f64 numpy arrays (exactly what_compute_losspasses). Signature-compatible with adaptive'sloss_per_simplexfunctions, so the adaptive side can swap it in with one import;value_scaleis accepted and unused, like the reference.Performance
End-to-end
LearnerNDon adaptive 1.5.0 with both wired in the way the adaptive-side integration would (examples/learnernd_batched_apis.py):Every configuration samples identical points to the baseline. The win grows with dimension (vertex stars get bigger), which is exactly where
LearnerNDis used.Deliberately not moved here: the simplex priority queue (generic data structure, fixable in adaptive with a lazy-deletion heap) and the pending-point/subtriangulation bookkeeping (that would absorb LearnerND's state machine and break user-pluggable loss functions).
Also includes a README benchmark refresh against adaptive 1.5.0: the usage section now leads with the automatic backend selection (
pip install "adaptive[rust]"), and the end-to-end table was re-measured — LearnerND's pure-Python path got faster in 1.5.0, so the honest headline ratio is now 3.3× (was 3.7×).Testing
tests/test_batched_queries.py: brute-force parity in 2D/3D/4D (including vertex probes), equivalence with the exacttell_pendingneighbour loop it replaces, hint/stale-hint/empty-hint behaviour,candidatesfiltering,epsforwarding, anddefault_lossparity againstadaptive.learner.learnerND.default_lossfor scalar/vector values, numpy arrays, and plain lists.uv sync --locked, adaptive 1.3.2). Against adaptive 1.5.0, everything passes except the pre-existingtest_learnernd_with_neighbor_aware_loss_runsfailure: 1.5.0 auto-selects the Rust backend, which surfaces the known collinearsimplex_volume_in_embeddingdivergence (raises where the reference returns 0.0) — unrelated to this change, needs its own fix before bumping the lock.cargo clippy -D warnings,cargo fmt,ruff(0.11.0) all clean.Follow-up: a small adaptive-side PR can adopt both —
tell_pendingcollapses to onesimplices_containingcall, andtriangulation_backendre-exportsdefault_loss.