Skip to content

Guard resample() against unbounded output allocations (#1295)#1297

Merged
brendancol merged 2 commits intomainfrom
issue-1295
Apr 28, 2026
Merged

Guard resample() against unbounded output allocations (#1295)#1297
brendancol merged 2 commits intomainfrom
issue-1295

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Summary

Closes #1295.

resample() did not bound the output dimensions derived from user-supplied scale_factor or target_resolution. The eager numpy and cupy backends allocated the full (out_h, out_w) working buffer up front, so scale_factor=1e9 on a 4x4 raster requested ~190 EB before any guard ran.

This adds _available_memory_bytes() / _available_gpu_memory_bytes() and _check_resample_memory / _check_resample_gpu_memory helpers (12 B/cell budget covering the float64 working buffer, float32 output, and the scipy/cupyx map_coordinates temporary). The guard runs in resample() after the output shape is computed but before backend dispatch. Eager numpy and cupy paths run the guard; dask paths skip it because per-chunk allocations are already bounded by chunk size.

Same pattern as #1287 (kde / line_density), #1284 (focal), #1283 (geodesic), #1262 (cost_distance), #1267 (diffuse).

Test plan

  • pytest xrspatial/tests/test_resample.py passes (62 tests, 6 new in TestMemoryGuard)
  • scale_factor=1e9 raises MemoryError
  • target_resolution=1e-9 raises MemoryError
  • aggregate-method ValueError still wins over MemoryError
  • normal inputs (scale_factor=2.0) still work
  • error message names scale_factor and target_resolution
  • dask path skips the guard

resample() did not bound the output dimensions derived from user-supplied
scale_factor or target_resolution. _output_shape returned
(round(in_h * scale_y), round(in_w * scale_x)) and that was handed to the
eager numpy and cupy backends, which allocated np.empty / cupy.empty /
map_coordinates buffers of that size with no memory check.
scale_factor=1e9 on a 4x4 raster requested ~190 EB before this commit.

Add _available_memory_bytes() / _available_gpu_memory_bytes() and
_check_resample_memory / _check_resample_gpu_memory helpers (12 B/cell
budget covering the float64 working buffer, float32 output, and the
scipy/cupyx map_coordinates temporary), wired into resample() before
backend dispatch. The eager numpy and cupy paths run the guard; dask
paths skip it because per-chunk allocations are already bounded by
chunk size.

Same memory-guard pattern as kde / line_density (#1287), focal (#1284),
geodesic (#1283), cost_distance (#1262), and diffuse (#1267).

Tests in TestMemoryGuard cover huge scale_factor, huge inverse
target_resolution, the aggregate-method ValueError still winning over
MemoryError, normal inputs still working, error message contents, and
the dask path bypassing the guard.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Apr 28, 2026
@brendancol
Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Co-authored-by: brendancol <433221+brendancol@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 28, 2026

@copilot resolve the merge conflicts in this pull request

Merged main into this branch (commit 8d56b13). The merge was clean — the only change pulled in was the sieve memory guard from #1298, which didn't conflict with the resample changes here.

@brendancol brendancol merged commit c44cbb4 into main Apr 28, 2026
1 check was pending
brendancol added a commit that referenced this pull request Apr 29, 2026
…#1319)

Fixes #1318.

flow_accumulation() on the numpy and cupy backends had no memory check.
_flow_accum_cpu allocated accum (8 B/px) + in_degree (4 B/px) + valid
(1 B/px) + queue_r/queue_c (8 B/px each) ~ 29 B/pixel of working memory
plus the caller's input array. _flow_accum_cupy did the same shape on
the device at ~16 B/pixel. A 50000x50000 numpy raster asked for ~72 GB
of host memory before anything errored out.

Adds _available_memory_bytes / _available_gpu_memory_bytes helpers and
_check_memory / _check_gpu_memory budget checks at 50% of available
RAM/VRAM. Wires them into the public flow_accumulation_d8() dispatch
before the eager numpy and cupy paths run. Dask paths skip the guard
because per-tile allocations are bounded by chunk size.

Mirrors the pattern from sieve (#1298), kde (#1289), resample (#1297),
sky_view_factor (#1300), surface_distance (#1305).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guard resample() against unbounded scale_factor / target_resolution

2 participants