Skip to content

slope: drop the per-cell NaN early-out in the planar CPU kernel #3739

Description

@brendancol

Reason or Problem

The planar CPU slope kernel (_cpu in xrspatial/slope.py) starts every interior cell with

if np.isnan(data[y, x]):
    continue

On DEMs with scattered nodata (masked water, cloud holes, void-filled SRTM edges) the branch outcome depends on the data and the CPU can't predict it. What the skip saves is a few float32 multiplies and one arctan, and that is cheaper than the mispredicts.

A spike removed the branch and timed a 2000x4000 float32 DEM (Gaussian bump plus default_rng(71942).normal(0, 2) noise). With 30% random NaN it went from 69 ms to 33 ms (0.47x). With the left half NaN, a contiguous block the predictor handles well, it was 0.87x. With no NaN at all it was within noise, 0.95x to 1.0x. Results were bitwise identical in every case.

Proposal

Drop the early-out. out is pre-filled with NaN, and the 3x3 Horn stencil's arithmetic already carries any neighbour NaN into p. The only case the branch actually protects is a NaN centre with eight valid neighbours (the #2761 regression), and a select after the arithmetic handles that:

r = np.arctan(p) * 57.29578
ctr = data[y, x]
out[y, x] = r if ctr == ctr else np.nan

No fastmath, no dtype change, no reordering of the neighbour loads. The GPU and geodesic kernels are untouched.

Usage: nothing changes for callers. Same slope() signature, same results.

Value: about 2x on speckled-nodata DEMs for the numpy and dask+numpy backends.

Stakeholders and Impacts

Anyone calling slope() on CPU. The dask+numpy path calls the same kernel per chunk, so it gets the same win.

This should also come with a NaN-bearing asv benchmark (SlopeNaN) so the nodata path has a timing from now on. One catch: get_xr_dataarray(include_nan=True) in benchmarks/benchmarks/common.py only sets the single corner cell [0, 0] to NaN, which sits on the border the kernel never visits. The benchmark has to add its own speckle on top or it measures the NaN-free path again.

Drawbacks

Cells with a NaN centre now run the stencil and an arctan before being masked. That is the cost being traded away, and the numbers above say it is smaller than the branch.

Alternatives

Leave the branch in and accept the mispredict cost. A per-row "any NaN" pre-check was also considered; it adds code and I could not see it beating the plain select.

Unresolved Questions

None.

Additional Notes or Context

Gates before merge: pytest xrspatial/tests/test_slope.py, a direct A/B against the current kernel with np.array_equal(..., equal_nan=True) on the three regimes above plus 3x3, 1xN and int16 inputs, and a test that pins NaN to exactly the centre-NaN cells and their 8-neighbours on a speckled raster.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestperformancePR touches performance-sensitive code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions