perf: optimize every custom periodogram implementation across all backends - #15
Merged
Merged
Conversation
- scatter_add: numpy via bincount (buffered; ~10-30x over unbuffered add.at), cupy via cupyx.scatter_add - new row-wise scatter_add_rows / scatter_counts_rows: torch scatters a stride-0 expanded view (no (P,N) broadcast copy, no flat index); numpy gets one fused bincount pass - BLS/CE/TLS batch kernels use the row-wise shims; TLS also pads once to the widest template and accumulates the correlation in place; BLS hoists the per-duration column arange - PDM bins each point once into n_bins*n_covers fine bins and regroups covers exactly (roll + group-sum): 3 scatters instead of 3*n_covers on the vectorized path, 3 shared-memory atomics per point instead of 3*n_covers in the CUDA kernel. PDM numpy: 3652 -> 1289 ms on a 3k-point/20k-period sweep, bit-identical theta. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tensor Every normal-equation entry is analytically a harmonic trig sum (product-to-sum identities), so compute C_m/S_m for m<=2H via the Chebyshev recurrence from ONE cos/sin evaluation and assemble the tiny (F,d,d) Gram from them: O(F*N*d^2) -> O(F*N*H) work, O(d) less transient memory (no more multi-GB design tensors at 1e5 points), and gemm-free on every backend by construction, which retires the Blackwell cuBLAS workaround branch. Same math, same ridge; checksums unchanged. 20k points x 30k frequencies, H=3: numpy 81.6s -> 30.1s, cupy 2.04s -> 0.71s, torch:cuda 2.12s -> 0.66s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base-grid sums for w and w*y share one angle matrix, and the doubled-frequency sums follow exactly from the double-angle identities (cos2t = 2cos^2 t - 1, sin2t = 2 sin t cos t), so the portable direct path now evaluates cos/sin once per frequency instead of six times. The frequency chunk is also auto-capped by N so the (chunk, N) transients stay ~64 MB on any device. 5k points x 100k frequencies: torch:cpu 3.06s -> 1.66s, torch:cuda 264ms -> 150ms; max deviation vs finufft unchanged at 2.2e-11. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t on device The w and w*y transforms share their nonuniform points, so they now run as one n_trans=2 NUFFT (finufft and cufinufft, plan cache keyed by (nf, n_trans)). The one-shot cufinufft path keeps all three sums on the GPU and assembles the power there, so only the final spectrum crosses back: 194 -> 290 lc/s on 800-point curves; engine path 869 -> 888 lc/s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One-iteration-per-trial prange kernels mirroring the CUDA designs (PDM uses the fine-bin regroup; SL uses a stable mergesort argsort so tied phases pair identically to the array-API backends; MHAOV runs the Chebyshev harmonic recurrence in scalar registers with a per-frequency d x d solve). cpu/auto now resolve to numba when the [fast] extra is installed via a shared fast_cpu_backend attribute, which also replaces the bespoke BLS resolve_backend override. 3k points, warm kernels, vs the vectorized numpy paths: PDM 306x, CE 135x, MHAOV 57x, TLS 53x, SL 23x; parity <= 2.5e-11 (TLS bit-identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e cache - BLS/PDM/CE/TLS kernel sources are templated on the working precision (REAL) and honor settings.precision on the cupy backends: float64 stays the default; float32 is an opt-in that runs the FLOP-bound box/matched- filter scans at full consumer-GPU rate (Blackwell GeForce: BLS 610 -> 71 ms, TLS 34 -> 4 ms, ~8.6x). PDM keeps double accumulators (the ssd = sq - sum^2/cnt form is cancellation-prone) and CE counts are now exact int shared atomics (half the shared memory) with the entropy reduction spread across the block instead of one thread. - TLS caches its concatenated templates in shared memory. - BLS gains a caller-owned per-light-curve device cache so the segmented search uploads tau/yw/ivar once per run (cupy + torch), computes t_min on the host (no per-segment device sync), and returns all seven outputs in one stacked D2H copy. Multiband keeps one cache per band. - numba BLS zeroes only the n_bins+1 bin prefix actually used per period. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New one-block-per-period RawKernel: folds, bitonic-sorts the (phase, original-index) pairs in shared memory (index tie-break = the same stable order as the array-API paths), and reduces the string length with no (P, N) intermediates. Curves beyond the shared-memory capacity (~8k points on Blackwell) fall back to the vectorized path. 3k points x 50k periods: 123 ms vs 9.3 s numpy, parity 1e-13 with forced phase ties. - torch path uses torch.sort(stable=True) to fuse the argsort + row-gather pair (57 ms on torch:cuda). - ensure_shared_memory now opts in whenever dynamic+static shared exceeds the 48 KB default; dynamic == 48 KB alongside static arrays previously failed with CUDA_ERROR_INVALID_VALUE. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tests/test_fast_backends.py: numba-vs-numpy parity for PDM/CE/SL/ MHAOV/TLS (SL with forced phase ties), float32 CUDA kernel checks (BLS tolerates isolated near-tie box flips but requires the same detected period), scatter-shim unit tests (numpy + torch), BLS device-cache reuse equivalence, and cpu->numba resolution for all six methods; new requires_numba marker. - CHANGELOG Performance section; backends/installation/methods/index docs updated for the numba tier and float32 kernels; mypy override for cupyx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ubuntu-latest image stopped shipping libEGL.so.1, so pytest-qt now fails at configure importing PySide6 QtGui (INTERNALERROR before any test runs). Unrelated to the code changes on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tjayasinghe
added a commit
that referenced
this pull request
Jul 2, 2026
…ons merge Re-ran the full validation + benchmark suite against merged PR #15 (numba CPU tier for PDM/CE/String-Length/MHAOV/TLS, algorithmic rewrites, precision- templated CUDA kernels). Correctness holds at the same tolerances as before, but the new CPU tier is now fast enough to match or beat the GPU on a single light curve for PDM/CE/MHAOV, overturning the old "50-190x GPU speedup" figures. Updated REPORT.md, docs/benchmarks.md, docs/guide/backends.md, docs/guide/batch.md, and README.md to reflect the current numbers, and fixed the stale hardcoded prose in make_report.py so future re-runs stay accurate. Co-Authored-By: Claude Sonnet 5 <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.
Implements the full optimization plan across all custom (non-reference) periodogram implementations — numpy, numba, cupy RawKernel, and torch/array-API tiers — plus the extension of the multicore numba CPU tier from BLS to all six ported methods.
Algorithmic (all backends at once)
New numba CPU tier (PDM, CE, String-Length, MHAOV, TLS)
prange kernels mirroring the CUDA designs;
cpu/autoresolve to them via a sharedfast_cpu_backendattribute (replaces the bespoke BLS override). Warm speedups vs the vectorized numpy paths: PDM 306x, CE 135x, MHAOV 57x, TLS 53x, SL 23x; parity <= 2.5e-11 (TLS bit-identical; SL stable-sort tie order preserved via mergesort).GPU
precision: ~8.6x for BLS (610->71 ms segmented) and TLS on GeForce-class FP64-limited cards; float64 remains the default; float32 BLS guards empty windows with a sum-scaled ivar floor.torch.sort(stable=True).Shims
scatter_addnumpy via bufferedbincount(vs unbufferedadd.at), cupy viacupyx.scatter_add; new row-wise scatter helpers avoid materializing (P,N) broadcast copies on torch.ensure_shared_memorynow opts in on dynamic+static totals (fixes a CUDA_ERROR_INVALID_VALUE edge at exactly 48 KB).Not included (documented reasoning)
Verification
233 tests pass (12 new in
tests/test_fast_backends.py, incl. GPU float32 and tie-stability checks) on a machine with numba + cupy + CUDA torch (Blackwell); ruff and mypy --strict clean; docs build with -W. All measured speedups are in the commit messages; checksums/parities quoted per commit.🤖 Generated with Claude Code