Gate CUDA reproject fast path on WGS84-compatible datums (#3094) - #3119
Merged
Conversation
try_cuda_transform never got the non-WGS84 datum guard that #2651 added to the CPU fast paths. The projected CRS matchers accept any datum in the Helmert table, so cupy and dask+cupy reprojects between WGS84 and e.g. EPSG:27700 ran the WGS84 Krueger series with no datum shift and landed ~100 m off while numpy went through pyproj. Bail to the CPU path (which defers these pairs to pyproj) and add CUDA-side tests mirroring TestNonWgsDatumNumbaFastPath plus an end-to-end numpy-vs-cupy parity check.
brendancol
commented
Jun 9, 2026
brendancol
left a comment
Contributor
Author
There was a problem hiding this comment.
PR Review: Gate CUDA reproject fast path on WGS84-compatible datums (#3094)
Blockers (must fix before merge)
None found.
Suggestions (should fix, not blocking)
None.
Nits (optional improvements)
_get_datum_paramsrunscrs.to_dict()(which goes through pyproj'sto_proj4) twice per chunk now that the guard sits intry_cuda_transform(xrspatial/reproject/_projections_cuda.py:751). The CPU fast path pays the same per-chunk cost intry_numba_transform, so this is parity rather than a regression. If chunk-loop profiling ever shows it, a small memo cache keyed on the CRS would fix both paths at once. Not needed for this PR.
What looks good
- The guard is a faithful mirror of the #2651 CPU fix and sits before the cupy array allocation, so bailed pairs don't pay for device buffers they never use.
- Root cause is correctly identified: the projected-CRS matchers (
_tmerc_params,_lcc_params, ...) accept any datum in the Helmert table via_is_wgs84_compatible_ellipsoid, so EPSG:27700 reached the WGS84 Krueger kernels. The guard cuts that off for every affected datum (OSGB36, DHDN, MGI, ED50, NAD27, ...) rather than special-casing one code. - Test coverage is the right shape: both directions for 27700, the NAD27 geographic case, a WGS84-UTM check that proves the fast path is not over-blocked, and an end-to-end numpy-vs-cupy parity test with exact NaN-mask agreement. The parity test would have caught the original bug (~0.094 value error on [0,1] data).
- Post-guard, the cupy worker falls back to the CPU coordinate path, which already defers these pairs to pyproj, so all four backends now agree by construction.
Checklist
- Algorithm matches reference (#2651 CPU guard)
- All implemented backends produce consistent results (numpy-vs-cupy parity test, run on a GPU host)
- NaN handling is correct (mask equality asserted)
- Edge cases are covered by tests (both directions, geographic + projected non-WGS84 sides, fast-path-still-active)
- Dask chunk boundaries handled correctly (guard is per-chunk, no shape changes)
- No premature materialization or unnecessary copies (guard placed before allocation)
- Benchmark not needed (correctness fix; affected pairs previously returned wrong data)
- README feature matrix unchanged (no API or backend-support change)
- Docstrings/comments explain the why, with issue references
Resolve sweep-accuracy-state.csv conflict: keep the rasterize row added on main and the updated reproject row (#3094) from this branch.
…eproject-2026-06-09-01
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.
Closes #3094
try_cuda_transform. The CUDA dispatcher matched projected CRSs whose datum sits in the Helmert table (OSGB36, DHDN, MGI, ED50, NAD27), then projected them with WGS84 constants and no datum shift, putting cupy and dask+cupy results ~100 m off while numpy went through pyproj. With the guard, those pairs return None and the chunk worker falls back to the CPU path.TestNonWgsDatumCudaFastPath: bail-out checks for EPSG:27700 (both directions) and NAD27, a check that the WGS84 UTM fast path stays active, and an end-to-end numpy-vs-cupy parity test for a 4326 -> 27700 reproject (NaN masks equal, values within 1e-4).Backend coverage: numpy and dask+numpy were already correct; this fixes cupy and dask+cupy.
Test plan:
pytest xrspatial/tests/test_reproject.py -k NonWgsDatum(10 passed, CPU + CUDA classes, run on a GPU host)pytest xrspatial/tests/test_reproject.py -k "cupy or cuda or CuPy"(48 passed on a GPU host)try_cuda_transformcoordinates off by ~96 m vs pyproj; end-to-end cupy vs numpy max diff 0.094 on [0,1] data