Speed up rhef via sort-and-group inner loop#324
Conversation
fd20995 to
23a2ba7
Compare
The per-radial-bin filter loop in rhef rebuilt a (map_r >= lo & map_r < hi)
mask on every iteration, scanning all N pixels per bin. At lp_size=4096
with 2048 bins that meant ~33 G boolean operations just to assign pixels
to bins, before any ranking happened — the dominant cost.
Replace with a single sort-and-group pass:
- One searchsorted on the bin lower edges gives every pixel its bin
index in O(N log B).
- One stable argsort groups same-bin pixels into contiguous slices.
- The per-bin ranking step is then identical to the old code, just
indexed into the sorted-slice view instead of via a boolean mask.
Output is byte-identical to the original implementation across all three
ranking methods (scipy, numpy, inplace) and across application_radius,
upsilon, and overlapping-bin configurations — verified by a new
test_rhef_matches_reference_loop equivalence suite that re-implements
the old per-bin loop inline as the oracle.
Total wall-clock improvement at 4096^2 in the upstream API (where the
WCS pixel-to-world lookup in find_pixel_radii / blackout_pixels_above_radius
remains a constant fixed cost): roughly 2x at 256 bins, 4.5x at 720 bins,
10x at 2048 bins. Inner-loop-only speedup is much higher; the WCS step
is the next bottleneck and a candidate for a follow-up PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
23a2ba7 to
cd43419
Compare
3763415 to
ad31d1f
Compare
…ation scikit-image 2.0 (currently dev) raises ``PendingSkimage2Change`` warnings from ``skimage.morphology.white_tophat`` because it will switch the default ``mode`` from ``'reflect'`` to ``'ignore'``. The CI matrix runs ``py314-devdeps`` against the dev wheel, so the warning is being raised in ``sunkit_image.stara`` and pytest's ``filterwarnings = error`` config promotes it to a failure (already failing on ``main`` at the time of this commit, blocking unrelated PRs). Pass ``mode='reflect'`` explicitly to lock in the historical behaviour; the value is the current default so existing call sites are unchanged. This is a drive-by fix included in this PR only because the failing test ``test_stara`` blocks the upstream CI matrix.
ad31d1f to
6ace76e
Compare
Gilly + another Claude landed two upstream-pending optimizations in his sunkit-image fork: - sunpy/sunkit-image#324: sort-and-group rhef inner loop (~10× at the bin density we use) - sunpy/sunkit-image#325: analytic find_pixel_radii fast path for non-rotated helioprojective-Cartesian maps (~7×) Combined, a 4096² RHEF drops from ~2.1 s to ~150 ms on the Render box — roughly 14× end-to-end. Side benefits include making the "HQ Filtered" tier feasible to render on demand for any user-picked date, and unlocking the hourly auto-render Gilly wants for gilly.space/sun. Both PRs are API-compatible (no signature changes to radial.rhef or utils.find_pixel_radii) so the call sites in api/main.py:90-91 and api/main.py:1232/1235 need no changes. Source: temporary merged branch https://github.com/GillySpace27/sunkit-image/tree/solar-archive-fast-rhef which I created by `git merge --no-ff origin/rhef-fast-kernel` on top of `origin/wcs-fast-path`. Clean auto-merge, no conflicts. TODO: flip back to a pinned PyPI release once both PRs land in a tagged upstream sunkit-image release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Friendly ping for a review whenever someone has a cycle — CI is green (21/21) and the change is self-contained: it swaps the per-bin boolean-mask loop in @ayshih since you're already in this corner of the code on #325, your eyes would be very welcome here too. |
There was a problem hiding this comment.
The actual implementation looks good to me. Can you confirm that sunkit_image.tests.test_radial.test_fig_rhef would be able to catch if this new implementation had significant errors?
My primary suggestions are about documentation. In my opinion, the comments in the function's code should not refer to the old implementation, but the comments in the test module needs to elaborate more about the motivation/history given the complexity of the tests
- rhef: document the overlapping-bin assignment rule as a docstring Note and reword the inner-loop comments to describe the design directly rather than refer to the removed per-bin loop; expand the bin-index clip comment to explain why clipping a -1 index is safe (in_bin is already False there). - test_radial: expand the reference-oracle docstring with the motivation/history for testing against an inlined copy, and note test_fig_rhef as the real-data backstop; use np.array_equal in the upsilon equivalence test for consistency. - stara: move the skimage 2.0 white_tophat deprecation filter out of the library and into pytest.ini's filterwarnings ignore list. - changelog: reword per reviewer suggestion.
|
Re: confirming 🤖 Posted by Claude Code |
|
Thanks for the review! I think your comments were against
On your |
|
The failing The difference image is blank (RMS < tolerance) — a hash-only mismatch: the committed baseline The RHEF kernel change is bit-identical to the previous implementation across all three ranking methods, covered by the (passing) 🤖 Posted by Claude Code |
…lone flake)
The build installed sunkit-image from the fork via `git+https://…@branch`, and
pip's partial ("promisor remote") clone flaked ~half of Render's no-cache builds
(`fatal: early EOF / index-pack failed / could not fetch <sha>`), making every
deploy a coin-flip. Pinning to PyPI isn't an option — the fast RHEF kernels
(sunpy/sunkit-image#324 + #325, 2–10× faster; ~2.1s → ~150ms for a 4096² RHEF)
aren't in a tagged release yet.
Fix: build the fork (commit 41e89767) into a wheel once and install THAT, so the
build does no git clone. sunkit-image is pure-Python → portable py3-none-any
wheel. Stripped the bundled test-data FITS (sunkit_image/data/test/*, runtime
never touches them) to shrink 6.2 MB → 81 KB, RECORD rewritten to match.
Verified it installs and imports with rhef + the fast rank kernel present.
requirements.txt now points at ./vendor/sunkit_image-…-py3-none-any.whl with
rebuild instructions. Deterministic builds, same speed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Motivation
On large solar maps
~sunkit_image.radial.rhefspends most of its time bucketing pixels into bins, not equalizing ranks. The per-bin loop rebuilds a(map_r >= lo & map_r < hi)boolean mask on every iteration, so the work scales O(N × nbins) — at a 4096² image with 2048 bins that is ~33 G boolean operations before any ranking happens. The kernel cost shows up before the user sees any output and grows with how fine they want their bins, which is exactly the wrong scaling for an algorithm whose whole point is "rank pixels per radial bin."This PR removes that bottleneck. As the original author of
rhefinsunkit-image, this had been on my back burner for a while; the downstream PUNCH analysis I drive now hit the wall hard enough to force the fix.Summary
Replace the per-bin mask loop with a single sort-and-group pass:
searchsortedon the bin lower edges gives every pixel its bin index in O(N log nbins).argsortgroups same-bin pixels into contiguous slices.Equivalence
Output is byte-identical to the original implementation across both supported ranking methods (
scipy,numpy) and acrossapplication_radius,upsilon, and overlapping-bin configurations. A newtest_rhef_matches_reference_loopequivalence suite re-implements the old per-bin loop inline as the oracle and asserts bothnp.array_equal(out[finite], ref[finite])ANDnp.array_equal(np.isfinite(out), np.isfinite(ref))— the latter catching the single-corner-pixel edge case wherer == edges_hi[-1]lands in no bin under< hisemantics.Performance
Measured on a synthetic
Mapwithmethod="scipy", scipy 1.13, numpy 2.0, macOS arm64:The kernel cost no longer scales with bin count; wins grow with both image size and bin density.
cProfileon the 4096²/720 case shows the remaining ~2.5 s is dominated byfind_pixel_radii/blackout_pixels_above_radius's WCS pixel-to-world lookup (~2.1 s of the 2.5 s — called twice). The inner RHEF kernel itself is ~150 ms. Caching that WCS step across the two calls is the subject of a follow-up PR (#325).Notes
plot_settings["norm"]side-effect are all preserved.changelog/324.feature.rstis included.skimage.morphology.white_tophat'smode='reflect'argument insunkit_image/stara.py, which was failing onpy314-devdepsagainst the scikit-image 2.0 deprecation. The fix is conditional on the kwarg's availability so it stays compatible with the project's minimum scikit-image (0.20). Happy to split it into its own PR if maintainers prefer.cc @sunpy/sunkit-image-maintainers